ctx-cc 4.1.1 → 4.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/README.md +5 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/auto.js +0 -287
- package/src/commits.js +0 -94
- package/src/context.js +0 -241
- package/src/handoff.js +0 -156
- package/src/hooks.js +0 -218
- package/src/lifecycle.js +0 -194
- package/src/metrics.js +0 -198
- package/src/pipeline.js +0 -269
- package/src/review-gate.js +0 -338
- package/src/runner.js +0 -120
- package/src/state.js +0 -267
- package/src/worktree.js +0 -244
package/src/metrics.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
import { readState } from './state.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Collect metrics from STATE.json history and git log.
|
|
8
|
-
* Returns a metrics object with all computed stats.
|
|
9
|
-
*/
|
|
10
|
-
export function collectMetrics(ctxDir, projectDir) {
|
|
11
|
-
const state = readState(ctxDir);
|
|
12
|
-
const prd = loadPrd(ctxDir);
|
|
13
|
-
const gitStats = collectGitStats(projectDir);
|
|
14
|
-
|
|
15
|
-
const agentHistory = state?.agentHistory || [];
|
|
16
|
-
const completedTasks = state?.completedTasks || [];
|
|
17
|
-
const reviewHistory = state?.reviewGate?.history || [];
|
|
18
|
-
|
|
19
|
-
// Stories
|
|
20
|
-
const stories = prd?.stories || [];
|
|
21
|
-
const totalStories = stories.length;
|
|
22
|
-
const passedStories = stories.filter(s => s.passes).length;
|
|
23
|
-
|
|
24
|
-
// Agent metrics
|
|
25
|
-
const agentCounts = {};
|
|
26
|
-
let totalAgentDuration = 0;
|
|
27
|
-
for (const entry of agentHistory) {
|
|
28
|
-
const name = entry.agent?.replace('ctx-', '').replace('.md', '') || 'unknown';
|
|
29
|
-
agentCounts[name] = (agentCounts[name] || 0) + 1;
|
|
30
|
-
if (entry.invokedAt && entry.completedAt) {
|
|
31
|
-
totalAgentDuration += new Date(entry.completedAt) - new Date(entry.invokedAt);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Review metrics
|
|
36
|
-
const totalReviews = reviewHistory.length;
|
|
37
|
-
const passedFirstTry = reviewHistory.filter(r => r.cycle === 1 && r.result === 'pass').length;
|
|
38
|
-
const avgCycles = totalReviews > 0
|
|
39
|
-
? reviewHistory.reduce((sum, r) => sum + r.cycle, 0) / totalReviews
|
|
40
|
-
: 0;
|
|
41
|
-
|
|
42
|
-
// Token estimates (rough: based on context payloads)
|
|
43
|
-
const estimatedTokens = agentHistory.length * 15000; // ~15K per agent invocation average
|
|
44
|
-
|
|
45
|
-
// Time per story
|
|
46
|
-
const avgTimePerStory = completedTasks.length > 0 && state?.session?.startedAt
|
|
47
|
-
? (Date.now() - new Date(state.session.startedAt).getTime()) / Math.max(passedStories, 1)
|
|
48
|
-
: 0;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
stories: {
|
|
52
|
-
total: totalStories,
|
|
53
|
-
passed: passedStories,
|
|
54
|
-
remaining: totalStories - passedStories,
|
|
55
|
-
completionRate: totalStories > 0 ? Math.round(passedStories / totalStories * 100) : 0,
|
|
56
|
-
},
|
|
57
|
-
agents: {
|
|
58
|
-
totalInvocations: agentHistory.length,
|
|
59
|
-
byType: agentCounts,
|
|
60
|
-
totalDurationMs: totalAgentDuration,
|
|
61
|
-
avgDurationMs: agentHistory.length > 0 ? Math.round(totalAgentDuration / agentHistory.length) : 0,
|
|
62
|
-
},
|
|
63
|
-
reviews: {
|
|
64
|
-
total: totalReviews,
|
|
65
|
-
passedFirstTry,
|
|
66
|
-
firstTryRate: totalReviews > 0 ? Math.round(passedFirstTry / totalReviews * 100) : 0,
|
|
67
|
-
avgCycles: Math.round(avgCycles * 10) / 10,
|
|
68
|
-
},
|
|
69
|
-
tasks: {
|
|
70
|
-
completed: completedTasks.length,
|
|
71
|
-
},
|
|
72
|
-
tokens: {
|
|
73
|
-
estimated: estimatedTokens,
|
|
74
|
-
estimatedCost: estimateTokenCost(estimatedTokens),
|
|
75
|
-
},
|
|
76
|
-
git: gitStats,
|
|
77
|
-
session: {
|
|
78
|
-
startedAt: state?.session?.startedAt || null,
|
|
79
|
-
lastActivity: state?.session?.lastActivity || null,
|
|
80
|
-
avgTimePerStoryMs: Math.round(avgTimePerStory),
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Format metrics for human-readable display.
|
|
87
|
-
*/
|
|
88
|
-
export function formatMetrics(metrics) {
|
|
89
|
-
const lines = [];
|
|
90
|
-
|
|
91
|
-
// Stories
|
|
92
|
-
lines.push(' Stories');
|
|
93
|
-
lines.push(` Completed: ${metrics.stories.passed}/${metrics.stories.total} (${metrics.stories.completionRate}%)`);
|
|
94
|
-
lines.push(` Remaining: ${metrics.stories.remaining}`);
|
|
95
|
-
lines.push('');
|
|
96
|
-
|
|
97
|
-
// Agents
|
|
98
|
-
lines.push(' Agent Invocations');
|
|
99
|
-
lines.push(` Total: ${metrics.agents.totalInvocations}`);
|
|
100
|
-
if (Object.keys(metrics.agents.byType).length > 0) {
|
|
101
|
-
const sorted = Object.entries(metrics.agents.byType).sort((a, b) => b[1] - a[1]);
|
|
102
|
-
for (const [name, count] of sorted) {
|
|
103
|
-
lines.push(` ${name.padEnd(20)} ${count}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (metrics.agents.avgDurationMs > 0) {
|
|
107
|
-
lines.push(` Avg duration: ${formatDuration(metrics.agents.avgDurationMs)}`);
|
|
108
|
-
}
|
|
109
|
-
lines.push('');
|
|
110
|
-
|
|
111
|
-
// Reviews
|
|
112
|
-
if (metrics.reviews.total > 0) {
|
|
113
|
-
lines.push(' Review Gate');
|
|
114
|
-
lines.push(` Reviews: ${metrics.reviews.total}`);
|
|
115
|
-
lines.push(` First-try pass: ${metrics.reviews.passedFirstTry} (${metrics.reviews.firstTryRate}%)`);
|
|
116
|
-
lines.push(` Avg cycles: ${metrics.reviews.avgCycles}`);
|
|
117
|
-
lines.push('');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Tokens
|
|
121
|
-
if (metrics.tokens.estimated > 0) {
|
|
122
|
-
lines.push(' Token Usage (estimated)');
|
|
123
|
-
lines.push(` Total: ~${formatNumber(metrics.tokens.estimated)} tokens`);
|
|
124
|
-
lines.push(` Est. cost: ~$${metrics.tokens.estimatedCost.toFixed(2)}`);
|
|
125
|
-
lines.push('');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Git
|
|
129
|
-
if (metrics.git.ctxCommits > 0) {
|
|
130
|
-
lines.push(' Git');
|
|
131
|
-
lines.push(` CTX commits: ${metrics.git.ctxCommits}`);
|
|
132
|
-
lines.push(` Total commits: ${metrics.git.totalCommits}`);
|
|
133
|
-
lines.push('');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Session
|
|
137
|
-
if (metrics.session.avgTimePerStoryMs > 0) {
|
|
138
|
-
lines.push(' Session');
|
|
139
|
-
lines.push(` Avg per story: ${formatDuration(metrics.session.avgTimePerStoryMs)}`);
|
|
140
|
-
lines.push(` Started: ${metrics.session.startedAt || 'N/A'}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return lines.join('\n');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Format metrics as JSON for external processing.
|
|
148
|
-
*/
|
|
149
|
-
export function formatMetricsJson(metrics) {
|
|
150
|
-
return JSON.stringify(metrics, null, 2);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- internal ---
|
|
154
|
-
|
|
155
|
-
function loadPrd(ctxDir) {
|
|
156
|
-
try {
|
|
157
|
-
return JSON.parse(fs.readFileSync(path.join(ctxDir, 'PRD.json'), 'utf-8'));
|
|
158
|
-
} catch {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function collectGitStats(projectDir) {
|
|
164
|
-
try {
|
|
165
|
-
const ctxCommits = parseInt(
|
|
166
|
-
execSync('git log --oneline --grep="^ctx(" --no-color 2>/dev/null | wc -l', {
|
|
167
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
168
|
-
}).trim(), 10) || 0;
|
|
169
|
-
|
|
170
|
-
const totalCommits = parseInt(
|
|
171
|
-
execSync('git rev-list --count HEAD 2>/dev/null', {
|
|
172
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
173
|
-
}).trim(), 10) || 0;
|
|
174
|
-
|
|
175
|
-
return { ctxCommits, totalCommits };
|
|
176
|
-
} catch {
|
|
177
|
-
return { ctxCommits: 0, totalCommits: 0 };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function estimateTokenCost(tokens) {
|
|
182
|
-
// Rough estimate: $3/M input + $15/M output, assume 60/40 split
|
|
183
|
-
const inputTokens = tokens * 0.6;
|
|
184
|
-
const outputTokens = tokens * 0.4;
|
|
185
|
-
return (inputTokens / 1_000_000 * 3) + (outputTokens / 1_000_000 * 15);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatDuration(ms) {
|
|
189
|
-
if (ms < 1000) return `${ms}ms`;
|
|
190
|
-
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
191
|
-
return `${Math.round(ms / 60000)}m`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function formatNumber(n) {
|
|
195
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
196
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
197
|
-
return String(n);
|
|
198
|
-
}
|
package/src/pipeline.js
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { readState, writeState, transitionPhase, logAgentInvocation, completeAgentInvocation } from './state.js';
|
|
4
|
-
import { buildContext } from './context.js';
|
|
5
|
-
import { runAgent } from './runner.js';
|
|
6
|
-
import { discoverAgents } from './agents.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Valid pipeline sequences. Each step maps to an agent file.
|
|
10
|
-
*/
|
|
11
|
-
const AGENT_FILES = {
|
|
12
|
-
plan: 'ctx-planner.md',
|
|
13
|
-
execute: 'ctx-executor.md',
|
|
14
|
-
verify: 'ctx-verifier.md',
|
|
15
|
-
review: 'ctx-reviewer.md',
|
|
16
|
-
audit: 'ctx-auditor.md',
|
|
17
|
-
research: 'ctx-researcher.md',
|
|
18
|
-
debug: 'ctx-debugger.md',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validate that a pipeline sequence is logically valid.
|
|
23
|
-
* Returns { valid, error }.
|
|
24
|
-
*/
|
|
25
|
-
export function validatePipeline(steps) {
|
|
26
|
-
if (!steps || steps.length === 0) {
|
|
27
|
-
return { valid: false, error: 'Pipeline requires at least one step.' };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (const step of steps) {
|
|
31
|
-
if (!AGENT_FILES[step]) {
|
|
32
|
-
const available = Object.keys(AGENT_FILES).join(', ');
|
|
33
|
-
return { valid: false, error: `Unknown pipeline step "${step}". Available: ${available}` };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Validate ordering constraints
|
|
38
|
-
const idx = (s) => steps.indexOf(s);
|
|
39
|
-
if (steps.includes('execute') && steps.includes('plan') && idx('execute') < idx('plan')) {
|
|
40
|
-
return { valid: false, error: 'Cannot execute before planning.' };
|
|
41
|
-
}
|
|
42
|
-
if (steps.includes('verify') && steps.includes('execute') && idx('verify') < idx('execute')) {
|
|
43
|
-
return { valid: false, error: 'Cannot verify before executing.' };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { valid: true, error: null };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a pipeline string like "plan,execute,verify" into steps.
|
|
51
|
-
*/
|
|
52
|
-
export function parsePipelineString(str) {
|
|
53
|
-
return str.split(',').map(s => s.trim()).filter(Boolean);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Execute a pipeline of agents sequentially.
|
|
58
|
-
* Each agent's output (or summary) is passed as context to the next.
|
|
59
|
-
*
|
|
60
|
-
* Options:
|
|
61
|
-
* steps - array of step names: ['plan', 'execute', 'verify']
|
|
62
|
-
* message - initial user message
|
|
63
|
-
* ctxDir - .ctx directory path
|
|
64
|
-
* projectDir - project root
|
|
65
|
-
* agentsDir - agents directory
|
|
66
|
-
* streaming - stream output (default true)
|
|
67
|
-
* timeout - per-agent timeout in ms
|
|
68
|
-
* onStep - callback({ step, index, total, status })
|
|
69
|
-
*
|
|
70
|
-
* Returns { completed: string[], failed: string|null, outputs: object }
|
|
71
|
-
*/
|
|
72
|
-
export async function executePipeline({ steps, message, ctxDir, projectDir, agentsDir, streaming = true, timeout = 300000, onStep = null }) {
|
|
73
|
-
const { valid, error } = validatePipeline(steps);
|
|
74
|
-
if (!valid) throw new Error(error);
|
|
75
|
-
|
|
76
|
-
// Initialize pipeline state
|
|
77
|
-
const pipelineState = {
|
|
78
|
-
steps,
|
|
79
|
-
currentStep: 0,
|
|
80
|
-
status: 'running',
|
|
81
|
-
startedAt: new Date().toISOString(),
|
|
82
|
-
completedSteps: [],
|
|
83
|
-
failedStep: null,
|
|
84
|
-
outputs: {},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Save pipeline state
|
|
88
|
-
const state = readState(ctxDir);
|
|
89
|
-
if (state) {
|
|
90
|
-
state.pipeline = pipelineState;
|
|
91
|
-
writeState(ctxDir, state);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let previousOutput = message;
|
|
95
|
-
|
|
96
|
-
for (let i = 0; i < steps.length; i++) {
|
|
97
|
-
const step = steps[i];
|
|
98
|
-
const agentFile = AGENT_FILES[step];
|
|
99
|
-
const agentPath = path.join(agentsDir, agentFile);
|
|
100
|
-
|
|
101
|
-
// Notify callback
|
|
102
|
-
if (onStep) onStep({ step, index: i, total: steps.length, status: 'starting' });
|
|
103
|
-
|
|
104
|
-
// Update pipeline state
|
|
105
|
-
pipelineState.currentStep = i;
|
|
106
|
-
if (state) {
|
|
107
|
-
state.pipeline = pipelineState;
|
|
108
|
-
writeState(ctxDir, state);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Build agent-specific context
|
|
112
|
-
const { context } = buildContext(step, projectDir, ctxDir);
|
|
113
|
-
|
|
114
|
-
// Compose prompt: previous output feeds into next agent
|
|
115
|
-
const agentMessage = i === 0
|
|
116
|
-
? previousOutput
|
|
117
|
-
: `Previous agent (${steps[i - 1]}) output summary:\n${summarizeOutput(previousOutput)}\n\nOriginal request: ${message}`;
|
|
118
|
-
|
|
119
|
-
// Log invocation
|
|
120
|
-
logAgentInvocation(ctxDir, agentFile, `Pipeline step ${i + 1}/${steps.length}: ${step}`);
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const result = await runAgent({
|
|
124
|
-
agentPath,
|
|
125
|
-
message: agentMessage,
|
|
126
|
-
streaming,
|
|
127
|
-
timeout,
|
|
128
|
-
context,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
completeAgentInvocation(ctxDir, agentFile);
|
|
132
|
-
|
|
133
|
-
// Store output for next agent
|
|
134
|
-
previousOutput = result.stdout || `Step "${step}" completed successfully.`;
|
|
135
|
-
pipelineState.outputs[step] = previousOutput.slice(0, 2000); // Cap stored output
|
|
136
|
-
pipelineState.completedSteps.push(step);
|
|
137
|
-
|
|
138
|
-
// Save step output to file for reference
|
|
139
|
-
saveStepOutput(ctxDir, step, i, previousOutput);
|
|
140
|
-
|
|
141
|
-
if (onStep) onStep({ step, index: i, total: steps.length, status: 'completed' });
|
|
142
|
-
|
|
143
|
-
} catch (err) {
|
|
144
|
-
pipelineState.failedStep = step;
|
|
145
|
-
pipelineState.status = 'failed';
|
|
146
|
-
pipelineState.failedAt = new Date().toISOString();
|
|
147
|
-
pipelineState.failureReason = err.message;
|
|
148
|
-
|
|
149
|
-
if (state) {
|
|
150
|
-
state.pipeline = pipelineState;
|
|
151
|
-
writeState(ctxDir, state);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (onStep) onStep({ step, index: i, total: steps.length, status: 'failed' });
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
completed: pipelineState.completedSteps,
|
|
158
|
-
failed: step,
|
|
159
|
-
error: err.message,
|
|
160
|
-
outputs: pipelineState.outputs,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Pipeline complete
|
|
166
|
-
pipelineState.status = 'completed';
|
|
167
|
-
pipelineState.completedAt = new Date().toISOString();
|
|
168
|
-
if (state) {
|
|
169
|
-
state.pipeline = pipelineState;
|
|
170
|
-
writeState(ctxDir, state);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
completed: pipelineState.completedSteps,
|
|
175
|
-
failed: null,
|
|
176
|
-
error: null,
|
|
177
|
-
outputs: pipelineState.outputs,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Resume a failed pipeline from the failed step.
|
|
183
|
-
*/
|
|
184
|
-
export async function resumePipeline({ ctxDir, projectDir, agentsDir, streaming = true, timeout = 300000, onStep = null }) {
|
|
185
|
-
const state = readState(ctxDir);
|
|
186
|
-
if (!state?.pipeline) {
|
|
187
|
-
throw new Error('No pipeline state found. Start a new pipeline with: ctx-cc pipeline <steps> <message>');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const { pipeline } = state;
|
|
191
|
-
if (pipeline.status !== 'failed') {
|
|
192
|
-
throw new Error(`Pipeline is "${pipeline.status}", not failed. Nothing to resume.`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Rebuild steps from the failed step onward
|
|
196
|
-
const failedIdx = pipeline.steps.indexOf(pipeline.failedStep);
|
|
197
|
-
const remainingSteps = pipeline.steps.slice(failedIdx);
|
|
198
|
-
const previousMessage = pipeline.outputs[pipeline.steps[failedIdx - 1]] || 'Resume from failure.';
|
|
199
|
-
|
|
200
|
-
return executePipeline({
|
|
201
|
-
steps: remainingSteps,
|
|
202
|
-
message: previousMessage,
|
|
203
|
-
ctxDir,
|
|
204
|
-
projectDir,
|
|
205
|
-
agentsDir,
|
|
206
|
-
streaming,
|
|
207
|
-
timeout,
|
|
208
|
-
onStep,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get pipeline status from state.
|
|
214
|
-
*/
|
|
215
|
-
export function getPipelineStatus(ctxDir) {
|
|
216
|
-
const state = readState(ctxDir);
|
|
217
|
-
if (!state?.pipeline) return null;
|
|
218
|
-
return state.pipeline;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Format pipeline status for display.
|
|
223
|
-
*/
|
|
224
|
-
export function formatPipelineStatus(pipeline) {
|
|
225
|
-
if (!pipeline) return ' No active pipeline.';
|
|
226
|
-
|
|
227
|
-
const lines = [];
|
|
228
|
-
lines.push(` Status: ${pipeline.status}`);
|
|
229
|
-
lines.push(` Steps: ${pipeline.steps.join(' → ')}`);
|
|
230
|
-
lines.push('');
|
|
231
|
-
|
|
232
|
-
for (let i = 0; i < pipeline.steps.length; i++) {
|
|
233
|
-
const step = pipeline.steps[i];
|
|
234
|
-
let icon = '○'; // pending
|
|
235
|
-
if (pipeline.completedSteps.includes(step)) icon = '✓';
|
|
236
|
-
if (step === pipeline.failedStep) icon = '✗';
|
|
237
|
-
if (i === pipeline.currentStep && pipeline.status === 'running') icon = '▸';
|
|
238
|
-
|
|
239
|
-
lines.push(` ${icon} ${step}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (pipeline.failedStep) {
|
|
243
|
-
lines.push('');
|
|
244
|
-
lines.push(` Failed at: ${pipeline.failedStep}`);
|
|
245
|
-
lines.push(` Reason: ${pipeline.failureReason || 'unknown'}`);
|
|
246
|
-
lines.push(` Resume: ctx-cc pipeline resume`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return lines.join('\n');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// --- internal ---
|
|
253
|
-
|
|
254
|
-
function summarizeOutput(output) {
|
|
255
|
-
if (!output) return 'No output.';
|
|
256
|
-
// Take first 1500 chars as summary
|
|
257
|
-
const maxLen = 1500;
|
|
258
|
-
if (output.length <= maxLen) return output;
|
|
259
|
-
return output.slice(0, maxLen) + '\n\n... (output truncated)';
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function saveStepOutput(ctxDir, step, index, output) {
|
|
263
|
-
const pipeDir = path.join(ctxDir, 'pipeline');
|
|
264
|
-
if (!fs.existsSync(pipeDir)) fs.mkdirSync(pipeDir, { recursive: true });
|
|
265
|
-
const filePath = path.join(pipeDir, `${String(index + 1).padStart(2, '0')}-${step}.md`);
|
|
266
|
-
fs.writeFileSync(filePath, `# Pipeline Step: ${step}\n\n${output}\n`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export { AGENT_FILES };
|