@dzhechkov/skills-feature-adr 1.5.14 → 1.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dz-manifest.json CHANGED
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "8c96e82b41fb8479befb56797a367b3cc2119cc5b022b18fdd18f129ef8cd0f0"
16
+ "sha256": "66f3b2cf7fdcf9efbf64c7d5cc36b34ffc82b151a9bc07e6fda5fc47d4fba72e"
17
17
  },
18
18
  {
19
19
  "path": "bin/cli.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": "package.json",
28
- "sha256": "0cdd048a5aacd52294f72b5903b5a94c049ad61680dbcac532d5cb3fbd45c736"
28
+ "sha256": "e74b1a22e0260a61a3ab37512366402c4e6231b85625028a86219ee3f5c21e12"
29
29
  },
30
30
  {
31
31
  "path": "src/cli.js",
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "path": "templates/.claude/skills/feature-adr/SKILL.md",
116
- "sha256": "67eb730c815e6b8f9b9b61084c67ceb85440548f00b21f52187d16e81e3d7af8"
116
+ "sha256": "28c265603481f976a52ea4c627e11194f3c0283801ea93d02cbb9f4248b43c68"
117
117
  },
118
118
  {
119
119
  "path": "templates/.claude/skills/feature-adr/examples/sample-feature-output.md",
@@ -149,15 +149,15 @@
149
149
  },
150
150
  {
151
151
  "path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
152
- "sha256": "5c8d4c79d5329702b8c75b9afb36ae335aa63c99e74045c0afa98e12658ee975"
152
+ "sha256": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
153
153
  },
154
154
  {
155
155
  "path": "templates/.claude/skills/feature-adr/modules/07-code.md",
156
- "sha256": "fb054591e86d55cee184eab96b5f0440053ada8fa9edf619e4a66ec5ac371587"
156
+ "sha256": "db79f8d026cc47edd1e5d2f30d0e1455c55c570dd022d549d440985a3e5940a5"
157
157
  },
158
158
  {
159
159
  "path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
160
- "sha256": "5355e2b36ab13b6acb65abe5b206685915ef2b08191b4a3130574620012b9135"
160
+ "sha256": "d7a967926feec9b0ec173ac7042ae3abd368644d8e1dba9a81a770794417fcb3"
161
161
  },
162
162
  {
163
163
  "path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
@@ -247,9 +247,13 @@
247
247
  "path": "templates/.claude/skills/feature-adr/references/qe-checklist.md",
248
248
  "sha256": "238d8896996dc53559f58b24aa7eca966cc8845c3b9dda6d982c717c657b91e4"
249
249
  },
250
+ {
251
+ "path": "templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs",
252
+ "sha256": "19c3991bfb5e88205c9ff6d4d44037c136a190581bab199d3928196e4616fe15"
253
+ },
250
254
  {
251
255
  "path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
252
- "sha256": "eb77822f121ade10bd6265b0f0145ec38507a07d2cb72ba5fa8336c962096bda"
256
+ "sha256": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
253
257
  },
254
258
  {
255
259
  "path": "templates/.claude/skills/feature-adr/scripts/markdown-masker.mjs",
@@ -317,7 +321,7 @@
317
321
  },
318
322
  {
319
323
  "path": "templates/.claude/workflows/feature-adr.js",
320
- "sha256": "6d1bb93be6fae296281ac8f9fc2f266e5f09336cf6e183912fab5ff3bd0d3fd8"
324
+ "sha256": "7928cc90575eaef4f4626849f4490631676bc4ce6eb80e8f94c99d9e215bbd77"
321
325
  },
322
326
  {
323
327
  "path": "templates/lib/memory-protocol.md",
@@ -329,5 +333,5 @@
329
333
  }
330
334
  ]
331
335
  },
332
- "signature": "+DKyJg37w6aHqhg/UhCpQ+y+w/e66HEEr9hgOv/TttSIMc6TokGEbvJpxEw2pdu19Q0SHlA/o6pSucM+h3KEDw=="
336
+ "signature": "wSJpaUNRsFXAD5Ye7V5y8FPgcvrLDEq6peQpK/RadbFsD518KglZDn5GNzV4DJE6i+mkHbH9tIwr+HIYXKN4BA=="
333
337
  }
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @dzhechkov/skills-feature-adr
2
2
 
3
+ Current package version: `1.5.15`. <!-- dz:version -->
4
+
5
+ Site: https://aicoding.space · Source: https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/skills-feature-adr
6
+
3
7
  **Spec-Driven Development pipeline for AI coding agents (Claude Code, Codex, …)**
4
8
 
5
9
  An 11-step, complexity-routed pipeline that makes an AI coding agent build a feature the way a
@@ -37,6 +41,21 @@ npx @dzhechkov/skills-feature-adr init
37
41
 
38
42
  After installation, open Claude Code in your project directory and use `/feature-adr`.
39
43
 
44
+ Plain usage guidance joins the existing stage writer to `usage --by-stage --project` with explicit FA/
45
+ Wf source selection and observed receipt IDs. It preserves unknown splits/prices, caller estimates and
46
+ separate conservation/inventory/source verification. No billing inference, new ledger or paid replay.
47
+
48
+ Plain Step 8 bridge guidance now passes current `--round`, `--round-run` and `--task` from the existing
49
+ round receipt with the execution `--project`. Explicit conflicts refuse before reviewer work; the
50
+ bridge's invocation `runId` stays distinct from pipeline identity. Native Workflow QE remains its own
51
+ review path. Historical window correlation is disclosed as lower assurance, without guessed identity.
52
+
53
+ Step 7 uses the installed `scripts/build-coder-context.mjs` helper to include literal requirements,
54
+ plan tasks and ADR Decision/Confirmation. Workflow reads current inputs before code checkpoint
55
+ lookup; plain coding runs the same helper and reads or embeds its successful `promptBlock`.
56
+ Missing required sections, invalid files and exceeded UTF-8 bounds refuse coding instead of trimming
57
+ the context. Existing decision recall and code-wrapper routing remain in place.
58
+
40
59
  ---
41
60
 
42
61
  ## What You Get
@@ -1363,3 +1382,22 @@ harness-core's `src/markdown-masker.ts`. It runs without a core build. Amendment
1363
1382
  and K2 share the parser while retaining their existing unclosed-block and indentation policies.
1364
1383
  The four-space indented-code gap remains open for amendment checks and K2; swarm briefs retain their
1365
1384
  existing masking of indented code. Versions are unchanged in this staged change.
1385
+
1386
+ ### Codex companion for feature-adr
1387
+
1388
+ `dz statusline --watch --project "/path/to/worktree" --brain "/path/to/shared-brain"
1389
+ --slug "feature-slug" --run-id "stable-run-id"` adds an explicitly launched adjacent terminal
1390
+ companion, including Plain runs. Until installed, invoke the worktree-built
1391
+ `node packages/@dzhechkov/harness-cli/dist/bin.js statusline --watch ...`. This extends the existing
1392
+ command inventory. Canonical feature-adr guidance supplies the quoted producer/observer recipe: record
1393
+ a stable run ID and actual tier at the START of each step, then `done` on real completion; recall/teach
1394
+ remain scoped to the shared brain. Project run state and brain counts are separate. One slug retains
1395
+ one latest run; report freshness is not process liveness and stage position is not passed gates.
1396
+
1397
+ The readonly companion requires a dedicated stdout TTY, uses serial 2-second refreshes (0.25–60
1398
+ allowed), sanitizes/clips text and handles resize. Below 40x8 it shows a size warning. Ctrl-C/SIGTERM
1399
+ exit 0; output failure 1; invalid/piped watch 2. Failed/absent counts and optional values are explicit;
1400
+ watch v1 ETA is unavailable and global source inventory omitted. No stdin/raw mode, models, logical
1401
+ store writes or automatic terminal/settings changes. Normal SQLite ephemeral WAL/SHM sidecars are
1402
+ permitted. One-shot Claude text/JSON/ETA remain unchanged. Codex native footer capability is not
1403
+ asserted; parity names manual `dz statusline --watch` access.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.5.14",
3
+ "version": "1.5.15",
4
4
  "description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
5
5
  "bin": {
6
6
  "skills-feature-adr": "./bin/cli.js"
@@ -49,7 +49,7 @@
49
49
  "url": "git+https://github.com/djd1m/dz-harness.git",
50
50
  "directory": "packages/@dzhechkov/skills-feature-adr"
51
51
  },
52
- "homepage": "https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/skills-feature-adr#readme",
52
+ "homepage": "https://aicoding.space",
53
53
  "bugs": {
54
54
  "url": "https://github.com/djd1m/dz-harness/issues"
55
55
  },
package/sbom.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "8c96e82b41fb8479befb56797a367b3cc2119cc5b022b18fdd18f129ef8cd0f0"
38
+ "content": "66f3b2cf7fdcf9efbf64c7d5cc36b34ffc82b151a9bc07e6fda5fc47d4fba72e"
39
39
  }
40
40
  ]
41
41
  },
@@ -69,7 +69,7 @@
69
69
  },
70
70
  {
71
71
  "name": "dz:canonical-json-sha256-v2",
72
- "value": "0cdd048a5aacd52294f72b5903b5a94c049ad61680dbcac532d5cb3fbd45c736"
72
+ "value": "e74b1a22e0260a61a3ab37512366402c4e6231b85625028a86219ee3f5c21e12"
73
73
  }
74
74
  ]
75
75
  },
@@ -289,7 +289,7 @@
289
289
  "hashes": [
290
290
  {
291
291
  "alg": "SHA-256",
292
- "content": "67eb730c815e6b8f9b9b61084c67ceb85440548f00b21f52187d16e81e3d7af8"
292
+ "content": "28c265603481f976a52ea4c627e11194f3c0283801ea93d02cbb9f4248b43c68"
293
293
  }
294
294
  ]
295
295
  },
@@ -379,7 +379,7 @@
379
379
  "hashes": [
380
380
  {
381
381
  "alg": "SHA-256",
382
- "content": "5c8d4c79d5329702b8c75b9afb36ae335aa63c99e74045c0afa98e12658ee975"
382
+ "content": "d7a2a4e56b24451da234b1ac4ef440ba9b0370617e1c0cde8ad5f3d575a53209"
383
383
  }
384
384
  ]
385
385
  },
@@ -389,7 +389,7 @@
389
389
  "hashes": [
390
390
  {
391
391
  "alg": "SHA-256",
392
- "content": "fb054591e86d55cee184eab96b5f0440053ada8fa9edf619e4a66ec5ac371587"
392
+ "content": "db79f8d026cc47edd1e5d2f30d0e1455c55c570dd022d549d440985a3e5940a5"
393
393
  }
394
394
  ]
