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.
- package/.claude/skills/cc-act/PLAYBOOK.md +2 -2
- package/.claude/skills/cc-act/SKILL.md +2 -2
- package/.claude/skills/cc-act/scripts/{archive-requirement.sh → archive-change.sh} +7 -7
- package/.claude/skills/cc-investigate/CHANGELOG.md +5 -0
- package/.claude/skills/cc-investigate/SKILL.md +2 -2
- package/.claude/skills/cc-plan/CHANGELOG.md +34 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +22 -17
- package/.claude/skills/cc-plan/SKILL.md +135 -12
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +51 -0
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +2 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +66 -3
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +37 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +33 -10
- package/.claude/skills/cc-plan/scripts/next-change-key.sh +78 -0
- package/.claude/skills/cc-review/CHANGELOG.md +7 -0
- package/.claude/skills/cc-review/PLAYBOOK.md +54 -0
- package/.claude/skills/cc-review/SKILL.md +173 -0
- package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +81 -0
- package/.claude/skills/cc-review/references/implementation-review-branch.md +115 -0
- package/.claude/skills/cc-review/references/plan-review-branch.md +116 -0
- package/.claude/skills/cc-review/references/review-methods.md +126 -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/CHANGELOG.md +21 -0
- package/README.md +10 -5
- package/README.zh-CN.md +10 -5
- package/bin/cc-devflow-cli.js +135 -2
- package/config/distributable-skills.json +2 -0
- package/docs/CLAUDE.md +1 -1
- package/docs/examples/example-bindings.json +5 -4
- 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 +42 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +345 -65
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +2 -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 +34 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +197 -39
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +2 -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 +34 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +89 -8
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +2 -1
- package/docs/examples/pdca-loop/roadmap.json +16 -2
- package/docs/examples/scripts/check-example-bindings.sh +2 -0
- package/docs/guides/getting-started.md +13 -10
- package/docs/guides/getting-started.zh-CN.md +13 -10
- package/lib/skill-runtime/__tests__/archive-change.test.js +124 -0
- package/lib/skill-runtime/__tests__/autopilot.test.js +13 -10
- package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +1 -0
- package/lib/skill-runtime/__tests__/paths.test.js +106 -1
- package/lib/skill-runtime/__tests__/query.test.js +49 -0
- package/lib/skill-runtime/archive-change.js +64 -0
- package/lib/skill-runtime/artifacts.js +2 -2
- package/lib/skill-runtime/intent.js +14 -14
- package/lib/skill-runtime/operations/autopilot-shared.js +4 -4
- package/lib/skill-runtime/paths.js +60 -7
- package/lib/skill-runtime/query-registry.js +3 -3
- package/lib/skill-runtime/query.js +30 -30
- 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
|
|
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
|
|
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-
|
|
79
|
-
4. cc-
|
|
80
|
-
5. cc-
|
|
81
|
-
6.
|
|
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`
|
|
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
|
|
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
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
110
|
+
goal
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
await runApprove({
|
|
@@ -160,16 +162,17 @@ describe('runAutopilot', () => {
|
|
|
160
162
|
}
|
|
161
163
|
});
|
|
162
164
|
|
|
163
|
-
|
|
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
|
|
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);
|