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,1972 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { RUNTIME_ROOT } from "../constants.js";
5
- import { DELEGATION_DISPATCH_SURFACES, DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES, DELEGATION_PHASES } from "../delegation.js";
6
- function resolveCliRuntimeForGeneratedHook() {
7
- const here = fileURLToPath(import.meta.url);
8
- // Vitest runs init/sync from src/ and expects helpers to execute the same
9
- // source runtime, even when a stale dist/ exists in the repository.
10
- if (process.env.VITEST === "true") {
11
- const sourceCli = path.resolve(path.dirname(here), "..", "cli.ts");
12
- const viteNode = path.resolve(path.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
13
- if (existsSync(sourceCli) && existsSync(viteNode)) {
14
- return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
15
- }
16
- }
17
- const candidates = [
18
- path.resolve(path.dirname(here), "..", "cli.js"),
19
- path.resolve(path.dirname(here), "..", "..", "dist", "cli.js")
20
- ];
21
- for (const candidate of candidates) {
22
- // Synchronous probe runs only during cclaw-cli init/sync generation.
23
- // The generated hook receives a concrete path and does not need a global bin.
24
- if (existsSync(candidate))
25
- return { entrypoint: candidate, argsPrefix: [] };
26
- }
27
- return { entrypoint: null, argsPrefix: [] };
28
- }
29
- function internalHelperScript(helperName, internalSubcommand, usage, options) {
30
- const cliRuntime = resolveCliRuntimeForGeneratedHook();
31
- return `#!/usr/bin/env node
32
- import fs from "node:fs/promises";
33
- import path from "node:path";
34
- import process from "node:process";
35
- import { spawn } from "node:child_process";
36
-
37
- const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
38
- const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
39
- const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
40
- const HELPER_NAME = ${JSON.stringify(helperName)};
41
- const INTERNAL_SUBCOMMAND = ${JSON.stringify(internalSubcommand)};
42
- const USAGE = ${JSON.stringify(usage)};
43
- const POSITIONAL_ARG_NAME = ${JSON.stringify(options?.positionalArgName ?? null)};
44
- const POSITIONAL_ARG_REQUIRED = ${JSON.stringify(options?.positionalArgRequired === true)};
45
- const DEFAULT_QUIET_ENV_VAR = ${JSON.stringify(options?.defaultQuietEnvVar ?? null)};
46
-
47
- async function detectRoot() {
48
- const candidates = [
49
- process.env.CCLAW_PROJECT_ROOT,
50
- process.env.CLAUDE_PROJECT_DIR,
51
- process.env.CURSOR_PROJECT_DIR,
52
- process.env.CURSOR_PROJECT_ROOT,
53
- process.env.OPENCODE_PROJECT_DIR,
54
- process.env.OPENCODE_PROJECT_ROOT,
55
- process.cwd()
56
- ].filter((value) => typeof value === "string" && value.length > 0);
57
-
58
- for (const candidate of candidates) {
59
- try {
60
- const runtimePath = path.join(candidate, RUNTIME_ROOT);
61
- const stat = await fs.stat(runtimePath);
62
- if (stat.isDirectory()) return candidate;
63
- } catch {
64
- // continue
65
- }
66
- }
67
- return candidates[0] || process.cwd();
68
- }
69
-
70
- function printUsage() {
71
- process.stderr.write(USAGE + "\\n");
72
- }
73
-
74
- async function main() {
75
- const [, , ...argvTokens] = process.argv;
76
- if (argvTokens.includes("--help") || argvTokens.includes("-h")) {
77
- printUsage();
78
- return;
79
- }
80
- let positionalArg = "";
81
- let flags = argvTokens;
82
- if (POSITIONAL_ARG_NAME !== null) {
83
- positionalArg = (argvTokens[0] ?? "").trim();
84
- flags = argvTokens.slice(1);
85
- if (POSITIONAL_ARG_REQUIRED && positionalArg.length === 0) {
86
- printUsage();
87
- process.exitCode = 1;
88
- return;
89
- }
90
- }
91
-
92
- if (DEFAULT_QUIET_ENV_VAR !== null) {
93
- const envRaw = process.env[DEFAULT_QUIET_ENV_VAR];
94
- if (typeof envRaw !== "string" || envRaw.trim().length === 0) {
95
- process.env[DEFAULT_QUIET_ENV_VAR] = "1";
96
- }
97
- const quietRaw = (process.env[DEFAULT_QUIET_ENV_VAR] ?? "").trim().toLowerCase();
98
- const quietEnabled = !/^(0|false|no|off)$/u.test(quietRaw);
99
- const alreadyQuiet = flags.includes("--quiet");
100
- if (quietEnabled && !alreadyQuiet) {
101
- flags = [...flags, "--quiet"];
102
- }
103
- }
104
-
105
- const root = await detectRoot();
106
- const runtimePath = path.join(root, RUNTIME_ROOT);
107
- try {
108
- const stat = await fs.stat(runtimePath);
109
- if (!stat.isDirectory()) throw new Error("not-dir");
110
- } catch {
111
- process.stderr.write("[cclaw] " + HELPER_NAME + ": runtime root not found at " + runtimePath + "\\n");
112
- process.exitCode = 1;
113
- return;
114
- }
115
-
116
- const cliEntrypoint = process.env.CCLAW_CLI_JS || CCLAW_CLI_ENTRYPOINT;
117
- const cliArgsPrefix = process.env.CCLAW_CLI_JS ? [] : CCLAW_CLI_ARGS_PREFIX;
118
- if (!cliEntrypoint || cliEntrypoint.trim().length === 0) {
119
- process.stderr.write(
120
- "[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint is missing. Re-run npx cclaw-cli sync, or set CCLAW_CLI_JS=/absolute/path/to/dist/cli.js for this session.\\n"
121
- );
122
- process.exitCode = 1;
123
- return;
124
- }
125
-
126
- try {
127
- const stat = await fs.stat(cliEntrypoint);
128
- if (!stat.isFile()) throw new Error("not-file");
129
- for (const argPath of cliArgsPrefix) {
130
- if (typeof argPath !== "string" || argPath.startsWith("-")) continue;
131
- const argStat = await fs.stat(argPath);
132
- if (!argStat.isFile()) throw new Error("arg-not-file");
133
- }
134
- } catch {
135
- process.stderr.write(
136
- "[cclaw] " + HELPER_NAME + ": local Node runtime entrypoint not found at " + cliEntrypoint + ". Re-run npx cclaw-cli sync, or set CCLAW_CLI_JS=/absolute/path/to/dist/cli.js for this session.\\n"
137
- );
138
- process.exitCode = 1;
139
- return;
140
- }
141
-
142
- const internalArgs =
143
- POSITIONAL_ARG_NAME !== null
144
- ? [INTERNAL_SUBCOMMAND, positionalArg, ...flags]
145
- : [INTERNAL_SUBCOMMAND, ...flags];
146
-
147
- const child = spawn(process.execPath, [cliEntrypoint, ...cliArgsPrefix, "internal", ...internalArgs], {
148
- cwd: root,
149
- env: process.env,
150
- stdio: "inherit"
151
- });
152
- let spawnErrored = false;
153
-
154
- child.on("error", (error) => {
155
- spawnErrored = true;
156
- const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
157
- if (code === "ENOENT") {
158
- process.stderr.write(
159
- "[cclaw] " + HELPER_NAME + ": node executable not found while invoking local runtime. Re-run npx cclaw-cli sync.\\n"
160
- );
161
- } else {
162
- process.stderr.write(
163
- "[cclaw] " + HELPER_NAME + ": failed to invoke local Node runtime (" +
164
- (error instanceof Error ? error.message : String(error)) +
165
- ").\\n"
166
- );
167
- }
168
- process.exitCode = 1;
169
- });
170
-
171
- child.on("close", (code, signal) => {
172
- if (spawnErrored) {
173
- process.exitCode = 1;
174
- return;
175
- }
176
- if (signal) {
177
- process.exitCode = 1;
178
- return;
179
- }
180
- process.exitCode = typeof code === "number" && code >= 0 ? code : 1;
181
- });
182
- }
183
-
184
- void main();
185
- `;
186
- }
187
- export function startFlowScript() {
188
- return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--discovery-mode=<lean|guided|deep>] [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
189
- }
190
- export function cancelRunScript() {
191
- return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
192
- }
193
- export function stageCompleteScript() {
194
- return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver=<token>] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
195
- positionalArgName: "stage",
196
- positionalArgRequired: true,
197
- defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
198
- });
199
- }
200
- export function delegationRecordScript() {
201
- return `#!/usr/bin/env node
202
- import { createHash } from "node:crypto";
203
- import { spawn } from "node:child_process";
204
- import fs from "node:fs/promises";
205
- import path from "node:path";
206
- import process from "node:process";
207
-
208
- const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
209
- const VALID_STATUSES = new Set(["scheduled", "launched", "acknowledged", "completed", "failed", "waived", "stale"]);
210
- const TERMINAL = new Set(["completed", "failed", "waived", "stale"]);
211
- const VALID_DISPATCH_SURFACES = ${JSON.stringify([...DELEGATION_DISPATCH_SURFACES])};
212
- const VALID_DISPATCH_SURFACES_SET = new Set(VALID_DISPATCH_SURFACES);
213
- const SURFACE_PATH_PREFIXES = ${JSON.stringify(DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES)};
214
- const VALID_DELEGATION_PHASES = ${JSON.stringify([...DELEGATION_PHASES])};
215
- const VALID_DELEGATION_PHASES_SET = new Set(VALID_DELEGATION_PHASES);
216
- const LEDGER_SCHEMA_VERSION = 3;
217
- const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
218
-
219
- async function verifyFlowStateGuardInline(root) {
220
- const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
221
- const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
222
- let raw;
223
- try {
224
- raw = await fs.readFile(statePath, "utf8");
225
- } catch {
226
- return;
227
- }
228
- let guard;
229
- try {
230
- const guardRaw = await fs.readFile(guardPath, "utf8");
231
- guard = JSON.parse(guardRaw);
232
- } catch {
233
- return;
234
- }
235
- if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") return;
236
- const actual = createHash("sha256").update(raw, "utf8").digest("hex");
237
- if (actual === guard.sha256) return;
238
- process.stderr.write(
239
- "[cclaw] delegation-record: flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
240
- "expected sha: " + guard.sha256 + "\\n" +
241
- "actual sha: " + actual + "\\n" +
242
- "last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
243
- "do not edit flow-state.json by hand. To recover, run:\\n" +
244
- " cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
245
- );
246
- process.exit(2);
247
- }
248
-
249
- function parseArgs(argv) {
250
- const args = {};
251
- for (const raw of argv) {
252
- const valueMatch = /^--([^=]+)=(.*)$/u.exec(raw);
253
- if (valueMatch) {
254
- args[valueMatch[1]] = valueMatch[2];
255
- continue;
256
- }
257
- const flagMatch = /^--([^=]+)$/u.exec(raw);
258
- if (flagMatch) args[flagMatch[1]] = true;
259
- }
260
- return args;
261
- }
262
-
263
- async function exists(filePath) {
264
- try {
265
- await fs.access(filePath);
266
- return true;
267
- } catch {
268
- return false;
269
- }
270
- }
271
-
272
- async function detectRoot() {
273
- const candidates = [
274
- process.env.CCLAW_PROJECT_ROOT,
275
- process.env.CLAUDE_PROJECT_DIR,
276
- process.env.CURSOR_PROJECT_DIR,
277
- process.env.CURSOR_PROJECT_ROOT,
278
- process.env.OPENCODE_PROJECT_DIR,
279
- process.env.OPENCODE_PROJECT_ROOT,
280
- process.cwd()
281
- ].filter((value) => typeof value === "string" && value.length > 0);
282
- for (const candidate of candidates) {
283
- if (await exists(path.join(candidate, RUNTIME_ROOT))) return candidate;
284
- }
285
- return candidates[0] || process.cwd();
286
- }
287
-
288
- async function readRunId(root) {
289
- try {
290
- const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
291
- const parsed = JSON.parse(raw);
292
- return typeof parsed.activeRunId === "string" ? parsed.activeRunId : "unknown-run";
293
- } catch {
294
- return "unknown-run";
295
- }
296
- }
297
-
298
- // Read \`tddGreenMinElapsedMs\` from flow-state.json. Defaults to 4000ms
299
- // when missing or invalid. Operators set 0 to disable the freshness floor
300
- // while keeping RED-test-name and passing-assertion checks active.
301
- async function readTddGreenMinElapsedMsInline(root) {
302
- try {
303
- const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
304
- const parsed = JSON.parse(raw);
305
- if (parsed && typeof parsed.tddGreenMinElapsedMs === "number" && parsed.tddGreenMinElapsedMs >= 0) {
306
- return Math.floor(parsed.tddGreenMinElapsedMs);
307
- }
308
- return 4000;
309
- } catch {
310
- return 4000;
311
- }
312
- }
313
-
314
- // Match the RED test name into the GREEN evidenceRef. Returns the
315
- // basename or stem (without extension) of the most-specific path token
316
- // in the RED row's first evidenceRef. We deliberately use a substring
317
- // match, not equality, so callers can include richer text like
318
- // "REGRESSION: cargo test --test foo => 8 passed; 0 failed".
319
- function extractRedTestNameInline(redEvidenceRef) {
320
- if (typeof redEvidenceRef !== "string") return null;
321
- const trimmed = redEvidenceRef.trim();
322
- if (trimmed.length === 0) return null;
323
- // Path-shaped token (foo/bar/baz_test.rs or src/foo.test.ts).
324
- const pathMatch = /[A-Za-z0-9_./-]+/u.exec(trimmed);
325
- if (pathMatch) {
326
- const token = pathMatch[0];
327
- const slashIdx = token.lastIndexOf("/");
328
- const base = slashIdx >= 0 ? token.slice(slashIdx + 1) : token;
329
- const dotIdx = base.indexOf(".");
330
- const stem = dotIdx > 0 ? base.slice(0, dotIdx) : base;
331
- if (stem.length >= 4) return stem;
332
- return base;
333
- }
334
- return trimmed;
335
- }
336
-
337
- // Match canonical runner pass lines using language-agnostic examples:
338
- // Node/TS (vitest/jest): "=> N passed; 0 failed" or "Tests: N passed"
339
- // Python (pytest): "===== N passed in 0.42s ====="
340
- // Go (go test): "ok pkg 0.123s"
341
- // Rust (cargo test): "test result: ok. N passed; 0 failed"
342
- // Java/JVM (maven/surefire): "Tests run: N, Failures: 0, Errors: 0"
343
- // We accept a generic "passed/failed" shape plus runner-specific patterns.
344
- const GREEN_PASS_PATTERNS = [
345
- /=>\\s*\\d+\\s+passed/iu,
346
- /\\b\\d+\\s+passed[;,]\\s*0\\s+failed\\b/iu,
347
- /\\btest\\s+result:\\s*ok\\b/iu,
348
- /\\b\\d+\\s+passed\\s+in\\s+\\d+(?:\\.\\d+)?\\s*s\\b/iu,
349
- /^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu,
350
- /tests\\s+run\\s*:\\s*\\d+\\s*,\\s*failures\\s*:\\s*0\\s*,\\s*errors\\s*:\\s*0/iu
351
- ];
352
-
353
- function matchesPassingAssertionInline(value) {
354
- if (typeof value !== "string") return false;
355
- return GREEN_PASS_PATTERNS.some((re) => re.test(value));
356
- }
357
-
358
- async function readDelegationEvents(root) {
359
- try {
360
- const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-events.jsonl"), "utf8");
361
- return raw
362
- .split(/\\r?\\n/u)
363
- .filter((line) => line.trim().length > 0)
364
- .map((line) => {
365
- try {
366
- return JSON.parse(line);
367
- } catch {
368
- return null;
369
- }
370
- })
371
- .filter((event) => event && typeof event === "object");
372
- } catch {
373
- return [];
374
- }
375
- }
376
-
377
- async function appendAuditEventInline(root, payload) {
378
- const stateDir = path.join(root, RUNTIME_ROOT, "state");
379
- await fs.mkdir(stateDir, { recursive: true });
380
- await fs.appendFile(
381
- path.join(stateDir, "delegation-events.jsonl"),
382
- JSON.stringify(payload) + "\\n",
383
- { encoding: "utf8", mode: 0o600 }
384
- );
385
- }
386
-
387
- function hasPriorAck(events, args, runId) {
388
- return events.some((event) =>
389
- event.runId === runId &&
390
- event.stage === args.stage &&
391
- event.agent === args.agent &&
392
- event.spanId === args["span-id"] &&
393
- event.event === "acknowledged" &&
394
- typeof event.ackTs === "string" &&
395
- event.ackTs.length > 0
396
- );
397
- }
398
-
399
- function usage() {
400
- process.stderr.write([
401
- "Usage:",
402
- " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--reason=<slug>] [--json]",
403
- " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
404
- " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
405
- " node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped [--audit-reason=\\\"<comma-separated reasons>\\\"] [--slice-ids=\\\"S-1,S-2\\\"] [--json] # non-delegation audit row",
406
- "",
407
- "Allowed --dispatch-surface values:",
408
- " " + VALID_DISPATCH_SURFACES.join(", "),
409
- "",
410
- "Per-surface allowed --agent-definition-path prefixes:",
411
- ...VALID_DISPATCH_SURFACES.map((surface) => " " + surface + ": " + (SURFACE_PATH_PREFIXES[surface].length === 0 ? "(any)" : SURFACE_PATH_PREFIXES[surface].join(", "))),
412
- "",
413
- "Dispatch dedup:",
414
- " --supersede=<prevSpanId> close the previous active span on this (stage, agent) as 'stale' before recording the new scheduled row",
415
- " --allow-parallel record both spans as concurrent; new row is tagged allowParallel: true",
416
- "",
417
- "TDD parallel scheduler:",
418
- " --paths=<a,b,c> repo-relative paths the slice-builder will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
419
- " --override-cap=<int> raise the slice worker fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_BUILDERS overrides globally)",
420
- " --reason=<slug> required with --override-cap so cap bypasses are auditable (e.g. red-checkpoint-retry)",
421
- "",
422
- "TDD slice phase tagging:",
423
- " --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
424
- " --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
425
- " --refactor-rationale=<t> required for deferred refactor paths; must be >=80 chars and mention slice + task context (e.g. S-12 / T-103).",
426
- " --refactor-outcome=<m> one of inline|deferred. Folds REFACTOR into the phase=green event so a single row can close RED→GREEN→REFACTOR. Pair --refactor-outcome=deferred with --refactor-rationale.",
427
- " --risk-tier=<t> one of low|medium|high. high triggers integration-overseer in conditional mode.",
428
- ""
429
- ].join("\\n") + "\\n");
430
- }
431
-
432
- function emitProblems(problems, json, code) {
433
- const exitCode = typeof code === "number" ? code : 1;
434
- if (json) {
435
- process.stdout.write(JSON.stringify({ ok: false, problems, allowedDispatchSurfaces: VALID_DISPATCH_SURFACES }, null, 2) + "\\n");
436
- } else {
437
- usage();
438
- process.stderr.write("[cclaw] delegation-record: " + problems.join("; ") + "\\n");
439
- }
440
- process.exitCode = exitCode;
441
- }
442
-
443
- function emitErrorJson(error, details, json) {
444
- if (json) {
445
- process.stdout.write(JSON.stringify({ ok: false, error, details }, null, 2) + "\\n");
446
- } else {
447
- process.stderr.write("[cclaw] delegation-record: error: " + error + " — " + JSON.stringify(details) + "\\n");
448
- }
449
- process.exit(2);
450
- }
451
-
452
- // keep in sync with validateMonotonicTimestamps in src/delegation.ts
453
- function validateMonotonicTimestampsInline(stamped, prior) {
454
- const startTs = stamped.startTs;
455
- if (stamped.launchedTs && startTs && stamped.launchedTs < startTs) {
456
- return { field: "launchedTs", actual: stamped.launchedTs, bound: startTs };
457
- }
458
- if (stamped.ackTs) {
459
- const ackBound = stamped.launchedTs || startTs;
460
- if (ackBound && stamped.ackTs < ackBound) {
461
- return { field: "ackTs", actual: stamped.ackTs, bound: ackBound };
462
- }
463
- }
464
- if (stamped.completedTs) {
465
- const completedBound = stamped.ackTs || stamped.launchedTs || startTs;
466
- if (completedBound && stamped.completedTs < completedBound) {
467
- return { field: "completedTs", actual: stamped.completedTs, bound: completedBound };
468
- }
469
- }
470
- if (!stamped.spanId) return null;
471
- const priorForSpan = (prior || []).filter((entry) => entry && entry.spanId === stamped.spanId);
472
- if (priorForSpan.length === 0) return null;
473
- const tsValues = priorForSpan
474
- .map((entry) => entry.ts || entry.startTs || "")
475
- .filter((ts) => ts.length > 0);
476
- if (tsValues.length === 0) return null;
477
- let latest = tsValues[0];
478
- for (let i = 1; i < tsValues.length; i += 1) {
479
- if (tsValues[i] > latest) latest = tsValues[i];
480
- }
481
- const stampedTs = stamped.ts || stamped.startTs || "";
482
- if (stampedTs && stampedTs < latest) {
483
- return { field: "ts", actual: stampedTs, bound: latest };
484
- }
485
- return null;
486
- }
487
-
488
- function normalizeRelPath(value) {
489
- return String(value || "").replace(/\\\\/gu, "/").replace(/^\\.\\//u, "");
490
- }
491
-
492
- function dispatchSurfaceMatchesPath(surface, agentDefinitionPath) {
493
- const allowed = SURFACE_PATH_PREFIXES[surface] || [];
494
- if (allowed.length === 0) return true;
495
- const normalized = normalizeRelPath(agentDefinitionPath);
496
- return allowed.some((prefix) => normalized === prefix.replace(/\\/$/u, "") || normalized.startsWith(prefix));
497
- }
498
-
499
- async function pathExists(filePath) {
500
- try {
501
- const stat = await fs.stat(filePath);
502
- return stat.isFile() || stat.isDirectory();
503
- } catch {
504
- return false;
505
- }
506
- }
507
-
508
- function normalizeEvidenceRefs(args) {
509
- if (Array.isArray(args["evidence-refs"])) {
510
- return args["evidence-refs"]
511
- .filter((ref) => typeof ref === "string" && ref.trim().length > 0)
512
- .map((ref) => ref.trim());
513
- }
514
- if (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) {
515
- return [args["evidence-ref"].trim()];
516
- }
517
- return [];
518
- }
519
-
520
- function validateDeferredRationaleInline(rationaleRaw, args) {
521
- const rationale = typeof rationaleRaw === "string" ? rationaleRaw.trim() : "";
522
- if (rationale.length === 0) {
523
- return "missing";
524
- }
525
- if (rationale.length < 80) {
526
- return "too-short";
527
- }
528
- const lower = rationale.toLowerCase();
529
- const sliceRaw = typeof args.slice === "string" ? args.slice.trim().toLowerCase() : "";
530
- const hasSliceMention =
531
- (sliceRaw.length > 0 && lower.includes(sliceRaw)) ||
532
- /\\bs-\\d+\\b/iu.test(rationale);
533
- const hasTaskMention =
534
- /\\bt-\\d{3}[a-z]?(?:\\.\\d{1,3})?\\b/iu.test(rationale) ||
535
- /\\btask\\b/iu.test(rationale);
536
- if (!hasSliceMention || !hasTaskMention) {
537
- return "missing-context";
538
- }
539
- return "ok";
540
- }
541
-
542
- function buildRow(args, status, runId, now, options) {
543
- const fulfillmentMode = args["dispatch-surface"] === "role-switch"
544
- ? "role-switch"
545
- : args["dispatch-surface"] === "cursor-task" || args["dispatch-surface"] === "generic-task"
546
- ? "generic-dispatch"
547
- : "isolated";
548
- // Inherit the span's startTs from prior rows so monotonic validation
549
- // can compare against the original schedule, not the row write time.
550
- const startTs = (options && options.spanStartTs) || now;
551
- // claimedPaths from --paths=<comma-separated>. Empty arrays are dropped.
552
- const claimedPathsRaw = typeof args.paths === "string" ? args.paths : "";
553
- const claimedPaths = claimedPathsRaw
554
- .split(",")
555
- .map((value) => value.trim())
556
- .filter((value) => value.length > 0);
557
- // TDD slice tagging via --slice / --phase. Phase must be one of the
558
- // canonical enum values; the inline validator rejects unknown phases
559
- // before the row hits the ledger.
560
- const sliceId =
561
- typeof args.slice === "string" && args.slice.trim().length > 0
562
- ? args.slice.trim()
563
- : undefined;
564
- const phase =
565
- typeof args.phase === "string" && args.phase.trim().length > 0
566
- ? args.phase.trim()
567
- : undefined;
568
- // When --refactor-rationale is supplied it is folded into
569
- // evidenceRefs[0] so the linter (which reads evidenceRefs only) can
570
- // surface the rationale without touching new fields. The user may
571
- // also pass --evidence-ref containing the rationale text.
572
- let resolvedEvidenceRefs = normalizeEvidenceRefs(args);
573
- if (
574
- phase === "refactor-deferred" &&
575
- typeof args["refactor-rationale"] === "string" &&
576
- args["refactor-rationale"].trim().length > 0
577
- ) {
578
- const rationale = args["refactor-rationale"].trim();
579
- if (!resolvedEvidenceRefs.includes(rationale)) {
580
- resolvedEvidenceRefs = [rationale, ...resolvedEvidenceRefs];
581
- }
582
- }
583
- // refactorOutcome folds REFACTOR into a phase=green event. We also
584
- // accept it on phase=refactor / phase=refactor-deferred for controllers
585
- // that emit it on the per-phase lifecycle. When mode=deferred and a
586
- // --refactor-rationale is supplied we mirror the rationale into
587
- // evidenceRefs[0] so the linter keeps reading evidence (matches the
588
- // refactor-deferred behavior).
589
- const refactorOutcomeMode =
590
- typeof args["refactor-outcome"] === "string"
591
- ? args["refactor-outcome"].trim()
592
- : "";
593
- let refactorOutcome;
594
- if (refactorOutcomeMode === "inline" || refactorOutcomeMode === "deferred") {
595
- const rationaleRaw =
596
- typeof args["refactor-rationale"] === "string"
597
- ? args["refactor-rationale"].trim()
598
- : "";
599
- refactorOutcome = {
600
- mode: refactorOutcomeMode,
601
- ...(rationaleRaw.length > 0 ? { rationale: rationaleRaw } : {})
602
- };
603
- if (
604
- refactorOutcomeMode === "deferred" &&
605
- rationaleRaw.length > 0 &&
606
- !resolvedEvidenceRefs.includes(rationaleRaw)
607
- ) {
608
- resolvedEvidenceRefs = [rationaleRaw, ...resolvedEvidenceRefs];
609
- }
610
- }
611
- const riskTierRaw =
612
- typeof args["risk-tier"] === "string" ? args["risk-tier"].trim() : "";
613
- const riskTier =
614
- riskTierRaw === "low" || riskTierRaw === "medium" || riskTierRaw === "high"
615
- ? riskTierRaw
616
- : undefined;
617
- const worktreePath =
618
- typeof args["worktree-path"] === "string" && args["worktree-path"].trim().length > 0
619
- ? args["worktree-path"].trim()
620
- : undefined;
621
- return {
622
- stage: args.stage,
623
- agent: args.agent,
624
- mode: args.mode,
625
- status,
626
- spanId: args["span-id"],
627
- dispatchId: args["dispatch-id"],
628
- workerRunId: args["worker-run-id"],
629
- dispatchSurface: args["dispatch-surface"],
630
- agentDefinitionPath: args["agent-definition-path"],
631
- fulfillmentMode,
632
- waiverReason: args["waiver-reason"],
633
- evidenceRefs: resolvedEvidenceRefs,
634
- runId,
635
- startTs,
636
- ts: now,
637
- launchedTs: args["launched-ts"] || (status === "launched" ? now : undefined),
638
- ackTs: args["ack-ts"] || (status === "acknowledged" ? now : undefined),
639
- completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
640
- endTs: TERMINAL.has(status) ? now : undefined,
641
- schemaVersion: LEDGER_SCHEMA_VERSION,
642
- allowParallel: args["allow-parallel"] === true ? true : undefined,
643
- claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined,
644
- worktreePath,
645
- sliceId,
646
- phase,
647
- refactorOutcome,
648
- riskTier
649
- };
650
- }
651
-
652
- async function readDelegationLedgerEntries(root) {
653
- try {
654
- const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-log.json"), "utf8");
655
- const parsed = JSON.parse(raw);
656
- if (parsed && Array.isArray(parsed.entries)) return parsed.entries;
657
- } catch {
658
- // empty / missing ledger is fine for dedup + monotonicity checks
659
- }
660
- return [];
661
- }
662
-
663
- // keep in sync with findActiveSpanForPair / DispatchDuplicateError in src/delegation.ts
664
- function findActiveSpanForPairInline(stage, agent, runId, entries) {
665
- const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
666
- const effectiveTs = (entry) =>
667
- entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
668
- const latestBySpan = new Map();
669
- for (const entry of entries) {
670
- if (!entry || typeof entry !== "object") continue;
671
- if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
672
- // Strict run-scope: entries without a runId are treated as foreign so
673
- // they cannot keep an old span "active" across runs and trip
674
- // dispatch_duplicate on a fresh dispatch.
675
- if (typeof entry.runId !== "string" || entry.runId.length === 0) continue;
676
- if (entry.runId !== runId) continue;
677
- if (entry.stage !== stage || entry.agent !== agent) continue;
678
- const existing = latestBySpan.get(entry.spanId);
679
- if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
680
- latestBySpan.set(entry.spanId, entry);
681
- }
682
- }
683
- for (const entry of latestBySpan.values()) {
684
- if (ACTIVE_STATUSES.has(entry.status)) return entry;
685
- }
686
- return null;
687
- }
688
-
689
- // keep in sync with computeActiveSubagents in src/delegation.ts
690
- function computeActiveSubagentsInline(entries) {
691
- const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
692
- const effectiveTs = (entry) =>
693
- entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
694
- const latestBySpan = new Map();
695
- for (const entry of entries) {
696
- if (!entry || typeof entry !== "object") continue;
697
- if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
698
- const existing = latestBySpan.get(entry.spanId);
699
- if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
700
- latestBySpan.set(entry.spanId, entry);
701
- }
702
- }
703
- const active = [];
704
- for (const entry of latestBySpan.values()) {
705
- if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
706
- }
707
- return active;
708
- }
709
-
710
- // keep in sync with validateFileOverlap in src/delegation.ts
711
- function validateFileOverlapInline(stamped, activeEntries) {
712
- if (stamped.agent !== "slice-builder" || stamped.stage !== "tdd") {
713
- return { autoParallel: false, conflict: null };
714
- }
715
- const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
716
- if (newPaths.length === 0) {
717
- return { autoParallel: false, conflict: null };
718
- }
719
- const sameLane = activeEntries.filter(
720
- (entry) =>
721
- entry.stage === stamped.stage &&
722
- entry.agent === stamped.agent &&
723
- entry.spanId !== stamped.spanId
724
- );
725
- if (sameLane.length === 0) {
726
- return { autoParallel: true, conflict: null };
727
- }
728
- for (const existing of sameLane) {
729
- const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
730
- if (existingPaths.length === 0) {
731
- return { autoParallel: false, conflict: null };
732
- }
733
- const overlap = newPaths.filter((p) => existingPaths.includes(p));
734
- if (overlap.length > 0) {
735
- return {
736
- autoParallel: false,
737
- conflict: {
738
- existingSpanId: existing.spanId || "unknown",
739
- newSpanId: stamped.spanId || "unknown",
740
- pair: { stage: stamped.stage, agent: stamped.agent },
741
- conflictingPaths: overlap
742
- }
743
- };
744
- }
745
- }
746
- return { autoParallel: true, conflict: null };
747
- }
748
-
749
- const MAX_PARALLEL_SLICE_BUILDERS_INLINE = 5;
750
-
751
- function readMaxParallelOverrideFromEnvInline() {
752
- const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_BUILDERS;
753
- if (typeof raw !== "string" || raw.trim().length === 0) return null;
754
- const parsed = Number(raw);
755
- if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) return null;
756
- return parsed;
757
- }
758
-
759
- // keep in sync with validateFanOutCap in src/delegation.ts
760
- function validateFanOutCapInline(stamped, activeEntries, override) {
761
- if (stamped.agent !== "slice-builder" || stamped.stage !== "tdd") return null;
762
- if (stamped.status !== "scheduled") return null;
763
- let cap;
764
- if (override !== null && override !== undefined && Number.isInteger(override) && override >= 1) {
765
- cap = override;
766
- } else {
767
- cap = readMaxParallelOverrideFromEnvInline() || MAX_PARALLEL_SLICE_BUILDERS_INLINE;
768
- }
769
- const sameLaneActive = activeEntries.filter(
770
- (entry) =>
771
- entry.stage === stamped.stage &&
772
- entry.agent === stamped.agent &&
773
- entry.spanId !== stamped.spanId
774
- );
775
- if (sameLaneActive.length + 1 > cap) {
776
- return {
777
- cap,
778
- active: sameLaneActive.length,
779
- pair: { stage: stamped.stage, agent: stamped.agent }
780
- };
781
- }
782
- return null;
783
- }
784
-
785
- function enforceDispatchDedupInline(stamped, priorEntries, args) {
786
- if (stamped.status !== "scheduled") return null;
787
- if (args["allow-parallel"] === true) return null;
788
- const existing = findActiveSpanForPairInline(
789
- stamped.stage,
790
- stamped.agent,
791
- stamped.runId,
792
- priorEntries
793
- );
794
- if (!existing || existing.spanId === stamped.spanId) return null;
795
- if (typeof args.supersede === "string" && args.supersede.length > 0) {
796
- if (args.supersede !== existing.spanId) {
797
- return {
798
- kind: "supersede-mismatch",
799
- details: {
800
- requested: args.supersede,
801
- actualActiveSpanId: existing.spanId,
802
- stage: stamped.stage,
803
- agent: stamped.agent
804
- }
805
- };
806
- }
807
- return { kind: "supersede", existing };
808
- }
809
- return {
810
- kind: "error",
811
- details: {
812
- existingSpanId: existing.spanId,
813
- existingStatus: existing.status,
814
- newSpanId: stamped.spanId,
815
- pair: { stage: stamped.stage, agent: stamped.agent },
816
- hint: "pass --supersede=" + existing.spanId + " to close the previous span as stale, or --allow-parallel to record both as concurrent"
817
- }
818
- };
819
- }
820
-
821
- async function acquireDelegationLogLock(stateDir) {
822
- const lockDir = path.join(stateDir, "delegation-log.json.lock");
823
- const maxWaitMs = 3000;
824
- const startMs = Date.now();
825
- let delayMs = 25;
826
- while (true) {
827
- try {
828
- await fs.mkdir(lockDir, { recursive: false });
829
- return lockDir;
830
- } catch (err) {
831
- const code = err && typeof err === "object" && "code" in err ? err.code : "";
832
- if (code !== "EEXIST") throw err;
833
- if (Date.now() - startMs >= maxWaitMs) {
834
- process.stderr.write(
835
- "[cclaw] delegation-record: timeout waiting for delegation-log.json.lock (max " + maxWaitMs + "ms)\\n"
836
- );
837
- process.exit(2);
838
- }
839
- const jitter = Math.floor(Math.random() * 25);
840
- await new Promise((resolve) => setTimeout(resolve, delayMs + jitter));
841
- delayMs = Math.min(delayMs * 2, 200);
842
- }
843
- }
844
- }
845
-
846
- async function releaseDelegationLogLock(lockDir) {
847
- try {
848
- await fs.rm(lockDir, { recursive: true, force: true });
849
- } catch {
850
- // best-effort release
851
- }
852
- }
853
-
854
- async function writeDelegationLedgerAtomic(ledgerPath, ledger) {
855
- const dir = path.dirname(ledgerPath);
856
- const tmp =
857
- path.join(dir, ".delegation-log.json." + process.pid + "." + Date.now() + "." + Math.random().toString(16).slice(2) + ".tmp");
858
- await fs.writeFile(tmp, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
859
- await fs.rename(tmp, ledgerPath);
860
- }
861
-
862
- async function persistEntry(root, runId, clean, event, options = {}) {
863
- const stateDir = path.join(root, RUNTIME_ROOT, "state");
864
- await fs.mkdir(stateDir, { recursive: true });
865
- await fs.appendFile(path.join(stateDir, "delegation-events.jsonl"), JSON.stringify(event) + "\\n", { encoding: "utf8", mode: 0o600 });
866
-
867
- const ledgerPath = path.join(stateDir, "delegation-log.json");
868
- let ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
869
- const lockDir = await acquireDelegationLogLock(stateDir);
870
- try {
871
- try {
872
- ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
873
- if (!Array.isArray(ledger.entries)) ledger.entries = [];
874
- } catch {
875
- ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
876
- }
877
-
878
- // Rerecord semantics: replace any pre-existing row with the same spanId
879
- // (regardless of its status) so the legacy v1/v2 row is upgraded to v3
880
- // shape on disk. The append path keeps the historical dedup semantics:
881
- // an exact (spanId, status, phase) triple is dropped to keep retried hooks
882
- // idempotent. Including \`phase\` in the dedup key is required because a
883
- // single TDD slice-builder span legitimately emits FOUR rows with
884
- // status=completed (one each for phase=red|green|refactor|doc); a
885
- // dedup on (spanId, status) alone would silently drop GREEN/REFACTOR/DOC
886
- // and leave the linter reporting tdd_slice_green_missing for slices
887
- // whose work actually landed.
888
- if (options.replaceBySpanId) {
889
- ledger.entries = ledger.entries.filter((entry) => entry.spanId !== clean.spanId);
890
- ledger.entries.push(clean);
891
- ledger.runId = runId;
892
- ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
893
- await writeDelegationLedgerAtomic(ledgerPath, ledger);
894
- } else if (!ledger.entries.some((entry) =>
895
- entry.spanId === clean.spanId &&
896
- entry.status === clean.status &&
897
- (entry.phase ?? null) === (clean.phase ?? null)
898
- )) {
899
- ledger.entries.push(clean);
900
- ledger.runId = runId;
901
- ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
902
- await writeDelegationLedgerAtomic(ledgerPath, ledger);
903
- }
904
- } finally {
905
- await releaseDelegationLogLock(lockDir);
906
- }
907
-
908
- // keep in sync with computeActiveSubagents in src/delegation.ts
909
- const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
910
- const effectiveTs = (entry) =>
911
- entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
912
- const latestBySpan = new Map();
913
- for (const entry of ledger.entries) {
914
- if (!entry || typeof entry !== "object" || typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
915
- const existing = latestBySpan.get(entry.spanId);
916
- if (!existing) {
917
- latestBySpan.set(entry.spanId, entry);
918
- continue;
919
- }
920
- if (effectiveTs(entry) >= effectiveTs(existing)) {
921
- latestBySpan.set(entry.spanId, entry);
922
- }
923
- }
924
- const active = [];
925
- for (const entry of latestBySpan.values()) {
926
- if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
927
- }
928
- active.sort((a, b) => {
929
- const aKey = a.startTs || a.ts || "";
930
- const bKey = b.startTs || b.ts || "";
931
- if (aKey === bKey) return 0;
932
- return aKey < bKey ? -1 : 1;
933
- });
934
- await fs.writeFile(path.join(stateDir, "subagents.json"), JSON.stringify({ active, updatedAt: event.eventTs }, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
935
- }
936
-
937
- async function findLegacyEntry(root, spanId) {
938
- const ledgerPath = path.join(root, RUNTIME_ROOT, "state", "delegation-log.json");
939
- let ledger;
940
- try {
941
- ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
942
- } catch {
943
- return null;
944
- }
945
- if (!ledger || !Array.isArray(ledger.entries)) return null;
946
- return ledger.entries.find((entry) => entry && entry.spanId === spanId) || null;
947
- }
948
-
949
- // Allow-list of non-delegation audit events the controller can emit via
950
- // the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS in
951
- // src/delegation.ts.
952
- const VALID_AUDIT_KINDS = new Set([
953
- "cclaw_integration_overseer_skipped",
954
- "cclaw_allow_parallel_auto_flip"
955
- ]);
956
-
957
- async function runAuditEmit(args, json) {
958
- const kind = String(args["audit-kind"]).trim();
959
- if (!VALID_AUDIT_KINDS.has(kind)) {
960
- emitProblems([
961
- "invalid --audit-kind: " + kind +
962
- " (allowed: " + [...VALID_AUDIT_KINDS].join(", ") + ")"
963
- ], json, 2);
964
- return;
965
- }
966
- const root = await detectRoot();
967
- const runId = await readRunId(root);
968
- const reason = typeof args["audit-reason"] === "string"
969
- ? args["audit-reason"].trim()
970
- : "";
971
- const sliceIdsRaw = typeof args["slice-ids"] === "string"
972
- ? args["slice-ids"].trim()
973
- : "";
974
- const sliceIds = sliceIdsRaw.length > 0
975
- ? sliceIdsRaw
976
- .split(",")
977
- .map((value) => value.trim())
978
- .filter((value) => value.length > 0)
979
- : [];
980
- const ts = new Date().toISOString();
981
- const payload = {
982
- event: kind,
983
- runId,
984
- ts,
985
- eventTs: ts,
986
- ...(reason.length > 0 ? { reasons: reason.split(",").map((r) => r.trim()).filter((r) => r.length > 0) } : {}),
987
- ...(sliceIds.length > 0 ? { sliceIds } : {})
988
- };
989
- const stateDir = path.join(root, RUNTIME_ROOT, "state");
990
- try {
991
- await fs.mkdir(stateDir, { recursive: true });
992
- await fs.appendFile(
993
- path.join(stateDir, "delegation-events.jsonl"),
994
- JSON.stringify(payload) + "\\n",
995
- { encoding: "utf8", mode: 0o600 }
996
- );
997
- } catch (error) {
998
- const message = error && typeof error === "object" && "message" in error
999
- ? String(error.message)
1000
- : String(error);
1001
- emitErrorJson("audit_emit_failed", { kind, message }, json);
1002
- return;
1003
- }
1004
- if (json) {
1005
- process.stdout.write(JSON.stringify({
1006
- ok: true,
1007
- command: "audit-emit",
1008
- auditKind: kind,
1009
- runId,
1010
- sliceIds,
1011
- ts
1012
- }, null, 2) + "\\n");
1013
- } else {
1014
- process.stdout.write("[cclaw] audit emitted: " + kind + " (run=" + runId + ", ts=" + ts + ")\\n");
1015
- }
1016
- }
1017
-
1018
- async function runRerecord(args, json) {
1019
- const problems = [];
1020
- for (const key of ["span-id", "dispatch-id", "dispatch-surface", "agent-definition-path"]) {
1021
- if (!args[key]) problems.push("missing --" + key);
1022
- }
1023
- if (args["dispatch-surface"] && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
1024
- problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
1025
- }
1026
- if (problems.length > 0) {
1027
- emitProblems(problems, json, 2);
1028
- return;
1029
- }
1030
- const root = await detectRoot();
1031
- const now = new Date().toISOString();
1032
- const runId = await readRunId(root);
1033
- const legacyEntry = await findLegacyEntry(root, args["span-id"]);
1034
- if (!legacyEntry) {
1035
- emitProblems(["no legacy ledger entry found for --span-id=" + args["span-id"]], json, 1);
1036
- return;
1037
- }
1038
- const explicitEvidenceRef =
1039
- typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0
1040
- ? args["evidence-ref"].trim()
1041
- : "";
1042
- const legacyEvidenceRefs = Array.isArray(legacyEntry.evidenceRefs)
1043
- ? legacyEntry.evidenceRefs
1044
- .filter((ref) => typeof ref === "string" && ref.trim().length > 0)
1045
- .map((ref) => ref.trim())
1046
- : [];
1047
- const mergedEvidenceRefs = explicitEvidenceRef.length > 0
1048
- ? [explicitEvidenceRef]
1049
- : legacyEvidenceRefs;
1050
- if (args["dispatch-surface"] !== "role-switch") {
1051
- if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
1052
- const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
1053
- emitProblems([
1054
- "--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")"
1055
- ], json, 2);
1056
- return;
1057
- }
1058
- const exists = await pathExists(path.join(root, args["agent-definition-path"]));
1059
- if (!exists) {
1060
- emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
1061
- return;
1062
- }
1063
- }
1064
- const merged = {
1065
- stage: legacyEntry.stage,
1066
- agent: legacyEntry.agent,
1067
- mode: legacyEntry.mode || "mandatory",
1068
- "span-id": args["span-id"],
1069
- "dispatch-id": args["dispatch-id"],
1070
- "worker-run-id": args["worker-run-id"] || legacyEntry.workerRunId,
1071
- "dispatch-surface": args["dispatch-surface"],
1072
- "agent-definition-path": args["agent-definition-path"],
1073
- "ack-ts": args["ack-ts"] || legacyEntry.ackTs || now,
1074
- "completed-ts": args["completed-ts"] || legacyEntry.completedTs || now,
1075
- "launched-ts": args["launched-ts"] || legacyEntry.launchedTs || now,
1076
- "evidence-ref": explicitEvidenceRef.length > 0 ? explicitEvidenceRef : undefined,
1077
- "evidence-refs": mergedEvidenceRefs
1078
- };
1079
- const status = "completed";
1080
- const clean = Object.fromEntries(Object.entries(buildRow(merged, status, runId, now)).filter(([, value]) => value !== undefined));
1081
- clean.fulfillmentMode = clean.dispatchSurface === "role-switch" ? "role-switch" : (clean.dispatchSurface === "cursor-task" || clean.dispatchSurface === "generic-task" ? "generic-dispatch" : "isolated");
1082
- const event = { ...clean, event: status, eventTs: now, rerecord: true };
1083
- await persistEntry(root, runId, clean, event, { replaceBySpanId: true });
1084
- process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
1085
- }
1086
-
1087
- const LIFECYCLE_PHASES = ["scheduled", "launched", "acknowledged", "completed"];
1088
-
1089
- function mergeSpanTemplate(spanEvents) {
1090
- const base = {};
1091
- const keys = [
1092
- "stage",
1093
- "agent",
1094
- "mode",
1095
- "runId",
1096
- "dispatchId",
1097
- "dispatchSurface",
1098
- "agentDefinitionPath",
1099
- "workerRunId",
1100
- "fulfillmentMode",
1101
- "schemaVersion",
1102
- "parentSpanId",
1103
- "evidenceRefs",
1104
- "waiverReason"
1105
- ];
1106
- for (const e of spanEvents) {
1107
- if (!e || typeof e !== "object") continue;
1108
- for (const k of keys) {
1109
- if (base[k] === undefined && e[k] !== undefined) {
1110
- base[k] = e[k];
1111
- }
1112
- }
1113
- }
1114
- return base;
1115
- }
1116
-
1117
- function repairFulfillmentMode(base) {
1118
- if (base.fulfillmentMode) return base.fulfillmentMode;
1119
- if (base.dispatchSurface === "role-switch") return "role-switch";
1120
- if (base.dispatchSurface === "cursor-task" || base.dispatchSurface === "generic-task") {
1121
- return "generic-dispatch";
1122
- }
1123
- return "isolated";
1124
- }
1125
-
1126
- async function runRepair(args, json) {
1127
- const problems = [];
1128
- if (!args["span-id"]) problems.push("repair mode requires --span-id");
1129
- if (!args["repair-reason"] || String(args["repair-reason"]).trim().length === 0) {
1130
- problems.push("repair mode requires --repair-reason=<text>");
1131
- }
1132
- if (problems.length > 0) {
1133
- emitProblems(problems, json, 2);
1134
- return;
1135
- }
1136
- const spanId = args["span-id"];
1137
- const repairedReason = String(args["repair-reason"]).trim();
1138
- const root = await detectRoot();
1139
- const events = await readDelegationEvents(root);
1140
- const spanEvents = events.filter(
1141
- (e) => e && e.spanId === spanId && typeof e.event === "string" && LIFECYCLE_PHASES.includes(e.event)
1142
- );
1143
- if (spanEvents.length === 0) {
1144
- emitProblems(
1145
- ["repair refused: no lifecycle delegation-events.jsonl rows found for --span-id=" + spanId],
1146
- json,
1147
- 2
1148
- );
1149
- return;
1150
- }
1151
- const present = new Set(spanEvents.map((e) => e.event));
1152
- const base = mergeSpanTemplate(spanEvents);
1153
- if (!base.stage || !base.agent || !base.mode) {
1154
- emitProblems(["repair refused: span events missing stage/agent/mode to clone"], json, 2);
1155
- return;
1156
- }
1157
- const runId =
1158
- typeof base.runId === "string" && base.runId.length > 0 ? base.runId : await readRunId(root);
1159
- const fulfillmentMode = repairFulfillmentMode(base);
1160
- const schemaVersion =
1161
- typeof base.schemaVersion === "number" && base.schemaVersion > 0
1162
- ? base.schemaVersion
1163
- : LEDGER_SCHEMA_VERSION;
1164
- const evidenceRefs = Array.isArray(base.evidenceRefs)
1165
- ? base.evidenceRefs.filter((r) => typeof r === "string" && r.trim().length > 0)
1166
- : [];
1167
- const now = new Date().toISOString();
1168
- const appended = [];
1169
-
1170
- for (const status of LIFECYCLE_PHASES) {
1171
- if (present.has(status)) continue;
1172
- if (status === "completed" && base.dispatchSurface !== "role-switch") {
1173
- if (!base.dispatchId || !base.dispatchSurface || !base.agentDefinitionPath) {
1174
- emitProblems(
1175
- [
1176
- "repair refused: cannot synthesize completed row without dispatchId, dispatchSurface, and agentDefinitionPath on span " +
1177
- spanId
1178
- ],
1179
- json,
1180
- 2
1181
- );
1182
- return;
1183
- }
1184
- }
1185
- if (status === "completed" && base.dispatchSurface === "role-switch" && evidenceRefs.length === 0) {
1186
- emitProblems(
1187
- ["repair refused: role-switch completed synthesis requires evidenceRefs on span " + spanId],
1188
- json,
1189
- 2
1190
- );
1191
- return;
1192
- }
1193
- const launchedTs =
1194
- status === "launched" || status === "acknowledged" || status === "completed" ? now : undefined;
1195
- const ackTs = status === "acknowledged" || status === "completed" ? now : undefined;
1196
- const completedTs = status === "completed" ? now : undefined;
1197
- const endTs = status === "completed" ? now : undefined;
1198
- const row = {
1199
- stage: base.stage,
1200
- agent: base.agent,
1201
- mode: base.mode,
1202
- status,
1203
- spanId,
1204
- dispatchId: base.dispatchId,
1205
- workerRunId: base.workerRunId,
1206
- dispatchSurface: base.dispatchSurface,
1207
- agentDefinitionPath: base.agentDefinitionPath,
1208
- fulfillmentMode,
1209
- evidenceRefs,
1210
- runId,
1211
- startTs: now,
1212
- ts: now,
1213
- launchedTs,
1214
- ackTs,
1215
- completedTs,
1216
- endTs,
1217
- schemaVersion
1218
- };
1219
- const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
1220
- const event = { ...clean, event: status, eventTs: now, repairedAt: now, repairedReason };
1221
- await persistEntry(root, runId, clean, event);
1222
- present.add(status);
1223
- appended.push(status);
1224
- }
1225
-
1226
- if (json) {
1227
- process.stdout.write(
1228
- JSON.stringify({ ok: true, repair: true, spanId, appended, repairedAt: now, repairedReason }, null, 2) + "\\n"
1229
- );
1230
- }
1231
- }
1232
-
1233
- async function runSliceCommitIfNeeded(root, row, runId) {
1234
- if (
1235
- row.stage !== "tdd" ||
1236
- row.agent !== "slice-builder" ||
1237
- row.status !== "completed" ||
1238
- row.phase !== "doc"
1239
- ) {
1240
- return { ok: true, skipped: true };
1241
- }
1242
- const sliceId = typeof row.sliceId === "string" ? row.sliceId.trim() : "";
1243
- const spanId = typeof row.spanId === "string" ? row.spanId.trim() : "";
1244
- if (sliceId.length === 0 || spanId.length === 0) {
1245
- return { ok: true, skipped: true };
1246
- }
1247
- const helperPath = path.join(root, RUNTIME_ROOT, "hooks", "slice-commit.mjs");
1248
- if (!(await exists(helperPath))) {
1249
- return { ok: true, skipped: true };
1250
- }
1251
- const helperArgs = [
1252
- helperPath,
1253
- "--json",
1254
- "--quiet",
1255
- "--slice=" + sliceId,
1256
- "--span-id=" + spanId,
1257
- "--run-id=" + runId
1258
- ];
1259
- let explicitWorktreePath =
1260
- typeof row.worktreePath === "string" && row.worktreePath.trim().length > 0
1261
- ? row.worktreePath.trim()
1262
- : "";
1263
- if (explicitWorktreePath.length === 0) {
1264
- const priorLedger = await readDelegationLedgerEntries(root);
1265
- const priorSpanPath = priorLedger
1266
- .filter((entry) => entry && entry.spanId === spanId && entry.runId === runId)
1267
- .map((entry) =>
1268
- entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
1269
- .find((value) => value.length > 0);
1270
- if (priorSpanPath) {
1271
- explicitWorktreePath = priorSpanPath;
1272
- }
1273
- }
1274
- if (explicitWorktreePath.length > 0) {
1275
- helperArgs.push("--worktree-path=" + explicitWorktreePath);
1276
- }
1277
- if (typeof row.taskId === "string" && row.taskId.trim().length > 0) {
1278
- helperArgs.push("--task-id=" + row.taskId.trim());
1279
- }
1280
- if (Array.isArray(row.claimedPaths) && row.claimedPaths.length > 0) {
1281
- helperArgs.push("--claimed-paths=" + row.claimedPaths.join(","));
1282
- }
1283
- if (Array.isArray(row.evidenceRefs) && row.evidenceRefs.length > 0) {
1284
- const title = String(row.evidenceRefs[0] || "").trim();
1285
- if (title.length > 0) {
1286
- helperArgs.push("--title=" + title.slice(0, 120));
1287
- }
1288
- }
1289
-
1290
- return await new Promise((resolve) => {
1291
- const child = spawn(process.execPath, helperArgs, {
1292
- cwd: root,
1293
- env: process.env,
1294
- stdio: ["ignore", "pipe", "pipe"]
1295
- });
1296
- let out = "";
1297
- let err = "";
1298
- child.stdout.on("data", (chunk) => {
1299
- out += String(chunk ?? "");
1300
- });
1301
- child.stderr.on("data", (chunk) => {
1302
- err += String(chunk ?? "");
1303
- });
1304
- child.on("error", (error) => {
1305
- resolve({
1306
- ok: false,
1307
- errorCode: "slice_commit_failed",
1308
- details: {
1309
- message: error instanceof Error ? error.message : String(error)
1310
- }
1311
- });
1312
- });
1313
- child.on("close", (code) => {
1314
- let payload = null;
1315
- const trimmed = out.trim();
1316
- if (trimmed.length > 0) {
1317
- try {
1318
- payload = JSON.parse(trimmed);
1319
- } catch {
1320
- payload = null;
1321
- }
1322
- }
1323
- if (code === 0) {
1324
- resolve({ ok: true, payload });
1325
- return;
1326
- }
1327
- const payloadCode =
1328
- payload && typeof payload === "object" && typeof payload.errorCode === "string"
1329
- ? payload.errorCode
1330
- : "slice_commit_failed";
1331
- resolve({
1332
- ok: false,
1333
- errorCode: payloadCode,
1334
- details:
1335
- payload && typeof payload === "object"
1336
- ? payload
1337
- : {
1338
- stderr: err.trim(),
1339
- stdout: out.trim()
1340
- }
1341
- });
1342
- });
1343
- });
1344
- }
1345
-
1346
- async function runSliceWorktreePrepareIfNeeded(root, row, runId) {
1347
- if (
1348
- row.stage !== "tdd" ||
1349
- row.agent !== "slice-builder" ||
1350
- row.status !== "scheduled"
1351
- ) {
1352
- return { ok: true, skipped: true };
1353
- }
1354
- const sliceId = typeof row.sliceId === "string" ? row.sliceId.trim() : "";
1355
- const spanId = typeof row.spanId === "string" ? row.spanId.trim() : "";
1356
- if (sliceId.length === 0 || spanId.length === 0) {
1357
- return { ok: true, skipped: true };
1358
- }
1359
- const helperPath = path.join(root, RUNTIME_ROOT, "hooks", "slice-commit.mjs");
1360
- if (!(await exists(helperPath))) {
1361
- return { ok: true, skipped: true };
1362
- }
1363
- const helperArgs = [
1364
- helperPath,
1365
- "--json",
1366
- "--quiet",
1367
- "--prepare-worktree",
1368
- "--slice=" + sliceId,
1369
- "--span-id=" + spanId,
1370
- "--run-id=" + runId
1371
- ];
1372
- if (Array.isArray(row.claimedPaths) && row.claimedPaths.length > 0) {
1373
- helperArgs.push("--claimed-paths=" + row.claimedPaths.join(","));
1374
- }
1375
- return await new Promise((resolve) => {
1376
- const child = spawn(process.execPath, helperArgs, {
1377
- cwd: root,
1378
- env: process.env,
1379
- stdio: ["ignore", "pipe", "pipe"]
1380
- });
1381
- let out = "";
1382
- let err = "";
1383
- child.stdout.on("data", (chunk) => {
1384
- out += String(chunk ?? "");
1385
- });
1386
- child.stderr.on("data", (chunk) => {
1387
- err += String(chunk ?? "");
1388
- });
1389
- child.on("error", (error) => {
1390
- resolve({
1391
- ok: false,
1392
- errorCode: "worktree_prepare_failed",
1393
- details: {
1394
- message: error instanceof Error ? error.message : String(error)
1395
- }
1396
- });
1397
- });
1398
- child.on("close", (code) => {
1399
- let payload = null;
1400
- const trimmed = out.trim();
1401
- if (trimmed.length > 0) {
1402
- try {
1403
- payload = JSON.parse(trimmed);
1404
- } catch {
1405
- payload = null;
1406
- }
1407
- }
1408
- if (code === 0) {
1409
- resolve({ ok: true, payload });
1410
- return;
1411
- }
1412
- const payloadCode =
1413
- payload && typeof payload === "object" && typeof payload.errorCode === "string"
1414
- ? payload.errorCode
1415
- : "worktree_prepare_failed";
1416
- resolve({
1417
- ok: false,
1418
- errorCode: payloadCode,
1419
- details:
1420
- payload && typeof payload === "object"
1421
- ? payload
1422
- : {
1423
- stderr: err.trim(),
1424
- stdout: out.trim()
1425
- }
1426
- });
1427
- });
1428
- });
1429
- }
1430
-
1431
- async function main() {
1432
- const args = parseArgs(process.argv.slice(2));
1433
- const json = args.json !== undefined;
1434
-
1435
- const guardRoot = await detectRoot();
1436
- await verifyFlowStateGuardInline(guardRoot);
1437
-
1438
- if (args.repair) {
1439
- await runRepair(args, json);
1440
- return;
1441
- }
1442
-
1443
- if (args.rerecord) {
1444
- await runRerecord(args, json);
1445
- return;
1446
- }
1447
-
1448
- // Audit-only emit path. When the controller wants to record a
1449
- // non-delegation audit row (e.g. \`cclaw_integration_overseer_skipped\`
1450
- // when the wave heuristic chose to skip the overseer dispatch), pass
1451
- // --audit-kind=<event-name> [--audit-reason=<text>] [--slice-ids=<csv>]
1452
- // and the helper appends a single line to delegation-events.jsonl
1453
- // without touching the lifecycle ledger. The kind must be in the
1454
- // canonical allow-list so a typo cannot inject an unrecognized event.
1455
- if (typeof args["audit-kind"] === "string" && args["audit-kind"].trim().length > 0) {
1456
- await runAuditEmit(args, json);
1457
- return;
1458
- }
1459
-
1460
- const problems = [];
1461
- if (!args.stage) problems.push("missing --stage");
1462
- if (!args.agent) problems.push("missing --agent");
1463
- if (args.mode !== "mandatory" && args.mode !== "proactive") problems.push("--mode must be mandatory or proactive");
1464
- if (!VALID_STATUSES.has(args.status)) problems.push("invalid --status");
1465
- if (!args["span-id"]) problems.push("missing --span-id");
1466
- if (args.status === "waived" && !args["waiver-reason"]) problems.push("waived status requires --waiver-reason");
1467
-
1468
- // Strict --dispatch-surface enum validation: any provided surface must be
1469
- // in the canonical allow-list. Do this BEFORE we use the value to gate
1470
- // completed/role-switch fields.
1471
- if (args["dispatch-surface"] !== undefined && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
1472
- problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
1473
- emitProblems(problems, json, 2);
1474
- return;
1475
- }
1476
-
1477
- // TDD slice phase tagging validation. --phase is strictly enum-bound;
1478
- // --slice must be a non-empty string when provided;
1479
- // --phase=refactor-deferred requires either an explicit
1480
- // --refactor-rationale or an --evidence-ref with rationale text so the
1481
- // linter has something to render.
1482
- if (args.phase !== undefined && !VALID_DELEGATION_PHASES_SET.has(args.phase)) {
1483
- problems.push("invalid --phase (allowed: " + VALID_DELEGATION_PHASES.join(", ") + ")");
1484
- emitProblems(problems, json, 2);
1485
- return;
1486
- }
1487
- if (args.slice !== undefined && (typeof args.slice !== "string" || args.slice.trim().length === 0)) {
1488
- problems.push("--slice requires a non-empty value");
1489
- emitProblems(problems, json, 2);
1490
- return;
1491
- }
1492
- // 7.6.0 — phase-event status validation.
1493
- // \`--phase=<phase>\` carries phase-level granularity (RED/GREEN/REFACTOR/DOC
1494
- // outcomes). It is only meaningful on terminal statuses
1495
- // (\`completed\` or \`failed\`). The dispatch-level ack (no phase) keeps
1496
- // \`--status=acknowledged\`. Refuse acknowledged/launched/scheduled/waived/stale
1497
- // rows that carry a phase so phantom-open slices cannot be recorded.
1498
- if (
1499
- typeof args.phase === "string" &&
1500
- args.phase.length > 0 &&
1501
- args.status !== "completed" &&
1502
- args.status !== "failed"
1503
- ) {
1504
- const sliceFlag = typeof args.slice === "string" && args.slice.length > 0
1505
- ? "--slice=" + args.slice + " "
1506
- : "";
1507
- const spanFlag = typeof args["span-id"] === "string" && args["span-id"].length > 0
1508
- ? "--span-id=" + args["span-id"] + " "
1509
- : "";
1510
- const correctedCommandHint =
1511
- "node .cclaw/hooks/delegation-record.mjs --stage=" + (args.stage || "<stage>") +
1512
- " --agent=" + (args.agent || "<agent>") +
1513
- " --mode=" + (args.mode || "mandatory") +
1514
- " --status=completed --phase=" + args.phase +
1515
- " " + sliceFlag + spanFlag +
1516
- '--evidence-ref="<phase outcome>"';
1517
- emitErrorJson(
1518
- "phase_event_requires_completed_or_failed_status",
1519
- {
1520
- phase: args.phase,
1521
- status: args.status,
1522
- spanId: args["span-id"] || "unknown",
1523
- correctedCommandHint
1524
- },
1525
- json
1526
- );
1527
- return;
1528
- }
1529
- if (args.phase === "refactor-deferred") {
1530
- const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
1531
- if (rationaleQuality !== "ok") {
1532
- if (rationaleQuality === "missing") {
1533
- problems.push("--phase=refactor-deferred requires --refactor-rationale=<text>");
1534
- } else if (rationaleQuality === "too-short") {
1535
- problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
1536
- } else {
1537
- problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
1538
- }
1539
- emitProblems(problems, json, 2);
1540
- return;
1541
- }
1542
- }
1543
-
1544
- // --refactor-outcome must be one of inline|deferred. When mode=deferred
1545
- // a rationale is required (either --refactor-rationale or --evidence-ref
1546
- // carrying the rationale text). --risk-tier must be one of low|medium|high
1547
- // if provided.
1548
- if (
1549
- args["refactor-outcome"] !== undefined &&
1550
- args["refactor-outcome"] !== "inline" &&
1551
- args["refactor-outcome"] !== "deferred"
1552
- ) {
1553
- problems.push("invalid --refactor-outcome (allowed: inline, deferred)");
1554
- emitProblems(problems, json, 2);
1555
- return;
1556
- }
1557
- if (args["refactor-outcome"] === "deferred") {
1558
- const rationaleQuality = validateDeferredRationaleInline(args["refactor-rationale"], args);
1559
- if (rationaleQuality !== "ok") {
1560
- if (rationaleQuality === "missing") {
1561
- problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text>");
1562
- } else if (rationaleQuality === "too-short") {
1563
- problems.push("--refactor-rationale for deferred refactor must be at least 80 characters");
1564
- } else {
1565
- problems.push("--refactor-rationale for deferred refactor must mention slice/task context (e.g. S-12 and T-103)");
1566
- }
1567
- emitProblems(problems, json, 2);
1568
- return;
1569
- }
1570
- }
1571
- if (
1572
- args["risk-tier"] !== undefined &&
1573
- args["risk-tier"] !== "low" &&
1574
- args["risk-tier"] !== "medium" &&
1575
- args["risk-tier"] !== "high"
1576
- ) {
1577
- problems.push("invalid --risk-tier (allowed: low, medium, high)");
1578
- emitProblems(problems, json, 2);
1579
- return;
1580
- }
1581
- if (args["override-cap"] !== undefined) {
1582
- const overrideRaw = String(args["override-cap"]).trim();
1583
- const overrideNum = Number(overrideRaw);
1584
- if (!Number.isInteger(overrideNum) || overrideNum < 1) {
1585
- problems.push("--override-cap must be an integer >= 1");
1586
- emitProblems(problems, json, 2);
1587
- return;
1588
- }
1589
- const reasonRaw = typeof args.reason === "string" ? args.reason.trim() : "";
1590
- if (reasonRaw.length === 0) {
1591
- problems.push("--override-cap requires --reason=<slug>");
1592
- emitProblems(problems, json, 2);
1593
- return;
1594
- }
1595
- }
1596
-
1597
- if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
1598
- for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
1599
- if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
1600
- }
1601
- }
1602
- if (args.status === "completed" && args["dispatch-surface"] === "role-switch" && !args["evidence-ref"]) {
1603
- problems.push("completed role-switch status requires --evidence-ref");
1604
- }
1605
-
1606
- // Validate --agent-definition-path against the surface and on-disk
1607
- // existence whenever both are provided.
1608
- if (args["dispatch-surface"] && args["agent-definition-path"] && args["dispatch-surface"] !== "role-switch" && args["dispatch-surface"] !== "manual") {
1609
- if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
1610
- const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
1611
- problems.push("--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")");
1612
- }
1613
- }
1614
-
1615
- if (problems.length > 0) {
1616
- emitProblems(problems, json, 2);
1617
- return;
1618
- }
1619
-
1620
- const root = await detectRoot();
1621
- const now = new Date().toISOString();
1622
- const runId = await readRunId(root);
1623
-
1624
- // For completed isolated/generic rows, --agent-definition-path must
1625
- // resolve to an existing file or directory inside the project. This
1626
- // catches typos and stale generated agent paths before they enter the
1627
- // ledger. Skipped for role-switch (no agent file is generated) and
1628
- // manual (intentionally free-form).
1629
- if (
1630
- args.status === "completed" &&
1631
- args["dispatch-surface"] &&
1632
- args["dispatch-surface"] !== "role-switch" &&
1633
- args["dispatch-surface"] !== "manual" &&
1634
- args["agent-definition-path"]
1635
- ) {
1636
- const exists = await pathExists(path.join(root, args["agent-definition-path"]));
1637
- if (!exists) {
1638
- emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
1639
- return;
1640
- }
1641
- }
1642
-
1643
- // Completed isolated/generic rows require explicit --ack-ts OR a prior
1644
- // acknowledged event for the same span. fulfillmentMode=isolated cannot
1645
- // be claimed without an ACK timestamp anchor.
1646
- if (args.status === "completed" && args["dispatch-surface"] !== "role-switch" && !args["ack-ts"]) {
1647
- const priorEvents = await readDelegationEvents(root);
1648
- if (!hasPriorAck(priorEvents, args, runId)) {
1649
- const ackProblem = "completed isolated/generic status requires prior acknowledged event for same span or --ack-ts";
1650
- emitProblems([ackProblem], json, 2);
1651
- return;
1652
- }
1653
- }
1654
-
1655
- const status = args.status;
1656
- const priorLedger = await readDelegationLedgerEntries(root);
1657
- const priorForSpan = priorLedger.filter((e) => e && e.spanId === args["span-id"]);
1658
- const inheritedWorktreePath = priorForSpan
1659
- .map((entry) =>
1660
- entry && typeof entry.worktreePath === "string" ? entry.worktreePath.trim() : "")
1661
- .find((value) => value.length > 0);
1662
- if (
1663
- inheritedWorktreePath &&
1664
- (typeof args["worktree-path"] !== "string" || args["worktree-path"].trim().length === 0)
1665
- ) {
1666
- args["worktree-path"] = inheritedWorktreePath;
1667
- }
1668
- const inheritedStartTs = priorForSpan
1669
- .map((e) => e.startTs)
1670
- .filter((ts) => typeof ts === "string" && ts.length > 0)
1671
- .sort()[0];
1672
- // When no prior row exists, fall back to the earliest user-supplied
1673
- // event timestamp so the monotonic validator never sees the row write
1674
- // time overshoot the real event timestamps.
1675
- const lifecycleCandidates = [
1676
- inheritedStartTs,
1677
- args["launched-ts"],
1678
- args["ack-ts"],
1679
- args["completed-ts"],
1680
- now
1681
- ].filter((value) => typeof value === "string" && value.length > 0);
1682
- const spanStartTs = inheritedStartTs ||
1683
- lifecycleCandidates.reduce((min, candidate) => (candidate < min ? candidate : min), now);
1684
- const row = buildRow(args, status, runId, now, { spanStartTs });
1685
- const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
1686
- const event = { ...clean, event: status, eventTs: now };
1687
- let autoParallelAuditEvent = null;
1688
-
1689
- const violation = validateMonotonicTimestampsInline(clean, priorLedger);
1690
- if (violation) {
1691
- emitErrorJson("delegation_timestamp_non_monotonic", violation, json);
1692
- return;
1693
- }
1694
-
1695
- // File-overlap scheduler + fan-out cap. Run before the dispatch
1696
- // dedup so disjoint claimedPaths can auto-promote to allowParallel,
1697
- // emit an audit event for the flip, and bypass the duplicate guard.
1698
- if (status === "scheduled") {
1699
- const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
1700
- const activeForRun = computeActiveSubagentsInline(sameRunPrior);
1701
- const overlap = validateFileOverlapInline(clean, activeForRun);
1702
- if (overlap.conflict) {
1703
- emitErrorJson("dispatch_overlap", overlap.conflict, json);
1704
- return;
1705
- }
1706
- if (overlap.autoParallel && clean.allowParallel !== true) {
1707
- clean.allowParallel = true;
1708
- args["allow-parallel"] = true;
1709
- event.allowParallel = true;
1710
- autoParallelAuditEvent = {
1711
- event: "cclaw_allow_parallel_auto_flip",
1712
- runId,
1713
- ts: now,
1714
- eventTs: now,
1715
- stage: clean.stage,
1716
- agent: clean.agent,
1717
- spanId: clean.spanId,
1718
- sliceId: clean.sliceId,
1719
- reason: "disjoint-claimed-paths-auto-flip",
1720
- claimedPaths: Array.isArray(clean.claimedPaths) ? clean.claimedPaths : []
1721
- };
1722
- }
1723
- const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
1724
- const override = overrideRaw !== null ? Number(overrideRaw) : null;
1725
- const capViolation = validateFanOutCapInline(clean, activeForRun, override);
1726
- if (capViolation) {
1727
- emitErrorJson("dispatch_cap", capViolation, json);
1728
- return;
1729
- }
1730
- const preparedWorktree = await runSliceWorktreePrepareIfNeeded(root, clean, runId);
1731
- if (!preparedWorktree.ok) {
1732
- emitErrorJson(
1733
- preparedWorktree.errorCode || "worktree_prepare_failed",
1734
- preparedWorktree.details || {},
1735
- json
1736
- );
1737
- return;
1738
- }
1739
- if (
1740
- preparedWorktree.payload &&
1741
- typeof preparedWorktree.payload === "object" &&
1742
- typeof preparedWorktree.payload.worktreePath === "string" &&
1743
- preparedWorktree.payload.worktreePath.trim().length > 0
1744
- ) {
1745
- clean.worktreePath = preparedWorktree.payload.worktreePath.trim();
1746
- event.worktreePath = clean.worktreePath;
1747
- }
1748
- }
1749
- const dedupViolation = enforceDispatchDedupInline(clean, priorLedger, args);
1750
- if (dedupViolation) {
1751
- if (dedupViolation.kind === "supersede") {
1752
- const stalenessTs = new Date(new Date(now).getTime() - 1).toISOString();
1753
- const staleRow = {
1754
- stage: dedupViolation.existing.stage,
1755
- agent: dedupViolation.existing.agent,
1756
- mode: dedupViolation.existing.mode,
1757
- status: "stale",
1758
- spanId: dedupViolation.existing.spanId,
1759
- runId,
1760
- startTs: dedupViolation.existing.startTs || stalenessTs,
1761
- ts: stalenessTs,
1762
- endTs: stalenessTs,
1763
- supersededBy: clean.spanId,
1764
- schemaVersion: LEDGER_SCHEMA_VERSION
1765
- };
1766
- const staleEvent = { ...staleRow, event: "stale", eventTs: stalenessTs };
1767
- await persistEntry(root, runId, staleRow, staleEvent);
1768
- } else if (dedupViolation.kind === "error") {
1769
- emitErrorJson("dispatch_duplicate", dedupViolation.details, json);
1770
- return;
1771
- } else if (dedupViolation.kind === "supersede-mismatch") {
1772
- emitErrorJson("dispatch_supersede_mismatch", dedupViolation.details, json);
1773
- return;
1774
- }
1775
- }
1776
-
1777
- // GREEN evidence freshness contract for \`slice-builder --phase green
1778
- // --status=completed\`. Three checks:
1779
- // 1. green_evidence_red_test_mismatch — evidenceRefs[0] must contain
1780
- // the basename/stem of the RED span's first evidenceRef.
1781
- // 2. green_evidence_passing_assertion_missing — evidenceRefs[0]
1782
- // must carry a recognized passing-assertion line ("=> N passed;
1783
- // 0 failed" or runner-specific equivalents).
1784
- // 3. green_evidence_too_fresh — completedTs minus ackTs must be
1785
- // >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
1786
- // Escape hatch for legitimate observational GREENs (cross-slice
1787
- // handoff, no-op verification): --green-mode=observational.
1788
- if (
1789
- clean.stage === "tdd" &&
1790
- clean.agent === "slice-builder" &&
1791
- clean.phase === "green" &&
1792
- clean.status === "completed"
1793
- ) {
1794
- const isObservational =
1795
- typeof args["green-mode"] === "string" &&
1796
- args["green-mode"].trim().toLowerCase() === "observational";
1797
- const greenEvidenceFirst =
1798
- Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
1799
- ? String(clean.evidenceRefs[0])
1800
- : "";
1801
-
1802
- // Locate the matching RED row's first evidenceRef in the events log.
1803
- const priorEvents = await readDelegationEvents(root);
1804
- let redEvidenceRef = null;
1805
- for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
1806
- const ev = priorEvents[i];
1807
- if (!ev) continue;
1808
- if (ev.runId !== runId) continue;
1809
- if (ev.stage !== "tdd") continue;
1810
- if (ev.sliceId !== clean.sliceId) continue;
1811
- if (ev.phase !== "red") continue;
1812
- if (Array.isArray(ev.evidenceRefs) && ev.evidenceRefs.length > 0) {
1813
- redEvidenceRef = String(ev.evidenceRefs[0] || "");
1814
- break;
1815
- }
1816
- }
1817
-
1818
- // The freshness contract only fires when there's a matching RED row
1819
- // for this slice in the active run. Without RED context we have
1820
- // nothing to verify GREEN against (legacy ledger imports, RED
1821
- // happened outside cclaw harness, or test fixtures that bypass
1822
- // RED). Once a RED row is present, the contract becomes
1823
- // mandatory unless explicitly waived via --green-mode=observational.
1824
- const hasRedContext = redEvidenceRef !== null;
1825
- const escapeFastGreen = isObservational;
1826
-
1827
- if (hasRedContext && !escapeFastGreen) {
1828
- // Check 1: RED test name match.
1829
- const stem = extractRedTestNameInline(redEvidenceRef);
1830
- if (stem && greenEvidenceFirst.length > 0 && !greenEvidenceFirst.toLowerCase().includes(stem.toLowerCase())) {
1831
- emitErrorJson(
1832
- "green_evidence_red_test_mismatch",
1833
- {
1834
- sliceId: clean.sliceId,
1835
- redEvidenceFirst: redEvidenceRef,
1836
- greenEvidenceFirst,
1837
- expectedSubstring: stem,
1838
- remediation:
1839
- "evidenceRefs[0] on the GREEN row must reference the same test the RED row cited. Re-run the matching RED test, capture its passing output, and pass it as --evidence-ref."
1840
- },
1841
- json
1842
- );
1843
- return;
1844
- }
1845
-
1846
- // Check 2: passing-assertion line.
1847
- if (greenEvidenceFirst.length > 0 && !matchesPassingAssertionInline(greenEvidenceFirst)) {
1848
- emitErrorJson(
1849
- "green_evidence_passing_assertion_missing",
1850
- {
1851
- sliceId: clean.sliceId,
1852
- greenEvidenceFirst,
1853
- remediation:
1854
- "evidenceRefs[0] on the GREEN row must contain a passing-assertion line (language-agnostic examples: Node/Vitest \\"=> N passed; 0 failed\\", Python/Pytest \\"N passed in 0.42s\\", Go \\"ok pkg 0.12s\\", Rust \\"test result: ok\\", Java/Maven \\"Tests run: N, Failures: 0, Errors: 0\\"). Re-run the test and paste a fresh runner line."
1855
- },
1856
- json
1857
- );
1858
- return;
1859
- }
1860
-
1861
- // Check 3: fast-green floor. ackTs is required upstream; we use
1862
- // the persisted ackTs from prior events when not provided on this
1863
- // row.
1864
- const minMs = await readTddGreenMinElapsedMsInline(root);
1865
- if (minMs > 0 && clean.completedTs) {
1866
- let ackTs = clean.ackTs;
1867
- if (!ackTs) {
1868
- for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
1869
- const ev = priorEvents[i];
1870
- if (!ev) continue;
1871
- if (ev.spanId !== clean.spanId) continue;
1872
- if (typeof ev.ackTs === "string" && ev.ackTs.length > 0) {
1873
- ackTs = ev.ackTs;
1874
- break;
1875
- }
1876
- }
1877
- }
1878
- if (ackTs) {
1879
- const completedMs = Date.parse(clean.completedTs);
1880
- const ackMs = Date.parse(ackTs);
1881
- if (Number.isFinite(completedMs) && Number.isFinite(ackMs)) {
1882
- const elapsed = completedMs - ackMs;
1883
- if (elapsed < minMs) {
1884
- emitErrorJson(
1885
- "green_evidence_too_fresh",
1886
- {
1887
- sliceId: clean.sliceId,
1888
- ackTs,
1889
- completedTs: clean.completedTs,
1890
- elapsedMs: elapsed,
1891
- minMs,
1892
- remediation:
1893
- "GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --green-mode=observational for legitimate no-op verification spans."
1894
- },
1895
- json
1896
- );
1897
- return;
1898
- }
1899
- }
1900
- }
1901
- }
1902
- }
1903
- }
1904
-
1905
- const sliceCommitResult = await runSliceCommitIfNeeded(root, clean, runId);
1906
- if (!sliceCommitResult.ok) {
1907
- emitErrorJson(
1908
- sliceCommitResult.errorCode || "slice_commit_failed",
1909
- sliceCommitResult.details || {},
1910
- json
1911
- );
1912
- return;
1913
- }
1914
- if (
1915
- sliceCommitResult.payload &&
1916
- typeof sliceCommitResult.payload === "object" &&
1917
- typeof sliceCommitResult.payload.commitSha === "string"
1918
- ) {
1919
- event.sliceCommitSha = sliceCommitResult.payload.commitSha;
1920
- }
1921
-
1922
- await persistEntry(root, runId, clean, event);
1923
- if (autoParallelAuditEvent) {
1924
- await appendAuditEventInline(root, autoParallelAuditEvent);
1925
- }
1926
-
1927
- process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
1928
- }
1929
-
1930
- void main();
1931
- `;
1932
- }
1933
- export function sliceCommitScript() {
1934
- return internalHelperScript("slice-commit", "slice-commit", "Usage: node " + RUNTIME_ROOT + "/hooks/slice-commit.mjs --slice=<S-N> --span-id=<span-id> [--task-id=<T-id>] [--title=<text>] [--run-id=<run-id>] [--worktree-path=<abs-or-rel-path>] [--prepare-worktree] [--claimed-paths=<path1,path2,...>] [--claimed-path=<path> ...] [--json] [--quiet]");
1935
- }
1936
- export function runHookCmdScript() {
1937
- return `: << 'CMDBLOCK'
1938
- @echo off
1939
- REM Cross-platform wrapper for cclaw Node hook runtime.
1940
- REM Windows executes this batch block; Unix shells treat it as a heredoc comment.
1941
- if "%~1"=="" (
1942
- echo [cclaw] run-hook.cmd: missing hook name >&2
1943
- exit /b 1
1944
- )
1945
- set "HOOK_DIR=%~dp0"
1946
- set "RUNTIME=%HOOK_DIR%run-hook.mjs"
1947
- where node >nul 2>nul
1948
- if %ERRORLEVEL% neq 0 (
1949
- REM Best-effort: missing node should not block harness execution loops.
1950
- echo [cclaw] run-hook.cmd: node not found; cclaw hook skipped. Run npx cclaw-cli sync. >&2
1951
- exit /b 0
1952
- )
1953
- node "%RUNTIME%" %*
1954
- exit /b %ERRORLEVEL%
1955
- CMDBLOCK
1956
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
1957
- if [ "$#" -lt 1 ]; then
1958
- echo "[cclaw] run-hook.cmd: missing hook name" >&2
1959
- exit 1
1960
- fi
1961
- if ! command -v node >/dev/null 2>&1; then
1962
- echo "[cclaw] run-hook.cmd: node not found; cclaw hook skipped. Run npx cclaw-cli sync." >&2
1963
- exit 0
1964
- fi
1965
- exec node "\${SCRIPT_DIR}/run-hook.mjs" "$@"
1966
- `;
1967
- }
1968
- export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
1969
- export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
1970
- export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
1971
- export { nodeHookRuntimeScript } from "./node-hooks.js";
1972
- export { opencodePluginJs } from "./opencode-plugin.js";