cc-devflow 4.5.5 → 4.5.7

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 (73) hide show
  1. package/.claude/skills/cc-act/PLAYBOOK.md +2 -2
  2. package/.claude/skills/cc-act/SKILL.md +2 -2
  3. package/.claude/skills/cc-act/scripts/{archive-requirement.sh → archive-change.sh} +7 -7
  4. package/.claude/skills/cc-investigate/CHANGELOG.md +5 -0
  5. package/.claude/skills/cc-investigate/SKILL.md +2 -2
  6. package/.claude/skills/cc-plan/CHANGELOG.md +34 -0
  7. package/.claude/skills/cc-plan/PLAYBOOK.md +22 -17
  8. package/.claude/skills/cc-plan/SKILL.md +135 -12
  9. package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +51 -0
  10. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +2 -0
  11. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +66 -3
  12. package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +37 -0
  13. package/.claude/skills/cc-plan/references/planning-contract.md +33 -10
  14. package/.claude/skills/cc-plan/scripts/next-change-key.sh +78 -0
  15. package/.claude/skills/cc-review/CHANGELOG.md +7 -0
  16. package/.claude/skills/cc-review/PLAYBOOK.md +54 -0
  17. package/.claude/skills/cc-review/SKILL.md +173 -0
  18. package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +81 -0
  19. package/.claude/skills/cc-review/references/implementation-review-branch.md +115 -0
  20. package/.claude/skills/cc-review/references/plan-review-branch.md +116 -0
  21. package/.claude/skills/cc-review/references/review-methods.md +126 -0
  22. package/.claude/skills/cc-roadmap/CHANGELOG.md +6 -0
  23. package/.claude/skills/cc-roadmap/SKILL.md +102 -8
  24. package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +3 -0
  25. package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +23 -0
  26. package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +20 -1
  27. package/.claude/skills/cc-roadmap/references/roadmap-dialogue.md +28 -13
  28. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/markdown.js +18 -0
  29. package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/schema.js +8 -0
  30. package/CHANGELOG.md +21 -0
  31. package/README.md +10 -5
  32. package/README.zh-CN.md +10 -5
  33. package/bin/cc-devflow-cli.js +135 -2
  34. package/config/distributable-skills.json +2 -0
  35. package/docs/CLAUDE.md +1 -1
  36. package/docs/examples/example-bindings.json +5 -4
  37. package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
  38. package/docs/examples/full-design-blocked/README.md +1 -1
  39. package/docs/examples/full-design-blocked/ROADMAP.md +16 -1
  40. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +42 -1
  41. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +345 -65
  42. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +2 -1
  43. package/docs/examples/full-design-blocked/roadmap.json +18 -2
  44. package/docs/examples/local-handoff/BACKLOG.md +1 -1
  45. package/docs/examples/local-handoff/README.md +1 -1
  46. package/docs/examples/local-handoff/ROADMAP.md +16 -1
  47. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +34 -1
  48. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +197 -39
  49. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +2 -1
  50. package/docs/examples/local-handoff/roadmap.json +16 -2
  51. package/docs/examples/pdca-loop/BACKLOG.md +1 -1
  52. package/docs/examples/pdca-loop/README.md +1 -1
  53. package/docs/examples/pdca-loop/ROADMAP.md +16 -1
  54. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +34 -1
  55. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +89 -8
  56. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +2 -1
  57. package/docs/examples/pdca-loop/roadmap.json +16 -2
  58. package/docs/examples/scripts/check-example-bindings.sh +2 -0
  59. package/docs/guides/getting-started.md +13 -10
  60. package/docs/guides/getting-started.zh-CN.md +13 -10
  61. package/lib/skill-runtime/__tests__/archive-change.test.js +124 -0
  62. package/lib/skill-runtime/__tests__/autopilot.test.js +13 -10
  63. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +1 -0
  64. package/lib/skill-runtime/__tests__/paths.test.js +106 -1
  65. package/lib/skill-runtime/__tests__/query.test.js +49 -0
  66. package/lib/skill-runtime/archive-change.js +64 -0
  67. package/lib/skill-runtime/artifacts.js +2 -2
  68. package/lib/skill-runtime/intent.js +14 -14
  69. package/lib/skill-runtime/operations/autopilot-shared.js +4 -4
  70. package/lib/skill-runtime/paths.js +60 -7
  71. package/lib/skill-runtime/query-registry.js +3 -3
  72. package/lib/skill-runtime/query.js +30 -30
  73. package/package.json +2 -1
