cc-devflow 4.5.8 → 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 +33 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +9 -4
- package/.claude/skills/cc-act/SKILL.md +73 -12
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_INDEX_TEMPLATE.md +30 -0
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_PRINCIPLES_TEMPLATE.md +29 -0
- package/.claude/skills/cc-act/assets/PROJECT_POSTMORTEM_TEMPLATE.md +103 -0
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +61 -5
- package/.claude/skills/cc-act/references/closure-contract.md +4 -1
- package/.claude/skills/cc-act/references/git-commit-guidelines.md +342 -37
- package/.claude/skills/cc-act/scripts/cc-act-common.sh +29 -1
- package/.claude/skills/cc-act/scripts/render-pr-brief.sh +164 -0
- package/.claude/skills/cc-act/scripts/sync-act-docs.sh +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 +23 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +7 -7
- package/.claude/skills/cc-do/SKILL.md +49 -45
- package/.claude/skills/cc-do/references/execution-recovery.md +18 -13
- package/.claude/skills/cc-do/scripts/build-task-context.sh +13 -22
- package/.claude/skills/cc-do/scripts/mark-task-complete.sh +0 -6
- 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 +34 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +21 -5
- package/.claude/skills/cc-investigate/SKILL.md +97 -40
- package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +66 -4
- package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +30 -59
- package/.claude/skills/cc-investigate/assets/{ANALYSIS_TEMPLATE.md → legacy/ANALYSIS_TEMPLATE.md} +48 -0
- package/.claude/skills/cc-investigate/references/investigation-contract.md +16 -2
- package/.claude/skills/cc-investigate/scripts/bootstrap-analysis.sh +1 -1
- package/.claude/skills/cc-next/CHANGELOG.md +6 -0
- package/.claude/skills/cc-next/PLAYBOOK.md +26 -4
- package/.claude/skills/cc-next/SKILL.md +39 -4
- package/.claude/skills/cc-plan/CHANGELOG.md +38 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +60 -53
- package/.claude/skills/cc-plan/SKILL.md +164 -87
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +101 -9
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +58 -229
- package/.claude/skills/cc-plan/assets/{DESIGN_TEMPLATE.md → legacy/DESIGN_TEMPLATE.md} +68 -0
- package/.claude/skills/cc-plan/assets/{TINY_DESIGN_TEMPLATE.md → legacy/TINY_DESIGN_TEMPLATE.md} +47 -1
- package/.claude/skills/cc-plan/references/planning-contract.md +48 -33
- 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/.claude/skills/cc-roadmap/CHANGELOG.md +6 -0
- package/.claude/skills/cc-roadmap/PLAYBOOK.md +30 -0
- package/.claude/skills/cc-roadmap/SKILL.md +45 -8
- package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +8 -0
- package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +22 -0
- package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +32 -1
- package/.claude/skills/cc-roadmap/references/roadmap-dialogue.md +14 -14
- package/CHANGELOG.md +28 -0
- package/CONTRIBUTING.md +40 -4
- package/CONTRIBUTING.zh-CN.md +40 -4
- package/README.md +57 -43
- package/README.zh-CN.md +57 -43
- package/bin/cc-devflow-cli.js +293 -36
- package/docs/examples/START-HERE.md +5 -4
- package/docs/examples/example-bindings.json +10 -10
- package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
- package/docs/examples/full-design-blocked/README.md +2 -2
- package/docs/examples/full-design-blocked/ROADMAP.md +1 -1
- 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 +29 -312
- 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/full-design-blocked/roadmap.json +1 -1
- package/docs/examples/local-handoff/BACKLOG.md +1 -1
- package/docs/examples/local-handoff/README.md +2 -2
- package/docs/examples/local-handoff/ROADMAP.md +1 -1
- 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 +27 -210
- 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/local-handoff/roadmap.json +1 -1
- package/docs/examples/pdca-loop/BACKLOG.md +1 -1
- package/docs/examples/pdca-loop/README.md +2 -2
- package/docs/examples/pdca-loop/ROADMAP.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +65 -1
- 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 +26 -228
- 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/pdca-loop/roadmap.json +1 -1
- package/docs/examples/scripts/check-example-bindings.sh +11 -5
- package/docs/get-shit-done-strategy-audit.md +22 -22
- package/docs/guides/artifact-contract.md +44 -0
- 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/docs/guides/project-postmortem.md +78 -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 +63 -2
- 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 +76 -2
- 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 +25 -13
- package/lib/skill-runtime/query-registry.js +2 -2
- package/lib/skill-runtime/query.js +16 -3
- package/lib/skill-runtime/review-records.js +123 -0
- package/lib/skill-runtime/review.js +246 -11
- package/lib/skill-runtime/schemas.js +179 -15
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [INPUT]: 依赖 store 读取 tasks.md
|
|
2
|
+
* [INPUT]: 依赖 store 读取 tasks.md 与输出路径,接收 changeId/changeKey,依赖 schemas 校验 manifest。
|
|
3
3
|
* [OUTPUT]: 对外提供 tasks.md → task-manifest.json 的解析与生成能力。
|
|
4
4
|
* [POS]: skill runtime 计划编排层,被 operations/plan 直接调用。
|
|
5
5
|
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
getTaskManifestPath,
|
|
15
15
|
getTasksMarkdownPath
|
|
16
16
|
} = require('./store');
|
|
17
|
+
const path = require('path');
|
|
17
18
|
const { parseManifest } = require('./schemas');
|
|
18
19
|
const { isTaskCompletedStatus } = require('./lifecycle');
|
|
19
20
|
|
|
@@ -130,12 +131,21 @@ function dedupeList(values) {
|
|
|
130
131
|
return [...new Set(values.filter(Boolean))];
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function pushMissing(target, values) {
|
|
135
|
+
for (const value of values) {
|
|
136
|
+
if (value && !target.includes(value)) {
|
|
137
|
+
target.push(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
function pickPrimaryTestTarget(task) {
|
|
134
143
|
return [...(task.files || []), ...(task.touches || [])].find((item) => /\.test\./i.test(item)) || '';
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
function enrichTaskMetadata(task) {
|
|
146
|
+
function enrichTaskMetadata(task, options = {}) {
|
|
138
147
|
const primaryTestTarget = pickPrimaryTestTarget(task);
|
|
148
|
+
const defaultReadFiles = options.defaultReadFiles || ['design.md'];
|
|
139
149
|
|
|
140
150
|
if (task.acceptance.length === 0) {
|
|
141
151
|
if (task.type === 'TEST') {
|
|
@@ -162,9 +172,9 @@ function enrichTaskMetadata(task) {
|
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
if (task.context.readFiles.length === 0) {
|
|
165
|
-
task.context.readFiles
|
|
175
|
+
pushMissing(task.context.readFiles, defaultReadFiles);
|
|
166
176
|
if (task.type === 'TEST') {
|
|
167
|
-
task.context.readFiles
|
|
177
|
+
pushMissing(task.context.readFiles, ['tasks.md']);
|
|
168
178
|
}
|
|
169
179
|
if (primaryTestTarget && !task.context.readFiles.includes(primaryTestTarget)) {
|
|
170
180
|
task.context.readFiles.push(primaryTestTarget);
|
|
@@ -192,7 +202,7 @@ function parseDependsOn(rawTail, fallbackDependsOn, isParallel) {
|
|
|
192
202
|
return [fallbackDependsOn];
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
function parseTasksMarkdown(content) {
|
|
205
|
+
function parseTasksMarkdown(content, options = {}) {
|
|
196
206
|
const lines = content.split(/\r?\n/);
|
|
197
207
|
const tasks = [];
|
|
198
208
|
let previousTaskId = null;
|
|
@@ -213,7 +223,7 @@ function parseTasksMarkdown(content) {
|
|
|
213
223
|
task.context.readFiles = dedupeList(task.context.readFiles);
|
|
214
224
|
task.context.commands = dedupeList(task.context.commands);
|
|
215
225
|
task.context.notes = dedupeList(task.context.notes);
|
|
216
|
-
enrichTaskMetadata(task);
|
|
226
|
+
enrichTaskMetadata(task, options);
|
|
217
227
|
task.acceptance = dedupeList(task.acceptance);
|
|
218
228
|
task.verification = dedupeList(task.verification);
|
|
219
229
|
task.evidence = dedupeList(task.evidence);
|
|
@@ -480,31 +490,33 @@ function deriveManifestExecutionState(tasks) {
|
|
|
480
490
|
function applyManifestExecutionState(manifest, updatedAt = nowIso()) {
|
|
481
491
|
const executionState = deriveManifestExecutionState(manifest.tasks || []);
|
|
482
492
|
manifest.currentTaskId = executionState.currentTaskId;
|
|
483
|
-
manifest.activePhase
|
|
493
|
+
delete manifest.activePhase;
|
|
484
494
|
manifest.updatedAt = updatedAt;
|
|
485
495
|
return manifest;
|
|
486
496
|
}
|
|
487
497
|
|
|
488
|
-
async function createTaskManifest({ repoRoot, changeId, goal, overwrite = false }) {
|
|
489
|
-
const
|
|
498
|
+
async function createTaskManifest({ repoRoot, changeId, changeKey, goal, overwrite = false }) {
|
|
499
|
+
const pathOptions = changeKey ? { changeKey } : {};
|
|
500
|
+
const manifestPath = getTaskManifestPath(repoRoot, changeId, pathOptions);
|
|
490
501
|
const previous = await readJson(manifestPath, null);
|
|
491
502
|
|
|
492
503
|
if (!overwrite && (await exists(manifestPath))) {
|
|
493
504
|
return parseManifest(previous);
|
|
494
505
|
}
|
|
495
506
|
|
|
496
|
-
const tasksPath = getTasksMarkdownPath(repoRoot, changeId);
|
|
507
|
+
const tasksPath = getTasksMarkdownPath(repoRoot, changeId, pathOptions);
|
|
497
508
|
const hasTasksFile = await exists(tasksPath);
|
|
498
|
-
const
|
|
509
|
+
const legacyDesignPath = path.join(path.dirname(tasksPath), 'design.md');
|
|
510
|
+
const defaultReadFiles = await exists(legacyDesignPath) ? ['design.md'] : ['tasks.md'];
|
|
511
|
+
const rawTasks = hasTasksFile ? parseTasksMarkdown(await readText(tasksPath), { defaultReadFiles }) : [];
|
|
499
512
|
const tasks = rawTasks.length > 0 ? rawTasks : buildDefaultTasks(changeId);
|
|
500
513
|
const previousPlanVersion = previous?.metadata?.planVersion || 0;
|
|
501
514
|
const manifest = applyManifestExecutionState({
|
|
502
515
|
changeId,
|
|
503
|
-
goal: goal || `Deliver ${changeId} safely with
|
|
516
|
+
goal: goal || `Deliver ${changeId} safely with task-state truth.`,
|
|
504
517
|
createdAt: previous?.createdAt || nowIso(),
|
|
505
518
|
updatedAt: nowIso(),
|
|
506
519
|
currentTaskId: null,
|
|
507
|
-
activePhase: null,
|
|
508
520
|
tasks,
|
|
509
521
|
metadata: {
|
|
510
522
|
source: hasTasksFile ? 'tasks.md' : 'default',
|
|
@@ -59,7 +59,7 @@ function createQueryRegistry(entries) {
|
|
|
59
59
|
`Missing required query artifact: ${missingRefs.join(', ')}`,
|
|
60
60
|
{
|
|
61
61
|
artifactRefs: missingRefs,
|
|
62
|
-
rescueAction: 'create required
|
|
62
|
+
rescueAction: 'create required workflow artifacts before running this query'
|
|
63
63
|
}
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -84,7 +84,7 @@ function createQueryRegistry(entries) {
|
|
|
84
84
|
event: `query.${queryId}.failed`,
|
|
85
85
|
changeId: context.changeId,
|
|
86
86
|
artifactRefs,
|
|
87
|
-
nextAction: error.rescueAction || 'inspect-
|
|
87
|
+
nextAction: error.rescueAction || 'inspect-workflow-artifacts'
|
|
88
88
|
})
|
|
89
89
|
};
|
|
90
90
|
}
|
|
@@ -23,6 +23,11 @@ const {
|
|
|
23
23
|
const { createQueryRegistry } = require('./query-registry');
|
|
24
24
|
const { namedError } = require('./errors');
|
|
25
25
|
const { deriveShipReadiness } = require('./readiness');
|
|
26
|
+
const {
|
|
27
|
+
getWorkflowContext,
|
|
28
|
+
getWorkflowContextArtifactRefs,
|
|
29
|
+
getWorkflowContextRequiredArtifactRefs
|
|
30
|
+
} = require('./workflow-context');
|
|
26
31
|
|
|
27
32
|
async function readQueryArtifact(filePath, { required = true } = {}) {
|
|
28
33
|
try {
|
|
@@ -34,7 +39,7 @@ async function readQueryArtifact(filePath, { required = true } = {}) {
|
|
|
34
39
|
`Missing required query artifact: ${filePath}`,
|
|
35
40
|
{
|
|
36
41
|
artifactRefs: [filePath],
|
|
37
|
-
rescueAction: 'create required
|
|
42
|
+
rescueAction: 'create required workflow artifacts before running this query'
|
|
38
43
|
}
|
|
39
44
|
);
|
|
40
45
|
}
|
|
@@ -50,7 +55,7 @@ async function readQueryArtifact(filePath, { required = true } = {}) {
|
|
|
50
55
|
`Invalid query artifact ${filePath}: ${error.message}`,
|
|
51
56
|
{
|
|
52
57
|
artifactRefs: [filePath],
|
|
53
|
-
rescueAction: 'repair or regenerate the invalid
|
|
58
|
+
rescueAction: 'repair or regenerate the invalid workflow artifact before running this query',
|
|
54
59
|
details: {
|
|
55
60
|
cause: error.name || 'Error'
|
|
56
61
|
}
|
|
@@ -81,7 +86,7 @@ async function getNextTask(repoRoot, changeId, options = {}) {
|
|
|
81
86
|
const manifestPath = getTaskManifestPath(repoRoot, changeId, options);
|
|
82
87
|
const manifest = await readQueryArtifact(manifestPath);
|
|
83
88
|
const executionState = deriveManifestExecutionState(manifest.tasks || []);
|
|
84
|
-
const activePhase =
|
|
89
|
+
const activePhase = executionState.activePhase;
|
|
85
90
|
const completedIds = new Set(
|
|
86
91
|
(manifest.tasks || [])
|
|
87
92
|
.filter((task) => isTaskCompletedStatus(task.status))
|
|
@@ -212,6 +217,13 @@ const registry = createQueryRegistry([
|
|
|
212
217
|
id: 'ship-readiness',
|
|
213
218
|
artifactRefs: ({ repoRoot, changeId, changeKey }) => queryArtifactRefs(repoRoot, changeId, ['report'], { changeKey }),
|
|
214
219
|
handler: ({ repoRoot, changeId, changeKey }) => getShipReadiness(repoRoot, changeId, { changeKey })
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'workflow-context',
|
|
223
|
+
artifactRefs: ({ repoRoot, changeId, changeKey }) => getWorkflowContextArtifactRefs(repoRoot, changeId, { changeKey }),
|
|
224
|
+
requiredArtifactRefs: ({ repoRoot, changeId, changeKey }) => getWorkflowContextRequiredArtifactRefs(repoRoot, changeId, { changeKey }),
|
|
225
|
+
nextAction: 'read-compact-workflow-context',
|
|
226
|
+
handler: ({ repoRoot, changeId, changeKey }) => getWorkflowContext(repoRoot, changeId, { changeKey })
|
|
215
227
|
}
|
|
216
228
|
]);
|
|
217
229
|
|
|
@@ -220,6 +232,7 @@ module.exports = {
|
|
|
220
232
|
getNextTask,
|
|
221
233
|
getFullState,
|
|
222
234
|
getShipReadiness,
|
|
235
|
+
getWorkflowContext,
|
|
223
236
|
listQueryIds: registry.listQueryIds,
|
|
224
237
|
runQuery: registry.runQuery
|
|
225
238
|
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 path/change paths 和 review ledger event 列表。
|
|
3
|
+
* [OUTPUT]: 提供 review 记录路径、reviewId 分配、CLI 参数归一化和 ledger 容错解析。
|
|
4
|
+
* [POS]: review durable records 的小核心;不写文件,只计算稳定标识和路径。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const { getChangePaths } = require('./paths');
|
|
11
|
+
const { parseReviewLedgerEvent } = require('./schemas');
|
|
12
|
+
|
|
13
|
+
function getReviewLedgerPath(repoRoot, changeId, options = {}) {
|
|
14
|
+
return path.join(getChangePaths(repoRoot, changeId, options).reviewDir, 'review-ledger.jsonl');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getReviewFindingsPath(repoRoot, changeId, options = {}) {
|
|
18
|
+
return path.join(getChangePaths(repoRoot, changeId, options).reviewDir, 'review-findings.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function reviewIdDate(date = new Date()) {
|
|
22
|
+
const year = String(date.getUTCFullYear());
|
|
23
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
24
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
25
|
+
return `${year}${month}${day}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nextReviewId(events = [], date = new Date()) {
|
|
29
|
+
const datePart = reviewIdDate(date);
|
|
30
|
+
const prefix = `RVW-${datePart}-`;
|
|
31
|
+
const maxSequence = events.reduce((max, event) => {
|
|
32
|
+
const reviewId = String(event?.reviewId || '');
|
|
33
|
+
if (!reviewId.startsWith(prefix)) {
|
|
34
|
+
return max;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sequence = Number(reviewId.slice(prefix.length));
|
|
38
|
+
return Number.isInteger(sequence) && sequence > max ? sequence : max;
|
|
39
|
+
}, 0);
|
|
40
|
+
|
|
41
|
+
return `${prefix}${String(maxSequence + 1).padStart(3, '0')}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseSkippedNode(value) {
|
|
45
|
+
const text = String(value || '').trim();
|
|
46
|
+
const separator = text.indexOf(':');
|
|
47
|
+
if (separator === -1) {
|
|
48
|
+
return { node: text, reason: '' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
node: text.slice(0, separator).trim(),
|
|
53
|
+
reason: text.slice(separator + 1).trim()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function freshnessForLedger(entries, errors) {
|
|
58
|
+
if (errors.length > 0) {
|
|
59
|
+
return {
|
|
60
|
+
status: 'unknown',
|
|
61
|
+
reviewedCommit: '',
|
|
62
|
+
currentCommit: '',
|
|
63
|
+
commitsSinceReview: null,
|
|
64
|
+
staleReason: 'review ledger contained unparseable rows'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const latestClosed = [...entries].reverse().find((event) => event.event === 'review-closed');
|
|
69
|
+
const started = latestClosed
|
|
70
|
+
? entries.find((event) => event.reviewId === latestClosed.reviewId && event.event === 'review-started')
|
|
71
|
+
: [...entries].reverse().find((event) => event.event === 'review-started');
|
|
72
|
+
const headSha = started?.headSha || '';
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
status: headSha ? 'fresh' : 'unknown',
|
|
76
|
+
reviewedCommit: headSha,
|
|
77
|
+
currentCommit: headSha,
|
|
78
|
+
commitsSinceReview: headSha ? 0 : null,
|
|
79
|
+
staleReason: headSha ? '' : 'review ledger has no started event with headSha'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseReviewLedger(text = '') {
|
|
84
|
+
const entries = [];
|
|
85
|
+
const errors = [];
|
|
86
|
+
|
|
87
|
+
String(text || '').split(/\r?\n/).forEach((line, index) => {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
entries.push(parseReviewLedgerEvent(JSON.parse(trimmed)));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
errors.push(`line ${index + 1}: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const latestClosed = [...entries].reverse().find((event) => event.event === 'review-closed');
|
|
101
|
+
const findings = entries.filter((event) => event.event === 'review-finding-added');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
entries,
|
|
105
|
+
errors,
|
|
106
|
+
freshness: freshnessForLedger(entries, errors),
|
|
107
|
+
summary: {
|
|
108
|
+
status: latestClosed?.status || 'blocked',
|
|
109
|
+
blockingCount: latestClosed?.blockingCount ?? findings.filter((event) => event.displayTier === 'blocking').length,
|
|
110
|
+
warningCount: latestClosed?.warningCount ?? findings.filter((event) => event.displayTier === 'warning').length,
|
|
111
|
+
next: latestClosed?.next || (errors.length > 0 ? 'cc-do' : 'cc-check')
|
|
112
|
+
},
|
|
113
|
+
findings
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
getReviewFindingsPath,
|
|
119
|
+
getReviewLedgerPath,
|
|
120
|
+
nextReviewId,
|
|
121
|
+
parseReviewLedger,
|
|
122
|
+
parseSkippedNode
|
|
123
|
+
};
|
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
|
+
exists,
|
|
10
|
+
readJson,
|
|
11
|
+
readText,
|
|
9
12
|
runCommand
|
|
10
13
|
} = require('./store');
|
|
14
|
+
const { getChangePaths } = require('./paths');
|
|
15
|
+
const { parseReviewFindingsDoc } = require('./schemas');
|
|
16
|
+
const {
|
|
17
|
+
getReviewFindingsPath,
|
|
18
|
+
getReviewLedgerPath,
|
|
19
|
+
parseReviewLedger
|
|
20
|
+
} = require('./review-records');
|
|
11
21
|
|
|
12
22
|
const CODEX_BOUNDARY = [
|
|
13
23
|
'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/.',
|
|
@@ -77,9 +87,11 @@ function makeFinding({
|
|
|
77
87
|
line,
|
|
78
88
|
action = 'none',
|
|
79
89
|
status = 'open',
|
|
80
|
-
fingerprint
|
|
90
|
+
fingerprint,
|
|
91
|
+
confidenceScore,
|
|
92
|
+
displayTier
|
|
81
93
|
}) {
|
|
82
|
-
const
|
|
94
|
+
const derivedConfidenceScore = severityRank(severity) >= severityRank('important') ? 8 : 6;
|
|
83
95
|
return {
|
|
84
96
|
id,
|
|
85
97
|
source,
|
|
@@ -92,11 +104,11 @@ function makeFinding({
|
|
|
92
104
|
...(typeof line === 'number' ? { line } : {}),
|
|
93
105
|
action,
|
|
94
106
|
status,
|
|
95
|
-
confidenceScore,
|
|
107
|
+
confidenceScore: confidenceScore || derivedConfidenceScore,
|
|
96
108
|
fingerprint: fingerprint || `${source}:${category}:${id}`,
|
|
97
|
-
displayTier: status === 'informational'
|
|
109
|
+
displayTier: displayTier || (status === 'informational'
|
|
98
110
|
? 'info'
|
|
99
|
-
: (severityRank(severity) >= severityRank('important') ? 'blocking' : 'warning'),
|
|
111
|
+
: (severityRank(severity) >= severityRank('important') ? 'blocking' : 'warning')),
|
|
100
112
|
suppressionReason: null
|
|
101
113
|
};
|
|
102
114
|
}
|
|
@@ -401,6 +413,218 @@ function buildCodexErrorFinding(source, summary, details, severity = 'important'
|
|
|
401
413
|
});
|
|
402
414
|
}
|
|
403
415
|
|
|
416
|
+
async function firstExistingPath(paths) {
|
|
417
|
+
for (const filePath of paths) {
|
|
418
|
+
if (await exists(filePath)) {
|
|
419
|
+
return filePath;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function recordStatus(summary = {}, findings = []) {
|
|
426
|
+
if (summary.status === 'blocked') {
|
|
427
|
+
return 'blocked';
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
summary.status === 'findings'
|
|
431
|
+
|| Number(summary.blockingCount || 0) > 0
|
|
432
|
+
|| findings.some((item) => item.displayTier === 'blocking' || severityRank(item.severity) >= severityRank('important'))
|
|
433
|
+
) {
|
|
434
|
+
return 'fail';
|
|
435
|
+
}
|
|
436
|
+
return 'pass';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function reviewRecordSeverity(value) {
|
|
440
|
+
return value === 'advisory' ? 'minor' : value;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function reviewRecordAction(route, displayTier) {
|
|
444
|
+
if (route === 'cc-plan') return 'reroute-cc-plan';
|
|
445
|
+
if (route === 'cc-investigate') return 'reroute-cc-investigate';
|
|
446
|
+
if (route === 'cc-do') return displayTier === 'blocking' ? 'fix_now' : 'reroute-cc-do';
|
|
447
|
+
return displayTier === 'blocking' ? 'fix_now' : 'follow_up';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function makeReviewRecordFinding(item, index) {
|
|
451
|
+
const displayTier = item.displayTier || 'blocking';
|
|
452
|
+
const status = displayTier === 'info'
|
|
453
|
+
? 'informational'
|
|
454
|
+
: (displayTier === 'suppressed' ? 'accepted' : 'open');
|
|
455
|
+
return makeFinding({
|
|
456
|
+
id: item.id || item.findingId || `review-record-${index + 1}`,
|
|
457
|
+
source: 'review-records',
|
|
458
|
+
scope: 'requirement',
|
|
459
|
+
category: 'review-record',
|
|
460
|
+
severity: reviewRecordSeverity(item.severity),
|
|
461
|
+
summary: truncate(item.evidence || item.recommendation || 'Review finding', 240),
|
|
462
|
+
details: item.recommendation || item.evidence || '',
|
|
463
|
+
file: item.path,
|
|
464
|
+
action: reviewRecordAction(item.route, displayTier),
|
|
465
|
+
status,
|
|
466
|
+
fingerprint: item.fingerprint,
|
|
467
|
+
confidenceScore: Number(item.confidence) || undefined,
|
|
468
|
+
displayTier
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function normalizeFreshness(freshness = {}) {
|
|
473
|
+
return {
|
|
474
|
+
status: freshness.status || 'unknown',
|
|
475
|
+
reviewedCommit: freshness.reviewedCommit || '',
|
|
476
|
+
currentCommit: freshness.currentCommit || '',
|
|
477
|
+
commitsSinceReview: freshness.commitsSinceReview ?? null,
|
|
478
|
+
staleReason: freshness.staleReason || ''
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function makeRecordReviewer(recordReview) {
|
|
483
|
+
return makeReviewer({
|
|
484
|
+
key: 'requirement-review-records',
|
|
485
|
+
scope: 'requirement',
|
|
486
|
+
mode: 'structured',
|
|
487
|
+
source: 'runtime',
|
|
488
|
+
status: recordReview.status,
|
|
489
|
+
summary: recordReview.summary,
|
|
490
|
+
evidence: [
|
|
491
|
+
makeEvidence('file', 'review record source', recordReview.source, recordReview.summary)
|
|
492
|
+
],
|
|
493
|
+
findings: recordReview.findings || []
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function readReviewFindingsDoc(filePath) {
|
|
498
|
+
try {
|
|
499
|
+
const doc = parseReviewFindingsDoc(await readJson(filePath));
|
|
500
|
+
const findings = doc.findings.map(makeReviewRecordFinding);
|
|
501
|
+
return {
|
|
502
|
+
source: 'review-findings.json',
|
|
503
|
+
status: recordStatus(doc.summary, findings),
|
|
504
|
+
required: true,
|
|
505
|
+
summary: `review-findings.json reports ${doc.summary.status} with ${doc.summary.blockingCount} blocking finding(s).`,
|
|
506
|
+
freshness: normalizeFreshness(doc.freshness),
|
|
507
|
+
reviewers: [],
|
|
508
|
+
findings,
|
|
509
|
+
errors: []
|
|
510
|
+
};
|
|
511
|
+
} catch (error) {
|
|
512
|
+
return {
|
|
513
|
+
source: 'review-findings.json',
|
|
514
|
+
status: 'blocked',
|
|
515
|
+
required: true,
|
|
516
|
+
summary: `review-findings.json is unreadable: ${error.message}`,
|
|
517
|
+
freshness: normalizeFreshness({ status: 'unknown', staleReason: error.message }),
|
|
518
|
+
reviewers: [],
|
|
519
|
+
findings: [makeFinding({
|
|
520
|
+
id: 'review-findings-unreadable',
|
|
521
|
+
source: 'review-records',
|
|
522
|
+
scope: 'requirement',
|
|
523
|
+
category: 'review-record',
|
|
524
|
+
severity: 'important',
|
|
525
|
+
summary: 'review-findings.json is unreadable.',
|
|
526
|
+
details: error.message,
|
|
527
|
+
action: 'fix_now'
|
|
528
|
+
})],
|
|
529
|
+
errors: [error.message]
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function readReviewLedger(filePath) {
|
|
535
|
+
const parsed = parseReviewLedger(await readText(filePath, ''));
|
|
536
|
+
const findings = parsed.findings.map(makeReviewRecordFinding);
|
|
537
|
+
return {
|
|
538
|
+
source: 'review-ledger.jsonl',
|
|
539
|
+
status: recordStatus(parsed.summary, findings),
|
|
540
|
+
required: true,
|
|
541
|
+
summary: `review ledger reports ${parsed.summary.status} with ${parsed.summary.blockingCount} blocking finding(s).`,
|
|
542
|
+
freshness: normalizeFreshness(parsed.freshness),
|
|
543
|
+
reviewers: [],
|
|
544
|
+
findings,
|
|
545
|
+
errors: parsed.errors
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function runReviewRecordSection({ repoRoot, changeId }) {
|
|
550
|
+
const change = getChangePaths(repoRoot, changeId);
|
|
551
|
+
const findingsPath = await firstExistingPath([
|
|
552
|
+
getReviewFindingsPath(repoRoot, changeId, { changeKey: change.changeKey }),
|
|
553
|
+
`${change.reviewDir}/cc-review-findings.json`
|
|
554
|
+
]);
|
|
555
|
+
if (findingsPath) {
|
|
556
|
+
const recordReview = await readReviewFindingsDoc(findingsPath);
|
|
557
|
+
return {
|
|
558
|
+
...recordReview,
|
|
559
|
+
reviewers: [makeRecordReviewer(recordReview)]
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const ledgerPath = await firstExistingPath([
|
|
564
|
+
getReviewLedgerPath(repoRoot, changeId, { changeKey: change.changeKey }),
|
|
565
|
+
`${change.reviewDir}/cc-review-ledger.jsonl`
|
|
566
|
+
]);
|
|
567
|
+
if (ledgerPath) {
|
|
568
|
+
const recordReview = await readReviewLedger(ledgerPath);
|
|
569
|
+
return {
|
|
570
|
+
...recordReview,
|
|
571
|
+
reviewers: [makeRecordReviewer(recordReview)]
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const legacyReportPath = await firstExistingPath([
|
|
576
|
+
`${change.reviewDir}/cc-review-report.md`
|
|
577
|
+
]);
|
|
578
|
+
if (legacyReportPath) {
|
|
579
|
+
const recordReview = {
|
|
580
|
+
source: 'cc-review-report.md',
|
|
581
|
+
status: 'pass',
|
|
582
|
+
required: true,
|
|
583
|
+
summary: 'legacy cc-review-report.md found; review freshness is unknown.',
|
|
584
|
+
freshness: normalizeFreshness({
|
|
585
|
+
status: 'unknown',
|
|
586
|
+
staleReason: 'legacy cc-review-report.md has no machine freshness fields'
|
|
587
|
+
}),
|
|
588
|
+
reviewers: [],
|
|
589
|
+
findings: [],
|
|
590
|
+
errors: []
|
|
591
|
+
};
|
|
592
|
+
return {
|
|
593
|
+
...recordReview,
|
|
594
|
+
reviewers: [makeRecordReviewer(recordReview)]
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const finding = makeFinding({
|
|
599
|
+
id: 'review-missing',
|
|
600
|
+
source: 'review-records',
|
|
601
|
+
scope: 'requirement',
|
|
602
|
+
category: 'review-record',
|
|
603
|
+
severity: 'important',
|
|
604
|
+
summary: 'review-missing: no review records are present.',
|
|
605
|
+
details: 'Expected review-findings.json, review-ledger.jsonl, or legacy cc-review-report.md before cc-check can pass.',
|
|
606
|
+
action: 'fix_now',
|
|
607
|
+
fingerprint: `${change.changeKey}:review-missing`
|
|
608
|
+
});
|
|
609
|
+
const recordReview = {
|
|
610
|
+
source: 'review-records',
|
|
611
|
+
status: 'blocked',
|
|
612
|
+
required: true,
|
|
613
|
+
summary: 'review-missing: no review records found.',
|
|
614
|
+
freshness: normalizeFreshness({
|
|
615
|
+
status: 'unknown',
|
|
616
|
+
staleReason: 'review-missing'
|
|
617
|
+
}),
|
|
618
|
+
reviewers: [],
|
|
619
|
+
findings: [finding],
|
|
620
|
+
errors: ['review-missing']
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
...recordReview,
|
|
624
|
+
reviewers: [makeRecordReviewer(recordReview)]
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
404
628
|
async function runDiffReviewSection({ repoRoot, strict, skipReview }) {
|
|
405
629
|
if (!strict) {
|
|
406
630
|
return {
|
|
@@ -526,10 +750,10 @@ function flattenFindings(...sections) {
|
|
|
526
750
|
return sections.flatMap((section) => section.findings || []);
|
|
527
751
|
}
|
|
528
752
|
|
|
529
|
-
function deriveReviewStatus(taskReviews, diffReview) {
|
|
753
|
+
function deriveReviewStatus(taskReviews, diffReview, recordReview) {
|
|
530
754
|
let status = 'pass';
|
|
531
755
|
|
|
532
|
-
for (const section of [taskReviews, diffReview]) {
|
|
756
|
+
for (const section of [taskReviews, recordReview, diffReview]) {
|
|
533
757
|
if (!section.required && section.status === 'skipped') {
|
|
534
758
|
continue;
|
|
535
759
|
}
|
|
@@ -541,20 +765,22 @@ function deriveReviewStatus(taskReviews, diffReview) {
|
|
|
541
765
|
|
|
542
766
|
async function runReviewSuite({ repoRoot, changeId, manifest, strict, skipReview }) {
|
|
543
767
|
const taskReviews = await runTaskReviewSection({ repoRoot, changeId, manifest });
|
|
768
|
+
const recordReview = await runReviewRecordSection({ repoRoot, changeId });
|
|
544
769
|
const diffReview = await runDiffReviewSection({ repoRoot, strict, skipReview });
|
|
545
|
-
const findings = flattenFindings(taskReviews, diffReview);
|
|
546
|
-
const status = deriveReviewStatus(taskReviews, diffReview);
|
|
770
|
+
const findings = flattenFindings(taskReviews, recordReview, diffReview);
|
|
771
|
+
const status = deriveReviewStatus(taskReviews, diffReview, recordReview);
|
|
547
772
|
const currentCommit = await detectCurrentCommit(repoRoot);
|
|
548
773
|
const details = [
|
|
549
774
|
taskReviews.summary,
|
|
775
|
+
recordReview.summary,
|
|
550
776
|
diffReview.summary
|
|
551
777
|
].filter(Boolean).join(' ');
|
|
552
778
|
|
|
553
779
|
return {
|
|
554
780
|
status,
|
|
555
|
-
summary: `Task review: ${taskReviews.status}. Diff review: ${diffReview.status}.`,
|
|
781
|
+
summary: `Task review: ${taskReviews.status}. Record review: ${recordReview.status}. Diff review: ${diffReview.status}.`,
|
|
556
782
|
details,
|
|
557
|
-
freshness: {
|
|
783
|
+
freshness: recordReview.freshness || {
|
|
558
784
|
status: 'fresh',
|
|
559
785
|
reviewedCommit: currentCommit,
|
|
560
786
|
currentCommit,
|
|
@@ -570,6 +796,14 @@ async function runReviewSuite({ repoRoot, changeId, manifest, strict, skipReview
|
|
|
570
796
|
summary: taskReviews.summary,
|
|
571
797
|
skipReason: '',
|
|
572
798
|
findings: []
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: 'review-records',
|
|
802
|
+
status: recordReview.status,
|
|
803
|
+
required: true,
|
|
804
|
+
summary: recordReview.summary,
|
|
805
|
+
skipReason: '',
|
|
806
|
+
findings: recordReview.findings || []
|
|
573
807
|
}
|
|
574
808
|
],
|
|
575
809
|
runtime: {
|
|
@@ -603,6 +837,7 @@ async function runReviewSuite({ repoRoot, changeId, manifest, strict, skipReview
|
|
|
603
837
|
tddException: null
|
|
604
838
|
},
|
|
605
839
|
taskReviews,
|
|
840
|
+
recordReview,
|
|
606
841
|
diffReview,
|
|
607
842
|
findings
|
|
608
843
|
};
|