cc-devflow 4.5.6 → 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 (65) 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 +22 -0
  7. package/.claude/skills/cc-plan/PLAYBOOK.md +20 -17
  8. package/.claude/skills/cc-plan/SKILL.md +91 -19
  9. package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +42 -0
  10. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +2 -0
  11. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +36 -2
  12. package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +30 -0
  13. package/.claude/skills/cc-plan/references/planning-contract.md +20 -15
  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 +9 -0
  31. package/README.md +9 -4
  32. package/README.zh-CN.md +9 -4
  33. package/bin/cc-devflow-cli.js +119 -0
  34. package/config/distributable-skills.json +2 -0
  35. package/docs/examples/example-bindings.json +5 -4
  36. package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
  37. package/docs/examples/full-design-blocked/README.md +1 -1
  38. package/docs/examples/full-design-blocked/ROADMAP.md +16 -1
  39. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +36 -3
  40. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +295 -71
  41. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +2 -1
  42. package/docs/examples/full-design-blocked/roadmap.json +18 -2
  43. package/docs/examples/local-handoff/BACKLOG.md +1 -1
  44. package/docs/examples/local-handoff/README.md +1 -1
  45. package/docs/examples/local-handoff/ROADMAP.md +16 -1
  46. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +27 -1
  47. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +170 -41
  48. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +2 -1
  49. package/docs/examples/local-handoff/roadmap.json +16 -2
  50. package/docs/examples/pdca-loop/BACKLOG.md +1 -1
  51. package/docs/examples/pdca-loop/README.md +1 -1
  52. package/docs/examples/pdca-loop/ROADMAP.md +16 -1
  53. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +27 -1
  54. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +62 -10
  55. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +2 -1
  56. package/docs/examples/pdca-loop/roadmap.json +16 -2
  57. package/docs/examples/scripts/check-example-bindings.sh +2 -0
  58. package/docs/guides/getting-started.md +12 -9
  59. package/docs/guides/getting-started.zh-CN.md +12 -9
  60. package/lib/skill-runtime/__tests__/archive-change.test.js +124 -0
  61. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +1 -0
  62. package/lib/skill-runtime/__tests__/paths.test.js +81 -1
  63. package/lib/skill-runtime/archive-change.js +64 -0
  64. package/lib/skill-runtime/paths.js +32 -0
  65. 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
 
@@ -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,6 +89,7 @@ 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
 
@@ -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
+ });
@@ -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', () => {
@@ -65,3 +66,82 @@ describe('change directory naming', () => {
65
66
  fs.rmSync(repoRoot, { recursive: true, force: true });
66
67
  });
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
+ });
147
+ });
@@ -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 };
@@ -161,6 +161,37 @@ function resolveChangeKey(repoRoot, changeId, options = {}) {
161
161
  return buildChangeKey(changeId, options);
162
162
  }
163
163
 
164
+ function nextChangeKey(repoRoot, prefix, description) {
165
+ const upper = String(prefix || '').toUpperCase();
166
+ if (upper !== 'REQ' && upper !== 'FIX') {
167
+ throw new Error(`Invalid prefix "${prefix}". Use REQ or FIX.`);
168
+ }
169
+
170
+ const numberPattern = new RegExp(`^${upper}-(\\d+)`);
171
+ let maxNum = 0;
172
+ let padWidth = 3;
173
+
174
+ for (const name of listChangeKeys(repoRoot)) {
175
+ const match = name.match(numberPattern);
176
+ if (match) {
177
+ const numStr = match[1];
178
+ const num = parseInt(numStr, 10);
179
+ if (num > maxNum) maxNum = num;
180
+ if (numStr.length > padWidth) padWidth = numStr.length;
181
+ }
182
+ }
183
+
184
+ const paddedNum = String(maxNum + 1).padStart(padWidth, '0');
185
+ const changeId = `${upper}-${paddedNum}`;
186
+ const slug = slugifySegment(
187
+ stripChangeIdPrefix(changeId, description) || description,
188
+ 'change'
189
+ );
190
+ const changeKey = `${changeId}-${slug}`;
191
+
192
+ return { changeId, changeKey };
193
+ }
194
+
164
195
  function getChangePaths(repoRoot, changeId, options = {}) {
165
196
  const changeKey = resolveChangeKey(repoRoot, changeId, options);
166
197
  const changeDir = path.join(getChangesRoot(repoRoot), changeKey);
@@ -226,6 +257,7 @@ module.exports = {
226
257
  getDevflowRoot,
227
258
  getChangesRoot,
228
259
  getWorkspacesRoot,
260
+ nextChangeKey,
229
261
  resolveChangeKey,
230
262
  buildChangeKey,
231
263
  getChangePaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-devflow",
3
- "version": "4.5.6",
3
+ "version": "4.5.7",
4
4
  "description": "Multi-platform CLI and skill pack for agent coding",
5
5
  "main": "bin/cc-devflow.js",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  ".claude/skills/cc-plan/",
15
15
  ".claude/skills/cc-investigate/",
16
16
  ".claude/skills/cc-do/",
17
+ ".claude/skills/cc-review/",
17
18
  ".claude/skills/cc-check/",
18
19
  ".claude/skills/cc-act/",
19
20
  ".claude/skills/cc-spec-init/",