395
395
  },
@@ -399,7 +399,7 @@
399
399
  "hashes": [
400
400
  {
401
401
  "alg": "SHA-256",
402
- "content": "5355e2b36ab13b6acb65abe5b206685915ef2b08191b4a3130574620012b9135"
402
+ "content": "d7a967926feec9b0ec173ac7042ae3abd368644d8e1dba9a81a770794417fcb3"
403
403
  }
404
404
  ]
405
405
  },
@@ -623,13 +623,23 @@
623
623
  }
624
624
  ]
625
625
  },
626
+ {
627
+ "type": "file",
628
+ "name": "templates/.claude/skills/feature-adr/scripts/build-coder-context.mjs",
629
+ "hashes": [
630
+ {
631
+ "alg": "SHA-256",
632
+ "content": "19c3991bfb5e88205c9ff6d4d44037c136a190581bab199d3928196e4616fe15"
633
+ }
634
+ ]
635
+ },
626
636
  {
627
637
  "type": "file",
628
638
  "name": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
629
639
  "hashes": [
630
640
  {
631
641
  "alg": "SHA-256",
632
- "content": "eb77822f121ade10bd6265b0f0145ec38507a07d2cb72ba5fa8336c962096bda"
642
+ "content": "8b93949ce4f671d932c5389050db3a3e2750efcbed69681d69f392ccf4d2a168"
633
643
  }
634
644
  ]
635
645
  },
@@ -799,7 +809,7 @@
799
809
  "hashes": [
800
810
  {
801
811
  "alg": "SHA-256",
802
- "content": "6d1bb93be6fae296281ac8f9fc2f266e5f09336cf6e183912fab5ff3bd0d3fd8"
812
+ "content": "7928cc90575eaef4f4626849f4490631676bc4ce6eb80e8f94c99d9e215bbd77"
803
813
  }
804
814
  ]
805
815
  },
@@ -562,13 +562,91 @@ only the statusline:
562
562
 
563
563
  **Record the panel at the START of every step, not only at Steps 0/8/9.** The panel shows the last step
564
564
  that reported; a pipeline that reports three times per run shows a stale step for most of its life. Emit
565
- `dz statusline --fa-record --slug <slug> --step "<Step N Name>" --recalled <n> --stored <n>` as the first
565
+ `dz statusline --fa-record --project "<worktree>" --slug "<slug>" --run-id "<stable-run-id>" --tier M --step "<Step N Name>" --recalled <n> --stored <n>` as the first
566
566
  action of each step. The recall/teach counts only change at Steps 0/8/9; the *step label* changes at every
567
567
  one of them.
568
568
 
569
569
  *Honesty note:* the panel is live only insofar as the pipeline records state — it reflects what the
570
570
  pipeline actually did with the loop (recalls that ran, stores that landed), not an aspirational count.
571
571
 
572
+ ### Plain observed usage receipts
573
+
574
+ Use the existing witnessed writer for usage you actually observed. At a real stage boundary,
575
+ capture the execution project, stable run/task, verbatim stage, actual model/family/role, attempt,
576
+ tier/mode and source window/IDs available to this host. Missing fields remain null with their reason;
577
+ do not infer input/cache from total, price from a model family, or tokens from invocation budgets.
578
+
579
+ ```bash
580
+ dz feature-adr-record --kind ledger --stage "$CURRENT_STAGE" --project "$EXECUTION_PROJECT" \
581
+ --row "$OBSERVED_STAGE_ROW_JSON" --rollout-id "$ACTUAL_SESSION_ID" --turn-id "$ACTUAL_TURN_ID" --json
582
+ dz usage --by-stage --project "$EXECUTION_PROJECT" --source fa-ledger --run "$STABLE_RUN_ID" --json
583
+ ```
584
+
585
+ `OBSERVED_STAGE_ROW_JSON` is real host metadata: `runId`, `taskId`, `stage`, `model`, `family`, `role`,
586
+ `attempt`, `tier`, `mode`, `tokens`/dimensions when observed, and optional separate `estimate` with
587
+ tokens/costUsd/method/source/capturedAt. Omit selectors not known; window/cwd/model-only correlation
588
+ is labelled legacy-window and cannot claim exact source verification. Source roots can be named with
589
+ `--codex-sessions`. Exact IDs are validated against existing receipts; no new IDs or recall/teach occur.
590
+ The source receipt subset is captured once; later source append cannot enlarge the old row.
591
+ Captured payload integrity and every pricing-bearing dimension must match the original scoped source.
592
+ A reported monetary amount belongs to one observation, not each expanded token receipt. Preserve an
593
+ actual observation ID/scope/basis in optional `reportedCostObservation: { id, scope, basis }` within
594
+ `--row` when known; otherwise a captured source scope supplies a stable identity and unrelated money
595
+ attribution stays unavailable. Reimports of the same observation count once; conflicting amounts are
596
+ diagnosed. Missing money differs from zero. Exports must avoid every selected authoritative source,
597
+ including custom run directories and symlink aliases. Invalid Claude counters retain nulls/diagnostics;
598
+ a declared invalid total cannot derive a replacement, and accounting validity does not rewrite the
599
+ actual generation outcome.
600
+
601
+ For Wf, use `--source workflow-budget --run <id>` and optional `--run-dir <dir>` for its existing
602
+ budget/trace/state. Auto source collisions require an explicit source; joined Wf summary projections
603
+ never add another cost. Preserve reported total basis and cache/reasoning subsets. Wf budget.spent
604
+ counts dispatch units; native Workflow's existing budget delta is output-only. Neither is raw total.
605
+
606
+ Reports separate conservation, expected inventory and independent amount verification. Without
607
+ a same-scope witness, verified totals are null even when reported values conserve. Unknown rates or
608
+ split keep primary estimated USD null; static family estimates, provider-reported USD and caller
609
+ pre-run estimates are separate, never current exact prices or billed amounts. Billing remains unobserved.
610
+
611
+ ### Codex companion terminal (Plain included)
612
+
613
+ Open an adjacent terminal or a manual tmux split and launch the observer explicitly. Codex does not
614
+ have a dz native command-provider footer. Keep the producer's project, slug and stable run ID equal to
615
+ the observer's; use the actual complexity tier at the START of every active step. Brain is the shared
616
+ learning store for recall/teach, while project is the worktree containing run slots and branch.
617
+
618
+ ```bash
619
+ # Set these to your actual absolute paths. Use the worktree build until the change is installed.
620
+ PANEL_CLI="/path/to/worktree/packages/@dzhechkov/harness-cli/dist/bin.js"
621
+ PANEL_PROJECT="/path/to/worktree"
622
+ PANEL_BRAIN="/path/to/canonical-brain"
623
+ PANEL_SLUG="feature-slug"
624
+ PANEL_RUN="feature-20261002-1" # choose once per invocation, retain at every step
625
+ node "$PANEL_CLI" statusline --watch --project "$PANEL_PROJECT" --brain "$PANEL_BRAIN" \
626
+ --slug "$PANEL_SLUG" --run-id "$PANEL_RUN" --interval 2
627
+ # In the producer terminal, at the START of each real step (example: tier M):
628
+ node "$PANEL_CLI" statusline --fa-record --project "$PANEL_PROJECT" --slug "$PANEL_SLUG" \
629
+ --run-id "$PANEL_RUN" --tier M --step "Step 7 Code" --recalled 3 --stored 0
630
+ # Only after the run actually completes; this is not a claim that QE passed:
631
+ node "$PANEL_CLI" statusline --fa-record --project "$PANEL_PROJECT" --slug "$PANEL_SLUG" \
632
+ --run-id "$PANEL_RUN" --tier M --step "done" --recalled 3 --stored 0
633
+ ```
634
+
635
+ Use `dz recall ... --project "$PANEL_BRAIN"` and `dz teach ... --project "$PANEL_BRAIN"` for actual
636
+ learning. Supply measured cumulative counters, not the example numbers. The observer counts its brain
637
+ source directly; the slot's producer pool is not a shared-brain inventory. One slug holds one latest
638
+ run, so a replacement makes an exact old selection missing. No selectors means visibly automatic
639
+ selection. Freshness measures producer-report age, not process liveness: fresh <30 minutes, stale
640
+ 30–<90, expired >=90; completed remains completed. Stage position is not completed gates.
641
+
642
+ Watch v1 displays ETA unavailable, unknown optional values and unavailable failed/absent learning
643
+ sources, and omits ambiguous global source inventory. It uses escaped ASCII text, a dedicated stdout
644
+ TTY, 0.25–60 second intervals (default 2), and a size warning below 40 columns/8 rows. Ctrl-C/SIGTERM
645
+ exit 0; output failures exit 1; piped output and watch+JSON/install/record combinations exit 2.
646
+ Observation never reads stdin, runs models or writes logical store state. SQLite-managed ephemeral
647
+ WAL/SHM files are permitted; no application locks, repair or schema changes occur. One-shot Claude
648
+ statusline/JSON and its ETA keep their existing behavior. Do not start a terminal automatically.
649
+
572
650
  ### What changes with `--full-qe`
573
651
 
574
652
  Full agentic-qe protocols for the same 9 core skills. No new agents, just deeper methodology.
@@ -235,6 +235,16 @@ C2 recognises JS/TS, pytest, Go, Rust, JVM and .NET test paths, extensible per p
235
235
 
236
236
  Never proceed on a non-zero exit, and never treat empty output as a pass — the last line
237
237
  (`K2 plan-completeness: PASS|FAIL|NOT-ESTABLISHED`) is the verdict, and its absence is not one.
