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 +1 -1
- package/src/config.ts +2 -0
- package/src/engine.ts +5 -4
- package/src/state.test.ts +40 -0
- package/src/state.ts +6 -1
- package/templates/pipeline/prompts/groom.md +6 -0
package/package.json
CHANGED
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
|
|
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
|
|
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 <=
|
|
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
|
|
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:
|