cc-devflow 4.5.6 → 4.5.8
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 +6 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +11 -2
- package/.claude/skills/cc-act/SKILL.md +17 -7
- package/.claude/skills/cc-act/references/closure-contract.md +4 -0
- package/.claude/skills/cc-act/scripts/{archive-requirement.sh → archive-change.sh} +7 -7
- package/.claude/skills/cc-act/scripts/detect-ship-target.sh +27 -0
- package/.claude/skills/cc-act/scripts/ensure-ship-branch.sh +93 -0
- package/.claude/skills/cc-act/scripts/generate-status-report.sh +6 -0
- package/.claude/skills/cc-act/scripts/render-pr-brief.sh +6 -0
- package/.claude/skills/cc-act/scripts/sync-act-docs.sh +14 -0
- package/.claude/skills/cc-dev/CHANGELOG.md +5 -0
- package/.claude/skills/cc-dev/PLAYBOOK.md +63 -0
- package/.claude/skills/cc-dev/SKILL.md +168 -0
- package/.claude/skills/cc-do/CHANGELOG.md +6 -0
- package/.claude/skills/cc-do/SKILL.md +23 -1
- package/.claude/skills/cc-investigate/CHANGELOG.md +5 -0
- package/.claude/skills/cc-investigate/SKILL.md +2 -2
- package/.claude/skills/cc-next/CHANGELOG.md +5 -0
- package/.claude/skills/cc-next/PLAYBOOK.md +52 -0
- package/.claude/skills/cc-next/SKILL.md +161 -0
- package/.claude/skills/cc-plan/CHANGELOG.md +28 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +20 -17
- package/.claude/skills/cc-plan/SKILL.md +135 -21
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +42 -0
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +28 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +84 -2
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +30 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +31 -15
- package/.claude/skills/cc-plan/scripts/next-change-key.sh +78 -0
- package/.claude/skills/cc-pr-land/CHANGELOG.md +5 -0
- package/.claude/skills/cc-pr-land/PLAYBOOK.md +45 -0
- package/.claude/skills/cc-pr-land/SKILL.md +157 -0
- package/.claude/skills/cc-pr-review/CHANGELOG.md +5 -0
- package/.claude/skills/cc-pr-review/PLAYBOOK.md +46 -0
- package/.claude/skills/cc-pr-review/SKILL.md +142 -0
- package/.claude/skills/cc-review/CHANGELOG.md +28 -0
- package/.claude/skills/cc-review/PLAYBOOK.md +108 -0
- package/.claude/skills/cc-review/SKILL.md +340 -0
- package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +85 -0
- package/.claude/skills/cc-review/references/implementation-review-branch.md +152 -0
- package/.claude/skills/cc-review/references/plan-review-branch.md +151 -0
- package/.claude/skills/cc-review/references/review-methods.md +221 -0
- package/.claude/skills/cc-review/scripts/collect-review-context.sh +80 -0
- package/.claude/skills/cc-roadmap/CHANGELOG.md +6 -0
- package/.claude/skills/cc-roadmap/SKILL.md +102 -8
- package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +3 -0
- package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +23 -0
- package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +20 -1
- package/.claude/skills/cc-roadmap/references/roadmap-dialogue.md +28 -13
- package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/markdown.js +18 -0
- package/.claude/skills/cc-roadmap/scripts/lib/roadmap-tracking/schema.js +8 -0
- package/.claude/skills/cc-simplify/CHANGELOG.md +6 -0
- package/.claude/skills/cc-simplify/SKILL.md +19 -8
- package/CHANGELOG.md +16 -0
- package/README.md +58 -4
- package/README.zh-CN.md +58 -4
- package/bin/cc-devflow-cli.js +119 -0
- package/config/distributable-skills.json +10 -0
- package/docs/assets/cc-devflow-pr-harness-en.svg +153 -0
- package/docs/assets/cc-devflow-pr-harness-zh.svg +152 -0
- package/docs/assets/wechat-group-qr.jpg +0 -0
- package/docs/examples/example-bindings.json +11 -6
- package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
- package/docs/examples/full-design-blocked/README.md +1 -1
- package/docs/examples/full-design-blocked/ROADMAP.md +16 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +36 -3
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +604 -76
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +46 -1
- package/docs/examples/full-design-blocked/roadmap.json +18 -2
- package/docs/examples/local-handoff/BACKLOG.md +1 -1
- package/docs/examples/local-handoff/README.md +1 -1
- package/docs/examples/local-handoff/ROADMAP.md +16 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +27 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +366 -44
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +36 -1
- package/docs/examples/local-handoff/roadmap.json +16 -2
- package/docs/examples/pdca-loop/BACKLOG.md +1 -1
- package/docs/examples/pdca-loop/README.md +1 -1
- package/docs/examples/pdca-loop/ROADMAP.md +16 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +27 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +259 -14
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +36 -1
- package/docs/examples/pdca-loop/roadmap.json +16 -2
- package/docs/examples/scripts/check-example-bindings.sh +21 -1
- package/docs/guides/getting-started.md +12 -9
- package/docs/guides/getting-started.zh-CN.md +12 -9
- package/lib/skill-runtime/__tests__/archive-change.test.js +124 -0
- package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +1 -0
- package/lib/skill-runtime/__tests__/paths.test.js +81 -1
- package/lib/skill-runtime/archive-change.js +64 -0
- package/lib/skill-runtime/paths.js +32 -0
- package/package.json +7 -1
|
@@ -11,13 +11,13 @@ CC-DevFlow 现在有两条入口:
|
|
|
11
11
|
- `cc-devflow init`:把整包 `.claude` 安装到你的项目里
|
|
12
12
|
- `cc-devflow adapt`:生成 Codex、Cursor、Qwen、Antigravity 等平台产物
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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-
|
|
79
|
-
4. cc-
|
|
80
|
-
5. cc-
|
|
81
|
-
6.
|
|
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-plan.md`、`cc-review-ledger.jsonl`、`cc-review-report.md`、可选 `cc-review-agent-results.jsonl`,以及可选的结构化深度 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
|
|
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.
|
|
3
|
+
"version": "4.5.8",
|
|
4
4
|
"description": "Multi-platform CLI and skill pack for agent coding",
|
|
5
5
|
"main": "bin/cc-devflow.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
"bin/",
|
|
11
11
|
"lib/",
|
|
12
12
|
"config/",
|
|
13
|
+
"docs/assets/",
|
|
13
14
|
".claude/skills/cc-roadmap/",
|
|
15
|
+
".claude/skills/cc-next/",
|
|
16
|
+
".claude/skills/cc-dev/",
|
|
14
17
|
".claude/skills/cc-plan/",
|
|
15
18
|
".claude/skills/cc-investigate/",
|
|
16
19
|
".claude/skills/cc-do/",
|
|
20
|
+
".claude/skills/cc-review/",
|
|
21
|
+
".claude/skills/cc-pr-review/",
|
|
22
|
+
".claude/skills/cc-pr-land/",
|
|
17
23
|
".claude/skills/cc-check/",
|
|
18
24
|
".claude/skills/cc-act/",
|
|
19
25
|
".claude/skills/cc-spec-init/",
|