@dzhechkov/skills-feature-adr 1.3.66 → 1.4.0

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
@@ -5,7 +5,7 @@
5
5
  "files": [
6
6
  {
7
7
  "path": "CHANGELOG.md",
8
- "sha256": "e4f50d4f757f877b54c8d25dfc1321776545be4cb814d5f17ee1128ee2320982"
8
+ "sha256": "bbb43101a686369a8662f47fe7b7aa75073b614a188c2752cd81d9570620bb3d"
9
9
  },
10
10
  {
11
11
  "path": "LICENSE",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "5cca2d4a5337177ee9a072d49d14f73386af75398f52d907d757d4c052afdf81"
16
+ "sha256": "469873d967c4438afd0b25ec8b013347841e07328f508f6786f90cc6aa2f09b9"
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": "b76f1b1d1ce3d8b22be0c2354fe3bd22833513da8b418c7c93423ae75f4018ae"
28
+ "sha256": "c1e4d257167fd2462e0c8e09f248fc1aef332aa34fb09dddd5940bdcc30ea34b"
29
29
  },
30
30
  {
31
31
  "path": "scripts/sync-templates.js",
@@ -121,7 +121,7 @@
121
121
  },
122
122
  {
123
123
  "path": "templates/.claude/skills/feature-adr/SKILL.md",
124
- "sha256": "02d2a081e4bc9e7f5d42e55a4e21a68484af8b45216ff74fe5fc4322cb689a96"
124
+ "sha256": "bdedfed69d985eb6969f158a72d92f63c6b1b993008c3661f7ffc2c6a7ccfbae"
125
125
  },
126
126
  {
127
127
  "path": "templates/.claude/skills/feature-adr/examples/sample-feature-output.md",
@@ -133,31 +133,31 @@
133
133
  },
134
134
  {
135
135
  "path": "templates/.claude/skills/feature-adr/modules/01-requirements.md",
136
- "sha256": "7b599efd3122be7ce2247bba1c9204e1b3c09322ad56ed4cedcbd4dd471ed41e"
136
+ "sha256": "e6ea3a224b555d5b7410cf2ddf02a3fb8d009fa01109463353e2b993c318270c"
137
137
  },
138
138
  {
139
139
  "path": "templates/.claude/skills/feature-adr/modules/02-research.md",
140
- "sha256": "55099189082d872d08a9d40b11782d8d08256955cbea617aa457756eb9fa1a07"
140
+ "sha256": "e2506e42251c74485188473acc4586f841afbec3ae7c5eaa8a3d8f73f6e14cc5"
141
141
  },
142
142
  {
143
143
  "path": "templates/.claude/skills/feature-adr/modules/03-adr.md",
144
- "sha256": "742f1d362fd4d1b1b731fe81b7aa794886aa58126005ad67ef0a27775c80f082"
144
+ "sha256": "ea03eee136c89267888b8034baa1a1cb0057314a2e745df71654a9ebee5d46b6"
145
145
  },
146
146
  {
147
147
  "path": "templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md",
148
- "sha256": "220e06d0d67643f09df868df32d1ec3acdc205653a5ef977d171791d9ce77b74"
148
+ "sha256": "d48a8d6eebf9760cc8f1a4f19df0e6afc31fcc1284b5eacfb604ad8ced6a0b53"
149
149
  },
150
150
  {
151
151
  "path": "templates/.claude/skills/feature-adr/modules/04-ddd.md",
152
- "sha256": "c6cf98ab0dd80183ad507aca12edf164ee79a5fb60eee84d674cf73fe3a68646"
152
+ "sha256": "e9504cf1183bc0105167dc06108860882941fcf1b5be79d1978d8212c410ffbb"
153
153
  },
154
154
  {
155
155
  "path": "templates/.claude/skills/feature-adr/modules/05-architecture.md",
156
- "sha256": "227ec469154348afc414ef4751cc34ea5bb87ecb063cf196eff254615a6b5e00"
156
+ "sha256": "2ef7cf316903f0cbb1fabe1f0e1770c22b2cb3a11a4118cac0529c25074b5e8a"
157
157
  },
158
158
  {
159
159
  "path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
160
- "sha256": "443043f6f3a3c2ebdcc4d40f9d13d799581056fb37890a2bb1fa61662825229b"
160
+ "sha256": "c8fdd1877d8d9515ab29f52c3db49ddd9612b5797bc071e6208def860c02e6f2"
161
161
  },
162
162
  {
163
163
  "path": "templates/.claude/skills/feature-adr/modules/07-code.md",
@@ -321,7 +321,7 @@
321
321
  },
322
322
  {
323
323
  "path": "templates/.claude/workflows/feature-adr.js",
324
- "sha256": "4904cf2040f024647c561fb5bd11aab5c7c0b70190dc4505b1bd69572cf25072"
324
+ "sha256": "1be239d2f02612bc33027767d9930eae8a2d6d5afd427d1485198bbdd821a3c7"
325
325
  },
326
326
  {
327
327
  "path": "templates/lib/memory-protocol.md",
@@ -349,5 +349,5 @@
349
349
  }
350
350
  ]
351
351
  },
352
- "signature": "nKR64NSSz41u0CCXl4HdsXqTmS5QUqIIYZdRn4xVoEj/wuzF7Vb7X21mte94etpLINVimxi1pTdUQYOO5GlyCw=="
352
+ "signature": "gixeRY/3+gjSSS3a+DfVwn40SVlD9Gr2sCh3H8afGWd1+Dzo8R8CmD61qG0cLeDdHn2NgzRWwvICHe+0HFlYCA=="
353
353
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-08-20
4
+
5
+ ### Changed — the composite design checkpoint
6
+
7
+ - The Step 1–5 design fan is checkpointed **per sibling** instead of as one composite entry, so one
8
+ dead agent no longer discards three finished siblings. Each sibling's resume key carries its own
9
+ steering inputs, including its prompt text: fixing one step's instructions invalidates that step
10
+ and nothing else.
11
+ - An incomplete fan is **refused** at the Step-5/6 boundary (`phase: 'design-incomplete'`) instead of
12
+ being handed to the planner. Previously the completeness verdict was computed and only logged, and
13
+ Step 6 planned off a null design. The refusal captures training pairs and appends a cost-ledger row
14
+ on the way out, so a stopped run is neither silent nor invisible to cost analysis.
15
+ - Three named reasons with distinct repairs — `substage-missing`, `artifact-missing`,
16
+ `probe-not-established`. Under `resume:'force'` the artifact-missing repair says `resume:'never'`,
17
+ because force skips artifact probes and a plain re-invoke would loop forever.
18
+ - The artifact check no longer lists a directory: it runs `[ -f <exact path> ]` per required
19
+ artifact. A listing is a list of filenames, and a file whose NAME ends in a newline was measured
20
+ satisfying the requirement for the real file.
21
+ - The probe transcript is validated strictly rather than scanned. It is relayed by an agent, not read
22
+ from a pipe, and an agent that narrates the expected output emits the token byte-identically —
23
+ which produced a false pass on a missing artifact before this release. Inconclusive is never a pass.
24
+
25
+ ### Requires
26
+
27
+ - `@dzhechkov/harness-core >= 0.5.4`. The checkpoint schema is now `fa-ckpt-3`: every existing
28
+ `.fa-state/checkpoints.jsonl` reads as no checkpoint, so each in-flight feature re-runs
29
+ router+design+plan once.
30
+
31
+ ### Verification
32
+
33
+ Eight cross-family review rounds (Codex `gpt-5.6-sol` at xhigh, each pinned to a sha256 of the
34
+ reviewed files): D → D → D → B → C → C → D → **A**, the last with no new defects. Full ladder in
35
+ `features/feature-adr-hardening/composite-design-checkpoint.md`.
36
+
37
+
3
38
  ### Follow-up (QE LOW gaps closed before publish)
