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,1030 +1,19 @@
1
- // src/content/node-hooks.ts
2
- import { existsSync } from "node:fs";
3
- import path2 from "node:path";
4
- import { fileURLToPath as fileURLToPath2 } from "node:url";
5
-
6
- // src/constants.ts
7
- import { readFileSync } from "node:fs";
8
- import path from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- var RUNTIME_ROOT = ".cclaw";
11
- function readPackageVersion() {
12
- try {
13
- const here = path.dirname(fileURLToPath(import.meta.url));
14
- const candidates = [
15
- path.resolve(here, "../package.json"),
16
- path.resolve(here, "../../package.json")
17
- ];
18
- for (const candidate of candidates) {
19
- try {
20
- const raw = readFileSync(candidate, "utf8");
21
- const parsed = JSON.parse(raw);
22
- if (parsed.name === "cclaw-cli" && typeof parsed.version === "string") {
23
- return parsed.version;
24
- }
25
- } catch {
26
- continue;
27
- }
28
- }
29
- } catch {
30
- }
31
- return "0.0.0-dev";
32
- }
33
- var CCLAW_VERSION = readPackageVersion();
34
- var REQUIRED_DIRS = [
35
- RUNTIME_ROOT,
36
- `${RUNTIME_ROOT}/commands`,
37
- `${RUNTIME_ROOT}/skills`,
38
- `${RUNTIME_ROOT}/templates`,
39
- `${RUNTIME_ROOT}/templates/state-contracts`,
40
- `${RUNTIME_ROOT}/artifacts`,
41
- `${RUNTIME_ROOT}/wave-plans`,
42
- `${RUNTIME_ROOT}/archive`,
43
- `${RUNTIME_ROOT}/worktrees`,
44
- `${RUNTIME_ROOT}/state`,
45
- `${RUNTIME_ROOT}/rules`,
46
- `${RUNTIME_ROOT}/agents`,
47
- `${RUNTIME_ROOT}/hooks`,
48
- `${RUNTIME_ROOT}/skills/review-prompts`
49
- ];
50
- var REQUIRED_GITIGNORE_PATTERNS = [
51
- "# cclaw generated artifacts",
52
- `${RUNTIME_ROOT}/`,
53
- ".claude/commands/cc-*.md",
54
- ".claude/commands/cc.md",
55
- ".cursor/commands/cc-*.md",
56
- ".cursor/commands/cc.md",
57
- ".opencode/commands/cc-*.md",
58
- ".opencode/commands/cc.md",
59
- // Codex uses skill-kind shims under `.agents/skills/cc*/` since
60
- // Codex shim layout (renamed from the older `cclaw-cc*` layout).
61
- // `cclaw sync` and `cclaw uninstall` both auto-remove the legacy
62
- // `cclaw-cc*` directories.
63
- ".agents/skills/cc/SKILL.md",
64
- ".agents/skills/cc-*/SKILL.md",
65
- ".claude/hooks/hooks.json",
66
- ".cursor/hooks.json",
67
- ".codex/hooks.json",
68
- ".opencode/plugins/cclaw-plugin.mjs",
69
- ".cursor/rules/cclaw-workflow.mdc"
70
- ];
71
-
72
- // src/content/runtime-shared-snippets.ts
73
- var SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS = `
74
- function summarizeFlowState(rawState) {
75
- const state =
76
- rawState && typeof rawState === "object" && !Array.isArray(rawState)
77
- ? rawState
78
- : {};
79
- return {
80
- stage: typeof state.currentStage === "string" ? state.currentStage : "none",
81
- completed: Array.isArray(state.completedStages) ? state.completedStages.length : 0,
82
- activeRunId: typeof state.activeRunId === "string" ? state.activeRunId : "none"
83
- };
84
- }
85
-
86
- function parseKnowledgeDigest(rawKnowledge, currentStage, maxRows = 6) {
87
- const text = typeof rawKnowledge === "string" ? rawKnowledge : "";
88
- if (text.trim().length === 0) {
89
- return { learningsCount: 0, lines: [] };
90
- }
91
- const rows = text
92
- .split(/\\r?\\n/gu)
93
- .map((line) => line.trim())
94
- .filter((line) => line.length > 0);
95
- let learningsCount = 0;
96
- const parsedRows = [];
97
- for (const line of rows) {
98
- if (line.startsWith("{")) learningsCount += 1;
99
- try {
100
- const parsed = JSON.parse(line);
101
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
102
- parsedRows.push(parsed);
103
- } catch {
104
- // ignore malformed knowledge line in digest
105
- }
106
- }
107
- const lines = parsedRows
108
- .filter((row) => {
109
- const stage = typeof row.stage === "string" ? row.stage : null;
110
- return stage === null || stage === currentStage;
111
- })
112
- .slice(-maxRows)
113
- .reverse()
114
- .map((row) => {
115
- const confidence = typeof row.confidence === "string" ? row.confidence : "unknown";
116
- const stage = typeof row.stage === "string" ? row.stage : "global";
117
- const trigger = typeof row.trigger === "string" ? row.trigger : "trigger";
118
- const action = typeof row.action === "string" ? row.action : "action";
119
- return "- [" + confidence + " \u2022 " + stage + "] " + trigger + " -> " + action;
120
- });
121
- return { learningsCount, lines };
122
- }
123
-
124
- function activeArtifactsPathLabel(runtimeRoot) {
125
- return String(runtimeRoot || ".cclaw") + "/artifacts/";
126
- }
127
- `;
128
- var SHARED_STAGE_SUPPORT_SNIPPETS = `
129
- const STAGE_IDS = ["brainstorm", "scope", "design", "spec", "plan", "tdd", "review", "ship"];
130
- const REVIEW_PROMPT_BY_STAGE = {
131
- brainstorm: "brainstorm-self-review.md",
132
- scope: "scope-ceo-review.md",
133
- design: "design-eng-review.md"
134
- };
135
- const REVIEW_PROMPT_FILES = Object.values(REVIEW_PROMPT_BY_STAGE);
136
-
137
- function isKnownStageId(stage) {
138
- return typeof stage === "string" && STAGE_IDS.includes(stage);
139
- }
140
-
141
- function reviewPromptFileName(stage) {
142
- if (!isKnownStageId(stage)) return null;
143
- const name = REVIEW_PROMPT_BY_STAGE[stage];
144
- return typeof name === "string" ? name : null;
145
- }
146
- `;
147
-
148
- // src/content/node-hooks.ts
149
- function resolveCliRuntimeForGeneratedHook() {
150
- const here = fileURLToPath2(import.meta.url);
151
- const candidates = [
152
- path2.resolve(path2.dirname(here), "..", "cli.js"),
153
- path2.resolve(path2.dirname(here), "..", "..", "dist", "cli.js")
154
- ];
155
- for (const candidate of candidates) {
156
- if (existsSync(candidate)) return { entrypoint: candidate, argsPrefix: [] };
157
- }
158
- if (process.env.VITEST === "true") {
159
- const sourceCli = path2.resolve(path2.dirname(here), "..", "cli.ts");
160
- const viteNode = path2.resolve(path2.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
161
- if (existsSync(sourceCli) && existsSync(viteNode)) {
162
- return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
163
- }
164
- }
165
- return { entrypoint: null, argsPrefix: [] };
166
- }
167
- function nodeHookRuntimeScript(options = {}) {
168
- void options;
169
- const defaultHookProfile = "standard";
170
- const defaultDisabledHooks = [];
171
- const cliRuntime = resolveCliRuntimeForGeneratedHook();
172
- return `#!/usr/bin/env node
173
- import { createHash } from "node:crypto";
174
- import fs from "node:fs/promises";
1
+ // src/runtime/run-hook.entry.ts
175
2
  import path from "node:path";
176
- import process from "node:process";
177
- import { spawn } from "node:child_process";
178
-
179
- const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
180
- const FLOW_STATE_GUARD_REL_PATH = RUNTIME_ROOT + "/.flow-state.guard.json";
181
- const CCLAW_CLI_ENTRYPOINT = ${JSON.stringify(cliRuntime.entrypoint)};
182
- const CCLAW_CLI_ARGS_PREFIX = ${JSON.stringify(cliRuntime.argsPrefix)};
183
- const DEFAULT_HOOK_PROFILE = ${JSON.stringify(defaultHookProfile)};
184
- const DEFAULT_DISABLED_HOOKS = ${JSON.stringify(defaultDisabledHooks)};
185
- const HOOK_PROFILE_VALUES = new Set(["minimal", "standard", "strict"]);
186
- const MINIMAL_PROFILE_ALLOWED_HOOKS = new Set([
187
- "session-start",
188
- "stop-handoff"
189
- ]);
190
-
191
- ${SHARED_FLOW_AND_KNOWLEDGE_SNIPPETS}
192
- ${SHARED_STAGE_SUPPORT_SNIPPETS}
193
-
194
- function normalizeHookToken(value) {
195
- return String(value == null ? "" : value).trim().toLowerCase();
3
+ import { pathToFileURL } from "node:url";
4
+ async function runHookByName(projectRoot, hookFile) {
5
+ const hookPath = path.join(projectRoot, ".cclaw", "hooks", hookFile);
6
+ const url = pathToFileURL(hookPath).href;
7
+ await import(url);
196
8
  }
197
-
198
- function parseHookProfile(rawValue, fallback = "standard") {
199
- const normalized = normalizeHookToken(rawValue);
200
- if (HOOK_PROFILE_VALUES.has(normalized)) return normalized;
201
- return fallback;
202
- }
203
-
204
- function parseDisabledHooksCsv(rawValue) {
205
- const raw = typeof rawValue === "string" ? rawValue : "";
206
- if (raw.trim().length === 0) return [];
207
- const out = [];
208
- for (const token of raw.split(",")) {
209
- const normalized = normalizeHookToken(token);
210
- if (normalized.length === 0) continue;
211
- if (!out.includes(normalized)) out.push(normalized);
9
+ if (import.meta.url === `file://${process.argv[1]}`) {
10
+ const [, , hookFile] = process.argv;
11
+ if (!hookFile) {
12
+ process.stderr.write("usage: run-hook.entry.js <hook-file.mjs>\n");
13
+ process.exit(2);
212
14
  }
213
- return out;
214
- }
215
-
216
- function parseInlineYamlList(rawValue) {
217
- const raw = typeof rawValue === "string" ? rawValue.trim() : "";
218
- if (!raw.startsWith("[") || !raw.endsWith("]")) return [];
219
- const inside = raw.slice(1, -1).trim();
220
- if (inside.length === 0) return [];
221
- return inside.split(",").map((token) => normalizeHookToken(token.replace(/^['"]|['"]$/g, ""))).filter((token) => token.length > 0);
222
- }
223
-
224
- function parseConfigHookProfile(rawYaml) {
225
- if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
226
- return "";
227
- }
228
- const match = rawYaml.match(/^\\s*hookProfile\\s*:\\s*([A-Za-z0-9_-]+)\\s*$/m);
229
- if (!match || typeof match[1] !== "string") return "";
230
- return parseHookProfile(match[1], "");
231
- }
232
-
233
- function parseConfigDisabledHooks(rawYaml) {
234
- if (typeof rawYaml !== "string" || rawYaml.trim().length === 0) {
235
- return [];
236
- }
237
- const lines = rawYaml.split(/\\r?\\n/u);
238
- const out = [];
239
- for (let i = 0; i < lines.length; i += 1) {
240
- const line = lines[i];
241
- const inlineMatch = line.match(/^\\s*disabledHooks\\s*:\\s*(\\[[^\\]]*\\])\\s*$/u);
242
- if (inlineMatch) {
243
- for (const value of parseInlineYamlList(inlineMatch[1])) {
244
- if (!out.includes(value)) out.push(value);
245
- }
246
- continue;
247
- }
248
- const blockMatch = line.match(/^(\\s*)disabledHooks\\s*:\\s*$/u);
249
- if (!blockMatch) continue;
250
- const baseIndent = blockMatch[1] ? blockMatch[1].length : 0;
251
- for (let j = i + 1; j < lines.length; j += 1) {
252
- const nextLine = lines[j];
253
- const indent = (nextLine.match(/^(\\s*)/u)?.[1].length ?? 0);
254
- const trimmed = nextLine.trim();
255
- if (trimmed.length === 0) continue;
256
- if (indent <= baseIndent) break;
257
- const itemMatch = nextLine.match(/^\\s*-\\s*(.+?)\\s*$/u);
258
- if (!itemMatch) continue;
259
- const normalized = normalizeHookToken(itemMatch[1].replace(/^['"]|['"]$/g, ""));
260
- if (normalized.length === 0) continue;
261
- if (!out.includes(normalized)) out.push(normalized);
262
- }
263
- }
264
- return out;
265
- }
266
-
267
- async function readConfigHookPolicy(root) {
268
- const configPath = path.join(root, RUNTIME_ROOT, "config.yaml");
269
- const raw = await readTextFile(configPath, "");
270
- const profile = parseConfigHookProfile(raw);
271
- const disabledHooks = parseConfigDisabledHooks(raw);
272
- return { profile, disabledHooks };
273
- }
274
-
275
- async function resolveHookPolicy(root) {
276
- const fromConfig = await readConfigHookPolicy(root);
277
- const configProfile = parseHookProfile(fromConfig.profile, DEFAULT_HOOK_PROFILE);
278
- const configDisabledHooks = Array.isArray(fromConfig.disabledHooks) && fromConfig.disabledHooks.length > 0
279
- ? fromConfig.disabledHooks
280
- : DEFAULT_DISABLED_HOOKS;
281
-
282
- const envProfileRaw = process.env.CCLAW_HOOK_PROFILE;
283
- const envProfile = parseHookProfile(envProfileRaw, "");
284
- const profile = envProfile.length > 0 ? envProfile : configProfile;
285
-
286
- const envDisabledRaw = process.env.CCLAW_DISABLED_HOOKS;
287
- const envDisabledHooks = parseDisabledHooksCsv(envDisabledRaw);
288
- const disabledHooks = envDisabledHooks.length > 0 ? envDisabledHooks : configDisabledHooks;
289
- const disabled = new Set(disabledHooks.map((value) => normalizeHookToken(value)));
290
- return { profile, disabled };
291
- }
292
-
293
- function hookDisabledByProfile(profile, hookName) {
294
- if (profile !== "minimal") return false;
295
- return !MINIMAL_PROFILE_ALLOWED_HOOKS.has(hookName);
296
- }
297
-
298
- function isHookDisabled(policy, hookName) {
299
- if (policy.disabled.has(hookName)) return true;
300
- return hookDisabledByProfile(policy.profile, hookName);
301
- }
302
-
303
- function toObject(value) {
304
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
305
- return value;
306
- }
307
-
308
- function safeParseJson(raw, fallback = {}) {
309
- if (typeof raw !== "string" || raw.trim().length === 0) {
310
- return fallback;
311
- }
312
- try {
313
- const parsed = JSON.parse(raw);
314
- return parsed === undefined ? fallback : parsed;
315
- } catch {
316
- return fallback;
317
- }
318
- }
319
-
320
- // === atomic/locked state I/O =========================================
321
- //
322
- // The generated hook script runs OUTSIDE the cclaw CLI process, so it
323
- // cannot import \`fs-utils.ts\`. These helpers mirror \`writeFileSafe\` and
324
- // \`withDirectoryLock\` just enough to keep hook-owned state files
325
- // atomic and free of interleaved concurrent writes.
326
-
327
- function hookSleep(ms) {
328
- return new Promise((resolve) => setTimeout(resolve, ms));
329
- }
330
-
331
- async function withDirectoryLockInline(lockPath, fn, options = {}) {
332
- const retries = Number.isFinite(options.retries) ? options.retries : 200;
333
- const retryDelayMs = Number.isFinite(options.retryDelayMs) ? options.retryDelayMs : 20;
334
- const staleAfterMs = Number.isFinite(options.staleAfterMs) ? options.staleAfterMs : 60000;
335
- try {
336
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
337
- } catch {
338
- // parent may already exist
339
- }
340
- let acquired = false;
341
- let lastError = null;
342
- for (let attempt = 0; attempt < retries; attempt += 1) {
343
- try {
344
- await fs.mkdir(lockPath);
345
- acquired = true;
346
- break;
347
- } catch (error) {
348
- lastError = error;
349
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
350
- if (code !== "EEXIST") {
351
- throw error;
352
- }
353
- try {
354
- const stat = await fs.stat(lockPath);
355
- if (!stat.isDirectory()) {
356
- throw new Error("Lock path exists but is not a directory: " + lockPath);
357
- }
358
- if (Date.now() - stat.mtimeMs > staleAfterMs) {
359
- await fs.rm(lockPath, { recursive: true, force: true });
360
- continue;
361
- }
362
- } catch (statError) {
363
- if (
364
- statError instanceof Error &&
365
- statError.message.startsWith("Lock path exists but is not a directory")
366
- ) {
367
- throw statError;
368
- }
369
- // lock vanished between retries
370
- }
371
- await hookSleep(retryDelayMs);
372
- }
373
- }
374
- if (!acquired) {
375
- const details = lastError instanceof Error ? lastError.message : String(lastError);
376
- throw new Error(
377
- "cclaw hook: failed to acquire lock " + lockPath + " (attempts=" + retries + ", lastError=" + details + ")"
378
- );
379
- }
380
- try {
381
- return await fn();
382
- } finally {
383
- await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
384
- }
385
- }
386
-
387
- async function writeFileAtomic(filePath, content, options = {}) {
388
- await fs.mkdir(path.dirname(filePath), { recursive: true });
389
- const tempPath = path.join(
390
- path.dirname(filePath),
391
- "." + path.basename(filePath) + ".tmp-" + process.pid + "-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8)
392
- );
393
- await fs.writeFile(tempPath, content, { encoding: "utf8" });
394
- // Windows' fs.rename can fail transiently with EPERM/EBUSY/EACCES when the
395
- // destination file is held open by another process (antivirus, indexer,
396
- // or a sibling hook invocation racing on the same file). Retry with tiny
397
- // backoff before falling back to copyFile.
398
- const renameRetryableCodes = new Set(["EPERM", "EBUSY", "EACCES"]);
399
- let attempt = 0;
400
- const maxAttempts = 6;
401
- while (true) {
402
- try {
403
- await fs.rename(tempPath, filePath);
404
- if (options.mode !== undefined) {
405
- await fs.chmod(filePath, options.mode).catch(() => undefined);
406
- }
407
- return;
408
- } catch (error) {
409
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
410
- if (code === "EXDEV") {
411
- try {
412
- await fs.copyFile(tempPath, filePath);
413
- } finally {
414
- await fs.unlink(tempPath).catch(() => undefined);
415
- }
416
- if (options.mode !== undefined) {
417
- await fs.chmod(filePath, options.mode).catch(() => undefined);
418
- }
419
- return;
420
- }
421
- if (renameRetryableCodes.has(code) && attempt < maxAttempts) {
422
- attempt += 1;
423
- await hookSleep(10 * attempt + Math.floor(Math.random() * 10));
424
- continue;
425
- }
426
- if (renameRetryableCodes.has(code)) {
427
- // Last-resort fallback: copy-then-unlink. Not atomic, but the
428
- // directory lock around this call already serializes writers.
429
- try {
430
- await fs.copyFile(tempPath, filePath);
431
- if (options.mode !== undefined) {
432
- await fs.chmod(filePath, options.mode).catch(() => undefined);
433
- }
434
- return;
435
- } finally {
436
- await fs.unlink(tempPath).catch(() => undefined);
437
- }
438
- }
439
- await fs.unlink(tempPath).catch(() => undefined);
440
- throw error;
441
- }
442
- }
443
- }
444
-
445
- function lockPathFor(filePath) {
446
- return filePath + ".lock";
447
- }
448
-
449
- async function recordHookError(root, stage, detail) {
450
- try {
451
- const errorsPath = path.join(root, RUNTIME_ROOT, "state", "hook-errors.jsonl");
452
- await fs.mkdir(path.dirname(errorsPath), { recursive: true });
453
- const payload = JSON.stringify({
454
- ts: new Date().toISOString(),
455
- stage: typeof stage === "string" ? stage : "unknown",
456
- detail: typeof detail === "string" ? detail : String(detail)
457
- });
458
- await fs.appendFile(errorsPath, payload + "\\n", "utf8");
459
- } catch {
460
- // diagnostics must never cascade
461
- }
462
- }
463
-
464
- async function readJsonFile(filePath, fallback = {}, options = {}) {
465
- try {
466
- const raw = await fs.readFile(filePath, "utf8");
467
- if (typeof raw !== "string" || raw.trim().length === 0) {
468
- return fallback;
469
- }
470
- try {
471
- const parsed = JSON.parse(raw);
472
- return parsed === undefined ? fallback : parsed;
473
- } catch (parseErr) {
474
- // Emit a diagnostic breadcrumb instead of silently returning fallback.
475
- // The hook must still continue (soft-fail), but the corruption is
476
- // now visible in \`state/hook-errors.jsonl\` and to \`npx cclaw-cli sync\`.
477
- if (options.root) {
478
- await recordHookError(
479
- options.root,
480
- options.stage || "read-json",
481
- "corrupt-json file=" + filePath + " error=" + (parseErr instanceof Error ? parseErr.message : String(parseErr))
482
- );
483
- }
484
- return fallback;
485
- }
486
- } catch {
487
- return fallback;
488
- }
489
- }
490
-
491
- async function writeJsonFile(filePath, value) {
492
- const next = JSON.stringify(value, null, 2) + "\\n";
493
- await withDirectoryLockInline(lockPathFor(filePath), async () => {
494
- await writeFileAtomic(filePath, next);
495
- });
496
- }
497
-
498
- async function readTextFile(filePath, fallback = "") {
499
- try {
500
- return await fs.readFile(filePath, "utf8");
501
- } catch {
502
- return fallback;
503
- }
504
- }
505
-
506
- // CLI-compatible knowledge lock. Must match
507
- // src/knowledge-store.ts::knowledgeLockPath exactly so the hook and the
508
- // CLI serialize on the same mutex when reading / appending
509
- // knowledge.jsonl. Drift here re-introduces the race we just closed.
510
- function knowledgeLockPathInline(root) {
511
- return path.join(root, RUNTIME_ROOT, "state", ".knowledge.lock");
512
- }
513
-
514
- async function readTextFileLocked(lockPath, filePath, fallback = "") {
515
- return withDirectoryLockInline(lockPath, async () => {
516
- try {
517
- return await fs.readFile(filePath, "utf8");
518
- } catch {
519
- return fallback;
520
- }
521
- });
522
- }
523
-
524
- async function readStdin() {
525
- return await new Promise((resolve) => {
526
- let data = "";
527
- process.stdin.setEncoding("utf8");
528
- process.stdin.on("data", (chunk) => {
529
- data += String(chunk);
530
- });
531
- process.stdin.on("end", () => resolve(data));
532
- process.stdin.on("error", () => resolve(""));
533
- });
534
- }
535
-
536
- function detectHarness(env) {
537
- if (env.CLAUDE_PROJECT_DIR) return "claude";
538
- if (env.CURSOR_PROJECT_DIR || env.CURSOR_PROJECT_ROOT) return "cursor";
539
- if (env.OPENCODE_PROJECT_DIR || env.OPENCODE_PROJECT_ROOT) return "opencode";
540
- return "codex";
541
- }
542
-
543
- async function detectRoot(env) {
544
- const candidates = [
545
- env.CCLAW_PROJECT_ROOT,
546
- env.CLAUDE_PROJECT_DIR,
547
- env.CURSOR_PROJECT_DIR,
548
- env.CURSOR_PROJECT_ROOT,
549
- env.OPENCODE_PROJECT_DIR,
550
- env.OPENCODE_PROJECT_ROOT,
551
- process.cwd()
552
- ].filter((value) => typeof value === "string" && value.length > 0);
553
- for (const candidate of candidates) {
554
- try {
555
- const runtimePath = path.join(candidate, RUNTIME_ROOT);
556
- const stat = await fs.stat(runtimePath);
557
- if (stat.isDirectory()) return { root: candidate, foundRuntime: true };
558
- } catch {
559
- // continue
560
- }
561
- }
562
- return { root: candidates[0] || process.cwd(), foundRuntime: false };
563
- }
564
-
565
- function normalizeText(value) {
566
- return String(value || "").replace(/\\s+/gu, " ").trim();
567
- }
568
-
569
- async function verifyFlowStateGuardInline(root, hookName) {
570
- const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
571
- const guardPath = path.join(root, FLOW_STATE_GUARD_REL_PATH);
572
- let raw;
573
- try {
574
- raw = await fs.readFile(statePath, "utf8");
575
- } catch {
576
- return true;
577
- }
578
- let guard;
579
- try {
580
- const guardRaw = await fs.readFile(guardPath, "utf8");
581
- guard = JSON.parse(guardRaw);
582
- } catch {
583
- return true;
584
- }
585
- if (!guard || typeof guard !== "object" || typeof guard.sha256 !== "string") {
586
- return true;
587
- }
588
- const actual = createHash("sha256").update(raw, "utf8").digest("hex");
589
- if (actual === guard.sha256) return true;
590
- const hookLabel = typeof hookName === "string" && hookName.length > 0 ? hookName : "hook";
591
- process.stderr.write(
592
- "[cclaw] " + hookLabel + ": flow-state guard mismatch: " + (guard.runId || "unknown-run") + "\\n" +
593
- "expected sha: " + guard.sha256 + "\\n" +
594
- "actual sha: " + actual + "\\n" +
595
- "last writer: " + (guard.writerSubsystem || "unknown") + "@" + (guard.writtenAt || "unknown") + "\\n" +
596
- "do not edit flow-state.json by hand. To recover, run:\\n" +
597
- " cclaw-cli internal flow-state-repair --reason \\"manual_edit_recovery\\"\\n"
598
- );
599
- await recordHookError(root, hookLabel, "flow-state guard mismatch actual=" + actual + " expected=" + guard.sha256).catch(() => undefined);
600
- return false;
601
- }
602
-
603
- async function readFlowState(root) {
604
- const statePath = path.join(root, RUNTIME_ROOT, "state", "flow-state.json");
605
- // Loud-on-corrupt: if flow-state.json exists but fails JSON.parse, log
606
- // a breadcrumb into state/hook-errors.jsonl before falling back to an
607
- // empty object. Silent fallbacks used to mask stale CLI+hook drift.
608
- const parsed = await readJsonFile(statePath, {}, { root, stage: "read-flow-state" });
609
- const obj = toObject(parsed) || {};
610
- const summary = summarizeFlowState(obj);
611
- return {
612
- filePath: statePath,
613
- currentStage: summary.stage,
614
- activeRunId: summary.activeRunId === "none" ? "active" : summary.activeRunId,
615
- completedCount: summary.completed,
616
- raw: obj
617
- };
618
- }
619
-
620
- async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
621
- const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
622
- // Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
623
- // Falls back to a local read if nothing is passed in.
624
- const raw = typeof prereadRaw === "string"
625
- ? prereadRaw
626
- : await readTextFile(knowledgeFile, "");
627
- const digest = parseKnowledgeDigest(raw, currentStage, 6);
628
- return {
629
- digestLines: digest.lines,
630
- learningsCount: digest.learningsCount
631
- };
632
- }
633
-
634
- async function readStageSupportContext(root, currentStage) {
635
- if (!isKnownStageId(currentStage)) return [];
636
- const stage = currentStage;
637
-
638
- const parts = [];
639
- const contractPath = path.join(root, RUNTIME_ROOT, "templates", "state-contracts", stage + ".json");
640
- const contract = (await readTextFile(contractPath, "")).trim();
641
- if (contract.length > 0) {
642
- parts.push(
643
- "Current stage state contract (read before drafting or editing the stage artifact):\\n" +
644
- contract
645
- );
646
- }
647
-
648
- const promptName = reviewPromptFileName(stage);
649
- if (typeof promptName === "string") {
650
- const promptPath = path.join(root, RUNTIME_ROOT, "skills", "review-prompts", promptName);
651
- const prompt = (await readTextFile(promptPath, "")).trim();
652
- if (prompt.length > 0) {
653
- parts.push(
654
- "Current stage calibrated review prompt (use before asking for approval/completion):\\n" +
655
- prompt
656
- );
657
- }
658
- }
659
-
660
- return parts;
661
- }
662
-
663
- async function handleSessionStart(runtime) {
664
- const state = await readFlowState(runtime.root);
665
- const metaSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "using-cclaw", "SKILL.md");
666
-
667
-
668
- // Read knowledge.jsonl exactly once per session-start while holding the
669
- // SAME lock CLI writers acquire in \`appendKnowledge\`. Guarantees we never
670
- // see a partial (mid-write) snapshot. Both the digest and
671
- // compound-readiness derive from this single read.
672
- const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
673
- const knowledgeRaw = await readTextFileLocked(
674
- knowledgeLockPathInline(runtime.root),
675
- knowledgeFilePath,
676
- ""
677
- );
678
- const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
679
-
680
- // honest-core: session-start no longer runs background helper
681
- // pipelines or digest caches. It rehydrates flow + knowledge only.
682
- const ralphLoopLine = "";
683
- const earlyLoopLine = "";
684
- const compoundReadinessLine = "";
685
- const staleStages = toObject(state.raw.staleStages) || {};
686
- const staleStageNames = Object.keys(staleStages);
687
- const interactionHints = toObject(state.raw.interactionHints) || {};
688
- const stageInteractionHint = toObject(interactionHints[state.currentStage]);
689
- const skipQuestionsHintActive = stageInteractionHint?.skipQuestions === true;
690
- const skipQuestionsSource = typeof stageInteractionHint?.sourceStage === "string"
691
- ? stageInteractionHint.sourceStage
692
- : "";
693
- const skipQuestionsRecordedAt = typeof stageInteractionHint?.recordedAt === "string"
694
- ? stageInteractionHint.recordedAt
695
- : "";
696
- const metaContent = (await readTextFile(metaSkillFile, "")).trim();
697
- const ironLawsSkillFile = path.join(runtime.root, RUNTIME_ROOT, "skills", "iron-laws", "SKILL.md");
698
- const ironLawsContent = (await readTextFile(ironLawsSkillFile, "")).trim();
699
- const stageSupportContext = await readStageSupportContext(runtime.root, state.currentStage);
700
-
701
- const parts = [
702
- "cclaw loaded. Flow: stage=" +
703
- state.currentStage +
704
- " (" +
705
- String(state.completedCount) +
706
- "/8 completed, run=" +
707
- state.activeRunId +
708
- "). Active artifacts: " +
709
- activeArtifactsPathLabel(RUNTIME_ROOT) +
710
- " Learnings: " +
711
- String(knowledge.learningsCount) +
712
- " entries."
713
- ];
714
- if (ralphLoopLine.length > 0) {
715
- parts.push(ralphLoopLine);
716
- }
717
- if (earlyLoopLine.length > 0) {
718
- parts.push(earlyLoopLine);
719
- }
720
- if (compoundReadinessLine.length > 0) {
721
- parts.push(compoundReadinessLine);
722
- }
723
- if (staleStageNames.length > 0) {
724
- parts.push(
725
- "Stale stages pending acknowledgement: " +
726
- staleStageNames.join(", ") +
727
- " (use npx cclaw-cli internal rewind --ack <stage> after redo)."
728
- );
729
- }
730
- if (skipQuestionsHintActive) {
731
- parts.push(
732
- "Adaptive elicitation hint: this stage inherits a prior user stop signal (--skip-questions" +
733
- (skipQuestionsSource ? " from " + skipQuestionsSource : "") +
734
- (skipQuestionsRecordedAt ? " at " + skipQuestionsRecordedAt : "") +
735
- "). Draft with available context unless irreversible/security override checks still require explicit confirmation."
736
- );
737
- }
738
- if (knowledge.digestLines.length > 0) {
739
- parts.push(
740
- "Knowledge digest (top relevant entries):\\n" +
741
- knowledge.digestLines.join("\\n")
742
- );
743
- }
744
- if (stageSupportContext.length > 0) {
745
- parts.push(...stageSupportContext);
746
- }
747
- if (metaContent.length > 0) {
748
- parts.push(metaContent);
749
- }
750
- // load iron-laws content into the session-start digest so the
751
- // non-negotiable workflow constraints are visible from the first turn,
752
- // not lazily on tool dispatch.
753
- if (ironLawsContent.length > 0) {
754
- parts.push(ironLawsContent);
755
- }
756
-
757
- const context = parts.join("\\n");
758
- if (runtime.harness === "claude" || runtime.harness === "codex") {
759
- runtime.writeJson({
760
- hookSpecificOutput: {
761
- hookEventName: "SessionStart",
762
- additionalContext: context
763
- }
764
- });
765
- return 0;
766
- }
767
- runtime.writeJson({ additional_context: context });
768
- return 0;
769
- }
770
-
771
- async function isGitDirty(root) {
772
- return await new Promise((resolve) => {
773
- const child = spawn("git", ["-C", root, "status", "--porcelain"], {
774
- stdio: ["ignore", "pipe", "ignore"]
775
- });
776
- let output = "";
777
- child.stdout.on("data", (chunk) => {
778
- output += String(chunk);
779
- });
780
- child.on("error", () => resolve("unknown"));
781
- child.on("close", (code) => {
782
- if (code !== 0) {
783
- resolve("unknown");
784
- } else {
785
- resolve(output.trim().length > 0 ? "dirty" : "clean");
786
- }
787
- });
788
- });
789
- }
790
-
791
- const STOP_BLOCK_LIMIT_PER_TRANSCRIPT = 2;
792
-
793
- function asBoolean(value) {
794
- if (value === true || value === false) return value;
795
- if (typeof value === "number") return Number.isFinite(value) && value !== 0;
796
- if (typeof value !== "string") return false;
797
- const normalized = value.trim().toLowerCase();
798
- if (normalized.length === 0) return false;
799
- return ["1", "true", "yes", "on"].includes(normalized);
800
- }
801
-
802
- function stringTokenHit(value, tokens) {
803
- const normalized = normalizeText(value).toLowerCase();
804
- if (normalized.length === 0) return false;
805
- return tokens.some((token) => normalized.includes(token));
806
- }
807
-
808
- function sanitizeStopSessionKey(raw) {
809
- const normalized = normalizeText(raw)
810
- .toLowerCase()
811
- .replace(/[^a-z0-9._-]+/gu, "-")
812
- .replace(/^-+|-+$/gu, "");
813
- return normalized.length > 0 ? normalized.slice(0, 96) : "global";
814
- }
815
-
816
- function extractStopSignals(input, fallbackSessionKey) {
817
- const event = toObject(input.event) || {};
818
- const session = toObject(input.session) || {};
819
- const contextLimit =
820
- asBoolean(input.context_limit) ||
821
- asBoolean(input.contextLimit) ||
822
- asBoolean(event.context_limit) ||
823
- asBoolean(event.contextLimit) ||
824
- stringTokenHit(input.reason, ["context_limit", "context limit"]) ||
825
- stringTokenHit(event.reason, ["context_limit", "context limit"]) ||
826
- stringTokenHit(input.stop_reason, ["context_limit", "context limit"]) ||
827
- stringTokenHit(event.stop_reason, ["context_limit", "context limit"]);
828
- const userAbort =
829
- asBoolean(input.user_abort) ||
830
- asBoolean(input.userAbort) ||
831
- asBoolean(input.user_cancelled) ||
832
- asBoolean(input.userCancelled) ||
833
- asBoolean(event.user_abort) ||
834
- asBoolean(event.userAbort) ||
835
- stringTokenHit(input.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
836
- stringTokenHit(event.reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
837
- stringTokenHit(input.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]) ||
838
- stringTokenHit(event.stop_reason, ["user_abort", "user abort", "cancelled by user", "stop button", "ctrl+c"]);
839
- const stopHookActive =
840
- asBoolean(input.stop_hook_active) ||
841
- asBoolean(input.stopHookActive) ||
842
- asBoolean(event.stop_hook_active) ||
843
- asBoolean(event.stopHookActive);
844
-
845
- const sessionKeyCandidate =
846
- (typeof input.transcript_id === "string" && input.transcript_id) ||
847
- (typeof input.transcriptId === "string" && input.transcriptId) ||
848
- (typeof input.session_id === "string" && input.session_id) ||
849
- (typeof input.sessionId === "string" && input.sessionId) ||
850
- (typeof session.id === "string" && session.id) ||
851
- fallbackSessionKey;
852
- const sessionKey = sanitizeStopSessionKey(sessionKeyCandidate);
853
-
854
- return {
855
- contextLimit,
856
- userAbort,
857
- stopHookActive,
858
- sessionKey
859
- };
860
- }
861
-
862
- async function handleStopHandoff(runtime) {
863
- const state = await readFlowState(runtime.root);
864
- const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
865
- const input = toObject(runtime.inputData) || {};
866
- const loopCount =
867
- typeof input.loop_count === "number" && Number.isFinite(input.loop_count)
868
- ? Math.trunc(input.loop_count)
869
- : 0;
870
-
871
- const dirtyState = await isGitDirty(runtime.root);
872
- const stopSignals = extractStopSignals(input, "run-" + state.activeRunId);
873
- const safetyBypassActive = stopSignals.stopHookActive || stopSignals.userAbort || stopSignals.contextLimit;
874
- if (dirtyState === "dirty" && !safetyBypassActive) {
875
- const stopBlocksPath = path.join(stateDir, "stop-blocks-" + stopSignals.sessionKey + ".json");
876
- const prior = toObject(await readJsonFile(stopBlocksPath, {})) || {};
877
- const priorCount =
878
- typeof prior.blockCount === "number" && Number.isFinite(prior.blockCount)
879
- ? Math.max(0, Math.trunc(prior.blockCount))
880
- : 0;
881
- if (priorCount < STOP_BLOCK_LIMIT_PER_TRANSCRIPT) {
882
- const nextCount = priorCount + 1;
883
- await writeJsonFile(stopBlocksPath, {
884
- schemaVersion: 1,
885
- sessionKey: stopSignals.sessionKey,
886
- blockCount: nextCount,
887
- updatedAt: new Date().toISOString()
888
- });
889
- process.stderr.write(
890
- '[cclaw] Stop blocked by iron law "stop-clean-or-handoff": working tree is dirty. Commit/revert changes or record blockers in the current artifact before ending the session.\\n'
891
- );
892
- return 1;
893
- }
894
- process.stderr.write(
895
- '[cclaw] Stop advisory: dirty working tree detected, but block limit reached for this transcript (max 2). Continuing with handoff reminder only.\\n'
896
- );
897
- } else if (dirtyState === "dirty" && safetyBypassActive) {
898
- const reason = stopSignals.stopHookActive
899
- ? "stop_hook_active"
900
- : stopSignals.userAbort
901
- ? "user_abort"
902
- : "context_limit";
903
- process.stderr.write(
904
- "[cclaw] Stop advisory: bypassing strict stop block due to safety rule (" + reason + ").\\n"
905
- );
906
- }
907
-
908
- const closeoutObj = toObject(state.raw.closeout) || {};
909
- const shipSubstate = typeof closeoutObj.shipSubstate === "string" ? closeoutObj.shipSubstate : "idle";
910
- const closeoutContext =
911
- state.currentStage === "ship" || shipSubstate !== "idle"
912
- ? " closeout.shipSubstate=" + shipSubstate + "; closeout chain=post_ship_review -> archive; continue closeout with /cc."
913
- : "";
914
-
915
- const message =
916
- "Cclaw: session ending (stage=" +
917
- state.currentStage +
918
- ", run=" +
919
- state.activeRunId +
920
- ")." +
921
- closeoutContext +
922
- " Active artifacts stay in " +
923
- RUNTIME_ROOT +
924
- "/artifacts until archive. Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match current intent, (3) if you discovered a non-obvious rule/pattern during stage work, add it to the current artifact ## Learnings section so stage-complete can harvest it, (4) commit or revert pending changes.";
925
-
926
- if (runtime.harness === "cursor") {
927
- if (loopCount === 0) {
928
- runtime.writeJson({ followup_message: message });
929
- } else {
930
- runtime.writeJson({});
931
- }
932
- return 0;
933
- }
934
-
935
- runtime.writeJson({ systemMessage: message });
936
- return 0;
937
- }
938
-
939
- function normalizeHookName(rawName) {
940
- const value = normalizeText(rawName).toLowerCase();
941
- if (value === "session-start") return "session-start";
942
- if (value === "stop-handoff" || value === "stop") return "stop-handoff";
943
- if (value === "stop-checkpoint") return "stop-handoff";
944
- if (value === "session-rehydrate") return "session-start";
945
- return "";
946
- }
947
-
948
- async function main() {
949
- const hookName = normalizeHookName(process.argv[2] || "");
950
- if (!hookName) {
951
- process.stderr.write(
952
- "[cclaw] run-hook: usage: node " +
953
- RUNTIME_ROOT +
954
- "/hooks/run-hook.mjs <session-start|stop-handoff>\\n"
955
- );
956
- process.exitCode = 1;
957
- return;
958
- }
959
-
960
- const harness = detectHarness(process.env);
961
- const { root, foundRuntime } = await detectRoot(process.env);
962
- if (!foundRuntime) {
963
- // No .cclaw/ runtime in any candidate root \u2014 this directory is not
964
- // initialized for cclaw. Exit 0 silently so hooks never block harnesses
965
- // that run in unrelated repos; users initialize with \`cclaw init\`.
966
- process.exitCode = 0;
967
- return;
968
- }
969
- const inputRaw = await readStdin();
970
- const inputData = safeParseJson(inputRaw, {});
971
- const runtime = {
972
- harness,
973
- root,
974
- inputRaw,
975
- inputData,
976
- writeJson(value) {
977
- process.stdout.write(JSON.stringify(value) + "\\n");
978
- }
979
- };
980
-
981
- try {
982
- const policy = await resolveHookPolicy(runtime.root);
983
- if (isHookDisabled(policy, hookName)) {
984
- // Honor CCLAW_HOOK_PROFILE / CCLAW_DISABLED_HOOKS / config disabledHooks.
985
- // Disabled hooks must exit 0 quietly so harnesses keep running.
986
- process.exitCode = 0;
987
- return;
988
- }
989
- if (hookName === "session-start" || hookName === "stop-handoff") {
990
- const guardOk = await verifyFlowStateGuardInline(runtime.root, hookName);
991
- if (!guardOk) {
992
- process.exitCode = 2;
993
- return;
994
- }
995
- }
996
- if (hookName === "session-start") {
997
- process.exitCode = await handleSessionStart(runtime);
998
- return;
999
- }
1000
- if (hookName === "stop-handoff") {
1001
- process.exitCode = await handleStopHandoff(runtime);
1002
- return;
1003
- }
1004
- process.stderr.write("[cclaw] run-hook: unsupported hook " + hookName + "\\n");
1005
- process.exitCode = 1;
1006
- } catch (error) {
1007
- process.stderr.write(
1008
- "[cclaw] run-hook: " +
1009
- hookName +
1010
- " failed: " +
1011
- (error instanceof Error ? error.message : String(error)) +
1012
- "\\n"
1013
- );
1014
- process.exitCode = 1;
1015
- }
1016
- }
1017
-
1018
- void main();
1019
- `;
1020
- }
1021
-
1022
- // src/runtime/run-hook.entry.ts
1023
- function buildRunHookRuntimeScript(options = {}) {
1024
- return nodeHookRuntimeScript(options);
15
+ await runHookByName(process.cwd(), hookFile);
1025
16
  }
1026
- var run_hook_entry_default = buildRunHookRuntimeScript;
1027
17
  export {
1028
- buildRunHookRuntimeScript,
1029
- run_hook_entry_default as default
18
+ runHookByName
1030
19
  };