baldart 4.16.2 → 4.17.1

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.
@@ -1,180 +1,307 @@
1
1
  export const meta = {
2
2
  name: 'new2-resolve',
3
3
  description:
4
- "Self-healing resolution pass for the autonomous new2 batch workflow. Called by new2 whenever a deterministic gate would otherwise need a human: a card fail/blocker (ac-unmet | blocker | qa-fail | e2e-blocked | merge-blocker), a legitimate scope-EXPANDING finding (scope-expansion), or a cross-cutting edge (agent-crash | baseline-fail). Runs an aggressive fix→verify loop (targeted coder judged multi-attempt adversarial re-verify); its TERMINAL is auto-materialising a tracked follow-up card so nothing is ever silently dropped. Domain-Override aware: doc fixes go to doc-reviewer, security/migration to coder and NEVER inline-bypassed. Agents Read /new's reference modules for semantics (args.refModulesBase). Uses agent()/parallel() only — no nested workflows.",
4
+ "Self-healing resolution pass for the autonomous new2 batch workflow. Called by new2 whenever a deterministic gate would otherwise need a human: a card fail/blocker (ac-unmet | blocker | qa-fail | e2e-blocked | merge-blocker), a legitimate scope-EXPANDING finding (scope-expansion), or a cross-cutting edge (agent-crash | baseline-fail). Tier-1 targeted fix with a TERMINAL short-circuit (skips the costly multi-attempt when the problem is impossible-by-definition, verified not trusted), then a judged multi-attempt; a MANDATORY adversarial judge cross-checks every verified claim against the real diff (prevents fabricated success). Specialized per domain (doc→doc-reviewer, ui→ui-expert, security→security-reviewer judge, perf→api-perf-cost-auditor judge). Terminal is a tracked follow-up. Accepts a `findings` list (batched per area). Uses agent()/parallel() only — no nested workflows.",
5
5
  phases: [
6
- { title: 'Diagnose', detail: 'classify the problem + decide scope-expansion boundary' },
6
+ { title: 'Diagnose', detail: 'classify + terminal short-circuit + scope-expansion boundary' },
7
7
  { title: 'Repair', detail: 'targeted fix, then judged multi-attempt if needed' },
8
- { title: 'Verify', detail: 'adversarial re-verify against the originating gate' },
8
+ { title: 'Verify', detail: 'mandatory adversarial judge + JS cross-check of verified files' },
9
9
  ],
10
10
  }
11
11
 
12
12
  // ───────────────────────────────────────────────────────────────────────────
13
13
  // args contract (from new2.js resolve()):
14
- // kind 'ac-unmet'|'blocker'|'qa-fail'|'e2e-blocked'|'merge-blocker'
15
- // |'scope-expansion'|'agent-crash'|'baseline-fail'
16
- // cardId string card (or finding id) the problem belongs to
17
- // evidence string verbatim AC / failing gate / finding evidence
18
- // worktreePath string where the fix happens
19
- // mayEditPaths string[] the card's file-ownership MAY-EDIT set
20
- // scopeFiles string[] files in scope for re-verify
21
- // domain 'doc'|'security'|'migration'|'code'|'perf'|'test'
22
- // refModulesBase string /new reference modules dir (semantics)
23
- // config object parsed baldart.config.yml
24
- //
25
- // return (consumed by new2.js):
26
- // { status: 'resolved'|'followup'|'fatal', followupCard?, reason? }
14
+ // kind, cardId, evidence, worktreePath, mayEditPaths[], scopeFiles[], domain,
15
+ // refModulesBase, config, ts, findings[] (batched: [{kind,evidence,domain}])
16
+ // return: { status:'resolved'|'followup'|'fatal', followupCard?, reason?, outOfScopeFindings? }
27
17
  // ───────────────────────────────────────────────────────────────────────────
28
18
 
29
- const a = args || {}
19
+ // F-004 tolerate args delivered as a JSON string.
20
+ let a = args || {}
21
+ if (typeof a === 'string') { try { a = JSON.parse(a) } catch (_) { a = {} } }
30
22
  const kind = a.kind || 'blocker'
31
23
  const card = a.cardId || 'CARD'
32
24
  const evidence = a.evidence || ''
33
25
  const worktree = a.worktreePath || '(cwd)'
