@dzhechkov/skills-feature-adr 1.5.3 → 1.5.5
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 +9 -9
- package/README.md +22 -2
- package/package.json +1 -1
- package/sbom.json +13 -9
- package/templates/.claude/skills/feature-adr/modules/05-architecture.md +26 -0
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +17 -2
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +19 -0
- package/templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs +64 -10
- package/templates/.claude/workflows/feature-adr.js +172 -18
package/.dz-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version":
|
|
3
|
+
"version": 3,
|
|
4
4
|
"pack": "skills-feature-adr",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "README.md",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "bc3de443b7ec23acea734d141164c15a0dc9b262db293581e26b8ebe3b93cc44"
|
|
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": "d027e16c3e24d354656f1463b9d7d7c1514259f69524644f1bd4314db8b960d1"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "src/cli.js",
|
|
@@ -145,11 +145,11 @@
|
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"path": "templates/.claude/skills/feature-adr/modules/05-architecture.md",
|
|
148
|
-
"sha256": "
|
|
148
|
+
"sha256": "4d20dbc85ea5bb93ca90fd8cd089255bab9c76a912ca3ad954f3a4bb670f3c45"
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
|
|
152
|
-
"sha256": "
|
|
152
|
+
"sha256": "c624e0ade0314ca07913ba64e092d4655f86201856e8188c3e616b2320226c1e"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"path": "templates/.claude/skills/feature-adr/modules/07-code.md",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
|
|
160
|
-
"sha256": "
|
|
160
|
+
"sha256": "ea8fb546acb7f09816d2fc9f9189a86f2400b8cec91c9ba4b82b68a8e32920bb"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
},
|
|
250
250
|
{
|
|
251
251
|
"path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
|
|
252
|
-
"sha256": "
|
|
252
|
+
"sha256": "630eb9047258ad0333082f407cd34522e4c278650f22ef6dba1ef897ec5cce71"
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"path": "templates/.claude/skills/frontend-design/LICENSE.txt",
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
},
|
|
314
314
|
{
|
|
315
315
|
"path": "templates/.claude/workflows/feature-adr.js",
|
|
316
|
-
"sha256": "
|
|
316
|
+
"sha256": "587b36287ce9e9b85b1dfc41fc50d95297f5ac6be32e1e702e0b89b5df0a7892"
|
|
317
317
|
},
|
|
318
318
|
{
|
|
319
319
|
"path": "templates/lib/memory-protocol.md",
|
|
@@ -325,5 +325,5 @@
|
|
|
325
325
|
}
|
|
326
326
|
]
|
|
327
327
|
},
|
|
328
|
-
"signature": "
|
|
328
|
+
"signature": "QqAf37Y10ocnldYVOw3ci5jzMrd3XIcmHIx6thuagPMZlIoVYHHp74jJX4kHkHvRFNdBF7+O51/F3/7d9jDKAA=="
|
|
329
329
|
}
|
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ refused BY NAME: `path traversal ('..' segment)`, `degenerate path segment`, `em
|
|
|
169
169
|
`illegal character '<c>'`, `empty stem after the leading dot`. The old substring traversal test also
|
|
170
170
|
rejected the ordinary filename `foo..bar.ts`; it is a segment test now.
|
|
171
171
|
|
|
172
|
-
### The Step-8 amendment gate runs a command instead of judging (v1.5.
|
|
172
|
+
### The Step-8 amendment gate runs a command instead of judging (v1.5.5)
|
|
173
173
|
|
|
174
174
|
Every `AM-N` row must resolve to a test found INSIDE the file the row names, and the check is
|
|
175
175
|
`dz amendment-check --slug <slug> --json` rather than a paragraph asking the reviewer to confirm it.
|
|
@@ -1113,7 +1113,27 @@ deliberately excluded — "skeleton first" is nonsense there.
|
|
|
1113
1113
|
|
|
1114
1114
|
## Status
|
|
1115
1115
|
|
|
1116
|
-
`1.5.
|
|
1116
|
+
`1.5.4` — **Step 5 now asks how the shipped feature will be watched.** The architecture artifact must
|
|
1117
|
+
carry a section headed exactly `Observability` answering what the feature logs, what it counts, what a
|
|
1118
|
+
failure looks like from outside, and who would notice. **"Nothing to observe" is a complete answer** —
|
|
1119
|
+
a pure refactor or a CI-only gate genuinely emits nothing, and a check that cannot express a true fact
|
|
1120
|
+
gets switched off. What is not acceptable is leaving the question unanswered. MEASURED before the
|
|
1121
|
+
change: the word appeared nowhere in the pipeline's prompts, and ZERO of 107 architecture artifacts
|
|
1122
|
+
carried such a section, while two fully-written observability skills (818 and 946 lines) sat
|
|
1123
|
+
unreachable because nothing called them. `dz score` reports the answer as a discipline — descriptive,
|
|
1124
|
+
never a gate, because the whole existing corpus predates the requirement.
|
|
1125
|
+
|
|
1126
|
+
Also in the bundled workflow: the K2 gate stops GUESSING the workspace. When it was not pinned, the
|
|
1127
|
+
workspace candidate was taken from the gate agent's own working directory, which against an external
|
|
1128
|
+
target repo resolved to that repo — so the candidate silently became a duplicate of the repo
|
|
1129
|
+
candidate and a skill installed in the workspace was never found (`NOT-ESTABLISHED`, exit 3, the
|
|
1130
|
+
coding step never ran). New `args.workspace` pins it, the shipped call site passes it, each candidate
|
|
1131
|
+
is labelled in the audit line, and a `K2_GATE_NOTE` fires when two collapse onto one path. And a
|
|
1132
|
+
pre-code probe that returns nothing no longer becomes an all-null baseline that later reads as
|
|
1133
|
+
"every target changed".
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
`1.5.3` — **the workflow stops crashing on the way into Step 7.** `1.5.2` shipped a workflow that
|
|
1117
1137
|
CALLED three helpers it never defined — `changeSetProbeCmd`, `parseHashProbe`, `changedFromHashes`
|
|
1118
1138
|
(5 call sites, 0 definitions). `QE_SCOPE` defaults to `uncommitted`, so the guarded branch was true
|
|
1119
1139
|
by default and every ultracode run that reached the coding step died with a `ReferenceError` while
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/skills-feature-adr",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
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
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"hashes": [
|
|
36
36
|
{
|
|
37
37
|
"alg": "SHA-256",
|
|
38
|
-
"content": "
|
|
38
|
+
"content": "bc3de443b7ec23acea734d141164c15a0dc9b262db293581e26b8ebe3b93cc44"
|
|
39
39
|
}
|
|
40
40
|
]
|
|
41
41
|
},
|
|
@@ -62,10 +62,14 @@
|
|
|
62
62
|
{
|
|
63
63
|
"type": "file",
|
|
64
64
|
"name": "package.json",
|
|
65
|
-
"
|
|
65
|
+
"properties": [
|
|
66
66
|
{
|
|
67
|
-
"
|
|
68
|
-
"
|
|
67
|
+
"name": "dz:digest-basis",
|
|
68
|
+
"value": "package-json-ordered-conditions-v2"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "dz:canonical-json-sha256-v2",
|
|
72
|
+
"value": "d027e16c3e24d354656f1463b9d7d7c1514259f69524644f1bd4314db8b960d1"
|
|
69
73
|
}
|
|
70
74
|
]
|
|
71
75
|
},
|
|
@@ -365,7 +369,7 @@
|
|
|
365
369
|
"hashes": [
|
|
366
370
|
{
|
|
367
371
|
"alg": "SHA-256",
|
|
368
|
-
"content": "
|
|
372
|
+
"content": "4d20dbc85ea5bb93ca90fd8cd089255bab9c76a912ca3ad954f3a4bb670f3c45"
|
|
369
373
|
}
|
|
370
374
|
]
|
|
371
375
|
},
|
|
@@ -375,7 +379,7 @@
|
|
|
375
379
|
"hashes": [
|
|
376
380
|
{
|
|
377
381
|
"alg": "SHA-256",
|
|
378
|
-
"content": "
|
|
382
|
+
"content": "c624e0ade0314ca07913ba64e092d4655f86201856e8188c3e616b2320226c1e"
|
|
379
383
|
}
|
|
380
384
|
]
|
|
381
385
|
},
|
|
@@ -395,7 +399,7 @@
|
|
|
395
399
|
"hashes": [
|
|
396
400
|
{
|
|
397
401
|
"alg": "SHA-256",
|
|
398
|
-
"content": "
|
|
402
|
+
"content": "ea8fb546acb7f09816d2fc9f9189a86f2400b8cec91c9ba4b82b68a8e32920bb"
|
|
399
403
|
}
|
|
400
404
|
]
|
|
401
405
|
},
|
|
@@ -625,7 +629,7 @@
|
|
|
625
629
|
"hashes": [
|
|
626
630
|
{
|
|
627
631
|
"alg": "SHA-256",
|
|
628
|
-
"content": "
|
|
632
|
+
"content": "630eb9047258ad0333082f407cd34522e4c278650f22ef6dba1ef897ec5cce71"
|
|
629
633
|
}
|
|
630
634
|
]
|
|
631
635
|
},
|
|
@@ -785,7 +789,7 @@
|
|
|
785
789
|
"hashes": [
|
|
786
790
|
{
|
|
787
791
|
"alg": "SHA-256",
|
|
788
|
-
"content": "
|
|
792
|
+
"content": "587b36287ce9e9b85b1dfc41fc50d95297f5ac6be32e1e702e0b89b5df0a7892"
|
|
789
793
|
}
|
|
790
794
|
]
|
|
791
795
|
},
|
|
@@ -118,6 +118,32 @@ GET /api/v1/{resource}/:id
|
|
|
118
118
|
Errors: 401, 404
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
## Observability — a MANDATORY section of `05_architecture.md`
|
|
122
|
+
|
|
123
|
+
The artifact must carry a section headed exactly **`Observability`**, answering how anyone would
|
|
124
|
+
know this feature is working once it ships:
|
|
125
|
+
|
|
126
|
+
- what it **logs**, and at what level;
|
|
127
|
+
- what it **counts** — the one or two numbers that would move if it broke;
|
|
128
|
+
- what a **failure looks like from outside** — the symptom, not the stack trace;
|
|
129
|
+
- **who would notice**, and how.
|
|
130
|
+
|
|
131
|
+
If the feature genuinely emits nothing at runtime — a pure refactor, a doc change, a gate that only
|
|
132
|
+
runs in CI — write **"nothing to observe"** and say why. That is a complete answer, not a gap. What
|
|
133
|
+
is not acceptable is leaving the question unanswered.
|
|
134
|
+
|
|
135
|
+
The heading is read by a machine (`dz score` reports it as a discipline), so it is spelled exactly
|
|
136
|
+
`Observability` — the prompt above and the check derive that word from one shared constant, because
|
|
137
|
+
a prompt asking for one heading while a check greps another produces a gate that fails every honest
|
|
138
|
+
run.
|
|
139
|
+
|
|
140
|
+
**Why this section exists.** MEASURED 2026-08-25: across 107 existing `05_architecture.md` files,
|
|
141
|
+
ZERO carried such a section, and the word `observability` appeared nowhere in the pipeline's own
|
|
142
|
+
prompts. The pipeline kept five telemetry stores about itself and asked for none about what it
|
|
143
|
+
built. The two capable skills — `observability` (818 lines) and `observability-testing-patterns`
|
|
144
|
+
(946 lines) — were fully written and unreachable, because nothing called them. This section is the
|
|
145
|
+
call site.
|
|
146
|
+
|
|
121
147
|
## Write discipline (the 180-second rule)
|
|
122
148
|
|
|
123
149
|
An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
|
|
@@ -130,8 +130,15 @@ Before finalizing the plan, validate completeness:
|
|
|
130
130
|
1. Cross-reference every `{REQUIREMENT}` (FR-N) → at least one TASK covers it
|
|
131
131
|
2. Cross-reference every `{ADR_DECISION}` → at least one TASK implements it
|
|
132
132
|
3. Cross-reference every critical risk from `{QUALITY_RISKS}` → mitigation in some TASK
|
|
133
|
-
4.
|
|
134
|
-
|
|
133
|
+
4. Name every acid token `A<n>` from `00_complexity_assessment.md` VERBATIM in the plan (C4), each
|
|
134
|
+
bound to the TASK that owns it and the TEST that proves the refusal — the gate reads them with a
|
|
135
|
+
word-boundary regex, so a paraphrase, a range (`A1-A7`) or a renamed token does not count
|
|
136
|
+
5. Carry an `Inputs read:` line naming at least `03_adr` and `05_architecture` (C5). Put it at the
|
|
137
|
+
TOP of the plan or immediately BEFORE the trailing `EXPECTED_CODE_TARGETS:` block — never after
|
|
138
|
+
it. The plan must END with that targets block, so "end with Inputs read" would contradict the
|
|
139
|
+
layout the coder reads (cross-family QE, gpt-5.6-sol)
|
|
140
|
+
6. If gaps found → add missing TASKs and re-order the DAG
|
|
141
|
+
7. **Iterate until no gaps remain** (max 3 iterations, then flag for user)
|
|
135
142
|
|
|
136
143
|
This gap-check loop ensures the implementation plan is complete before coding begins.
|
|
137
144
|
|
|
@@ -190,6 +197,14 @@ Create `features/<slug>/06_implementation_plan.md` with:
|
|
|
190
197
|
the NEXT `AM-N` begins. Multi-line amendments are fine; what is NOT fine is putting a marker after
|
|
191
198
|
the following amendment has already started, because it then belongs to that one. A bare range
|
|
192
199
|
like `AM-1..AM-4` never opens a row.
|
|
200
|
+
TWO machines read this row with ONE grammar and different depths: the K2 C6 gate asks whether the
|
|
201
|
+
row is well-formed; `dz amendment-check` asks whether the named test RESOLVES to a real title in a
|
|
202
|
+
real file. So the marker carries ALL THREE parts — an arrow (`→` or `->`), the test id in
|
|
203
|
+
backticks, and the file as `` in `path` ``:
|
|
204
|
+
`AM-N (source): <change>. Confirmation: <property> → test \`test_name\` in \`path/to/file\` (fails if reverted).`
|
|
205
|
+
A marker WITHOUT the file used to pass the plan gate and then fail Step 8 with `no-file-named` —
|
|
206
|
+
that is the exact defect this shape removes. The alternative complete form is a retraction:
|
|
207
|
+
`superseded by AM-N`, which both machines accept.
|
|
193
208
|
A safeguard amendment's named test must prove it TRIGGERS on a real input, not merely that its code
|
|
194
209
|
path exists. Step 8 verifies every named test exists and is non-vacuous (`dz discrimination-check`).
|
|
195
210
|
|
|
@@ -246,6 +246,25 @@ ITERATION {N}:
|
|
|
246
246
|
Max iterations: 3. After 3 iterations, flag remaining gaps for user decision.
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
**Do NOT count the iterations yourself — ASK.** Before starting another rework round, run:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
dz qe-rounds --slug <slug>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`0` under the ceiling, another round is within budget · `1` **STOP** — the ceiling is reached, the
|
|
256
|
+
remaining gaps go to the owner · `2` NOT ESTABLISHED, which is never "zero rounds".
|
|
257
|
+
|
|
258
|
+
It reads what `dz qe-bridge` already wrote and records nothing of its own, so it answers correctly
|
|
259
|
+
across restarts and for runs already past.
|
|
260
|
+
|
|
261
|
+
**Why this is a command and not a sentence you remember.** Until 2026-08-27 this rule existed ONLY as
|
|
262
|
+
the line above: `grep -rl 'ReviewScope|scopeId|findingId|lineage'` over harness-core and harness-cli
|
|
263
|
+
returned nothing, so there was no counter anywhere. Every restart of the agent forgot the sentence.
|
|
264
|
+
MEASURED cost on real data: one slug reached **38** graded rounds and 4 attempts with no verdict,
|
|
265
|
+
against a ceiling of 3. A rule at layer 4 of the cost-of-detection ladder fails silently — nobody
|
|
266
|
+
complains that the ceiling was missed, it is simply never reached.
|
|
267
|
+
|
|
249
268
|
This loop implements the "did we miss something?" recursive check from the original pipeline spec.
|
|
250
269
|
|
|
251
270
|
### 6. Acceptance Criteria Validation
|
|
@@ -301,15 +301,38 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
301
301
|
// Line-scan, not regex-over-document: \Z is Python, and $-lookahead under /m matches every
|
|
302
302
|
// line end — a section with no FOLLOWING heading silently failed to parse (caught by the
|
|
303
303
|
// fixture the moment the tests ran).
|
|
304
|
-
|
|
304
|
+
// Fenced blocks are BLANKED line-for-line, preserving numbering and offsets: a fenced
|
|
305
|
+
// `## Amendments` could become the section heading, and a fenced example row could either open a
|
|
306
|
+
// phantom amendment or hand a real testless one someone else's marker. Third fence-blindness
|
|
307
|
+
// found in a checker today, so it is closed here by construction rather than by care.
|
|
308
|
+
const rawLines = plan.split('\n');
|
|
309
|
+
const planLines = [];
|
|
310
|
+
{
|
|
311
|
+
let fence = null;
|
|
312
|
+
for (const line of rawLines) {
|
|
313
|
+
const open = /^ {0,3}(```+|~~~+)/.exec(line);
|
|
314
|
+
if (fence === null && open) { fence = open[1][0]; planLines.push(''); continue; }
|
|
315
|
+
if (fence !== null) {
|
|
316
|
+
planLines.push('');
|
|
317
|
+
if (new RegExp('^ {0,3}' + fence + '{3,}\\s*$').test(line)) fence = null;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
planLines.push(line);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
305
323
|
let sectionStart = -1, sectionEnd = -1, cursor = 0;
|
|
306
324
|
for (const pl of planLines) {
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
// The SAME heading shape amendment-trace.ts accepts: up to three leading spaces, two to four
|
|
326
|
+
// hashes, and trailing text allowed. C6 required exactly `##` with nothing after, so the two
|
|
327
|
+
// tools disagreed about where the section even IS — the divergence this feature exists to end.
|
|
328
|
+
if (sectionStart < 0 && /^ {0,3}#{2,4}\s+Amendments\b/.test(pl)) sectionStart = cursor + pl.length + 1;
|
|
329
|
+
else if (sectionStart >= 0 && sectionEnd < 0 && /^ {0,3}#{1,4}\s/.test(pl)) sectionEnd = cursor;
|
|
309
330
|
cursor += pl.length + 1;
|
|
310
331
|
}
|
|
311
332
|
if (sectionStart >= 0 && sectionEnd < 0) sectionEnd = plan.length;
|
|
312
|
-
|
|
333
|
+
// Sliced from the MASKED text so the offsets computed above line up with what is scanned.
|
|
334
|
+
const maskedPlan = planLines.join('\n');
|
|
335
|
+
const amSection = sectionStart >= 0 ? maskedPlan.slice(sectionStart, sectionEnd) : '';
|
|
313
336
|
// Only a DEFINITION-shaped line counts as a stray: a list item opening with the AM token.
|
|
314
337
|
// A mid-prose REFERENCE («per 01_requirements.md (AM-1..AM-6)», «(AM-1: the clause stays
|
|
315
338
|
// dropped)») cites an amendment defined in ANOTHER artifact and is legitimate — the first cut
|
|
@@ -317,7 +340,11 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
317
340
|
{
|
|
318
341
|
let cursor2 = 0;
|
|
319
342
|
for (const pl of planLines) {
|
|
320
|
-
|
|
343
|
+
// The SAME row shape as the in-section rule, or the two disagree about what a row is: a
|
|
344
|
+
// bullet-less row was invisible here while being a row there (so it evaded this check), and
|
|
345
|
+
// this side lacked the range guard, so «AM-1..AM-4 are covered elsewhere» was falsely
|
|
346
|
+
// reported as a definition. One shape, one meaning.
|
|
347
|
+
const isDef = /^\s*(?:[-*|]\s*)?\*{0,2}AM-(?:CP-)?\d+\*{0,2}\b(?!\s*\.)/.test(pl);
|
|
321
348
|
const inSection = sectionStart >= 0 && cursor2 >= sectionStart && cursor2 < sectionEnd;
|
|
322
349
|
if (isDef && !inSection) {
|
|
323
350
|
const tok = (/AM-(?:CP-)?\d+/.exec(pl) || ['AM-?'])[0];
|
|
@@ -346,16 +373,39 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
346
373
|
// check entirely (MEASURED). The `(?!\s*\.)` guard is what refuses a wrapped `AM-1..AM-4;`
|
|
347
374
|
// range preamble, which used to open a phantom amendment and double-count AM-1 — it costs 0 rows.
|
|
348
375
|
const DEF = /^(?:[-*|]\s*)?\*{0,2}(AM-(?:CP-)?\d+)\*{0,2}\b(?!\s*\.)/;
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
376
|
+
// The marker must carry what the LATER gate needs to resolve: an arrow, a backticked test id,
|
|
377
|
+
// and a file as `in \`path\``. Until 2026-08-25 this accepted a bare `-> test`, so a plan cleared
|
|
378
|
+
// K2 and then failed Step-8 `dz amendment-check` with `no-file-named` — the two tools read one
|
|
379
|
+
// row under different contracts (field report doc-26). Same grammar, different depth: this asks
|
|
380
|
+
// whether the row is WELL-FORMED, amendment-check asks whether it RESOLVES.
|
|
381
|
+
// The two-id form `-> tests \`a\` and \`b\`` is corpus-canonical; matching only the singular
|
|
382
|
+
// turned two green rows red when this was last touched.
|
|
383
|
+
const MARK = /(?:\u2192|->)\s*tests?\s+`[^`]+`(?:\s*(?:and|и)\s*`[^`]+`)?[\s\S]{0,40}?\bin\s+`[^`]+`/;
|
|
352
384
|
const defs = [];
|
|
353
385
|
secLines.forEach((l, i) => { const m = DEF.exec(l.trim()); if (m) defs.push({ i, id: m[1] }); });
|
|
354
386
|
for (let k = 0; k < defs.length; k++) {
|
|
355
387
|
const to = k + 1 < defs.length ? defs[k + 1].i : secLines.length;
|
|
356
388
|
const block = secLines.slice(defs[k].i, to).join('\n');
|
|
357
389
|
const hasTest = MARK.test(block);
|
|
358
|
-
|
|
390
|
+
// A MENTION is not a CLAIM — the third instance of that class found today. An unanchored
|
|
391
|
+
// substring test passes on "NOT superseded by AM-9" and on a sentence quoting the form.
|
|
392
|
+
// Anchoring on POSITION was tried and rejected: real rows put prose between the id and the
|
|
393
|
+
// retraction, so a position rule refuses legitimate corpus forms. The rule is therefore about
|
|
394
|
+
// NEGATION and QUOTATION, the two ways a mention differs from a claim — plus the successor
|
|
395
|
+
// must EXIST among the amendments defined here, since a retraction pointing at AM-999999 is
|
|
396
|
+
// not a retraction but a way through the gate.
|
|
397
|
+
let superseded = false;
|
|
398
|
+
for (const bl of block.split('\n')) {
|
|
399
|
+
const re = /superseded by (AM-(?:CP-)?\d+)\b/ig;
|
|
400
|
+
for (let m = re.exec(bl); m !== null; m = re.exec(bl)) {
|
|
401
|
+
const before = bl.slice(0, m.index);
|
|
402
|
+
if (/\b(?:not|never|no|isn't|is not|rather than|instead of)\s*$/i.test(before)) continue;
|
|
403
|
+
const quotes = (before.match(/[`"«]/g) || []).length;
|
|
404
|
+
if (quotes % 2 === 1) continue;
|
|
405
|
+
if (m[1] && defs.some((d) => d.id.toLowerCase() === m[1].toLowerCase())) { superseded = true; break; }
|
|
406
|
+
}
|
|
407
|
+
if (superseded) break;
|
|
408
|
+
}
|
|
359
409
|
if (!hasTest && !superseded) failures.push(`C6: ${defs[k].id} carries neither \`\u2192 test <name>\` nor \`superseded by AM-N\` — an amendment without a confirmation is a wish, and a retracted one must say its successor`);
|
|
360
410
|
}
|
|
361
411
|
}
|
|
@@ -368,7 +418,11 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
|
|
|
368
418
|
// with both sides named and never blocks.
|
|
369
419
|
{
|
|
370
420
|
const adrAll = adrFiles.map((f) => { try { return readFileSync(join(adrDir, f), 'utf-8'); } catch { return ''; } }).join('\n');
|
|
371
|
-
|
|
421
|
+
// The tail is OPTIONAL and stops at the first closing backtick (`[^`]*`, never `.*`): a command
|
|
422
|
+
// cited WITH its real arguments must be visible on BOTH sides, or a genuine ADR<->plan divergence
|
|
423
|
+
// hides whenever both files write the natural form (ADR-002, backlog 420e5b79). The CAPTURE stays
|
|
424
|
+
// the bare name, so the comparison remains a set of command NAMES and flag differences never warn.
|
|
425
|
+
const cmds = (t) => new Set([...t.matchAll(/`dz ([a-z][a-z0-9-]+)(?:\s[^`]*)?`/g)].map((m) => m[1]));
|
|
372
426
|
const inAdr = cmds(adrAll); const inPlan = cmds(plan);
|
|
373
427
|
const onlyAdr = [...inAdr].filter((c) => !inPlan.has(c));
|
|
374
428
|
const onlyPlan = [...inPlan].filter((c) => !inAdr.has(c));
|
|
@@ -505,9 +505,31 @@ function captureFailureRecord(stage, mode, reason, detail) {
|
|
|
505
505
|
}
|
|
506
506
|
function tpFamily(spec) { return /codex|gpt|openai/i.test(String(spec == null ? '' : spec)) ? 'codex' : 'claude' }
|
|
507
507
|
function tpText(v) { if (typeof v === 'string') return v; if (v === null || v === undefined) return ''; try { const s = JSON.stringify(v); return typeof s === 'string' ? s : String(v) } catch (e) { return String(v) } }
|
|
508
|
+
// Operator-profile redaction (mirror of harness-core redactProfileBlock — ADR-001 Decision 5 /
|
|
509
|
+
// CF-6 of operator-profile). The PERSIST seam in `dz feature-adr-record` redacts independently;
|
|
510
|
+
// this mirror exists so the truncation fnv1a64 hashes below are hashes of the REDACTED text and
|
|
511
|
+
// never fingerprint personal data, exactly like the core builder. Unterminated block fails
|
|
512
|
+
// CLOSED: everything from the start marker to the end of the text is dropped.
|
|
513
|
+
const TP_PROFILE_START = '<!-- dz:profile:start -->'
|
|
514
|
+
const TP_PROFILE_END = '<!-- dz:profile:end -->'
|
|
515
|
+
const TP_PROFILE_REDACTED = '[dz:profile REDACTED]'
|
|
516
|
+
function tpRedact(text) {
|
|
517
|
+
if (typeof text !== 'string' || text === '') return typeof text === 'string' ? text : ''
|
|
518
|
+
let out = ''
|
|
519
|
+
let rest = text
|
|
520
|
+
for (;;) {
|
|
521
|
+
const start = rest.indexOf(TP_PROFILE_START)
|
|
522
|
+
if (start === -1) return out + rest
|
|
523
|
+
out += rest.slice(0, start) + TP_PROFILE_REDACTED
|
|
524
|
+
const end = rest.indexOf(TP_PROFILE_END, start + TP_PROFILE_START.length)
|
|
525
|
+
if (end === -1) return out
|
|
526
|
+
rest = rest.slice(end + TP_PROFILE_END.length)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
508
529
|
function buildTrainingPair(slug, stage, ts, inputRaw, outputRaw, evaluation, provenance, captureMode, resumed) {
|
|
509
|
-
|
|
510
|
-
let
|
|
530
|
+
// Redaction FIRST, before the oversize guard — same order as the core builder.
|
|
531
|
+
let input = tpRedact(tpText(inputRaw))
|
|
532
|
+
let output = tpRedact(tpText(outputRaw))
|
|
511
533
|
let truncated = null
|
|
512
534
|
if (input.length + output.length > TP_MAX_IO) {
|
|
513
535
|
truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) }
|
|
@@ -921,9 +943,23 @@ function codexExecPlan(stage, promptChars, probedId, scoped) {
|
|
|
921
943
|
// A model id is user input (args.codexModel) and lands in a shell command the agent runs. Cross-model
|
|
922
944
|
// review (codex exec, 2026-07-10) found it interpolated unquoted. Plain ids only, quoted anyway.
|
|
923
945
|
function isSafeCodexId(id) { return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(String(id)) }
|
|
924
|
-
|
|
946
|
+
const TIMEOUT_BINS = { timeout: true, gtimeout: true }
|
|
947
|
+
|
|
948
|
+
// Which binary bounds a dispatched run. timeout(1) is GNU coreutils and is NOT on macOS; brew's
|
|
949
|
+
// coreutils installs it as gtimeout. An ALLOWLIST, because this value goes into a shell command.
|
|
950
|
+
// MEASURED 2026-08-25: with neither present the dispatch exits 127 and cross-family QE — a NAMED
|
|
951
|
+
// safety property — silently did not happen for a whole run.
|
|
952
|
+
// Deliberately NOT the perl alarm+exec form: MEASURED, it exits 142 while GNU timeout exits 124,
|
|
953
|
+
// and classifyCodexQeOutcome keys timeout on exit === 124. A remedy that breaks the classifier is
|
|
954
|
+
// worse than the defect it fixes.
|
|
955
|
+
function timeoutBinOrDefault(bin) {
|
|
956
|
+
const b = typeof bin === 'string' ? bin : ''
|
|
957
|
+
return TIMEOUT_BINS[b] === true ? b : 'timeout'
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function codexProbeCommand(id, timeoutBin) {
|
|
925
961
|
if (!isSafeCodexId(id)) return null
|
|
926
|
-
return "
|
|
962
|
+
return timeoutBinOrDefault(timeoutBin) + " 60 codex exec -m '" + id + "' 'Reply with exactly: OK' < /dev/null"
|
|
927
963
|
}
|
|
928
964
|
|
|
929
965
|
// A verdict must NAME its grade. "Looks good" is not a review — cross-model review caught this too.
|
|
@@ -956,7 +992,7 @@ const CODEX_REVIEW_TIMEOUT_SECONDS = 600
|
|
|
956
992
|
const CODEX_REVIEW_DEFAULT_EFFORT = 'high'
|
|
957
993
|
const CODEX_TIMEOUT = 'CODEX_TIMEOUT'
|
|
958
994
|
const CODEX_QE_SIGNAL_PREFIX = 'CODEX-QE-SIGNAL'
|
|
959
|
-
const CODEX_QE_DECLINE_KINDS = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling']
|
|
995
|
+
const CODEX_QE_DECLINE_KINDS = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling', 'wrong-tree']
|
|
960
996
|
const SCOPED_QE_MAX_FILES = 3
|
|
961
997
|
const SCOPED_QE_MAX_QUESTIONS = 4
|
|
962
998
|
const SCOPED_QE_MAX_PATH_CHARS = 200
|
|
@@ -974,6 +1010,23 @@ function isSafeCodexRef(ref) {
|
|
|
974
1010
|
// --uncommitted alike. Either mistake presents as a review that silently did not happen, so both are
|
|
975
1011
|
// refusals in the builder, not comments. carriesPrompt is kept as a field: it is the one boolean a
|
|
976
1012
|
// future CLI would flip.
|
|
1013
|
+
function codexSq(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'" }
|
|
1014
|
+
function codexCd(repo) { return repo === '' ? '' : 'cd ' + codexSq(repo) + ' && ' }
|
|
1015
|
+
// Field report 27: BOTH codex dispatches ran with no working directory, so they read the SESSION cwd.
|
|
1016
|
+
// On a run against an external checkout that is a different tree — and the review still exits 0 with a
|
|
1017
|
+
// Grade the pipeline records as a verdict, while crossFamilyQe.happened stays true. codex exec takes
|
|
1018
|
+
// -C; codex review has no such flag (measured on codex-cli 0.149.1) and needs a cd prefix.
|
|
1019
|
+
function codexExecCommand(input) {
|
|
1020
|
+
const o = input || {}
|
|
1021
|
+
const modelId = String(o.modelId === undefined || o.modelId === null ? '' : o.modelId)
|
|
1022
|
+
if (!isSafeCodexId(modelId)) return null
|
|
1023
|
+
const raw = Number(o.timeoutSeconds)
|
|
1024
|
+
const seconds = (raw === raw && raw !== Infinity && raw > 0) ? Math.floor(raw) : CODEX_EXEC_TIMEOUT_SECONDS
|
|
1025
|
+
const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo)
|
|
1026
|
+
const cd = repo === '' ? '' : ' -C ' + codexSq(repo)
|
|
1027
|
+
return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd + ' -m ' + codexSq(modelId) + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null'
|
|
1028
|
+
}
|
|
1029
|
+
|
|
977
1030
|
function codexReviewCommand(input) {
|
|
978
1031
|
const o = input || {}
|
|
979
1032
|
const scope = (o.scope === undefined || o.scope === null || o.scope === '') ? 'uncommitted' : String(o.scope)
|
|
@@ -988,7 +1041,8 @@ function codexReviewCommand(input) {
|
|
|
988
1041
|
if (scope !== 'uncommitted' && !isSafeCodexRef(ref)) return { cmd: null, carriesPrompt: false, scope: scope, reason: 'unsafe id or ref' }
|
|
989
1042
|
const raw = Number(o.timeoutSeconds)
|
|
990
1043
|
const seconds = (raw === raw && raw !== Infinity && raw > 0) ? Math.floor(raw) : CODEX_REVIEW_TIMEOUT_SECONDS
|
|
991
|
-
|
|
1044
|
+
const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo)
|
|
1045
|
+
let cmd = codexCd(repo) + timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + " codex review -c model='" + modelId + "' -c model_reasoning_effort='" + effort + "'"
|
|
992
1046
|
if (scope === 'commit') cmd += " --commit '" + ref + "'"
|
|
993
1047
|
else if (scope === 'base') cmd += " --base '" + ref + "'"
|
|
994
1048
|
else cmd += ' --uncommitted'
|
|
@@ -1097,6 +1151,37 @@ function gradeFromReviewFindings(findings) {
|
|
|
1097
1151
|
// called it 'unusable-output' and told the operator to fix a tool that works. And exit 0 is a
|
|
1098
1152
|
// SUCCESSFUL review even when it finds blockers (MEASURED probe 0.2), so tool-error is exit not in
|
|
1099
1153
|
// {0,124} only. signalExpected defaults TRUE (fail closed): a swallowed sentinel is a tool-error.
|
|
1154
|
+
// Field report 27. A review dispatched into the wrong working directory does not error: the declared
|
|
1155
|
+
// paths simply are not there, the model says so in prose, and the command still exits 0 — often with
|
|
1156
|
+
// a Grade line, which the pipeline then records as a verdict about code nobody read. Narrow on
|
|
1157
|
+
// purpose: a review of a file-handling module may legitimately DISCUSS "No such file or directory",
|
|
1158
|
+
// and a mention is not a claim — so one LINE must carry both the phrase and a declared path.
|
|
1159
|
+
function codexReviewMissedItsFiles(body, declaredFiles) {
|
|
1160
|
+
const text = String(body === undefined || body === null ? '' : body)
|
|
1161
|
+
const files = Array.isArray(declaredFiles) ? declaredFiles.filter((f) => typeof f === 'string' && f !== '') : []
|
|
1162
|
+
if (text === '' || files.length === 0) return false
|
|
1163
|
+
// Only quotes, whitespace and a colon may sit between the path and the failure. Prose may not —
|
|
1164
|
+
// and that single restriction is what separates "the tool could not open this path" from "this
|
|
1165
|
+
// finding is ABOUT this path": a review finding always names its file, so anything looser marks
|
|
1166
|
+
// every file-handling review as wrong-tree. (Codex, gpt-5.6-sol, on the first version of this
|
|
1167
|
+
// function: the finding line "- [P2] Do not swallow file not found - src/io.ts:42" plus a stated
|
|
1168
|
+
// grade C was classified wrong-tree, discarding a valid cross-family verdict and falling back to
|
|
1169
|
+
// same-family QE — the guard against a false-clean review destroying a true one.)
|
|
1170
|
+
const GAP = '["\'\u2018\u2019\u201c\u201d\u0060(\\[\\s:,]{0,4}'
|
|
1171
|
+
const NOT_FOUND = 'no such file or directory|file not found|not found|does not exist|is not present|cannot be found'
|
|
1172
|
+
const VERB = '(?:cannot|can\'t|could not|couldn\'t|unable to|failed to|error(?: while)?)\\s+(?:open|read|find|access|stat|locate|load)'
|
|
1173
|
+
for (const f of files) {
|
|
1174
|
+
const q = f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1175
|
+
const shapes = [
|
|
1176
|
+
q + GAP + '(?:' + NOT_FOUND + ')', // src/io.ts: No such file or directory
|
|
1177
|
+
'(?:' + NOT_FOUND + ')' + GAP + q, // No such file or directory: src/io.ts
|
|
1178
|
+
VERB + GAP + q, // cannot open 'src/io.ts'
|
|
1179
|
+
]
|
|
1180
|
+
for (const shape of shapes) if (new RegExp(shape, 'i').test(text)) return true
|
|
1181
|
+
}
|
|
1182
|
+
return false
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1100
1185
|
function classifyCodexQeOutcome(input) {
|
|
1101
1186
|
const o = input || {}
|
|
1102
1187
|
const body = String(o.body === undefined || o.body === null ? '' : o.body)
|
|
@@ -1117,6 +1202,9 @@ function classifyCodexQeOutcome(input) {
|
|
|
1117
1202
|
} else if (exit !== 0) {
|
|
1118
1203
|
return { kind: 'tool-error' }
|
|
1119
1204
|
}
|
|
1205
|
+
// BEFORE the grade rule: a wrong-tree review usually DOES state a grade, and that clean letter
|
|
1206
|
+
// about unread code is the most dangerous thing this pipeline can emit.
|
|
1207
|
+
if (codexReviewMissedItsFiles(body, o.declaredFiles)) return { kind: 'wrong-tree' }
|
|
1120
1208
|
if (grade !== null) return { kind: 'verdict' }
|
|
1121
1209
|
return { kind: 'no-verdict' }
|
|
1122
1210
|
}
|
|
@@ -1137,11 +1225,14 @@ function codexQeDeclineReason(kind, detail) {
|
|
|
1137
1225
|
const exit = (d.exit === undefined || d.exit === null) ? '?' : String(d.exit)
|
|
1138
1226
|
const chars = (d.chars === undefined || d.chars === null) ? '?' : String(d.chars)
|
|
1139
1227
|
const extra = (d.detail === undefined || d.detail === null || String(d.detail) === '') ? 'no detail' : String(d.detail)
|
|
1228
|
+
if (canonical === 'wrong-tree') return 'codex reported that the declared file(s) do not exist — the review ran in the WRONG working directory and its verdict is about a tree nobody asked for; ' + files + ' file(s) declared, exit ' + exit + ' (' + extra + ')'
|
|
1140
1229
|
if (canonical === 'timeout') return 'codex review timed out after ' + elapsed + 's on scope ' + ref + ' (' + files + ' files) — NARROW the scope (this is reconnaissance cost, not thinking time)'
|
|
1141
1230
|
if (canonical === 'no-verdict') return 'codex answered in ' + elapsed + 's but named no grade — not a verdict'
|
|
1142
1231
|
if (canonical === 'tool-error') return 'codex review exited ' + exit + ' — FIX the invocation (' + extra + ')'
|
|
1143
1232
|
if (canonical === 'unusable-output') return 'codex exec unusable — ' + ((d.reason === undefined || d.reason === null || String(d.reason) === '') ? 'codex exec returned no text' : String(d.reason))
|
|
1144
|
-
|
|
1233
|
+
// The detail is the ONE field carrying the shell error, and this branch used to drop it while
|
|
1234
|
+
// tool-error right above rendered it — the asymmetry that made the field report unfixable blind.
|
|
1235
|
+
if (canonical === 'unavailable') return 'codex not used — ' + ((d.reason === undefined || d.reason === null || String(d.reason) === '') ? 'codex exec reported it could not run' : String(d.reason)) + (extra === 'no detail' ? '' : ' (' + extra + ')')
|
|
1145
1236
|
if (canonical === 'over-ceiling') return 'prompt is ' + chars + ' chars / unscoped — refused before dispatch'
|
|
1146
1237
|
throw new Error('codexQeDeclineReason: unknown kind ' + k)
|
|
1147
1238
|
}
|
|
@@ -1157,12 +1248,12 @@ function codexDeclineReason(a, b) {
|
|
|
1157
1248
|
// this input may never have been wrapped (a saved fixture, a report on disk), so a missing sentinel
|
|
1158
1249
|
// means "no machine signal exists", not "the tool failed". The PIPELINE must not use that leniency —
|
|
1159
1250
|
// the dispatch below calls classifyCodexQeOutcome directly with signalExpected true.
|
|
1160
|
-
function parseCodexReviewResult(text) {
|
|
1251
|
+
function parseCodexReviewResult(text, declaredFiles) {
|
|
1161
1252
|
const sig = parseCodexReviewSignal(text)
|
|
1162
1253
|
const findings = parseCodexReviewFindings(sig.body)
|
|
1163
1254
|
const stated = parseCodexGrade(sig.body)
|
|
1164
1255
|
const grade = (stated !== null) ? stated : gradeFromReviewFindings(findings)
|
|
1165
|
-
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: sig.signalPresent })
|
|
1256
|
+
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: sig.signalPresent, declaredFiles: declaredFiles })
|
|
1166
1257
|
const kind = (outcome.kind === 'unusable-output') ? 'empty' : outcome.kind
|
|
1167
1258
|
const ok = kind === 'verdict'
|
|
1168
1259
|
const reason = ok ? null : codexQeDeclineReason(kind, { elapsedSeconds: sig.elapsedSeconds, exit: sig.exit, chars: sig.body.length })
|
|
@@ -1186,12 +1277,35 @@ async function safeCodexAgent(prompt, opts) {
|
|
|
1186
1277
|
|
|
1187
1278
|
// CX-1: the allowlist says a name is spellable; only a probe says it answers. One probe per run.
|
|
1188
1279
|
let _probedCodexId
|
|
1280
|
+
// Which timeout binary this machine actually has. ONE cached shell probe, run before either
|
|
1281
|
+
// dispatch mode spends a model call.
|
|
1282
|
+
//
|
|
1283
|
+
// The model probe below is NOT a substitute: it is judged by the agent's prose ("reply with its
|
|
1284
|
+
// stdout only"), so on the machine in field report doc-24 it passed while timeout(1) was absent —
|
|
1285
|
+
// the agent evidently retried without the missing prefix — and only the strict "run this VERBATIM"
|
|
1286
|
+
// dispatch surfaced the 127. A shell fact must be checked by a shell, once, deterministically.
|
|
1287
|
+
let _timeoutBin
|
|
1288
|
+
async function probeTimeoutBin() {
|
|
1289
|
+
if (_timeoutBin !== undefined) return _timeoutBin
|
|
1290
|
+
const cmd = 'command -v timeout >/dev/null 2>&1 && echo timeout || { command -v gtimeout >/dev/null 2>&1 && echo gtimeout || echo NONE; }'
|
|
1291
|
+
const out = await agent('Run EXACTLY this via Bash and reply with its stdout only: ' + cmd, { label: 'probe:timeout-bin', phase: 'Route', model: 'haiku', effort: 'low' })
|
|
1292
|
+
const t = String(out === null || out === undefined ? '' : out).trim()
|
|
1293
|
+
_timeoutBin = TIMEOUT_BINS[t] === true ? t : null
|
|
1294
|
+
if (_timeoutBin === null) log('codex: NEITHER timeout(1) NOR gtimeout is on PATH — every codex dispatch would exit 127; on macOS: brew install coreutils')
|
|
1295
|
+
else if (_timeoutBin !== 'timeout') log('codex: bounding runs with ' + _timeoutBin + ' (timeout(1) is absent — darwin without coreutils on PATH)')
|
|
1296
|
+
return _timeoutBin
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1189
1299
|
async function probeCodexId() {
|
|
1190
1300
|
if (_probedCodexId !== undefined) return _probedCodexId
|
|
1191
1301
|
const raw = (CODEX_MODEL && CODEX_MODEL !== 'auto') ? [CODEX_MODEL, 'gpt-5.6-sol', 'gpt-5.5'] : ['gpt-5.6-sol', 'gpt-5.5']
|
|
1192
1302
|
const ids = raw.filter(isSafeCodexId)
|
|
1193
1303
|
for (const id of ids) {
|
|
1194
|
-
|
|
1304
|
+
// The probe is built with the SAME binary the real dispatch will use, so a machine that cannot
|
|
1305
|
+
// bound a run fails here rather than mid-QE.
|
|
1306
|
+
const tbin = await probeTimeoutBin()
|
|
1307
|
+
if (tbin === null) { _probedCodexId = null; return null }
|
|
1308
|
+
const cmd = codexProbeCommand(id, tbin)
|
|
1195
1309
|
if (!cmd) { log('codex: refusing unsafe model id ' + id); continue }
|
|
1196
1310
|
const out = await agent('Run EXACTLY this via Bash and reply with its stdout only: ' + cmd + ' — if it fails or times out reply with exactly ' + CODEX_UNAVAILABLE, { label: 'probe:' + id, phase: 'Route', model: 'haiku', effort: 'low' })
|
|
1197
1311
|
if (out && /\bOK\b/.test(String(out)) && String(out).indexOf(CODEX_UNAVAILABLE) === -1) {
|
|
@@ -1249,7 +1363,7 @@ async function runCodexQeCommand(stage, cmd, phaseName, label, probed, mode, sco
|
|
|
1249
1363
|
// the closing line, may read it.
|
|
1250
1364
|
const stated = (allowStatedGrade === true) ? parseCodexGrade(sig.body) : null
|
|
1251
1365
|
const grade = (stated !== null) ? stated : gradeFromReviewFindings(findings)
|
|
1252
|
-
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: true })
|
|
1366
|
+
const outcome = classifyCodexQeOutcome({ exit: sig.exit, body: sig.body, grade: grade, findings: findings, signalExpected: true, declaredFiles: files })
|
|
1253
1367
|
if (outcome.kind !== 'verdict') {
|
|
1254
1368
|
return noteCodexDecline(stage, outcome.kind, { elapsedSeconds: sig.elapsedSeconds, exit: sig.exit, ref: scopeRef, files: files, chars: cmd.length, reason: parseCodexExecResult(sig.body).reason, detail: sig.body.slice(0, 160) })
|
|
1255
1369
|
}
|
|
@@ -1263,7 +1377,7 @@ async function codexReviewAgent(stage, scope, scopeRef, phaseName) {
|
|
|
1263
1377
|
lastCodexDecline = null
|
|
1264
1378
|
const probed = await probeCodexId()
|
|
1265
1379
|
if (!probed) return noteCodexDecline(stage, 'unavailable', { reason: 'no codex model id answered the probe' })
|
|
1266
|
-
const built = codexReviewCommand({ scope: scope, ref: scopeRef, modelId: probed, reasoning: 'high', timeoutSeconds: CODEX_REVIEW_TIMEOUT_SECONDS })
|
|
1380
|
+
const built = codexReviewCommand({ scope: scope, ref: scopeRef, modelId: probed, reasoning: 'high', timeoutSeconds: CODEX_REVIEW_TIMEOUT_SECONDS, timeoutBin: await probeTimeoutBin(), repo: REPO })
|
|
1267
1381
|
if (built.cmd === null) return noteCodexDecline(stage, 'tool-error', { exit: 2, detail: built.reason })
|
|
1268
1382
|
return await runCodexQeCommand(stage, built.cmd, phaseName, stage + ':codex-review', probed, 'A', built.scope + (scopeRef ? ' ' + scopeRef : ''), [], false)
|
|
1269
1383
|
}
|
|
@@ -1275,7 +1389,11 @@ async function codexExecAgent(stage, prompt, phaseName, scoped, files) {
|
|
|
1275
1389
|
const probed = await probeCodexId()
|
|
1276
1390
|
const plan = codexExecPlan(stage, prompt.length, probed, scoped)
|
|
1277
1391
|
if (plan.mode !== 'exec') return noteCodexDecline(stage, 'unavailable', { reason: plan.reason })
|
|
1278
|
-
|
|
1392
|
+
// Was JSON.stringify(...) — DOUBLE quotes, in which the shell still expands a command substitution, and the
|
|
1393
|
+
// prompt carries the user's own feature description. Single-quoted through codexSq closes that
|
|
1394
|
+
// as a side effect of pinning the working directory.
|
|
1395
|
+
const inner = codexExecCommand({ modelId: probed, prompt: prompt, timeoutBin: await probeTimeoutBin(), timeoutSeconds: CODEX_EXEC_TIMEOUT_SECONDS, repo: REPO })
|
|
1396
|
+
if (inner === null) return noteCodexDecline(stage, 'unavailable', { reason: 'unsafe codex id ' + String(probed) })
|
|
1279
1397
|
return await runCodexQeCommand(stage, inner, phaseName, stage + ':codex-exec', probed, 'B', 'declared-targets(' + (files || []).length + ' declared, <=' + SCOPED_QE_MAX_FILES + ' reviewed)', files || [], true)
|
|
1280
1398
|
}
|
|
1281
1399
|
|
|
@@ -1978,7 +2096,7 @@ const DISCRIMINATION_GATE = '\u00a742 TEST-DISCRIMINATION GATE (run right after
|
|
|
1978
2096
|
// amendment is a mini-ADR: it carries a one-line Confirmation naming the test that falsifies it. Machine-checkable
|
|
1979
2097
|
// shape (a linter can assert the `→ test ` token); Step-8 verifies existence + non-vacuity via the SAME
|
|
1980
2098
|
// dz discrimination-check that guards the ADR property (cost-of-detection ladder: judgment → step gate).
|
|
1981
|
-
const AMENDMENT_RULE = 'AMENDMENT CONFIRMATION DISCIPLINE (every amendment is a mini-ADR): whenever a correction/amendment is folded in (a QCSD CONDITIONAL condition, a challenge-panel confirmed finding, or a user checkpoint steer), record it in a `## Amendments` section as a fixed-shape row: `AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if reverted).` — naming the test that would FAIL if the amendment were reverted/broken. The row is MACHINE-READ by the K2 C6 gate: the marker (or a superseded-by-AM-N note) must sit inside the OWN block of that amendment — anywhere between its AM-N line and the line where the NEXT AM-N begins. Multi-line amendments are fine; what is NOT fine is placing a marker after the following amendment has already started, because it then belongs to that one. A bare range such as AM-1..AM-4 never opens a row. For a SAFEGUARD amendment (a warning/guard/fallback), the named test must prove the safeguard actually TRIGGERS on a real input — not merely that its code path exists (a structurally-dead safeguard passes an existence test and never fires in production).'
|
|
2099
|
+
const AMENDMENT_RULE = 'AMENDMENT CONFIRMATION DISCIPLINE (every amendment is a mini-ADR): whenever a correction/amendment is folded in (a QCSD CONDITIONAL condition, a challenge-panel confirmed finding, or a user checkpoint steer), record it in a `## Amendments` section as a fixed-shape row: `AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if reverted).` — naming the test that would FAIL if the amendment were reverted/broken. The row is MACHINE-READ by the K2 C6 gate: the marker (or a superseded-by-AM-N note) must sit inside the OWN block of that amendment — anywhere between its AM-N line and the line where the NEXT AM-N begins. Multi-line amendments are fine; what is NOT fine is placing a marker after the following amendment has already started, because it then belongs to that one. A bare range such as AM-1..AM-4 never opens a row. The row is read by TWO machines with the SAME grammar and different depths: the K2 C6 gate asks whether the row is well-formed, and dz amendment-check asks whether the named test RESOLVES. So the marker must carry ALL THREE parts: an arrow, the test id in backticks, and the file — AM-N (source): <change>. Confirmation: <property> -> test <backticked name> in <backticked path> (fails if reverted). A marker without the file passes the plan gate and then FAILS Step 8, which is the exact defect this shape removes. The alternative form is a retraction: superseded by AM-N, which both machines accept as a complete answer. For a SAFEGUARD amendment (a warning/guard/fallback), the named test must prove the safeguard actually TRIGGERS on a real input — not merely that its code path exists (a structurally-dead safeguard passes an existence test and never fires in production).'
|
|
1982
2100
|
const AMENDMENT_GATE = 'AMENDMENT GATE (P2): do NOT judge this yourself — RUN the check and report what it says. Via Bash run EXACTLY `' + DZ + ' amendment-check --slug ' + SLUG + ' --json` (add `--feature-dir ' + FDIR + '` if the slug does not resolve from your CWD). Parse the JSON and report `amendments: {outcome, counts, reasons}` in your return object. outcome `pass` or `skip` clears the gate; `fail` is a HIGH gap and every reason must be quoted verbatim into the QE report; `not-established` means the check could not be run or the grammar matched nothing — that is NEVER a pass, report it as inconclusive with the tool error. Empty stdout, a crash, or a missing `dz` is `not-established`, not a clean gate. This check proves each amendment RESOLVES to a real test; it does NOT prove the test discriminates — vacuity stays with the discrimination gate above. ' +
|
|
1983
2101
|
'IO-ON-PURE-PATH + FIXTURE-SWAP HUNT (P5): in the test diff, hunt for replacements of broken/unbound fixtures with healthy ones — the old fixture was probably a NEGATIVE CONTROL proving a path was I/O-free; each such swap requires a compensating negative resource-down test. If the code diff adds I/O (DB/network/file) to a previously-pure path — especially startup/lifespan/health — require a negative resource-down test (broken/unbound resource → the path degrades per its declared contract: fail-open for advisory, explicit fail-fast for load-bearing). Missing → HIGH gap.'
|
|
1984
2102
|
|
|
@@ -2075,9 +2193,35 @@ await agent('Run EXACTLY this one shell command via your Bash tool and report it
|
|
|
2075
2193
|
// R2 polymorphic-feature-adr (ADR-001): probe the project skill manifest ONCE at Step 0. Returns only
|
|
2076
2194
|
// {hasManifest, report} (small, reliable). NO manifest ⇒ PS_GUIDANCE returns '' for every stage, so the
|
|
2077
2195
|
// stage prompts are byte-identical to today (FR-7 load-bearing). Fail-open: a probe error ⇒ generic run.
|
|
2196
|
+
// Field report doc-25b: BOTH the probe below and PS_GUIDANCE hardcoded `cd REPO`, so on a run whose
|
|
2197
|
+
// REPO is an external checkout the manifest installed in the WORKSPACE was unreachable — the run
|
|
2198
|
+
// recorded an honest `polymorphism:null` and every project lens silently went missing. Symmetric with
|
|
2199
|
+
// the doc-21 fix for the K2 gate: probe the target repo FIRST (a repo's own conventions are
|
|
2200
|
+
// authoritative for it) and fall back to the workspace ONLY when the target has no manifest and WS is
|
|
2201
|
+
// a genuinely different root. The choice is made in the SHELL by `grep -q`, never by the agent's
|
|
2202
|
+
// judgment. ONE builder feeds both call sites, so they cannot drift apart again.
|
|
2203
|
+
// Checkpoint note: POLY.hasManifest and fnv1a64(POLY.report) are checkpoint-hash inputs, so a run that
|
|
2204
|
+
// NOW finds a manifest it used to miss legitimately re-spends design/code/qe on resume — that is the
|
|
2205
|
+
// stale-input promise working, not a regression.
|
|
2206
|
+
// MIRROR of harness-core `projectSkillsOneRoot` / `projectSkillsProbeCommand` (the workflow sandbox
|
|
2207
|
+
// has no imports). Behaviour is pinned against the export by a drift test that extracts BOTH copies.
|
|
2208
|
+
function psSq(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'" }
|
|
2209
|
+
function projectSkillsOneRoot(dzBin, root) { return 'cd ' + psSq(root) + ' && ' + dzBin + ' project-skills --project ' + psSq(root) + ' --stages-json' }
|
|
2210
|
+
function projectSkillsProbeCommand(dzBin, repo, workspace) {
|
|
2211
|
+
const primary = projectSkillsOneRoot(dzBin, repo)
|
|
2212
|
+
if (workspace === null || workspace === undefined || workspace === repo) return primary
|
|
2213
|
+
return 'o=$(' + primary + ' 2>/dev/null); echo "$o" | grep -q \'"hasManifest":true\' || o=$(' + projectSkillsOneRoot(dzBin, workspace) + ' 2>/dev/null); echo "$o"'
|
|
2214
|
+
}
|
|
2215
|
+
// WS is populated ONLY by args.workspace or a relative args.repo, so the reporter's own invocation
|
|
2216
|
+
// (absolute args.repo, no args.workspace) leaves it null and the fallback would never arm — the exact
|
|
2217
|
+
// run doc-28 describes. BRAIN is the canonical pattern store, defaulting to REPO but pinned by
|
|
2218
|
+
// args.brain to the workspace precisely when the coder works in a target checkout, so it is the
|
|
2219
|
+
// second-best evidence of where the workspace is. Falling back to it costs nothing when it equals
|
|
2220
|
+
// REPO (the builder collapses to the single-root form).
|
|
2221
|
+
const PS_CMD = projectSkillsProbeCommand(DZ, REPO, WS !== null ? WS : (BRAIN !== REPO ? BRAIN : null))
|
|
2078
2222
|
let POLY = { hasManifest: false, report: '' }
|
|
2079
2223
|
try {
|
|
2080
|
-
const psProbe = await agent('Run EXACTLY this one shell command via your Bash tool:
|
|
2224
|
+
const psProbe = await agent('Run EXACTLY this one shell command via your Bash tool: ' + PS_CMD + '. It prints one JSON line {hasManifest, design, code, qe, report}. Return ONLY {hasManifest, report} from it (drop the big design/code/qe strings).', { label: 'project-skills:step0', phase: 'Router', effort: 'low', schema: PROJECT_SKILLS })
|
|
2081
2225
|
if (psProbe && typeof psProbe === 'object') POLY = psProbe
|
|
2082
2226
|
} catch (e) { /* fail-open — generic run */ }
|
|
2083
2227
|
if (POLY.hasManifest) log('Project skills: manifest active — folding project guidance into design/code/qe')
|
|
@@ -2085,7 +2229,7 @@ if (POLY.hasManifest) log('Project skills: manifest active — folding project g
|
|
|
2085
2229
|
// agent fetches its OWN stage's guidance from `dz project-skills` (exact file content, no model
|
|
2086
2230
|
// transcription). Empty string when no manifest ⇒ `prompt + PS_GUIDANCE(...)` is a no-op (byte-identical).
|
|
2087
2231
|
const PS_GUIDANCE = (stage) => POLY.hasManifest
|
|
2088
|
-
? '\n\nPROJECT-SPECIFIC GUIDANCE (polymorphic feature-adr): this project ships a skill manifest. Via your Bash tool run EXACTLY `
|
|
2232
|
+
? '\n\nPROJECT-SPECIFIC GUIDANCE (polymorphic feature-adr): this project ships a skill manifest. Via your Bash tool run EXACTLY `' + PS_CMD + '`, parse the JSON, and treat its `' + stage + '` field as AUTHORITATIVE project guidance for THIS step (product vision, conventions, checklists) — honor it as a hard constraint. If the command errors or hasManifest is false, ignore this paragraph.'
|
|
2089
2233
|
: ''
|
|
2090
2234
|
|
|
2091
2235
|
// Steps 1-5: Design (tier-gated thunks built explicitly - no inline ternary-null)
|
|
@@ -2125,7 +2269,7 @@ if (isMplus) {
|
|
|
2125
2269
|
designThunks.push(() => subCheckpoint('qcsd', qcsdPromptText, qcsdOpts, ['03.5_ideation_report.md'], () => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd')))
|
|
2126
2270
|
designPairMeta.push({ role: 'design:qcsd', prompt: qcsdPromptText, model: modelLabel(qcsdOpts), lessons: [] })
|
|
2127
2271
|
const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
|
|
2128
|
-
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
|
|
2272
|
+
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). It MUST carry a section headed exactly "Observability" answering how anyone would know this feature is working once it ships: what it logs, what it counts, what a failure looks like from outside, and who would notice. If the feature genuinely emits nothing at runtime — a pure refactor, a CI-only gate — say "nothing to observe" and why; that is a complete answer, not a gap. What is not acceptable is leaving the question unanswered.' + archExtra + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
|
|
2129
2273
|
designThunks.push(() => subCheckpoint('architecture', archPromptText, archOpts, ['05_architecture.md'].concat(isLplus ? ['04_domain_model.md'] : []), () => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture')))
|
|
2130
2274
|
designPairMeta.push({ role: 'design:architecture', prompt: archPromptText, model: modelLabel(archOpts), lessons: [] })
|
|
2131
2275
|
}
|
|
@@ -2285,7 +2429,7 @@ if (!fanVerdict.complete) {
|
|
|
2285
2429
|
// unavailable/errors — the pipeline never blocks on Codex.
|
|
2286
2430
|
phase('Plan')
|
|
2287
2431
|
await usageProbe('Plan')
|
|
2288
|
-
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
|
|
2432
|
+
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/. The K2 plan-completeness gate blocks Step 7 until the plan satisfies these too, so write them in as you author, not afterwards: (C1) every ADR under 03_adr/ is cited as `ADR-<n>` by the task that implements it; (C2) every test path named in an ADR Confirmation stanza appears verbatim in the plan, bound to the task that writes it; (C4) every acid token `A<n>` from 00_complexity_assessment.md is named verbatim, bound to its owning task and to the test that proves the refusal. 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
|
|
2289
2433
|
// Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
|
|
2290
2434
|
// DEFAULT_MODELS/coder-fold) or the DEFAULT_MODELS.plan ('sonnet') under routing; else {} (BC).
|
|
2291
2435
|
const planModel = resolveStageModel('plan')
|
|
@@ -2613,6 +2757,16 @@ if (codeStage && codeStage.modelUsed) modelsUsed.code = codeStage.modelUsed + (r
|
|
|
2613
2757
|
// landedNote comes FROM the checkpointed composite (the barrier ran inside the code stage — QE #3):
|
|
2614
2758
|
// on resume the ORIGINAL barrier signal is restored verbatim with a resumed marker prepended.
|
|
2615
2759
|
let landedNote = codeStage ? String(codeStage.landedNote || '') : ''
|
|
2760
|
+
// These two are re-read from codeStage for the SAME reason `code`, `codexCodeText` and `landedNote`
|
|
2761
|
+
// above are: the Step-7 body is an arrow function, so its `let`s die at its closing brace. Both were
|
|
2762
|
+
// referenced BARE at top level — `expectedTargets` by the Step-8 writer-quiescence probe and
|
|
2763
|
+
// `landingStatus` by the promise-tag block — which is a runtime ReferenceError, not a stale value.
|
|
2764
|
+
// The wrapper landed 2026-08-19 (wave1-instrument-repair) and the quiescence probe 2026-08-24
|
|
2765
|
+
// (qe-writer-quiescence) referenced the name across it. Found 2026-08-25 by the scope-aware half of
|
|
2766
|
+
// workflow-free-identifiers.test.ts on its first run; the flat half could not see it, because both
|
|
2767
|
+
// names ARE declared — three hundred lines away, inside a scope that had already closed.
|
|
2768
|
+
const expectedTargets = codeStage && Array.isArray(codeStage.expectedTargets) ? codeStage.expectedTargets : []
|
|
2769
|
+
const landingStatus = codeStage ? codeStage.landingStatus : null
|
|
2616
2770
|
if (resumedStages.indexOf('code') !== -1 && landedNote !== '') {
|
|
2617
2771
|
landedNote = '\n\n[RESUMED from checkpoint — the landing barrier below ran in the ORIGINAL run; the change-manifest artifact was re-verified present by the resume probe]' + landedNote
|
|
2618
2772
|
}
|