@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. 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
+ });