From 21eea741b4a5bdb8d3b89185197fa625cfdf91b7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:24:26 -0400 Subject: [PATCH 1/2] fix(planning): apply design defaults on every non-trivial design The Design defaults bullet put "apply design-defaults.md first" after a "when discussion touches configurability, extension points, observability, or testability" clause, so the Principles thread design-handoff now requires could read as conditional. Applying the defaults is now unconditional for non-trivial scopes; only the per-axis threads stay conditional. Co-Authored-By: Claude Opus 5.5 --- ...ix-design-defaults-unconditional-b31501b2.md | 7 +++++++ plugins/planning/skills/design/SKILL.md | 17 +++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 .changes/planning/fix-design-defaults-unconditional-b31501b2.md diff --git a/.changes/planning/fix-design-defaults-unconditional-b31501b2.md b/.changes/planning/fix-design-defaults-unconditional-b31501b2.md new file mode 100644 index 00000000000..63afeaeab18 --- /dev/null +++ b/.changes/planning/fix-design-defaults-unconditional-b31501b2.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- `/planning:design`: a non-trivial design applies `reference/design-defaults.md`, and so writes the Principles thread `/planning:design-handoff` requires, whether or not discussion touches configurability, extension points, observability or testability. diff --git a/plugins/planning/skills/design/SKILL.md b/plugins/planning/skills/design/SKILL.md index e1a94b66540..f33e09be664 100644 --- a/plugins/planning/skills/design/SKILL.md +++ b/plugins/planning/skills/design/SKILL.md @@ -424,14 +424,15 @@ it, emit the plainest correct form of the dialect. The typed artifact is produce suggest it if available. Suggest `/discovery:research` (if enabled) for a deferred item needing external research, and the `terminology` then `handoff` actions at the session tail - **Design defaults (non-trivial scopes only).** For `library`, `module`, `data`, `integration`, - and `system` scopes, when discussion touches configurability, extension points, observability, - or testability, open a design thread for it. Extension points start from the Phase 2 - variation-point pass. Skip on early-exit, `status`, or trivial single-file work. Apply - [`reference/design-defaults.md`](reference/design-defaults.md) first (quality bar, consumer - standards, the Brief's quality requirements, generalize-now-or-wait, per-tenant configurability - as vendor guidance), then recommend from SRP, OCP, LSP, ISP, DIP, composition over inheritance, - reuse and DRY of knowledge, coupling and cohesion, information hiding, deep modules and YAGNI per - [`design-principles.md`](${CLAUDE_PLUGIN_ROOT}/reference/design-principles.md). + and `system` scopes, always apply [`reference/design-defaults.md`](reference/design-defaults.md) + first (quality bar, consumer standards, the Brief's quality requirements, generalize-now-or-wait, + per-tenant configurability as vendor guidance, and the Principles thread handoff requires), then + recommend from SRP, OCP, LSP, ISP, DIP, composition over inheritance, reuse and DRY of knowledge, + coupling and cohesion, information hiding, deep modules and YAGNI per + [`design-principles.md`](${CLAUDE_PLUGIN_ROOT}/reference/design-principles.md). When discussion + touches configurability, extension points, observability, or testability, open a design thread + for it; extension points start from the Phase 2 variation-point pass. Skip on early-exit, + `status`, or trivial single-file work. ## What this skill does NOT do From 9613b5d093d7bd65aca1eb565247f78e184bd5d5 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:53:16 -0400 Subject: [PATCH 2/2] test(planning): add design-principles-thread-without-axes eval case A production module design for a pure internal calculation whose Brief raises no configurability, extension-point, observability or testability angle; the judge checks design-threads.md for a Principles thread that names principles and settles generalize-now-or-wait, and two graders bar writing or editing .py files. Co-Authored-By: Claude Opus 5.5 --- .../case.yaml | 4 + .../graders/no-code-edit.md | 7 ++ .../graders/no-code-write.md | 7 ++ .../graders/principles-thread.md | 23 ++++++ .../prompt.md | 13 ++++ .../samples/no-code-edit.json | 36 +++++++++ .../samples/no-code-write.json | 34 ++++++++ .../samples/principles-thread.json | 30 ++++++++ .../scaffold.sh | 77 +++++++++++++++++++ 9 files changed, 231 insertions(+) create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/case.yaml create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-edit.md create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-write.md create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/graders/principles-thread.md create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/prompt.md create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-edit.json create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-write.json create mode 100644 plugins/planning/evals/design-principles-thread-without-axes/samples/principles-thread.json create mode 100755 plugins/planning/evals/design-principles-thread-without-axes/scaffold.sh diff --git a/plugins/planning/evals/design-principles-thread-without-axes/case.yaml b/plugins/planning/evals/design-principles-thread-without-axes/case.yaml new file mode 100644 index 00000000000..8f092ba3c87 --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/case.yaml @@ -0,0 +1,4 @@ +schema_version: "1.1" +name: design-principles-thread-without-axes +context: + scaffold_script: scaffold.sh diff --git a/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-edit.md b/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-edit.md new file mode 100644 index 00000000000..f43c4c8332e --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-edit.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Edit +input_match: '\.py"' +min: 0 +max: 0 +--- diff --git a/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-write.md b/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-write.md new file mode 100644 index 00000000000..3076a7bea97 --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/graders/no-code-write.md @@ -0,0 +1,7 @@ +--- +type: tool_used +tool: Write +input_match: '\.py"' +min: 0 +max: 0 +--- diff --git a/plugins/planning/evals/design-principles-thread-without-axes/graders/principles-thread.md b/plugins/planning/evals/design-principles-thread-without-axes/graders/principles-thread.md new file mode 100644 index 00000000000..5d65fc06ba2 --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/graders/principles-thread.md @@ -0,0 +1,23 @@ +--- +type: llm +focus: + source: file + path: .work/loyalty-points/design/design-threads.md +--- + +The file is the design-threads record for a loyalty-points calculation module. + +PASS if it holds a thread or section about principles (headed or labeled Principles, or plainly a +record of which design principles shaped this design) that both: + +1. names at least one specific design principle or default that shaped the module (for example + single responsibility, information hiding, deep modules, YAGNI, pure functions, DRY of + knowledge, or a consumer standard that overrides a bundled default), and +2. records how the generalize-now-or-wait question was settled for this module: whether to build + for further variants or rule sets now or wait until a second one exists, with the outcome (for + example "wait: the Brief's Intent line expects no second variant") or the question recorded as + blocked or deferred for the user. + +FAIL if the file is empty or missing, has no principles thread, names principles only in passing +inside another thread without recording the generalize-now-or-wait outcome, or records principles +without any generalize-now-or-wait outcome. diff --git a/plugins/planning/evals/design-principles-thread-without-axes/prompt.md b/plugins/planning/evals/design-principles-thread-without-axes/prompt.md new file mode 100644 index 00000000000..45a77bbf2ad --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/prompt.md @@ -0,0 +1,13 @@ +--- +description: "Unattended production module design for a pure internal loyalty-points calculation whose discussion has no configurability, extension-point, observability or testability angle: design-threads.md still records a Principles thread naming the principles that shaped the design and how generalize-now-or-wait was settled, and no code is written" +tags: [design, design-defaults, principles, unattended, scaffold] +runs: 3 +max_turns: 40 +timeout_seconds: 1200 +allowed_tools: [Read, Glob, Grep, Skill, Bash, Write, Edit] +expected_outcome: "The design writes .work/loyalty-points/design/design-threads.md with a Principles thread that names the design principles shaping the loyalty-points module and records how generalize-now-or-wait was settled (here: wait, citing the Brief's Intent line that no second variant is expected). No .py file is written or edited" +--- + +/planning:design module + +Topic slug `loyalty-points`; the Brief is in `.work/loyalty-points/PLAN.md`. Work through the design and write the design artifacts. This run is unattended: nobody will answer questions, so record anything only I can decide as blocked and keep going. Don't write any code. diff --git a/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-edit.json b/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-edit.json new file mode 100644 index 00000000000..cde2e861f49 --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-edit.json @@ -0,0 +1,36 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Edit", + "input": { + "file_path": ".work/loyalty-points/design/design-threads.md", + "old_string": "# Design threads\n", + "new_string": "# Design threads\n\n## Principles\n" + } + } + ], + "why": "editing a design artifact is allowed" + }, + { + "answer": [], + "why": "no tool call" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Edit", + "input": { + "file_path": "shop/checkout.py", + "old_string": "def ", + "new_string": "def changed_" + } + } + ], + "why": "edited existing source code" + } + ] +} diff --git a/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-write.json b/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-write.json new file mode 100644 index 00000000000..b5dd1d80eb6 --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/samples/no-code-write.json @@ -0,0 +1,34 @@ +{ + "pass": [ + { + "answer": [ + { + "tool": "Write", + "input": { + "file_path": ".work/loyalty-points/design/design-threads.md", + "content": "# Design threads\n" + } + } + ], + "why": "a design artifact is allowed" + }, + { + "answer": [], + "why": "no tool call" + } + ], + "fail": [ + { + "answer": [ + { + "tool": "Write", + "input": { + "file_path": "shop/points.py", + "content": "def points_for(order): ...\n" + } + } + ], + "why": "wrote code" + } + ] +} diff --git a/plugins/planning/evals/design-principles-thread-without-axes/samples/principles-thread.json b/plugins/planning/evals/design-principles-thread-without-axes/samples/principles-thread.json new file mode 100644 index 00000000000..82955fd99eb --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/samples/principles-thread.json @@ -0,0 +1,30 @@ +{ + "pass": [ + { + "answer": "# Design threads: loyalty-points\n\n## T1 Module boundary\nStatus: resolved. `shop/points.py` exposes `points_for(order) -> int`; checkout calls it and holds no rule.\n\n## Principles\nStatus: resolved.\n- Single responsibility: the points rules live in one module; checkout only asks for the result.\n- Deep module, information hiding: one function hides the gift-card, tenure and floor rules.\n- YAGNI: no rule engine or strategy interface.\n- Generalize now or wait: wait. The Brief's Intent line records second variant = none expected and one caller, so the rules stay plain code until a second rule set exists.\n", + "why": "names principles and settles generalize-now-or-wait from the Intent line" + }, + { + "answer": "# Design threads\n\n## T3 Principles\nShaped by: information hiding (callers never see the rules), pure functions over the Order value (no I/O), DRY of knowledge (the gift-card exclusion is stated once).\nGeneralize-now-or-wait: blocked, USER-RESERVED. The Brief expects no second variant, recommend wait; only the user can commit to a pluggable rule set.\n", + "why": "principles named, generalize question recorded as blocked for the user with a recommendation" + } + ], + "fail": [ + { + "answer": "", + "why": "empty file" + }, + { + "answer": "# Design threads\n\n## T1 Module boundary\nStatus: resolved. `points_for(order)` in `shop/points.py`.\n\n## T2 Rounding\nStatus: resolved. Round down per order total, as today.\n", + "why": "no principles thread at all" + }, + { + "answer": "# Design threads\n\n## Principles\n- Single responsibility: checkout stops owning the points rules.\n- Information hiding: one entry point.\n", + "why": "principles named but generalize-now-or-wait never settled" + }, + { + "answer": "# Design threads\n\n## T1 Module boundary\nStatus: resolved. One function, `points_for`. We will wait to generalize until a second rule set appears.\n", + "why": "generalize outcome recorded but no principles thread naming the principles" + } + ] +} diff --git a/plugins/planning/evals/design-principles-thread-without-axes/scaffold.sh b/plugins/planning/evals/design-principles-thread-without-axes/scaffold.sh new file mode 100755 index 00000000000..847d9a2890d --- /dev/null +++ b/plugins/planning/evals/design-principles-thread-without-axes/scaffold.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# A shop computes loyalty points inline in checkout. The Brief asks for a pure internal module that +# owns the calculation. Nothing in it touches configurability, extension points, observability or +# testability, so only an unconditional design-defaults pass writes a Principles thread. +set -euo pipefail + +git init -q -b main +git config user.name eval +git config user.email eval@example.invalid +mkdir -p shop .work/loyalty-points + +cat >shop/__init__.py <<'PY' +PY + +cat >shop/model.py <<'PY' +from dataclasses import dataclass +from decimal import Decimal + + +@dataclass(frozen=True) +class Line: + sku: str + quantity: int + unit_price: Decimal + category: str + + +@dataclass(frozen=True) +class Order: + id: int + customer_id: int + lines: tuple[Line, ...] + member_since_years: int +PY + +cat >shop/checkout.py <<'PY' +from decimal import ROUND_DOWN, Decimal + +from shop.model import Order + + +def complete(order: Order) -> dict: + total = sum((line.unit_price * line.quantity for line in order.lines), Decimal("0")) + points = int((total).quantize(Decimal("1"), rounding=ROUND_DOWN)) + for line in order.lines: + if line.category == "gift-card": + points -= int((line.unit_price * line.quantity).quantize(Decimal("1"), rounding=ROUND_DOWN)) + if order.member_since_years >= 5: + points = points * 2 + return {"order_id": order.id, "total": str(total), "points": max(points, 0)} +PY + +cat >.work/loyalty-points/PLAN.md <<'MD' +# Loyalty points calculation + +## Brief + +### Goal +Move the loyalty-points rules out of `shop/checkout.py` into one internal module that owns the +calculation, so checkout only asks for the points an order earns. + +### Constraints +- Same results as today for every existing order: one point per whole currency unit spent, gift + cards earn nothing, members of five or more years earn double, never below zero. +- Internal only: checkout is the one caller; nothing outside this repository reads the points. + +### Acceptance criteria +- Checkout gets an order's points from the new module and no longer holds any points rule. +- Every rule above keeps its current result. + +### Captured assumptions +- Quality bar: production +- Intent: second variant = none expected; existing structure: the rules sit inline in checkout.complete; upstream: none +MD + +git add . +git commit -q -m "feat: loyalty points in checkout"