cc-devflow 4.5.9 → 4.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +6 -0
  2. package/.claude/skills/cc-act/SKILL.md +12 -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 +17 -0
  7. package/.claude/skills/cc-check/PLAYBOOK.md +1 -0
  8. package/.claude/skills/cc-check/SKILL.md +9 -5
  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 +5 -0
  12. package/.claude/skills/cc-dev/SKILL.md +26 -1
  13. package/.claude/skills/cc-do/CHANGELOG.md +12 -0
  14. package/.claude/skills/cc-do/PLAYBOOK.md +7 -7
  15. package/.claude/skills/cc-do/SKILL.md +35 -37
  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 +17 -0
  23. package/.claude/skills/cc-investigate/PLAYBOOK.md +6 -5
  24. package/.claude/skills/cc-investigate/SKILL.md +56 -44
  25. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +48 -5
  26. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +4 -3
  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 +2 -2
  29. package/.claude/skills/cc-investigate/scripts/bootstrap-analysis.sh +1 -1
  30. package/.claude/skills/cc-plan/CHANGELOG.md +19 -0
  31. package/.claude/skills/cc-plan/PLAYBOOK.md +55 -53
  32. package/.claude/skills/cc-plan/SKILL.md +101 -85
  33. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +47 -14
  34. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +4 -2
  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 +11 -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 +16 -0
  47. package/CONTRIBUTING.md +40 -4
  48. package/CONTRIBUTING.zh-CN.md +40 -4
  49. package/README.md +20 -8
  50. package/README.zh-CN.md +20 -8
  51. package/bin/cc-devflow-cli.js +293 -36
  52. package/docs/examples/START-HERE.md +5 -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 +1 -1
  73. package/docs/guides/getting-started.md +10 -8
  74. package/docs/guides/getting-started.zh-CN.md +10 -8
  75. package/docs/guides/minimize-artifacts.md +123 -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__/cli-bootstrap.integration.test.js +2 -2
  81. package/lib/skill-runtime/__tests__/dispatch.test.js +8 -38
  82. package/lib/skill-runtime/__tests__/intent.test.js +4 -20
  83. package/lib/skill-runtime/__tests__/lifecycle.test.js +1 -1
  84. package/lib/skill-runtime/__tests__/paths.test.js +7 -1
  85. package/lib/skill-runtime/__tests__/planner.tdd.test.js +61 -0
  86. package/lib/skill-runtime/__tests__/prepare-pr.test.js +3 -16
  87. package/lib/skill-runtime/__tests__/query.test.js +388 -7
  88. package/lib/skill-runtime/__tests__/review-check-integration.test.js +148 -0
  89. package/lib/skill-runtime/__tests__/review-records.test.js +619 -0
  90. package/lib/skill-runtime/__tests__/runtime.integration.test.js +64 -23
  91. package/lib/skill-runtime/__tests__/schemas.test.js +43 -0
  92. package/lib/skill-runtime/__tests__/task-contract-migrate.test.js +137 -0
  93. package/lib/skill-runtime/__tests__/task-contract.test.js +783 -0
  94. package/lib/skill-runtime/__tests__/verify-artifacts.test.js +203 -0
  95. package/lib/skill-runtime/__tests__/worker-run.test.js +4 -11
  96. package/lib/skill-runtime/__tests__/workflow-context-legacy-fallback.test.js +31 -0
  97. package/lib/skill-runtime/__tests__/workflow-context.test.js +98 -0
  98. package/lib/skill-runtime/artifacts.js +0 -5
  99. package/lib/skill-runtime/context-index.js +545 -0
  100. package/lib/skill-runtime/intent.js +9 -33
  101. package/lib/skill-runtime/lifecycle.js +1 -1
  102. package/lib/skill-runtime/operations/CLAUDE.md +2 -2
  103. package/lib/skill-runtime/operations/dispatch.js +4 -42
  104. package/lib/skill-runtime/operations/init.js +2 -6
  105. package/lib/skill-runtime/operations/janitor.js +2 -18
  106. package/lib/skill-runtime/operations/resume.js +21 -38
  107. package/lib/skill-runtime/operations/review-records.js +265 -0
  108. package/lib/skill-runtime/operations/snapshot.js +1 -1
  109. package/lib/skill-runtime/operations/task-contract.js +524 -0
  110. package/lib/skill-runtime/operations/worker-run.js +2 -30
  111. package/lib/skill-runtime/paths.js +4 -4
  112. package/lib/skill-runtime/planner.js +24 -11
  113. package/lib/skill-runtime/query-registry.js +2 -2
  114. package/lib/skill-runtime/query.js +15 -2
  115. package/lib/skill-runtime/review-records.js +123 -0
  116. package/lib/skill-runtime/review.js +246 -11
  117. package/lib/skill-runtime/schemas.js +174 -12
  118. package/lib/skill-runtime/store.js +0 -10
  119. package/lib/skill-runtime/task-contract.js +187 -0
  120. package/lib/skill-runtime/workflow-context.js +748 -0
  121. package/package.json +7 -4