@@ -11,13 +11,13 @@ CC-DevFlow has two entry paths:
11
11
  - `cc-devflow init`: install the whole `.claude` pack into your project
12
12
  - `cc-devflow adapt`: generate platform outputs such as Codex rules
13
13
 
14
- The workflow itself is driven by six visible skills:
14
+ The core workflow is driven by six visible skills, with `cc-review` available as an optional deep review pass:
15
15
 
16
16
  ```text
17
17
  cc-roadmap
18
18
 
19
- PDCA: cc-plan -> cc-do -> cc-check -> cc-act
20
- IDCA: cc-investigate -> cc-do -> cc-check -> cc-act
19
+ PDCA: cc-plan -> [cc-review] -> cc-do -> [cc-review] -> cc-check -> cc-act
20
+ IDCA: cc-investigate -> [cc-review] -> cc-do -> [cc-review] -> cc-check -> cc-act
21
21
  ```
22
22
 
23
23
  The public skills are the visible harness. Each distributed `SKILL.md` now carries structured frontmatter plus a `Harness Contract`, and each `PLAYBOOK.md` carries the stage transition rules in a `Visible State Machine` section.
@@ -36,7 +36,7 @@ The public skills are the visible harness. Each distributed `SKILL.md` now carri
36
36
  npx cc-devflow init --dir /path/to/your/project
37
37
  ```
38
38
 
39
- The whole-pack install includes the six visible workflow skills plus `cc-spec-init` and `cc-simplify` as maintenance helpers.
39
+ The whole-pack install includes the six core workflow skills, optional `cc-review`, plus `cc-spec-init` and `cc-simplify` as maintenance helpers.
40
40
 
41
41
  ### Single Skill Install
42
42
 
@@ -75,10 +75,12 @@ Use the skills in this order:
75
75
  ```text
76
76
  1. cc-roadmap
77
77
  2. choose cc-plan or cc-investigate
78
- 3. cc-do
79
- 4. cc-check
80
- 5. cc-act
81
- 6. repeat
78
+ 3. optional cc-review for complex frozen plans or investigations
79
+ 4. cc-do
80
+ 5. optional cc-review for complex implementations
81
+ 6. cc-check
82
+ 7. cc-act
83
+ 8. repeat
82
84
  ```
83
85
 
84
86
  Typical outputs:
@@ -87,6 +89,7 @@ Typical outputs:
87
89
  - `cc-spec-init` writes `devflow/specs/INDEX.md`, capability specs, and `change-meta.json`
88
90
  - `cc-plan` writes `planning/design.md`, `planning/tasks.md`, `task-manifest.json`, and `change-meta.json`
89
91
  - `cc-investigate` writes `planning/analysis.md`, `planning/tasks.md`, `task-manifest.json`, and `change-meta.json`
92
+ - `cc-review` writes `cc-review-report.md` and optional structured findings for deep plan or implementation review
90
93
  - `cc-check` writes `report-card.json`
91
94
  - `cc-act` writes exactly one final handoff file: `handoff/pr-brief.md`, `handoff/resume-index.md`, or `handoff/release-note.md`
92
95
 
@@ -94,7 +97,7 @@ Capability truth lives in `devflow/specs/`.
94
97
  Change truth lives in `devflow/changes/<change>/`.
95
98
 
96
99
  - Keep `INDEX.md` plus capability markdown under `devflow/specs/`.
97
- - Name new change directories as `REQ-<number>-<description>` for requirements or `FIX-<number>-<description>` for bug fixes. `REQ` and `FIX` are separate number sequences; old lowercase directories are compatibility reads only.
100
+ - Name new change directories as `REQ-<number>-<description>` for requirements or `FIX-<number>-<description>` for bug fixes. `REQ` and `FIX` advance as separate local sequences, so cross-prefix duplicates are valid. Parallel worktrees may still repeat numbers; the full change key, especially the description, distinguishes the work. Old lowercase directories are compatibility reads only.
98
101
  - Keep `change-state.json`, `change-meta.json`, planning docs, `task-manifest.json`, optional `team-state.json`, task `checkpoint.json`, `report-card.json`, and one final handoff file under each `devflow/changes/<change>/`.
99
102
  - Worker prompts, journals, assignments, and session logs belong under `devflow/workspaces/<change>/` as ephemeral scratch.
100
103
 
@@ -151,7 +154,7 @@ npx cc-devflow adapt --cwd /path/to/your/project --platform codex
151
154
 
152
155
  If your project has no optional `.claude/commands/` input, this is expected: the compiler will still generate the skills registry and mirror the distributed skill set for Codex.
153
156
 
154
- Codex mirrors the distributed skills from `.claude/skills/<skill>/` into `.codex/skills/<skill>/`. That set includes the six public workflow skills plus `cc-spec-init` and `cc-simplify`, and the mirror is additive-only: existing project-owned Codex skills are preserved instead of being deleted.
157
+ Codex mirrors the distributed skills from `.claude/skills/<skill>/` into `.codex/skills/<skill>/`. That set includes the six core workflow skills, optional `cc-review`, `cc-spec-init`, and `cc-simplify`, and the mirror is additive-only: existing project-owned Codex skills are preserved instead of being deleted.
155
158
 
156
159
  ### Keep skills and examples in sync
157
160
 
@@ -11,13 +11,13 @@ CC-DevFlow 现在有两条入口:
11
11
  - `cc-devflow init`:把整包 `.claude` 安装到你的项目里
12
12
  - `cc-devflow adapt`:生成 Codex、Cursor、Qwen、Antigravity 等平台产物
13
13
 
14
- 真正的工作流由 6 个可见 Skill 组成:
14
+ 核心工作流由 6 个可见 Skill 组成,复杂工作可选 `cc-review` 做深度 Review:
15
15
 
16
16
  ```text
