cc-devflow 4.5.3 → 4.5.5

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.
Files changed (104) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +12 -0
  2. package/.claude/skills/cc-act/PLAYBOOK.md +28 -5
  3. package/.claude/skills/cc-act/SKILL.md +45 -12
  4. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +39 -0
  5. package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +16 -0
  6. package/.claude/skills/cc-act/references/closure-contract.md +3 -0
  7. package/.claude/skills/cc-act/scripts/cc-act-common.sh +48 -0
  8. package/.claude/skills/cc-act/scripts/generate-status-report.sh +3 -0
  9. package/.claude/skills/cc-act/scripts/render-pr-brief.sh +6 -0
  10. package/.claude/skills/cc-act/scripts/sync-act-docs.sh +13 -0
  11. package/.claude/skills/cc-check/CHANGELOG.md +6 -0
  12. package/.claude/skills/cc-check/PLAYBOOK.md +4 -0
  13. package/.claude/skills/cc-check/SKILL.md +15 -2
  14. package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +18 -0
  15. package/.claude/skills/cc-do/CHANGELOG.md +12 -0
  16. package/.claude/skills/cc-do/PLAYBOOK.md +13 -10
  17. package/.claude/skills/cc-do/SKILL.md +40 -16
  18. package/.claude/skills/cc-do/references/execution-recovery.md +12 -0
  19. package/.claude/skills/cc-do/references/parallel-dispatch.md +6 -4
  20. package/.claude/skills/cc-do/scripts/detect-file-conflicts.sh +49 -3
  21. package/.claude/skills/cc-investigate/CHANGELOG.md +12 -0
  22. package/.claude/skills/cc-investigate/PLAYBOOK.md +12 -1
  23. package/.claude/skills/cc-investigate/SKILL.md +31 -5
  24. package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +44 -0
  25. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +1 -0
  26. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +9 -1
  27. package/.claude/skills/cc-investigate/references/investigation-contract.md +2 -0
  28. package/.claude/skills/cc-plan/CHANGELOG.md +29 -0
  29. package/.claude/skills/cc-plan/PLAYBOOK.md +43 -17
  30. package/.claude/skills/cc-plan/SKILL.md +85 -44
  31. package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +109 -3
  32. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +32 -5
  33. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +85 -4
  34. package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +78 -0
  35. package/.claude/skills/cc-plan/references/planning-contract.md +29 -7
  36. package/.claude/skills/cc-roadmap/CHANGELOG.md +12 -0
  37. package/.claude/skills/cc-roadmap/PLAYBOOK.md +15 -9
  38. package/.claude/skills/cc-roadmap/SKILL.md +22 -16
  39. package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +3 -1
  40. package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +11 -1
  41. package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +57 -10
  42. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/markdown.js +68 -3
  43. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/schema.js +120 -0
  44. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/store.js +25 -1
  45. package/.claude/skills/cc-roadmap/scripts/locate-roadmap-item.sh +13 -5
  46. package/.claude/skills/cc-roadmap/scripts/roadmap-tracking.js +3 -3
  47. package/.claude/skills/cc-roadmap/scripts/sync-roadmap-progress.sh +3 -3
  48. package/CHANGELOG.md +15 -0
  49. package/README.md +5 -5
  50. package/README.zh-CN.md +5 -5
  51. package/bin/cc-devflow-cli.js +93 -2
  52. package/docs/CLAUDE.md +1 -1
  53. package/docs/examples/START-HERE.md +3 -3
  54. package/docs/examples/example-bindings.json +27 -10
  55. package/docs/examples/full-design-blocked/BACKLOG.md +4 -2
  56. package/docs/examples/full-design-blocked/README.md +4 -4
  57. package/docs/examples/full-design-blocked/ROADMAP.md +16 -2
  58. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +39 -1
  59. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +41 -0
  60. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +8 -1
  61. package/docs/examples/full-design-blocked/roadmap.json +123 -0
  62. package/docs/examples/local-handoff/BACKLOG.md +4 -2
  63. package/docs/examples/local-handoff/README.md +4 -4
  64. package/docs/examples/local-handoff/ROADMAP.md +16 -2
  65. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +19 -1
  66. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +26 -0
  67. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +8 -1
  68. package/docs/examples/local-handoff/roadmap.json +121 -0
  69. package/docs/examples/pdca-loop/BACKLOG.md +4 -2
  70. package/docs/examples/pdca-loop/README.md +4 -4
  71. package/docs/examples/pdca-loop/ROADMAP.md +16 -2
  72. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +19 -1
  73. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +22 -3
  74. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +8 -1
  75. package/docs/examples/pdca-loop/roadmap.json +191 -0
  76. package/docs/examples/scripts/check-example-bindings.sh +7 -4
  77. package/docs/get-shit-done-strategy-audit.md +518 -0
  78. package/docs/guides/getting-started.md +2 -2
  79. package/docs/guides/getting-started.zh-CN.md +2 -2
  80. package/lib/compiler/__tests__/inventory.test.js +51 -0
  81. package/lib/compiler/__tests__/skills-registry.test.js +17 -3
  82. package/lib/compiler/inventory.js +78 -0
  83. package/lib/skill-runtime/__tests__/approve.test.js +92 -0
  84. package/lib/skill-runtime/__tests__/autopilot.test.js +4 -0
  85. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +9 -1
  86. package/lib/skill-runtime/__tests__/planner.tdd.test.js +20 -0
  87. package/lib/skill-runtime/__tests__/query.test.js +147 -1
  88. package/lib/skill-runtime/__tests__/readiness.test.js +53 -0
  89. package/lib/skill-runtime/__tests__/release.test.js +85 -0
  90. package/lib/skill-runtime/__tests__/runtime.integration.test.js +11 -0
  91. package/lib/skill-runtime/__tests__/schemas.test.js +56 -0
  92. package/lib/skill-runtime/__tests__/worker-run.test.js +29 -0
  93. package/lib/skill-runtime/errors.js +39 -0
  94. package/lib/skill-runtime/index.js +8 -0
  95. package/lib/skill-runtime/operations/approve.js +17 -2
  96. package/lib/skill-runtime/operations/release.js +6 -3
  97. package/lib/skill-runtime/operations/worker-run.js +30 -0
  98. package/lib/skill-runtime/planner.js +10 -2
  99. package/lib/skill-runtime/query-registry.js +101 -0
  100. package/lib/skill-runtime/query.js +159 -91
  101. package/lib/skill-runtime/readiness.js +84 -0
  102. package/lib/skill-runtime/schemas.js +28 -3
  103. package/lib/skill-runtime/trace.js +22 -0
  104. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ /**
2
+ * [INPUT]: 接收 repo root 与 distributable skill 配置。
3
+ * [OUTPUT]: 校验 managed skill inventory 与 Codex mirror inventory 是否一致。
4
+ * [POS]: compiler/publish gate 的清单奇偶校验层,防止新增 skill 逃出配置与 mirror 管理。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ function listSkillDirs(root, relativeRoot) {
12
+ const skillsRoot = path.join(root, relativeRoot);
13
+
14
+ if (!fs.existsSync(skillsRoot)) {
15
+ return [];
16
+ }
17
+
18
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
19
+ .filter((entry) => entry.isDirectory())
20
+ .filter((entry) => fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
21
+ .map((entry) => entry.name)
22
+ .sort();
23
+ }
24
+
25
+ function missingFrom(expected, actual) {
26
+ return expected.filter((item) => !actual.includes(item));
27
+ }
28
+
29
+ function extraFrom(actual, expected) {
30
+ return actual.filter((item) => !expected.includes(item));
31
+ }
32
+
33
+ function validateSkillInventory({
34
+ root,
35
+ publicSkills = [],
36
+ distributedSkills = [],
37
+ internalSkills = [],
38
+ codexSkills: expectedCodexSkills = publicSkills
39
+ }) {
40
+ const errors = [];
41
+ const configured = [...new Set([...distributedSkills, ...internalSkills])].sort();
42
+ const sourceSkills = listSkillDirs(root, '.claude/skills');
43
+
44
+ for (const skillName of missingFrom(distributedSkills, sourceSkills)) {
45
+ errors.push(`Missing distributed skill directory: ${skillName}`);
46
+ }
47
+
48
+ for (const skillName of extraFrom(sourceSkills, configured)) {
49
+ errors.push(`Unconfigured skill directory: ${skillName}`);
50
+ }
51
+
52
+ for (const skillName of publicSkills) {
53
+ const playbookPath = path.join(root, '.claude/skills', skillName, 'PLAYBOOK.md');
54
+ if (!fs.existsSync(playbookPath)) {
55
+ errors.push(`Public skill missing PLAYBOOK.md: ${skillName}`);
56
+ }
57
+ }
58
+
59
+ const codexRoot = path.join(root, '.codex/skills');
60
+ if (fs.existsSync(codexRoot)) {
61
+ const actualCodexSkills = listSkillDirs(root, '.codex/skills');
62
+
63
+ for (const skillName of missingFrom(expectedCodexSkills, actualCodexSkills)) {
64
+ errors.push(`Codex mirror missing public skill: ${skillName}`);
65
+ }
66
+
67
+ for (const skillName of extraFrom(actualCodexSkills, expectedCodexSkills)) {
68
+ errors.push(`Codex mirror has unconfigured public skill: ${skillName}`);
69
+ }
70
+ }
71
+
72
+ return errors;
73
+ }
74
+
75
+ module.exports = {
76
+ listSkillDirs,
77
+ validateSkillInventory
78
+ };
@@ -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
 
@@ -229,6 +229,11 @@ describe('cc-devflow cli distribution bootstrap', () => {
229
229
  );
230
230
  expect(codexRoadmapSkill.data.writes).toEqual(
231
231
  expect.arrayContaining([
232
+ expect.objectContaining({
233
+ path: 'devflow/roadmap.json',
234
+ durability: 'durable',
235
+ required: true
236
+ }),
232
237
  expect.objectContaining({
233
238
  path: 'devflow/ROADMAP.md',
234
239
  durability: 'durable',
@@ -237,10 +242,13 @@ describe('cc-devflow cli distribution bootstrap', () => {
237
242
  expect.objectContaining({
238
243
  path: 'devflow/BACKLOG.md',
239
244
  durability: 'durable',
240
- required: true
245
+ required: false
241
246
  })
242
247
  ])
243
248
  );
249
+ expect(codexRoadmapSkill.data.writes).not.toEqual(
250
+ expect.arrayContaining([expect.objectContaining({ path: 'devflow/roadmap-tracking.json', required: true })])
251
+ );
244
252
  });
245
253
 
246
254
  test('adapt mirrors Codex skills without baking project YAML output policy', () => {
@@ -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 { getFullState, getNextTask, getProgress } = require('../query');
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
  });