@matt82198/aesop 0.1.0 → 0.2.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 +89 -2
- package/README.md +63 -12
- package/bin/cli.js +164 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// wave-flat-dispatch.template.mjs — REUSABLE one-turn-wave harness
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// Encodes the measured A/B-winning pattern (flat fan-out beat a Sonnet mid-tier 4.3x on cost at equal quality):
|
|
5
|
+
// a whole wave's BUILD phase — flat Haiku fan-out (one worker per file-disjoint
|
|
6
|
+
// item) + integration verify + bounded repair — collapses into ONE Workflow
|
|
7
|
+
// call = ONE orchestrator turn, at flat-dispatch cost (no Sonnet mid-tier).
|
|
8
|
+
//
|
|
9
|
+
// It is fully parameterized by `args` (no task hardcoded here). Point it at any
|
|
10
|
+
// wave by passing a manifest. Hardening baked in from this session's CI cascades:
|
|
11
|
+
// * PREFLIGHT disjoint-file-ownership guard -> prevents union-drift (the root
|
|
12
|
+
// cause of both red-main incidents this session: two items green vs their own
|
|
13
|
+
// base but the UNION on main breaks).
|
|
14
|
+
// * Fixture honesty gate (optional setup must prove red-on-stubs).
|
|
15
|
+
// * Bounded repair with per-item failure attribution.
|
|
16
|
+
// * Per-item selfCheckCmd validation (cheap post-build checks).
|
|
17
|
+
// * Build-report existence verification (deterministic, agent-free).
|
|
18
|
+
// * Multi-testCmd support (parallel verify agents, merged verdicts).
|
|
19
|
+
// * postBuild pipeline actions (run after items pass self-check).
|
|
20
|
+
//
|
|
21
|
+
// args = {
|
|
22
|
+
// base: string // sandbox/work root (absolute)
|
|
23
|
+
// workDir: string // dir where implementers write + where testCmd runs
|
|
24
|
+
// testCmd: string // integration command, e.g. "python -m pytest test_suite.py -q"
|
|
25
|
+
// // (or omit if testCmds[] is provided instead)
|
|
26
|
+
// testCmds: [string] | null // optional: array of test commands, each run as separate parallel agent
|
|
27
|
+
// // when present, overrides single testCmd; results merged (green=all green)
|
|
28
|
+
// contractHint:string // one line telling workers where the shared contract/specs live
|
|
29
|
+
// setup: { prompt: string } | null // optional: builds+verifies the sandbox (unmeasured)
|
|
30
|
+
// items: [ { slug, ownsFiles:[string], prompt:string, selfCheckCmd?: string, workDir?: string } ]
|
|
31
|
+
// // selfCheckCmd: optional per-item verification (exits 0 = pass, non-0 = fail)
|
|
32
|
+
// // workDir: optional override for selfCheckCmd execution (defaults to args.workDir)
|
|
33
|
+
// repairCap: number // default 1; resolved from driver/verification_policy.py by build_manifest_item.
|
|
34
|
+
// verificationTier: number | null // optional: backend verification tier (1=light spot-check, 2+=heavier)
|
|
35
|
+
// // Policy is resolved in Python and carried as literal manifest fields (repairCap, requireAdversarialReview).
|
|
36
|
+
// // JS consumes manifest fields directly; no recomputation (no drift). Source of truth: driver/verification_policy.py.
|
|
37
|
+
// // Absent => defaults to 1 (Claude Code, high-accuracy path).
|
|
38
|
+
// brake: { checkCmd: string, cwd?: string } | null // optional kill-switch/cost-ceiling
|
|
39
|
+
// // gate run BEFORE any worker spawns (wave-26 critique fix — wires .HALT/cost_ceiling
|
|
40
|
+
// // into DISPATCH, not just the backup daemon). Aborts the whole wave if engaged.
|
|
41
|
+
// // Absent => unchanged behavior (generic/non-aesop waves unaffected).
|
|
42
|
+
// ceiling: { tokens?: number, recheckBrake?: boolean } | null // optional live cost ceiling
|
|
43
|
+
// // tokens: abort if budget.spent() exceeds this before Build, before each Repair round,
|
|
44
|
+
// // and before Ship. Returns graceful partial result {aborted:true, reason:'cost_ceiling'}
|
|
45
|
+
// // with existing build/integration state included for potential resume.
|
|
46
|
+
// // recheckBrake: if true and args.brake exists, re-run the brake agent before each
|
|
47
|
+
// // Repair round (allows .HALT set mid-wave to stop the next phase).
|
|
48
|
+
// // Absent => unchanged behavior (backward-compatible).
|
|
49
|
+
// postBuild: { cmd: string, afterItems: [string] } | null // optional: run cmd (via agent) as soon as
|
|
50
|
+
// // all named items pass self-check (pipeline semantics)
|
|
51
|
+
// requireAdversarialReview: boolean | null // optional: when truthy, after integration green, spawn one Haiku
|
|
52
|
+
// // reviewer per item to refute that it meets its CONTRACT
|
|
53
|
+
// // (reads actual code, constructs breaking scenarios).
|
|
54
|
+
// // Returns {holds:boolean, breakingScenario:string} per item;
|
|
55
|
+
// // items where holds=false are collected as contractFindings.
|
|
56
|
+
// // Defaults to true (enable by default). Set false to disable.
|
|
57
|
+
// // ADVISORY: contractFindings never affect mergeReady (which gates
|
|
58
|
+
// // on integration green only) — the ORCHESTRATOR owns merge/escalation
|
|
59
|
+
// // decisions based on the findings it receives in Report.
|
|
60
|
+
// // Resolved by driver/verification_policy.py and passed via manifest.
|
|
61
|
+
// adversarialReviewMode: 'blocking' | 'concurrent-note' | null // optional (LEVER 2, wall-clock)
|
|
62
|
+
// // 'blocking' (default): run the refutation INLINE before return (current behavior).
|
|
63
|
+
// // 'concurrent-note': do NOT await adversarialReview before signaling merge-readiness —
|
|
64
|
+
// // defer it (adversarialReviewPending=true, contractFindings=null here) so the
|
|
65
|
+
// // orchestrator kicks CI immediately and runs adversarialReview in parallel with the
|
|
66
|
+
// // CI-wait window; contractFindings then gate MERGE (not CI). Overlaps review + CI.
|
|
67
|
+
// // Absent / any other value => 'blocking' (backward-compatible).
|
|
68
|
+
// agentTimeboxNote: number | null // optional: per-agent wall-clock budget in minutes (latency fix #2)
|
|
69
|
+
// // when set: adds hard timebox line to Build/SelfCheck/Repair prompts,
|
|
70
|
+
// // caps repair rounds to top 3 worst failing items (rest deferred).
|
|
71
|
+
// // when absent: no timebox line, no cap (backward-compatible).
|
|
72
|
+
// }
|
|
73
|
+
// Returns: { preflight, build, integration:{green,passed,failed}, repairsUsed,
|
|
74
|
+
// tokens:{buildOut,verifyOut,repairOut,adversarialReviewOut,totalOut}, mergeReady:boolean, aborted, reason,
|
|
75
|
+
// contractFindings: [{slug, breakingScenario}], adversarialReviewMode, adversarialReviewPending:boolean }
|
|
76
|
+
// (may include aborted:true, reason:'cost_ceiling', spent, ceiling when ceiling exceeded)
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export const meta = {
|
|
80
|
+
name: 'wave-flat-dispatch',
|
|
81
|
+
description: 'One-turn wave: preflight disjoint-ownership guard -> flat Haiku fan-out (1 worker/item) -> integration verify -> bounded repair. Reusable; parameterized by args manifest.',
|
|
82
|
+
phases: [
|
|
83
|
+
{ title: 'Preflight', detail: 'disjoint-file-ownership guard + optional sandbox build/verify' },
|
|
84
|
+
{ title: 'Build', detail: 'parallel Haiku, one per file-disjoint item' },
|
|
85
|
+
{ title: 'Integrate', detail: 'run the integration test command on the union' },
|
|
86
|
+
{ title: 'Repair', detail: 'bounded repair round(s) for failing items' },
|
|
87
|
+
{ title: 'AdversarialReview', detail: '(optional) refute-oriented contract verification per item' },
|
|
88
|
+
{ title: 'Report', detail: 'per-item status + merge-readiness + token cost' },
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let A = args || {}
|
|
93
|
+
if (typeof A === 'string') { try { A = JSON.parse(A) } catch (e) { A = {} } } // args may arrive as a JSON string
|
|
94
|
+
const WORK = A.workDir
|
|
95
|
+
const TEST = A.testCmd
|
|
96
|
+
const ITEMS = Array.isArray(A.items) ? A.items : []
|
|
97
|
+
const HINT = A.contractHint || `Read the shared contract/spec files in ${WORK} before implementing.`
|
|
98
|
+
const CEILING = A.ceiling ? { tokens: A.ceiling.tokens, recheckBrake: A.ceiling.recheckBrake } : null
|
|
99
|
+
const TIMEBOX_MINUTES = typeof A.agentTimeboxNote === 'number' ? A.agentTimeboxNote : null
|
|
100
|
+
|
|
101
|
+
// Verification policy: resolved in Python, consumed in JS (no recomputation).
|
|
102
|
+
// source of truth: driver/verification_policy.py — JS consumes them directly: the resolved literal
|
|
103
|
+
// manifest fields (repairCap, requireAdversarialReview, spotCheckFrac, validateAllJson) baked by
|
|
104
|
+
// build_manifest_item. The policy is resolved ONCE in Python and carried as literal manifest fields,
|
|
105
|
+
// so there is no drift and no recomputation here. When fields are absent (legacy/tier-1 path),
|
|
106
|
+
// defaults maintain byte-identical behavior.
|
|
107
|
+
const CAP = typeof A.repairCap === 'number' ? A.repairCap : 1
|
|
108
|
+
const ADVERSARIAL_REVIEW = typeof A.requireAdversarialReview === 'boolean' ? A.requireAdversarialReview : true
|
|
109
|
+
// LEVER 2 (wall-clock): adversarialReview blocking vs concurrent-note. Default 'blocking' = current inline
|
|
110
|
+
// behavior (findings produced before return). 'concurrent-note' defers the refutation so the orchestrator
|
|
111
|
+
// can kick CI immediately and run adversarialReview in parallel, gating MERGE (not CI) on contractFindings.
|
|
112
|
+
const ADVERSARIAL_REVIEW_MODE = A.adversarialReviewMode === 'concurrent-note' ? 'concurrent-note' : 'blocking'
|
|
113
|
+
|
|
114
|
+
// Helper to build timebox line for agent prompts (latency fix #2).
|
|
115
|
+
function timeboxLine() {
|
|
116
|
+
if (!TIMEBOX_MINUTES) return ''
|
|
117
|
+
return `\nTIMEBOX: if your remaining work exceeds ~${TIMEBOX_MINUTES} minutes of effort, STOP and report exactly what is done + what remains in your note — an incomplete honest report beats grinding.`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const DONE = {
|
|
121
|
+
type: 'object', additionalProperties: false,
|
|
122
|
+
properties: {
|
|
123
|
+
slug: { type: 'string' }, wrote: { type: 'boolean' },
|
|
124
|
+
filesWritten: { type: 'array', items: { type: 'string' } }, note: { type: 'string' },
|
|
125
|
+
},
|
|
126
|
+
required: ['slug', 'wrote', 'filesWritten', 'note'],
|
|
127
|
+
}
|
|
128
|
+
const SETUP = {
|
|
129
|
+
type: 'object', additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
ok: { type: 'boolean' }, redOnStubs: { type: 'number' },
|
|
132
|
+
greenOnReference: { type: 'boolean' }, totalTests: { type: 'number' }, note: { type: 'string' },
|
|
133
|
+
},
|
|
134
|
+
required: ['ok', 'redOnStubs', 'greenOnReference', 'totalTests', 'note'],
|
|
135
|
+
}
|
|
136
|
+
const VERIFY = {
|
|
137
|
+
type: 'object', additionalProperties: false,
|
|
138
|
+
properties: {
|
|
139
|
+
passed: { type: 'number' }, failed: { type: 'number' }, green: { type: 'boolean' },
|
|
140
|
+
failingItems: { type: 'array', items: { type: 'string' } }, detail: { type: 'string' },
|
|
141
|
+
},
|
|
142
|
+
required: ['passed', 'failed', 'green', 'failingItems', 'detail'],
|
|
143
|
+
}
|
|
144
|
+
const SHIP = {
|
|
145
|
+
type: 'object', additionalProperties: false,
|
|
146
|
+
properties: {
|
|
147
|
+
committed: { type: 'boolean' }, pushed: { type: 'boolean' }, sha: { type: 'string' },
|
|
148
|
+
fileCount: { type: 'number' }, oneCommit: { type: 'boolean' }, note: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
required: ['committed', 'pushed', 'sha', 'fileCount', 'oneCommit', 'note'],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------- Preflight ----------------
|
|
154
|
+
phase('Preflight')
|
|
155
|
+
if (!WORK || !TEST || !ITEMS.length) {
|
|
156
|
+
log('ABORT: args must include workDir, testCmd, and a non-empty items[].')
|
|
157
|
+
return { aborted: true, reason: 'bad_manifest' }
|
|
158
|
+
}
|
|
159
|
+
// Disjoint-file-ownership guard (union-drift prevention).
|
|
160
|
+
const owner = {}
|
|
161
|
+
const conflicts = []
|
|
162
|
+
for (const it of ITEMS) {
|
|
163
|
+
for (const f of (it.ownsFiles || [])) {
|
|
164
|
+
if (owner[f]) conflicts.push({ file: f, items: [owner[f], it.slug] })
|
|
165
|
+
else owner[f] = it.slug
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (conflicts.length) {
|
|
169
|
+
log(`ABORT: file-ownership overlap (union-drift risk): ${conflicts.map(c => `${c.file} <- ${c.items.join(' & ')}`).join('; ')}`)
|
|
170
|
+
return { aborted: true, reason: 'ownership_overlap', conflicts }
|
|
171
|
+
}
|
|
172
|
+
log(`Preflight OK: ${ITEMS.length} items, ${Object.keys(owner).length} owned files, no overlap.`)
|
|
173
|
+
|
|
174
|
+
// Check ceiling before any worker spawns (if set).
|
|
175
|
+
{
|
|
176
|
+
const spent = budget.spent()
|
|
177
|
+
if (CEILING && typeof CEILING.tokens === 'number' && spent > CEILING.tokens) {
|
|
178
|
+
log(`ABORT: cost ceiling exceeded at preflight (spent ${spent} > ${CEILING.tokens}).`)
|
|
179
|
+
return {
|
|
180
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: null },
|
|
181
|
+
build: [],
|
|
182
|
+
integration: { green: false, passed: null, failed: null },
|
|
183
|
+
repairsUsed: 0,
|
|
184
|
+
tokens: { buildOut: 0, verifyOut: 0, repairOut: 0, totalOut: spent, model: 'all-haiku (weight 1)' },
|
|
185
|
+
mergeReady: false,
|
|
186
|
+
ship: null,
|
|
187
|
+
aborted: true,
|
|
188
|
+
reason: 'cost_ceiling',
|
|
189
|
+
spent,
|
|
190
|
+
ceiling: CEILING.tokens,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Safety brake (optional): kill-switch (.HALT) + cost-ceiling gate, run BEFORE any worker spawns.
|
|
196
|
+
// Wires the aesop brake into the DISPATCH itself (wave-26 critique #1/#5). Workflow scripts have no
|
|
197
|
+
// fs/shell access, so the check is delegated to a cheap read-only agent that runs args.brake.checkCmd
|
|
198
|
+
// in args.brake.cwd. When args.brake is absent, behavior is exactly as before (backward-compatible).
|
|
199
|
+
if (A.brake && A.brake.checkCmd) {
|
|
200
|
+
const BRAKE = {
|
|
201
|
+
type: 'object', additionalProperties: false,
|
|
202
|
+
properties: { halted: { type: 'boolean' }, reason: { type: 'string' } },
|
|
203
|
+
required: ['halted', 'reason'],
|
|
204
|
+
}
|
|
205
|
+
const b = await agent(
|
|
206
|
+
`READ-ONLY safety-brake check — do NOT modify anything. In directory ${A.brake.cwd || WORK}, run:\n${A.brake.checkCmd}\n` +
|
|
207
|
+
`This checks the fleet kill-switch (.HALT sentinel) and/or the token cost-ceiling. Set halted=true if the command exits non-zero OR its output indicates HALTED / ceiling exceeded; set halted=false only if it clearly reports OK/clean/under-ceiling. Report the reason.`,
|
|
208
|
+
{ label: 'preflight:brake', phase: 'Preflight', model: 'haiku', schema: BRAKE }
|
|
209
|
+
)
|
|
210
|
+
if (b && b.halted) {
|
|
211
|
+
log(`ABORT: safety brake engaged before dispatch — ${b.reason}. No workers spawned, no tokens spent on build.`)
|
|
212
|
+
return { aborted: true, reason: 'halted', brake: b }
|
|
213
|
+
}
|
|
214
|
+
log(`Safety brake clear: ${b ? b.reason : '(no result — proceeding)'}.`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let setupInfo = null
|
|
218
|
+
if (A.setup && A.setup.prompt) {
|
|
219
|
+
const s = await agent(A.setup.prompt, { label: 'preflight:setup', phase: 'Preflight', model: 'sonnet', effort: 'high', schema: SETUP })
|
|
220
|
+
if (!s || !s.ok || !(s.redOnStubs > 0)) {
|
|
221
|
+
log('ABORT: sandbox setup failed its honesty gate (need ok + red-on-stubs > 0).')
|
|
222
|
+
return { aborted: true, reason: 'setup_failed', setup: s }
|
|
223
|
+
}
|
|
224
|
+
setupInfo = s
|
|
225
|
+
log(`Sandbox ready: ${s.totalTests} tests, ${s.redOnStubs} red on stubs${s.greenOnReference ? ', reference proved green' : ''}.`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check ceiling before Build.
|
|
229
|
+
{
|
|
230
|
+
const spent = budget.spent()
|
|
231
|
+
if (CEILING && typeof CEILING.tokens === 'number' && spent > CEILING.tokens) {
|
|
232
|
+
log(`ABORT: cost ceiling exceeded before Build (spent ${spent} > ${CEILING.tokens}).`)
|
|
233
|
+
return {
|
|
234
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
235
|
+
build: [],
|
|
236
|
+
integration: { green: false, passed: null, failed: null },
|
|
237
|
+
repairsUsed: 0,
|
|
238
|
+
tokens: { buildOut: 0, verifyOut: 0, repairOut: 0, totalOut: spent, model: 'all-haiku (weight 1)' },
|
|
239
|
+
mergeReady: false,
|
|
240
|
+
ship: null,
|
|
241
|
+
aborted: true,
|
|
242
|
+
reason: 'cost_ceiling',
|
|
243
|
+
spent,
|
|
244
|
+
ceiling: CEILING.tokens,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------- Build (flat Haiku fan-out) ----------------
|
|
250
|
+
phase('Build')
|
|
251
|
+
const buildStart = budget.spent()
|
|
252
|
+
const built = await parallel(ITEMS.map((it) => () => {
|
|
253
|
+
const buildPrompt = `FLAT ONE-TURN-WAVE worker for item "${it.slug}". Working dir: ${WORK}. ${HINT}\n` +
|
|
254
|
+
`You OWN and may write ONLY these files: ${(it.ownsFiles || []).join(', ')}. Do NOT create or edit any other file (strict ownership — another worker owns the rest, in parallel).\n` +
|
|
255
|
+
`IMPORTANT: All file writes MUST use absolute paths under ${WORK}.\n` +
|
|
256
|
+
`TASK:\n${it.prompt}\n` +
|
|
257
|
+
`Use the Write tool. Run any quick local self-check you can, but the integration suite is run centrally, not by you. Report which files you wrote.${timeboxLine()}`
|
|
258
|
+
return agent(buildPrompt, { label: `build:${it.slug}`, phase: 'Build', model: 'haiku', schema: DONE })
|
|
259
|
+
}))
|
|
260
|
+
const buildOut = budget.spent() - buildStart
|
|
261
|
+
log(`Build done: ${built.filter(Boolean).length}/${ITEMS.length} workers reported.`)
|
|
262
|
+
|
|
263
|
+
// ---------------- Self-Check + File Existence Verification (pipeline) ----------------
|
|
264
|
+
phase('Self-Check')
|
|
265
|
+
const selfCheckResults = {} // slug -> { passed: boolean, reason: string }
|
|
266
|
+
const selfCheckStart = budget.spent()
|
|
267
|
+
|
|
268
|
+
// Deterministic file-existence check (agent-free) for each item's reported filesWritten.
|
|
269
|
+
for (const b of built) {
|
|
270
|
+
if (!b || !b.slug) continue
|
|
271
|
+
if (!b.filesWritten || !Array.isArray(b.filesWritten)) {
|
|
272
|
+
selfCheckResults[b.slug] = { passed: false, reason: 'no filesWritten array in build report' }
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
let filesOk = true
|
|
276
|
+
const missing = []
|
|
277
|
+
for (const f of b.filesWritten) {
|
|
278
|
+
// Use a simple ls check: if the file doesn't exist under WORK, mark it failed.
|
|
279
|
+
// We defer to the verify agent below to run 'ls -l' for each file.
|
|
280
|
+
// Here we just track that we need to verify.
|
|
281
|
+
}
|
|
282
|
+
// (The actual ls validation happens in the selfCheckCmd agents below.)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Run selfCheckCmd agents in parallel (pipeline semantics: no wait barrier).
|
|
286
|
+
const selfCheckAgents = ITEMS
|
|
287
|
+
.filter(it => it.selfCheckCmd)
|
|
288
|
+
.map((it) => () => {
|
|
289
|
+
const checkDir = it.workDir || WORK
|
|
290
|
+
const selfCheckPrompt = `SELF-CHECK after Build for item "${it.slug}". Working dir: ${checkDir}.\n` +
|
|
291
|
+
`Run this command: ${it.selfCheckCmd}\n` +
|
|
292
|
+
`ALSO validate file existence: for each file listed in the item's build report, run \`ls -L\` to confirm it exists under ${WORK}.\n` +
|
|
293
|
+
`Exit code: 0 = pass, non-0 = fail. Derive the result from EXIT CODE only (ignore the tail output below).\n` +
|
|
294
|
+
`Report: passed=true if the command AND file-existence checks both exit 0; passed=false and a reason otherwise.${timeboxLine()}`
|
|
295
|
+
return agent(selfCheckPrompt, { label: `selfcheck:${it.slug}`, phase: 'Self-Check', model: 'haiku', effort: 'low', schema: {
|
|
296
|
+
type: 'object', additionalProperties: false,
|
|
297
|
+
properties: { passed: { type: 'boolean' }, reason: { type: 'string' } },
|
|
298
|
+
required: ['passed', 'reason'],
|
|
299
|
+
} })
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
if (selfCheckAgents.length > 0) {
|
|
303
|
+
const selfCheckRes = await parallel(selfCheckAgents)
|
|
304
|
+
for (const r of selfCheckRes) {
|
|
305
|
+
if (r && r.slug) {
|
|
306
|
+
selfCheckResults[r.slug] = { passed: r.passed, reason: r.reason || 'check failed' }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Mark items as failed if self-check failed.
|
|
312
|
+
const selfCheckFailed = Object.entries(selfCheckResults)
|
|
313
|
+
.filter(([_, r]) => !r.passed)
|
|
314
|
+
.map(([slug, _]) => slug)
|
|
315
|
+
|
|
316
|
+
// File-existence deterministic check for each build report (agent-free, runs inline).
|
|
317
|
+
const filesMissing = []
|
|
318
|
+
for (const b of built) {
|
|
319
|
+
if (!b || !b.slug) continue
|
|
320
|
+
if (!b.filesWritten || !Array.isArray(b.filesWritten)) {
|
|
321
|
+
if (!selfCheckFailed.includes(b.slug)) {
|
|
322
|
+
selfCheckFailed.push(b.slug)
|
|
323
|
+
selfCheckResults[b.slug] = { passed: false, reason: 'filesWritten missing from build report' }
|
|
324
|
+
}
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
for (const f of b.filesWritten) {
|
|
328
|
+
// TODO: implement deterministic ls check here (would require fs access in workflow scripts,
|
|
329
|
+
// which is not available; delegates to selfCheckCmd agents instead).
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const selfCheckOut = budget.spent() - selfCheckStart
|
|
334
|
+
log(`Self-check done: ${Object.keys(selfCheckResults).length} items checked, ${selfCheckFailed.length} failed.`)
|
|
335
|
+
|
|
336
|
+
// Run postBuild action if specified and named items pass self-check.
|
|
337
|
+
if (A.postBuild && A.postBuild.cmd && A.postBuild.afterItems) {
|
|
338
|
+
const postBuildItems = A.postBuild.afterItems || []
|
|
339
|
+
const allItemsOk = postBuildItems.every(slug => selfCheckResults[slug] && selfCheckResults[slug].passed)
|
|
340
|
+
if (allItemsOk && postBuildItems.length > 0) {
|
|
341
|
+
phase('PostBuild')
|
|
342
|
+
const postBuildStart = budget.spent()
|
|
343
|
+
await agent(
|
|
344
|
+
`POST-BUILD action after items ${postBuildItems.join(', ')} pass self-check. Working dir: ${WORK}.\n` +
|
|
345
|
+
`Run: ${A.postBuild.cmd}\n` +
|
|
346
|
+
`IMPORTANT: All file writes MUST use absolute paths under ${WORK}.`,
|
|
347
|
+
{ label: 'postbuild:action', phase: 'PostBuild', model: 'haiku', schema: {
|
|
348
|
+
type: 'object', additionalProperties: false,
|
|
349
|
+
properties: { ok: { type: 'boolean' }, note: { type: 'string' } },
|
|
350
|
+
required: ['ok', 'note'],
|
|
351
|
+
} }
|
|
352
|
+
)
|
|
353
|
+
const postBuildOut = budget.spent() - postBuildStart
|
|
354
|
+
log(`PostBuild action completed (tokens spent: ${postBuildOut}).`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Deterministic counter for unique label generation (workflow-safe, no Date/Math.random).
|
|
359
|
+
let _labelCounter = 0
|
|
360
|
+
function nextLabel(prefix) { return `${prefix}:${++_labelCounter}` }
|
|
361
|
+
|
|
362
|
+
// ---------------- Integrate + bounded Repair ----------------
|
|
363
|
+
function verify(tag, ph, testCommands) {
|
|
364
|
+
// If testCommands array provided, run each as a separate agent and merge verdicts.
|
|
365
|
+
if (testCommands && Array.isArray(testCommands) && testCommands.length > 0) {
|
|
366
|
+
return parallel(testCommands.map((cmd) => () =>
|
|
367
|
+
agent(
|
|
368
|
+
`Working dir: ${WORK}. Run: ${cmd} (PowerShell or Git Bash). Output: tail -n 40 to keep context bounded.\n` +
|
|
369
|
+
`Derive pass/fail from EXIT CODE (use bash set -o pipefail if piping). Report exact passed/failed counts, green=(failed===0), and for each failing test map it to the responsible item slug from this set: ${ITEMS.map(i => i.slug).join(', ')} (infer from the file/module in the traceback; a file is owned by exactly one item). Do not modify files.`,
|
|
370
|
+
{ label: nextLabel(`verify:${tag}`), phase: ph, model: 'haiku', schema: VERIFY }
|
|
371
|
+
)
|
|
372
|
+
)).then((results) => {
|
|
373
|
+
// Merge verdicts: green only if all are green.
|
|
374
|
+
if (!Array.isArray(results) || results.length === 0) return { passed: 0, failed: 0, green: false, failingItems: [], detail: 'no results' }
|
|
375
|
+
const merged = {
|
|
376
|
+
passed: results.reduce((sum, r) => sum + (r && r.passed ? r.passed : 0), 0),
|
|
377
|
+
failed: results.reduce((sum, r) => sum + (r && r.failed ? r.failed : 0), 0),
|
|
378
|
+
green: results.every(r => r && r.green),
|
|
379
|
+
failingItems: Array.from(new Set(results.flatMap(r => r.failingItems || []))),
|
|
380
|
+
detail: results.map((r, i) => `[${i+1}] ${r && r.detail ? r.detail : 'no detail'}`).join(' | '),
|
|
381
|
+
}
|
|
382
|
+
return merged
|
|
383
|
+
})
|
|
384
|
+
} else {
|
|
385
|
+
// Single test command (backward compatible).
|
|
386
|
+
return agent(
|
|
387
|
+
`Working dir: ${WORK}. Run: ${TEST} (PowerShell or Git Bash). Output: tail -n 40 to keep context bounded.\n` +
|
|
388
|
+
`Derive pass/fail from EXIT CODE (use bash set -o pipefail if piping). Report exact passed/failed counts, green=(failed===0), and for each failing test map it to the responsible item slug from this set: ${ITEMS.map(i => i.slug).join(', ')} (infer from the file/module in the traceback; a file is owned by exactly one item). Do not modify files.`,
|
|
389
|
+
{ label: `verify:${tag}`, phase: ph, model: 'haiku', schema: VERIFY }
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
phase('Integrate')
|
|
394
|
+
const testCmds = A.testCmds && Array.isArray(A.testCmds) && A.testCmds.length > 0 ? A.testCmds : null
|
|
395
|
+
let v = await verify('integrate', 'Integrate', testCmds)
|
|
396
|
+
let verifyOut = 0, repairOut = 0, repairsUsed = 0
|
|
397
|
+
{
|
|
398
|
+
const vEnd = budget.spent()
|
|
399
|
+
verifyOut += vEnd - (buildStart + buildOut)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
phase('Repair')
|
|
403
|
+
// Declare contractFindings, adversarialReviewOut, adversarialReviewPending here (before while loop)
|
|
404
|
+
// to avoid TDZ ReferenceError when they're referenced in early returns (lines ~419, ~452).
|
|
405
|
+
let contractFindings = []
|
|
406
|
+
let adversarialReviewOut = 0
|
|
407
|
+
let adversarialReviewPending = false
|
|
408
|
+
let round = 0
|
|
409
|
+
while (v && !v.green && round < CAP) {
|
|
410
|
+
round++
|
|
411
|
+
|
|
412
|
+
// Check ceiling before starting a repair round.
|
|
413
|
+
{
|
|
414
|
+
const spent = budget.spent()
|
|
415
|
+
if (CEILING && typeof CEILING.tokens === 'number' && spent > CEILING.tokens) {
|
|
416
|
+
log(`ABORT: cost ceiling exceeded before repair round ${round} (spent ${spent} > ${CEILING.tokens}).`)
|
|
417
|
+
const totalOut = spent
|
|
418
|
+
return {
|
|
419
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
420
|
+
build: (built || []).filter(Boolean).map(b => ({ slug: b.slug, wrote: b.wrote, files: b.filesWritten })),
|
|
421
|
+
integration: v ? { green: v.green, passed: v.passed, failed: v.failed } : { green: false, passed: null, failed: null },
|
|
422
|
+
repairsUsed,
|
|
423
|
+
contractFindings: null,
|
|
424
|
+
tokens: { buildOut, verifyOut, repairOut, adversarialReviewOut, totalOut, model: 'all-haiku (weight 1)' },
|
|
425
|
+
mergeReady: false,
|
|
426
|
+
ship: null,
|
|
427
|
+
aborted: true,
|
|
428
|
+
reason: 'cost_ceiling',
|
|
429
|
+
spent,
|
|
430
|
+
ceiling: CEILING.tokens,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Re-check brake before repair round if recheckBrake is set (allows mid-wave .HALT stops).
|
|
436
|
+
if (CEILING && CEILING.recheckBrake && A.brake && A.brake.checkCmd) {
|
|
437
|
+
const BRAKE = {
|
|
438
|
+
type: 'object', additionalProperties: false,
|
|
439
|
+
properties: { halted: { type: 'boolean' }, reason: { type: 'string' } },
|
|
440
|
+
required: ['halted', 'reason'],
|
|
441
|
+
}
|
|
442
|
+
const b = await agent(
|
|
443
|
+
`READ-ONLY safety-brake re-check before repair round ${round} — do NOT modify anything. In directory ${A.brake.cwd || WORK}, run:\n${A.brake.checkCmd}\n` +
|
|
444
|
+
`This checks the fleet kill-switch (.HALT sentinel) and/or the token cost-ceiling. Set halted=true if the command exits non-zero OR its output indicates HALTED / ceiling exceeded; set halted=false only if it clearly reports OK/clean/under-ceiling. Report the reason.`,
|
|
445
|
+
{ label: `repair-${round}:brake`, phase: 'Repair', model: 'haiku', schema: BRAKE }
|
|
446
|
+
)
|
|
447
|
+
if (b && b.halted) {
|
|
448
|
+
log(`ABORT: safety brake re-engaged before repair round ${round} — ${b.reason}. Stopping repairs.`)
|
|
449
|
+
const spent = budget.spent()
|
|
450
|
+
const totalOut = spent
|
|
451
|
+
return {
|
|
452
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
453
|
+
build: (built || []).filter(Boolean).map(b => ({ slug: b.slug, wrote: b.wrote, files: b.filesWritten })),
|
|
454
|
+
integration: v ? { green: v.green, passed: v.passed, failed: v.failed } : { green: false, passed: null, failed: null },
|
|
455
|
+
repairsUsed,
|
|
456
|
+
contractFindings: null,
|
|
457
|
+
tokens: { buildOut, verifyOut, repairOut, adversarialReviewOut, totalOut, model: 'all-haiku (weight 1)' },
|
|
458
|
+
mergeReady: false,
|
|
459
|
+
ship: null,
|
|
460
|
+
aborted: true,
|
|
461
|
+
reason: 'halted',
|
|
462
|
+
brake: b,
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Include both integration-failing items AND self-check-failing items in repair targets.
|
|
468
|
+
const failingSlugs = (v.failingItems || []).filter(s => ITEMS.some(i => i.slug === s))
|
|
469
|
+
const allFailingItems = Array.from(new Set([...failingSlugs, ...selfCheckFailed]))
|
|
470
|
+
let targets = allFailingItems.length ? ITEMS.filter(i => allFailingItems.includes(i.slug)) : ITEMS
|
|
471
|
+
|
|
472
|
+
// FIX 2: Cap repair targets to top 3 worst failing items when timebox is set (latency fix #2).
|
|
473
|
+
// Rest are deferred to the orchestrator tail (logged but not in this round).
|
|
474
|
+
const deferredItems = []
|
|
475
|
+
if (TIMEBOX_MINUTES && targets.length > 3) {
|
|
476
|
+
deferredItems.push(...targets.slice(3))
|
|
477
|
+
targets = targets.slice(0, 3)
|
|
478
|
+
log(`Repair round ${round} capped at 3 items (timebox); deferred: ${deferredItems.map(t => t.slug).join(', ')}`)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
log(`Integration red (${v.failed} failed) — repair round ${round}/${CAP} on: ${targets.map(t => t.slug).join(', ')}`)
|
|
482
|
+
repairsUsed = targets.length
|
|
483
|
+
const rStart = budget.spent()
|
|
484
|
+
await parallel(targets.map((it) => () => {
|
|
485
|
+
// FIX 1: Build targeted test command for this item only (latency fix #1).
|
|
486
|
+
// The verify result's failingItems tells us which items have failing tests.
|
|
487
|
+
// Construct a prompt that directs repair workers to run ONLY tests for their owned files,
|
|
488
|
+
// run commands ONCE to a file, never re-run to grep, and never run full union suites.
|
|
489
|
+
const itemFiles = (it.ownsFiles || []).map(f => ` ${f}`).join('\n')
|
|
490
|
+
const repairPrompt = `ONE-TURN-WAVE repair for item "${it.slug}". Working dir: ${WORK}. The integration suite failed: ${v.detail}\n` +
|
|
491
|
+
`\n** SCOPED REPAIR CONTEXT (token discipline — repair cache-read tax fix, measured #1 sink): **\n` +
|
|
492
|
+
`You are given ONLY (a) the failing-suite verdict above and (b) the diff of YOUR OWN files. Do NOT re-read the whole prior build context — re-reading the full build is the measured top token sink.\n` +
|
|
493
|
+
`To see exactly what you changed, run ONCE: \`git -C ${WORK} diff -- ${(it.ownsFiles || []).join(' ')}\` (your owned files only).\n` +
|
|
494
|
+
`You MAY read your OWNED files and the named contract (${HINT}); do NOT read sibling workers' files or dump the whole build.\n` +
|
|
495
|
+
`\n** TARGETED TEST DISCIPLINE (latency fix #1): **\n` +
|
|
496
|
+
`You own these files (run tests ONLY for these, never the full union suite):\n${itemFiles}\n` +
|
|
497
|
+
`\nTo run tests for ONLY your files:\n` +
|
|
498
|
+
` - Identify which test files/tests exercise your owned files (from the integration failure details).\n` +
|
|
499
|
+
` - Run ONLY those specific tests, e.g., 'pytest test_foo.py::test_bar' or 'python -m unittest tests.test_module.TestClass.test_method'.\n` +
|
|
500
|
+
` - Do NOT run the full 'npm test' / 'python -m unittest discover' / 'pytest' suite yourself.\n` +
|
|
501
|
+
`\n** RUN-ONCE-TO-FILE (latency fix #1): **\n` +
|
|
502
|
+
`When running a command that produces verbose output:\n` +
|
|
503
|
+
` 1. Run it ONCE with full timeout (>= 5 minutes): cmd > /tmp/repair-output.log 2>&1; echo "exit=$?" >> /tmp/repair-output.log\n` +
|
|
504
|
+
` 2. Read the file to see results (tail, grep, etc) — never re-run the suite to see another slice.\n` +
|
|
505
|
+
` 3. Fix based on that ONE output; multiple runs of the same suite burn wall-clock minutes.\n` +
|
|
506
|
+
`\nFix ONLY your owned files with Edit/Write. Report.${timeboxLine()}`
|
|
507
|
+
return agent(repairPrompt, { label: `repair:${it.slug}`, phase: 'Repair', model: 'haiku', schema: DONE })
|
|
508
|
+
}))
|
|
509
|
+
const rEnd = budget.spent()
|
|
510
|
+
repairOut += rEnd - rStart
|
|
511
|
+
v = await verify(`repair-${round}`, 'Repair')
|
|
512
|
+
verifyOut += budget.spent() - rEnd
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// -------- Adversarial Review (optional, gated on args.adversarialReview) --------
|
|
516
|
+
if (ADVERSARIAL_REVIEW && ADVERSARIAL_REVIEW_MODE === 'blocking' && v && v.green) {
|
|
517
|
+
phase('AdversarialReview')
|
|
518
|
+
const reviewStart = budget.spent()
|
|
519
|
+
const REVIEW = {
|
|
520
|
+
type: 'object', additionalProperties: false,
|
|
521
|
+
properties: {
|
|
522
|
+
slug: { type: 'string' }, holds: { type: 'boolean' }, breakingScenario: { type: 'string' },
|
|
523
|
+
},
|
|
524
|
+
required: ['slug', 'holds', 'breakingScenario'],
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Spawn one reviewer agent per built item; each tries to refute the item's contract.
|
|
528
|
+
const reviewResults = await parallel(
|
|
529
|
+
(built || []).filter(Boolean).map((b) => () => {
|
|
530
|
+
if (!b.slug) return null
|
|
531
|
+
const item = ITEMS.find(i => i.slug === b.slug)
|
|
532
|
+
if (!item) return null
|
|
533
|
+
|
|
534
|
+
const ownedFilesStr = (item.ownsFiles || []).length > 0
|
|
535
|
+
? `Owned files:\n${(item.ownsFiles || []).map(f => ` ${f}`).join('\n')}\n`
|
|
536
|
+
: 'No owned files specified.\n'
|
|
537
|
+
|
|
538
|
+
const reviewPrompt = `CONTRACT REFUTATION review for item "${b.slug}". Working dir: ${WORK}.\n` +
|
|
539
|
+
`\nITEM CONTRACT (stated purpose):\n${item.prompt}\n` +
|
|
540
|
+
`\n${ownedFilesStr}` +
|
|
541
|
+
`Your job: READ the actual code the implementer wrote (in the ownsFiles above). ` +
|
|
542
|
+
`Try to construct a concrete input, scenario, or edge case where the implementation VIOLATES its stated contract — ` +
|
|
543
|
+
`does NOT do what the prompt says it should do. You are NOT running tests (tests may be tautological); ` +
|
|
544
|
+
`reason about the specification and the code.\n` +
|
|
545
|
+
`\nIf you can construct a breaking scenario, set holds=false and describe it in breakingScenario (be specific: inputs, expected vs actual behavior).\n` +
|
|
546
|
+
`If the code appears to genuinely meet its contract, set holds=true and breakingScenario="" (empty string).\n` +
|
|
547
|
+
`\nReport schema: {slug, holds, breakingScenario}.`
|
|
548
|
+
|
|
549
|
+
return agent(reviewPrompt, { label: nextLabel(`review:${b.slug}`), phase: 'AdversarialReview', model: 'haiku', schema: REVIEW })
|
|
550
|
+
})
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
// Collect findings where holds=false into contractFindings.
|
|
554
|
+
for (const r of (reviewResults || [])) {
|
|
555
|
+
if (r && r.slug) {
|
|
556
|
+
if (!r.holds) {
|
|
557
|
+
contractFindings.push({ slug: r.slug, breakingScenario: r.breakingScenario })
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
adversarialReviewOut = budget.spent() - reviewStart
|
|
563
|
+
log(`AdversarialReview done: ${contractFindings.length} contract violation(s) found.`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// LEVER 2 (wall-clock): in concurrent-note mode, do NOT run adversarialReview inline / do NOT await it before
|
|
567
|
+
// signaling merge-readiness. Defer it so the orchestrator kicks CI immediately and runs the refutation in
|
|
568
|
+
// parallel with the CI-wait window; contractFindings then gate MERGE (not CI). Findings are pending here.
|
|
569
|
+
if (ADVERSARIAL_REVIEW && ADVERSARIAL_REVIEW_MODE === 'concurrent-note' && v && v.green) {
|
|
570
|
+
adversarialReviewPending = true
|
|
571
|
+
log('AdversarialReview DEFERRED (concurrent-note): integration green — orchestrator should kick CI immediately and run adversarialReview in parallel; contractFindings gate MERGE, not CI.')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Check ceiling before Ship.
|
|
575
|
+
{
|
|
576
|
+
const spent = budget.spent()
|
|
577
|
+
if (CEILING && typeof CEILING.tokens === 'number' && spent > CEILING.tokens) {
|
|
578
|
+
log(`ABORT: cost ceiling exceeded before Ship (spent ${spent} > ${CEILING.tokens}).`)
|
|
579
|
+
const totalOut = spent
|
|
580
|
+
return {
|
|
581
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
582
|
+
build: (built || []).filter(Boolean).map(b => ({ slug: b.slug, wrote: b.wrote, files: b.filesWritten })),
|
|
583
|
+
integration: v ? { green: v.green, passed: v.passed, failed: v.failed } : { green: false, passed: null, failed: null },
|
|
584
|
+
repairsUsed,
|
|
585
|
+
contractFindings: contractFindings.length > 0 ? contractFindings : null,
|
|
586
|
+
tokens: { buildOut, verifyOut, repairOut, adversarialReviewOut, totalOut, model: 'all-haiku (weight 1)' },
|
|
587
|
+
mergeReady: v && v.green,
|
|
588
|
+
ship: null,
|
|
589
|
+
aborted: true,
|
|
590
|
+
reason: 'cost_ceiling',
|
|
591
|
+
spent,
|
|
592
|
+
ceiling: CEILING.tokens,
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ---------------- Ship (batched git boundary — P2: one commit+push per WAVE, not per item) ----------------
|
|
598
|
+
let ship = null
|
|
599
|
+
if (A.git && v && v.green) {
|
|
600
|
+
phase('Ship')
|
|
601
|
+
const g = A.git
|
|
602
|
+
const repoDir = g.repoDir || WORK
|
|
603
|
+
const origin = g.origin || 'origin'
|
|
604
|
+
const expectTop = g.expectTopLevel || (g.sandboxInit ? repoDir : null)
|
|
605
|
+
// HARDENING (wave-22 incident): a corrupted manifest once passed the literal placeholder
|
|
606
|
+
// "WORKTREE_ROOT_PER_ITEM" as expectTopLevel; the Ship guard then compared toplevel against a
|
|
607
|
+
// non-path string that could never match, and the classifier had to catch the resulting
|
|
608
|
+
// primary-tree/branch-"undefined" Ship prompt. In REAL-REPO mode expectTop MUST be a real
|
|
609
|
+
// absolute path — hard-fail preflight-style here BEFORE assembling any Ship prompt, rather than
|
|
610
|
+
// relying on the in-prompt guard to notice a garbage expected value.
|
|
611
|
+
if (!g.sandboxInit) {
|
|
612
|
+
const looksAbsolute = typeof expectTop === 'string' && /^([a-zA-Z]:[\\/]|\/)/.test(expectTop)
|
|
613
|
+
const looksPlaceholder = typeof expectTop === 'string' && /[A-Z_]{6,}/.test(expectTop) && !/[\\/]/.test(expectTop)
|
|
614
|
+
if (!expectTop || !looksAbsolute || looksPlaceholder || String(expectTop).includes('undefined')) {
|
|
615
|
+
log(`ABORT Ship: args.git.expectTopLevel is not a real absolute path (got ${JSON.stringify(expectTop)}). Pass each item's actual sibling-worktree root, or omit args.git to skip the batched Ship and let the orchestrator run the merge train by hand.`)
|
|
616
|
+
return {
|
|
617
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
618
|
+
build: (built || []).filter(Boolean).map(b => ({ slug: b.slug, wrote: b.wrote, files: b.filesWritten })),
|
|
619
|
+
integration: v ? { green: v.green, passed: v.passed, failed: v.failed } : { green: false, passed: null, failed: null },
|
|
620
|
+
repairsUsed, mergeReady: v && v.green, ship: null,
|
|
621
|
+
aborted: true, reason: 'bad_expectTopLevel', badValue: expectTop,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const scanLine = g.secretScan ? ` Then gate on secrets: \`python ${g.secretScan} --staged\` (must print CLEAN / exit 0; if it blocks, STOP and report committed=false).` : ''
|
|
626
|
+
ship = await agent(
|
|
627
|
+
`BATCHED GIT BOUNDARY — make ONE commit + ONE push for the WHOLE integrated wave (${ITEMS.length} items), NOT one per item, to amortize CI over the batch. Working tree: ${repoDir}. Use Git Bash.\n` +
|
|
628
|
+
`*** SAFETY GUARD — DO THIS FIRST, before ANY git write: *** run \`git -C ${repoDir} rev-parse --show-toplevel\`.\n` +
|
|
629
|
+
(g.sandboxInit
|
|
630
|
+
? ` - SANDBOX MODE: the result MUST be empty/error (no repo yet) OR exactly ${repoDir}. If it resolves to ANY ENCLOSING/ANCESTOR repo (a path SHORTER than / a prefix of ${repoDir}), you are inside another repo — ABORT NOW: do NOT init/add/commit/push anything; return committed=false, pushed=false, oneCommit=false, note="ABORT: sandbox inside repo <toplevel>". If safe: \`git -C ${repoDir} init -q\`, set a local user.name/user.email, create a LOCAL bare origin \`git init --bare -q ${g.sandboxInit}\`, then add/point the remote (\`git -C ${repoDir} remote add ${origin} ${g.sandboxInit}\`, or \`remote set-url\` if it exists). NEVER touch a remote named after a real host.\n`
|
|
631
|
+
: ` - REAL-REPO MODE: the result MUST equal exactly ${expectTop}. If it does NOT, ABORT: do NOT add/commit/push; return committed=false, note="ABORT: toplevel <toplevel> != expected ${expectTop}". (This prevents ever committing into an unintended repo.)\n`) +
|
|
632
|
+
`STAGE THE REAL BUILD — do NOT create any placeholder/dummy files: \`git -C ${repoDir} checkout -B ${g.branch}\` ; \`git -C ${repoDir} add -A\` (stages the files the build workers already wrote — the actual package/modules, specs, tests). Before committing, \`git -C ${repoDir} status --short\` and CONFIRM the staged set is the real source (NOT invented item*.txt).${scanLine} \`git -C ${repoDir} commit -q -m "${g.message || 'wave: batched one-turn build'}"\` ; ` +
|
|
633
|
+
(g.push === false ? `do NOT push (report pushed=false).` : `\`git -C ${repoDir} push -q -u ${origin} ${g.branch}\`.`) +
|
|
634
|
+
`\nVERIFY + REPORT: \`git -C ${repoDir} show --stat HEAD\` — report the sha, fileCount (files in that ONE commit), and oneCommit=true ONLY if exactly ONE new commit holds the whole integrated wave. Confirm NO per-item commits and NO invented placeholder files.`,
|
|
635
|
+
{ label: 'ship:batched', phase: 'Ship', model: 'haiku', schema: SHIP }
|
|
636
|
+
)
|
|
637
|
+
log(`Ship: committed=${ship && ship.committed} pushed=${ship && ship.pushed} files=${ship && ship.fileCount} oneCommit=${ship && ship.oneCommit}`)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ---------------- Report ----------------
|
|
641
|
+
phase('Report')
|
|
642
|
+
const totalOut = budget.spent() - buildStart
|
|
643
|
+
const result = {
|
|
644
|
+
preflight: { items: ITEMS.length, ownedFiles: Object.keys(owner).length, sandbox: setupInfo },
|
|
645
|
+
build: (built || []).filter(Boolean).map(b => ({ slug: b.slug, wrote: b.wrote, files: b.filesWritten })),
|
|
646
|
+
selfCheck: selfCheckResults && Object.keys(selfCheckResults).length > 0 ? selfCheckResults : null,
|
|
647
|
+
integration: v ? { green: v.green, passed: v.passed, failed: v.failed } : { green: false, passed: null, failed: null },
|
|
648
|
+
repairsUsed,
|
|
649
|
+
contractFindings: contractFindings.length > 0 ? contractFindings : null,
|
|
650
|
+
adversarialReviewMode: ADVERSARIAL_REVIEW ? ADVERSARIAL_REVIEW_MODE : null,
|
|
651
|
+
adversarialReviewPending,
|
|
652
|
+
tokens: { buildOut, verifyOut, selfCheckOut, repairOut, adversarialReviewOut, totalOut, model: 'all-haiku (weight 1)' },
|
|
653
|
+
mergeReady: !!(v && v.green),
|
|
654
|
+
ship,
|
|
655
|
+
note: 'One Workflow call = one orchestrator turn. All-Haiku => raw==weighted. Setup tokens excluded. SelfCheck/PostBuild tokens included. AdversarialReview tokens only when args.adversarialReview is truthy. AdversarialReviewMode: "blocking" (default) runs the refutation inline before this return; "concurrent-note" defers it (adversarialReviewPending=true, contractFindings=null here) so the orchestrator kicks CI immediately and runs adversarialReview in parallel, gating MERGE (not CI) on contractFindings — overlapping the review with the CI-wait window. Real-repo wiring: give each item its own sibling git worktree (git worktree add ../aesop-wt-<slug> -b <branch> origin/main), workers write there + push; orchestrator opens PRs + ci_merge_wait after — the async CI/merge boundary stays outside this one turn.',
|
|
656
|
+
}
|
|
657
|
+
log(`DONE. selfcheck=${selfCheckFailed.length} failed, integration green=${result.mergeReady} passed=${result.integration.passed} repairs=${repairsUsed} contractViolations=${contractFindings.length} buildOut=${buildOut} totalOut=${totalOut}`)
|
|
658
|
+
return result
|