34
26
  const mayEdit = Array.isArray(a.mayEditPaths) ? a.mayEditPaths : []
35
27
  const scopeFiles = Array.isArray(a.scopeFiles) ? a.scopeFiles : []
36
- const domain = a.domain || 'code'
28
+ // F-038 — review agents emit FREEFORM domain strings (e.g. 'documentation', 'frontend').
29
+ // Normalize to the routing buckets the fixer/judge maps key on, so a doc finding never
30
+ // falls through to the costly `coder` (the v4.17.0 'documentation'→coder regression).
31
+ function normDomain(d) {
32
+ const s = String(d || 'code').toLowerCase()
33
+ if (/doc|wiki|ssot|readme/.test(s)) return 'doc'
34
+ if (/ui|frontend|front-end|design|css|visual|component/.test(s)) return 'ui'
35
+ if (/sec|auth|secret|rls|tenant|xss|csrf|injection/.test(s)) return 'security'
36
+ if (/migrat|schema|ddl|\bsql\b/.test(s)) return 'migration'
37
+ if (/perf|cost|n\+1|latency|throughput/.test(s)) return 'perf'
38
+ if (/\btest|qa\b|spec|coverage/.test(s)) return 'test'
39
+ return 'code'
40
+ }
41
+ const domain = normDomain(a.domain || 'code')
42
+ const findings = Array.isArray(a.findings) && a.findings.length ? a.findings : [{ kind, evidence, domain }]
37
43
  const REF = (a.refModulesBase || '.claude/skills/new/references').replace(/\/$/, '')
38
44
  const cfg = a.config || {}
39
45
  const paths = cfg.paths || {}
40
46
  const backlogDir = paths.backlog_dir || 'backlog'
41
47
  const protectedDomain = domain === 'security' || domain === 'migration'
42
48
 
49
+ // F-019 — bounded immediate retry on transient errors (shared pattern; no import allowed).
50
+ const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
51
+ function isTransient(e) { return TRANSIENT.test(String((e && e.message) || e || '')) }
52
+ async function agentSafe(prompt, opts, maxAttempts) {
53
+ const cap = maxAttempts || 3
54
+ let lastErr = null
55
+ for (let i = 0; i < cap; i++) {
56
+ try {
57
+ const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
58
+ return await agent(prompt, o)
59
+ } catch (e) { lastErr = e; if (!isTransient(e)) throw e }
60
+ }
61
+ const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr)); err.transientExhausted = true; throw err
62
+ }
63
+
43
64
  const FIX_SCHEMA = {
44
65
  type: 'object', required: ['applied', 'verified'], additionalProperties: true,
45
66
  properties: {
46
- applied: { type: 'boolean', description: 'true if an edit was made' },
47
- verified: { type: 'boolean', description: 'true if the originating gate now passes (re-run it)' },
48
- crashed: { type: 'boolean', description: 'agent could not act (for agent-crash routing)' },
67
+ applied: { type: 'boolean' },
68
+ verified: { type: 'boolean', description: 'true only if you re-ran the originating gate and it passed' },
69
+ // F-008 terminal short-circuit (impossible-by-definition). Not trusted blindly.
70
+ terminal: { type: 'boolean', description: 'true if no in-MAY-EDIT fix can ever satisfy the gate' },
71
+ terminalReason: { enum: ['out-of-ownership', 'baseline-not-reached', 'owner-gated', 'not-a-code-defect', ''] },
72
+ remedyFiles: { type: 'array', items: { type: 'string' }, description: 'files the real fix would require editing (for out-of-ownership JS verification)' },
73
+ crashed: { type: 'boolean' },
74
+ // F-022 — incidental findings outside this card's MAY-EDIT (so they are not dropped).
75
+ outOfScopeFindings: { type: 'array', items: { type: 'object', additionalProperties: true } },
49
76
  note: { type: 'string' },
50
77
  },
51
78
  }