4
39
  - **Upstream deletions**: `update` now removes files the template dropped (manifest-tracked orphans only; user-created untracked files are never touched) — previously `diff.missing` was computed but ignored. Shown in the summary + `--dry-run` (`- DEL`). Tests: Case G/H.
5
40
  - Removed a dead `unchanged` branch in the directory update path (unreachable — `diff.modified` guarantees bytes differ).
package/README.md CHANGED
@@ -106,6 +106,36 @@ ARCHITECTURE → IMPLEMENTATION → CODE → QE → FLEET QE
106
106
  # Full protocols + 6 extra skills, up to 7 fleet QE agents
107
107
  ```
108
108
 
109
+ ### The design fan resumes per sibling, and an incomplete design is REFUSED (v1.4.0)
110
+
111
+ Step 1–5 run as one parallel fan — requirements, ADR, QCSD, architecture. Until v1.4.0 the whole fan
112
+ was ONE checkpoint entry, so a single dead agent discarded three finished siblings and the next
113
+ invocation paid for all four again. Each sibling is now checkpointed on its own
114
+ (`design:requirements` / `design:adr` / `design:qcsd` / `design:architecture`), keyed on what steers
115
+ **it** — its own prompt text included, so correcting one step's instructions re-runs that step and
116
+ leaves the others alone.
117
+
118
+ The other half is what the pipeline is allowed to CONSUME. An incomplete fan no longer flows into
119
+ Step 6: the run stops at the Step-5/6 boundary and returns `phase: 'design-incomplete'` rather than
120
+ producing a plan with no ADR behind it. Three distinct reasons, each with its own repair:
121
+
122
+ | Reason | What happened | Repair printed |
123
+ |---|---|---|
124
+ | `substage-missing` | a sibling agent died (often a Claude limit) | re-invoke — the finished siblings resume free; or route that stage to Codex |
125
+ | `artifact-missing` | every sibling reported success, but a required file is not on disk | under `resume:'force'` it says use `resume:'never'` — force skips artifact probes, so re-invoking would loop |
126
+ | `probe-not-established` | the check could not be trusted at all | inconclusive is never a pass — refuse, then re-run |
127
+
128
+ The artifact check never lists a directory. It asks `[ -f <exact path> ]` per required artifact,
129
+ because a listing is a list of filenames and the data can impersonate the frame: measured, a file
130
+ whose NAME ends in a newline satisfied the requirement for the real file. And since the check is
131
+ relayed by an agent rather than read from a pipe, the whole transcript is validated — an agent that
132
+ merely *narrates* the expected output emits the token byte-identically, and that produced a false
133
+ pass before this release.
134
+
135
+ Requires `@dzhechkov/harness-core >= 0.5.4`. **One-time cost:** the checkpoint schema moved to
136
+ `fa-ckpt-3`, so every existing `.fa-state/checkpoints.jsonl` reads as no checkpoint and each
137
+ in-flight feature re-runs router+design+plan once.
138
+
109
139
  ### Durable checkpoints + resume (v1.3.64)
110
140
 
111
141
  Every expensive stage (router / design / plan / code / qe / fleet) checkpoints its result into
@@ -878,3 +908,38 @@ them inside the package's own monorepo is almost never what you want anyway.
878
908
  - **npm:** [https://www.npmjs.com/package/@dzhechkov/skills-feature-adr](https://www.npmjs.com/package/@dzhechkov/skills-feature-adr)
879
909
  - **Keysarium:** [https://www.npmjs.com/package/@dzhechkov/keysarium](https://www.npmjs.com/package/@dzhechkov/keysarium)
880
910
  - **BTO:** [https://www.npmjs.com/package/@dzhechkov/skills-bto](https://www.npmjs.com/package/@dzhechkov/skills-bto)
911
+
912
+ ## Write discipline — why the writing steps now build a skeleton first
913
+
914
+ MEASURED in the field on 2026-08-19 and 2026-08-20, two independent runs of the canonical pipeline
915
+ against a ~130-file repository: the steps that must produce a document — Step 5 (Architecture) and
916
+ Step 6 (Plan) — **never reached a write**. 18 attempts, zero file writes in every one. They died in
917
+ the READING phase: a shell result returns, the agent thinks about its next move, stays silent past
918
+ the runtime's 180-second inactivity watchdog, and is killed. One run cost ~4M tokens and 1h54m.
919
+
920
+ The failure is **deterministic, not unlucky**: thinking time grows with accumulated history, so on a
921
+ large enough repository unbounded exploration guarantees the kill. And the cause was in the
922
+ instructions, not the runtime — every writing step said, in effect, *"read the code, write the
923
+ document"*, with no reading budget and no order of operations. An agent obeying literally reads
924
+ until it dies.
925
+
926
+ The owner's control experiment is the whole evidence base, and it is n=1: same slice, same inputs,
927
+ same model, ONE added paragraph about write discipline → a 10-section skeleton on disk 8 minutes in,
928
+ first attempt, after six consecutive deaths.
929
+
930
+ So every document-producing step now carries:
931
+
932
+ 1. the FIRST file write happens within the first ~12 tool calls — a skeleton of section headings
933
+ with one line of intent under each;
934
+ 2. then fill it **one section per edit**, no edit longer than ~120 lines;
935
+ 3. never go more than 2 minutes without a tool call;
936
+ 4. when unsure whether to read more or to write — **write**.
937
+
938
+ **What this does not do**, stated plainly: it does not stop the deaths. The watchdog is unchanged.
939
+ It changes what survives one — previously nothing, now a skeleton on disk that the next attempt can
940
+ continue. The `~12` and `~120` are chosen parameters, not measured optima. Two possible regressions
941
+ were named by an independent reviewer and are **not measured**: an early skeleton may anchor a
942
+ structure chosen before understanding, and section-per-edit raises tool-call overhead.
943
+
944
+ Steps whose deliverable is a returned verdict rather than a document (the complexity router) are
945
+ deliberately excluded — "skeleton first" is nonsense there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.66",
3
+ "version": "1.4.0",
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"
package/sbom.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "hashes": [
16
16
  {
17
17
  "alg": "SHA-256",
18
- "content": "e4f50d4f757f877b54c8d25dfc1321776545be4cb814d5f17ee1128ee2320982"
18
+ "content": "bbb43101a686369a8662f47fe7b7aa75073b614a188c2752cd81d9570620bb3d"
19
19
  }
20
20
  ]
21
21
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "5cca2d4a5337177ee9a072d49d14f73386af75398f52d907d757d4c052afdf81"
38
+ "content": "469873d967c4438afd0b25ec8b013347841e07328f508f6786f90cc6aa2f09b9"
39
39
  }
40
40
  ]
41
41
  },
@@ -65,7 +65,7 @@
65
65
  "hashes": [
66
66
  {
67
67
  "alg": "SHA-256",
68
- "content": "b76f1b1d1ce3d8b22be0c2354fe3bd22833513da8b418c7c93423ae75f4018ae"
68
+ "content": "c1e4d257167fd2462e0c8e09f248fc1aef332aa34fb09dddd5940bdcc30ea34b"
69
69
  }
70
70
  ]
71
71
  },
@@ -305,7 +305,7 @@
305
305
  "hashes": [
306
306
  {
307
307
  "alg": "SHA-256",
308
- "content": "02d2a081e4bc9e7f5d42e55a4e21a68484af8b45216ff74fe5fc4322cb689a96"
308
+ "content": "bdedfed69d985eb6969f158a72d92f63c6b1b993008c3661f7ffc2c6a7ccfbae"
309
309
  }
310
310
  ]
311
311
  },
@@ -335,7 +335,7 @@
335
335
  "hashes": [
336
336
  {
337
337
  "alg": "SHA-256",
338
- "content": "7b599efd3122be7ce2247bba1c9204e1b3c09322ad56ed4cedcbd4dd471ed41e"
338
+ "content": "e6ea3a224b555d5b7410cf2ddf02a3fb8d009fa01109463353e2b993c318270c"
339
339
  }
340
340
  ]
341
341
  },
@@ -345,7 +345,7 @@
345
345
  "hashes": [
346
346
  {
347
347
  "alg": "SHA-256",
348
- "content": "55099189082d872d08a9d40b11782d8d08256955cbea617aa457756eb9fa1a07"
348
+ "content": "e2506e42251c74485188473acc4586f841afbec3ae7c5eaa8a3d8f73f6e14cc5"
349
349
  }
350
350
  ]
351
351
  },
@@ -355,7 +355,7 @@
355
355
  "hashes": [
356
356
  {
357
357
  "alg": "SHA-256",
358
- "content": "742f1d362fd4d1b1b731fe81b7aa794886aa58126005ad67ef0a27775c80f082"
358
+ "content": "ea03eee136c89267888b8034baa1a1cb0057314a2e745df71654a9ebee5d46b6"
359
359
  }
360
360
  ]
361
361
  },
@@ -365,7 +365,7 @@
365
365
  "hashes": [
366
366
  {
367
367
  "alg": "SHA-256",
368
- "content": "220e06d0d67643f09df868df32d1ec3acdc205653a5ef977d171791d9ce77b74"
368
+ "content": "d48a8d6eebf9760cc8f1a4f19df0e6afc31fcc1284b5eacfb604ad8ced6a0b53"
369
369
  }
370
370
  ]
371
371
  },
@@ -375,7 +375,7 @@
375
375
  "hashes": [
376
376
  {
377
377
  "alg": "SHA-256",
378
- "content": "c6cf98ab0dd80183ad507aca12edf164ee79a5fb60eee84d674cf73fe3a68646"
378
+ "content": "e9504cf1183bc0105167dc06108860882941fcf1b5be79d1978d8212c410ffbb"
379
379
  }
380
380
  ]
381
381
  },
@@ -385,7 +385,7 @@
385
385
  "hashes": [
386
386
  {
387
387
  "alg": "SHA-256",
388
- "content": "227ec469154348afc414ef4751cc34ea5bb87ecb063cf196eff254615a6b5e00"
388
+ "content": "2ef7cf316903f0cbb1fabe1f0e1770c22b2cb3a11a4118cac0529c25074b5e8a"
389
389
  }
390
390
  ]
391
391
  },
@@ -395,7 +395,7 @@
395
395
  "hashes": [
396
396
  {
397
397
  "alg": "SHA-256",
398
- "content": "443043f6f3a3c2ebdcc4d40f9d13d799581056fb37890a2bb1fa61662825229b"
398
+ "content": "c8fdd1877d8d9515ab29f52c3db49ddd9612b5797bc071e6208def860c02e6f2"
399
399
  }
400
400
  ]
401
401
  },
@@ -805,7 +805,7 @@
805
805
  "hashes": [
806
806
  {
807
807
  "alg": "SHA-256",
808
- "content": "4904cf2040f024647c561fb5bd11aab5c7c0b70190dc4505b1bd69572cf25072"
808
+ "content": "1be239d2f02612bc33027767d9930eae8a2d6d5afd427d1485198bbdd821a3c7"
809
809
  }
810
810
  ]
811
811
  },
@@ -226,6 +226,14 @@ Execute protocol
226
226
  → Checkpoint N
227
227
  ```
