cclaw-cli 7.7.1 → 8.1.1

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 (284) hide show
  1. package/README.md +211 -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 +107 -511
  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/harness-prompt.d.ts +26 -0
  73. package/dist/harness-prompt.js +142 -0
  74. package/dist/install.d.ts +35 -15
  75. package/dist/install.js +238 -1347
  76. package/dist/knowledge-store.d.ts +19 -163
  77. package/dist/knowledge-store.js +56 -590
  78. package/dist/logger.d.ts +8 -3
  79. package/dist/logger.js +13 -4
  80. package/dist/orchestrator-routing.d.ts +29 -0
  81. package/dist/orchestrator-routing.js +156 -0
  82. package/dist/run-persistence.d.ts +7 -118
  83. package/dist/run-persistence.js +29 -845
  84. package/dist/runtime/run-hook.entry.d.ts +1 -3
  85. package/dist/runtime/run-hook.entry.js +19 -4
  86. package/dist/runtime/run-hook.mjs +13 -1024
  87. package/dist/types.d.ts +25 -261
  88. package/dist/types.js +8 -36
  89. package/package.json +6 -3
  90. package/dist/artifact-linter/brainstorm.d.ts +0 -2
  91. package/dist/artifact-linter/brainstorm.js +0 -353
  92. package/dist/artifact-linter/design.d.ts +0 -18
  93. package/dist/artifact-linter/design.js +0 -444
  94. package/dist/artifact-linter/findings-dedup.d.ts +0 -56
  95. package/dist/artifact-linter/findings-dedup.js +0 -232
  96. package/dist/artifact-linter/plan.d.ts +0 -2
  97. package/dist/artifact-linter/plan.js +0 -826
  98. package/dist/artifact-linter/review-army.d.ts +0 -49
  99. package/dist/artifact-linter/review-army.js +0 -520
  100. package/dist/artifact-linter/review.d.ts +0 -2
  101. package/dist/artifact-linter/review.js +0 -113
  102. package/dist/artifact-linter/scope.d.ts +0 -2
  103. package/dist/artifact-linter/scope.js +0 -158
  104. package/dist/artifact-linter/shared.d.ts +0 -637
  105. package/dist/artifact-linter/shared.js +0 -2163
  106. package/dist/artifact-linter/ship.d.ts +0 -2
  107. package/dist/artifact-linter/ship.js +0 -250
  108. package/dist/artifact-linter/spec.d.ts +0 -2
  109. package/dist/artifact-linter/spec.js +0 -176
  110. package/dist/artifact-linter/tdd.d.ts +0 -118
  111. package/dist/artifact-linter/tdd.js +0 -1404
  112. package/dist/artifact-linter.d.ts +0 -15
  113. package/dist/artifact-linter.js +0 -517
  114. package/dist/codex-feature-flag.d.ts +0 -58
  115. package/dist/codex-feature-flag.js +0 -193
  116. package/dist/content/closeout-guidance.d.ts +0 -14
  117. package/dist/content/closeout-guidance.js +0 -44
  118. package/dist/content/diff-command.d.ts +0 -1
  119. package/dist/content/diff-command.js +0 -43
  120. package/dist/content/harness-doc.d.ts +0 -1
  121. package/dist/content/harness-doc.js +0 -65
  122. package/dist/content/hook-events.d.ts +0 -9
  123. package/dist/content/hook-events.js +0 -23
  124. package/dist/content/hook-manifest.d.ts +0 -81
  125. package/dist/content/hook-manifest.js +0 -156
  126. package/dist/content/hooks.d.ts +0 -11
  127. package/dist/content/hooks.js +0 -1972
  128. package/dist/content/idea.d.ts +0 -60
  129. package/dist/content/idea.js +0 -416
  130. package/dist/content/language-policy.d.ts +0 -2
  131. package/dist/content/language-policy.js +0 -13
  132. package/dist/content/learnings.d.ts +0 -6
  133. package/dist/content/learnings.js +0 -141
  134. package/dist/content/observe.d.ts +0 -19
  135. package/dist/content/observe.js +0 -86
  136. package/dist/content/opencode-plugin.d.ts +0 -1
  137. package/dist/content/opencode-plugin.js +0 -635
  138. package/dist/content/review-prompts.d.ts +0 -1
  139. package/dist/content/review-prompts.js +0 -104
  140. package/dist/content/runtime-shared-snippets.d.ts +0 -8
  141. package/dist/content/runtime-shared-snippets.js +0 -80
  142. package/dist/content/session-hooks.d.ts +0 -7
  143. package/dist/content/session-hooks.js +0 -107
  144. package/dist/content/skills-elicitation.d.ts +0 -1
  145. package/dist/content/skills-elicitation.js +0 -167
  146. package/dist/content/stage-command.d.ts +0 -2
  147. package/dist/content/stage-command.js +0 -17
  148. package/dist/content/stage-schema.d.ts +0 -117
  149. package/dist/content/stage-schema.js +0 -955
  150. package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
  151. package/dist/content/stages/_lint-metadata/index.js +0 -97
  152. package/dist/content/stages/brainstorm.d.ts +0 -2
  153. package/dist/content/stages/brainstorm.js +0 -184
  154. package/dist/content/stages/design.d.ts +0 -2
  155. package/dist/content/stages/design.js +0 -288
  156. package/dist/content/stages/index.d.ts +0 -8
  157. package/dist/content/stages/index.js +0 -11
  158. package/dist/content/stages/plan.d.ts +0 -2
  159. package/dist/content/stages/plan.js +0 -191
  160. package/dist/content/stages/review.d.ts +0 -2
  161. package/dist/content/stages/review.js +0 -240
  162. package/dist/content/stages/schema-types.d.ts +0 -203
  163. package/dist/content/stages/schema-types.js +0 -1
  164. package/dist/content/stages/scope.d.ts +0 -2
  165. package/dist/content/stages/scope.js +0 -254
  166. package/dist/content/stages/ship.d.ts +0 -2
  167. package/dist/content/stages/ship.js +0 -159
  168. package/dist/content/stages/spec.d.ts +0 -2
  169. package/dist/content/stages/spec.js +0 -170
  170. package/dist/content/stages/tdd.d.ts +0 -4
  171. package/dist/content/stages/tdd.js +0 -273
  172. package/dist/content/state-contracts.d.ts +0 -1
  173. package/dist/content/state-contracts.js +0 -63
  174. package/dist/content/status-command.d.ts +0 -4
  175. package/dist/content/status-command.js +0 -109
  176. package/dist/content/subagent-context-skills.d.ts +0 -4
  177. package/dist/content/subagent-context-skills.js +0 -279
  178. package/dist/content/subagents.d.ts +0 -3
  179. package/dist/content/subagents.js +0 -997
  180. package/dist/content/templates.d.ts +0 -26
  181. package/dist/content/templates.js +0 -1692
  182. package/dist/content/track-render-context.d.ts +0 -18
  183. package/dist/content/track-render-context.js +0 -53
  184. package/dist/content/tree-command.d.ts +0 -1
  185. package/dist/content/tree-command.js +0 -64
  186. package/dist/content/utility-skills.d.ts +0 -30
  187. package/dist/content/utility-skills.js +0 -160
  188. package/dist/content/view-command.d.ts +0 -2
  189. package/dist/content/view-command.js +0 -92
  190. package/dist/delegation.d.ts +0 -649
  191. package/dist/delegation.js +0 -1539
  192. package/dist/early-loop.d.ts +0 -70
  193. package/dist/early-loop.js +0 -302
  194. package/dist/execution-topology.d.ts +0 -44
  195. package/dist/execution-topology.js +0 -95
  196. package/dist/gate-evidence.d.ts +0 -85
  197. package/dist/gate-evidence.js +0 -631
  198. package/dist/harness-adapters.d.ts +0 -151
  199. package/dist/harness-adapters.js +0 -756
  200. package/dist/harness-selection.d.ts +0 -31
  201. package/dist/harness-selection.js +0 -214
  202. package/dist/hook-schema.d.ts +0 -6
  203. package/dist/hook-schema.js +0 -114
  204. package/dist/hook-schemas/claude-hooks.v1.json +0 -10
  205. package/dist/hook-schemas/codex-hooks.v1.json +0 -10
  206. package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
  207. package/dist/init-detect.d.ts +0 -2
  208. package/dist/init-detect.js +0 -50
  209. package/dist/internal/advance-stage/advance.d.ts +0 -89
  210. package/dist/internal/advance-stage/advance.js +0 -655
  211. package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
  212. package/dist/internal/advance-stage/cancel-run.js +0 -19
  213. package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
  214. package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
  215. package/dist/internal/advance-stage/helpers.d.ts +0 -14
  216. package/dist/internal/advance-stage/helpers.js +0 -145
  217. package/dist/internal/advance-stage/hook.d.ts +0 -8
  218. package/dist/internal/advance-stage/hook.js +0 -40
  219. package/dist/internal/advance-stage/parsers.d.ts +0 -72
  220. package/dist/internal/advance-stage/parsers.js +0 -357
  221. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
  222. package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
  223. package/dist/internal/advance-stage/review-loop.d.ts +0 -16
  224. package/dist/internal/advance-stage/review-loop.js +0 -199
  225. package/dist/internal/advance-stage/rewind.d.ts +0 -14
  226. package/dist/internal/advance-stage/rewind.js +0 -108
  227. package/dist/internal/advance-stage/start-flow.d.ts +0 -13
  228. package/dist/internal/advance-stage/start-flow.js +0 -241
  229. package/dist/internal/advance-stage/verify.d.ts +0 -21
  230. package/dist/internal/advance-stage/verify.js +0 -185
  231. package/dist/internal/advance-stage.d.ts +0 -7
  232. package/dist/internal/advance-stage.js +0 -138
  233. package/dist/internal/cohesion-contract-stub.d.ts +0 -24
  234. package/dist/internal/cohesion-contract-stub.js +0 -148
  235. package/dist/internal/compound-readiness.d.ts +0 -23
  236. package/dist/internal/compound-readiness.js +0 -102
  237. package/dist/internal/detect-public-api-changes.d.ts +0 -5
  238. package/dist/internal/detect-public-api-changes.js +0 -45
  239. package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
  240. package/dist/internal/detect-supply-chain-changes.js +0 -138
  241. package/dist/internal/early-loop-status.d.ts +0 -7
  242. package/dist/internal/early-loop-status.js +0 -93
  243. package/dist/internal/envelope-validate.d.ts +0 -7
  244. package/dist/internal/envelope-validate.js +0 -66
  245. package/dist/internal/flow-state-repair.d.ts +0 -20
  246. package/dist/internal/flow-state-repair.js +0 -104
  247. package/dist/internal/plan-split-waves.d.ts +0 -190
  248. package/dist/internal/plan-split-waves.js +0 -764
  249. package/dist/internal/runtime-integrity.d.ts +0 -7
  250. package/dist/internal/runtime-integrity.js +0 -268
  251. package/dist/internal/slice-commit.d.ts +0 -7
  252. package/dist/internal/slice-commit.js +0 -619
  253. package/dist/internal/tdd-loop-status.d.ts +0 -14
  254. package/dist/internal/tdd-loop-status.js +0 -68
  255. package/dist/internal/tdd-red-evidence.d.ts +0 -7
  256. package/dist/internal/tdd-red-evidence.js +0 -153
  257. package/dist/internal/waiver-grant.d.ts +0 -62
  258. package/dist/internal/waiver-grant.js +0 -294
  259. package/dist/internal/wave-status.d.ts +0 -74
  260. package/dist/internal/wave-status.js +0 -506
  261. package/dist/managed-resources.d.ts +0 -53
  262. package/dist/managed-resources.js +0 -313
  263. package/dist/policy.d.ts +0 -10
  264. package/dist/policy.js +0 -167
  265. package/dist/retro-gate.d.ts +0 -9
  266. package/dist/retro-gate.js +0 -47
  267. package/dist/run-archive.d.ts +0 -61
  268. package/dist/run-archive.js +0 -391
  269. package/dist/runs.d.ts +0 -2
  270. package/dist/runs.js +0 -2
  271. package/dist/stack-detection.d.ts +0 -116
  272. package/dist/stack-detection.js +0 -489
  273. package/dist/streaming/event-stream.d.ts +0 -31
  274. package/dist/streaming/event-stream.js +0 -114
  275. package/dist/tdd-cycle.d.ts +0 -107
  276. package/dist/tdd-cycle.js +0 -289
  277. package/dist/tdd-verification-evidence.d.ts +0 -17
  278. package/dist/tdd-verification-evidence.js +0 -122
  279. package/dist/track-heuristics.d.ts +0 -27
  280. package/dist/track-heuristics.js +0 -154
  281. package/dist/util/slice-id.d.ts +0 -58
  282. package/dist/util/slice-id.js +0 -89
  283. package/dist/worktree-manager.d.ts +0 -20
  284. 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
  }