@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,1048 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Executor
|
|
3
|
+
* Runs pipelines: sequential stages, parallel steps within stages.
|
|
4
|
+
* Council steps (planning, coding, decisioning, execution, etc.) create councils.
|
|
5
|
+
* Script steps run shell commands. Condition steps evaluate expressions.
|
|
6
|
+
* Gate steps pause for user approval.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Pipeline,
|
|
11
|
+
PipelineStage,
|
|
12
|
+
PipelineStep,
|
|
13
|
+
StepArtifact,
|
|
14
|
+
StepMeta,
|
|
15
|
+
RunManifest,
|
|
16
|
+
CouncilStepConfig,
|
|
17
|
+
LlmStepConfig,
|
|
18
|
+
GateStepConfig,
|
|
19
|
+
ScriptStepConfig,
|
|
20
|
+
ConditionStepConfig,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { migrateLlmConfig } from './types';
|
|
23
|
+
|
|
24
|
+
import { pipelineStore } from './store';
|
|
25
|
+
import { councilStore } from '../council/store';
|
|
26
|
+
import { createCouncilFromSetup } from '../council/factory';
|
|
27
|
+
import { DeliberationOrchestrator } from '../council/deliberation-orchestrator';
|
|
28
|
+
import { CodingOrchestrator } from '../council/coding-orchestrator';
|
|
29
|
+
import { getDecision, getLatestOutput } from '../council/context-store';
|
|
30
|
+
import { stripCompletedCouncil } from '../council/storage-cleanup';
|
|
31
|
+
import { buildAbbreviatedSummary } from '../services/deliberationSummary';
|
|
32
|
+
import type { Persona } from '../council/types';
|
|
33
|
+
import type { AgentInvocation, AgentResponse } from '../council/deliberation-orchestrator';
|
|
34
|
+
import type { MCPTool } from '../types/mcp';
|
|
35
|
+
import { verifyRequiredTools } from '../utils/filterTools';
|
|
36
|
+
|
|
37
|
+
import type { MemoryContext } from './memory-store';
|
|
38
|
+
import { buildMemoryContext, appendEntry, getNextRunNumber } from './memory-store';
|
|
39
|
+
import { sanitizeFolderName } from './run-output';
|
|
40
|
+
import {
|
|
41
|
+
buildRunDirName,
|
|
42
|
+
getRunsBaseDir,
|
|
43
|
+
buildStepOutputDir,
|
|
44
|
+
writeStepOutput,
|
|
45
|
+
writeRunManifest,
|
|
46
|
+
pruneOldRuns,
|
|
47
|
+
} from './run-output';
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Callback Types
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
export interface PipelineExecutorCallbacks {
|
|
54
|
+
/** Same invokeAgent used by DeliberationOrchestrator */
|
|
55
|
+
invokeAgent: (invocation: AgentInvocation, persona: Persona) => Promise<AgentResponse>;
|
|
56
|
+
|
|
57
|
+
/** Returns all currently available MCP tools (for pre-flight tool checks) */
|
|
58
|
+
getAvailableTools?: () => Map<string, { serverId: string; tools: MCPTool[] }>;
|
|
59
|
+
|
|
60
|
+
onStageStart?: (stageIndex: number) => void;
|
|
61
|
+
onStageComplete?: (stageIndex: number) => void;
|
|
62
|
+
onStepStart?: (stepId: string) => void;
|
|
63
|
+
onStepComplete?: (stepId: string, artifact: StepArtifact) => void;
|
|
64
|
+
onStepError?: (stepId: string, error: string) => void;
|
|
65
|
+
onGateWaiting?: (stepId: string, prompt: string) => Promise<boolean>;
|
|
66
|
+
onCouncilCreated?: (stepId: string, councilId: string) => void;
|
|
67
|
+
onAgentThinkingStart?: (persona: Persona, startedAt: number, prompt?: string) => void;
|
|
68
|
+
onAgentThinkingEnd?: (persona: Persona) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Platform Adapter (abstracts Tauri / Node.js)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export interface PlatformAdapter {
|
|
76
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
77
|
+
readFile?(path: string): Promise<string | null>;
|
|
78
|
+
runCommand?(cmd: string, cwd: string): Promise<{ stdout: string; stderr: string; exit_code: number; success: boolean }>;
|
|
79
|
+
setWorkingDir(dir: string): void;
|
|
80
|
+
getWorkingDir(): string;
|
|
81
|
+
saveDeliberationOutput?(council: any, mode: 'full' | 'abbreviated'): Promise<string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Input Template Rendering
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prepend a provenance header so downstream steps know what produced the content.
|
|
90
|
+
* Skip header for synthetic initial-input artifacts (stepId === '__initial__').
|
|
91
|
+
*/
|
|
92
|
+
function formatArtifactForInput(artifact: StepArtifact): string {
|
|
93
|
+
if (artifact.stepId === '__initial__') {
|
|
94
|
+
return artifact.content;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lines: string[] = [];
|
|
98
|
+
if (artifact.metadata?.stepName) {
|
|
99
|
+
const typeLabel = artifact.metadata.stepType ? ` (${artifact.metadata.stepType})` : '';
|
|
100
|
+
lines.push(`[Source: ${artifact.metadata.stepName}${typeLabel}]`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const outputType = artifact.metadata?.outputType || 'string';
|
|
104
|
+
const outputPath = artifact.metadata?.outputPath;
|
|
105
|
+
|
|
106
|
+
if (outputType === 'json') {
|
|
107
|
+
lines.push(`[Output type: json]`);
|
|
108
|
+
} else if (outputType === 'directory' && outputPath) {
|
|
109
|
+
lines.push(`[Output type: directory]`);
|
|
110
|
+
lines.push(`[Output directory: ${outputPath}]`);
|
|
111
|
+
lines.push(`IMPORTANT: The previous step produced output in the directory above. Use your tools to list and read the files in that directory to understand the full context of what was produced.`);
|
|
112
|
+
} else if (outputType === 'file' && outputPath) {
|
|
113
|
+
lines.push(`[Output type: file]`);
|
|
114
|
+
lines.push(`[Output file: ${outputPath}]`);
|
|
115
|
+
lines.push(`IMPORTANT: The previous step produced output in the file above. Use your tools to read that file to understand the full context of what was produced.`);
|
|
116
|
+
} else if (outputPath) {
|
|
117
|
+
lines.push(`[Output file: ${outputPath}]`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (lines.length > 0) {
|
|
121
|
+
return lines.join('\n') + '\n\n' + artifact.content;
|
|
122
|
+
}
|
|
123
|
+
return artifact.content;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse artifact content as JSON and walk a dot-separated path.
|
|
128
|
+
* Returns the stringified value at the path, or '' if parsing fails or path not found.
|
|
129
|
+
*/
|
|
130
|
+
function resolveJsonPath(content: string, dotPath: string): string {
|
|
131
|
+
try {
|
|
132
|
+
let obj = JSON.parse(content);
|
|
133
|
+
for (const key of dotPath.split('.')) {
|
|
134
|
+
if (obj == null || typeof obj !== 'object') return '';
|
|
135
|
+
obj = obj[key];
|
|
136
|
+
}
|
|
137
|
+
if (obj === undefined || obj === null) return '';
|
|
138
|
+
return typeof obj === 'string' ? obj : JSON.stringify(obj);
|
|
139
|
+
} catch {
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderInputTemplate(
|
|
145
|
+
template: string,
|
|
146
|
+
previousArtifacts: StepArtifact[],
|
|
147
|
+
memoryCtx?: MemoryContext
|
|
148
|
+
): string {
|
|
149
|
+
// "none" means no input from previous steps — step only sees its task
|
|
150
|
+
if (template === 'none') return '';
|
|
151
|
+
|
|
152
|
+
if (!template || template === '{{input}}') {
|
|
153
|
+
return previousArtifacts.map((a) => formatArtifactForInput(a)).join('\n\n---\n\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let result = template;
|
|
157
|
+
|
|
158
|
+
// Replace {{input}} with all artifacts joined (with provenance headers)
|
|
159
|
+
result = result.replace(
|
|
160
|
+
/\{\{input\}\}/g,
|
|
161
|
+
previousArtifacts.map((a) => formatArtifactForInput(a)).join('\n\n---\n\n')
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Replace {{input[N]}} with specific artifact (with provenance header)
|
|
165
|
+
result = result.replace(/\{\{input\[(\d+)\]\}\}/g, (_match, index) => {
|
|
166
|
+
const i = parseInt(index, 10);
|
|
167
|
+
return previousArtifacts[i] ? formatArtifactForInput(previousArtifacts[i]) : '';
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Replace {{file}} with all output file paths (newline-joined, non-null only)
|
|
171
|
+
result = result.replace(
|
|
172
|
+
/\{\{file\}\}/g,
|
|
173
|
+
previousArtifacts
|
|
174
|
+
.map((a) => a.metadata?.outputPath)
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join('\n')
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Replace {{file[N]}} with specific artifact's file path
|
|
180
|
+
result = result.replace(/\{\{file\[(\d+)\]\}\}/g, (_match, index) => {
|
|
181
|
+
const i = parseInt(index, 10);
|
|
182
|
+
return previousArtifacts[i]?.metadata?.outputPath || '';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Replace {{input.fieldName}} with JSON field from last artifact (dot-path walk)
|
|
186
|
+
result = result.replace(/\{\{input\.([a-zA-Z0-9_.]+)\}\}/g, (_match, path) => {
|
|
187
|
+
const last = previousArtifacts[previousArtifacts.length - 1];
|
|
188
|
+
if (!last) return '';
|
|
189
|
+
return resolveJsonPath(last.content, path);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Replace {{input[N].fieldName}} with JSON field from specific artifact
|
|
193
|
+
result = result.replace(/\{\{input\[(\d+)\]\.([a-zA-Z0-9_.]+)\}\}/g, (_match, index, path) => {
|
|
194
|
+
const i = parseInt(index, 10);
|
|
195
|
+
if (!previousArtifacts[i]) return '';
|
|
196
|
+
return resolveJsonPath(previousArtifacts[i].content, path);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---- Memory template variables ----
|
|
200
|
+
if (memoryCtx) {
|
|
201
|
+
// {{memory}} — all entries
|
|
202
|
+
result = result.replace(/\{\{memory\}\}/g, memoryCtx.all);
|
|
203
|
+
|
|
204
|
+
// {{memory.last_n(N)}} — last N entries (must be before {{memory.last.X}})
|
|
205
|
+
result = result.replace(/\{\{memory\.last_n\((\d+)\)\}\}/g, (_match, n) => {
|
|
206
|
+
return memoryCtx.lastN(parseInt(n, 10));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// {{memory.last.step_name}} — specific capture from last entry
|
|
210
|
+
result = result.replace(/\{\{memory\.last\.([a-zA-Z0-9_]+)\}\}/g, (_match, stepName) => {
|
|
211
|
+
return memoryCtx.lastCapture(stepName);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// {{memory.last}} — most recent entry
|
|
215
|
+
result = result.replace(/\{\{memory\.last\}\}/g, memoryCtx.last);
|
|
216
|
+
|
|
217
|
+
// {{memory.patterns}} — compressed pattern summaries
|
|
218
|
+
result = result.replace(/\{\{memory\.patterns\}\}/g, memoryCtx.patterns);
|
|
219
|
+
} else {
|
|
220
|
+
// No memory — resolve all memory templates to empty string
|
|
221
|
+
result = result.replace(/\{\{memory(?:\.[a-zA-Z0-9_()]+)*\}\}/g, '');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Pipeline Executor
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
export class PipelineExecutor {
|
|
232
|
+
private callbacks: PipelineExecutorCallbacks;
|
|
233
|
+
private platform: PlatformAdapter;
|
|
234
|
+
private aborted = false;
|
|
235
|
+
private runningPipelineId: string | null = null;
|
|
236
|
+
/** Set by condition steps to skip the next stage */
|
|
237
|
+
private skipNextStage = false;
|
|
238
|
+
/** Set by condition steps to stop the pipeline (completes, not fails) */
|
|
239
|
+
private stopPipeline = false;
|
|
240
|
+
/** Memory context loaded at pipeline start (if maintainMemory is enabled) */
|
|
241
|
+
private memoryCtx: MemoryContext | undefined = undefined;
|
|
242
|
+
/** Current run directory for output isolation */
|
|
243
|
+
private currentRunDir: string | null = null;
|
|
244
|
+
/** Current run number */
|
|
245
|
+
private currentRunNumber = 0;
|
|
246
|
+
/** Pipeline start timestamp */
|
|
247
|
+
private runStartedAt: string | null = null;
|
|
248
|
+
|
|
249
|
+
constructor(callbacks: PipelineExecutorCallbacks, platform: PlatformAdapter) {
|
|
250
|
+
this.callbacks = callbacks;
|
|
251
|
+
this.platform = platform;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Run a pipeline from its current stage index.
|
|
256
|
+
* Supports resume — skips completed stages.
|
|
257
|
+
*/
|
|
258
|
+
async run(pipelineId: string): Promise<void> {
|
|
259
|
+
const pipeline = pipelineStore.get(pipelineId);
|
|
260
|
+
if (!pipeline) throw new Error(`Pipeline not found: ${pipelineId}`);
|
|
261
|
+
|
|
262
|
+
if (pipeline.stages.length === 0) {
|
|
263
|
+
throw new Error('Pipeline has no stages');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.aborted = false;
|
|
267
|
+
this.skipNextStage = false;
|
|
268
|
+
this.stopPipeline = false;
|
|
269
|
+
this.runningPipelineId = pipelineId;
|
|
270
|
+
this.memoryCtx = undefined;
|
|
271
|
+
this.currentRunDir = null;
|
|
272
|
+
this.currentRunNumber = 0;
|
|
273
|
+
this.runStartedAt = new Date().toISOString();
|
|
274
|
+
pipelineStore.setPipelineStatus(pipelineId, 'running');
|
|
275
|
+
|
|
276
|
+
const workingDir = pipeline.settings.workingDirectory;
|
|
277
|
+
|
|
278
|
+
// Load memory context if maintainMemory is enabled
|
|
279
|
+
if (workingDir && pipeline.settings.schedule?.maintainMemory) {
|
|
280
|
+
try {
|
|
281
|
+
this.memoryCtx = await buildMemoryContext(this.platform, workingDir, pipelineId);
|
|
282
|
+
console.log('[PipelineExecutor] Loaded memory context for pipeline');
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.warn('[PipelineExecutor] Failed to load memory context:', err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Set up run output directory
|
|
289
|
+
const outputConfig = pipeline.settings.outputConfig;
|
|
290
|
+
if (workingDir && outputConfig?.enabled !== false) {
|
|
291
|
+
try {
|
|
292
|
+
this.currentRunNumber = await getNextRunNumber(this.platform, workingDir, pipelineId);
|
|
293
|
+
const runDirName = buildRunDirName(pipeline.name, this.currentRunNumber, new Date());
|
|
294
|
+
const runsBase = getRunsBaseDir(workingDir);
|
|
295
|
+
this.currentRunDir = `${runsBase}/${runDirName}`;
|
|
296
|
+
console.log(`[PipelineExecutor] Run output dir: ${this.currentRunDir}`);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.warn('[PipelineExecutor] Failed to set up run directory:', err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
for (let i = pipeline.currentStageIndex; i < pipeline.stages.length; i++) {
|
|
304
|
+
if (this.aborted) {
|
|
305
|
+
return; // status already set by abort()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Refresh pipeline state
|
|
309
|
+
const current = pipelineStore.get(pipelineId);
|
|
310
|
+
if (!current) throw new Error('Pipeline disappeared');
|
|
311
|
+
|
|
312
|
+
const stage = current.stages[i];
|
|
313
|
+
|
|
314
|
+
// Skip completed stages (for resume)
|
|
315
|
+
if (stage.steps.every((s) => s.status === 'completed' || s.status === 'skipped')) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check if a condition step requested skipping this stage
|
|
320
|
+
if (this.skipNextStage) {
|
|
321
|
+
this.skipNextStage = false;
|
|
322
|
+
// Mark all steps in this stage as skipped
|
|
323
|
+
for (const step of stage.steps) {
|
|
324
|
+
if (step.status !== 'completed' && step.status !== 'skipped') {
|
|
325
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'skipped');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
pipelineStore.advanceStage(pipelineId);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Collect previous stage artifacts (or initial input for stage 0)
|
|
333
|
+
const previousArtifacts = this.collectPreviousArtifacts(current, i);
|
|
334
|
+
|
|
335
|
+
this.callbacks.onStageStart?.(i);
|
|
336
|
+
|
|
337
|
+
// Run all steps in this stage
|
|
338
|
+
await this.runStage(pipelineId, stage, previousArtifacts, current.settings.failurePolicy, current.settings);
|
|
339
|
+
|
|
340
|
+
// Check abort again after stage completes (don't advance if aborted)
|
|
341
|
+
if (this.aborted) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
this.callbacks.onStageComplete?.(i);
|
|
346
|
+
|
|
347
|
+
// Advance stage index
|
|
348
|
+
pipelineStore.advanceStage(pipelineId);
|
|
349
|
+
|
|
350
|
+
// Check if a condition step requested stopping after this stage
|
|
351
|
+
if (this.stopPipeline) {
|
|
352
|
+
// Skip remaining stages
|
|
353
|
+
const updated = pipelineStore.get(pipelineId);
|
|
354
|
+
if (updated) {
|
|
355
|
+
for (let j = i + 1; j < updated.stages.length; j++) {
|
|
356
|
+
for (const step of updated.stages[j].steps) {
|
|
357
|
+
if (step.status === 'pending') {
|
|
358
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'skipped');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!this.aborted) {
|
|
368
|
+
pipelineStore.setPipelineStatus(pipelineId, 'completed');
|
|
369
|
+
|
|
370
|
+
// Post-completion: capture memory and write run manifest
|
|
371
|
+
const completedPipeline = pipelineStore.get(pipelineId);
|
|
372
|
+
let memoryUpdated = false;
|
|
373
|
+
|
|
374
|
+
if (completedPipeline) {
|
|
375
|
+
// Capture memory if enabled
|
|
376
|
+
if (workingDir && completedPipeline.settings.schedule?.maintainMemory) {
|
|
377
|
+
try {
|
|
378
|
+
await this.captureMemory(completedPipeline, workingDir);
|
|
379
|
+
memoryUpdated = true;
|
|
380
|
+
console.log('[PipelineExecutor] Memory entry captured');
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.warn('[PipelineExecutor] Failed to capture memory:', err);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Write run manifest
|
|
387
|
+
if (this.currentRunDir) {
|
|
388
|
+
try {
|
|
389
|
+
await this.writeManifest(completedPipeline, 'completed', memoryUpdated);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.warn('[PipelineExecutor] Failed to write run manifest:', err);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Prune old runs
|
|
396
|
+
const maxRetained = completedPipeline.settings.outputConfig?.maxRetainedRuns;
|
|
397
|
+
if (workingDir && maxRetained && maxRetained > 0) {
|
|
398
|
+
try {
|
|
399
|
+
const runsBase = getRunsBaseDir(workingDir);
|
|
400
|
+
await pruneOldRuns(this.platform, runsBase, completedPipeline.name, maxRetained);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.warn('[PipelineExecutor] Failed to prune old runs:', err);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (this.aborted) return; // don't overwrite 'paused' status on abort
|
|
409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
410
|
+
console.error('[PipelineExecutor] Pipeline failed:', message);
|
|
411
|
+
pipelineStore.setPipelineStatus(pipelineId, 'failed');
|
|
412
|
+
|
|
413
|
+
// Write failed manifest if we have a run directory
|
|
414
|
+
if (this.currentRunDir) {
|
|
415
|
+
const failedPipeline = pipelineStore.get(pipelineId);
|
|
416
|
+
if (failedPipeline) {
|
|
417
|
+
try {
|
|
418
|
+
await this.writeManifest(failedPipeline, 'failed', false);
|
|
419
|
+
} catch { /* best effort */ }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
throw error;
|
|
424
|
+
} finally {
|
|
425
|
+
this.runningPipelineId = null;
|
|
426
|
+
this.memoryCtx = undefined;
|
|
427
|
+
this.currentRunDir = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Abort a running pipeline. Sets status to 'paused' immediately and marks
|
|
433
|
+
* any currently-running steps as failed so the UI updates right away.
|
|
434
|
+
*/
|
|
435
|
+
abort(): void {
|
|
436
|
+
this.aborted = true;
|
|
437
|
+
if (this.runningPipelineId) {
|
|
438
|
+
const pipeline = pipelineStore.get(this.runningPipelineId);
|
|
439
|
+
if (pipeline) {
|
|
440
|
+
// Mark any running steps as failed
|
|
441
|
+
for (const stage of pipeline.stages) {
|
|
442
|
+
for (const step of stage.steps) {
|
|
443
|
+
if (step.status === 'running') {
|
|
444
|
+
pipelineStore.setStepStatus(this.runningPipelineId, step.id, 'failed', 'Aborted by user');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
pipelineStore.setPipelineStatus(this.runningPipelineId, 'failed');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --------------------------------------------------------------------------
|
|
454
|
+
// Stage Execution
|
|
455
|
+
// --------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
private collectPreviousArtifacts(
|
|
458
|
+
pipeline: Pipeline,
|
|
459
|
+
currentStageIndex: number
|
|
460
|
+
): StepArtifact[] {
|
|
461
|
+
if (currentStageIndex === 0) {
|
|
462
|
+
// For stage 0, create a synthetic artifact from initialInput
|
|
463
|
+
if (!pipeline.initialInput) return [];
|
|
464
|
+
return [
|
|
465
|
+
{
|
|
466
|
+
stepId: '__initial__',
|
|
467
|
+
content: pipeline.initialInput,
|
|
468
|
+
artifactType: 'output',
|
|
469
|
+
createdAt: new Date().toISOString(),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const previousStage = pipeline.stages[currentStageIndex - 1];
|
|
475
|
+
return previousStage.steps
|
|
476
|
+
.filter((s) => s.artifact)
|
|
477
|
+
.map((s) => s.artifact!);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private async runStage(
|
|
481
|
+
pipelineId: string,
|
|
482
|
+
stage: PipelineStage,
|
|
483
|
+
previousArtifacts: StepArtifact[],
|
|
484
|
+
failurePolicy: 'stop' | 'skip_step',
|
|
485
|
+
pipelineSettings: Pipeline['settings']
|
|
486
|
+
): Promise<void> {
|
|
487
|
+
const mode = stage.executionMode || 'sequential';
|
|
488
|
+
|
|
489
|
+
if (mode === 'parallel') {
|
|
490
|
+
// Run all steps concurrently
|
|
491
|
+
const results = await Promise.allSettled(
|
|
492
|
+
stage.steps.map((step) =>
|
|
493
|
+
this.runStep(pipelineId, step, previousArtifacts, pipelineSettings)
|
|
494
|
+
)
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
for (let i = 0; i < results.length; i++) {
|
|
498
|
+
const result = results[i];
|
|
499
|
+
if (result.status === 'rejected') {
|
|
500
|
+
const error = result.reason instanceof Error
|
|
501
|
+
? result.reason.message
|
|
502
|
+
: String(result.reason);
|
|
503
|
+
|
|
504
|
+
if (failurePolicy === 'stop') {
|
|
505
|
+
throw new Error(`Step "${stage.steps[i].name}" failed: ${error}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// Run steps one at a time, in order.
|
|
511
|
+
// Accumulate artifacts so later steps can reference earlier steps' outputs.
|
|
512
|
+
const accumulatedArtifacts = [...previousArtifacts];
|
|
513
|
+
for (const step of stage.steps) {
|
|
514
|
+
if (this.aborted || this.stopPipeline) return;
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
await this.runStep(pipelineId, step, accumulatedArtifacts, pipelineSettings);
|
|
518
|
+
// Add completed step's artifact for subsequent steps
|
|
519
|
+
const updated = pipelineStore.get(pipelineId);
|
|
520
|
+
const updatedStep = updated?.stages.flatMap(s => s.steps).find(s => s.id === step.id);
|
|
521
|
+
if (updatedStep?.artifact) {
|
|
522
|
+
accumulatedArtifacts.push(updatedStep.artifact);
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (failurePolicy === 'stop') {
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
// skip_step: already marked as failed in runStep
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// --------------------------------------------------------------------------
|
|
535
|
+
// Step Dispatch
|
|
536
|
+
// --------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
private async runStep(
|
|
539
|
+
pipelineId: string,
|
|
540
|
+
step: PipelineStep,
|
|
541
|
+
previousArtifacts: StepArtifact[],
|
|
542
|
+
pipelineSettings: Pipeline['settings']
|
|
543
|
+
): Promise<void> {
|
|
544
|
+
// Skip already completed steps (for resume)
|
|
545
|
+
if (step.status === 'completed' || step.status === 'skipped') return;
|
|
546
|
+
|
|
547
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'running');
|
|
548
|
+
this.callbacks.onStepStart?.(step.id);
|
|
549
|
+
|
|
550
|
+
// Per-step context — avoids mutating shared this.callbacks (race-safe for parallel steps)
|
|
551
|
+
const stepCtx = { councilId: null as string | null };
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
let artifact: StepArtifact;
|
|
555
|
+
|
|
556
|
+
if (step.config.type === 'gate') {
|
|
557
|
+
artifact = await this.runGateStep(pipelineId, step);
|
|
558
|
+
} else if (step.config.type === 'script') {
|
|
559
|
+
artifact = await this.runScriptStep(pipelineId, step, previousArtifacts);
|
|
560
|
+
} else if (step.config.type === 'condition') {
|
|
561
|
+
artifact = await this.runConditionStep(pipelineId, step, previousArtifacts);
|
|
562
|
+
} else {
|
|
563
|
+
// Convert LLM steps (decisioning/execution) to lightweight council configs
|
|
564
|
+
const councilStep = this.normalizeToCouncilStep(step);
|
|
565
|
+
artifact = await this.runCouncilStep(pipelineId, councilStep, previousArtifacts, pipelineSettings, stepCtx);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
pipelineStore.setStepArtifact(pipelineId, step.id, artifact);
|
|
569
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'completed');
|
|
570
|
+
|
|
571
|
+
// Write step output to isolated run directory
|
|
572
|
+
if (this.currentRunDir) {
|
|
573
|
+
try {
|
|
574
|
+
const loc = this.findStepLocation(pipelineId, step.id);
|
|
575
|
+
if (loc) {
|
|
576
|
+
const stepDir = buildStepOutputDir(
|
|
577
|
+
this.currentRunDir, loc.stageIndex, loc.stageName, loc.stepIndex, step.name
|
|
578
|
+
);
|
|
579
|
+
const meta: StepMeta = {
|
|
580
|
+
stepId: step.id,
|
|
581
|
+
stepName: step.name,
|
|
582
|
+
stepType: step.config.type,
|
|
583
|
+
stageIndex: loc.stageIndex,
|
|
584
|
+
stepIndex: loc.stepIndex,
|
|
585
|
+
startedAt: step.startedAt,
|
|
586
|
+
completedAt: step.completedAt,
|
|
587
|
+
status: 'completed',
|
|
588
|
+
outputType: (step.config as any).outputType || 'string',
|
|
589
|
+
councilId: artifact.metadata?.councilId,
|
|
590
|
+
model: artifact.metadata?.model,
|
|
591
|
+
tokensUsed: artifact.metadata?.tokensUsed,
|
|
592
|
+
};
|
|
593
|
+
const outputPath = await writeStepOutput(this.platform, stepDir, artifact, meta);
|
|
594
|
+
console.log(`[PipelineExecutor] Step output: ${outputPath}`);
|
|
595
|
+
// Update artifact metadata to point to the isolated output file
|
|
596
|
+
artifact.metadata = { ...artifact.metadata, outputPath };
|
|
597
|
+
pipelineStore.setStepArtifact(pipelineId, step.id, artifact);
|
|
598
|
+
}
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.warn('[PipelineExecutor] Failed to write step output:', err);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.callbacks.onStepComplete?.(step.id, artifact);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
607
|
+
|
|
608
|
+
// For steps that created a council before failing, write a partial
|
|
609
|
+
// artifact so the UI can still link to the deliberation ledger
|
|
610
|
+
if (stepCtx.councilId) {
|
|
611
|
+
pipelineStore.setStepArtifact(pipelineId, step.id, {
|
|
612
|
+
stepId: step.id,
|
|
613
|
+
content: `Step failed: ${message}`,
|
|
614
|
+
artifactType: 'output',
|
|
615
|
+
metadata: { councilId: stepCtx.councilId, stepName: step.name, stepType: step.config.type },
|
|
616
|
+
createdAt: new Date().toISOString(),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'failed', message);
|
|
621
|
+
this.callbacks.onStepError?.(step.id, message);
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Ensure a step has a CouncilStepConfig.
|
|
628
|
+
* Steps with councilSetup (new format) pass through unchanged.
|
|
629
|
+
* Legacy LlmStepConfig (flat model/provider/systemPrompt) is migrated.
|
|
630
|
+
*/
|
|
631
|
+
private normalizeToCouncilStep(step: PipelineStep): PipelineStep {
|
|
632
|
+
const config = step.config;
|
|
633
|
+
|
|
634
|
+
// Already a CouncilStepConfig — has councilSetup
|
|
635
|
+
if ('councilSetup' in config) {
|
|
636
|
+
return step;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Legacy LlmStepConfig — migrate to CouncilStepConfig
|
|
640
|
+
const migrated = migrateLlmConfig(config as LlmStepConfig);
|
|
641
|
+
return { ...step, config: migrated };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// --------------------------------------------------------------------------
|
|
645
|
+
// Council Step
|
|
646
|
+
// --------------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
private async runCouncilStep(
|
|
649
|
+
pipelineId: string,
|
|
650
|
+
step: PipelineStep,
|
|
651
|
+
previousArtifacts: StepArtifact[],
|
|
652
|
+
pipelineSettings: Pipeline['settings'],
|
|
653
|
+
stepCtx: { councilId: string | null }
|
|
654
|
+
): Promise<StepArtifact> {
|
|
655
|
+
const config = step.config as CouncilStepConfig;
|
|
656
|
+
|
|
657
|
+
// Build the problem: task (instructions) + pipeline input (optional) + input (context from previous steps)
|
|
658
|
+
const inputContext = renderInputTemplate(config.inputTemplate, previousArtifacts, this.memoryCtx);
|
|
659
|
+
const pipelineInput = config.includePipelineInput
|
|
660
|
+
? pipelineStore.get(pipelineId)?.initialInput || ''
|
|
661
|
+
: '';
|
|
662
|
+
const parts = [config.task, pipelineInput, inputContext].filter(Boolean);
|
|
663
|
+
const rawProblem = parts.join('\n\n---\n\n');
|
|
664
|
+
|
|
665
|
+
// Pre-flight: verify MCP tools referenced in step prompts are actually available
|
|
666
|
+
if (this.callbacks.getAvailableTools) {
|
|
667
|
+
const promptText = [
|
|
668
|
+
config.task || '',
|
|
669
|
+
...config.councilSetup.personas.map(p => p.systemPrompt || ''),
|
|
670
|
+
].join('\n');
|
|
671
|
+
verifyRequiredTools(this.callbacks.getAvailableTools(), promptText, step.name);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Resolve effective working directory with inheritance (default: constrained)
|
|
675
|
+
const isConstrained = pipelineSettings.directoryConstrained !== false;
|
|
676
|
+
const effectiveDir = isConstrained
|
|
677
|
+
? pipelineSettings.workingDirectory
|
|
678
|
+
: config.councilSetup.workingDirectory || pipelineSettings.workingDirectory;
|
|
679
|
+
|
|
680
|
+
// Create council via factory
|
|
681
|
+
const council = createCouncilFromSetup({
|
|
682
|
+
...config.councilSetup,
|
|
683
|
+
task: config.task,
|
|
684
|
+
topic: rawProblem.slice(0, 200),
|
|
685
|
+
workingDirectory: effectiveDir,
|
|
686
|
+
directoryConstrained: isConstrained,
|
|
687
|
+
saveDeliberation: true,
|
|
688
|
+
saveDeliberationMode: 'full',
|
|
689
|
+
stepType: config.type,
|
|
690
|
+
pipelinePrefix: '[Pipeline]',
|
|
691
|
+
pipelineId: pipelineId,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
stepCtx.councilId = council.id;
|
|
695
|
+
this.callbacks.onCouncilCreated?.(step.id, council.id);
|
|
696
|
+
|
|
697
|
+
// Branch: coding steps use CodingOrchestrator, planning uses DeliberationOrchestrator
|
|
698
|
+
const orchestratorCallbacks = {
|
|
699
|
+
invokeAgent: this.callbacks.invokeAgent,
|
|
700
|
+
onPhaseChange: (from: any, to: any) =>
|
|
701
|
+
console.log(`[Pipeline:Council] Phase: ${from} -> ${to}`),
|
|
702
|
+
onError: (err: Error, ctx: string) =>
|
|
703
|
+
console.error(`[Pipeline:Council] Error in ${ctx}:`, err),
|
|
704
|
+
onAgentThinkingStart: this.callbacks.onAgentThinkingStart,
|
|
705
|
+
onAgentThinkingEnd: this.callbacks.onAgentThinkingEnd,
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
if (config.type === 'coding') {
|
|
709
|
+
const codingOrchestrator = new CodingOrchestrator({
|
|
710
|
+
...orchestratorCallbacks,
|
|
711
|
+
runCommand: this.platform.runCommand,
|
|
712
|
+
readFile: this.platform.readFile,
|
|
713
|
+
});
|
|
714
|
+
await codingOrchestrator.runCodingWorkflow(council, rawProblem);
|
|
715
|
+
} else {
|
|
716
|
+
const deliberator = new DeliberationOrchestrator(orchestratorCallbacks);
|
|
717
|
+
await deliberator.runFullDeliberation(council, rawProblem);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Generate summary and save to disk (normally done by React useEffect, but
|
|
721
|
+
// pipeline executor doesn't render DeliberationView)
|
|
722
|
+
let workerOutputPath: string | undefined;
|
|
723
|
+
const completedCouncil = councilStore.get(council.id);
|
|
724
|
+
if (completedCouncil) {
|
|
725
|
+
const summary = buildAbbreviatedSummary(completedCouncil);
|
|
726
|
+
councilStore.updateDeliberationState(council.id, { completionSummary: summary });
|
|
727
|
+
|
|
728
|
+
// Save deliberation output to working directory
|
|
729
|
+
if (effectiveDir) {
|
|
730
|
+
try {
|
|
731
|
+
if (this.platform.saveDeliberationOutput) {
|
|
732
|
+
const outputDir = await this.platform.saveDeliberationOutput(completedCouncil, 'full');
|
|
733
|
+
console.log(`[Pipeline:Council] Saved deliberation output to: ${outputDir}`);
|
|
734
|
+
}
|
|
735
|
+
} catch (err) {
|
|
736
|
+
console.error('[Pipeline:Council] Failed to save deliberation output:', err);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Save worker output to run directory root (if output isolation active)
|
|
740
|
+
// Falls back to working directory if no run directory
|
|
741
|
+
const workerPersona = config.councilSetup.personas.find((p) => p.role === 'worker');
|
|
742
|
+
if (workerPersona && workerPersona.saveOutput !== false) {
|
|
743
|
+
const workerOutput = getLatestOutput(council.id);
|
|
744
|
+
if (workerOutput?.content) {
|
|
745
|
+
try {
|
|
746
|
+
const safeName = config.councilSetup.name
|
|
747
|
+
.toLowerCase().replace(/[^a-z0-9_-]/g, '_').replace(/_+/g, '_').slice(0, 50);
|
|
748
|
+
const suffix = config.type === 'coding' ? '_code.md' : config.type === 'review' ? '_review.md' : config.type === 'enrich' ? '_enrichment.md' : config.type === 'code_planning' ? '_plan.md' : '_output.md';
|
|
749
|
+
const outputBase = this.currentRunDir || effectiveDir.replace(/\/$/, '');
|
|
750
|
+
workerOutputPath = `${outputBase}/${safeName}${suffix}`;
|
|
751
|
+
await this.platform.writeFile(workerOutputPath, workerOutput.content);
|
|
752
|
+
console.log(`[Pipeline:Council] Saved worker output to: ${workerOutputPath}`);
|
|
753
|
+
} catch (err) {
|
|
754
|
+
console.error('[Pipeline:Council] Failed to save worker output:', err);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Extract artifact — decisioning steps use the decision, all others use worker output
|
|
762
|
+
const updatedCouncil = councilStore.get(council.id);
|
|
763
|
+
let content = '';
|
|
764
|
+
let artifactType: StepArtifact['artifactType'] = 'output';
|
|
765
|
+
const metadata: StepArtifact['metadata'] = {
|
|
766
|
+
councilId: council.id,
|
|
767
|
+
outputPath: workerOutputPath,
|
|
768
|
+
outputType: config.outputType || 'string',
|
|
769
|
+
stepName: step.name,
|
|
770
|
+
stepType: config.type,
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
if (config.type === 'analysis') {
|
|
774
|
+
const decision = getDecision(council.id);
|
|
775
|
+
if (decision?.content) {
|
|
776
|
+
content = decision.content;
|
|
777
|
+
artifactType = 'decision';
|
|
778
|
+
metadata.decisionId = decision.id;
|
|
779
|
+
} else {
|
|
780
|
+
// Lightweight analysis goes through runDirectExecution → worker output
|
|
781
|
+
const output = getLatestOutput(council.id);
|
|
782
|
+
content = output?.content || 'No decision or output was produced.';
|
|
783
|
+
artifactType = 'output';
|
|
784
|
+
metadata.outputId = output?.id;
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
const output = getLatestOutput(council.id);
|
|
788
|
+
content = output?.content || 'No output was produced.';
|
|
789
|
+
artifactType = 'output';
|
|
790
|
+
metadata.outputId = output?.id;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Strip the localStorage copy of this council's metadata to keep
|
|
794
|
+
// the mcp-councils key small. The authoritative council data
|
|
795
|
+
// (ledger, context, decision, etc.) remains in the in-memory
|
|
796
|
+
// CouncilDataStore, so it stays accessible for the rest of this session.
|
|
797
|
+
try {
|
|
798
|
+
stripCompletedCouncil(council.id);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
console.warn('[Pipeline] Non-fatal: failed to strip council localStorage copy:', err);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
stepId: step.id,
|
|
805
|
+
content,
|
|
806
|
+
artifactType,
|
|
807
|
+
metadata,
|
|
808
|
+
createdAt: new Date().toISOString(),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// --------------------------------------------------------------------------
|
|
813
|
+
// Script Step
|
|
814
|
+
// --------------------------------------------------------------------------
|
|
815
|
+
|
|
816
|
+
private async runScriptStep(
|
|
817
|
+
pipelineId: string,
|
|
818
|
+
step: PipelineStep,
|
|
819
|
+
previousArtifacts: StepArtifact[]
|
|
820
|
+
): Promise<StepArtifact> {
|
|
821
|
+
const config = step.config as ScriptStepConfig;
|
|
822
|
+
|
|
823
|
+
if (!this.platform.runCommand) {
|
|
824
|
+
throw new Error('Script steps require a platform with runCommand support');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!config.command.trim()) {
|
|
828
|
+
throw new Error('Script step has no command configured');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Render input from previous steps and export as $KONDI_INPUT env var.
|
|
832
|
+
// The input is shell-escaped and passed via env var to avoid injection.
|
|
833
|
+
const stepInput = renderInputTemplate(config.inputTemplate, previousArtifacts, this.memoryCtx);
|
|
834
|
+
const pipelineInput = config.includePipelineInput
|
|
835
|
+
? pipelineStore.get(pipelineId)?.initialInput || ''
|
|
836
|
+
: '';
|
|
837
|
+
const inputContext = [pipelineInput, stepInput].filter(Boolean).join('\n\n---\n\n');
|
|
838
|
+
const escaped = inputContext.replace(/'/g, "'\\''");
|
|
839
|
+
const command = `export KONDI_INPUT='${escaped}'\n${config.command}`;
|
|
840
|
+
|
|
841
|
+
const cwd = this.platform.getWorkingDir();
|
|
842
|
+
const result = await this.platform.runCommand(command, cwd);
|
|
843
|
+
|
|
844
|
+
if (!result.success) {
|
|
845
|
+
const errorDetail = result.stderr || result.stdout || `exit code ${result.exit_code}`;
|
|
846
|
+
throw new Error(`Script failed: ${errorDetail}`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
stepId: step.id,
|
|
851
|
+
content: result.stdout,
|
|
852
|
+
artifactType: 'output',
|
|
853
|
+
metadata: {
|
|
854
|
+
stepName: step.name,
|
|
855
|
+
stepType: 'script',
|
|
856
|
+
outputType: config.outputType || 'string',
|
|
857
|
+
},
|
|
858
|
+
createdAt: new Date().toISOString(),
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// --------------------------------------------------------------------------
|
|
863
|
+
// Condition Step
|
|
864
|
+
// --------------------------------------------------------------------------
|
|
865
|
+
|
|
866
|
+
private async runConditionStep(
|
|
867
|
+
pipelineId: string,
|
|
868
|
+
step: PipelineStep,
|
|
869
|
+
previousArtifacts: StepArtifact[]
|
|
870
|
+
): Promise<StepArtifact> {
|
|
871
|
+
const config = step.config as ConditionStepConfig;
|
|
872
|
+
|
|
873
|
+
if (!config.expression) {
|
|
874
|
+
throw new Error('Condition step has no expression configured');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const stepInput = renderInputTemplate(config.inputTemplate, previousArtifacts, this.memoryCtx);
|
|
878
|
+
const pipelineInput = config.includePipelineInput
|
|
879
|
+
? pipelineStore.get(pipelineId)?.initialInput || ''
|
|
880
|
+
: '';
|
|
881
|
+
const inputContext = [pipelineInput, stepInput].filter(Boolean).join('\n\n---\n\n');
|
|
882
|
+
|
|
883
|
+
// Evaluate the condition
|
|
884
|
+
let matches = false;
|
|
885
|
+
switch (config.mode) {
|
|
886
|
+
case 'contains':
|
|
887
|
+
matches = inputContext.includes(config.expression);
|
|
888
|
+
break;
|
|
889
|
+
case 'equals':
|
|
890
|
+
matches = inputContext.trim() === config.expression.trim();
|
|
891
|
+
break;
|
|
892
|
+
case 'regex':
|
|
893
|
+
try {
|
|
894
|
+
matches = new RegExp(config.expression).test(inputContext);
|
|
895
|
+
} catch {
|
|
896
|
+
throw new Error(`Invalid regex in condition: ${config.expression}`);
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const action = matches ? config.trueAction : config.falseAction;
|
|
902
|
+
const resultLabel = matches ? 'TRUE' : 'FALSE';
|
|
903
|
+
|
|
904
|
+
// Apply the action
|
|
905
|
+
if (action === 'skip_next_stage') {
|
|
906
|
+
this.skipNextStage = true;
|
|
907
|
+
} else if (action === 'stop') {
|
|
908
|
+
this.stopPipeline = true;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
stepId: step.id,
|
|
913
|
+
content: `Condition evaluated: ${resultLabel} (mode: ${config.mode}, expression: "${config.expression}"). Action: ${action}.`,
|
|
914
|
+
artifactType: 'output',
|
|
915
|
+
metadata: { stepName: step.name, stepType: 'condition' },
|
|
916
|
+
createdAt: new Date().toISOString(),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// --------------------------------------------------------------------------
|
|
921
|
+
// Gate Step
|
|
922
|
+
// --------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
private async runGateStep(
|
|
925
|
+
pipelineId: string,
|
|
926
|
+
step: PipelineStep
|
|
927
|
+
): Promise<StepArtifact> {
|
|
928
|
+
const config = step.config as GateStepConfig;
|
|
929
|
+
|
|
930
|
+
pipelineStore.setStepStatus(pipelineId, step.id, 'waiting_approval');
|
|
931
|
+
|
|
932
|
+
if (!this.callbacks.onGateWaiting) {
|
|
933
|
+
throw new Error('No gate approval handler configured');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const approved = await this.callbacks.onGateWaiting(step.id, config.approvalPrompt);
|
|
937
|
+
|
|
938
|
+
if (!approved) {
|
|
939
|
+
throw new Error('Gate step rejected by user');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
stepId: step.id,
|
|
944
|
+
content: 'Approved',
|
|
945
|
+
artifactType: 'approval',
|
|
946
|
+
createdAt: new Date().toISOString(),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// --------------------------------------------------------------------------
|
|
951
|
+
// Memory Capture
|
|
952
|
+
// --------------------------------------------------------------------------
|
|
953
|
+
|
|
954
|
+
private async captureMemory(pipeline: Pipeline, workingDir: string): Promise<void> {
|
|
955
|
+
const schedule = pipeline.settings.schedule;
|
|
956
|
+
if (!schedule?.maintainMemory) return;
|
|
957
|
+
|
|
958
|
+
const allSteps = pipeline.stages.flatMap((s) => s.steps);
|
|
959
|
+
const captureIds = schedule.captureStepIds;
|
|
960
|
+
|
|
961
|
+
// Determine which steps to capture
|
|
962
|
+
let stepsToCapture: PipelineStep[];
|
|
963
|
+
if (captureIds && captureIds.length > 0) {
|
|
964
|
+
stepsToCapture = captureIds
|
|
965
|
+
.map((id) => allSteps.find((s) => s.id === id))
|
|
966
|
+
.filter((s): s is PipelineStep => s !== undefined && s.artifact !== undefined);
|
|
967
|
+
} else {
|
|
968
|
+
// Default: capture last completed step
|
|
969
|
+
const lastCompleted = [...allSteps].reverse().find((s) => s.status === 'completed' && s.artifact);
|
|
970
|
+
stepsToCapture = lastCompleted ? [lastCompleted] : [];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (stepsToCapture.length === 0) return;
|
|
974
|
+
|
|
975
|
+
// Build captures map keyed by sanitized step name
|
|
976
|
+
const captures: Record<string, string> = {};
|
|
977
|
+
for (const step of stepsToCapture) {
|
|
978
|
+
const key = sanitizeFolderName(step.name);
|
|
979
|
+
captures[key] = step.artifact!.content;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const runNumber = this.currentRunNumber || await getNextRunNumber(this.platform, workingDir, pipeline.id);
|
|
983
|
+
|
|
984
|
+
await appendEntry(this.platform, workingDir, pipeline.id, {
|
|
985
|
+
runNumber,
|
|
986
|
+
runDate: new Date().toISOString(),
|
|
987
|
+
captures,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --------------------------------------------------------------------------
|
|
992
|
+
// Run Manifest
|
|
993
|
+
// --------------------------------------------------------------------------
|
|
994
|
+
|
|
995
|
+
private async writeManifest(
|
|
996
|
+
pipeline: Pipeline,
|
|
997
|
+
status: 'completed' | 'failed',
|
|
998
|
+
memoryUpdated: boolean
|
|
999
|
+
): Promise<void> {
|
|
1000
|
+
if (!this.currentRunDir) return;
|
|
1001
|
+
|
|
1002
|
+
const allSteps = pipeline.stages.flatMap((s) => s.steps);
|
|
1003
|
+
const totalTokens = allSteps.reduce(
|
|
1004
|
+
(sum, s) => sum + (s.artifact?.metadata?.tokensUsed || 0), 0
|
|
1005
|
+
);
|
|
1006
|
+
const startMs = this.runStartedAt ? new Date(this.runStartedAt).getTime() : Date.now();
|
|
1007
|
+
|
|
1008
|
+
const manifest: RunManifest = {
|
|
1009
|
+
runNumber: this.currentRunNumber,
|
|
1010
|
+
pipelineId: pipeline.id,
|
|
1011
|
+
pipelineName: pipeline.name,
|
|
1012
|
+
startedAt: this.runStartedAt || new Date().toISOString(),
|
|
1013
|
+
completedAt: new Date().toISOString(),
|
|
1014
|
+
status,
|
|
1015
|
+
initialInput: pipeline.initialInput,
|
|
1016
|
+
stageCount: pipeline.stages.length,
|
|
1017
|
+
stepCount: allSteps.length,
|
|
1018
|
+
totalTokens,
|
|
1019
|
+
totalDurationMs: Date.now() - startMs,
|
|
1020
|
+
memoryUpdated,
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
await writeRunManifest(this.platform, this.currentRunDir, manifest);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// --------------------------------------------------------------------------
|
|
1027
|
+
// Step Location Helper
|
|
1028
|
+
// --------------------------------------------------------------------------
|
|
1029
|
+
|
|
1030
|
+
private findStepLocation(pipelineId: string, stepId: string): {
|
|
1031
|
+
stageIndex: number;
|
|
1032
|
+
stageName: string;
|
|
1033
|
+
stepIndex: number;
|
|
1034
|
+
} | null {
|
|
1035
|
+
const pipeline = pipelineStore.get(pipelineId);
|
|
1036
|
+
if (!pipeline) return null;
|
|
1037
|
+
|
|
1038
|
+
for (let si = 0; si < pipeline.stages.length; si++) {
|
|
1039
|
+
const stage = pipeline.stages[si];
|
|
1040
|
+
for (let sti = 0; sti < stage.steps.length; sti++) {
|
|
1041
|
+
if (stage.steps[sti].id === stepId) {
|
|
1042
|
+
return { stageIndex: si, stageName: stage.name, stepIndex: sti };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|