17
17
  cc-roadmap
18
18
 
19
- PDCA: cc-plan -> cc-do -> cc-check -> cc-act
20
- IDCA: cc-investigate -> cc-do -> cc-check -> cc-act
19
+ PDCA: cc-plan -> [cc-review] -> cc-do -> [cc-review] -> cc-check -> cc-act
20
+ IDCA: cc-investigate -> [cc-review] -> cc-do -> [cc-review] -> cc-check -> cc-act
21
21
  ```
22
22
 
23
23
  公开 Skill 本身就是可见 harness。现在每个分发 `SKILL.md` 都带结构化 frontmatter 和 `Harness Contract`,每个 `PLAYBOOK.md` 都带 `Visible State Machine`,不再依赖隐藏运行时语义来理解阶段流转。
@@ -36,7 +36,7 @@ IDCA: cc-investigate -> cc-do -> cc-check -> cc-act
36
36
  npx cc-devflow init --dir /path/to/your/project
37
37
  ```
38
38
 
39
- 整包安装会带上 6 个可见 workflow skill,以及维护用的 `cc-spec-init` 和 `cc-simplify`。
39
+ 整包安装会带上 6 个核心 workflow skill、可选 `cc-review`,以及维护用的 `cc-spec-init` 和 `cc-simplify`。
40
40
 
41
41
  ### 单个 Skill 安装
42
42
 
@@ -75,10 +75,12 @@ find .codex/skills -mindepth 2 -maxdepth 2 -name SKILL.md | sort
75
75
  ```text
76
76
  1. cc-roadmap
77
77
  2. 在 cc-plan 和 cc-investigate 里二选一
78
- 3. cc-do
79
- 4. cc-check
80
- 5. cc-act
81
- 6. repeat
78
+ 3. 复杂计划或调查根因冻结后可选 cc-review
79
+ 4. cc-do
80
+ 5. 复杂实现可选 cc-review
81
+ 6. cc-check
82
+ 7. cc-act
83
+ 8. repeat
82
84
  ```
83
85
 
84
86
  常见产物:
@@ -87,13 +89,14 @@ find .codex/skills -mindepth 2 -maxdepth 2 -name SKILL.md | sort
87
89
  - `cc-spec-init` 产出 `devflow/specs/INDEX.md`、capability spec 和 `change-meta.json`
88
90
  - `cc-plan` 产出 `planning/design.md`、`planning/tasks.md`、`task-manifest.json` 和 `change-meta.json`
89
91
  - `cc-investigate` 产出 `planning/analysis.md`、`planning/tasks.md`、`task-manifest.json` 和 `change-meta.json`
