cclaw-cli 7.7.1 → 8.1.0

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 (282) hide show
  1. package/README.md +210 -134
  2. package/dist/artifact-frontmatter.d.ts +51 -0
  3. package/dist/artifact-frontmatter.js +131 -0
  4. package/dist/artifact-paths.d.ts +7 -27
  5. package/dist/artifact-paths.js +20 -249
  6. package/dist/cancel.d.ts +16 -0
  7. package/dist/cancel.js +66 -0
  8. package/dist/cli.d.ts +2 -27
  9. package/dist/cli.js +90 -508
  10. package/dist/compound.d.ts +26 -0
  11. package/dist/compound.js +96 -0
  12. package/dist/config.d.ts +14 -51
  13. package/dist/config.js +23 -359
  14. package/dist/constants.d.ts +11 -18
  15. package/dist/constants.js +19 -106
  16. package/dist/content/antipatterns.d.ts +1 -0
  17. package/dist/content/antipatterns.js +109 -0
  18. package/dist/content/artifact-templates.d.ts +10 -0
  19. package/dist/content/artifact-templates.js +550 -0
  20. package/dist/content/cancel-command.d.ts +2 -2
  21. package/dist/content/cancel-command.js +25 -17
  22. package/dist/content/core-agents.d.ts +9 -233
  23. package/dist/content/core-agents.js +39 -768
  24. package/dist/content/decision-protocol.d.ts +1 -12
  25. package/dist/content/decision-protocol.js +27 -20
  26. package/dist/content/examples.d.ts +8 -42
  27. package/dist/content/examples.js +293 -425
  28. package/dist/content/idea-command.d.ts +2 -0
  29. package/dist/content/idea-command.js +38 -0
  30. package/dist/content/iron-laws.d.ts +4 -138
  31. package/dist/content/iron-laws.js +18 -197
  32. package/dist/content/meta-skill.d.ts +1 -3
  33. package/dist/content/meta-skill.js +57 -134
  34. package/dist/content/node-hooks.d.ts +12 -8
  35. package/dist/content/node-hooks.js +188 -838
  36. package/dist/content/recovery.d.ts +8 -0
  37. package/dist/content/recovery.js +179 -0
  38. package/dist/content/reference-patterns.d.ts +4 -13
  39. package/dist/content/reference-patterns.js +260 -389
  40. package/dist/content/research-playbooks.d.ts +8 -8
  41. package/dist/content/research-playbooks.js +108 -121
  42. package/dist/content/review-loop.d.ts +6 -192
  43. package/dist/content/review-loop.js +29 -731
  44. package/dist/content/skills.d.ts +8 -38
  45. package/dist/content/skills.js +681 -732
  46. package/dist/content/specialist-prompts/architect.d.ts +1 -0
  47. package/dist/content/specialist-prompts/architect.js +225 -0
  48. package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
  49. package/dist/content/specialist-prompts/brainstormer.js +168 -0
  50. package/dist/content/specialist-prompts/index.d.ts +2 -0
  51. package/dist/content/specialist-prompts/index.js +14 -0
  52. package/dist/content/specialist-prompts/planner.d.ts +1 -0
  53. package/dist/content/specialist-prompts/planner.js +182 -0
  54. package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
  55. package/dist/content/specialist-prompts/reviewer.js +193 -0
  56. package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
  57. package/dist/content/specialist-prompts/security-reviewer.js +133 -0
  58. package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
  59. package/dist/content/specialist-prompts/slice-builder.js +232 -0
  60. package/dist/content/stage-playbooks.d.ts +8 -0
  61. package/dist/content/stage-playbooks.js +404 -0
  62. package/dist/content/start-command.d.ts +2 -12
  63. package/dist/content/start-command.js +221 -207
  64. package/dist/flow-state.d.ts +21 -178
  65. package/dist/flow-state.js +67 -170
  66. package/dist/fs-utils.d.ts +6 -26
  67. package/dist/fs-utils.js +29 -162
  68. package/dist/gitignore.d.ts +2 -1
  69. package/dist/gitignore.js +51 -34
  70. package/dist/harness-detect.d.ts +10 -0
  71. package/dist/harness-detect.js +29 -0
  72. package/dist/install.d.ts +27 -15
  73. package/dist/install.js +230 -1342
  74. package/dist/knowledge-store.d.ts +19 -163
  75. package/dist/knowledge-store.js +56 -590
  76. package/dist/logger.d.ts +8 -3
  77. package/dist/logger.js +13 -4
  78. package/dist/orchestrator-routing.d.ts +29 -0
  79. package/dist/orchestrator-routing.js +156 -0
  80. package/dist/run-persistence.d.ts +7 -118
  81. package/dist/run-persistence.js +29 -845
  82. package/dist/runtime/run-hook.entry.d.ts +1 -3
  83. package/dist/runtime/run-hook.entry.js +19 -4
  84. package/dist/runtime/run-hook.mjs +13 -1024
  85. package/dist/types.d.ts +25 -261
  86. package/dist/types.js +8 -36
  87. package/package.json +6 -3
  88. package/dist/artifact-linter/brainstorm.d.ts +0 -2
  89. package/dist/artifact-linter/brainstorm.js +0 -353
  90. package/dist/artifact-linter/design.d.ts +0 -18
  91. package/dist/artifact-linter/design.js +0 -444
  92. package/dist/artifact-linter/findings-dedup.d.ts +0 -56
  93. package/dist/artifact-linter/findings-dedup.js +0 -232
  94. package/dist/artifact-linter/plan.d.ts +0 -2
  95. package/dist/artifact-linter/plan.js +0 -826
  96. package/dist/artifact-linter/review-army.d.ts +0 -49
  97. package/dist/artifact-linter/review-army.js +0 -520
  98. package/dist/artifact-linter/review.d.ts +0 -2
  99. package/dist/artifact-linter/review.js +0 -113
  100. package/dist/artifact-linter/scope.d.ts +0 -2
  101. package/dist/artifact-linter/scope.js +0 -158
  102. package/dist/artifact-linter/shared.d.ts +0 -637
  103. package/dist/artifact-linter/shared.js +0 -2163
  104. package/dist/artifact-linter/ship.d.ts +0 -2
  105. package/dist/artifact-linter/ship.js +0 -250
  106. package/dist/artifact-linter/spec.d.ts +0 -2
  107. package/dist/artifact-linter/spec.js +0 -176
  108. package/dist/artifact-linter/tdd.d.ts +0 -118
  109. package/dist/artifact-linter/tdd.js +0 -1404
  110. package/dist/artifact-linter.d.ts +0 -15
  111. package/dist/artifact-linter.js +0 -517
  112. package/dist/codex-feature-flag.d.ts +0 -58
  113. package/dist/codex-feature-flag.js +0 -193
  114. package/dist/content/closeout-guidance.d.ts +0 -14
  115. package/dist/content/closeout-guidance.js +0 -44
  116. package/dist/content/diff-command.d.ts +0 -1
  117. package/dist/content/diff-command.js +0 -43
  118. package/dist/content/harness-doc.d.ts +0 -1
  119. package/dist/content/harness-doc.js +0 -65
  120. package/dist/content/hook-events.d.ts +0 -9
  121. package/dist/content/hook-events.js +0 -23
  122. package/dist/content/hook-manifest.d.ts +0 -81
  123. package/dist/content/hook-manifest.js +0 -156
  124. package/dist/content/hooks.d.ts +0 -11
  125. package/dist/content/hooks.js +0 -1972
  126. package/dist/content/idea.d.ts +0 -60
  127. package/dist/content/idea.js +0 -416
  128. package/dist/content/language-policy.d.ts +0 -2
  129. package/dist/content/language-policy.js +0 -13
  130. package/dist/content/learnings.d.ts +0 -6
  131. package/dist/content/learnings.js +0 -141
  132. package/dist/content/observe.d.ts +0 -19
  133. package/dist/content/observe.js +0 -86
  134. package/dist/content/opencode-plugin.d.ts +0 -1
  135. package/dist/content/opencode-plugin.js +0 -635
  136. package/dist/content/review-prompts.d.ts +0 -1
  137. package/dist/content/review-prompts.js +0 -104
  138. package/dist/content/runtime-shared-snippets.d.ts +0 -8
  139. package/dist/content/runtime-shared-snippets.js +0 -80
  140. package/dist/content/session-hooks.d.ts +0 -7
  141. package/dist/content/session-hooks.js +0 -107
  142. package/dist/content/skills-elicitation.d.ts +0 -1
  143. package/dist/content/skills-elicitation.js +0 -167
  144. package/dist/content/stage-command.d.ts +0 -2
  145. package/dist/content/stage-command.js +0 -17
  146. package/dist/content/stage-schema.d.ts +0 -117
  147. package/dist/content/stage-schema.js +0 -955
  148. package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
  149. package/dist/content/stages/_lint-metadata/index.js +0 -97
  150. package/dist/content/stages/brainstorm.d.ts +0 -2
  151. package/dist/content/stages/brainstorm.js +0 -184
  152. package/dist/content/stages/design.d.ts +0 -2
  153. package/dist/content/stages/design.js +0 -288
  154. package/dist/content/stages/index.d.ts +0 -8
  155. package/dist/content/stages/index.js +0 -11
  156. package/dist/content/stages/plan.d.ts +0 -2
  157. package/dist/content/stages/plan.js +0 -191
  158. package/dist/content/stages/review.d.ts +0 -2
  159. package/dist/content/stages/review.js +0 -240
  160. package/dist/content/stages/schema-types.d.ts +0 -203
  161. package/dist/content/stages/schema-types.js +0 -1
  162. package/dist/content/stages/scope.d.ts +0 -2
  163. package/dist/content/stages/scope.js +0 -254
  164. package/dist/content/stages/ship.d.ts +0 -2
  165. package/dist/content/stages/ship.js +0 -159
  166. package/dist/content/stages/spec.d.ts +0 -2
  167. package/dist/content/stages/spec.js +0 -170
  168. package/dist/content/stages/tdd.d.ts +0 -4
  169. package/dist/content/stages/tdd.js +0 -273
  170. package/dist/content/state-contracts.d.ts +0 -1
  171. package/dist/content/state-contracts.js +0 -63
  172. package/dist/content/status-command.d.ts +0 -4
  173. package/dist/content/status-command.js +0 -109
  174. package/dist/content/subagent-context-skills.d.ts +0 -4
  175. package/dist/content/subagent-context-skills.js +0 -279
  176. package/dist/content/subagents.d.ts +0 -3
  177. package/dist/content/subagents.js +0 -997
  178. package/dist/content/templates.d.ts +0 -26
  179. package/dist/content/templates.js +0 -1692
  180. package/dist/content/track-render-context.d.ts +0 -18
  181. package/dist/content/track-render-context.js +0 -53
  182. package/dist/content/tree-command.d.ts +0 -1
  183. package/dist/content/tree-command.js +0 -64
  184. package/dist/content/utility-skills.d.ts +0 -30
  185. package/dist/content/utility-skills.js +0 -160
  186. package/dist/content/view-command.d.ts +0 -2
  187. package/dist/content/view-command.js +0 -92
  188. package/dist/delegation.d.ts +0 -649
  189. package/dist/delegation.js +0 -1539
  190. package/dist/early-loop.d.ts +0 -70
  191. package/dist/early-loop.js +0 -302
  192. package/dist/execution-topology.d.ts +0 -44
  193. package/dist/execution-topology.js +0 -95
  194. package/dist/gate-evidence.d.ts +0 -85
  195. package/dist/gate-evidence.js +0 -631
  196. package/dist/harness-adapters.d.ts +0 -151
  197. package/dist/harness-adapters.js +0 -756
  198. package/dist/harness-selection.d.ts +0 -31
  199. package/dist/harness-selection.js +0 -214
  200. package/dist/hook-schema.d.ts +0 -6
  201. package/dist/hook-schema.js +0 -114
  202. package/dist/hook-schemas/claude-hooks.v1.json +0 -10
  203. package/dist/hook-schemas/codex-hooks.v1.json +0 -10
  204. package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
  205. package/dist/init-detect.d.ts +0 -2
  206. package/dist/init-detect.js +0 -50
  207. package/dist/internal/advance-stage/advance.d.ts +0 -89
  208. package/dist/internal/advance-stage/advance.js +0 -655
  209. package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
  210. package/dist/internal/advance-stage/cancel-run.js +0 -19
  211. package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
  212. package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
  213. package/dist/internal/advance-stage/helpers.d.ts +0 -14
  214. package/dist/internal/advance-stage/helpers.js +0 -145
  215. package/dist/internal/advance-stage/hook.d.ts +0 -8
  216. package/dist/internal/advance-stage/hook.js +0 -40
  217. package/dist/internal/advance-stage/parsers.d.ts +0 -72
  218. package/dist/internal/advance-stage/parsers.js +0 -357
  219. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
  220. package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
  221. package/dist/internal/advance-stage/review-loop.d.ts +0 -16
  222. package/dist/internal/advance-stage/review-loop.js +0 -199
  223. package/dist/internal/advance-stage/rewind.d.ts +0 -14
  224. package/dist/internal/advance-stage/rewind.js +0 -108
  225. package/dist/internal/advance-stage/start-flow.d.ts +0 -13
  226. package/dist/internal/advance-stage/start-flow.js +0 -241
  227. package/dist/internal/advance-stage/verify.d.ts +0 -21
  228. package/dist/internal/advance-stage/verify.js +0 -185
  229. package/dist/internal/advance-stage.d.ts +0 -7
  230. package/dist/internal/advance-stage.js +0 -138
  231. package/dist/internal/cohesion-contract-stub.d.ts +0 -24
  232. package/dist/internal/cohesion-contract-stub.js +0 -148
  233. package/dist/internal/compound-readiness.d.ts +0 -23
  234. package/dist/internal/compound-readiness.js +0 -102
  235. package/dist/internal/detect-public-api-changes.d.ts +0 -5
  236. package/dist/internal/detect-public-api-changes.js +0 -45
  237. package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
  238. package/dist/internal/detect-supply-chain-changes.js +0 -138
  239. package/dist/internal/early-loop-status.d.ts +0 -7
  240. package/dist/internal/early-loop-status.js +0 -93
  241. package/dist/internal/envelope-validate.d.ts +0 -7
  242. package/dist/internal/envelope-validate.js +0 -66
  243. package/dist/internal/flow-state-repair.d.ts +0 -20
  244. package/dist/internal/flow-state-repair.js +0 -104
  245. package/dist/internal/plan-split-waves.d.ts +0 -190
  246. package/dist/internal/plan-split-waves.js +0 -764
  247. package/dist/internal/runtime-integrity.d.ts +0 -7
  248. package/dist/internal/runtime-integrity.js +0 -268
  249. package/dist/internal/slice-commit.d.ts +0 -7
  250. package/dist/internal/slice-commit.js +0 -619
  251. package/dist/internal/tdd-loop-status.d.ts +0 -14
  252. package/dist/internal/tdd-loop-status.js +0 -68
  253. package/dist/internal/tdd-red-evidence.d.ts +0 -7
  254. package/dist/internal/tdd-red-evidence.js +0 -153
  255. package/dist/internal/waiver-grant.d.ts +0 -62
  256. package/dist/internal/waiver-grant.js +0 -294
  257. package/dist/internal/wave-status.d.ts +0 -74
  258. package/dist/internal/wave-status.js +0 -506
  259. package/dist/managed-resources.d.ts +0 -53
  260. package/dist/managed-resources.js +0 -313
  261. package/dist/policy.d.ts +0 -10
  262. package/dist/policy.js +0 -167
  263. package/dist/retro-gate.d.ts +0 -9
  264. package/dist/retro-gate.js +0 -47
  265. package/dist/run-archive.d.ts +0 -61
  266. package/dist/run-archive.js +0 -391
  267. package/dist/runs.d.ts +0 -2
  268. package/dist/runs.js +0 -2
  269. package/dist/stack-detection.d.ts +0 -116
  270. package/dist/stack-detection.js +0 -489
  271. package/dist/streaming/event-stream.d.ts +0 -31
  272. package/dist/streaming/event-stream.js +0 -114
  273. package/dist/tdd-cycle.d.ts +0 -107
  274. package/dist/tdd-cycle.js +0 -289
  275. package/dist/tdd-verification-evidence.d.ts +0 -17
  276. package/dist/tdd-verification-evidence.js +0 -122
  277. package/dist/track-heuristics.d.ts +0 -27
  278. package/dist/track-heuristics.js +0 -154
  279. package/dist/util/slice-id.d.ts +0 -58
  280. package/dist/util/slice-id.js +0 -89
  281. package/dist/worktree-manager.d.ts +0 -20
  282. package/dist/worktree-manager.js +0 -108
