cc-devflow 4.5.9 → 4.5.11

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.
Files changed (122) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +11 -0
  2. package/.claude/skills/cc-act/SKILL.md +19 -10
  3. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +1 -1
  4. package/.claude/skills/cc-act/references/closure-contract.md +1 -1
  5. package/.claude/skills/cc-act/references/git-commit-guidelines.md +1 -1
  6. package/.claude/skills/cc-check/CHANGELOG.md +23 -0
  7. package/.claude/skills/cc-check/PLAYBOOK.md +1 -0
  8. package/.claude/skills/cc-check/SKILL.md +15 -9
  9. package/.claude/skills/cc-check/references/review-contract.md +7 -0
  10. package/.claude/skills/cc-check/scripts/render-report-card.js +6 -1
  11. package/.claude/skills/cc-dev/CHANGELOG.md +10 -0
  12. package/.claude/skills/cc-dev/SKILL.md +34 -2
  13. package/.claude/skills/cc-do/CHANGELOG.md +18 -0
  14. package/.claude/skills/cc-do/PLAYBOOK.md +7 -7
  15. package/.claude/skills/cc-do/SKILL.md +47 -40
  16. package/.claude/skills/cc-do/references/execution-recovery.md +18 -13
  17. package/.claude/skills/cc-do/scripts/build-task-context.sh +4 -17
  18. package/.claude/skills/cc-do/scripts/record-review-decision.sh +4 -5
  19. package/.claude/skills/cc-do/scripts/recover-workflow.sh +9 -11
  20. package/.claude/skills/cc-do/scripts/verify-task-gates.sh +12 -10
  21. package/.claude/skills/cc-do/scripts/write-task-checkpoint.sh +7 -29
  22. package/.claude/skills/cc-investigate/CHANGELOG.md +24 -0
  23. package/.claude/skills/cc-investigate/PLAYBOOK.md +10 -9
  24. package/.claude/skills/cc-investigate/SKILL.md +163 -417
  25. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +56 -10
  26. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +6 -6
  27. package/.claude/skills/cc-investigate/assets/{ANALYSIS_TEMPLATE.md → legacy/ANALYSIS_TEMPLATE.md} +1 -0
  28. package/.claude/skills/cc-investigate/references/investigation-contract.md +5 -4
  29. package/.claude/skills/cc-investigate/scripts/bootstrap-analysis.sh +1 -1
  30. package/.claude/skills/cc-plan/CHANGELOG.md +32 -0
  31. package/.claude/skills/cc-plan/PLAYBOOK.md +55 -53
  32. package/.claude/skills/cc-plan/SKILL.md +209 -536
  33. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +50 -14
  34. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +5 -4
  35. package/.claude/skills/cc-plan/assets/{DESIGN_TEMPLATE.md → legacy/DESIGN_TEMPLATE.md} +1 -0
  36. package/.claude/skills/cc-plan/assets/{TINY_DESIGN_TEMPLATE.md → legacy/TINY_DESIGN_TEMPLATE.md} +1 -1
  37. package/.claude/skills/cc-plan/references/planning-contract.md +12 -10
  38. package/.claude/skills/cc-review/CHANGELOG.md +6 -0
  39. package/.claude/skills/cc-review/PLAYBOOK.md +9 -11
  40. package/.claude/skills/cc-review/SKILL.md +37 -61
  41. package/.claude/skills/cc-review/references/e2e-and-plugin-verification.md +1 -1
  42. package/.claude/skills/cc-review/references/implementation-review-branch.md +5 -5
  43. package/.claude/skills/cc-review/references/plan-review-branch.md +1 -1
  44. package/.claude/skills/cc-review/references/review-methods.md +4 -4
  45. package/.claude/skills/cc-review/scripts/collect-review-context.sh +14 -7
  46. package/CHANGELOG.md +30 -0
  47. package/CONTRIBUTING.md +40 -4
  48. package/CONTRIBUTING.zh-CN.md +40 -4
  49. package/README.md +22 -8
  50. package/README.zh-CN.md +22 -8
  51. package/bin/cc-devflow-cli.js +293 -36
  52. package/docs/examples/START-HERE.md +6 -4
  53. package/docs/examples/example-bindings.json +8 -8
  54. package/docs/examples/full-design-blocked/README.md +2 -2
  55. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +2 -1
  56. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/task-manifest.json +3 -2
  57. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +11 -8
  58. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/review/report-card.json +4 -4
  59. package/docs/examples/local-handoff/README.md +2 -2
  60. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +2 -1
  61. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/task-manifest.json +3 -2
  62. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +9 -6
  63. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/review/report-card.json +1 -1
  64. package/docs/examples/pdca-loop/README.md +2 -2
  65. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/handoff/pr-brief.md +2 -2
  66. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +2 -1
  67. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +2 -1
  68. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +9 -6
  69. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/review/report-card.json +1 -1
  70. package/docs/examples/scripts/check-example-bindings.sh +2 -0
  71. package/docs/get-shit-done-strategy-audit.md +22 -22
  72. package/docs/guides/artifact-contract.md +5 -1
  73. package/docs/guides/getting-started.md +11 -8
  74. package/docs/guides/getting-started.zh-CN.md +11 -8
  75. package/docs/guides/minimize-artifacts.md +137 -0
  76. package/lib/compiler/__tests__/skills-registry.test.js +2 -2
  77. package/lib/skill-runtime/CLAUDE.md +1 -1
  78. package/lib/skill-runtime/__tests__/autopilot.test.js +42 -6
  79. package/lib/skill-runtime/__tests__/benchmark-artifacts.test.js +165 -0
  80. package/lib/skill-runtime/__tests__/benchmark-skills.test.js +109 -0
  81. package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +2 -2
  82. package/lib/skill-runtime/__tests__/dispatch.test.js +8 -38
  83. package/lib/skill-runtime/__tests__/intent.test.js +4 -20
  84. package/lib/skill-runtime/__tests__/lifecycle.test.js +1 -1
  85. package/lib/skill-runtime/__tests__/paths.test.js +7 -1
  86. package/lib/skill-runtime/__tests__/planner.tdd.test.js +61 -0
  87. package/lib/skill-runtime/__tests__/prepare-pr.test.js +3 -16
  88. package/lib/skill-runtime/__tests__/query.test.js +388 -7
  89. package/lib/skill-runtime/__tests__/review-check-integration.test.js +148 -0
  90. package/lib/skill-runtime/__tests__/review-records.test.js +619 -0
  91. package/lib/skill-runtime/__tests__/runtime.integration.test.js +64 -23
  92. package/lib/skill-runtime/__tests__/schemas.test.js +43 -0
  93. package/lib/skill-runtime/__tests__/task-contract-migrate.test.js +137 -0
  94. package/lib/skill-runtime/__tests__/task-contract.test.js +874 -0
  95. package/lib/skill-runtime/__tests__/verify-artifacts.test.js +203 -0
  96. package/lib/skill-runtime/__tests__/worker-run.test.js +4 -11
  97. package/lib/skill-runtime/__tests__/workflow-context-legacy-fallback.test.js +31 -0
  98. package/lib/skill-runtime/__tests__/workflow-context.test.js +98 -0
  99. package/lib/skill-runtime/artifacts.js +0 -5
  100. package/lib/skill-runtime/context-index.js +545 -0
  101. package/lib/skill-runtime/intent.js +9 -33
  102. package/lib/skill-runtime/lifecycle.js +1 -1
  103. package/lib/skill-runtime/operations/CLAUDE.md +2 -2
  104. package/lib/skill-runtime/operations/dispatch.js +4 -42
  105. package/lib/skill-runtime/operations/init.js +2 -6
  106. package/lib/skill-runtime/operations/janitor.js +2 -18
  107. package/lib/skill-runtime/operations/resume.js +21 -38
  108. package/lib/skill-runtime/operations/review-records.js +265 -0
  109. package/lib/skill-runtime/operations/snapshot.js +1 -1
  110. package/lib/skill-runtime/operations/task-contract.js +593 -0
  111. package/lib/skill-runtime/operations/worker-run.js +2 -30
  112. package/lib/skill-runtime/paths.js +4 -4
  113. package/lib/skill-runtime/planner.js +24 -11
  114. package/lib/skill-runtime/query-registry.js +2 -2
  115. package/lib/skill-runtime/query.js +15 -2
  116. package/lib/skill-runtime/review-records.js +123 -0
  117. package/lib/skill-runtime/review.js +246 -11
  118. package/lib/skill-runtime/schemas.js +174 -12
  119. package/lib/skill-runtime/store.js +0 -10
  120. package/lib/skill-runtime/task-contract.js +188 -0
  121. package/lib/skill-runtime/workflow-context.js +748 -0
  122. package/package.json +6 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [INPUT]: 依赖 store 读取 tasks.md 与输出路径,依赖 schemas 校验 manifest。
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.push('design.md');
175
+ pushMissing(task.context.readFiles, defaultReadFiles);
166
176
  if (task.type === 'TEST') {
167
- task.context.readFiles.push('tasks.md');
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);
@@ -485,22 +495,25 @@ function applyManifestExecutionState(manifest, updatedAt = nowIso()) {
485
495
  return manifest;
486
496
  }
487
497
 
488
- async function createTaskManifest({ repoRoot, changeId, goal, overwrite = false }) {
489
- const manifestPath = getTaskManifestPath(repoRoot, changeId);
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 rawTasks = hasTasksFile ? parseTasksMarkdown(await readText(tasksPath)) : [];
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 auditable checkpoints.`,
516
+ goal: goal || `Deliver ${changeId} safely with task-state truth.`,
504
517
  createdAt: previous?.createdAt || nowIso(),
505
518
  updatedAt: nowIso(),
506
519
  currentTaskId: null,
@@ -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 runtime artifacts before running this query'
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-runtime-artifacts'
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 runtime artifacts before running this query'
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 runtime artifact before running this query',
58
+ rescueAction: 'repair or regenerate the invalid workflow artifact before running this query',
54
59
  details: {
55
60
  cause: error.name || 'Error'
56
61
  }
@@ -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 confidenceScore = severityRank(severity) >= severityRank('important') ? 8 : 6;
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
  };