@kognai/build 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * @kognai/build — npx kognai-build "<task>"
5
+ *
6
+ * Submit one task to the Kognai sovereign orchestrator runtime:
7
+ * a coder generates the file, two reviewers grade code quality,
8
+ * a third reviewer checks constitutional + legal + security compliance.
9
+ * Triple consensus decides ship or reject.
10
+ *
11
+ * This package is the BUILDER-FACING distribution. It uses the
12
+ * canonical Kognai orchestrator presentation language defined in
13
+ * docs/specs/orchestrator-presentation.md (in the kognai monorepo).
14
+ *
15
+ * Requires: ANTHROPIC_API_KEY env var. Optionally CLAUDE_MODEL to override
16
+ * the default model (claude-sonnet-4-20250514).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ require("../lib/boot-env"); // MUST be first: sets KOGNAI_ROOT before the engine loads (KSL scoping)
20
+ const node_fs_1 = require("node:fs");
21
+ const node_path_1 = require("node:path");
22
+ const node_readline_1 = require("node:readline");
23
+ // TICKET-233 Phase 1: route ALL model calls through the published engine.
24
+ const orchestrator_core_1 = require("@kognai/orchestrator-core");
25
+ const router_1 = require("../lib/router");
26
+ const swarm_1 = require("../lib/swarm");
27
+ const KSL_RUN_ID = 'kb-' + Math.floor(Date.now() / 1000).toString(36);
28
+ // TICKET-203 + TICKET-207: workspace primitive — auto-discover .kognai/ in cwd.
29
+ // When found: deliverables write to workspace.deliverable_root (not /tmp),
30
+ // planner gets knowledge + memory context, session summary appends at end.
31
+ const workspace_1 = require("../lib/workspace");
32
+ const knowledge_1 = require("../lib/knowledge");
33
+ const memory_1 = require("../lib/memory");
34
+ // TICKET-202 PACT onboarding: a signed mandate is the autonomy envelope. Aliased
35
+ // imports avoid clashing with this file's local estimateCost()/PlannedTask.
36
+ const registry_1 = require("../templates/registry");
37
+ const cost_estimator_1 = require("../lib/cost-estimator");
38
+ const mandate_1 = require("../lib/mandate");
39
+ // TICKET-233: validate template capabilities against the real registries.
40
+ const skill_bank_1 = require("../lib/skill-bank");
41
+ const tool_layer_1 = require("../lib/tool-layer");
42
+ const workspace = (() => {
43
+ try {
44
+ return (0, workspace_1.discoverWorkspace)();
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ })();
50
+ // ─── Visual primitives (canonical presentation language v1.0) ────────────────
51
+ const C = {
52
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m', italic: '\x1b[3m',
53
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
54
+ magenta: '\x1b[35m', cyan: '\x1b[36m', gray: '\x1b[90m',
55
+ jade: '\x1b[38;5;79m',
56
+ };
57
+ const out = (s = '') => process.stdout.write(s + '\n');
58
+ const W = 72;
59
+ function wrap(text, width, indent) {
60
+ return text.split('\n').flatMap((para) => {
61
+ if (!para.trim())
62
+ return [''];
63
+ const words = para.split(/\s+/);
64
+ const lines = [];
65
+ let cur = '';
66
+ for (const w of words) {
67
+ if (cur.length + w.length + 1 > width) {
68
+ if (cur)
69
+ lines.push(cur);
70
+ cur = w;
71
+ }
72
+ else {
73
+ cur = cur ? `${cur} ${w}` : w;
74
+ }
75
+ }
76
+ if (cur)
77
+ lines.push(cur);
78
+ return lines;
79
+ }).map((l) => indent + l).join('\n');
80
+ }
81
+ const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
82
+ const ms = (n) => `${(n / 1000).toFixed(1)}s`;
83
+ // TICKET-135 optimization profile axes (10 standard from ISO/IEC 25010 + 1 Kognai-native).
84
+ // 'unspecified' = legacy default, treated as no-op (no profile guidance, supervisor reviews neutral).
85
+ const VALID_PROFILES = ['security', 'reliability', 'performance', 'maintainability', 'compatibility', 'usability', 'portability', 'compliance', 'cost-efficiency', 'time-to-market', 'sovereignty'];
86
+ function parseArgs() {
87
+ const args = process.argv.slice(2);
88
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
89
+ out(`
90
+ ${C.bold}${C.jade}kognai-build${C.reset} ${C.gray}· sovereign orchestrator${C.reset}
91
+
92
+ Two surfaces. ${C.bold}Task mode${C.reset} runs the triple-supervisor on ONE
93
+ deliverable. ${C.bold}Goal mode${C.reset} decomposes the goal into tasks (CEO planner),
94
+ then runs each through the triple-supervisor — TICKET-135 hierarchy
95
+ visible end-to-end.
96
+
97
+ ${C.bold}Task mode${C.reset}
98
+ kognai-build "<task>" [--out <path>] [--profile <axis>] [--no-compliance]
99
+ npx @kognai/build "Postgres backup rotation script"
100
+ npx @kognai/build "Stripe webhook handler" --out src/webhook.ts
101
+ npx @kognai/build "rate limiter" --profile security
102
+
103
+ ${C.bold}Goal mode${C.reset}
104
+ kognai-build --goal "<goal>" [--out-dir <dir>] [--profile <axis>] [--no-compliance]
105
+ npx @kognai/build --goal "TypeScript SDK for a TODO REST API"
106
+ npx @kognai/build --goal "Express CRUD service with Postgres" --out-dir ./svc
107
+ npx @kognai/build --goal "auth service" --profile security --out-dir ./auth
108
+
109
+ ${C.bold}Mandate mode${C.reset} ${C.gray}(TICKET-202 — session = signed PACT mandate)${C.reset}
110
+ kognai-build --mandate "<goal>" [--out-dir <dir>] [--profile <axis>]
111
+ Recommends a template, previews capabilities + a heuristic cost envelope,
112
+ signs a local PACT mandate, then executes the goal within that envelope.
113
+ The signed mandate persists to .swarm-state/mandates/<id>.json.
114
+
115
+ ${C.bold}Swarm mode${C.reset} ${C.gray}(TICKET-233 — full @kognai/orchestrator-core swarm)${C.reset}
116
+ kognai-build --swarm "<goal>" [--out-dir <dir>] [--sovereign]
117
+ Decomposes the goal, then delegates to the full orchestrator swarm:
118
+ CEO planning · CTO governance gate · dual-supervisor review · reconciliation.
119
+ Heavier than the default pipeline; --sovereign runs it $0-local end to end.
120
+
121
+ ${C.bold}Sovereign${C.reset} ${C.gray}(any mode)${C.reset}
122
+ --sovereign Route all inference to local Ollama ($0; no ANTHROPIC_API_KEY needed).
123
+
124
+ ${C.bold}Optimization profile${C.reset} (TICKET-135 axes — per the runtime doctrine)
125
+ security · reliability · performance · maintainability · compatibility
126
+ usability · portability · compliance · cost-efficiency · time-to-market · sovereignty
127
+ When set, the coder optimizes for the axis AND the supervisor lens matches it.
128
+ Default: unspecified (no profile guidance, neutral review).
129
+
130
+ ${C.bold}Pipeline${C.reset}
131
+ Task mode: generate → code review × 2 → compliance review → ship | reject
132
+ Goal mode: CEO planner (decompose) → per-task pipeline → goal verdict
133
+ (sequential tasks; later tasks see earlier deliverables in context)
134
+
135
+ ${C.bold}Auth${C.reset}
136
+ export ANTHROPIC_API_KEY=sk-ant-... (required)
137
+ export CLAUDE_MODEL=claude-sonnet-4-20250514 (optional override)
138
+
139
+ ${C.bold}Compliance${C.reset} is non-overridable. A code-grade-A deliverable with a
140
+ compliance FAIL is rejected. Use --no-compliance only for trusted tasks.
141
+ `);
142
+ process.exit(args.length === 0 ? 1 : 0);
143
+ }
144
+ const positional = args.find((a) => !a.startsWith('--')) || '';
145
+ const mandate = args.includes('--mandate');
146
+ // --swarm delegates the decomposed goal to the full orchestrator-core swarm.
147
+ const swarm = args.includes('--swarm');
148
+ // --mandate is goal-shaped: it decomposes, estimates a cost envelope, and signs
149
+ // a PACT mandate before executing. It therefore implies goal mode. --swarm too.
150
+ const isGoal = args.includes('--goal') || mandate || swarm;
151
+ const outIdx = args.indexOf('--out');
152
+ const outDirIdx = args.indexOf('--out-dir');
153
+ const profileIdx = args.indexOf('--profile');
154
+ const compliance = !args.includes('--no-compliance');
155
+ const sid = Math.floor(Date.now() / 1000).toString(36).slice(-4);
156
+ let profile;
157
+ if (profileIdx >= 0) {
158
+ const p = args[profileIdx + 1];
159
+ if (!p || !VALID_PROFILES.includes(p)) {
160
+ out(`\n ${C.red}error:${C.reset} --profile must be one of: ${VALID_PROFILES.join(', ')}\n`);
161
+ process.exit(2);
162
+ }
163
+ profile = p;
164
+ }
165
+ return {
166
+ mode: isGoal ? 'goal' : 'task',
167
+ input: positional,
168
+ outPath: outIdx >= 0 ? args[outIdx + 1] : `/tmp/kognai-build-${Date.now()}.ts`,
169
+ outDir: outDirIdx >= 0 ? args[outDirIdx + 1] : (workspace?.deliverable_root ?? `/tmp/kognai-goal-${sid}`),
170
+ compliance,
171
+ profile,
172
+ mandate,
173
+ sovereign: args.includes('--sovereign'),
174
+ swarm,
175
+ };
176
+ }
177
+ /** Profile guidance appended to coder + reviewer system prompts when profile is set.
178
+ * Per TICKET-135: 'two engineers given the same task produce structurally different
179
+ * outputs depending on what they silently optimize for.' Making it explicit prevents
180
+ * the silent-axis drift. */
181
+ function profileGuidance(profile, role) {
182
+ if (!profile)
183
+ return '';
184
+ const axisDesc = {
185
+ 'security': 'confidentiality, integrity, authorization, threat resistance — favor secure defaults, fail closed, audit trails',
186
+ 'reliability': 'availability, fault tolerance, recoverability — favor retries, circuit breakers, SLO-friendly error handling',
187
+ 'performance': 'latency, throughput, resource efficiency — favor minimal allocations, batched IO, profile-aware data structures',
188
+ 'maintainability': 'modularity, modifiability, testability, readability — favor clear naming, small units, explicit contracts',
189
+ 'compatibility': 'interoperability, coexistence with other systems — favor standard formats, version-tolerant interfaces',
190
+ 'usability': 'UX quality, accessibility, learnability — favor clear errors, sensible defaults, discoverable interfaces',
191
+ 'portability': 'adaptability, installability, replaceability — favor pure functions, dep injection, no platform-specific calls',
192
+ 'compliance': 'regulatory adherence (GDPR, HIPAA, jurisdictional) — favor consent gates, data minimization, audit-friendly output',
193
+ 'cost-efficiency': 'infrastructure cost, operational cost, TCO — favor frugal designs, cache hits, avoid heavyweight services',
194
+ 'time-to-market': 'delivery speed — favor pragmatic over perfect, well-known patterns, minimal viable shape',
195
+ 'sovereignty': 'self-hosted / local / BYO-vault deployment over hosted services — favor local-first, no vendor lock-in, portable data formats',
196
+ };
197
+ const desc = axisDesc[profile] || profile;
198
+ if (role === 'coder') {
199
+ return `\n\nOPTIMIZATION PROFILE: this task is optimized for ${profile} (${desc}). When tradeoffs arise, favor this axis. Do not sacrifice it for stylistic gain.`;
200
+ }
201
+ return `\n\nOPTIMIZATION PROFILE: this deliverable was decomposed under '${profile}' (${desc}). Grade it against THIS axis — do not penalize for not optimizing other axes. A 'speed-first' deliverable that is verbose but fast should rate well; a 'maintainability-first' deliverable that is slower but cleaner should also rate well. Match your lens to the declared profile.`;
202
+ }
203
+ // ─── Model gateway (TICKET-233 Phase 1) ──────────────────────────────────────
204
+ // Every model call routes through @kognai/orchestrator-core's registered
205
+ // ModelRouter (see makeBuildRouter). CODER/REVIEWER models are now hints mapped
206
+ // to engine complexity tiers; the router decides cloud (BYO key) vs local ($0).
207
+ const CODER_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-6';
208
+ const REVIEWER_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-6';
209
+ function modelToComplexity(model) {
210
+ if (/opus/i.test(model))
211
+ return 'apex';
212
+ if (/haiku/i.test(model))
213
+ return 'nano';
214
+ return 'power'; // sonnet / default
215
+ }
216
+ async function callClaude(systemPrompt, userPrompt, maxTokens, model) {
217
+ const t0 = Date.now();
218
+ const r = await (0, orchestrator_core_1.getModelRouter)().callLLM(userPrompt, {
219
+ systemPrompt,
220
+ complexity: modelToComplexity(model),
221
+ maxTokens,
222
+ agentId: 'kognai-build',
223
+ taskType: 'build',
224
+ });
225
+ return { text: r.content, model: r.model, cost: r.cost_usd, ms: Date.now() - t0 };
226
+ }
227
+ async function generate(task, outPath, profile) {
228
+ const sys = `You are a Kognai coder agent. Output ONLY the file content for the deliverable — no commentary, no fences, no preamble. Production-quality, all imports, error handling. Target language inferred from file extension: ${outPath.split('.').pop() || 'ts'}.${profileGuidance(profile, 'coder')}`;
229
+ const r = await callClaude(sys, `Task: ${task}\nDeliverable file: ${outPath}\nOutput the complete file content now:`, 4096, CODER_MODEL);
230
+ const stripped = r.text.replace(/^```[\w]*\n/, '').replace(/\n```\s*$/, '').trim();
231
+ return { content: stripped, model: r.model, cost: r.cost, ms: r.ms };
232
+ }
233
+ // ─── Robust JSON extraction ──────────────────────────────────────────────────
234
+ function safeParseJSON(raw) {
235
+ let s = raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
236
+ s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
237
+ const m = s.match(/\{[\s\S]*\}/);
238
+ if (!m)
239
+ return null;
240
+ let candidate = m[0];
241
+ try {
242
+ return JSON.parse(candidate);
243
+ }
244
+ catch { /* fall through */ }
245
+ candidate = candidate.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":');
246
+ candidate = candidate.replace(/,(\s*[}\]])/g, '$1');
247
+ try {
248
+ return JSON.parse(candidate);
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ async function reviewCode(label, task, code, profile) {
255
+ const sys = `You are a Kognai code-quality reviewer. Grade the deliverable A–F on: correctness vs the task, completeness (no TODOs/stubs), code quality, error handling.
256
+
257
+ CRITICAL OUTPUT RULES:
258
+ - Reply with VALID JSON only. No markdown fences, no <think> blocks, no commentary.
259
+ - Use double-quoted keys: {"grade":"A","summary":"..."}, NOT {grade:"A",summary:"..."}.
260
+ - Grade must be exactly one of: A, B, C, D, F.
261
+ - Summary should be a complete thought (1-3 sentences). No mid-sentence truncation.
262
+
263
+ Example: {"grade":"B","summary":"Correct implementation with proper types and error handling. Minor: redundant regex check after early-return, and no upper-bound length validation."}${profileGuidance(profile, 'reviewer')}`;
264
+ const r = await callClaude(sys, `Task: ${task}\n\n--- DELIVERABLE ---\n${code}\n--- END ---\n\nGrade now (JSON only):`, 512, REVIEWER_MODEL);
265
+ const parsed = safeParseJSON(r.text);
266
+ if (!parsed || !parsed.grade) {
267
+ return { label, grade: 'F', summary: `unparseable reply (raw: ${r.text.slice(0, 200).replace(/\n/g, ' ')}…)`, model: r.model, cost: r.cost, ms: r.ms };
268
+ }
269
+ const grade = ['A', 'B', 'C', 'D', 'F'].includes(parsed.grade) ? parsed.grade : 'F';
270
+ return { label, grade, summary: String(parsed.summary || '(no summary)'), model: r.model, cost: r.cost, ms: r.ms };
271
+ }
272
+ async function reviewCompliance(task, code) {
273
+ const sys = `You are the Kognai COMPLIANCE reviewer — the third review pass that distinguishes Kognai's orchestrator from a thin LLM wrapper. Check the deliverable against:
274
+
275
+ CONSTITUTIONAL (the 3 Laws, non-overridable):
276
+ I. Never Harm — no code that damages clients, agents, or counterparties.
277
+ II. Earn Existence — no fake/deceptive functionality, no fabricated metrics, no hidden surveillance.
278
+ III. Transparency to Creator — no obfuscation, no embedded credentials, no telemetry without disclosure.
279
+
280
+ LEGAL / REGULATORY:
281
+ - No GDPR-violating PII handling without consent gates.
282
+ - No tax-evasion shortcuts.
283
+ - No license-incompatible code lift from copyleft sources.
284
+
285
+ SECURITY (auto-fail):
286
+ - Hard-coded secrets, API keys, private keys, wallet seeds.
287
+ - SQL injection, command injection, path traversal vulnerabilities.
288
+ - Disabled TLS, certificate validation bypass.
289
+ - eval() / unrestricted dynamic execution on untrusted input.
290
+
291
+ CRITICAL OUTPUT RULES:
292
+ - Reply with VALID JSON only. No markdown fences, no <think> blocks, no commentary.
293
+ - Use double-quoted keys: {"pass":true,"reasons":["..."]}.
294
+ - "pass" must be a JSON boolean.
295
+ - "reasons" must be a JSON array of short, complete strings.
296
+
297
+ Example PASS: {"pass":true,"reasons":["No secrets or credentials","No injection vectors","No PII handling or external calls"]}
298
+ Example FAIL: {"pass":false,"reasons":["Hard-coded API key on line 12","Missing consent gate before PII fetch"]}
299
+
300
+ PASS means SHIPPABLE under all 3 categories. FAIL on ANY violation — the supervisor cannot override.`;
301
+ const r = await callClaude(sys, `Task: ${task}\n\n--- DELIVERABLE ---\n${code}\n--- END ---\n\nCompliance verdict (JSON only):`, 1024, REVIEWER_MODEL);
302
+ const parsed = safeParseJSON(r.text);
303
+ if (!parsed || typeof parsed.pass !== 'boolean') {
304
+ return { pass: false, reasons: [`Unparseable reply — failing safe. Raw: ${r.text.slice(0, 200).replace(/\n/g, ' ')}…`], model: r.model, cost: r.cost, ms: r.ms };
305
+ }
306
+ return { pass: parsed.pass, reasons: Array.isArray(parsed.reasons) ? parsed.reasons.map(String) : [], model: r.model, cost: r.cost, ms: r.ms };
307
+ }
308
+ function gradeRank(g) { return { A: 5, B: 4, C: 3, D: 2, F: 1 }[g] ?? 0; }
309
+ function gradeColor(g) { return gradeRank(g) >= 4 ? C.green : gradeRank(g) >= 3 ? C.yellow : C.red; }
310
+ // ─── Presentation (canonical design language v1.0) ───────────────────────────
311
+ function sessionId() { return Math.floor(Date.now() / 1000).toString(36).slice(-4); }
312
+ function banner(compliance) {
313
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
314
+ const sid = sessionId();
315
+ const mode = compliance ? 'triple-supervisor + compliance' : 'triple-supervisor (compliance OFF)';
316
+ const left = `${C.jade}●${C.reset} ${C.bold}KOGNAI RUNTIME${C.reset} ${C.gray}·${C.reset} ${C.jade}${C.bold}ENGAGED${C.reset}`;
317
+ const right = `${C.gray}${ts}${C.reset}`;
318
+ const sub = ` ${C.gray}builder console · ${mode} · session #${sid}${C.reset}`;
319
+ const innerPad = W - visLen(left) - visLen(right) - 4;
320
+ out('');
321
+ out(` ${C.jade}╔${'═'.repeat(W - 2)}╗${C.reset}`);
322
+ out(` ${C.jade}║${C.reset} ${left}${' '.repeat(Math.max(2, innerPad))}${right} ${C.jade}║${C.reset}`);
323
+ out(` ${C.jade}║${C.reset}${sub}${' '.repeat(Math.max(0, W - visLen(sub) - 2))}${C.jade}║${C.reset}`);
324
+ out(` ${C.jade}╚${'═'.repeat(W - 2)}╝${C.reset}`);
325
+ // TICKET-207: workspace line below banner
326
+ if (workspace) {
327
+ out(` ${C.jade}Workspace:${C.reset} ${workspace.kognai_dir}`);
328
+ }
329
+ else {
330
+ out(` ${C.gray}Workspace: (none — deliverables to /tmp; create .kognai/config.yaml to bind)${C.reset}`);
331
+ }
332
+ out('');
333
+ }
334
+ function header(task, compliance) {
335
+ banner(compliance);
336
+ out(` ${C.gray}╭─ Task ${'─'.repeat(W - 9)}╮${C.reset}`);
337
+ out(wrap(task, W - 4, ` ${C.gray}│${C.reset} `));
338
+ out(` ${C.gray}╰${'─'.repeat(W - 2)}╯${C.reset}`);
339
+ out('');
340
+ }
341
+ function goalHeader(goal, compliance) {
342
+ banner(compliance);
343
+ out(` ${C.gray}╭─ Goal ${'─'.repeat(W - 9)}╮${C.reset}`);
344
+ out(wrap(goal, W - 4, ` ${C.gray}│${C.reset} `));
345
+ out(` ${C.gray}╰${'─'.repeat(W - 2)}╯${C.reset}`);
346
+ out('');
347
+ }
348
+ function section(label) {
349
+ out('');
350
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}${label}${C.reset}`);
351
+ out('');
352
+ }
353
+ function generationSummary(g) {
354
+ const lines = g.content.split('\n').length;
355
+ out(` ${C.gray}Routed to:${C.reset} ${C.bold}${g.model}${C.reset} ${C.gray}(${g.model.startsWith('claude') ? 'cloud' : 'local · $0'})${C.reset}`);
356
+ out(` ${C.gray}Produced:${C.reset} ${g.content.length} chars · ${lines} lines · ${ms(g.ms)} · ${C.gray}$${g.cost.toFixed(4)}${C.reset}`);
357
+ }
358
+ function codeReviewBlock(r) {
359
+ const colour = gradeColor(r.grade);
360
+ out(` ${C.bold}▸ ${r.label}${C.reset}${' '.repeat(Math.max(2, 28 - r.label.length))}${C.gray}Grade:${C.reset} ${colour}${C.bold}${r.grade}${C.reset} ${C.gray}${r.model} · ${ms(r.ms)}${C.reset}`);
361
+ out(wrap(r.summary, W - 8, ` `));
362
+ out('');
363
+ }
364
+ function complianceReviewBlock(r) {
365
+ const colour = r.pass ? C.green : C.red;
366
+ const verdict = r.pass ? 'PASS' : 'FAIL';
367
+ out(` ${C.bold}▸ Compliance review${C.reset}${' '.repeat(28 - 'Compliance review'.length)}${C.gray}Result:${C.reset} ${colour}${C.bold}${verdict}${C.reset} ${C.gray}${r.model} · ${ms(r.ms)}${C.reset}`);
368
+ for (const reason of r.reasons) {
369
+ const mark = r.pass ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
370
+ out(` ${mark} ${reason}`);
371
+ }
372
+ out('');
373
+ }
374
+ function verdictBlock(sup1, sup2, sup3, ship) {
375
+ const codeOk = gradeRank(sup1.grade) >= 3 || gradeRank(sup2.grade) >= 3;
376
+ const verdict = ship ? `${C.green}${C.bold}SHIPPABLE${C.reset}` : `${C.red}${C.bold}REJECTED${C.reset}`;
377
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}Verdict${C.reset}: ${verdict}`);
378
+ out('');
379
+ out(` ${C.gray}Code quality:${C.reset} ${codeOk ? C.green + 'PASS' : C.red + 'FAIL'}${C.reset} ${C.gray}(${sup1.label} ${sup1.grade}, ${sup2.label} ${sup2.grade} — threshold C)${C.reset}`);
380
+ out(` ${C.gray}Compliance:${C.reset} ${sup3.pass ? C.green + 'PASS' : C.red + 'FAIL'}${C.reset} ${C.gray}(${sup3.reasons.length} ${sup3.reasons.length === 1 ? 'finding' : 'findings'})${C.reset}`);
381
+ }
382
+ function deliverableBlock(content, outPath, ship) {
383
+ const lines = content.split('\n');
384
+ if (ship) {
385
+ out(` ${C.gray}Path:${C.reset} ${C.bold}${outPath}${C.reset}`);
386
+ out(` ${C.gray}Size:${C.reset} ${content.length} chars · ${lines.length} lines`);
387
+ out('');
388
+ out(` ${C.gray}── preview ${'─'.repeat(W - 14)}${C.reset}`);
389
+ for (const l of lines.slice(0, 15))
390
+ out(` ${C.dim}${l}${C.reset}`);
391
+ if (lines.length > 15)
392
+ out(` ${C.gray}… (${lines.length - 15} more lines — open the file at the path above)${C.reset}`);
393
+ out(` ${C.gray}${'─'.repeat(W - 4)}${C.reset}`);
394
+ }
395
+ else {
396
+ out(` ${C.red}Deliverable was NOT written.${C.reset}`);
397
+ out(` ${C.gray}Compliance failures are non-overridable by the supervisor; fix the${C.reset}`);
398
+ out(` ${C.gray}cited issues and resubmit, or pass --no-compliance for trusted tasks.${C.reset}`);
399
+ }
400
+ }
401
+ function footer(elapsedMs, totalCost, callCount, outPath, ship) {
402
+ out('');
403
+ out(` ${C.gray}Total: ${ms(elapsedMs)} · ${callCount} model calls · $${totalCost.toFixed(4)}${C.reset}`);
404
+ if (ship) {
405
+ out('');
406
+ out(` ${C.gray}Next:${C.reset}`);
407
+ out(` ${C.gray}cat${C.reset} ${outPath}${C.gray} # see the full file${C.reset}`);
408
+ out(` ${C.gray}# or rerun with${C.reset} ${C.bold}--out ${C.reset}${C.gray}your/project/file.ts to write into your repo${C.reset}`);
409
+ }
410
+ out('');
411
+ }
412
+ async function decompose(goal, outDir) {
413
+ // TICKET-207: workspace knowledge + memory prepended to planner system prompt
414
+ let prefix = '';
415
+ if (workspace) {
416
+ try {
417
+ const k = (0, knowledge_1.loadKnowledge)(workspace);
418
+ const m = (0, memory_1.loadMemory)(workspace);
419
+ prefix = (0, knowledge_1.renderKnowledgePrefix)(k) + (0, memory_1.renderMemoryPrefix)(m);
420
+ if (k.files_read.length || m.files_read.length) {
421
+ out(` ${C.gray}Workspace context: ${k.files_read.length} knowledge + ${m.files_read.length} memory file(s) loaded${C.reset}`);
422
+ }
423
+ }
424
+ catch (e) {
425
+ out(` ${C.gray}Workspace context: skipped (${(e?.message || String(e)).slice(0, 80)})${C.reset}`);
426
+ }
427
+ }
428
+ const sys = `${prefix}You are the Kognai CEO planner. You decompose a high-level builder goal into 1-5 atomic tasks that a coder agent can execute one-by-one. Each task ships ONE file.
429
+
430
+ CRITICAL OUTPUT RULES:
431
+ - Reply with VALID JSON only. No fences, no commentary.
432
+ - Use double-quoted keys.
433
+ - Shape: {"rationale":"<1-2 sentences why this decomposition>","tasks":[{"file":"<relative path>","description":"<concrete task>"}, ...]}
434
+ - Max 5 tasks. Order matters: later tasks may depend on earlier ones (e.g. types.ts before client.ts).
435
+ - File paths are relative; the caller writes them into a base directory.
436
+ - Each description is concrete enough that a coder agent can satisfy it without re-asking what the goal was.
437
+
438
+ Example for goal "TypeScript SDK for TODO REST API":
439
+ {"rationale":"Standard SDK shape: types first, then a client that consumes them, then a public entry point.","tasks":[{"file":"types.ts","description":"Define TypeScript types for Todo (id, title, done) and a TodoInput type for create operations"},{"file":"client.ts","description":"Implement TodoClient class with list/get/create/update/delete methods using fetch, backed by types from ./types"},{"file":"index.ts","description":"Public entry point re-exporting TodoClient and types"}]}`;
440
+ const r = await callClaude(sys, `Goal: ${goal}\nBase directory: ${outDir}\n\nDecompose now (JSON only):`, 2048, REVIEWER_MODEL);
441
+ const parsed = safeParseJSON(r.text);
442
+ if (!parsed || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
443
+ throw new Error(`planner returned unparseable plan. Raw: ${r.text.slice(0, 300)}`);
444
+ }
445
+ const tasks = parsed.tasks.slice(0, 5).map((t) => ({
446
+ file: String(t.file || 'untitled.ts'),
447
+ description: String(t.description || '(no description)'),
448
+ }));
449
+ return { tasks, rationale: String(parsed.rationale || ''), model: r.model, cost: r.cost, ms: r.ms };
450
+ }
451
+ function planningBlock(plan, outDir) {
452
+ out(` ${C.gray}Routed to:${C.reset} ${C.bold}${plan.model}${C.reset} ${C.gray}(planner)${C.reset}`);
453
+ out(` ${C.gray}Decomposed:${C.reset} ${plan.tasks.length} task${plan.tasks.length === 1 ? '' : 's'} · ${ms(plan.ms)} · ${C.gray}$${plan.cost.toFixed(4)}${C.reset}`);
454
+ out('');
455
+ if (plan.rationale) {
456
+ out(wrap(`${C.gray}Rationale:${C.reset} ${plan.rationale}`, W - 6, ' '));
457
+ out('');
458
+ }
459
+ out(` ${C.gray}Output dir:${C.reset} ${C.bold}${outDir}${C.reset}`);
460
+ out('');
461
+ for (let i = 0; i < plan.tasks.length; i++) {
462
+ const t = plan.tasks[i];
463
+ const num = `${i + 1}.`;
464
+ out(` ${C.bold}${C.jade}${num}${C.reset} ${C.bold}${t.file}${C.reset}`);
465
+ out(wrap(t.description, W - 9, ` ${C.gray}`).replace(/\x1b\[0m/g, C.reset) + C.reset);
466
+ out('');
467
+ }
468
+ }
469
+ function taskHeader(idx, total, file) {
470
+ out('');
471
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}Task ${idx}/${total}${C.reset} ${C.gray}—${C.reset} ${C.bold}${file}${C.reset}`);
472
+ out('');
473
+ }
474
+ function goalVerdictBlock(shipped, total, outDir, deliverables) {
475
+ const allShipped = shipped === total;
476
+ const verdict = allShipped
477
+ ? `${C.green}${C.bold}GOAL ACHIEVED${C.reset}`
478
+ : shipped > 0
479
+ ? `${C.yellow}${C.bold}PARTIAL (${shipped}/${total})${C.reset}`
480
+ : `${C.red}${C.bold}GOAL FAILED${C.reset}`;
481
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}Goal verdict${C.reset}: ${verdict}`);
482
+ out('');
483
+ out(` ${C.gray}Tasks shipped:${C.reset} ${shipped} / ${total}`);
484
+ out(` ${C.gray}Output dir:${C.reset} ${C.bold}${outDir}${C.reset}`);
485
+ if (deliverables.length > 0) {
486
+ out('');
487
+ out(` ${C.gray}Deliverables:${C.reset}`);
488
+ for (const d of deliverables)
489
+ out(` ${C.green}✓${C.reset} ${d}`);
490
+ }
491
+ }
492
+ // ─── Mandate flow (TICKET-202 PACT onboarding) ───────────────────────────────
493
+ // Between decomposition and execution: pick a template, preview capabilities,
494
+ // estimate a heuristic cost envelope, and sign a local PACT mandate. The signed
495
+ // mandate IS the autonomy envelope the swarm then runs within. Phase-1 core:
496
+ // non-interactive (signs on the founder's behalf); the interactive sign/cancel/edit
497
+ // prompt and sprint-runner envelope enforcement are the next slice.
498
+ function slugifyFile(file) {
499
+ return file.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'task';
500
+ }
501
+ // Build (but do NOT sign) the recommended mandate from the plan. Splitting build
502
+ // from sign lets us preview it and let the founder confirm before it's signed.
503
+ function prepareMandate(goal, plan, outDir, profile) {
504
+ // Template selection from the registry (confirmation-required posture per
505
+ // TICKET-140: take the top-ranked candidate). selectTemplates always returns a
506
+ // candidate when no profile filter excludes everything; fall back defensively.
507
+ const candidates = (0, registry_1.selectTemplates)(goal, undefined, 1);
508
+ const template = candidates[0]?.template ?? (0, registry_1.getTemplateById)('webapp-standard-default');
509
+ if (!template)
510
+ throw new Error('no template available for mandate selection');
511
+ // Bridge the goal-mode plan ({file, description}) to the cost-estimator's
512
+ // PlannedTask ({id, title, task_target}). Phase 1 routes every task cloud-code.
513
+ const costTasks = plan.tasks.map((t, i) => ({
514
+ id: `${i + 1}-${slugifyFile(t.file)}`,
515
+ title: t.description,
516
+ task_target: 'cloud-code',
517
+ }));
518
+ const envelope = (0, cost_estimator_1.estimateCost)(costTasks, template);
519
+ const profileObj = {
520
+ primary: profile || template.default_profile.primary,
521
+ secondary: template.default_profile.secondary,
522
+ hard_constraints: [],
523
+ };
524
+ const capabilities = {
525
+ skills: template.skills,
526
+ tools: template.tools,
527
+ agents: template.composition.roles.map((r) => r.role),
528
+ };
529
+ // Validate the template's declared capabilities against the skill-bank +
530
+ // tool-layer registries — surfaces drift between templates and the registries.
531
+ const missingSkills = (0, skill_bank_1.unknownSkillIds)(capabilities.skills);
532
+ const missingTools = (0, tool_layer_1.unknownToolIds)(capabilities.tools);
533
+ if (missingSkills.length)
534
+ out(` ${C.yellow}⚠ unknown skills (not in skill-bank):${C.reset} ${missingSkills.join(', ')}`);
535
+ if (missingTools.length)
536
+ out(` ${C.yellow}⚠ unknown tools (not in tool-layer):${C.reset} ${missingTools.join(', ')}`);
537
+ const success_criteria = [
538
+ `Ship all ${plan.tasks.length} planned deliverable(s) under triple-supervisor review`,
539
+ ...plan.tasks.slice(0, 8).map((t) => `${t.file}: ${t.description}`),
540
+ ];
541
+ const scope_boundaries = [
542
+ `Deliverables limited to ${outDir}`,
543
+ `Template ${template.id} · ${profileObj.primary} profile`,
544
+ ];
545
+ const unsigned = (0, mandate_1.buildMandate)({
546
+ objective: goal,
547
+ success_criteria,
548
+ scope_boundaries,
549
+ template_id: template.id,
550
+ profile: profileObj,
551
+ capabilities,
552
+ cost_envelope: envelope,
553
+ deadline: null,
554
+ });
555
+ return { unsigned, template };
556
+ }
557
+ function mandatePreviewBlock(u, template) {
558
+ section('Recommended Mandate');
559
+ const row = (k, v) => out(` ${C.gray}${k}${C.reset}${' '.repeat(Math.max(2, 14 - k.length))}${v}`);
560
+ row('Template', `${C.bold}${template.name}${C.reset} ${C.gray}(${template.id})${C.reset}`);
561
+ row('Intent', template.intent.slice(0, 3).join(' / '));
562
+ row('Profile', `${C.bold}${u.profile.primary}${C.reset}${u.profile.secondary.length ? `${C.gray} + ${u.profile.secondary.join(' × ')}${C.reset}` : ''}`);
563
+ row('Skills', u.capabilities.skills.join(', ') || '—');
564
+ row('Tools', u.capabilities.tools.join(', ') || '—');
565
+ row('Agents', u.capabilities.agents.join(', ') || '—');
566
+ row('Cost', `~$${u.cost_envelope.estimated_usdc.toFixed(4)} USDC ${C.gray}(hard cap $${u.cost_envelope.max_usdc.toFixed(4)})${C.reset}`);
567
+ row('Deadline', u.deadline ?? 'none');
568
+ out('');
569
+ for (const sb of u.scope_boundaries)
570
+ out(` ${C.gray}Scope:${C.reset} ${sb}`);
571
+ out('');
572
+ }
573
+ function mandateSignedLine(m) {
574
+ out(` ${C.jade}◇${C.reset} ${C.bold}Mandate ${m.mandate_id.slice(0, 8)}…${C.reset} ${C.green}signed${C.reset} ${C.gray}(${m.signed_by}, HMAC)${C.reset} — swarm executing within envelope.`);
575
+ out(` ${C.gray}Persisted:${C.reset} .swarm-state/mandates/${m.mandate_id}.json`);
576
+ out(` ${C.gray}Revoke:${C.reset} kognai /revoke ${m.mandate_id.slice(0, 8)}… ${C.gray}(halts at next task boundary)${C.reset}`);
577
+ out('');
578
+ }
579
+ // Interactive sign | cancel | edit gate (TICKET-202). Returns the signed mandate,
580
+ // or null if the founder cancels. On a non-TTY stdin (piped / CI) it auto-signs so
581
+ // scripted runs keep working. `edit` lets the founder set a deadline and add scope
582
+ // boundaries, then re-previews.
583
+ async function confirmAndSignMandate(u, template) {
584
+ if (!process.stdin.isTTY) {
585
+ out(` ${C.gray}(non-interactive stdin — auto-signing on the founder's behalf)${C.reset}`);
586
+ const m = (0, mandate_1.signMandate)(u, 'founder');
587
+ (0, mandate_1.persistMandate)(m);
588
+ mandateSignedLine(m);
589
+ return m;
590
+ }
591
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
592
+ const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
593
+ try {
594
+ for (let i = 0; i < 6; i++) {
595
+ const ans = (await ask(` ${C.jade}◇${C.reset} ${C.bold}sign${C.reset} | ${C.bold}cancel${C.reset} | ${C.bold}edit${C.reset} ${C.gray}›${C.reset} `)).toLowerCase();
596
+ if (ans === 'sign' || ans === 's' || ans === '') {
597
+ const m = (0, mandate_1.signMandate)(u, 'founder');
598
+ (0, mandate_1.persistMandate)(m);
599
+ mandateSignedLine(m);
600
+ return m;
601
+ }
602
+ if (ans === 'cancel' || ans === 'c' || ans === 'n') {
603
+ out(` ${C.yellow}Mandate cancelled — nothing signed, no tasks run.${C.reset}`);
604
+ out('');
605
+ return null;
606
+ }
607
+ if (ans === 'edit' || ans === 'e') {
608
+ const dl = await ask(` ${C.gray}deadline (ISO 8601, or blank for none) ›${C.reset} `);
609
+ u.deadline = dl ? dl : null;
610
+ const sc = await ask(` ${C.gray}add a scope boundary (blank to skip) ›${C.reset} `);
611
+ if (sc)
612
+ u.scope_boundaries = [...u.scope_boundaries, sc];
613
+ out('');
614
+ mandatePreviewBlock(u, template);
615
+ continue;
616
+ }
617
+ out(` ${C.yellow}please type ${C.bold}sign${C.reset}${C.yellow}, ${C.bold}cancel${C.reset}${C.yellow}, or ${C.bold}edit${C.reset}${C.yellow}.${C.reset}`);
618
+ }
619
+ out(` ${C.yellow}too many attempts — cancelled.${C.reset}`);
620
+ return null;
621
+ }
622
+ finally {
623
+ rl.close();
624
+ }
625
+ }
626
+ async function runOneTask(taskDesc, outPath, compliance, profile) {
627
+ const t0 = Date.now();
628
+ let cost = 0;
629
+ let calls = 0;
630
+ section('Generation');
631
+ const gen = await generate(taskDesc, outPath, profile);
632
+ generationSummary(gen);
633
+ cost += gen.cost;
634
+ calls += 1;
635
+ section('Review');
636
+ const reviews = await Promise.all([
637
+ reviewCode('Code review (primary)', taskDesc, gen.content, profile),
638
+ reviewCode('Code review (second pass)', taskDesc, gen.content, profile),
639
+ compliance
640
+ ? reviewCompliance(taskDesc, gen.content)
641
+ : Promise.resolve({ pass: true, reasons: ['SKIPPED (--no-compliance)'], model: '—', cost: 0, ms: 0 }),
642
+ ]);
643
+ const sup1 = reviews[0];
644
+ const sup2 = reviews[1];
645
+ const sup3 = reviews[2];
646
+ cost += sup1.cost + sup2.cost + sup3.cost;
647
+ calls += compliance ? 3 : 2;
648
+ codeReviewBlock(sup1);
649
+ codeReviewBlock(sup2);
650
+ complianceReviewBlock(sup3);
651
+ const codeOk = gradeRank(sup1.grade) >= 3 || gradeRank(sup2.grade) >= 3;
652
+ const ship = codeOk && sup3.pass;
653
+ verdictBlock(sup1, sup2, sup3, ship);
654
+ out('');
655
+ if (ship) {
656
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(outPath), { recursive: true });
657
+ (0, node_fs_1.writeFileSync)(outPath, gen.content);
658
+ }
659
+ section('Deliverable');
660
+ deliverableBlock(gen.content, outPath, ship);
661
+ // TICKET-233 Phase 1: KSL measurement — only when a .kognai workspace is bound
662
+ // (KOGNAI_ROOT set by boot-env), so plain CLI runs write nothing extra.
663
+ if (workspace) {
664
+ try {
665
+ orchestrator_core_1.ksl.tapAttempt({
666
+ sprint_id: KSL_RUN_ID,
667
+ task_id: (outPath.split('/').pop() || 'task').replace(/[^A-Za-z0-9._-]/g, '_'),
668
+ attempt: 1,
669
+ agent: 'kognai-build',
670
+ model: gen.model,
671
+ prompt: taskDesc,
672
+ reply: gen.content,
673
+ cost_usd: cost,
674
+ duration_ms: Date.now() - t0,
675
+ signal_strength: ship ? 'high' : 'low',
676
+ });
677
+ }
678
+ catch { /* KSL is best-effort — never block a deliverable */ }
679
+ }
680
+ return { ship, cost, calls };
681
+ }
682
+ // ─── Main ────────────────────────────────────────────────────────────────────
683
+ (async () => {
684
+ const args = parseArgs();
685
+ // TICKET-233 Phase 1: register the engine-backed router. --sovereign → $0 local.
686
+ (0, orchestrator_core_1.setModelRouter)((0, router_1.makeBuildRouter)(args.sovereign));
687
+ const t0 = Date.now();
688
+ const startedAtISO = new Date(t0).toISOString();
689
+ if (args.mode === 'task') {
690
+ header(args.input, args.compliance);
691
+ if (args.profile)
692
+ out(` ${C.gray}optimization profile:${C.reset} ${C.bold}${args.profile}${C.reset}\n`);
693
+ const r = await runOneTask(args.input, args.outPath, args.compliance, args.profile);
694
+ footer(Date.now() - t0, r.cost, r.calls, args.outPath, r.ship);
695
+ process.exit(r.ship ? 0 : 1);
696
+ }
697
+ // Goal mode: TICKET-135 hierarchy — planner decomposes goal → tasks → per-task pipeline → goal verdict
698
+ goalHeader(args.input, args.compliance);
699
+ if (args.profile)
700
+ out(` ${C.gray}optimization profile:${C.reset} ${C.bold}${args.profile}${C.reset}${C.gray} (cascades to every task)${C.reset}\n`);
701
+ section('Planning');
702
+ const plan = await decompose(args.input, args.outDir);
703
+ planningBlock(plan, args.outDir);
704
+ let cost = plan.cost;
705
+ let calls = 1;
706
+ // TICKET-202: preview + sign a PACT mandate from the plan before executing. The
707
+ // mandate is the autonomy envelope (template + capabilities + cost ceiling + scope).
708
+ // Cancelling at the prompt aborts before any task runs.
709
+ if (args.mandate) {
710
+ const { unsigned, template } = prepareMandate(args.input, plan, args.outDir, args.profile);
711
+ mandatePreviewBlock(unsigned, template);
712
+ const signed = await confirmAndSignMandate(unsigned, template);
713
+ if (!signed) {
714
+ footer(Date.now() - t0, cost, calls, args.outDir, false);
715
+ process.exit(0);
716
+ }
717
+ }
718
+ // TICKET-233 Phase 2: --swarm delegates the decomposed plan to the FULL
719
+ // orchestrator-core swarm (CEO · CTO governance gate · dual-supervisor) instead
720
+ // of the in-process triple-supervisor. Honors --sovereign ($0 local) via the
721
+ // registered router. The engine renders its own canonical swarm UI.
722
+ if (args.swarm) {
723
+ out(`\n ${C.gray}swarm mode — delegating to @kognai/orchestrator-core (CEO · CTO gate · dual-supervisor)${args.sovereign ? ' · sovereign $0' : ''}${C.reset}\n`);
724
+ const sw = await (0, swarm_1.runSwarm)(args.input, plan.tasks, args.outDir);
725
+ out(`\n ${C.bold}Swarm deliverables:${C.reset} ${sw.deliverables.length}/${sw.total} → ${args.outDir}`);
726
+ for (const d of sw.deliverables)
727
+ out(` ${C.green}✓${C.reset} ${d}`);
728
+ footer(Date.now() - t0, cost, calls, args.outDir, sw.deliverables.length > 0);
729
+ process.exit(sw.deliverables.length > 0 ? 0 : 1);
730
+ }
731
+ const shipped = [];
732
+ for (let i = 0; i < plan.tasks.length; i++) {
733
+ const t = plan.tasks[i];
734
+ const outPath = `${args.outDir}/${t.file}`;
735
+ taskHeader(i + 1, plan.tasks.length, t.file);
736
+ const r = await runOneTask(t.description, outPath, args.compliance, args.profile);
737
+ cost += r.cost;
738
+ calls += r.calls;
739
+ if (r.ship)
740
+ shipped.push(outPath);
741
+ }
742
+ out('');
743
+ goalVerdictBlock(shipped.length, plan.tasks.length, args.outDir, shipped);
744
+ footer(Date.now() - t0, cost, calls, args.outDir, shipped.length > 0);
745
+ // TICKET-207 #6: append a session summary to .kognai/memory/kognai/ after the
746
+ // goal verdict. Guarded by workspace presence + config opt-in. Never fatal —
747
+ // a memory-write failure must not fail an otherwise-successful build.
748
+ if (workspace && workspace.config.memory.append_session_summary) {
749
+ try {
750
+ const summaryPath = (0, memory_1.writeSessionSummary)(workspace, {
751
+ goal: args.input,
752
+ profile: args.profile,
753
+ tasks_shipped: shipped.length,
754
+ tasks_total: plan.tasks.length,
755
+ cost_usdc: cost,
756
+ files: shipped,
757
+ started_at: startedAtISO,
758
+ ended_at: new Date().toISOString(),
759
+ });
760
+ out(` ${C.gray}Memory:${C.reset} appended session summary to ${summaryPath}`);
761
+ out('');
762
+ }
763
+ catch (e) {
764
+ out(` ${C.gray}Memory: session summary skipped (${(e?.message || String(e)).slice(0, 80)})${C.reset}`);
765
+ out('');
766
+ }
767
+ }
768
+ process.exit(shipped.length === plan.tasks.length ? 0 : 1);
769
+ })().catch((e) => {
770
+ out('');
771
+ out(` ${C.red}${C.bold}error:${C.reset} ${e.message}`);
772
+ out('');
773
+ process.exit(2);
774
+ });