52
79
  const JUDGE_SCHEMA = {
53
80
  type: 'object', required: ['best'], additionalProperties: false,
54
- properties: { best: { type: 'number', description: '1-based index of the best attempt, or 0 if none works' }, rationale: { type: 'string' } },
81
+ properties: {
82
+ best: { type: 'number', description: '1-based index of the soundest verified attempt, or 0 if none truly holds' },
83
+ // F-015 — the files the judge INDEPENDENTLY confirmed exist and changed (cross-checked in JS).
84
+ verifiedFiles: { type: 'array', items: { type: 'string' } },
85
+ rationale: { type: 'string' },
86
+ },
87
+ }
88
+ const TERMINAL_JUDGE_SCHEMA = {
89
+ type: 'object', required: ['confirmed'], additionalProperties: false,
90
+ properties: { confirmed: { type: 'boolean', description: 'true if the terminal verdict genuinely holds' }, rationale: { type: 'string' } },
55
91
  }
56
92
  const FOLLOWUP_SCHEMA = {
57
93
  type: 'object', required: ['created', 'followupCard'], additionalProperties: true,
58
94
  properties: { created: { type: 'boolean' }, followupCard: { type: 'string' }, note: { type: 'string' } },
59
95
  }
60
96
 
97
+ // F-024 — domain-specialized fixer + judge (full map; reviewer-owns-its-domain — a doc
98
+ // finding is fixed by doc-reviewer, a security finding by security-reviewer, never coder).
99
+ const fixerAgent = ({ doc: 'doc-reviewer', ui: 'ui-expert', security: 'security-reviewer' })[domain] || 'coder'
100
+ const judgeAgent = (domain === 'security' || domain === 'migration') ? 'security-reviewer' : domain === 'perf' ? 'api-perf-cost-auditor' : 'code-reviewer'
101
+
102
+ const findingsBlock = findings.map((f, i) => ` ${i + 1}. [${f.kind || kind}/${f.domain || domain}] ${f.evidence}`).join('\n')
61
103
  const brief = [
62
104
  `Worktree: ${worktree} (cd into it; run all gates/git yourself).`,
63
105
  `Card: ${card}`,
64
106
  `Problem kind: ${kind}`,
65
- `Evidence: ${evidence}`,
107
+ findings.length > 1 ? `Findings to fix (batched — same fix-area):\n${findingsBlock}` : `Evidence: ${evidence}`,
66
108
  `MAY-EDIT (file-ownership — do NOT touch anything else): ${mayEdit.join(', ') || '(card scope)'}`,
67
109
  `Scope files for re-verify: ${scopeFiles.join(', ') || '(diff)'}`,
68
110
  `Domain: ${domain}${protectedDomain ? ' (PROTECTED — security/migration: never inline-bypass; the right specialist MUST apply)' : ''}`,
69
111
  `Reference modules (Read for exact semantics): ${REF}/`,
70
- `UI re-verify obligation: if your fix touches a UI file (app dir / components / *.css / *.scss), you MUST re-run the Phase 2.6 E2E review (per ${REF}/review-cycle.md) before declaring verified — a code-review/Codex fix can reintroduce visual/functional drift (mirrors codex-gate.md step 6).`,
71
- ].join('\n')
112
+ `UI re-verify obligation: if your fix touches a UI file, re-run the Phase 2.6 E2E review (per ${REF}/review-cycle.md) before declaring verified.`,
113
+ `If the fix requires editing files OUTSIDE MAY-EDIT, return terminal:true terminalReason:'out-of-ownership' remedyFiles:[...] (do NOT edit them). If the remedy is an owner-gated infra action, terminal:true terminalReason:'owner-gated'. Report any incidental defect outside MAY-EDIT in outOfScopeFindings.`,
114
+ ].filter(Boolean).join('\n')
72
115
 
73
- // The agent that OWNS a fix in this domain (Domain-Override): doc doc-reviewer.
74
- const fixerAgent = domain === 'doc' ? 'doc-reviewer' : 'coder'
116
+ // JS-deterministic ownership check (F-008 anti-escape-hatch): are remedy files ⊆ MAY-EDIT?
117
+ function filesInScope(files) {
118
+ if (!Array.isArray(files) || !files.length) return false
119
+ if (!mayEdit.length) return false
120
+ return files.every((f) => mayEdit.some((m) => String(f).includes(m) || m.includes(String(f))))
121
+ }
122
+ // F-033 — fabrication guard for the judge cross-check: a REAL fix touched AT LEAST ONE
123
+ // owned file. The judge naturally also lists adjacent changed files (e.g. the code a doc
124
+ // describes) — that is NOT fabrication, so `.every()` here over-rejected a sound fix and
125
+ // forced a wasted Tier-2 fan-out. `.some()` rejects only a verdict that confirmed ZERO
126
+ // owned files (pure phantom).
127
+ function someInScope(files) {
128
+ if (!Array.isArray(files) || !files.length || !mayEdit.length) return false
129
+ return files.some((f) => mayEdit.some((m) => String(f).includes(m) || m.includes(String(f))))
130
+ }
131
+ function collectOOS(...attemptObjs) {
132
+ const out = []
133
+ for (const o of attemptObjs) for (const x of (o && o.outOfScopeFindings) || []) out.push(x)
134
+ return out
135
+ }
75
136
 