@@ -1,856 +1,40 @@
1
- import { createHash } from "node:crypto";
2
1
  import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { RUNTIME_ROOT } from "./constants.js";
5
- import { nextStage, createInitialCloseoutState, createInitialFlowState, FLOW_STATE_SCHEMA_VERSION, isDiscoveryMode, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
6
- import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
7
- import { FLOW_STAGES } from "./types.js";
8
- export class InvalidStageTransitionError extends Error {
9
- from;
10
- to;
11
- constructor(from, to, message) {
12
- super(message);
13
- this.from = from;
14
- this.to = to;
15
- this.name = "InvalidStageTransitionError";
16
- }
17
- }
18
- const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
19
- const FLOW_STATE_GUARD_REL_PATH = `${RUNTIME_ROOT}/.flow-state.guard.json`;
20
- const FLOW_STATE_REPAIR_LOG_REL_PATH = `${RUNTIME_ROOT}/.flow-state-repair.log`;
21
- const ARCHIVE_DIR_REL_PATH = `${RUNTIME_ROOT}/archive`;
22
- const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
23
- const FLOW_STAGE_SET = new Set(FLOW_STAGES);
24
- const DEFAULT_WRITER_SUBSYSTEM = "cclaw-cli";
25
- const DEFAULT_REPAIR_REASON_PATTERN = /^[a-z][a-z0-9_-]{2,}$/u;
26
- export class FlowStateGuardMismatchError extends Error {
27
- expectedSha;
28
- actualSha;
29
- lastWriter;
30
- writtenAt;
31
- runId;
32
- statePath;
33
- guardPath;
34
- repairCommand;
35
- constructor(details) {
36
- super(`flow-state guard mismatch: ${details.runId}\n` +
37
- `expected sha: ${details.expectedSha}\n` +
38
- `actual sha: ${details.actualSha}\n` +
39
- `last writer: ${details.lastWriter}@${details.writtenAt}\n` +
40
- `do not edit flow-state.json by hand. To recover, run:\n` +
41
- ` ${details.repairCommand}`);
42
- this.name = "FlowStateGuardMismatchError";
43
- this.expectedSha = details.expectedSha;
44
- this.actualSha = details.actualSha;
45
- this.lastWriter = details.lastWriter;
46
- this.writtenAt = details.writtenAt;
47
- this.runId = details.runId;
48
- this.statePath = details.statePath;
49
- this.guardPath = details.guardPath;
50
- this.repairCommand = details.repairCommand;
51
- }
52
- }
53
- function canonicalFlowStateShaFromRaw(raw) {
54
- return createHash("sha256").update(raw, "utf8").digest("hex");
55
- }
56
- function guardSidecarPath(projectRoot) {
57
- return path.join(projectRoot, FLOW_STATE_GUARD_REL_PATH);
58
- }
59
- function repairLogPath(projectRoot) {
60
- return path.join(projectRoot, FLOW_STATE_REPAIR_LOG_REL_PATH);
61
- }
62
- function validateFlowTransition(prev, next) {
63
- if (prev.activeRunId !== next.activeRunId) {
64
- // New run — only reset paths may change the runId, but those set allowReset.
65
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change activeRunId from "${prev.activeRunId}" to "${next.activeRunId}" without allowReset.`);
66
- }
67
- // Track is immutable within a single run: stage schemas, gate sets, and
68
- // cross-stage reads all branch on track. Silently flipping the track
69
- // mid-run would let completed stages satisfy one gate tier and the
70
- // current stage re-read the catalog under a different tier.
71
- if (prev.track !== next.track) {
72
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change track from "${prev.track}" to "${next.track}" mid-run (activeRunId="${prev.activeRunId}"). Archive the run and start a new one to switch tracks.`);
73
- }
74
- if (prev.discoveryMode !== next.discoveryMode) {
75
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change discoveryMode from "${prev.discoveryMode}" to "${next.discoveryMode}" mid-run (activeRunId="${prev.activeRunId}"). Reclassify through start-flow or start a new run.`);
76
- }
77
- const newRewind = next.rewinds.length === prev.rewinds.length + 1
78
- ? next.rewinds[next.rewinds.length - 1]
79
- : undefined;
80
- const isManagedRewind = newRewind !== undefined
81
- && newRewind.fromStage === prev.currentStage
82
- && newRewind.toStage === next.currentStage
83
- && newRewind.invalidatedStages.includes(next.currentStage);
84
- const removedCompletedStages = prev.completedStages.filter((stage) => !next.completedStages.includes(stage));
85
- if (removedCompletedStages.length > 0 && !isManagedRewind) {
86
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage(s) ${removedCompletedStages.map((stage) => `"${stage}"`).join(", ")} were previously completed but are missing from the new state.`);
87
- }
88
- if (isManagedRewind) {
89
- const invalidated = new Set(newRewind.invalidatedStages);
90
- const unexpectedRemoved = removedCompletedStages.filter((stage) => !invalidated.has(stage));
91
- const missingMarkers = newRewind.invalidatedStages.filter((stage) => {
92
- const marker = next.staleStages[stage];
93
- return !marker || marker.rewindId !== newRewind.id;
94
- });
95
- if (unexpectedRemoved.length > 0 || missingMarkers.length > 0) {
96
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `managed rewind state is inconsistent: unexpectedRemoved=${unexpectedRemoved.join(",") || "none"}; missingMarkers=${missingMarkers.join(",") || "none"}.`);
97
- }
98
- return;
99
- }
100
- if (prev.currentStage === next.currentStage) {
101
- return;
102
- }
103
- const naturalForward = nextStage(prev.currentStage, prev.track);
104
- const isNaturalForward = naturalForward === next.currentStage;
105
- const isReviewRewind = prev.currentStage === "review" && next.currentStage === "tdd";
106
- if (!isNaturalForward && !isReviewRewind) {
107
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}" for track "${prev.track}". Use /cc to advance stages or archive the run to reset.`);
108
- }
109
- }
110
- function flowStatePath(projectRoot) {
3
+ import { FLOW_STATE_REL_PATH, RUNTIME_ROOT } from "./constants.js";
4
+ import { assertFlowStateV8, createInitialFlowStateV8 } from "./flow-state.js";
5
+ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
6
+ export function flowStatePath(projectRoot) {
111
7
  return path.join(projectRoot, FLOW_STATE_REL_PATH);
112
8
  }
113
- function flowStateLockPath(projectRoot) {
114
- return path.join(projectRoot, RUNTIME_ROOT, "state", ".flow-state.lock");
115
- }
116
- function archiveRoot(projectRoot) {
117
- return path.join(projectRoot, ARCHIVE_DIR_REL_PATH);
118
- }
119
- function activeArtifactsPath(projectRoot) {
120
- return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
121
- }
122
- function isFlowStage(value) {
123
- return typeof value === "string" && FLOW_STAGE_SET.has(value);
124
- }
125
- function sanitizeStringArray(value) {
126
- if (!Array.isArray(value)) {
127
- return [];
128
- }
129
- return value.filter((item) => typeof item === "string" && item.trim().length > 0);
130
- }
131
- function sanitizeCompletedStages(value) {
132
- if (!Array.isArray(value)) {
133
- return [];
134
- }
135
- const unique = new Set();
136
- const stages = [];
137
- for (const item of value) {
138
- if (isFlowStage(item) && !unique.has(item)) {
139
- unique.add(item);
140
- stages.push(item);
141
- }
142
- }
143
- return stages;
144
- }
145
- function sanitizeGuardEvidence(value) {
146
- if (!value || typeof value !== "object" || Array.isArray(value)) {
147
- return {};
148
- }
149
- const next = {};
150
- for (const [key, raw] of Object.entries(value)) {
151
- if (typeof raw === "string") {
152
- next[key] = raw;
153
- }
154
- }
155
- return next;
156
- }
157
- function sanitizeStageGateCatalog(value, fallback) {
158
- const uniqueStrings = (items) => [...new Set(items)];
159
- const next = {};
160
- for (const stage of FLOW_STAGES) {
161
- const base = fallback[stage];
162
- next[stage] = {
163
- required: [...base.required],
164
- recommended: [...base.recommended],
165
- conditional: [...base.conditional],
166
- triggered: [...base.triggered],
167
- passed: [...base.passed],
168
- blocked: [...base.blocked]
169
- };
170
- }
171
- if (!value || typeof value !== "object" || Array.isArray(value)) {
172
- return next;
173
- }
174
- const rawCatalog = value;
175
- for (const stage of FLOW_STAGES) {
176
- const rawStage = rawCatalog[stage];
177
- if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
178
- continue;
179
- }
180
- const typed = rawStage;
181
- const stageState = next[stage];
182
- const allowedGateIds = new Set([
183
- ...stageState.required,
184
- ...stageState.recommended,
185
- ...stageState.conditional
186
- ]);
187
- const conditionalGateIds = new Set(stageState.conditional);
188
- const passed = sanitizeStringArray(typed.passed).filter((gate) => allowedGateIds.has(gate));
189
- const blocked = sanitizeStringArray(typed.blocked).filter((gate) => allowedGateIds.has(gate));
190
- const triggeredFromState = sanitizeStringArray(typed.triggered).filter((gate) => conditionalGateIds.has(gate));
191
- const touchedConditionals = [...passed, ...blocked].filter((gate) => conditionalGateIds.has(gate));
192
- next[stage] = {
193
- required: [...stageState.required],
194
- recommended: [...stageState.recommended],
195
- conditional: [...stageState.conditional],
196
- triggered: uniqueStrings([...triggeredFromState, ...touchedConditionals]),
197
- passed,
198
- blocked
199
- };
200
- }
201
- return next;
202
- }
203
- function coerceTrack(value) {
204
- return isFlowTrack(value) ? value : "standard";
205
- }
206
- function coerceDiscoveryMode(value) {
207
- if (typeof value === "string") {
208
- const normalized = value.trim().toLowerCase();
209
- if (isDiscoveryMode(normalized))
210
- return normalized;
211
- }
212
- return "guided";
213
- }
214
- function coerceRepoSignals(value) {
215
- if (!value || typeof value !== "object" || Array.isArray(value)) {
216
- return undefined;
217
- }
218
- const typed = value;
219
- const fileCountRaw = typed.fileCount;
220
- const fileCount = typeof fileCountRaw === "number" && Number.isFinite(fileCountRaw) && fileCountRaw >= 0
221
- ? Math.min(Math.floor(fileCountRaw), 1_000_000)
222
- : undefined;
223
- const capturedAt = typeof typed.capturedAt === "string" ? typed.capturedAt.trim() : "";
224
- if (fileCount === undefined || !capturedAt) {
225
- return undefined;
226
- }
227
- return {
228
- fileCount,
229
- hasReadme: typed.hasReadme === true,
230
- hasPackageManifest: typed.hasPackageManifest === true,
231
- capturedAt
232
- };
233
- }
234
- /**
235
- * preserve `flow-state.json#taskClass`
236
- * across read/write round-trips. Before this audit fix the persistence
237
- * layer silently dropped the field, which made the bugfix-skip
238
- * (`mandatoryAgentsFor` short-circuit) and the artifact-validation
239
- * demotion both dead in practice: the only entry point that classified
240
- * a run was the unit-test harness passing `options.taskClass` directly
241
- * to `checkMandatoryDelegations`. The accepted union mirrors
242
- * `MandatoryDelegationTaskClass` plus `null` so callers can explicitly
243
- * clear the classification without dropping the property.
244
- */
245
- function coerceTaskClass(value) {
246
- if (value === undefined)
247
- return undefined;
248
- if (value === null)
249
- return null;
250
- if (value === "software-standard" ||
251
- value === "software-trivial" ||
252
- value === "software-bugfix") {
253
- return value;
254
- }
255
- return undefined;
256
- }
257
- function sanitizeSkippedStages(value, track) {
258
- const trackDefault = skippedStagesForTrack(track);
259
- if (!Array.isArray(value)) {
260
- return trackDefault;
261
- }
262
- const seen = new Set();
263
- const out = [];
264
- for (const raw of value) {
265
- if (isFlowStage(raw) && !seen.has(raw)) {
266
- seen.add(raw);
267
- out.push(raw);
268
- }
269
- }
270
- return out.length > 0 ? out : trackDefault;
271
- }
272
- function sanitizeStaleStages(value) {
273
- if (!value || typeof value !== "object" || Array.isArray(value)) {
274
- return {};
275
- }
276
- const out = {};
277
- for (const [stage, raw] of Object.entries(value)) {
278
- if (!isFlowStage(stage))
279
- continue;
280
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
281
- continue;
282
- const typed = raw;
283
- const rewindId = typeof typed.rewindId === "string" ? typed.rewindId : "";
284
- const reason = typeof typed.reason === "string" ? typed.reason : "";
285
- const markedAt = typeof typed.markedAt === "string" ? typed.markedAt : "";
286
- const acknowledgedAt = typeof typed.acknowledgedAt === "string" ? typed.acknowledgedAt : undefined;
287
- if (!rewindId || !reason || !markedAt) {
288
- continue;
289
- }
290
- out[stage] = {
291
- rewindId,
292
- reason,
293
- markedAt,
294
- acknowledgedAt
295
- };
296
- }
297
- return out;
298
- }
299
- function sanitizeCompletedStageMeta(value) {
300
- if (!value || typeof value !== "object" || Array.isArray(value)) {
301
- return undefined;
302
- }
303
- const out = {};
304
- for (const [key, raw] of Object.entries(value)) {
305
- if (!isFlowStage(key))
306
- continue;
307
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
308
- continue;
309
- const record = raw;
310
- const ca = typeof record.completedAt === "string" ? record.completedAt.trim() : "";
311
- if (ca.length > 0) {
312
- out[key] = { completedAt: ca };
313
- }
314
- }
315
- return Object.keys(out).length > 0 ? out : undefined;
316
- }
317
- function sanitizeRewinds(value) {
318
- if (!Array.isArray(value)) {
319
- return [];
320
- }
321
- const out = [];
322
- for (const raw of value) {
323
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
324
- continue;
325
- }
326
- const typed = raw;
327
- if (typeof typed.id !== "string" ||
328
- !isFlowStage(typed.fromStage) ||
329
- !isFlowStage(typed.toStage) ||
330
- typeof typed.reason !== "string" ||
331
- typeof typed.timestamp !== "string") {
332
- continue;
333
- }
334
- const invalidatedStages = Array.isArray(typed.invalidatedStages)
335
- ? typed.invalidatedStages.filter((stage) => isFlowStage(stage))
336
- : [];
337
- out.push({
338
- id: typed.id,
339
- fromStage: typed.fromStage,
340
- toStage: typed.toStage,
341
- reason: typed.reason,
342
- timestamp: typed.timestamp,
343
- invalidatedStages
344
- });
345
- }
346
- return out;
347
- }
348
- function sanitizeInteractionHints(value) {
349
- if (!value || typeof value !== "object" || Array.isArray(value)) {
350
- return {};
351
- }
352
- const out = {};
353
- for (const [stage, raw] of Object.entries(value)) {
354
- if (!isFlowStage(stage))
355
- continue;
356
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
357
- continue;
358
- const typed = raw;
359
- const skipQuestions = typed.skipQuestions === true ? true : undefined;
360
- const sourceStage = isFlowStage(typed.sourceStage) ? typed.sourceStage : undefined;
361
- const recordedAt = typeof typed.recordedAt === "string" ? typed.recordedAt : undefined;
362
- const fromIdeaArtifact = typeof typed.fromIdeaArtifact === "string" && typed.fromIdeaArtifact.trim().length > 0
363
- ? typed.fromIdeaArtifact.trim()
364
- : undefined;
365
- const fromIdeaCandidateId = typeof typed.fromIdeaCandidateId === "string" && typed.fromIdeaCandidateId.trim().length > 0
366
- ? typed.fromIdeaCandidateId.trim()
367
- : undefined;
368
- if (skipQuestions !== true &&
369
- !sourceStage &&
370
- !recordedAt &&
371
- !fromIdeaArtifact &&
372
- !fromIdeaCandidateId) {
373
- continue;
374
- }
375
- out[stage] = {
376
- ...(skipQuestions ? { skipQuestions } : {}),
377
- ...(sourceStage ? { sourceStage } : {}),
378
- ...(recordedAt ? { recordedAt } : {}),
379
- ...(fromIdeaArtifact ? { fromIdeaArtifact } : {}),
380
- ...(fromIdeaCandidateId ? { fromIdeaCandidateId } : {})
381
- };
382
- }
383
- return out;
384
- }
385
- function sanitizeRetroState(value) {
386
- const fallback = {
387
- required: false,
388
- completedAt: undefined,
389
- compoundEntries: 0
390
- };
391
- if (!value || typeof value !== "object" || Array.isArray(value)) {
392
- return fallback;
393
- }
394
- const typed = value;
395
- const required = typeof typed.required === "boolean" ? typed.required : false;
396
- const completedAt = typeof typed.completedAt === "string" ? typed.completedAt : undefined;
397
- const compoundEntriesRaw = typed.compoundEntries;
398
- const compoundEntries = typeof compoundEntriesRaw === "number" && Number.isFinite(compoundEntriesRaw) && compoundEntriesRaw >= 0
399
- ? Math.floor(compoundEntriesRaw)
400
- : 0;
401
- return {
402
- required,
403
- completedAt,
404
- compoundEntries
405
- };
406
- }
407
- function isShipSubstate(value) {
408
- return typeof value === "string" && SHIP_SUBSTATES.includes(value);
409
- }
410
- function sanitizeCloseoutState(value) {
411
- const fallback = createInitialCloseoutState();
412
- if (!value || typeof value !== "object" || Array.isArray(value)) {
413
- return fallback;
414
- }
415
- const typed = value;
416
- const rawShipSubstate = typeof typed.shipSubstate === "string" ? typed.shipSubstate : undefined;
417
- let shipSubstate;
418
- if (rawShipSubstate === "retro_review" || rawShipSubstate === "compound_review") {
419
- shipSubstate = "post_ship_review";
420
- }
421
- else {
422
- shipSubstate = isShipSubstate(rawShipSubstate) ? rawShipSubstate : fallback.shipSubstate;
423
- }
424
- const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
425
- const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
426
- const retroSkipReason = typeof typed.retroSkipReason === "string"
427
- ? typed.retroSkipReason.trim() || undefined
428
- : undefined;
429
- const retroSkipped = typed.retroSkipped === true && retroSkipReason !== undefined
430
- ? true
431
- : undefined;
432
- const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
433
- const compoundSkipReason = typeof typed.compoundSkipReason === "string"
434
- ? typed.compoundSkipReason.trim() || undefined
435
- : undefined;
436
- const compoundSkipped = typed.compoundSkipped === true && compoundSkipReason !== undefined
437
- ? true
438
- : undefined;
439
- const promotedRaw = typed.compoundPromoted;
440
- const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
441
- ? Math.floor(promotedRaw)
442
- : 0;
443
- // Demote shipSubstate when its closeout invariants are violated on disk. A
444
- // hand-edited flow-state could claim `ready_to_archive` without completing
445
- // the compound leg, which would let `archive` skip durable closeout proof.
446
- const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
447
- const compoundDone = compoundCompletedAt !== undefined || compoundPromoted > 0 || compoundSkipped === true;
448
- if (shipSubstate === "ready_to_archive" && (!retroDone || !compoundDone)) {
449
- shipSubstate = "post_ship_review";
450
- }
451
- return {
452
- shipSubstate,
453
- retroDraftedAt,
454
- retroAcceptedAt,
455
- retroSkipped,
456
- retroSkipReason,
457
- compoundCompletedAt,
458
- compoundSkipped,
459
- compoundSkipReason,
460
- compoundPromoted
461
- };
462
- }
463
- function coerceFlowState(parsed) {
464
- const track = coerceTrack(parsed.track);
465
- const discoveryMode = coerceDiscoveryMode(parsed.discoveryMode);
466
- const next = createInitialFlowState({ track, discoveryMode });
467
- const activeRunIdRaw = parsed.activeRunId;
468
- const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
469
- ? activeRunIdRaw.trim()
470
- : next.activeRunId;
471
- const taskClass = coerceTaskClass(parsed.taskClass);
472
- const repoSignals = coerceRepoSignals(parsed.repoSignals);
473
- const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
474
- const tddGreenMinElapsedMs = coerceTddGreenMinElapsedMs(parsed.tddGreenMinElapsedMs);
475
- const packageVersion = typeof parsed.packageVersion === "string" && parsed.packageVersion.trim().length > 0
476
- ? parsed.packageVersion.trim()
477
- : undefined;
478
- const state = {
479
- schemaVersion: FLOW_STATE_SCHEMA_VERSION,
480
- activeRunId,
481
- currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
482
- completedStages: sanitizeCompletedStages(parsed.completedStages),
483
- guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
484
- stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
485
- track,
486
- discoveryMode,
487
- ...(taskClass !== undefined ? { taskClass } : {}),
488
- ...(repoSignals ? { repoSignals } : {}),
489
- ...(completedStageMeta ? { completedStageMeta } : {}),
490
- ...(tddGreenMinElapsedMs !== undefined ? { tddGreenMinElapsedMs } : {}),
491
- ...(packageVersion ? { packageVersion } : {}),
492
- skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
493
- staleStages: sanitizeStaleStages(parsed.staleStages),
494
- rewinds: sanitizeRewinds(parsed.rewinds),
495
- interactionHints: sanitizeInteractionHints(parsed.interactionHints),
496
- retro: sanitizeRetroState(parsed.retro),
497
- closeout: sanitizeCloseoutState(parsed.closeout)
498
- };
499
- return { state };
500
- }
501
- /**
502
- * coerce `tddGreenMinElapsedMs` from disk. Mirrors the
503
- * defensive read in `effectiveTddGreenMinElapsedMs`: numbers ≥ 0 round
504
- * down to integers; everything else (NaN, strings, negatives) returns
505
- * undefined so the field is omitted from the rehydrated state and the
506
- * effective getter falls back to the documented 4000ms default.
507
- */
508
- function coerceTddGreenMinElapsedMs(value) {
509
- if (typeof value !== "number")
510
- return undefined;
511
- if (!Number.isFinite(value))
512
- return undefined;
513
- if (value < 0)
514
- return undefined;
515
- return Math.floor(value);
516
- }
517
- export class CorruptFlowStateError extends Error {
518
- statePath;
519
- quarantinedPath;
520
- constructor(statePath, quarantinedPath, cause) {
521
- super(`Corrupt flow-state.json detected at ${statePath}. ` +
522
- `Quarantined to ${quarantinedPath}. ` +
523
- `Inspect the quarantined file, reconcile by hand, then re-run your command ` +
524
- `or delete ${statePath} to start over. ` +
525
- `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`);
526
- this.name = "CorruptFlowStateError";
527
- this.statePath = statePath;
528
- this.quarantinedPath = quarantinedPath;
529
- if (cause instanceof Error) {
530
- this.cause = cause;
531
- }
532
- }
533
- }
534
- function quarantineTimestamp(date = new Date()) {
535
- return date.toISOString().replace(/[:.]/gu, "-");
536
- }
537
- async function quarantineCorruptState(statePath, cause) {
538
- const quarantinedPath = `${statePath}.corrupt-${quarantineTimestamp()}.json`;
539
- try {
540
- await fs.rename(statePath, quarantinedPath);
541
- }
542
- catch (renameErr) {
543
- try {
544
- const raw = await fs.readFile(statePath, "utf8");
545
- await fs.writeFile(quarantinedPath, raw, "utf8");
546
- await fs.unlink(statePath).catch(() => undefined);
547
- }
548
- catch {
549
- throw new CorruptFlowStateError(statePath, quarantinedPath, renameErr);
550
- }
551
- }
552
- throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
553
- }
554
- function buildRepairCommand(reason = "<manual_edit_recovery>") {
555
- return `cclaw-cli internal flow-state-repair --reason "${reason}"`;
556
- }
557
- async function readGuardSidecar(projectRoot) {
558
- const guardPath = guardSidecarPath(projectRoot);
559
- try {
560
- const raw = await fs.readFile(guardPath, "utf8");
561
- const parsed = JSON.parse(raw);
562
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
563
- return null;
564
- }
565
- const sha256 = typeof parsed.sha256 === "string" ? parsed.sha256 : "";
566
- const writtenAt = typeof parsed.writtenAt === "string" ? parsed.writtenAt : "";
567
- const writerSubsystem = typeof parsed.writerSubsystem === "string" ? parsed.writerSubsystem : "";
568
- const runId = typeof parsed.runId === "string" ? parsed.runId : "";
569
- if (!sha256 || !writtenAt || !writerSubsystem || !runId) {
570
- return null;
571
- }
572
- return { sha256, writtenAt, writerSubsystem, runId };
573
- }
574
- catch {
575
- return null;
576
- }
577
- }
578
- async function verifyFlowStateGuardFromRaw(projectRoot, statePath, rawContents) {
579
- const sidecar = await readGuardSidecar(projectRoot);
580
- if (!sidecar) {
581
- // Legacy: flow-state.json was written by a pre-guard runtime, or sidecar
582
- // was intentionally reset. Permit the read so existing projects keep
583
- // working; the next legitimate stage-complete writes a fresh sidecar.
584
- return;
585
- }
586
- const actualSha = canonicalFlowStateShaFromRaw(rawContents);
587
- if (actualSha === sidecar.sha256) {
588
- return;
589
- }
590
- throw new FlowStateGuardMismatchError({
591
- expectedSha: sidecar.sha256,
592
- actualSha,
593
- lastWriter: sidecar.writerSubsystem,
594
- writtenAt: sidecar.writtenAt,
595
- runId: sidecar.runId,
596
- statePath,
597
- guardPath: guardSidecarPath(projectRoot),
598
- repairCommand: buildRepairCommand("manual_edit_recovery")
599
- });
600
- }
601
- /**
602
- * Verify the on-disk flow-state against the sha256 sidecar. Throws
603
- * `FlowStateGuardMismatchError` when manual editing is detected. Safe to
604
- * call on projects that have never written a sidecar: a missing sidecar is
605
- * treated as "legacy runtime" and the check silently succeeds.
606
- */
607
- export async function verifyFlowStateGuard(projectRoot) {
608
- const statePath = flowStatePath(projectRoot);
609
- if (!(await exists(statePath)))
610
- return;
611
- let raw;
612
- try {
613
- raw = await fs.readFile(statePath, "utf8");
614
- }
615
- catch {
616
- return;
617
- }
618
- await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
619
- }
620
- export async function readFlowState(projectRoot, options = {}) {
621
- void options;
9
+ export async function ensureRunSystem(projectRoot) {
10
+ await ensureDir(path.join(projectRoot, RUNTIME_ROOT, "state"));
622
11
  const statePath = flowStatePath(projectRoot);
623
12
  if (!(await exists(statePath))) {
624
- return createInitialFlowState();
625
- }
626
- let raw;
627
- try {
628
- raw = await fs.readFile(statePath, "utf8");
629
- }
630
- catch (readErr) {
631
- throw new CorruptFlowStateError(statePath, statePath, readErr);
632
- }
633
- let parsed;
634
- try {
635
- parsed = JSON.parse(raw);
636
- }
637
- catch (parseErr) {
638
- await quarantineCorruptState(statePath, parseErr);
639
- }
640
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
641
- await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
642
- }
643
- return coerceFlowState(parsed).state;
644
- }
645
- /**
646
- * Guarded read wrapper used by runtime hook scripts and the repair CLI.
647
- * Unlike `readFlowState`, it enforces the sha256 sidecar before returning:
648
- * a manual edit to flow-state.json fails fast with
649
- * `FlowStateGuardMismatchError`.
650
- */
651
- export async function readFlowStateGuarded(projectRoot, options = {}) {
652
- void options;
653
- const statePath = flowStatePath(projectRoot);
654
- if (!(await exists(statePath))) {
655
- return createInitialFlowState();
656
- }
657
- let raw;
658
- try {
659
- raw = await fs.readFile(statePath, "utf8");
660
- }
661
- catch (readErr) {
662
- throw new CorruptFlowStateError(statePath, statePath, readErr);
663
- }
664
- await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
665
- let parsed;
666
- try {
667
- parsed = JSON.parse(raw);
668
- }
669
- catch (parseErr) {
670
- await quarantineCorruptState(statePath, parseErr);
671
- }
672
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
673
- await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
674
- }
675
- return coerceFlowState(parsed).state;
676
- }
677
- export async function writeFlowState(projectRoot, state, options = {}) {
678
- const writerSubsystem = options.writerSubsystem?.trim() || DEFAULT_WRITER_SUBSYSTEM;
679
- const doWrite = async () => {
680
- const statePath = flowStatePath(projectRoot);
681
- if (!options.allowReset && (await exists(statePath))) {
682
- try {
683
- const rawExisting = await fs.readFile(statePath, "utf8");
684
- const parsed = JSON.parse(rawExisting);
685
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
686
- const prev = coerceFlowState(parsed).state;
687
- validateFlowTransition(prev, state);
688
- }
689
- }
690
- catch (err) {
691
- if (err instanceof InvalidStageTransitionError) {
692
- throw err;
693
- }
694
- throw new Error(`cannot validate flow-state transition because ${FLOW_STATE_REL_PATH} is unreadable or corrupt (${err instanceof Error ? err.message : String(err)}). Run \`npx cclaw-cli sync\` and reconcile the state before retrying.`);
695
- }
696
- }
697
- const safe = coerceFlowState({ ...state }).state;
698
- const canonicalPayload = `${JSON.stringify(safe, null, 2)}\n`;
699
- const sha256 = canonicalFlowStateShaFromRaw(canonicalPayload);
700
- await writeFileSafe(statePath, canonicalPayload, { mode: 0o600 });
701
- const sidecar = {
702
- sha256,
703
- writtenAt: new Date().toISOString(),
704
- writerSubsystem,
705
- runId: safe.activeRunId
706
- };
707
- await writeFileSafe(guardSidecarPath(projectRoot), `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
708
- };
709
- if (options.skipLock) {
710
- await doWrite();
711
- }
712
- else {
713
- await withDirectoryLock(flowStateLockPath(projectRoot), doWrite);
13
+ await writeFlowState(projectRoot, createInitialFlowStateV8());
714
14
  }
715
15
  }
716
- /**
717
- * Named entry point for the write-guard workstream. Equivalent to
718
- * `writeFlowState`: the write always produces the sha256 sidecar via
719
- * the internal implementation so every existing writer inherits the
720
- * guard without rewriting callsites.
721
- */
722
- export async function writeFlowStateGuarded(projectRoot, state, options = {}) {
723
- await writeFlowState(projectRoot, state, options);
724
- }
725
- /**
726
- * backfill missing `completedStageMeta` rows for any stage that
727
- * already lives in `completedStages` but has no audit timestamp. Uses the
728
- * stage's artifact mtime when available, otherwise the current time. This
729
- * runs as part of `flow-state-repair` so older flow-state.json files
730
- * get their meta carried forward without a destructive rewrite.
731
- */
732
- async function backfillCompletedStageMeta(projectRoot, state) {
733
- const meta = { ...(state.completedStageMeta ?? {}) };
734
- const backfilled = [];
735
- for (const stage of state.completedStages) {
736
- if (meta[stage] && typeof meta[stage].completedAt === "string" && meta[stage].completedAt.length > 0) {
737
- continue;
738
- }
739
- let completedAt = new Date().toISOString();
740
- try {
741
- const { resolveArtifactPath } = await import("./artifact-paths.js");
742
- const resolved = await resolveArtifactPath(stage, {
743
- projectRoot,
744
- track: state.track,
745
- intent: "read"
746
- });
747
- const stat = await fs.stat(resolved.absPath);
748
- completedAt = new Date(stat.mtimeMs).toISOString();
749
- }
750
- catch {
751
- // artifact missing or unreadable — fall back to "now" so the meta row
752
- // is at least consistently populated; operators can re-edit if needed.
753
- }
754
- meta[stage] = { completedAt };
755
- backfilled.push(stage);
756
- }
757
- if (backfilled.length === 0) {
758
- return { state, backfilled };
759
- }
760
- return { state: { ...state, completedStageMeta: meta }, backfilled };
761
- }
762
- /**
763
- * Recompute the write-guard sidecar from the current on-disk flow-state
764
- * contents and append an audit entry to `.cclaw/.flow-state-repair.log`.
765
- * The reason is required so no repair happens without an operator-visible
766
- * rationale. Intended to be called only from the explicit
767
- * `cclaw-cli internal flow-state-repair` subcommand.
768
- */
769
- export async function repairFlowStateGuard(projectRoot, reason) {
770
- const trimmed = reason.trim();
771
- if (trimmed.length === 0) {
772
- throw new Error("flow-state-repair requires --reason=<slug> (e.g. --reason=\"manual_edit_recovery\").");
773
- }
774
- if (!DEFAULT_REPAIR_REASON_PATTERN.test(trimmed)) {
775
- throw new Error("flow-state-repair --reason must match /^[a-z][a-z0-9_-]{2,}$/ (short lowercase slug).");
776
- }
16
+ export async function readFlowState(projectRoot) {
777
17
  const statePath = flowStatePath(projectRoot);
778
18
  if (!(await exists(statePath))) {
779
- throw new Error(`flow-state-repair: ${FLOW_STATE_REL_PATH} does not exist; nothing to repair.`);
780
- }
781
- return withDirectoryLock(flowStateLockPath(projectRoot), async () => {
782
- let raw = await fs.readFile(statePath, "utf8");
783
- let runId = "unknown-run";
784
- let backfilledStages = [];
785
- try {
786
- const parsed = JSON.parse(raw);
787
- const coerced = coerceFlowState(parsed).state;
788
- runId = coerced.activeRunId;
789
- const { state: nextState, backfilled } = await backfillCompletedStageMeta(projectRoot, coerced);
790
- backfilledStages = backfilled;
791
- if (backfilled.length > 0) {
792
- // Persist the migrated state inside the same lock window so the
793
- // sha sidecar below covers the post-migration bytes, not the
794
- // pre-migration ones.
795
- await writeFlowState(projectRoot, nextState, {
796
- allowReset: true,
797
- skipLock: true,
798
- writerSubsystem: "flow-state-repair-backfill"
799
- });
800
- raw = await fs.readFile(statePath, "utf8");
801
- }
802
- }
803
- catch {
804
- // parsing failure falls back to "unknown-run"; repair intentionally
805
- // accepts the contents as-is so operators can recover even from
806
- // borderline JSON after manual edits.
807
- }
808
- const sha256 = canonicalFlowStateShaFromRaw(raw);
809
- const sidecar = {
810
- sha256,
811
- writtenAt: new Date().toISOString(),
812
- writerSubsystem: "flow-state-repair",
813
- runId
814
- };
815
- const guardPath = guardSidecarPath(projectRoot);
816
- await writeFileSafe(guardPath, `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
817
- const logPath = repairLogPath(projectRoot);
818
- await ensureDir(path.dirname(logPath));
819
- const backfillNote = backfilledStages.length > 0
820
- ? ` backfilledCompletedStageMeta=${backfilledStages.join(",")}`
821
- : "";
822
- const logLine = `${sidecar.writtenAt} reason=${trimmed} runId=${sidecar.runId} sha256=${sidecar.sha256}${backfillNote}\n`;
823
- await fs.appendFile(logPath, logLine, "utf8");
824
- return {
825
- sidecar,
826
- repairLogPath: logPath,
827
- guardPath,
828
- completedStageMetaBackfilled: backfilledStages
829
- };
830
- });
831
- }
832
- export function flowStateGuardSidecarPathFor(projectRoot) {
833
- return guardSidecarPath(projectRoot);
834
- }
835
- export function flowStateRepairLogPathFor(projectRoot) {
836
- return repairLogPath(projectRoot);
837
- }
838
- /**
839
- * Exposed path helper so callers that need to serialize a multi-step
840
- * state operation with flow-state writes (e.g. run archival) can
841
- * acquire the SAME lock directory used internally by `writeFlowState`.
842
- */
843
- export function flowStateLockPathFor(projectRoot) {
844
- return flowStateLockPath(projectRoot);
845
- }
846
- export async function ensureRunSystem(projectRoot, options = {}) {
847
- await ensureDir(archiveRoot(projectRoot));
848
- await ensureDir(activeArtifactsPath(projectRoot));
849
- const statePath = flowStatePath(projectRoot);
850
- const state = await readFlowState(projectRoot);
851
- const createIfMissing = options.createIfMissing !== false;
852
- if (createIfMissing && !(await exists(statePath))) {
853
- await writeFlowState(projectRoot, state, { allowReset: true });
854
- }
855
- return state;
19
+ const initial = createInitialFlowStateV8();
20
+ await writeFlowState(projectRoot, initial);
21
+ return initial;
22
+ }
23
+ const raw = await fs.readFile(statePath, "utf8");
24
+ const parsed = JSON.parse(raw);
25
+ assertFlowStateV8(parsed);
26
+ return parsed;
27
+ }
28
+ export async function writeFlowState(projectRoot, state) {
29
+ assertFlowStateV8(state);
30
+ await writeFileSafe(flowStatePath(projectRoot), `${JSON.stringify(state, null, 2)}\n`);
31
+ }
32
+ export async function resetFlowState(projectRoot) {
33
+ await writeFlowState(projectRoot, createInitialFlowStateV8());
34
+ }
35
+ export async function patchFlowState(projectRoot, patch) {
36
+ const current = await readFlowState(projectRoot);
37
+ const next = { ...current, ...patch };
38
+ await writeFlowState(projectRoot, next);
39
+ return next;
856
40
  }