228
228
 
229
+ **Write discipline — applies to EVERY step that produces a document.** Open the step's artifact within
230
+ your first ~12 tool calls as a SKELETON (its section headings, one line of intent each), then fill it one
231
+ section per edit, no edit longer than ~120 lines. Never go more than 2 minutes without a tool call, and
232
+ when you are unsure whether to read more or to write, WRITE. Reason: an executor silent for 180 seconds is
233
+ killed by the runtime, and thinking time grows with accumulated history — so unbounded exploration before
234
+ the first write is a deterministic death on a large repo, not bad luck. Each module restates this under
235
+ its own `## Write discipline (the 180-second rule)` heading.
236
+
229
237
  ### 4. Final Verification
230
238
  After Step 8 (or Step 9 for L/XL) completes, verify:
231
239
  - All mandatory artifacts exist per tier
@@ -616,6 +624,7 @@ npx @dzhechkov/skills-feature-adr init --with-learning --knowledge-extractor
616
624
  | Ignore NO-GO verdict | Proceed despite Step 3.5 NO-GO | BLOCK — rework required |
617
625
  | Skip gap loop | No gap detection in Step 8 | Missing requirements coverage check |
618
626
  | Skip fleet QE for L/XL | No Step 9 for large features | BLOCK — fleet assessment mandatory |
627
+ | Unbounded reading before the first write | The step's artifact still does not exist after ~12 tool calls | BLOCK — write the skeleton now; a silent executor is killed at 180 s |
619
628
 
620
629
  ## Checkpoint Format
621
630
 
@@ -77,6 +77,32 @@ Explicitly state:
77
77
  - **Dependencies:** What must exist before this feature works
78
78
  - **Dependents:** What will break if this feature changes
79
79
 
80
+ ## Write discipline (the 180-second rule)
81
+
82
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
83
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
84
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
85
+ nothing was ever on disk.
86
+
87
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
88
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
89
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
90
+
91
+ So, in this step:
92
+
93
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/01_requirements.md` containing only the headings this step
94
+ requires (Stakeholders · Functional requirements · Non-functional requirements · Constraints ·
95
+ Scope boundaries · Open questions), one line of intent under each. (S-tier is inline and has no
96
+ file — this section does not apply to it.)
97
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
98
+ file readable; none of them is allowed to wait for the section after it.
99
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
100
+ stop and write what you have — an edit is a checkpoint, not an interruption.
101
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
102
+ a perfect section you never reached does not.
103
+
104
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
105
+
80
106
  ## Output
81
107
 
82
108
  ### For S-tier (inline)
@@ -73,6 +73,32 @@ Alternative considered: [Y], rejected because [reasons].
73
73
 
74
74
  After both complete, synthesize results sequentially.
75
75
 
76
+ ## Write discipline (the 180-second rule)
77
+
78
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
79
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
80
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
81
+ nothing was ever on disk.
82
+
83
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
84
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
85
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
86
+
87
+ So, in this step:
88
+
89
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/02_research.md` containing only the headings this step
90
+ requires (Codebase patterns · External patterns · Library evaluation · Anti-patterns ·
91
+ Recommended approach), one line of intent under each — before the research agents report back,
92
+ not after.
93
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
94
+ file readable; none of them is allowed to wait for the section after it.
95
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
96
+ stop and write what you have — an edit is a checkpoint, not an interruption.
97
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
98
+ a perfect section you never reached does not.
99
+
100
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
101
+
76
102
  ## Output