76
- // ───────────────────────────────────────────────────────────────────────────
77
- // Phase Diagnose — branch by kind.
78
- // ───────────────────────────────────────────────────────────────────────────
79
137
  phase('Diagnose')
80
138
 
81
139
  // ---- baseline-fail (E2): try to make the trunk build; fatal if irrecoverable ---
82
140
  if (kind === 'baseline-fail') {
83
- const r = await agent(
84
- `The batch worktree's BASELINE build fails (trunk may not build — pre-existing). Try to make it build per ${REF}/setup.md step 6a.\n\n${brief}\n\nFix the build errors if they are tractable (missing dep, stale lock, trivial type error). Re-run tsc+lint+build. Return verified:true only if the baseline now passes. If the breakage is non-trivial/pre-existing and not safely fixable, return verified:false (the caller will treat the batch as fatal).`,
85
- { label: `resolve:baseline:${card}`, phase: 'Diagnose', agentType: 'coder', schema: FIX_SCHEMA }
86
- )
87
- if (r && r.verified) { log('baseline recovered.'); return { status: 'resolved' } }
88
- log('baseline irrecoverable — batch fatal.')
141
+ let r = null
142
+ try {
143
+ r = await agentSafe(
144
+ `The batch worktree's BASELINE build fails (trunk may not build). Try to make it build per ${REF}/setup.md step 6a.\n\n${brief}\n\nFix tractable build errors (missing dep, stale lock, trivial type error). Re-run tsc+lint+build. verified:true only if the baseline now passes; else verified:false.`,
145
+ { label: `resolve:baseline:${card}`, phase: 'Diagnose', agentType: 'coder', schema: FIX_SCHEMA }
146
+ )
147
+ } catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during baseline repair', outOfScopeFindings: [] } ; throw e }
148
+ if (r && r.verified) { log('baseline recovered.'); return { status: 'resolved', outOfScopeFindings: collectOOS(r) } }
89
149
  return { status: 'fatal', reason: 'baseline build irrecoverable' }
90
150
  }
91
151
 
