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