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.
- package/CHANGELOG.md +27 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/prd-card-writer.md +25 -0
- package/framework/.claude/skills/new2/SKILL.md +42 -17
- package/framework/.claude/workflows/new-final-review.js +66 -17
- package/framework/.claude/workflows/new2-resolve.js +219 -92
- package/framework/.claude/workflows/new2.js +432 -225
- package/framework/docs/WORKFLOWS.md +2 -2
- package/package.json +1 -1
|
@@ -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).
|
|
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
|
|
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
|
|
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
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
47
|
-
verified: { type: 'boolean', description: 'true if the originating gate
|
|
48
|
-
|
|
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: {
|
|
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
|
|
71
|
-
].
|
|
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
|
-
//
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
152
|
+
// ---- scope-expansion (Asse 2): deterministic boundary ---
|
|
93
153
|
if (kind === 'scope-expansion') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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)
|
|
170
|
+
// ---- agent-crash (E1) ---
|
|
106
171
|
if (kind === 'agent-crash') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
|
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
|
|
123
|
-
'e2e-blocked': `Fix
|
|
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 =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
`Tier-2 repair attempt #${i + 1} for card ${card} (${kind}), angle: ${angle}\n\n${brief}\n\n${gateHint}\n\nApply
|
|
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 =
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
}
|