@matt82198/aesop 0.1.0 → 0.3.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +117 -2
  2. package/README.md +59 -218
  3. package/bin/cli.js +168 -41
  4. package/daemons/run-watchdog.sh +16 -4
  5. package/daemons/selfheal.sh +231 -0
  6. package/docs/ANY-REPO.md +427 -0
  7. package/docs/CONTRIBUTING.md +72 -0
  8. package/docs/DEMO.md +334 -0
  9. package/docs/HOOK-INSTALL.md +15 -56
  10. package/docs/INSTALL.md +74 -4
  11. package/docs/PORTING.md +166 -0
  12. package/docs/README.md +33 -3
  13. package/docs/TEAM-STATE.md +372 -0
  14. package/docs/reproduce.md +33 -3
  15. package/driver/CLAUDE.md +150 -0
  16. package/driver/README.md +383 -0
  17. package/driver/aesop.config.example.json +80 -0
  18. package/driver/agent_driver.py +355 -0
  19. package/driver/backend_config.py +253 -0
  20. package/driver/claude_code_driver.py +198 -0
  21. package/driver/codex_driver.py +673 -0
  22. package/driver/openai_compatible_driver.py +249 -0
  23. package/driver/openai_transport.py +179 -0
  24. package/driver/verification_policy.py +75 -0
  25. package/driver/wave_bridge.py +254 -0
  26. package/driver/wave_loop.py +1408 -0
  27. package/driver/wave_scheduler.py +890 -0
  28. package/hooks/pre-push-policy.sh +131 -33
  29. package/mcp/server.mjs +320 -4
  30. package/monitor/collect-signals.mjs +69 -0
  31. package/package.json +22 -14
  32. package/skills/CLAUDE.md +132 -2
  33. package/skills/buildsystem/SKILL.md +330 -0
  34. package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
  35. package/skills/fleet/SKILL.md +113 -0
  36. package/skills/power/SKILL.md +246 -131
  37. package/state_store/__init__.py +4 -2
  38. package/state_store/api.py +19 -4
  39. package/state_store/coordination.py +209 -0
  40. package/state_store/identity.py +51 -0
  41. package/state_store/projections.py +63 -0
  42. package/state_store/read_api.py +156 -0
  43. package/state_store/store.py +185 -73
  44. package/state_store/write_api.py +462 -0
  45. package/templates/wave-presets/data.json +65 -0
  46. package/templates/wave-presets/library.json +65 -0
  47. package/templates/wave-presets/saas.json +64 -0
  48. package/tools/audit_report.py +388 -0
  49. package/tools/bench_runner.py +100 -3
  50. package/tools/ci_merge_wait.py +256 -35
  51. package/tools/ci_workflow_lint.py +430 -0
  52. package/tools/claudemd_drift.py +394 -0
  53. package/tools/claudemd_lint.py +359 -0
  54. package/tools/common.py +39 -3
  55. package/tools/cost_ceiling.py +166 -43
  56. package/tools/cost_econ.py +480 -0
  57. package/tools/cost_projection.py +559 -0
  58. package/tools/crossos_drift.py +394 -0
  59. package/tools/defect_escape.py +252 -0
  60. package/tools/doctor.js +1 -1
  61. package/tools/eod_sweep.py +188 -26
  62. package/tools/fleet.js +260 -0
  63. package/tools/fleet_ledger.py +209 -7
  64. package/tools/git_identity_check.py +315 -0
  65. package/tools/health-score.js +40 -0
  66. package/tools/health_score.py +361 -0
  67. package/tools/metrics_gate.py +13 -4
  68. package/tools/mutation_test.py +523 -0
  69. package/tools/portability_check.py +206 -0
  70. package/tools/proposals.mjs +47 -2
  71. package/tools/reconcile.py +7 -4
  72. package/tools/reproduce.js +405 -0
  73. package/tools/secret_scan.py +207 -65
  74. package/tools/self_stats.py +20 -0
  75. package/tools/stall_check.py +247 -16
  76. package/tools/stateapi_lint.py +325 -0
  77. package/tools/test_battery.py +173 -0
  78. package/tools/transcript_digest.py +380 -0
  79. package/tools/verify_activity_filter.py +437 -0
  80. package/tools/verify_agent_inspector.py +2 -0
  81. package/tools/verify_cost_panel.py +345 -0
  82. package/tools/verify_dash.py +2 -0
  83. package/tools/verify_dispatch_panel.py +301 -0
  84. package/tools/verify_failure_drilldown.py +188 -0
  85. package/tools/verify_prboard.py +2 -0
  86. package/tools/verify_scorecards.py +281 -0
  87. package/tools/verify_submit_encoding.py +2 -0
  88. package/tools/verify_ui_trio.py +409 -0
  89. package/tools/verify_wave_telemetry.py +268 -0
  90. package/tools/wave_backlog_analyzer.py +490 -0
  91. package/tools/wave_ledger_hook.py +150 -0
  92. package/tools/wave_preflight.py +779 -0
  93. package/tools/wave_resume.py +215 -0
  94. package/tools/wave_templates.py +340 -0
  95. package/ui/agents.py +68 -14
  96. package/ui/collectors.py +81 -55
  97. package/ui/config.py +7 -2
  98. package/ui/cost.py +231 -12
  99. package/ui/handler.py +383 -55
  100. package/ui/quality_scorecard.py +232 -0
  101. package/ui/sse.py +3 -3
  102. package/ui/wave_audit_tail.py +213 -0
  103. package/ui/wave_dispatch.py +280 -0
  104. package/ui/wave_failure.py +288 -0
  105. package/ui/wave_gantt.py +152 -0
  106. package/ui/wave_reasoning_tail.py +176 -0
  107. package/ui/wave_telemetry.py +383 -0
  108. package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
  109. package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
  110. package/ui/web/dist/index.html +2 -2
  111. package/bin/CLAUDE.md +0 -76
  112. package/daemons/CLAUDE.md +0 -36
  113. package/dash/CLAUDE.md +0 -32
  114. package/docs/archive/README.md +0 -3
  115. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
  116. package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
  117. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
  118. package/docs/archive/spikes/tiered-cognition/README.md +0 -27
  119. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
  120. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
  121. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
  122. package/hooks/CLAUDE.md +0 -89
  123. package/mcp/CLAUDE.md +0 -213
  124. package/monitor/CLAUDE.md +0 -40
  125. package/scan/CLAUDE.md +0 -30
  126. package/state_store/CLAUDE.md +0 -39
  127. package/tools/CLAUDE.md +0 -79
  128. package/ui/CLAUDE.md +0 -127
  129. package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
  130. 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