77
103
 
78
104
  Create `features/<slug>/02_research.md` with:
@@ -167,6 +167,32 @@ Each ADR must trace back to specific requirements:
167
167
  | Placeholder text | Template hints or TODOs remain | Remove or fill before checkpoint |
168
168
  | Supersession in-place | Existing ADR rewritten | Mint a new ADR and link it |
169
169
 
170
+ ## Write discipline (the 180-second rule)
171
+
172
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
173
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
174
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
175
+ nothing was ever on disk.
176
+
177
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
178
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
179
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
180
+
181
+ So, in this step:
182
+
183
+ 1. **Skeleton first — inside your first ~12 tool calls.** For each decision you have identified, write
184
+ `features/<slug>/03_adr/NNN-{decision-slug}.md` containing only the invariant-core headings
185
+ (Status · Context · Decision Drivers · Considered Options · Decision · Rationale · Consequences ·
186
+ Confirmation), one line of intent under each. A named-but-empty option beats an unwritten ADR.
187
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
188
+ file readable; none of them is allowed to wait for the section after it.
189
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
190
+ stop and write what you have — an edit is a checkpoint, not an interruption.
191
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
192
+ a perfect section you never reached does not.
193
+
194
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
195
+
170
196
  ## Output
171
197
 
172
198
  Create `features/<slug>/03_adr/` directory with:
@@ -154,6 +154,31 @@ AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if
154
154
  an existence test and never fires in production.
155
155
  - A cheap lint holds the shape: every `AM-N` line must contain a `→ test ` token naming a test that exists.
156
156
 
157
+ ## Write discipline (the 180-second rule)
158
+
159
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
160
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
161
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
162
+ nothing was ever on disk.
163
+
164
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
165
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
166
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
167
+
168
+ So, in this step:
169
+
170
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/03.5_ideation_report.md` containing only the eight
171
+ section headings of §5 (Executive summary … Amendments), one line of intent under each — while
172
+ the swarm is still running, not after every agent has returned.
173
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
174
+ file readable; none of them is allowed to wait for the section after it.
175
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
176
+ stop and write what you have — an edit is a checkpoint, not an interruption.
177
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
178
+ a perfect section you never reached does not.
179
+
180
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
181
+
157
182
  ## Output
158
183
 
159
184
  Create `features/<slug>/03.5_ideation_report.md` with full QCSD report.
@@ -92,6 +92,31 @@ Check that proposed domain model is compatible with existing code:
92
92
  - Can aggregates be implemented with existing ORM/framework?
93
93
  - Do relationships align with existing data access patterns?
94
94
 
95
+ ## Write discipline (the 180-second rule)
96
+
97
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
98
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
99
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
100
+ nothing was ever on disk.
101
+
102
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
103
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
104
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
105
+
106
+ So, in this step:
107
+
108
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/04_domain_model.md` containing only the headings this step
109
+ requires (Bounded context map · Ubiquitous language · Aggregates/Entities/VOs · Relationships ·
110
+ Codebase mapping), one line of intent under each.
111
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
112
+ file readable; none of them is allowed to wait for the section after it.
113
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
114
+ stop and write what you have — an edit is a checkpoint, not an interruption.
115
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
116
+ a perfect section you never reached does not.
117
+
118
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
119
+
95
120
  ## Output
96
121
 
97
122
  Create `features/<slug>/04_domain_model.md` with:
@@ -118,6 +118,32 @@ GET /api/v1/{resource}/:id
118
118
  Errors: 401, 404
119
119
  ```
120
120
 
121
+ ## Write discipline (the 180-second rule)
122
+
123
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
124
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
125
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
126
+ nothing was ever on disk.
127
+
128
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
129
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
130
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
131
+
132
+ So, in this step:
133
+
134
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/05_architecture.md` containing only the headings this step
135
+ requires at your tier (C4 Context · C4 Container · C4 Component · Sequence flows · Data & storage ·
136
+ API design), one line of intent under each. An empty `mermaid` fence with a caption is a
137
+ heading; fill it later.
138
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
139
+ file readable; none of them is allowed to wait for the section after it.
140
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
141
+ stop and write what you have — an edit is a checkpoint, not an interruption.
142
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
143
+ a perfect section you never reached does not.
144
+
145
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
146
+
121
147
  ## Output
122
148
 
123
149
  ### M-tier
@@ -135,6 +135,32 @@ Before finalizing the plan, validate completeness:
135
135
 
136
136
  This gap-check loop ensures the implementation plan is complete before coding begins.
137
137
 
138
+ ## Write discipline (the 180-second rule)
139
+
140
+ An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
141
+ runtime. Thinking time grows with the history already accumulated, so on a large repo "read everything,
142
+ then write the document" is not a risk — it is a deterministic death, and nothing survives it, because
143
+ nothing was ever on disk.
144
+
145
+ MEASURED on this harness: the writing steps died **18 times out of 18** in the reading phase without ever
146
+ writing a file. The control — same slice, same model, one added instruction to write a skeleton early —
147
+ landed the skeleton 8 minutes in, on the first attempt, after six consecutive deaths.
148
+
149
+ So, in this step:
150
+
151
+ 1. **Skeleton first — inside your first ~12 tool calls.** Write `features/<slug>/06_implementation_plan.md` containing only the headings
152
+ this step requires (Goal state · Tasks · Dependency order · Parallel groups · Checkpoints · Risk
153
+ assessment · Amendments · `EXPECTED_CODE_TARGETS:`), one line of intent under each. The K2 gate
154
+ reads this file — a plan that lives only in the conversation cannot pass it.
155
+ 2. **Then fill it one section per edit.** No single edit longer than ~120 lines. Every edit leaves the
156
+ file readable; none of them is allowed to wait for the section after it.
157
+ 3. **Never go more than 2 minutes without a tool call.** A thought that is getting long is the signal to
158
+ stop and write what you have — an edit is a checkpoint, not an interruption.
159
+ 4. **When you are unsure whether to read more or to write, WRITE.** A thin section refined later survives;
160
+ a perfect section you never reached does not.
161
+
162
+ The skeleton is not a draft to apologise for. It is the artifact, opened early.
163
+
138
164
  ## Output
139
165
 
140
166
  ### S-tier (minimal file — NOT inline-only)
@@ -44,6 +44,20 @@ function absolutizeRepo(raw, cwd) {
44
44
  }
45
45
  const ABSOLUTE_PATH_NOTE = ' All artifact paths in this prompt are ABSOLUTE. Read and write them exactly as given; do not cd first and do not re-relativize them.'
46
46
 
