@thispointon/kondi-chat 0.1.2
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/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI Pipeline Runner
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx cli/run-pipeline.ts <pipeline.json> [--working-dir <path>] [--model <model>] [--dry-run]
|
|
7
|
+
*
|
|
8
|
+
* Loads a pipeline JSON exported from the Kondi app and runs it using the same
|
|
9
|
+
* executor/orchestrator code, with output printed to the terminal.
|
|
10
|
+
*
|
|
11
|
+
* All personas, descriptions, inputs and outputs of every step are stored
|
|
12
|
+
* in an execution report saved to the working directory after completion.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── localStorage shim MUST be imported first ──
|
|
16
|
+
import { storage } from './localStorage-shim';
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import readline from 'node:readline';
|
|
21
|
+
import { pipelineStore } from '../pipeline/store';
|
|
22
|
+
import { PipelineExecutor } from '../pipeline/executor';
|
|
23
|
+
import type { PlatformAdapter } from '../pipeline/executor';
|
|
24
|
+
import type { Pipeline, CouncilStepConfig, LlmStepConfig, ScriptStepConfig, ConditionStepConfig } from '../pipeline/types';
|
|
25
|
+
import { migrateLlmConfig } from '../pipeline/types';
|
|
26
|
+
import { callLLM } from './llm-caller';
|
|
27
|
+
import { createNodePlatform } from './node-platform';
|
|
28
|
+
import { exportSession } from './session-export';
|
|
29
|
+
import { createBudgetAwareInvoker } from '../budget/budget-aware-invoker';
|
|
30
|
+
import { mapStepTypeToStage } from '../budget/phase-stage-map';
|
|
31
|
+
import type { ModelTier } from '../budget/budget-tracker';
|
|
32
|
+
|
|
33
|
+
// ── ANSI colors ──
|
|
34
|
+
const C = {
|
|
35
|
+
reset: '\x1b[0m',
|
|
36
|
+
bold: '\x1b[1m',
|
|
37
|
+
dim: '\x1b[2m',
|
|
38
|
+
green: '\x1b[32m',
|
|
39
|
+
yellow: '\x1b[33m',
|
|
40
|
+
red: '\x1b[31m',
|
|
41
|
+
cyan: '\x1b[36m',
|
|
42
|
+
magenta: '\x1b[35m',
|
|
43
|
+
blue: '\x1b[34m',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function log(color: string, prefix: string, msg: string) {
|
|
47
|
+
const ts = new Date().toLocaleTimeString();
|
|
48
|
+
console.log(`${C.dim}${ts}${C.reset} ${color}${C.bold}[${prefix}]${C.reset} ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Execution log (captures all step inputs/outputs for the report) ──
|
|
52
|
+
interface StepExecutionRecord {
|
|
53
|
+
stepId: string;
|
|
54
|
+
stepName: string;
|
|
55
|
+
stepType: string;
|
|
56
|
+
stageName: string;
|
|
57
|
+
startedAt: string;
|
|
58
|
+
completedAt?: string;
|
|
59
|
+
durationMs?: number;
|
|
60
|
+
status: string;
|
|
61
|
+
error?: string;
|
|
62
|
+
// Council-based steps
|
|
63
|
+
personas?: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
role: string;
|
|
66
|
+
model: string;
|
|
67
|
+
provider: string;
|
|
68
|
+
traits?: string[];
|
|
69
|
+
domain?: string;
|
|
70
|
+
suppressPersona?: boolean;
|
|
71
|
+
}>;
|
|
72
|
+
councilName?: string;
|
|
73
|
+
councilId?: string;
|
|
74
|
+
maxRounds?: number;
|
|
75
|
+
maxRevisions?: number;
|
|
76
|
+
expectedOutput?: string;
|
|
77
|
+
// Input/output
|
|
78
|
+
inputTemplate?: string;
|
|
79
|
+
resolvedInput?: string;
|
|
80
|
+
output?: string;
|
|
81
|
+
outputPath?: string;
|
|
82
|
+
artifactType?: string;
|
|
83
|
+
tokensUsed?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const executionLog: StepExecutionRecord[] = [];
|
|
87
|
+
|
|
88
|
+
// ── Parse CLI args ──
|
|
89
|
+
function parseArgs() {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
let pipelineFile: string | null = null;
|
|
92
|
+
let workingDir: string | null = null;
|
|
93
|
+
let model: string | null = null;
|
|
94
|
+
let pipelineName: string | null = null;
|
|
95
|
+
let dryRun = false;
|
|
96
|
+
let noCache = false;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
if (args[i] === '--working-dir' && args[i + 1]) {
|
|
100
|
+
workingDir = path.resolve(args[++i]);
|
|
101
|
+
} else if (args[i] === '--model' && args[i + 1]) {
|
|
102
|
+
model = args[++i];
|
|
103
|
+
} else if (args[i] === '--name' && args[i + 1]) {
|
|
104
|
+
pipelineName = args[++i];
|
|
105
|
+
} else if (args[i] === '--no-cache') {
|
|
106
|
+
noCache = true;
|
|
107
|
+
} else if (args[i] === '--dry-run') {
|
|
108
|
+
dryRun = true;
|
|
109
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
110
|
+
console.log(`
|
|
111
|
+
${C.bold}Kondi CLI Pipeline Runner${C.reset}
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
npx tsx cli/run-pipeline.ts <pipeline.json> [options]
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
--working-dir <path> Override the pipeline's working directory
|
|
118
|
+
--model <model> Override the model for all LLM steps
|
|
119
|
+
--name <name> Select pipeline by name (when file contains multiple)
|
|
120
|
+
--no-cache Disable Anthropic prompt caching (same as KONDI_NO_CACHE=1)
|
|
121
|
+
--dry-run Print pipeline structure without running
|
|
122
|
+
--help Show this help
|
|
123
|
+
|
|
124
|
+
Accepts two JSON formats:
|
|
125
|
+
1. Single pipeline (exported via "Export" button in the app)
|
|
126
|
+
2. Wrapped format: { "pipelines": [...] } — use --name to pick one
|
|
127
|
+
|
|
128
|
+
Output:
|
|
129
|
+
After execution, a comprehensive execution report is saved to:
|
|
130
|
+
<working-dir>/kondi-execution-report.json
|
|
131
|
+
This includes all personas, step inputs/outputs, timing, and artifacts.
|
|
132
|
+
`);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
} else if (!args[i].startsWith('--')) {
|
|
135
|
+
pipelineFile = path.resolve(args[i]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!pipelineFile) {
|
|
140
|
+
console.error(`${C.red}Error: No pipeline JSON file specified.${C.reset}`);
|
|
141
|
+
console.error(`Usage: npx tsx cli/run-pipeline.ts <pipeline.json> [--working-dir <path>]`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { pipelineFile, workingDir, model, pipelineName, dryRun, noCache };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Load pipeline JSON ──
|
|
149
|
+
function loadPipeline(
|
|
150
|
+
filePath: string,
|
|
151
|
+
workingDirOverride?: string | null,
|
|
152
|
+
modelOverride?: string | null,
|
|
153
|
+
pipelineName?: string | null,
|
|
154
|
+
): Pipeline {
|
|
155
|
+
if (!fs.existsSync(filePath)) {
|
|
156
|
+
console.error(`${C.red}Error: File not found: ${filePath}${C.reset}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
|
|
163
|
+
// Detect format: wrapped { pipelines: [...] } vs single Pipeline object
|
|
164
|
+
let pipeline: Pipeline;
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(parsed.pipelines)) {
|
|
167
|
+
// Wrapped format (localStorage export or multi-pipeline file)
|
|
168
|
+
const list: Pipeline[] = parsed.pipelines;
|
|
169
|
+
if (list.length === 0) {
|
|
170
|
+
console.error(`${C.red}Error: No pipelines found in file.${C.reset}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (pipelineName) {
|
|
175
|
+
const match = list.find(p =>
|
|
176
|
+
p.name.toLowerCase() === pipelineName.toLowerCase() ||
|
|
177
|
+
p.name.toLowerCase().includes(pipelineName.toLowerCase())
|
|
178
|
+
);
|
|
179
|
+
if (!match) {
|
|
180
|
+
console.error(`${C.red}Error: No pipeline matching "${pipelineName}". Available:${C.reset}`);
|
|
181
|
+
for (const p of list) console.error(` - ${p.name}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
pipeline = match;
|
|
185
|
+
} else if (list.length === 1) {
|
|
186
|
+
pipeline = list[0];
|
|
187
|
+
} else {
|
|
188
|
+
console.error(`${C.yellow}Multiple pipelines found. Use --name to select one:${C.reset}`);
|
|
189
|
+
for (const p of list) console.error(` - ${p.name}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
} else if (parsed.id && Array.isArray(parsed.stages)) {
|
|
193
|
+
// Single pipeline object (exported from app)
|
|
194
|
+
pipeline = parsed as Pipeline;
|
|
195
|
+
} else {
|
|
196
|
+
console.error(`${C.red}Error: Invalid pipeline JSON — expected a Pipeline object or { pipelines: [...] }.${C.reset}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Apply overrides
|
|
201
|
+
if (workingDirOverride) {
|
|
202
|
+
pipeline.settings.workingDirectory = workingDirOverride;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Reset execution state for a fresh run
|
|
206
|
+
pipeline.status = 'ready';
|
|
207
|
+
pipeline.currentStageIndex = 0;
|
|
208
|
+
for (const stage of pipeline.stages) {
|
|
209
|
+
for (const step of stage.steps) {
|
|
210
|
+
step.status = 'pending';
|
|
211
|
+
step.artifact = undefined;
|
|
212
|
+
step.error = undefined;
|
|
213
|
+
step.startedAt = undefined;
|
|
214
|
+
step.completedAt = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return pipeline;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Print pipeline structure with full persona details ──
|
|
222
|
+
function printPipelineStructure(pipeline: Pipeline) {
|
|
223
|
+
console.log(`\n${C.bold}${C.cyan}Pipeline: ${pipeline.name}${C.reset}`);
|
|
224
|
+
if (pipeline.description) console.log(`${C.dim}${pipeline.description}${C.reset}`);
|
|
225
|
+
console.log(`${C.dim}Working directory: ${pipeline.settings.workingDirectory || '(not set)'}${C.reset}`);
|
|
226
|
+
console.log(`${C.dim}Failure policy: ${pipeline.settings.failurePolicy}${C.reset}`);
|
|
227
|
+
if (pipeline.initialInput) {
|
|
228
|
+
console.log(`\n${C.bold}Initial Input:${C.reset}`);
|
|
229
|
+
console.log(`${C.dim}${pipeline.initialInput.slice(0, 300)}${pipeline.initialInput.length > 300 ? '...' : ''}${C.reset}`);
|
|
230
|
+
}
|
|
231
|
+
console.log();
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < pipeline.stages.length; i++) {
|
|
234
|
+
const stage = pipeline.stages[i];
|
|
235
|
+
console.log(` ${C.bold}Stage ${i + 1}: ${stage.name}${C.reset} (${stage.executionMode || 'sequential'})`);
|
|
236
|
+
for (const step of stage.steps) {
|
|
237
|
+
const typeColor = step.config.type === 'gate' ? C.yellow :
|
|
238
|
+
step.config.type === 'coding' ? C.magenta :
|
|
239
|
+
step.config.type === 'code_planning' ? C.blue :
|
|
240
|
+
step.config.type === 'council' ? C.yellow :
|
|
241
|
+
step.config.type === 'script' ? C.cyan :
|
|
242
|
+
step.config.type === 'condition' ? C.yellow : C.green;
|
|
243
|
+
console.log(` ${typeColor}[${step.config.type}]${C.reset} ${step.name}`);
|
|
244
|
+
if (step.description) {
|
|
245
|
+
console.log(` ${C.dim}${step.description}${C.reset}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Show personas for council steps (all non-gate types with councilSetup)
|
|
249
|
+
if ('councilSetup' in step.config) {
|
|
250
|
+
const config = step.config as CouncilStepConfig;
|
|
251
|
+
console.log(` ${C.dim}Council: ${config.councilSetup.name}${C.reset}`);
|
|
252
|
+
console.log(` ${C.dim}Rounds: ${config.councilSetup.maxRounds ?? 4}, Revisions: ${config.councilSetup.maxRevisions ?? 3}${C.reset}`);
|
|
253
|
+
if (config.councilSetup.expectedOutput) {
|
|
254
|
+
console.log(` ${C.dim}Expected: ${config.councilSetup.expectedOutput.slice(0, 120)}...${C.reset}`);
|
|
255
|
+
}
|
|
256
|
+
for (const p of config.councilSetup.personas) {
|
|
257
|
+
const roleColor = p.role === 'manager' ? C.blue :
|
|
258
|
+
p.role === 'worker' ? C.yellow :
|
|
259
|
+
p.role === 'reviewer' ? C.cyan : C.green;
|
|
260
|
+
console.log(` ${roleColor}${p.avatar || ''} ${p.name}${C.reset} (${p.role}) — ${p.model}`);
|
|
261
|
+
if (p.domain) console.log(` ${C.dim}Domain: ${p.domain}${C.reset}`);
|
|
262
|
+
if (p.traits?.length) console.log(` ${C.dim}Traits: ${p.traits.join(', ')}${C.reset}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Show script command
|
|
267
|
+
if (step.config.type === 'script') {
|
|
268
|
+
const config = step.config as ScriptStepConfig;
|
|
269
|
+
console.log(` ${C.dim}Command: ${config.command || '(empty)'}${C.reset}`);
|
|
270
|
+
if (config.outputType && config.outputType !== 'string') {
|
|
271
|
+
console.log(` ${C.dim}Output: ${config.outputType}${C.reset}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Show condition config
|
|
276
|
+
if (step.config.type === 'condition') {
|
|
277
|
+
const config = step.config as ConditionStepConfig;
|
|
278
|
+
console.log(` ${C.dim}Mode: ${config.mode}, Expression: "${config.expression}"${C.reset}`);
|
|
279
|
+
console.log(` ${C.dim}If TRUE: ${config.trueAction}, If FALSE: ${config.falseAction}${C.reset}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Show model for lightweight council steps (analysis/agent)
|
|
283
|
+
if (step.config.type === 'analysis' || step.config.type === 'agent') {
|
|
284
|
+
if ('councilSetup' in step.config) {
|
|
285
|
+
const config = step.config as CouncilStepConfig;
|
|
286
|
+
for (const p of config.councilSetup.personas) {
|
|
287
|
+
console.log(` ${C.dim}${p.name} (${p.role}) — ${p.model}${C.reset}`);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
const config = step.config as LlmStepConfig;
|
|
291
|
+
console.log(` ${C.dim}Model: ${config.model} (${config.provider})${C.reset}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
console.log();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Interactive gate prompt ──
|
|
300
|
+
async function promptGate(stepId: string, prompt: string): Promise<boolean> {
|
|
301
|
+
const rl = readline.createInterface({
|
|
302
|
+
input: process.stdin,
|
|
303
|
+
output: process.stdout,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return new Promise((resolve) => {
|
|
307
|
+
console.log(`\n${C.yellow}${C.bold}GATE: ${prompt}${C.reset}`);
|
|
308
|
+
rl.question(`${C.yellow}Approve? (y/n): ${C.reset}`, (answer) => {
|
|
309
|
+
rl.close();
|
|
310
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Save execution report ──
|
|
316
|
+
function saveExecutionReport(
|
|
317
|
+
pipeline: Pipeline,
|
|
318
|
+
workingDir: string,
|
|
319
|
+
startTime: number,
|
|
320
|
+
status: 'completed' | 'failed',
|
|
321
|
+
error?: string,
|
|
322
|
+
) {
|
|
323
|
+
const finalPipeline = pipelineStore.get(pipeline.id) || pipeline;
|
|
324
|
+
const elapsed = Date.now() - startTime;
|
|
325
|
+
|
|
326
|
+
const report = {
|
|
327
|
+
pipeline: {
|
|
328
|
+
id: finalPipeline.id,
|
|
329
|
+
name: finalPipeline.name,
|
|
330
|
+
description: finalPipeline.description,
|
|
331
|
+
initialInput: finalPipeline.initialInput,
|
|
332
|
+
workingDirectory: finalPipeline.settings.workingDirectory,
|
|
333
|
+
failurePolicy: finalPipeline.settings.failurePolicy,
|
|
334
|
+
directoryConstrained: finalPipeline.settings.directoryConstrained,
|
|
335
|
+
},
|
|
336
|
+
execution: {
|
|
337
|
+
status,
|
|
338
|
+
error,
|
|
339
|
+
startedAt: new Date(startTime).toISOString(),
|
|
340
|
+
completedAt: new Date().toISOString(),
|
|
341
|
+
durationMs: elapsed,
|
|
342
|
+
durationHuman: formatDuration(elapsed),
|
|
343
|
+
},
|
|
344
|
+
stages: finalPipeline.stages.map((stage) => ({
|
|
345
|
+
id: stage.id,
|
|
346
|
+
name: stage.name,
|
|
347
|
+
executionMode: stage.executionMode || 'sequential',
|
|
348
|
+
steps: stage.steps.map((step) => {
|
|
349
|
+
// Find matching execution log entry
|
|
350
|
+
const logEntry = executionLog.find(e => e.stepId === step.id);
|
|
351
|
+
|
|
352
|
+
const stepReport: Record<string, any> = {
|
|
353
|
+
id: step.id,
|
|
354
|
+
name: step.name,
|
|
355
|
+
description: step.description,
|
|
356
|
+
type: step.config.type,
|
|
357
|
+
status: step.status,
|
|
358
|
+
error: step.error,
|
|
359
|
+
startedAt: step.startedAt || logEntry?.startedAt,
|
|
360
|
+
completedAt: step.completedAt || logEntry?.completedAt,
|
|
361
|
+
durationMs: logEntry?.durationMs,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Council step details (all non-gate types with councilSetup)
|
|
365
|
+
if ('councilSetup' in step.config) {
|
|
366
|
+
const config = step.config as CouncilStepConfig;
|
|
367
|
+
stepReport.council = {
|
|
368
|
+
name: config.councilSetup.name,
|
|
369
|
+
councilId: logEntry?.councilId,
|
|
370
|
+
maxRounds: config.councilSetup.maxRounds,
|
|
371
|
+
maxRevisions: config.councilSetup.maxRevisions,
|
|
372
|
+
expectedOutput: config.councilSetup.expectedOutput,
|
|
373
|
+
personas: config.councilSetup.personas.map(p => ({
|
|
374
|
+
name: p.name,
|
|
375
|
+
role: p.role,
|
|
376
|
+
model: p.model,
|
|
377
|
+
provider: p.provider,
|
|
378
|
+
avatar: p.avatar,
|
|
379
|
+
traits: p.traits,
|
|
380
|
+
domain: p.domain,
|
|
381
|
+
suppressPersona: p.suppressPersona,
|
|
382
|
+
saveOutput: p.saveOutput,
|
|
383
|
+
})),
|
|
384
|
+
};
|
|
385
|
+
if (step.config.type === 'coding') {
|
|
386
|
+
stepReport.council.testCommand = config.councilSetup.testCommand;
|
|
387
|
+
stepReport.council.maxDebugCycles = config.councilSetup.maxDebugCycles;
|
|
388
|
+
stepReport.council.maxReviewCycles = config.councilSetup.maxReviewCycles;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Lightweight council step details (analysis/agent)
|
|
393
|
+
if (step.config.type === 'analysis' || step.config.type === 'agent') {
|
|
394
|
+
if ('councilSetup' in step.config) {
|
|
395
|
+
const config = step.config as CouncilStepConfig;
|
|
396
|
+
stepReport.council = {
|
|
397
|
+
name: config.councilSetup.name,
|
|
398
|
+
councilId: logEntry?.councilId,
|
|
399
|
+
maxRounds: config.councilSetup.maxRounds,
|
|
400
|
+
maxRevisions: config.councilSetup.maxRevisions,
|
|
401
|
+
personas: config.councilSetup.personas.map(p => ({
|
|
402
|
+
name: p.name, role: p.role, model: p.model, provider: p.provider,
|
|
403
|
+
})),
|
|
404
|
+
};
|
|
405
|
+
} else {
|
|
406
|
+
const config = step.config as LlmStepConfig;
|
|
407
|
+
stepReport.llm = {
|
|
408
|
+
model: config.model,
|
|
409
|
+
provider: config.provider,
|
|
410
|
+
systemPrompt: config.systemPrompt,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Input/output
|
|
416
|
+
stepReport.inputTemplate = (step.config as any).inputTemplate;
|
|
417
|
+
stepReport.resolvedInput = logEntry?.resolvedInput;
|
|
418
|
+
|
|
419
|
+
// Artifact (output)
|
|
420
|
+
if (step.artifact) {
|
|
421
|
+
stepReport.artifact = {
|
|
422
|
+
type: step.artifact.artifactType,
|
|
423
|
+
contentLength: step.artifact.content.length,
|
|
424
|
+
contentPreview: step.artifact.content.slice(0, 500),
|
|
425
|
+
outputPath: step.artifact.metadata?.outputPath,
|
|
426
|
+
tokensUsed: step.artifact.metadata?.tokensUsed,
|
|
427
|
+
councilId: step.artifact.metadata?.councilId,
|
|
428
|
+
createdAt: step.artifact.createdAt,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return stepReport;
|
|
433
|
+
}),
|
|
434
|
+
})),
|
|
435
|
+
steps: executionLog,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const reportPath = path.join(workingDir, 'kondi-execution-report.json');
|
|
439
|
+
try {
|
|
440
|
+
fs.mkdirSync(workingDir, { recursive: true });
|
|
441
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
442
|
+
log(C.cyan, 'Report', `Execution report saved to: ${reportPath}`);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error(`${C.red}Failed to save execution report:${C.reset}`, err);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return reportPath;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function formatDuration(ms: number): string {
|
|
451
|
+
if (ms < 1000) return `${ms}ms`;
|
|
452
|
+
const s = Math.floor(ms / 1000);
|
|
453
|
+
if (s < 60) return `${s}s`;
|
|
454
|
+
const m = Math.floor(s / 60);
|
|
455
|
+
const remaining = s % 60;
|
|
456
|
+
if (m < 60) return `${m}m ${remaining}s`;
|
|
457
|
+
const h = Math.floor(m / 60);
|
|
458
|
+
return `${h}h ${m % 60}m ${remaining}s`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Main ──
|
|
462
|
+
async function main() {
|
|
463
|
+
const { pipelineFile, workingDir, model, pipelineName, dryRun, noCache } = parseArgs();
|
|
464
|
+
const pipeline = loadPipeline(pipelineFile, workingDir, model, pipelineName);
|
|
465
|
+
|
|
466
|
+
printPipelineStructure(pipeline);
|
|
467
|
+
|
|
468
|
+
if (dryRun) {
|
|
469
|
+
console.log(`${C.dim}--dry-run: exiting without executing.${C.reset}`);
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Push pipeline into store (so executor can read/update it)
|
|
474
|
+
const storedPipelines = pipelineStore.getAll();
|
|
475
|
+
const existing = storedPipelines.find(p => p.id === pipeline.id);
|
|
476
|
+
if (existing) {
|
|
477
|
+
// Update in place
|
|
478
|
+
pipelineStore.update(pipeline.id, {
|
|
479
|
+
...pipeline,
|
|
480
|
+
status: 'ready',
|
|
481
|
+
currentStageIndex: 0,
|
|
482
|
+
});
|
|
483
|
+
// Reset all steps
|
|
484
|
+
for (const stage of pipeline.stages) {
|
|
485
|
+
for (const step of stage.steps) {
|
|
486
|
+
pipelineStore.setStepStatus(pipeline.id, step.id, 'pending');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
// Manually write into localStorage since create() generates a new id
|
|
491
|
+
const data = JSON.parse(storage.getItem('mcp-pipelines') || '{"version":5,"pipelines":[],"lastUpdated":""}');
|
|
492
|
+
data.pipelines.push(pipeline);
|
|
493
|
+
data.lastUpdated = new Date().toISOString();
|
|
494
|
+
storage.setItem('mcp-pipelines', JSON.stringify(data));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Resolve and validate working dir
|
|
498
|
+
const effectiveWorkingDir = pipeline.settings.workingDirectory || process.cwd();
|
|
499
|
+
if (!fs.existsSync(effectiveWorkingDir)) {
|
|
500
|
+
console.error(`${C.red}Error: Working directory does not exist: ${effectiveWorkingDir}${C.reset}`);
|
|
501
|
+
console.error(`${C.dim}Set it in the pipeline JSON under settings.workingDirectory, or use --working-dir${C.reset}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
console.log(`${C.dim}Working directory: ${effectiveWorkingDir}${C.reset}`);
|
|
505
|
+
|
|
506
|
+
// Create platform adapter
|
|
507
|
+
const platform: PlatformAdapter = createNodePlatform(effectiveWorkingDir);
|
|
508
|
+
|
|
509
|
+
// Track execution time and step timing
|
|
510
|
+
const startTime = Date.now();
|
|
511
|
+
const stepTimers: Record<string, number> = {};
|
|
512
|
+
|
|
513
|
+
// Create budget-aware invoker with persistence
|
|
514
|
+
const budgetInvoker = createBudgetAwareInvoker({
|
|
515
|
+
verbose: true,
|
|
516
|
+
restoreState: true,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Track current step type for budget stage mapping
|
|
520
|
+
// Initialize to 'council' to ensure first call (before step execution) maps to 'deliberation'
|
|
521
|
+
let currentStepType: string = 'council';
|
|
522
|
+
|
|
523
|
+
// Create executor
|
|
524
|
+
const executor = new PipelineExecutor({
|
|
525
|
+
invokeAgent: async (invocation, persona) => {
|
|
526
|
+
log(C.cyan, persona.name, `Invoking (${persona.model})...`);
|
|
527
|
+
|
|
528
|
+
// Map current step type to budget stage
|
|
529
|
+
const budgetStage = mapStepTypeToStage(currentStepType as any);
|
|
530
|
+
|
|
531
|
+
// Determine requested tier based on provider/model
|
|
532
|
+
let requestedTier: ModelTier = 'openai-mini';
|
|
533
|
+
if (persona.provider === 'anthropic-api' || persona.model?.includes('claude')) {
|
|
534
|
+
requestedTier = 'anthropic-premium';
|
|
535
|
+
} else if (persona.model?.includes('gpt-4o') && !persona.model?.includes('mini')) {
|
|
536
|
+
requestedTier = 'openai-mid';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Build allowedTools from allowedServerIds if set
|
|
540
|
+
const allowedTools = invocation.allowedServerIds
|
|
541
|
+
? ['Edit', 'Write', 'Read', 'Bash', 'Glob', 'Grep', ...invocation.allowedServerIds.map(id => `mcp__${id}`)]
|
|
542
|
+
: undefined;
|
|
543
|
+
|
|
544
|
+
// Use generous timeouts: workers get 30 min, Opus models get 20 min, others get 15 min.
|
|
545
|
+
const isWorker = persona.preferredDeliberationRole === 'worker';
|
|
546
|
+
const isOpus = persona.model?.includes('opus');
|
|
547
|
+
const timeoutMs = isWorker ? 1_800_000 : isOpus ? 1_200_000 : 900_000;
|
|
548
|
+
|
|
549
|
+
// Use budget-aware invoker
|
|
550
|
+
const result = await budgetInvoker.invoke({
|
|
551
|
+
stage: budgetStage,
|
|
552
|
+
requestedTier,
|
|
553
|
+
persona,
|
|
554
|
+
invocation: {
|
|
555
|
+
systemPrompt: invocation.systemPrompt,
|
|
556
|
+
userMessage: invocation.userMessage,
|
|
557
|
+
workingDirectory: platform.getWorkingDir(),
|
|
558
|
+
skipTools: invocation.skipTools,
|
|
559
|
+
allowedTools,
|
|
560
|
+
timeoutMs,
|
|
561
|
+
cacheableContext: invocation.cacheableContext,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
log(C.cyan, persona.name, `Done (${result.tokensUsed} tokens, ${(result.latencyMs / 1000).toFixed(1)}s, $${result.costUSD.toFixed(4)})`);
|
|
566
|
+
if (result.downgraded) {
|
|
567
|
+
log(C.yellow, 'Budget', `Downgraded to ${result.actualTier} (${result.reasonCode})`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return { ...result, sessionId: result.sessionId };
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
onStageStart: (idx) => {
|
|
574
|
+
const stage = pipelineStore.get(pipeline.id)?.stages[idx];
|
|
575
|
+
log(C.blue, 'Stage', `Starting stage ${idx + 1}: ${stage?.name || '?'}`);
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
onStageComplete: (idx) => {
|
|
579
|
+
const stage = pipelineStore.get(pipeline.id)?.stages[idx];
|
|
580
|
+
log(C.green, 'Stage', `Completed stage ${idx + 1}: ${stage?.name || '?'}`);
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
onStepStart: (stepId) => {
|
|
584
|
+
const p = pipelineStore.get(pipeline.id);
|
|
585
|
+
const step = p?.stages.flatMap(s => s.steps).find(s => s.id === stepId);
|
|
586
|
+
const stageName = p?.stages.find(s => s.steps.some(st => st.id === stepId))?.name || '?';
|
|
587
|
+
log(C.magenta, 'Step', `${step?.name || stepId} [${step?.config.type || '?'}]`);
|
|
588
|
+
stepTimers[stepId] = Date.now();
|
|
589
|
+
|
|
590
|
+
// Track current step type for budget mapping
|
|
591
|
+
if (step) {
|
|
592
|
+
currentStepType = step.config.type;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Create execution log entry
|
|
596
|
+
const record: StepExecutionRecord = {
|
|
597
|
+
stepId,
|
|
598
|
+
stepName: step?.name || stepId,
|
|
599
|
+
stepType: step?.config.type || 'unknown',
|
|
600
|
+
stageName,
|
|
601
|
+
startedAt: new Date().toISOString(),
|
|
602
|
+
status: 'running',
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Capture personas for council steps (all non-gate types with councilSetup)
|
|
606
|
+
if (step && 'councilSetup' in step.config) {
|
|
607
|
+
const config = step.config as CouncilStepConfig;
|
|
608
|
+
record.councilName = config.councilSetup.name;
|
|
609
|
+
record.maxRounds = config.councilSetup.maxRounds;
|
|
610
|
+
record.maxRevisions = config.councilSetup.maxRevisions;
|
|
611
|
+
record.expectedOutput = config.councilSetup.expectedOutput;
|
|
612
|
+
record.inputTemplate = config.inputTemplate;
|
|
613
|
+
record.personas = config.councilSetup.personas.map(p => ({
|
|
614
|
+
name: p.name,
|
|
615
|
+
role: p.role,
|
|
616
|
+
model: p.model,
|
|
617
|
+
provider: p.provider,
|
|
618
|
+
traits: p.traits,
|
|
619
|
+
domain: p.domain,
|
|
620
|
+
suppressPersona: p.suppressPersona,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (step && (step.config.type === 'analysis' || step.config.type === 'agent')) {
|
|
625
|
+
if ('councilSetup' in step.config) {
|
|
626
|
+
const config = step.config as CouncilStepConfig;
|
|
627
|
+
record.inputTemplate = config.inputTemplate;
|
|
628
|
+
record.personas = config.councilSetup.personas.map(p => ({
|
|
629
|
+
name: p.name, role: p.role, model: p.model, provider: p.provider,
|
|
630
|
+
}));
|
|
631
|
+
} else {
|
|
632
|
+
const config = step.config as LlmStepConfig;
|
|
633
|
+
record.inputTemplate = config.inputTemplate;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
executionLog.push(record);
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
onStepComplete: (stepId, artifact) => {
|
|
641
|
+
const p = pipelineStore.get(pipeline.id);
|
|
642
|
+
const step = p?.stages.flatMap(s => s.steps).find(s => s.id === stepId);
|
|
643
|
+
const durationMs = stepTimers[stepId] ? Date.now() - stepTimers[stepId] : 0;
|
|
644
|
+
log(C.green, 'Step', `${step?.name || stepId} completed (${formatDuration(durationMs)})`);
|
|
645
|
+
if (artifact.metadata?.outputPath) {
|
|
646
|
+
log(C.dim, 'Step', ` Output: ${artifact.metadata.outputPath}`);
|
|
647
|
+
}
|
|
648
|
+
// Print a preview of the artifact content
|
|
649
|
+
const preview = artifact.content.slice(0, 200).replace(/\n/g, ' ');
|
|
650
|
+
console.log(`${C.dim} ${preview}${artifact.content.length > 200 ? '...' : ''}${C.reset}`);
|
|
651
|
+
|
|
652
|
+
// Update execution log entry
|
|
653
|
+
const entry = executionLog.find(e => e.stepId === stepId);
|
|
654
|
+
if (entry) {
|
|
655
|
+
entry.completedAt = new Date().toISOString();
|
|
656
|
+
entry.durationMs = durationMs;
|
|
657
|
+
entry.status = 'completed';
|
|
658
|
+
entry.output = artifact.content;
|
|
659
|
+
entry.outputPath = artifact.metadata?.outputPath;
|
|
660
|
+
entry.artifactType = artifact.artifactType;
|
|
661
|
+
entry.tokensUsed = artifact.metadata?.tokensUsed;
|
|
662
|
+
entry.councilId = artifact.metadata?.councilId;
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
onStepError: (stepId, error) => {
|
|
667
|
+
const p = pipelineStore.get(pipeline.id);
|
|
668
|
+
const step = p?.stages.flatMap(s => s.steps).find(s => s.id === stepId);
|
|
669
|
+
log(C.red, 'Step', `${step?.name || stepId} failed: ${error}`);
|
|
670
|
+
|
|
671
|
+
// Update execution log entry
|
|
672
|
+
const entry = executionLog.find(e => e.stepId === stepId);
|
|
673
|
+
if (entry) {
|
|
674
|
+
entry.completedAt = new Date().toISOString();
|
|
675
|
+
entry.durationMs = stepTimers[stepId] ? Date.now() - stepTimers[stepId] : 0;
|
|
676
|
+
entry.status = 'failed';
|
|
677
|
+
entry.error = error;
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
onGateWaiting: promptGate,
|
|
682
|
+
|
|
683
|
+
onCouncilCreated: (stepId, councilId) => {
|
|
684
|
+
log(C.blue, 'Council', `Created ${councilId} for step ${stepId}`);
|
|
685
|
+
// Record council ID in execution log
|
|
686
|
+
const entry = executionLog.find(e => e.stepId === stepId);
|
|
687
|
+
if (entry) entry.councilId = councilId;
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
onAgentThinkingStart: (persona) => {
|
|
691
|
+
log(C.dim, 'Agent', `${persona.name} thinking...`);
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
onAgentThinkingEnd: (_persona) => {
|
|
695
|
+
// Intentionally quiet
|
|
696
|
+
},
|
|
697
|
+
}, platform);
|
|
698
|
+
|
|
699
|
+
// Run!
|
|
700
|
+
log(C.bold, 'Pipeline', `Starting execution: ${pipeline.name}`);
|
|
701
|
+
console.log();
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await executor.run(pipeline.id);
|
|
705
|
+
|
|
706
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
707
|
+
console.log();
|
|
708
|
+
log(C.green, 'Pipeline', `Completed successfully in ${elapsed}s`);
|
|
709
|
+
|
|
710
|
+
// Print final artifacts
|
|
711
|
+
const finalPipeline = pipelineStore.get(pipeline.id);
|
|
712
|
+
if (finalPipeline) {
|
|
713
|
+
console.log(`\n${C.bold}Final Artifacts:${C.reset}`);
|
|
714
|
+
for (const stage of finalPipeline.stages) {
|
|
715
|
+
for (const step of stage.steps) {
|
|
716
|
+
if (step.artifact) {
|
|
717
|
+
console.log(` ${C.cyan}${step.name}${C.reset}: ${step.artifact.artifactType}`);
|
|
718
|
+
if (step.artifact.metadata?.outputPath) {
|
|
719
|
+
console.log(` ${C.dim}-> ${step.artifact.metadata.outputPath}${C.reset}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Budget telemetry
|
|
727
|
+
const telemetry = budgetInvoker.getTelemetry();
|
|
728
|
+
console.log(`\n${C.bold}${C.cyan}Budget Summary${C.reset}`);
|
|
729
|
+
console.log(`${C.dim}Total spend: $${telemetry.totalSpendUSD.toFixed(4)} (${telemetry.runUtilization.toFixed(1)}% of run cap)${C.reset}`);
|
|
730
|
+
console.log(`${C.dim}Anthropic calls: ${telemetry.anthropicCalls} ($${telemetry.anthropicSpend.toFixed(4)})${C.reset}`);
|
|
731
|
+
if (telemetry.downgrades > 0) {
|
|
732
|
+
console.log(`${C.yellow}Downgrades: ${telemetry.downgrades}${C.reset}`);
|
|
733
|
+
for (const d of telemetry.recentDowngrades) {
|
|
734
|
+
console.log(` ${C.dim}${d.fromTier} → ${d.toTier} (${d.reasonCode})${C.reset}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Save execution report
|
|
739
|
+
saveExecutionReport(pipeline, effectiveWorkingDir, startTime, 'completed');
|
|
740
|
+
|
|
741
|
+
// Export session for GUI import
|
|
742
|
+
exportSession(pipeline.id, storage, {
|
|
743
|
+
status: 'completed',
|
|
744
|
+
startedAt: new Date(startTime).toISOString(),
|
|
745
|
+
completedAt: new Date().toISOString(),
|
|
746
|
+
durationMs: Date.now() - startTime,
|
|
747
|
+
workingDirectory: effectiveWorkingDir,
|
|
748
|
+
});
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
751
|
+
console.log();
|
|
752
|
+
log(C.red, 'Pipeline', `Failed after ${elapsed}s: ${(error as Error).message}`);
|
|
753
|
+
|
|
754
|
+
// Save execution report even on failure
|
|
755
|
+
saveExecutionReport(pipeline, effectiveWorkingDir, startTime, 'failed', (error as Error).message);
|
|
756
|
+
|
|
757
|
+
// Export session for GUI import (even on failure)
|
|
758
|
+
exportSession(pipeline.id, storage, {
|
|
759
|
+
status: 'failed',
|
|
760
|
+
startedAt: new Date(startTime).toISOString(),
|
|
761
|
+
completedAt: new Date().toISOString(),
|
|
762
|
+
durationMs: Date.now() - startTime,
|
|
763
|
+
workingDirectory: effectiveWorkingDir,
|
|
764
|
+
});
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
main().catch((err) => {
|
|
770
|
+
console.error(`${C.red}Unhandled error:${C.reset}`, err);
|
|
771
|
+
process.exit(1);
|
|
772
|
+
});
|