cc-devflow 4.5.5 → 4.5.6
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-plan/CHANGELOG.md +12 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +18 -16
- package/.claude/skills/cc-plan/SKILL.md +53 -2
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +9 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +31 -2
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +7 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +20 -2
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/cc-devflow-cli.js +16 -2
- package/docs/CLAUDE.md +1 -1
- package/docs/examples/example-bindings.json +1 -1
- package/docs/examples/full-design-blocked/README.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +9 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +58 -2
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +1 -1
- package/docs/examples/local-handoff/README.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +8 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +31 -2
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +1 -1
- package/docs/examples/pdca-loop/README.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +8 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +31 -2
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +1 -1
- package/docs/guides/getting-started.md +1 -1
- package/docs/guides/getting-started.zh-CN.md +1 -1
- package/lib/skill-runtime/__tests__/autopilot.test.js +13 -10
- package/lib/skill-runtime/__tests__/paths.test.js +25 -0
- package/lib/skill-runtime/__tests__/query.test.js +49 -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 +28 -7
- package/lib/skill-runtime/query-registry.js +3 -3
- package/lib/skill-runtime/query.js +30 -30
- package/package.json +1 -1
package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"requirementId": "REQ-003",
|
|
7
7
|
"requirementVersion": "REQ-003.v1",
|
|
8
8
|
"planningMeta": {
|
|
9
|
-
"reqPlanSkillVersion": "3.7.
|
|
9
|
+
"reqPlanSkillVersion": "3.7.7",
|
|
10
10
|
"designVersion": "design.v1",
|
|
11
11
|
"approvedAt": "2026-04-16T13:10:00.000Z",
|
|
12
12
|
"approvedBy": "user",
|
|
@@ -29,7 +29,36 @@
|
|
|
29
29
|
"testingDecisions": ["Test through the admin panel action and visible row data"],
|
|
30
30
|
"outOfScope": ["JSON export", "scheduled reporting", "shared reporting backend"],
|
|
31
31
|
"furtherNotes": ["Richer machine-readable exports should become a separate requirement"]
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"decisionQuestions": [
|
|
34
|
+
{
|
|
35
|
+
"questionId": "D1",
|
|
36
|
+
"gate": "approach-approval",
|
|
37
|
+
"knownEvidence": ["Existing admin panel already owns the visible summary rows", "No reporting backend is needed for the first useful export"],
|
|
38
|
+
"recommendation": "Approve the tiny-design CSV export",
|
|
39
|
+
"options": [
|
|
40
|
+
{
|
|
41
|
+
"id": "A",
|
|
42
|
+
"label": "Tiny design CSV export",
|
|
43
|
+
"recommended": true,
|
|
44
|
+
"completeness": "8/10",
|
|
45
|
+
"good": "Solves weekly reporting friction with the current panel data",
|
|
46
|
+
"costRisk": "Does not handle JSON, scheduling, or shared reporting contracts"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "B",
|
|
50
|
+
"label": "Shared reporting pipeline",
|
|
51
|
+
"recommended": false,
|
|
52
|
+
"completeness": "10/10",
|
|
53
|
+
"good": "Could support more formats and future scheduled reports",
|
|
54
|
+
"costRisk": "Creates a larger platform surface before the local export is proven"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"userChoice": "A",
|
|
58
|
+
"impact": "cc-do exports only visible rows from the current panel and avoids new reporting contracts",
|
|
59
|
+
"status": "answered"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
33
62
|
},
|
|
34
63
|
"currentTaskId": null,
|
|
35
64
|
"activePhase": null,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- Example version: `1.0.0`
|
|
6
6
|
- Last reviewed: `2026-04-17`
|
|
7
|
-
- Bound skills: `cc-roadmap@5.0.0`, `cc-plan@3.7.
|
|
7
|
+
- Bound skills: `cc-roadmap@5.0.0`, `cc-plan@3.7.7`, `cc-do@1.6.2`, `cc-check@1.10.1`, `cc-act@1.8.2`
|
|
8
8
|
|
|
9
9
|
This folder shows one minimal but complete `cc-roadmap -> cc-plan -> cc-do -> cc-check -> cc-act` loop.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- Requirement version: `REQ-001.v1`
|
|
6
6
|
- Design version: `design.v1`
|
|
7
|
-
- CC-Plan skill version: `3.7.
|
|
7
|
+
- CC-Plan skill version: `3.7.7`
|
|
8
8
|
- Requirement ID: `REQ-001`
|
|
9
9
|
- Design mode: `tiny-design`
|
|
10
10
|
- Why this stays `tiny-design`: the patch is limited to an existing dialog and test file, with no API or data model changes
|
|
@@ -75,8 +75,15 @@
|
|
|
75
75
|
- Ambiguity scan: pass; execution does not need to re-decide button placement or clipboard source
|
|
76
76
|
- Feasibility scan: pass; existing dialog and tests already cover the target surface
|
|
77
77
|
- PRD brief scan: pass; problem, story, implementation decision, testing decision, and out-of-scope are durable
|
|
78
|
+
- Decision question scan: pass; `D1` approved the tiny-design copy-action boundary
|
|
78
79
|
- Final recommendation: approved as `tiny-design`
|
|
79
80
|
|
|
81
|
+
## Decision Questions
|
|
82
|
+
|
|
83
|
+
| ID | Gate | Known evidence | Recommendation | User choice | Impact on `cc-do` | Status |
|
|
84
|
+
|----|------|----------------|----------------|-------------|-------------------|--------|
|
|
85
|
+
| D1 | approach-approval | Existing dialog already renders the invite URL and the change stays inside one UI/test surface | Approve the tiny-design copy action | Tiny design copy action | Keep implementation inside the share dialog; do not add backend or permission work | answered |
|
|
86
|
+
|
|
80
87
|
## Approval
|
|
81
88
|
|
|
82
89
|
- User approval status: approved
|
package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
24
|
"planningMeta": {
|
|
25
|
-
"reqPlanSkillVersion": "3.7.
|
|
25
|
+
"reqPlanSkillVersion": "3.7.7",
|
|
26
26
|
"designVersion": "design.v1",
|
|
27
27
|
"approvedAt": "2026-04-15T10:05:00.000Z",
|
|
28
28
|
"approvedBy": "user",
|
|
@@ -45,7 +45,36 @@
|
|
|
45
45
|
"testingDecisions": ["Test through the share dialog behavior, not an internal helper"],
|
|
46
46
|
"outOfScope": ["invite generation", "role controls", "analytics", "clipboard fallback redesign"],
|
|
47
47
|
"furtherNotes": ["Richer copied-state feedback is a separate UX requirement"]
|
|
48
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"decisionQuestions": [
|
|
50
|
+
{
|
|
51
|
+
"questionId": "D1",
|
|
52
|
+
"gate": "approach-approval",
|
|
53
|
+
"knownEvidence": ["Existing dialog already renders the invite URL", "The patch stays inside one UI/test surface"],
|
|
54
|
+
"recommendation": "Approve the tiny-design copy action",
|
|
55
|
+
"options": [
|
|
56
|
+
{
|
|
57
|
+
"id": "A",
|
|
58
|
+
"label": "Tiny design copy action",
|
|
59
|
+
"recommended": true,
|
|
60
|
+
"completeness": "8/10",
|
|
61
|
+
"good": "Ships the visible user win with one dialog and one behavior test",
|
|
62
|
+
"costRisk": "Leaves richer feedback and clipboard fallback work for later"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "B",
|
|
66
|
+
"label": "Full share-flow redesign",
|
|
67
|
+
"recommended": false,
|
|
68
|
+
"completeness": "10/10",
|
|
69
|
+
"good": "Could solve broader invite UX issues in one larger pass",
|
|
70
|
+
"costRisk": "Expands beyond the roadmap wedge and touches unrelated share contracts"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"userChoice": "A",
|
|
74
|
+
"impact": "cc-do keeps implementation inside the share dialog and avoids backend or permission work",
|
|
75
|
+
"status": "answered"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
49
78
|
},
|
|
50
79
|
"currentTaskId": null,
|
|
51
80
|
"activePhase": null,
|
|
@@ -94,7 +94,7 @@ Capability truth lives in `devflow/specs/`.
|
|
|
94
94
|
Change truth lives in `devflow/changes/<change>/`.
|
|
95
95
|
|
|
96
96
|
- 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`
|
|
97
|
+
- 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
98
|
- 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
99
|
- Worker prompts, journals, assignments, and session logs belong under `devflow/workspaces/<change>/` as ephemeral scratch.
|
|
100
100
|
|
|
@@ -93,7 +93,7 @@ find .codex/skills -mindepth 2 -maxdepth 2 -name SKILL.md | sort
|
|
|
93
93
|
durable truth 分两层:
|
|
94
94
|
|
|
95
95
|
- `devflow/specs/`:capability 真相,保留 `INDEX.md` 与 `capabilities/*.md`
|
|
96
|
-
- 新 change 目录必须命名为 `REQ-<number>-<description>`(需求)或 `FIX-<number>-<description>`(修复);`REQ` 和 `FIX`
|
|
96
|
+
- 新 change 目录必须命名为 `REQ-<number>-<description>`(需求)或 `FIX-<number>-<description>`(修复);`REQ` 和 `FIX` 分别维护自己的递增编号,跨前缀同号不是冲突;并行工作树造成重复编号时,完整 change key 的描述负责区分业务内容,旧小写目录只作为历史兼容读取。
|
|
97
97
|
- `devflow/changes/<change>/`:变更真相,保留 `change-state.json`、`change-meta.json`、planning 文档、`task-manifest.json`、可选 `team-state.json`、任务级 `checkpoint.json`、`report-card.json` 和唯一的最终 handoff 文件。
|
|
98
98
|
- worker prompt、journal、assignment、session log 统一放到 `devflow/workspaces/<change>/`,作为 ephemeral scratch。
|
|
99
99
|
|
|
@@ -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({
|
|
@@ -39,4 +39,29 @@ describe('change directory naming', () => {
|
|
|
39
39
|
|
|
40
40
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
test('allows duplicate numbers when descriptions differ', () => {
|
|
44
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
|
|
45
|
+
fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-existing-plan'), { recursive: true });
|
|
46
|
+
|
|
47
|
+
const paths = getChangePaths(repoRoot, 'REQ-123', { goal: 'Another plan' });
|
|
48
|
+
|
|
49
|
+
expect(paths.changeKey).toBe('REQ-123-another-plan');
|
|
50
|
+
expect(paths.changeDir).toBe(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-another-plan'));
|
|
51
|
+
|
|
52
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('requires explicit change keys when duplicate numbers already exist', () => {
|
|
56
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-paths-'));
|
|
57
|
+
fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-first-plan'), { recursive: true });
|
|
58
|
+
fs.mkdirSync(path.join(repoRoot, 'devflow', 'changes', 'REQ-123-second-plan'), { recursive: true });
|
|
59
|
+
|
|
60
|
+
expect(() => getChangePaths(repoRoot, 'REQ-123'))
|
|
61
|
+
.toThrow(/Ambiguous changeId "REQ-123"/);
|
|
62
|
+
expect(getChangePaths(repoRoot, 'REQ-123', { changeKey: 'REQ-123-second-plan' }).changeKey)
|
|
63
|
+
.toBe('REQ-123-second-plan');
|
|
64
|
+
|
|
65
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
66
|
+
});
|
|
42
67
|
});
|
|
@@ -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
|
|
|
@@ -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);
|
|
@@ -68,11 +68,11 @@ function summarizeGateSection(gates = []) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
async function readLatestCheckpoints(repoRoot, changeId, tasks = []) {
|
|
71
|
+
async function readLatestCheckpoints(repoRoot, changeId, tasks = [], options = {}) {
|
|
72
72
|
const checkpoints = [];
|
|
73
73
|
|
|
74
74
|
for (const task of tasks) {
|
|
75
|
-
const checkpoint = await readJson(getCheckpointPath(repoRoot, changeId, task.id), null);
|
|
75
|
+
const checkpoint = await readJson(getCheckpointPath(repoRoot, changeId, task.id, options), null);
|
|
76
76
|
if (!checkpoint) {
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
@@ -90,28 +90,28 @@ async function readLatestCheckpoints(repoRoot, changeId, tasks = []) {
|
|
|
90
90
|
return checkpoints;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function cleanupLegacyArtifacts(repoRoot, changeId, manifest) {
|
|
93
|
+
async function cleanupLegacyArtifacts(repoRoot, changeId, manifest, options = {}) {
|
|
94
94
|
const taskIds = (manifest?.tasks || []).map((task) => task.id);
|
|
95
|
-
const change = getChangePaths(repoRoot, changeId);
|
|
95
|
+
const change = getChangePaths(repoRoot, changeId, options);
|
|
96
96
|
|
|
97
97
|
await Promise.all(
|
|
98
|
-
getLegacyIntentProjectionPaths(repoRoot, changeId, taskIds).map((target) => removePath(target))
|
|
98
|
+
getLegacyIntentProjectionPaths(repoRoot, changeId, taskIds, options).map((target) => removePath(target))
|
|
99
99
|
);
|
|
100
100
|
await removePath(change.workersDir);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async function clearHandoffArtifacts(repoRoot, changeId, keep = null) {
|
|
103
|
+
async function clearHandoffArtifacts(repoRoot, changeId, keep = null, options = {}) {
|
|
104
104
|
await Promise.all(
|
|
105
|
-
getIntentHandoffArtifactPaths(repoRoot, changeId)
|
|
105
|
+
getIntentHandoffArtifactPaths(repoRoot, changeId, options)
|
|
106
106
|
.filter((target) => target !== keep)
|
|
107
107
|
.map((target) => removePath(target))
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
|
|
112
|
-
await ensureIntentScaffold(repoRoot, changeId);
|
|
111
|
+
async function writeResumeIndex(repoRoot, changeId, state, manifest, report, options = {}) {
|
|
112
|
+
await ensureIntentScaffold(repoRoot, changeId, options);
|
|
113
113
|
|
|
114
|
-
const checkpoints = await readLatestCheckpoints(repoRoot, changeId, manifest?.tasks || []);
|
|
114
|
+
const checkpoints = await readLatestCheckpoints(repoRoot, changeId, manifest?.tasks || [], options);
|
|
115
115
|
const lastGoodCheckpoint = checkpoints.find((checkpoint) => checkpoint.status === 'passed') || checkpoints[0] || null;
|
|
116
116
|
const blockers = [
|
|
117
117
|
...(manifest?.tasks || [])
|
|
@@ -119,8 +119,8 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
|
|
|
119
119
|
.map((task) => `${task.id}: ${task.lastError || 'Task failed'}`),
|
|
120
120
|
...(report?.blockingFindings || [])
|
|
121
121
|
];
|
|
122
|
-
const hasPrBrief = await exists(getIntentPrBriefPath(repoRoot, changeId));
|
|
123
|
-
const hasReleaseNote = await exists(getReleaseNotePath(repoRoot, changeId));
|
|
122
|
+
const hasPrBrief = await exists(getIntentPrBriefPath(repoRoot, changeId, options));
|
|
123
|
+
const hasReleaseNote = await exists(getReleaseNotePath(repoRoot, changeId, options));
|
|
124
124
|
const stage = deriveLifecycleStage({ state, manifest, report, hasPrBrief });
|
|
125
125
|
const approval = getApprovalState(state, manifest);
|
|
126
126
|
const goal = manifest?.goal || state?.goal || changeId;
|
|
@@ -131,7 +131,7 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
|
|
|
131
131
|
hasPrBrief,
|
|
132
132
|
hasReleaseNote
|
|
133
133
|
});
|
|
134
|
-
const resumePath = getIntentResumeIndexPath(repoRoot, changeId);
|
|
134
|
+
const resumePath = getIntentResumeIndexPath(repoRoot, changeId, options);
|
|
135
135
|
|
|
136
136
|
const content = [
|
|
137
137
|
`# Resume Index: ${changeId}`,
|
|
@@ -167,7 +167,7 @@ async function writeResumeIndex(repoRoot, changeId, state, manifest, report) {
|
|
|
167
167
|
])
|
|
168
168
|
].join('\n');
|
|
169
169
|
|
|
170
|
-
await clearHandoffArtifacts(repoRoot, changeId, resumePath);
|
|
170
|
+
await clearHandoffArtifacts(repoRoot, changeId, resumePath, options);
|
|
171
171
|
await writeText(resumePath, `${content}\n`);
|
|
172
172
|
|
|
173
173
|
return {
|
|
@@ -28,11 +28,11 @@ function hasUnresolvedTasks(manifest) {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async function loadState(repoRoot, changeId) {
|
|
31
|
+
async function loadState(repoRoot, changeId, options = {}) {
|
|
32
32
|
const [state, manifest, report] = await Promise.all([
|
|
33
|
-
readJson(getRuntimeStatePath(repoRoot, changeId), null),
|
|
34
|
-
readJson(getTaskManifestPath(repoRoot, changeId), null),
|
|
35
|
-
readJson(getReportCardPath(repoRoot, changeId), null)
|
|
33
|
+
readJson(getRuntimeStatePath(repoRoot, changeId, options), null),
|
|
34
|
+
readJson(getTaskManifestPath(repoRoot, changeId, options), null),
|
|
35
|
+
readJson(getReportCardPath(repoRoot, changeId, options), null)
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
return { state, manifest, report };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [INPUT]: 依赖 fs/path,接收 repoRoot/changeId/taskId/workerId 等定位参数。
|
|
3
3
|
* [OUTPUT]: 对外提供 devflow canonical layout 的唯一路径解析能力。
|
|
4
|
-
* [POS]: skill runtime
|
|
4
|
+
* [POS]: skill runtime 的路径真相源,完整 change key 才是目录身份。
|
|
5
5
|
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -85,17 +85,27 @@ function listChangeKeys(repoRoot) {
|
|
|
85
85
|
.sort();
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function
|
|
88
|
+
function findExistingChangeKeys(repoRoot, changeId) {
|
|
89
89
|
const canonicalPrefix = `${getChangeKeyPrefix(changeId)}-`;
|
|
90
90
|
const legacyKey = slugifySegment(changeId, 'change');
|
|
91
91
|
const legacyPrefix = `${legacyKey}-`;
|
|
92
92
|
|
|
93
|
-
return listChangeKeys(repoRoot).
|
|
93
|
+
return listChangeKeys(repoRoot).filter((name) => (
|
|
94
94
|
name === changeId
|
|
95
95
|
|| name === legacyKey
|
|
96
96
|
|| name.startsWith(canonicalPrefix)
|
|
97
97
|
|| name.startsWith(legacyPrefix)
|
|
98
|
-
))
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findExactChangeKey(repoRoot, changeId, changeKey) {
|
|
102
|
+
const slug = getChangeSlug(changeId, changeKey);
|
|
103
|
+
const aliases = new Set([
|
|
104
|
+
changeKey,
|
|
105
|
+
`${slugifySegment(changeId, 'change')}-${slug}`
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
return listChangeKeys(repoRoot).find((name) => aliases.has(name)) || null;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
function assertChangeKey(changeId, changeKey) {
|
|
@@ -132,9 +142,20 @@ function resolveChangeKey(repoRoot, changeId, options = {}) {
|
|
|
132
142
|
return assertChangeKey(changeId, options.changeKey);
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
145
|
+
const requestedSlug = options.slug || stripChangeIdPrefix(changeId, options.goal);
|
|
146
|
+
if (requestedSlug) {
|
|
147
|
+
const requestedKey = buildChangeKey(changeId, { slug: requestedSlug });
|
|
148
|
+
return findExactChangeKey(repoRoot, changeId, requestedKey) || requestedKey;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const existing = findExistingChangeKeys(repoRoot, changeId);
|
|
152
|
+
if (existing.length === 1) {
|
|
153
|
+
return existing[0];
|
|
154
|
+
}
|
|
155
|
+
if (existing.length > 1) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Ambiguous changeId "${changeId}": found ${existing.join(', ')}. Pass an explicit changeKey.`
|
|
158
|
+
);
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
return buildChangeKey(changeId, options);
|
|
@@ -48,10 +48,10 @@ function createQueryRegistry(entries) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
const requiredArtifactRefs = resolveArtifactRefs(entry, context, 'requiredArtifactRefs');
|
|
53
|
-
|
|
51
|
+
let artifactRefs = [];
|
|
54
52
|
try {
|
|
53
|
+
artifactRefs = resolveArtifactRefs(entry, context, 'artifactRefs');
|
|
54
|
+
const requiredArtifactRefs = resolveArtifactRefs(entry, context, 'requiredArtifactRefs');
|
|
55
55
|
const missingRefs = requiredArtifactRefs.filter((ref) => !fs.existsSync(ref));
|
|
56
56
|
if (missingRefs.length > 0) {
|
|
57
57
|
throw namedError(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [INPUT]: 依赖 store/artifacts/lifecycle 读取 requirement 工件,接收 repoRoot
|
|
2
|
+
* [INPUT]: 依赖 store/artifacts/lifecycle 读取 requirement 工件,接收 repoRoot、changeId 和可选 changeKey。
|
|
3
3
|
* [OUTPUT]: 对外提供 typed query registry 与兼容查询函数,附 named error 和 trace shape。
|
|
4
4
|
* [POS]: skill runtime 的薄查询兼容层,只读 artifact 与共享 lifecycle 语义,不再自带流程推导副本。
|
|
5
5
|
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
@@ -65,8 +65,8 @@ async function readQueryArtifact(filePath, { required = true } = {}) {
|
|
|
65
65
|
* @param {string} changeId - 需求 ID
|
|
66
66
|
* @returns {Promise<Object>} 进度统计对象
|
|
67
67
|
*/
|
|
68
|
-
async function getProgress(repoRoot, changeId) {
|
|
69
|
-
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
68
|
+
async function getProgress(repoRoot, changeId, options = {}) {
|
|
69
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId, options);
|
|
70
70
|
const manifest = await readQueryArtifact(manifestPath);
|
|
71
71
|
return deriveTaskProgress(manifest.tasks || []);
|
|
72
72
|
}
|
|
@@ -77,8 +77,8 @@ async function getProgress(repoRoot, changeId) {
|
|
|
77
77
|
* @param {string} changeId - 需求 ID
|
|
78
78
|
* @returns {Promise<Object|null>} 下一个任务对象或 null
|
|
79
79
|
*/
|
|
80
|
-
async function getNextTask(repoRoot, changeId) {
|
|
81
|
-
const manifestPath = getTaskManifestPath(repoRoot, changeId);
|
|
80
|
+
async function getNextTask(repoRoot, changeId, options = {}) {
|
|
81
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId, options);
|
|
82
82
|
const manifest = await readQueryArtifact(manifestPath);
|
|
83
83
|
const executionState = deriveManifestExecutionState(manifest.tasks || []);
|
|
84
84
|
const activePhase = manifest.activePhase ?? executionState.activePhase;
|
|
@@ -117,18 +117,18 @@ async function getNextTask(repoRoot, changeId) {
|
|
|
117
117
|
* @param {string} changeId - 需求 ID
|
|
118
118
|
* @returns {Promise<Object>} 完整状态对象
|
|
119
119
|
*/
|
|
120
|
-
async function getFullState(repoRoot, changeId) {
|
|
121
|
-
const statePath = getRuntimeStatePath(repoRoot, changeId);
|
|
122
|
-
const reportPath = getReportCardPath(repoRoot, changeId);
|
|
123
|
-
const prBriefPath = getIntentPrBriefPath(repoRoot, changeId);
|
|
120
|
+
async function getFullState(repoRoot, changeId, options = {}) {
|
|
121
|
+
const statePath = getRuntimeStatePath(repoRoot, changeId, options);
|
|
122
|
+
const reportPath = getReportCardPath(repoRoot, changeId, options);
|
|
123
|
+
const prBriefPath = getIntentPrBriefPath(repoRoot, changeId, options);
|
|
124
124
|
|
|
125
125
|
const [state, manifest, hasPrBrief] = await Promise.all([
|
|
126
126
|
readQueryArtifact(statePath),
|
|
127
|
-
readQueryArtifact(getTaskManifestPath(repoRoot, changeId)),
|
|
127
|
+
readQueryArtifact(getTaskManifestPath(repoRoot, changeId, options)),
|
|
128
128
|
exists(prBriefPath)
|
|
129
129
|
]);
|
|
130
|
-
const progress = await getProgress(repoRoot, changeId);
|
|
131
|
-
const nextTask = await getNextTask(repoRoot, changeId);
|
|
130
|
+
const progress = await getProgress(repoRoot, changeId, options);
|
|
131
|
+
const nextTask = await getNextTask(repoRoot, changeId, options);
|
|
132
132
|
const report = await readQueryArtifact(reportPath, { required: false });
|
|
133
133
|
|
|
134
134
|
return {
|
|
@@ -160,8 +160,8 @@ async function getFullState(repoRoot, changeId) {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
async function getShipReadiness(repoRoot, changeId) {
|
|
164
|
-
const reportPath = getReportCardPath(repoRoot, changeId);
|
|
163
|
+
async function getShipReadiness(repoRoot, changeId, options = {}) {
|
|
164
|
+
const reportPath = getReportCardPath(repoRoot, changeId, options);
|
|
165
165
|
const report = await readJson(reportPath, null);
|
|
166
166
|
|
|
167
167
|
if (!report) {
|
|
@@ -178,12 +178,12 @@ async function getShipReadiness(repoRoot, changeId) {
|
|
|
178
178
|
return deriveShipReadiness(report, { reportPath });
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function queryArtifactRefs(repoRoot, changeId, names) {
|
|
181
|
+
function queryArtifactRefs(repoRoot, changeId, names, options = {}) {
|
|
182
182
|
const refs = {
|
|
183
|
-
manifest: getTaskManifestPath(repoRoot, changeId),
|
|
184
|
-
state: getRuntimeStatePath(repoRoot, changeId),
|
|
185
|
-
report: getReportCardPath(repoRoot, changeId),
|
|
186
|
-
prBrief: getIntentPrBriefPath(repoRoot, changeId)
|
|
183
|
+
manifest: getTaskManifestPath(repoRoot, changeId, options),
|
|
184
|
+
state: getRuntimeStatePath(repoRoot, changeId, options),
|
|
185
|
+
report: getReportCardPath(repoRoot, changeId, options),
|
|
186
|
+
prBrief: getIntentPrBriefPath(repoRoot, changeId, options)
|
|
187
187
|
};
|
|
188
188
|
|
|
189
189
|
return names.map((name) => refs[name]).filter(Boolean);
|
|
@@ -192,26 +192,26 @@ function queryArtifactRefs(repoRoot, changeId, names) {
|
|
|
192
192
|
const registry = createQueryRegistry([
|
|
193
193
|
{
|
|
194
194
|
id: 'progress',
|
|
195
|
-
artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
|
|
196
|
-
requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
|
|
197
|
-
handler: ({ repoRoot, changeId }) => getProgress(repoRoot, changeId)
|
|
195
|
+
artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
|
|
196
|
+
requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
|
|
197
|
+
handler: ({ repoRoot, changeId, changeKey }) => getProgress(repoRoot, changeId, { changeKey })
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
id: 'next-task',
|
|
201
|
-
artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
|
|
202
|
-
requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
|
|
203
|
-
handler: ({ repoRoot, changeId }) => getNextTask(repoRoot, changeId)
|
|
201
|
+
artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
|
|
202
|
+
requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['manifest'], { changeKey }),
|
|
203
|
+
handler: ({ repoRoot, changeId, changeKey }) => getNextTask(repoRoot, changeId, { changeKey })
|
|
204
204
|
},
|
|
205
205
|
{
|
|
206
206
|
id: 'full-state',
|
|
207
|
-
artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest', 'report', 'prBrief']),
|
|
208
|
-
requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest']),
|
|
209
|
-
handler: ({ repoRoot, changeId }) => getFullState(repoRoot, changeId)
|
|
207
|
+
artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest', 'report', 'prBrief'], { changeKey }),
|
|
208
|
+
requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest'], { changeKey }),
|
|
209
|
+
handler: ({ repoRoot, changeId, changeKey }) => getFullState(repoRoot, changeId, { changeKey })
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
id: 'ship-readiness',
|
|
213
|
-
artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['report']),
|
|
214
|
-
handler: ({ repoRoot, changeId }) => getShipReadiness(repoRoot, changeId)
|
|
213
|
+
artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['report'], { changeKey }),
|
|
214
|
+
handler: ({ repoRoot, changeId, changeKey }) => getShipReadiness(repoRoot, changeId, { changeKey })
|
|
215
215
|
}
|
|
216
216
|
]);
|
|
217
217
|
|