Development record
2026-08-21 — Bug-hunt round 4, branch bughunt-b/round-4. Baseline (make preflight
decisiondcn-2608210000000001
2026-08-21 — Bug-hunt round 4, branch bughunt-b/round-4. Baseline (make preflight
- gofmt -l .) green before any change. Five parallel Opus 5 hunters swept the four dimensions; each candidate was adversarially refuted by an independent Opus 5 subagent before capture. Findings: 8 substantive, 7 nitpick, 2 refuted. Fixed and resolved this round (code, each with a watched-fail test): iss-2608211132061930 (MAJOR) lint.LoadConfig read the docs-lint/record-lint config with a raw os.ReadFile while its .abcd/*.json siblings guard.Load/rules.Load/positioning.LoadConfig all guard it — hook-reachable (ahoy.Detect) and cross-repo-clonable, so a committed config symlink to a FIFO wedged docs lint/lint/ahoy/hook/cite refresh, /dev/zero OOMed the CLI, and an out-of-repo target ran the whole ruleset from a file the repo does not own; now fsutil.ReadGuarded + an Lstat dir-symlink refusal. iss-2608211134281912 four stdin operand readers (ideate/lessons/synthesis/readSource) read exactly cap and silently truncated an over-cap payload — worst on history capture (stored an 8 MiB prefix under a sha256 idempotency key over the prefix, breaking spc-4's refuse-whole invariant); added readCappedStdin (cap+1 probe). iss-2608211139076040 the iss-392 canonRecordID fix left four sibling lint keyspaces raw (delivery_state bucket + citation, spec intent-existence, superseded_by, spec_id_unique), so a zero-padded id slipped a gate open or false-blocked a link; routed every key and lookup through canonRecordID. iss-2608211140410761 abcd adr-N confirmed the frontmatter id byte-exact while record-lint and the citation resolver treat padded/quoted/cased ids as one handle, reporting a present ADR absent (and abcd adr-0003 routing then refusing); now a parsed-handle compare rendering the canonical id. Docs/record fixes: iss-2608211142146585 + iss-2608211142142469 the release-gate runbook claimed the verify job runs macOS+Linux (it is ubuntu-only; ci.yml's check job is the matrix and the merge gate) and framed the semantic-receipt gate as dormant-until-public-flip though it is public and has fail-closed two releases (v0.3.0/iss-108, v0.6.0/iss-326). iss-2608211142496517 cmd/scaffold-sync named a "scaffold-sync workflow" that was built and rejected (iss-209). iss-2608211143184945
- iss-2608211143185943 the README Go badge (1.25→1.26) and the retired "next free id" capture description. iss-2608211144265046 commands/lint.md documented a line:0 sentinel the omitempty JSON never emits. iss-2608211144555085 the ADR index omitted the accepted adr-45 row (third recurrence of the class; iss-38 wants such indexes gated). iss-2608211145448208 the DCO deferral text adr-43 retired survived in AGENTS.md and scripts/check-attribution.sh. iss-2608211146222734 the banned pre-adr-30 "roadmap/intents" literal survived in AGENTS.md and .abcd/README.md, outside the lint roots. iss-2608211146562243 RD001 was documented unconditional though check-reviews.sh exempts 7 of 10 live dirs (sha-keyed receipts). iss-2608211147403129 the ADR-RFC frontmatter pairing contract disagreed across three surfaces; reconciled rfcs/README.md and rfc-2. Refuted (kept out of the ledger): the CHANGELOG [Unreleased] Fixed-vs-additive sectioning (the cut discards Unreleased prose and re-derives sections from records; owned by open iss-256); the .abcd/work tier map "omission" (highlight enumerations, not inventories; iss-333 already blessed the table); and the launch DenyNamespaces case-fold symlink slip (inert — the hardlink alias map rejects via aliasDenied). Model routing: orchestration on Claude Fable 5; five parallel hunters and ten per-finding adversarial refuters on Claude Opus 5; the dual pre-merge review runs one reviewer as Claude Fable 5 and one as Claude Opus 5.
Frontmatter
| id | dcn-2608210000000001 |
|---|---|
| type | decision |
| date | 2026-08-21 |
.abcd/work/decisions/dcn-2608210000000001-bug-hunt-round-4-branch-bughunt-b.md
open on GitHub ↗ · commit history ↗
mentions28
- adr-3Directory location is the source of truth for lifecycle state
- adr-30Design-record information architecture — flat artefact-type folders
- adr-43Contributions are inbound = outbound MIT, and the trust boundary is the organisation's role ladder
- adr-45Record ids are timestamp-numeric, capture-stable, and never allocated by looking at the maximum
- iss-38hand-maintained index drift: intents/README.md corpus listings have drifted from the filesystem; commands/README.md lists three of the seven plugin verb files; internal/README.md still describes core as two capabilities with adapter/scanner as a planned seam; the repo README Layout omits skills/ from the plugin surface. Detector (per adr-5 derive-dont-store): hand enumerations of sibling files are generated or deleted — a lint flags a README that enumerates directory contents by hand, or the enumeration is emitted by tooling with a drift test. Acceptance corpus: the four stale indexes above.
- iss-108iss-35 semantic release gate was self-referential under changelog-driven auto-release (adr-37): it armed receipt_gate with the tagged commit but read receipts from that commit's own tree, which can never hold a receipt naming itself. Dormant while private, it fail-closed the first public release (v0.3.0).
- iss-209Every dependabot PR that bumps a pinned action in .github/workflows/release.yml or auto-release.yml fails TestSelfScaffoldParity and can never go green on its own. Observed on PR #211 (actions/attest 4.2.0 -> 4.2.2): 'release.yml: abcd rendering is not byte-identical to the committed workflow', failing check on BOTH platforms while gitleaks, record-lint, smoke and zizmor all pass. Root cause: the committed workflow is a GENERATED artefact. internal/core/launch/scaffold/templates/release.yml.tmpl:270 and .github/workflows/release.yml:248 both pin actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 (v4.2.0), and TestSelfScaffoldParity asserts the two are byte-identical so that abcd's own release dogfoods the machinery a managed repo receives. Dependabot edits only the derived file, so parity breaks by construction on every such bump — this is recurring and structural, not a one-off. The obvious fix (bring the scaffold template into dependabot's scope) is NOT AVAILABLE: the github-actions ecosystem discovers only .github/workflows/*.yml and composite action.yml manifests under the configured directory, and no package-ecosystem scans an arbitrary .tmpl under internal/. .github/dependabot.yml today declares gomod and github-actions, both at directory /. Note also the wrong-direction trap: auto-rendering the scaffold on a dependabot PR would REVERT the bump, since render flows template -> workflow. Options, none adopted: (a) make the parity failure self-explaining — the message says 'not byte-identical' and prints the first diff but never names the template path or says 'an action bump must be applied to the template too', which is the cheapest change and matches the loud-staging principle; (b) propagate the bump backwards — a job on dependabot PRs that applies the changed pin to the template and commits to the bot branch, which is the durable fix but is custom automation writing to a bot branch; (c) drop these two workflows from dependabot's scope and manage their pins by hand from the template, trading automation for consistency. Secondary risk worth weighing: a permanently-red dependabot PR trains the maintainer to discount red CI on exactly the PRs where CI most needs to be trusted.
- iss-256Post-cutover, a hand-written CHANGELOG Unreleased entry blocks every derived cut: ingest refuses a non-empty Unreleased section by design, but nothing stops the entry landing in the first place — the contributor habit predates the derived flow, and the first collision happened the day after the cutover (a merged PR added an Unreleased entry for work that had no resolved record, wedging the next cut until the entry was converted into a record by hand). Detector: a record-lint or CI rule that blocks a PR adding lines under '## [Unreleased]'; acceptance: such a PR fails its record gate with a message naming the record-first flow.
- iss-326The v0.6.0 tag is permanently unpublishable: its roll branch carried no receipts commit, the receipt gate fail-closed at release time, and the re-release path builds only from the tagged commit whose frozen tree can never contain receipts naming its own content commit. The tag stays as a record (anti-tag-move); v0.6.1 is the recovery
- iss-333brief/05-internals/03-configuration.md's Plugin-internal development namespace tree draws the pre-adr-30 layout: roadmap/intents/{drafts,planned,shipped}, research/adr/, research/phase/, and specs/ under .abcd/ — the record's only map of its own namespace routes a reader to four paths that do not exist; the blocker moved-intents-path rule is defeated only by the ASCII art splitting the string
- iss-392bughunt round-3 confirmed nitpick batch — zero-padded record-id filenames evade the id-uniqueness lint in all three families; the go-version lockstep test skips .yaml workflows; scripts/*.sh lack the eol=lf pin the repo's own policy states; agents/CHANGELOG announces lifeboat-reviewer 0.1.1 while the prompt carries 0.1.0; five live records still route to the pre-flattening commands/abcd/ and retired docs/reference pages
- iss-2608211132061930lint.LoadConfig reads the docs-lint/record-lint config with a raw os.ReadFile — no O_NOFOLLOW, no regular-file check, no size cap — while its .abcd/*.json siblings guard.Load, rules.Load and positioning.LoadConfig all guard the read as a trust boundary. The read is hook-reachable (ahoy.Detect via hook session-start/end) and cross-repo-clonable, so a committed .abcd/docs-lint.json symlink to a FIFO wedges abcd docs lint / lint / ahoy / hook / cite refresh and the record-lint CI leg, a /dev/zero target OOMs the CLI, and an out-of-repo symlink target runs the entire ruleset from a file the repository does not own (gate substitution).
- iss-2608211134281912Four untrusted-operand stdin readers (readIdeatePayload, readLessonsPayload, readSynthesisPayload, readSource) read exactly cap bytes via a bare io.LimitReader(cap), so an over-cap '-' payload is silently truncated into a severed prefix instead of refused, while each reader's file transport (readGuardedOperand) refuses the same bytes with fsutil.ErrTooBig. Worst on the history capture path: history.Capture performs no parse and no cap check, so an over-cap stdin transcript is stored as an 8 MiB prefix, reported 'stored', under a sha256 idempotency key computed over the prefix — breaking spc-4's refuse-whole invariant. Also lets an over-cap ideate verdict past both the MaxPayloadBytes cap gate and the dec.More() trailing-data gate at a crafted boundary. Unswept siblings of resolved iss-201/iss-347; every site's doc comment claims stdin parity with the file cap.
- iss-2608211139076040The iss-392 canonRecordID fix canonicalised four record-id-uniqueness scan sites but missed the sibling sites sharing the same keyspace: deliverystate.go keyed the intent bucket map and its CHANGELOG citations on the raw id spelling (so a zero-padded intent filename or citation silently failed the drafts-citation delivery_state gate open), speclinks.go/KnownIntents and the superseded_by (lint.go) and spec intent-existence (lint.go validateSpec) lookups keyed/looked-up raw (false-blocking a padded twin), and spec_id_unique keyed on the raw frontmatter id (fail-open on a padded spc twin, the very hole iss-392 claimed to close for 'all three families'). The playbook's second spine: a pattern fixed at 4 of N sites leaves N-4 latent.
- iss-2608211140410761abcd <id> ADR dispatch (record.go describeADR) confirms the frontmatter id with a byte-exact 'fields["id"].Value != id' compare (frontmatter.Field.Value is TrimSpace-only, quotes not stripped), while record-lint schema.go compares parsed handles (a tested tolerance, TestRecordSchemaFilenameIDComparesNumerically) and the citation resolver rebuilds ids from the parsed integer. So an ADR written id: "adr-12" (quoted), adr-0012 (padded), or ADR-12 (cased) is lint-green and citation-resolvable but abcd adr-12 reports 'not found'. A live facet against the shipped corpus: abcd adr-0003 (padded, admitted by IDRe) routes to the file then refuses. Second site under iss-285's quote-normalisation-split principle.
- iss-2608211142142469The release-gate runbook describes the semantic-receipt gate as 'Dormant until the public flip' (README.md blockquote) and phrases arming as a future condition ('Once the fail-closed verify rule is armed, the tag is rejected'), but the repo is public and the gate is armed and fed receipts on every release — it has fail-closed two: v0.3.0 (iss-108, the self-reference defect) and v0.6.0 (iss-326, a missing receipts commit, leaving a permanently unpublishable tag). The runbook a maintainer follows at tag time thus tells them receipts are not yet required, which is iss-326's proximate cause. It also misstates the failure mode ('the tag is rejected' — the tag survives, anti-tag-move; the publication is refused) and self-contradicts at the adjacent parenthetical ('dormant... never exercised' vs iss-108's v0.3.0 firing). Inverts the enforcement-claims-are-facts principle (an under-claimed live gate).
- iss-2608211142146585The release-gate runbook (.abcd/development/release-gate/README.md) misdescribes release.yml's verify job in two present-tense claims a maintainer reads at tag time. (1) It says the verify job runs the nine gates 'on macOS + Linux', but release.yml's verify job is runs-on: ubuntu-latest with no matrix — Linux only; the macOS coverage is ci.yml's check job. The gate_lockstep record-lint rule mirrors only the verify STEP NAMES, never runs-on, so the platform claim is outside its reach. (2) 'The verify job gates the merge' is false: verify runs post-merge on the tag/workflow_call; ci.yml's required checks gate the merge. Doc-accuracy family of resolved iss-182.
- iss-2608211142496517cmd/scaffold-sync/main.go's package doc comment states the command is 'run by the scaffold-sync workflow on a dependabot branch', but no such workflow exists or ever existed: the string appears nowhere in .github/, and iss-209 records that a workflow_run automation was built and rejected in review (a GITHUB_TOKEN push raises no events, leaving the PR permanently pending; and it was unsound). Makefile:72-76 states the opposite outright ('Nothing in CI calls either target'). The false claim misdirects whoever debugs a red TestSelfScaffoldParity to wait for automation that will never run or re-attempt the dead end iss-209 exists to stop.
- iss-2608211143184945README.md's Go badge advertises 'Go 1.25' while go.mod declares go 1.26.7 and every CI/release pin is 1.26.7; CHANGELOG records the deliberate move to 1.26 for support-window reasons. The badge is the front page's only toolchain statement, and 1.25 is a version the project just retired for security. GOTOOLCHAIN=auto self-corrects a from-source build, so harm is bounded, but the front page contradicts a deliberate security decision. The go-version lockstep test walks .github/workflows only, so no gate covers the badge.
- iss-2608211143185943README.md described abcd capture as minting records 'with the next free id' — the proper name of the max+1 allocation protocol that adr-45/spc-33/itd-114 abolished after live collisions (one costing a release re-cut). 'Next free id' is the codebase's own name for the retired mechanism (recordid.MaxAcrossRefs, lint.go). The README is the only public document still describing it, setting the wrong mental model (small sequential successor-comparable ids) and legitimising the hand-add-at-max+1 path iss-74 identified as a collision source. Written 2026-08-17, pre-rollout.
- iss-2608211144265046commands/lint.md documented repolint JSON findings as carrying 'a file and line (line 0 means the finding is not tied to one line)', but repolint.go tags Line as json:line,omitempty so a zero line is omitted entirely, never emitted as 0. Line-less findings are the majority (only docs-currency/privacy-hygiene/identity-positioning set Line). An agent following the doc and testing line==0, or formatting file:line, produces file:undefined. The brief 16-lint.md carries the true contract but named only two line-carrying rules, omitting identity-positioning (rule_positioning.go also sets Line). commands/prepare-this-repo.md implied the same line sentinel.
- iss-2608211144555085The ADR index (.abcd/development/decisions/adrs/README.md) ends at the adr-44 row while adr-45 is accepted (2026-08-20) and cited as settled law by six live records (the invariants brief, itd-114, spc-33, .abcd/work/issues/README.md, two open issues, DECISIONS.md). 37 ADR files vs 36 index rows — exactly one missing. The index is the declared hand-maintained roll-call (adrs/README.md) and is not among index_drift's four registered indexes, so it is structurally ungated. Third recurrence of this class in the same table (after resolved iss-296 and iss-374), which iss-38's decision (hand-kept indexes must be deleted or gated) predicts.
- iss-2608211145448208adr-43 (accepted 2026-08-19) decided 'No CLA, and no DCO' and named as its own consequence 'the DCO deferral text retires from every surface that carried it', but the deferral survived in two live surfaces the ADR's landing commit did not touch: AGENTS.md (symlinked as CLAUDE.md/GEMINI.md, injected into every agent session) said 'A human-only Signed-off-by: (DCO) is deferred to the public flip or the first outside contribution', and scripts/check-attribution.sh carried it twice — a comment and the runtime error text shown to a contributor whose commit trips COAUTHOR_RE — stating the false premise that the repo defers DCO until it goes public (it is already public). CONTRIBUTING.md already says 'no CLA and no Signed-off-by requirement', so the surfaces contradicted each other.
- iss-2608211146222734The retired pre-adr-30 path literal 'roadmap/intents' — a record-lint blocker token (moved-intents-path: 'intents live at intents/') — survived in AGENTS.md and .abcd/README.md, the two canonical where-things-live maps, both outside record-lint's roots (.abcd/development) so the armed token is structurally blind to them. Read in context beside the real nested path decisions/adrs, the slash reads as a path separator, i.e. the banned pre-adr-30 layout. A 2026-07-06 review already adjudicated both sites stale (fix #6). CLAUDE.md is a symlink to AGENTS.md, so the line is injected into every agent session and risks an agent propagating the banned path into development/ where it is a live blocker. Live instance of open iss-279's detector gap (banned_tokens cannot reach AGENTS.md/CONTRIBUTING.md).
- iss-2608211146562243RD001 is documented as unconditional in both definition sites (.abcd/work/reviews/README.md and brief/05-internals/06-lint.md name only the root README as exempt), but scripts/check-reviews.sh exempts 40-hex sha-keyed semantic-gate receipt directories — 7 of the 10 live review directories. The exemption is principled and recorded elsewhere ([dcn-2607180000000001](../../decisions/dcn-2607180000000001-the-iss-35-semantic-release-gate-was.md), spc-14, itd-93 acceptance criterion), but a reader auditing the folder against either charter sees 7 apparent blockers, and 06-lint.md says the rule 'ports into internal/core/lint', so an implementer working from the record alone would port it without the exemption and turn the gate permanently red.
- iss-2608211147403129The ADR-RFC pairing contract disagreed across three surfaces. adrs/README.md declared RFCs carry 'related_adrs: [adr-N, ...]', but rfcs/README.md (the RFC frontmatter spec and its bidirectional table) defined no such field; and rfc-2 carried 'spawned_from: adr-43' — a field rfcs/README.md documented as itd-N only — while omitting spawned_intents/related_intents/authors/related_adrs, so adr-43's declared pair (related_rfcs: [rfc-2]) was one-sided. The RFC store is ungated (record_schema registers adr/itd/spc/iss only). rfc-2 was the sole outlier in the two-file store; landed with adr-43 (2026-08-19) with no recorded follow-up. Structurally identical to resolved iss-360 (adr-7 outlier).
- iss-2608271702469341intake.md graduates to the durable record when the contribute-triage verb ships: .abcd/work/intake.md stays in the working tier today as the live S1-S6 external-contribution runbook (the 2026-08-27 structural review re-adjudicated its placement: live operating state, cited in force by [dcn-2608190000000002](../../decisions/dcn-2608190000000002-review-agents-keep-their-bash-grant.md) and filed-against by ledger records), but its own header declares it the script-first MVP rung that a future 'abcd contribute triage' verb absorbs. When that verb ships, the protocol content is no longer working state and the document's home moves up: promote the durable protocol description into .abcd/development/ (brief surface or discipline record) in a change that says why, leave or redirect the working-tier path for in-flight citers (iss-276 and resolved iss-2608261533173466 cite found_at .abcd/work/intake.md; [dcn-2608190000000002](../../decisions/dcn-2608190000000002-review-agents-keep-their-bash-grant.md) cites its S4 rule), and record the supersession of the highlight-not-inventory adjudication ([dcn-2608210000000001](../../decisions/dcn-2608210000000001-bug-hunt-round-4-branch-bughunt-b.md), [dcn-2608220000000001](../../decisions/dcn-2608220000000001-bug-hunt-round-7-branch-bughunt-b.md)) for this file. Trigger is the verb shipping, not a date.
- spc-4start-the-transcript-clock