cc-devflow 4.5.11 → 4.5.12
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 +18 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +17 -269
- package/.claude/skills/cc-act/SKILL.md +38 -425
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_INDEX_TEMPLATE.md +2 -13
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_TEMPLATE.md +1 -9
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +21 -177
- package/.claude/skills/cc-act/references/closure-contract.md +12 -63
- package/.claude/skills/cc-act/references/git-commit-guidelines.md +5 -5
- package/.claude/skills/cc-act/scripts/cc-act-common.sh +5 -322
- package/.claude/skills/cc-act/scripts/detect-ship-target.sh +11 -2
- package/.claude/skills/cc-act/scripts/inspect-git-index.sh +58 -0
- package/.claude/skills/cc-act/scripts/render-pr-brief.sh +40 -440
- package/.claude/skills/cc-act/scripts/verify-act-gate.sh +10 -50
- package/.claude/skills/cc-check/CHANGELOG.md +18 -0
- package/.claude/skills/cc-check/PLAYBOOK.md +19 -273
- package/.claude/skills/cc-check/SKILL.md +33 -456
- package/.claude/skills/cc-check/references/review-contract.md +12 -147
- package/.claude/skills/cc-dev/CHANGELOG.md +15 -0
- package/.claude/skills/cc-dev/PLAYBOOK.md +1 -1
- package/.claude/skills/cc-dev/SKILL.md +52 -137
- package/.claude/skills/cc-dev/scripts/resolve-cc-devflow.sh +181 -0
- package/.claude/skills/cc-do/CHANGELOG.md +11 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +19 -113
- package/.claude/skills/cc-do/SKILL.md +39 -245
- package/.claude/skills/cc-do/references/execution-recovery.md +15 -109
- package/.claude/skills/cc-do/scripts/cc-do-common.sh +5 -57
- package/.claude/skills/cc-do/scripts/check-task-status.sh +35 -65
- package/.claude/skills/cc-do/scripts/mark-task-complete.sh +9 -46
- package/.claude/skills/cc-do/scripts/select-ready-tasks.sh +29 -97
- package/.claude/skills/cc-investigate/CHANGELOG.md +16 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +20 -180
- package/.claude/skills/cc-investigate/SKILL.md +64 -246
- package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +48 -98
- package/.claude/skills/cc-investigate/references/investigation-contract.md +14 -218
- package/.claude/skills/cc-next/CHANGELOG.md +6 -0
- package/.claude/skills/cc-next/PLAYBOOK.md +12 -8
- package/.claude/skills/cc-next/SKILL.md +34 -140
- package/.claude/skills/cc-plan/CHANGELOG.md +16 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +22 -161
- package/.claude/skills/cc-plan/SKILL.md +45 -295
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +30 -228
- package/.claude/skills/cc-plan/references/planning-contract.md +24 -161
- package/.claude/skills/cc-plan/scripts/next-change-key.sh +8 -44
- package/.claude/skills/cc-plan/scripts/parse-task-dependencies.js +2 -2
- package/.claude/skills/cc-plan/scripts/validate-scope.sh +1 -1
- package/.claude/skills/cc-pr-land/SKILL.md +14 -114
- package/.claude/skills/cc-pr-review/CHANGELOG.md +4 -0
- package/.claude/skills/cc-pr-review/SKILL.md +20 -103
- package/.claude/skills/cc-review/CHANGELOG.md +17 -0
- package/.claude/skills/cc-review/PLAYBOOK.md +13 -86
- package/.claude/skills/cc-review/SKILL.md +53 -241
- package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +2 -2
- package/.claude/skills/cc-review/references/implementation-review-branch.md +7 -147
- package/.claude/skills/cc-review/references/plan-review-branch.md +5 -147
- package/.claude/skills/cc-review/references/review-methods.md +10 -218
- package/.claude/skills/cc-review/scripts/collect-review-context.sh +4 -63
- package/.claude/skills/cc-roadmap/PLAYBOOK.md +1 -1
- package/.claude/skills/cc-roadmap/SKILL.md +3 -3
- package/.claude/skills/cc-simplify/CHANGELOG.md +7 -0
- package/.claude/skills/cc-simplify/SKILL.md +26 -21
- package/.claude/skills/cc-spec-init/PLAYBOOK.md +12 -48
- package/.claude/skills/cc-spec-init/SKILL.md +29 -132
- package/.claude/skills/cc-spec-init/references/spec-contract.md +8 -17
- package/CHANGELOG.md +13 -0
- package/bin/cc-devflow-cli.js +20 -260
- package/bin/cc-devflow.js +44 -7
- package/docs/commands/README.md +1 -1
- package/docs/commands/README.zh-CN.md +1 -1
- package/docs/examples/README.md +1 -1
- package/docs/examples/START-HERE.md +14 -15
- package/docs/examples/example-bindings.json +11 -11
- package/docs/examples/full-design-blocked/README.md +4 -6
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/{planning/tasks.md → task.md} +20 -15
- package/docs/examples/local-handoff/README.md +8 -11
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/handoff/pr-brief.md +31 -0
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/{planning/tasks.md → task.md} +18 -13
- package/docs/examples/pdca-loop/README.md +6 -9
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +9 -11
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/{planning/tasks.md → task.md} +18 -13
- package/docs/examples/scripts/check-example-bindings.sh +11 -62
- package/docs/guides/artifact-contract.md +10 -40
- package/docs/guides/getting-started.md +8 -8
- package/docs/guides/getting-started.zh-CN.md +8 -8
- package/docs/guides/minimize-artifacts.md +16 -130
- package/docs/guides/project-postmortem.md +14 -71
- package/lib/compiler/__tests__/skills-registry.test.js +9 -8
- package/lib/compiler/resource-copier.js +29 -0
- package/lib/skill-runtime/__tests__/archive-change.test.js +2 -2
- package/lib/skill-runtime/__tests__/benchmark-skills.test.js +3 -3
- package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +14 -4
- package/lib/skill-runtime/errors.js +3 -3
- package/lib/skill-runtime/index.js +5 -23
- package/lib/skill-runtime/paths.js +5 -52
- package/lib/skill-runtime/query-registry.js +4 -4
- package/lib/skill-runtime/query.js +89 -201
- package/lib/skill-runtime/store.js +4 -40
- package/lib/skill-runtime/trace.js +2 -2
- package/package.json +2 -5
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_PRINCIPLES_TEMPLATE.md +0 -29
- package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +0 -54
- package/.claude/skills/cc-act/scripts/generate-status-report.sh +0 -92
- package/.claude/skills/cc-act/scripts/sync-act-docs.sh +0 -355
- package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +0 -234
- package/.claude/skills/cc-check/scripts/render-report-card.js +0 -438
- package/.claude/skills/cc-check/scripts/verify-gate.sh +0 -85
- package/.claude/skills/cc-do/scripts/build-task-context.sh +0 -175
- package/.claude/skills/cc-do/scripts/record-review-decision.sh +0 -88
- package/.claude/skills/cc-do/scripts/recover-workflow.sh +0 -82
- package/.claude/skills/cc-do/scripts/run-problem-analysis.sh +0 -70
- package/.claude/skills/cc-do/scripts/verify-task-gates.sh +0 -109
- package/.claude/skills/cc-do/scripts/write-task-checkpoint.sh +0 -92
- package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +0 -224
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +0 -178
- package/.claude/skills/cc-spec-init/assets/CHANGE_META_TEMPLATE.json +0 -28
- package/.claude/skills/cc-spec-init/scripts/validate-spec-links.sh +0 -45
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +0 -234
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +0 -488
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/review/report-card.json +0 -189
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/handoff/resume-index.md +0 -39
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/handoff/status.md +0 -29
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +0 -123
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +0 -292
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/review/report-card.json +0 -136
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/status.md +0 -29
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +0 -124
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +0 -292
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/review/report-card.json +0 -136
- package/docs/get-shit-done-strategy-audit.md +0 -518
- package/docs/skill-runtime-migration.md +0 -46
- package/lib/skill-runtime/__tests__/approve.test.js +0 -92
- package/lib/skill-runtime/__tests__/autopilot.test.js +0 -253
- package/lib/skill-runtime/__tests__/benchmark-artifacts.test.js +0 -165
- package/lib/skill-runtime/__tests__/delegation.test.js +0 -97
- package/lib/skill-runtime/__tests__/dispatch.test.js +0 -237
- package/lib/skill-runtime/__tests__/intent.test.js +0 -203
- package/lib/skill-runtime/__tests__/lifecycle.test.js +0 -169
- package/lib/skill-runtime/__tests__/planner.tdd.test.js +0 -331
- package/lib/skill-runtime/__tests__/prepare-pr.test.js +0 -126
- package/lib/skill-runtime/__tests__/query.test.js +0 -860
- package/lib/skill-runtime/__tests__/readiness.test.js +0 -53
- package/lib/skill-runtime/__tests__/release.test.js +0 -85
- package/lib/skill-runtime/__tests__/review-check-integration.test.js +0 -148
- package/lib/skill-runtime/__tests__/review-records.test.js +0 -619
- package/lib/skill-runtime/__tests__/runtime.integration.test.js +0 -351
- package/lib/skill-runtime/__tests__/schemas.test.js +0 -337
- package/lib/skill-runtime/__tests__/task-contract-migrate.test.js +0 -137
- package/lib/skill-runtime/__tests__/task-contract.test.js +0 -874
- package/lib/skill-runtime/__tests__/team-state.test.js +0 -51
- package/lib/skill-runtime/__tests__/verify-artifacts.test.js +0 -203
- package/lib/skill-runtime/__tests__/worker-run.test.js +0 -275
- package/lib/skill-runtime/__tests__/worker.test.js +0 -56
- package/lib/skill-runtime/__tests__/workflow-context-legacy-fallback.test.js +0 -31
- package/lib/skill-runtime/__tests__/workflow-context.test.js +0 -98
- package/lib/skill-runtime/artifacts.js +0 -88
- package/lib/skill-runtime/context-index.js +0 -545
- package/lib/skill-runtime/delegation.js +0 -533
- package/lib/skill-runtime/intent.js +0 -309
- package/lib/skill-runtime/lifecycle.js +0 -294
- package/lib/skill-runtime/operations/CLAUDE.md +0 -19
- package/lib/skill-runtime/operations/approve.js +0 -81
- package/lib/skill-runtime/operations/autopilot-core.js +0 -337
- package/lib/skill-runtime/operations/autopilot-execution.js +0 -307
- package/lib/skill-runtime/operations/autopilot-shared.js +0 -48
- package/lib/skill-runtime/operations/autopilot.js +0 -163
- package/lib/skill-runtime/operations/dispatch.js +0 -416
- package/lib/skill-runtime/operations/init.js +0 -60
- package/lib/skill-runtime/operations/janitor.js +0 -61
- package/lib/skill-runtime/operations/plan.js +0 -59
- package/lib/skill-runtime/operations/prepare-pr.js +0 -25
- package/lib/skill-runtime/operations/release.js +0 -99
- package/lib/skill-runtime/operations/resume.js +0 -126
- package/lib/skill-runtime/operations/review-records.js +0 -265
- package/lib/skill-runtime/operations/snapshot.js +0 -45
- package/lib/skill-runtime/operations/task-contract.js +0 -593
- package/lib/skill-runtime/operations/verify.js +0 -170
- package/lib/skill-runtime/operations/worker-run.js +0 -531
- package/lib/skill-runtime/operations/worker.js +0 -33
- package/lib/skill-runtime/planner.js +0 -539
- package/lib/skill-runtime/readiness.js +0 -84
- package/lib/skill-runtime/review-records.js +0 -123
- package/lib/skill-runtime/review.js +0 -855
- package/lib/skill-runtime/schemas.js +0 -746
- package/lib/skill-runtime/task-contract.js +0 -188
- package/lib/skill-runtime/team-state.js +0 -122
- package/lib/skill-runtime/workflow-context.js +0 -748
|
@@ -1,874 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [INPUT]: 依赖 task-contract parser、operation 层与 CLI 子进程。
|
|
3
|
-
* [OUTPUT]: 通过 parser 返回值、生成文件、validate 规则与 CLI stderr 证明 tasks.md contract 行为。
|
|
4
|
-
* [POS]: REQ-003-minimize-workflow-artifacts T002-T006 的 Red/Green 证据。
|
|
5
|
-
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
extractTasksContractSummary,
|
|
10
|
-
extractTasksRootCauseContract
|
|
11
|
-
} = require('../task-contract');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const os = require('os');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const { spawnSync } = require('child_process');
|
|
16
|
-
const { runCompile, runValidate } = require('../operations/task-contract');
|
|
17
|
-
|
|
18
|
-
const CLI_BIN = path.resolve(__dirname, '../../..', 'bin/cc-devflow-cli.js');
|
|
19
|
-
|
|
20
|
-
const SAMPLE_WITH_HEADING = `# REQ-003-minimize-workflow-artifacts
|
|
21
|
-
|
|
22
|
-
## Plan Meta
|
|
23
|
-
|
|
24
|
-
Some plan meta prose.
|
|
25
|
-
|
|
26
|
-
## Contract Summary
|
|
27
|
-
|
|
28
|
-
Change: REQ-003-minimize-workflow-artifacts
|
|
29
|
-
Mode: plan
|
|
30
|
-
Profile: deep
|
|
31
|
-
Approval: approved (ExitPlanMode 2026-05-12)
|
|
32
|
-
|
|
33
|
-
Goal:
|
|
34
|
-
- Collapse default artifact surface.
|
|
35
|
-
- Switch workflow-context canonical refs.
|
|
36
|
-
|
|
37
|
-
Do Not Do:
|
|
38
|
-
- Do not migrate REQ-001 / REQ-002 by default.
|
|
39
|
-
- Do not rebuild task-manifest.json.
|
|
40
|
-
|
|
41
|
-
Approved Direction:
|
|
42
|
-
- Single REQ-003 PR covering v2 PR-A + PR-B + PR-C.
|
|
43
|
-
- 18 vertical slices.
|
|
44
|
-
|
|
45
|
-
Acceptance:
|
|
46
|
-
- New REQ-003-example directory emits only tasks.md.
|
|
47
|
-
- verify:artifacts passes.
|
|
48
|
-
|
|
49
|
-
Verification:
|
|
50
|
-
|
|
51
|
-
\`\`\`bash
|
|
52
|
-
npm test
|
|
53
|
-
npm run verify:artifacts
|
|
54
|
-
\`\`\`
|
|
55
|
-
|
|
56
|
-
Risk / Escalate If:
|
|
57
|
-
- benchmark-workflow-context-tokens fails for REQ-001.
|
|
58
|
-
- Total LOC exceeds 3500.
|
|
59
|
-
|
|
60
|
-
## Implementation Surface Map
|
|
61
|
-
|
|
62
|
-
Other content that must not leak into the Contract Summary block.
|
|
63
|
-
`;
|
|
64
|
-
|
|
65
|
-
const SAMPLE_WITHOUT_HEADING = `# REQ-999
|
|
66
|
-
|
|
67
|
-
## Plan Meta
|
|
68
|
-
|
|
69
|
-
Just plan meta.
|
|
70
|
-
|
|
71
|
-
## Something Else
|
|
72
|
-
|
|
73
|
-
Unrelated.
|
|
74
|
-
`;
|
|
75
|
-
|
|
76
|
-
describe('extractTasksContractSummary', () => {
|
|
77
|
-
describe('when `## Contract Summary` heading is present', () => {
|
|
78
|
-
test('returns found:true', () => {
|
|
79
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
80
|
-
expect(result.found).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('content spans from the heading line up to (but not including) the next H2 heading', () => {
|
|
84
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
85
|
-
expect(result.content).toMatch(/^## Contract Summary/);
|
|
86
|
-
expect(result.content).not.toMatch(/## Implementation Surface Map/);
|
|
87
|
-
expect(result.content).not.toMatch(/Other content that must not leak/);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('content includes the trailing risk block before the next heading', () => {
|
|
91
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
92
|
-
expect(result.content).toMatch(/Risk \/ Escalate If:/);
|
|
93
|
-
expect(result.content).toMatch(/Total LOC exceeds 3500\./);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('fields parses Change as a single-line KV', () => {
|
|
97
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
98
|
-
expect(result.fields.change).toBe('REQ-003-minimize-workflow-artifacts');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('fields parses Mode and Profile as single-line KVs', () => {
|
|
102
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
103
|
-
expect(result.fields.mode).toBe('plan');
|
|
104
|
-
expect(result.fields.profile).toBe('deep');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('fields parses Approval as a single-line KV preserving inline parentheses', () => {
|
|
108
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
109
|
-
expect(result.fields.approval).toBe('approved (ExitPlanMode 2026-05-12)');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('fields parses Goal as a multi-line bullet list', () => {
|
|
113
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
114
|
-
expect(Array.isArray(result.fields.goal)).toBe(true);
|
|
115
|
-
expect(result.fields.goal).toEqual([
|
|
116
|
-
'Collapse default artifact surface.',
|
|
117
|
-
'Switch workflow-context canonical refs.'
|
|
118
|
-
]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('fields parses Do Not Do as a bullet list', () => {
|
|
122
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
123
|
-
expect(result.fields.doNotDo).toEqual([
|
|
124
|
-
'Do not migrate REQ-001 / REQ-002 by default.',
|
|
125
|
-
'Do not rebuild task-manifest.json.'
|
|
126
|
-
]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('fields parses Approved Direction as a bullet list', () => {
|
|
130
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
131
|
-
expect(result.fields.approvedDirection).toEqual([
|
|
132
|
-
'Single REQ-003 PR covering v2 PR-A + PR-B + PR-C.',
|
|
133
|
-
'18 vertical slices.'
|
|
134
|
-
]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('fields parses Acceptance as a bullet list', () => {
|
|
138
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
139
|
-
expect(result.fields.acceptance).toEqual([
|
|
140
|
-
'New REQ-003-example directory emits only tasks.md.',
|
|
141
|
-
'verify:artifacts passes.'
|
|
142
|
-
]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('fields parses Verification as a fenced code block string', () => {
|
|
146
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
147
|
-
expect(typeof result.fields.verification).toBe('string');
|
|
148
|
-
expect(result.fields.verification).toMatch(/npm test/);
|
|
149
|
-
expect(result.fields.verification).toMatch(/npm run verify:artifacts/);
|
|
150
|
-
expect(result.fields.verification).not.toMatch(/```/);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('fields parses Risk / Escalate If as a bullet list', () => {
|
|
154
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
155
|
-
expect(result.fields.risk).toEqual([
|
|
156
|
-
'benchmark-workflow-context-tokens fails for REQ-001.',
|
|
157
|
-
'Total LOC exceeds 3500.'
|
|
158
|
-
]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('fields does not include keys from sections outside Contract Summary', () => {
|
|
162
|
-
const result = extractTasksContractSummary(SAMPLE_WITH_HEADING);
|
|
163
|
-
expect(result.fields).not.toHaveProperty('implementationSurfaceMap');
|
|
164
|
-
expect(result.fields).not.toHaveProperty('something');
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('when `## Contract Summary` heading is absent', () => {
|
|
169
|
-
test('returns found:false', () => {
|
|
170
|
-
const result = extractTasksContractSummary(SAMPLE_WITHOUT_HEADING);
|
|
171
|
-
expect(result.found).toBe(false);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('returns empty content and empty fields', () => {
|
|
175
|
-
const result = extractTasksContractSummary(SAMPLE_WITHOUT_HEADING);
|
|
176
|
-
expect(result.content).toBe('');
|
|
177
|
-
expect(result.fields).toEqual({});
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('edge cases', () => {
|
|
182
|
-
test('treats a heading-only block with no body as found:true with empty fields', () => {
|
|
183
|
-
const headingOnly = '## Contract Summary\n\n## Next Heading\n\nBody\n';
|
|
184
|
-
const result = extractTasksContractSummary(headingOnly);
|
|
185
|
-
expect(result.found).toBe(true);
|
|
186
|
-
expect(result.content).toMatch(/^## Contract Summary/);
|
|
187
|
-
expect(result.fields).toEqual({});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('is case-sensitive on the canonical heading spelling', () => {
|
|
191
|
-
const wrongCase = '## contract summary\n\nChange: REQ-999\n\n## Next\n';
|
|
192
|
-
const result = extractTasksContractSummary(wrongCase);
|
|
193
|
-
expect(result.found).toBe(false);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('throws a TypeError when input is not a string', () => {
|
|
197
|
-
expect(() => extractTasksContractSummary(null)).toThrow(TypeError);
|
|
198
|
-
expect(() => extractTasksContractSummary(undefined)).toThrow(TypeError);
|
|
199
|
-
expect(() => extractTasksContractSummary(42)).toThrow(TypeError);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const FIX_SAMPLE_WITH_HEADING = `# FIX-042-cache-stampede
|
|
205
|
-
|
|
206
|
-
## Plan Meta
|
|
207
|
-
|
|
208
|
-
Just meta.
|
|
209
|
-
|
|
210
|
-
## Root Cause Contract
|
|
211
|
-
|
|
212
|
-
Change: FIX-042-cache-stampede
|
|
213
|
-
Mode: investigation
|
|
214
|
-
Profile: standard
|
|
215
|
-
Diagnosis: cache stampede on cold start under parallel burst.
|
|
216
|
-
|
|
217
|
-
Symptom:
|
|
218
|
-
- p99 latency spike to 12s every deploy.
|
|
219
|
-
- Redis CPU saturates at 100% for ~20s.
|
|
220
|
-
|
|
221
|
-
Reproduction:
|
|
222
|
-
- Restart app with empty cache.
|
|
223
|
-
- Send 200 rps synthetic burst.
|
|
224
|
-
|
|
225
|
-
Expected:
|
|
226
|
-
- First miss population single-flight; subsequent callers await the in-flight promise.
|
|
227
|
-
|
|
228
|
-
Actual:
|
|
229
|
-
- N parallel callers all miss, all hit origin, all write back.
|
|
230
|
-
|
|
231
|
-
Root Cause:
|
|
232
|
-
- Missing in-flight promise map in \`lib/cache/remote.js\`.
|
|
233
|
-
|
|
234
|
-
Evidence Chain:
|
|
235
|
-
- flamegraph 2026-05-10 shows 184 origin calls within 50ms.
|
|
236
|
-
- \`remote.js:42\` unconditionally calls \`origin.fetch\` under a cache-miss branch.
|
|
237
|
-
|
|
238
|
-
Repair Boundary:
|
|
239
|
-
- Introduce in-flight map keyed by cache key; reject propagation errors to all awaiters.
|
|
240
|
-
- Do not touch eviction policy or TTL logic.
|
|
241
|
-
|
|
242
|
-
Verification:
|
|
243
|
-
|
|
244
|
-
\`\`\`bash
|
|
245
|
-
npm test -- lib/cache/remote.test.js
|
|
246
|
-
\`\`\`
|
|
247
|
-
|
|
248
|
-
Prevention:
|
|
249
|
-
- Add a regression test that fires 50 parallel callers with an empty cache and asserts origin.fetch called once.
|
|
250
|
-
|
|
251
|
-
Risk / Escalate If:
|
|
252
|
-
- If the in-flight map leaks on rejection, memory grows unbounded — escalate to owner.
|
|
253
|
-
- If fix adds >5% p50 latency, roll back.
|
|
254
|
-
|
|
255
|
-
## Repair Plan
|
|
256
|
-
|
|
257
|
-
Other content that must not leak into the Root Cause Contract block.
|
|
258
|
-
`;
|
|
259
|
-
|
|
260
|
-
const FIX_SAMPLE_WITHOUT_HEADING = `# FIX-000
|
|
261
|
-
|
|
262
|
-
## Plan Meta
|
|
263
|
-
|
|
264
|
-
Nothing here.
|
|
265
|
-
|
|
266
|
-
## Something Else
|
|
267
|
-
|
|
268
|
-
Unrelated.
|
|
269
|
-
`;
|
|
270
|
-
|
|
271
|
-
describe('extractTasksRootCauseContract', () => {
|
|
272
|
-
describe('when `## Root Cause Contract` heading is present', () => {
|
|
273
|
-
test('returns found:true', () => {
|
|
274
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
275
|
-
expect(result.found).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('content spans from the heading line up to (but not including) the next H2 heading', () => {
|
|
279
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
280
|
-
expect(result.content).toMatch(/^## Root Cause Contract/);
|
|
281
|
-
expect(result.content).not.toMatch(/## Repair Plan/);
|
|
282
|
-
expect(result.content).not.toMatch(/Other content that must not leak/);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('fields parses Change / Mode / Profile as single-line KVs', () => {
|
|
286
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
287
|
-
expect(result.fields.change).toBe('FIX-042-cache-stampede');
|
|
288
|
-
expect(result.fields.mode).toBe('investigation');
|
|
289
|
-
expect(result.fields.profile).toBe('standard');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test('fields parses Diagnosis as a single-line KV preserving punctuation', () => {
|
|
293
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
294
|
-
expect(result.fields.diagnosis).toBe('cache stampede on cold start under parallel burst.');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test('fields parses Symptom as a bullet list', () => {
|
|
298
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
299
|
-
expect(result.fields.symptom).toEqual([
|
|
300
|
-
'p99 latency spike to 12s every deploy.',
|
|
301
|
-
'Redis CPU saturates at 100% for ~20s.'
|
|
302
|
-
]);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test('fields parses Reproduction as a bullet list', () => {
|
|
306
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
307
|
-
expect(result.fields.reproduction).toEqual([
|
|
308
|
-
'Restart app with empty cache.',
|
|
309
|
-
'Send 200 rps synthetic burst.'
|
|
310
|
-
]);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test('fields parses Expected as a bullet list', () => {
|
|
314
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
315
|
-
expect(result.fields.expected).toEqual([
|
|
316
|
-
'First miss population single-flight; subsequent callers await the in-flight promise.'
|
|
317
|
-
]);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test('fields parses Actual as a bullet list', () => {
|
|
321
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
322
|
-
expect(result.fields.actual).toEqual([
|
|
323
|
-
'N parallel callers all miss, all hit origin, all write back.'
|
|
324
|
-
]);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('fields parses Root Cause as a bullet list preserving inline code spans', () => {
|
|
328
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
329
|
-
expect(result.fields.rootCause).toEqual([
|
|
330
|
-
'Missing in-flight promise map in `lib/cache/remote.js`.'
|
|
331
|
-
]);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('fields parses Evidence Chain as a bullet list', () => {
|
|
335
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
336
|
-
expect(result.fields.evidenceChain).toEqual([
|
|
337
|
-
'flamegraph 2026-05-10 shows 184 origin calls within 50ms.',
|
|
338
|
-
'`remote.js:42` unconditionally calls `origin.fetch` under a cache-miss branch.'
|
|
339
|
-
]);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test('fields parses Repair Boundary as a bullet list', () => {
|
|
343
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
344
|
-
expect(result.fields.repairBoundary).toEqual([
|
|
345
|
-
'Introduce in-flight map keyed by cache key; reject propagation errors to all awaiters.',
|
|
346
|
-
'Do not touch eviction policy or TTL logic.'
|
|
347
|
-
]);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test('fields parses Verification as a fenced command block', () => {
|
|
351
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
352
|
-
expect(result.fields.verification).toBe('npm test -- lib/cache/remote.test.js');
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('fields parses Prevention as a bullet list', () => {
|
|
356
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
357
|
-
expect(result.fields.prevention).toEqual([
|
|
358
|
-
'Add a regression test that fires 50 parallel callers with an empty cache and asserts origin.fetch called once.'
|
|
359
|
-
]);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
test('fields parses Risk / Escalate If as a bullet list', () => {
|
|
363
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
364
|
-
expect(result.fields.risk).toEqual([
|
|
365
|
-
'If the in-flight map leaks on rejection, memory grows unbounded — escalate to owner.',
|
|
366
|
-
'If fix adds >5% p50 latency, roll back.'
|
|
367
|
-
]);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('fields does not include keys from sections outside Root Cause Contract', () => {
|
|
371
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITH_HEADING);
|
|
372
|
-
expect(result.fields).not.toHaveProperty('repairPlan');
|
|
373
|
-
expect(result.fields).not.toHaveProperty('something');
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
describe('when `## Root Cause Contract` heading is absent', () => {
|
|
378
|
-
test('returns found:false with empty content and empty fields', () => {
|
|
379
|
-
const result = extractTasksRootCauseContract(FIX_SAMPLE_WITHOUT_HEADING);
|
|
380
|
-
expect(result.found).toBe(false);
|
|
381
|
-
expect(result.content).toBe('');
|
|
382
|
-
expect(result.fields).toEqual({});
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
describe('edge cases', () => {
|
|
387
|
-
test('is case-sensitive on the canonical heading spelling', () => {
|
|
388
|
-
const wrongCase = '## root cause contract\n\nChange: FIX-999\n\n## Next\n';
|
|
389
|
-
const result = extractTasksRootCauseContract(wrongCase);
|
|
390
|
-
expect(result.found).toBe(false);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
test('throws a TypeError when input is not a string', () => {
|
|
394
|
-
expect(() => extractTasksRootCauseContract(null)).toThrow(TypeError);
|
|
395
|
-
expect(() => extractTasksRootCauseContract(undefined)).toThrow(TypeError);
|
|
396
|
-
expect(() => extractTasksRootCauseContract({})).toThrow(TypeError);
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe('extractTasksContractSummary and extractTasksRootCauseContract share independent contracts', () => {
|
|
402
|
-
test('a document with only Contract Summary returns found:false for Root Cause Contract', () => {
|
|
403
|
-
const r = extractTasksRootCauseContract(SAMPLE_WITH_HEADING);
|
|
404
|
-
expect(r.found).toBe(false);
|
|
405
|
-
expect(r.fields).toEqual({});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test('a document with only Root Cause Contract returns found:false for Contract Summary', () => {
|
|
409
|
-
const r = extractTasksContractSummary(FIX_SAMPLE_WITH_HEADING);
|
|
410
|
-
expect(r.found).toBe(false);
|
|
411
|
-
expect(r.fields).toEqual({});
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
describe('task-contract compile', () => {
|
|
416
|
-
let repoRoot;
|
|
417
|
-
|
|
418
|
-
beforeEach(() => {
|
|
419
|
-
repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-task-contract-'));
|
|
420
|
-
fs.writeFileSync(path.join(repoRoot, 'package.json'), '{"name":"tmp"}\n');
|
|
421
|
-
fs.mkdirSync(path.join(repoRoot, '.git'));
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
afterEach(() => {
|
|
425
|
-
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test('writes manifest whose metadata.source equals tasks.md', async () => {
|
|
429
|
-
const changeId = 'REQ-777';
|
|
430
|
-
const changeKey = 'REQ-777-compile-manifest';
|
|
431
|
-
const planningDir = path.join(repoRoot, 'devflow', 'changes', changeKey, 'planning');
|
|
432
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
433
|
-
fs.writeFileSync(
|
|
434
|
-
path.join(planningDir, 'tasks.md'),
|
|
435
|
-
[
|
|
436
|
-
'# TASKS',
|
|
437
|
-
'',
|
|
438
|
-
'## Contract Summary',
|
|
439
|
-
'',
|
|
440
|
-
'Change: REQ-777-compile-manifest',
|
|
441
|
-
'Mode: plan',
|
|
442
|
-
'Profile: standard',
|
|
443
|
-
'Approval: approved',
|
|
444
|
-
'',
|
|
445
|
-
'Goal:',
|
|
446
|
-
'- Compile manifest from tasks.md.',
|
|
447
|
-
'',
|
|
448
|
-
'Do Not Do:',
|
|
449
|
-
'- Do not add validate or migrate behavior.',
|
|
450
|
-
'',
|
|
451
|
-
'Approved Direction:',
|
|
452
|
-
'- Reuse planner.createTaskManifest.',
|
|
453
|
-
'',
|
|
454
|
-
'Acceptance:',
|
|
455
|
-
'- Manifest metadata.source is tasks.md.',
|
|
456
|
-
'',
|
|
457
|
-
'Verification:',
|
|
458
|
-
'',
|
|
459
|
-
'```bash',
|
|
460
|
-
'npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
461
|
-
'```',
|
|
462
|
-
'',
|
|
463
|
-
'Risk / Escalate If:',
|
|
464
|
-
'- Missing tasks.md should exit 3.',
|
|
465
|
-
'',
|
|
466
|
-
'## Phase 1: Compile',
|
|
467
|
-
'',
|
|
468
|
-
'- [ ] T001 compile manifest',
|
|
469
|
-
' Goal: Write task-manifest.json from tasks.md.',
|
|
470
|
-
' Files: `lib/skill-runtime/operations/task-contract.js`',
|
|
471
|
-
' Verification: npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
472
|
-
' Evidence: manifest metadata assertions',
|
|
473
|
-
''
|
|
474
|
-
].join('\n')
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
const result = await runCompile({ repoRoot, changeId, changeKey });
|
|
478
|
-
const manifestPath = path.join(planningDir, 'task-manifest.json');
|
|
479
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
480
|
-
|
|
481
|
-
expect(result).toEqual(expect.objectContaining({
|
|
482
|
-
code: 0,
|
|
483
|
-
changeId,
|
|
484
|
-
changeKey,
|
|
485
|
-
manifestPath
|
|
486
|
-
}));
|
|
487
|
-
expect(manifest.metadata.source).toBe('tasks.md');
|
|
488
|
-
expect(manifest.metadata.generatedBy).toBe('cc-devflow task-contract');
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test('returns exit code 3 when planning/tasks.md is missing', async () => {
|
|
492
|
-
const changeId = 'REQ-778';
|
|
493
|
-
const changeKey = 'REQ-778-missing-tasks';
|
|
494
|
-
const planningDir = path.join(repoRoot, 'devflow', 'changes', changeKey, 'planning');
|
|
495
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
496
|
-
|
|
497
|
-
const result = await runCompile({ repoRoot, changeId, changeKey });
|
|
498
|
-
|
|
499
|
-
expect(result.code).toBe(3);
|
|
500
|
-
expect(result.error).toMatch(/Missing planning\/tasks\.md/);
|
|
501
|
-
expect(fs.existsSync(path.join(planningDir, 'task-manifest.json'))).toBe(false);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test('extracts goal and acceptance into change-meta.json', async () => {
|
|
505
|
-
const changeId = 'REQ-780';
|
|
506
|
-
const changeKey = 'REQ-780-compile-change-meta';
|
|
507
|
-
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
508
|
-
const planningDir = path.join(changeDir, 'planning');
|
|
509
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
510
|
-
fs.writeFileSync(
|
|
511
|
-
path.join(planningDir, 'tasks.md'),
|
|
512
|
-
[
|
|
513
|
-
'# TASKS',
|
|
514
|
-
'',
|
|
515
|
-
'## Contract Summary',
|
|
516
|
-
'',
|
|
517
|
-
'Change: REQ-780-compile-change-meta',
|
|
518
|
-
'Mode: plan',
|
|
519
|
-
'Profile: standard',
|
|
520
|
-
'Approval: approved',
|
|
521
|
-
'',
|
|
522
|
-
'Goal:',
|
|
523
|
-
'- Generate change metadata from the contract.',
|
|
524
|
-
'- Keep the manifest writer unchanged.',
|
|
525
|
-
'',
|
|
526
|
-
'Do Not Do:',
|
|
527
|
-
'- Do not add validate or migrate behavior.',
|
|
528
|
-
'',
|
|
529
|
-
'Approved Direction:',
|
|
530
|
-
'- The Contract Summary is the human-authored source.',
|
|
531
|
-
'',
|
|
532
|
-
'Acceptance:',
|
|
533
|
-
'- change-meta.json contains goal items.',
|
|
534
|
-
'- change-meta.json records specReference.',
|
|
535
|
-
'',
|
|
536
|
-
'Verification:',
|
|
537
|
-
'',
|
|
538
|
-
'```bash',
|
|
539
|
-
'npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
540
|
-
'```',
|
|
541
|
-
'',
|
|
542
|
-
'Risk / Escalate If:',
|
|
543
|
-
'- Missing Contract Summary exits 3.',
|
|
544
|
-
'',
|
|
545
|
-
'## Phase 1: Compile',
|
|
546
|
-
'',
|
|
547
|
-
'- [ ] T001 compile metadata',
|
|
548
|
-
' Goal: Write change-meta.json from tasks.md.',
|
|
549
|
-
' Files: `lib/skill-runtime/operations/task-contract.js`',
|
|
550
|
-
' Verification: npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
551
|
-
' Evidence: change-meta.json field assertions',
|
|
552
|
-
''
|
|
553
|
-
].join('\n')
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
const result = await runCompile({ repoRoot, changeId, changeKey });
|
|
557
|
-
const changeMeta = JSON.parse(fs.readFileSync(path.join(changeDir, 'change-meta.json'), 'utf8'));
|
|
558
|
-
|
|
559
|
-
expect(result.code).toBe(0);
|
|
560
|
-
expect(changeMeta.goal).toEqual([
|
|
561
|
-
'Generate change metadata from the contract.',
|
|
562
|
-
'Keep the manifest writer unchanged.'
|
|
563
|
-
]);
|
|
564
|
-
expect(changeMeta.acceptance).toEqual([
|
|
565
|
-
'change-meta.json contains goal items.',
|
|
566
|
-
'change-meta.json records specReference.'
|
|
567
|
-
]);
|
|
568
|
-
expect(changeMeta.specReference).toEqual({
|
|
569
|
-
source: 'planning/tasks.md#contract-summary',
|
|
570
|
-
change: 'REQ-780-compile-change-meta',
|
|
571
|
-
mode: 'plan',
|
|
572
|
-
profile: 'standard',
|
|
573
|
-
approval: 'approved'
|
|
574
|
-
});
|
|
575
|
-
expect(changeMeta._meta.generatedBy).toBe('cc-devflow task-contract');
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
test('compiles Root Cause Contract into generated manifest and change-meta', async () => {
|
|
579
|
-
const changeId = 'FIX-781';
|
|
580
|
-
const changeKey = 'FIX-781-compile-root-cause';
|
|
581
|
-
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
582
|
-
const planningDir = path.join(changeDir, 'planning');
|
|
583
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
584
|
-
fs.writeFileSync(
|
|
585
|
-
path.join(planningDir, 'tasks.md'),
|
|
586
|
-
[
|
|
587
|
-
'# TASKS',
|
|
588
|
-
'',
|
|
589
|
-
'## Root Cause Contract',
|
|
590
|
-
'',
|
|
591
|
-
'Change: FIX-781-compile-root-cause',
|
|
592
|
-
'Mode: investigation',
|
|
593
|
-
'Profile: standard',
|
|
594
|
-
'Diagnosis: confirmed',
|
|
595
|
-
'',
|
|
596
|
-
'Symptom:',
|
|
597
|
-
'- Task-contract compile rejects bug investigations.',
|
|
598
|
-
'',
|
|
599
|
-
'Reproduction:',
|
|
600
|
-
'- Run compile against a Root Cause Contract-only tasks.md.',
|
|
601
|
-
'',
|
|
602
|
-
'Expected:',
|
|
603
|
-
'- CLI owns task-manifest.json and change-meta.json.',
|
|
604
|
-
'',
|
|
605
|
-
'Actual:',
|
|
606
|
-
'- Compile required Contract Summary.',
|
|
607
|
-
'',
|
|
608
|
-
'Root Cause:',
|
|
609
|
-
'- The compile path reused only the planning contract parser.',
|
|
610
|
-
'',
|
|
611
|
-
'Evidence Chain:',
|
|
612
|
-
'- validate already accepts Root Cause Contract.',
|
|
613
|
-
'',
|
|
614
|
-
'Repair Boundary:',
|
|
615
|
-
'- Accept Root Cause Contract as the human-authored source.',
|
|
616
|
-
'',
|
|
617
|
-
'Verification:',
|
|
618
|
-
'',
|
|
619
|
-
'```bash',
|
|
620
|
-
'npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
621
|
-
'```',
|
|
622
|
-
'',
|
|
623
|
-
'Prevention:',
|
|
624
|
-
'- Keep generated machine records behind task-contract compile.',
|
|
625
|
-
'',
|
|
626
|
-
'Risk / Escalate If:',
|
|
627
|
-
'- Generated metadata loses the root-cause source.',
|
|
628
|
-
'',
|
|
629
|
-
'## Phase 1: Compile',
|
|
630
|
-
'',
|
|
631
|
-
'- [ ] T001 compile root-cause metadata',
|
|
632
|
-
' Goal: Generate machine artifacts from the root-cause contract.',
|
|
633
|
-
' Files: `lib/skill-runtime/operations/task-contract.js`',
|
|
634
|
-
' Verification: npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
635
|
-
' Evidence: generated metadata assertions',
|
|
636
|
-
''
|
|
637
|
-
].join('\n')
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
const result = await runCompile({ repoRoot, changeId, changeKey });
|
|
641
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(planningDir, 'task-manifest.json'), 'utf8'));
|
|
642
|
-
const changeMeta = JSON.parse(fs.readFileSync(path.join(changeDir, 'change-meta.json'), 'utf8'));
|
|
643
|
-
|
|
644
|
-
expect(result.code).toBe(0);
|
|
645
|
-
expect(manifest.goal).toBe('The compile path reused only the planning contract parser.');
|
|
646
|
-
expect(manifest.metadata.generatedBy).toBe('cc-devflow task-contract');
|
|
647
|
-
expect(changeMeta.specReference.source).toBe('planning/tasks.md#root-cause-contract');
|
|
648
|
-
expect(changeMeta.rootCause.confirmed).toEqual([
|
|
649
|
-
'The compile path reused only the planning contract parser.'
|
|
650
|
-
]);
|
|
651
|
-
expect(changeMeta.acceptance).toEqual([
|
|
652
|
-
'Accept Root Cause Contract as the human-authored source.',
|
|
653
|
-
'npm test -- lib/skill-runtime/__tests__/task-contract.test.js'
|
|
654
|
-
]);
|
|
655
|
-
expect(changeMeta._meta.generatedBy).toBe('cc-devflow task-contract');
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
test('returns exit code 3 when Contract Summary is missing', async () => {
|
|
659
|
-
const changeId = 'REQ-781';
|
|
660
|
-
const changeKey = 'REQ-781-missing-contract-summary';
|
|
661
|
-
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
662
|
-
const planningDir = path.join(changeDir, 'planning');
|
|
663
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
664
|
-
fs.writeFileSync(
|
|
665
|
-
path.join(planningDir, 'tasks.md'),
|
|
666
|
-
[
|
|
667
|
-
'# TASKS',
|
|
668
|
-
'',
|
|
669
|
-
'## Phase 1: Compile',
|
|
670
|
-
'',
|
|
671
|
-
'- [ ] T001 compile metadata',
|
|
672
|
-
' Goal: Write nothing when Contract Summary is absent.',
|
|
673
|
-
' Files: `lib/skill-runtime/operations/task-contract.js`',
|
|
674
|
-
' Verification: npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
675
|
-
' Evidence: exit code assertion',
|
|
676
|
-
''
|
|
677
|
-
].join('\n')
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
const result = await runCompile({ repoRoot, changeId, changeKey });
|
|
681
|
-
|
|
682
|
-
expect(result.code).toBe(3);
|
|
683
|
-
expect(result.error).toMatch(/Missing ## Contract Summary or ## Root Cause Contract/);
|
|
684
|
-
expect(fs.existsSync(path.join(changeDir, 'change-meta.json'))).toBe(false);
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
describe('task-contract validate', () => {
|
|
689
|
-
let repoRoot;
|
|
690
|
-
|
|
691
|
-
beforeEach(() => {
|
|
692
|
-
repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-devflow-task-validate-'));
|
|
693
|
-
fs.writeFileSync(path.join(repoRoot, 'package.json'), '{"name":"tmp"}\n');
|
|
694
|
-
fs.mkdirSync(path.join(repoRoot, '.git'));
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
afterEach(() => {
|
|
698
|
-
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
function tasksMarkdown(changeKey, options = {}) {
|
|
702
|
-
const {
|
|
703
|
-
profile = 'standard',
|
|
704
|
-
includeContract = true,
|
|
705
|
-
taskCount = 1,
|
|
706
|
-
extraBody = ''
|
|
707
|
-
} = options;
|
|
708
|
-
const contract = includeContract
|
|
709
|
-
? [
|
|
710
|
-
'## Contract Summary',
|
|
711
|
-
'',
|
|
712
|
-
`Change: ${changeKey}`,
|
|
713
|
-
'Mode: plan',
|
|
714
|
-
`Profile: ${profile}`,
|
|
715
|
-
'Approval: approved',
|
|
716
|
-
'',
|
|
717
|
-
'Goal:',
|
|
718
|
-
'- Validate artifact contract.',
|
|
719
|
-
'',
|
|
720
|
-
'Do Not Do:',
|
|
721
|
-
'- Do not add migrate behavior.',
|
|
722
|
-
'',
|
|
723
|
-
'Approved Direction:',
|
|
724
|
-
'- Validate with a rule table.',
|
|
725
|
-
'',
|
|
726
|
-
'Acceptance:',
|
|
727
|
-
'- Validate exits with rule-specific codes.',
|
|
728
|
-
'',
|
|
729
|
-
'Verification:',
|
|
730
|
-
'',
|
|
731
|
-
'```bash',
|
|
732
|
-
'npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
733
|
-
'```',
|
|
734
|
-
'',
|
|
735
|
-
'Risk / Escalate If:',
|
|
736
|
-
'- Budget overflow exits 6.',
|
|
737
|
-
''
|
|
738
|
-
]
|
|
739
|
-
: [];
|
|
740
|
-
const tasks = Array.from({ length: taskCount }, (_, index) => [
|
|
741
|
-
`- [ ] T${String(index + 1).padStart(3, '0')} validate slice ${index + 1}`,
|
|
742
|
-
' Goal: Keep validation deterministic.',
|
|
743
|
-
' Files: `lib/skill-runtime/operations/task-contract.js`',
|
|
744
|
-
' Verification: npm test -- lib/skill-runtime/__tests__/task-contract.test.js',
|
|
745
|
-
' Evidence: exit code assertion',
|
|
746
|
-
` Vertical slice: Slice ${index + 1}`,
|
|
747
|
-
''
|
|
748
|
-
].join('\n'));
|
|
749
|
-
return ['# TASKS', '', ...contract, extraBody, '## Phase 1: Validate', '', ...tasks].join('\n');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async function seedCompiledChange(changeId, changeKey, options = {}) {
|
|
753
|
-
const changeDir = path.join(repoRoot, 'devflow', 'changes', changeKey);
|
|
754
|
-
const planningDir = path.join(changeDir, 'planning');
|
|
755
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
756
|
-
fs.writeFileSync(path.join(planningDir, 'tasks.md'), tasksMarkdown(changeKey, options));
|
|
757
|
-
const compileResult = await runCompile({ repoRoot, changeId, changeKey });
|
|
758
|
-
expect(compileResult.code).toBe(0);
|
|
759
|
-
return { changeDir, planningDir };
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const ruleCases = [
|
|
763
|
-
['C1', 2, async (ctx) => fs.writeFileSync(path.join(ctx.planningDir, 'design.md'), '# legacy design\n')],
|
|
764
|
-
['C2', 2, async (ctx) => fs.writeFileSync(path.join(ctx.planningDir, 'analysis.md'), '# legacy analysis\n')],
|
|
765
|
-
['C3', 2, async (ctx) => {
|
|
766
|
-
const reviewDir = path.join(ctx.changeDir, 'review');
|
|
767
|
-
fs.mkdirSync(reviewDir, { recursive: true });
|
|
768
|
-
fs.writeFileSync(path.join(reviewDir, 'cc-review-report.md'), '# legacy review report\n');
|
|
769
|
-
}],
|
|
770
|
-
['C5', 3, async (ctx) => {
|
|
771
|
-
const manifestPath = path.join(ctx.planningDir, 'task-manifest.json');
|
|
772
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
773
|
-
manifest.metadata.generatedBy = 'manual';
|
|
774
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
775
|
-
}],
|
|
776
|
-
['C6', 3, async (ctx) => {
|
|
777
|
-
const metaPath = path.join(ctx.changeDir, 'change-meta.json');
|
|
778
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
779
|
-
meta._meta.generatedBy = 'manual';
|
|
780
|
-
fs.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`);
|
|
781
|
-
}],
|
|
782
|
-
['C8', 5, async (ctx) => {
|
|
783
|
-
const reviewDir = path.join(ctx.changeDir, 'review');
|
|
784
|
-
fs.mkdirSync(reviewDir, { recursive: true });
|
|
785
|
-
fs.writeFileSync(path.join(reviewDir, 'review-ledger.jsonl'), '{"event":"review-started"}\n');
|
|
786
|
-
}],
|
|
787
|
-
['C9', 5, async (ctx) => {
|
|
788
|
-
const reviewDir = path.join(ctx.changeDir, 'review');
|
|
789
|
-
fs.mkdirSync(reviewDir, { recursive: true });
|
|
790
|
-
fs.writeFileSync(path.join(reviewDir, 'report-card.json'), '{"overall":"pass"}\n');
|
|
791
|
-
}]
|
|
792
|
-
];
|
|
793
|
-
|
|
794
|
-
test.each(ruleCases)('%s returns exit code %i', async (ruleId, exitCode, mutate) => {
|
|
795
|
-
const changeId = `REQ-${ruleId.slice(1).padStart(3, '0')}`;
|
|
796
|
-
const changeKey = `${changeId.toLowerCase()}-validate-${ruleId.toLowerCase()}`.toUpperCase().replace('_', '-');
|
|
797
|
-
const ctx = await seedCompiledChange(changeId, changeKey);
|
|
798
|
-
await mutate(ctx);
|
|
799
|
-
|
|
800
|
-
const result = await runValidate({ repoRoot, changeId, changeKey });
|
|
801
|
-
|
|
802
|
-
expect(result.code).toBe(exitCode);
|
|
803
|
-
expect(result.stderr).toContain(ruleId);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
test('returns exit code 3 for C4 when Contract Summary is missing', async () => {
|
|
807
|
-
const changeId = 'REQ-704';
|
|
808
|
-
const changeKey = 'REQ-704-missing-contract';
|
|
809
|
-
const planningDir = path.join(repoRoot, 'devflow', 'changes', changeKey, 'planning');
|
|
810
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
811
|
-
fs.writeFileSync(path.join(planningDir, 'tasks.md'), tasksMarkdown(changeKey, { includeContract: false }));
|
|
812
|
-
|
|
813
|
-
const result = await runValidate({ repoRoot, changeId, changeKey });
|
|
814
|
-
|
|
815
|
-
expect(result.code).toBe(3);
|
|
816
|
-
expect(result.stderr).toContain('C4');
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test('returns exit code 4 for C7 when tiny profile has more than one tracer slice', async () => {
|
|
820
|
-
const changeId = 'REQ-707';
|
|
821
|
-
const changeKey = 'REQ-707-tiny-over-sliced';
|
|
822
|
-
await seedCompiledChange(changeId, changeKey, { profile: 'tiny', taskCount: 2 });
|
|
823
|
-
|
|
824
|
-
const result = await runValidate({ repoRoot, changeId, changeKey });
|
|
825
|
-
|
|
826
|
-
expect(result.code).toBe(4);
|
|
827
|
-
expect(result.stderr).toContain('C7');
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
test('returns exit code 6 for C10 when tasks.md exceeds the profile budget', async () => {
|
|
831
|
-
const changeId = 'REQ-710';
|
|
832
|
-
const changeKey = 'REQ-710-budget-overflow';
|
|
833
|
-
await seedCompiledChange(changeId, changeKey, {
|
|
834
|
-
profile: 'standard',
|
|
835
|
-
extraBody: 'x'.repeat(12000)
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
const result = await runValidate({ repoRoot, changeId, changeKey });
|
|
839
|
-
|
|
840
|
-
expect(result.code).toBe(6);
|
|
841
|
-
expect(result.stderr).toContain('C10');
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test('passes a valid compiled task contract', async () => {
|
|
845
|
-
const changeId = 'REQ-799';
|
|
846
|
-
const changeKey = 'REQ-799-valid-contract';
|
|
847
|
-
await seedCompiledChange(changeId, changeKey);
|
|
848
|
-
|
|
849
|
-
const result = await runValidate({ repoRoot, changeId, changeKey });
|
|
850
|
-
|
|
851
|
-
expect(result).toEqual(expect.objectContaining({
|
|
852
|
-
code: 0,
|
|
853
|
-
ok: true,
|
|
854
|
-
violations: []
|
|
855
|
-
}));
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
test('CLI validate failure prints the violated rule id', async () => {
|
|
859
|
-
const changeId = 'REQ-711';
|
|
860
|
-
const changeKey = 'REQ-711-cli-rule-output';
|
|
861
|
-
const { planningDir } = await seedCompiledChange(changeId, changeKey);
|
|
862
|
-
fs.writeFileSync(path.join(planningDir, 'design.md'), '# legacy design\n');
|
|
863
|
-
|
|
864
|
-
const result = spawnSync(
|
|
865
|
-
process.execPath,
|
|
866
|
-
[CLI_BIN, 'task-contract', 'validate', '--cwd', repoRoot, '--change', changeId, '--change-key', changeKey],
|
|
867
|
-
{ encoding: 'utf8' }
|
|
868
|
-
);
|
|
869
|
-
|
|
870
|
-
expect(result.status).toBe(2);
|
|
871
|
-
expect(result.stderr).toContain('C1');
|
|
872
|
-
expect(result.stderr).toContain('planning/design.md');
|
|
873
|
-
});
|
|
874
|
-
});
|