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