47
+ // WRITE DISCIPLINE — the anti-watchdog clause for every step whose DELIVERABLE IS A DOCUMENT.
48
+ // MEASURED (field report P14, two independent runs, agent journals on disk): Steps 5 and 6 never
49
+ // reached a single write — 18/18 attempts ended with FILE_WRITES=0, all of them killed inside the
50
+ // READING phase after >180 s of silence between tool calls; ~4M tokens and 1h54m per run bought
51
+ // nothing. Thinking time grows with accumulated history, so on a large repo this is DETERMINISTIC,
52
+ // not unlucky. The prompts said 'READ the code. Write <file>' with no reading budget and no order of
53
+ // operations, and the executor obeyed literally. Control experiment (owner, same slice/inputs/model,
54
+ // ONE added paragraph on write discipline): a 10-section skeleton landed 8 minutes in, on the FIRST
55
+ // attempt, after six consecutive deaths. One variable changed, opposite outcome. NOT CLAIMED: that
56
+ // this removes the watchdog class — it is a layer-4 prompt instruction, so it is probabilistic by
57
+ // construction; the deterministic half is the layer-1 test that this clause is PRESENT in every
58
+ // writing-step prompt (harness-core/test/feature-adr-write-discipline.test.ts).
59
+ const WRITE_DISCIPLINE = ' WRITE DISCIPLINE (mandatory, not advice). 1. Your FIRST file write must happen within your first 12 tool calls: create the SKELETON of the artifact — every section heading plus one line of intent under each. 2. Then fill it in ONE SECTION PER EDIT; no single edit longer than ~120 lines. 3. Never go more than 2 minutes without a tool call. 4. If you are unsure whether to read more or to write, WRITE. Reason: an executor that shows no activity for 180 seconds is KILLED by the runtime, so unbounded exploration before the first write makes the step die having written nothing at all.'
60
+
47
61
  const UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f]/
48
62
  const DOT_DOT_SEGMENT = /(^|\/)\.\.(\/|$)/
49
63
  function hasUnsafePathChars(p) { return UNSAFE_PATH_CHARS.test(String(p)) }
@@ -128,7 +142,7 @@ const CKPT_FILE = FDIR + '/.fa-state/checkpoints.jsonl'
128
142
  // M10 Stage-A, feature loop-designer). This region is now a GENERATED BLOB (regen-diff-gated by
129
143
  // loop-blobs-regen.test.ts): edit the canonical TS FIRST, run node scripts/gen-loop-blobs.mjs,
130
144
  // then re-splice. The value-pinned wiring tests in feature-adr-checkpoints.test.ts stay the net.
131
- // ── BEGIN BLOB checkpoints@1.1.0 sha256:aa730483f52a9f6263751138d4514fe9a6a3f4f191897c86d1e035f3da890574 src=packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts ──
145
+ // ── BEGIN BLOB checkpoints@1.1.0 sha256:d4dc6c956be3dd279b225bc5061ec4255babc44ffb5ca58646c9b6ea04a0c8c2 src=packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts ──
132
146
  const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];
