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.
- package/.claude/skills/cc-act/CHANGELOG.md +12 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +28 -5
- package/.claude/skills/cc-act/SKILL.md +45 -12
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +39 -0
- package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +16 -0
- package/.claude/skills/cc-act/references/closure-contract.md +3 -0
- package/.claude/skills/cc-act/scripts/cc-act-common.sh +48 -0
- package/.claude/skills/cc-act/scripts/generate-status-report.sh +3 -0
- package/.claude/skills/cc-act/scripts/render-pr-brief.sh +6 -0
- package/.claude/skills/cc-act/scripts/sync-act-docs.sh +13 -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 +12 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +13 -10
- package/.claude/skills/cc-do/SKILL.md +40 -16
- package/.claude/skills/cc-do/references/execution-recovery.md +12 -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 +12 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +12 -1
- package/.claude/skills/cc-investigate/SKILL.md +31 -5
- package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +44 -0
- package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +1 -0
- package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +9 -1
- package/.claude/skills/cc-investigate/references/investigation-contract.md +2 -0
- package/.claude/skills/cc-plan/CHANGELOG.md +29 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +43 -17
- package/.claude/skills/cc-plan/SKILL.md +85 -44
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +109 -3
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +32 -5
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +85 -4
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +78 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +29 -7
- package/.claude/skills/cc-roadmap/CHANGELOG.md +12 -0
- package/.claude/skills/cc-roadmap/PLAYBOOK.md +15 -9
- package/.claude/skills/cc-roadmap/SKILL.md +22 -16
- package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +3 -1
- package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +11 -1
- package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +57 -10
- package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/markdown.js +68 -3
- package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/schema.js +120 -0
- package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/store.js +25 -1
- package/.claude/skills/cc-roadmap/scripts/locate-roadmap-item.sh +13 -5
- package/.claude/skills/cc-roadmap/scripts/roadmap-tracking.js +3 -3
- package/.claude/skills/cc-roadmap/scripts/sync-roadmap-progress.sh +3 -3
- package/CHANGELOG.md +15 -0
- package/README.md +5 -5
- package/README.zh-CN.md +5 -5
- package/bin/cc-devflow-cli.js +93 -2
- package/docs/CLAUDE.md +1 -1
- package/docs/examples/START-HERE.md +3 -3
- package/docs/examples/example-bindings.json +27 -10
- package/docs/examples/full-design-blocked/BACKLOG.md +4 -2
- package/docs/examples/full-design-blocked/README.md +4 -4
- package/docs/examples/full-design-blocked/ROADMAP.md +16 -2
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +39 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +41 -0
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +8 -1
- package/docs/examples/full-design-blocked/roadmap.json +123 -0
- package/docs/examples/local-handoff/BACKLOG.md +4 -2
- package/docs/examples/local-handoff/README.md +4 -4
- package/docs/examples/local-handoff/ROADMAP.md +16 -2
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +19 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +26 -0
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +8 -1
- package/docs/examples/local-handoff/roadmap.json +121 -0
- package/docs/examples/pdca-loop/BACKLOG.md +4 -2
- package/docs/examples/pdca-loop/README.md +4 -4
- package/docs/examples/pdca-loop/ROADMAP.md +16 -2
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +19 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +22 -3
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +8 -1
- package/docs/examples/pdca-loop/roadmap.json +191 -0
- package/docs/examples/scripts/check-example-bindings.sh +7 -4
- package/docs/get-shit-done-strategy-audit.md +518 -0
- package/docs/guides/getting-started.md +2 -2
- package/docs/guides/getting-started.zh-CN.md +2 -2
- package/lib/compiler/__tests__/inventory.test.js +51 -0
- package/lib/compiler/__tests__/skills-registry.test.js +17 -3
- 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__/cli-bootstrap.integration.test.js +9 -1
- 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,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:
|
|
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 {
|
|
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
|
});
|