@@ -1,24 +1,21 @@
1
1
  /**
2
- * [INPUT]: 依赖 manifest/checkpoint/events 与 shell 执行能力,接收并行度与重试参数。
3
- * [OUTPUT]: 更新 task-manifest 状态,写入每任务 events.jsonl 与 checkpoint.json
2
+ * [INPUT]: 依赖 manifest/events 与 shell 执行能力,接收并行度与重试参数。
3
+ * [OUTPUT]: 更新 task-manifest 状态,失败或 debug 时写入每任务 events.jsonl。
4
4
  * [POS]: skill runtime Stage-4 执行入口,被内部执行与恢复链路复用。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
6
  */
7
7
 
8
8
  const {
9
9
  nowIso,
10
- ensureDir,
11
10
  appendJsonl,
12
11
  writeJson,
13
12
  readJson,
14
13
  runCommand,
15
14
  getTaskManifestPath,
16
15
  getRuntimeStatePath,
17
- getRuntimeTaskDir,
18
- getEventsPath,
19
- getCheckpointPath
16
+ getEventsPath
20
17
  } = require('../store');
21
- const { parseManifest, parseCheckpoint } = require('../schemas');
18
+ const { parseManifest } = require('../schemas');
22
19
  const { applyManifestExecutionState } = require('../planner');
23
20
  const { syncIntentMemory } = require('../intent');
