@dzhechkov/skills-feature-adr 1.3.47 → 1.3.48
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/README.md
CHANGED
|
@@ -163,7 +163,8 @@ The three Codex knobs above are shortcuts. `args.models` is the **general dial**
|
|
|
163
163
|
routes each of the 11 pipeline stages to an optimal model. Keys: `{router, requirements, research, adr,
|
|
164
164
|
ideation, ddd, architecture, plan, code, qe, fleet}`. Each value is a **spec** — Claude
|
|
165
165
|
`fable|opus|sonnet|haiku`, or Codex `codex` / `codex:<id>` / `codex:<id>:<reasoning>`
|
|
166
|
-
(`reasoning ∈ low|medium|high|xhigh`; ids incl. `gpt-5.5`, `gpt-5.6`
|
|
166
|
+
(`reasoning ∈ low|medium|high|xhigh`; ids incl. `gpt-5.5`, `gpt-5.6`, `gpt-5.6-sol` — the last is the
|
|
167
|
+
default auto-top reviewer; ids are account-specific, so probe with `codex exec -m <id>` before pinning).
|
|
167
168
|
|
|
168
169
|
**Recommended DEFAULT TABLE** (applied only when you opt in — one `args.models` key or any Codex knob
|
|
169
170
|
turns it on; otherwise every stage is session-inherited, byte-identical to today):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/skills-feature-adr",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.48",
|
|
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"
|
|
@@ -23,7 +23,16 @@ The script lives at `.claude/workflows/feature-adr.js`.
|
|
|
23
23
|
|
|
24
24
|
The pipeline ALWAYS runs the feature-adr Pattern memory loop **in-process** — non-optional:
|
|
25
25
|
- **Step 0** recalls learned patterns (`dz recall`) → folds the top ones into requirements/ADR as `{LEARNED_PATTERNS}` + records the recalled count in the live panel (`dz statusline --fa-record`).
|
|
26
|
-
- **Step 8**
|
|
26
|
+
- **Step 8** compares candidate lessons against the Step-0 recalled pattern text. If a lesson is already covered, it reinforces the existing pattern (`dz teach --reinforce <id-or-text>`) and records the reinforced count; only genuinely new lessons are taught with `dz teach`. The loop pays off (recall), reinforces under-ranked patterns, and grows only when there is new knowledge.
|
|
27
|
+
|
|
28
|
+
**Canonical brain pin (`args.brain`) — never fragment the loop.** The loop only compounds if Step-0 recall
|
|
29
|
+
and Step-8 teach hit ONE store. Both are pinned to a canonical **brain store** via `args.brain` (default =
|
|
30
|
+
the workspace `REPO`) — the workflow emits `cd <brain> && dz recall/teach … --project <brain>`, so a lesson
|
|
31
|
+
taught from a Step-7 coder that `cd`'d into a target repo still lands in the brain, not that repo's `.dz`.
|
|
32
|
+
Omitting `args.brain` is byte-identical to today for a workspace-CWD run. Override it with a stable absolute
|
|
33
|
+
path when the coder works in a target checkout. Recover an accidentally-fragmented store by exporting from
|
|
34
|
+
the stray repo and merging into the brain: `cd <stray> && dz recall --all --json > /tmp/stray.json` → `dz
|
|
35
|
+
teach --from-json /tmp/stray.json --project <brain>` (exact-text dedup, idempotent).
|
|
27
36
|
|
|
28
37
|
## Optional Codex routing (opt-in, pre-flight ASK)
|
|
29
38
|
|
|
@@ -38,7 +47,50 @@ overridable via `args`:
|
|
|
38
47
|
| `qeReviewer` | 8 (QE/tests) | `claude` (default) · `codex` · `codex-fallback` | same fallback, for the QE/testing stage |
|
|
39
48
|
| `codexModel` | — | `auto` (default — Codex self-selects the top available) · or a specific id your account exposes (e.g. `gpt-5.5`, `gpt-5.4`) | which Codex model to use |
|
|
40
49
|
|
|
41
|
-
The result reports `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` so you can see who did what.
|
|
50
|
+
The result reports `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` / `modelsUsed` so you can see who did what.
|
|
51
|
+
|
|
52
|
+
### Per-stage model routing — `args.models` (the general mechanism)
|
|
53
|
+
|
|
54
|
+
`args.models` is an optional per-stage map that routes EACH pipeline stage to an optimal model — one dial
|
|
55
|
+
over the 11 stages `{router, requirements, research, adr, ideation, ddd, architecture, plan, code, qe,
|
|
56
|
+
fleet}`. Each value is a **spec**: Claude `fable|opus|sonnet|haiku`, or Codex `codex` / `codex:<id>` /
|
|
57
|
+
`codex:<id>:<reasoning>` (reasoning ∈ `none|minimal|low|medium|high|xhigh`; ids incl. `gpt-5.5`, `gpt-5.6`). A resolver
|
|
58
|
+
turns the spec into that stage's `agent()` opts (`{model}` for Claude — role `agentType`s preserved;
|
|
59
|
+
`{agentType:'codex:codex-rescue', codexModel, _reasoning}` for Codex).
|
|
60
|
+
|
|
61
|
+
> **`<reasoning>` effort plumbing (honest scope):** the resolved `_reasoning` is RECORDED in
|
|
62
|
+
> `modelsUsed` (the who-did-what report) and is plumbed into each Codex dispatch prompt as an
|
|
63
|
+
> explicit per-call effort hint, e.g. `(If you are the Codex runtime, run at --effort xhigh.)`.
|
|
64
|
+
> The hint is derived from the same resolved opts that render `codex:<id>:<reasoning>`, so the
|
|
65
|
+
> dispatched effort and `modelsUsed` label must match for that call. `~/.codex/config.toml`
|
|
66
|
+
> `model_reasoning_effort` remains the fallback default when no Codex stage runs or no per-call
|
|
67
|
+
> effort hint is emitted.
|
|
68
|
+
|
|
69
|
+
**Recommended DEFAULT TABLE** (applied ONLY when routing is opted into — any one `args.models` key OR any
|
|
70
|
+
Codex knob turns it on; otherwise every stage is session-inherited, byte-identical to today):
|
|
71
|
+
|
|
72
|
+
| router | requirements | research | adr | ideation | ddd | architecture | plan | code | qe | fleet |
|
|
73
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
74
|
+
| `fable` | `sonnet` | `sonnet`¹ | `opus` | `sonnet` | `opus`¹ | `opus` | `sonnet` | coder² | cross-model³ | `sonnet` |
|
|
75
|
+
|
|
76
|
+
¹ `research` folds into `requirements`, `ddd` into `architecture` (single shared call) — recorded in
|
|
77
|
+
`modelsUsed` but not a separate call. ² `code` defaults to the `coder` knob (Claude `opus` by default).
|
|
78
|
+
³ `qe` — see below.
|
|
79
|
+
|
|
80
|
+
**Cross-model QE default (load-bearing rationale).** When `args.models.qe` is UNSET, QE auto-routes to the
|
|
81
|
+
**OTHER family than the coder** — the model that writes code must NOT also self-QE; independent cross-model
|
|
82
|
+
review catches what self-review misses (this is the ADR-named safety property with a dedicated test). So
|
|
83
|
+
coder=Codex ⇒ QE=Claude (`opus`); coder=Claude ⇒ QE=Codex (`codex:<top>:high`), falling back to a Claude
|
|
84
|
+
reviewer if Codex is unavailable (never blocks).
|
|
85
|
+
|
|
86
|
+
**Precedence (documented once):** `args.models[stage]` > the legacy `planner`/`coder`/`qeReviewer`/
|
|
87
|
+
`codexModel` knob > `DEFAULT_MODELS[stage]`. The knobs remain shortcuts that fill only unspecified stages;
|
|
88
|
+
`codexModel` seeds the default id for a bare `'codex'` spec. Codex-fallback (Claude-first, Codex on
|
|
89
|
+
limit-null) stays a knob-only behavior; a direct `models.code='codex'` means codex-first.
|
|
90
|
+
|
|
91
|
+
**gpt-5.6-ready:** adding a new Codex id is a DATA-ONLY edit to the `KNOWN_CODEX` allowlist — no new control
|
|
92
|
+
flow. Example: `Workflow({ scriptPath: '.claude/workflows/feature-adr.js', args: { slug, description,
|
|
93
|
+
tier: 'L', models: { code: 'opus', qe: 'codex:gpt-5.6:high' } } })` — Claude codes, Codex independently QEs.
|
|
42
94
|
|
|
43
95
|
**Pre-flight — detect + ASK (do this BEFORE launching a run):**
|
|
44
96
|
```bash
|
|
@@ -60,6 +112,32 @@ Without this, the Code agent returns null and the run stalls. With `coder: 'code
|
|
|
60
112
|
detects the null, logs *"Claude unavailable (limit?) — falling back to Codex auto"*, and finishes
|
|
61
113
|
the code + tests on Codex — no restart, no lost work.
|
|
62
114
|
|
|
115
|
+
**Codex dispatch is decided by the DELIVERABLE, not by the knob (ADR-001, codex-routing-honesty).**
|
|
116
|
+
`codex:codex-rescue` is a fire-and-forget Claude *wrapper*: it dispatches and returns immediately, so its
|
|
117
|
+
return value is a **stub**. That is correct for a stage whose deliverable is a FILE written out-of-band
|
|
118
|
+
**and** which verifies the write landed — `code` (Step-7.5 `git status` poll) and `plan` (requires
|
|
119
|
+
`06_implementation_plan.md` to appear, else it falls back to the Claude planner). For every other stage the
|
|
120
|
+
deliverable **is** the return value, and a stub reads exactly like a clean review. Those stages now dispatch
|
|
121
|
+
through `codex exec` — synchronous, real stdout. The workflow script is sandboxed (no `child_process`), so
|
|
122
|
+
an ordinary Claude agent runs the command and returns Codex's words verbatim: **the agent is the shell.**
|
|
123
|
+
|
|
124
|
+
Three consequences you must know before routing a stage to Codex:
|
|
125
|
+
|
|
126
|
+
- **A QE verdict is parsed, never synthesised.** Empty, whitespace-only, or sentinel output is *not* a
|
|
127
|
+
clean review; text without a grade is *not* a verdict. Either case falls back to a Claude reviewer and
|
|
128
|
+
logs that cross-model QE did **not** happen. The deleted `{grade: 'codex-review', gaps: []}` was the bug.
|
|
129
|
+
- **There is a prompt ceiling.** `codex exec` answers a trivial prompt in ~6 s and a one-line code question
|
|
130
|
+
in ~12 s, but stalls past 280 s on a 55-line payload (MEASURED). Prompts over
|
|
131
|
+
`CODEX_EXEC_PROMPT_CEILING_CHARS` (1200) are not sent; the stage falls back to Claude **loudly**, because
|
|
132
|
+
a silent downgrade to same-model QE is a worse lie than the stub.
|
|
133
|
+
- **The exec agent is a Claude agent.** Under true Claude exhaustion it dies exactly like the wrapper. This
|
|
134
|
+
change makes the review honest; it does **not** make Codex a lifeboat for a Claude limit. The pre-emptive
|
|
135
|
+
usage switch is the limit defence — and it is disarmed while `memory.usage.*` is unset.
|
|
136
|
+
|
|
137
|
+
`safeCodexAgent` demotes an "agent type not found" error to `null` → Claude fallback, and rethrows every
|
|
138
|
+
other error, so real bugs still surface. The model id is **probed once per run**: the allowlist says a name
|
|
139
|
+
is spellable, only a probe says it answers.
|
|
140
|
+
|
|
63
141
|
**Codex-landed barrier (Step 7.5):** Codex applies edits OUT-OF-BAND via its own runtime, so a naive
|
|
64
142
|
pipeline runs Step-8 QE before the async write flushes and false-grades *"Step 7 never ran"* (grade D on
|
|
65
143
|
real, landed code — observed on the goap-ed25519 fix). When Codex was the coder, feature-adr now polls
|
|
@@ -80,6 +158,46 @@ Workflow({ scriptPath: '.claude/workflows/feature-adr.js', args: { slug: 'add-x'
|
|
|
80
158
|
**Headless Codex login on a VPS (no browser):** `codex login --device-auth` (prints a code + URL you
|
|
81
159
|
approve on another device) or `printenv OPENAI_API_KEY | codex login --with-api-key`.
|
|
82
160
|
|
|
161
|
+
## Usage-adaptive routing (pre-emptive Codex switch under limit pressure)
|
|
162
|
+
|
|
163
|
+
When routing is opted into, feature-adr probes Claude SESSION (active 5h-block) + WEEKLY (rolling 7d)
|
|
164
|
+
usage at EACH phase boundary via a minimal effort-low agent running `dz usage --json`, and PRE-EMPTIVELY
|
|
165
|
+
switches all remaining stages to Codex when usage is high — BEFORE the phase launches. Three knobs:
|
|
166
|
+
|
|
167
|
+
| Knob (`args.*`) | Default | Effect |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `usageAdaptive` | `true` when routing is requested; `false` otherwise | `true` forces it on without other routing; `false` disables ALL probes (byte-identical to today — zero probes, no override) |
|
|
170
|
+
| `usageThreshold` | `70` | the `>=` percent (SESSION or WEEKLY) that triggers the switch (boundary `=70` counts as over) |
|
|
171
|
+
| `usageReasoning` | the `OVERRIDE_REASONING` map | per-stage reasoning under the override (merge over: design/code/plan → `xhigh`; router/qe/fleet → `high`) |
|
|
172
|
+
|
|
173
|
+
**Two-way, hysteretic.** `>= usageThreshold` on EITHER metric ⇒ switch ALL remaining stages to
|
|
174
|
+
`codex:<top>` (`gpt-5.6`-ready via `KNOWN_CODEX`). BOTH metrics back below (positive numbers, not nulls)
|
|
175
|
+
⇒ RESTORE the normal mix. **Fail-safe asymmetry:** a probe that returns null (the agent DIED — often
|
|
176
|
+
MEANS the limit was hit) fail-safe-switches TO Codex; a value-null (unconfigured limits) flips NOTHING —
|
|
177
|
+
it never sends a fresh run to Codex and never restores an active override (no flapping). A reactive belt
|
|
178
|
+
also flips the override when a stage agent returns null while not already overridden, so the *remaining*
|
|
179
|
+
stages skip the wall.
|
|
180
|
+
|
|
181
|
+
**The wrapper lesson (why pre-emptive, not just reactive).** `codex:codex-rescue` is a Claude wrapper
|
|
182
|
+
subagent — at TRUE exhaustion even the Codex dispatch dies. So the switch MUST happen BEFORE exhaustion,
|
|
183
|
+
which is exactly what the 70% probe does; the reactive belt is a best-effort backstop, not the defense.
|
|
184
|
+
|
|
185
|
+
**Cross-model-QE bounded exception (FR-2.9).** Under the override, coder AND QE are both Codex (the
|
|
186
|
+
cross-model self-QE guard is consciously suspended — a Claude QE reviewer is precisely the agent that
|
|
187
|
+
dies under limit pressure). The ` (usage-switched)` suffix + `usageEvents` keep it auditable; run an
|
|
188
|
+
independent re-QE after limits reset if the switch fired during Step 8.
|
|
189
|
+
|
|
190
|
+
**Config + honesty.** Set `memory.usage.sessionTokenLimit` / `memory.usage.weeklyTokenLimit` in
|
|
191
|
+
`.dz/config.json` (OPTIONAL — absent ⇒ `pct=null`, no switch). Percentages are ESTIMATES from local
|
|
192
|
+
transcript aggregation (no official API); calibrate by scaling a limit by `X/100` when a real limit-hit
|
|
193
|
+
lands at an estimated `X%`. Observability: switched stages carry ` (usage-switched)` in `modelsUsed`;
|
|
194
|
+
both return objects gain `usageEvents` (`[{phase, sessionPct, weeklyPct, action}]`, action ∈ the LOCKED
|
|
195
|
+
6-value set `switch|restore|keep|fail-safe-switch|reactive-switch|none`) + `usageThreshold`.
|
|
196
|
+
|
|
197
|
+
**Example:** `Workflow({ scriptPath: '.claude/workflows/feature-adr.js', args: { slug, description,
|
|
198
|
+
tier: 'L', models: { code: 'opus' }, usageAdaptive: true, usageThreshold: 70 } })` — Claude codes until
|
|
199
|
+
usage crosses 70%, then every remaining stage runs on Codex until it drops back below.
|
|
200
|
+
|
|
83
201
|
## Hybrid checkpoints (router decides)
|
|
84
202
|
|
|
85
203
|
- **S / M** → run autonomously to completion; present a final consolidated review (ADR + plan + QE).
|
|
@@ -493,6 +493,12 @@ only the statusline:
|
|
|
493
493
|
🎓 Learning: {recalled} patterns recalled for this feature, {stored} new stored this run
|
|
494
494
|
```
|
|
495
495
|
|
|
496
|
+
**Record the panel at the START of every step, not only at Steps 0/8/9.** The panel shows the last step
|
|
497
|
+
that reported; a pipeline that reports three times per run shows a stale step for most of its life. Emit
|
|
498
|
+
`dz statusline --fa-record --slug <slug> --step "<Step N Name>" --recalled <n> --stored <n>` as the first
|
|
499
|
+
action of each step. The recall/teach counts only change at Steps 0/8/9; the *step label* changes at every
|
|
500
|
+
one of them.
|
|
501
|
+
|
|
496
502
|
*Honesty note:* the panel is live only insofar as the pipeline records state — it reflects what the
|
|
497
503
|
pipeline actually did with the loop (recalls that ran, stores that landed), not an aspirational count.
|
|
498
504
|
|
|
@@ -21,7 +21,65 @@ const STOP_AFTER = A.stopAfter || null
|
|
|
21
21
|
// PORTABLE: project root comes from args.repo (default '.', i.e. the cwd the workflow's agents run in),
|
|
22
22
|
// never a hardcoded path — so this ships inside @dzhechkov/skills-feature-adr and runs in any project.
|
|
23
23
|
// The monorepo passes args.repo + args.dzBin explicitly to target its dev build.
|
|
24
|
-
|
|
24
|
+
// ADR-001 (absolute-artifact-paths): a RELATIVE root means different things to different agents. Once a
|
|
25
|
+
// coder cd's elsewhere, a later agent resolves ./features/<slug>/03_adr against another cwd and reports a
|
|
26
|
+
// confident FALSE BLOCKER. The sandbox has no filesystem and no Node API, so the script cannot call
|
|
27
|
+
// process.cwd(): the absolute root arrives as an absolute args.repo, or from an agent that runs pwd.
|
|
28
|
+
function isAbsolutePosix(p) { return typeof p === 'string' && p.charAt(0) === '/' }
|
|
29
|
+
function normalizeRepoPath(p) {
|
|
30
|
+
const collapsed = String(p).replace(/\/{2,}/g, '/').replace(/\/\.(?=\/|$)/g, '')
|
|
31
|
+
const trimmed = collapsed.replace(/\/+$/, '')
|
|
32
|
+
return trimmed === '' ? '/' : trimmed
|
|
33
|
+
}
|
|
34
|
+
function absolutizeRepo(raw, cwd) {
|
|
35
|
+
const r = (typeof raw === 'string' && raw.length > 0) ? raw : '.'
|
|
36
|
+
if (isAbsolutePosix(r)) return normalizeRepoPath(r)
|
|
37
|
+
const base = normalizeRepoPath(cwd)
|
|
38
|
+
const rel = r.replace(/^\.\/+/, '').replace(/^\.$/, '')
|
|
39
|
+
return rel === '' ? base : normalizeRepoPath(base + '/' + rel)
|
|
40
|
+
}
|
|
41
|
+
const ABSOLUTE_PATH_NOTE = ' All artifact paths in this prompt are ABSOLUTE. Read and write them exactly as given; do not cd first and do not re-relativize them.'
|
|
42
|
+
|
|
43
|
+
const UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f]/
|
|
44
|
+
const DOT_DOT_SEGMENT = /(^|\/)\.\.(\/|$)/
|
|
45
|
+
function hasUnsafePathChars(p) { return UNSAFE_PATH_CHARS.test(String(p)) }
|
|
46
|
+
function hasDotDotSegment(p) { return DOT_DOT_SEGMENT.test(String(p)) }
|
|
47
|
+
function isSafeSlug(slug) { return typeof slug === 'string' && /^[a-z0-9][a-z0-9-]{0,39}$/.test(slug) }
|
|
48
|
+
function checkArtifactRoot(root) {
|
|
49
|
+
if (!isAbsolutePosix(root)) return 'artifact root is not absolute: ' + JSON.stringify(root)
|
|
50
|
+
if (hasUnsafePathChars(root)) return 'artifact root contains control characters'
|
|
51
|
+
if (hasDotDotSegment(root)) return 'artifact root contains a ".." segment: ' + root
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
// args.repo may be anything the caller passed; a non-string must not throw on .replace (CM-3).
|
|
55
|
+
function coerceRepoArg(raw) { return (typeof raw === 'string' && raw.length > 0) ? raw.replace(/\/+$/, '') : '.' }
|
|
56
|
+
// pwd output can be chatty; take the LAST line that is actually an absolute path, not the last line (CM-4).
|
|
57
|
+
function pickAbsolutePathLine(text) {
|
|
58
|
+
if (typeof text !== 'string') return null
|
|
59
|
+
const abs = text.split('\n').map(function (l) { return l.trim() }).filter(isAbsolutePosix)
|
|
60
|
+
return abs.length ? abs[abs.length - 1] : null
|
|
61
|
+
}
|
|
62
|
+
const REPO_RAW = coerceRepoArg(A.repo)
|
|
63
|
+
let REPO = REPO_RAW
|
|
64
|
+
if (!isAbsolutePosix(REPO_RAW)) {
|
|
65
|
+
// The agent is the shell (same pattern as codexExecAgent). Two attempts, then STOP.
|
|
66
|
+
let cwd = null
|
|
67
|
+
for (let attempt = 1; attempt <= 2 && !cwd; attempt++) {
|
|
68
|
+
const pwdOut = await agent('Run EXACTLY this via Bash and reply with ONLY the absolute path it prints, nothing else: pwd -P', { label: 'resolve-root:' + attempt, phase: 'Route', model: 'haiku', effort: 'low' })
|
|
69
|
+
cwd = pickAbsolutePathLine(pwdOut === null ? null : String(pwdOut))
|
|
70
|
+
if (!cwd) log('root: attempt ' + attempt + ' could not resolve the absolute root (pwd agent returned no absolute path)')
|
|
71
|
+
}
|
|
72
|
+
// CM-1/CM-2: continuing on a relative root would violate the ADR's load-bearing property and let a
|
|
73
|
+
// downstream agent emit a false BLOCKER. A log is not a guard. Fail fast instead of lying later.
|
|
74
|
+
if (!cwd) throw new Error('feature-adr: could not resolve an absolute artifact root after 2 attempts; refusing to run with a relative root (a downstream agent would report a false BLOCKER). Pass an absolute args.repo.')
|
|
75
|
+
REPO = absolutizeRepo(REPO_RAW, cwd)
|
|
76
|
+
log('root: resolved ' + REPO_RAW + ' -> ' + REPO)
|
|
77
|
+
}
|
|
78
|
+
// Round-2 review: validate the root and the slug BEFORE either is embedded in a prompt. A newline in
|
|
79
|
+
// args.repo splits a path across lines; a '..' slug escapes the features directory.
|
|
80
|
+
const rootProblem = checkArtifactRoot(REPO)
|
|
81
|
+
if (rootProblem) throw new Error('feature-adr: ' + rootProblem + ' - refusing to embed it in agent prompts')
|
|
82
|
+
if (!isSafeSlug(SLUG)) throw new Error('feature-adr: unsafe slug ' + JSON.stringify(SLUG) + ' - kebab-case Latin, max 40 chars (a ".." slug would escape features/)')
|
|
25
83
|
const FDIR = REPO + '/features/' + SLUG
|
|
26
84
|
// The dz CLI: bare `dz` (on PATH for installed users) unless the caller overrides with a bin path.
|
|
27
85
|
const DZ = A.dzBin || 'dz'
|
|
@@ -70,7 +128,7 @@ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
|
|
|
70
128
|
// (string concat, explicit if/return, object-literal tables — NO template literals, NO inline ?:agent())
|
|
71
129
|
// mirror of src/feature-adr-routing.ts; keep the two in lock-step (a drift test asserts it).
|
|
72
130
|
const MODELS = (A.models && typeof A.models === 'object') ? A.models : {}
|
|
73
|
-
const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1 }
|
|
131
|
+
const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-sol': 1 }
|
|
74
132
|
const CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 }
|
|
75
133
|
const VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 }
|
|
76
134
|
const DEFAULT_MODELS = { router: 'fable', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: null, qe: null, fleet: 'sonnet' }
|
|
@@ -248,6 +306,102 @@ function stageLabel(baseLabel, opts) {
|
|
|
248
306
|
// Codex-QE run, do ZERO extra work (byte-identical to today).
|
|
249
307
|
function needsLandedBarrier(opts) { return !!(opts && opts.agentType === 'codex:codex-rescue') }
|
|
250
308
|
|
|
309
|
+
// ── CODEX DISPATCH BY DELIVERABLE (ADR-001, mirror of feature-adr-routing.ts) ──────────────────
|
|
310
|
+
// codex:codex-rescue is a fire-and-forget Claude WRAPPER: its return value is a STUB. That is fine
|
|
311
|
+
// for a stage whose deliverable is a FILE written out-of-band AND which verifies the write landed
|
|
312
|
+
// (code → Step-7.5 git poll; plan → requires 06_implementation_plan.md to land, else Claude).
|
|
313
|
+
// It is catastrophic for a stage whose deliverable is its RETURN VALUE: a stub reads exactly like a
|
|
314
|
+
// clean review. This workflow is sandboxed (no child_process), so an ordinary Claude agent runs
|
|
315
|
+
// codex exec and returns Codex stdout verbatim — the agent is the shell.
|
|
316
|
+
const WRAPPER_STAGES = { code: 1, plan: 1 }
|
|
317
|
+
function codexDispatchMode(stage) { return WRAPPER_STAGES[stage] ? 'wrapper' : 'exec' }
|
|
318
|
+
|
|
319
|
+
// A SANITY bound on prompt size, NOT a stall guard. The earlier 1200-char ceiling was justified by
|
|
320
|
+
// "codex exec stalls on a 55-line payload"; twin experiments refuted that (2026-07-10): 4000 chars of
|
|
321
|
+
// padding answered in 4s and a 3156-char / 56-line adversarial code review in 14s. The stalls are
|
|
322
|
+
// INTERMITTENT latency — the same input hung at 60s and answered at 14s minutes apart. The real guard
|
|
323
|
+
// is the bounded timeout + the CODEX_UNAVAILABLE sentinel: a slow exec becomes an explicit
|
|
324
|
+
// "unavailable", never a passed review.
|
|
325
|
+
const CODEX_EXEC_PROMPT_CEILING_CHARS = 24000
|
|
326
|
+
const CODEX_UNAVAILABLE = 'CODEX_UNAVAILABLE'
|
|
327
|
+
|
|
328
|
+
function codexExecPlan(stage, promptChars, probedId) {
|
|
329
|
+
if (codexDispatchMode(stage) === 'wrapper') return { mode: 'wrapper', reason: 'deliverable is a landed file' }
|
|
330
|
+
if (!probedId) return { mode: 'claude', reason: 'no codex model id answered the probe' }
|
|
331
|
+
if (promptChars > CODEX_EXEC_PROMPT_CEILING_CHARS) {
|
|
332
|
+
return { mode: 'claude', reason: 'prompt is ' + promptChars + ' chars, over the ' + CODEX_EXEC_PROMPT_CEILING_CHARS + '-char codex exec ceiling (it would stall)' }
|
|
333
|
+
}
|
|
334
|
+
return { mode: 'exec', reason: 'codex exec on ' + probedId }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// A model id is user input (args.codexModel) and lands in a shell command the agent runs. Cross-model
|
|
338
|
+
// review (codex exec, 2026-07-10) found it interpolated unquoted. Plain ids only, quoted anyway.
|
|
339
|
+
function isSafeCodexId(id) { return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(String(id)) }
|
|
340
|
+
function codexProbeCommand(id) {
|
|
341
|
+
if (!isSafeCodexId(id)) return null
|
|
342
|
+
return "timeout 60 codex exec -m '" + id + "' 'Reply with exactly: OK'"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// A verdict must NAME its grade. "Looks good" is not a review — cross-model review caught this too.
|
|
346
|
+
function parseCodexGrade(text) {
|
|
347
|
+
const m = /\bgrade\s*[:=]?\s*([A-D])\b/i.exec(String(text))
|
|
348
|
+
return (m && m[1]) ? m[1].toUpperCase() : null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// R2, the most dangerous line here: an EMPTY reply must NEVER read as "no findings".
|
|
352
|
+
function parseCodexExecResult(text) {
|
|
353
|
+
const t = (typeof text === 'string') ? text.trim() : ''
|
|
354
|
+
if (t.length === 0) return { ok: false, text: '', reason: 'codex exec returned no text' }
|
|
355
|
+
if (t.indexOf(CODEX_UNAVAILABLE) !== -1) return { ok: false, text: t, reason: 'codex exec reported it could not run' }
|
|
356
|
+
return { ok: true, text: t, reason: 'codex answered' }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isAgentTypeMissingError(err) {
|
|
360
|
+
const msg = (err && err.message) ? err.message : String(err)
|
|
361
|
+
return /agent type .*not found|unknown agent type|no such agent/i.test(msg)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// CX-3: a workflow naming an agent type the harness lacks must FALL BACK, not die. Any other error
|
|
365
|
+
// still propagates — we do not hide real bugs behind a fallback.
|
|
366
|
+
async function safeCodexAgent(prompt, opts) {
|
|
367
|
+
try { return await agent(prompt, opts) }
|
|
368
|
+
catch (err) {
|
|
369
|
+
if (isAgentTypeMissingError(err)) { log('codex: agent type unavailable — falling back to Claude (' + String(err) + ')'); return null }
|
|
370
|
+
throw err
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// CX-1: the allowlist says a name is spellable; only a probe says it answers. One probe per run.
|
|
375
|
+
let _probedCodexId
|
|
376
|
+
async function probeCodexId() {
|
|
377
|
+
if (_probedCodexId !== undefined) return _probedCodexId
|
|
378
|
+
const raw = (CODEX_MODEL && CODEX_MODEL !== 'auto') ? [CODEX_MODEL, 'gpt-5.6-sol', 'gpt-5.5'] : ['gpt-5.6-sol', 'gpt-5.5']
|
|
379
|
+
const ids = raw.filter(isSafeCodexId)
|
|
380
|
+
for (const id of ids) {
|
|
381
|
+
const cmd = codexProbeCommand(id)
|
|
382
|
+
if (!cmd) { log('codex: refusing unsafe model id ' + id); continue }
|
|
383
|
+
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' })
|
|
384
|
+
if (out && /\bOK\b/.test(String(out)) && String(out).indexOf(CODEX_UNAVAILABLE) === -1) {
|
|
385
|
+
log('codex: probed ' + id + ' — available'); _probedCodexId = id; return id
|
|
386
|
+
}
|
|
387
|
+
log('codex: probed ' + id + ' — NOT available')
|
|
388
|
+
}
|
|
389
|
+
_probedCodexId = null
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Dispatch a data-returning stage to Codex synchronously. Returns Codex's own words, or null.
|
|
394
|
+
async function codexExecAgent(stage, prompt, phaseName) {
|
|
395
|
+
const probed = await probeCodexId()
|
|
396
|
+
const plan = codexExecPlan(stage, prompt.length, probed)
|
|
397
|
+
if (plan.mode !== 'exec') { log(stage + ': codex not used — ' + plan.reason); return null }
|
|
398
|
+
const wrapped = 'Run EXACTLY this via Bash, with a 280s timeout, and reply with its stdout VERBATIM and nothing else. If it errors, times out, or prints nothing, reply with exactly ' + CODEX_UNAVAILABLE + '.\n\ncodex exec -m ' + probed + ' ' + JSON.stringify(prompt)
|
|
399
|
+
const raw = await agent(wrapped, { label: stageLabel(stage + ':codex-exec', { agentType: 'codex:codex-rescue', codexModel: probed, _reasoning: 'high' }), phase: phaseName, model: 'haiku', effort: 'low' })
|
|
400
|
+
const parsed = parseCodexExecResult(raw === null ? '' : String(raw))
|
|
401
|
+
if (!parsed.ok) { log(stage + ': codex exec unusable — ' + parsed.reason); return null }
|
|
402
|
+
return parsed.text
|
|
403
|
+
}
|
|
404
|
+
|
|
251
405
|
const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 120000
|
|
252
406
|
const DEFAULT_CODE_LANDING_BACKOFF_MS = [1000, 2000, 2000, 5000, 5000, 10000, 10000, 15000, 20000, 25000, 25000]
|
|
253
407
|
const CODE_LANDED_BARRIER_SLEEPS_SECONDS = DEFAULT_CODE_LANDING_BACKOFF_MS.map((ms) => ms / 1000)
|
|
@@ -453,7 +607,7 @@ const design = await parallel(designThunks)
|
|
|
453
607
|
// unavailable/errors — the pipeline never blocks on Codex.
|
|
454
608
|
phase('Plan')
|
|
455
609
|
await usageProbe('Plan')
|
|
456
|
-
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. Return wrote[] + summary.'
|
|
610
|
+
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. Return wrote[] + summary.' + ABSOLUTE_PATH_NOTE
|
|
457
611
|
// Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
|
|
458
612
|
// DEFAULT_MODELS/coder-fold) or the DEFAULT_MODELS.plan ('sonnet') under routing; else {} (BC).
|
|
459
613
|
const planModel = resolveStageModel('plan')
|
|
@@ -463,7 +617,7 @@ if (planIsCodex) {
|
|
|
463
617
|
const planCodexLabelOpts = (planModel.agentType === 'codex:codex-rescue') ? planModel : specToOpts('codex:' + CODEX_MODEL + ':high')
|
|
464
618
|
modelsUsed.plan = modelLabel(planCodexLabelOpts)
|
|
465
619
|
const codexPlanOpts = mergeOpts({ label: stageLabel('plan:codex', planCodexLabelOpts), phase: 'Plan', agentType: 'codex:codex-rescue' }, planCodexLabelOpts)
|
|
466
|
-
const codexPlan = await
|
|
620
|
+
const codexPlan = await safeCodexAgent(planPrompt + codexEffortHint(codexPlanOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until 06_implementation_plan.md is fully written to disk.', codexPlanOpts)
|
|
467
621
|
// Codex-landed barrier for the plan artifact: a stub return is NOT proof the file was written
|
|
468
622
|
// (codex writes out-of-band). Require the artifact to LAND; otherwise fall through to the Claude planner.
|
|
469
623
|
const planLanded = codexPlan ? await agent('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
|
|
@@ -499,7 +653,7 @@ if (stopHere) {
|
|
|
499
653
|
// Step 7: Code (optional Codex fallback on Claude-limit exhaustion)
|
|
500
654
|
phase('Code')
|
|
501
655
|
await usageProbe('Code')
|
|
502
|
-
const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). Implement the feature per the plan + ADR + architecture in ' + FDIR + '. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). 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.'
|
|
656
|
+
const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). Implement the feature per the plan + ADR + architecture in ' + FDIR + '. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). 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
|
|
503
657
|
// Resolve the coder model. args.models.code wins (a direct 'codex' spec = codex-first); else the legacy
|
|
504
658
|
// CODER knob drives it (with its codex-fallback null-guard). resolveStageModel('code') folds both via the
|
|
505
659
|
// code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
|
|
@@ -521,7 +675,7 @@ if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
|
|
|
521
675
|
const codeCodexLabelOpts = codeModel.agentType ? codeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
|
|
522
676
|
const codeCodexOpts = mergeOpts({ label: stageLabel('code:codex', codeCodexLabelOpts), phase: 'Code', agentType: 'codex:codex-rescue' }, codeCodexLabelOpts)
|
|
523
677
|
const codexExpectedTargetsHint = '\n\nBecause this is running on Codex, include a final EXPECTED_CODE_TARGETS: block listing the repo-relative production/test files you expect to create or modify. List only real code/test/config/docs targets outside features/, .dz/, .agentic-qe/, and roam/. Example:\nEXPECTED_CODE_TARGETS:\n- packages/example/src/file.ts\n- packages/example/test/file.test.ts'
|
|
524
|
-
const codexCode = await
|
|
678
|
+
const codexCode = await safeCodexAgent(codePrompt + CODEX_HINT + codexEffortHint(codeCodexOpts) + codexExpectedTargetsHint, codeCodexOpts)
|
|
525
679
|
if (codexCode) { codexCodeText = String(codexCode); code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: codexCodeText.slice(0, 500) }; coderUsed = codeIsCodexFirst ? 'codex' : 'codex-fallback'; modelsUsed.code = modelLabel(codeCodexLabelOpts) }
|
|
526
680
|
}
|
|
527
681
|
|
|
@@ -541,7 +695,7 @@ if (needsCodeLandedBarrier(coderUsed)) {
|
|
|
541
695
|
// Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
|
|
542
696
|
phase('QE')
|
|
543
697
|
await usageProbe('QE')
|
|
544
|
-
const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test - the recurring lesson). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + landedNote
|
|
698
|
+
const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test - the recurring lesson). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + ABSOLUTE_PATH_NOTE + landedNote
|
|
545
699
|
// CROSS-MODEL QE (load-bearing): resolveStageModel('qe') derives the OTHER family than the resolved
|
|
546
700
|
// coder when args.models.qe is unset (coder-codex ⇒ opus; coder-Claude ⇒ codex, or opus if codex absent).
|
|
547
701
|
// An explicit args.models.qe wins. A Claude qe spec is merged onto the qe-code-reviewer base (role
|
|
@@ -564,9 +718,22 @@ if (qe === null && !qeIsCodex) reactiveBelt('QE')
|
|
|
564
718
|
if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
|
|
565
719
|
if (QE_REVIEWER === 'codex-fallback' && !qeIsCodex) log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
|
|
566
720
|
const qeCodexLabelOpts = qeModel.agentType ? qeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
721
|
+
// ADR-001: QE's deliverable is its RETURN VALUE, so it dispatches through `codex exec` (synchronous,
|
|
722
|
+
// real text), never the fire-and-forget wrapper. And the verdict is PARSED, never synthesised: the
|
|
723
|
+
// deleted `{grade:'codex-review', gaps: []}` turned a stub into a clean review.
|
|
724
|
+
const codexQeText = await codexExecAgent('qe', qePrompt + CODEX_HINT + codexEffortHint(qeCodexLabelOpts), 'QE')
|
|
725
|
+
const codexGrade = codexQeText ? parseCodexGrade(codexQeText) : null
|
|
726
|
+
if (codexQeText && codexGrade) {
|
|
727
|
+
qe = { grade: codexGrade, gaps: [], codeTestsAdequate: null, docTestsPresent: null, summary: codexQeText.slice(0, 1500) }
|
|
728
|
+
qeReviewerUsed = qeIsCodex ? 'codex' : 'codex-fallback'
|
|
729
|
+
modelsUsed.qe = modelLabel(qeCodexLabelOpts)
|
|
730
|
+
} else if (codexQeText) {
|
|
731
|
+
// Text without a grade is not a verdict. Defaulting to 'C' would re-invent the fabrication this
|
|
732
|
+
// ADR deletes, only quieter. Fall through to the Claude belt.
|
|
733
|
+
log('QE: codex answered but named no grade — not a verdict; falling back to a Claude reviewer')
|
|
734
|
+
} else {
|
|
735
|
+
log('QE: codex produced no usable review — the Claude belt below will run (cross-model QE did NOT happen)')
|
|
736
|
+
}
|
|
570
737
|
}
|
|
571
738
|
// Belt: if a codex-first QE returned null (codex unavailable), fall back to a Claude reviewer — never block.
|
|
572
739
|
if (qe === null && qeIsCodex) {
|