92
- // ---- scope-expansion (Asse 2): deterministic boundary, no arbitrary threshold ---
152
+ // ---- scope-expansion (Asse 2): deterministic boundary ---
93
153
  if (kind === 'scope-expansion') {
94
- // Integrate iff: within MAY-EDIT ownership AND not security/migration AND not a NEW AC.
95
- const decide = await agent(
96
- `A review finding EXPANDS scope beyond card ${card}'s acceptance criteria (not a fail — extra legitimate work). Decide per the deterministic boundary (no line-count threshold):\n` +
97
- `INTEGRATE NOW iff ALL hold: (1) the fix stays inside MAY-EDIT ownership; (2) domain is NOT security/migration; (3) it does NOT introduce a new user-facing acceptance criterion. Otherwise → MATERIALISE A FOLLOW-UP.\n\n${brief}\n\n` +
98
- `If INTEGRATE: apply the fix via the domain owner discipline (you are ${fixerAgent}), re-run lint+tsc, return applied:true verified:true. If FOLLOW-UP: return applied:false verified:false note:'needs-followup: <why boundary failed>'. Do NOT prompt.`,
99
- { label: `resolve:scope:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
100
- )
101
- if (decide && decide.verified) { log('scope-expansion integrated within ownership.'); return { status: 'resolved' } }
102
- return await materialiseFollowup('scope-expansion', (decide && decide.note) || 'outside ownership / new AC / protected domain')
154
+ let decide = null
155
+ try {
156
+ decide = await agentSafe(
157
+ `A review finding EXPANDS scope beyond card ${card}'s acceptance criteria. Decide per the deterministic boundary (no line-count threshold):\n` +
158
+ `INTEGRATE NOW iff ALL: (1) fix stays inside MAY-EDIT; (2) domain NOT security/migration; (3) no NEW user-facing AC. Else MATERIALISE A FOLLOW-UP.\n\n${brief}\n\n` +
159
+ `If INTEGRATE: apply (you are ${fixerAgent}), re-run lint+tsc, return applied:true verified:true. If FOLLOW-UP: applied:false verified:false note:'needs-followup: <why>'.`,
160
+ { label: `resolve:scope:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
161
+ )
162
+ } catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during scope-expansion', outOfScopeFindings: [] }; throw e }
163
+ if (decide && decide.verified) {
164
+ const ok = await judgeVerify([{ i: 1, r: decide }])
165
+ if (ok.ok) { log('scope-expansion integrated within ownership.'); return { status: 'resolved', outOfScopeFindings: collectOOS(decide) } }
166
+ }
167
+ return await materialiseFollowup('scope-expansion', (decide && decide.note) || 'outside ownership / new AC / protected', collectOOS(decide))
103
168
  }
104
169
 
105
- // ---- agent-crash (E1): retry → route to the right specialist; protected = never bypass --
170
+ // ---- agent-crash (E1) ---
106
171
  if (kind === 'agent-crash') {
107
- const r = await agent(
108
- `A subagent crashed while handling this work. Re-attempt it with the CORRECT specialist (${fixerAgent}) per ${REF}/commit.md § Sub-agent failure protocol.\n\n${brief}\n\nRetry the work once. ${protectedDomain ? 'This is a PROTECTED domain (security/migration): if you cannot complete it, return verified:false — NEVER inline-bypass.' : 'If transient, complete it.'} Re-verify the originating gate. Return verified:true only if done + verified.`,
109
- { label: `resolve:crash:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
110
- )
111
- if (r && r.verified) return { status: 'resolved' }
112
- return await materialiseFollowup('agent-crash', 'subagent crash unrecovered' + (protectedDomain ? ' (protected domain — not bypassed)' : ''))
172
+ let r = null
173
+ try {
174
+ r = await agentSafe(
175
+ `A subagent crashed. Re-attempt with the CORRECT specialist (${fixerAgent}) per ${REF}/commit.md § Sub-agent failure protocol.\n\n${brief}\n\nRetry once. ${protectedDomain ? 'PROTECTED domain: if you cannot complete it, verified:false — NEVER inline-bypass.' : ''} Re-verify the gate; verified:true only if done.`,
176
+ { label: `resolve:crash:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
177
+ )
178
+ } catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during crash recovery', outOfScopeFindings: [] }; throw e }
179
+ if (r && r.verified) { const ok = await judgeVerify([{ i: 1, r }]); if (ok.ok) return { status: 'resolved', outOfScopeFindings: collectOOS(r) } }
180
+ return await materialiseFollowup('agent-crash', 'subagent crash unrecovered' + (protectedDomain ? ' (protected — not bypassed)' : ''), collectOOS(r))
113
181
  }
114
182
 
115
183
  // ───────────────────────────────────────────────────────────────────────────
116
- // Asse 1 (ac-unmet | blocker | qa-fail | e2e-blocked | merge-blocker)
117
- // Tier 1: targeted fix → re-verify. Tier 2: judged multi-attempt. Terminal: follow-up.
184
+ // Asse 1 — Tier-1 (with terminal short-circuit) judged multi-attempt follow-up.
118
185
  // ───────────────────────────────────────────────────────────────────────────
119
186
  const gateHint = {
120
- 'ac-unmet': `Implement the unmet acceptance criterion EXACTLY (no scope expansion), per ${REF}/completeness.md Phase 2.5b. Re-verify the AC against the code.`,
187
+ 'ac-unmet': `Implement the unmet acceptance criterion EXACTLY, per ${REF}/completeness.md Phase 2.5b. Re-verify against the code.`,
121
188
  blocker: `Fix the blocking finding per ${REF}/codex-gate.md / ${REF}/completeness.md. Re-run the failing gate.`,
122
- 'qa-fail': `Fix the failing QA gates per ${REF}/review-cycle.md Phase 3.5 — fix the code, not the tests (unless a test is wrong). Re-run qa-sentinel.`,
123
- 'e2e-blocked': `Fix the implementation so the E2E review passes per ${REF}/review-cycle.md Phase 2.6 (never override known-broken UI). Re-run /e2e-review.`,
189
+ 'qa-fail': `Fix the failing QA gates per ${REF}/review-cycle.md Phase 3.5 — fix the code, not the tests. Re-run qa-sentinel.`,
190
+ 'e2e-blocked': `Fix so the E2E review passes per ${REF}/review-cycle.md Phase 2.6. Re-run /e2e-review.`,
124
191
  'merge-blocker': `Fix the verified BLOCKER/HIGH from the final review per ${REF}/final-review.md F.5. Re-verify.`,
125
192
  }[kind] || `Fix the blocker and re-verify.`
126
193
 
127
194
  phase('Repair')
128
- let attempt = await agent(
129
- `Tier-1 targeted repair for card ${card} (${kind}).\n\n${brief}\n\n${gateHint}\n\nApply the minimal correct fix within MAY-EDIT only. Re-run the originating gate yourself and report verified honestly (do NOT claim verified without re-running it).`,
130
- { label: `resolve:${kind}:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
131
- )
132
- if (attempt && attempt.verified) { log(`${kind} resolved (tier 1).`); return { status: 'resolved' } }
133
-
134
- // Tier 2 judged multi-attempt (parallel divergent fixes judge adversarial verify).
135
- // Budget guard: skip the fan-out when the run is near its token target.
136
- const canFanOut = !budget.total || budget.remaining() > 60000
195
+ let attempt = null
196
+ try {
197
+ attempt = await agentSafe(
198
+ `Tier-1 targeted repair for card ${card} (${kind}).\n\n${brief}\n\n${gateHint}\n\nApply the minimal correct fix within MAY-EDIT only. Re-run the originating gate and report verified honestly (never claim verified without re-running it).`,
199
+ { label: `resolve:${kind}:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
200
+ )
201
+ } catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during tier-1', outOfScopeFindings: [] }; throw e }
202
+
203
+ // F-008 terminal short-circuit, verified not trusted.
204
+ if (attempt && attempt.terminal) {
205
+ const tr = attempt.terminalReason || ''
206
+ let confirmed = false
207
+ if (tr === 'out-of-ownership') {
208
+ confirmed = !filesInScope(attempt.remedyFiles) // genuinely terminal iff remedy files are NOT in MAY-EDIT
209
+ } else {
210
+ // owner-gated / not-a-code-defect / baseline-not-reached — ratify with the judge.
211
+ try {
212
+ const tj = await agentSafe(
213
+ `A repair agent declared this problem TERMINAL (reason: ${tr}) — no in-MAY-EDIT code fix can satisfy it.\n\n${brief}\n\nIndependently verify: is that verdict genuinely correct, or is it an excuse to avoid work? Return confirmed:true only if it truly holds.`,
214
+ { label: `resolve:terminal-judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: TERMINAL_JUDGE_SCHEMA }
215
+ )
216
+ confirmed = !!(tj && tj.confirmed)
217
+ } catch (_) { confirmed = false }
218
+ }
219
+ if (confirmed) { log(`${kind} terminal (${tr}) — short-circuit to follow-up.`); return await materialiseFollowup(kind, `terminal: ${tr} — ${attempt.note || ''}`, collectOOS(attempt)) }
220
+ log(`terminal verdict (${tr}) rejected — proceeding to multi-attempt.`)
221
+ }
222
+
223
+ // F-015 — a verified Tier-1 still goes through the mandatory judge cross-check.
224
+ if (attempt && attempt.verified) {
225
+ const ok = await judgeVerify([{ i: 1, r: attempt }])
226
+ if (ok.ok) { log(`${kind} resolved (tier 1, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt) } }
227
+ log('tier-1 verified rejected by judge cross-check — escalating.')
228
+ }
229
+
230
+ // Tier 2 — judged multi-attempt (budget-guarded; single specialist for protected domains).
231
+ const canFanOut = (typeof budget === 'undefined' || !budget.total || budget.remaining() > 60000)
232
+ let tier2OOS = []
137
233
  if (canFanOut && !protectedDomain) {
138
234
  phase('Repair')
139
- const angles = [
235
+ // F-036 the 3-angle fan-out (root-cause / call-site / reuse-utility) is code-bug-shaped.
236
+ // For deterministic non-code domains (doc/test) it is 3× waste — a single targeted retry
237
+ // is enough. Reserve the fan-out for code/perf/ui where the angles genuinely diverge.
238
+ const codeShaped = !/^(doc|test)$/.test(domain)
239
+ const angles = codeShaped ? [
140
240
  'Fix at the root cause (change the underlying logic/contract).',
141
241
  'Fix defensively at the call site (guard/validate without changing the contract).',
142
242
  'Fix by reusing an existing utility/pattern instead of new code.',
243
+ ] : [
244
+ 'Apply the single correct fix — the residual is deterministic: re-derive the exact change and apply it.',
143
245
  ]
144
- const attempts = (await parallel(angles.map((angle, i) => () =>
145
- agent(
146
- `Tier-2 repair attempt #${i + 1} for card ${card} (${kind}), angle: ${angle}\n\n${brief}\n\n${gateHint}\n\nApply your fix within MAY-EDIT, re-run the gate, and report whether it now passes. Work in isolation; the best attempt will be selected.`,
246
+ const tries = (await parallel(angles.map((angle, i) => () =>
247
+ agentSafe(
248
+ `Tier-2 repair attempt #${i + 1} for card ${card} (${kind}), angle: ${angle}\n\n${brief}\n\n${gateHint}\n\nApply within MAY-EDIT, re-run the gate, report whether it passes. Work in isolation; the best attempt is selected.`,
147
249
  { label: `resolve:${kind}:${card}#${i + 1}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
148
250
  ).then((r) => ({ i: i + 1, r })).catch(() => null)
149
251
  ))).filter(Boolean)
150
-
151
- const verified = attempts.filter((x) => x.r && x.r.verified)
252
+ tier2OOS = collectOOS(...tries.map((t) => t.r))
253
+ const verified = tries.filter((x) => x.r && x.r.verified)
152
254
  if (verified.length) {
153
- phase('Verify')
154
- // Adversarial pick: judge which verified attempt is the soundest (refute the rest).
155
- const judge = await agent(
156
- `Adversarially judge these repair attempts for card ${card} (${kind}). Each claims the gate now passes. Pick the SOUNDEST (best minimal, least regression-prone) — try to refute each with path:line reasoning. Return its 1-based index, or 0 if none truly holds.\n\nEvidence: ${evidence}\nAttempts that claim verified: ${verified.map((v) => `#${v.i}: ${v.r.note || ''}`).join(' | ')}`,
157
- { label: `resolve:judge:${card}`, phase: 'Verify', agentType: 'code-reviewer', schema: JUDGE_SCHEMA }
158
- )
159
- if (judge && judge.best > 0) { log(`${kind} resolved (tier 2, attempt #${judge.best}).`); return { status: 'resolved' } }
255
+ const ok = await judgeVerify(verified)
256
+ if (ok.ok) { log(`${kind} resolved (tier 2, attempt #${ok.best}, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt).concat(tier2OOS) } }
160
257
  }
161
258
  } else if (protectedDomain) {
162
- log('protected domain — single specialist attempt only (no parallel fan-out).')
259
+ log('protected domain — single specialist attempt only (no fan-out).')
163
260
  } else {
164
261
  log('budget near target — skipping tier-2 fan-out.')
165
262
  }
166
263
 
167
- // Terminal nothing resolved autonomously: materialise a tracked follow-up card.
168
- return await materialiseFollowup(kind, attempt && attempt.note ? attempt.note : 'unresolved after repair tiers')
264
+ return await materialiseFollowup(kind, (attempt && attempt.note) || 'unresolved after repair tiers', collectOOS(attempt).concat(tier2OOS))
169
265
 
170
266
  // ───────────────────────────────────────────────────────────────────────────
171
- async function materialiseFollowup(k, reason) {
172
- const r = await agent(
173
- `Materialise a follow-up backlog card so this residual is TRACKED, not dropped (per ${REF}/completeness.md Phase 2.5b option 3 — the sanctioned, NON-silent deferral).\n\n${brief}\nKind: ${k}\nReason it could not be auto-resolved: ${reason}\n\n` +
174
- `Create ${backlogDir}/${card}-followup-<gate>.yml with status: TODO and the MINIMUM valid fields (non-empty requirements ≥1 derived from the residual, acceptance_criteria ≥1 = the verbatim residual, files_likely_touched ≥1 from this card's ownership). It MUST pass the /new pre-flight field check. Return the created card id.`,
175
- { label: `resolve:followup:${card}`, phase: 'Verify', agentType: 'general-purpose', schema: FOLLOWUP_SCHEMA }
176
- )
177
- const followupCard = (r && r.followupCard) || `${card}-followup`
178
- log(`${k} follow-up card ${followupCard} (nothing dropped).`)
179
- return { status: 'followup', followupCard, reason }
267
+ // F-015/F-033 mandatory adversarial judge + deterministic JS cross-check.
268
+ // The judge (cross-model code-reviewer / domain specialist) does its OWN grep and
269
+ // returns the files it independently confirmed changed; we cross-check MAY-EDIT.
270
+ async function judgeVerify(verifiedAttempts) {
271
+ if (!verifiedAttempts.length) return { ok: false, best: 0 }
272
+ let judge = null
273
+ try {
274
+ judge = await agentSafe(
275
+ `Adversarially judge these repair attempts for card ${card} (${kind}). Each CLAIMS the gate now passes — verify INDEPENDENTLY (grep/read the files yourself; do NOT trust the claim). Pick the SOUNDEST (best minimal, least regression-prone); return 0 if NONE genuinely holds. Also return verifiedFiles = the files you independently confirmed exist AND changed.\n\nEvidence: ${evidence}\nAttempts claiming verified: ${verifiedAttempts.map((v) => `#${v.i}: ${(v.r && v.r.note) || ''}`).join(' | ')}`,
276
+ { label: `resolve:judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: JUDGE_SCHEMA }
277
+ )
278
+ } catch (e) { if (e && e.transientExhausted) return { ok: false, best: 0 }; throw e }
279
+ if (!judge || !(judge.best > 0)) return { ok: false, best: 0 }
280
+ // Deterministic JS cross-check: the judge-confirmed files must be within MAY-EDIT.
281
+ if (mayEdit.length && Array.isArray(judge.verifiedFiles) && judge.verifiedFiles.length && !someInScope(judge.verifiedFiles)) {
282
+ log('judge verifiedFiles are ENTIRELY outside MAY-EDIT — rejecting (possible fabrication).')
283
+ return { ok: false, best: 0 }
284
+ }
285
+ return { ok: true, best: judge.best }
286
+ }
287
+
288
+ async function materialiseFollowup(k, reason, oos) {
289
+ let r = null
290
+ try {
291
+ // F-039 — backlog cards are owned by prd-card-writer (card-template + Rule C
292
+ // review_profile + owner_agent + traceability), NOT a hand-written Haiku stub.
293
+ r = await agentSafe(
294
+ `Create ONE follow-up backlog card so this residual is TRACKED, not dropped (per ${REF}/completeness.md Phase 2.5b option 3). You are prd-card-writer: apply your card-template, Rule C (review_profile), owner_agent routing, and traceability rules — do NOT emit a minimal stub.\n\n${brief}\nKind: ${k}\nResidual domain: ${domain}\nReason unresolved: ${reason}\n\n` +
295
+ `Write ${backlogDir}/${card}-followup-<gate>.yml with status: TODO, derived from the residual: requirements + acceptance_criteria (the verbatim residual as ≥1 AC), owner_agent routed to the residual domain (${domain}), review_profile per Rule C, files_likely_touched ≥1 from the card ownership / remedy files. It MUST pass the /new pre-flight field check. Return the created card id.`,
296
+ { label: `resolve:followup:${card}`, phase: 'Verify', agentType: 'prd-card-writer', schema: FOLLOWUP_SCHEMA }
297
+ )
298
+ } catch (e) {
299
+ // F-020 — could not materialise (e.g. outage): return WITHOUT a followupCard so the
300
+ // SKILL writes it from the offline-safe residual ledger. Never claim it was created.
301
+ log(`follow-up materialisation failed (${String(e && e.message)}) — skill will reconcile.`)
302
+ return { status: 'followup', followupCard: null, reason, outOfScopeFindings: oos || [] }
303
+ }
304
+ const followupCard = (r && r.created && r.followupCard) ? r.followupCard : null
305
+ log(`${k} → follow-up ${followupCard || '(deferred to skill)'} (nothing dropped).`)
306
+ return { status: 'followup', followupCard, reason, outOfScopeFindings: oos || [] }
180
307
  }