238
+
239
+ **Where an id counts (the default since 2026-09-27, owner decision).** C1 and C8 read the plan's
240
+ TASK LINES only: a heading, a list item or a table row. An `ADR-<n>` or `FR-<n>` that appears only
241
+ in a prose paragraph, a fenced code block (the SPARC-GOAP ```yaml goal state included), an HTML
242
+ comment, the `## Amendments` section or the `EXPECTED_CODE_TARGETS:` block is a mention, not a task,
243
+ and the gate FAILs it with `(cited only outside task lines)`. Write the id on the FIRST line of the
244
+ task that implements it: a wrapped continuation line of a list item is not read either. Measured on the archive when the default changed: 56 of 102 plans that had
245
+ passed would fail this reader, so a plan written before that date may be red on a re-check — move
246
+ the citation onto a task line, or re-check that one plan with `--no-require-task-lines` and say so
247
+ in the checkpoint banner.
238
248
  What it checks: C1 every ADR **decision** (a `# ADR-NNN` / `## ADR-NNN` heading INSIDE the file, not
239
249
  just the filename prefix — a file with several headings owes several plan citations) has a plan task
240
250
  citing it · C2 every ADR Confirmation test path is named in the plan · C3 the `EXPECTED_CODE_TARGETS:`
@@ -26,6 +26,35 @@ opus (complex code generation)
26
26
 
27
27
  ### 1. Pre-Implementation Checklist
28
28
 
29
+ ### Current literal context (plain and delegated coding)
30
+
31
+ Before coding or delegating, run the installed helper beside this module. Set `CONTEXT_HELPER` to
32
+ the absolute `scripts/build-coder-context.mjs` path of the skill installation you are reading;
33
+ set `FEATURE_DIR` to the absolute target `features/<slug>` directory and use the actual tier:
34
+
35
+ ```bash
36
+ node "$CONTEXT_HELPER" "$FEATURE_DIR" --tier=M
37
+ ```
38
+
39
+ The command emits exactly one JSON envelope and exits 0 only for `status: "complete"`. On any
40
+ nonzero exit, unavailable/incomplete status, malformed JSON or required missing/empty section,
41
+ stop before coding and repair the named input. Never paste a partial result as complete. A missing
42
+ helper requires restoring this skill installation, not inventing a replacement block.
43
+
44
+ For every delegated coder assignment, paste the successful envelope's literal `promptBlock` into
45
+ the actual prompt, then append the existing advisory decision-recall block once. Keep the source
46
+ paths below for deeper reading. For single-agent in-session coding, read this generated block
47
+ directly before implementing. Requirements and plan are included in full; each ADR supplies its
48
+ Decision and Confirmation with source labels. M/L/XL require at least one ADR; S can have none.
49
+
50
+ The same canonical helper is used by the programmatic Workflow before code checkpoint lookup.
51
+ It fingerprints full current inputs and binds the prompt separately, so changed documents cannot
52
+ reuse old code. The plain mode boundary is an executable helper command plus these required
53
+ read/embedding instructions; there is no separately automated plain dispatcher. Pure/fixture tests
54
+ do not establish a live model relay's authenticity. Helper bounds are 64 documents, 256 KiB/file,
55
+ 1 MiB read and 96 KiB UTF-8 for the entire labelled block; exceeded bounds refuse without trimming.
56
+ These are document limits, not a new limit on the existing coder wrapper's final prompt.
57
+
29
58
  Before writing any code:
30
59
  - [ ] Read existing similar implementations in codebase
31
60
  - [ ] Identify naming conventions (files, classes, functions, variables)
@@ -23,6 +23,30 @@ sonnet (analytical evaluation)
23
23
 
24
24
  ## Protocol
25
25
 
26
+ ### Bridge identity for plain Step 8
27
+
28
+ When the plain host invokes the existing Claude review bridge, take round number, pipeline run and
29
+ task from the current round receipt/state in the execution project. Pass the fields actually present:
30
+
31
+ ```bash
32
+ dz qe-bridge --family claude --slug "$FEATURE_SLUG" --project "$EXECUTION_PROJECT" \
33
+ --round "$CURRENT_ROUND" --round-run "$CURRENT_ROUND_RUN" --task "$CURRENT_TASK" \
34
+ --coder-family codex
35
+ ```
36
+
37
+ Do not mint missing identifiers or point `--project` at a separate learning brain. Explicit identity
38
+ must agree with one readable open round before any probe or review child; conflicts refuse with exit 2.
39
+ The bridge freezes that snapshot for the signoff. Signoff `roundRun` is the pipeline run; its existing
40
+ `runId` identifies the separate bridge invocation and audit filenames. No-flags standalone reviews
41
+ retain best-effort lookup and visibly unbound provenance when authority is absent or unavailable.
42
+
43
+ Round close validates every present identity field before selection. Full, partial and genuinely
44
+ identity-free window assurance appear as `round-run-task`, `partial-identity` and `legacy-window`.
45
+ Malformed or foreign claims cannot become a manual-close fallback. This contract governs the
46
+ existing plain bridge and control-review caller; native Workflow QE already runs its own review and
47
+ must not invoke a second bridge to manufacture this receipt. Local fake-child tests do not establish
48
+ a live Claude model roundtrip or secure every native Codex QE receipt.
49
+
26
50
  ### 1. Smoke Tests (All tiers)
27
51
 
28
52
  ```
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // One parser for workflow and plain Step 7. Importing this module performs no IO.
3
+ import { createHash } from 'node:crypto';
4
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync } from 'node:fs';
5
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const SCHEMA = 'fa-coder-context-1';
9
+ const MAX_DOCUMENTS = 64;
10
+ const MAX_FILE_BYTES = 256 * 1024;
11
+ const MAX_TOTAL_BYTES = 1024 * 1024;
12
+ const MAX_PROMPT_BYTES = 96 * 1024;
13
+ const sha = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
14
+ const kindOf = (path) => path === '01_requirements.md' ? 'requirements'
15
+ : path === '06_implementation_plan.md' ? 'plan'
16
+ : /^03_adr\/[0-9]{3}-[^/\\\x00-\x1f]+\.md$/.test(path) ? 'adr' : null;
17
+ const diagnostic = (source, reason, section) => ({ source, reason, severity: 'error', ...(section ? { section } : {}) });
18
+
19
+ // One lexical pass recognizes comments only outside Markdown code/escapes, retaining offsets.
20
+ function markdown(text) {
21
+ const headings = [];
22
+ const visible = [];
23
+ let fence = null;
24
+ let comment = false;
25
+ let inlineEnd = -1;
26
+ let offset = 0;
27
+ for (const line of text.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
28
+ const clean = line.replace(/\r?\n$/, '').replace(/^\uFEFF/, '');
29
+ const delimiter = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(clean);
30
+ if (fence) {
31
+ if (delimiter && delimiter[1][0] === fence.char && delimiter[1].length >= fence.length && delimiter[2].trim() === '') fence = null;
32
+ else visible.push(line);
33
+ } else if (!comment && inlineEnd <= offset && delimiter && !(delimiter[1][0] === '`' && delimiter[2].includes('`'))) {
34
+ fence = { char: delimiter[1][0], length: delimiter[1].length };
35
+ } else {
36
+ const wasComment = comment;
37
+ let masked = '';
38
+ for (let i = 0; i < line.length;) {
39
+ if (comment) {
40
+ if (line.startsWith('-->', i)) { masked += ' '; i += 3; comment = false; }
41
+ else { masked += /[\r\n]/.test(line[i]) ? line[i] : ' '; i++; }
42
+ continue;
43
+ }
44
+ if (offset + i < inlineEnd) { masked += line[i++]; continue; }
45
+ // An escaped '<' cannot start a comment; paired backslashes still allow the next opener.
46
+ if (line[i] === '\\' && i + 1 < line.length && /[!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~]/.test(line[i + 1])) {
47
+ masked += line.slice(i, i + 2); i += 2; continue;
48
+ }
49
+ if (line[i] === '`') {
50
+ const run = /^`+/.exec(line.slice(i))[0];
51
+ const rest = text.slice(offset + i + run.length);
52
+ // Code spans can wrap within a paragraph, but cannot hide a subsequent block heading/fence.
53
+ const boundary = /\n[ \t]*(?:\r?\n|#{1,6}[ \t]|`{3,}|~{3,})/.exec(rest);
54
+ const paragraph = boundary ? rest.slice(0, boundary.index) : rest;
55
+ const close = [...paragraph.matchAll(/`+/g)].find((match) => match[0].length === run.length);
56
+ if (close) inlineEnd = offset + i + run.length + close.index + run.length;
57
+ masked += run; i += run.length; continue;
58
+ }
59
+ if (line.startsWith('<!--', i)) { comment = true; masked += ' '; i += 4; continue; }
60
+ masked += line[i++];
61
+ }
62
+ const heading = !wasComment && /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/.exec(masked.replace(/\r?\n$/, '').replace(/^\uFEFF/, ''));
63
+ if (heading) headings.push({ depth: heading[1].length, title: heading[2].replace(/[ \t]+#+[ \t]*$/, '').trim(), start: offset, body: offset + line.length });
64
+ else visible.push(masked);
65
+ }
66
+ offset += line.length;
67
+ }
68
+ return { headings, unclosed: fence !== null, unclosedComment: comment, substantive: visible.join('').trim() !== '' };
69
+ }
70
+
71
+ /** Pure: consumes already-read documents only, and hashes the same snapshot used for the block. */
72
+ export function buildCoderContext({ tier, documents }) {
73
+ const diagnostics = [];
74
+ const sources = [];
75
+ const blocks = [];
76
+ let bounded = true;
77
+ if (!['S', 'M', 'L', 'XL'].includes(tier)) diagnostics.push(diagnostic('tier', 'invalid-tier'));
78
+ if (!Array.isArray(documents)) documents = [];
79
+ if (documents.length > MAX_DOCUMENTS) { diagnostics.push(diagnostic('documents', 'document-limit')); bounded = false; }
80
+ const sorted = [...documents].sort((a, b) => String(a?.path).localeCompare(String(b?.path), 'en'));
81
+ const seen = new Set();
82
+ let total = 0;
83
+ for (const doc of sorted.slice(0, MAX_DOCUMENTS)) {
84
+ const path = doc?.path;
85
+ const kind = typeof path === 'string' && path.length <= 512 ? kindOf(path) : null;
86
+ if (!kind || typeof doc.text !== 'string') { diagnostics.push(diagnostic(String(path ?? 'documents'), 'invalid-source')); bounded = false; continue; }
87
+ if (seen.has(path)) { diagnostics.push(diagnostic(path, 'duplicate-source')); bounded = false; continue; }
88
+ seen.add(path);
89
+ const bytes = Buffer.byteLength(doc.text, 'utf8'); total += bytes;
90
+ const status = bytes > MAX_FILE_BYTES ? 'oversized' : 'read';
91
+ sources.push({ path, kind, bytes, digest: status === 'read' ? sha(doc.text) : null, status });
92
+ if (status !== 'read') { diagnostics.push(diagnostic(path, 'file-limit')); bounded = false; continue; }
93
+ const parsed = markdown(doc.text);
94
+ if (parsed.unclosed) diagnostics.push(diagnostic(path, 'unclosed-fence'));
95
+ if (parsed.unclosedComment) diagnostics.push(diagnostic(path, 'unclosed-comment'));
96
+ if (kind !== 'adr') {
97
+ if (!parsed.substantive) diagnostics.push(diagnostic(path, 'empty-section', 'body'));
98
+ blocks.push('\n\n### Source: ' + path + ' — body\n' + doc.text);
99
+ continue;
100
+ }
101
+ for (const section of ['Decision', 'Confirmation']) {
102
+ const matches = parsed.headings.filter((h) => h.depth >= 2 && h.title.toLowerCase() === section.toLowerCase());
103
+ if (matches.length !== 1) { diagnostics.push(diagnostic(path, matches.length ? 'duplicate-section' : 'missing-section', section)); continue; }
104
+ const heading = matches[0];
105
+ const next = parsed.headings.find((h) => h.start > heading.start && h.depth <= heading.depth);
106
+ const body = doc.text.slice(heading.body, next ? next.start : doc.text.length);
107
+ if (!markdown(body).substantive) diagnostics.push(diagnostic(path, 'empty-section', section));
108
+ blocks.push('\n\n### Source: ' + path + ' — ' + section + '\n' + body);
109
+ }
110
+ }
111
+ if (total > MAX_TOTAL_BYTES) { diagnostics.push(diagnostic('documents', 'aggregate-limit')); bounded = false; }
112
+ for (const path of ['01_requirements.md', '06_implementation_plan.md']) {
113
+ if (!seen.has(path)) {
114
+ sources.push({ path, kind: kindOf(path), bytes: 0, digest: null, status: 'missing' });
115
+ diagnostics.push(diagnostic(path, 'missing-source'));
116
+ }
117
+ }
118
+ if (tier !== 'S' && !sources.some((s) => s.kind === 'adr')) diagnostics.push(diagnostic('03_adr', 'missing-source'));
119
+ let promptBlock = '\n\n## Current coder context (literal source snapshot)' + blocks.join('');
120
+ if (tier === 'S' && !sources.some((s) => s.kind === 'adr')) promptBlock += '\n\nADR sources: none selected (tier S).';
121
+ if (Buffer.byteLength(promptBlock, 'utf8') > MAX_PROMPT_BYTES) {
122
+ diagnostics.push(diagnostic('promptBlock', 'prompt-limit')); promptBlock = '';
123
+ }
124
+ return { schema: SCHEMA, status: diagnostics.length ? 'incomplete' : 'complete', promptBlock,
125
+ digest: bounded ? sha(JSON.stringify([SCHEMA, tier, sorted.map((d) => [d.path, d.text])])) : null,
126
+ sources: sources.sort((a, b) => a.path.localeCompare(b.path, 'en')), diagnostics };
127
+ }
128
+
129
+ function hostContext(feature, tier) {
130
+ const failures = [];
131
+ const documents = [];
132
+ const failedSources = [];
133
+ let root;
134
+ const fail = (path, reason, status = 'unreadable', bytes = 0) => {
135
+ failures.push(diagnostic(path, reason));
136
+ failedSources.push({ path, kind: kindOf(path) ?? 'adr', bytes, digest: null, status });
137
+ };
138
+ try {
139
+ if (lstatSync(feature).isSymbolicLink() || !lstatSync(feature).isDirectory()) throw new Error('invalid-root');
140
+ root = realpathSync(feature);
141
+ } catch { return { schema: SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic(feature, 'invalid-root')] }; }
142
+ let paths = ['01_requirements.md', '06_implementation_plan.md'];
143
+ const adrDir = join(root, '03_adr');
144
+ try {
145
+ const stat = lstatSync(adrDir);
146
+ if (stat.isSymbolicLink() || !stat.isDirectory()) fail('03_adr', 'invalid-file', 'invalid');
147
+ else paths.push(...readdirSync(adrDir).filter((name) => /^[0-9]{3}-.+\.md$/.test(name)).sort().map((name) => '03_adr/' + name));
148
+ } catch (error) { if (error.code !== 'ENOENT') fail('03_adr', 'read-failure'); }
149
+ if (paths.length > MAX_DOCUMENTS) return { schema: SCHEMA, status: 'incomplete', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic('documents', 'document-limit')] };
150
+ let total = 0;
151
+ for (const path of paths) {
152
+ let fd;
153
+ try {
154
+ const file = join(root, path);
155
+ const stat = lstatSync(file);
156
+ if (stat.isSymbolicLink() || !stat.isFile()) { fail(path, 'invalid-file', 'invalid'); continue; }
157
+ const actual = realpathSync(file);
158
+ const rel = relative(root, actual);
159
+ if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) { fail(path, 'outside-root', 'invalid'); continue; }
160
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
161
+ const opened = fstatSync(fd);
162
+ // Compare the opened descriptor with the contained file checked before open.
163
+ if (!opened.isFile() || opened.ino !== stat.ino || opened.dev !== stat.dev || realpathSync(file) !== actual) { fail(path, 'invalid-file', 'invalid'); continue; }
164
+ const chunks = []; let bytes = 0;
165
+ while (true) {
166
+ const chunk = Buffer.alloc(Math.min(16384, MAX_FILE_BYTES + 1 - bytes));
167
+ const count = readSync(fd, chunk, 0, chunk.length, null);
168
+ if (count === 0) break;
169
+ bytes += count; total += count; chunks.push(chunk.subarray(0, count));
170
+ if (bytes > MAX_FILE_BYTES || total > MAX_TOTAL_BYTES) break;
171
+ }
172
+ if (bytes > MAX_FILE_BYTES) { fail(path, 'file-limit', 'oversized', bytes); continue; }
173
+ if (total > MAX_TOTAL_BYTES) { fail(path, 'aggregate-limit', 'oversized', bytes); break; }
174
+ try { documents.push({ path, text: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks)) }); }
175
+ catch { fail(path, 'invalid-utf8', 'invalid', bytes); }
176
+ } catch (error) { fail(path, error.code === 'ENOENT' ? 'missing-source' : 'read-failure', error.code === 'ENOENT' ? 'missing' : 'unreadable'); }
177
+ finally { if (fd !== undefined) closeSync(fd); }
178
+ }
179
+ const result = buildCoderContext({ tier, documents });
180
+ if (failures.length) {
181
+ const failed = new Set(failedSources.map((s) => s.path));
182
+ result.sources = result.sources.filter((s) => !failed.has(s.path)).concat(failedSources).sort((a, b) => a.path.localeCompare(b.path, 'en'));
183
+ result.diagnostics = result.diagnostics.filter((d) => !(failed.has(d.source) && d.reason === 'missing-source')).concat(failures);
184
+ result.status = failures.every((d) => ['file-limit', 'aggregate-limit'].includes(d.reason)) ? 'incomplete' : 'unavailable';
185
+ result.digest = null;
186
+ }
187
+ return result;
188
+ }
189
+
190
+ function main(argv) {
191
+ const feature = argv[0];
192
+ const tier = argv.find((arg) => arg.startsWith('--tier='))?.slice(7);
193
+ let result;
194
+ if (!feature || !['S', 'M', 'L', 'XL'].includes(tier) || argv.some((arg, i) => i > 0 && arg !== '--tier=' + tier)) {
195
+ result = { schema: SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [diagnostic('arguments', 'invalid-arguments')] };
196
+ } else result = hostContext(resolve(feature), tier);
197
+ console.log(JSON.stringify(result));
198
+ return result.status === 'complete' ? 0 : 1;
199
+ }
200
+
201
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) process.exitCode = main(process.argv.slice(2));
@@ -3,7 +3,7 @@
3
3
  // Generalized from features/wave1-instrument-repair/check-plan-completeness.mjs (that copy is the
4
4
  // historical artifact of its run and stays untouched); this one is parameterized by feature dir.
5
5
  //
6
- // USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2] [--require-requirements]
6
+ // USAGE: node .claude/skills/feature-adr/scripts/check-plan-completeness.mjs [<feature-dir>] [--tier=M] [--acid=A1,A2] [--require-requirements] [--no-require-task-lines]
7
7
  // <feature-dir> defaults to the current working directory.
8
8
  // --tier=S|M|L|XL closes the ADR-less dodge (see S-TIER HONESTY); omitting it keeps the
9
9
  // heuristic, and the skip note then names the dodge out loud.
@@ -11,6 +11,12 @@
11
11
  // pipeline passes this flag, so the check is introduced two-shot (ADR-001 plan-inherits-
12
12
  // requirements): WARN first so the corpus can be measured without repainting every green
13
13
  // fixture red, FAIL once the planner prompt names the contract (feature-adr.js Step 6).
14
+ // C1 and C8 read the plan's TASK LINES by DEFAULT (C10 below; owner decision 2026-09-27,
15
+ // backlog 95565da0): an id cited only in prose, fenced code, an HTML comment, `## Amendments`
16
+ // or EXPECTED_CODE_TARGETS is not a task. --no-require-task-lines is the explicit way back
17
+ // to the raw-plan reader, where C10 only WARNs — for re-checking a plan written before the
18
+ // default changed. --require-task-lines is still accepted and means the default; passing
19
+ // BOTH is a contradiction and is refused (NOT-ESTABLISHED), never resolved silently.
14
20
  //
15
21
  // VERDICT CONTRACT (unchanged from the proven copy — never a silent pass):
16
22
  // PASS exit 0 last line: `K2 plan-completeness: PASS (...)`
@@ -20,16 +26,22 @@
20
26
  // ONE regex parses all three verdicts — the exit codes and their meanings are identical.)
21
27
  // Set difference over IDENTIFIERS, not text similarity.
22
28
  //
23
- // KNOWN LIMITATION (measured on the discrimination twin, 2026-08-19): C1 is a grep — a PROSE
24
- // mention of "ADR-002" satisfies it exactly like a task reference. The real N14 (backlog
25
- // 3dbd2851-adjacent) must parse task structure. Kept honest here: C1 catches "forgot entirely",
26
- // not "mentioned but not tasked".
27
- //
28
- // KNOWN LIMITATION (fix round 1, 2026-09-16, same class as C1 above): C8 is also a grep — a PROSE
29
- // mention of "FR-3" satisfies it exactly like a task reference. Kept honest here too: C8 catches
30
- // "forgot entirely", not "mentioned but not tasked". Masking the PLAN side (not just the 01/ADR
31
- // side) for C1 and C8 together, so a prose mention stops satisfying either check, is a separate
32
- // backlog item — filed by the lead, not chased here.
29
+ // C1 AND C8 SHARE ONE PLAN-SIDE READER (k2-reads-masked-plan, backlog c03903eb, 2026-09-27).
30
+ // Until then both were a grep over the RAW plan: an id in prose, a fenced code block, an HTML
31
+ // comment, the `## Amendments` section or the EXPECTED_CODE_TARGETS block counted as a task
32
+ // (named here as a limit since 2026-08-19 for C1 and 2026-09-16 for C8). `planTaskText()` now
33
+ // answers "which plan text is a task" ONCE, for both checks — so one phrase can never pass one
34
+ // check and fail the other. Introduced WARN-first (C10), because the corpus said so; the FAIL
35
+ // half became the DEFAULT on 2026-09-27 by owner decision, with this measurement on the table:
36
+ // MEASURED 2026-09-27 over all 472 features/*/06_implementation_plan.md, pipeline mode
37
+ // (--require-requirements, tier read from 00): of 101 green plans, the task-line reader turns 55
38
+ // red (an earlier snapshot of the same day; the count the owner decided on, taken later over 473
39
+ // plans, was 56 of 102), and even mask-only (prose kept) turns 11 red — the SPARC-GOAP ```yaml goal-state block and
40
+ // «ADR-001 governs every task» prose are the corpus's own idioms, not forgeries. So a plan that
41
+ // was green before 2026-09-27 may be red on a re-check: that is the decision, not a regression —
42
+ // cite the id on a task line, or re-check that plan with --no-require-task-lines and say so.
43
+ // NAMED LIMIT: `planClaimsAdrWork` (S-tier honesty) and C2 still read the raw plan — a CLAIM of
44
+ // ADR work in prose is still a claim, and C2 names test paths, not ids.
33
45
  //
34
46
  // KNOWN LIMITATION (C9): only byte-identical copies are visible before the edit. Already drifted
35
47
  // or intentionally different pinned copies remain the identity tests' job; the frozen
@@ -96,6 +108,8 @@ const tierArg = argv.find((a) => a.startsWith('--tier='));
96
108
  const TIER = tierArg ? tierArg.slice('--tier='.length).trim().toUpperCase() : null;
97
109
  const TIER_REQUIRES_ADR = TIER === 'M' || TIER === 'L' || TIER === 'XL';
98
110
  const REQUIRE_REQUIREMENTS = argv.includes('--require-requirements');
111
+ const NO_REQUIRE_TASK_LINES = argv.includes('--no-require-task-lines');
112
+ const REQUIRE_TASK_LINES = !NO_REQUIRE_TASK_LINES;
99
113
  const dirArg = argv.find((a) => !a.startsWith('--'));
100
114
  const FDIR = resolve(dirArg && dirArg !== '' ? (isAbsolute(dirArg) ? dirArg : join(process.cwd(), dirArg)) : process.cwd());
101
115
  const planPath = join(FDIR, '06_implementation_plan.md');
@@ -113,6 +127,7 @@ const safe = (v) => String(v)
113
127
  .slice(0, 300);
114
128
  const out = (s) => console.log(s);
115
129
  const notEstablished = (why) => { out(`K2 plan-completeness: NOT-ESTABLISHED — ${safe(why)}`); process.exit(3); };
130
+ if (NO_REQUIRE_TASK_LINES && argv.includes('--require-task-lines')) notEstablished('both --require-task-lines and --no-require-task-lines were passed — pick one reader');
116
131
  let failures = [], warnings = [], skips = [];
117
132
 
118
133
  // Lead delta after Codex rounds 2+3 (2026-09-16, plan-inherits-requirements): on the DECLARATION side
@@ -136,6 +151,54 @@ if (!existsSync(FDIR)) notEstablished(`feature dir absent: ${FDIR}`);
136
151
  if (!existsSync(planPath)) notEstablished('06_implementation_plan.md absent');
137
152
  const plan = readFileSync(planPath, 'utf-8');
138
153
  if (plan.trim().length < 200) notEstablished('plan suspiciously small (<200 chars)');
154
+ // The EXPECTED_CODE_TARGETS block as C3 reads it (C3 below reuses THIS match — one reader).
155
+ const blockM = plan.match(/EXPECTED_CODE_TARGETS:\s*\n((?:\s*[-*]\s*.+\n?)+)/);
156
+
157
+ // `## Amendments` as C6 reads it: up to three leading spaces, two to four hashes, trailing text
158
+ // allowed; the section ends at the next level-1..4 heading. Returned as LINE indices over masked
159
+ // lines — [headingLine, endLine). One definition for C6 and planTaskText, or the two drift.
160
+ function findAmendmentsSection(maskedLines) {
161
+ let head = -1, end = -1;
162
+ for (let i = 0; i < maskedLines.length; i++) {
163
+ const pl = maskedLines[i];
164
+ if (head < 0 && /^ {0,3}#{2,4}\s+Amendments\b/.test(pl)) head = i;
165
+ else if (head >= 0 && end < 0 && /^ {0,3}#{1,4}\s/.test(pl)) end = i;
166
+ }
167
+ if (head >= 0 && end < 0) end = maskedLines.length;
168
+ return { head, end };
169
+ }
170
+
171
+ // C10 — the TASK LINES of the plan, the text C1/C8 cite against (the default reader).
172
+ // Masked first (fences and HTML comments blanked, unclosed fence hidden — the K2 reader policy),
173
+ // then: drop the `## Amendments` section and the EXPECTED_CODE_TARGETS block, and keep only
174
+ // headings, list items and table rows. Table rows are kept deliberately, beyond the backlog's
175
+ // «headings/list items»: task tables (`| T1 | … |`) are a corpus form — MEASURED 2026-09-27,
176
+ // dropping them turns 12 more green plans red in pipeline mode (67 vs 55) and 26 more in WARN mode.
177
+ // NAMED LIMIT: a list item or table row that is itself prose («- Note: ADR-001 governs …») still
178
+ // counts — the reader tells structure from prose, not a task from a remark.
179
+ function planTaskText(planText) {
180
+ const lines = maskMarkdown(planText, { unclosed: 'mask' }).split('\n');
181
+ const am = findAmendmentsSection(lines);
182
+ let tFrom = -1, tTo = -1;
183
+ if (blockM) {
184
+ tFrom = planText.slice(0, blockM.index).split('\n').length - 1;
185
+ tTo = planText.slice(0, blockM.index + blockM[0].length - 1).split('\n').length - 1;
186
+ }
187
+ const keep = [];
188
+ for (let i = 0; i < lines.length; i++) {
189
+ if (am.head >= 0 && i >= am.head && i < am.end) continue;
190
+ if (tFrom >= 0 && i >= tFrom && i <= tTo) continue;
191
+ const l = lines[i];
192
+ if (/^ {0,3}#{1,6}\s/.test(l) || /^\s*(?:[-*+]|\d+[.)])\s+/.test(l) || /^\s*\|/.test(l)) keep.push(l);
193
+ }
194
+ return keep.join('\n');
195
+ }
196
+ const planTasks = planTaskText(plan);
197
+ // What C1/C8 cite against, and the ids that pass the raw read but have no task line (C10).
198
+ const citeText = REQUIRE_TASK_LINES ? planTasks : plan;
199
+ const outsideTasksOnly = { C1: [], C8: [] };
200
+ const citedOutsideOnly = (re) => re.test(plan) && !re.test(planTasks);
201
+
139
202
  const adrFiles = existsSync(adrDir) ? readdirSync(adrDir).filter(f => f.endsWith('.md')).sort() : [];
140
203
  const planClaimsAdrWork = /\bADR-\d+/.test(plan);
141
204
  if (adrFiles.length === 0 && planClaimsAdrWork) notEstablished('no ADR files under 03_adr/, yet the plan cites ADR-<n> — completeness cannot be established');
@@ -243,7 +306,8 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
243
306
  for (const n of headingNums) {
244
307
  const id = `ADR-${n}`;
245
308
  const re = new RegExp(`ADR-0*${Number(n)}\\b`);
246
- if (!re.test(plan)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it`);
309
+ if (!re.test(citeText)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it${citedOutsideOnly(re) ? ' (cited only outside task lines)' : ''}`);
310
+ else if (citedOutsideOnly(re)) outsideTasksOnly.C1.push(id);
247
311
  }
248
312
  continue;
249
313
  }
@@ -251,7 +315,8 @@ if (adrFiles.length === 0 && TIER_REQUIRES_ADR) {
251
315
  warnings.push(`C1: ${safe(f)} has no ADR-NNN heading — falling back to the filename prefix`);
252
316
  const id = `ADR-${m[1]}`;
253
317
  const re = new RegExp(`ADR-0*${Number(m[1])}\\b`);
254
- if (!re.test(plan)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it`);
318
+ if (!re.test(citeText)) failures.push(`C1: ${id} (${safe(f)}) has NO task in the plan referencing it${citedOutsideOnly(re) ? ' (cited only outside task lines)' : ''}`);
319
+ else if (citedOutsideOnly(re)) outsideTasksOnly.C1.push(id);
255
320
  }
256
321
 
257
322
  // C2 — every Confirmation-listed test file path appears in the plan
@@ -342,7 +407,6 @@ function classifyTargetPath(path) {
342
407
  }
343
408
 
344
409
  // C3 — EXPECTED_CODE_TARGETS block, line-level validation
345
- const blockM = plan.match(/EXPECTED_CODE_TARGETS:\s*\n((?:\s*[-*]\s*.+\n?)+)/);
346
410
  const listedTargets = new Set();
347
411
  if (!blockM) failures.push('C3: no EXPECTED_CODE_TARGETS: block in the plan');
348
412
  else {
@@ -477,16 +541,14 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
477
541
  // phantom amendment or hand a real testless one someone else's marker. Third fence-blindness
478
542
  // found in a checker today, so it is closed here by construction rather than by care.
479
543
  const planLines = maskMarkdown(plan, { unclosed: 'mask' }).split('\n');
480
- let sectionStart = -1, sectionEnd = -1, cursor = 0;
481
- for (const pl of planLines) {
482
- // The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
483
- // hashes, and trailing text allowed. C6 required exactly `##` with nothing after, so the two
484
- // tools disagreed about where the section even IS — the divergence this feature exists to end.
485
- if (sectionStart < 0 && /^ {0,3}#{2,4}\s+Amendments\b/.test(pl)) sectionStart = cursor + pl.length + 1;
486
- else if (sectionStart >= 0 && sectionEnd < 0 && /^ {0,3}#{1,4}\s/.test(pl)) sectionEnd = cursor;
487
- cursor += pl.length + 1;
488
- }
489
- if (sectionStart >= 0 && sectionEnd < 0) sectionEnd = plan.length;
544
+ // The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
545
+ // hashes, and trailing text allowed. C6 required exactly `##` with nothing after, so the two
546
+ // tools disagreed about where the section even IS — the divergence this feature exists to end.
547
+ // The section boundary itself is `findAmendmentsSection` — shared with C10, one answer.
548
+ const amRange = findAmendmentsSection(planLines);
549
+ const lineOffset = (idx) => planLines.slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0);
550
+ const sectionStart = amRange.head < 0 ? -1 : lineOffset(amRange.head + 1);
551
+ const sectionEnd = amRange.head < 0 ? -1 : (amRange.end < planLines.length ? lineOffset(amRange.end) : plan.length);
490
552
  // Sliced from the MASKED text so the offsets computed above line up with what is scanned.
491
553
  const maskedPlan = planLines.join('\n');
492
554
  const amSection = sectionStart >= 0 ? maskedPlan.slice(sectionStart, sectionEnd) : '';
@@ -623,7 +685,9 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
623
685
  if (reqIds.length === 0) {
624
686
  warnings.push('C8: 01_requirements.md declares NO requirement ids in the contract shapes (FR-N / NFR-N / AC-N / C-N at line start) — nothing to cover');
625
687
  } else {
626
- const missing = reqIds.filter((id) => !new RegExp('\\b' + escapeReqId(id) + '\\b').test(plan));
688
+ const reqRe = (id) => new RegExp('\\b' + escapeReqId(id) + '\\b');
689
+ const missing = reqIds.filter((id) => !reqRe(id).test(citeText));
690
+ for (const id of reqIds) if (!missing.includes(id) && citedOutsideOnly(reqRe(id))) outsideTasksOnly.C8.push(id);
627
691
  if (missing.length === 0) {
628
692
  // Silence is not a verdict (K7): a check that ran and found nothing wrong must still print,
629
693
  // or a reader cannot tell "C8 ran clean" from "C8 never ran". Deliberately NOT a PASS/FAIL/
@@ -631,7 +695,7 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
631
695
  // mid-stream is exactly the G-F1 forgery class this script's `safe()` already defends against.
632
696
  out(`NOTE C8: all ${reqIds.length} requirement ids referenced`);
633
697
  } else if (REQUIRE_REQUIREMENTS) {
634
- for (const id of missing) failures.push(`C8: ${id} (01_requirements.md) is not referenced by the plan`);
698
+ for (const id of missing) failures.push(`C8: ${id} (01_requirements.md) is not referenced by the plan${citedOutsideOnly(reqRe(id)) ? ' (cited only outside task lines)' : ''}`);
635
699
  } else {
636
700
  const shown = missing.slice(0, 12);
637
701
  const more = missing.length > 12 ? `, …and ${missing.length - 12} more` : '';
@@ -641,6 +705,23 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
641
705
  }
642
706
  }
643
707
 
708
+ // C10 — ids that C1/C8 counted as covered, but whose ONLY citations sit outside task lines
709
+ // (prose, fenced code, an HTML comment, `## Amendments`, the EXPECTED_CODE_TARGETS block). Reached
710
+ // ONLY under --no-require-task-lines: by default C1/C8 already read task lines, so this list is
711
+ // empty by construction. One line per check, ids capped like C8's WARN.
712
+ for (const check of ['C1', 'C8']) {
713
+ const ids = [...new Set(outsideTasksOnly[check])];
714
+ if (ids.length === 0) continue;
715
+ const shown = ids.slice(0, 12);
716
+ const more = ids.length > 12 ? `, …and ${ids.length - 12} more` : '';
717
+ // Say what dropping the opt-out would do in THIS invocation (Codex astra r1 MINOR): C1 always
718
+ // FAILs by default; C8 FAILs only when --require-requirements is also passed — otherwise a WARN.
719
+ const consequence = check === 'C8' && !REQUIRE_REQUIREMENTS
720
+ ? 'without --no-require-task-lines this stays a C8 WARN (C8 is enforced only with --require-requirements; add that flag and the default reader fails it)'
721
+ : `these ids count as covered only because of --no-require-task-lines; the default makes this a ${check} FAIL`;
722
+ warnings.push(`C10: ${ids.length} ${check} id(s) are cited only outside task lines (prose, fenced code, HTML comment, ## Amendments, EXPECTED_CODE_TARGETS): ${shown.join(', ')}${more} — a mention is not a task; ${consequence}`);
723
+ }
724
+
644
725
  // C5 — Inputs read line
645
726
  if (!/Inputs read:/i.test(plan)) warnings.push('C5: no "Inputs read:" line (wave-2 seam, WARN only)');
646
727
  else for (const need of ['03_adr','05_architecture']) if (!plan.includes(need)) warnings.push(`C5: Inputs read line missing ${need}`);
@@ -3521,8 +3521,80 @@ function codeLandingLivenessProbeCmd(repo, plan, baselineAbsPath, jobId, waitSec
3521
3521
  // A Bash one-liner that waits for a Codex OUT-OF-BAND artifact write to LAND: polls up to ~40s until
3522
3522
  // the file exists, is non-empty, AND its size is stable across two reads (write finished). Assumes a
3523
3523
  // fresh feature slug (no stale same-path artifact) — true for a normal /feature-adr run.
3524
+ // The printed count is stripped to digits (tr -cd 0-9): BSD/macOS wc -c left-pads it, and
3525
+ // parseLandedProbe accepts only landed=<digits> (Codex astra r1 MAJOR, landed-barrier-anchored-line).
3524
3526
  function landedProbeCmd(f) {
3525
- return 'f="' + f + '"; last=-1; for i in 1 2 3 4 5 6 7 8; do if [ -s "$f" ]; then s=$(wc -c < "$f"); if [ "$s" = "$last" ]; then break; fi; last=$s; fi; sleep 5; done; [ -s "$f" ] && echo "landed=$(wc -c < "$f")" || echo "absent"'
3527
+ return 'f="' + f + '"; last=-1; for i in 1 2 3 4 5 6 7 8; do if [ -s "$f" ]; then s=$(wc -c < "$f"); if [ "$s" = "$last" ]; then break; fi; last=$s; fi; sleep 5; done; [ -s "$f" ] && echo "landed=$(wc -c < "$f" | tr -cd 0-9)" || echo "absent"'
3528
+ }
3529
+
3530
+ // Mirror of parseLandedProbe in harness-core/src/feature-adr-routing.ts (body-pinned by the drift
3531
+ // guard in test/feature-adr-model-routing.test.ts; backlog 1f0353f7bdb53588). The probe above writes
3532
+ // its verdict as its LAST line, so only the last non-empty line decides and it must be exactly
3533
+ // landed=<positive digits>; a landed= anywhere else in the transport agent's reply is not a landing.
3534
+ // Trailing blank lines and code-fence lines are tolerated; any other trailing text is not.
3535
+ function parseLandedProbe(raw) {
3536
+ const lines = String(raw === null || raw === undefined ? '' : raw).split('\n').map(function (l) { return l.trim() }).filter(function (l) { return l !== '' && !/^\x60\x60\x60+[\w-]*$/.test(l) })
3537
+ if (lines.length === 0) return { landed: false, bytes: null, reason: 'empty-agent-reply' }
3538
+ const m = /^landed=(\d+)$/.exec(String(lines[lines.length - 1]))
3539
+ if (m === null) return { landed: false, bytes: null, reason: 'no-landed-line' }
3540
+ const bytes = Number(m[1])
3541
+ if (!(bytes > 0)) return { landed: false, bytes: bytes, reason: 'zero-bytes' }
3542
+ return { landed: true, bytes: bytes, reason: 'landed' }
3543
+ }
3544
+ // Inline coder context host contract. Pure wire validation only; filesystem IO stays in the host.
3545
+ const CODER_CONTEXT_SCHEMA = 'fa-coder-context-1'
3546
+ function coderContextCommand(repo, featureDir, tier, opts) {
3547
+ const q = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'"
3548
+ const absolute = (value) => {
3549
+ if (typeof value !== 'string' || value.charAt(0) !== '/' || /(^|\/)\.\.(\/|$)/.test(value) || /[\x00-\x1f]/.test(value)) throw new Error('coder context paths must be absolute without traversal or control characters')
3550
+ return value
3551
+ }
3552
+ if (['S', 'M', 'L', 'XL'].indexOf(tier) === -1) throw new Error('invalid coder context tier')
3553
+ const script = '.claude/skills/feature-adr/scripts/build-coder-context.mjs'
3554
+ const explicit = opts && opts.script != null ? absolute(opts.script) : null
3555
+ const workspace = opts && opts.workspace != null ? absolute(opts.workspace) : null
3556
+ absolute(repo)
3557
+ const selected = featureDir.charAt(0) === '/' ? absolute(featureDir) : absolute(repo + '/' + featureDir)
3558
+ const unavailable = JSON.stringify({ schema: CODER_CONTEXT_SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [{ source: script, reason: 'helper-missing', severity: 'error' }] })
3559
+ return [
3560
+ 'CC_WORKSPACE=' + (workspace === null ? '$(pwd -P)' : q(workspace)) + "; CC_SCRIPT=''",
3561
+ 'CC_ONE=' + (explicit === null ? "''" : q(explicit)) + '; CC_TWO="$CC_WORKSPACE/' + script + '"; CC_THREE=' + q(repo + '/' + script),
3562
+ 'for c in "$CC_ONE" "$CC_TWO" "$CC_THREE"; do [ -n "$c" ] && [ -f "$c" ] && { CC_SCRIPT="$c"; break; }; done',
3563
+ 'if [ -n "$CC_SCRIPT" ]; then node "$CC_SCRIPT" ' + q(selected) + ' --tier=' + q(tier) + '; else printf \'%s\\n\' ' + q(unavailable) + '; printf \'coder context helper missing; tried: %s | %s | %s\\n\' "$CC_ONE" "$CC_TWO" "$CC_THREE" >&2; exit 1; fi',
3564
+ ].join('\n')
3565
+ }
3566
+ function coderContextUtf8Bytes(value) {
3567
+ let bytes = 0
3568
+ for (const ch of value) { const cp = ch.codePointAt(0); bytes += cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4 }
3569
+ return bytes
3570
+ }
3571
+ function parseCoderContextEnvelope(raw, tier) {
3572
+ const failed = (source, reason) => ({ schema: CODER_CONTEXT_SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [{ source: source, reason: reason, severity: 'error' }] })
3573
+ if (typeof raw !== 'string' || coderContextUtf8Bytes(raw) > 2 * 1024 * 1024) return failed('relay', 'invalid-envelope')
3574
+ let value
3575
+ try { value = JSON.parse(raw) } catch (_) { return failed('relay', 'invalid-json') }
3576
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.schema !== CODER_CONTEXT_SCHEMA || ['complete', 'incomplete', 'unavailable'].indexOf(value.status) === -1 || typeof value.promptBlock !== 'string' || !Array.isArray(value.sources) || !Array.isArray(value.diagnostics)) return failed('relay', 'invalid-envelope')
3577
+ const hash = (v) => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v)
3578
+ if (!(value.digest === null || hash(value.digest)) || coderContextUtf8Bytes(value.promptBlock) > 96 * 1024 || value.sources.length > 64 || value.diagnostics.length > 256) return failed('relay', 'invalid-envelope')
3579
+ const seen = new Set()
3580
+ let total = 0
3581
+ for (const source of value.sources) {
3582
+ if (!source || typeof source !== 'object' || typeof source.path !== 'string' || source.path.length > 512 || seen.has(source.path) || /[\x00-\x1f]/.test(source.path)) return failed('relay', 'invalid-source')
3583
+ const kind = source.path === '01_requirements.md' ? 'requirements' : source.path === '06_implementation_plan.md' ? 'plan' : /^03_adr\/[0-9]{3}-[^/\\\x00-\x1f]+\.md$/.test(source.path) || source.path === '03_adr' ? 'adr' : null
3584
+ if (kind === null || source.kind !== kind || !Number.isInteger(source.bytes) || source.bytes < 0 || ['read', 'missing', 'unreadable', 'invalid', 'oversized'].indexOf(source.status) === -1 || !(source.digest === null || hash(source.digest))) return failed(source.path, 'invalid-source')
3585
+ if (source.status === 'read' && (!hash(source.digest) || source.bytes > 256 * 1024 || source.path === '03_adr')) return failed(source.path, 'invalid-source')
3586
+ seen.add(source.path); total += source.bytes
3587
+ }
3588
+ const reasons = ['invalid-tier', 'invalid-source', 'duplicate-source', 'document-limit', 'file-limit', 'aggregate-limit', 'unclosed-fence', 'unclosed-comment', 'empty-section', 'duplicate-section', 'missing-section', 'missing-source', 'prompt-limit', 'invalid-root', 'invalid-file', 'outside-root', 'read-failure', 'invalid-utf8', 'invalid-arguments', 'helper-missing']
3589
+ for (const d of value.diagnostics) {
3590
+ if (!d || typeof d !== 'object' || typeof d.source !== 'string' || d.source.length > 4096 || reasons.indexOf(d.reason) === -1 || ['error', 'warning'].indexOf(d.severity) === -1 || (d.section !== undefined && typeof d.section !== 'string')) return failed('relay', 'invalid-diagnostic')
3591
+ }
3592
+ if (value.status === 'complete') {
3593
+ if (!hash(value.digest) || !value.promptBlock.trim() || total > 1024 * 1024 || value.diagnostics.some((d) => d.severity === 'error') || value.sources.some((s) => s.status !== 'read' || s.bytes === 0)) return failed('relay', 'contradictory-complete')
3594
+ for (const path of ['01_requirements.md', '06_implementation_plan.md']) if (!seen.has(path)) return failed(path, 'missing-source')
3595
+ if (tier !== 'S' && !value.sources.some((s) => s.kind === 'adr')) return failed('03_adr', 'missing-source')
3596
+ }
3597
+ return value
3526
3598
  }
3527
3599
 
3528
3600
  // ── K2 plan-completeness gate (feature fa-plan-gate-wiring) ────────────────────────────────────
@@ -3885,7 +3957,7 @@ async function designStage(promptText, opts, artifactPath, baseLabel) {
3885
3957
  const designRungHolder = newRung()
3886
3958
  const res = await safeCodexAgent(promptText + codexEffortHint(codexOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until the file is fully written to disk.', codexOpts, designRungHolder)
3887
3959
  const probe = await dispatchAgent(newRung(), 'Confirm a Codex OUT-OF-BAND artifact write has LANDED before the next stage reads it. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(artifactPath), { label: 'design:confirm-landed', phase: 'Design', effort: 'low' })
3888
- if (res && probe && /landed=/.test(String(probe))) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
3960
+ if (res && probe && parseLandedProbe(probe).landed) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
3889
3961
  log('design artifact did not land on codex (' + artifactPath + ') — falling back to Claude')
3890
3962
  const fallbackOpts = {}
3891
3963
  // R18: ONE mapping for the whole class. The boolean below still writes the provenance TEXT (a
@@ -4605,7 +4677,7 @@ if (registryOutcome !== 'unverified') registryOutcome = designIncompleteOutcome
4605
4677
  phase('Plan')
4606
4678
  await recordRegistryEvent('heartbeat', 'Plan')
4607
4679
  await usageProbe('Plan')
4608
- const planPrompt = 'Step 6 (SPARC-GOAP implementation plan) of /feature-adr for "' + DESC + '" (' + SLUG + ', tier ' + tier + '). READ THESE INPUTS FIRST, by name: ' + FDIR + '/01_requirements.md, every ' + FDIR + '/03_adr/NNN-*.md, ' + FDIR + '/05_architecture.md, and ' + FDIR + '/03.5_ideation_report.md / ' + FDIR + '/04_domain_model.md when present. Then decompose into milestones + concrete tasks with success metrics. Write ' + FDIR + '/06_implementation_plan.md. END the plan with a trailing `EXPECTED_CODE_TARGETS:` block listing, one per line as `- <repo-relative path>`, EVERY production/test/config/doc file Step 7 is expected to create or modify. This block is machine-read by the Step-7.5 landing barrier: only paths it ESTABLISHES can ever count as landed, so an absent or unpollable block makes the barrier verdict INCONCLUSIVE. List only real targets outside features/, .dz/, .agentic-qe/ and roam/. The K2 plan-completeness gate blocks Step 7 until the plan satisfies these too, so write them in as you author, not afterwards: (C1) every ADR under 03_adr/ is cited as `ADR-<n>` by the task that implements it; (C2) every test path named in an ADR Confirmation stanza appears verbatim in the plan, bound to the task that writes it; (C4) every acid token `A<n>` from 00_complexity_assessment.md is named verbatim, bound to its owning task and to the test that proves the refusal. (C8) every requirement id declared in 01_requirements.md (FR-N, NFR-N, AC-N, C-N) is cited by the task that covers it. If any corrections from Step 3.5 (a CONDITIONAL verdict) or other sources are folded into this plan, carry them in a `## Amendments` section. ' + AMENDMENT_RULE + ' Return wrote[] + summary.' + ABSOLUTE_PATH_NOTE + WRITE_DISCIPLINE
4680
+ const planPrompt = 'Step 6 (SPARC-GOAP implementation plan) of /feature-adr for "' + DESC + '" (' + SLUG + ', tier ' + tier + '). READ THESE INPUTS FIRST, by name: ' + FDIR + '/01_requirements.md, every ' + FDIR + '/03_adr/NNN-*.md, ' + FDIR + '/05_architecture.md, and ' + FDIR + '/03.5_ideation_report.md / ' + FDIR + '/04_domain_model.md when present. Then decompose into milestones + concrete tasks with success metrics. Write ' + FDIR + '/06_implementation_plan.md. END the plan with a trailing `EXPECTED_CODE_TARGETS:` block listing, one per line as `- <repo-relative path>`, EVERY production/test/config/doc file Step 7 is expected to create or modify. This block is machine-read by the Step-7.5 landing barrier: only paths it ESTABLISHES can ever count as landed, so an absent or unpollable block makes the barrier verdict INCONCLUSIVE. List only real targets outside features/, .dz/, .agentic-qe/ and roam/. The K2 plan-completeness gate blocks Step 7 until the plan satisfies these too, so write them in as you author, not afterwards: (C1) every ADR under 03_adr/ is cited as `ADR-<n>` by the task that implements it; (C2) every test path named in an ADR Confirmation stanza appears verbatim in the plan, bound to the task that writes it; (C4) every acid token `A<n>` from 00_complexity_assessment.md is named verbatim, bound to its owning task and to the test that proves the refusal. (C8) every requirement id declared in 01_requirements.md (FR-N, NFR-N, AC-N, C-N) is cited by the task that covers it. For C1/C8 an id counts ONLY on the first line of a heading, list item or table row (not prose, not fenced code). If any corrections from Step 3.5 (a CONDITIONAL verdict) or other sources are folded into this plan, carry them in a `## Amendments` section. ' + AMENDMENT_RULE + ' Return wrote[] + summary.' + ABSOLUTE_PATH_NOTE + WRITE_DISCIPLINE
4609
4681
  const planContext = buildDecisionContext({ slug: SLUG, decisionKind: 'plan-route-selection', description: DESC, tier: tier, codeHint: CODE_HINT, upstreamDigest: fnv1a64(JSON.stringify(design === undefined ? null : design)) })
4610
4682
  let planRecallCapture = { promptBlock: '', selected: [] }
4611
4683
  // Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
@@ -4638,7 +4710,7 @@ if (planIsCodex) {
4638
4710
  // Codex-landed barrier for the plan artifact: a stub return is NOT proof the file was written
4639
4711
  // (codex writes out-of-band). Require the artifact to LAND; otherwise fall through to the Claude planner.
4640
4712
  const planLanded = codexPlan ? await dispatchAgent(newRung(), 'Confirm the Codex plan write has LANDED. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(FDIR + '/06_implementation_plan.md'), { label: 'plan:confirm-landed', phase: 'Plan', effort: 'low' }) : null
4641
- if (codexPlan && planLanded && /landed=/.test(String(planLanded))) {
4713
+ if (codexPlan && planLanded && parseLandedProbe(planLanded).landed) {
4642
4714
  plan = { wrote: [FDIR + '/06_implementation_plan.md'], summary: String(codexPlan).slice(0, 500), planner: 'codex' }
4643
4715
  log('Plan: Codex (top model) — artifact landed')
4644
4716
  } else {
@@ -4837,7 +4909,7 @@ if (plan && planGate.verdict === 'fail' && planGate.reason === 'script-verdict')
4837
4909
  // Codex writes out-of-band, so a stub return is not proof of landing — reuse the SAME
4838
4910
  // landed-barrier probe the first Codex plan dispatch used (landedProbeCmd), not a copy of it.
4839
4911
  const repairLanded = codexRepair ? await dispatchAgent(newRung(), 'Confirm the Codex plan-repair write has LANDED. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(FDIR + '/06_implementation_plan.md'), { label: 'plan:repair-confirm-landed', phase: 'Plan', effort: 'low' }) : null
4840
- if (codexRepair && repairLanded && /landed=/.test(String(repairLanded))) repaired = { wrote: [FDIR + '/06_implementation_plan.md'], summary: String(codexRepair).slice(0, 500) }
4912
+ if (codexRepair && repairLanded && parseLandedProbe(repairLanded).landed) repaired = { wrote: [FDIR + '/06_implementation_plan.md'], summary: String(codexRepair).slice(0, 500) }
4841
4913
  } else {
4842
4914
  const repairClaudeModel = planIsCodex ? {} : planModel
4843
4915
  const repairClaudeOpts = mergeOpts({ label: stageLabel(planIsCodex ? 'plan:repair-claude-fb' : 'plan:repair', repairClaudeModel), phase: 'Plan', schema: ARTIFACT, _stage: 'plan-repair', _reason: 'plan-repair' }, repairClaudeModel)
@@ -5021,7 +5093,22 @@ await usageProbe('Code')
5021
5093
  // nothing; every hand-dispatched round that carried this preamble landed code. The routing is
5022
5094
  // decided before this prompt exists, so saying so is the whole fix.
5023
5095
  const codePromptBase = 'GATE-ANSWERED — the routing questions are already settled and must NOT be asked again: the mode and the coder family were chosen before this dispatch, you ARE the coder, and an independent cross-family QE runs after you. This dispatch is non-interactive: asking a question and exiting returns exit 0 with nothing written, which is indistinguishable from a crash to everything downstream. FIRST, via Bash run EXACTLY `mkdir -p ' + FDIR + '/.fa-state && git -C ' + REPO + ' rev-parse HEAD > "' + FDIR + '/.fa-state/base-ref.tmp" && mv "' + FDIR + '/.fa-state/base-ref.tmp" "' + FDIR + '/.fa-state/base-ref"` — an atomic record of HEAD before your changes; Step 8 scopes `--added-since` on it (AM-2). Begin implementing immediately.\n\nStep 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ THESE INPUTS FIRST, by name (0691e163: the coder used to get one directory pointer; measured over three real runs, the plan was opened by all coders but the ADR unevenly and requirements/domain model not at all): ' + FDIR + '/06_implementation_plan.md (the tasks + EXPECTED_CODE_TARGETS + Amendments), every ' + FDIR + '/03_adr/NNN-*.md (each names a load-bearing property and its Required automated check), ' + FDIR + '/05_architecture.md, ' + FDIR + '/01_requirements.md, and ' + FDIR + '/04_domain_model.md when present (L/XL). Then implement the feature. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). If the plan carries a `## Amendments` section, implement every AM-N row AND its named Confirmation test (for a safeguard amendment: a test proving it FIRES on a real input). IO-ON-PURE-PATH RULE: if your diff adds I/O (DB/network/file) to a previously-pure path — especially a startup/lifespan/health path — also write a NEGATIVE resource-down test (broken/unbound resource handle → the path degrades per its declared contract: fail-open for an advisory feature, explicit fail-fast for a load-bearing one) alongside the happy-path test; never fix a failing test by swapping a broken fixture for a healthy one without keeping BOTH cases. Follow repo conventions; build must pass. Write a change manifest ' + FDIR + '/07_code_changes/change_manifest.md listing every file touched. Return wrote[] (incl. real source files) + summary.' + ABSOLUTE_PATH_NOTE + PS_GUIDANCE('code')
5024
- let codePrompt = codePromptBase
5096
+ // Read current documents on EVERY invocation, before lookup, using one captured host snapshot.
5097
+ let coderContextSnapshot
5098
+ try {
5099
+ const contextCommand = coderContextCommand(REPO, FDIR, tier, { script: A.coderContextScript, workspace: WS === null ? undefined : WS })
5100
+ const contextRaw = await dispatchAgent(newRung(), 'Run EXACTLY this shell snippet via Bash as ONE command. Return stdout VERBATIM as the single JSON envelope, with no narration or fences. Preserve command exit status; never synthesize a complete result:\n' + contextCommand, { label: 'code:context', phase: 'Code', effort: 'low' })
5101
+ coderContextSnapshot = parseCoderContextEnvelope(contextRaw, tier)
5102
+ } catch (_) {
5103
+ coderContextSnapshot = { schema: CODER_CONTEXT_SCHEMA, status: 'unavailable', promptBlock: '', digest: null, sources: [], diagnostics: [{ source: 'host-command', reason: 'read-failure', severity: 'error' }] }
5104
+ }
5105
+ if (coderContextSnapshot.status !== 'complete') {
5106
+ log('Step 7 refused: required coder context ' + coderContextSnapshot.status + ': ' + JSON.stringify(coderContextSnapshot.diagnostics))
5107
+ return { tier: tier, phase: 'code-context-failed', outcome: 'unverified', slug: SLUG, artifactsDir: FDIR, coderContext: coderContextSnapshot, gates: { code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', note: 'Required current coder context was not established; no code checkpoint lookup, decision recall or coder dispatch occurred.' }
5108
+ }
5109
+ const coderContextFingerprint = fnv1a64(coderContextSnapshot.promptBlock)
5110
+ const coderContextMetadata = { schema: coderContextSnapshot.schema, digest: coderContextSnapshot.digest, promptFingerprint: coderContextFingerprint }
5111
+ let codePrompt = codePromptBase + coderContextSnapshot.promptBlock
5025
5112
  // Resolve the coder model. args.models.code wins (a direct 'codex' spec = codex-first); else the legacy
5026
5113
  // CODER knob drives it (with its codex-fallback null-guard). resolveStageModel('code') folds both via the
5027
5114
  // code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
@@ -5042,7 +5129,7 @@ const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), p
5042
5129
  // the checkpoint (it only feeds the expected-targets parse, already consumed by the original run).
5043
5130
  // R6: the landing token is salted into the code stage's PARTS (not CKPT_SCHEMA_VERSION, which
5044
5131
  // stays 'fa-ckpt-2' deliberately) so ONLY this stage's pre-protocol checkpoints hash stale.
5045
- const codeHash = ckptHash('code', [tier, DESC, fnv1a64(JSON.stringify(plan === undefined ? null : plan)), CODER, MODELS.code === undefined ? null : MODELS.code, CODEX_MODEL, PRIMARY, BUDGET_MODE, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, LANDING_HASH_TOKEN])
5132
+ const codeHash = ckptHash('code', [tier, DESC, fnv1a64(JSON.stringify(plan === undefined ? null : plan)), CODER, MODELS.code === undefined ? null : MODELS.code, CODEX_MODEL, PRIMARY, BUDGET_MODE, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, LANDING_HASH_TOKEN, coderContextSnapshot.digest, coderContextFingerprint])
5046
5133
  const codeComposite = await withCheckpoint('code', 'Code', codeHash, async () => {
5047
5134
  let code = null
5048
5135
  let coderUsed = 'claude'
@@ -5052,7 +5139,7 @@ let codexJobId = null
5052
5139
  let baselineCapture = null
5053
5140
  const codeContext = buildDecisionContext({ slug: SLUG, decisionKind: 'code-implementation', description: DESC, tier: tier, codeHint: CODE_HINT, upstreamDigest: fnv1a64(JSON.stringify(plan === undefined ? null : plan)) })
5054
5141
  const codeRecall = await prepareDecisionRecall(codeContext, 'Code', 'decision-recall:step7')
5055
- codePrompt = codePromptBase + codeRecall.promptBlock
5142
+ codePrompt = codePromptBase + coderContextSnapshot.promptBlock + codeRecall.promptBlock
5056
5143
  if (!codeIsCodexFirst) {
5057
5144
  // R14-1: claimed at DISPATCH. The later `return null` exits only this withCheckpoint CALLBACK — the
5058
5145
  // run continues and can return `completed-unverified`, so a success-only write left the report with
@@ -5200,12 +5287,12 @@ if (needsCodeLandedBarrier(coderUsed)) {
5200
5287
  }
5201
5288
  const codeStageResult = { code: code, coderUsed: coderUsed, codexCodeText: String(codexCodeText).slice(0, 4000), codexJobId: codexJobId, modelUsed: modelsUsed.code, landedNote: landedNote, landingStatus: landingStatus, landingProtocol: LANDING_PROTOCOL_VERSION, scrapeDiagnostic: scrapeDiagnostic, expectedTargets: expectedTargets }
5202
5289
  if (landingReason !== null) codeStageResult.landingReason = landingReason
5203
- return { stageResult: codeStageResult, decisionRecall: codeRecall }
5204
- }, { validate: function (value) { return !!value && typeof value === 'object' && value.stageResult !== null && value.stageResult !== undefined && codeStageResultShapeValid(value.stageResult) && value.decisionRecall && typeof value.decisionRecall.promptBlock === 'string' }, persist: function (r) { return codeCheckpointPersistAllowed(r.stageResult.landingStatus, needsCodeLandedBarrier(r.stageResult.coderUsed)) } })
5290
+ return { stageResult: codeStageResult, decisionRecall: codeRecall, coderContext: coderContextMetadata }
5291
+ }, { validate: function (value) { return !!value && typeof value === 'object' && value.stageResult !== null && value.stageResult !== undefined && codeStageResultShapeValid(value.stageResult) && value.decisionRecall && typeof value.decisionRecall.promptBlock === 'string' && value.coderContext && value.coderContext.schema === coderContextMetadata.schema && value.coderContext.digest === coderContextMetadata.digest && value.coderContext.promptFingerprint === coderContextMetadata.promptFingerprint }, persist: function (r) { return codeCheckpointPersistAllowed(r.stageResult.landingStatus, needsCodeLandedBarrier(r.stageResult.coderUsed)) } })
5205
5292
  let codeStage = null
5206
5293
  if (codeComposite && typeof codeComposite === 'object') {
5207
5294
  codeStage = codeComposite.stageResult
5208
- codePrompt = codePromptBase + (codeComposite.decisionRecall && typeof codeComposite.decisionRecall.promptBlock === 'string' ? codeComposite.decisionRecall.promptBlock : '')
5295
+ codePrompt = codePromptBase + coderContextSnapshot.promptBlock + (codeComposite.decisionRecall && typeof codeComposite.decisionRecall.promptBlock === 'string' ? codeComposite.decisionRecall.promptBlock : '')
5209
5296
  }
5210
5297
  let code = codeStage ? codeStage.code : null
5211
5298
  coderUsed = codeStage ? codeStage.coderUsed : 'claude'