feat(planning): always-on unwanted-behaviour coverage prompt, EARS tags under the convention - #3882
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7bce920b55
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…io over discovered repos (#3901) Closes #3816 ## Summary Nothing in this marketplace answered "what systems does this organization have, who owns them, what do they run on, and how do they relate". The `repo-fleet-hygiene` plugin discovers repositories but feeds cleanup, and `architecture:improve` works inside a single codebase. This PR adds the missing altitude: two new skills in the `architecture` plugin that chart a *set* of repositories as a C4 System Landscape plus an application-portfolio table, together with the consumer-configuration surface that says where those artifacts land. ## Fix **`map-landscape`** charts a set of repositories. Discovery is argument-selected: - `--repos <a,b>` charts exactly the listed repositories, with no discovery at all. - `--root <dir>` (repeatable) delegates bounded discovery and canonical-checkout resolution to `/repo-fleet-hygiene:audit --plan-file` when that plugin is installed, confirms `schema_version: 1`, and **filters the plan's `repositories[]` to the requested roots** because that collaborator's configured scope is additive. An absent plugin, a missing plan, or another schema version falls back to an announced bundled walk (depth 5, `.git` marks a repository and is not descended into, skips `node_modules`/`vendor`/`.venv`, canonical from the first `git worktree list --porcelain` record). - Neither argument stops and names both forms. The session's working directory is never scanned. Facts come from `skills/map-landscape/scripts/portfolio-facts.sh`, never derived by hand: `owner` from CODEOWNERS then the remote's owner segment and **never a commit author**, `runtime` (all hits, first primary), `target_framework`, `dependencies` (sorted, deduplicated, capped at 25), and a local-HEAD `last_touched`. Anything no probe derives is the literal `unknown`, carried through rather than guessed, with an `evidence{}` object naming the file behind each fact. Relationships are model judgment behind a hard evidence rule: an edge exists only where a fact in the source names the target, and the matched string IS the edge description. Output is `landscape.dsl` with a `systemLandscape` view under the `structurizr` dialect, or `landscape.md` with a `C4Context` block under `mermaid`, plus `portfolio.md`. Mermaid ships no landscape diagram type and marks its C4 syntax experimental (<https://mermaid.js.org/syntax/c4.html>, verified 2026-09-06); the skill states that asymmetry rather than papering over it. **`setup`** owns the declaration: a convention doc at the consumer's convention home under the config-cascade expression doctrine, resolved through the vendored `lib/resolve-convention-home.sh` (enrolled in `scripts/sync-resolve-convention-home.sh`). `check` is read-only and reports PASS/FAIL/INFO with one remediation line per FAIL; `apply` converges exactly two artifacts, the marked `convention-home` pointer region and `<home>/architecture/README.md`, idempotently, and re-reads from disk to report the values it observed. `architecture_dir` has no default on purpose: guessing a directory would write two generated files into a tree nobody asked for, so an undeclared and unconfirmed value stops `map-landscape` and routes here. `landscape_dialect` defaults to `mermaid`. No retired layers; this surface is new. ## Manual smoke Explicit list, `repo-fleet-hygiene` not consulted, over three real checkouts: ```text $ portfolio-facts.sh D:/repos/.../autonomy-demo-scratch D:/repos/.../ci-workflows D:/repos/.../github-iac {"name":"autonomy-demo-scratch",...,"owner":"kyle-sexton","runtime":"shell","target_framework":"unknown","dependencies":[],"last_touched":"2026-07-31T01:04:19-04:00",...} {"name":"ci-workflows",...,"owner":"melodic-software","runtime":"dotnet","target_framework":"unknown","dependencies":[],"last_touched":"2026-09-06T16:32:41-04:00",...} {"name":"github-iac",...,"owner":"melodic-software","runtime":"dotnet,node","target_framework":"unknown","dependencies":["../../MelodicSoftware.GithubIac.csproj","Microsoft.NET.Test.Sdk","OpenTelemetry.Api","OpenTelemetry.Exporter.OpenTelemetryProtocol","Pulumi","Pulumi.Github","xunit","xunit.runner.visualstudio"],"last_touched":"2026-09-06T22:29:01-04:00",...} ``` The rendered `portfolio.md` carries one row per repository across all six columns. The rendered mermaid `landscape.md` groups the three under two `Enterprise_Boundary` blocks (`kyle-sexton`, `melodic-software`) and draws **no** `Rel` line, because no collected fact in any of the three names another: `github-iac`'s only `ProjectReference` resolves inside itself. That is the evidence rule working, not a gap. The `--root` + collaborator path and the artifact-write path are model-driven and cannot be proven by a repository gate; the eval suite pins them (cases 2 and 3 of `map-landscape/evals/evals.json`). ## Verification - `portfolio-facts.test.sh`: **44 cases, 0 failed** (mktemp fixtures per ecosystem, plus the empty repo, the owner ladder order, the dependency cap, and the bad-path exit code). - `shellcheck` on both scripts: clean. - Every fenced Sanity Check in the issue: pass. - `check-changed-skills.sh`, `validate-plugins.sh`, `validate-plugin-contracts.mjs`, `check-skill-count-claims.sh --check`, `check-orphaned-fixtures.sh --check`, `check-purged-em-dashes.sh`, `check-fixture-git-isolation.sh --check`, `check-stale-base-overlap.sh --check`, `check-changelog-parity.sh` in all three modes, `sync-resolve-convention-home.sh --check`, `markdownlint-cli2` on every changed markdown: pass. - Cheat sheet and catalog regenerated with their generators, never hand-edited. Collecting facts originally took over three minutes for three repositories on a Windows checkout, because the obvious spellings (a `find` per probe, `printf | awk` inside `$(...)` per emitted field) each cost a fork. One walk per repository plus fork-free escaping brought that to 38s; a separate commit isolates the change. ## Related - Refs #3801 (parent). - Blocked by #3807, the planning slice whose amendment note fixed this issue body as the worker contract. - #3817 is in flight against `plugins/planning/skills/design-handoff/`; no overlap with this diff. - PR #3882 adds `plugins/planning/lib/resolve-convention-home.sh` to the same `copies=(...)` array in `scripts/sync-resolve-convention-home.sh` that this PR appends `plugins/architecture/lib/...` to. Whichever lands second keeps both entries. - Does not close: the `repo-fleet-hygiene` plan schema stays owned by that plugin; this PR only reads `schema_version`, `repositories[].discovered`, `.canonical` and `.remote`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lup (#3904) Closes #3824 ## What changed Close-out mode's acceptance-criteria rollup already anchors the pass on the container's criteria, already gives every criterion a verdict of `delivered` / `partial` / `missing` / `unverifiable`, and already renders unconditionally. What it lacked was any awareness of a criterion's requirement pattern, so the shape of a tagged set (every `unwanted-behaviour` criterion `unverifiable` while the `event-driven` ones were all `delivered`) was discarded. The rollup now gains **one conditional column** naming each criterion's requirement pattern when any criterion retrieved in Step 2 opens with a bracketed EARS tag. - The cell carries one of exactly five names, `ubiquitous`, `event-driven`, `state-driven`, `unwanted-behaviour`, `optional-feature`, the vocabulary the writing side (#3821 / #3882) emits. A bracket holding anything else leaves the cell empty rather than echoing raw text. - Detection is a leading bracket holding one of those names, nothing more. No flag, no lever, no convention key, and no resolution ladder. A checklist marker (`- [ ]`) is named explicitly as the bracket that is not a tag, because the repo's own Agent Brief template emits criteria that way. - **Every criterion still gets a row.** A partially tagged set shows its untagged rows with an empty pattern cell rather than dropping them, which is the invisibility the rollup exists to remove. A set carrying no tag renders exactly as it does today, with no extra column. - Step 2's extraction bullet now says to keep the criterion line whole, so the tag survives the read. ## What deliberately did not change - **No second verdict vocabulary.** The existing four values stay; no `met`, no `not-met`. - **The blocking rule is untouched.** Only a `missing` or `wrong` finding keeps the container open. That rule is co-owned by the work-items container lifecycle and changing it is a container-level decision, not a side effect of adding a column. `plugins/work-items/` is not in this diff. ## Verification A fresh-context verifier that did not write the change confirmed all nine checked points with no CRITICALs: no second verdict vocabulary, blocking rule unchanged, `plugins/work-items/` untouched, one row per criterion on a partially tagged set, the five names verbatim (including British `-behaviour`), unchanged rendering with no tags, no flag/lever/convention key, scope held to one column, and house mechanics (em-dash purge on the README, changelog order, version parity). Its one non-blocking tightening (the checklist-marker ambiguity) is applied as the second commit. Gates run locally: `check-changelog-parity.sh --check-bump main` (pass), `check-purged-em-dashes.sh` (pass), `validate-plugin-contracts.mjs` (pass), `markdownlint-cli2` on the changed markdown (0 issues). Review plugin bumped 0.26.19 to 0.26.20. Note: #3821 / #3882 (the writing side that emits the tags) is still open, so this reader lands before its writer. The vocabulary is pinned to that PR's stated contract. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
38396c2 to
5148dbd
Compare
|
PR body contract — issue linkage This PR body does not yet satisfy the issue-linkage contract:
Edit the body and this comment updates itself on the next run. |
…R convention (#3898) Closes #3818 Adds `/architecture:record-decision`, a thin skill that records one architecture decision into whatever ADR convention the consuming repository already has, plus presence-gated routes to it from three planning skills. Four sequential phases, one commit each. ## Phase 1: skill and shared discovery ladder - `plugins/architecture/skills/record-decision/SKILL.md`. Discovers the ADR directory, numbering scheme and record shape in use, writes exactly one record that follows them, and reports what it found before it writes. Where nothing is declared and nothing exists, it names the rungs it searched, offers two or three shapes, points at the upstream catalog by URL, and writes nothing until the human chooses. - `plugins/architecture/reference/adr-discovery.md`. The plugin's single owner of the discovery ladder (declared, then existing directory, then none) plus the numbering and shape inference rules. - `improve/actions/deepening.md` drops its inline directory list for a link to that reference, keeping the declared-location-first rule and the closing sentence verbatim. The upstream catalog (`joelparkerhenderson/architecture-decision-record`) is **cited by URL and never vendored**: it is CC BY-NC-SA 4.0, recorded in the skill as a four-part verification record (claim, basis, as-of 2026-09-06, recheck trigger). No catalog prose appears anywhere in this diff. ## Phase 2: evals and fixtures Seven rich-form cases over three fixture trees (an existing convention declared by a directory README, an inferred shape carrying a deliberate duplicate `ADR-008`, and a tree with no convention). Coverage: trigger and happy path, shape inference with a duplicate reported and not renamed, offer-and-defer, the write that follows a human's choice, the refusal to vendor catalog prose, the scope refusal for index and supersession, and the admission test on a decision that does not earn a record. ## Phase 3: planning routes Each edit is presence-gated and keeps today's behaviour as the fallback: - `interview/SKILL.md` line 155 bullet and the interview-outputs line; new Composition row; eval cases 17 and 18 covering both branches. - `design-handoff/SKILL.md` gains the "ADR candidates" bullet in the handoff summary. The offer never blocks the handoff. - `plan/context/close-out.md` step 2. The ADR admission test is unchanged. ## Phase 4: surfaces and regen architecture `0.8.0`, planning `0.38.1`, matching CHANGELOG entries, `adr` and `decision-record` in keywords and marketplace tags (the two lists match sorted), README sections, and regenerated `docs/CATALOG.md` and `docs/SKILL-CHEAT-SHEET.md`. ## Overlap with other open PRs Stated plainly, per the collision brief: - `plugins/planning/skills/interview/SKILL.md` is also edited by **#3882** (`feat/3821-ears-capture`). This PR touches only the two body lines and the one Composition row the issue names. - `plugins/planning/skills/design-handoff/SKILL.md` was also edited by **#3899**, which has since merged; this branch rebased onto it cleanly and its coverage-report section is untouched. - `plugins/planning/.claude-plugin/plugin.json`, `CHANGELOG.md` and `README.md` are also touched by **#3882**. - `plugins/architecture/.claude-plugin/plugin.json`, `CHANGELOG.md` and `improve/actions/deepening.md` are also touched by **#3896** (`claude/3609-rendered-views-retrofit`, still open) and by **#3901**, which merged as `0.7.0` while this branch was in flight. This branch rebased onto #3901 and preserves its `map-landscape` and `setup` entries, keywords and tags. **Version note.** The issue specifies planning `0.36.5` to `0.36.6`. Main moved twice under this branch while it was in flight: #3879 landed `0.37.0` and #3899 landed `0.38.0`. This branch therefore takes **`0.38.1`**, the next unused number above main, and preserves both landed entries intact below it. Architecture likewise moved: #3901 landed `0.7.0` for its own new skill, so this branch takes **`0.8.0`** (a minor bump, matching the convention that a new skill takes one) and keeps #3901's entry, keywords and tags intact alongside its own. ## Manual check Run against this repository, the skill's discovery reports `docs/adr/`, `NNNN-kebab.md`, no README or template file, shape inferred from `0030` (Status and Date list lines, then `## Context`, `## Decision`, `## Consequences`), duplicates at `0018`, `0025` and `0028`, and proposes `0031-<kebab>.md`. Run in an empty `git init` directory it finds no convention on any rung and creates nothing. Both match the issue's stated expectations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gs under the convention `/planning:interview` and `/planning:prd` gain two behaviours at acceptance-criteria capture. The first is always on: one prompt, asked once, about a missing unwanted-behaviour (IF-THEN) case and a missing state-driven (WHILE) case, with "neither applies" a valid answer. It is deliberately not a Q<N> row in the interview's open-question register, so it can never hold the register gate hostage to a question that carries no decision. Non-interactive runs skip the ask instead of blocking, and say so: a dispatched worker, a forked subagent, a headless invocation, or the PRD's `synthesize` path report in the returned summary that unwanted-behaviour and state-driven coverage went unexamined, and record the same line durably (the Brief's Captured assumptions; the PRD's Open questions). The unattended condition stays caller-declared, never sniffed, matching the skill's existing doctrine. The second is convention-gated. When the team's `acceptance_criteria_format` resolves to `ears`, each emitted criterion takes a bracketed pattern prefix drawn from exactly five names -- ubiquitous, event-driven, state-driven, unwanted-behaviour, optional-feature -- on the Brief's existing plain-bullet shape. That vocabulary is a contract with the reader at container close-out, so the names are spelled out verbatim in both skill bodies and in the Brief-template context file. Under `free-text`, the default and every degrade, criteria are emitted untagged and byte-comparable in shape to the previous release; the template placeholder `- <testable criterion>` is unchanged and the Brief stays plain bullets, never checkboxes. Both skills RESTATE the authoring-formats resolution ladder in their own bodies rather than citing a convention document: an installed plugin never sees the publishing repository's docs/, so a path citation there would make the publisher a runtime dependency of the consumer's session. To give the ladder's step 2 a resolver to call instead of hand-parsing the consumer's root instruction file, the planning plugin enrolls as a carrier of the cross-plugin lib/resolve-convention-home.sh cluster via the enrollment path scripts/sync-resolve-convention-home.sh documents. Both Boundary sections name Gherkin export as a deferred extension point and build none of it. Both descriptions name the trigger phrases that reach acceptance-criteria capture. Closes #3821 Refs #3799 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…ended ladder The unattended-path ladder in context/loop.md is where a reader looks first for what happens to an unanswerable question, and its step 3 routes one to a `blocked` register row and a named blocker. State the coverage prompt's exemption there too, so the one path that could re-route it into the register is closed at the place a reader would take it. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…empts only itself Review finding: on `auto` routed to synthesize-directly the coverage prompt is the sole question asked, which the capture section forbids registering, while Step 3's gate says a register exists and applies the moment ANY question exists. That path asked a question it could not register and then tripped a gate demanding the row -- `check-open-questions.sh` exits 2 (missing register) and HALTS. Resolved as an explicit carve-out rather than by loosening the gate: the coverage prompt carries no decision, so it writes no row and does not by itself bring the gate into scope. Stated at all three sites a reader lands on -- the capture section, Step 3's register-gate paragraph, and loop.md's "Gate before locking" -- because a reader arriving at any one of them must see it. Each statement is scoped so it cannot be read as exempting a real question that happens to be asked beside it: any other question, a residue decision, a frontier round, a gap surfaced mid-synthesis, or a `blocked` row from an unattended run, still writes its row at ask-time and still brings the gate into scope. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…ests it moved Review finding 2: the frontmatter edit plus additions inside five other digest-protected regions left interview-defenses.test.sh pinning the pre-change hashes, so the suite failed on this branch. Before touching a hash, each of the eight regions was re-read against the two defenses the suite exists to protect, per the procedure the suite documents (there is no regeneration script; the failure message emits the new digest and the contract is re-read, confirm, then update). All eight additions are additive and none qualifies the auto-guard, the `lock` STOP-on-gap rule, or the unattended ladder. One adjacency the re-read did surface is now closed. The suite's own header names "the guard is satisfied by recording the residue under `### Captured assumptions`" as a demonstrated inversion, and this change writes a coverage note to that very section. Both statements of it now say outright that the line records an unrun check, is never a place to park a decision, satisfies no part of the auto-guard, and leaves the unattended ladder's `blocked` + USER-RESERVED outcome standing. Digests refreshed: SKILL.md frontmatter, Stance, Step 3, Step 4, "does NOT do"; loop.md open-question register, Step 4, Unattended path. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
… too
A fresh-context verifier found the carve-out incomplete. Three sites state the
register's ask-time row rule unconditionally, and none of them mentioned the
exemption: SKILL.md's Emit-checklist line ("emitted the moment **any** round is
asked"), SKILL.md's "Register at ask-time" rule ("write one `open` row per
question"), and loop.md's "Write at ask-time, not at answer-time".
That is a row-writing contradiction distinct from the gate-halting one already
fixed, and it bites on the in-round path: `me` and `auto`-routed-to-Q&A ask the
coverage prompt inside a round, where those three lines demand a row the capture
section forbids.
Each site now carries the exemption, and each scopes it to the coverage prompt
alone: every real question in the same round is still registered at ask-time
exactly as before, and nothing here changes when a round is registered or what the
gate then demands.
Refs #3821
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…t moved The ask-time carve-out landed inside two digest-protected regions: SKILL.md's Stance section (which houses the in-round no-silent-resolve rule) and loop.md's open-question register section (which binds gaps and blockers to the gate). Same procedure as the earlier eight, which is the one the suite documents: there is no regeneration script, so each region is re-read against the two defenses, the defense confirmed intact, and the digest the failure message emits is taken. Neither addition narrows what counts as a gap, licenses an assumption over a `blocked` row, or governs the rules below it; each is a single scoped paragraph naming exactly one exempt item and restating that every real question in the same round is registered as always. Branch and current origin/main both report PASS=80 FAIL=9; the nine are jq-version-dependent eval-case digests over an evals.json this branch does not touch, and they reproduce identically on a pristine main worktree. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…er's antecedent Two non-blocking findings from a second fresh-context verifier. A seventh site stated the ask-time rule without the exemption: loop.md's Frontier rounds item 5, "The open-question register is written earlier still, at ask-time, in every mode". Lower severity than the six already carved out, since it fires only on paths where a register legitimately exists, but it is the same contradiction and now carries the same clause. It sits in an unpinned region, so no digest moved for it. The unattended ladder's closing paragraph opens "**This** preserves the auto-guard rather than carving an exception in it", and inserting the coverage-prompt exemption directly above it put a nearer antecedent in front of that "This" -- the adjacent-paragraph shape the defenses suite's own header names as a demonstrated attack. The exemption now sits BELOW that paragraph, so "This" refers to the ladder again, and it gained a clause stating that a decision genuinely the user's still takes rung 3 exactly as written. Two digests refreshed for the move (loop.md Unattended path, and the open-question register section that encloses it), by the same re-read-then-take procedure; the test diff remains sha256 constants only. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
…ed content The rebase onto main conflicted on two digests in interview-defenses.test.sh. Main had refreshed the SKILL.md Stance and "does NOT do" section digests for its own ADR-routing edits; this branch had refreshed the same two for the coverage prompt. Neither side's hash describes the merged sections, so neither was taken. Both conflicts were resolved to an impossible sentinel first, forcing the suite to report the merged digest rather than letting a carried-across value quietly pass. Each merged region was then re-read: main's ADR routing survives intact (three `architecture:record-decision` references), the only modified lines are this branch's own five, the in-round no-silent-resolve rule and the `lock` fudge prohibition both still stand, and only then was the reported digest taken. Both measured values differ from main's AND from this branch's previous ones, which is the evidence that no hash crossed the conflict: Stance main 103534de… / ours 6b42cb50… -> merged 2afe3208… "does NOT do" main 00e72b23… / ours 6c8a57d6… -> merged 02754ea5… The eval-case roster digest main refreshed for its new case is left exactly as main wrote it. This branch does not touch evals.json, so main's value is the correct one and re-measuring it locally would only reintroduce the environment's jq collation difference. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4
5148dbd to
5808c5d
Compare
plugins/planning/README.md is a declared purged surface, and the convention-resolution paragraph introduced two em dashes, which failed the lint lane's purged-surface gate. Rewrite the sentence so the failure cases read as a list rather than an aside. Count is back to 10, matching main; the remaining ten are pre-existing, and three of those sit inside the generated plugin-options block. Refs #3821 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alect resolves (#3926) Closes #3924 Refs #3799, #3882 ## Summary `/planning:design` could not read the `diagram_dialect` convention keys, because its own skill body told the running agent the resolver it needed did not exist. `plugins/planning/skills/design/SKILL.md:222` said "the planning plugin ships no bundled resolver", and `:224` said "**Until the resolver is bundled, a configured dialect cannot be read at all.** ... this plugin carries no copy of the shared resolver, so every run resolves through step 6 and takes the default". Both statements were false on `main`. `plugins/planning/lib/resolve-convention-home.sh` is present and executable, and the sibling skills in the same plugin name it: `plugins/planning/skills/interview/SKILL.md:268-270` and `plugins/planning/skills/prd/SKILL.md:296-298`. The cause was ordering. #3879 landed the design skill at 2026-09-06T23:43:06Z; #3882 landed the resolver nine hours later and did not sweep design back. The effect on `main` was that a consumer setting `diagram_dialect.data` to `dbml` was silently served `mermaid`, and the system-scope C4 container view was unreachable for every consumer, since `diagram_dialect.system` could never resolve to a value. Three surrounding documents carried reader claims that were also wrong on `main`, all of the same shape: a value that reads as sourced but was never checked against the code. ## Fix Six files, no behavior outside the four documents named in the issue. 1. **`plugins/planning/skills/design/SKILL.md`.** The false clause at `:222` and the whole "Until the resolver is bundled" paragraph at `:224` are replaced with the resolver's real path and exit-code contract, phrased as `interview/SKILL.md:268-270` and `prd/SKILL.md:296-298` already phrase it, with only the degrade value adapted from `free-text` to this skill's own defaults (`mermaid` for the data artifact, no C4 view for the system scope). The "tracked separately" pointer is dropped, since it pointed at #3881, which closed as refuted. The untrusted-input rule and the "restated here rather than cited because an installed plugin never sees the publishing repository at runtime" rationale are preserved. One punctuation change inside the preserved rule, disclosed so a diff of that hunk is not a surprise: the break after "The convention doc is untrusted input" is now a colon where it was an em dash, so the rewritten line introduces no em dash into a repository mid de-slop campaign. Every word of the rule is unchanged. No dialect branch below the ladder moved. 2. **`docs/PLUGIN-PHILOSOPHY.md:669`.** The row named `/work-items:decompose` and `/review:quality-gate` as readers. Neither reads a key. The row now names only the three planning skills. 3. **`docs/conventions/authoring-formats/README.md`.** "no skill reads either key on `main` today" is removed from both places it appeared. The column heading "Declared consumer (not yet reading)" becomes "Reading skill", and the rows now name `/planning:interview`, `/planning:prd` and `/planning:design`. `/work-items:decompose` and `/review:quality-gate` are dropped from the table and stated explicitly below it as consuming the produced artifact without reading a key, so the document's own closing rule ("A skill appears here once it actually reads the key") stays true of the table. The `:167` contract, "each consuming slice adds its own reading and updates its row in the same change", is honoured by this change. 4. **`docs/conventions/config-cascade/README.md:303`.** "Conformance becomes verifiable when the first resolver lands" and "**No skill on `main` reads these keys yet**" are dropped, replaced with which skills read which keys through which resolver. 5. **`plugins/planning/.claude-plugin/plugin.json`** bumped `0.39.0` to `0.39.1`, with a matching new `## [0.39.1]` entry at the top of **`plugins/planning/CHANGELOG.md`**. No test digest needed refreshing: `plugins/planning/tests/interview-defenses.test.sh` pins content over `interview/SKILL.md` and `prd/SKILL.md`, and this change touches neither. The one `grep -rn design plugins/planning/tests/` hit is the word "design" inside an interview pin's prose, not a pin over the design skill. ## Verification Behavior was attempted, not read. **Fixture run.** A fixture repository was built under the platform temp directory (never inside the repo), carrying a convention-home pointer line in `AGENTS.md` and a `docs/conventions/authoring-formats/README.md` setting `diagram_dialect.data` to `dbml`. ``` $ bash plugins/planning/lib/resolve-convention-home.sh --root "$FX" --explain root: /tmp/ccp3924-fixture AGENTS.md: one CLAUDE.md: none chosen: AGENTS.md (one) token: docs/conventions docs/conventions EXIT=0 ``` Reading the key through the resolved home returned the configured value, not the default: ```` $ sed -n '/^```yaml/,/^```/p' "$FX/$(bash "$R" --root "$FX")/authoring-formats/README.md" contract_version: 1.0.0 acceptance_criteria_format: ears diagram_dialect: data: dbml system: likec4 ```` **Exit-code contract, confirmed against the script rather than copied from interview's wording.** ``` $ bash "$R" --root "$FX-nopointer" no convention-home region in AGENTS.md or CLAUDE.md under /tmp/ccp3924-fixture-nopointer; ask the operator for the home EXIT=1 $ bash "$R" --bogus ERROR: unknown argument: --bogus EXIT=2 $ bash "$R" --root "$FX-does-not-exist" ERROR: --root is not a directory: /tmp/ccp3924-fixture-does-not-exist EXIT=2 $ bash "$R" --root "$FX-badgrammar" # two pointer lines in one region FAIL: two pointer lines in one convention-home region (AGENTS.md); keep exactly one EXIT=3 ``` The script's own `--help` states the same contract: "Exit: 0 resolved (home on stdout); 1 no pointer anywhere (ask); 2 usage; 3 FAIL". That is what the new design text now documents. **Every reader claim left standing, confirmed by `git grep` inside that skill's own files.** | Claim | Command | Result | |---|---|---| | `/planning:interview` reads `acceptance_criteria_format` | `git grep -lE "acceptance_criteria_format\|resolve-convention-home" -- plugins/planning/skills/interview` | `SKILL.md`, `context/loop.md` | | `/planning:prd` reads `acceptance_criteria_format` | `git grep -lE "acceptance_criteria_format\|resolve-convention-home" -- plugins/planning/skills/prd` | `SKILL.md` | | `/planning:design` reads `diagram_dialect` | `git grep -lE "diagram_dialect" -- plugins/planning/skills/design` | `SKILL.md` | **Every skill removed as a reader, confirmed at zero hits.** | Claim | Command | Result | |---|---|---| | `/work-items:decompose` reads no key | `git grep -nE "acceptance_criteria_format\|diagram_dialect\|resolve-convention-home" -- plugins/work-items` | no output | | `/review:quality-gate` reads no key | `git grep -nE "acceptance_criteria_format\|diagram_dialect\|resolve-convention-home" -- plugins/review` | no output | Both skills also state it themselves: `plugins/work-items/skills/decompose/context/container-lifecycle.md:46` reads "no flag, no lever, no convention key", and `plugins/review/skills/quality-gate/context/close-out.md:383` reads "no flag, no lever, and no convention key is read here". **Local gates.** - `CHECK_SKILL_SKILLS_ROOT=plugins/planning/skills bash plugins/skill-quality/scripts/check-skill.sh design` gives `PASS, 0 errors, 3 warning(s)`. All three warnings are pre-existing and structural (line count over the 200 soft target, no Gotchas surface, a numbered procedure at lines 156-162 with no completion-criteria signal); none is introduced here. - `bash scripts/check-purged-em-dashes.sh` gives `98 declared paths, 130 files scanned, no em dashes`. - `bash scripts/check-changelog-parity.sh --check` passes. - `npx markdownlint-cli2` over the five changed markdown files reports 0 issues. - `git diff | grep -c $'\r'` is 0. ## Related - Closes #3924. - Refs #3799, the container whose close-out review graded acceptance criterion 4 as FAILED and found this. The typing half of that criterion shipped correctly in #3879; the dialect-selection half was unreachable. #3799 stays open until this lands. - Refs #3882, which landed `plugins/planning/lib/resolve-convention-home.sh` nine hours after #3879 landed the design skill, without sweeping design back. - #3881 was the prior tracking issue for this gap and closed as refuted, which is why the "tracked separately" pointer is removed rather than repointed. - Out of scope on purpose: the two acceptance-criteria templates (the generic `Criterion 1` and `agent-brief.md`'s `Specific, testable criterion 1`) are a separate known follow-up and are untouched here. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Closes #3821. Phase 2 of the typed-ticket-body lane under #3799.
What changed
/planning:interviewand/planning:prdgain two behaviours at acceptance-criteria capture.Always on, regardless of any convention. One prompt, asked once, about a missing
unwanted-behaviour (
IF-THEN) case and a missing state-driven (WHILE) case. Not a per-criterioninterrogation, and "neither applies" closes it.
Non-interactive runs skip it and say so. A dispatched worker, a forked subagent, a headless
invocation, or the PRD's
synthesizepath skip the ask instead of blocking, state in the returnedsummary that unwanted-behaviour and state-driven coverage went unexamined, and record the same line
durably (the Brief's
### Captured assumptions; the PRD's Open questions). The unattendedcondition stays caller-declared, never sniffed, matching the skill's existing doctrine. That
recorded line is scoped explicitly: it records an unrun check, is never a place to park a decision,
satisfies no part of the auto-guard, and leaves the unattended ladder's
blocked+arbiter: USER-RESERVED outcome standing.
Convention-gated tags. When
acceptance_criteria_formatresolves toears, each emittedcriterion takes a bracketed prefix from exactly five names —
ubiquitous,event-driven,state-driven,unwanted-behaviour,optional-feature— on the Brief's existing plain-bulletshape:
That vocabulary is the contract #3824 matches on, so the names are spelled verbatim in both
SKILL.md bodies and in
interview/context/loop.md. Underfree-text— the default and everydegrade — criteria are emitted untagged and byte-comparable in shape to the previous release. The
template placeholder
- <testable criterion>is untouched, and the Brief stays plain bullets,never checkboxes (that form belongs to decompose).
Both skills restate the authoring-formats resolution ladder in their own bodies rather than
citing it. An installed plugin never sees this repository's
docs/conventions/, so a path citationwould make this publisher a runtime dependency of the consumer's session.
The register carve-out (review finding 1)
The coverage prompt carries no decision, so it writes no open-question-register row. That
contradicted the register rules until it was carved out explicitly: on the synthesize-directly path
the prompt is the only question asked, so the run would ask a question it may not register and then
halt on
check-open-questions.shexit 2 (no '## Open-question register' section).Resolved as a carve-out, not by loosening the gate, and stated at all seven sites that state a
register rule:
SKILL.mdEmit-checklist,SKILL.md"Register at ask-time", the new"Acceptance-criteria capture" section,
SKILL.mdStep 3's register gate, and inloop.mdFrontier-rounds item 5, "Write at ask-time", and "Gate before locking". Each is scoped so it cannot
be read as exempting a real question that merely rides along in the same round.
Defense digests (review finding 2)
plugins/planning/tests/interview-defenses.test.shpins byte-exact lines and region digests so aprose edit cannot silently weaken the
lockSTOP-on-gap rule or the auto-guard. Ten digests wererefreshed. There is no regeneration script — the suite documents a manual procedure ("re-read
the defense, confirm it still holds, then update the skill body and this suite in one change") and
each failure emits the region's new digest. Every region was diffed against
mainand re-readagainst both defenses before its hash was taken.
The test diff is hashes only: 16 changed lines, 0 of them anything but a bare sha256 constant.
No assertion removed, no
pin_exactline altered, no boundary re-anchored.Two independent fresh-context verifiers (neither wrote the change) checked the carve-out and the
digest refresh against the attack shapes the suite's own header enumerates. Both returned no
blockers; the second found the two gaps now closed above (the seventh register site, and an
antecedent adjacency in the unattended ladder).
Boundary stretch, flagged deliberately
The ladder's step 2 says "use the bundled resolver where the plugin ships one; never hand-parse the
root file". Planning shipped none, which would have made
earsunreachable. So planning enrollsas a carrier of the cross-plugin
lib/resolve-convention-home.shcluster, via the enrollment pathscripts/sync-resolve-convention-home.shdocuments.#3901enrolledarchitectureon that sameline while this PR was open; both carriers are now listed.
Overlap with other open PRs
feat/3817-handoff-coverage) touchesplugins/planning/.claude-plugin/plugin.json,CHANGELOG.mdandREADME.md. No skill-body overlap — its edits are confined toskills/design-handoff/, mine toskills/interview/andskills/prd/.scripts/sync-resolve-convention-home.shwas also touched by the merged feat(architecture): map a C4 system landscape and application portfolio over discovered repos #3901.Planning takes 0.39.0 (main is at 0.38.0); changelog kept in descending order.
Verification
scripts/check-changed-skills.sh origin/main2 skill(s) checked, 0 failedplugins/planning/tests/interview-defenses.test.shPASS=80 FAIL=9— identical to a pristineorigin/mainworktree (same FAIL set, samewantandgot)npx markdownlint-cli2on changed markdownSummary: 0 issues in 0 filesnode scripts/validate-plugin-contracts.mjs3349 plugin files checkedscripts/check-changelog-parity.sh --check-bump origin/mainscripts/check-stale-base-overlap.sh --check origin/mainHEAD is up to date with origin/main, exit 0scripts/sync-resolve-convention-home.sh --checkAll 3 plugin copies match plugins/claude-config/lib/resolve-convention-home.sh.The 9 residual failures in the defenses suite are pre-existing and environment-dependent (jq 1.8.2
locally): all are
pin_case_digest/roster assertions overevals.json, which this branch does notmodify (
git diff origin/main -- plugins/planning/skills/interview/evals/is empty), and theyreproduce identically on pristine main, where CI is green.
Out of scope
Detecting or consuming the tags (#3824). Slice-level criteria authorship. Converting the Brief
template to checkboxes. Gherkin export, named in both Boundary sections as a deferred extension
point and built nowhere. No eval exercises the new paths;
/skill-quality:checkpasses for bothskills as the issue requires.
🤖 Generated with Claude Code
https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4