92
+ - `cc-review` 产出 `cc-review-report.md`,以及可选的结构化深度 Review findings
90
93
  - `cc-check` 产出 `report-card.json`
91
94
  - `cc-act` 只产出一个最终 handoff 文件:`handoff/pr-brief.md`、`handoff/resume-index.md` 或 `handoff/release-note.md`
92
95
 
93
96
  durable truth 分两层:
94
97
 
95
98
  - `devflow/specs/`:capability 真相,保留 `INDEX.md` 与 `capabilities/*.md`
96
- - 新 change 目录必须命名为 `REQ-<number>-<description>`(需求)或 `FIX-<number>-<description>`(修复);`REQ` 和 `FIX` 分别维护自己的递增编号,旧小写目录只作为历史兼容读取。
99
+ - 新 change 目录必须命名为 `REQ-<number>-<description>`(需求)或 `FIX-<number>-<description>`(修复);`REQ` 和 `FIX` 分别维护自己的递增编号,跨前缀同号不是冲突;并行工作树造成重复编号时,完整 change key 的描述负责区分业务内容,旧小写目录只作为历史兼容读取。
97
100
  - `devflow/changes/<change>/`:变更真相,保留 `change-state.json`、`change-meta.json`、planning 文档、`task-manifest.json`、可选 `team-state.json`、任务级 `checkpoint.json`、`report-card.json` 和唯一的最终 handoff 文件。
98
101
  - worker prompt、journal、assignment、session log 统一放到 `devflow/workspaces/<change>/`,作为 ephemeral scratch。
99
102
 
@@ -150,7 +153,7 @@ npx cc-devflow adapt --cwd /path/to/your/project --platform codex
150
153
 
151
154
  如果你的项目没有可选的 `.claude/commands/` 输入目录,这也是正常的;编译器仍然会生成 skills registry,并为 Codex 镜像正式分发 skill 集合。
152
155
 
153
- Codex 现在会把正式分发的 skill 从 `.claude/skills/<skill>/` 镜像到 `.codex/skills/<skill>/`。这套集合包含 6 个公开 workflow skill 和维护类 skill `cc-spec-init`、`cc-simplify`,并且镜像是纯增量的:项目里已有的自定义 Codex skill 不会被删除。
156
+ Codex 现在会把正式分发的 skill 从 `.claude/skills/<skill>/` 镜像到 `.codex/skills/<skill>/`。这套集合包含 6 个核心 workflow skill、可选 `cc-review` 和维护类 skill `cc-spec-init`、`cc-simplify`,并且镜像是纯增量的:项目里已有的自定义 Codex skill 不会被删除。
154
157
 
155
158
  ### 保持 skill 和样例同步
156
159
 
