baldart 4.16.1 → 4.17.0
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 +25 -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/new2-resolve.js +187 -90
- package/framework/.claude/workflows/new2.js +416 -225
- package/framework/docs/WORKFLOWS.md +2 -2
- package/package.json +1 -1
- package/src/utils/__tests__/classify-divergence.test.js +21 -0
- package/src/utils/__tests__/overlay-capture.test.js +50 -0
- package/src/utils/git.js +39 -2
- package/src/utils/overlay-capture.js +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.17.0] - 2026-06-07
|
|
9
|
+
|
|
10
|
+
**`new2` hardening single-wave: 36 finding dal primo run reale risolti in un colpo.** Il primo run reale di `new2` (batch FEAT-0021, 9 card) sotto un outage 529 prolungato è costato **7.4M token / 100 agent / 5h08m producendo solo 6/9 card reali**, con 2 stati `DONE` falsi nel backlog, codice security RLS-bypass non revisionato finito su `develop` via un "safety commit", e 1 follow-up persa. Una raccolta diagnostica end-to-end (36 finding verificati su codice + telemetria reale) ha isolato cause **strutturali**; questo rilascio le risolve tutte in un'unica wave, riportando il costo atteso a ~1.5-2.5M e — prioritario — azzerando le violazioni di integrità/sicurezza sotto degradazione. **MINOR** (cambio sostanziale di capacità del workflow esperimentale `new2`; nessuna rimozione/cambio layout d'install; **nessuna nuova chiave `baldart.config.yml`** → schema-propagation N/A).
|
|
11
|
+
|
|
12
|
+
> **Why.** `new2` è un A/B di `/new` per misurare l'economia di contesto del workflow-hosting. Il run reale ha mostrato che, oltre al costo, l'autonomia zero-ask degradava in modo **non sicuro** sotto outage (DONE falsi, codice non revisionato su trunk, follow-up persa) — esattamente il recovery che `/new` (gated, main-loop) avrebbe fermato. Decisioni utente vincolanti: *Decision-auto, infra-deferred* (niente `BALDART_AUTONOMOUS=1`; azioni infra owner-gated sempre policy-deferred) e *agenti specializzati come `/new`* (no general-purpose dove conta la qualità).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **`framework/.claude/workflows/new2.js` — scheduler a DAG + resilienza + integrità.** Il for-loop sequenziale e il ramo team-mode sono sostituiti da uno **scheduler dependency-gated** (semantica single-worktree): una card parte solo quando TUTTE le dipendenze in-batch sono `committed`; i dipendenti di una dep fallita vanno `blocked` (niente resolve doomed, F-017/F-021); fallimento transitorio → `pending` + re-invocazione (cap); outage sostenuto → **degrada pulito** (`degraded:true` + worktree clean + return-as-ledger, F-012/F-019/F-020). Implement usa l'**owner_agent** della card; review è un **fan-out specializzato** sfrondato dal `review_profile` (F-024/F-025); commit è **Haiku** con `git status` reconcile, mai `git add -A` (F-023). Skip-completed idempotente su commit-sha+receipt+no-open-followup, NON sul flag `DONE` (F-026). Merge **integrity-gated**: mai force-DONE (F-029), mai commit di codice non revisionato (F-030), mai merge di batch incompleto/degradato (F-031). Telemetria veritiera: `total_tokens` via `budget.spent()`, `agent_count`, `degraded`, `cards_real_done` vs `cards_force_done`=0 (F-032). Parse-guard `args` (F-001).
|
|
16
|
+
- **`framework/.claude/workflows/new2-resolve.js` — short-circuit + anti-fabbricazione + batching.** Verdetto **terminale** che salta il multi-attempt quando il problema è impossibile-by-definition (`out-of-ownership` verificato in JS contro MAY-EDIT; altri reason ratificati dal judge → niente escape-hatch, F-008). **Judge avversariale OBBLIGATORIO** su ogni claim `verified`, con **cross-check JS** dei file confermati indipendentemente dal judge (defeats fabricated success, beccata 2× nel run reale — F-015/F-033). Lista **`findings` batchata** (1 resolve per fix-area, F-007/F-035/F-037). Fixer/judge **per dominio** (doc→doc-reviewer, ui→ui-expert, security→security-reviewer judge, perf→api-perf-cost-auditor judge — F-024). `outOfScopeFindings` non più persi (F-022). Follow-up su Haiku, **offline-safe** (deferita al skill se nessun agente può scrivere, F-020). Parse-guard `args` (F-004).
|
|
17
|
+
- **`framework/.claude/skills/new2/SKILL.md` — reconcile + resume + telemetria reale.** Step 4 passa `ts` (il workflow non ha clock). Step 5: materializza le follow-up dei `residuals` non ancora su disco (offline-safe — il skill ha il filesystem → chiude "nothing dropped" sotto outage, F-020), **rilancia** con `resumeFromRunId` se `degraded` (idempotente via skip-completed), registra telemetria veritiera (`wall_clock_s`, `followups_on_disk` contando i file — F-032).
|
|
18
|
+
- **`framework/.claude/agents/prd-card-writer.md` — Rule A.1 (AC↔ownership consistency).** Fix a monte del difetto ricorrente "AC fuori dal MAY-EDIT" (F-016): ogni AC dev'essere soddisfabile editando i file della propria card.
|
|
19
|
+
- **`framework/docs/WORKFLOWS.md`** — righe `new2`/`new2-resolve` aggiornate al comportamento v4.17.0.
|
|
20
|
+
|
|
21
|
+
## [4.16.2] - 2026-06-06
|
|
22
|
+
|
|
23
|
+
**`baldart update` non si blocca più all'infinito su un edit `.framework/` ormai assorbito a monte.** Un edit locale a un file framework (es. `agents/ui-expert.md`) committato con un subject che *non* contiene la parola "baldart" sfuggiva alla classificazione `chore-wrapper` (rumore) e veniva marcato `custom-overlay-able`. Quel commit resta per sempre ancestor di `HEAD` ma mai di `FETCH_HEAD`, quindi `git log FETCH_HEAD..HEAD -- .framework` lo ripesca a **ogni rilascio successivo** — e il flusso seamless si fermava sul gate `divergence-capture-incomplete` perché l'overlay (additivo) non riproduceva byte-a-byte il file `.framework`, anche quando il contenuto era già identico all'upstream e la personalizzazione viveva correttamente nell'overlay. Risultato: lo stesso commit "fantasma" rifaceva muro a ogni `update`, senza via d'uscita via overlay (riconciliare un overlay additivo non può mai far passare il round-trip stretto). **PATCH** (bugfix del detector di divergenza + reset-safety degli overlay additivi; nessun cambio di capability, nessuna chiave `baldart.config.yml`).
|
|
24
|
+
|
|
25
|
+
> **Why.** Il check di fedeltà capture+verify pretendeva `merge(base_upstream, overlay) == file_.framework`. Per un overlay `extend` (additivo) questo è **strutturalmente falso** quando `.framework` è pristine-upstream: il merge aggiunge sezioni, il file no → "stale" eterno. E il classifier non distingueva un edit *vivo* (da preservare) da uno *assorbito* (contenuto già a monte → un re-sync non perde nulla). Due correzioni complementari chiudono la classe di falso positivo per tutti i consumer.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`src/utils/git.js` — categoria `absorbed` nel classifier di divergenza.** `classifyDivergence` ora demota a rumore (`absorbed`) ogni commit `custom-*` i cui file `.framework/` toccati sono **byte-identici all'upstream (`FETCH_HEAD`)** — confronto di blob-SHA, prefisso `.framework/` strippato per mappare il path upstream; conservativo (file fuori da `.framework/`, mancante a monte, o SHA diverso → resta drift reale). `aggregateDivergenceClass` include `absorbed` tra le `noiseCategories`, così un edit ormai assorbito non spinge più la classe verso `overlay-able`/`mixed`: la batch torna `all-noise` → auto-resolve pulito.
|
|
30
|
+
- **`src/utils/overlay-capture.js` — reset-safety per overlay additivi (`captureAndVerify`).** Quando esiste già un overlay agent/command, il file `.framework` viene riconosciuto come **reset-safe** (nessun edit inline da perdere) se è pristine: (a) byte-identico alla base upstream verso cui si farebbe reset, **oppure** (b) corrisponde alla base con cui l'overlay è stato autorato (`base_file_sha` in frontmatter) — pristine anche dopo che l'upstream è andato avanti. In quel caso è `captured` (non più `stale`), invece di pretendere il round-trip stretto che un overlay additivo non può mai soddisfare. Il caso *genuinamente* stale (edit inline reale che l'overlay non riproduce) resta correttamente un blocker.
|
|
31
|
+
- **Test.** `classify-divergence.test.js`: 3 fixture per `absorbed` (da solo / con merge plumbing → `all-noise`; con overlay-able genuino → `overlay-able`). `overlay-capture.test.js`: 2 fixture per la reset-safety (overlay additivo + `.framework` == base → `captured`; upstream spostato ma `.framework` pristine vs `base_file_sha` → `captured`).
|
|
32
|
+
|
|
8
33
|
## [4.16.1] - 2026-06-05
|
|
9
34
|
|
|
10
35
|
**Review di allineamento `new` ↔ `new2`: 9 fix di fedeltà sui workflow `new2`/`new2-resolve` emersi da una review adversariale a 3 reviewer paralleli.** La v4.16.0 ha introdotto `new2` come variante workflow-hosted di `/new`; una review di allineamento (gate-coverage · child-workflow wiring · control-flow) ha refutato l'implementazione su difetti reali — di semantica/allineamento, non di esecuzione (il wiring tra i 3 workflow era contract-fedele). **PATCH** (bugfix di allineamento del nuovo `new2`, nessun cambio di capability, nessuna chiave `baldart.config.yml`).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.17.0
|
|
@@ -136,6 +136,31 @@ that list — not this one — is the source of truth.
|
|
|
136
136
|
**Epic cards** use `owner_agent: ""` (empty string) because they are
|
|
137
137
|
trackers, not implementation. The dispatcher skips them.
|
|
138
138
|
|
|
139
|
+
### Rule A.1 — AC↔ownership consistency (every AC must be satisfiable inside the card)
|
|
140
|
+
|
|
141
|
+
For EVERY acceptance criterion you write, the file(s) that AC requires editing
|
|
142
|
+
MUST be a subset of that card's `files_likely_touched` (its MAY-EDIT ownership).
|
|
143
|
+
An AC whose ONLY implementation site lives in another card's file is
|
|
144
|
+
**structurally unsatisfiable** by this card — at execution time it can never
|
|
145
|
+
close, so it burns an entire resolve cycle and then defers anyway (and, on
|
|
146
|
+
impossible tasks, *induces* fabricated "fixed" claims). Concretely:
|
|
147
|
+
|
|
148
|
+
- If an AC needs the JSX **render** of a value, the card that owns the render
|
|
149
|
+
file (e.g. `homepage-store.tsx`) must carry that AC — not the data/view-model
|
|
150
|
+
card that merely computes the value.
|
|
151
|
+
- If an AC needs **wiring at a call-site** (e.g. `after(() => dispatch())` inside
|
|
152
|
+
a create funnel in `data.ts`), assign it to the card that owns that file, or
|
|
153
|
+
add that file to this card's `files_likely_touched`, or express the dependency
|
|
154
|
+
explicitly via `depends_on` + a card that owns the site.
|
|
155
|
+
- An AC whose remedy is an **owner-gated infra action** (remote DB push, deploy,
|
|
156
|
+
secret rotation, DNS) is NOT a code AC — phrase it as a tracked manual/ops
|
|
157
|
+
step, never as a per-card code acceptance criterion.
|
|
158
|
+
|
|
159
|
+
Self-check before emitting cards: for each AC, name the file it edits and
|
|
160
|
+
confirm that file is in the same card's `files_likely_touched`. If it isn't,
|
|
161
|
+
move the AC or widen ownership. (This is the upstream fix for the recurring
|
|
162
|
+
"AC outside MAY-EDIT" failure; `new2`/`/new` also re-assert it at pre-flight.)
|
|
163
|
+
|
|
139
164
|
### Rule B — UI cards (`ui-expert`) have UI-ONLY scope
|
|
140
165
|
|
|
141
166
|
When you assign `owner_agent: ui-expert` to a card, its scope MUST cover
|
|
@@ -99,30 +99,55 @@ Workflow({ name: 'new2', args: {
|
|
|
99
99
|
metricsDir, // $METRICS
|
|
100
100
|
refModulesBase, // .claude/skills/new/references (semantic SSOT)
|
|
101
101
|
config, // the parsed baldart.config.yml (paths.*/stack.*/features.*/git.*)
|
|
102
|
+
ts, // ISO timestamp NOW — the workflow has no clock (Date.now() unavailable there)
|
|
102
103
|
flags: { stats, effort, full }
|
|
103
104
|
}})
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
Record the kickoff time (`ts`) before the call — Step 5 uses it to stamp the
|
|
108
|
+
real wall-clock duration (the workflow cannot read a clock).
|
|
109
|
+
|
|
106
110
|
The workflow runs the entire batch in the background. **Do not poll it** — it
|
|
107
111
|
returns when the batch is done. It returns:
|
|
108
112
|
|
|
109
113
|
```
|
|
110
|
-
{ report, perCardResults, gateLedger, residualFollowups,
|
|
114
|
+
{ report, perCardResults, gateLedger, residualFollowups, residuals,
|
|
115
|
+
degraded, degradationReasons, telemetry }
|
|
111
116
|
```
|
|
112
117
|
|
|
113
|
-
`report`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
- `report` — ready-to-show markdown batch summary.
|
|
119
|
+
- `residuals` — the **OFFLINE-SAFE ledger of record**: every residual the workflow
|
|
120
|
+
could not finish, each `{ card, kind, evidence, materialized }`. A residual with
|
|
121
|
+
`materialized:false` has NO follow-up card on disk yet (e.g. the workflow hit an
|
|
122
|
+
outage where no agent could write the file). **You (the skill) must reconcile it.**
|
|
123
|
+
- `degraded` / `degradationReasons` — the batch stopped early under a sustained
|
|
124
|
+
outage (or another degradation). The batch is NOT complete; it must be resumed.
|
|
125
|
+
- `telemetry` — the Phase-8 record (`variant:"new2"`).
|
|
126
|
+
|
|
127
|
+
### Step 5 — Reconcile, resume, present, record
|
|
128
|
+
|
|
129
|
+
1. **Materialise missing follow-ups (offline-safe — you have filesystem access, the
|
|
130
|
+
workflow does not).** For every `residuals[]` entry with `materialized:false`,
|
|
131
|
+
write a follow-up card `${paths.backlog_dir}/<card>-followup-<kind>.yml` (status:
|
|
132
|
+
TODO; ≥1 requirement derived from the residual; acceptance_criteria = the verbatim
|
|
133
|
+
residual; files_likely_touched from the card's ownership) — it MUST pass the `/new`
|
|
134
|
+
pre-flight field check. This is the layer that guarantees **nothing is ever
|
|
135
|
+
dropped, even when every agent was dead** during the run.
|
|
136
|
+
2. **Resume if degraded.** If `degraded` is true, re-invoke the workflow with
|
|
137
|
+
`Workflow({ scriptPath, resumeFromRunId })` (same `args` + the new `ts`). The
|
|
138
|
+
per-card **skip-completed** guard makes the resume idempotent — already-committed
|
|
139
|
+
cards are skipped, only the incomplete/blocked ones run. Repeat until `degraded`
|
|
140
|
+
is false (or the same cards stall twice → surface to the user).
|
|
141
|
+
3. **Present.** Print `report` verbatim. Surface `residuals` prominently
|
|
142
|
+
("questi residui sono tracciati come follow-up: …") — the post-run review that
|
|
143
|
+
replaced the ~25 mid-run questions. If `degraded`, say so plainly (the run was
|
|
144
|
+
incomplete and resumed).
|
|
145
|
+
4. **Record truthful telemetry.** Before appending `telemetry` to
|
|
146
|
+
`${metricsDir}/skill-runs.jsonl`, fill the fields the workflow could not compute:
|
|
147
|
+
`wall_clock_s` (now − kickoff `ts`) and `followups_on_disk` (count the actual
|
|
148
|
+
follow-up files on disk, NOT `residualFollowups.length` — which double-counts).
|
|
149
|
+
`total_tokens`/`agent_count` come from the workflow (`budget.spent()` delta +
|
|
150
|
+
spawn counter); if `total_tokens` is null, run the `/new` Phase-8 `-stats` script
|
|
151
|
+
to backfill real `usage`. Keep `degraded`/`degradation_reasons` in the record so
|
|
152
|
+
the A/B comparison can exclude or weight degraded runs. Do NOT re-summarise the
|
|
153
|
+
cards — the workflow already did.
|
|
@@ -1,32 +1,24 @@
|
|
|
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 || ''
|
|
@@ -34,106 +26,187 @@ 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
|
const domain = a.domain || 'code'
|
|
29
|
+
const findings = Array.isArray(a.findings) && a.findings.length ? a.findings : [{ kind, evidence, domain }]
|
|
37
30
|
const REF = (a.refModulesBase || '.claude/skills/new/references').replace(/\/$/, '')
|
|
38
31
|
const cfg = a.config || {}
|
|
39
32
|
const paths = cfg.paths || {}
|
|
40
33
|
const backlogDir = paths.backlog_dir || 'backlog'
|
|
41
34
|
const protectedDomain = domain === 'security' || domain === 'migration'
|
|
42
35
|
|
|
36
|
+
// F-019 — bounded immediate retry on transient errors (shared pattern; no import allowed).
|
|
37
|
+
const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
|
|
38
|
+
function isTransient(e) { return TRANSIENT.test(String((e && e.message) || e || '')) }
|
|
39
|
+
async function agentSafe(prompt, opts, maxAttempts) {
|
|
40
|
+
const cap = maxAttempts || 3
|
|
41
|
+
let lastErr = null
|
|
42
|
+
for (let i = 0; i < cap; i++) {
|
|
43
|
+
try {
|
|
44
|
+
const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
|
|
45
|
+
return await agent(prompt, o)
|
|
46
|
+
} catch (e) { lastErr = e; if (!isTransient(e)) throw e }
|
|
47
|
+
}
|
|
48
|
+
const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr)); err.transientExhausted = true; throw err
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
const FIX_SCHEMA = {
|
|
44
52
|
type: 'object', required: ['applied', 'verified'], additionalProperties: true,
|
|
45
53
|
properties: {
|
|
46
|
-
applied: { type: 'boolean'
|
|
47
|
-
verified: { type: 'boolean', description: 'true if the originating gate
|
|
48
|
-
|
|
54
|
+
applied: { type: 'boolean' },
|
|
55
|
+
verified: { type: 'boolean', description: 'true only if you re-ran the originating gate and it passed' },
|
|
56
|
+
// F-008 — terminal short-circuit (impossible-by-definition). Not trusted blindly.
|
|
57
|
+
terminal: { type: 'boolean', description: 'true if no in-MAY-EDIT fix can ever satisfy the gate' },
|
|
58
|
+
terminalReason: { enum: ['out-of-ownership', 'baseline-not-reached', 'owner-gated', 'not-a-code-defect', ''] },
|
|
59
|
+
remedyFiles: { type: 'array', items: { type: 'string' }, description: 'files the real fix would require editing (for out-of-ownership JS verification)' },
|
|
60
|
+
crashed: { type: 'boolean' },
|
|
61
|
+
// F-022 — incidental findings outside this card's MAY-EDIT (so they are not dropped).
|
|
62
|
+
outOfScopeFindings: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
49
63
|
note: { type: 'string' },
|
|
50
64
|
},
|
|
51
65
|
}
|
|
52
66
|
const JUDGE_SCHEMA = {
|
|
53
67
|
type: 'object', required: ['best'], additionalProperties: false,
|
|
54
|
-
properties: {
|
|
68
|
+
properties: {
|
|
69
|
+
best: { type: 'number', description: '1-based index of the soundest verified attempt, or 0 if none truly holds' },
|
|
70
|
+
// F-015 — the files the judge INDEPENDENTLY confirmed exist and changed (cross-checked in JS).
|
|
71
|
+
verifiedFiles: { type: 'array', items: { type: 'string' } },
|
|
72
|
+
rationale: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
const TERMINAL_JUDGE_SCHEMA = {
|
|
76
|
+
type: 'object', required: ['confirmed'], additionalProperties: false,
|
|
77
|
+
properties: { confirmed: { type: 'boolean', description: 'true if the terminal verdict genuinely holds' }, rationale: { type: 'string' } },
|
|
55
78
|
}
|
|
56
79
|
const FOLLOWUP_SCHEMA = {
|
|
57
80
|
type: 'object', required: ['created', 'followupCard'], additionalProperties: true,
|
|
58
81
|
properties: { created: { type: 'boolean' }, followupCard: { type: 'string' }, note: { type: 'string' } },
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
// F-024 — domain-specialized fixer + judge (full map, not just doc|coder).
|
|
85
|
+
const fixerAgent = ({ doc: 'doc-reviewer', ui: 'ui-expert' })[domain] || 'coder'
|
|
86
|
+
const judgeAgent = (domain === 'security' || domain === 'migration') ? 'security-reviewer' : domain === 'perf' ? 'api-perf-cost-auditor' : 'code-reviewer'
|
|
87
|
+
|
|
88
|
+
const findingsBlock = findings.map((f, i) => ` ${i + 1}. [${f.kind || kind}/${f.domain || domain}] ${f.evidence}`).join('\n')
|
|
61
89
|
const brief = [
|
|
62
90
|
`Worktree: ${worktree} (cd into it; run all gates/git yourself).`,
|
|
63
91
|
`Card: ${card}`,
|
|
64
92
|
`Problem kind: ${kind}`,
|
|
65
|
-
`Evidence: ${evidence}`,
|
|
93
|
+
findings.length > 1 ? `Findings to fix (batched — same fix-area):\n${findingsBlock}` : `Evidence: ${evidence}`,
|
|
66
94
|
`MAY-EDIT (file-ownership — do NOT touch anything else): ${mayEdit.join(', ') || '(card scope)'}`,
|
|
67
95
|
`Scope files for re-verify: ${scopeFiles.join(', ') || '(diff)'}`,
|
|
68
96
|
`Domain: ${domain}${protectedDomain ? ' (PROTECTED — security/migration: never inline-bypass; the right specialist MUST apply)' : ''}`,
|
|
69
97
|
`Reference modules (Read for exact semantics): ${REF}/`,
|
|
70
|
-
`UI re-verify obligation: if your fix touches a UI file
|
|
71
|
-
].
|
|
98
|
+
`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.`,
|
|
99
|
+
`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.`,
|
|
100
|
+
].filter(Boolean).join('\n')
|
|
72
101
|
|
|
73
|
-
//
|
|
74
|
-
|
|
102
|
+
// JS-deterministic ownership check (F-008 anti-escape-hatch): are remedy files ⊆ MAY-EDIT?
|
|
103
|
+
function filesInScope(files) {
|
|
104
|
+
if (!Array.isArray(files) || !files.length) return false
|
|
105
|
+
if (!mayEdit.length) return false
|
|
106
|
+
return files.every((f) => mayEdit.some((m) => String(f).includes(m) || m.includes(String(f))))
|
|
107
|
+
}
|
|
108
|
+
function collectOOS(...attemptObjs) {
|
|
109
|
+
const out = []
|
|
110
|
+
for (const o of attemptObjs) for (const x of (o && o.outOfScopeFindings) || []) out.push(x)
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
75
113
|
|
|
76
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
77
|
-
// Phase Diagnose — branch by kind.
|
|
78
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
79
114
|
phase('Diagnose')
|
|
80
115
|
|
|
81
116
|
// ---- baseline-fail (E2): try to make the trunk build; fatal if irrecoverable ---
|
|
82
117
|
if (kind === 'baseline-fail') {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
let r = null
|
|
119
|
+
try {
|
|
120
|
+
r = await agentSafe(
|
|
121
|
+
`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.`,
|
|
122
|
+
{ label: `resolve:baseline:${card}`, phase: 'Diagnose', agentType: 'coder', schema: FIX_SCHEMA }
|
|
123
|
+
)
|
|
124
|
+
} catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during baseline repair', outOfScopeFindings: [] } ; throw e }
|
|
125
|
+
if (r && r.verified) { log('baseline recovered.'); return { status: 'resolved', outOfScopeFindings: collectOOS(r) } }
|
|
89
126
|
return { status: 'fatal', reason: 'baseline build irrecoverable' }
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
// ---- scope-expansion (Asse 2): deterministic boundary
|
|
129
|
+
// ---- scope-expansion (Asse 2): deterministic boundary ---
|
|
93
130
|
if (kind === 'scope-expansion') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
let decide = null
|
|
132
|
+
try {
|
|
133
|
+
decide = await agentSafe(
|
|
134
|
+
`A review finding EXPANDS scope beyond card ${card}'s acceptance criteria. Decide per the deterministic boundary (no line-count threshold):\n` +
|
|
135
|
+
`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` +
|
|
136
|
+
`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>'.`,
|
|
137
|
+
{ label: `resolve:scope:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
|
|
138
|
+
)
|
|
139
|
+
} catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during scope-expansion', outOfScopeFindings: [] }; throw e }
|
|
140
|
+
if (decide && decide.verified) {
|
|
141
|
+
const ok = await judgeVerify([{ i: 1, r: decide }])
|
|
142
|
+
if (ok.ok) { log('scope-expansion integrated within ownership.'); return { status: 'resolved', outOfScopeFindings: collectOOS(decide) } }
|
|
143
|
+
}
|
|
144
|
+
return await materialiseFollowup('scope-expansion', (decide && decide.note) || 'outside ownership / new AC / protected', collectOOS(decide))
|
|
103
145
|
}
|
|
104
146
|
|
|
105
|
-
// ---- agent-crash (E1)
|
|
147
|
+
// ---- agent-crash (E1) ---
|
|
106
148
|
if (kind === 'agent-crash') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
let r = null
|
|
150
|
+
try {
|
|
151
|
+
r = await agentSafe(
|
|
152
|
+
`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.`,
|
|
153
|
+
{ label: `resolve:crash:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
|
|
154
|
+
)
|
|
155
|
+
} catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during crash recovery', outOfScopeFindings: [] }; throw e }
|
|
156
|
+
if (r && r.verified) { const ok = await judgeVerify([{ i: 1, r }]); if (ok.ok) return { status: 'resolved', outOfScopeFindings: collectOOS(r) } }
|
|
157
|
+
return await materialiseFollowup('agent-crash', 'subagent crash unrecovered' + (protectedDomain ? ' (protected — not bypassed)' : ''), collectOOS(r))
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
// ───────────────────────────────────────────────────────────────────────────
|
|
116
|
-
// Asse 1
|
|
117
|
-
// Tier 1: targeted fix → re-verify. Tier 2: judged multi-attempt. Terminal: follow-up.
|
|
161
|
+
// Asse 1 — Tier-1 (with terminal short-circuit) → judged multi-attempt → follow-up.
|
|
118
162
|
// ───────────────────────────────────────────────────────────────────────────
|
|
119
163
|
const gateHint = {
|
|
120
|
-
'ac-unmet': `Implement the unmet acceptance criterion EXACTLY
|
|
164
|
+
'ac-unmet': `Implement the unmet acceptance criterion EXACTLY, per ${REF}/completeness.md Phase 2.5b. Re-verify against the code.`,
|
|
121
165
|
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
|
|
166
|
+
'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.`,
|
|
167
|
+
'e2e-blocked': `Fix so the E2E review passes per ${REF}/review-cycle.md Phase 2.6. Re-run /e2e-review.`,
|
|
124
168
|
'merge-blocker': `Fix the verified BLOCKER/HIGH from the final review per ${REF}/final-review.md F.5. Re-verify.`,
|
|
125
169
|
}[kind] || `Fix the blocker and re-verify.`
|
|
126
170
|
|
|
127
171
|
phase('Repair')
|
|
128
|
-
let attempt =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
172
|
+
let attempt = null
|
|
173
|
+
try {
|
|
174
|
+
attempt = await agentSafe(
|
|
175
|
+
`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).`,
|
|
176
|
+
{ label: `resolve:${kind}:${card}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
|
|
177
|
+
)
|
|
178
|
+
} catch (e) { if (e && e.transientExhausted) return { status: 'followup', reason: 'outage during tier-1', outOfScopeFindings: [] }; throw e }
|
|
179
|
+
|
|
180
|
+
// F-008 — terminal short-circuit, verified not trusted.
|
|
181
|
+
if (attempt && attempt.terminal) {
|
|
182
|
+
const tr = attempt.terminalReason || ''
|
|
183
|
+
let confirmed = false
|
|
184
|
+
if (tr === 'out-of-ownership') {
|
|
185
|
+
confirmed = !filesInScope(attempt.remedyFiles) // genuinely terminal iff remedy files are NOT in MAY-EDIT
|
|
186
|
+
} else {
|
|
187
|
+
// owner-gated / not-a-code-defect / baseline-not-reached — ratify with the judge.
|
|
188
|
+
try {
|
|
189
|
+
const tj = await agentSafe(
|
|
190
|
+
`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.`,
|
|
191
|
+
{ label: `resolve:terminal-judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: TERMINAL_JUDGE_SCHEMA }
|
|
192
|
+
)
|
|
193
|
+
confirmed = !!(tj && tj.confirmed)
|
|
194
|
+
} catch (_) { confirmed = false }
|
|
195
|
+
}
|
|
196
|
+
if (confirmed) { log(`${kind} terminal (${tr}) — short-circuit to follow-up.`); return await materialiseFollowup(kind, `terminal: ${tr} — ${attempt.note || ''}`, collectOOS(attempt)) }
|
|
197
|
+
log(`terminal verdict (${tr}) rejected — proceeding to multi-attempt.`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// F-015 — a verified Tier-1 still goes through the mandatory judge cross-check.
|
|
201
|
+
if (attempt && attempt.verified) {
|
|
202
|
+
const ok = await judgeVerify([{ i: 1, r: attempt }])
|
|
203
|
+
if (ok.ok) { log(`${kind} resolved (tier 1, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt) } }
|
|
204
|
+
log('tier-1 verified rejected by judge cross-check — escalating.')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Tier 2 — judged multi-attempt (budget-guarded; single specialist for protected domains).
|
|
208
|
+
const canFanOut = (typeof budget === 'undefined' || !budget.total || budget.remaining() > 60000)
|
|
209
|
+
let tier2OOS = []
|
|
137
210
|
if (canFanOut && !protectedDomain) {
|
|
138
211
|
phase('Repair')
|
|
139
212
|
const angles = [
|
|
@@ -141,40 +214,64 @@ if (canFanOut && !protectedDomain) {
|
|
|
141
214
|
'Fix defensively at the call site (guard/validate without changing the contract).',
|
|
142
215
|
'Fix by reusing an existing utility/pattern instead of new code.',
|
|
143
216
|
]
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
`Tier-2 repair attempt #${i + 1} for card ${card} (${kind}), angle: ${angle}\n\n${brief}\n\n${gateHint}\n\nApply
|
|
217
|
+
const tries = (await parallel(angles.map((angle, i) => () =>
|
|
218
|
+
agentSafe(
|
|
219
|
+
`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
220
|
{ label: `resolve:${kind}:${card}#${i + 1}`, phase: 'Repair', agentType: fixerAgent, schema: FIX_SCHEMA }
|
|
148
221
|
).then((r) => ({ i: i + 1, r })).catch(() => null)
|
|
149
222
|
))).filter(Boolean)
|
|
150
|
-
|
|
151
|
-
const verified =
|
|
223
|
+
tier2OOS = collectOOS(...tries.map((t) => t.r))
|
|
224
|
+
const verified = tries.filter((x) => x.r && x.r.verified)
|
|
152
225
|
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' } }
|
|
226
|
+
const ok = await judgeVerify(verified)
|
|
227
|
+
if (ok.ok) { log(`${kind} resolved (tier 2, attempt #${ok.best}, judge-confirmed).`); return { status: 'resolved', outOfScopeFindings: collectOOS(attempt).concat(tier2OOS) } }
|
|
160
228
|
}
|
|
161
229
|
} else if (protectedDomain) {
|
|
162
|
-
log('protected domain — single specialist attempt only (no
|
|
230
|
+
log('protected domain — single specialist attempt only (no fan-out).')
|
|
163
231
|
} else {
|
|
164
232
|
log('budget near target — skipping tier-2 fan-out.')
|
|
165
233
|
}
|
|
166
234
|
|
|
167
|
-
|
|
168
|
-
return await materialiseFollowup(kind, attempt && attempt.note ? attempt.note : 'unresolved after repair tiers')
|
|
235
|
+
return await materialiseFollowup(kind, (attempt && attempt.note) || 'unresolved after repair tiers', collectOOS(attempt).concat(tier2OOS))
|
|
169
236
|
|
|
170
237
|
// ───────────────────────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
238
|
+
// F-015/F-033 — mandatory adversarial judge + deterministic JS cross-check.
|
|
239
|
+
// The judge (cross-model code-reviewer / domain specialist) does its OWN grep and
|
|
240
|
+
// returns the files it independently confirmed changed; we cross-check ⊆ MAY-EDIT.
|
|
241
|
+
async function judgeVerify(verifiedAttempts) {
|
|
242
|
+
if (!verifiedAttempts.length) return { ok: false, best: 0 }
|
|
243
|
+
let judge = null
|
|
244
|
+
try {
|
|
245
|
+
judge = await agentSafe(
|
|
246
|
+
`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(' | ')}`,
|
|
247
|
+
{ label: `resolve:judge:${card}`, phase: 'Verify', agentType: judgeAgent, schema: JUDGE_SCHEMA }
|
|
248
|
+
)
|
|
249
|
+
} catch (e) { if (e && e.transientExhausted) return { ok: false, best: 0 }; throw e }
|
|
250
|
+
if (!judge || !(judge.best > 0)) return { ok: false, best: 0 }
|
|
251
|
+
// Deterministic JS cross-check: the judge-confirmed files must be within MAY-EDIT.
|
|
252
|
+
if (mayEdit.length && Array.isArray(judge.verifiedFiles) && judge.verifiedFiles.length && !filesInScope(judge.verifiedFiles)) {
|
|
253
|
+
log('judge verifiedFiles fall outside MAY-EDIT — rejecting (possible fabrication).')
|
|
254
|
+
return { ok: false, best: 0 }
|
|
255
|
+
}
|
|
256
|
+
return { ok: true, best: judge.best }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function materialiseFollowup(k, reason, oos) {
|
|
260
|
+
let r = null
|
|
261
|
+
try {
|
|
262
|
+
// F-023 — mechanical YAML write → Haiku.
|
|
263
|
+
r = await agentSafe(
|
|
264
|
+
`Materialise a follow-up backlog card so this residual is TRACKED, not dropped (per ${REF}/completeness.md Phase 2.5b option 3). MECHANICAL — do not re-read large modules.\n\n${brief}\nKind: ${k}\nReason unresolved: ${reason}\n\n` +
|
|
265
|
+
`Create ${backlogDir}/${card}-followup-<gate>.yml: status: TODO + MINIMUM valid fields (requirements ≥1 from the residual, acceptance_criteria ≥1 = verbatim residual, files_likely_touched ≥1 from the card ownership / remedy files). MUST pass the /new pre-flight field check. Return the created card id.`,
|
|
266
|
+
{ label: `resolve:followup:${card}`, phase: 'Verify', agentType: 'general-purpose', model: 'haiku', schema: FOLLOWUP_SCHEMA }
|
|
267
|
+
)
|
|
268
|
+
} catch (e) {
|
|
269
|
+
// F-020 — could not materialise (e.g. outage): return WITHOUT a followupCard so the
|
|
270
|
+
// SKILL writes it from the offline-safe residual ledger. Never claim it was created.
|
|
271
|
+
log(`follow-up materialisation failed (${String(e && e.message)}) — skill will reconcile.`)
|
|
272
|
+
return { status: 'followup', followupCard: null, reason, outOfScopeFindings: oos || [] }
|
|
273
|
+
}
|
|
274
|
+
const followupCard = (r && r.created && r.followupCard) ? r.followupCard : null
|
|
275
|
+
log(`${k} → follow-up ${followupCard || '(deferred to skill)'} (nothing dropped).`)
|
|
276
|
+
return { status: 'followup', followupCard, reason, outOfScopeFindings: oos || [] }
|
|
180
277
|
}
|