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.
@@ -1,13 +1,28 @@
1
1
  #!/usr/bin/env node
2
- // Register tsx as an in-process ESM loader, then import and run the TypeScript CLI
3
- // directly in this process. This keeps everything single-process so signal handlers
4
- // registered by the engine work correctly (no subprocess forwarding needed).
5
- import { register } from 'node:module';
6
- import { pathToFileURL } from 'node:url';
7
-
8
- // tsx/esm is the public loader API (works across tsx versions and npm hoisting)
9
- register('tsx/esm', pathToFileURL('./'));
10
-
11
- // Now TypeScript imports resolve correctly in this process
12
- const { run } = await import('../src/cli.ts');
13
- await run(process.argv.slice(2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pipeline",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Autonomous Claude Code pipeline engine. Install into any repo, write a BRIEF.md, and let Claude build your project phase by phase.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 outputPath = join(context.projectDir, '.pipeline', 'step-output.log');
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) => {
@@ -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, logFile } = context;
16
+ const { projectDir, config } = context;
17
17
  const pipelineDir = join(projectDir, '.pipeline');
18
- const outputPath = join(pipelineDir, 'step-output.log');
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
- const line = `[tool:start] ${data.tool_name} ${JSON.stringify(data.tool_input ?? {}).slice(0, 120)}`;
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
- const line = `[tool:done] ${data.tool_name} ${success ? '✓' : '✗'}`;
61
- logLine(line);
62
- appendOutput(line);
55
+ appendOutput(`[tool:done] ${data.tool_name} ${success ? '✓' : '✗'}`);
63
56
  }] }],
64
57
  SubagentStart: [{ hooks: [async (data: any) => {
65
- const line = `[subagent:start] ${data.agent_id ?? ''}`;
66
- logLine(line);
67
- appendOutput(line);
58
+ appendOutput(`[subagent:start] ${data.agent_id ?? ''}`);
68
59
  }] }],
69
60
  SubagentStop: [{ hooks: [async (data: any) => {
70
- const line = `[subagent:done] ${data.agent_id ?? ''}`;
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
 
@@ -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 outputPath = join(pipelineDir, 'step-output.log');
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 outputPath = join(projectDir, '.pipeline', 'step-output.log');
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-output.log for real-time activity from agents.
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
- if (!existsSync(outputPath)) return;
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);