@@ -0,0 +1,124 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const { archiveChange, restoreChange, listArchived, getArchiveRoot } = require('../archive-change');
6
+
7
+ function makeTempRepo() {
8
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-archive-'));
9
+ const changesDir = path.join(repoRoot, 'devflow', 'changes');
10
+ fs.mkdirSync(changesDir, { recursive: true });
11
+ return repoRoot;
12
+ }
13
+
14
+ afterEach(() => {});
15
+
16
+ describe('archiveChange', () => {
17
+ test('moves change directory to archive/YYYY-MM/', () => {
18
+ const repoRoot = makeTempRepo();
19
+ const changeDir = path.join(repoRoot, 'devflow', 'changes', 'REQ-001-feature');
20
+ fs.mkdirSync(changeDir, { recursive: true });
21
+ fs.writeFileSync(path.join(changeDir, 'proposal.md'), '# test');
22
+
23
+ const result = archiveChange(repoRoot, 'REQ-001-feature');
24
+ const month = new Date().toISOString().slice(0, 7);
25
+
26
+ expect(result.month).toBe(month);
27
+ expect(fs.existsSync(result.archived)).toBe(true);
28
+ expect(fs.existsSync(path.join(result.archived, 'proposal.md'))).toBe(true);
29
+ expect(fs.existsSync(changeDir)).toBe(false);
30
+
31
+ fs.rmSync(repoRoot, { recursive: true, force: true });
32
+ });
33
+
34
+ test('throws when change directory does not exist', () => {
35
+ const repoRoot = makeTempRepo();
36
+
37
+ expect(() => archiveChange(repoRoot, 'REQ-999-nonexistent'))
38
+ .toThrow(/not found/);
39
+
40
+ fs.rmSync(repoRoot, { recursive: true, force: true });
41
+ });
42
+
43
+ test('throws when archive target already exists', () => {
44
+ const repoRoot = makeTempRepo();
45
+ const changeDir = path.join(repoRoot, 'devflow', 'changes', 'REQ-001-dup');
46
+ fs.mkdirSync(changeDir, { recursive: true });
47
+
48
+ const month = new Date().toISOString().slice(0, 7);
49
+ const existingArchive = path.join(getArchiveRoot(repoRoot), month, 'REQ-001-dup');
50
+ fs.mkdirSync(existingArchive, { recursive: true });
51
+
52
+ expect(() => archiveChange(repoRoot, 'REQ-001-dup'))
53
+ .toThrow(/already exists/);
54
+
55
+ fs.rmSync(repoRoot, { recursive: true, force: true });
56
+ });
57
+ });
58
+
59
+ describe('restoreChange', () => {
60
+ test('moves archived directory back to changes/', () => {
61
+ const repoRoot = makeTempRepo();
62
+ const archivedDir = path.join(repoRoot, 'devflow', 'changes', 'archive', '2026-04', 'REQ-002-restored');
63
+ fs.mkdirSync(archivedDir, { recursive: true });
64
+ fs.writeFileSync(path.join(archivedDir, 'tasks.md'), '- [ ] task');
65
+
66
+ const result = restoreChange(repoRoot, archivedDir);
67
+
68
+ expect(result.changeKey).toBe('REQ-002-restored');
69
+ expect(fs.existsSync(result.restored)).toBe(true);
70
+ expect(fs.existsSync(path.join(result.restored, 'tasks.md'))).toBe(true);
71
+ expect(fs.existsSync(archivedDir)).toBe(false);
72
+
73
+ fs.rmSync(repoRoot, { recursive: true, force: true });
74
+ });
75
+
76
+ test('throws when archived directory does not exist', () => {
77
+ const repoRoot = makeTempRepo();
78
+
79
+ expect(() => restoreChange(repoRoot, '/tmp/nonexistent-archive-path'))
80
+ .toThrow(/not found/);
81
+
82
+ fs.rmSync(repoRoot, { recursive: true, force: true });
83
+ });
84
+
85
+ test('throws when change already exists in active directory', () => {
86
+ const repoRoot = makeTempRepo();
87
+ const activeDir = path.join(repoRoot, 'devflow', 'changes', 'REQ-003-conflict');
88
+ fs.mkdirSync(activeDir, { recursive: true });
89
+
90
+ const archivedDir = path.join(repoRoot, 'devflow', 'changes', 'archive', '2026-03', 'REQ-003-conflict');
91
+ fs.mkdirSync(archivedDir, { recursive: true });
92
+
93
+ expect(() => restoreChange(repoRoot, archivedDir))
94
+ .toThrow(/already exists/);
95
+
96
+ fs.rmSync(repoRoot, { recursive: true, force: true });
97
+ });
98
+ });
99
+
100
+ describe('listArchived', () => {
101
+ test('returns empty array when no archive exists', () => {
102
+ const repoRoot = makeTempRepo();
103
+ expect(listArchived(repoRoot)).toEqual([]);
104
+ fs.rmSync(repoRoot, { recursive: true, force: true });
105
+ });
106
+
107
+ test('lists archived changes across months', () => {
108
+ const repoRoot = makeTempRepo();
109
+ const archiveRoot = getArchiveRoot(repoRoot);
110
+
111
+ fs.mkdirSync(path.join(archiveRoot, '2026-03', 'REQ-001-old'), { recursive: true });
112
+ fs.mkdirSync(path.join(archiveRoot, '2026-04', 'FIX-002-bugfix'), { recursive: true });
113
+ fs.mkdirSync(path.join(archiveRoot, '2026-04', 'REQ-003-another'), { recursive: true });
114
+
115
+ const items = listArchived(repoRoot);
116
+
117
+ expect(items).toHaveLength(3);
118
+ expect(items[0]).toEqual(expect.objectContaining({ month: '2026-03', changeKey: 'REQ-001-old' }));
119
+ expect(items[1]).toEqual(expect.objectContaining({ month: '2026-04', changeKey: 'FIX-002-bugfix' }));
120
+ expect(items[2]).toEqual(expect.objectContaining({ month: '2026-04', changeKey: 'REQ-003-another' }));
121
+
122
+ fs.rmSync(repoRoot, { recursive: true, force: true });
123
+ });
124
+ });
@@ -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({
@@ -199,6 +199,7 @@ describe('cc-devflow cli distribution bootstrap', () => {
199
199
  expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-plan', 'SKILL.md'))).toBe(true);
200
200
  expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-investigate', 'SKILL.md'))).toBe(true);
201
201
  expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-do', 'SKILL.md'))).toBe(true);
202
+ expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-review', 'SKILL.md'))).toBe(true);
202
203
  expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-check', 'SKILL.md'))).toBe(true);
203
204
  expect(fs.existsSync(path.join(repoRoot, '.codex', 'skills', 'cc-act', 'SKILL.md'))).toBe(true);
204
205
 
@@ -5,7 +5,8 @@ const path = require('path');
5
5
  const {
6
6
  buildChangeKey,
7
7
  getChangePaths,
8
- getChangeSlug
8
+ getChangeSlug,
9
+ nextChangeKey
9
10
  } = require('../paths');
10
11
 
11
12
  describe('change directory naming', () => {
@@ -39,4 +40,108 @@ describe('change directory naming', () => {
39
40
 
40
41
  fs.rmSync(repoRoot, { recursive: true, force: true });
41
42
  });
43
+
44
+ test('allows duplicate numbers when descriptions differ', () => {
45
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
46
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-existing-plan'), { recursive: true });
47
+
48
+ const paths = getChangePaths(repoRoot, 'REQ-123', { goal: 'Another plan' });
49
+
50
+ expect(paths.changeKey).toBe('REQ-123-another-plan');
51
+ expect(paths.changeDir).toBe(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-another-plan'));
52
+
53
+ fs.rmSync(repoRoot, { recursive: true, force: true });
54
+ });
55
+
56
+ test('requires explicit change keys when duplicate numbers already exist', () => {
57
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
58
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-first-plan'), { recursive: true });
59
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-second-plan'), { recursive: true });
60
+
61
+ expect(() => getChangePaths(repoRoot, 'REQ-123'))
62
+ .toThrow(/Ambiguous changeId "REQ-123"/);
63
+ expect(getChangePaths(repoRoot, 'REQ-123', { changeKey: 'REQ-123-second-plan' }).changeKey)
64
+ .toBe('REQ-123-second-plan');
65
+
66
+ fs.rmSync(repoRoot, { recursive: true, force: true });
67
+ });
68
+ });
69
+
70
+ describe('nextChangeKey', () => {
71
+ test('starts at 001 when no existing directories', () => {
72
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
73
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes'), { recursive: true });
74
+
75
+ const result = nextChangeKey(repoRoot, 'REQ', 'my feature');
76
+ expect(result.changeId).toBe('REQ-001');
77
+ expect(result.changeKey).toBe('REQ-001-my-feature');
78
+
79
+ fs.rmSync(repoRoot, { recursive: true, force: true });
80
+ });
81
+
82
+ test('increments from existing max', () => {
83
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
84
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-001-first'), { recursive: true });
85
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-002-second'), { recursive: true });
86
+
87
+ const result = nextChangeKey(repoRoot, 'REQ', 'third feature');
88
+ expect(result.changeId).toBe('REQ-003');
89
+ expect(result.changeKey).toBe('REQ-003-third-feature');
90
+
91
+ fs.rmSync(repoRoot, { recursive: true, force: true });
92
+ });
93
+
94
+ test('REQ and FIX have independent numbering', () => {
95
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
96
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-005-feature'), { recursive: true });
97
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'FIX-002-bugfix'), { recursive: true });
98
+
99
+ const reqResult = nextChangeKey(repoRoot, 'REQ', 'next req');
100
+ expect(reqResult.changeId).toBe('REQ-006');
101
+
102
+ const fixResult = nextChangeKey(repoRoot, 'FIX', 'next fix');
103
+ expect(fixResult.changeId).toBe('FIX-003');
104
+
105
+ fs.rmSync(repoRoot, { recursive: true, force: true });
106
+ });
107
+
108
+ test('preserves existing padding width', () => {
109
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
110
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-0001-wide'), { recursive: true });
111
+
112
+ const result = nextChangeKey(repoRoot, 'REQ', 'narrow');
113
+ expect(result.changeId).toBe('REQ-0002');
114
+
115
+ fs.rmSync(repoRoot, { recursive: true, force: true });
116
+ });
117
+
118
+ test('rejects invalid prefix', () => {
119
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
120
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes'), { recursive: true });
121
+
122
+ expect(() => nextChangeKey(repoRoot, 'BUG', 'test')).toThrow(/Invalid prefix/);
123
+
124
+ fs.rmSync(repoRoot, { recursive: true, force: true });
125
+ });
126
+
127
+ test('handles missing changes directory', () => {
128
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
129
+
130
+ const result = nextChangeKey(repoRoot, 'FIX', 'first ever');
131
+ expect(result.changeId).toBe('FIX-001');
132
+ expect(result.changeKey).toBe('FIX-001-first-ever');
133
+
134
+ fs.rmSync(repoRoot, { recursive: true, force: true });
135
+ });
136
+
137
+ test('slugifies Chinese descriptions', () => {
138
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-next-'));
139
+ fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes'), { recursive: true });
140
+
141
+ const result = nextChangeKey(repoRoot, 'REQ', '统一配置');
142
+ expect(result.changeId).toBe('REQ-001');
143
+ expect(result.changeKey).toBe('REQ-001-统一配置');
144
+
145
+ fs.rmSync(repoRoot, { recursive: true, force: true });
146
+ });
42
147
  });
@@ -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
 
@@ -0,0 +1,64 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getChangesRoot } = require('./paths.js');
4
+
5
+ function getArchiveRoot(repoRoot) {
6
+ return path.join(getChangesRoot(repoRoot), 'archive');
7
+ }
8
+
9
+ function archiveChange(repoRoot, changeKey) {
10
+ const changesRoot = getChangesRoot(repoRoot);
11
+ const changeDir = path.join(changesRoot, changeKey);
12
+
13
+ if (!fs.existsSync(changeDir) || !fs.statSync(changeDir).isDirectory()) {
14
+ throw new Error(`Change directory not found: ${changeDir}`);
15
+ }
16
+
17
+ const month = new Date().toISOString().slice(0, 7);
18
+ const archiveDir = path.join(getArchiveRoot(repoRoot), month);
19
+ fs.mkdirSync(archiveDir, { recursive: true });
20
+
21
+ const dest = path.join(archiveDir, changeKey);
22
+ if (fs.existsSync(dest)) {
23
+ throw new Error(`Archive target already exists: ${dest}`);
24
+ }
25
+
26
+ fs.renameSync(changeDir, dest);
27
+ return { archived: dest, month };
28
+ }
29
+
30
+ function restoreChange(repoRoot, archivedPath) {
31
+ if (!fs.existsSync(archivedPath) || !fs.statSync(archivedPath).isDirectory()) {
32
+ throw new Error(`Archived directory not found: ${archivedPath}`);
33
+ }
34
+
35
+ const changeKey = path.basename(archivedPath);
36
+ const changesRoot = getChangesRoot(repoRoot);
37
+ const dest = path.join(changesRoot, changeKey);
38
+
39
+ if (fs.existsSync(dest)) {
40
+ throw new Error(`Change already exists in active directory: ${dest}`);
41
+ }
42
+
43
+ fs.mkdirSync(changesRoot, { recursive: true });
44
+ fs.renameSync(archivedPath, dest);
45
+ return { restored: dest, changeKey };
46
+ }
47
+
48
+ function listArchived(repoRoot) {
49
+ const archiveRoot = getArchiveRoot(repoRoot);
50
+ if (!fs.existsSync(archiveRoot)) return [];
51
+
52
+ const results = [];
53
+ for (const month of fs.readdirSync(archiveRoot, { withFileTypes: true })) {
54
+ if (!month.isDirectory()) continue;
55
+ const monthDir = path.join(archiveRoot, month.name);
56
+ for (const entry of fs.readdirSync(monthDir, { withFileTypes: true })) {
57
+ if (!entry.isDirectory()) continue;
58
+ results.push({ month: month.name, changeKey: entry.name, path: path.join(monthDir, entry.name) });
59
+ }
60
+ }
61
+ return results;
62
+ }
63
+
64
+ module.exports = { archiveChange, restoreChange, listArchived, getArchiveRoot };
@@ -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);