cc-devflow 4.5.3 → 4.5.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/.claude/skills/cc-act/CHANGELOG.md +6 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +7 -0
- package/.claude/skills/cc-act/SKILL.md +25 -2
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +29 -0
- package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +8 -0
- package/.claude/skills/cc-check/CHANGELOG.md +6 -0
- package/.claude/skills/cc-check/PLAYBOOK.md +4 -0
- package/.claude/skills/cc-check/SKILL.md +15 -2
- package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +18 -0
- package/.claude/skills/cc-do/CHANGELOG.md +6 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +6 -4
- package/.claude/skills/cc-do/SKILL.md +14 -5
- package/.claude/skills/cc-do/references/execution-recovery.md +3 -0
- package/.claude/skills/cc-do/references/parallel-dispatch.md +6 -4
- package/.claude/skills/cc-do/scripts/detect-file-conflicts.sh +49 -3
- package/.claude/skills/cc-investigate/CHANGELOG.md +6 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +7 -0
- package/.claude/skills/cc-investigate/SKILL.md +10 -1
- package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +30 -0
- package/.claude/skills/cc-plan/CHANGELOG.md +6 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +16 -10
- package/.claude/skills/cc-plan/SKILL.md +11 -4
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +41 -0
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +4 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +32 -3
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +34 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +11 -3
- package/CHANGELOG.md +8 -0
- package/bin/cc-devflow-cli.js +93 -2
- package/docs/examples/example-bindings.json +5 -5
- package/docs/examples/full-design-blocked/README.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +1 -1
- package/docs/examples/local-handoff/README.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +1 -1
- package/docs/examples/pdca-loop/README.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +1 -1
- package/docs/get-shit-done-strategy-audit.md +518 -0
- package/lib/compiler/__tests__/inventory.test.js +51 -0
- package/lib/compiler/inventory.js +78 -0
- package/lib/skill-runtime/__tests__/approve.test.js +92 -0
- package/lib/skill-runtime/__tests__/autopilot.test.js +4 -0
- package/lib/skill-runtime/__tests__/planner.tdd.test.js +20 -0
- package/lib/skill-runtime/__tests__/query.test.js +147 -1
- package/lib/skill-runtime/__tests__/readiness.test.js +53 -0
- package/lib/skill-runtime/__tests__/release.test.js +85 -0
- package/lib/skill-runtime/__tests__/runtime.integration.test.js +11 -0
- package/lib/skill-runtime/__tests__/schemas.test.js +56 -0
- package/lib/skill-runtime/__tests__/worker-run.test.js +29 -0
- package/lib/skill-runtime/errors.js +39 -0
- package/lib/skill-runtime/index.js +8 -0
- package/lib/skill-runtime/operations/approve.js +17 -2
- package/lib/skill-runtime/operations/release.js +6 -3
- package/lib/skill-runtime/operations/worker-run.js +30 -0
- package/lib/skill-runtime/planner.js +10 -2
- package/lib/skill-runtime/query-registry.js +101 -0
- package/lib/skill-runtime/query.js +159 -91
- package/lib/skill-runtime/readiness.js +84 -0
- package/lib/skill-runtime/schemas.js +28 -3
- package/lib/skill-runtime/trace.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { runApprove } = require('../operations/approve');
|
|
6
|
+
const {
|
|
7
|
+
getRuntimeStatePath,
|
|
8
|
+
getTaskManifestPath
|
|
9
|
+
} = require('../store');
|
|
10
|
+
|
|
11
|
+
function writeJson(filePath, value) {
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('runApprove', () => {
|
|
17
|
+
test('throws named error when change-state is missing', async () => {
|
|
18
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-approve-missing-state-'));
|
|
19
|
+
|
|
20
|
+
await expect(runApprove({
|
|
21
|
+
repoRoot,
|
|
22
|
+
changeId: 'REQ-123',
|
|
23
|
+
executionMode: 'direct'
|
|
24
|
+
})).rejects.toMatchObject({
|
|
25
|
+
name: 'MissingChangeStateError',
|
|
26
|
+
artifactRefs: [
|
|
27
|
+
expect.stringContaining('change-state.json')
|
|
28
|
+
],
|
|
29
|
+
rescueAction: 'run cc-roadmap or cc-plan init before approving execution'
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('throws named error when task manifest is missing', async () => {
|
|
34
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-approve-missing-manifest-'));
|
|
35
|
+
|
|
36
|
+
writeJson(getRuntimeStatePath(repoRoot, 'REQ-123'), {
|
|
37
|
+
changeId: 'REQ-123',
|
|
38
|
+
goal: 'Approve only concrete plans',
|
|
39
|
+
status: 'planned',
|
|
40
|
+
initializedAt: '2026-03-25T01:00:00.000Z',
|
|
41
|
+
updatedAt: '2026-03-25T01:00:00.000Z'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await expect(runApprove({
|
|
45
|
+
repoRoot,
|
|
46
|
+
changeId: 'REQ-123',
|
|
47
|
+
executionMode: 'direct'
|
|
48
|
+
})).rejects.toMatchObject({
|
|
49
|
+
name: 'MissingTaskManifestError',
|
|
50
|
+
artifactRefs: [
|
|
51
|
+
expect.stringContaining('task-manifest.json')
|
|
52
|
+
],
|
|
53
|
+
rescueAction: 'run cc-plan to create planning/task-manifest.json before approving execution'
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('approves the current manifest plan version', async () => {
|
|
58
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-approve-pass-'));
|
|
59
|
+
|
|
60
|
+
writeJson(getRuntimeStatePath(repoRoot, 'REQ-123'), {
|
|
61
|
+
changeId: 'REQ-123',
|
|
62
|
+
goal: 'Approve current plan',
|
|
63
|
+
status: 'planned',
|
|
64
|
+
initializedAt: '2026-03-25T01:00:00.000Z',
|
|
65
|
+
updatedAt: '2026-03-25T01:00:00.000Z'
|
|
66
|
+
});
|
|
67
|
+
writeJson(getTaskManifestPath(repoRoot, 'REQ-123'), {
|
|
68
|
+
changeId: 'REQ-123',
|
|
69
|
+
goal: 'Approve current plan',
|
|
70
|
+
createdAt: '2026-03-25T01:00:00.000Z',
|
|
71
|
+
updatedAt: '2026-03-25T01:00:00.000Z',
|
|
72
|
+
tasks: [],
|
|
73
|
+
metadata: {
|
|
74
|
+
source: 'default',
|
|
75
|
+
generatedBy: 'test',
|
|
76
|
+
planVersion: 7
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await runApprove({
|
|
81
|
+
repoRoot,
|
|
82
|
+
changeId: 'REQ-123',
|
|
83
|
+
executionMode: 'direct'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).toMatchObject({
|
|
87
|
+
status: 'approved',
|
|
88
|
+
executionMode: 'direct',
|
|
89
|
+
planVersion: 7
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -34,6 +34,10 @@ function markManifestReviewsPassed(repoRoot, changeId) {
|
|
|
34
34
|
code: 'pass'
|
|
35
35
|
}
|
|
36
36
|
}));
|
|
37
|
+
manifest.spec = manifest.spec || {
|
|
38
|
+
primaryCapability: 'autopilot-runtime',
|
|
39
|
+
specFiles: ['devflow/specs/autopilot-runtime.md']
|
|
40
|
+
};
|
|
37
41
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
10
11
|
|
|
11
12
|
const { parseTasksMarkdown, createTaskManifest, deriveManifestExecutionState } = require('../planner');
|
|
12
13
|
|
|
@@ -64,6 +65,25 @@ describe('TDD Order Validation', () => {
|
|
|
64
65
|
expect(tasks[1].context.readFiles).toEqual(['design.md', 'src/counter.test.ts']);
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
test('should quote generated run command titles as shell data', () => {
|
|
69
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-planner-shell-'));
|
|
70
|
+
const markerPath = path.join(repoRoot, 'pwned');
|
|
71
|
+
const markdown = `
|
|
72
|
+
- [ ] T001 hostile title " && touch ${markerPath} && echo "
|
|
73
|
+
`.trim();
|
|
74
|
+
|
|
75
|
+
const [task] = parseTasksMarkdown(markdown);
|
|
76
|
+
const result = spawnSync(task.run[0], {
|
|
77
|
+
cwd: repoRoot,
|
|
78
|
+
shell: true,
|
|
79
|
+
encoding: 'utf8'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result.status).toBe(0);
|
|
83
|
+
expect(result.stdout).toContain('hostile title');
|
|
84
|
+
expect(fs.existsSync(markerPath)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
67
87
|
test('should backfill minimum metadata for TEST and IMPL tasks from plain TASKS lines', () => {
|
|
68
88
|
const markdown = `
|
|
69
89
|
## Phase 1: Build
|
|
@@ -2,7 +2,13 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
getFullState,
|
|
7
|
+
getNextTask,
|
|
8
|
+
getProgress,
|
|
9
|
+
listQueryIds,
|
|
10
|
+
runQuery
|
|
11
|
+
} = require('../query');
|
|
6
12
|
const {
|
|
7
13
|
getRuntimeStatePath,
|
|
8
14
|
getTaskManifestPath,
|
|
@@ -281,4 +287,144 @@ describe('query helpers', () => {
|
|
|
281
287
|
|
|
282
288
|
expect(next.id).toBe('T002');
|
|
283
289
|
});
|
|
290
|
+
|
|
291
|
+
test('dispatches typed query ids with trace metadata', async () => {
|
|
292
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-registry-'));
|
|
293
|
+
|
|
294
|
+
writeJson(getTaskManifestPath(repoRoot, 'REQ-126'), {
|
|
295
|
+
changeId: 'REQ-126',
|
|
296
|
+
goal: 'Expose typed query registry',
|
|
297
|
+
createdAt: '2026-03-25T01:05:00.000Z',
|
|
298
|
+
updatedAt: '2026-03-25T01:10:00.000Z',
|
|
299
|
+
tasks: [
|
|
300
|
+
{ id: 'T001', status: 'pending' }
|
|
301
|
+
],
|
|
302
|
+
metadata: {
|
|
303
|
+
source: 'default',
|
|
304
|
+
generatedBy: 'test',
|
|
305
|
+
planVersion: 1
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await expect(runQuery('progress', { repoRoot, changeId: 'REQ-126' })).resolves.toMatchObject({
|
|
310
|
+
ok: true,
|
|
311
|
+
queryId: 'progress',
|
|
312
|
+
data: {
|
|
313
|
+
totalTasks: 1,
|
|
314
|
+
pendingTasks: 1
|
|
315
|
+
},
|
|
316
|
+
trace: {
|
|
317
|
+
artifactRefs: expect.arrayContaining([
|
|
318
|
+
expect.stringContaining('task-manifest.json')
|
|
319
|
+
]),
|
|
320
|
+
nextAction: 'read-query-result'
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(listQueryIds()).toEqual(expect.arrayContaining(['full-state', 'next-task', 'progress']));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('returns a named error for unknown query ids', async () => {
|
|
328
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-unknown-'));
|
|
329
|
+
|
|
330
|
+
await expect(runQuery('unknown-query', { repoRoot, changeId: 'REQ-127' })).resolves.toMatchObject({
|
|
331
|
+
ok: false,
|
|
332
|
+
queryId: 'unknown-query',
|
|
333
|
+
error: {
|
|
334
|
+
name: 'UnknownQueryError',
|
|
335
|
+
rescueAction: 'use one of: full-state, next-task, progress, ship-readiness'
|
|
336
|
+
},
|
|
337
|
+
trace: {
|
|
338
|
+
nextAction: 'choose-supported-query'
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('returns a named error when typed queries miss required artifacts', async () => {
|
|
344
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-missing-manifest-'));
|
|
345
|
+
|
|
346
|
+
await expect(runQuery('progress', { repoRoot, changeId: 'REQ-130' })).resolves.toMatchObject({
|
|
347
|
+
ok: false,
|
|
348
|
+
queryId: 'progress',
|
|
349
|
+
error: {
|
|
350
|
+
name: 'MissingQueryArtifactError',
|
|
351
|
+
artifactRefs: [
|
|
352
|
+
expect.stringContaining('task-manifest.json')
|
|
353
|
+
],
|
|
354
|
+
rescueAction: 'create required runtime artifacts before running this query'
|
|
355
|
+
},
|
|
356
|
+
trace: {
|
|
357
|
+
event: 'query.progress.failed',
|
|
358
|
+
nextAction: 'create required runtime artifacts before running this query'
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('returns a named error when required query artifacts are malformed', async () => {
|
|
364
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-invalid-manifest-'));
|
|
365
|
+
const manifestPath = getTaskManifestPath(repoRoot, 'REQ-131');
|
|
366
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
367
|
+
fs.writeFileSync(manifestPath, '{bad json\n');
|
|
368
|
+
|
|
369
|
+
await expect(runQuery('progress', { repoRoot, changeId: 'REQ-131' })).resolves.toMatchObject({
|
|
370
|
+
ok: false,
|
|
371
|
+
queryId: 'progress',
|
|
372
|
+
error: {
|
|
373
|
+
name: 'InvalidQueryArtifactError',
|
|
374
|
+
artifactRefs: [
|
|
375
|
+
expect.stringContaining('task-manifest.json')
|
|
376
|
+
],
|
|
377
|
+
rescueAction: 'repair or regenerate the invalid runtime artifact before running this query'
|
|
378
|
+
},
|
|
379
|
+
trace: {
|
|
380
|
+
event: 'query.progress.failed',
|
|
381
|
+
nextAction: 'repair or regenerate the invalid runtime artifact before running this query'
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('returns MissingReportCardError for ship readiness without report card', async () => {
|
|
387
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-missing-report-'));
|
|
388
|
+
|
|
389
|
+
await expect(runQuery('ship-readiness', { repoRoot, changeId: 'REQ-128' })).resolves.toMatchObject({
|
|
390
|
+
ok: false,
|
|
391
|
+
queryId: 'ship-readiness',
|
|
392
|
+
error: {
|
|
393
|
+
name: 'MissingReportCardError',
|
|
394
|
+
artifactRefs: [
|
|
395
|
+
expect.stringContaining('report-card.json')
|
|
396
|
+
],
|
|
397
|
+
rescueAction: 'run cc-check and create review/report-card.json before cc-act'
|
|
398
|
+
},
|
|
399
|
+
trace: {
|
|
400
|
+
nextAction: 'run cc-check and create review/report-card.json before cc-act'
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('reports ship readiness from report-card truth', async () => {
|
|
406
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-ship-ready-'));
|
|
407
|
+
|
|
408
|
+
writeJson(getReportCardPath(repoRoot, 'REQ-129'), {
|
|
409
|
+
changeId: 'REQ-129',
|
|
410
|
+
verdict: 'pass',
|
|
411
|
+
overall: 'pass',
|
|
412
|
+
reroute: 'none',
|
|
413
|
+
specSyncReady: true,
|
|
414
|
+
blockingFindings: [],
|
|
415
|
+
timestamp: '2026-03-25T01:11:00.000Z'
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await expect(runQuery('ship-readiness', { repoRoot, changeId: 'REQ-129' })).resolves.toMatchObject({
|
|
419
|
+
ok: true,
|
|
420
|
+
queryId: 'ship-readiness',
|
|
421
|
+
data: {
|
|
422
|
+
ready: true,
|
|
423
|
+
verdict: 'pass',
|
|
424
|
+
reroute: 'none',
|
|
425
|
+
specSyncReady: true,
|
|
426
|
+
blockers: []
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
284
430
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const {
|
|
2
|
+
assertShipReady,
|
|
3
|
+
deriveShipReadiness
|
|
4
|
+
} = require('../readiness');
|
|
5
|
+
|
|
6
|
+
describe('ship readiness', () => {
|
|
7
|
+
test('derives one shared readiness verdict from report-card truth', () => {
|
|
8
|
+
const report = {
|
|
9
|
+
verdict: 'pass',
|
|
10
|
+
overall: 'pass',
|
|
11
|
+
reroute: 'none',
|
|
12
|
+
specSyncReady: true,
|
|
13
|
+
blockingFindings: [],
|
|
14
|
+
gaps: [],
|
|
15
|
+
timestamp: '2026-03-25T01:11:00.000Z'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
expect(deriveShipReadiness(report, { reportPath: '/tmp/report-card.json' })).toEqual({
|
|
19
|
+
ready: true,
|
|
20
|
+
verdict: 'pass',
|
|
21
|
+
reroute: 'none',
|
|
22
|
+
specSyncReady: true,
|
|
23
|
+
blockers: [],
|
|
24
|
+
reportPath: '/tmp/report-card.json',
|
|
25
|
+
timestamp: '2026-03-25T01:11:00.000Z'
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('throws named release errors from the same readiness blockers', () => {
|
|
30
|
+
const report = {
|
|
31
|
+
verdict: 'pass',
|
|
32
|
+
overall: 'pass',
|
|
33
|
+
reroute: 'cc-do',
|
|
34
|
+
specSyncReady: false,
|
|
35
|
+
blockingFindings: ['review: stale'],
|
|
36
|
+
gaps: ['spec gap'],
|
|
37
|
+
timestamp: '2026-03-25T01:11:00.000Z'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
expect(() => assertShipReady(report, {
|
|
41
|
+
reportPath: '/tmp/report-card.json',
|
|
42
|
+
errorName: 'ReleaseReadinessError',
|
|
43
|
+
rescueAction: 'run cc-check until ship-readiness is ready before release'
|
|
44
|
+
})).toThrow(expect.objectContaining({
|
|
45
|
+
name: 'ReleaseReadinessError',
|
|
46
|
+
artifactRefs: ['/tmp/report-card.json'],
|
|
47
|
+
rescueAction: 'run cc-check until ship-readiness is ready before release',
|
|
48
|
+
details: {
|
|
49
|
+
blockers: ['reroute is cc-do', 'specSyncReady is not true', 'review: stale', 'spec gap']
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { runRelease } = require('../operations/release');
|
|
6
|
+
const {
|
|
7
|
+
getRuntimeStatePath,
|
|
8
|
+
getTaskManifestPath,
|
|
9
|
+
getReportCardPath,
|
|
10
|
+
getReleaseNotePath
|
|
11
|
+
} = require('../store');
|
|
12
|
+
|
|
13
|
+
function writeJson(filePath, value) {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeReleaseFixture(repoRoot, reportOverrides = {}) {
|
|
19
|
+
writeJson(getRuntimeStatePath(repoRoot, 'REQ-123'), {
|
|
20
|
+
changeId: 'REQ-123',
|
|
21
|
+
goal: 'Release only when ship ready',
|
|
22
|
+
status: 'verified',
|
|
23
|
+
initializedAt: '2026-03-25T01:00:00.000Z',
|
|
24
|
+
plannedAt: '2026-03-25T01:01:00.000Z',
|
|
25
|
+
verifiedAt: '2026-03-25T01:02:00.000Z',
|
|
26
|
+
updatedAt: '2026-03-25T01:02:00.000Z'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
writeJson(getTaskManifestPath(repoRoot, 'REQ-123'), {
|
|
30
|
+
changeId: 'REQ-123',
|
|
31
|
+
goal: 'Release only when ship ready',
|
|
32
|
+
createdAt: '2026-03-25T01:00:00.000Z',
|
|
33
|
+
updatedAt: '2026-03-25T01:02:00.000Z',
|
|
34
|
+
tasks: [
|
|
35
|
+
{
|
|
36
|
+
id: 'T001',
|
|
37
|
+
title: 'Finish change',
|
|
38
|
+
type: 'IMPL',
|
|
39
|
+
dependsOn: [],
|
|
40
|
+
touches: ['src/a.ts'],
|
|
41
|
+
run: ['echo ok'],
|
|
42
|
+
checks: [],
|
|
43
|
+
status: 'passed',
|
|
44
|
+
attempts: 1,
|
|
45
|
+
maxRetries: 1
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
metadata: {
|
|
49
|
+
source: 'default',
|
|
50
|
+
generatedBy: 'test',
|
|
51
|
+
planVersion: 1
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
writeJson(getReportCardPath(repoRoot, 'REQ-123'), {
|
|
56
|
+
changeId: 'REQ-123',
|
|
57
|
+
verdict: 'pass',
|
|
58
|
+
overall: 'pass',
|
|
59
|
+
specSyncReady: false,
|
|
60
|
+
reroute: 'cc-do',
|
|
61
|
+
quickGates: [],
|
|
62
|
+
strictGates: [],
|
|
63
|
+
review: { status: 'pass', summary: 'review-ok', details: '' },
|
|
64
|
+
blockingFindings: [],
|
|
65
|
+
gaps: [],
|
|
66
|
+
timestamp: '2026-03-25T01:03:00.000Z',
|
|
67
|
+
...reportOverrides
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('runRelease', () => {
|
|
72
|
+
test('blocks reports that pass verification but are not ship ready', async () => {
|
|
73
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-release-readiness-'));
|
|
74
|
+
writeReleaseFixture(repoRoot);
|
|
75
|
+
|
|
76
|
+
await expect(runRelease({ repoRoot, changeId: 'REQ-123' })).rejects.toMatchObject({
|
|
77
|
+
name: 'ReleaseReadinessError',
|
|
78
|
+
rescueAction: 'run cc-check until ship-readiness is ready before release'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const runtimeState = JSON.parse(fs.readFileSync(getRuntimeStatePath(repoRoot, 'REQ-123'), 'utf8'));
|
|
82
|
+
expect(runtimeState.status).toBe('verified');
|
|
83
|
+
expect(fs.existsSync(getReleaseNotePath(repoRoot, 'REQ-123'))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -91,6 +91,16 @@ describe('Skill runtime', () => {
|
|
|
91
91
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
async function markManifestSpec(changeId) {
|
|
95
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
96
|
+
const manifest = await readJson(manifestPath);
|
|
97
|
+
manifest.spec = {
|
|
98
|
+
primaryCapability: 'skill-runtime-pipeline',
|
|
99
|
+
specFiles: ['devflow/specs/skill-runtime.md']
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
test('runs init -> snapshot -> plan -> dispatch -> verify -> release', async () => {
|
|
95
105
|
const changeId = 'REQ-999';
|
|
96
106
|
await runInit({ repoRoot, changeId, goal: 'Test skill runtime pipeline' });
|
|
@@ -121,6 +131,7 @@ describe('Skill runtime', () => {
|
|
|
121
131
|
const manifest = await readJson(getTaskManifestPath(repoRoot, changeId));
|
|
122
132
|
expect(manifest.tasks.every((task) => task.status === 'passed')).toBe(true);
|
|
123
133
|
|
|
134
|
+
await markManifestSpec(changeId);
|
|
124
135
|
await markManifestReviews(changeId, 'pass');
|
|
125
136
|
|
|
126
137
|
const verifyResult = await runVerify({
|
|
@@ -204,4 +204,60 @@ describe('Manifest schema hard constraints', () => {
|
|
|
204
204
|
}
|
|
205
205
|
})).toThrow(/share touches/);
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
test('rejects conflicting parallel tasks with nested touches in same phase', () => {
|
|
209
|
+
expect(() => parseManifest({
|
|
210
|
+
changeId: 'REQ-558',
|
|
211
|
+
goal: 'Reject nested parallel plan',
|
|
212
|
+
createdAt: '2026-04-10T01:00:00.000Z',
|
|
213
|
+
updatedAt: '2026-04-10T01:05:00.000Z',
|
|
214
|
+
currentTaskId: 'T001',
|
|
215
|
+
activePhase: 1,
|
|
216
|
+
tasks: [
|
|
217
|
+
{
|
|
218
|
+
id: 'T001',
|
|
219
|
+
title: '[TEST] A',
|
|
220
|
+
type: 'TEST',
|
|
221
|
+
phase: 1,
|
|
222
|
+
parallel: true,
|
|
223
|
+
dependsOn: [],
|
|
224
|
+
touches: ['packages/billing'],
|
|
225
|
+
run: ['echo ok'],
|
|
226
|
+
checks: ['npm test -- a'],
|
|
227
|
+
acceptance: ['Prove A fails'],
|
|
228
|
+
verification: ['npm test -- a'],
|
|
229
|
+
evidence: ['failing output'],
|
|
230
|
+
context: { readFiles: ['design.md'], commands: ['npm test -- a'], notes: [] },
|
|
231
|
+
reviews: { spec: 'pending', code: 'pending' },
|
|
232
|
+
status: 'pending',
|
|
233
|
+
attempts: 0,
|
|
234
|
+
maxRetries: 1
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'T002',
|
|
238
|
+
title: '[TEST] B',
|
|
239
|
+
type: 'TEST',
|
|
240
|
+
phase: 1,
|
|
241
|
+
parallel: true,
|
|
242
|
+
dependsOn: [],
|
|
243
|
+
touches: ['packages/billing/src/invoices.js'],
|
|
244
|
+
run: ['echo ok'],
|
|
245
|
+
checks: ['npm test -- b'],
|
|
246
|
+
acceptance: ['Prove B fails'],
|
|
247
|
+
verification: ['npm test -- b'],
|
|
248
|
+
evidence: ['failing output'],
|
|
249
|
+
context: { readFiles: ['design.md'], commands: ['npm test -- b'], notes: [] },
|
|
250
|
+
reviews: { spec: 'pending', code: 'pending' },
|
|
251
|
+
status: 'pending',
|
|
252
|
+
attempts: 0,
|
|
253
|
+
maxRetries: 1
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
metadata: {
|
|
257
|
+
source: 'tasks.md',
|
|
258
|
+
generatedBy: 'test',
|
|
259
|
+
planVersion: 1
|
|
260
|
+
}
|
|
261
|
+
})).toThrow(/share touches: packages\/billing/);
|
|
262
|
+
});
|
|
207
263
|
});
|
|
@@ -250,4 +250,33 @@ describe('runWorkerCommand', () => {
|
|
|
250
250
|
expect(nextManifest.tasks.find((task) => task.id === 'T002').status).toBe('pending');
|
|
251
251
|
expect(nextManifest.tasks.find((task) => task.id === 'T003').status).toBe('passed');
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
test('blocks stale worker assignments when manifest planVersion has moved on', async () => {
|
|
255
|
+
const repoRoot = setupRepoRoot('cc-devflow-worker-run-stale-plan-');
|
|
256
|
+
const manifest = createManifest();
|
|
257
|
+
writeManifest(repoRoot, manifest);
|
|
258
|
+
const delegation = await syncDelegationRuntime(repoRoot, 'REQ-123', manifest);
|
|
259
|
+
const workerId = delegation.assignments.find((item) => item.taskId === 'T002').workerId;
|
|
260
|
+
|
|
261
|
+
writeManifest(repoRoot, {
|
|
262
|
+
...manifest,
|
|
263
|
+
metadata: {
|
|
264
|
+
...manifest.metadata,
|
|
265
|
+
planVersion: 3
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await expect(runWorkerCommand({
|
|
270
|
+
repoRoot,
|
|
271
|
+
changeId: 'REQ-123',
|
|
272
|
+
workerId,
|
|
273
|
+
command: 'printf "should-not-run" > blocked.txt'
|
|
274
|
+
})).rejects.toMatchObject({
|
|
275
|
+
name: 'StalePlanVersionError',
|
|
276
|
+
rescueAction: 'rerun delegation sync for current planVersion before worker-run'
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(fs.existsSync(path.join(repoRoot, 'blocked.txt'))).toBe(false);
|
|
280
|
+
expect(fs.existsSync(getCheckpointPath(repoRoot, 'REQ-123', 'T002'))).toBe(false);
|
|
281
|
+
});
|
|
253
282
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 接收 runtime/query/compiler 边界抛出的错误或失败字段。
|
|
3
|
+
* [OUTPUT]: 生成可序列化 named error,保留 artifact refs 与 rescue action。
|
|
4
|
+
* [POS]: skill runtime 的失败语义层,避免用 null/false/string 表达可恢复失败。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class SkillRuntimeError extends Error {
|
|
9
|
+
constructor(name, message, options = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.artifactRefs = options.artifactRefs || [];
|
|
13
|
+
this.rescueAction = options.rescueAction || 'inspect-runtime-artifacts';
|
|
14
|
+
this.details = options.details || {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function namedError(name, message, options = {}) {
|
|
19
|
+
return new SkillRuntimeError(name, message, options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function serializeError(error, fallbackName = 'SkillRuntimeError') {
|
|
23
|
+
const name = error?.name || fallbackName;
|
|
24
|
+
const message = error?.message || String(error || 'Unknown runtime error');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
message,
|
|
29
|
+
artifactRefs: error?.artifactRefs || [],
|
|
30
|
+
rescueAction: error?.rescueAction || 'inspect-runtime-artifacts',
|
|
31
|
+
details: error?.details || {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
SkillRuntimeError,
|
|
37
|
+
namedError,
|
|
38
|
+
serializeError
|
|
39
|
+
};
|
|
@@ -9,6 +9,10 @@ const store = require('./store');
|
|
|
9
9
|
const schemas = require('./schemas');
|
|
10
10
|
const planner = require('./planner');
|
|
11
11
|
const query = require('./query');
|
|
12
|
+
const queryRegistry = require('./query-registry');
|
|
13
|
+
const errors = require('./errors');
|
|
14
|
+
const trace = require('./trace');
|
|
15
|
+
const readiness = require('./readiness');
|
|
12
16
|
const intent = require('./intent');
|
|
13
17
|
const artifacts = require('./artifacts');
|
|
14
18
|
const lifecycle = require('./lifecycle');
|
|
@@ -24,6 +28,10 @@ module.exports = {
|
|
|
24
28
|
...schemas,
|
|
25
29
|
...planner,
|
|
26
30
|
...query,
|
|
31
|
+
...queryRegistry,
|
|
32
|
+
...errors,
|
|
33
|
+
...trace,
|
|
34
|
+
...readiness,
|
|
27
35
|
...artifacts,
|
|
28
36
|
...intent,
|
|
29
37
|
...lifecycle,
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
const { parseRuntimeState, parseManifest } = require('../schemas');
|
|
16
16
|
const { syncIntentMemory } = require('../intent');
|
|
17
17
|
const { normalizeExecutionMode } = require('../lifecycle');
|
|
18
|
+
const { namedError } = require('../errors');
|
|
18
19
|
|
|
19
20
|
async function runApprove({ repoRoot, changeId, executionMode }) {
|
|
20
21
|
const statePath = getRuntimeStatePath(repoRoot, changeId);
|
|
@@ -23,11 +24,25 @@ async function runApprove({ repoRoot, changeId, executionMode }) {
|
|
|
23
24
|
const rawManifest = await readJson(manifestPath, null);
|
|
24
25
|
|
|
25
26
|
if (!rawState) {
|
|
26
|
-
throw
|
|
27
|
+
throw namedError(
|
|
28
|
+
'MissingChangeStateError',
|
|
29
|
+
`Cannot approve ${changeId}: change-state.json is missing`,
|
|
30
|
+
{
|
|
31
|
+
artifactRefs: [statePath],
|
|
32
|
+
rescueAction: 'run cc-roadmap or cc-plan init before approving execution'
|
|
33
|
+
}
|
|
34
|
+
);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
if (!rawManifest) {
|
|
30
|
-
throw
|
|
38
|
+
throw namedError(
|
|
39
|
+
'MissingTaskManifestError',
|
|
40
|
+
`Cannot approve ${changeId}: task-manifest.json is missing`,
|
|
41
|
+
{
|
|
42
|
+
artifactRefs: [manifestPath],
|
|
43
|
+
rescueAction: 'run cc-plan to create planning/task-manifest.json before approving execution'
|
|
44
|
+
}
|
|
45
|
+
);
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
const state = parseRuntimeState(rawState);
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
} = require('../store');
|
|
18
18
|
const { parseReportCard, parseManifest } = require('../schemas');
|
|
19
19
|
const { syncIntentMemory } = require('../intent');
|
|
20
|
+
const { assertShipReady } = require('../readiness');
|
|
20
21
|
|
|
21
22
|
function formatReleaseNote({ changeId, manifest, report }) {
|
|
22
23
|
const passedTasks = manifest.tasks.filter((task) => task.status === 'passed');
|
|
@@ -59,9 +60,11 @@ async function runRelease({ repoRoot, changeId }) {
|
|
|
59
60
|
const manifest = parseManifest(await readJson(manifestPath));
|
|
60
61
|
const previousState = await readJson(statePath, null);
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
assertShipReady(report, {
|
|
64
|
+
reportPath,
|
|
65
|
+
errorName: 'ReleaseReadinessError',
|
|
66
|
+
rescueAction: 'run cc-check until ship-readiness is ready before release'
|
|
67
|
+
});
|
|
65
68
|
|
|
66
69
|
const note = formatReleaseNote({ changeId, manifest, report });
|
|
67
70
|
const releaseNotePath = getReleaseNotePath(repoRoot, changeId);
|