cc-pipeline 0.6.2 → 0.6.4
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/bin/cc-pipeline.js +27 -12
- package/package.json +1 -1
- package/src/agents/bash.ts +4 -2
- package/src/agents/claudecode.ts +10 -23
- package/src/agents/codex.ts +4 -2
- package/src/tui/App.ts +5 -5
package/bin/cc-pipeline.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
// tsx v4+ requires --import, not the loader hooks API.
|
|
3
|
+
// Spawn node with --import tsx/esm, resolving tsx relative to this package
|
|
4
|
+
// so it works regardless of npm hoisting.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const tsxEsm = pathToFileURL(require.resolve('tsx/esm')).href;
|
|
14
|
+
const cli = join(__dirname, '../src/cli.ts');
|
|
15
|
+
|
|
16
|
+
const child = spawn(
|
|
17
|
+
process.execPath,
|
|
18
|
+
['--import', tsxEsm, cli, ...process.argv.slice(2)],
|
|
19
|
+
{ stdio: 'inherit' }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// SIGINT is broadcast to the whole process group on Ctrl-C, but SIGTERM is not.
|
|
23
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
24
|
+
|
|
25
|
+
child.on('exit', (code, signal) => {
|
|
26
|
+
if (signal) process.kill(process.pid, signal);
|
|
27
|
+
else process.exit(code ?? 0);
|
|
28
|
+
});
|
package/package.json
CHANGED
package/src/agents/bash.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { BaseAgent, agentState, AgentContext, AgentResult, StepDef } from './base.js';
|
|
5
5
|
|
|
@@ -20,7 +20,9 @@ export class BashAgent extends BaseAgent {
|
|
|
20
20
|
|
|
21
21
|
console.log(` Executing: ${cmd}`);
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const logDir = join(context.projectDir, '.pipeline', 'logs', `phase-${phase}`);
|
|
24
|
+
mkdirSync(logDir, { recursive: true });
|
|
25
|
+
const outputPath = join(logDir, `step-${step.name}.log`);
|
|
24
26
|
writeFileSync(outputPath, `$ ${cmd}\n`, 'utf-8');
|
|
25
27
|
|
|
26
28
|
return new Promise((resolve) => {
|
package/src/agents/claudecode.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync, appendFileSync } from 'node:fs';
|
|
1
|
+
import { writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
4
|
import { BaseAgent, agentState, AgentContext, AgentResult, StepDef } from './base.js';
|
|
@@ -13,9 +13,11 @@ import { pipelineEvents } from '../events.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export class ClaudeCodeAgent extends BaseAgent {
|
|
15
15
|
async run(phase: number, step: StepDef, promptPath: string | null, model: string, context: AgentContext): Promise<AgentResult> {
|
|
16
|
-
const { projectDir, config
|
|
16
|
+
const { projectDir, config } = context;
|
|
17
17
|
const pipelineDir = join(projectDir, '.pipeline');
|
|
18
|
-
const
|
|
18
|
+
const logDir = join(pipelineDir, 'logs', `phase-${phase}`);
|
|
19
|
+
mkdirSync(logDir, { recursive: true });
|
|
20
|
+
const outputPath = join(logDir, `step-${step.name}.log`);
|
|
19
21
|
|
|
20
22
|
const promptText = generatePrompt(projectDir, config, phase, promptPath);
|
|
21
23
|
writeFileSync(join(pipelineDir, 'current-prompt.md'), promptText, 'utf-8');
|
|
@@ -31,11 +33,6 @@ export class ClaudeCodeAgent extends BaseAgent {
|
|
|
31
33
|
// Clear output file so TUI file-tailer sees only this step's content
|
|
32
34
|
writeFileSync(outputPath, '', 'utf-8');
|
|
33
35
|
|
|
34
|
-
const logLine = (msg: string) => {
|
|
35
|
-
if (logFile) {
|
|
36
|
-
try { appendFileSync(logFile, msg + '\n', 'utf-8'); } catch (_) {}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
36
|
const appendOutput = (line: string) => {
|
|
40
37
|
try { appendFileSync(outputPath, line + '\n', 'utf-8'); } catch (_) {}
|
|
41
38
|
};
|
|
@@ -51,29 +48,19 @@ export class ClaudeCodeAgent extends BaseAgent {
|
|
|
51
48
|
env: { ...process.env, CLAUDECODE: undefined },
|
|
52
49
|
hooks: {
|
|
53
50
|
PreToolUse: [{ hooks: [async (data: any) => {
|
|
54
|
-
|
|
55
|
-
logLine(line);
|
|
56
|
-
appendOutput(line);
|
|
51
|
+
appendOutput(`[tool:start] ${data.tool_name} ${JSON.stringify(data.tool_input ?? {}).slice(0, 120)}`);
|
|
57
52
|
}] }],
|
|
58
53
|
PostToolUse: [{ hooks: [async (data: any) => {
|
|
59
54
|
const success = !data.tool_response?.is_error;
|
|
60
|
-
|
|
61
|
-
logLine(line);
|
|
62
|
-
appendOutput(line);
|
|
55
|
+
appendOutput(`[tool:done] ${data.tool_name} ${success ? '✓' : '✗'}`);
|
|
63
56
|
}] }],
|
|
64
57
|
SubagentStart: [{ hooks: [async (data: any) => {
|
|
65
|
-
|
|
66
|
-
logLine(line);
|
|
67
|
-
appendOutput(line);
|
|
58
|
+
appendOutput(`[subagent:start] ${data.agent_id ?? ''}`);
|
|
68
59
|
}] }],
|
|
69
60
|
SubagentStop: [{ hooks: [async (data: any) => {
|
|
70
|
-
|
|
71
|
-
logLine(line);
|
|
72
|
-
appendOutput(line);
|
|
73
|
-
}] }],
|
|
74
|
-
Stop: [{ hooks: [async (_data: any) => {
|
|
75
|
-
logLine(`[session:stop]`);
|
|
61
|
+
appendOutput(`[subagent:done] ${data.agent_id ?? ''}`);
|
|
76
62
|
}] }],
|
|
63
|
+
Stop: [{ hooks: [async (_data: any) => {}] }],
|
|
77
64
|
},
|
|
78
65
|
};
|
|
79
66
|
|
package/src/agents/codex.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, ChildProcess } from 'node:child_process';
|
|
2
|
-
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { BaseAgent, agentState, AgentContext, AgentResult, StepDef } from './base.js';
|
|
5
5
|
import { generatePrompt } from '../prompts.js';
|
|
@@ -25,7 +25,9 @@ export class CodexAgent extends BaseAgent {
|
|
|
25
25
|
async run(phase: number, step: StepDef, promptPath: string | null, model: string, context: AgentContext): Promise<AgentResult> {
|
|
26
26
|
const { projectDir } = context;
|
|
27
27
|
const pipelineDir = join(projectDir, '.pipeline');
|
|
28
|
-
const
|
|
28
|
+
const logDir = join(pipelineDir, 'logs', `phase-${phase}`);
|
|
29
|
+
mkdirSync(logDir, { recursive: true });
|
|
30
|
+
const outputPath = join(logDir, `step-${step.name}.log`);
|
|
29
31
|
|
|
30
32
|
const promptText = generatePrompt(projectDir, context.config, phase, promptPath);
|
|
31
33
|
writeFileSync(join(pipelineDir, 'current-prompt.md'), promptText, 'utf-8');
|
package/src/tui/App.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function App({ events, projectDir }: AppProps) {
|
|
|
108
108
|
const startTime = useState(() => Date.now())[0];
|
|
109
109
|
const stepStartRef = useRef(0);
|
|
110
110
|
const fileOffsetRef = useRef(0);
|
|
111
|
-
const
|
|
111
|
+
const outputPathRef = useRef('');
|
|
112
112
|
|
|
113
113
|
useEffect(() => {
|
|
114
114
|
const tick = setInterval(() => {
|
|
@@ -125,16 +125,15 @@ export function App({ events, projectDir }: AppProps) {
|
|
|
125
125
|
setPhaseDescription(loadPhaseDescription(projectDir, phasesDir, currentPhase));
|
|
126
126
|
}, [currentPhase]);
|
|
127
127
|
|
|
128
|
-
// Poll step
|
|
128
|
+
// Poll the per-step log file for real-time activity from agents.
|
|
129
129
|
// Hooks inside the Agent SDK query() run in a worker context and can't emit
|
|
130
130
|
// to this process's EventEmitter, so we use file-based IPC instead.
|
|
131
131
|
useEffect(() => {
|
|
132
132
|
const poll = () => {
|
|
133
133
|
try {
|
|
134
|
-
|
|
134
|
+
const outputPath = outputPathRef.current;
|
|
135
|
+
if (!outputPath || !existsSync(outputPath)) return;
|
|
135
136
|
const stat = statSync(outputPath);
|
|
136
|
-
// File was truncated (new step started) — reset
|
|
137
|
-
if (stat.size < fileOffsetRef.current) fileOffsetRef.current = 0;
|
|
138
137
|
if (stat.size === fileOffsetRef.current) return;
|
|
139
138
|
|
|
140
139
|
// Read only new bytes — avoids loading the whole file each tick
|
|
@@ -219,6 +218,7 @@ export function App({ events, projectDir }: AppProps) {
|
|
|
219
218
|
setCurrentAgent(d.agent ?? '');
|
|
220
219
|
setCurrentModel(d.model ?? '');
|
|
221
220
|
setTextLines([]);
|
|
221
|
+
outputPathRef.current = join(projectDir, '.pipeline', 'logs', `phase-${d.phase}`, `step-${d.step}.log`);
|
|
222
222
|
fileOffsetRef.current = 0;
|
|
223
223
|
stepStartRef.current = Date.now();
|
|
224
224
|
setStepElapsed(0);
|