@dzhechkov/skills-feature-adr 1.3.67 → 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 +5 -5
- package/CHANGELOG.md +35 -0
- package/README.md +30 -0
- package/package.json +1 -1
- package/sbom.json +4 -4
- package/templates/.claude/workflows/feature-adr.js +201 -8
package/.dz-manifest.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "CHANGELOG.md",
|
|
8
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
28
|
+
"sha256": "c1e4d257167fd2462e0c8e09f248fc1aef332aa34fb09dddd5940bdcc30ea34b"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "scripts/sync-templates.js",
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "templates/.claude/workflows/feature-adr.js",
|
|
324
|
-
"sha256": "
|
|
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": "
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/skills-feature-adr",
|
|
3
|
-
"version": "1.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
68
|
+
"content": "c1e4d257167fd2462e0c8e09f248fc1aef332aa34fb09dddd5940bdcc30ea34b"
|
|
69
69
|
}
|
|
70
70
|
]
|
|
71
71
|
},
|
|
@@ -805,7 +805,7 @@
|
|
|
805
805
|
"hashes": [
|
|
806
806
|
{
|
|
807
807
|
"alg": "SHA-256",
|
|
808
|
-
"content": "
|
|
808
|
+
"content": "1be239d2f02612bc33027767d9930eae8a2d6d5afd427d1485198bbdd821a3c7"
|
|
809
809
|
}
|
|
810
810
|
]
|
|
811
811
|
},
|
|
@@ -142,7 +142,7 @@ const CKPT_FILE = FDIR + '/.fa-state/checkpoints.jsonl'
|
|
|
142
142
|
// M10 Stage-A, feature loop-designer). This region is now a GENERATED BLOB (regen-diff-gated by
|
|
143
143
|
// loop-blobs-regen.test.ts): edit the canonical TS FIRST, run node scripts/gen-loop-blobs.mjs,
|
|
144
144
|
// then re-splice. The value-pinned wiring tests in feature-adr-checkpoints.test.ts stay the net.
|
|
145
|
-
// ── BEGIN BLOB checkpoints@1.1.0 sha256:
|
|
145
|
+
// ── BEGIN BLOB checkpoints@1.1.0 sha256:d4dc6c956be3dd279b225bc5061ec4255babc44ffb5ca58646c9b6ea04a0c8c2 src=packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts ──
|
|
146
146
|
const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];
|
|
147
147
|
const STAGE_ARTIFACTS = {
|
|
148
148
|
router: null,
|
|
@@ -153,7 +153,7 @@ const STAGE_ARTIFACTS = {
|
|
|
153
153
|
fleet: '09_fleet_qe_assessment.md',
|
|
154
154
|
};
|
|
155
155
|
const CHECKPOINT_MAX_RESULT_CHARS = 12000;
|
|
156
|
-
const CKPT_SCHEMA_VERSION = 'fa-ckpt-
|
|
156
|
+
const CKPT_SCHEMA_VERSION = 'fa-ckpt-3';
|
|
157
157
|
function fnv1a(str) {
|
|
158
158
|
let h = 0x811c9dc5;
|
|
159
159
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -171,6 +171,33 @@ function checkpointInputHash(stage, parts) {
|
|
|
171
171
|
function resumeMode(raw) {
|
|
172
172
|
return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';
|
|
173
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
|
+
}
|
|
174
201
|
function decideCheckpointResume(opts) {
|
|
175
202
|
if (opts.mode === 'never')
|
|
176
203
|
return { resume: false, reason: 'mode-never' };
|
|
@@ -250,10 +277,41 @@ function checkpointAppendCmd(fdirAbs, line) {
|
|
|
250
277
|
const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');
|
|
251
278
|
return 'mkdir -p ' + dir + " && printf '%s\\n' " + shellQuote(line) + ' >> ' + file;
|
|
252
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
|
+
}
|
|
253
307
|
// ── END BLOB checkpoints@1.1.0 ──
|
|
254
308
|
// glue (workflow-owned, hand-reviewed): legacy mirror names → the blob exports. The hash
|
|
255
309
|
// composition is UNCHANGED (same CKPT_SCHEMA_VERSION salt + JSON-tuple), so recorded
|
|
256
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) + '>>>'
|
|
257
315
|
const CKPT_LS_SENTINEL = CHECKPOINT_LS_SENTINEL
|
|
258
316
|
const CKPT_MAX = CHECKPOINT_MAX_RESULT_CHARS
|
|
259
317
|
const STAGE_ARTIFACTS_CKPT = STAGE_ARTIFACTS
|
|
@@ -1465,19 +1523,19 @@ modelsUsed.ddd = modelLabel(archOpts)
|
|
|
1465
1523
|
// which aligns with design[i] in the parallel result — one pair per design sub-stage.
|
|
1466
1524
|
const designPairMeta = []
|
|
1467
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
|
|
1468
|
-
designThunks.push(() => designStage(reqPromptText, reqOpts, FDIR + '/01_requirements.md', 'requirements'))
|
|
1526
|
+
designThunks.push(() => subCheckpoint('requirements', reqPromptText, reqOpts, ['01_requirements.md'].concat(isLplus ? ['02_research.md'] : []), () => designStage(reqPromptText, reqOpts, FDIR + '/01_requirements.md', 'requirements')))
|
|
1469
1527
|
// the requirements prompt is where the Step-0 recalled lessons are INJECTED — recorded per pair
|
|
1470
1528
|
designPairMeta.push({ role: 'design:requirements', prompt: reqPromptText, model: modelLabel(reqOpts), lessons: (LEARNED && LEARNED !== 'none recalled') ? [String(LEARNED)] : [] })
|
|
1471
1529
|
if (isMplus) {
|
|
1472
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
|
|
1473
|
-
designThunks.push(() => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr'))
|
|
1531
|
+
designThunks.push(() => subCheckpoint('adr', adrPromptText, adrOpts, ['03_adr/001-' + SLUG + '.md'], () => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr')))
|
|
1474
1532
|
designPairMeta.push({ role: 'design:adr', prompt: adrPromptText, model: modelLabel(adrOpts), lessons: [] })
|
|
1475
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
|
|
1476
|
-
designThunks.push(() => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd'))
|
|
1534
|
+
designThunks.push(() => subCheckpoint('qcsd', qcsdPromptText, qcsdOpts, ['03.5_ideation_report.md'], () => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd')))
|
|
1477
1535
|
designPairMeta.push({ role: 'design:qcsd', prompt: qcsdPromptText, model: modelLabel(qcsdOpts), lessons: [] })
|
|
1478
1536
|
const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
|
|
1479
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
|
|
1480
|
-
designThunks.push(() => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture'))
|
|
1538
|
+
designThunks.push(() => subCheckpoint('architecture', archPromptText, archOpts, ['05_architecture.md'].concat(isLplus ? ['04_domain_model.md'] : []), () => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture')))
|
|
1481
1539
|
designPairMeta.push({ role: 'design:architecture', prompt: archPromptText, model: modelLabel(archOpts), lessons: [] })
|
|
1482
1540
|
}
|
|
1483
1541
|
// design checkpoint: ONE composite entry for the whole parallel design fan (requirements/ADR/QCSD/
|
|
@@ -1490,10 +1548,145 @@ const designHash = ckptHash('design', [tier, DESC, CODE_HINT, fnv1a64(String(LEA
|
|
|
1490
1548
|
const designArtifacts = ['01_requirements.md']
|
|
1491
1549
|
if (isMplus) designArtifacts.push('03_adr/001-' + SLUG + '.md', '03.5_ideation_report.md', '05_architecture.md')
|
|
1492
1550
|
if (isLplus) designArtifacts.push('02_research.md', '04_domain_model.md')
|
|
1493
|
-
|
|
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
|
|
1494
1640
|
// training pairs: one per design sub-stage (designPairMeta[i] ↔ design[i]); a null sub-result emits
|
|
1495
1641
|
// no pair (a dead agent produced no output). Design has no per-stage QE grade — grade:null honestly.
|
|
1496
|
-
|
|
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
|
+
}
|
|
1497
1690
|
|
|
1498
1691
|
// Step 6: Plan — optionally routed to Codex's top model (opt-in via args.planner='codex').
|
|
1499
1692
|
// The user opts in at pre-flight ('use the top Codex model for planning?'); we route the Plan step to
|