cc-pipeline 0.7.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pipeline",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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": {
package/src/config.ts CHANGED
@@ -19,6 +19,7 @@ export interface PipelineConfig {
19
19
  name: string;
20
20
  version: number;
21
21
  phasesDir: string;
22
+ maxPhases: number;
22
23
  steps: StepConfig[];
23
24
  usageCheck: { when: string };
24
25
  usageLimits: { sessionBudgetUSD: number; weeklyBudgetUSD: number };
@@ -45,6 +46,7 @@ export function loadConfig(projectDir: string): PipelineConfig {
45
46
  name: raw.name || 'Unnamed Pipeline',
46
47
  version: raw.version || 1,
47
48
  phasesDir: raw.phases_dir || 'docs/phases',
49
+ maxPhases: raw.max_phases ?? 100,
48
50
  steps: [],
49
51
  usageCheck: raw.usage_check || { when: 'phase_boundary' },
50
52
  usageLimits: {
package/src/engine.ts CHANGED
@@ -10,7 +10,7 @@ import { CodexAgent } from './agents/codex.js';
10
10
  import { pipelineEvents } from './events.js';
11
11
  import { computeUsagePercentages } from './usage.js';
12
12
 
13
- const MAX_PHASES = 20;
13
+ const DEFAULT_MAX_PHASES = 100;
14
14
 
15
15
  /**
16
16
  * Main pipeline engine loop.
@@ -21,7 +21,7 @@ const MAX_PHASES = 20;
21
21
  * - Print banner
22
22
  * - Loop through phases, executing steps
23
23
  * - Check for PROJECT COMPLETE in reflections
24
- * - Handle phase limits and MAX_PHASES
24
+ * - Handle phase limits and maxPhases
25
25
  * - Signal handling for clean shutdown
26
26
  */
27
27
  export async function runEngine(projectDir: string, options: any = {}) {
@@ -40,6 +40,7 @@ export async function runEngine(projectDir: string, options: any = {}) {
40
40
 
41
41
  // Load config
42
42
  const config = loadConfig(projectDir);
43
+ const maxPhases = config.maxPhases ?? DEFAULT_MAX_PHASES;
43
44
 
44
45
  // Derive current state
45
46
  const state = getCurrentState(logFile);
@@ -110,7 +111,7 @@ export async function runEngine(projectDir: string, options: any = {}) {
110
111
 
111
112
  const phaseLimit = options.phases || 0;
112
113
 
113
- while (phase <= MAX_PHASES) {
114
+ while (phase <= maxPhases) {
114
115
  pipelineEvents.emit('phase:start', { phase });
115
116
 
116
117
  // Execute all steps in phase
@@ -221,7 +222,7 @@ export async function runEngine(projectDir: string, options: any = {}) {
221
222
  }
222
223
  }
223
224
 
224
- log(`Hit MAX_PHASES (${MAX_PHASES}). Stopping.`);
225
+ log(`Hit max phases (${maxPhases}). Stopping.`);
225
226
  } finally {
226
227
  cleanup();
227
228
  }
package/src/state.test.ts CHANGED
@@ -357,6 +357,46 @@ test('deriveResumePoint: advance to next phase after last step', () => {
357
357
  rmSync(tempDir, { recursive: true });
358
358
  });
359
359
 
360
+ test('getCurrentState: project_complete returns done state (same as phase_complete)', () => {
361
+ const tempDir = mkdtempSync(join(tmpdir(), 'cc-pipeline-test-'));
362
+ const logFile = join(tempDir, 'pipeline.jsonl');
363
+
364
+ const events = [
365
+ { event: 'step_start', phase: 34, step: 'groom', ts: '2025-01-01T00:00:00Z' },
366
+ { event: 'step_done', phase: 34, step: 'groom', status: 'ok', ts: '2025-01-01T00:01:00Z' },
367
+ { event: 'project_complete', phase: 34, ts: '2025-01-01T00:02:00Z' },
368
+ ];
369
+ writeFileSync(logFile, events.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf8');
370
+
371
+ const state = getCurrentState(logFile);
372
+
373
+ assert.strictEqual(state.phase, 34);
374
+ assert.strictEqual(state.step, 'done');
375
+ assert.strictEqual(state.status, 'complete');
376
+
377
+ rmSync(tempDir, { recursive: true });
378
+ });
379
+
380
+ test('deriveResumePoint: project_complete advances to next phase first step', () => {
381
+ const tempDir = mkdtempSync(join(tmpdir(), 'cc-pipeline-test-'));
382
+ const logFile = join(tempDir, 'pipeline.jsonl');
383
+
384
+ const events = [
385
+ { event: 'step_start', phase: 34, step: 'spec', ts: '2025-01-01T00:00:00Z' },
386
+ { event: 'step_done', phase: 34, step: 'spec', status: 'ok', ts: '2025-01-01T00:01:00Z' },
387
+ { event: 'project_complete', phase: 34, ts: '2025-01-01T00:02:00Z' },
388
+ ];
389
+ writeFileSync(logFile, events.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf8');
390
+
391
+ const resume = deriveResumePoint(logFile, mockSteps);
392
+
393
+ // project_complete is treated as phase_complete — next run starts fresh in the next phase
394
+ assert.strictEqual(resume.phase, 35);
395
+ assert.strictEqual(resume.stepName, 'spec');
396
+
397
+ rmSync(tempDir, { recursive: true });
398
+ });
399
+
360
400
  test('deriveResumePoint: handles unknown step by starting from beginning', () => {
361
401
  const tempDir = mkdtempSync(join(tmpdir(), 'cc-pipeline-test-'));
362
402
  const logFile = join(tempDir, 'pipeline.jsonl');
package/src/state.ts CHANGED
@@ -75,7 +75,7 @@ export function getCurrentState(logFile: string): { phase: number; step: string;
75
75
  }
76
76
 
77
77
  // Find last relevant event
78
- const relevantEvents = ['step_start', 'step_done', 'step_skip', 'phase_complete'];
78
+ const relevantEvents = ['step_start', 'step_done', 'step_skip', 'phase_complete', 'project_complete'];
79
79
  const lastEvent = events
80
80
  .filter(e => relevantEvents.includes(e.event))
81
81
  .pop();
@@ -98,6 +98,11 @@ export function getCurrentState(logFile: string): { phase: number; step: string;
98
98
  case 'step_skip':
99
99
  return { phase, step, status: 'complete' };
100
100
  case 'phase_complete':
101
+ case 'project_complete':
102
+ // Treat project_complete the same as phase_complete — resume starts at next phase.
103
+ // This ensures that if the user adds new Epics after completion and reruns, the
104
+ // engine advances to the next phase and runs groom fresh rather than resuming
105
+ // mid-phase after the groom step.
101
106
  return { phase, step: 'done', status: 'complete' };
102
107
  default:
103
108
  return { phase: 1, step: 'pending', status: 'ready' };
@@ -14,6 +14,12 @@ Current phase: {{PHASE}}
14
14
 
15
15
  ## Determine Your Mode
16
16
 
17
+ > **Always scan `docs/epics/` before deciding.** Read every file there.
18
+ > A "draft" Epic has a Goal + Acceptance Criteria but an empty or missing
19
+ > `## Research & Decisions` section. If ANY draft Epic exists — even if
20
+ > NEXT.md says "Next: none" or is empty — treat this as Mode 2 (Transition).
21
+ > Only write PROJECT COMPLETE when you have confirmed zero draft Epics remain.
22
+
17
23
  ### Mode 1: Bootstrap (Phase 1, no Epics exist)
18
24
 
19
25
  If `docs/epics/` doesn't exist or is empty: