cc-devflow 4.5.4 → 4.5.6

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 (84) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +6 -0
  2. package/.claude/skills/cc-act/PLAYBOOK.md +21 -5
  3. package/.claude/skills/cc-act/SKILL.md +21 -11
  4. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +10 -0
  5. package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +8 -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-do/CHANGELOG.md +6 -0
  12. package/.claude/skills/cc-do/PLAYBOOK.md +7 -6
  13. package/.claude/skills/cc-do/SKILL.md +27 -12
  14. package/.claude/skills/cc-do/references/execution-recovery.md +9 -0
  15. package/.claude/skills/cc-investigate/CHANGELOG.md +6 -0
  16. package/.claude/skills/cc-investigate/PLAYBOOK.md +5 -1
  17. package/.claude/skills/cc-investigate/SKILL.md +22 -5
  18. package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +14 -0
  19. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +1 -0
  20. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +9 -1
  21. package/.claude/skills/cc-investigate/references/investigation-contract.md +2 -0
  22. package/.claude/skills/cc-plan/CHANGELOG.md +35 -0
  23. package/.claude/skills/cc-plan/PLAYBOOK.md +41 -19
  24. package/.claude/skills/cc-plan/SKILL.md +132 -47
  25. package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +77 -3
  26. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +28 -5
  27. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +84 -3
  28. package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +51 -0
  29. package/.claude/skills/cc-plan/references/planning-contract.md +47 -15
  30. package/.claude/skills/cc-roadmap/CHANGELOG.md +12 -0
  31. package/.claude/skills/cc-roadmap/PLAYBOOK.md +15 -9
  32. package/.claude/skills/cc-roadmap/SKILL.md +22 -16
  33. package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +3 -1
  34. package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +11 -1
  35. package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +57 -10
  36. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/markdown.js +68 -3
  37. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/schema.js +120 -0
  38. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/store.js +25 -1
  39. package/.claude/skills/cc-roadmap/scripts/locate-roadmap-item.sh +13 -5
  40. package/.claude/skills/cc-roadmap/scripts/roadmap-tracking.js +3 -3
  41. package/.claude/skills/cc-roadmap/scripts/sync-roadmap-progress.sh +3 -3
  42. package/CHANGELOG.md +19 -0
  43. package/README.md +5 -5
  44. package/README.zh-CN.md +5 -5
  45. package/bin/cc-devflow-cli.js +16 -2
  46. package/docs/CLAUDE.md +1 -1
  47. package/docs/examples/START-HERE.md +3 -3
  48. package/docs/examples/example-bindings.json +26 -9
  49. package/docs/examples/full-design-blocked/BACKLOG.md +4 -2
  50. package/docs/examples/full-design-blocked/README.md +4 -4
  51. package/docs/examples/full-design-blocked/ROADMAP.md +16 -2
  52. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +47 -1
  53. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +97 -0
  54. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +8 -1
  55. package/docs/examples/full-design-blocked/roadmap.json +123 -0
  56. package/docs/examples/local-handoff/BACKLOG.md +4 -2
  57. package/docs/examples/local-handoff/README.md +4 -4
  58. package/docs/examples/local-handoff/ROADMAP.md +16 -2
  59. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +26 -1
  60. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +55 -0
  61. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +8 -1
  62. package/docs/examples/local-handoff/roadmap.json +121 -0
  63. package/docs/examples/pdca-loop/BACKLOG.md +4 -2
  64. package/docs/examples/pdca-loop/README.md +4 -4
  65. package/docs/examples/pdca-loop/ROADMAP.md +16 -2
  66. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +26 -1
  67. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +51 -3
  68. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +8 -1
  69. package/docs/examples/pdca-loop/roadmap.json +191 -0
  70. package/docs/examples/scripts/check-example-bindings.sh +7 -4
  71. package/docs/guides/getting-started.md +2 -2
  72. package/docs/guides/getting-started.zh-CN.md +2 -2
  73. package/lib/compiler/__tests__/skills-registry.test.js +17 -3
  74. package/lib/skill-runtime/__tests__/autopilot.test.js +13 -10
  75. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +9 -1
  76. package/lib/skill-runtime/__tests__/paths.test.js +25 -0
  77. package/lib/skill-runtime/__tests__/query.test.js +49 -0
  78. package/lib/skill-runtime/artifacts.js +2 -2
  79. package/lib/skill-runtime/intent.js +14 -14
  80. package/lib/skill-runtime/operations/autopilot-shared.js +4 -4
  81. package/lib/skill-runtime/paths.js +28 -7
  82. package/lib/skill-runtime/query-registry.js +3 -3
  83. package/lib/skill-runtime/query.js +30 -30
  84. package/package.json +1 -1
