Skip to content

feat(planning): always-on unwanted-behaviour coverage prompt, EARS tags under the convention - #3882

Merged
kyle-sexton merged 9 commits into
mainfrom
feat/3821-ears-capture
Sep 7, 2026
Merged

kyle-sexton merged 9 commits into
mainfrom
feat/3821-ears-capture

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #3821. Phase 2 of the typed-ticket-body lane under #3799.

What changed

/planning:interview and /planning:prd gain 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-criterion
interrogation, 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 synthesize path skip the ask instead of blocking, state 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. 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_format resolves to ears, each emitted
criterion takes a bracketed prefix from exactly five names — ubiquitous, event-driven,
state-driven, unwanted-behaviour, optional-feature — on the Brief's existing plain-bullet
shape:

- [event-driven] WHEN the upload completes, the manifest is rewritten
- [unwanted-behaviour] IF the upload fails, THEN the partial manifest is discarded

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. 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 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 citation
would 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.sh exit 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.md Emit-checklist, SKILL.md "Register at ask-time", the new
"Acceptance-criteria capture" section, SKILL.md Step 3's register gate, and in loop.md
Frontier-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.sh pins byte-exact lines and region digests so a
prose edit cannot silently weaken the lock STOP-on-gap rule or the auto-guard. Ten digests were
refreshed. 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 main and re-read
against 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_exact line 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 ears unreachable. So planning 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. #3901 enrolled architecture on that same
line while this PR was open; both carriers are now listed.

Overlap with other open PRs

Planning takes 0.39.0 (main is at 0.38.0); changelog kept in descending order.

Verification

Check Result
scripts/check-changed-skills.sh origin/main 2 skill(s) checked, 0 failed
plugins/planning/tests/interview-defenses.test.sh PASS=80 FAIL=9 — identical to a pristine origin/main worktree (same FAIL set, same want and got)
npx markdownlint-cli2 on changed markdown Summary: 0 issues in 0 files
node scripts/validate-plugin-contracts.mjs exit 0, 3349 plugin files checked
scripts/check-changelog-parity.sh --check-bump origin/main exit 0
scripts/check-stale-base-overlap.sh --check origin/main HEAD is up to date with origin/main, exit 0
scripts/sync-resolve-convention-home.sh --check All 3 plugin copies match plugins/claude-config/lib/resolve-convention-home.sh.
test-file diff 16 changed lines, 0 non-sha256

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 over evals.json, which this branch does not
modify (git diff origin/main -- plugins/planning/skills/interview/evals/ is empty), and they
reproduce 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:check passes for both
skills as the issue requires.

🤖 Generated with Claude Code

https://claude.ai/code/session_012jiwedVq2GxuzN7siXQbr4

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T21:46:56.726241Z 7bce920 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread plugins/planning/skills/interview/SKILL.md
Comment thread plugins/planning/skills/interview/SKILL.md
@kyle-sexton
kyle-sexton enabled auto-merge (squash) September 7, 2026 00:41
kyle-sexton added a commit that referenced this pull request Sep 7, 2026
…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>
kyle-sexton added a commit that referenced this pull request Sep 7, 2026
…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>
@kyle-sexton
kyle-sexton force-pushed the feat/3821-ears-capture branch from 38396c2 to 5148dbd Compare September 7, 2026 03:58
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR body contract — issue linkage

This PR body does not yet satisfy the issue-linkage contract:

  • Missing a "## Summary" section. Describe what this PR changes and why, in a sentence or two.
  • Missing a "## Fix" section. State the concrete change and how it addresses the problem.
  • Missing a "## Related" section. List related PRs, ADRs, or decision-log entries this PR does not close.

Edit the body and this comment updates itself on the next run.

kyle-sexton added a commit that referenced this pull request Sep 7, 2026
…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>
kyle-sexton and others added 8 commits September 7, 2026 00:32
…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
@kyle-sexton
kyle-sexton force-pushed the feat/3821-ears-capture branch from 5148dbd to 5808c5d Compare September 7, 2026 08:02
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>
@kyle-sexton
kyle-sexton merged commit cfdd736 into main Sep 7, 2026
12 checks passed
@kyle-sexton
kyle-sexton deleted the feat/3821-ears-capture branch September 7, 2026 08:41
kyle-sexton added a commit that referenced this pull request Sep 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

planning/interview+prd: unwanted-behaviour coverage prompt always on, EARS format opt-in via convention doc

1 participant