@kognai/orchestrator-core 0.1.2 → 0.1.4

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.
@@ -0,0 +1,1567 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Orchestrator = void 0;
4
+ /**
5
+ * engine-orchestrator.ts — the Orchestrator runtime loop extracted from orchestrate-engine.ts
6
+ * (TICKET-231 engine split 5). The core agent-orchestration class: spawns the leadership +
7
+ * coding agents, runs the sprint loop, dual-supervisor review, CTO/CEO cycle, reporting.
8
+ * Pulls primitives from the engine-primitives leaf and the agent classes/CodingAgent/loaders
9
+ * directly; the 5 engine-resident glue helpers come back from './orchestrate-engine' via a
10
+ * runtime-safe circular import (the class uses them at call-time, not module-load).
11
+ */
12
+ const fs_1 = require("fs");
13
+ const child_process_1 = require("child_process");
14
+ const crypto_1 = require("crypto");
15
+ const mc_client_1 = require("./mc-client");
16
+ const chomsky_runner_1 = require("./chomsky-runner");
17
+ const cto_approval_gate_1 = require("./cto-approval-gate");
18
+ const wallet_state_1 = require("./wallet-state");
19
+ const brainx_swarm_bridge_1 = require("./brainx-swarm-bridge");
20
+ const event_bus_publisher_1 = require("./event-bus-publisher");
21
+ const aar_middleware_1 = require("./aar-middleware");
22
+ const skill_crystalliser_1 = require("./skill-crystalliser");
23
+ const trust_score_updater_1 = require("./trust-score-updater");
24
+ const code_asset_crystalliser_1 = require("./code-asset-crystalliser");
25
+ const monotask_state_machine_1 = require("./monotask-state-machine");
26
+ const code_failure_logger_1 = require("./code-failure-logger");
27
+ const token_budget_validator_1 = require("./token-budget-validator");
28
+ const decomposer_feedback_1 = require("./decomposer-feedback");
29
+ const phantom_workspace_1 = require("./omel/phantom-workspace");
30
+ const wipe_witness_1 = require("./omel/wipe-witness");
31
+ const human_brake_1 = require("./omel/human-brake");
32
+ const perm_judge_1 = require("./perm-judge");
33
+ const orchestrator_tap_1 = require("./ksl/orchestrator-tap");
34
+ const engine_primitives_1 = require("./engine-primitives");
35
+ const engine_agents_1 = require("./engine-agents");
36
+ const engine_coding_agent_1 = require("./engine-coding-agent");
37
+ const engine_loaders_1 = require("./engine-loaders");
38
+ const orchestrate_engine_1 = require("./orchestrate-engine");
39
+ const build_triage_1 = require("./build-triage");
40
+ class Orchestrator {
41
+ spawnGate;
42
+ ceo;
43
+ cto;
44
+ supervisor;
45
+ supervisor2;
46
+ agents = new Map();
47
+ tasks = [];
48
+ // TICKET-234: ceremony-depth path for this run. 'regulated' = full ceremony
49
+ // (default, safe); 'fast' = coder → single-supervisor → QA safety floor only.
50
+ // Set in run() by classifyBuildPath after tasks load. Defaults to regulated.
51
+ buildPath = 'regulated';
52
+ triage = null;
53
+ stats = { tasksExecuted: 0, approved: 0, rejected: 0, totalTokens: 0, conflicts: 0, escalations: 0 };
54
+ // Per-task structured run records — written to swarm run report at end of sprint
55
+ taskRuns = [];
56
+ /**
57
+ * Persist a single task's status back to the on-disk sprint file.
58
+ *
59
+ * Sprint-1547 fix: previously, task statuses were only written at end-of-run
60
+ * (line ~3055). Any exception, OOM, or SIGKILL between an approval and the
61
+ * end-of-run write dropped the approval — the sprint file still said
62
+ * 'pending', the sprint-runner cron repicked the same sprint, and the same
63
+ * task ran again. Sprint-1545 looped 30+ times overnight from this.
64
+ *
65
+ * Read-modify-write so concurrent edits to OTHER tasks (e.g. another
66
+ * Claude session) survive — we only overwrite this task's slot.
67
+ *
68
+ * Failure must NOT block the next task. Logged and swallowed.
69
+ */
70
+ persistTaskStatus(task) {
71
+ const sprintFile = process.argv[2] || 'sprints/current.json';
72
+ try {
73
+ if (!(0, fs_1.existsSync)(sprintFile))
74
+ return;
75
+ const sprintRaw = JSON.parse((0, fs_1.readFileSync)(sprintFile, 'utf-8'));
76
+ const arr = Array.isArray(sprintRaw.tasks) ? sprintRaw.tasks : null;
77
+ if (!arr)
78
+ return;
79
+ const idx = arr.findIndex((t) => t && t.id === task.id);
80
+ if (idx < 0)
81
+ return;
82
+ arr[idx] = { ...arr[idx], status: task.status };
83
+ // sprint-1566 F0/F0c: also persist rejected_reason when set (replaced-by-split, over-budget)
84
+ if (task.rejected_reason)
85
+ arr[idx].rejected_reason = task.rejected_reason;
86
+ // CTO-006 telemetry-blackout hotfix (2026-05-29, Slice A of TICKET-135):
87
+ // persist attempt_count + score + grade + rejection_reason. Without
88
+ // these, the .swarm-state file showed only {status, attempt_count: 1}
89
+ // even after 3 attempts, and every CTO retrospective marked score=? +
90
+ // attempts=?. 10 consecutive zero-ship sprints diagnosed as "we can't
91
+ // root-cause because telemetry is dark." This wires the orchestrator's
92
+ // in-memory fields → ACTIVE → .swarm-state via sprint-runner's sync.
93
+ if (task.attempt_count != null)
94
+ arr[idx].attempt_count = task.attempt_count;
95
+ if (task.score != null)
96
+ arr[idx].score = task.score;
97
+ if (task.grade != null)
98
+ arr[idx].grade = task.grade;
99
+ if (task.rejection_reason != null)
100
+ arr[idx].rejection_reason = task.rejection_reason;
101
+ // TICKET-094 FIX: atomic write via temp+rename. Plain writeFileSync was
102
+ // vulnerable to concurrent-reader-sees-empty-file races AND interleaved-
103
+ // writer truncation. The disk-state revert that stranded sprint-1588 +
104
+ // sprint-1589 in all-pending despite "Synced N updates" was almost
105
+ // certainly the orchestrator + sprint-runner.ts sync racing on the
106
+ // same file at sprint end. Tmp+rename is atomic on POSIX — readers see
107
+ // either old or new, never half.
108
+ const tmp = `${sprintFile}.tmp.${process.pid}.${Date.now()}`;
109
+ (0, fs_1.writeFileSync)(tmp, JSON.stringify(sprintRaw, null, 2));
110
+ require('fs').renameSync(tmp, sprintFile);
111
+ }
112
+ catch (e) {
113
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [persist] Failed to update ${task.id} status: ${(e?.message || '').substring(0, 100)}`);
114
+ }
115
+ }
116
+ /** sprint-1566 F0/F0d: inject decomposer-split sub-tasks into the active
117
+ * sprint file as new pending tasks. Sprint-runner picks them up on the next
118
+ * cron tick. The original task stays in the file with status='replaced-by-split'.
119
+ *
120
+ * Founder fix 2026-05-27: ALSO push to this.tasks (in-memory). The sprint-end
121
+ * writeFileSync at line 4088 dumps this.tasks back to ACTIVE, overwriting
122
+ * whatever we wrote to disk here. Without the in-memory push, injected
123
+ * sub-tasks were silently wiped at sprint-end and the persistence fix
124
+ * in sprint-runner.ts saw nothing to forward to the source sprint file.
125
+ * This is the root cause of the 0-ship pattern in sprint-1596/1597. */
126
+ injectSplitTasks(original, splits, rationale) {
127
+ const sprintFile = process.argv[2] || 'sprints/current.json';
128
+ if (!(0, fs_1.existsSync)(sprintFile))
129
+ return;
130
+ const sprintRaw = JSON.parse((0, fs_1.readFileSync)(sprintFile, 'utf-8'));
131
+ if (!Array.isArray(sprintRaw.tasks))
132
+ return;
133
+ const existingIds = new Set(sprintRaw.tasks.map((t) => t?.id));
134
+ let injected = 0;
135
+ for (const s of splits) {
136
+ if (existingIds.has(s.id))
137
+ continue; // idempotent: already split before
138
+ const newTask = {
139
+ ...s,
140
+ attempt_count: 0,
141
+ parent_task_id: original.id,
142
+ split_rationale: rationale,
143
+ injected_at: new Date().toISOString(),
144
+ };
145
+ sprintRaw.tasks.push(newTask);
146
+ // Also push to in-memory — survives the sprint-end ACTIVE rewrite.
147
+ this.tasks.push(newTask);
148
+ injected++;
149
+ }
150
+ if (injected > 0)
151
+ (0, fs_1.writeFileSync)(sprintFile, JSON.stringify(sprintRaw, null, 2));
152
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` [inject] Added ${injected} split sub-tasks to ${sprintFile} (in-memory + on-disk)`);
153
+ }
154
+ // TICKET-225: optional template-supplied spawn governance gate, threaded
155
+ // from runOrchestrator(config) down to AgentCreator. Undefined = no gate.
156
+ constructor(spawnGate) {
157
+ this.spawnGate = spawnGate;
158
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '\n╔══════════════════════════════════════════════════════════╗');
159
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '║ Kognai Swarm Orchestrator v2.17 — V17 Architecture ║');
160
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '║ Local-first · ClawRouter cloud · DeepSeek reviews ║');
161
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '╚══════════════════════════════════════════════════════════╝\n');
162
+ // Leadership layer (CEO = Claude via Anthropic; Sup1 = DeepSeek/Sonnet;
163
+ // Sup2 = Haiku with DeepSeek fallback. Both supervisors were originally
164
+ // Sonnet + Codex — see file-header history note.)
165
+ this.ceo = new engine_agents_1.CEOAgent();
166
+ this.supervisor = new engine_agents_1.SupervisorAgent();
167
+ this.supervisor2 = new engine_agents_1.Supervisor2Agent();
168
+ // Technology layer (MiniMax)
169
+ this.cto = new engine_agents_1.CTOAgent();
170
+ // Execution layer — dynamically load all coding agents from agents/ directory
171
+ const skipAgents = ['ceo', 'supervisor', 'skills', 'cto', 'cmo'];
172
+ const agentDirs = (0, fs_1.existsSync)('./agents') ? (0, fs_1.readdirSync)('./agents').filter(d => {
173
+ if (skipAgents.includes(d))
174
+ return false;
175
+ return (0, fs_1.existsSync)(`./agents/${d}/prompt.md`);
176
+ }) : [];
177
+ // Constitutional preamble — injected into every agent's system prompt
178
+ const constitutionalPreamble = (0, engine_loaders_1.loadConstitutionalPreamble)();
179
+ if (constitutionalPreamble) {
180
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ' ⚖️ Constitutional preamble loaded — will bind all agents');
181
+ }
182
+ for (const name of agentDirs) {
183
+ const promptPath = `./agents/${name}/prompt.md`;
184
+ const rawPrompt = (0, fs_1.readFileSync)(promptPath, 'utf-8');
185
+ const prompt = constitutionalPreamble + rawPrompt;
186
+ this.agents.set(name, new engine_coding_agent_1.CodingAgent(name, prompt));
187
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, `+ Loaded ${name} agent (MiniMax M2.5)`);
188
+ }
189
+ // Agent count: CEO + Sup1 + Sup2 (all 3 Anthropic-bound, with provider routing happening per-call)
190
+ // + 1 CMO (qwen3:4b local) + 1 CTO (MiniMax) + N coders (MiniMax)
191
+ const totalAgents = 3 + 1 + 1 + this.agents.size;
192
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\n✓ ${totalAgents} agents loaded (3 Anthropic-bound leadership + 1 CMO qwen3:4b + ${1 + this.agents.size} MiniMax)\n`);
193
+ }
194
+ loadTasks() {
195
+ const sprintFile = process.argv[2] || 'sprints/current.json';
196
+ if (!(0, fs_1.existsSync)(sprintFile)) {
197
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, `Sprint file not found: ${sprintFile}`);
198
+ process.exit(1);
199
+ }
200
+ const sprint = JSON.parse((0, fs_1.readFileSync)(sprintFile, 'utf-8'));
201
+ this.tasks = sprint.tasks || [];
202
+ const _sprintId = sprintFile.replace(/.*\//, '').replace('.json', '');
203
+ // Normalize deliverables: CEO planner may emit flat string[] instead of {code,tests,docs}
204
+ for (const task of this.tasks) {
205
+ const d = task.deliverables;
206
+ if (!d) {
207
+ // Sprint JSON may omit deliverables — default from task_target
208
+ const target = task.task_target;
209
+ task.deliverables = { code: target ? [target] : [], tests: [], docs: [] };
210
+ }
211
+ else if (Array.isArray(d)) {
212
+ task.deliverables = {
213
+ code: d.filter((f) => f.indexOf("test") === -1 && f.indexOf("spec") === -1 && f.slice(-3) !== ".md"),
214
+ tests: d.filter((f) => f.indexOf("test") !== -1 || f.indexOf("spec") !== -1),
215
+ docs: d.filter((f) => f.slice(-3) === ".md"),
216
+ };
217
+ }
218
+ // Normalize description → context: sprint JSON files may use either field name
219
+ if (!task.context && task.description) {
220
+ task.context = task.description;
221
+ }
222
+ // Ensure context is always a string (never undefined)
223
+ if (!task.context)
224
+ task.context = `${task.id}: ${task.title || task.type}`;
225
+ // Normalize priority: sprint JSON may omit it
226
+ if (!task.priority)
227
+ task.priority = 'medium';
228
+ // Normalize status: sprint JSON may omit it. Without a status the task is
229
+ // silently skipped in Phase 2 (`if (task.status !== 'pending') continue`),
230
+ // so the full ceremony runs but executes 0 tasks. Default a MISSING status
231
+ // to 'pending'; explicit terminal states (done/skipped/rejected/approved)
232
+ // are preserved so re-runs don't re-execute finished work.
233
+ if (!task.status)
234
+ task.status = 'pending';
235
+ // Fix: task_target used as file path (e.g., 'scripts/lib/foo.ts') must be cleared
236
+ // so it doesn't confuse the routing switch which expects: local|cloud-code|cloud-exec|cloud-post
237
+ const VALID_ROUTING_TARGETS = ['local', 'cloud-code', 'cloud-exec', 'cloud-post'];
238
+ if (task.task_target && !VALID_ROUTING_TARGETS.includes(task.task_target)) {
239
+ delete task.task_target; // file path already captured in deliverables.code
240
+ }
241
+ // Stamp sprint_id — avoids 'unknown' in logs/routing/YYYY-MM-DD.jsonl
242
+ if (!task.sprint_id)
243
+ task.sprint_id = _sprintId;
244
+ }
245
+ // Reset stale in_progress tasks back to pending
246
+ for (const task of this.tasks) {
247
+ if (task.status === 'in_progress' || task.status === 'review') {
248
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Resetting stale task ${task.id} (${task.status} -> pending)`);
249
+ task.status = 'pending';
250
+ }
251
+ }
252
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, `Loaded ${this.tasks.length} tasks from ${sprintFile}`);
253
+ }
254
+ // ===== Truncation Detection =====
255
+ isTruncationRejection(review) {
256
+ const truncationKeywords = [
257
+ 'truncat', 'incomplete', 'cut off', 'cuts off', 'ends abruptly',
258
+ 'missing implementation', 'missing the actual', 'file is incomplete',
259
+ 'cuts off mid', 'missing core functionality', 'missing the entire',
260
+ ];
261
+ const text = (review.summary + ' ' +
262
+ (review.issues || []).map(i => i.description).join(' ')).toLowerCase();
263
+ return truncationKeywords.some(kw => text.includes(kw));
264
+ }
265
+ // ===== CTO Task Decomposition (for truncation-prone tasks) =====
266
+ async ctoDecomposeTask(task) {
267
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, `\n[cto-decompose] 🔧 CTO splitting ${task.id} into smaller sub-tasks...`);
268
+ const allDeliverables = [
269
+ ...(task.deliverables.code || []),
270
+ ...(task.deliverables.tests || []),
271
+ ];
272
+ const userPrompt = `A task keeps failing because MiniMax M2.5 truncates output when generating multiple files.
273
+
274
+ ## Failed Task
275
+ - ID: ${task.id}
276
+ - Agent: ${task.agent}
277
+ - Context: ${task.context.substring(0, 1500)}
278
+ - Deliverable files: ${allDeliverables.join(', ')}
279
+
280
+ ## Problem
281
+ MiniMax M2.5 has a ~4500 token output limit per call. When a task has ${allDeliverables.length} files, each file gets less space and code gets truncated.
282
+
283
+ ## Your Job
284
+ Split this task into smaller sub-tasks. Each sub-task must have at most 1 code file + 1 test file (2 files max).
285
+
286
+ ## Rules
287
+ 1. Types/interfaces files FIRST (other files depend on them)
288
+ 2. Barrel/index export files LAST (they import from everything else)
289
+ 3. Each sub-task must be self-contained (agent can generate it without seeing other sub-task results)
290
+ 4. Include enough context in each sub-task for the agent to know what to generate
291
+ 5. Maximum 5 sub-tasks
292
+
293
+ ## Output Format
294
+ Return a JSON array of sub-task specs:
295
+ [
296
+ {
297
+ "sub_id": "${task.id}-A",
298
+ "context": "Full task context for this sub-task including what types/interfaces to define",
299
+ "code": ["path/to/file.ts"],
300
+ "tests": ["path/to/file.test.ts"]
301
+ }
302
+ ]
303
+
304
+ ONLY output the JSON array. No markdown, no explanation.`;
305
+ try {
306
+ const response = await (0, engine_primitives_1.callLLM)('clawrouter', 'deepseek/deepseek-chat', this.cto['systemPrompt'] || '', userPrompt, 120000, 'cto', 'fallback_task_decomposer');
307
+ let content = response.choices?.[0]?.message?.content || '';
308
+ content = content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
309
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
310
+ if (!jsonMatch) {
311
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ' CTO decomposition returned no JSON, falling back to mechanical split');
312
+ return this.fallbackDecompose(task);
313
+ }
314
+ const specs = JSON.parse(jsonMatch[0]);
315
+ if (!Array.isArray(specs) || specs.length < 2) {
316
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ' CTO returned <2 sub-tasks, falling back to mechanical split');
317
+ return this.fallbackDecompose(task);
318
+ }
319
+ // Convert specs to AgentTask objects
320
+ const subtasks = specs.slice(0, 5).map((spec, i) => ({
321
+ id: spec.sub_id || `${task.id}-${String.fromCharCode(65 + i)}`,
322
+ agent: task.agent,
323
+ type: task.type,
324
+ priority: task.priority,
325
+ dependencies: i > 0 ? [specs[i - 1].sub_id || `${task.id}-${String.fromCharCode(64 + i)}`] : [],
326
+ context: spec.context,
327
+ deliverables: {
328
+ code: spec.code || [],
329
+ tests: spec.tests || [],
330
+ },
331
+ status: 'pending',
332
+ }));
333
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` ✓ CTO decomposed ${task.id} into ${subtasks.length} sub-tasks:`);
334
+ for (const st of subtasks) {
335
+ const files = [...(st.deliverables.code || []), ...(st.deliverables.tests || [])];
336
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` ${st.id}: ${files.join(', ')}`);
337
+ }
338
+ return subtasks;
339
+ }
340
+ catch (error) {
341
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` CTO decomposition failed: ${error.message}, using fallback`);
342
+ return this.fallbackDecompose(task);
343
+ }
344
+ }
345
+ // ===== Fallback: Mechanical file-based split =====
346
+ fallbackDecompose(task) {
347
+ const codeFiles = task.deliverables.code || [];
348
+ const testFiles = task.deliverables.tests || [];
349
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [fallback] Mechanically splitting ${task.id} by file...`);
350
+ const subtasks = [];
351
+ for (let i = 0; i < codeFiles.length; i++) {
352
+ const code = codeFiles[i];
353
+ // Find matching test file
354
+ const baseName = code.replace(/\.ts$/, '').split('/').pop() || '';
355
+ const matchingTest = testFiles.find(t => t.includes(baseName) && (t.includes('.test.') || t.includes('.spec.')));
356
+ subtasks.push({
357
+ id: `${task.id}-${String.fromCharCode(65 + i)}`,
358
+ agent: task.agent,
359
+ type: task.type,
360
+ priority: task.priority,
361
+ dependencies: i > 0 ? [`${task.id}-${String.fromCharCode(64 + i)}`] : [],
362
+ context: `${task.context}\n\n## SUB-TASK: Generate ONLY the file "${code}"${matchingTest ? ` and its test "${matchingTest}"` : ''}.\nThis is part of a larger task that was split to avoid truncation. Focus on this file only. Make it complete and self-contained.`,
363
+ deliverables: {
364
+ code: [code],
365
+ tests: matchingTest ? [matchingTest] : [],
366
+ },
367
+ status: 'pending',
368
+ });
369
+ }
370
+ // Handle orphan test files (tests without matching code file)
371
+ const usedTests = subtasks.flatMap(st => st.deliverables.tests || []);
372
+ const orphanTests = testFiles.filter(t => !usedTests.includes(t));
373
+ if (orphanTests.length > 0) {
374
+ subtasks.push({
375
+ id: `${task.id}-${String.fromCharCode(65 + codeFiles.length)}`,
376
+ agent: task.agent,
377
+ type: task.type,
378
+ priority: task.priority,
379
+ dependencies: subtasks.length > 0 ? [subtasks[subtasks.length - 1].id] : [],
380
+ context: `${task.context}\n\n## SUB-TASK: Generate ONLY the test file(s): ${orphanTests.join(', ')}.\nAll source code files have already been generated. Write tests that import from the existing source files.`,
381
+ deliverables: {
382
+ code: [],
383
+ tests: orphanTests,
384
+ },
385
+ status: 'pending',
386
+ });
387
+ }
388
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` ✓ Fallback split ${task.id} into ${subtasks.length} sub-tasks`);
389
+ return subtasks;
390
+ }
391
+ // ===== Sub-task executor (limited retries, no recursive decomposition) =====
392
+ async executeSubTask(subtask, maxRetries) {
393
+ // Sprint 1309: default to 'coder' when subtask.agent is not set
394
+ const subAgentName = subtask.agent || 'coder';
395
+ const agent = this.agents.get(subAgentName);
396
+ if (!agent) {
397
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Agent not found for sub-task: ${subAgentName}`);
398
+ return false;
399
+ }
400
+ let lastReview;
401
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
402
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, `\n [sub-task] ${subtask.id} | Attempt: ${attempt}/${maxRetries}`);
403
+ this.stats.tasksExecuted++;
404
+ // AMD-08: depth=1 (sub-agent chain)
405
+ if (!monotask_state_machine_1.MonotaskSM.claim(subtask.agent, subtask.id, 1)) {
406
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [monotask] ${subtask.agent} unavailable — skipping sub-task attempt ${attempt}`);
407
+ continue;
408
+ }
409
+ monotask_state_machine_1.MonotaskSM.start(subtask.agent, subtask.id);
410
+ const subStart = Date.now();
411
+ const result = await agent.execute(subtask, lastReview);
412
+ if (result.files.length === 0) {
413
+ const dels = [...(subtask.deliverables?.code || []), ...(subtask.deliverables?.tests || []), ...(subtask.deliverables?.docs || [])];
414
+ const reasons = subtask._failureReasons || [];
415
+ const inferred = reasons.length ? reasons.join('; ') : (dels.length === 0 ? 'empty-deliverables' : 'unknown');
416
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` ✗ No files produced for sub-task ${subtask.id} [model=${result.model || 'n/a'}, deliverables=${dels.length}, reason=${inferred}]`);
417
+ monotask_state_machine_1.MonotaskSM.release(subtask.agent, subtask.id, `no files: ${inferred.slice(0, 60)}`);
418
+ return false;
419
+ }
420
+ // Supervisor review. TICKET-234: fast track → single supervisor; regulated → dual.
421
+ let review;
422
+ if (this.buildPath === 'fast') {
423
+ review = await this.supervisor.reviewTask(subtask, result.files);
424
+ }
425
+ else {
426
+ const [review1, review2] = await Promise.all([
427
+ this.supervisor.reviewTask(subtask, result.files),
428
+ this.supervisor2.reviewTask(subtask, result.files),
429
+ ]);
430
+ const dualResult = await (0, engine_agents_1.reconcileSupervisorReviews)(review1, review2, subtask, this.ceo);
431
+ review = dualResult.finalReview;
432
+ if (!dualResult.consensus)
433
+ this.stats.conflicts++;
434
+ if (dualResult.escalatedToCEO)
435
+ this.stats.escalations++;
436
+ }
437
+ lastReview = review;
438
+ // TICKET-214: instrument SUB-TASK attempts. The MAIN task loop already taps KSL +
439
+ // records reputation, but the split sub-task loop did neither — so split sprints
440
+ // went unrecorded in KSL and their rejections were never filed. Tap + score every
441
+ // attempt here too. Best-effort, non-fatal.
442
+ (0, orchestrate_engine_1.recordAgentScore)(subtask.agent, review.score);
443
+ try {
444
+ const _sid = (0, orchestrate_engine_1.resolveActiveSprintId)();
445
+ (0, orchestrator_tap_1.tapAttempt)({
446
+ sprint_id: _sid, task_id: subtask.id, attempt, agent: subtask.agent,
447
+ model: result.model || 'unknown',
448
+ prompt: String(subtask.context || subtask.title || subtask.id),
449
+ reply: (result.files || []).map((f) => f.content || '').join('\n').slice(0, 20000),
450
+ duration_ms: Date.now() - subStart,
451
+ });
452
+ }
453
+ catch { /* non-fatal */ }
454
+ if (review.verdict === 'APPROVED') {
455
+ this.stats.approved++;
456
+ monotask_state_machine_1.MonotaskSM.complete(subtask.agent, subtask.id);
457
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` ✓ Sub-task ${subtask.id} APPROVED on attempt ${attempt} (${review.score}/100)`);
458
+ return true;
459
+ }
460
+ this.stats.rejected++;
461
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` ↻ Sub-task ${subtask.id} REJECTED on attempt ${attempt} (${review.score}/100)`);
462
+ try {
463
+ (0, code_failure_logger_1.logCodeFailure)({ taskId: subtask.id, sprintId: (0, orchestrate_engine_1.resolveActiveSprintId)(), agentId: (0, orchestrate_engine_1.resolveAgentDid)(subtask.agent), attemptNum: attempt, score: review.score || 0, model: result.model || 'unknown', rejectionReason: review.summary || 'sub-task rejected', issues: review.issues || [], failType: 'supervisor_rejected' });
464
+ }
465
+ catch { /* non-fatal */ }
466
+ (0, engine_primitives_1.safeResetLastCommit)(subtask.id, subtask.agent, subtask.type, ' ');
467
+ monotask_state_machine_1.MonotaskSM.release(subtask.agent, subtask.id, `rejected attempt ${attempt}`);
468
+ }
469
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` ✗ Sub-task ${subtask.id} FAILED after ${maxRetries} attempts`);
470
+ return false;
471
+ }
472
+ // ===== Main task executor with CTO auto-decomposition =====
473
+ async executeTask(task) {
474
+ // Sprint 1309: default to 'coder' when task.agent is not set (queue-prescribed sprints omit agent field)
475
+ const agentName = task.agent || 'coder';
476
+ const agent = this.agents.get(agentName);
477
+ if (!agent) {
478
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, `Agent not found: ${agentName}`);
479
+ task.status = 'rejected';
480
+ this.persistTaskStatus(task);
481
+ // Record failure in taskRuns
482
+ this.taskRuns.push({
483
+ task_id: task.id, title: task.title || task.id, type: task.type,
484
+ task_target: task.task_target || 'cloud-code',
485
+ status: 'rejected', attempts: 0, model_used: '', provider: '',
486
+ tokens_total: 0, duration_seconds: 0, files_written: [],
487
+ review: null, error: `Agent not found: ${agentName}`, rejection_reason: 'Agent not found',
488
+ });
489
+ return;
490
+ }
491
+ // Sprint 706: BrainX — inject memories before task execution
492
+ try {
493
+ if (this._brainxBridge) {
494
+ const injection = await this._brainxBridge.injectMemories(task.agent);
495
+ if (injection.memory_count > 0)
496
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [BrainX] Injected ${injection.memory_count} memories for ${task.agent}`);
497
+ }
498
+ }
499
+ catch { /* BrainX injection is non-blocking */ }
500
+ const taskRunStart = Date.now();
501
+ const taskRun = {
502
+ task_id: task.id,
503
+ title: task.title || task.id,
504
+ type: task.type,
505
+ task_target: task.task_target || 'cloud-code',
506
+ status: 'pending',
507
+ attempts: 0,
508
+ model_used: '',
509
+ provider: '',
510
+ tokens_total: 0,
511
+ duration_seconds: 0,
512
+ files_written: [],
513
+ review: null,
514
+ error: null,
515
+ rejection_reason: null,
516
+ };
517
+ // sprint-1566 F0b: aligned with sprint-runner's PER_TASK_LIFETIME_MAX_ATTEMPTS=5.
518
+ // Was 10 (single-line const hidden under a misleading comment claiming it had
519
+ // already been lowered — it hadn't). At 10 retries × ~14K tokens/attempt
520
+ // (MiniMax + dual review) a single stuck task could burn ~140K tokens before
521
+ // the lifetime gate had a chance to look at it on the next run.
522
+ const MAX_RETRIES = parseInt(process.env.MAX_RETRIES_PER_RUN || '3', 10);
523
+ const TRUNCATION_THRESHOLD = 1;
524
+ // sprint-1566 F0c: per-task token budget. Caps cumulative tokens spent on
525
+ // one task across its retries so a single task can't eat the daily wallet.
526
+ // Raised 2026-05-27 from 25K → 50K: sprint-1590 lost 3 of 6 tasks because
527
+ // first-attempt token spend (30-62K) routinely exceeded the 25K cap,
528
+ // killing retries before supervisor rejection feedback could be applied.
529
+ // 50K = ~$0.10 worst-case at DeepSeek pricing for first attempt + 1 retry.
530
+ // TICKET-209 (2026-05-29): bumped 100k → 200k. With EDIT-MODE now the
531
+ // default for modify tasks, 200k is more headroom than legitimate work
532
+ // needs — but covers the long-tail of large create tasks (multi-section
533
+ // spec docs, full-module rewrites) without forcing escalation.
534
+ const PER_TASK_TOKEN_BUDGET = parseInt(process.env.PER_TASK_TOKEN_BUDGET || '200000', 10);
535
+ let taskTokensSpent = 0;
536
+ let truncationCount = 0;
537
+ let lastReview;
538
+ // OMEL AMD-13: Create isolated tmpdir for this task (cleaned up in finally)
539
+ const phantomCtx = phantom_workspace_1.phantomWorkspace.create(task.id);
540
+ try {
541
+ // CTO-20260528-002 (2026-05-27): one-file-per-call enforcement.
542
+ // The store_page incident in v12r+1 was a multi-file task that nobody
543
+ // pre-screened. Atomic tasks are easier to review, retry, and roll back.
544
+ // If a task targets >1 source file, force a per-file split up front —
545
+ // routeToDecomposer's Strategy A handles this deterministically.
546
+ {
547
+ const codeFiles = task.deliverables?.code?.filter((f) => !/__tests__|\.test\./.test(f)) || [];
548
+ const editFiles = task.deliverables?.edits || [];
549
+ const sourceFileCount = codeFiles.length + editFiles.length;
550
+ if (sourceFileCount > 1 && task.agent === 'coder') {
551
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [Atomicity] PRE-FLIGHT REJECT: ${task.id} targets ${sourceFileCount} source files (one-file-per-call policy)`);
552
+ const route = (0, decomposer_feedback_1.routeToDecomposer)({
553
+ original_task_id: task.id,
554
+ rejection_signal: 'needs_resplit',
555
+ suggested_splits: [...codeFiles, ...editFiles],
556
+ learnings_ref: 'docs/learnings.md §1',
557
+ original_task: task,
558
+ });
559
+ if ('task_split' in route) {
560
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` [Atomicity] split into ${route.task_split.length} per-file sub-tasks`);
561
+ try {
562
+ this.injectSplitTasks(task, route.task_split, route.rationale);
563
+ }
564
+ catch (e) {
565
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Inject failed: ${e.message?.slice(0, 120)}`);
566
+ }
567
+ task.status = 'replaced-by-split';
568
+ task.rejected_reason = `Replaced by ${route.task_split.length} per-file sub-tasks (one-file-per-call policy)`;
569
+ this.persistTaskStatus(task);
570
+ taskRun.status = 'replaced-by-split';
571
+ taskRun.rejection_reason = 'Atomicity pre-flight: multi-file task';
572
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
573
+ this.taskRuns.push(taskRun);
574
+ phantom_workspace_1.phantomWorkspace.cleanup(phantomCtx);
575
+ return;
576
+ }
577
+ // Unsplittable multi-file (e.g. zero deliverables resolved) → fall through
578
+ // to the token validator so we don't hard-block tasks the decomposer can't
579
+ // disambiguate. Logged for founder review.
580
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [Atomicity] decomposer could not split — proceeding (founder review): ${route.reason}`);
581
+ }
582
+ }
583
+ // sprint-1566 F0: pre-flight token-budget validator. If the task's output
584
+ // is predicted to exceed the MiniMax truncation point (~4500 tokens),
585
+ // route to decomposer-feedback for a structural re-split BEFORE any LLM
586
+ // is dispatched. Stops the truncation cascade at the source.
587
+ const validation = (0, token_budget_validator_1.validateTask)(task);
588
+ if (!validation.ok) {
589
+ // Discriminated-union narrow via Extract — boolean discriminator alone
590
+ // isn't reliably narrowing under our tsconfig + alias-imported types.
591
+ const rej = validation;
592
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [TokenBudget] PRE-FLIGHT REJECT: ${task.id} — est ${rej.estimated_tokens} tokens > threshold`);
593
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Reason: ${rej.reason}`);
594
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Suggested split: ${rej.suggested_split.join(', ')}`);
595
+ const route = (0, decomposer_feedback_1.routeToDecomposer)({
596
+ original_task_id: task.id,
597
+ rejection_signal: 'needs_resplit',
598
+ original_estimate_tokens: rej.estimated_tokens,
599
+ suggested_splits: rej.suggested_split,
600
+ learnings_ref: 'docs/learnings.md §1',
601
+ original_task: task,
602
+ });
603
+ if ('task_split' in route) {
604
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` [DecomposerFeedback] split into ${route.task_split.length} sub-tasks via ${route.strategy}`);
605
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` ${route.rationale}`);
606
+ // Inject splits as new pending tasks so they get picked up next run.
607
+ // Persist to the active sprint file so sprint-runner sees them.
608
+ try {
609
+ this.injectSplitTasks(task, route.task_split, route.rationale);
610
+ }
611
+ catch (e) {
612
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Inject failed: ${e.message?.slice(0, 120)}`);
613
+ }
614
+ task.status = 'replaced-by-split';
615
+ task.rejected_reason = `Replaced by ${route.task_split.length} per-${route.strategy === 'per_file' ? 'file' : 'part'} sub-tasks (pre-flight budget gate)`;
616
+ this.persistTaskStatus(task);
617
+ taskRun.status = 'replaced-by-split';
618
+ taskRun.rejection_reason = `Token-budget pre-flight: ${rej.reason}`;
619
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
620
+ this.taskRuns.push(taskRun);
621
+ phantom_workspace_1.phantomWorkspace.cleanup(phantomCtx);
622
+ return;
623
+ }
624
+ else {
625
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` [DecomposerFeedback] cannot split → escalate to founder: ${route.reason}`);
626
+ task.status = 'rejected';
627
+ task.rejected_reason = route.reason;
628
+ this.persistTaskStatus(task);
629
+ taskRun.status = 'rejected';
630
+ taskRun.error = route.reason;
631
+ taskRun.rejection_reason = 'Token-budget pre-flight + unsplittable';
632
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
633
+ this.taskRuns.push(taskRun);
634
+ phantom_workspace_1.phantomWorkspace.cleanup(phantomCtx);
635
+ return;
636
+ }
637
+ }
638
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
639
+ // CTO-006 telemetry-blackout hotfix (2026-05-29, Slice A of TICKET-135):
640
+ // record attempt_count on the task so it propagates via persistTaskStatus
641
+ // → ACTIVE → .swarm-state → CTO's retrospective. Without this, every
642
+ // post-mortem said attempt_count=? after 3 attempts. 10 zero-ship
643
+ // sprints diagnosed by the CTO as "we can't root-cause because
644
+ // telemetry is dark."
645
+ task.attempt_count = attempt;
646
+ // Reset per-attempt flags so the escalation pact only fires when THIS
647
+ // attempt's execution actually tripped integrity/truncation, not stale
648
+ // state from a prior attempt.
649
+ delete task._integrityFailed;
650
+ // sprint-1566 F0c: per-task token budget check at start of each attempt
651
+ if (taskTokensSpent >= PER_TASK_TOKEN_BUDGET) {
652
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` [TokenBudget] PER-TASK BUDGET EXCEEDED: ${task.id} spent ${taskTokensSpent} > ${PER_TASK_TOKEN_BUDGET} tokens — aborting retries`);
653
+ task.status = 'rejected';
654
+ task.rejected_reason = `Per-task budget exceeded: ${taskTokensSpent} > ${PER_TASK_TOKEN_BUDGET} tokens after ${attempt - 1} attempts`;
655
+ taskRun.status = 'rejected';
656
+ taskRun.error = `Per-task token budget exceeded (${taskTokensSpent}/${PER_TASK_TOKEN_BUDGET})`;
657
+ taskRun.rejection_reason = 'Per-task budget exceeded';
658
+ // Route to decomposer for over-budget too — same as truncation
659
+ try {
660
+ const route = (0, decomposer_feedback_1.routeToDecomposer)({
661
+ original_task_id: task.id,
662
+ rejection_signal: 'over_budget',
663
+ original_estimate_tokens: taskTokensSpent,
664
+ suggested_splits: [],
665
+ learnings_ref: 'docs/learnings.md §1',
666
+ original_task: task,
667
+ });
668
+ if ('task_split' in route) {
669
+ this.injectSplitTasks(task, route.task_split, route.rationale);
670
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` [DecomposerFeedback] over-budget → ${route.task_split.length} sub-tasks injected for next run`);
671
+ }
672
+ else {
673
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [DecomposerFeedback] over-budget unsplittable: ${route.reason}`);
674
+ }
675
+ }
676
+ catch (e) {
677
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Inject failed: ${e.message?.slice(0, 120)}`);
678
+ }
679
+ break;
680
+ }
681
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, `\n${'='.repeat(60)}`);
682
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, `Task: ${task.id} | Agent: ${task.agent} | Attempt: ${attempt}/${MAX_RETRIES}`);
683
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, `${'='.repeat(60)}`);
684
+ // AMD-26 KSL: snapshot per-attempt context for the tap.
685
+ let kslSprintId = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
686
+ // sprint-runner passes logs/sprint-runner-active.json — read the real sprint_id from its contents.
687
+ if (kslSprintId === 'sprint-runner-active') {
688
+ try {
689
+ kslSprintId = JSON.parse((0, fs_1.readFileSync)(process.argv[2], 'utf-8')).sprint_id || kslSprintId;
690
+ }
691
+ catch { /* fall back to path-derived */ }
692
+ }
693
+ const kslAttemptStart = Date.now();
694
+ const kslPrompt = String(task.context || task.title || task.id);
695
+ task.status = 'in_progress';
696
+ if (attempt === 1) {
697
+ const _sprintId = kslSprintId;
698
+ (0, event_bus_publisher_1.publishTaskStarted)(task.agent, _sprintId, task.id, task.title || task.id).catch(() => { });
699
+ }
700
+ this.stats.tasksExecuted++;
701
+ taskRun.attempts = attempt;
702
+ // AMD-08: IDLE → RESERVED → ACTIVE (per attempt)
703
+ if (!monotask_state_machine_1.MonotaskSM.claim(task.agent, task.id)) {
704
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [monotask] ${task.agent} unavailable — skipping attempt ${attempt}`);
705
+ continue;
706
+ }
707
+ monotask_state_machine_1.MonotaskSM.start(task.agent, task.id);
708
+ // OMEL AMD-13: WipeWitness — capture file state before agent writes
709
+ const preTokens = new Map();
710
+ for (const f of (task.deliverables?.code || [])) {
711
+ if ((0, fs_1.existsSync)(f))
712
+ preTokens.set(f, wipe_witness_1.wipeWitness.beforeWrite(f, task.agent));
713
+ }
714
+ // OMEL AMD-13: HumanBrake — require approval for bulk_overwrite on high-risk files
715
+ if (task.type === 'modify' && (task.deliverables?.code || []).length > 0) {
716
+ const firstFile = (task.deliverables?.code || [])[0] || '';
717
+ if (human_brake_1.humanBrake.isHighRisk('bulk_overwrite', { filePath: firstFile })) {
718
+ const approval = await human_brake_1.humanBrake.requireApproval('bulk_overwrite');
719
+ if (!approval.approved) {
720
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [HumanBrake] SKIPPED: ${task.id} — ${approval.reason || 'not approved'}`);
721
+ task.status = 'skipped';
722
+ monotask_state_machine_1.MonotaskSM.release(task.agent, task.id, `human brake: ${approval.reason || 'not approved'}`);
723
+ break; // exit attempt loop — task will not execute
724
+ }
725
+ }
726
+ }
727
+ // Execute with rejection feedback if retrying
728
+ const tokensBefore = engine_primitives_1._globalTokensThisRun;
729
+ let result;
730
+ // Chomsky gate — evaluate + rewrite (max 2×) + log all evals (Sprint 1513)
731
+ if (task.context) {
732
+ try {
733
+ const chomskyRun = await (0, chomsky_runner_1.runChomskyGate)(task.context, agentName);
734
+ if (chomskyRun.rewrites > 0) {
735
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` [Chomsky] ${chomskyRun.rewrites}× rewrite — score ${chomskyRun.initialScore}→${chomskyRun.finalScore}/10`);
736
+ task.context = chomskyRun.finalPrompt;
737
+ }
738
+ else if (!chomskyRun.passed) {
739
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [Chomsky] score ${chomskyRun.finalScore}/10 — pass-through (max rewrites reached)`);
740
+ }
741
+ }
742
+ catch { /* gate is non-blocking — fail open */ }
743
+ }
744
+ try {
745
+ result = await agent.execute(task, lastReview);
746
+ }
747
+ catch (execErr) {
748
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` ✗ Execution error: ${execErr.message?.substring(0, 200)}`);
749
+ monotask_state_machine_1.MonotaskSM.release(task.agent, task.id, `exec error: ${execErr.message?.substring(0, 80)}`);
750
+ if (attempt < MAX_RETRIES) {
751
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Retrying after execution error (attempt ${attempt}/${MAX_RETRIES})...`);
752
+ continue;
753
+ }
754
+ throw execErr; // exhausted retries
755
+ }
756
+ // OMEL AMD-13: WipeWitness — compare after write, emit shrink alert if > 50% loss
757
+ for (const f of result.files) {
758
+ const tok = preTokens.get(f);
759
+ if (tok)
760
+ wipe_witness_1.wipeWitness.afterWrite(tok, (0, fs_1.existsSync)(f) ? (0, fs_1.statSync)(f).size : 0);
761
+ }
762
+ const deltaTokens = engine_primitives_1._globalTokensThisRun - tokensBefore;
763
+ taskRun.tokens_total += deltaTokens;
764
+ taskTokensSpent += deltaTokens; // sprint-1566 F0c: per-task budget tracking
765
+ taskRun.model_used = result.model || taskRun.model_used;
766
+ if (result.files.length === 0) {
767
+ // 2026-05-27 diagnostic patch: structured "no files produced" rejection.
768
+ // Captures (a) declared deliverable count, (b) model used, (c) failure
769
+ // reasons collected during execute() (e.g. edit-mode-empty:foo.ts:380lines,
770
+ // truncated, empty-edit-array). Replaces an opaque single-line log that
771
+ // gave the founder no idea why the swarm was no-oping.
772
+ const dels = [...(task.deliverables?.code || []), ...(task.deliverables?.tests || []), ...(task.deliverables?.docs || [])];
773
+ const reasons = task._failureReasons || [];
774
+ const inferred = reasons.length ? reasons.join('; ') : (dels.length === 0 ? 'empty-deliverables' : 'unknown');
775
+ const structured = `No files produced [model=${result.model || 'n/a'}, type=${task.type}, deliverables=${dels.length}, reason=${inferred}]`;
776
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` ✗ ${structured}`);
777
+ monotask_state_machine_1.MonotaskSM.release(task.agent, task.id, `no files: ${inferred.slice(0, 60)}`);
778
+ task.status = 'rejected';
779
+ this.persistTaskStatus(task);
780
+ taskRun.status = 'rejected';
781
+ taskRun.error = structured;
782
+ taskRun.rejection_reason = structured;
783
+ taskRun.failure_mode = inferred;
784
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
785
+ this.taskRuns.push(taskRun);
786
+ return;
787
+ }
788
+ // B.10: Local QA gate — fast PASS/FAIL before expensive cloud supervisor
789
+ const qaFileContents = result.files.map(f => ({ path: f, content: (0, fs_1.existsSync)(f) ? (0, fs_1.readFileSync)(f, 'utf-8') : '' }));
790
+ const qaResult = await (0, engine_primitives_1.localQAGate)(task, qaFileContents);
791
+ if (!qaResult.pass) {
792
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [QA-gate] FAIL — ${qaResult.reason}`);
793
+ this.stats.rejected++;
794
+ task.status = 'rejected'; // will be reset on retry
795
+ (0, engine_primitives_1.safeResetLastCommit)(task.id, task.agent, task.type, ' ');
796
+ (0, code_failure_logger_1.logCodeFailure)({ taskId: task.id, sprintId: (0, orchestrate_engine_1.resolveActiveSprintId)(), agentId: (0, orchestrate_engine_1.resolveAgentDid)(task.agent), attemptNum: attempt, score: 0, model: taskRun.model_used || result?.model || task.model || 'unknown', rejectionReason: qaResult.reason, issues: [], failType: 'qa_gate' });
797
+ monotask_state_machine_1.MonotaskSM.release(task.agent, task.id, `QA gate: ${qaResult.reason}`);
798
+ if (attempt < MAX_RETRIES) {
799
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ' QA gate failed — retrying without supervisor...');
800
+ continue;
801
+ }
802
+ taskRun.status = 'rejected';
803
+ taskRun.rejection_reason = `QA gate: ${qaResult.reason}`;
804
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
805
+ this.taskRuns.push(taskRun);
806
+ return;
807
+ }
808
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [QA-gate] PASS — ${qaResult.reason}`);
809
+ // Supervisor review. TICKET-234: fast track runs ONE supervisor (no dual
810
+ // review / reconcile / CEO escalation); regulated runs the dual-supervisor
811
+ // ceremony. The QA gate above already enforced the safety floor
812
+ // (typecheck/compile/no-secrets) on both paths.
813
+ task.status = 'review';
814
+ let review;
815
+ if (this.buildPath === 'fast') {
816
+ review = await this.supervisor.reviewTask(task, result.files);
817
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' [fast-track] single-supervisor review (dual-supervisor skipped)');
818
+ }
819
+ else {
820
+ const [review1, review2] = await Promise.all([
821
+ this.supervisor.reviewTask(task, result.files),
822
+ this.supervisor2.reviewTask(task, result.files),
823
+ ]);
824
+ const dualResult = await (0, engine_agents_1.reconcileSupervisorReviews)(review1, review2, task, this.ceo);
825
+ review = dualResult.finalReview;
826
+ if (!dualResult.consensus)
827
+ this.stats.conflicts++;
828
+ if (dualResult.escalatedToCEO)
829
+ this.stats.escalations++;
830
+ }
831
+ lastReview = review;
832
+ task.output = { files: result.files, commit: '', model: result.model, review };
833
+ // CTO-006 telemetry-blackout hotfix (2026-05-29, Slice A of TICKET-135):
834
+ // record score + grade + rejection_reason on the task so they propagate
835
+ // via persistTaskStatus. Without this, every post-mortem said "score=?".
836
+ task.score = review?.score;
837
+ task.grade = review?.grade;
838
+ task.rejection_reason = review?.verdict !== 'APPROVED' ? (review?.summary?.slice(0, 240) ?? null) : null;
839
+ if (review.verdict === 'APPROVED') {
840
+ task.status = 'done';
841
+ this.persistTaskStatus(task); // sprint-1547: persist before any post-approval work that could exit early
842
+ this.stats.approved++;
843
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\n✓ Task ${task.id} APPROVED on attempt ${attempt} (${review.score}/100)`);
844
+ const _sprintIdApproved = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
845
+ (0, event_bus_publisher_1.publishTaskCompleted)(task.agent, _sprintIdApproved, task.id, task.title || task.id, 0).catch(() => { });
846
+ aar_middleware_1.AARMiddleware.generateAndLog({ agentId: task.agent, taskId: task.id, sprintId: _sprintIdApproved, skillId: task.skill_id || task.type || 'code-generation', outcomeScore: review.score, actionSummary: (task.title || task.id).substring(0, 140), status: 'success' }).catch(() => { });
847
+ // AMD-20: PRM Judge — constitutional reward signal for approved tasks
848
+ (0, perm_judge_1.scoreTask)({ task_id: task.id, agent_id: task.agent, sprint_id: _sprintIdApproved, task_title: task.title || task.id, output_summary: review.summary || '', status: 'done' }).catch(() => { });
849
+ (0, trust_score_updater_1.updateTrustScore)(task.agent, 'approved', review.score); // Sprint 703: Dynamic trust update
850
+ // Sprint 706: BrainX — store success memory
851
+ try {
852
+ if (this._brainxBridge)
853
+ await this._brainxBridge.storeTaskMemory({ agent_id: task.agent, task_id: task.id, task_title: task.title || task.id, outcome: 'success', score: review.score, summary: (task.title || task.id).substring(0, 200), files_modified: result.files || [] });
854
+ }
855
+ catch { /* non-blocking */ }
856
+ (0, skill_crystalliser_1.crystalliseSkill)({ agentId: task.agent, taskId: task.id, sprintId: _sprintIdApproved, taskTitle: task.title || task.id, taskType: task.type || 'feature', model: task.model || 'qwen3:14b', taskTarget: task.task_target || 'local', score: review.score, approachSummary: (task.title || task.id).substring(0, 200), keyPatterns: review.strengths || [], antiPatterns: [] });
857
+ (0, code_asset_crystalliser_1.crystalliseCodeAsset)({ agentId: task.agent, sprintId: _sprintIdApproved, taskId: task.id, taskTitle: task.title || task.id, files: result.files, supervisorScore: review.score, origin: 'kognai-core' });
858
+ monotask_state_machine_1.MonotaskSM.complete(task.agent, task.id);
859
+ taskRun.status = 'done';
860
+ taskRun.files_written = result.files;
861
+ taskRun.review = {
862
+ verdict: review.verdict,
863
+ score: review.score,
864
+ grade: review.grade,
865
+ score_rationale: review.score_rationale,
866
+ strengths: review.strengths,
867
+ };
868
+ // SCORE protocol hook (founder rule 2026-05-27): every supervisor
869
+ // grade against a spawned-citizen agent feeds the citizen's reputation
870
+ // via the ACP rubric. Founding agents (CEO/sup/sherlock/etc.) aren't
871
+ // in the citizens registry yet so they're skipped here — backfill TBD.
872
+ try {
873
+ (0, engine_primitives_1.recordScoreForCitizen)(task.agent, _sprintIdApproved, task.id, review.grade, 'approved-path');
874
+ }
875
+ catch (e) {
876
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [SCORE] skip: ${(e?.message || '').slice(0, 100)}`);
877
+ }
878
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
879
+ this.taskRuns.push(taskRun);
880
+ (0, orchestrator_tap_1.tapAttempt)({
881
+ sprint_id: kslSprintId, task_id: task.id, attempt, agent: task.agent,
882
+ model: result.model || taskRun.model_used || 'unknown',
883
+ prompt: kslPrompt,
884
+ reply: `[approved score ${review.score}/100] files: ${result.files.join(', ')}\n\n${review.summary || ''}`,
885
+ tools_used: [], errors: [], cost_usd: 0,
886
+ duration_ms: Date.now() - kslAttemptStart,
887
+ });
888
+ return;
889
+ }
890
+ // Rejected — check for truncation pattern
891
+ this.stats.rejected++;
892
+ taskRun.review = {
893
+ verdict: review.verdict,
894
+ score: review.score,
895
+ grade: review.grade,
896
+ score_rationale: review.score_rationale,
897
+ issues: review.issues,
898
+ summary: review.summary,
899
+ };
900
+ // SCORE hook (rejected path) — record a negative-grade evaluation against
901
+ // the citizen so reputation actually moves on bad work.
902
+ try {
903
+ const _sid = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
904
+ (0, engine_primitives_1.recordScoreForCitizen)(task.agent, _sid, task.id, review.grade, 'rejected-path');
905
+ }
906
+ catch (e) {
907
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [SCORE] skip: ${(e?.message || '').slice(0, 100)}`);
908
+ }
909
+ if (this.isTruncationRejection(review)) {
910
+ truncationCount++;
911
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, `\n↻ Task ${task.id} REJECTED on attempt ${attempt} (${review.score}/100) [TRUNCATION ${truncationCount}/${TRUNCATION_THRESHOLD}]`);
912
+ // 2026-05-28 model-escalation pact: deterministic CTO/CEO sign-off via
913
+ // policy. If DeepSeek truncated this task once, the NEXT retry routes
914
+ // through ClawRouter → claude-sonnet-4.6. Cost ceiling ~$0.10 per
915
+ // escalation at typical token volumes — well under the daily wallet.
916
+ // Cleared in CodingAgent.execute after consumption.
917
+ task._escalateNext = 'TRUNCATION';
918
+ }
919
+ else if (task._integrityFailed) {
920
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, `\n↻ Task ${task.id} REJECTED on attempt ${attempt} (${review.score}/100) [INTEGRITY-FAILED]`);
921
+ // Same pact: destructive-rewrite (file shrank past integrity threshold)
922
+ // signals the cheap model can't hold the file's contract — upgrade.
923
+ task._escalateNext = 'INTEGRITY_FAILED';
924
+ }
925
+ else if ((review?.score ?? 100) < 30 &&
926
+ attempt < MAX_RETRIES &&
927
+ ((task.deliverables?.code || []).some((f) => /\.(md|mdx)$/i.test(f))
928
+ || ['research', 'spec', 'docs'].includes((task.type || '').toLowerCase()))) {
929
+ // 2026-05-28 pact expansion: low-score spec/docs rejections also escalate.
930
+ // sprint-1613 failure mode: DeepSeek hit a ~3-5k output ceiling on a
931
+ // 300-line spec request and gave up at one section, then the same
932
+ // capacity ceiling re-trapped 5 sub-task retries. Truncation/integrity
933
+ // checks didn't catch it (the file was complete, just not the SPEC).
934
+ // Catch low-score (<30) markdown/spec rejections explicitly so the
935
+ // next retry hits Sonnet instead of redo-on-the-same-cheap-model.
936
+ // Guarded by attempt < MAX_RETRIES so we don't waste the flag on the
937
+ // final attempt where there's no retry to consume it.
938
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, `\n↻ Task ${task.id} REJECTED on attempt ${attempt} (${review.score}/100) [LOW-SCORE-SPEC]`);
939
+ task._escalateNext = 'LOW_SCORE_SPEC';
940
+ }
941
+ else {
942
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, `\n↻ Task ${task.id} REJECTED on attempt ${attempt} (${review.score}/100)`);
943
+ }
944
+ (0, engine_primitives_1.safeResetLastCommit)(task.id, task.agent, task.type, ' ');
945
+ (0, code_failure_logger_1.logCodeFailure)({ taskId: task.id, sprintId: (0, orchestrate_engine_1.resolveActiveSprintId)(), agentId: (0, orchestrate_engine_1.resolveAgentDid)(task.agent), attemptNum: attempt, score: review?.score || 0, model: taskRun.model_used || result?.model || task.model || 'unknown', rejectionReason: review?.summary || 'supervisor rejected', issues: review?.issues || [], failType: 'supervisor_rejected' });
946
+ // Sprint 701: AAR logging on REJECTION path (governance remediation)
947
+ const _sprintIdRejected = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
948
+ aar_middleware_1.AARMiddleware.generateAndLog({ agentId: task.agent, taskId: task.id, sprintId: _sprintIdRejected, skillId: task.skill_id || task.type || 'code-generation', outcomeScore: review?.score || 0, actionSummary: `REJECTED: ${(task.title || task.id).substring(0, 120)} (attempt ${attempt})`, status: 'rejected' }).catch(() => { });
949
+ (0, trust_score_updater_1.updateTrustScore)(task.agent, 'rejected', review?.score || 0); // Sprint 703: Dynamic trust update
950
+ // Sprint 706: BrainX — store failure memory
951
+ try {
952
+ if (this._brainxBridge)
953
+ await this._brainxBridge.storeTaskMemory({ agent_id: task.agent, task_id: task.id, task_title: task.title || task.id, outcome: 'failure', score: review?.score || 0, summary: `REJECTED: ${(task.title || task.id).substring(0, 180)}`, files_modified: [] });
954
+ }
955
+ catch { /* non-blocking */ }
956
+ monotask_state_machine_1.MonotaskSM.release(task.agent, task.id, `rejected attempt ${attempt}`);
957
+ (0, orchestrator_tap_1.tapAttempt)({
958
+ sprint_id: kslSprintId, task_id: task.id, attempt, agent: task.agent,
959
+ model: taskRun.model_used || result?.model || task.model || 'unknown',
960
+ prompt: kslPrompt,
961
+ reply: `[rejected score ${review?.score || 0}/100] ${review?.summary || 'supervisor rejected'}`,
962
+ tools_used: [],
963
+ errors: [{ kind: 'supervisor_rejected', message: review?.summary || 'supervisor rejected' }],
964
+ cost_usd: 0,
965
+ duration_ms: Date.now() - kslAttemptStart,
966
+ });
967
+ // CTO AUTO-DECOMPOSE: After N consecutive truncation rejections, split the task
968
+ if (truncationCount >= TRUNCATION_THRESHOLD) {
969
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, `\n🔧 TRUNCATION THRESHOLD REACHED — CTO decomposing ${task.id}...`);
970
+ const subtasks = await this.ctoDecomposeTask(task);
971
+ if (subtasks.length > 1) {
972
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` Executing ${subtasks.length} sub-tasks sequentially...`);
973
+ let allPassed = true;
974
+ for (const subtask of subtasks) {
975
+ // Check sub-task dependencies
976
+ const subDeps = subtask.dependencies || [];
977
+ const unmetSubDeps = subDeps.filter(d => {
978
+ const depSt = subtasks.find(s => s.id === d);
979
+ return depSt && depSt.status !== 'done';
980
+ });
981
+ if (unmetSubDeps.length > 0) {
982
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Skipping sub-task ${subtask.id}: unmet deps [${unmetSubDeps.join(', ')}]`);
983
+ allPassed = false;
984
+ continue;
985
+ }
986
+ const passed = await this.executeSubTask(subtask, 5);
987
+ if (passed) {
988
+ subtask.status = 'done';
989
+ }
990
+ else {
991
+ allPassed = false;
992
+ subtask.status = 'rejected';
993
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Sub-task ${subtask.id} failed — stopping decomposition chain`);
994
+ break;
995
+ }
996
+ }
997
+ if (allPassed) {
998
+ task.status = 'done';
999
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\n✓ Task ${task.id} COMPLETED via CTO decomposition (${subtasks.length} sub-tasks)`);
1000
+ taskRun.status = 'done';
1001
+ taskRun.files_written = subtasks.flatMap(st => st.deliverables.code || []);
1002
+ }
1003
+ else {
1004
+ task.status = 'rejected';
1005
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, `\n✗ Task ${task.id} FAILED even after CTO decomposition`);
1006
+ taskRun.status = 'rejected';
1007
+ taskRun.rejection_reason = review.summary;
1008
+ }
1009
+ this.persistTaskStatus(task);
1010
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
1011
+ this.taskRuns.push(taskRun);
1012
+ return;
1013
+ }
1014
+ // If decomposition returned <=1 task, continue with normal retry loop
1015
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ' Decomposition produced ≤1 sub-task, continuing normal retries...');
1016
+ truncationCount = 0; // Reset to avoid re-triggering
1017
+ }
1018
+ if (attempt < MAX_RETRIES) {
1019
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Retrying with rejection feedback...`);
1020
+ }
1021
+ }
1022
+ task.status = 'rejected';
1023
+ this.persistTaskStatus(task);
1024
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, `\n✗ Task ${task.id} FAILED after ${MAX_RETRIES} attempts`);
1025
+ const _sprintIdFailed = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
1026
+ (0, event_bus_publisher_1.publishTaskFailed)(task.agent, _sprintIdFailed, task.id, task.title || task.id, lastReview?.summary || `Failed after ${MAX_RETRIES} attempts`).catch(() => { });
1027
+ taskRun.status = 'rejected';
1028
+ taskRun.rejection_reason = lastReview?.summary || `Failed after ${MAX_RETRIES} attempts`;
1029
+ taskRun.duration_seconds = Math.round((Date.now() - taskRunStart) / 1000);
1030
+ this.taskRuns.push(taskRun);
1031
+ }
1032
+ finally {
1033
+ // OMEL AMD-13: Always wipe the phantom tmpdir on task exit (success or failure)
1034
+ phantom_workspace_1.phantomWorkspace.cleanup(phantomCtx);
1035
+ }
1036
+ }
1037
+ async run() {
1038
+ const startTime = Date.now();
1039
+ const sprintStartTime = new Date().toISOString();
1040
+ let gitHeadBefore = 'unknown';
1041
+ try {
1042
+ gitHeadBefore = (0, child_process_1.execSync)('git rev-parse --short HEAD', { timeout: 5000 }).toString().trim();
1043
+ }
1044
+ catch { /* ok */ }
1045
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '\n🚀 Starting orchestration run...\n');
1046
+ if (engine_primitives_1.SOVEREIGN_MODE)
1047
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ' ⚡ SOVEREIGN MODE — all inference local ($0 cost floor)');
1048
+ (0, wallet_state_1.logWalletStatus)();
1049
+ // Mission Control — connect and register this sprint run
1050
+ const mc = (0, mc_client_1.createMCClient)('sprint-orchestrator', 'worker');
1051
+ let mcConnected = false;
1052
+ try {
1053
+ await mc.connect();
1054
+ mcConnected = true;
1055
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' [MC] Connected to Mission Control');
1056
+ }
1057
+ catch {
1058
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' [MC] Mission Control unavailable — running without telemetry');
1059
+ }
1060
+ // 1. Load tasks
1061
+ this.loadTasks();
1062
+ // 069-06: emit sprint started event
1063
+ const _evtSprintId = (process.argv[2] || 'sprints/current.json').replace(/.*\//, '').replace('.json', '');
1064
+ // Sprint 706: BrainX swarm bridge — create at sprint start
1065
+ let brainxBridge = null;
1066
+ try {
1067
+ const agentIds = Array.from(new Set(this.tasks.map(t => t.agent)));
1068
+ brainxBridge = (0, brainx_swarm_bridge_1.createSwarmBridge)(`swarm-${Date.now()}`, _evtSprintId, agentIds);
1069
+ this._brainxBridge = brainxBridge;
1070
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [BrainX] Bridge created for ${agentIds.length} agents`);
1071
+ }
1072
+ catch (e) {
1073
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [BrainX] Bridge creation skipped: ${e.message}`);
1074
+ }
1075
+ (0, event_bus_publisher_1.publishSprintStarted)(_evtSprintId, this.tasks.filter(t => t.status === 'pending').length).catch(() => { });
1076
+ if (this.tasks.length === 0) {
1077
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, 'No tasks to execute');
1078
+ if (mcConnected)
1079
+ await mc.disconnect().catch(() => { });
1080
+ return;
1081
+ }
1082
+ // ── BUILD TRIAGE — TICKET-234 (ceremony-depth routing) ─────────────────
1083
+ // Classify the build BEFORE the swarm ceremony: simple, non-sensitive,
1084
+ // single-file builds take the FAST path (single-supervisor + QA safety
1085
+ // floor, no CTO/CEO post-sprint ceremony, no templates); everything
1086
+ // sensitive/sovereign/complex/ambiguous takes the REGULATED full ceremony.
1087
+ // Pure heuristic — negligible cost vs the ceremony it gates. SOVEREIGN_MODE
1088
+ // always forces regulated: a sovereign build is regulated by definition.
1089
+ {
1090
+ const triageSprint = JSON.parse((0, fs_1.readFileSync)(process.argv[2] || 'sprints/current.json', 'utf-8'));
1091
+ this.triage = (0, build_triage_1.classifyBuildPath)(triageSprint, this.tasks);
1092
+ if (engine_primitives_1.SOVEREIGN_MODE && this.triage.path === 'fast') {
1093
+ this.triage = { ...this.triage, path: 'regulated', reason: 'sovereign mode → regulated by definition', triggers: [...this.triage.triggers, 'sovereign:mode'], sovereign: true };
1094
+ }
1095
+ this.buildPath = this.triage.path;
1096
+ (0, build_triage_1.logBuildTriage)(_evtSprintId, this.triage);
1097
+ const col = this.buildPath === 'fast' ? engine_primitives_1.c.green : engine_primitives_1.c.magenta;
1098
+ (0, engine_primitives_1.log)(col, `\n--- Build Triage: ${this.buildPath.toUpperCase()} ---`);
1099
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` ${this.triage.reason}`);
1100
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` complexity=${this.triage.complexity} tasks=${this.triage.taskCount} code-files=${this.triage.codeFileCount}${this.triage.triggers.length ? ` · triggers: ${this.triage.triggers.join(', ')}` : ''}`);
1101
+ if (this.buildPath === 'fast')
1102
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' Fast track: single-supervisor + QA safety floor; skipping dual-supervisor, CTO/CEO post-sprint ceremony, and templates.');
1103
+ }
1104
+ // ── CTO APPROVAL GATE — Exec Protocol §17 ──────────────────────────────
1105
+ // Every autonomous sprint must be approved by the CTO agent before execution.
1106
+ // Human-submitted sprints (source: 'human') are auto-approved.
1107
+ // Prevents the swarm from inventing its own work outside the execution plan.
1108
+ {
1109
+ const sprintFile = process.argv[2] || 'sprints/current.json';
1110
+ const sprintRaw = JSON.parse((0, fs_1.readFileSync)(sprintFile, 'utf-8'));
1111
+ const sprintSource = sprintRaw.source || (sprintRaw.swarm === 'NOT USED' ? 'human' : 'autonomous_loop');
1112
+ const proposal = {
1113
+ sprint_id: _evtSprintId,
1114
+ title: sprintRaw.name || sprintRaw.title || _evtSprintId,
1115
+ description: sprintRaw.goal || sprintRaw.description || '',
1116
+ tasks: this.tasks.map(t => `${t.id}: ${t.title || t.context || t.type}`),
1117
+ estimated_complexity: sprintRaw.estimated_complexity || 'medium',
1118
+ source: sprintSource,
1119
+ // Sprint 1457 BUGFIX: pass Rule 3 contract fields from sprint JSON to CTO gate
1120
+ inputs: sprintRaw.inputs,
1121
+ outputs: sprintRaw.outputs,
1122
+ success_criteria: sprintRaw.success_criteria,
1123
+ };
1124
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, `\n--- CTO Approval Gate ---`);
1125
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Sprint: ${proposal.sprint_id} — "${proposal.title}"`);
1126
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Source: ${proposal.source} (${proposal.tasks.length} tasks)`);
1127
+ const ctoResult = await (0, cto_approval_gate_1.requestCTOApproval)(proposal, process.cwd(), 'kognai');
1128
+ if (!ctoResult.approved) {
1129
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` ✘ CTO REJECTED: ${ctoResult.reason}`);
1130
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Plan reference: ${ctoResult.plan_reference}`);
1131
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Confidence: ${ctoResult.cto_confidence}%`);
1132
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Sprint ${_evtSprintId} will NOT execute. Saving rejection to sprint file.`);
1133
+ // Write rejection to sprint file so the loop doesn't retry
1134
+ try {
1135
+ sprintRaw.cto_gate = {
1136
+ approved: false,
1137
+ reason: ctoResult.reason,
1138
+ plan_reference: ctoResult.plan_reference,
1139
+ confidence: ctoResult.cto_confidence,
1140
+ timestamp: ctoResult.timestamp,
1141
+ };
1142
+ (0, fs_1.writeFileSync)(sprintFile, JSON.stringify(sprintRaw, null, 2));
1143
+ }
1144
+ catch { /* non-critical */ }
1145
+ if (mcConnected)
1146
+ await mc.disconnect().catch(() => { });
1147
+ return;
1148
+ }
1149
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` ✓ CTO APPROVED: ${ctoResult.reason}`);
1150
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Plan reference: ${ctoResult.plan_reference} (confidence: ${ctoResult.cto_confidence}%)`);
1151
+ }
1152
+ // ── End CTO Gate ────────────────────────────────────────────────────────
1153
+ // 2. CEO initial assessment (B.14: once per sprint, not once per conflict)
1154
+ // TICKET-234: regulated path only — the fast track skips the CEO ceremony.
1155
+ if (this.buildPath === 'regulated') {
1156
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, '\n--- Phase 1: CEO Initial Assessment ---');
1157
+ await this.ceo.reviewSprintProgress(this.tasks);
1158
+ }
1159
+ else {
1160
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, '\n--- Phase 1: CEO Initial Assessment — SKIPPED (fast track) ---');
1161
+ }
1162
+ const _ceoIntentDone = true; // flag for Phase 5: only re-run if ≥2 rejected
1163
+ // 3. Execute coding tasks with review loop
1164
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, '\n--- Phase 2: Sprint Execution ---');
1165
+ // Auto-cascade: if a task is rejected, immediately mark all downstream dependents as skipped
1166
+ // This prevents tasks from being stuck as 'pending' forever across retries
1167
+ let cascaded = true;
1168
+ while (cascaded) {
1169
+ cascaded = false;
1170
+ for (const task of this.tasks) {
1171
+ if (task.status !== 'pending')
1172
+ continue;
1173
+ const deps = task.dependencies || [];
1174
+ const blockedBy = deps.filter(d => {
1175
+ const depTask = this.tasks.find(t => t.id === d);
1176
+ return depTask && (depTask.status === 'rejected' || depTask.status === 'skipped');
1177
+ });
1178
+ if (blockedBy.length > 0) {
1179
+ task.status = 'skipped';
1180
+ task.skippedReason = `Blocked by: ${blockedBy.join(', ')}`;
1181
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Auto-skipped ${task.id}: blocked by rejected/skipped deps [${blockedBy.join(', ')}]`);
1182
+ cascaded = true;
1183
+ }
1184
+ }
1185
+ }
1186
+ // B.16: Wave-based parallel fan-out
1187
+ // Each wave = all tasks whose dependencies are satisfied and that don't share output files.
1188
+ // Serialized only when deliverable paths overlap (file conflict detection).
1189
+ const remaining = this.tasks.filter(t => t.status === 'pending');
1190
+ while (remaining.length > 0) {
1191
+ // Find tasks whose dependencies are all done/skipped
1192
+ const ready = remaining.filter(task => {
1193
+ const deps = task.dependencies || [];
1194
+ return deps.every(d => {
1195
+ const dep = this.tasks.find(t => t.id === d);
1196
+ return !dep || dep.status === 'done' || dep.status === 'skipped';
1197
+ });
1198
+ });
1199
+ if (ready.length === 0)
1200
+ break; // dependency deadlock — bail
1201
+ // File conflict detection: build wave without overlapping deliverables
1202
+ const filesInWave = new Set();
1203
+ const wave = [];
1204
+ for (const task of ready) {
1205
+ const taskFiles = [
1206
+ ...(task.deliverables?.code || []),
1207
+ ...(task.deliverables?.tests || []),
1208
+ ...(task.deliverables?.docs || []),
1209
+ ];
1210
+ const hasConflict = taskFiles.some(f => filesInWave.has(f));
1211
+ if (!hasConflict) {
1212
+ wave.push(task);
1213
+ taskFiles.forEach(f => filesInWave.add(f));
1214
+ }
1215
+ // conflicting tasks stay in remaining for next wave
1216
+ }
1217
+ if (wave.length === 0)
1218
+ wave.push(ready[0]); // break deadlock: force one task
1219
+ // B.16: Split wave by task_target — local tasks serialized (Ollama can only run one at a time),
1220
+ // cloud tasks run concurrently. Prevents Ollama queue timeout on parallel fan-out.
1221
+ const localWave = wave.filter(t => t.task_target === 'local');
1222
+ const cloudWave = wave.filter(t => t.task_target !== 'local');
1223
+ // B.16-RL: Cloud concurrency cap — prevents burning the Claude 5h token budget.
1224
+ // Default: 1 (serial). Override: MAX_CLOUD_CONCURRENCY env var.
1225
+ const MAX_CLOUD_CONCURRENCY = parseInt(process.env.MAX_CLOUD_CONCURRENCY ?? '1', 10);
1226
+ if (cloudWave.length > 1 && MAX_CLOUD_CONCURRENCY > 1) {
1227
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, ` [B.16] Parallel fan-out: ${cloudWave.length} cloud tasks (cap: ${MAX_CLOUD_CONCURRENCY})`);
1228
+ }
1229
+ else if (cloudWave.length > 1) {
1230
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, ` [B.16-RL] Serial cloud execution: ${cloudWave.length} tasks (MAX_CLOUD_CONCURRENCY=1)`);
1231
+ }
1232
+ if (localWave.length > 1) {
1233
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, ` [B.16] Sequential execution: ${localWave.length} local tasks (Ollama serialized)`);
1234
+ }
1235
+ else if (localWave.length === 1 && cloudWave.length === 0) {
1236
+ // single task, no label needed
1237
+ }
1238
+ // Execute cloud tasks in batches of MAX_CLOUD_CONCURRENCY
1239
+ for (let i = 0; i < cloudWave.length; i += MAX_CLOUD_CONCURRENCY) {
1240
+ const batch = cloudWave.slice(i, i + MAX_CLOUD_CONCURRENCY);
1241
+ await Promise.all(batch.map(t => this.executeTask(t)));
1242
+ }
1243
+ for (const t of localWave) {
1244
+ await this.executeTask(t);
1245
+ }
1246
+ // Remove executed tasks from remaining
1247
+ for (const t of wave) {
1248
+ const idx = remaining.indexOf(t);
1249
+ if (idx >= 0)
1250
+ remaining.splice(idx, 1);
1251
+ }
1252
+ // Cascade rejections
1253
+ cascaded = true;
1254
+ while (cascaded) {
1255
+ cascaded = false;
1256
+ for (const t of this.tasks) {
1257
+ if (t.status !== 'pending')
1258
+ continue;
1259
+ const tDeps = t.dependencies || [];
1260
+ const tBlocked = tDeps.filter(d => {
1261
+ const depTask = this.tasks.find(x => x.id === d);
1262
+ return depTask && (depTask.status === 'rejected' || depTask.status === 'skipped');
1263
+ });
1264
+ if (tBlocked.length > 0) {
1265
+ t.status = 'skipped';
1266
+ t.skippedReason = `Blocked by: ${tBlocked.join(', ')}`;
1267
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Auto-skipped ${t.id}: blocked by deps [${tBlocked.join(', ')}]`);
1268
+ cascaded = true;
1269
+ }
1270
+ }
1271
+ }
1272
+ // Remove newly-skipped/rejected from remaining
1273
+ for (let i = remaining.length - 1; i >= 0; i--) {
1274
+ if (remaining[i].status === 'skipped' || remaining[i].status === 'rejected') {
1275
+ remaining.splice(i, 1);
1276
+ }
1277
+ }
1278
+ }
1279
+ // 4. CTO data-driven analysis + CMO reports
1280
+ // TICKET-234: Phases 3–6 (CTO/CEO governance ceremony + agent minting +
1281
+ // daily report) run on the REGULATED path only. Fast track ships after the
1282
+ // per-task review + QA safety floor; the financial telemetry below (wallet
1283
+ // burn + budget freeze/warning) still runs on both paths.
1284
+ let ctoReport = { summary: '', proposals: [], metrics_reviewed: [] };
1285
+ let ctoDecisions = '';
1286
+ let cmoReports = '';
1287
+ if (this.buildPath === 'regulated') {
1288
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, '\n--- Phase 3: CTO Data-Driven Analysis + CMO Reports ---');
1289
+ try {
1290
+ ctoReport = await this.cto.analyze();
1291
+ // Load CMO reports (produced independently by Manus AI runner)
1292
+ cmoReports = (0, engine_loaders_1.loadCMOReports)();
1293
+ if (cmoReports) {
1294
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ' CMO reports found — will include in CEO review');
1295
+ }
1296
+ else {
1297
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' No CMO reports available yet');
1298
+ }
1299
+ // Load CTO tech-watch reports (produced independently by run-cto-techwatch.ts)
1300
+ const ctoTechWatch = (0, engine_loaders_1.loadCTOTechWatchReports)();
1301
+ if (ctoTechWatch) {
1302
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ' CTO tech-watch reports found — will include in CEO review');
1303
+ cmoReports = cmoReports ? cmoReports + '\n\n' + ctoTechWatch : ctoTechWatch;
1304
+ }
1305
+ else {
1306
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' No CTO tech-watch reports available yet');
1307
+ }
1308
+ // Load Grok intelligence feed (Grok AI monitors X/Twitter for OpenClaw news)
1309
+ const grokFeed = (0, engine_loaders_1.loadGrokFeed)();
1310
+ if (grokFeed) {
1311
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ' Grok intelligence feed found — will include in CEO review');
1312
+ cmoReports = cmoReports ? cmoReports + '\n\n' + grokFeed : grokFeed;
1313
+ }
1314
+ else {
1315
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' No Grok feed reports available');
1316
+ }
1317
+ // Load Owner Directives (highest priority — always included)
1318
+ const ownerDirectives = (0, engine_loaders_1.loadOwnerDirectives)();
1319
+ if (ownerDirectives) {
1320
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, " Owner directives found — will include in CEO review (highest priority)");
1321
+ cmoReports = ownerDirectives + (cmoReports ? "\n\n" + cmoReports : "");
1322
+ }
1323
+ // 5. CEO reviews CTO proposals + CMO reports
1324
+ if (ctoReport.proposals.length > 0 || cmoReports) {
1325
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, '\n--- Phase 4: CEO Reviews CTO Proposals + CMO Reports ---');
1326
+ ctoDecisions = await this.ceo.reviewCTOProposals(ctoReport);
1327
+ // Persist CEO decisions for CTO feedback loop + approved proposals tracking
1328
+ (0, orchestrate_engine_1.persistCEODecisions)(ctoDecisions, ctoReport);
1329
+ // Handle approved new_agent proposals
1330
+ const agentCreator = new engine_agents_1.AgentCreator(this.spawnGate);
1331
+ for (const proposal of ctoReport.proposals) {
1332
+ if (proposal.category === 'new_agent' && proposal.agent_spec) {
1333
+ // Check if CEO approved this specific proposal
1334
+ if (ctoDecisions.includes(proposal.id) && ctoDecisions.toUpperCase().includes('APPROVED')) {
1335
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\n 🤖 CEO approved new agent: ${proposal.agent_spec.name}`);
1336
+ agentCreator.createAgent(proposal.agent_spec);
1337
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` Agent will be loaded on next orchestrator run.`);
1338
+ }
1339
+ else {
1340
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` CEO did not approve agent: ${proposal.agent_spec.name}`);
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ else {
1346
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ' No proposals from CTO — stack is current and optimized');
1347
+ ctoDecisions = 'No proposals to review.';
1348
+ }
1349
+ }
1350
+ catch (error) {
1351
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` CTO/CEO review cycle skipped: ${error.message}`);
1352
+ ctoDecisions = 'CTO analysis was not performed this run.';
1353
+ }
1354
+ }
1355
+ else {
1356
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, '\n--- Phases 3–4: CTO/CEO Governance Ceremony — SKIPPED (fast track) ---');
1357
+ }
1358
+ // 6. CEO final assessment — B.14: only runs if ≥2 tasks rejected (skips if sprint went well)
1359
+ const rejectedCount = this.tasks.filter(t => t.status === 'rejected').length;
1360
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, '\n--- Phase 5: CEO Final Assessment ---');
1361
+ if (this.buildPath === 'fast') {
1362
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' Fast track — skipping CEO final assessment');
1363
+ }
1364
+ else if (rejectedCount >= 2) {
1365
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ` ${rejectedCount} tasks rejected — CEO reviewing...`);
1366
+ await this.ceo.reviewSprintProgress(this.tasks);
1367
+ }
1368
+ else {
1369
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Only ${rejectedCount} rejected — skipping CEO reassessment (sprint OK)`);
1370
+ }
1371
+ (0, wallet_state_1.logWalletStatus)(); // Print wallet burn after sprint execution
1372
+ // 069-06: emit budget events if thresholds crossed
1373
+ try {
1374
+ const _ws = (0, wallet_state_1.getWalletState)();
1375
+ if (_ws.burnPct >= 95)
1376
+ (0, event_bus_publisher_1.publishBudgetFreeze)(_evtSprintId, _ws.burnPct).catch(() => { });
1377
+ else if (_ws.burnPct >= 80)
1378
+ (0, event_bus_publisher_1.publishBudgetWarning)(_evtSprintId, _ws.burnPct, _ws.spentThisMonth, _ws.monthlyBudget).catch(() => { });
1379
+ }
1380
+ catch { /* wallet state unavailable */ }
1381
+ // 6b. CTO autonomous post-sprint analysis — regulated path only (TICKET-234)
1382
+ let postSprintReport = '';
1383
+ if (this.buildPath === 'regulated') {
1384
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, '\n--- Phase 5b: CTO Post-Sprint Analysis (Autonomous) ---');
1385
+ try {
1386
+ postSprintReport = await this.cto.postSprintAnalysis(this.tasks, this.stats);
1387
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ' Post-sprint analysis saved to reports/cto/');
1388
+ // Extract proposals from post-sprint analysis and feed into CEO review
1389
+ const jsonMatch = postSprintReport.match(/```json\s*([\s\S]*?)```/);
1390
+ if (jsonMatch) {
1391
+ try {
1392
+ const parsed = JSON.parse(jsonMatch[1].trim());
1393
+ const postSprintProposals = parsed.proposals || [];
1394
+ if (postSprintProposals.length > 0) {
1395
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` Found ${postSprintProposals.length} proposals — sending to CEO for autonomous review`);
1396
+ // Build CTOReport for CEO review
1397
+ const postSprintCTOReport = {
1398
+ summary: parsed.summary || 'Post-sprint analysis proposals',
1399
+ proposals: postSprintProposals.map((p) => ({
1400
+ id: p.id,
1401
+ title: p.title,
1402
+ category: p.category,
1403
+ description: p.description,
1404
+ estimated_impact: p.estimated_impact || '',
1405
+ risk_level: p.risk_level || 'medium',
1406
+ implementation_steps: p.implementation_steps || [],
1407
+ })),
1408
+ metrics_reviewed: ['sprint_results', 'failure_patterns', 'trend_analysis'],
1409
+ };
1410
+ // Phase 5c: CEO autonomously reviews post-sprint proposals
1411
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, '\n--- Phase 5c: CEO Reviews Post-Sprint Proposals (Autonomous) ---');
1412
+ const postSprintDecisions = await this.ceo.reviewCTOProposals(postSprintCTOReport);
1413
+ // Persist CEO decisions + update approved-proposals tracker
1414
+ (0, orchestrate_engine_1.persistCEODecisions)(postSprintDecisions, postSprintCTOReport);
1415
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ' CEO post-sprint proposal review complete');
1416
+ }
1417
+ else {
1418
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ' No proposals in post-sprint analysis');
1419
+ }
1420
+ }
1421
+ catch (parseErr) {
1422
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Could not parse post-sprint proposals JSON: ${parseErr.message}`);
1423
+ }
1424
+ }
1425
+ }
1426
+ catch (error) {
1427
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Post-sprint analysis skipped: ${error.message}`);
1428
+ }
1429
+ // 7. CEO generates daily report
1430
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, '\n--- Phase 6: Daily Report Generation ---');
1431
+ const ctoReportStr = `Summary: ${ctoReport.summary}\nProposals: ${ctoReport.proposals.length}\n${ctoReport.proposals.map(p => `- [${p.category}] ${p.title} (${p.risk_level})`).join('\n')}`;
1432
+ const grokSection = (0, engine_loaders_1.loadGrokFeed)();
1433
+ const cmoSection = cmoReports
1434
+ ? '\n\n## CMO Activity (Manus AI)\n' + cmoReports.substring(0, 2000)
1435
+ : '\n\nCMO: No reports available this cycle.';
1436
+ const grokForReport = grokSection ? '\n\n## Grok Intelligence Feed\nGrok AI reports available — included in CTO/CEO analysis.' : '\n\nGrok: No feed reports this cycle.';
1437
+ const postSprintSection = postSprintReport ? '\n\n## CTO Post-Sprint Analysis\n' + postSprintReport.substring(0, 2000) : '';
1438
+ await this.ceo.generateDailyReport(this.tasks, this.stats, ctoReportStr + cmoSection + grokForReport + postSprintSection, ctoDecisions);
1439
+ }
1440
+ else {
1441
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, '\n--- Phases 5b–6: Post-Sprint Ceremony + Daily Report — SKIPPED (fast track) ---');
1442
+ }
1443
+ // 8. Save updated sprint state
1444
+ const sprintFile = process.argv[2] || 'sprints/current.json';
1445
+ (0, fs_1.writeFileSync)(sprintFile, JSON.stringify({ tasks: this.tasks }, null, 2));
1446
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\nSprint state saved to ${sprintFile}`);
1447
+ // Sprint 706: BrainX — close bridge at sprint end
1448
+ try {
1449
+ if (brainxBridge)
1450
+ await brainxBridge.close();
1451
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ' [BrainX] Bridge closed');
1452
+ }
1453
+ catch { /* non-blocking */ }
1454
+ // 069-06: emit sprint completed event
1455
+ const completedCount = this.tasks.filter(t => t.status === 'done').length;
1456
+ (0, event_bus_publisher_1.publishSprintCompleted)(_evtSprintId, this.tasks.length, completedCount).catch(() => { });
1457
+ // 8b. Sync global token count into stats
1458
+ this.stats.totalTokens = engine_primitives_1._globalTokensThisRun;
1459
+ // 8c. Generate structured swarm run report
1460
+ try {
1461
+ (0, fs_1.mkdirSync)('reports/swarm-runs', { recursive: true });
1462
+ (0, fs_1.mkdirSync)('logs/swarm-runs', { recursive: true });
1463
+ let gitHeadAfter = 'unknown';
1464
+ let gitBranch = 'unknown';
1465
+ try {
1466
+ gitHeadAfter = (0, child_process_1.execSync)('git rev-parse --short HEAD', { timeout: 5000 }).toString().trim();
1467
+ gitBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { timeout: 5000 }).toString().trim();
1468
+ }
1469
+ catch { /* ok */ }
1470
+ // sprint-1566 F0e + F3: read from the real per-call aggregator (filled
1471
+ // by recordModelCall after every callLLM) instead of the prior pattern
1472
+ // that read taskRun.model_used (always 'unknown') and used a stale 5-row
1473
+ // pricing dict. modelUsage now has provider/calls/input/output/tokens/
1474
+ // cost_usd per model with real per-call cost from llm-cost-table.
1475
+ const modelUsage = (0, engine_primitives_1.getModelsUsedReport)();
1476
+ const totalCostUsd = (0, engine_primitives_1.getTotalCostUsd)();
1477
+ const runReport = {
1478
+ schema_version: '2.0.0', // CTO-20260528-001: bumped when capturing grade + score_rationale per-task
1479
+ run_id: (0, crypto_1.randomUUID)(),
1480
+ project: 'kognai',
1481
+ sprint_file: sprintFile,
1482
+ started_at: sprintStartTime,
1483
+ finished_at: new Date().toISOString(),
1484
+ duration_seconds: Math.round((Date.now() - startTime) / 1000),
1485
+ git_branch: gitBranch,
1486
+ git_head_before: gitHeadBefore,
1487
+ git_head_after: gitHeadAfter,
1488
+ sovereign_mode: engine_primitives_1.SOVEREIGN_MODE,
1489
+ build_path: this.buildPath, // TICKET-234: 'regulated' | 'fast'
1490
+ triage: this.triage, // TICKET-234: classifier reason + triggers
1491
+ summary: {
1492
+ total_tasks: this.tasks.length,
1493
+ done: this.tasks.filter(t => t.status === 'done').length,
1494
+ rejected: this.tasks.filter(t => t.status === 'rejected').length,
1495
+ skipped: this.tasks.filter(t => t.status === 'skipped').length,
1496
+ approval_rate: +(this.stats.approved / Math.max(this.stats.tasksExecuted, 1)).toFixed(2),
1497
+ total_tokens: this.stats.totalTokens,
1498
+ supervisor_conflicts: this.stats.conflicts,
1499
+ ceo_escalations: this.stats.escalations,
1500
+ },
1501
+ models_used: modelUsage,
1502
+ total_cost_usd: +totalCostUsd.toFixed(4),
1503
+ tasks: this.taskRuns,
1504
+ };
1505
+ // 1. Timestamped individual report (never overwritten)
1506
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1507
+ const reportPath = `reports/swarm-runs/${ts}.json`;
1508
+ (0, fs_1.writeFileSync)(reportPath, JSON.stringify(runReport, null, 2));
1509
+ // 2. Latest pointer (for quick dashboard access)
1510
+ (0, fs_1.writeFileSync)('reports/swarm-runs/latest-run.json', JSON.stringify(runReport, null, 2));
1511
+ // 3. Daily aggregate (accumulates ALL runs for the day)
1512
+ const today = new Date().toISOString().slice(0, 10);
1513
+ const dailyPath = `reports/swarm-runs/daily-${today}.json`;
1514
+ let dailyRuns = [];
1515
+ try {
1516
+ dailyRuns = JSON.parse((0, fs_1.readFileSync)(dailyPath, 'utf-8'));
1517
+ }
1518
+ catch { /* first run today */ }
1519
+ dailyRuns.push(runReport);
1520
+ (0, fs_1.writeFileSync)(dailyPath, JSON.stringify(dailyRuns, null, 2));
1521
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, `\n📊 Swarm run report: ${reportPath}`);
1522
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` Daily aggregate: ${dailyPath} (${dailyRuns.length} run(s) today)`);
1523
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` Tokens: ${this.stats.totalTokens.toLocaleString()} | Est. cost: $${totalCostUsd.toFixed(4)}`);
1524
+ // 8d. Daily cost digest — persist ClawRouter spend summary (§17.5)
1525
+ try {
1526
+ const digest = (0, engine_primitives_1.getDailyCostDigest)();
1527
+ const digestPath = `logs/clawrouter/digest-${today}.json`;
1528
+ (0, fs_1.writeFileSync)(digestPath, JSON.stringify(digest, null, 2));
1529
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` 💰 Cost digest: $${digest.total_usd.toFixed(4)} across ${digest.call_count} calls (saved ${digest.tokens_saved_by_qcg} tokens via QCG)`);
1530
+ }
1531
+ catch { /* non-critical */ }
1532
+ }
1533
+ catch (err) {
1534
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` [WARN] Swarm run report failed: ${err.message}`);
1535
+ }
1536
+ // 9. Post-sprint: PM2 reload backend + smoke test
1537
+ await (0, orchestrate_engine_1.postSprintSmokeTest)();
1538
+ // Final summary
1539
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1540
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '\n╔══════════════════════════════════════════════════════════╗');
1541
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '║ Orchestration Complete ║');
1542
+ (0, engine_primitives_1.log)(engine_primitives_1.c.bold, '╚══════════════════════════════════════════════════════════╝');
1543
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` Tasks executed: ${this.stats.tasksExecuted}`);
1544
+ (0, engine_primitives_1.log)(engine_primitives_1.c.green, ` Approved: ${this.stats.approved}`);
1545
+ (0, engine_primitives_1.log)(engine_primitives_1.c.red, ` Rejected: ${this.stats.rejected}`);
1546
+ (0, engine_primitives_1.log)(engine_primitives_1.c.yellow, ` Supervisor conflicts: ${this.stats.conflicts}`);
1547
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ` CEO escalations: ${this.stats.escalations}`);
1548
+ (0, engine_primitives_1.log)(engine_primitives_1.c.cyan, ` CTO proposals: ${ctoReport.proposals.length}`);
1549
+ (0, engine_primitives_1.log)(engine_primitives_1.c.magenta, ` CMO reports: ${cmoReports ? 'loaded' : 'none'}`);
1550
+ (0, engine_primitives_1.log)(engine_primitives_1.c.blue, ` Total time: ${elapsed}s`);
1551
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` Pipeline: CEO → MiniMax code → Dual review (DeepSeek + Haiku) → CEO resolves conflicts → CTO → CMO/Grok → Post-sprint analysis → Daily report`);
1552
+ // Mission Control — report final stats and disconnect
1553
+ if (mcConnected) {
1554
+ try {
1555
+ if (this.stats.totalTokens > 0) {
1556
+ await mc.reportTokens('MiniMax-M2.5', this.stats.totalTokens, 0, 'sprint_run');
1557
+ }
1558
+ await mc.disconnect();
1559
+ (0, engine_primitives_1.log)(engine_primitives_1.c.gray, ` [MC] Sprint reported: ${this.stats.approved} approved / ${this.stats.rejected} rejected / ${this.stats.totalTokens} tokens`);
1560
+ }
1561
+ catch { /* non-critical */ }
1562
+ }
1563
+ }
1564
+ }
1565
+ exports.Orchestrator = Orchestrator;
1566
+ // ===== Post-sprint smoke test =====
1567
+ // <<<EXTRACT-END-orchestrator