Development record
fail-closed capture surface: a misspelled capture subcommand (e.g. capture resovle) is swallowed as capture text and files a NEW issue instead of erroring (internal/surface/cli/cli.go:455) — a typo becomes a ledger mutation; --json errors emit raw Go text, not JSON (internal/surface/cli/cli.go:165), and abcd docs lint without a config surfaces a raw file error. Detector (per unrecognized-input-never-writes): a surface test convention where every mutating verb has a malformed-input case asserting an error, a did-you-mean for near-misses, and no write occurred, plus a --json error-shape contract test. Acceptance corpus: the three instances above — the detector is proven when it flags all three.
fail-closed capture surface: a misspelled capture subcommand (e.g. capture resovle) is swallowed as capture text and files a NEW issue instead of erroring (internal/surface/cli/cli.go:455) — a typo becomes a ledger mutation; --json errors emit raw Go text, not JSON (internal/surface/cli/cli.go:165), and abcd docs lint without a config surfaces a raw file error. Detector (per unrecognized-input-never-writes): a surface test convention where every mutating verb has a malformed-input case asserting an error, a did-you-mean for near-misses, and no write occurred, plus a --json error-shape contract test. Acceptance corpus: the three instances above — the detector is proven when it flags all three.
Frontmatter
| id | iss-29 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-29-fail-closed-capture-surface.md
open on GitHub ↗ · commit history ↗
mentions5
- dcn-26070800000000152026-07-08 — Post-review recording follows fix-the-detector: findings are
- dcn-26071200000000012026-07-12 — clean-slate hardening run STEP 0 triage. A fresh adversarial
- iss-53Every mutating verb's brief surface row carries at least one If/Then unwanted-behaviour clause in the EARS pattern (If <unwanted condition>, then the system shall <refusal or recovery>), making the refusal a first-class, testable spec item before any code exists. The convention serves specification completeness: unwanted-behaviour handling is the dominant omission class in specs, and a row that only describes the happy path leaves the fail-closed behaviour to be invented at implementation time. This feeds the iss-29 fail-closed test convention, which needs a spec-level clause to test against. Detector/acceptance: a surface-row lint flags any mutating verb whose row lacks an If/Then clause, and each clause maps to at least one fail-closed test.
- iss-76cli.Run now routes all command errors through the --json envelope, so any verb that returns a bare *fs.PathError (os.ReadFile/os.Open failures not wrapped by core) emits the absolute local path into machine JSON output, violating no-absolute-paths-in-machine-output. iss-29 sanitised the docs-lint config-load branch specifically; the systemic fix is to sanitise PathError-bearing errors at the Run() boundary (or audit which errors reach the envelope). Detector: a table test that runs each verb's known filesystem-error path under --json and asserts the envelope carries no absolute path. Pre-existing as stderr text; newly widened to --json by cli.Run.
- iss-210A lone token that a user plausibly expects to be a subverb, but which is not one, is swallowed as free text and WRITES A RECORD on both mutating verb families — 'abcd intent status' files drafts/itd-N-status.md and 'abcd capture status' files issues/open/iss-N-status.md, each exit 0. Verified 2026-08-11 by building HEAD into a scratch repo and running both. This is a residual hole in the unrecognized-input-never-writes rule (iss-29) that iss-104's fix does not cover: suspectedTypoedSubcommand (internal/surface/cli/cli.go:2000) fires only on a NEAR-MISS of a real subverb, bestDist initialised to 3 so only edit distances 1 and 2 refuse. It is deliberately high-precision so a genuine multi-word prose title whose first word resembles a verb still files — the controls confirm that half works: 'capture resovle iss-1 note' and 'intent paln itd-1' both exit 2 with a did-you-mean and write nothing. But 'status' is edit distance >=4 from every real subverb in either family (intent: new/plan/ready/link/review/ingest; capture: list/resolve/wontfix), so the guard never engages and the word becomes the record title. The class is any plausible-but-nonexistent subverb far from every real one; 'abcd intent list' happens to be caught only by the accident that levenshtein(list,link)=2. NOT an intent-vs-capture asymmetry: the two behave identically, so a fix must land on the shared guard, not on the intent path alone. Fix direction: edit distance is the wrong discriminator for this class. A LONE single token with no other args is never a useful issue or intent title, so treat it as an unrecognized subcommand regardless of distance — refuse, exit 2, write nothing, name the real subverbs, and say how to force prose (quote it, or add words) so the terse-free-text escape hatch stays open. Detector: extend intent_surface_test.go and the capture surface test with a lone-token case per mutating verb family asserting exit 2 and zero records written, watched fail before the change; the existing near-miss cases must keep passing, and a multi-word prose title must still file. Impact on resolve is 'fix' at best and arguably 'breaking' — an unattended script or loop that shells 'abcd capture <one-word>' expecting a file would start failing — so it is NOT additive-class. Standing risk this closes: any autonomous loop that shells a bare word into either verb silently grows the ledger with junk records that a human must later triage.