@@ -195,7 +195,7 @@ describe('Skills Registry Generator', () => {
195
195
  );
196
196
  });
197
197
 
198
- test('should publish roadmap outputs under devflow', async () => {
198
+ test('should publish unified roadmap state outputs under devflow', async () => {
199
199
  const { generateSkillsRegistryV2 } = require('../skills-registry');
200
200
 
201
201
  const result = await generateSkillsRegistryV2('.claude/skills');
@@ -204,14 +204,28 @@ describe('Skills Registry Generator', () => {
204
204
  expect(roadmap).toBeDefined();
205
205
  expect(roadmap.writes).toEqual(
206
206
  expect.arrayContaining([
207
- expect.objectContaining({ path: 'devflow/ROADMAP.md' }),
208
- expect.objectContaining({ path: 'devflow/BACKLOG.md' })
207
+ expect.objectContaining({
208
+ path: 'devflow/roadmap.json',
209
+ durability: 'durable',
210
+ required: true
211
+ }),
212
+ expect.objectContaining({
213
+ path: 'devflow/ROADMAP.md',
214
+ durability: 'durable',
215
+ required: true
216
+ }),
217
+ expect.objectContaining({
218
+ path: 'devflow/BACKLOG.md',
219
+ durability: 'durable',
220
+ required: false
221
+ })
209
222
  ])
210
223
  );
211
224
  expect(roadmap.writes).not.toEqual(
212
225
  expect.arrayContaining([
213
226
  expect.objectContaining({ path: 'ROADMAP.md' }),
214
227
  expect.objectContaining({ path: 'BACKLOG.md' }),
228
+ expect.objectContaining({ path: 'devflow/roadmap-tracking.json', required: true }),
215
229
  expect.objectContaining({ path: 'devflow/roadmap/roadmap.md' }),
216
230
  expect.objectContaining({ path: 'devflow/roadmap/backlog.md' })
217
231
  ])
@@ -55,9 +55,10 @@ describe('runAutopilot', () => {
55
55
  }
56
56
  });
57
57
 
58
- fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123')), { recursive: true });
58
+ const goal = 'Ship thin autopilot loop';
59
+ fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123', { goal })), { recursive: true });
59
60
  fs.writeFileSync(
60
- getTasksMarkdownPath(repoRoot, 'REQ-123'),
61
+ getTasksMarkdownPath(repoRoot, 'REQ-123', { goal }),
61
62
  [
62
63
  '- [ ] T001 启动 autopilot 闭环 (src/a.ts)',
63
64
  '- [ ] T002 多文件委派任务 [P] (src/a.ts,src/b.ts)'
@@ -67,14 +68,14 @@ describe('runAutopilot', () => {
67
68
  const result = await runAutopilot({
68
69
  repoRoot,
69
70
  changeId: 'REQ-123',
70
- goal: 'Ship thin autopilot loop'
71
+ goal
71
72
  });
72
73
 
73
74
  const runtimeState = JSON.parse(fs.readFileSync(getRuntimeStatePath(repoRoot, 'REQ-123'), 'utf8'));
74
75
 
75
76
  expect(result.executed).toEqual(['init', 'snapshot', 'plan']);
76
77
  expect(result.currentStage).toBe('approve');
77
- expect(runtimeState.changeKey).toBe('REQ-123-change');
78
+ expect(runtimeState.changeKey).toBe('REQ-123-ship-thin-autopilot-loop');
78
79
  expect(fs.existsSync(getReportCardPath(repoRoot, 'REQ-123'))).toBe(false);
79
80
  expect(fs.existsSync(getIntentPrBriefPath(repoRoot, 'REQ-123'))).toBe(false);
80
81
  expect(fs.existsSync(getIntentResumeIndexPath(repoRoot, 'REQ-123'))).toBe(false);
@@ -93,9 +94,10 @@ describe('runAutopilot', () => {
93
94
  }
94
95
  });
95
96
 
96
- fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123')), { recursive: true });
97
+ const goal = 'Ship thin autopilot loop with delegated workers';
98
+ fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123', { goal })), { recursive: true });
97
99
  fs.writeFileSync(
98
- getTasksMarkdownPath(repoRoot, 'REQ-123'),
100
+ getTasksMarkdownPath(repoRoot, 'REQ-123', { goal }),
99
101
  [
100
102
  '- [ ] T001 直接执行任务 (src/a.ts)',
101
103
  '- [ ] T002 委派实现任务 [P] (src/a.ts,src/b.ts)'
@@ -105,7 +107,7 @@ describe('runAutopilot', () => {
105
107
  await runAutopilot({
106
108
  repoRoot,
107
109
  changeId: 'REQ-123',
108
- goal: 'Ship thin autopilot loop with delegated workers'
110
+ goal
109
111
  });
110
112
 
111
113
  await runApprove({
@@ -160,16 +162,17 @@ describe('runAutopilot', () => {
160
162
  }
161
163
  });
162
164
 
163
- fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123')), { recursive: true });
165
+ const goal = 'Ship thin autopilot loop and release';
166
+ fs.mkdirSync(path.dirname(getTasksMarkdownPath(repoRoot, 'REQ-123', { goal })), { recursive: true });
164
167
  fs.writeFileSync(
165
- getTasksMarkdownPath(repoRoot, 'REQ-123'),
168
+ getTasksMarkdownPath(repoRoot, 'REQ-123', { goal }),
166
169
  '- [ ] T001 发布前收尾 (src/a.ts)'
167
170
  );
168
171
 
169
172
  await runAutopilot({
170
173
  repoRoot,
171
174
  changeId: 'REQ-123',
172
- goal: 'Ship thin autopilot loop and release'
175
+ goal
173
176
  });
174
177
 
175
178
  await runApprove({
@@ -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', () => {
@@ -39,4 +39,29 @@ describe('change directory naming', () => {
39
39
 
40
40
  fs.rmSync(repoRoot, { recursive: true, force: true });
41
41
  });
42
+
43
+ test('allows duplicate numbers when descriptions differ', () => {
44
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
45
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-existing-plan'), { recursive: true });
46
+
47
+ const paths = getChangePaths(repoRoot, 'REQ-123', { goal: 'Another plan' });
48
+
49
+ expect(paths.changeKey).toBe('REQ-123-another-plan');
50
+ expect(paths.changeDir).toBe(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-another-plan'));
51
+
52
+ fs.rmSync(repoRoot, { recursive: true, force: true });
53
+ });
54
+
55
+ test('requires explicit change keys when duplicate numbers already exist', () => {
56
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
57
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-first-plan'), { recursive: true });
58
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-second-plan'), { recursive: true });
59
+
60
+ expect(() => getChangePaths(repoRoot, 'REQ-123'))
61
+ .toThrow(/Ambiguous changeId "REQ-123"/);
62
+ expect(getChangePaths(repoRoot, 'REQ-123', { changeKey: 'REQ-123-second-plan' }).changeKey)
63
+ .toBe('REQ-123-second-plan');
64
+
65
+ fs.rmSync(repoRoot, { recursive: true, force: true });
66
+ });
42
67
  });
@@ -324,6 +324,55 @@ describe('query helpers', () => {
324
324
  expect(listQueryIds()).toEqual(expect.arrayContaining(['full-state', 'next-task', 'progress']));
325
325
  });
326
326
 
327
+ test('requires a full change key when duplicate local numbers exist', async () => {
328
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-duplicate-key-'));
329
+ const firstKey = 'REQ-132-first-plan';
330
+ const secondKey = 'REQ-132-second-plan';
331
+
332
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', firstKey), { recursive: true });
333
+ writeJson(getTaskManifestPath(repoRoot, 'REQ-132', { changeKey: secondKey }), {
334
+ changeId: 'REQ-132',
335
+ goal: 'Resolve duplicate local numbers',
336
+ createdAt: '2026-05-08T01:00:00.000Z',
337
+ updatedAt: '2026-05-08T01:05:00.000Z',
338
+ tasks: [
339
+ { id: 'T001', status: 'passed' },
340
+ { id: 'T002', status: 'pending' }
341
+ ],
342
+ metadata: {
343
+ source: 'test',
344
+ generatedBy: 'test',
345
+ planVersion: 1
346
+ }
347
+ });
348
+
349
+ await expect(runQuery('progress', { repoRoot, changeId: 'REQ-132' })).resolves.toMatchObject({
350
+ ok: false,
351
+ queryId: 'progress',
352
+ error: {
353
+ message: expect.stringContaining('Ambiguous changeId "REQ-132"')
354
+ },
355
+ trace: {
356
+ event: 'query.progress.failed',
357
+ nextAction: 'inspect-runtime-artifacts'
358
+ }
359
+ });
360
+
361
+ await expect(runQuery('progress', {
362
+ repoRoot,
363
+ changeId: 'REQ-132',
364
+ changeKey: secondKey
365
+ })).resolves.toMatchObject({
366
+ ok: true,
367
+ queryId: 'progress',
368
+ data: {
369
+ totalTasks: 2,
370
+ completedTasks: 1,
371
+ pendingTasks: 1
372
+ }
373
+ });
374
+ });
375
+
327
376
  test('returns a named error for unknown query ids', async () => {
328
377
  const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-query-unknown-'));
329
378
 
@@ -68,8 +68,8 @@ function getLegacyIntentProjectionPaths(repoRoot, goalId, taskIds = [], options
68
68
  ];
69
69
  }
70
70
 
71
- async function ensureIntentScaffold(repoRoot, goalId) {
72
- const change = getChangePaths(repoRoot, goalId);
71
+ async function ensureIntentScaffold(repoRoot, goalId, options = {}) {
72
+ const change = getChangePaths(repoRoot, goalId, options);
73
73
 
74
74
  await ensureDir(change.changeDir);
75
75
  await ensureDir(change.metaDir);
@@ -68,11 +68,11 @@ function summarizeGateSection(gates = []) {
68
68
  });
69
69
  }
70
70
 
71
- async function readLatestCheckpoints(repoRoot, changeId, tasks = []) {
71
+ async function readLatestCheckpoints(repoRoot, changeId, tasks = [], options = {}) {
72
72
  const checkpoints = [];
73
73
 
74
74
  for (const task of tasks) {
75
- const checkpoint = await readJson(getCheckpointPath(repoRoot, changeId, task.id), null);
75
+ const checkpoint = await readJson(getCheckpointPath(repoRoot, changeId, task.id, options), null);
76
76
  if (!checkpoint) {
77
77
  continue;
78
78
  }
@@ -90,28 +90,28 @@ async function readLatestCheckpoints(repoRoot, changeId, tasks = []) {
90
90
  return checkpoints;
91
91
  }
92
92
 
93
- async function cleanupLegacyArtifacts(repoRoot, changeId, manifest) {
93
+ async function cleanupLegacyArtifacts(repoRoot, changeId, manifest, options = {}) {
94
94
  const taskIds = (manifest?.tasks || []).map((task) => task.id);
95
- const change = getChangePaths(repoRoot, changeId);
95
+ const change = getChangePaths(repoRoot, changeId, options);
96
96
 
97
97
  await Promise.all(
98
- getLegacyIntentProjectionPaths(repoRoot, changeId, taskIds).map((target) => removePath(target))
98
+ getLegacyIntentProjectionPaths(repoRoot, changeId, taskIds, options).map((target) => removePath(target))
99
99
  );
100
100
  await removePath(change.workersDir);
101
101
  }
102
102
 
103
- async function clearHandoffArtifacts(repoRoot, changeId, keep = null) {
103
+ async function clearHandoffArtifacts(repoRoot, changeId, keep = null, options = {}) {
104
104
  await Promise.all(
105
- getIntentHandoffArtifactPaths(repoRoot, changeId)
105
+ getIntentHandoffArtifactPaths(repoRoot, changeId, options)
106
106
  .filter((target) => target !== keep)
107
107
  .map((target) => removePath(target))
108
108
  );
109
109
  }
110
110
 
111
- async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
112
- await ensureIntentScaffold(repoRoot, changeId);
111
+ async function writeResumeIndex(repoRoot, changeId, state, manifest, report, options = {}) {
112
+ await ensureIntentScaffold(repoRoot, changeId, options);
113
113
 
114
- const checkpoints = await readLatestCheckpoints(repoRoot, changeId, manifest?.tasks || []);
114
+ const checkpoints = await readLatestCheckpoints(repoRoot, changeId, manifest?.tasks || [], options);
115
115
  const lastGoodCheckpoint = checkpoints.find((checkpoint) => checkpoint.status === 'passed') || checkpoints[0] || null;
116
116
  const blockers = [
117
117
  ...(manifest?.tasks || [])
@@ -119,8 +119,8 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
119
119
  .map((task) => `${task.id}: ${task.lastError || 'Task failed'}`),
120
120
  ...(report?.blockingFindings || [])
121
121
  ];
122
- const hasPrBrief = await exists(getIntentPrBriefPath(repoRoot, changeId));
123
- const hasReleaseNote = await exists(getReleaseNotePath(repoRoot, changeId));
122
+ const hasPrBrief = await exists(getIntentPrBriefPath(repoRoot, changeId, options));
123
+ const hasReleaseNote = await exists(getReleaseNotePath(repoRoot, changeId, options));
124
124
  const stage = deriveLifecycleStage({ state, manifest, report, hasPrBrief });
125
125
  const approval = getApprovalState(state, manifest);
126
126
  const goal = manifest?.goal || state?.goal || changeId;
@@ -131,7 +131,7 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
131
131
  hasPrBrief,
132
132
  hasReleaseNote
133
133
  });
134
- const resumePath = getIntentResumeIndexPath(repoRoot, changeId);
134
+ const resumePath = getIntentResumeIndexPath(repoRoot, changeId, options);
135
135
 
136
136
  const content = [
137
137
  `# Resume Index: ${changeId}`,
@@ -167,7 +167,7 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
167
167
  ])
168
168
  ].join('\n');
169
169
 
170
- await clearHandoffArtifacts(repoRoot, changeId, resumePath);
170
+ await clearHandoffArtifacts(repoRoot, changeId, resumePath, options);
171
171
  await writeText(resumePath, `${content}\n`);
172
172
 
173
173
  return {
@@ -28,11 +28,11 @@ function hasUnresolvedTasks(manifest) {
28
28
  );
29
29
  }
30
30
 
31
- async function loadState(repoRoot, changeId) {
31
+ async function loadState(repoRoot, changeId, options = {}) {
32
32
  const [state, manifest, report] = await Promise.all([
33
- readJson(getRuntimeStatePath(repoRoot, changeId), null),
34
- readJson(getTaskManifestPath(repoRoot, changeId), null),
35
- readJson(getReportCardPath(repoRoot, changeId), null)
33
+ readJson(getRuntimeStatePath(repoRoot, changeId, options), null),
34
+ readJson(getTaskManifestPath(repoRoot, changeId, options), null),
35
+ readJson(getReportCardPath(repoRoot, changeId, options), null)
36
36
  ]);
37
37
 
38
38
  return { state, manifest, report };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * [INPUT]: 依赖 fs/path,接收 repoRoot/changeId/taskId/workerId 等定位参数。
3
3
  * [OUTPUT]: 对外提供 devflow canonical layout 的唯一路径解析能力。
4
- * [POS]: skill runtime 的路径真相源,强制新 change 目录使用 REQ/FIX 前缀。
4
+ * [POS]: skill runtime 的路径真相源,完整 change key 才是目录身份。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
6
  */
7
7
 
@@ -85,17 +85,27 @@ function listChangeKeys(repoRoot) {
85
85
  .sort();
86
86
  }
87
87
 
88
- function findExistingChangeKey(repoRoot, changeId) {
88
+ function findExistingChangeKeys(repoRoot, changeId) {
89
89
  const canonicalPrefix = `${getChangeKeyPrefix(changeId)}-`;
90
90
  const legacyKey = slugifySegment(changeId, 'change');
91
91
  const legacyPrefix = `${legacyKey}-`;
92
92
 
93
- return listChangeKeys(repoRoot).find((name) => (
93
+ return listChangeKeys(repoRoot).filter((name) => (
94
94
  name === changeId
95
95
  || name === legacyKey
96
96
  || name.startsWith(canonicalPrefix)
97
97
  || name.startsWith(legacyPrefix)
98
- )) || null;
98
+ ));
99
+ }
100
+
101
+ function findExactChangeKey(repoRoot, changeId, changeKey) {
102
+ const slug = getChangeSlug(changeId, changeKey);
103
+ const aliases = new Set([
104
+ changeKey,
105
+ `${slugifySegment(changeId, 'change')}-${slug}`
106
+ ]);
107
+
108
+ return listChangeKeys(repoRoot).find((name) => aliases.has(name)) || null;
99
109
  }
100
110
 
101
111
  function assertChangeKey(changeId, changeKey) {
@@ -132,9 +142,20 @@ function resolveChangeKey(repoRoot, changeId, options = {}) {
132
142
  return assertChangeKey(changeId, options.changeKey);
133
143
  }
134
144
 
135
- const existing = findExistingChangeKey(repoRoot, changeId);
136
- if (existing) {
137
- return existing;
145
+ const requestedSlug = options.slug || stripChangeIdPrefix(changeId, options.goal);
146
+ if (requestedSlug) {
147
+ const requestedKey = buildChangeKey(changeId, { slug: requestedSlug });
148
+ return findExactChangeKey(repoRoot, changeId, requestedKey) || requestedKey;
149
+ }
150
+
151
+ const existing = findExistingChangeKeys(repoRoot, changeId);
152
+ if (existing.length === 1) {
153
+ return existing[0];
154
+ }
155
+ if (existing.length > 1) {
156
+ throw new Error(
157
+ `Ambiguous changeId "${changeId}": found ${existing.join(', ')}. Pass an explicit changeKey.`
158
+ );
138
159
  }
139
160
 
140
161
  return buildChangeKey(changeId, options);
@@ -48,10 +48,10 @@ function createQueryRegistry(entries) {
48
48
  };
49
49
  }
50
50
 
51
- const artifactRefs = resolveArtifactRefs(entry, context, 'artifactRefs');
52
- const requiredArtifactRefs = resolveArtifactRefs(entry, context, 'requiredArtifactRefs');
53
-
51
+ let artifactRefs = [];
54
52
  try {
53
+ artifactRefs = resolveArtifactRefs(entry, context, 'artifactRefs');
54
+ const requiredArtifactRefs = resolveArtifactRefs(entry, context, 'requiredArtifactRefs');
55
55
  const missingRefs = requiredArtifactRefs.filter((ref) => !fs.existsSync(ref));
56
56
  if (missingRefs.length > 0) {
57
57
  throw namedError(
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [INPUT]: 依赖 store/artifacts/lifecycle 读取 requirement 工件,接收 repoRoot changeId
2
+ * [INPUT]: 依赖 store/artifacts/lifecycle 读取 requirement 工件,接收 repoRoot、changeId 和可选 changeKey
3
3
  * [OUTPUT]: 对外提供 typed query registry 与兼容查询函数,附 named error 和 trace shape。
4
4
  * [POS]: skill runtime 的薄查询兼容层,只读 artifact 与共享 lifecycle 语义,不再自带流程推导副本。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
@@ -65,8 +65,8 @@ async function readQueryArtifact(filePath, { required = true } = {}) {
65
65
  * @param {string} changeId - 需求 ID
66
66
  * @returns {Promise<Object>} 进度统计对象
67
67
  */
68
- async function getProgress(repoRoot, changeId) {
69
- const manifestPath = getTaskManifestPath(repoRoot, changeId);
68
+ async function getProgress(repoRoot, changeId, options = {}) {
69
+ const manifestPath = getTaskManifestPath(repoRoot, changeId, options);
70
70
  const manifest = await readQueryArtifact(manifestPath);
71
71
  return deriveTaskProgress(manifest.tasks || []);
72
72
  }
@@ -77,8 +77,8 @@ async function getProgress(repoRoot, changeId) {
77
77
  * @param {string} changeId - 需求 ID
78
78
  * @returns {Promise<Object|null>} 下一个任务对象或 null
79
79
  */
80
- async function getNextTask(repoRoot, changeId) {
81
- const manifestPath = getTaskManifestPath(repoRoot, changeId);
80
+ async function getNextTask(repoRoot, changeId, options = {}) {
81
+ const manifestPath = getTaskManifestPath(repoRoot, changeId, options);
82
82
  const manifest = await readQueryArtifact(manifestPath);
83
83
  const executionState = deriveManifestExecutionState(manifest.tasks || []);
84
84
  const activePhase = manifest.activePhase ?? executionState.activePhase;
@@ -117,18 +117,18 @@ async function getNextTask(repoRoot, changeId) {
117
117
  * @param {string} changeId - 需求 ID
118
118
  * @returns {Promise<Object>} 完整状态对象
119
119
  */
120
- async function getFullState(repoRoot, changeId) {
121
- const statePath = getRuntimeStatePath(repoRoot, changeId);
122
- const reportPath = getReportCardPath(repoRoot, changeId);
123
- const prBriefPath = getIntentPrBriefPath(repoRoot, changeId);
120
+ async function getFullState(repoRoot, changeId, options = {}) {
121
+ const statePath = getRuntimeStatePath(repoRoot, changeId, options);
122
+ const reportPath = getReportCardPath(repoRoot, changeId, options);
123
+ const prBriefPath = getIntentPrBriefPath(repoRoot, changeId, options);
124
124
 
125
125
  const [state, manifest, hasPrBrief] = await Promise.all([
126
126
  readQueryArtifact(statePath),
127
- readQueryArtifact(getTaskManifestPath(repoRoot, changeId)),
127
+ readQueryArtifact(getTaskManifestPath(repoRoot, changeId, options)),
128
128
  exists(prBriefPath)
129
129
  ]);
130
- const progress = await getProgress(repoRoot, changeId);
131
- const nextTask = await getNextTask(repoRoot, changeId);
130
+ const progress = await getProgress(repoRoot, changeId, options);
131
+ const nextTask = await getNextTask(repoRoot, changeId, options);
132
132
  const report = await readQueryArtifact(reportPath, { required: false });
133
133
 
134
134
  return {
@@ -160,8 +160,8 @@ async function getFullState(repoRoot, changeId) {
160
160
  };
161
161
  }
162
162
 
163
- async function getShipReadiness(repoRoot, changeId) {
164
- const reportPath = getReportCardPath(repoRoot, changeId);
163
+ async function getShipReadiness(repoRoot, changeId, options = {}) {
164
+ const reportPath = getReportCardPath(repoRoot, changeId, options);
165
165
  const report = await readJson(reportPath, null);
166
166
 
167
167
  if (!report) {
@@ -178,12 +178,12 @@ async function getShipReadiness(repoRoot, changeId) {
178
178
  return deriveShipReadiness(report, { reportPath });
179
179
  }
180
180
 
181
- function queryArtifactRefs(repoRoot, changeId, names) {
181
+ function queryArtifactRefs(repoRoot, changeId, names, options = {}) {
182
182
  const refs = {
183
- manifest: getTaskManifestPath(repoRoot, changeId),
184
- state: getRuntimeStatePath(repoRoot, changeId),
185
- report: getReportCardPath(repoRoot, changeId),
186
- prBrief: getIntentPrBriefPath(repoRoot, changeId)
183
+ manifest: getTaskManifestPath(repoRoot, changeId, options),
184
+ state: getRuntimeStatePath(repoRoot, changeId, options),
185
+ report: getReportCardPath(repoRoot, changeId, options),
186
+ prBrief: getIntentPrBriefPath(repoRoot, changeId, options)
187
187
  };
188
188
 
189
189
  return names.map((name) => refs[name]).filter(Boolean);
@@ -192,26 +192,26 @@ function queryArtifactRefs(repoRoot, changeId, names) {
192
192
  const registry = createQueryRegistry([
193
193
  {
194
194
  id: 'progress',
195
- artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
196
- requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
197
- handler: ({ repoRoot, changeId }) => getProgress(repoRoot, changeId)
195
+ artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
196
+ requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
197
+ handler: ({ repoRoot, changeId, changeKey }) => getProgress(repoRoot, changeId, { changeKey })
198
198
  },
199
199
  {
200
200
  id: 'next-task',
201
- artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
202
- requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
203
- handler: ({ repoRoot, changeId }) => getNextTask(repoRoot, changeId)
201
+ artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
202
+ requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
203
+ handler: ({ repoRoot, changeId, changeKey }) => getNextTask(repoRoot, changeId, { changeKey })
204
204
  },
205
205
  {
206
206
  id: 'full-state',
207
- artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest', 'report', 'prBrief']),
208
- requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest']),
209
- handler: ({ repoRoot, changeId }) => getFullState(repoRoot, changeId)
207
+ artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest', 'report', 'prBrief'], { changeKey }),
208
+ requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest'], { changeKey }),
209
+ handler: ({ repoRoot, changeId, changeKey }) => getFullState(repoRoot, changeId, { changeKey })
210
210
  },
211
211
  {
212
212
  id: 'ship-readiness',
213
- artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['report']),
214
- handler: ({ repoRoot, changeId }) => getShipReadiness(repoRoot, changeId)
213
+ artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['report'], { changeKey }),
214
+ handler: ({ repoRoot, changeId, changeKey }) => getShipReadiness(repoRoot, changeId, { changeKey })
215
215
  }
216
216
  ]);
217
217
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-devflow",
3
- "version": "4.5.4",
3
+ "version": "4.5.6",
4
4
  "description": "Multi-platform CLI and skill pack for agent coding",
5
5
  "main": "bin/cc-devflow.js",
6
6
  "bin": {