cc-devflow 4.5.9 → 4.5.10
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/SKILL.md +12 -10
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +1 -1
- package/.claude/skills/cc-act/references/closure-contract.md +1 -1
- package/.claude/skills/cc-act/references/git-commit-guidelines.md +1 -1
- package/.claude/skills/cc-check/CHANGELOG.md +17 -0
- package/.claude/skills/cc-check/PLAYBOOK.md +1 -0
- package/.claude/skills/cc-check/SKILL.md +9 -5
- package/.claude/skills/cc-check/references/review-contract.md +7 -0
- package/.claude/skills/cc-check/scripts/render-report-card.js +6 -1
- package/.claude/skills/cc-dev/CHANGELOG.md +5 -0
- package/.claude/skills/cc-dev/SKILL.md +26 -1
- package/.claude/skills/cc-do/CHANGELOG.md +12 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +7 -7
- package/.claude/skills/cc-do/SKILL.md +35 -37
- package/.claude/skills/cc-do/references/execution-recovery.md +18 -13
- package/.claude/skills/cc-do/scripts/build-task-context.sh +4 -17
- package/.claude/skills/cc-do/scripts/record-review-decision.sh +4 -5
- package/.claude/skills/cc-do/scripts/recover-workflow.sh +9 -11
- package/.claude/skills/cc-do/scripts/verify-task-gates.sh +12 -10
- package/.claude/skills/cc-do/scripts/write-task-checkpoint.sh +7 -29
- package/.claude/skills/cc-investigate/CHANGELOG.md +17 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +6 -5
- package/.claude/skills/cc-investigate/SKILL.md +56 -44
- package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +48 -5
- package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +4 -3
- package/.claude/skills/cc-investigate/assets/{ANALYSIS_TEMPLATE.md → legacy/ANALYSIS_TEMPLATE.md} +1 -0
- package/.claude/skills/cc-investigate/references/investigation-contract.md +2 -2
- package/.claude/skills/cc-investigate/scripts/bootstrap-analysis.sh +1 -1
- package/.claude/skills/cc-plan/CHANGELOG.md +19 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +55 -53
- package/.claude/skills/cc-plan/SKILL.md +101 -85
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +47 -14
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +4 -2
- package/.claude/skills/cc-plan/assets/{DESIGN_TEMPLATE.md → legacy/DESIGN_TEMPLATE.md} +1 -0
- package/.claude/skills/cc-plan/assets/{TINY_DESIGN_TEMPLATE.md → legacy/TINY_DESIGN_TEMPLATE.md} +1 -1
- package/.claude/skills/cc-plan/references/planning-contract.md +11 -10
- package/.claude/skills/cc-review/CHANGELOG.md +6 -0
- package/.claude/skills/cc-review/PLAYBOOK.md +9 -11
- package/.claude/skills/cc-review/SKILL.md +37 -61
- package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +1 -1
- package/.claude/skills/cc-review/references/implementation-review-branch.md +5 -5
- package/.claude/skills/cc-review/references/plan-review-branch.md +1 -1
- package/.claude/skills/cc-review/references/review-methods.md +4 -4
- package/.claude/skills/cc-review/scripts/collect-review-context.sh +14 -7
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +40 -4
- package/CONTRIBUTING.zh-CN.md +40 -4
- package/README.md +20 -8
- package/README.zh-CN.md +20 -8
- package/bin/cc-devflow-cli.js +293 -36
- package/docs/examples/START-HERE.md +5 -4
- package/docs/examples/example-bindings.json +8 -8
- package/docs/examples/full-design-blocked/README.md +2 -2
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +2 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +3 -2
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +11 -8
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/review/report-card.json +4 -4
- package/docs/examples/local-handoff/README.md +2 -2
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +2 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +3 -2
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +9 -6
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/review/report-card.json +1 -1
- package/docs/examples/pdca-loop/README.md +2 -2
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +2 -2
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +2 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +2 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +9 -6
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/review/report-card.json +1 -1
- package/docs/examples/scripts/check-example-bindings.sh +2 -0
- package/docs/get-shit-done-strategy-audit.md +22 -22
- package/docs/guides/artifact-contract.md +1 -1
- package/docs/guides/getting-started.md +10 -8
- package/docs/guides/getting-started.zh-CN.md +10 -8
- package/docs/guides/minimize-artifacts.md +123 -0
- package/lib/compiler/__tests__/skills-registry.test.js +2 -2
- package/lib/skill-runtime/CLAUDE.md +1 -1
- package/lib/skill-runtime/__tests__/autopilot.test.js +42 -6
- package/lib/skill-runtime/__tests__/benchmark-artifacts.test.js +165 -0
- package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +2 -2
- package/lib/skill-runtime/__tests__/dispatch.test.js +8 -38
- package/lib/skill-runtime/__tests__/intent.test.js +4 -20
- package/lib/skill-runtime/__tests__/lifecycle.test.js +1 -1
- package/lib/skill-runtime/__tests__/paths.test.js +7 -1
- package/lib/skill-runtime/__tests__/planner.tdd.test.js +61 -0
- package/lib/skill-runtime/__tests__/prepare-pr.test.js +3 -16
- package/lib/skill-runtime/__tests__/query.test.js +388 -7
- package/lib/skill-runtime/__tests__/review-check-integration.test.js +148 -0
- package/lib/skill-runtime/__tests__/review-records.test.js +619 -0
- package/lib/skill-runtime/__tests__/runtime.integration.test.js +64 -23
- package/lib/skill-runtime/__tests__/schemas.test.js +43 -0
- package/lib/skill-runtime/__tests__/task-contract-migrate.test.js +137 -0
- package/lib/skill-runtime/__tests__/task-contract.test.js +783 -0
- package/lib/skill-runtime/__tests__/verify-artifacts.test.js +203 -0
- package/lib/skill-runtime/__tests__/worker-run.test.js +4 -11
- package/lib/skill-runtime/__tests__/workflow-context-legacy-fallback.test.js +31 -0
- package/lib/skill-runtime/__tests__/workflow-context.test.js +98 -0
- package/lib/skill-runtime/artifacts.js +0 -5
- package/lib/skill-runtime/context-index.js +545 -0
- package/lib/skill-runtime/intent.js +9 -33
- package/lib/skill-runtime/lifecycle.js +1 -1
- package/lib/skill-runtime/operations/CLAUDE.md +2 -2
- package/lib/skill-runtime/operations/dispatch.js +4 -42
- package/lib/skill-runtime/operations/init.js +2 -6
- package/lib/skill-runtime/operations/janitor.js +2 -18
- package/lib/skill-runtime/operations/resume.js +21 -38
- package/lib/skill-runtime/operations/review-records.js +265 -0
- package/lib/skill-runtime/operations/snapshot.js +1 -1
- package/lib/skill-runtime/operations/task-contract.js +524 -0
- package/lib/skill-runtime/operations/worker-run.js +2 -30
- package/lib/skill-runtime/paths.js +4 -4
- package/lib/skill-runtime/planner.js +24 -11
- package/lib/skill-runtime/query-registry.js +2 -2
- package/lib/skill-runtime/query.js +15 -2
- package/lib/skill-runtime/review-records.js +123 -0
- package/lib/skill-runtime/review.js +246 -11
- package/lib/skill-runtime/schemas.js +174 -12
- package/lib/skill-runtime/store.js +0 -10
- package/lib/skill-runtime/task-contract.js +187 -0
- package/lib/skill-runtime/workflow-context.js +748 -0
- package/package.json +7 -4
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
职责分组
|
|
5
5
|
入口层: `cli.js` 负责命令分发,`index.js` 提供给测试和内部脚本的稳定聚合入口。
|
|
6
6
|
基础层: `schemas.js`、`store.js`、`paths.js` 管住契约、持久化与路径规则,避免执行层重复造轮子。
|
|
7
|
-
状态层: `artifacts.js`、`lifecycle.js`、`query.js`、`review.js`、`team-state.js` 维护运行时真相源与只读查询。
|
|
7
|
+
状态层: `artifacts.js`、`lifecycle.js`、`query.js`、`workflow-context.js`、`review.js`、`team-state.js` 维护运行时真相源与只读查询。
|
|
8
8
|
规划与交接: `planner.js`、`intent.js`、`delegation.js` 把任务解析、handoff 生成和 team/workspace 委派收口成统一语义。
|
|
9
9
|
阶段操作: `operations/` 是唯一 stage 入口目录;具体阶段边界见 `operations/CLAUDE.md`。
|
|
10
10
|
测试布局: `__tests__/` 紧贴模块放置单元、回归与集成测试;顶层 `test/` 不再承载 `skill-runtime` 私有测试。
|
|
@@ -9,13 +9,13 @@ const {
|
|
|
9
9
|
getTaskManifestPath,
|
|
10
10
|
getReportCardPath,
|
|
11
11
|
getReleaseNotePath,
|
|
12
|
-
getRuntimeStatePath
|
|
13
|
-
getCheckpointPath
|
|
12
|
+
getRuntimeStatePath
|
|
14
13
|
} = require('../store');
|
|
15
14
|
const {
|
|
16
15
|
getIntentResumeIndexPath,
|
|
17
16
|
getIntentPrBriefPath
|
|
18
17
|
} = require('../artifacts');
|
|
18
|
+
const { getChangePaths } = require('../paths');
|
|
19
19
|
|
|
20
20
|
jest.setTimeout(20000);
|
|
21
21
|
|
|
@@ -41,6 +41,41 @@ function markManifestReviewsPassed(repoRoot, changeId) {
|
|
|
41
41
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function writeCleanReviewLedger(repoRoot, changeId) {
|
|
45
|
+
const change = getChangePaths(repoRoot, changeId);
|
|
46
|
+
const ledgerPath = path.join(change.reviewDir, 'review-ledger.jsonl');
|
|
47
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(ledgerPath, [
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
schema: 'review-ledger.v2',
|
|
51
|
+
change: change.changeKey,
|
|
52
|
+
reviewId: 'RVW-20260512-001',
|
|
53
|
+
createdAt: '2026-05-12T00:00:00.000Z',
|
|
54
|
+
createdBy: 'cc-devflow-cli',
|
|
55
|
+
event: 'review-started',
|
|
56
|
+
mode: 'implementation',
|
|
57
|
+
scope: 'current-diff',
|
|
58
|
+
baseSha: 'abc123',
|
|
59
|
+
headSha: 'def456',
|
|
60
|
+
selectedNodes: [],
|
|
61
|
+
skippedNodes: [],
|
|
62
|
+
riskLanes: []
|
|
63
|
+
}),
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
schema: 'review-ledger.v2',
|
|
66
|
+
change: change.changeKey,
|
|
67
|
+
reviewId: 'RVW-20260512-001',
|
|
68
|
+
createdAt: '2026-05-12T00:01:00.000Z',
|
|
69
|
+
createdBy: 'cc-devflow-cli',
|
|
70
|
+
event: 'review-closed',
|
|
71
|
+
status: 'clean',
|
|
72
|
+
blockingCount: 0,
|
|
73
|
+
warningCount: 0,
|
|
74
|
+
next: 'cc-check'
|
|
75
|
+
})
|
|
76
|
+
].join('\n'));
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
describe('runAutopilot', () => {
|
|
45
80
|
test('stops at the approval gate after planning without writing approval-phase handoff markdown', async () => {
|
|
46
81
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-autopilot-'));
|
|
@@ -81,7 +116,7 @@ describe('runAutopilot', () => {
|
|
|
81
116
|
expect(fs.existsSync(getIntentResumeIndexPath(repoRoot, 'REQ-123'))).toBe(false);
|
|
82
117
|
});
|
|
83
118
|
|
|
84
|
-
test('resumes after approval, executes delegated work, and prepares a PR from
|
|
119
|
+
test('resumes after approval, executes delegated work, and prepares a PR from task state', async () => {
|
|
85
120
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-autopilot-workers-'));
|
|
86
121
|
|
|
87
122
|
writeJson(path.join(repoRoot, 'package.json'), {
|
|
@@ -123,17 +158,16 @@ describe('runAutopilot', () => {
|
|
|
123
158
|
});
|
|
124
159
|
|
|
125
160
|
const manifest = JSON.parse(fs.readFileSync(getTaskManifestPath(repoRoot, 'REQ-123'), 'utf8'));
|
|
126
|
-
const delegatedCheckpoint = JSON.parse(fs.readFileSync(getCheckpointPath(repoRoot, 'REQ-123', 'T002'), 'utf8'));
|
|
127
161
|
const report = JSON.parse(fs.readFileSync(getReportCardPath(repoRoot, 'REQ-123'), 'utf8'));
|
|
128
162
|
|
|
129
163
|
expect(firstRun.executed).toEqual(expect.arrayContaining(['delegate', 'worker-run', 'dispatch', 'verify']));
|
|
130
164
|
expect(firstRun.currentStage).toBe('verify');
|
|
131
165
|
expect(manifest.tasks.find((task) => task.id === 'T002').status).toBe('passed');
|
|
132
|
-
expect(delegatedCheckpoint.outputExcerpt).toContain('delegate-ok');
|
|
133
166
|
expect(report.review.status).toBe('blocked');
|
|
134
167
|
expect(fs.existsSync(getIntentPrBriefPath(repoRoot, 'REQ-123'))).toBe(false);
|
|
135
168
|
|
|
136
169
|
markManifestReviewsPassed(repoRoot, 'REQ-123');
|
|
170
|
+
writeCleanReviewLedger(repoRoot, 'REQ-123');
|
|
137
171
|
|
|
138
172
|
const secondRun = await runAutopilot({
|
|
139
173
|
repoRoot,
|
|
@@ -146,7 +180,8 @@ describe('runAutopilot', () => {
|
|
|
146
180
|
|
|
147
181
|
expect(secondRun.executed).toEqual(expect.arrayContaining(['verify', 'prepare-pr']));
|
|
148
182
|
expect(secondRun.currentStage).toBe('prepare-pr');
|
|
149
|
-
expect(prBrief).toContain('
|
|
183
|
+
expect(prBrief).toContain('planning/task-manifest.json');
|
|
184
|
+
expect(prBrief).not.toContain('checkpoint.json');
|
|
150
185
|
});
|
|
151
186
|
|
|
152
187
|
test('runs release after prepare-pr when requested for an approved plan', async () => {
|
|
@@ -195,6 +230,7 @@ describe('runAutopilot', () => {
|
|
|
195
230
|
expect(fs.existsSync(getReleaseNotePath(repoRoot, 'REQ-123'))).toBe(false);
|
|
196
231
|
|
|
197
232
|
markManifestReviewsPassed(repoRoot, 'REQ-123');
|
|
233
|
+
writeCleanReviewLedger(repoRoot, 'REQ-123');
|
|
198
234
|
|
|
199
235
|
const result = await runAutopilot({
|
|
200
236
|
repoRoot,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 scripts/benchmark-artifacts.js 导出的 runBenchmarkArtifacts 和临时 artifact fixture。
|
|
3
|
+
* [OUTPUT]: 验证 benchmark:artifacts 使用 ceil(len/4) 估算并报告 profile 阈值 savings。
|
|
4
|
+
* [POS]: REQ-003-minimize-workflow-artifacts T017 的 Red/Green 证据。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const { runBenchmarkArtifacts } = require('../../../scripts/benchmark-artifacts');
|
|
14
|
+
|
|
15
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
16
|
+
const BENCHMARK_SCRIPT = path.join(REPO_ROOT, 'scripts', 'benchmark-artifacts.js');
|
|
17
|
+
|
|
18
|
+
function writeText(filePath, text) {
|
|
19
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filePath, text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeJson(filePath, value) {
|
|
24
|
+
writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function contractTasks({ changeKey, profile = 'standard', filler = '' }) {
|
|
28
|
+
return [
|
|
29
|
+
'# Tasks',
|
|
30
|
+
'',
|
|
31
|
+
'## Contract Summary',
|
|
32
|
+
'',
|
|
33
|
+
`Change: ${changeKey}`,
|
|
34
|
+
'Mode: plan',
|
|
35
|
+
`Profile: ${profile}`,
|
|
36
|
+
'Approval: approved',
|
|
37
|
+
'',
|
|
38
|
+
'Goal:',
|
|
39
|
+
'- Minimize workflow artifacts.',
|
|
40
|
+
'',
|
|
41
|
+
'Do Not Do:',
|
|
42
|
+
'- Do not change token estimator math.',
|
|
43
|
+
'',
|
|
44
|
+
'Approved Direction:',
|
|
45
|
+
'- Use tasks.md plus generated JSON records.',
|
|
46
|
+
'',
|
|
47
|
+
'Acceptance:',
|
|
48
|
+
'- Benchmark savings stay above threshold.',
|
|
49
|
+
'',
|
|
50
|
+
'Verification:',
|
|
51
|
+
'',
|
|
52
|
+
'```bash',
|
|
53
|
+
'npm run benchmark:artifacts',
|
|
54
|
+
'```',
|
|
55
|
+
'',
|
|
56
|
+
'Risk / Escalate If:',
|
|
57
|
+
'- Savings fall below profile threshold.',
|
|
58
|
+
'',
|
|
59
|
+
filler,
|
|
60
|
+
'## Phase 1',
|
|
61
|
+
'',
|
|
62
|
+
'- [ ] T001 benchmark minimized artifact surface',
|
|
63
|
+
' Vertical slice: Slice 1',
|
|
64
|
+
''
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function seedLegacyBaseline(repoRoot, changeKey, size = 6000) {
|
|
69
|
+
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
70
|
+
writeText(path.join(changeDir, 'planning', 'design.md'), `# Design\n\n${'d'.repeat(size)}\n`);
|
|
71
|
+
writeText(path.join(changeDir, 'planning', 'analysis.md'), `# Analysis\n\n${'a'.repeat(size / 2)}\n`);
|
|
72
|
+
writeText(path.join(changeDir, 'planning', 'tasks.md'), `# Tasks\n\n${'t'.repeat(size / 2)}\n`);
|
|
73
|
+
writeJson(path.join(changeDir, 'planning', 'task-manifest.json'), { changeId: changeKey, tasks: [] });
|
|
74
|
+
writeJson(path.join(changeDir, 'change-meta.json'), { changeId: changeKey, goal: ['legacy'] });
|
|
75
|
+
writeJson(path.join(changeDir, 'review', 'report-card.json'), { overall: 'pass' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function seedMinimizedChange(repoRoot, changeKey, options = {}) {
|
|
79
|
+
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
80
|
+
writeText(path.join(changeDir, 'planning', 'tasks.md'), contractTasks({ changeKey, ...options }));
|
|
81
|
+
writeJson(path.join(changeDir, 'planning', 'task-manifest.json'), {
|
|
82
|
+
changeId: changeKey,
|
|
83
|
+
metadata: { source: 'tasks.md', generatedBy: 'cc-devflow task-contract', planVersion: 1 },
|
|
84
|
+
tasks: []
|
|
85
|
+
});
|
|
86
|
+
writeJson(path.join(changeDir, 'change-meta.json'), {
|
|
87
|
+
changeId: changeKey,
|
|
88
|
+
_meta: { generatedBy: 'cc-devflow task-contract' }
|
|
89
|
+
});
|
|
90
|
+
writeJson(path.join(changeDir, 'review', 'review-ledger.jsonl'), { note: 'counted as text by benchmark' });
|
|
91
|
+
writeJson(path.join(changeDir, 'review', 'review-findings.json'), { findings: [] });
|
|
92
|
+
writeJson(path.join(changeDir, 'review', 'report-card.json'), { overall: 'pass' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('benchmark:artifacts', () => {
|
|
96
|
+
let repoRoot;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-benchmark-artifacts-'));
|
|
100
|
+
seedLegacyBaseline(repoRoot, 'REQ-001-legacy-baseline');
|
|
101
|
+
seedLegacyBaseline(repoRoot, 'REQ-002-legacy-baseline');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('reports standard savings >= 30% for REQ-003-example', () => {
|
|
109
|
+
seedMinimizedChange(repoRoot, 'REQ-003-example', { profile: 'standard' });
|
|
110
|
+
|
|
111
|
+
const result = runBenchmarkArtifacts(repoRoot);
|
|
112
|
+
const row = result.rows.find((item) => item.changeKey === 'REQ-003-example');
|
|
113
|
+
|
|
114
|
+
expect(result.code).toBe(0);
|
|
115
|
+
expect(row).toMatchObject({
|
|
116
|
+
profile: 'standard',
|
|
117
|
+
threshold_pct: 30,
|
|
118
|
+
correctness_pass: true
|
|
119
|
+
});
|
|
120
|
+
expect(row.savings_vs_baseline_pct).toBeGreaterThanOrEqual(30);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('reports tiny savings >= 60% for tiny fixture', () => {
|
|
124
|
+
seedMinimizedChange(repoRoot, 'REQ-004-tiny-example', { profile: 'tiny' });
|
|
125
|
+
|
|
126
|
+
const result = runBenchmarkArtifacts(repoRoot);
|
|
127
|
+
const row = result.rows.find((item) => item.changeKey === 'REQ-004-tiny-example');
|
|
128
|
+
|
|
129
|
+
expect(result.code).toBe(0);
|
|
130
|
+
expect(row).toMatchObject({
|
|
131
|
+
profile: 'tiny',
|
|
132
|
+
threshold_pct: 60,
|
|
133
|
+
correctness_pass: true
|
|
134
|
+
});
|
|
135
|
+
expect(row.savings_vs_baseline_pct).toBeGreaterThanOrEqual(60);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('exits 1 when savings are below the profile threshold', () => {
|
|
139
|
+
seedMinimizedChange(repoRoot, 'REQ-005-bloated-example', {
|
|
140
|
+
profile: 'standard',
|
|
141
|
+
filler: 'x'.repeat(20000)
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = runBenchmarkArtifacts(repoRoot);
|
|
145
|
+
|
|
146
|
+
expect(result.code).toBe(1);
|
|
147
|
+
expect(result.rows[0]).toMatchObject({ correctness_pass: false });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('CLI prints stdout JSON array', () => {
|
|
151
|
+
seedMinimizedChange(repoRoot, 'REQ-003-example', { profile: 'standard' });
|
|
152
|
+
|
|
153
|
+
const result = spawnSync(process.execPath, [BENCHMARK_SCRIPT, repoRoot], { encoding: 'utf8' });
|
|
154
|
+
const rows = JSON.parse(result.stdout);
|
|
155
|
+
|
|
156
|
+
expect(result.status).toBe(0);
|
|
157
|
+
expect(Array.isArray(rows)).toBe(true);
|
|
158
|
+
expect(rows[0]).toHaveProperty('savings_vs_baseline_pct');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('package.json exposes npm run benchmark:artifacts', () => {
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
163
|
+
expect(pkg.scripts['benchmark:artifacts']).toBe('node scripts/benchmark-artifacts.js');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -217,9 +217,9 @@ describe('cc-devflow cli distribution bootstrap', () => {
|
|
|
217
217
|
expect(codexDoSkill.data.writes).toEqual(
|
|
218
218
|
expect.arrayContaining([
|
|
219
219
|
expect.objectContaining({
|
|
220
|
-
path: 'devflow/changes/<change-key>/execution/tasks/<task-id>/
|
|
220
|
+
path: 'devflow/changes/<change-key>/execution/tasks/<task-id>/events.jsonl',
|
|
221
221
|
durability: 'durable',
|
|
222
|
-
required:
|
|
222
|
+
required: false
|
|
223
223
|
})
|
|
224
224
|
])
|
|
225
225
|
);
|
|
@@ -7,7 +7,6 @@ const { runResume } = require('../operations/resume');
|
|
|
7
7
|
const {
|
|
8
8
|
getRuntimeStatePath,
|
|
9
9
|
getTaskManifestPath,
|
|
10
|
-
getCheckpointPath,
|
|
11
10
|
getEventsPath
|
|
12
11
|
} = require('../store');
|
|
13
12
|
|
|
@@ -76,7 +75,7 @@ describe('runDispatch', () => {
|
|
|
76
75
|
expect(nextManifest.tasks[0].status).toBe('pending');
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
test('rejects stale results when planVersion changes during task execution and records it in
|
|
78
|
+
test('rejects stale results when planVersion changes during task execution and records it in manifest and events', async () => {
|
|
80
79
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-dispatch-'));
|
|
81
80
|
const manifestPath = getTaskManifestPath(repoRoot, 'REQ-123');
|
|
82
81
|
|
|
@@ -133,28 +132,25 @@ describe('runDispatch', () => {
|
|
|
133
132
|
});
|
|
134
133
|
|
|
135
134
|
const nextManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
136
|
-
const checkpoint = JSON.parse(fs.readFileSync(getCheckpointPath(repoRoot, 'REQ-123', 'T001'), 'utf8'));
|
|
137
135
|
const events = fs.readFileSync(getEventsPath(repoRoot, 'REQ-123', 'T001'), 'utf8');
|
|
138
136
|
|
|
139
137
|
expect(result.success).toBe(false);
|
|
140
138
|
expect(nextManifest.tasks[0].status).toBe('failed');
|
|
141
139
|
expect(nextManifest.tasks[0].lastError).toContain('Stale result rejected');
|
|
142
|
-
expect(checkpoint.planVersion).toBe(1);
|
|
143
|
-
expect(checkpoint.error).toContain('Stale result rejected');
|
|
144
140
|
expect(events).toContain('task_stale_rejected');
|
|
145
141
|
});
|
|
146
142
|
|
|
147
|
-
test('restores unresolved work from the latest stable
|
|
143
|
+
test('restores unresolved work from the latest stable manifest state on resume without creating handoff markdown', async () => {
|
|
148
144
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-resume-stable-'));
|
|
149
145
|
const changeId = 'REQ-123';
|
|
150
146
|
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
151
147
|
|
|
152
148
|
writeJson(getRuntimeStatePath(repoRoot, changeId), {
|
|
153
149
|
changeId,
|
|
154
|
-
changeKey: 'REQ-123-recover-from-stable-
|
|
155
|
-
slug: 'recover-from-stable-
|
|
150
|
+
changeKey: 'REQ-123-recover-from-stable-state',
|
|
151
|
+
slug: 'recover-from-stable-state',
|
|
156
152
|
createdAt: '2026-04-09T01:00:00.000Z',
|
|
157
|
-
goal: 'Recover from stable
|
|
153
|
+
goal: 'Recover from stable state',
|
|
158
154
|
status: 'in_progress',
|
|
159
155
|
initializedAt: '2026-04-09T01:00:00.000Z',
|
|
160
156
|
plannedAt: '2026-04-09T01:01:00.000Z',
|
|
@@ -169,13 +165,13 @@ describe('runDispatch', () => {
|
|
|
169
165
|
|
|
170
166
|
writeJson(manifestPath, {
|
|
171
167
|
changeId,
|
|
172
|
-
goal: 'Recover from stable
|
|
168
|
+
goal: 'Recover from stable state',
|
|
173
169
|
createdAt: '2026-04-09T01:00:00.000Z',
|
|
174
170
|
updatedAt: '2026-04-09T01:02:00.000Z',
|
|
175
171
|
tasks: [
|
|
176
172
|
{
|
|
177
173
|
id: 'T001',
|
|
178
|
-
title: 'Stable
|
|
174
|
+
title: 'Stable completed task',
|
|
179
175
|
type: 'TEST',
|
|
180
176
|
dependsOn: [],
|
|
181
177
|
touches: ['src/a.ts'],
|
|
@@ -219,32 +215,6 @@ describe('runDispatch', () => {
|
|
|
219
215
|
}
|
|
220
216
|
});
|
|
221
217
|
|
|
222
|
-
writeJson(getCheckpointPath(repoRoot, changeId, 'T001'), {
|
|
223
|
-
changeId,
|
|
224
|
-
taskId: 'T001',
|
|
225
|
-
sessionId: 'stable-session',
|
|
226
|
-
planVersion: 1,
|
|
227
|
-
status: 'passed',
|
|
228
|
-
summary: 'Task passed after 1 attempt(s)',
|
|
229
|
-
error: '',
|
|
230
|
-
outputExcerpt: '',
|
|
231
|
-
timestamp: '2026-04-09T01:05:00.000Z',
|
|
232
|
-
attempt: 1
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
writeJson(getCheckpointPath(repoRoot, changeId, 'T002'), {
|
|
236
|
-
changeId,
|
|
237
|
-
taskId: 'T002',
|
|
238
|
-
sessionId: 'failed-session',
|
|
239
|
-
planVersion: 1,
|
|
240
|
-
status: 'failed',
|
|
241
|
-
summary: 'Task failed: Command failed',
|
|
242
|
-
error: 'Command failed',
|
|
243
|
-
outputExcerpt: 'Command failed',
|
|
244
|
-
timestamp: '2026-04-09T01:06:00.000Z',
|
|
245
|
-
attempt: 2
|
|
246
|
-
});
|
|
247
|
-
|
|
248
218
|
const result = await runResume({
|
|
249
219
|
repoRoot,
|
|
250
220
|
changeId,
|
|
@@ -255,7 +225,7 @@ describe('runDispatch', () => {
|
|
|
255
225
|
const nextManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
256
226
|
|
|
257
227
|
expect(result.success).toBe(true);
|
|
258
|
-
expect(result.
|
|
228
|
+
expect(result.restoredState).toMatchObject({
|
|
259
229
|
taskId: 'T001',
|
|
260
230
|
status: 'passed'
|
|
261
231
|
});
|
|
@@ -15,8 +15,7 @@ const {
|
|
|
15
15
|
const {
|
|
16
16
|
getRuntimeStatePath,
|
|
17
17
|
getTaskManifestPath,
|
|
18
|
-
getReportCardPath
|
|
19
|
-
getCheckpointPath
|
|
18
|
+
getReportCardPath
|
|
20
19
|
} = require('../store');
|
|
21
20
|
|
|
22
21
|
function writeJson(filePath, value) {
|
|
@@ -86,21 +85,6 @@ describe('intent handoff bridge', () => {
|
|
|
86
85
|
}
|
|
87
86
|
);
|
|
88
87
|
|
|
89
|
-
writeJson(
|
|
90
|
-
getCheckpointPath(repoRoot, 'REQ-123', 'T001'),
|
|
91
|
-
{
|
|
92
|
-
changeId: 'REQ-123',
|
|
93
|
-
taskId: 'T001',
|
|
94
|
-
sessionId: 'T001-session',
|
|
95
|
-
planVersion: 1,
|
|
96
|
-
status: 'passed',
|
|
97
|
-
summary: 'Task passed after 1 attempt',
|
|
98
|
-
error: '',
|
|
99
|
-
outputExcerpt: 'worker-ok',
|
|
100
|
-
timestamp: '2026-03-25T01:11:00.000Z',
|
|
101
|
-
attempt: 1
|
|
102
|
-
}
|
|
103
|
-
);
|
|
104
88
|
});
|
|
105
89
|
|
|
106
90
|
test('syncIntentMemory removes legacy projection files instead of writing them', async () => {
|
|
@@ -152,7 +136,7 @@ describe('intent handoff bridge', () => {
|
|
|
152
136
|
}
|
|
153
137
|
});
|
|
154
138
|
|
|
155
|
-
test('writes pr brief from manifest
|
|
139
|
+
test('writes pr brief from manifest task state and report-card only', async () => {
|
|
156
140
|
const prBriefPath = getIntentPrBriefPath(repoRoot, 'REQ-123');
|
|
157
141
|
const manifestPath = getTaskManifestPath(repoRoot, 'REQ-123');
|
|
158
142
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -208,8 +192,8 @@ describe('intent handoff bridge', () => {
|
|
|
208
192
|
|
|
209
193
|
expect(prBrief).toContain('PR Brief: REQ-123');
|
|
210
194
|
expect(prBrief).toContain('Suggested Title: feat(req-123): Deliver autopilot memory bridge');
|
|
211
|
-
expect(prBrief).toContain('
|
|
212
|
-
expect(prBrief).toContain('
|
|
195
|
+
expect(prBrief).toContain('planning/tasks.md');
|
|
196
|
+
expect(prBrief).toContain('planning/task-manifest.json');
|
|
213
197
|
expect(prBrief).toContain('存在 1 个 skipped 任务,需要确认是否可接受:T002');
|
|
214
198
|
expect(prBrief).not.toContain('result.md');
|
|
215
199
|
for (const filePath of getIntentHandoffArtifactPaths(repoRoot, 'REQ-123')) {
|
|
@@ -104,7 +104,7 @@ describe('lifecycle helpers', () => {
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
expect(deriveLifecycleNextAction({ state: approvedState, manifest, report: null })).toBe(
|
|
107
|
-
'优先修复失败任务 T002
|
|
107
|
+
'优先修复失败任务 T002,然后从 task-manifest 的最近稳定状态恢复。'
|
|
108
108
|
);
|
|
109
109
|
expect(
|
|
110
110
|
deriveLifecycleNextAction({
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
buildChangeKey,
|
|
7
|
+
getChangeKeyPrefix,
|
|
7
8
|
getChangePaths,
|
|
8
9
|
getChangeSlug,
|
|
9
10
|
nextChangeKey
|
|
@@ -17,11 +18,16 @@ describe('change directory naming', () => {
|
|
|
17
18
|
.toBe('FIX-123-修复-目录-命名');
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
test('derives the numeric prefix from full change keys', () => {
|
|
22
|
+
expect(getChangeKeyPrefix('REQ-123-plan-folder-contract')).toBe('REQ-123');
|
|
23
|
+
expect(getChangeKeyPrefix('FIX-456-crash-on-start')).toBe('FIX-456');
|
|
24
|
+
});
|
|
25
|
+
|
|
20
26
|
test('rejects change ids and explicit keys outside the canonical prefix contract', () => {
|
|
21
27
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
|
|
22
28
|
|
|
23
29
|
expect(() => buildChangeKey('BUG-123', { goal: 'legacy bug id' }))
|
|
24
|
-
.toThrow(/REQ-<number
|
|
30
|
+
.toThrow(/REQ-<number>\[-description\] or FIX-<number>\[-description\]/);
|
|
25
31
|
expect(() => getChangePaths(repoRoot, 'REQ-123', { changeKey: 'req-123-lowercase' }))
|
|
26
32
|
.toThrow(/Expected REQ-123-<description>/);
|
|
27
33
|
|
|
@@ -107,6 +107,67 @@ describe('TDD Order Validation', () => {
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
describe('Manifest execution state', () => {
|
|
110
|
+
test('createTaskManifest reads tasks.md when legacy design.md is absent', async () => {
|
|
111
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-planner-new-contract-'));
|
|
112
|
+
const changeId = 'REQ-322';
|
|
113
|
+
const changeKey = 'REQ-322-new-contract';
|
|
114
|
+
const tasksPath = path.join(repoRoot, 'devflow', 'changes', changeKey, 'planning', 'tasks.md');
|
|
115
|
+
fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
tasksPath,
|
|
118
|
+
[
|
|
119
|
+
'## Phase 1: Build',
|
|
120
|
+
'',
|
|
121
|
+
'- [ ] T001 [TEST] Counter behavior `src/counter.test.ts`',
|
|
122
|
+
'- [ ] T002 [IMPL] Counter behavior (dependsOn:T001) `src/counter.ts`'
|
|
123
|
+
].join('\n')
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const manifest = await createTaskManifest({
|
|
127
|
+
repoRoot,
|
|
128
|
+
changeId,
|
|
129
|
+
changeKey,
|
|
130
|
+
goal: 'Exercise new contract readFiles',
|
|
131
|
+
overwrite: true
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(manifest.tasks[0].context.readFiles).toEqual(['tasks.md', 'src/counter.test.ts']);
|
|
135
|
+
expect(manifest.tasks[1].context.readFiles).toEqual(['tasks.md']);
|
|
136
|
+
|
|
137
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('createTaskManifest keeps design.md when legacy design.md exists', async () => {
|
|
141
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-planner-legacy-contract-'));
|
|
142
|
+
const changeId = 'REQ-323';
|
|
143
|
+
const changeKey = 'REQ-323-legacy-contract';
|
|
144
|
+
const planningDir = path.join(repoRoot, 'devflow', 'changes', changeKey, 'planning');
|
|
145
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(path.join(planningDir, 'design.md'), '# DESIGN\n');
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(planningDir, 'tasks.md'),
|
|
149
|
+
[
|
|
150
|
+
'## Phase 1: Build',
|
|
151
|
+
'',
|
|
152
|
+
'- [ ] T001 [TEST] Counter behavior `src/counter.test.ts`',
|
|
153
|
+
'- [ ] T002 [IMPL] Counter behavior (dependsOn:T001) `src/counter.ts`'
|
|
154
|
+
].join('\n')
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const manifest = await createTaskManifest({
|
|
158
|
+
repoRoot,
|
|
159
|
+
changeId,
|
|
160
|
+
changeKey,
|
|
161
|
+
goal: 'Exercise legacy contract readFiles',
|
|
162
|
+
overwrite: true
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(manifest.tasks[0].context.readFiles).toEqual(['design.md', 'tasks.md', 'src/counter.test.ts']);
|
|
166
|
+
expect(manifest.tasks[1].context.readFiles).toEqual(['design.md']);
|
|
167
|
+
|
|
168
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
110
171
|
test('should derive active phase and current task from ready tasks', () => {
|
|
111
172
|
const state = deriveManifestExecutionState([
|
|
112
173
|
{ id: 'T001', phase: 1, status: 'passed', dependsOn: [] },
|
|
@@ -6,8 +6,7 @@ const { runPreparePr } = require('../operations/prepare-pr');
|
|
|
6
6
|
const {
|
|
7
7
|
getRuntimeStatePath,
|
|
8
8
|
getTaskManifestPath,
|
|
9
|
-
getReportCardPath
|
|
10
|
-
getCheckpointPath
|
|
9
|
+
getReportCardPath
|
|
11
10
|
} = require('../store');
|
|
12
11
|
const {
|
|
13
12
|
getIntentPrBriefPath,
|
|
@@ -94,19 +93,6 @@ describe('runPreparePr', () => {
|
|
|
94
93
|
timestamp: '2026-03-25T01:11:00.000Z'
|
|
95
94
|
});
|
|
96
95
|
|
|
97
|
-
writeJson(getCheckpointPath(repoRoot, 'REQ-123', 'T001'), {
|
|
98
|
-
changeId: 'REQ-123',
|
|
99
|
-
taskId: 'T001',
|
|
100
|
-
sessionId: 'task-session',
|
|
101
|
-
planVersion: 1,
|
|
102
|
-
status: 'passed',
|
|
103
|
-
summary: 'Task passed after 1 attempt',
|
|
104
|
-
error: '',
|
|
105
|
-
outputExcerpt: 'ok',
|
|
106
|
-
timestamp: '2026-03-25T01:09:00.000Z',
|
|
107
|
-
attempt: 1
|
|
108
|
-
});
|
|
109
|
-
|
|
110
96
|
writeJson(getRuntimeStatePath(repoRoot, 'REQ-123'), {
|
|
111
97
|
...JSON.parse(fs.readFileSync(getRuntimeStatePath(repoRoot, 'REQ-123'), 'utf8')),
|
|
112
98
|
approval: {
|
|
@@ -130,7 +116,8 @@ describe('runPreparePr', () => {
|
|
|
130
116
|
expect(result.status).toBe('prepared');
|
|
131
117
|
expect(result.suggestedTitle).toBe('feat(req-123): Prepare a PR-ready brief');
|
|
132
118
|
expect(prBrief).toContain('PR Brief: REQ-123');
|
|
133
|
-
expect(prBrief).toContain('
|
|
119
|
+
expect(prBrief).toContain('planning/task-manifest.json');
|
|
120
|
+
expect(prBrief).not.toContain('execution/tasks/T001/checkpoint.json');
|
|
134
121
|
expect(prBrief).not.toContain('result.md');
|
|
135
122
|
for (const filePath of getIntentHandoffArtifactPaths(repoRoot, 'REQ-123')) {
|
|
136
123
|
expect(fs.existsSync(filePath)).toBe(filePath === prBriefPath);
|