133
147
  const STAGE_ARTIFACTS = {
134
148
  router: null,
@@ -139,7 +153,7 @@ const STAGE_ARTIFACTS = {
139
153
  fleet: '09_fleet_qe_assessment.md',
140
154
  };
141
155
  const CHECKPOINT_MAX_RESULT_CHARS = 12000;
142
- const CKPT_SCHEMA_VERSION = 'fa-ckpt-2';
156
+ const CKPT_SCHEMA_VERSION = 'fa-ckpt-3';
143
157
  function fnv1a(str) {
144
158
  let h = 0x811c9dc5;
145
159
  for (let i = 0; i < str.length; i++) {
@@ -157,6 +171,33 @@ function checkpointInputHash(stage, parts) {
157
171
  function resumeMode(raw) {
158
172
  return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';
159
173
  }
174
+ const DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];
175
+ function designStageKey(sub) {
176
+ return 'design:' + sub;
177
+ }
178
+ function decideDesignFanResume(opts) {
179
+ const missingSubstages = [];
180
+ opts.required.forEach((sub, i) => {
181
+ const r = opts.results[i];
182
+ if (r === null || r === undefined)
183
+ missingSubstages.push(sub);
184
+ });
185
+ const missingArtifacts = [];
186
+ let probeMissing = false;
187
+ if (opts.artifacts.length > 0) {
188
+ if (opts.postRunListing === null)
189
+ probeMissing = true;
190
+ else
191
+ for (const rel of opts.artifacts)
192
+ if (!opts.postRunListing.has(rel))
193
+ missingArtifacts.push(rel);
194
+ }
195
+ const reason = missingSubstages.length > 0 ? 'substage-missing'
196
+ : probeMissing ? 'probe-not-established'
197
+ : missingArtifacts.length > 0 ? 'artifact-missing'
198
+ : 'ok';
199
+ return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };
200
+ }
160
201
  function decideCheckpointResume(opts) {
161
202
  if (opts.mode === 'never')
162
203
  return { resume: false, reason: 'mode-never' };
@@ -236,10 +277,41 @@ function checkpointAppendCmd(fdirAbs, line) {
236
277
  const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');
237
278
  return 'mkdir -p ' + dir + " && printf '%s\\n' " + shellQuote(line) + ' >> ' + file;
238
279
  }
280
+ function parseArtifactProbe(opts) {
281
+ if (opts.stdout === null || opts.stdout === undefined)
282
+ return null;
283
+ const known = new Map();
284
+ for (const rel of opts.required)
285
+ known.set('HAVE:' + rel, rel);
286
+ const found = new Set();
287
+ let sentinels = 0;
288
+ for (const raw of String(opts.stdout).split('\n')) {
289
+ const line = raw.trim();
290
+ if (line === '')
291
+ continue;
292
+ if (line === opts.sentinel) {
293
+ sentinels++;
294
+ continue;
295
+ }
296
+ if (sentinels > 0)
297
+ return null;
298
+ const rel = known.get(line);
299
+ if (rel === undefined)
300
+ return null;
301
+ found.add(rel);
302
+ }
303
+ if (sentinels !== 1)
304
+ return null;
305
+ return found;
306
+ }
239
307
  // ── END BLOB checkpoints@1.1.0 ──
240
308
  // glue (workflow-owned, hand-reviewed): legacy mirror names → the blob exports. The hash
241
309
  // composition is UNCHANGED (same CKPT_SCHEMA_VERSION salt + JSON-tuple), so recorded
242
310
  // checkpoints from pre-wrap runs still resume.
311
+ // Run-specific, not a fixed literal: a fixed one is a string an ARTIFACT FILE could be named, and a
312
+ // listing is exactly a list of filenames. Derived from the slug + dir so it stays stable across a
313
+ // resume (the workflow sandbox has no randomness, and a random one would break nothing here anyway).
314
+ const FAN_LS_SENTINEL = '<<<DZ-FAN-LS-END:' + fnv1a64(SLUG + '\0' + FDIR) + '>>>'
243
315
  const CKPT_LS_SENTINEL = CHECKPOINT_LS_SENTINEL
244
316
  const CKPT_MAX = CHECKPOINT_MAX_RESULT_CHARS
245
317
  const STAGE_ARTIFACTS_CKPT = STAGE_ARTIFACTS
@@ -1450,20 +1522,20 @@ modelsUsed.ddd = modelLabel(archOpts)
1450
1522
  // record the exact input each design agent received. designPairMeta[i] aligns with designThunks[i],
1451
1523
  // which aligns with design[i] in the parallel result — one pair per design sub-stage.
1452
1524
  const designPairMeta = []
1453
- const reqPromptText = 'Step 1 (Requirements)' + (isLplus ? ' + Step 2 (Research)' : '') + ' of /feature-adr for "' + DESC + '" (tier ' + tier + ', slug ' + SLUG + '). Code: ' + CODE_HINT + '. APPLY these Step-0 recalled LEARNED PATTERNS (fold the applicable ones into requirements/constraints - the loop paying off): ' + LEARNED + '. Write ' + FDIR + '/01_requirements.md (functional + non-functional requirements, acceptance criteria, constraints, and an "Applied learned patterns" note).' + reqExtra + ' Return wrote[] + a 1-line summary.' + PS_GUIDANCE('design')
1454
- designThunks.push(() => designStage(reqPromptText, reqOpts, FDIR + '/01_requirements.md', 'requirements'))
1525
+ const reqPromptText = 'Step 1 (Requirements)' + (isLplus ? ' + Step 2 (Research)' : '') + ' of /feature-adr for "' + DESC + '" (tier ' + tier + ', slug ' + SLUG + '). Code: ' + CODE_HINT + '. APPLY these Step-0 recalled LEARNED PATTERNS (fold the applicable ones into requirements/constraints - the loop paying off): ' + LEARNED + '. Write ' + FDIR + '/01_requirements.md (functional + non-functional requirements, acceptance criteria, constraints, and an "Applied learned patterns" note).' + reqExtra + ' Return wrote[] + a 1-line summary.' + PS_GUIDANCE('design') + WRITE_DISCIPLINE
1526
+ designThunks.push(() => subCheckpoint('requirements', reqPromptText, reqOpts, ['01_requirements.md'].concat(isLplus ? ['02_research.md'] : []), () => designStage(reqPromptText, reqOpts, FDIR + '/01_requirements.md', 'requirements')))
1455
1527
  // the requirements prompt is where the Step-0 recalled lessons are INJECTED — recorded per pair
1456
1528
  designPairMeta.push({ role: 'design:requirements', prompt: reqPromptText, model: modelLabel(reqOpts), lessons: (LEARNED && LEARNED !== 'none recalled') ? [String(LEARNED)] : [] })
1457
1529
  if (isMplus) {
1458
- const adrPromptText = 'Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-' + SLUG + '.md as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb — the auto-filename tracks the feature slug, so the IMPERATIVE signal lives in the title); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.'
1459
- designThunks.push(() => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr'))
1530
+ const adrPromptText = 'Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-' + SLUG + '.md as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb — the auto-filename tracks the feature slug, so the IMPERATIVE signal lives in the title); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.' + WRITE_DISCIPLINE
1531
+ designThunks.push(() => subCheckpoint('adr', adrPromptText, adrOpts, ['03_adr/001-' + SLUG + '.md'], () => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr')))
1460
1532
  designPairMeta.push({ role: 'design:adr', prompt: adrPromptText, model: modelLabel(adrOpts), lessons: [] })
1461
- const qcsdPromptText = 'Step 3.5 (QCSD ideation swarm - HTSM quality criteria + SFDIPOT risk) of /feature-adr for "' + DESC + '" (' + SLUG + '). Assess quality criteria + product-factors risk. Write ' + FDIR + '/03.5_ideation_report.md with a GO/CONDITIONAL/NO-GO verdict + top quality risks for QE. On a CONDITIONAL verdict, write each condition as an amendment row in a `## Amendments` section. ' + AMENDMENT_RULE + ' Return wrote[] + summary.'
1462
- designThunks.push(() => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd'))
1533
+ const qcsdPromptText = 'Step 3.5 (QCSD ideation swarm - HTSM quality criteria + SFDIPOT risk) of /feature-adr for "' + DESC + '" (' + SLUG + '). Assess quality criteria + product-factors risk. Write ' + FDIR + '/03.5_ideation_report.md with a GO/CONDITIONAL/NO-GO verdict + top quality risks for QE. On a CONDITIONAL verdict, write each condition as an amendment row in a `## Amendments` section. ' + AMENDMENT_RULE + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
1534
+ designThunks.push(() => subCheckpoint('qcsd', qcsdPromptText, qcsdOpts, ['03.5_ideation_report.md'], () => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd')))
1463
1535
  designPairMeta.push({ role: 'design:qcsd', prompt: qcsdPromptText, model: modelLabel(qcsdOpts), lessons: [] })
1464
1536
  const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
1465
- const archPromptText = (isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape).' + archExtra + ' Return wrote[] + summary.'
1466
- designThunks.push(() => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture'))
1537
+ const archPromptText = (isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape).' + archExtra + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
1538
+ designThunks.push(() => subCheckpoint('architecture', archPromptText, archOpts, ['05_architecture.md'].concat(isLplus ? ['04_domain_model.md'] : []), () => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture')))
1467
1539
  designPairMeta.push({ role: 'design:architecture', prompt: archPromptText, model: modelLabel(archOpts), lessons: [] })
1468
1540
  }
1469
1541
  // design checkpoint: ONE composite entry for the whole parallel design fan (requirements/ADR/QCSD/
@@ -1476,10 +1548,145 @@ const designHash = ckptHash('design', [tier, DESC, CODE_HINT, fnv1a64(String(LEA
1476
1548
  const designArtifacts = ['01_requirements.md']
1477
1549
  if (isMplus) designArtifacts.push('03_adr/001-' + SLUG + '.md', '03.5_ideation_report.md', '05_architecture.md')
1478
1550
  if (isLplus) designArtifacts.push('02_research.md', '04_domain_model.md')
1479
- const design = await withCheckpoint('design', 'Design', designHash, async () => parallel(designThunks), { artifacts: designArtifacts })
1551
+ /**
1552
+ * PER-SIBLING checkpoint (SP-2, no-amplification). Before 2026-08-20 the whole fan shared ONE
1553
+ * all-or-nothing entry, so a single dead agent discarded three finished siblings — and because every
1554
+ * downstream hash is content-addressed on the design RESULT, re-running them produced different
1555
+ * summaries and a completed plan then read `stale-input` too. One death re-spent the entire run,
1556
+ * every time, which is why the field report saw 18 attempts rather than 6.
1557
+ *
1558
+ * `withCheckpoint` needs no change for this: it already does its own lookup, hash, artifact probe
1559
+ * and append per call. Nesting is the whole fix.
1560
+ */
1561
+ function subCheckpoint(sub, prompt, opts, artifacts, thunk) {
1562
+ const key = 'design:' + sub
1563
+ // SP-3 (independence). The hash carries THIS sibling's own steering inputs — including its PROMPT
1564
+ // TEXT (owner decision 2026-08-20). Two consequences, both intended:
1565
+ // • a fix to one step's instructions invalidates exactly that step and nothing else, so a feature
1566
+ // still IN FLIGHT picks the corrected prompt up on its next invocation. A feature that already
1567
+ // finished is never re-invoked, so its written artifacts are untouched — which is the line the
1568
+ // owner drew: correct what has not run, leave alone what has.
1569
+ // • changing the ADR model no longer invalidates requirements. The old fan hash lumped every
1570
+ // model spec together, so one dial moved four stages.
1571
+ // The hash carries what actually steers THIS sibling, and nothing else. Review round 1 measured
1572
+ // both directions of wrongness in the first version:
1573
+ // OVER-invalidating — FIXED, by DELETION. CODEX_MODEL was folded in unconditionally, so changing
1574
+ // args.codexModel invalidated a sibling explicitly pinned to `sonnet`. Round 1 narrowed it to
1575
+ // Codex siblings; round 2 showed that was still wrong — a sibling pinned to
1576
+ // 'codex:gpt-5.6-sol:high' resolves its own id, yet still moved when the global dial moved. It
1577
+ // is gone entirely, because `modelLabel(opts)` — already in this hash — renders
1578
+ // 'codex:<resolvedId>:<reasoning>', and specToOpts resolves a bare 'codex' spec THROUGH
1579
+ // CODEX_MODEL. So the effective model is captured in every case, pinned or inherited, and a
1580
+ // second copy of it could only ever add false invalidation.
1581
+ // UNDER-invalidating — NAMED, NOT FIXED, because it cannot be fixed here. Editing the CONTENT of
1582
+ // a project-skills guidance file without changing which files exist leaves this hash unchanged,
1583
+ // so a stale sibling resumes. The reason is structural: the Step-0 probe deliberately returns
1584
+ // only {hasManifest, report} and DROPS the guidance text ("drop the big design/code/qe
1585
+ // strings"), so the workflow never holds the content to hash. My first fix claimed the prompt
1586
+ // covered it via PS_GUIDANCE — MEASURED FALSE: of the four sibling prompts only `requirements`
1587
+ // embeds PS_GUIDANCE; adr, qcsd and architecture do not. POLY.report therefore stays in the
1588
+ // hash as the best available proxy — it names the source files, so adding or removing one does
1589
+ // invalidate. Closing this properly means carrying a content digest out of the probe, which is
1590
+ // a change to the probe, not to this line.
1591
+ const h = ckptHash(key, [tier, DESC, CODE_HINT, fnv1a64(String(prompt)), modelLabel(opts), POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, sub === 'requirements' ? fnv1a64(String(LEARNED)) : null])
1592
+ return withCheckpoint(key, 'Design', h, thunk, { artifacts: artifacts })
1593
+ }
1594
+
1595
+ const designSubsRequired = ['requirements'].concat(isMplus ? ['adr', 'qcsd', 'architecture'] : [])
1596
+ const designFan = await parallel(designThunks)
1597
+ // SP-1 (completeness) — what may be CONSUMED is a different question from what may be WRITTEN. The
1598
+ // old code answered the second by crippling the first. Codex QE #2 (a one-file probe accepting a
1599
+ // design with no ADR) is preserved here, on the read side where it belongs.
1600
+ // LIVE results, never the start-of-run snapshot. Cross-family review round 1 (grade D) caught the
1601
+ // snapshot version: a stale non-null entry from a PREVIOUS run survives in it even when this run's
1602
+ // retry returned null, so an incomplete design would be declared complete — reopening the very hole
1603
+ // the old all-or-nothing gate existed to close. A sibling that returns non-null wrote its artifact;
1604
+ // a sibling that died returns null. Nothing needs re-probing to know that.
1605
+ // The artifact half needs a listing taken AFTER the fan, and nothing else will do. Round 2 caught the
1606
+ // version fed CKPT_LISTING (taken once at run start, so on a fresh slug it cannot contain what the fan
1607
+ // is about to write — every fresh M+ run read as incomplete). Round 3 caught the version with no probe
1608
+ // at all: an L-tier requirements sibling that writes 01_requirements.md, skips 02_research.md and
1609
+ // returns non-null was accepted, and Step 6 planned with no research behind it. A non-null result is
1610
+ // the agent's own word about its own work. One effort-low `find` is what turns it into evidence.
1611
+ // The probe must PROVE it ran, and it must not be forgeable by the very thing it inspects.
1612
+ // Three rounds of review taught the shape:
1613
+ // • round 4 — `|| true` around a suppressed listing made an unreadable FDIR indistinguishable from
1614
+ // an empty one, so the refusal named the wrong cause and printed the wrong repair;
1615
+ // • round 5 — `find | sed; echo SENT` emitted the sentinel even when find itself failed, because sed
1616
+ // masks the pipeline status;
1617
+ // • round 6 — and the deeper problem: a LISTING is a list of filenames, so the data can impersonate
1618
+ // the frame. A file named "01_requirements.md\n" prints as a line reading `01_requirements.md`
1619
+ // plus a blank one, and satisfied a requirement for the real file. No amount of sentinel hardening
1620
+ // fixes that, because the forgery is inside the payload.
1621
+ // So the probe no longer prints filenames AT ALL. It asks `[ -f <exact rel> ]` once per required
1622
+ // artifact and echoes a fixed token we already know the text of. A filename can no longer produce a
1623
+ // line, so it can no longer forge one — and `-f` on an exact path cannot be satisfied by a name that
1624
+ // merely renders like it. MEASURED red/green below in a real shell against a newline-bearing filename.
1625
+ let fanChecks = ''
1626
+ for (const rel of designArtifacts) fanChecks += '[ -f ' + shq(rel) + ' ] && echo ' + shq('HAVE:' + rel) + '; '
1627
+ const fanLsCmd = 'cd ' + shq(FDIR) + ' 2>/dev/null && { ' + fanChecks + 'echo ' + shq(FAN_LS_SENTINEL) + '; } || true'
1628
+ const fanLsOut = await agent('Run EXACTLY this via Bash and return its stdout VERBATIM (it may be empty) with NO code fences and NO commentary: ' + fanLsCmd, { label: 'design:artifact-probe', phase: 'Design', effort: 'low' })
1629
+ // The transcript is validated STRICTLY, not scanned. Round 7 measured the difference: an agent that
1630
+ // narrates ("Expected output when present: HAVE:01_requirements.md … Actual stdout: …") emits a line
1631
+ // byte-identical to the real token, and a parser that merely LOOKED for the token passed a design whose
1632
+ // artifact did not exist. parseArtifactProbe accepts only a subset of the known tokens followed by
1633
+ // exactly one sentinel and nothing else; anything unexpected returns null — inconclusive, never a pass.
1634
+ // It cannot stop an agent that deliberately emits the exact expected transcript; that residual is the
1635
+ // same trust the checkpoint reader and the Step-7.5 landing barrier already place in a relaying agent.
1636
+ const fanListing = parseArtifactProbe({ stdout: fanLsOut, sentinel: FAN_LS_SENTINEL, required: designArtifacts })
1637
+ if (fanListing === null) log('design artifact probe NOT ESTABLISHED — the transcript was not the command\'s own output (no completion sentinel, a second one, or an unexpected line). This is inconclusive, not clean.')
1638
+ const fanVerdict = decideDesignFanResume({ results: designFan, required: designSubsRequired, artifacts: designArtifacts, postRunListing: fanListing })
1639
+ const design = designFan
1480
1640
  // training pairs: one per design sub-stage (designPairMeta[i] ↔ design[i]); a null sub-result emits
1481
1641
  // no pair (a dead agent produced no output). Design has no per-stage QE grade — grade:null honestly.
1482
- if (design) await capturePairs('design', 'Design', designPairMeta.map(function (m, i) { return { input: m.prompt, output: design[i], evaluation: { grade: null, gradedBy: null, lessonsInjected: m.lessons }, provenance: { model: m.model, family: tpFamily(m.model), role: m.role } } }))
1642
+ // Pair capture has to answer TWO questions per-sibling that the whole-stage guard inside capturePairs
1643
+ // cannot: which siblings produced output THIS run, and whether the stage as a whole is a resume.
1644
+ // • The guard tests resumedStages for the exact name 'design'; per-sibling checkpointing records
1645
+ // 'design:requirements' instead, so every repair run read as wholly-live and re-appended the
1646
+ // resumed siblings' pairs (round 3).
1647
+ // • Filtering the resumed siblings out unconditionally then killed the BACKFILL path — the recovery
1648
+ // that exists for a run whose pairs were never written (round 4). Backfill is file-absence-guarded,
1649
+ // so it only ever recovers a wholly-missing file, and that is exactly the all-resumed case.
1650
+ // So: all four resumed ⇒ the stage IS a resume, hand capturePairs every record under a guard stage it
1651
+ // will read as resumed, and let the absence guard decide. Otherwise ⇒ emit only what ran this run.
1652
+ const designPairRecords = designPairMeta.map(function (m, i) { return { input: m.prompt, output: design ? design[i] : null, evaluation: { grade: null, gradedBy: null, lessonsInjected: m.lessons }, provenance: { model: m.model, family: tpFamily(m.model), role: m.role } } })
1653
+ const designAllResumed = designSubsRequired.length > 0 && designSubsRequired.every(function (sub) { return resumedStages.indexOf('design:' + sub) !== -1 })
1654
+ // NAMED, not fixed: a MIXED repair run whose earlier pair file was lost entirely does not recover the
1655
+ // resumed siblings' pairs — backfill would skip anyway once the file exists, and re-appending them in
1656
+ // capture mode is the duplication round 3 caught. Recovering that case needs per-record dedup inside
1657
+ // capturePairs, which is a change to the capture layer, not to this call.
1658
+ if (design && designAllResumed) await capturePairs('design', 'Design', designPairRecords, 'design:' + designSubsRequired[0])
1659
+ else if (design) await capturePairs('design', 'Design', designPairRecords.map(function (r, i) { return (resumedStages.indexOf('design:' + designSubsRequired[i]) !== -1) ? null : r }))
1660
+
1661
+ // SP-1 ENFORCED. Round 2 was right that the previous line only LOGGED: the comment above promised a
1662
+ // read-side gate and the body handed a fan containing nulls straight to Step 6, which then planned
1663
+ // off a missing ADR — the exact outcome Codex QE #2 named. Refusing here is cheap precisely because
1664
+ // this feature made the fan resumable: every sibling that DID finish is checkpointed, so the repair
1665
+ // re-invoke re-spends only the missing ones. (Pairs are captured first: what ran deserves its record.)
1666
+ if (!fanVerdict.complete) {
1667
+ const missing = fanVerdict.missingSubstages.join(', ')
1668
+ const missingArt = fanVerdict.missingArtifacts.join(', ')
1669
+ const what = fanVerdict.reason === 'substage-missing' ? 'sub-stage(s) [' + missing + '] returned nothing (a dead or limit-exhausted agent)'
1670
+ : fanVerdict.reason === 'probe-not-established' ? 'the post-run artifact probe could not be read, so completeness is NOT ESTABLISHED (never a pass)'
1671
+ : 'the design artifact(s) [' + missingArt + '] are absent although every sub-stage reported success — an agent said it wrote a file it did not write'
1672
+ // Honest about the repair: "resume for free" is only true when resume is actually available. With
1673
+ // checkpoints off or resume:'never' the whole fan re-runs, and saying otherwise would misprice the retry.
1674
+ // resume:'force' deliberately trusts the input hash and SKIPS the artifact probe, so a sibling that
1675
+ // reported success without writing its file resumes forever and this gate refuses forever. Telling
1676
+ // that operator to "just re-invoke" would be a loop, not a repair (round 5).
1677
+ const repair = (CHECKPOINTS_ON && RESUME_MODE === 'force' && fanVerdict.reason === 'artifact-missing')
1678
+ ? "HOW TO REPAIR: this run used resume:'force', which skips the artifact probe when resuming — so the sibling that reported success without writing [" + missingArt + "] would resume again and hit this same refusal. Re-invoke with args.resume='never' (or delete features/" + SLUG + "/.fa-state/) to make it actually re-run."
1679
+ : (CHECKPOINTS_ON && RESUME_MODE !== 'never')
1680
+ ? 'HOW TO REPAIR: re-invoke with the SAME slug — the sub-stages that finished are individually checkpointed and resume for free, so only the failing one re-runs.'
1681
+ : 'HOW TO REPAIR: re-invoke with the SAME slug. NOTE: this run had ' + (CHECKPOINTS_ON ? "resume:'never', so its finished siblings ARE recorded — drop that argument and the retry re-runs only the failing one" : 'checkpoints disabled, so this run recorded NOTHING — dropping args.checkpoints:false does not make THIS retry cheap (the whole fan re-runs and is re-spent once more); it only makes the run AFTER it cheap') + '.'
1682
+ log('design fan INCOMPLETE (' + fanVerdict.reason + ') — ' + what + '. REFUSING to plan off a partial design.')
1683
+ // A run that stops here still SPENT its router + design phases, so it belongs in the cost ledger —
1684
+ // the same reason the K2 plan-gate refusal logs one. An unlogged refusal makes design stops
1685
+ // invisible to cost analysis, which is how a stage that keeps dying stays cheap-looking.
1686
+ // (coderUsed/qe are the outer bindings, both still null here, so the row reports null honestly.)
1687
+ await appendRunCostRow('design-gate', 'Design')
1688
+ return { tier: tier, phase: 'design-incomplete', slug: SLUG, artifactsDir: FDIR, missingSubstages: fanVerdict.missingSubstages, missingArtifacts: fanVerdict.missingArtifacts, reason: fanVerdict.reason, modelsUsed: modelsUsed, gates: { design: fanVerdict.reason === 'probe-not-established' ? 'not-established' : 'incomplete', plan: 'not-run', planCompleteness: 'not-run', challengePanel: 'not-run', code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'REFUSED at the Step-5/6 boundary: ' + what + ', so the design is incomplete and Step 6 was NOT dispatched. Planning off a partial design produces a plan with no ADR behind it. ' + repair + ' If a sibling died on a Claude limit, add usage-adaptive routing or route that stage to Codex first (args.models). To rebuild the whole design from scratch instead, re-invoke with args.resume=\'never\'.' }
1689
+ }
1483
1690
 
1484
1691
  // Step 6: Plan — optionally routed to Codex's top model (opt-in via args.planner='codex').
1485
1692
  // The user opts in at pre-flight ('use the top Codex model for planning?'); we route the Plan step to
@@ -1487,7 +1694,7 @@ if (design) await capturePairs('design', 'Design', designPairMeta.map(function (
1487
1694
  // unavailable/errors — the pipeline never blocks on Codex.
1488
1695
  phase('Plan')
1489
1696
  await usageProbe('Plan')
1490
- const planPrompt = 'Step 6 (SPARC-GOAP implementation plan) of /feature-adr for "' + DESC + '" (' + SLUG + ', tier ' + tier + '). Given the requirements + ADR + architecture in ' + FDIR + ', 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/. 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
1697
+ const planPrompt = 'Step 6 (SPARC-GOAP implementation plan) of /feature-adr for "' + DESC + '" (' + SLUG + ', tier ' + tier + '). Given the requirements + ADR + architecture in ' + FDIR + ', 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/. 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
1491
1698
  // Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
1492
1699
  // DEFAULT_MODELS/coder-fold) or the DEFAULT_MODELS.plan ('sonnet') under routing; else {} (BC).
1493
1700
  const planModel = resolveStageModel('plan')