From ce5982e1c1767dfd2066977aaf0d6a47e917d047 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:37:09 +0000 Subject: [PATCH 01/20] chore: capture round-9 bug-hunt findings Eighteen records: twelve confirmed substantive findings, five confirmed nitpicks, and one adjacent defect noticed during refutation (recorded, not fixed here). Assisted-by: Claude:claude-fable-5 --- ...tall-script-template-misses-the-eol-lf-pin-its.md | 12 ++++++++++++ ...ose-kept-the-pre-migration-body-the-cleanprose.md | 12 ++++++++++++ ...d-missing-schema-version-vanishes-from-capture.md | 12 ++++++++++++ ...nts-quote-the-superseded-rs001-leave-open-rule.md | 12 ++++++++++++ ...one-plugin-only-command-where-the-plugin-surfa.md | 12 ++++++++++++ ...ep-still-carries-the-tiebreak-its-own-amendmen.md | 12 ++++++++++++ ...plines-bucket-was-never-swept-for-the-spc-28-i.md | 12 ++++++++++++ ...phantom-task-classes-enum-as-source-of-truth-a.md | 12 ++++++++++++ ...es-work-against-the-retired-terminology-tree-i.md | 12 ++++++++++++ ...-leg-is-dead-under-a-pathspec-and-a-git-failur.md | 12 ++++++++++++ ...er-promises-a-breaking-section-the-derived-ing.md | 12 ++++++++++++ ...ed-by-writes-an-unverified-cross-reference-its.md | 12 ++++++++++++ ...y-terms-used-still-lists-the-retired-core-epic.md | 12 ++++++++++++ ...ompare-spec-id-raw-while-lint-canonicalises-so.md | 12 ++++++++++++ ...e-claims-the-record-explorer-ships-the-data-th.md | 12 ++++++++++++ ...gents-and-the-ci-yml-header-understate-the-lin.md | 12 ++++++++++++ ...s-the-one-unbounded-git-read-on-the-lifeboat-p.md | 12 ++++++++++++ ...swallows-intent-tree-read-errors-the-spec-half.md | 12 ++++++++++++ 18 files changed, 216 insertions(+) create mode 100644 .abcd/work/issues/open/iss-2608261437040448-the-served-install-script-template-misses-the-eol-lf-pin-its.md create mode 100644 .abcd/work/issues/open/iss-2608261437040578-cleanlessonprose-kept-the-pre-migration-body-the-cleanprose.md create mode 100644 .abcd/work/issues/open/iss-2608261437041050-a-ledger-record-missing-schema-version-vanishes-from-capture.md create mode 100644 .abcd/work/issues/open/iss-2608261437041111-readme-and-agents-quote-the-superseded-rs001-leave-open-rule.md create mode 100644 .abcd/work/issues/open/iss-2608261437042550-readme-counts-one-plugin-only-command-where-the-plugin-surfa.md create mode 100644 .abcd/work/issues/open/iss-2608261437042674-itd-5-scope-step-still-carries-the-tiebreak-its-own-amendmen.md create mode 100644 .abcd/work/issues/open/iss-2608261437043634-the-live-disciplines-bucket-was-never-swept-for-the-spc-28-i.md create mode 100644 .abcd/work/issues/open/iss-2608261437043962-itd-5-names-a-phantom-task-classes-enum-as-source-of-truth-a.md create mode 100644 .abcd/work/issues/open/iss-2608261437044340-itd-43-specifies-work-against-the-retired-terminology-tree-i.md create mode 100644 .abcd/work/issues/open/iss-2608261437044382-rd002-s-rename-leg-is-dead-under-a-pathspec-and-a-git-failur.md create mode 100644 .abcd/work/issues/open/iss-2608261437046261-changelog-header-promises-a-breaking-section-the-derived-ing.md create mode 100644 .abcd/work/issues/open/iss-2608261437046287-capture-blocked-by-writes-an-unverified-cross-reference-its.md create mode 100644 .abcd/work/issues/open/iss-2608261437046944-itd-24-glossary-terms-used-still-lists-the-retired-core-epic.md create mode 100644 .abcd/work/issues/open/iss-2608261437047643-intent-verbs-compare-spec-id-raw-while-lint-canonicalises-so.md create mode 100644 .abcd/work/issues/open/iss-2608261437047965-site-src-readme-claims-the-record-explorer-ships-the-data-th.md create mode 100644 .abcd/work/issues/open/iss-2608261437047992-contributing-agents-and-the-ci-yml-header-understate-the-lin.md create mode 100644 .abcd/work/issues/open/iss-2608261437048689-firstrootsha-is-the-one-unbounded-git-read-on-the-lifeboat-p.md create mode 100644 .abcd/work/issues/open/iss-2608261437049307-scanspeclinks-swallows-intent-tree-read-errors-the-spec-half.md diff --git a/.abcd/work/issues/open/iss-2608261437040448-the-served-install-script-template-misses-the-eol-lf-pin-its.md b/.abcd/work/issues/open/iss-2608261437040448-the-served-install-script-template-misses-the-eol-lf-pin-its.md new file mode 100644 index 00000000..b4ab6846 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437040448-the-served-install-script-template-misses-the-eol-lf-pin-its.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437040448" +slug: "the-served-install-script-template-misses-the-eol-lf-pin-its" +severity: "nitpick" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".gitattributes" +--- + +the served install script template misses the eol=lf pin its shell siblings carry \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437040578-cleanlessonprose-kept-the-pre-migration-body-the-cleanprose.md b/.abcd/work/issues/open/iss-2608261437040578-cleanlessonprose-kept-the-pre-migration-body-the-cleanprose.md new file mode 100644 index 00000000..6a6fb2a2 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437040578-cleanlessonprose-kept-the-pre-migration-body-the-cleanprose.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437040578" +slug: "cleanlessonprose-kept-the-pre-migration-body-the-cleanprose" +severity: "major" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/lifeboat/graveyard_lessons.go" +--- + +cleanLessonProse kept the pre-migration body the CleanProse consolidation claims routed \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437041050-a-ledger-record-missing-schema-version-vanishes-from-capture.md b/.abcd/work/issues/open/iss-2608261437041050-a-ledger-record-missing-schema-version-vanishes-from-capture.md new file mode 100644 index 00000000..d165273a --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437041050-a-ledger-record-missing-schema-version-vanishes-from-capture.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437041050" +slug: "a-ledger-record-missing-schema-version-vanishes-from-capture" +severity: "major" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/lint/schema.go" +--- + +a ledger record missing schema_version vanishes from capture surfaces while every gate stays green \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437041111-readme-and-agents-quote-the-superseded-rs001-leave-open-rule.md b/.abcd/work/issues/open/iss-2608261437041111-readme-and-agents-quote-the-superseded-rs001-leave-open-rule.md new file mode 100644 index 00000000..979788ed --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437041111-readme-and-agents-quote-the-superseded-rs001-leave-open-rule.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437041111" +slug: "readme-and-agents-quote-the-superseded-rs001-leave-open-rule" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "README.md" +--- + +README and AGENTS quote the superseded RS001 leave-open rule the round-8 fix retired \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437042550-readme-counts-one-plugin-only-command-where-the-plugin-surfa.md b/.abcd/work/issues/open/iss-2608261437042550-readme-counts-one-plugin-only-command-where-the-plugin-surfa.md new file mode 100644 index 00000000..f0b5c411 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437042550-readme-counts-one-plugin-only-command-where-the-plugin-surfa.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437042550" +slug: "readme-counts-one-plugin-only-command-where-the-plugin-surfa" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "README.md" +--- + +README counts one plugin-only command where the plugin surface carries three \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437042674-itd-5-scope-step-still-carries-the-tiebreak-its-own-amendmen.md b/.abcd/work/issues/open/iss-2608261437042674-itd-5-scope-step-still-carries-the-tiebreak-its-own-amendmen.md new file mode 100644 index 00000000..01781ba4 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437042674-itd-5-scope-step-still-carries-the-tiebreak-its-own-amendmen.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437042674" +slug: "itd-5-scope-step-still-carries-the-tiebreak-its-own-amendmen" +severity: "nitpick" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md" +--- + +itd-5 scope step still carries the tiebreak its own amendment struck \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437043634-the-live-disciplines-bucket-was-never-swept-for-the-spc-28-i.md b/.abcd/work/issues/open/iss-2608261437043634-the-live-disciplines-bucket-was-never-swept-for-the-spc-28-i.md new file mode 100644 index 00000000..04e27502 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437043634-the-live-disciplines-bucket-was-never-swept-for-the-spc-28-i.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437043634" +slug: "the-live-disciplines-bucket-was-never-swept-for-the-spc-28-i" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".abcd/development/intents/disciplines" +--- + +the live disciplines bucket was never swept for the spc-28 intent-auditor rename \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437043962-itd-5-names-a-phantom-task-classes-enum-as-source-of-truth-a.md b/.abcd/work/issues/open/iss-2608261437043962-itd-5-names-a-phantom-task-classes-enum-as-source-of-truth-a.md new file mode 100644 index 00000000..bdf3507a --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437043962-itd-5-names-a-phantom-task-classes-enum-as-source-of-truth-a.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437043962" +slug: "itd-5-names-a-phantom-task-classes-enum-as-source-of-truth-a" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md" +--- + +itd-5 names a phantom task_classes enum as source of truth and carries the retired intent_review token \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437044340-itd-43-specifies-work-against-the-retired-terminology-tree-i.md b/.abcd/work/issues/open/iss-2608261437044340-itd-43-specifies-work-against-the-retired-terminology-tree-i.md new file mode 100644 index 00000000..f053d8b6 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437044340-itd-43-specifies-work-against-the-retired-terminology-tree-i.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437044340" +slug: "itd-43-specifies-work-against-the-retired-terminology-tree-i" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".abcd/development/intents/planned/itd-43-epic-to-spec-terminology.md" +--- + +itd-43 specifies work against the retired terminology tree its own spec records as delivered \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437044382-rd002-s-rename-leg-is-dead-under-a-pathspec-and-a-git-failur.md b/.abcd/work/issues/open/iss-2608261437044382-rd002-s-rename-leg-is-dead-under-a-pathspec-and-a-git-failur.md new file mode 100644 index 00000000..a75b5c5d --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437044382-rd002-s-rename-leg-is-dead-under-a-pathspec-and-a-git-failur.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437044382" +slug: "rd002-s-rename-leg-is-dead-under-a-pathspec-and-a-git-failur" +severity: "major" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "scripts/check-reviews.sh" +--- + +RD002's rename leg is dead under a pathspec and a git failure reads as clean \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437046261-changelog-header-promises-a-breaking-section-the-derived-ing.md b/.abcd/work/issues/open/iss-2608261437046261-changelog-header-promises-a-breaking-section-the-derived-ing.md new file mode 100644 index 00000000..b0239af2 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437046261-changelog-header-promises-a-breaking-section-the-derived-ing.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437046261" +slug: "changelog-header-promises-a-breaking-section-the-derived-ing" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "CHANGELOG.md" +--- + +CHANGELOG header promises a Breaking section the derived ingest cannot emit \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437046287-capture-blocked-by-writes-an-unverified-cross-reference-its.md b/.abcd/work/issues/open/iss-2608261437046287-capture-blocked-by-writes-an-unverified-cross-reference-its.md new file mode 100644 index 00000000..2dc1a650 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437046287-capture-blocked-by-writes-an-unverified-cross-reference-its.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437046287" +slug: "capture-blocked-by-writes-an-unverified-cross-reference-its" +severity: "major" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/capture/workflow.go" +--- + +capture --blocked-by writes an unverified cross-reference its own blocker then refuses \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437046944-itd-24-glossary-terms-used-still-lists-the-retired-core-epic.md b/.abcd/work/issues/open/iss-2608261437046944-itd-24-glossary-terms-used-still-lists-the-retired-core-epic.md new file mode 100644 index 00000000..a1b5f4f8 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437046944-itd-24-glossary-terms-used-still-lists-the-retired-core-epic.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437046944" +slug: "itd-24-glossary-terms-used-still-lists-the-retired-core-epic" +severity: "nitpick" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: ".abcd/development/intents/planned/itd-24-reflect-command.md" +--- + +itd-24 glossary_terms_used still lists the retired core/epic term \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437047643-intent-verbs-compare-spec-id-raw-while-lint-canonicalises-so.md b/.abcd/work/issues/open/iss-2608261437047643-intent-verbs-compare-spec-id-raw-while-lint-canonicalises-so.md new file mode 100644 index 00000000..07d18940 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437047643-intent-verbs-compare-spec-id-raw-while-lint-canonicalises-so.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437047643" +slug: "intent-verbs-compare-spec-id-raw-while-lint-canonicalises-so" +severity: "minor" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/intent/lifecycle.go" +--- + +intent verbs compare spec_id raw while lint canonicalises so a lint-green slug or zero-padded spelling bricks reconcile and ready \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437047965-site-src-readme-claims-the-record-explorer-ships-the-data-th.md b/.abcd/work/issues/open/iss-2608261437047965-site-src-readme-claims-the-record-explorer-ships-the-data-th.md new file mode 100644 index 00000000..a8c8ff06 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437047965-site-src-readme-claims-the-record-explorer-ships-the-data-th.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437047965" +slug: "site-src-readme-claims-the-record-explorer-ships-the-data-th" +severity: "minor" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "site-src/README.md" +--- + +site-src README claims the record explorer ships the data-theme control nothing sets \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437047992-contributing-agents-and-the-ci-yml-header-understate-the-lin.md b/.abcd/work/issues/open/iss-2608261437047992-contributing-agents-and-the-ci-yml-header-understate-the-lin.md new file mode 100644 index 00000000..cd29d891 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437047992-contributing-agents-and-the-ci-yml-header-understate-the-lin.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437047992" +slug: "contributing-agents-and-the-ci-yml-header-understate-the-lin" +severity: "nitpick" +category: "observation" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "CONTRIBUTING.md" +--- + +CONTRIBUTING AGENTS and the ci.yml header understate the Linux leg and the record-lint job \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437048689-firstrootsha-is-the-one-unbounded-git-read-on-the-lifeboat-p.md b/.abcd/work/issues/open/iss-2608261437048689-firstrootsha-is-the-one-unbounded-git-read-on-the-lifeboat-p.md new file mode 100644 index 00000000..f7055074 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437048689-firstrootsha-is-the-one-unbounded-git-read-on-the-lifeboat-p.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437048689" +slug: "firstrootsha-is-the-one-unbounded-git-read-on-the-lifeboat-p" +severity: "nitpick" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/lifeboat/probe.go" +--- + +firstRootSHA is the one unbounded git read on the lifeboat probe path \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-2608261437049307-scanspeclinks-swallows-intent-tree-read-errors-the-spec-half.md b/.abcd/work/issues/open/iss-2608261437049307-scanspeclinks-swallows-intent-tree-read-errors-the-spec-half.md new file mode 100644 index 00000000..e3d8aee7 --- /dev/null +++ b/.abcd/work/issues/open/iss-2608261437049307-scanspeclinks-swallows-intent-tree-read-errors-the-spec-half.md @@ -0,0 +1,12 @@ +--- +schema_version: 1 +id: "iss-2608261437049307" +slug: "scanspeclinks-swallows-intent-tree-read-errors-the-spec-half" +severity: "nitpick" +category: "bug" +source: "agent-observation" +found_during: "bughunt-b-round-9" +found_at: "internal/core/lint/speclinks.go" +--- + +ScanSpecLinks swallows intent-tree read errors the spec half propagates \ No newline at end of file From 15e7d9128426948de24d0f5716be883373f17d46 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:38:55 +0000 Subject: [PATCH 02/20] docs: RS001 requires entering a terminal folder, at every site that states it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-8 gate correction changed RS001 from leaves-open/ to enters-resolved/-or-wontfix/, and the docs kept the superseded rule: the README's flagship refusal example quoted a message the gate no longer emits, and AGENTS.md and the record-lands-with-the-act principle both taught the predicate under which a bare delete of the open record satisfies the trailer — the exact hole that correction closed. Resolves: iss-2608261437041111 Assisted-by: Claude:claude-fable-5 --- .../development/principles/the-record-lands-with-the-act.md | 5 +++-- AGENTS.md | 5 +++-- README.md | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.abcd/development/principles/the-record-lands-with-the-act.md b/.abcd/development/principles/the-record-lands-with-the-act.md index 3270c796..bb2d0a2f 100644 --- a/.abcd/development/principles/the-record-lands-with-the-act.md +++ b/.abcd/development/principles/the-record-lands-with-the-act.md @@ -48,8 +48,9 @@ opportunity produces nothing. **Live instance.** iss-2608241347321757 dissolves the dependency. `make lint-issues` (RS001) requires a commit carrying a `Resolves: iss-N` trailer to -move that record out of `open/` in the same diff, so resolution lands inside the -fixing commit and no post-merge step exists to forget. `resolved_by.commit` +land that record in `resolved/` or `wontfix/` in the same diff — a bare delete +of the open record satisfies nothing — so resolution lands inside the fixing +commit and no post-merge step exists to forget. `resolved_by.commit` becomes optional-but-verified: RS002 checks that a stamp added in a range names a reachable commit, and RS003 checks that every stamp already in the ledger stays reachable under squash and rebase merges. diff --git a/AGENTS.md b/AGENTS.md index ccc4b377..71fb132c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,8 +168,9 @@ irreversible; guessing downward costs nothing.** blocks the next release. - **A change that fixes a captured issue resolves it in the same change**, and says so with a `Resolves: iss-N` trailer. `lint-issues` (RS001) refuses a - trailer whose record does not leave `.abcd/work/issues/open/` in the same - diff. Resolution is deliberately not a post-merge step: a step that happens + trailer whose record does not enter `.abcd/work/issues/resolved/` or + `.abcd/work/issues/wontfix/` in the same diff — a bare delete of the open + record satisfies nothing. Resolution is deliberately not a post-merge step: a step that happens after the merge is the one that gets forgotten, and a fixed-but-open issue leaves no marker to find it by. Resolving without a trailer stays legal — a stale issue closed on its own merits has no fixing commit to name. diff --git a/README.md b/README.md index c7613d15..d905b4e7 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ stop it: ```text $ git push -check-issue-resolution: RS001 commit c1e2c4c3d915 declares 'Resolves: iss-317', but iss-317 does not leave .abcd/work/issues/open/ in origin/main..HEAD. Resolve it in this change (abcd capture resolve iss-317 ...) or drop the trailer. +check-issue-resolution: RS001 commit c1e2c4c3d915 declares 'Resolves: iss-317', but iss-317 does not enter .abcd/work/issues/resolved/ or .abcd/work/issues/wontfix/ in origin/main..HEAD. Resolve it in this change (abcd capture resolve iss-317 ...) or drop the trailer. check-issue-resolution: FAILED — 1 violation(s) ``` From 6428a251e4b8035811452b78153b8c548826e568 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:39:08 +0000 Subject: [PATCH 03/20] docs: the plugin surface adds three commands, not one The README counted /abcd:prepare-this-repo as the only plugin-side command the CLI lacks; /abcd:consult and /abcd:ingest are likewise host-delegated markdown with no Go verb (the brief's surface chapters and the 2026-07-11 sign-off both record the three together), so a reader looking for 'abcd consult' or 'abcd ingest' in the terminal found only 'unknown command'. Resolves: iss-2608261437042550 Assisted-by: Claude:claude-fable-5 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d905b4e7..b1acb71d 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,9 @@ abcd — /path/to/your-repo work tiers: [development work work.local] ``` -The plugin surface adds one command the CLI does not carry: `/abcd:prepare-this-repo` gives a repository with no record yet the three-tier -`.abcd/` layout, an `AGENTS.md` router, and the commit gates. +The plugin surface adds three commands the CLI does not carry, all host-delegated markdown with no Go verb behind them. The one that matters +first: `/abcd:prepare-this-repo` gives a repository with no record yet the three-tier `.abcd/` layout, an `AGENTS.md` router, and the commit +gates. (`/abcd:consult` and `/abcd:ingest` drive a local sources corpus.) From there, three verbs cover most of a first session. In a plugin session, `/abcd:lint` checks the repository against the working conventions and names what is missing. `/abcd:capture "…"` files a half-formed observation to the issue ledger so it survives the session that noticed it. `/abcd:intent "…"` opens a user-facing change as a press-release intent, which is where a shipping change starts. From a7e602f6476d9a8e7917968cf0071840d6e1dd43 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:39:38 +0000 Subject: [PATCH 04/20] docs: the derived changelog signals a pre-1.0 break by the bump, not a Breaking section The CHANGELOG header promised each pre-1.0 break a **Breaking** section, but the derived ingest renders a closed set of six Keep a Changelog sections and refuses any other, so the promise is unfulfillable for every cut since v0.6.0's final manual roll. The header now states the derived truth; adr-37 carries a dated amendment recording that its interim called-out-under-Breaking clause lapsed when itd-73 landed, and the version-arithmetic comment quotes the amendment instead of the lapsed clause. Resolves: iss-2608261437046261 Assisted-by: Claude:claude-fable-5 --- .../decisions/adrs/0037-changelog-driven-releases.md | 7 +++++++ CHANGELOG.md | 7 +++++-- internal/core/changelog/version.go | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.abcd/development/decisions/adrs/0037-changelog-driven-releases.md b/.abcd/development/decisions/adrs/0037-changelog-driven-releases.md index f99f27d5..d70afa91 100644 --- a/.abcd/development/decisions/adrs/0037-changelog-driven-releases.md +++ b/.abcd/development/decisions/adrs/0037-changelog-driven-releases.md @@ -59,6 +59,13 @@ release automation: here is the *recording and cutting* instrument either way, not a second source of the number. + > **Amendment (2026-08-26).** itd-73 has landed: the ingest derives the + > number and renders the six closed Keep a Changelog sections, so a + > pre-1.0 break is signalled by the minor bump and the section its record + > belongs to, not by a **Breaking** heading — the derived renderer has no + > such section. The "called out under **Breaking**" clause above described + > the interim hand-rolled regime, whose last cut was v0.6.0. + ## Alternatives Considered - **Manual tag push as the decision.** The status quo. Rejected as policy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 676ece2c..056dd5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ All notable changes to abcd are recorded here. The format follows uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with a leading `v`. -Before v1.0.0, minor releases may make breaking changes; each one is -called out in a **Breaking** section. +Before v1.0.0, a minor release may make breaking changes: a record +declaring `impact: breaking` bumps the minor, and its entry appears under +the Keep a Changelog section it belongs to — usually **Changed** or +**Removed**. The version number is the signal; sections at v0.6.0 and +earlier were rolled by hand and some carry a **Breaking** heading. ## [Unreleased] diff --git a/internal/core/changelog/version.go b/internal/core/changelog/version.go index 382b8078..f6e135a1 100644 --- a/internal/core/changelog/version.go +++ b/internal/core/changelog/version.go @@ -23,8 +23,9 @@ import "github.com/intentdriven/abcd/internal/core/launch" // fix -> patch++ // // The pre-1.0 row is load-bearing, not a shortcut. While abcd is at 0.x it has -// declared no stable surface, so a break bumps the minor (ADR-37: "pre-1.0, a -// minor may break, called out under Breaking"). The consequence is deliberate: +// declared no stable surface, so a break bumps the minor (ADR-37, as amended: +// pre-1.0, a minor may break — the bump itself is the signal, since the +// derived changelog has no Breaking section). The consequence is deliberate: // NO input can derive 1.0.0 from a 0.x base. The first 1.0.0 is a human's // explicit override, because declaring stability is a product decision no set of // records can make on the maintainer's behalf. From 371c1f4c848e85927df0359e8f0923f24dfb3ae1 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:39:51 +0000 Subject: [PATCH 05/20] docs: no page sets data-theme yet, and the build README says so The record explorer shipped without the theme control; the stylesheet's own header was corrected in that change and the build README's claim that the explorer ships the control was left behind, contradicting the sibling file one directory over. Resolves: iss-2608261437047965 Assisted-by: Claude:claude-fable-5 --- site-src/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site-src/README.md b/site-src/README.md index 3fd3d800..0f0d35ce 100644 --- a/site-src/README.md +++ b/site-src/README.md @@ -8,8 +8,8 @@ comes from the repository. This directory holds: added here that no field reads fails the build. - `site.css` — the stylesheet: light is the base and dark redefines tokens only. The landing page follows the reader's system preference; a page that sets an - explicit `data-theme` attribute is honoured too, and the record explorer is - the slice that ships the control which sets one. + explicit `data-theme` attribute is honoured too, though no page sets one yet — + those blocks are carried deferred for a future theme control. - `site.js` — the landing page's only script: install tabs, copy buttons, and nothing else. It adds no words of its own — every string it shows is read back from the markup, where the build put it from `ui.json`. No analytics, no From 30b965b6208c5748d91e21f48334d9a071f62775 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:40:30 +0000 Subject: [PATCH 06/20] docs: itd-24 cites the glossary's spec term, not its banned synonym MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glossary_terms_used machine field listed both core/epic — a term file that does not exist, whose name is the armed forbidden synonym — and its successor core/spec. The dangling entry sat in the blind spot between forbidden_synonyms (frontmatter out of scope) and record_schema (field not cross-referenced). core/spec already covers the concept, so the retired entry is dropped losslessly. Resolves: iss-2608261437046944 Assisted-by: Claude:claude-fable-5 --- .abcd/development/intents/planned/itd-24-reflect-command.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.abcd/development/intents/planned/itd-24-reflect-command.md b/.abcd/development/intents/planned/itd-24-reflect-command.md index 9a5fca1a..24a4b4a0 100644 --- a/.abcd/development/intents/planned/itd-24-reflect-command.md +++ b/.abcd/development/intents/planned/itd-24-reflect-command.md @@ -6,7 +6,7 @@ kind: bundle-member bundle: spc-83-operator-surfaces suggested_kind: null reclassification_history: [] -glossary_terms_used: [core/phase, core/intent, core/voyage, core/persona, core/brief, core/epic, core/lifeboat, core/oracle, core/spec, interview/embark, distribution/release] +glossary_terms_used: [core/phase, core/intent, core/voyage, core/persona, core/brief, core/lifeboat, core/oracle, core/spec, interview/embark, distribution/release] grill_session_id: e6a24d86-e133-495f-8dec-94dec21449ea grilled_at: 2026-05-16T15:37:17Z grilled_intent_hash: 8412a59b575df882fc4a370ab01404796cad4dd9e120d0519e9918d3ea891c61 From 66c06e15464c820cc514ef18998dda0daf0cd39d Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:40:30 +0000 Subject: [PATCH 07/20] chore: pin lf line endings on the served install script template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five shell pins in .gitattributes missed site-src/install.sh.tmpl — the one shell artefact the repository distributes: the site build copies its bytes into the /install.sh users pipe into sh, where a trailing carriage return on every line is a syntax error. Today every publishing path runs on a Linux runner, so the exposure is latent; the pin gives the template the control its siblings already have. Resolves: iss-2608261437040448 Assisted-by: Claude:claude-fable-5 --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitattributes b/.gitattributes index c28c0719..a7ef930c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,3 +26,9 @@ hooks/*.sh text eol=lf # via `bash scripts/...`); a CRLF checkout breaks both — a \r is a syntax error # to bash — so the pinned pre-push hook would then die one hop later here. scripts/*.sh text eol=lf +# site-src/install.sh.tmpl is the committed source of the /install.sh the site +# serves: `abcd site build` copies its bytes out (plus one stamp comment) and +# users pipe the result straight into `sh`, where a trailing \r on every line +# is a syntax error — no shebang required to break it. A render from a CRLF +# checkout would publish an installer that cannot run. +site-src/install.sh.tmpl text eol=lf From eff3168cb00a422144159645f3a6f1d487a27453 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:41:22 +0000 Subject: [PATCH 08/20] docs: sweep the live disciplines bucket onto the intent-auditor rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spc-28 rename (intent-fidelity-reviewer to intent-auditor, abcd intent review to abcd intent audit) listed disciplines/ in neither its swept inventory nor its not-swept exemptions, and the bucket is live state with no reachable reconciliation event — intent audit refuses a non-shipped intent, so the audited-when-first-audited promise in these files could never fire. Twenty-one substitutions across itd-1, itd-37, itd-81 and itd-84, including the two /abcd:intent review invocations in itd-1's spc-12 clause; stored artefact spellings (abcd-review markers, review- receipt directories, the logbook paths) stay frozen per spc-28, and drafts/ and planned/ stay unswept under the iss-94 convention. spc-28's not-swept paragraph records the late sweep. Resolves: iss-2608261437043634 Assisted-by: Claude:claude-fable-5 --- .../disciplines/itd-1-acceptance-gates.md | 20 +++++++++---------- .../itd-37-modification-grammar.md | 12 +++++------ .../disciplines/itd-81-judge-calibration.md | 6 +++--- .../itd-84-intent-decomposition.md | 2 +- ...s-audit-abcd-intent-review-becomes-abcd.md | 8 +++++++- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.abcd/development/intents/disciplines/itd-1-acceptance-gates.md b/.abcd/development/intents/disciplines/itd-1-acceptance-gates.md index 2a20f538..56bf0ecd 100644 --- a/.abcd/development/intents/disciplines/itd-1-acceptance-gates.md +++ b/.abcd/development/intents/disciplines/itd-1-acceptance-gates.md @@ -2,7 +2,7 @@ id: itd-1 slug: acceptance-gates kind: discipline -kind_notes: "Cross-cutting acceptance-criteria gate; applied via the record lint (internal/core/lint) at /abcd:intent plan time and verified by intent-fidelity-reviewer (single-document role) on every shipped intent." +kind_notes: "Cross-cutting acceptance-criteria gate; applied via the record lint (internal/core/lint) at /abcd:intent plan time and verified by intent-auditor (single-document role) on every shipped intent." suggested_kind: null spec_id: null reclassification_history: @@ -14,11 +14,11 @@ severity: critical ## Rule -Every standalone or bundle-member intent in `drafts/` and `planned/` carries a `## Acceptance Criteria` section with at least one well-formed Given-When-Then bullet. Every discipline intent in `disciplines/` carries the same. `/abcd:intent plan` refuses to promote an intent without it (hard block via the record lint, `internal/core/lint`). Every shipped intent's `## Audit Notes` section contains per-criterion verdicts (`MET` / `MET_WITH_CONCERNS` / `NOT_MET` / `INCONCLUSIVE`) emitted by the `intent-fidelity-reviewer` agent's single-document role. +Every standalone or bundle-member intent in `drafts/` and `planned/` carries a `## Acceptance Criteria` section with at least one well-formed Given-When-Then bullet. Every discipline intent in `disciplines/` carries the same. `/abcd:intent plan` refuses to promote an intent without it (hard block via the record lint, `internal/core/lint`). Every shipped intent's `## Audit Notes` section contains per-criterion verdicts (`MET` / `MET_WITH_CONCERNS` / `NOT_MET` / `INCONCLUSIVE`) emitted by the `intent-auditor` agent's single-document role. ## Why -abcd's intent format captures direction (press release or `## Rule`) and scope (in/out lists). Without acceptance criteria it cannot capture **the verifiable bar for "shipped"**. The `intent-fidelity-reviewer` agent is otherwise reduced to interpreting prose, which means drift detection depends on the reviewer's judgement rather than on a pre-committed standard. +abcd's intent format captures direction (press release or `## Rule`) and scope (in/out lists). Without acceptance criteria it cannot capture **the verifiable bar for "shipped"**. The `intent-auditor` agent is otherwise reduced to interpreting prose, which means drift detection depends on the reviewer's judgement rather than on a pre-committed standard. Prior art ([PAUL][paul]) treats acceptance criteria as a hard gate: defined before tasks, verified by an Execute/Qualify loop, with multi-state escalation outcomes (`DONE` / `DONE_WITH_CONCERNS` / `NEEDS_CONTEXT` / `BLOCKED`). The full PAUL framework is more than abcd currently needs; the **acceptance-criteria pattern** ([Given-When-Then][bdd-given-when-then]) is the load-bearing piece. @@ -28,7 +28,7 @@ This is a small schema bump with a large quality return. Every intent gets a mea - **`## Acceptance Criteria` section** required in every intent template (standalone, bundle-member, *and* discipline). At least one Given-When-Then bullet. The section header is fixed (parser depends on it). - **Hard-block validation in `/abcd:intent plan`** — intent cannot transition `drafts/` → `planned/` (or `drafts/` → `disciplines/`) without at least one well-formed acceptance criterion. Lint code: `IL002` (delivered by spc-8; see `05-internals/06-lint.md`). -- **`intent-fidelity-reviewer` single-document role** — when auditing a shipped intent, the agent emits a per-criterion verdict block into the intent's own `## Audit Notes` section. The writer maintains a single delimited `### itd-1 review ` block (machine-fenced so a repeat review *replaces* it in place — the section never accumulates stale blocks; git history is the prior-review trail): +- **`intent-auditor` single-document role** — when auditing a shipped intent, the agent emits a per-criterion verdict block into the intent's own `## Audit Notes` section. The writer maintains a single delimited `### itd-1 review ` block (machine-fenced so a repeat review *replaces* it in place — the section never accumulates stale blocks; git history is the prior-review trail): ``` ## Audit Notes @@ -42,11 +42,11 @@ This is a small schema bump with a large quality return. Every intent gets a mea Overall: MET / MET_WITH_CONCERNS / NOT_MET / INCONCLUSIVE ``` - This `## Audit Notes` write is the **verdict of record**; each run also writes a per-run forensic copy at `.abcd/logbook/audit/review-/report.{json,md}`. **spc-12 ships the manual reviewer** (`/abcd:intent review `) plus the `## Audit Notes` / `review-` writers; **automatic invocation on the `planned → shipped` transition is deferred to the lifecycle-owning spec** (`spc-6`). Until that lands, a shipped intent's `## Audit Notes` is populated only when `/abcd:intent review` is run by hand. + This `## Audit Notes` write is the **verdict of record**; each run also writes a per-run forensic copy at `.abcd/logbook/audit/review-/report.{json,md}`. **spc-12 ships the manual reviewer** (`/abcd:intent audit `) plus the `## Audit Notes` / `review-` writers; **automatic invocation on the `planned → shipped` transition is deferred to the lifecycle-owning spec** (`spc-6`). Until that lands, a shipped intent's `## Audit Notes` is populated only when `/abcd:intent audit` is run by hand. - **Escalation states** — four states, lifted from PAUL. Binary pass/fail loses information; four states preserve nuance without exploding. -- **Verdict family disjointness** (cross-referenced from [`05-internals/01-agents.md § Verdict-tag protocol`](../../brief/05-internals/01-agents.md#verdict-tag-protocol)). The four criterion verdicts above (`MET` / `MET_WITH_CONCERNS` / `NOT_MET` / `INCONCLUSIVE`) score *promise vs reality on a shipped intent* — they belong to `intent-fidelity-reviewer`'s Role 1 output. They are **deliberately disjoint from review verdicts** (`SHIP` / `NEEDS_WORK` / `MAJOR_RETHINK`) which score *changes/runs* (oracle reviews of plans, implementations, completions; consumed by the native receipt schema validator). The two enums never mix — review verdicts emit on a *change*, criterion verdicts emit on a *promise*. This disjointness was reinforced 2026-05-08 when idea-4's pre-review draft conflated the two families ("NOT_MET on an agent run" — wrong; criterion verdicts apply to intents not agents). Closing-the-loop signals on agents (per Frontier Awareness, idea-4) MUST use canary/golden-test/operator-tagged failure signals, NOT spec-level criterion verdicts. +- **Verdict family disjointness** (cross-referenced from [`05-internals/01-agents.md § Verdict-tag protocol`](../../brief/05-internals/01-agents.md#verdict-tag-protocol)). The four criterion verdicts above (`MET` / `MET_WITH_CONCERNS` / `NOT_MET` / `INCONCLUSIVE`) score *promise vs reality on a shipped intent* — they belong to `intent-auditor`'s Role 1 output. They are **deliberately disjoint from review verdicts** (`SHIP` / `NEEDS_WORK` / `MAJOR_RETHINK`) which score *changes/runs* (oracle reviews of plans, implementations, completions; consumed by the native receipt schema validator). The two enums never mix — review verdicts emit on a *change*, criterion verdicts emit on a *promise*. This disjointness was reinforced 2026-05-08 when idea-4's pre-review draft conflated the two families ("NOT_MET on an agent run" — wrong; criterion verdicts apply to intents not agents). Closing-the-loop signals on agents (per Frontier Awareness, idea-4) MUST use canary/golden-test/operator-tagged failure signals, NOT spec-level criterion verdicts. - **Intent template update** — the intent template the binary scaffolds (`internal/core`) includes the `## Acceptance Criteria` section with one example criterion. The discipline template (separate file) includes the same section. -- **Inheritance into every other spec** — every native spec plan-reviewed under abcd inherits the discipline's gate: the spec must reference the parent intent's acceptance criteria as the verification bar, and `intent-fidelity-reviewer` checks delivered reality against them on shipping. +- **Inheritance into every other spec** — every native spec plan-reviewed under abcd inherits the discipline's gate: the spec must reference the parent intent's acceptance criteria as the verification bar, and `intent-auditor` checks delivered reality against them on shipping. ## What's Out of Scope @@ -58,12 +58,12 @@ This is a small schema bump with a large quality return. Every intent gets a mea ## Acceptance Criteria -> _Yes, this discipline eats its own dog food. The criteria below describe how the discipline itself is checked — by the record lint (`internal/core/lint`) at promotion time and by `intent-fidelity-reviewer`'s single-document role on every shipped intent._ +> _Yes, this discipline eats its own dog food. The criteria below describe how the discipline itself is checked — by the record lint (`internal/core/lint`) at promotion time and by `intent-auditor`'s single-document role on every shipped intent._ - **Given** a draft intent without an `## Acceptance Criteria` section, **when** the user runs `/abcd:intent plan itd-N`, **then** the command refuses to promote the intent and lists the missing section as the reason. - **Given** a draft intent with a malformed acceptance section (e.g. no Given-When-Then bullets, or a header but empty body), **when** `/abcd:intent plan` runs, **then** the lint emits a specific error pointing at the malformed line. - **Given** a discipline-kind intent in `drafts/` without a `## Acceptance Criteria` section, **when** the user attempts to promote it via `/abcd:intent plan --kind discipline`, **then** the same hard-block applies — disciplines are not exempt from their own rule. -- **Given** a shipped intent with three acceptance criteria, **when** `intent-fidelity-reviewer` runs, **then** the resulting Audit Notes contain exactly three per-criterion verdict lines plus an overall status. *(Audit note 2026-05-07: this criterion is verifiable only after the reviewer's implementation lands in a separate spec; until then it is a paper-only gate. The reviewer itself will retroactively run against this discipline when implemented.)* +- **Given** a shipped intent with three acceptance criteria, **when** `intent-auditor` runs, **then** the resulting Audit Notes contain exactly three per-criterion verdict lines plus an overall status. *(Audit note 2026-05-07: this criterion is verifiable only after the reviewer's implementation lands in a separate spec; until then it is a paper-only gate. The reviewer itself will retroactively run against this discipline when implemented.)* - **Given** a reviewer verdict of `NOT_MET` on any criterion, **when** the reviewer writes its report, **then** the report includes a "Divergence" sub-section explaining what was delivered vs. what was promised — not just a label. - **Given** the brief is updated, **when** a contributor reads `04-surfaces/05-intent.md` (intent system), **then** they find the acceptance-pattern requirement and one worked example with all four verdict types. - **Given** a native spec that traces back to a parent intent, **when** the spec's plan-review runs, **then** the review verifies the spec's acceptance section references (or is structurally compatible with) the parent intent's `## Acceptance Criteria` bullets — drift between the two surfaces is flagged. @@ -79,7 +79,7 @@ This is a small schema bump with a large quality return. Every intent gets a mea ## Audit Notes -_Empty. Populated by intent-fidelity-reviewer's single-document role when this discipline is first audited. Note: disciplines never "ship" in the standalone-intent sense — they are audited continuously via the rule-applies-to-every-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any spec that violated this discipline (e.g., merged without an AC section)._ +_Empty. Populated by intent-auditor's single-document role when this discipline is first audited. Note: disciplines never "ship" in the standalone-intent sense — they are audited continuously via the rule-applies-to-every-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any spec that violated this discipline (e.g., merged without an AC section)._ ## References diff --git a/.abcd/development/intents/disciplines/itd-37-modification-grammar.md b/.abcd/development/intents/disciplines/itd-37-modification-grammar.md index 7aea8f6b..d5c35982 100644 --- a/.abcd/development/intents/disciplines/itd-37-modification-grammar.md +++ b/.abcd/development/intents/disciplines/itd-37-modification-grammar.md @@ -2,7 +2,7 @@ id: itd-37 slug: modification-grammar kind: discipline -kind_notes: "Cross-cutting modification-grammar gate; applied at every spec plan-review and ship time via concreteness lint (MG001-MG003) + intent-fidelity-reviewer Role 1 boilerplate detection (MG004). Every spec inherits this rule. Closes Naur's Modification axis — the genuinely new gap in abcd's theory transmission (Mapping + Justification already partially captured by press release + audit notes)." +kind_notes: "Cross-cutting modification-grammar gate; applied at every spec plan-review and ship time via concreteness lint (MG001-MG003) + intent-auditor Role 1 boilerplate detection (MG004). Every spec inherits this rule. Closes Naur's Modification axis — the genuinely new gap in abcd's theory transmission (Mapping + Justification already partially captured by press release + audit notes)." suggested_kind: null spec_id: null reclassification_history: [] @@ -38,7 +38,7 @@ The discipline is named for what's actually new — **modification grammar**, no **Why two-layer enforcement.** Boilerplate-rot is the discipline's principal failure mode. A `## Modification Grammar` section that says *"extending requires care; modifying requires understanding the design"* is non-empty, parseable, content-free — and **worse than no section at all** because it occupies cross-cutting query surface with noise. A regex-level "non-empty" lint catches the laziest failure but not plausible-sounding boilerplate. Two layers: 1. **Concreteness lint (mechanical)** rejects sections without a concrete reference (code block / file path / line ref). Lint codes `MG001`-`MG003`. -2. **Semantic boilerplate detection (reviewer judgement)** runs `intent-fidelity-reviewer` Role 1 (the discipline role per itd-1) at spec plan-review and ship time. Prompt-encoded test: ***"Strip the spec name. Could this `## Modification Grammar` text describe a different spec? If yes, reject."*** Lint code `MG004`. +2. **Semantic boilerplate detection (reviewer judgement)** runs `intent-auditor` Role 1 (the discipline role per itd-1) at spec plan-review and ship time. Prompt-encoded test: ***"Strip the spec name. Could this `## Modification Grammar` text describe a different spec? If yes, reject."*** Lint code `MG004`. The semantic enforcement is genuine LLM-judgement work. The discipline owns the requirement explicitly — regex cannot catch boilerplate. @@ -57,7 +57,7 @@ The semantic enforcement is genuine LLM-judgement work. The discipline owns the - `MG001` — section missing. - `MG002` — sub-heading missing or empty. - `MG003` — sub-heading present but contains no concrete reference (code block / file path / line ref). -- **Semantic boilerplate detection** via `intent-fidelity-reviewer` Role 1's discipline-checking pass. New prompt-encoded test ("could this describe a different spec?"). Lint code `MG004` — Role 1 boilerplate verdict. +- **Semantic boilerplate detection** via `intent-auditor` Role 1's discipline-checking pass. New prompt-encoded test ("could this describe a different spec?"). Lint code `MG004` — Role 1 boilerplate verdict. - **Specific rejection criteria** the prompt encodes: (a) `Extends cleanly` rejected if it doesn't name a concrete extension point with a constraint; (b) `Breaks the design` rejected if it doesn't name a specific failure mode; (c) `Why` rejected if it's a list of cases without an underlying rule, OR if the rule could equally describe a different spec. - **`principle-distiller` extraction trigger** — at spec completion, the curator (per itd-36's role extension): - Writes append-only `spec_modification_grammar_.md` to `.abcd/memory/` with `source.class: spec_modification_grammar`. @@ -79,12 +79,12 @@ The semantic enforcement is genuine LLM-judgement work. The discipline owns the ## Acceptance Criteria -> _BDD format, per [itd-1 acceptance gates](itd-1-acceptance-gates.md). These gates are checked by `intent-fidelity-reviewer` Role 1 against every spec plan-reviewed under abcd._ +> _BDD format, per [itd-1 acceptance gates](itd-1-acceptance-gates.md). These gates are checked by `intent-auditor` Role 1 against every spec plan-reviewed under abcd._ - **Given** a spec without a `## Modification Grammar` section, **when** plan-review runs, **then** the intent lint emits `MG001` and blocks promotion. - **Given** a spec with `## Modification Grammar` but missing one of the three required sub-headings (`Extends cleanly` / `Breaks the design` / `Why`), **when** plan-review runs, **then** the lint emits `MG002` naming the missing sub-heading. - **Given** a spec where any of the three sub-headings is present but contains no concrete reference (no code block, no file path, no line ref), **when** plan-review runs, **then** the lint emits `MG003` and points at the offending sub-heading. -- **Given** a spec where `## Modification Grammar` content could equally describe a different spec (boilerplate failure), **when** `intent-fidelity-reviewer` Role 1 runs the discipline check, **then** the reviewer emits `MG004` with the rejection reason ("strip-the-name test fails: this prose describes [generic concern] not [this spec's specifics]"). +- **Given** a spec where `## Modification Grammar` content could equally describe a different spec (boilerplate failure), **when** `intent-auditor` Role 1 runs the discipline check, **then** the reviewer emits `MG004` with the rejection reason ("strip-the-name test fails: this prose describes [generic concern] not [this spec's specifics]"). - **Given** a spec with `### Ripple > Vocabulary delta` introducing a new term not registered in either registry named by `02-constraints/04-naming.md`, **when** plan-review runs, **then** the intent lint is designed to emit `VR001` and block promotion until the term is registered (reserved; not yet implemented — today this is a review-time check, not a lint gate). - **Given** a spec transitions to shipped, **when** `principle-distiller` runs the extraction pass, **then** an append-only memory page `spec_modification_grammar_.md` is written to `.abcd/memory/` with `source.class: spec_modification_grammar` AND a curator-merged page `modification_grammar_.md` is updated with `source.class: modification_grammar`. - **Given** the cost-discipline boundary (itd-37 is first expensive discipline; ~15-30 min capture per spec), **when** any spec proposes adding "modification grammar exemption for trivial specs", **then** the proposal is rejected — trivial-self-exemption invites loophole-driven bypass; trivial specs produce short Modification Grammar sections, not absent ones. @@ -101,7 +101,7 @@ The semantic enforcement is genuine LLM-judgement work. The discipline owns the ## Audit Notes -_Empty. Populated by `intent-fidelity-reviewer` Role 1 (single-document fidelity per itd-1) when this discipline is first audited. Like itd-1, this discipline is audited continuously via the rule-applies-to-every-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any spec that violated the discipline (e.g., shipped without `## Modification Grammar`, or with boilerplate `MG004` not caught at plan-review)._ +_Empty. Populated by `intent-auditor` Role 1 (single-document fidelity per itd-1) when this discipline is first audited. Like itd-1, this discipline is audited continuously via the rule-applies-to-every-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any spec that violated the discipline (e.g., shipped without `## Modification Grammar`, or with boilerplate `MG004` not caught at plan-review)._ ## References diff --git a/.abcd/development/intents/disciplines/itd-81-judge-calibration.md b/.abcd/development/intents/disciplines/itd-81-judge-calibration.md index 35aede7c..1fce0948 100644 --- a/.abcd/development/intents/disciplines/itd-81-judge-calibration.md +++ b/.abcd/development/intents/disciplines/itd-81-judge-calibration.md @@ -15,7 +15,7 @@ severity: major ## Rule Every agent abcd ships that renders a **verdict** on code or artefacts — today -`intent-fidelity-reviewer`, and every reviewer, auditor, linter-with-judgement, +`intent-auditor`, and every reviewer, auditor, linter-with-judgement, and oracle that follows — is calibrated against a labelled corpus before its prompt is locked, and re-scored whenever the prompt changes. @@ -156,7 +156,7 @@ length tiebreak is struck; the pre-flight gate is the corpus score.** ## Open Questions - **Where the TNR floor starts.** A floor set before we have a baseline is a - guess. Proposal: run the current `intent-fidelity-reviewer` against a first + guess. Proposal: run the current `intent-auditor` against a first corpus, take the measured TNR as the floor, and ratchet — never regress. Resolve at the first judge spec's T1. - **Corpus size before the number means anything.** 30–50 cases is the working @@ -190,7 +190,7 @@ length tiebreak is struck; the pre-flight gate is the corpus score.** ## Audit Notes -_Empty. Populated by `intent-fidelity-reviewer`'s single-document role when this +_Empty. Populated by `intent-auditor`'s single-document role when this discipline is first audited. Like itd-1 and itd-5, audited continuously via rule-applies-to-every-judge-agent-spec semantics rather than a planned→shipped transition._ diff --git a/.abcd/development/intents/disciplines/itd-84-intent-decomposition.md b/.abcd/development/intents/disciplines/itd-84-intent-decomposition.md index aec2bc5f..5afdacae 100644 --- a/.abcd/development/intents/disciplines/itd-84-intent-decomposition.md +++ b/.abcd/development/intents/disciplines/itd-84-intent-decomposition.md @@ -182,7 +182,7 @@ optional layer, never the headline. ## Audit Notes -_Empty. Populated by `intent-fidelity-reviewer`'s single-document role when this +_Empty. Populated by `intent-auditor`'s single-document role when this discipline is first audited. Like itd-1, itd-5, and itd-81, audited continuously via rule-applies-to-every-capture semantics rather than a planned→shipped transition._ diff --git a/.abcd/development/specs/open/spc-28-the-intent-audit-says-audit-abcd-intent-review-becomes-abcd.md b/.abcd/development/specs/open/spc-28-the-intent-audit-says-audit-abcd-intent-review-becomes-abcd.md index 19b59d55..698ec172 100644 --- a/.abcd/development/specs/open/spc-28-the-intent-audit-says-audit-abcd-intent-review-becomes-abcd.md +++ b/.abcd/development/specs/open/spc-28-the-intent-audit-says-audit-abcd-intent-review-becomes-abcd.md @@ -47,7 +47,13 @@ Record (current-state surfaces only): research notes, dated plans, resolved/wontfix issues, shipped/superseded intents, `DECISIONS.md`, review-store artefacts, git history. A test or grep in the PR description demonstrates remaining occurrences are all in these -classes. +classes. Amendment (2026-08-26): `intents/disciplines/` appeared in neither +the swept inventory nor this list and was swept late (iss-2608261437043634) — +it is live state with no reachable reconciliation event, since `intent audit` +refuses a non-shipped intent; `drafts/` and `planned/` stay deliberately +unswept under the iss-94 convention (each reconciles when next planned). +Stored artefact formats (`abcd-review:` markers, `review-` receipt +directories) keep their frozen spellings. ## Approach From c961ce607262f4e90889bca6133fa9c17ae79811 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:41:22 +0000 Subject: [PATCH 09/20] docs: itd-5 points task_classes at the naming registry, with intent_audit The discipline named a machine-readable task_classes enum in internal/core/lint that has never existed and carried the retired intent_review token; the naming registry declared itself the source of truth today (iss-265) and the token flipped to intent_audit with spc-28. The three remaining intent-auditor name renames in this file ride the disciplines sweep landing beside this change. Resolves: iss-2608261437043962 Assisted-by: Claude:claude-fable-5 --- .../intents/disciplines/itd-5-prompt-quality-additions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md b/.abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md index 754bcc66..d313a891 100644 --- a/.abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md +++ b/.abcd/development/intents/disciplines/itd-5-prompt-quality-additions.md @@ -77,7 +77,7 @@ The discipline is project-agnostic: any project shipping LLM-driven agents under designed_for: "" ``` -- `task_classes` is a closed-enum list of tokens drawn from the controlled vocabulary in [`02-constraints/04-naming.md`](../../brief/02-constraints/04-naming.md) (`Reserved vocabulary § task_classes`). The machine-readable source of truth is the `task_classes` enum in `internal/core/lint`. Initial set (~10 tokens, PR-to-extend): `oracle_review`, `intent_review`, `spec_planning`, `code_rescue`, `principle_distillation`, `lifeboat_packing`, `audit`, `lint`, `surface_render`, `cross_document_audit`. +- `task_classes` is a closed-enum list of tokens drawn from the controlled vocabulary in [`02-constraints/04-naming.md`](../../brief/02-constraints/04-naming.md) (`Reserved vocabulary § task_classes`), which is the source of truth today — the binary carries no `task_classes` schema and no check reads the field (iss-265); a machine-readable enum in the binary is a design target, not a shipped artefact. Initial set (~10 tokens, PR-to-extend): `oracle_review`, `intent_audit`, `spec_planning`, `code_rescue`, `principle_distillation`, `lifeboat_packing`, `audit`, `lint`, `surface_render`, `cross_document_audit`. - `designed_for` is a free-text 1-line description of the agent's intended task class (for human readers — does not participate in lint, and is NEVER read to infer scope). - **Validation in `lint_prompts`** — strictly set-membership, NEVER inference: - (i) `capability_scope` field is present and parses; `task_classes` is a non-empty inline list; `designed_for` is a string. @@ -99,7 +99,7 @@ Every native spec that ships an agent inherits all four rules above as acceptanc - If the agent reads untrusted input (per the in-scope list), the spec's task list includes a canary-fixture task. - The agent's `agents/.md` will carry `capability_scope` with valid `task_classes` tokens at close. -`intent-fidelity-reviewer`'s single-document role (per the [itd-1 discipline](itd-1-acceptance-gates.md)) checks delivered reality against this discipline's acceptance criteria when each agent spec ships. +`intent-auditor`'s single-document role (per the [itd-1 discipline](itd-1-acceptance-gates.md)) checks delivered reality against this discipline's acceptance criteria when each agent spec ships. ## What's Out of Scope @@ -112,7 +112,7 @@ Every native spec that ships an agent inherits all four rules above as acceptanc ## Acceptance Criteria -> _BDD format, per the [itd-1 discipline](itd-1-acceptance-gates.md). The criteria below describe how this discipline is checked — by `lint_prompts` continuously, and by `intent-fidelity-reviewer`'s single-document role on every agent spec._ +> _BDD format, per the [itd-1 discipline](itd-1-acceptance-gates.md). The criteria below describe how this discipline is checked — by `lint_prompts` continuously, and by `intent-auditor`'s single-document role on every agent spec._ - **Given** a fresh checkout, **when** the prompt linter runs, **then** it passes only if every `agents/*.md` has `prompt_version: ` and a corresponding `CHANGELOG.md` entry exists for `1.0.0`. - **Given** any agent at v1.0.0 lock-time, **when** the agent's spec closes, **then** `agents/CHANGELOG.md` contains an entry with the self-improvement pre-flight outcome (oracle variant accepted | candidate retained + reason). @@ -138,7 +138,7 @@ Every native spec that ships an agent inherits all four rules above as acceptanc ## Audit Notes -_Empty. Populated by intent-fidelity-reviewer's single-document role when this discipline is first audited. Note: like itd-1, this discipline is audited continuously via the rule-applies-to-every-agent-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any agent spec that violated this discipline (e.g., shipped without `prompt_version`, or without canary fixtures despite reading untrusted input)._ +_Empty. Populated by intent-auditor's single-document role when this discipline is first audited. Note: like itd-1, this discipline is audited continuously via the rule-applies-to-every-agent-spec semantics rather than via a planned→shipped transition. The reviewer's findings here record any agent spec that violated this discipline (e.g., shipped without `prompt_version`, or without canary fixtures despite reading untrusted input)._ ## References From cefa27703abbc8010e17a4f3502376cad9ad3c9b Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:42:23 +0000 Subject: [PATCH 10/20] docs: itd-43 reconciles onto the delivered glossary rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The planned intent still specified work against the retired terminology/ tree: it asserted an epic.md term file that does not exist, pointed its canonical-term scope bullet and acceptance criterion at a path that cannot come into existence, kept two open questions its own spec records as answered, and named two schema files the Go rebuild never carried. Each stale site now states the delivered truth spc-8 already verified — the term file lives at brief/glossary/core/spec.md with epic forbidden — while the live remainder (the reviews-subsystem rename, correctly blocked on itd-28) stands untouched. Resolves: iss-2608261437044340 Assisted-by: Claude:claude-fable-5 --- .../itd-43-epic-to-spec-terminology.md | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.abcd/development/intents/planned/itd-43-epic-to-spec-terminology.md b/.abcd/development/intents/planned/itd-43-epic-to-spec-terminology.md index f517bb46..c8cefead 100644 --- a/.abcd/development/intents/planned/itd-43-epic-to-spec-terminology.md +++ b/.abcd/development/intents/planned/itd-43-epic-to-spec-terminology.md @@ -14,9 +14,9 @@ severity: minor ## Press Release -> **abcd speaks one word for a specced block of work — `spec` — everywhere a product thinker or contributor reads: no `epic` left behind in a heading, a review type, a schema field, or the glossary.** The native spec store is `spec` throughout, and abcd's `spec_id` intent-frontmatter field already carries the concept. Where abcd's *surfaces* still say `"epic"` — the reviews subsystem (`epic-review` type, `## Epic:` headings, `epic_id` review-directory identifiers), and prose across the brief, docs, command help, and the canonical `terminology/core/epic.md` term file — this intent makes the vocabulary single. Every one of those surfaces is abcd-owned; there is no vendored external epic/spec boundary to preserve, so the rename runs clean through the reviews subsystem, the schemas, and the glossary. +> **abcd speaks one word for a specced block of work — `spec` — everywhere a product thinker or contributor reads: no `epic` left behind in a heading, a review type, a schema field, or the glossary.** The native spec store is `spec` throughout, and abcd's `spec_id` intent-frontmatter field already carries the concept. Where abcd's *surfaces* still say `"epic"` — the reviews subsystem (`epic-review` type, `## Epic:` headings, `epic_id` review-directory identifiers), and prose across the brief, docs, and command help — this intent makes the vocabulary single. Every one of those surfaces is abcd-owned; there is no vendored external epic/spec boundary to preserve, so the rename runs clean through the reviews subsystem, the schemas, and the glossary. > -> "I'd renamed the field and thought I was done — then a contributor opened `terminology/core/epic.md` and asked which word was real," said Kira, framework author. "abcd's whole pitch is that each concept has one canonical term. Having `spec` in the schema and `epic` in the glossary was exactly the drift the glossary exists to prevent. One sweep, one word, and the term file is the source of truth again." +> "I'd renamed the field and thought I was done — then a contributor opened the glossary's `epic` term file and asked which word was real," said Kira, framework author. "abcd's whole pitch is that each concept has one canonical term. Having `spec` in the schema and `epic` in the glossary was exactly the drift the glossary exists to prevent. One sweep, one word, and the term file is the source of truth again." ## Status @@ -28,20 +28,20 @@ Scope* below is what this intent carries. ## Why This Matters -abcd's [terminology discipline](../../brief/glossary/) exists to kill exactly one failure: the same concept named two ways, drifting until two readers mean different things. Right now abcd commits that failure about its own core noun. The schema and all 41 intent files say `spec_id`; `terminology/core/epic.md` still defines the concept as `"epic"`; the reviews subsystem still classifies `epic-review`. A framework that enforces ubiquitous language cannot itself be bilingual about its central term. +abcd's [terminology discipline](../../brief/glossary/) exists to kill exactly one failure: the same concept named two ways, drifting until two readers mean different things. Right now abcd commits that failure about its own core noun. The schema and the intent corpus say `spec_id`; the glossary half is delivered — `brief/glossary/core/spec.md` carries `term: spec` with `epic` in `forbidden_synonyms` (spc-8) — but the reviews subsystem still classifies `epic-review`. A framework that enforces ubiquitous language cannot itself be bilingual about its central term. The `epic_id`→`spec_id` field rename was done separately and first, on purpose — it had to be atomic (schema + data + code, or intent-lint validation fails). What remains does **not** break anything: it is inconsistency, not breakage, which is why it is its own intent rather than an emergency fix. But unaddressed it erodes the glossary's authority and confuses every new contributor. -The reviews subsystem, the schemas, and the spec store are all abcd-owned — there is no vendored external plugin whose `epic`/`spec` aliases must be preserved, so the rename is a coherent internal sweep rather than a negotiation across a boundary. The single-source-of-truth rule decides the order: `terminology/core/spec.md` is canonical for the concept, so it is renamed first and everything else conforms to it. +The reviews subsystem, the schemas, and the spec store are all abcd-owned — there is no vendored external plugin whose `epic`/`spec` aliases must be preserved, so the rename is a coherent internal sweep rather than a negotiation across a boundary. The single-source-of-truth rule decides the order: `brief/glossary/core/spec.md` is canonical for the concept, so it was renamed first and everything else conforms to it. ## What's In Scope -- **Rename the canonical term file** `terminology/core/epic.md` → `terminology/core/spec.md`, with `term: spec` and `epic` recorded as a `forbidden_synonyms` entry so the lint catches regressions. +- **Rename the canonical term file** — delivered with spc-8: the term file lives at `brief/glossary/core/spec.md` with `term: spec` and `epic` in `forbidden_synonyms`, and the `GL002` lint catches regressions. - **Reviews subsystem rename** — the review-index, review-postprocess, and review-verify surfaces: `epic_id` parameters → `spec_id`, the `--epic` CLI flag, the `## Epic:` rendered heading, the `epic_id` JSON field, and the `epic-review`/`epic` review-type tokens. All of it is abcd-owned, so the review-type token becomes `spec-review` throughout with no external token to accommodate. -- **`issue.schema.json`** — `related_epics` → `related_specs`, nested `epic` key → `spec`, descriptions updated. -- **`grill-report.schema.json`** — prose mentions of `epic`/task ID updated. +- **`issue.schema.json`** — moot in the Go rebuild (spc-8): no `*.schema.json` exists in the tree, and the native validator already uses `related_specs` exclusively. +- **`grill-report.schema.json`** — moot in the Go rebuild (spc-8): the file does not exist. - **Prose sweep** — `intents/README.md`, the brief (`04-surfaces/`, `02-constraints/`, etc.), `docs/reference/{commands,facilitator,review-schema}.md`, `commands/intent.md`, the grill `SKILL.md` boundary message, project READMEs: `epic` as a noun → `spec`. -- **The native spec store's README** — the directory-purpose prose still says `epics`; align to `specs`. +- **The native spec store's README** — moot in the Go rebuild (spc-8): `.abcd/development/specs/` carries no README. ## What's Out of Scope @@ -53,16 +53,15 @@ The reviews subsystem, the schemas, and the spec store are all abcd-owned — th > _BDD format, per the itd-1 discipline._ - **Given** the rename is complete, **when** a contributor greps abcd-owned files for `"epic"` as a standalone noun, **then** no live reference remains — only historical git-tracked records. -- **Given** the terminology directory, **when** a contributor looks up the concept, **then** it resolves to `terminology/core/spec.md` with `term: spec`, and `epic` appears there only as a `forbidden_synonyms` entry. +- **Given** the glossary, **when** a contributor looks up the concept, **then** it resolves to `brief/glossary/core/spec.md` with `term: spec`, and `epic` appears there only as a `forbidden_synonyms` entry. - **Given** the reviews subsystem is renamed, **when** a review is classified, **then** classification succeeds against the `spec-review` type the native reviews surface emits — one internal token, no desync. -- **Given** `issue.schema.json` is updated, **when** an issue links to a spec, **then** it uses `related_specs`, and an issue file using the old `related_epics` key fails schema validation. +- **Given** the issue ledger, **when** an issue links to a spec, **then** it uses `related_specs` — met by the Go rebuild's validator (spc-8); no `*.schema.json` remains to update. - **Given** the prose sweep is complete, **when** `internal/core/lint` runs, **then** no forbidden-synonym (`GL002`) violation for `epic` is raised by any abcd-owned intent or doc. ## Open Questions - Sequencing against the `intents/README.md` v1/v2/v3 → phase migration (logged separately in a working-log entry): both rewrite `intents/README.md`. Run the README migration first and this sweep second, or merge them into one README pass? -- Should `terminology/core/epic.md` be renamed (git mv → `spec.md`) or kept as a stub redirecting to `spec.md`? A stub preserves inbound links but adds a file the glossary must explain. -- Does `epic.md`'s definition body need rewriting, or only its `term` field and filename? The concept is unchanged; only the word changes. +- ~~Should the term file be renamed or kept as a stub?~~ Answered by the tree (spc-8): the rename happened with no stub, and the definition body carries `term: spec`. ## Audit Notes From c3a46c5942a49161d0dfc170589f0531d96b703f Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:43:24 +0000 Subject: [PATCH 11/20] fix: intent verbs resolve spec_id by number, the way lint already matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A slug-suffixed (spc-9-widget) or zero-padded (spc-09) spec_id is lint-green — record-lint's spec rules match on the spec number, and the spec store's reservation check tolerates both spellings by recorded design — yet Reconcile compared the raw string, spec.Store.Lookup compared exactly, and the audit path re-applied the strict argument grammar to the stored field, so a record every gate passes bricked intent ready, spec close, intent link and the audit emit. Lookup now resolves by spec number after an exact-match pass, Reconcile compares via the shared SameNum helper, and the stored spec_id is validated with HasNum; the strict grammar still guards CLI arguments. An unparseable value matches nothing, so two malformed ids never compare equal. Resolves: iss-2608261437047643 Assisted-by: Claude:claude-fable-5 --- internal/core/intent/audit.go | 10 ++++-- internal/core/intent/audit_test.go | 15 +++++++++ internal/core/intent/intent_test.go | 47 ++++++++++++++++++++++++++++ internal/core/intent/lifecycle.go | 5 ++- internal/core/intent/ready_test.go | 21 +++++++++++++ internal/core/spec/spec.go | 48 ++++++++++++++++++++++++++--- internal/core/spec/spec_test.go | 27 ++++++++++++++++ 7 files changed, 165 insertions(+), 8 deletions(-) diff --git a/internal/core/intent/audit.go b/internal/core/intent/audit.go index dd077ddd..a1de3543 100644 --- a/internal/core/intent/audit.go +++ b/internal/core/intent/audit.go @@ -11,6 +11,7 @@ import ( "strings" "syscall" + "github.com/intentdriven/abcd/internal/core/spec" "github.com/intentdriven/abcd/internal/fsutil" ) @@ -179,8 +180,11 @@ func emitAuditForIntent(repoRoot string, it Intent) (AuditEmitResult, error) { if !intentIDRe.MatchString(it.ID) { return AuditEmitResult{}, fmt.Errorf("intent: id %q must match ^itd-[0-9]+$", it.ID) } - if !specIDRe.MatchString(it.SpecID) { - return AuditEmitResult{}, fmt.Errorf("intent: spec id %q must match ^spc-[0-9]+$", it.SpecID) + // The stored spec_id is checked the tolerant way (a number must be readable + // from it), not against the strict argument grammar: record-lint accepts a + // slug-suffixed or zero-padded spec_id, and no path is built from this value. + if !spec.HasNum(it.SpecID) { + return AuditEmitResult{}, fmt.Errorf("intent: spec id %q must carry a spec number (spc-N)", it.SpecID) } abs := filepath.Join(repoRoot, it.Path) data, err := readRepoFile(abs, it.Path) @@ -253,7 +257,7 @@ func ReEmitAudit(repoRoot, intentID string) (AuditEmitResult, error) { if it.Bucket != BucketShipped { return AuditEmitResult{}, fmt.Errorf("intent: %s is in %s, not shipped; only a shipped intent owes a fidelity audit", intentID, it.Bucket) } - if !specIDRe.MatchString(it.SpecID) { + if !spec.HasNum(it.SpecID) { return AuditEmitResult{}, fmt.Errorf("intent: %s has no well-formed spec_id (%q); refusing to emit a review", intentID, it.SpecID) } return emitAuditForIntent(repoRoot, it) diff --git a/internal/core/intent/audit_test.go b/internal/core/intent/audit_test.go index 5dbc9ed0..c3a57398 100644 --- a/internal/core/intent/audit_test.go +++ b/internal/core/intent/audit_test.go @@ -504,3 +504,18 @@ func TestIngestVerdictRefusesSymlink(t *testing.T) { t.Errorf("unexpected error for symlinked verdict: %v", err) } } + +// TestReEmitAuditToleratesSpecIDSpelling proves the manual re-emit verb accepts +// the same lint-green spec_id spellings the ship move does. +func TestReEmitAuditToleratesSpecIDSpelling(t *testing.T) { + root := t.TempDir() + writeFile(t, root, shippedDir+"/itd-10-alpha.md", plannedLinked("itd-10", "alpha", "spc-1-alpha")) + + res, err := ReEmitAudit(root, "itd-10") + if err != nil { + t.Fatalf("ReEmitAudit must accept a slug-suffixed spec_id: %v", err) + } + if res.ReceiptID == "" || res.Status != "owed" { + t.Fatalf("ReEmitAudit result = %+v, want an owed receipt", res) + } +} diff --git a/internal/core/intent/intent_test.go b/internal/core/intent/intent_test.go index 146462fc..41dbca97 100644 --- a/internal/core/intent/intent_test.go +++ b/internal/core/intent/intent_test.go @@ -613,3 +613,50 @@ func TestSetFrontmatterFieldsToleratesDelimiterTrailingSpace(t *testing.T) { t.Fatalf("inserted keys must precede the closing delimiter, not enter the body\n---\n%s", out) } } + +// TestReconcileToleratesSpecIDSpelling proves the lifecycle verb yields to the +// record lint: record-lint compares a spec_id by NUMBER, so a slug suffix or a +// zero-padded id is lint-green — and a lint-green record must never be refused +// by the verb that ships it. +func TestReconcileToleratesSpecIDSpelling(t *testing.T) { + for _, specID := range []string{"spc-1-alpha", "spc-01"} { + t.Run(specID, func(t *testing.T) { + root := t.TempDir() + writeFile(t, root, plannedDir+"/itd-10-alpha.md", plannedLinked("itd-10", "alpha", specID)) + writeFile(t, root, specsOpen+"/spc-1-alpha.md", specNaming("spc-1", "alpha", "itd-10")) + + res, err := Reconcile(root, "spc-1") + if err != nil { + t.Fatalf("Reconcile must accept the lint-green spec_id %q: %v", specID, err) + } + if !res.IntentMoved || res.To != BucketShipped { + t.Fatalf("Reconcile result = %+v", res) + } + // The audit emit reads the same stored spec_id, so it must tolerate it too. + if res.AuditEmitError != "" { + t.Fatalf("audit emit must accept spec_id %q: %s", specID, res.AuditEmitError) + } + }) + } +} + +// TestLinkResolvesSpecByNumber proves the strict ^spc-[0-9]+$ ARGUMENT grammar +// still stands while the store lookup behind it compares by number, so a +// zero-padded spec record is linkable by its canonical id. +func TestLinkResolvesSpecByNumber(t *testing.T) { + root := t.TempDir() + writeFile(t, root, plannedDir+"/itd-10-alpha.md", + "---\nid: itd-10\nslug: alpha\nspec_id: null\nkind: standalone\n---\n# alpha\n") + writeFile(t, root, specsOpen+"/spc-01-alpha.md", specNaming("spc-01", "alpha", "itd-10")) + + res, err := Link(root, "itd-10", "spc-1") + if err != nil { + t.Fatalf("Link must resolve a zero-padded spec record: %v", err) + } + if res.Spec.ID != "spc-01" { + t.Fatalf("Link spec = %+v, want the spc-01 record", res.Spec) + } + if _, err := Link(root, "itd-10", "spc-1-alpha"); err == nil { + t.Fatal("Link must keep the strict ^spc-[0-9]+$ grammar for its argument") + } +} diff --git a/internal/core/intent/lifecycle.go b/internal/core/intent/lifecycle.go index 1a6d3bee..659a438b 100644 --- a/internal/core/intent/lifecycle.go +++ b/internal/core/intent/lifecycle.go @@ -303,7 +303,10 @@ func Reconcile(repoRoot, specID string) (ReconcileResult, error) { // Bidirectional agreement: the intent must point back at THIS spec. A null or // mismatched spec_id is drift (a one-sided link) — fail closed rather than ship // an intent that names a different, or no, spec. - if it.SpecID != specID { + // The comparison is canonical (spec.SameNum), not literal: record-lint matches + // a spec_id on its NUMBER, so a slug-suffixed or zero-padded value is + // lint-green and this verb must not refuse what the lint accepts. + if !spec.SameNum(it.SpecID, specID) { return ReconcileResult{}, fmt.Errorf("intent: %s spec_id is %q but spec %s claims it (bidirectional link disagrees); refusing to reconcile", intentID, it.SpecID, specID) } // Bucket guard runs BEFORE any move, so an unexpected bucket (drafts, diff --git a/internal/core/intent/ready_test.go b/internal/core/intent/ready_test.go index 43bbd19d..3f634ff5 100644 --- a/internal/core/intent/ready_test.go +++ b/internal/core/intent/ready_test.go @@ -264,3 +264,24 @@ func TestReadyFaults(t *testing.T) { t.Fatal("a symlinked intent record must be a fault") } } + +// TestReadySpecLinkToleratesSpecIDSpelling mirrors Reconcile's tolerance in the +// report: the spec_link check reads the same stored spec_id, so a lint-green +// slug-suffixed or zero-padded value must report the link as held, not missing. +func TestReadySpecLinkToleratesSpecIDSpelling(t *testing.T) { + for _, specID := range []string{"spc-1-alpha", "spc-01"} { + t.Run(specID, func(t *testing.T) { + root := t.TempDir() + writeFile(t, root, plannedDir+"/itd-10-alpha.md", plannedLinked("itd-10", "alpha", specID)) + writeFile(t, root, specsOpen+"/spc-1-alpha.md", specNaming("spc-1", "alpha", "itd-10")) + + res, err := Ready(root, "itd-10") + if err != nil { + t.Fatal(err) + } + if link := checkByName(t, res, "spec_link"); !link.OK { + t.Fatalf("spec_link = %+v, want OK for the lint-green spec_id %q", link, specID) + } + }) + } +} diff --git a/internal/core/spec/spec.go b/internal/core/spec/spec.go index 12a51c45..6c569941 100644 --- a/internal/core/spec/spec.go +++ b/internal/core/spec/spec.go @@ -78,13 +78,27 @@ type Store struct { Specs []Spec `json:"specs"` } -// Lookup returns the spec with the given id; ok is false when absent. +// Lookup returns the spec the given reference names; ok is false when absent. +// +// Matching is on the spec NUMBER, not the literal string, because that is the +// comparison record-lint makes: a spec_id is written bare (spc-9), with its slug +// (spc-9-widget), and zero-padded (spc-009) across the record, and all three are +// lint-green. A literal-only compare would let the lifecycle verbs refuse a +// record the lint accepts. An exact string match still wins when the store holds +// one, so a caller that names a record precisely gets that record; two specs +// sharing a number is a record defect the lint's spec_id_unique rule flags (the +// mint never produces one, since NextID allocates max+1 over the same numbers). func (s Store) Lookup(specID string) (Spec, bool) { for _, sp := range s.Specs { if sp.ID == specID { return sp, true } } + for _, sp := range s.Specs { + if SameNum(sp.ID, specID) { + return sp, true + } + } return Spec{}, false } @@ -114,9 +128,16 @@ func Validate(s Spec) error { // specNum extracts the numeric N from a spec id or spec_id value, or 0 if none. func specNum(id string) int { + n, _ := parseSpecNum(id) + return n +} + +// parseSpecNum extracts the numeric N from a spec id or spec_id value; ok is +// false when the value carries no usable number. +func parseSpecNum(id string) (int, bool) { m := specNumRe.FindStringSubmatch(id) if m == nil { - return 0 + return 0, false } n, err := strconv.Atoi(m[1]) if err != nil { @@ -124,9 +145,28 @@ func specNum(id string) int { // reservation: Atoi returns the clamped MaxInt64 alongside the error, and // keeping it would make NextID compute max+1 and wrap to a NEGATIVE id // (spc--9223…). Treat it as no number so the id space stays sane. - return 0 + return 0, false } - return n + return n, true +} + +// SameNum reports whether two spec references name the same spec number — the +// canonical comparison the record lint makes, so a verb using it can never +// refuse a spelling the lint accepts (spc-9, spc-9-widget, spc-009 are one +// spec). A value carrying no usable number (null, "spc-", "spc-abc", an +// over-int64 N) matches nothing, including another such value. +func SameNum(a, b string) bool { + an, aok := parseSpecNum(a) + bn, bok := parseSpecNum(b) + return aok && bok && an == bn +} + +// HasNum reports whether v is a spec reference carrying a usable number. It is +// the tolerant counterpart of the ^spc-[0-9]+$ argument grammar, for validating +// a STORED spec_id, which the record lint lets carry a slug or zero padding. +func HasNum(v string) bool { + _, ok := parseSpecNum(v) + return ok } // stubMarker is the opening of the author-guidance placeholder renderSpec mints. diff --git a/internal/core/spec/spec_test.go b/internal/core/spec/spec_test.go index 932fe0b7..d2e64b6e 100644 --- a/internal/core/spec/spec_test.go +++ b/internal/core/spec/spec_test.go @@ -88,3 +88,30 @@ func TestStoreLookupAndByIntent(t *testing.T) { t.Fatal("ByIntent(itd-77) unexpectedly found") } } + +// TestLookupResolvesBySpecNumber proves the store yields to the record lint: a +// spec_id is written bare, zero-padded, and with its slug across the corpus, and +// record-lint compares the NUMBER, so a literal-only Lookup would refuse a +// lint-green record. An exact string match still wins when the store holds one. +func TestLookupResolvesBySpecNumber(t *testing.T) { + store := Store{Specs: []Spec{ + {ID: "spc-9", Intent: "itd-1"}, + {ID: "spc-10", Intent: "itd-2"}, + }} + for _, ref := range []string{"spc-9", "spc-9-widget", "spc-009"} { + sp, ok := store.Lookup(ref) + if !ok || sp.ID != "spc-9" { + t.Errorf("Lookup(%q) = %+v, %v; want spc-9, true", ref, sp, ok) + } + } + for _, ref := range []string{"spc-11", "spc-", "null", ""} { + if sp, ok := store.Lookup(ref); ok { + t.Errorf("Lookup(%q) = %+v, true; want no match", ref, sp) + } + } + // A store carrying both spellings resolves to the record the caller named. + both := Store{Specs: []Spec{{ID: "spc-009", Intent: "itd-1"}, {ID: "spc-9", Intent: "itd-2"}}} + if sp, ok := both.Lookup("spc-9"); !ok || sp.ID != "spc-9" { + t.Errorf("Lookup(spc-9) = %+v, %v; want the exact-match record", sp, ok) + } +} From 5d964a9e2ab7793b446fe11ab7551e81d67c66fe Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:43:24 +0000 Subject: [PATCH 12/20] fix: ScanSpecLinks propagates intent-tree read errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intent half of the shared traversal swallowed every walk and per-file read error while the spec half ten lines later fails closed, and the fail-open half feeds the release cut's stale-intent refusal — an unreadable intent file contributed zero links, so the guard against a silent under-bump was disarmed exactly when the tree was damaged. The walk error is now captured and propagated, keeping the one documented tolerance: a missing intents root is an unpopulated repository, not a fault. Resolves: iss-2608261437049307 Assisted-by: Claude:claude-fable-5 --- internal/core/lint/speclinks.go | 23 +++++++++++++++++++---- internal/core/lint/speclinks_test.go | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/internal/core/lint/speclinks.go b/internal/core/lint/speclinks.go index 93571be2..50729da2 100644 --- a/internal/core/lint/speclinks.go +++ b/internal/core/lint/speclinks.go @@ -94,6 +94,9 @@ func (x SpecLinkIndex) SpecBucket(specID string) (string, bool) { // ScanSpecLinks reads the intent buckets and the spec store once, both relative // to repoRoot. A missing tree contributes nothing and is not an error, mirroring // the rest of the record lint: an unpopulated repository is a state, not a fault. +// A tree that is present but cannot be read IS a fault and is returned: both +// halves fail closed, so no consumer ever reads an index that says "nothing +// here" about records it merely failed to open. // // top supplies the content exemptions. They are recorded per spec rather than // applied here, because they exempt a file from the lint's CONTENT checks — they @@ -103,13 +106,23 @@ func ScanSpecLinks(repoRoot, intentsDir, specsDir string, top Config) (SpecLinkI var idx SpecLinkIndex intentsRoot := filepath.Join(repoRoot, filepath.FromSlash(intentsDir)) - _ = filepath.WalkDir(intentsRoot, func(path string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() || !intentFileRe.MatchString(d.Name()) { + if err := filepath.WalkDir(intentsRoot, func(path string, d os.DirEntry, err error) error { + if err != nil { + // A missing tree is the one soft case (and the guard that keeps the + // nil DirEntry WalkDir hands the failed root out of the checks below); + // every other walk error is a tree that could not be read, which is a + // fault, not an absence — fail closed exactly as the spec half does. + if path == intentsRoot && os.IsNotExist(err) { + return nil + } + return err + } + if d.IsDir() || !intentFileRe.MatchString(d.Name()) { return nil } content, rerr := os.ReadFile(path) if rerr != nil { - return nil + return rerr } fields := frontmatterFields(strings.Split(string(content), "\n")) id := fields["id"].value @@ -134,7 +147,9 @@ func ScanSpecLinks(repoRoot, intentsDir, specsDir string, top Config) (SpecLinkI SpecID: fields["spec_id"].value, }) return nil - }) + }); err != nil { + return SpecLinkIndex{}, err + } specsRoot := filepath.Join(repoRoot, filepath.FromSlash(specsDir)) for _, bucket := range specBucketNames { diff --git a/internal/core/lint/speclinks_test.go b/internal/core/lint/speclinks_test.go index cc02867c..09b89951 100644 --- a/internal/core/lint/speclinks_test.go +++ b/internal/core/lint/speclinks_test.go @@ -87,3 +87,18 @@ func TestScanSpecLinksMissingTreesAreSoft(t *testing.T) { t.Errorf("scanned %+v, want an empty index", idx) } } + +// TestScanSpecLinksUnreadableIntentIsHard proves the intent half fails closed +// exactly as the spec half does: a record that lists but cannot be read is a +// fault, not an absence. Swallowing it would hand the release cut's stale-intent +// refusal an index that says "no intents" about a tree it could not read. +func TestScanSpecLinksUnreadableIntentIsHard(t *testing.T) { + root := specLinkRepo(t) + planned := filepath.Join(root, "record", "intents", "planned") + if err := os.Symlink(filepath.Join(planned, "gone.md"), filepath.Join(planned, "itd-96-dangling.md")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + if _, err := ScanSpecLinks(root, "record/intents", "record/specs", Config{}); err == nil { + t.Fatal("ScanSpecLinks must propagate an unreadable intent record") + } +} From 2d00855e4a3f3e4fc4ae3eed95c3a3201414d8d1 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:45:09 +0000 Subject: [PATCH 13/20] fix: lesson prose routes through the canonical CleanProse primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cleanLessonProse kept the pre-migration body its sibling cleanSynthProse was moved off: it neutralised only the HTML-comment pair, not the full opener class, and did so before Sanitize — the exact ordering the canonical primitive documents as unsafe, since a masked control byte after < forges a processing instruction. The consolidation's own doc claimed lifeboat routed through CleanProse; this was the boundary the sweep missed. The body now delegates with the unchanged 4096-byte cap. Resolves: iss-2608261437040578 Assisted-by: Claude:claude-fable-5 --- internal/core/lifeboat/graveyard_lessons.go | 23 +++++-------- .../core/lifeboat/graveyard_lessons_test.go | 33 +++++++++++++++++++ .../core/lifeboat/synthesis_principles.go | 3 +- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/internal/core/lifeboat/graveyard_lessons.go b/internal/core/lifeboat/graveyard_lessons.go index b9b70e99..72b10e24 100644 --- a/internal/core/lifeboat/graveyard_lessons.go +++ b/internal/core/lifeboat/graveyard_lessons.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/intentdriven/abcd/internal/fsutil" + "github.com/intentdriven/abcd/internal/termsafe" ) // IngestLessons validates host-produced lesson JSON against a PACKED lifeboat and @@ -310,20 +311,12 @@ func filterEvidence(refs []string, ids map[string]bool) []string { return out } -// cleanLessonProse sanitises untrusted lesson prose: it collapses newlines and -// neutralises HTML-comment delimiters (oneLine-style, so the prose can neither -// break its line nor forge an abcd marker), strips control characters, then caps -// the length. An empty result signals the entry should be dropped. +// cleanLessonProse sanitises untrusted lesson prose: it collapses newlines, +// neutralises HTML openers (so the prose can neither break its line nor forge an +// abcd marker), strips control characters, then caps the length. An empty result +// signals the entry should be dropped. The cleaning itself is termsafe's — the +// canonical home this seam and cleanSynthProse both route through, rather than +// keeping divergent copies. func cleanLessonProse(s string) string { - s = strings.ReplaceAll(s, "\r", " ") - s = strings.ReplaceAll(s, "\n", " ") - s = strings.ReplaceAll(s, "", "-- >") - s = sanitize(s) // C0/DEL -> ?, tab -> space (the coverage helper) - s = strings.TrimSpace(s) - if len(s) > maxLessonProseBytes { - s = strings.ToValidUTF8(s[:maxLessonProseBytes], "") - s = strings.TrimSpace(s) - } - return s + return termsafe.CleanProse(s, maxLessonProseBytes) } diff --git a/internal/core/lifeboat/graveyard_lessons_test.go b/internal/core/lifeboat/graveyard_lessons_test.go index 7816a275..007c5ce0 100644 --- a/internal/core/lifeboat/graveyard_lessons_test.go +++ b/internal/core/lifeboat/graveyard_lessons_test.go @@ -354,6 +354,39 @@ func TestIngestLessonsSanitisesProse(t *testing.T) { } } +// TestIngestLessonsNeutralisesRawHTMLOpener pins the wider neutralisation the +// termsafe seam carries: a bare HTML opener (not just a comment marker) is broken +// with a space, so packed prose cannot smuggle a tag into a rendered lesson. +func TestIngestLessonsNeutralisesRawHTMLOpener(t *testing.T) { + dir := stdFixture(t) + raw := payload(t, Lesson{ID: "les-tag", Lesson: "before after", + Confidence: ConfidenceHigh, Evidence: []string{"adr-12"}}) + if _, err := IngestLessons(dir, raw); err != nil { + t.Fatalf("IngestLessons: %v", err) + } + got := readWrittenLessons(t, dir).Lessons[0].Lesson + if !strings.Contains(got, "< script") { + t.Errorf("raw HTML opener not neutralised: %q", got) + } +} + +// TestIngestLessonsNeutralisesAfterSanitise pins the ORDER of the two steps: +// Sanitize substitutes '?' for a masked control byte, so a '<' followed by an +// escape becomes ' Date: Wed, 26 Aug 2026 14:45:09 +0000 Subject: [PATCH 14/20] fix: bound the lifeboat probe's root-sha read firstRootSHA was the one git read on the untrusted-repo probe path still using the uncapped runner, against the file's own every-read-is- bounded contract; a repo with many parentless roots buffers one line per root. The read now goes through RunLimited with rev-list -n 1, which returns the same first root, and a source-guard test keeps the package off the uncapped runner. Resolves: iss-2608261437048689 Assisted-by: Claude:claude-fable-5 --- internal/core/lifeboat/probe.go | 2 +- internal/core/lifeboat/probe_test.go | 58 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/core/lifeboat/probe.go b/internal/core/lifeboat/probe.go index 7ce1d459..5e716444 100644 --- a/internal/core/lifeboat/probe.go +++ b/internal/core/lifeboat/probe.go @@ -554,7 +554,7 @@ func isSkipDir(name string) bool { // firstRootSHA returns the canonical (first) root-commit SHA, or "". func firstRootSHA(repoRoot string) string { - out, err := gitutil.Run(repoRoot, "rev-list", "--max-parents=0", "HEAD") + out, err := gitutil.RunLimited(repoRoot, maxGitOutputBytes, "rev-list", "-n", "1", "--max-parents=0", "HEAD") if err != nil { return "" } diff --git a/internal/core/lifeboat/probe_test.go b/internal/core/lifeboat/probe_test.go index 45c2b235..671145f0 100644 --- a/internal/core/lifeboat/probe_test.go +++ b/internal/core/lifeboat/probe_test.go @@ -500,3 +500,61 @@ func itoa(n int64) string { } return string(b) } + +// TestFirstRootSHAOctopusMerge pins the canonical root over a history with more +// than one root commit: an octopus merge joins three unrelated roots, and +// firstRootSHA must report the same one git's own `rev-list -n 1` does — the +// identity every cross-repo mapping keys on. +func TestFirstRootSHAOctopusMerge(t *testing.T) { + r := gvNewRepo(t) + r.write("a.txt", "a\n") + r.addCommit("root one") + for _, b := range []string{"b2", "b3"} { + r.git("checkout", "-q", "--orphan", b) + r.git("rm", "-q", "-rf", ".") + r.write(b+".txt", b+"\n") + r.addCommit("root " + b) + } + r.git("checkout", "-q", "-f", "main") + // git merge refuses an octopus of unrelated histories, so the merge commit is + // built with plumbing — the shape is what matters, not how it was made. + tree := strings.TrimSpace(r.git("rev-parse", "main^{tree}")) + parents := []string{"commit-tree", tree, "-m", "octopus"} + for _, ref := range []string{"main", "b2", "b3"} { + parents = append(parents, "-p", strings.TrimSpace(r.git("rev-parse", ref))) + } + merge := strings.TrimSpace(r.git(parents...)) + r.git("update-ref", "refs/heads/main", merge) + r.git("reset", "-q", "--hard", "main") + + want := strings.TrimSpace(r.git("rev-list", "-n", "1", "--max-parents=0", "HEAD")) + if got := firstRootSHA(r.dir); got != want { + t.Errorf("firstRootSHA = %q, want %q", got, want) + } +} + +// TestNoUncappedGitRun is a source guard: every git call in this package buffers +// untrusted repository output, so it must go through the capped RunLimited form. +// The uncapped gitutil.Run reads a hostile repo's stdout without a ceiling, and +// one uncapped call is enough to reopen that. Asserted over the sources rather +// than per call site, so a new one cannot be added quietly. +func TestNoUncappedGitRun(t *testing.T) { + // Built at run time so this file is not itself a match. + needle := "gitutil.Run" + "(" + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") { + continue + } + data, err := os.ReadFile(e.Name()) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), needle) { + t.Errorf("%s calls the uncapped %s — use gitutil.RunLimited with maxGitOutputBytes", e.Name(), needle) + } + } +} From 15b849d66e9dc77a48c14b88acf929a74030ee78 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:45:19 +0000 Subject: [PATCH 15/20] fix: RD002 sees renames and deletions, and a git failure refuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-file probe ran git log --diff-filter=MR under a pathspec limited to the new path, where diffcore can never form a rename pair — so renaming a review file in the same commit as a rewrite passed the append-only gate, and the stderr swallow made any git failure read as a clean corpus. One history pass over the reviews root now flags every modify, rename, delete or type change of a dated review file, closing the deferred deletion case with the same line; git failures refuse with exit 2, an unborn HEAD reports loudly that nothing is covered, and a cases harness pins all eight polarities in the Makefile gate and CI. The brief's RD002 row states the delivered rule. Resolves: iss-2608261437044382 Assisted-by: Claude:claude-fable-5 --- .../development/brief/05-internals/06-lint.md | 2 +- .github/workflows/ci.yml | 3 + Makefile | 5 +- scripts/check-reviews-cases.sh | 190 ++++++++++++++++++ scripts/check-reviews.sh | 71 ++++++- 5 files changed, 263 insertions(+), 8 deletions(-) create mode 100755 scripts/check-reviews-cases.sh diff --git a/.abcd/development/brief/05-internals/06-lint.md b/.abcd/development/brief/05-internals/06-lint.md index d4c92c06..4d26260d 100644 --- a/.abcd/development/brief/05-internals/06-lint.md +++ b/.abcd/development/brief/05-internals/06-lint.md @@ -112,7 +112,7 @@ Lint codes are 2-letter category prefix + 3-digit numeric suffix: `AAnnn`. Categ - `RC007` DONE spec references an itd-N with NO intent file in any bucket — reference-to-nothing. Same DONE-spec walk + extraction priority as `RC006`. Severity: info. **Delivered** in spc-48 .2. The gate keeps its REPORT-not-block exit grammar (clean/info/warn → exit 0; nonzero only for a config-promoted blocker or a malformed invocation) — `RC006`/`RC007` do not change it. - `RD001` review directory malformed — a directory under `.abcd/work/reviews/` is not named `-` or lacks the required `00-summary.md` consolidated report. The charter's `reviews/README.md` sits at the root and is exempt; so are the 40-hex sha-keyed semantic-gate receipt directories (`.abcd/work/reviews/<40-hex>/.json`, iss-35), a distinct artefact class with their own `receipt_gate` integrity check. Severity: blocker. Enforced now by the standalone pre-push/CI gate `scripts/check-reviews.sh`; ports into `internal/core/lint`. -- `RD002` review file edited after creation — a file inside a dated review directory carries a post-creation modify/rename in committed history, violating the append-only rule (a review is immutable; reality is never edited to match a review, nor a review to match reality). Deletion detection over history is deferred to the ported Go implementation. Severity: blocker. Enforced now by `scripts/check-reviews.sh`, which needs full git history: on a shallow checkout the script refuses (exit 2) rather than pass vacuously, since past the graft boundary every file reads as newly added and the append-only check covers nothing. The RS-family gate (`scripts/check-issue-resolution.sh`) shares the refusal for the same reason — an unfetched commit is indistinguishable from an absent one. The CI `record-lint` job checks out with full depth. +- `RD002` review file changed after creation — a file inside a dated review directory carries a post-creation modify, rename or delete in committed history, violating the append-only rule (a review is immutable; reality is never edited to match a review, nor a review to match reality). All three shapes refuse: the gate makes ONE history pass over the reviews root (`git log --name-status --diff-filter=DMRT`) and flags every status line whose path falls in the dated-directory population, rather than probing each working-tree file — a pathspec-scoped log never reports a rename, and a deleted file leaves no tree path to probe from. Severity: blocker. Enforced now by `scripts/check-reviews.sh`, which needs full git history: on a shallow checkout the script refuses (exit 2) rather than pass vacuously, since past the graft boundary every file reads as newly added and the append-only check covers nothing. Every other git failure refuses the same way (exit 2) instead of reading as a clean history; the one exception is an unborn HEAD — a freshly scaffolded repo — which passes while saying loudly that it covered nothing. `scripts/check-reviews-cases.sh` stages each shape in a scratch repository and asserts both polarities, so the refusal is a watched fact rather than a claim. The RS-family gate (`scripts/check-issue-resolution.sh`) shares the refusal for the same reason — an unfetched commit is indistinguishable from an absent one. The CI `record-lint` job checks out with full depth. - `RD003` review path leak — a review document contains an absolute personal path (`/Users/…`, `/home//…`, `C:\Users\…`) rather than a repo-relative one. Severity: blocker. Enforced now by `scripts/check-reviews.sh`; ports into `internal/core/lint`. The privacy scan is narrower here than the launch PII gate — it targets only the personal-path leak the charter forbids. The charter's two **semantic** rules — the provenance discriminator ("no abcd verb invocation produced it → it belongs here") and "reviews are not a shadow backlog" (findings graduate to intents/issues/ADRs) — are not machine-decidable and stay conventions, enforced by review, not lint. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c75274eb..949b0ddc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,6 +349,9 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Reviews-charter gate can fail (cases) + run: bash scripts/check-reviews-cases.sh + - name: Reviews-charter discipline (RD001-RD003) run: bash scripts/check-reviews.sh diff --git a/Makefile b/Makefile index 771391c9..7703e847 100644 --- a/Makefile +++ b/Makefile @@ -40,8 +40,11 @@ vet: # Deterministic gate for the .abcd/work/reviews/ charter (RD001-RD003) — a # stopgap until these codes land in internal/core/lint. Needs full git history # (RD002 is append-only over committed history): on a shallow checkout the -# script refuses (exit 2) rather than pass vacuously with nothing covered. +# script refuses (exit 2) rather than pass vacuously with nothing covered. The +# cases run first, as in lint-issues: a gate nobody has watched fail is an +# enforcement claim with no evidence behind it. lint-reviews: + @bash scripts/check-reviews-cases.sh @bash scripts/check-reviews.sh # AI-attribution gate (AGENTS.md § Attribution). Checks the commit trailers on diff --git a/scripts/check-reviews-cases.sh b/scripts/check-reviews-cases.sh new file mode 100755 index 00000000..c88a03cd --- /dev/null +++ b/scripts/check-reviews-cases.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# Proves check-reviews.sh can actually FAIL, and fails for the right reasons. +# +# RD002 is an append-only claim over committed history, and the claim is a fact +# about the history or it is nothing. Its first implementation probed each +# working-tree file with a pathspec-scoped `git log --diff-filter=MR`, which can +# never report an R — rename detection needs both sides in view — and swallowed +# git's own errors, so a rename and a broken git both read as clean. That is the +# worst shape a gate can take: green while it sees nothing. Every rule here is +# asserted in BOTH directions — a violating fixture must be refused, and a clean +# one must pass — because a check that only ever sees clean input proves nothing +# about refusal. +# +# Fixtures are built in a scratch repository, never against this one: the rule is +# about committed history, and history is cheap to stage in a throwaway repo and +# impossible to stage honestly in a tree someone is working in. +# +# Usage: check-reviews-cases.sh +# Exit 0 all cases behaved, 1 a case did not. +set -euo pipefail + +# Hermetic git (iss-28, iss-313): every scratch-repo command below is `git -C +# "$d" …`, but an inherited absolute GIT_DIR overrides -C and redirects these +# fixture commits onto the ambient repository — which then reports all-green while +# its real history is rewritten. An inherited GIT_CONFIG_GLOBAL / core.hooksPath +# also fires the developer's global hooks inside the scratch repos and breaks the +# run for a reason that has nothing to do with the reviews charter. Neutralise the +# ambient git environment before the first git call, matching +# check-issue-resolution-cases.sh and gitutil.IsolatedEnv. (commit.gpgsign is set +# per-repo below; this closes the rest.) +unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \ + GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_CONFIG GIT_CONFIG_COUNT GIT_CONFIG_PARAMETERS +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0 + +GATE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-reviews.sh" +[ -x "$GATE" ] || { + echo "cases: gate not executable: $GATE" >&2 + exit 2 +} + +failures=0 +tmproot="$(mktemp -d)" +trap 'rm -rf "$tmproot"' EXIT + +REV_DIR=".abcd/work/reviews" +SCOPE="$REV_DIR/2026-01-01-a-scope" + +# expect