24
21
  const {
@@ -84,12 +81,6 @@ function selectBatch(readyTasks, parallel) {
84
81
  return selected;
85
82
  }
86
83
 
87
- async function writeCheckpoint(repoRoot, changeId, taskId, payload, options = {}) {
88
- const checkpointPath = getCheckpointPath(repoRoot, changeId, taskId);
89
- const checkpoint = parseCheckpoint(payload);
90
- await writeJson(checkpointPath, checkpoint);
91
- }
92
-
93
84
  async function writeEvent(repoRoot, changeId, taskId, event, debug = false) {
94
85
  if (!debug && !String(event.type || '').includes('failed') && !String(event.type || '').includes('rejected')) {
95
86
  return;
@@ -105,9 +96,6 @@ async function writeManifest(repoRoot, changeId, manifest) {
105
96
  }
106
97
 
107
98
  async function executeTask({ repoRoot, changeId, task, retryOverride, debug = false }) {
108
- const taskRuntimeDir = getRuntimeTaskDir(repoRoot, changeId, task.id);
109
- await ensureDir(taskRuntimeDir);
110
-
111
99
  const assignment = await findAssignment(repoRoot, changeId, task.id);
112
100
  const worker = await ensureWorkerWorkspace(repoRoot, changeId, assignment);
113
101
  const sessionId = toSessionId(task.id);
@@ -200,19 +188,6 @@ async function executeTask({ repoRoot, changeId, task, retryOverride, debug = fa
200
188
  task.status = 'passed';
201
189
  task.lastError = undefined;
202
190
 
203
- await writeCheckpoint(repoRoot, changeId, task.id, {
204
- changeId,
205
- taskId: task.id,
206
- sessionId,
207
- planVersion: expectedPlanVersion,
208
- status: task.status,
209
- summary: `Task passed after ${task.attempts} attempt(s)`,
210
- error: '',
211
- outputExcerpt: '',
212
- timestamp: nowIso(),
213
- attempt: task.attempts
214
- });
215
-
216
191
  await writeEvent(repoRoot, changeId, task.id, {
217
192
  type: 'task_passed',
218
193
  changeId,
@@ -246,19 +221,6 @@ async function executeTask({ repoRoot, changeId, task, retryOverride, debug = fa
246
221
  task.status = 'failed';
247
222
  task.lastError = failureMessage;
248
223
 
249
- await writeCheckpoint(repoRoot, changeId, task.id, {
250
- changeId,
251
- taskId: task.id,
252
- sessionId,
253
- planVersion: expectedPlanVersion,
254
- status: task.status,
255
- summary: `Task failed: ${failureMessage.slice(0, 240)}`,
256
- error: failureMessage,
257
- outputExcerpt: failureMessage.slice(0, 400),
258
- timestamp: nowIso(),
259
- attempt: task.attempts
260
- });
261
-
262
224
  await writeEvent(repoRoot, changeId, task.id, {
263
225
  type: failureMessage.includes('Stale result rejected') ? 'task_stale_rejected' : 'task_failed',
264
226
  changeId,
@@ -11,7 +11,6 @@ const {
11
11
  writeJson,
12
12
  readJson,
13
13
  getChangeDir,
14
- getRuntimeChangeDir,
15
14
  getRuntimeStatePath
16
15
  } = require('../store');
17
16
  const { getChangePaths, getChangeSlug } = require('../paths');
@@ -19,14 +18,11 @@ const { getChangePaths, getChangeSlug } = require('../paths');
19
18
  async function runInit({ repoRoot, changeId, goal }) {
20
19
  const changePaths = getChangePaths(repoRoot, changeId, { goal });
21
20
  const changeDir = getChangeDir(repoRoot, changeId, { goal });
22
- const runtimeDir = getRuntimeChangeDir(repoRoot, changeId, { goal });
23
21
  const statePath = getRuntimeStatePath(repoRoot, changeId, { goal });
24
22
 
25
23
  await ensureDir(changeDir);
26
24
  await ensureDir(changePaths.metaDir);
27
25
  await ensureDir(changePaths.planningDir);
28
- await ensureDir(runtimeDir);
29
- await ensureDir(changePaths.tasksDir);
30
26
  await ensureDir(changePaths.reviewDir);
31
27
  await ensureDir(changePaths.handoffDir);
32
28
 
@@ -37,7 +33,7 @@ async function runInit({ repoRoot, changeId, goal }) {
37
33
  changeKey: changePaths.changeKey,
38
34
  slug: getChangeSlug(changeId, changePaths.changeKey),
39
35
  createdAt,
40
- goal: goal || previous.goal || `Deliver ${changeId} safely with auditable checkpoints.`,
36
+ goal: goal || previous.goal || `Deliver ${changeId} safely with task-state truth.`,
41
37
  status: 'initialized',
42
38
  initializedAt: previous.initializedAt || nowIso(),
43
39
  approval: {
@@ -52,7 +48,7 @@ async function runInit({ repoRoot, changeId, goal }) {
52
48
  return {
53
49
  changeId,
54
50
  changeDir,
55
- runtimeDir,
51
+ runtimeDir: changePaths.executionDir,
56
52
  changeKey: changePaths.changeKey,
57
53
  statePath,
58
54
  status: nextState.status
@@ -1,6 +1,6 @@
1
1
  /**
2
- * [INPUT]: 依赖 devflow/changes 下 execution/tasks 目录与 checkpoint 状态,接收保留小时阈值。
3
- * [OUTPUT]: 删除过期且非运行中的任务运行态目录,并输出清理统计。
2
+ * [INPUT]: 依赖 devflow/changes 下 execution/tasks 目录与 mtime,接收保留小时阈值。
3
+ * [OUTPUT]: 删除过期任务运行态目录,并输出清理统计。
4
4
  * [POS]: skill runtime 熵清理入口,被 CLI `harness:janitor` 调用。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
6
  */
@@ -8,7 +8,6 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const {
11
- readJson,
12
11
  listDirectories,
13
12
  getRuntimeRoot
14
13
  } = require('../store');
@@ -17,17 +16,6 @@ async function removeDirectoryRecursive(dirPath) {
17
16
  await fs.promises.rm(dirPath, { recursive: true, force: true });
18
17
  }
19
18
 
20
- async function isTaskActive(taskDir) {
21
- const checkpointPath = path.join(taskDir, 'checkpoint.json');
22
- const checkpoint = await readJson(checkpointPath, null);
23
-
24
- if (!checkpoint) {
25
- return false;
26
- }
27
-
28
- return checkpoint.status === 'running';
29
- }
30
-
31
19
  async function runJanitor({ repoRoot, hours = 72 }) {
32
20
  const runtimeRoot = getRuntimeRoot(repoRoot);
33
21
  const cutoffMs = Date.now() - Number(hours) * 60 * 60 * 1000;
@@ -46,10 +34,6 @@ async function runJanitor({ repoRoot, hours = 72 }) {
46
34
  continue;
47
35
  }
48
36
 
49
- if (await isTaskActive(taskDir)) {
50
- continue;
51
- }
52
-
53
37
  await removeDirectoryRecursive(taskDir);
54
38
  removedTaskDirs += 1;
55
39
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * [INPUT]: 依赖 manifest/checkpoint 当前状态与 dispatch 执行器,接收 changeId/并行度/重试参数。
3
- * [OUTPUT]: 将失败执行明确恢复到最近稳定 checkpoint,把未稳定任务重新排队后继续执行。
2
+ * [INPUT]: 依赖 manifest 当前状态与 dispatch 执行器,接收 changeId/并行度/重试参数。
3
+ * [OUTPUT]: 将失败执行恢复到最近已通过的 manifest 任务边界,把未稳定任务重新排队后继续执行。
4
4
  * [POS]: skill runtime 恢复执行入口,供内部恢复链路复用。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
6
  */
@@ -9,8 +9,7 @@ const {
9
9
  readJson,
10
10
  writeJson,
11
11
  getTaskManifestPath,
12
- getRuntimeStatePath,
13
- getCheckpointPath
12
+ getRuntimeStatePath
14
13
  } = require('../store');
15
14
  const { parseManifest } = require('../schemas');
16
15
  const { applyManifestExecutionState } = require('../planner');
@@ -18,33 +17,17 @@ const { runDispatch } = require('./dispatch');
18
17
  const { syncIntentMemory } = require('../intent');
19
18
  const { getApprovalState, isExecutionApproved } = require('../lifecycle');
20
19
 
21
- async function resolveStableCheckpoint(repoRoot, changeId, manifest) {
22
- const checkpoints = await Promise.all(
23
- manifest.tasks.map(async (task) => {
24
- const checkpoint = await readJson(getCheckpointPath(repoRoot, changeId, task.id), null);
25
- if (!checkpoint) {
26
- return null;
27
- }
28
-
29
- return {
30
- taskId: task.id,
31
- status: checkpoint.status,
32
- timestamp: checkpoint.timestamp,
33
- summary: checkpoint.summary,
34
- planVersion: checkpoint.planVersion || null
35
- };
36
- })
37
- );
38
-
39
- const latestPassed = checkpoints
40
- .filter((checkpoint) =>
41
- checkpoint &&
42
- checkpoint.status === 'passed' &&
43
- checkpoint.planVersion === (manifest.metadata?.planVersion || 1)
44
- )
45
- .sort((left, right) => right.timestamp.localeCompare(left.timestamp))[0];
46
-
47
- return latestPassed || null;
20
+ async function resolveStableManifestPoint(manifest) {
21
+ const passedTasks = manifest.tasks
22
+ .filter((task) => task.status === 'passed')
23
+ .map((task) => ({
24
+ taskId: task.id,
25
+ status: task.status,
26
+ title: task.title || '',
27
+ planVersion: manifest.metadata?.planVersion || 1
28
+ }));
29
+
30
+ return passedTasks[passedTasks.length - 1] || null;
48
31
  }
49
32
 
50
33
  function restoreTaskToPending(task, restoreLabel) {
@@ -54,8 +37,8 @@ function restoreTaskToPending(task, restoreLabel) {
54
37
  task.lastError = `Restored from ${restoreLabel}.${previousError}`.trim();
55
38
  }
56
39
 
57
- function requeueFromStableCheckpoint(manifest, restoreTarget) {
58
- const restoreLabel = restoreTarget ? `stable checkpoint ${restoreTarget.taskId}` : 'execution start';
40
+ function requeueFromStableManifestPoint(manifest, restoreTarget) {
41
+ const restoreLabel = restoreTarget ? `stable manifest task ${restoreTarget.taskId}` : 'execution start';
59
42
  const restoredTaskIds = [];
60
43
 
61
44
  for (const task of manifest.tasks) {
@@ -106,16 +89,16 @@ async function runResume({
106
89
  }
107
90
 
108
91
  if (fromCheckpoint && fromCheckpoint !== 'stable') {
109
- throw new Error(`Unsupported --from-checkpoint value: ${fromCheckpoint}`);
92
+ throw new Error(`Unsupported resume point: ${fromCheckpoint}`);
110
93
  }
111
94
 
112
- const restoreTarget = await resolveStableCheckpoint(repoRoot, changeId, manifest);
113
- const restoreSummary = requeueFromStableCheckpoint(manifest, restoreTarget);
95
+ const restoreTarget = await resolveStableManifestPoint(manifest);
96
+ const restoreSummary = requeueFromStableManifestPoint(manifest, restoreTarget);
114
97
 
115
98
  applyManifestExecutionState(manifest);
116
99
  await writeJson(manifestPath, manifest);
117
100
  await syncIntentMemory(repoRoot, changeId, {
118
- event: 'resume_restored_checkpoint',
101
+ event: 'resume_restored_manifest_state',
119
102
  reason: restoreSummary.restoredTaskIds.length > 0
120
103
  ? `Restored ${restoreSummary.restoredTaskIds.join(', ')} from ${restoreSummary.restoreLabel}`
121
104
  : `Resume confirmed ${restoreSummary.restoreLabel}; no unresolved tasks required requeue`
@@ -132,7 +115,7 @@ async function runResume({
132
115
 
133
116
  return {
134
117
  ...dispatchResult,
135
- restoredCheckpoint: restoreTarget,
118
+ restoredState: restoreTarget,
136
119
  restoredTasks: restoreSummary.restoredTaskIds,
137
120
  reason: dispatchResult.reason || `Resumed from ${restoreSummary.restoreLabel}`
138
121
  };
@@ -0,0 +1,265 @@
1
+ /**
2
+ * [INPUT]: 依赖 review-records helper、store.appendJsonl、ReviewLedgerEventSchema。
3
+ * [OUTPUT]: 实现 review durable records CLI operation,当前只写 review-started。
4
+ * [POS]: CLI 与 durable review ledger 的边界层;负责校验后追加 JSONL。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ const path = require('path');
9
+
10
+ const { getChangePaths } = require('../paths');
11
+ const { appendJsonl, readText, writeText } = require('../store');
12
+ const { parseReviewLedgerEvent } = require('../schemas');
13
+ const {
14
+ getReviewLedgerPath,
15
+ nextReviewId,
16
+ parseSkippedNode
17
+ } = require('../review-records');
18
+
19
+ function readLedgerEvents(text) {
20
+ return String(text || '')
21
+ .split('\n')
22
+ .map((line) => line.trim())
23
+ .filter(Boolean)
24
+ .map((line) => JSON.parse(line));
25
+ }
26
+
27
+ function normalizeArray(values) {
28
+ return (values || [])
29
+ .flatMap((value) => String(value || '').split(','))
30
+ .map((value) => value.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ async function loadLedger(repoRoot, changeId, changeKey) {
35
+ const change = getChangePaths(repoRoot, changeId, { changeKey });
36
+ const ledgerPath = getReviewLedgerPath(repoRoot, changeId, { changeKey });
37
+ const events = readLedgerEvents(await readText(ledgerPath, ''));
38
+ return { change, ledgerPath, events };
39
+ }
40
+
41
+ function hasStarted(events, reviewId) {
42
+ return events.some((event) => event.reviewId === reviewId && event.event === 'review-started');
43
+ }
44
+
45
+ function hasClosed(events, reviewId) {
46
+ return events.some((event) => event.reviewId === reviewId && event.event === 'review-closed');
47
+ }
48
+
49
+ async function appendReviewEvent(options, eventFields) {
50
+ const {
51
+ repoRoot,
52
+ changeId,
53
+ changeKey,
54
+ reviewId,
55
+ now = new Date()
56
+ } = options;
57
+ const { change, ledgerPath, events } = await loadLedger(repoRoot, changeId, changeKey);
58
+
59
+ if (!reviewId || !hasStarted(events, reviewId)) {
60
+ return {
61
+ code: 4,
62
+ stderr: `Review ${reviewId || '<missing>'} has not been started`
63
+ };
64
+ }
65
+
66
+ if (eventFields.event === 'review-closed' && hasClosed(events, reviewId)) {
67
+ return {
68
+ code: 4,
69
+ stderr: `Review ${reviewId} is already closed`
70
+ };
71
+ }
72
+
73
+ const event = parseReviewLedgerEvent({
74
+ schema: 'review-ledger.v2',
75
+ change: change.changeKey,
76
+ reviewId,
77
+ createdAt: now.toISOString(),
78
+ createdBy: 'cc-devflow-cli',
79
+ ...eventFields
80
+ });
81
+
82
+ await appendJsonl(ledgerPath, event);
83
+
84
+ return {
85
+ code: 0,
86
+ reviewId,
87
+ ledgerPath: path.relative(repoRoot, ledgerPath),
88
+ event: event.event
89
+ };
90
+ }
91
+
92
+ async function runReviewStart(options) {
93
+ const {
94
+ repoRoot,
95
+ changeId,
96
+ changeKey,
97
+ mode = 'implementation',
98
+ scope = 'current-diff',
99
+ baseSha = 'unknown',
100
+ headSha = 'unknown',
101
+ selectedNodes = [],
102
+ skippedNodes = [],
103
+ riskLanes = [],
104
+ now = new Date()
105
+ } = options;
106
+ const change = getChangePaths(repoRoot, changeId, { changeKey });
107
+ const ledgerPath = getReviewLedgerPath(repoRoot, changeId, { changeKey });
108
+ const existingEvents = readLedgerEvents(await readText(ledgerPath, ''));
109
+ const reviewId = nextReviewId(existingEvents, now);
110
+ const event = parseReviewLedgerEvent({
111
+ schema: 'review-ledger.v2',
112
+ change: change.changeKey,
113
+ reviewId,
114
+ createdAt: now.toISOString(),
115
+ createdBy: 'cc-devflow-cli',
116
+ event: 'review-started',
117
+ mode,
118
+ scope,
119
+ baseSha,
120
+ headSha,
121
+ selectedNodes: normalizeArray(selectedNodes),
122
+ skippedNodes: skippedNodes.map(parseSkippedNode),
123
+ riskLanes: normalizeArray(riskLanes)
124
+ });
125
+
126
+ await appendJsonl(ledgerPath, event);
127
+
128
+ return {
129
+ code: 0,
130
+ reviewId,
131
+ ledgerPath: path.relative(repoRoot, ledgerPath),
132
+ event: event.event
133
+ };
134
+ }
135
+
136
+ async function runReviewRecordNode(options) {
137
+ return appendReviewEvent(options, {
138
+ event: 'review-node-checked',
139
+ nodeId: options.nodeId,
140
+ mode: options.mode || 'implementation',
141
+ target: options.target,
142
+ status: options.status || 'checked',
143
+ coverage: normalizeArray(options.coverage),
144
+ evidenceRefs: normalizeArray(options.evidenceRefs),
145
+ findings: normalizeArray(options.findings),
146
+ next: options.next || 'cc-check'
147
+ });
148
+ }
149
+
150
+ async function runReviewAddFinding(options) {
151
+ return appendReviewEvent(options, {
152
+ event: 'review-finding-added',
153
+ findingId: options.findingId,
154
+ severity: options.severity || 'important',
155
+ confidence: Number(options.confidence),
156
+ displayTier: options.displayTier || 'blocking',
157
+ fingerprint: options.fingerprint,
158
+ scope: options.scope,
159
+ path: options.path,
160
+ evidence: options.evidence,
161
+ recommendation: options.recommendation,
162
+ route: options.route || 'cc-do'
163
+ });
164
+ }
165
+
166
+ async function runReviewClose(options) {
167
+ return appendReviewEvent(options, {
168
+ event: 'review-closed',
169
+ status: options.status || 'clean',
170
+ blockingCount: Number(options.blockingCount || 0),
171
+ warningCount: Number(options.warningCount || 0),
172
+ next: options.next || 'cc-check'
173
+ });
174
+ }
175
+
176
+ function renderReviewMarkdown({ changeKey, reviewId, events }) {
177
+ const closed = events.find((event) => event.event === 'review-closed');
178
+ const findings = events.filter((event) => event.event === 'review-finding-added');
179
+ const lines = [
180
+ '---',
181
+ 'Output language: en',
182
+ `reviewId: ${reviewId}`,
183
+ `change: ${changeKey}`,
184
+ '---',
185
+ '',
186
+ `# Review ${reviewId}`,
187
+ '',
188
+ '## Summary',
189
+ '',
190
+ `- Status: ${closed?.status || 'open'}`,
191
+ `- Blocking findings: ${closed?.blockingCount ?? 0}`,
192
+ `- Warnings: ${closed?.warningCount ?? 0}`,
193
+ `- Next: ${closed?.next || 'cc-check'}`,
194
+ '',
195
+ '## Findings',
196
+ ''
197
+ ];
198
+
199
+ if (findings.length === 0) {
200
+ lines.push('- None');
201
+ } else {
202
+ for (const finding of findings) {
203
+ lines.push(
204
+ `- ${finding.findingId} [${finding.severity}/${finding.displayTier}, confidence ${finding.confidence}] ${finding.path}`,
205
+ ` - Evidence: ${finding.evidence}`,
206
+ ` - Recommendation: ${finding.recommendation}`,
207
+ ` - Route: ${finding.route}`
208
+ );
209
+ }
210
+ }
211
+
212
+ lines.push('', '## Ledger Events', '');
213
+ for (const event of events) {
214
+ lines.push(`- ${event.event} at ${event.createdAt}`);
215
+ }
216
+
217
+ return `${lines.join('\n')}\n`;
218
+ }
219
+
220
+ async function runReviewRender(options) {
221
+ const {
222
+ repoRoot,
223
+ changeId,
224
+ changeKey,
225
+ reviewId,
226
+ output
227
+ } = options;
228
+ const { change, events } = await loadLedger(repoRoot, changeId, changeKey);
229
+ const selectedReviewId = reviewId
230
+ || [...events].reverse().find((event) => event.event === 'review-started')?.reviewId;
231
+
232
+ if (!selectedReviewId) {
233
+ return { code: 4, stderr: 'No reviewId available to render' };
234
+ }
235
+
236
+ if (!output) {
237
+ return { code: 4, stderr: 'review render --output is required' };
238
+ }
239
+
240
+ const reviewEvents = events.filter((event) => event.reviewId === selectedReviewId);
241
+ if (reviewEvents.length === 0) {
242
+ return { code: 4, stderr: `Review ${selectedReviewId} has no ledger events` };
243
+ }
244
+
245
+ const outputPath = path.isAbsolute(output) ? output : path.join(repoRoot, output);
246
+ await writeText(outputPath, renderReviewMarkdown({
247
+ changeKey: change.changeKey,
248
+ reviewId: selectedReviewId,
249
+ events: reviewEvents
250
+ }));
251
+
252
+ return {
253
+ code: 0,
254
+ reviewId: selectedReviewId,
255
+ outputPath: path.relative(repoRoot, outputPath)
256
+ };
257
+ }
258
+
259
+ module.exports = {
260
+ runReviewAddFinding,
261
+ runReviewClose,
262
+ runReviewRecordNode,
263
+ runReviewRender,
264
+ runReviewStart
265
+ };
@@ -28,7 +28,7 @@ async function collectGitFacts(repoRoot) {
28
28
 
29
29
  async function runPlanningSnapshot({ repoRoot, changeId, goal }) {
30
30
  const state = (await readJson(getRuntimeStatePath(repoRoot, changeId), {})) || {};
31
- const effectiveGoal = goal || state.goal || `Deliver ${changeId} safely with auditable checkpoints.`;
31
+ const effectiveGoal = goal || state.goal || `Deliver ${changeId} safely with task-state truth.`;
32
32
  const gitFacts = await collectGitFacts(repoRoot);
33
33
  const scripts = await getPackageScripts(repoRoot);
34
34