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
package/dist/install.js CHANGED
@@ -1,1393 +1,281 @@
1
- import { execFile } from "node:child_process";
2
1
  import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { promisify } from "node:util";
5
- import { CCLAW_VERSION, FLOW_VERSION, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
6
- import { writeConfig, createDefaultConfig, readConfig, configPath, detectAdvancedKeys } from "./config.js";
7
- import { learnSkillMarkdown } from "./content/learnings.js";
8
- import { stageCommandShimMarkdown } from "./content/stage-command.js";
9
- import { ideaCommandContract, ideaCommandSkillMarkdown } from "./content/idea.js";
10
- import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
11
- import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-command.js";
12
- import { cancelCommandContract, cancelCommandSkillMarkdown } from "./content/cancel-command.js";
13
- import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
14
- import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
- import { ironLawsSkillMarkdown } from "./content/iron-laws.js";
16
- import { stageCompleteScript, startFlowScript, cancelRunScript, runHookCmdScript, delegationRecordScript, sliceCommitScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
17
- import { nodeHookRuntimeScript } from "./content/node-hooks.js";
18
- import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
19
- import { ARTIFACT_TEMPLATES, CURSOR_GUIDELINES_RULE_MDC, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
20
- import { STATE_CONTRACTS } from "./content/state-contracts.js";
21
- import { REVIEW_PROMPTS } from "./content/review-prompts.js";
22
- import { stageSkillFolder, stageSkillMarkdown, executingWavesSkillMarkdown } from "./content/skills.js";
23
- import { adaptiveElicitationSkillMarkdown } from "./content/skills-elicitation.js";
24
- import { LANGUAGE_RULE_PACK_DIR, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
25
- import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
26
- import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
27
- import { CCLAW_AGENTS } from "./content/core-agents.js";
28
- import { createInitialFlowState } from "./flow-state.js";
29
- import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
30
- import { ManagedResourceSession, readManagedResourceManifest, setActiveManagedResourceSession } from "./managed-resources.js";
31
- import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
32
- import { HARNESS_ADAPTERS, harnessShimFileNames, harnessShimSkillNames, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
33
- import { validateHookDocument } from "./hook-schema.js";
34
- import { detectHarnesses } from "./init-detect.js";
35
- import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
36
- import { CorruptFlowStateError, ensureRunSystem } from "./runs.js";
37
- import { formatNextParallelWaveSyncHint, mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./internal/plan-split-waves.js";
38
- import { FLOW_STAGES } from "./types.js";
39
- const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
40
- const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
41
- const CURSOR_GUIDELINES_REL_PATH = ".cursor/rules/cclaw-guidelines.mdc";
42
- const INIT_SENTINEL_FILE = ".init-in-progress";
43
- const execFileAsync = promisify(execFile);
44
- function runtimePath(projectRoot, ...segments) {
45
- return path.join(projectRoot, RUNTIME_ROOT, ...segments);
46
- }
47
- async function writeInitSentinel(projectRoot, operation) {
48
- const sentinelPath = runtimePath(projectRoot, "state", INIT_SENTINEL_FILE);
49
- await ensureDir(path.dirname(sentinelPath));
50
- await writeFileSafe(sentinelPath, `${JSON.stringify({ operation, startedAt: new Date().toISOString() }, null, 2)}\n`);
51
- return sentinelPath;
52
- }
53
- async function warnStaleInitSentinel(projectRoot, operation) {
54
- const sentinelPath = runtimePath(projectRoot, "state", INIT_SENTINEL_FILE);
55
- if (!(await exists(sentinelPath)))
56
- return;
57
- let startedAt = "unknown time";
58
- try {
59
- const raw = await fs.readFile(sentinelPath, "utf8");
60
- const parsed = JSON.parse(raw);
61
- if (parsed && typeof parsed.startedAt === "string" && parsed.startedAt.trim().length > 0) {
62
- startedAt = parsed.startedAt;
63
- }
64
- }
65
- catch {
66
- // best-effort parse of stale sentinel metadata
67
- }
68
- process.stderr.write(`[${operation}] Detected stale .init-in-progress sentinel from ${startedAt}; previous run may have crashed. Continuing.\n`);
69
- }
70
- async function removeBestEffort(targetPath, recursive = false) {
71
- try {
72
- await fs.rm(targetPath, { recursive, force: true });
73
- }
74
- catch {
75
- // best-effort cleanup
76
- }
77
- }
78
- const DEPRECATED_UTILITY_SKILL_FOLDERS = [
79
- "project-learnings",
80
- "auto-orchestration",
81
- "autoplan",
82
- "red-first-testing",
83
- "incremental-implementation",
84
- "subagent-driven-development",
85
- "dispatching-parallel-agents",
86
- "session-guidelines",
87
- "security-review",
88
- "documentation",
89
- "browser-qa-testing",
90
- "feature-workspaces",
91
- "security",
92
- "debugging",
93
- "performance",
94
- "ci-cd",
95
- "docs",
96
- "executing-plans",
97
- "verification-before-completion",
98
- "finishing-a-development-branch",
99
- "context-engineering",
100
- "source-driven-development",
101
- "frontend-accessibility",
102
- "landscape-check",
103
- "knowledge-curation",
104
- "retrospective",
105
- "document-review",
106
- "flow-status",
107
- "flow-tree",
108
- "flow-diff"
109
- ];
110
- const DEPRECATED_STAGE_SKILL_FOLDERS = [
111
- "brainstorming",
112
- "scope-shaping",
113
- "engineering-design-lock",
114
- "specification-authoring",
115
- "planning-and-task-breakdown",
116
- "test-driven-development",
117
- "two-layer-review",
118
- "shipping-and-handoff"
119
- ];
120
- const DEPRECATED_AGENT_FILES = [
121
- "securityer.md",
122
- "spec-reviewer.md",
123
- "code-reviewer.md",
124
- "repo-research-analyst.md",
125
- "learnings-researcher.md",
126
- "framework-docs-researcher.md",
127
- "best-practices-researcher.md",
128
- "git-history-analyzer.md",
129
- "test-author.md",
130
- "slice-implementer.md",
131
- "slice-documenter.md"
132
- ];
133
- const DEPRECATED_COMMAND_FILES = [
134
- "learn.md",
135
- "finish.md",
136
- "status.md",
137
- "tree.md",
138
- "diff.md",
139
- "feature.md",
140
- "ops.md",
141
- "tdd-log.md",
142
- "retro.md",
143
- "compound.md",
144
- "archive.md",
145
- "rewind.md"
146
- ];
147
- const DEPRECATED_SKILL_FILES = [
148
- ["flow-finish", "SKILL.md"],
149
- ["flow-ops", "SKILL.md"],
150
- ["flow-retro", "SKILL.md"],
151
- ["flow-compound", "SKILL.md"],
152
- ["flow-archive", "SKILL.md"],
153
- ["flow-rewind", "SKILL.md"],
154
- ["using-git-worktrees", "SKILL.md"]
155
- ];
156
- // Skill folders whose entire directory should be removed on sync so the
157
- // abandoned tree doesn't linger in user projects.
158
- const DEPRECATED_SKILL_FOLDERS_FULL = [
159
- "tdd-cycle-log"
160
- ];
161
- const DEPRECATED_STATE_FILES = [
162
- "checkpoint.json",
163
- "flow-state.snapshot.json",
164
- "stage-activity.jsonl",
165
- "knowledge-digest.md",
166
- "suggestion-memory.json",
167
- "harness-gaps.json",
168
- "context-mode.json",
169
- "session-digest.md",
170
- "context-warnings.jsonl",
171
- "tdd-cycle-log.jsonl"
172
- ];
173
- // Files under `<runtime>/artifacts/` that older releases generated and
174
- // the current release removes. `cclaw-cli sync` deletes each so existing
175
- // installs lose the obsolete sidecar without requiring manual cleanup.
176
- const DEPRECATED_ARTIFACT_FILES = [
177
- "06-tdd-slices.jsonl"
178
- ];
179
- const DEPRECATED_HOOK_FILES = [
180
- "observe.sh",
181
- "summarize-observations.sh",
182
- "summarize-observations.mjs",
183
- "_lib.sh",
184
- "session-start.sh",
185
- "stop-checkpoint.sh",
186
- "stage-complete.sh",
187
- "pre-compact.sh",
188
- "prompt-guard.sh",
189
- "workflow-guard.sh",
190
- "context-monitor.sh"
191
- ];
192
- const DEPRECATED_RUNTIME_ROOT_FILES = ["learnings.jsonl", "observations.jsonl"];
193
- const DEPRECATED_RUNTIME_DIRS = ["evals", "references", "contexts"];
194
- async function resolveGitHooksDir(projectRoot) {
195
- try {
196
- const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], {
197
- cwd: projectRoot
198
- });
199
- const rel = stdout.trim();
200
- if (rel.length === 0) {
201
- return null;
202
- }
203
- return path.resolve(projectRoot, rel);
204
- }
205
- catch {
206
- return null;
207
- }
208
- }
209
- // Older versions installed Node-based git pre-commit/pre-push relays under
210
- // `.git/hooks/*` and a runtime tree at `.cclaw/hooks/git/`. cclaw no longer
211
- // manages git hooks; the cleanup below stays so existing installs shed the
212
- // leftover files on next sync/uninstall.
213
- const LEGACY_GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
214
- const LEGACY_GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
215
- async function cleanupLegacyManagedGitHookRelays(projectRoot) {
216
- const hooksDir = await resolveGitHooksDir(projectRoot);
217
- if (hooksDir) {
218
- for (const hookName of ["pre-commit", "pre-push"]) {
219
- const hookPath = path.join(hooksDir, hookName);
220
- if (!(await exists(hookPath)))
221
- continue;
222
- let content = "";
223
- try {
224
- content = await fs.readFile(hookPath, "utf8");
225
- }
226
- catch {
227
- content = "";
228
- }
229
- if (!content.includes(LEGACY_GIT_HOOK_MANAGED_MARKER))
230
- continue;
231
- await fs.rm(hookPath, { force: true });
232
- }
233
- }
234
- try {
235
- await fs.rm(path.join(projectRoot, LEGACY_GIT_HOOK_RUNTIME_REL_DIR), { recursive: true, force: true });
236
- }
237
- catch {
238
- // best-effort cleanup
239
- }
240
- }
241
- async function ensureStructure(projectRoot) {
242
- for (const dir of REQUIRED_DIRS) {
3
+ import { CANCELLED_DIR_REL_PATH, CCLAW_VERSION, FLOWS_ROOT, HOOKS_REL_PATH, LIB_ROOT, RUNTIME_ROOT, SHIPPED_DIR_REL_PATH, STATE_REL_PATH } from "./constants.js";
4
+ import { CORE_AGENTS, renderAgentMarkdown } from "./content/core-agents.js";
5
+ import { ARTIFACT_TEMPLATES, planTemplateForSlug, templateBody } from "./content/artifact-templates.js";
6
+ import { AUTO_TRIGGER_SKILLS } from "./content/skills.js";
7
+ import { EXAMPLES, EXAMPLES_INDEX } from "./content/examples.js";
8
+ import { REFERENCE_PATTERNS, REFERENCE_PATTERNS_INDEX } from "./content/reference-patterns.js";
9
+ import { STAGE_PLAYBOOKS, STAGE_PLAYBOOKS_INDEX } from "./content/stage-playbooks.js";
10
+ import { RESEARCH_PLAYBOOKS, RESEARCH_PLAYBOOKS_INDEX } from "./content/research-playbooks.js";
11
+ import { RECOVERY_PLAYBOOKS, RECOVERY_INDEX } from "./content/recovery.js";
12
+ import { ANTIPATTERNS } from "./content/antipatterns.js";
13
+ import { DECISION_PROTOCOL } from "./content/decision-protocol.js";
14
+ import { META_SKILL } from "./content/meta-skill.js";
15
+ import { COMMIT_HELPER_HOOK_SPEC, NODE_HOOKS, SESSION_START_HOOK_SPEC, STOP_HANDOFF_HOOK_SPEC } from "./content/node-hooks.js";
16
+ import { renderStartCommand } from "./content/start-command.js";
17
+ import { renderCancelCommand } from "./content/cancel-command.js";
18
+ import { renderIdeaCommand } from "./content/idea-command.js";
19
+ import { ensureDir, exists, removePath, writeFileSafe } from "./fs-utils.js";
20
+ import { ensureRunSystem } from "./run-persistence.js";
21
+ import { createDefaultConfig, readConfig, renderConfig } from "./config.js";
22
+ import { detectHarnesses, NO_HARNESS_DETECTED_MESSAGE } from "./harness-detect.js";
23
+ import { ensureGitignorePatterns, removeGitignorePatterns } from "./gitignore.js";
24
+ import { HARNESS_IDS } from "./types.js";
25
+ import { ironLawsMarkdown } from "./content/iron-laws.js";
26
+ const HARNESS_LAYOUTS = {
27
+ claude: {
28
+ id: "claude",
29
+ commandsDir: ".claude/commands",
30
+ agentsDir: ".claude/agents",
31
+ skillsDir: ".claude/skills/cclaw",
32
+ hooksConfig: { dir: ".claude/hooks", fileName: "hooks.json" }
33
+ },
34
+ cursor: {
35
+ id: "cursor",
36
+ commandsDir: ".cursor/commands",
37
+ agentsDir: ".cursor/agents",
38
+ skillsDir: ".cursor/skills/cclaw",
39
+ hooksConfig: { dir: ".cursor", fileName: "hooks.json" }
40
+ },
41
+ opencode: {
42
+ id: "opencode",
43
+ commandsDir: ".opencode/commands",
44
+ agentsDir: ".opencode/agents",
45
+ skillsDir: ".opencode/skills/cclaw",
46
+ hooksConfig: { dir: ".opencode/plugins", fileName: "cclaw-plugin.mjs" }
47
+ },
48
+ codex: {
49
+ id: "codex",
50
+ commandsDir: ".codex/commands",
51
+ agentsDir: ".codex/agents",
52
+ skillsDir: ".codex/skills/cclaw",
53
+ hooksConfig: { dir: ".codex", fileName: "hooks.json" }
54
+ }
55
+ };
56
+ export async function ensureRuntimeRoot(projectRoot) {
57
+ const root = path.join(projectRoot, RUNTIME_ROOT);
58
+ for (const dir of [
59
+ STATE_REL_PATH,
60
+ HOOKS_REL_PATH,
61
+ FLOWS_ROOT,
62
+ SHIPPED_DIR_REL_PATH,
63
+ CANCELLED_DIR_REL_PATH,
64
+ LIB_ROOT,
65
+ path.join(LIB_ROOT, "agents"),
66
+ path.join(LIB_ROOT, "skills"),
67
+ path.join(LIB_ROOT, "templates"),
68
+ path.join(LIB_ROOT, "runbooks"),
69
+ path.join(LIB_ROOT, "patterns"),
70
+ path.join(LIB_ROOT, "research"),
71
+ path.join(LIB_ROOT, "recovery"),
72
+ path.join(LIB_ROOT, "examples")
73
+ ]) {
243
74
  await ensureDir(path.join(projectRoot, dir));
244
75
  }
76
+ await writeFileSafe(path.join(root, ".gitkeep"), "cclaw runtime root. Generated by cclaw-cli; safe to keep in version control.\n");
245
77
  }
246
- async function writeArtifactTemplates(projectRoot) {
247
- await Promise.all(Object.entries(ARTIFACT_TEMPLATES).map(async ([fileName, content]) => {
248
- await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
249
- }));
250
- await Promise.all(Object.entries(STATE_CONTRACTS).map(async ([fileName, content]) => {
251
- await writeFileSafe(runtimePath(projectRoot, "templates", "state-contracts", fileName), content);
252
- }));
253
- }
254
- async function writeWavePlansScaffold(projectRoot) {
255
- await writeFileSafe(runtimePath(projectRoot, "wave-plans", ".gitkeep"), "");
256
- }
257
- async function writeSkills(projectRoot, config) {
258
- void config;
259
- const manifest = await readManagedResourceManifest(projectRoot).catch(() => null);
260
- const packageVersion = manifest?.packageVersion ?? null;
261
- const skillTrack = "standard";
262
- for (const stage of FLOW_STAGES) {
263
- const folder = stageSkillFolder(stage);
264
- await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), stageSkillMarkdown(stage, skillTrack, packageVersion));
265
- }
266
- // Utility skills (not flow stages)
267
- await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
268
- await writeFileSafe(runtimePath(projectRoot, "skills", "flow-idea", "SKILL.md"), ideaCommandSkillMarkdown());
269
- await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
270
- await writeFileSafe(runtimePath(projectRoot, "skills", "flow-view", "SKILL.md"), viewCommandSkillMarkdown());
271
- await writeFileSafe(runtimePath(projectRoot, "skills", "flow-cancel", "SKILL.md"), cancelCommandSkillMarkdown());
272
- await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
273
- await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
274
- await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
275
- await writeFileSafe(runtimePath(projectRoot, "skills", "iron-laws", "SKILL.md"), ironLawsSkillMarkdown());
276
- await writeFileSafe(runtimePath(projectRoot, "skills", "executing-waves", "SKILL.md"), executingWavesSkillMarkdown());
277
- await writeFileSafe(runtimePath(projectRoot, "skills", "adaptive-elicitation", "SKILL.md"), adaptiveElicitationSkillMarkdown());
278
- await writeFileSafe(runtimePath(projectRoot, "skills", META_SKILL_NAME, "SKILL.md"), usingCclawSkillMarkdown());
279
- // In-thread research procedures (no YAML frontmatter, not delegated personas).
280
- for (const [fileName, markdown] of Object.entries(RESEARCH_PLAYBOOKS)) {
281
- await writeFileSafe(runtimePath(projectRoot, "skills", "research", fileName), markdown);
282
- }
283
- for (const [fileName, markdown] of Object.entries(REVIEW_PROMPTS)) {
284
- await writeFileSafe(runtimePath(projectRoot, "skills", "review-prompts", fileName), markdown);
285
- }
286
- for (const [folderName, markdown] of Object.entries(SUBAGENT_CONTEXT_SKILLS)) {
287
- await writeFileSafe(runtimePath(projectRoot, "skills", folderName, "SKILL.md"), markdown);
288
- }
289
- await fs.rm(runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR), { recursive: true, force: true });
290
- for (const legacyFolder of LEGACY_LANGUAGE_RULE_PACK_FOLDERS) {
291
- const legacyPath = runtimePath(projectRoot, "skills", legacyFolder);
292
- if (await exists(legacyPath)) {
293
- await fs.rm(legacyPath, { recursive: true, force: true });
294
- }
295
- }
296
- }
297
- async function writeEntryCommands(projectRoot) {
298
- await writeFileSafe(runtimePath(projectRoot, "commands", "start.md"), startCommandContract());
299
- await writeFileSafe(runtimePath(projectRoot, "commands", "idea.md"), ideaCommandContract());
300
- await writeFileSafe(runtimePath(projectRoot, "commands", "view.md"), viewCommandContract());
301
- await writeFileSafe(runtimePath(projectRoot, "commands", "cancel.md"), cancelCommandContract());
302
- for (const stage of FLOW_STAGES) {
303
- await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandShimMarkdown(stage));
304
- }
78
+ async function writeHookFile(projectRoot, hook) {
79
+ const hookPath = path.join(projectRoot, HOOKS_REL_PATH, hook.fileName);
80
+ await writeFileSafe(hookPath, hook.body);
81
+ await fs.chmod(hookPath, 0o755);
305
82
  }
306
- function toObject(value) {
307
- if (!value || typeof value !== "object" || Array.isArray(value)) {
308
- return null;
83
+ async function writeAgentFiles(projectRoot) {
84
+ for (const agent of CORE_AGENTS) {
85
+ const agentPath = path.join(projectRoot, LIB_ROOT, "agents", `${agent.id}.md`);
86
+ await writeFileSafe(agentPath, renderAgentMarkdown(agent));
309
87
  }
310
- return value;
311
88
  }
312
- /**
313
- * Removes // and /* *\/ comments only outside JSON strings (double-quoted).
314
- * Used for recovering user-edited hook JSON without corrupting string contents.
315
- */
316
- function stripJsonCommentsOutsideStrings(input) {
317
- let out = "";
318
- let i = 0;
319
- let inString = false;
320
- let escape = false;
321
- while (i < input.length) {
322
- const c = input[i];
323
- if (inString) {
324
- out += c;
325
- if (escape) {
326
- escape = false;
327
- }
328
- else if (c === "\\") {
329
- escape = true;
330
- }
331
- else if (c === '"') {
332
- inString = false;
333
- }
334
- i += 1;
335
- continue;
336
- }
337
- if (c === '"') {
338
- inString = true;
339
- out += c;
340
- i += 1;
341
- continue;
342
- }
343
- const next = input[i + 1];
344
- if (c === "/" && next === "/") {
345
- while (i < input.length && input[i] !== "\n" && input[i] !== "\r")
346
- i += 1;
347
- continue;
348
- }
349
- if (c === "/" && next === "*") {
350
- i += 2;
351
- while (i < input.length - 1 && !(input[i] === "*" && input[i + 1] === "/"))
352
- i += 1;
353
- i = Math.min(i + 2, input.length);
354
- continue;
355
- }
356
- out += c;
357
- i += 1;
89
+ async function writeRuntimeSkills(projectRoot) {
90
+ for (const skill of AUTO_TRIGGER_SKILLS) {
91
+ const target = path.join(projectRoot, LIB_ROOT, "skills", skill.fileName);
92
+ await writeFileSafe(target, skill.body);
358
93
  }
359
- return out;
360
94
  }
361
- function normalizeJsonLike(raw) {
362
- return stripJsonCommentsOutsideStrings(raw).replace(/,\s*([}\]])/gu, "$1");
363
- }
364
- function tryParseHookDocument(raw) {
365
- try {
366
- return { parsed: JSON.parse(raw), recovered: false };
367
- }
368
- catch {
369
- // continue with relaxed parse
370
- }
371
- try {
372
- return { parsed: JSON.parse(normalizeJsonLike(raw)), recovered: true };
373
- }
374
- catch {
375
- return null;
95
+ async function writeTemplates(projectRoot) {
96
+ for (const template of ARTIFACT_TEMPLATES) {
97
+ const target = path.join(projectRoot, LIB_ROOT, "templates", template.fileName);
98
+ await writeFileSafe(target, template.body);
376
99
  }
100
+ await writeFileSafe(path.join(projectRoot, LIB_ROOT, "templates", "iron-laws.md"), ironLawsMarkdown());
377
101
  }
378
- function opencodeConfigCandidates(projectRoot) {
379
- return [
380
- path.join(projectRoot, "opencode.json"),
381
- path.join(projectRoot, "opencode.jsonc"),
382
- path.join(projectRoot, ".opencode", "opencode.json"),
383
- path.join(projectRoot, ".opencode", "opencode.jsonc")
384
- ];
102
+ async function writeIdeasSeed(projectRoot) {
103
+ const target = path.join(projectRoot, RUNTIME_ROOT, "ideas.md");
104
+ if (await exists(target))
105
+ return;
106
+ await writeFileSafe(target, templateBody("ideas"));
385
107
  }
386
- function normalizeOpenCodePluginEntry(entry) {
387
- if (typeof entry === "string" && entry.trim().length > 0) {
388
- return entry.trim();
389
- }
390
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
391
- return null;
108
+ async function writeStageRunbooks(projectRoot) {
109
+ const dir = path.join(projectRoot, LIB_ROOT, "runbooks");
110
+ for (const playbook of STAGE_PLAYBOOKS) {
111
+ await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
392
112
  }
393
- const obj = entry;
394
- for (const key of ["path", "src", "plugin"]) {
395
- const value = obj[key];
396
- if (typeof value === "string" && value.trim().length > 0) {
397
- return value.trim();
398
- }
399
- }
400
- return null;
113
+ await writeFileSafe(path.join(dir, "index.md"), STAGE_PLAYBOOKS_INDEX);
401
114
  }
402
- function mergeOpenCodePluginConfig(existingDoc, pluginRelPath) {
403
- const root = toObject(existingDoc) ?? {};
404
- const pluginsRaw = Array.isArray(root.plugin) ? [...root.plugin] : [];
405
- const normalized = new Set(pluginsRaw.map((entry) => normalizeOpenCodePluginEntry(entry)).filter(Boolean));
406
- if (!normalized.has(pluginRelPath)) {
407
- pluginsRaw.push(pluginRelPath);
115
+ async function writeReferencePatterns(projectRoot) {
116
+ const dir = path.join(projectRoot, LIB_ROOT, "patterns");
117
+ for (const pattern of REFERENCE_PATTERNS) {
118
+ await writeFileSafe(path.join(dir, pattern.fileName), pattern.body);
408
119
  }
409
- const permission = toObject(root.permission) ?? {};
410
- const permissionChanged = permission.question !== "allow";
411
- const changed = !normalized.has(pluginRelPath) ||
412
- !Array.isArray(root.plugin) ||
413
- permissionChanged ||
414
- !toObject(root.permission);
415
- return {
416
- merged: {
417
- ...root,
418
- plugin: pluginsRaw,
419
- permission: {
420
- ...permission,
421
- question: "allow"
422
- }
423
- },
424
- changed
425
- };
120
+ await writeFileSafe(path.join(dir, "index.md"), REFERENCE_PATTERNS_INDEX);
426
121
  }
427
- async function resolveOpenCodeConfigPath(projectRoot) {
428
- for (const candidate of opencodeConfigCandidates(projectRoot)) {
429
- if (await exists(candidate)) {
430
- return candidate;
431
- }
122
+ async function writeResearchPlaybooks(projectRoot) {
123
+ const dir = path.join(projectRoot, LIB_ROOT, "research");
124
+ for (const playbook of RESEARCH_PLAYBOOKS) {
125
+ await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
432
126
  }
433
- return path.join(projectRoot, "opencode.json");
127
+ await writeFileSafe(path.join(dir, "index.md"), RESEARCH_PLAYBOOKS_INDEX);
434
128
  }
435
- async function writeMergedOpenCodePluginConfig(projectRoot, pluginRelPath) {
436
- const configPath = await resolveOpenCodeConfigPath(projectRoot);
437
- await ensureDir(path.dirname(configPath));
438
- let existingDoc = {};
439
- if (await exists(configPath)) {
440
- try {
441
- const raw = await fs.readFile(configPath, "utf8");
442
- const parsed = tryParseHookDocument(raw);
443
- existingDoc = parsed?.parsed ?? {};
444
- }
445
- catch {
446
- existingDoc = {};
447
- }
448
- }
449
- const { merged, changed } = mergeOpenCodePluginConfig(existingDoc, pluginRelPath);
450
- if (changed || !(await exists(configPath))) {
451
- await writeFileSafe(configPath, `${JSON.stringify(merged, null, 2)}\n`);
129
+ async function writeRecoveryPlaybooks(projectRoot) {
130
+ const dir = path.join(projectRoot, LIB_ROOT, "recovery");
131
+ for (const playbook of RECOVERY_PLAYBOOKS) {
132
+ await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
452
133
  }
134
+ await writeFileSafe(path.join(dir, "index.md"), RECOVERY_INDEX);
453
135
  }
454
- async function removeManagedOpenCodePluginConfig(projectRoot, pluginRelPath) {
455
- for (const configPath of opencodeConfigCandidates(projectRoot)) {
456
- if (!(await exists(configPath)))
457
- continue;
458
- let parsed = null;
459
- try {
460
- const raw = await fs.readFile(configPath, "utf8");
461
- parsed = tryParseHookDocument(raw)?.parsed ?? null;
462
- }
463
- catch {
464
- parsed = null;
465
- }
466
- const root = toObject(parsed);
467
- if (!root || !Array.isArray(root.plugin))
468
- continue;
469
- const filtered = root.plugin.filter((entry) => normalizeOpenCodePluginEntry(entry) !== pluginRelPath);
470
- if (filtered.length === root.plugin.length) {
471
- continue;
472
- }
473
- root.plugin = filtered;
474
- const remainingKeys = Object.keys(root).filter((k) => k !== "plugin" || filtered.length > 0);
475
- if (remainingKeys.length === 0 || (remainingKeys.length === 1 && remainingKeys[0] === "plugin" && filtered.length === 0)) {
476
- await fs.rm(configPath, { force: true });
477
- }
478
- else {
479
- if (filtered.length === 0) {
480
- delete root.plugin;
481
- }
482
- await writeFileSafe(configPath, `${JSON.stringify(root, null, 2)}\n`);
483
- }
136
+ async function writeExamples(projectRoot) {
137
+ const dir = path.join(projectRoot, LIB_ROOT, "examples");
138
+ for (const example of EXAMPLES) {
139
+ await writeFileSafe(path.join(dir, example.fileName), example.body);
484
140
  }
141
+ await writeFileSafe(path.join(dir, "index.md"), EXAMPLES_INDEX);
485
142
  }
486
- function backupFileNameForHook(projectRoot, hookFilePath) {
487
- const rel = path.relative(projectRoot, hookFilePath).replace(/[\\/]/gu, "__");
488
- const ts = new Date().toISOString().replace(/[:.]/gu, "-");
489
- return `${rel}.${ts}.bak`;
490
- }
491
- function harnessForHookFile(projectRoot, hookFilePath) {
492
- const rel = path.relative(projectRoot, hookFilePath).replace(/\\/gu, "/");
493
- if (rel === ".claude/hooks/hooks.json")
494
- return "claude";
495
- if (rel === ".cursor/hooks.json")
496
- return "cursor";
497
- if (rel === ".codex/hooks.json")
498
- return "codex";
499
- return null;
500
- }
501
- async function pruneOldHookBackups(backupsDir, maxBackups = 20) {
502
- let entries = [];
503
- try {
504
- entries = await fs.readdir(backupsDir);
505
- }
506
- catch {
507
- entries = [];
508
- }
509
- if (entries.length <= maxBackups)
510
- return;
511
- const withStats = await Promise.all(entries.map(async (entry) => {
512
- const fullPath = path.join(backupsDir, entry);
513
- const stat = await fs.stat(fullPath);
514
- return { fullPath, mtimeMs: stat.mtimeMs };
515
- }));
516
- withStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
517
- const stale = withStats.slice(maxBackups);
518
- await Promise.all(stale.map(async (item) => {
519
- await fs.rm(item.fullPath, { force: true });
520
- }));
143
+ async function writeAntipatterns(projectRoot) {
144
+ await writeFileSafe(path.join(projectRoot, LIB_ROOT, "antipatterns.md"), ANTIPATTERNS);
521
145
  }
522
- async function backupHookFile(projectRoot, hookFilePath, rawContent) {
523
- const backupsDir = runtimePath(projectRoot, "backups", "hooks");
524
- await ensureDir(backupsDir);
525
- const fileName = backupFileNameForHook(projectRoot, hookFilePath);
526
- const backupPath = path.join(backupsDir, fileName);
527
- await writeFileSafe(backupPath, rawContent);
528
- await pruneOldHookBackups(backupsDir);
529
- return backupPath;
146
+ async function writeDecisionProtocol(projectRoot) {
147
+ await writeFileSafe(path.join(projectRoot, LIB_ROOT, "decision-protocol.md"), DECISION_PROTOCOL);
530
148
  }
531
- function normalizeHookCommandForDedupe(command) {
532
- return command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
149
+ async function writeMetaSkill(projectRoot) {
150
+ await writeFileSafe(path.join(projectRoot, LIB_ROOT, "skills", "cclaw-meta.md"), META_SKILL);
533
151
  }
534
- function dedupeHookEntryByCommand(entry, seenCommands) {
535
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
536
- return entry;
537
- }
538
- const obj = entry;
539
- let changed = false;
540
- if (typeof obj.command === "string") {
541
- const normalized = normalizeHookCommandForDedupe(obj.command);
542
- if (seenCommands.has(normalized)) {
543
- return undefined;
544
- }
545
- seenCommands.add(normalized);
546
- }
547
- if (Array.isArray(obj.hooks)) {
548
- const hooks = [];
549
- for (const nested of obj.hooks) {
550
- const deduped = dedupeHookEntryByCommand(nested, seenCommands);
551
- if (deduped !== undefined) {
552
- hooks.push(deduped);
553
- }
554
- else {
555
- changed = true;
556
- }
557
- }
558
- if (hooks.length !== obj.hooks.length) {
559
- changed = true;
560
- }
561
- if (hooks.length === 0 && typeof obj.command !== "string") {
562
- return undefined;
563
- }
564
- return changed ? { ...obj, hooks } : entry;
152
+ async function writeHarnessAssets(projectRoot, layout) {
153
+ await ensureDir(path.join(projectRoot, layout.commandsDir));
154
+ await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc.md"), renderStartCommand());
155
+ await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc-cancel.md"), renderCancelCommand());
156
+ await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc-idea.md"), renderIdeaCommand());
157
+ await ensureDir(path.join(projectRoot, layout.agentsDir));
158
+ for (const agent of CORE_AGENTS) {
159
+ await writeFileSafe(path.join(projectRoot, layout.agentsDir, `${agent.id}.md`), renderAgentMarkdown(agent));
565
160
  }
566
- return entry;
567
- }
568
- function dedupeHookEntriesByCommand(entries) {
569
- const seenCommands = new Set();
570
- const deduped = [];
571
- for (const entry of entries) {
572
- const next = dedupeHookEntryByCommand(entry, seenCommands);
573
- if (next !== undefined) {
574
- deduped.push(next);
575
- }
161
+ await ensureDir(path.join(projectRoot, layout.skillsDir));
162
+ for (const skill of AUTO_TRIGGER_SKILLS) {
163
+ await writeFileSafe(path.join(projectRoot, layout.skillsDir, skill.fileName), skill.body);
576
164
  }
577
- return deduped;
578
- }
579
- function mergeHookDocuments(existingDoc, generatedDoc) {
580
- const generatedRoot = toObject(generatedDoc) ?? {};
581
- const generatedHooks = toObject(generatedRoot.hooks) ?? {};
582
- const strippedExisting = stripManagedHookCommands(existingDoc).updated;
583
- const existingRoot = toObject(strippedExisting) ?? {};
584
- const existingHooks = toObject(existingRoot.hooks) ?? {};
585
- const mergedHooks = { ...existingHooks };
586
- for (const [eventName, generatedEntries] of Object.entries(generatedHooks)) {
587
- const existingEntries = existingHooks[eventName];
588
- if (Array.isArray(generatedEntries)) {
589
- const preservedEntries = Array.isArray(existingEntries) ? existingEntries : [];
590
- mergedHooks[eventName] = dedupeHookEntriesByCommand([...generatedEntries, ...preservedEntries]);
591
- continue;
592
- }
593
- // Defensive: malformed generated event payload must not wipe user hooks.
594
- if (Array.isArray(existingEntries)) {
595
- mergedHooks[eventName] = existingEntries;
165
+ if (layout.hooksConfig) {
166
+ const { dir, fileName } = layout.hooksConfig;
167
+ await ensureDir(path.join(projectRoot, dir));
168
+ if (fileName.endsWith(".mjs")) {
169
+ const moduleBody = `// cclaw opencode plugin (minimal). Wires session-start and stop-handoff hooks.\nimport { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nfunction run(filePath) {\n return () => spawn(process.execPath, [path.join(\"${HOOKS_REL_PATH}\", filePath)], { stdio: \"inherit\" });\n}\nexport default {\n events: {\n \"session.start\": run(\"session-start.mjs\"),\n \"session.stop\": run(\"stop-handoff.mjs\")\n }\n};\n`;
170
+ await writeFileSafe(path.join(projectRoot, dir, fileName), moduleBody);
596
171
  }
597
172
  else {
598
- mergedHooks[eventName] = generatedEntries;
599
- }
600
- }
601
- const mergedRoot = {
602
- ...existingRoot,
603
- hooks: mergedHooks
604
- };
605
- for (const [key, value] of Object.entries(generatedRoot)) {
606
- if (key === "hooks")
607
- continue;
608
- if (!(key in mergedRoot)) {
609
- mergedRoot[key] = value;
610
- }
611
- }
612
- return mergedRoot;
613
- }
614
- async function writeMergedHookJson(projectRoot, hookFilePath, generatedJson) {
615
- let existingDoc = {};
616
- if (await exists(hookFilePath)) {
617
- try {
618
- const raw = await fs.readFile(hookFilePath, "utf8");
619
- const parsed = tryParseHookDocument(raw);
620
- if (parsed) {
621
- existingDoc = parsed.parsed;
622
- if (parsed.recovered) {
623
- await backupHookFile(projectRoot, hookFilePath, raw);
173
+ const hooksJson = {
174
+ version: 1,
175
+ generatedBy: `cclaw-cli@${CCLAW_VERSION}`,
176
+ events: {
177
+ "session.start": [
178
+ { command: "node", args: [`./${HOOKS_REL_PATH}/${SESSION_START_HOOK_SPEC.fileName}`] }
179
+ ],
180
+ "session.stop": [
181
+ { command: "node", args: [`./${HOOKS_REL_PATH}/${STOP_HANDOFF_HOOK_SPEC.fileName}`] }
182
+ ]
624
183
  }
625
- }
626
- else {
627
- await backupHookFile(projectRoot, hookFilePath, raw);
628
- existingDoc = {};
629
- }
630
- }
631
- catch {
632
- existingDoc = {};
633
- }
634
- }
635
- const generatedDoc = JSON.parse(generatedJson);
636
- const harness = harnessForHookFile(projectRoot, hookFilePath);
637
- if (harness) {
638
- const generatedSchema = validateHookDocument(harness, generatedDoc);
639
- if (!generatedSchema.ok) {
640
- throw new Error(`[sync fail-fast] Hook document drift detected for ${harness}: generated hook document is invalid (${generatedSchema.errors.join("; ")}). ` +
641
- "Run `npx cclaw-cli sync` to regenerate managed hooks or repair the generated hook shape manually.");
642
- }
643
- }
644
- const mergedDoc = mergeHookDocuments(existingDoc, generatedDoc);
645
- if (harness) {
646
- const mergedSchema = validateHookDocument(harness, mergedDoc);
647
- if (!mergedSchema.ok) {
648
- throw new Error(`[sync fail-fast] Hook document drift detected for ${harness}: merged hook document is invalid (${mergedSchema.errors.join("; ")}). ` +
649
- "Run `npx cclaw-cli sync` after fixing the custom hook entry or remove the malformed user-authored hook block.");
650
- }
651
- }
652
- await writeFileSafe(hookFilePath, `${JSON.stringify(mergedDoc, null, 2)}\n`);
653
- }
654
- async function readBundledRunHookRuntimeScript(options) {
655
- const bundleUrl = new URL("./runtime/run-hook.mjs", import.meta.url);
656
- try {
657
- await fs.stat(bundleUrl);
658
- }
659
- catch {
660
- return null;
661
- }
662
- try {
663
- const moduleUrl = `${bundleUrl.href}?ts=${Date.now()}`;
664
- const loaded = await import(moduleUrl);
665
- const factory = typeof loaded.buildRunHookRuntimeScript === "function"
666
- ? loaded.buildRunHookRuntimeScript
667
- : typeof loaded.default === "function"
668
- ? loaded.default
669
- : null;
670
- if (!factory)
671
- return null;
672
- const script = factory(options);
673
- if (typeof script !== "string")
674
- return null;
675
- return script.trim().length > 0 ? script : null;
676
- }
677
- catch {
678
- return null;
679
- }
680
- }
681
- async function writeHooks(projectRoot, config) {
682
- const harnesses = config.harnesses;
683
- const hooksDir = runtimePath(projectRoot, "hooks");
684
- const stateDir = runtimePath(projectRoot, "state");
685
- await ensureDir(hooksDir);
686
- await ensureDir(stateDir);
687
- await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
688
- await writeFileSafe(path.join(hooksDir, "start-flow.mjs"), startFlowScript());
689
- await writeFileSafe(path.join(hooksDir, "cancel-run.mjs"), cancelRunScript());
690
- const hookRuntimeOptions = {};
691
- const bundledHookRuntime = await readBundledRunHookRuntimeScript(hookRuntimeOptions);
692
- await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), bundledHookRuntime ?? nodeHookRuntimeScript(hookRuntimeOptions));
693
- await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookCmdScript());
694
- await writeFileSafe(path.join(hooksDir, "delegation-record.mjs"), delegationRecordScript());
695
- await writeFileSafe(path.join(hooksDir, "slice-commit.mjs"), sliceCommitScript());
696
- const opencodePluginSource = opencodePluginJs();
697
- await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
698
- try {
699
- for (const script of [
700
- "stage-complete.mjs",
701
- "start-flow.mjs",
702
- "run-hook.mjs",
703
- "run-hook.cmd",
704
- "delegation-record.mjs",
705
- "slice-commit.mjs",
706
- "opencode-plugin.mjs",
707
- "cancel-run.mjs"
708
- ]) {
709
- await fs.chmod(path.join(hooksDir, script), 0o755);
710
- }
711
- }
712
- catch {
713
- // chmod may fail on some filesystems
714
- }
715
- if (harnesses.includes("opencode")) {
716
- const opencodePluginsDir = path.join(projectRoot, ".opencode/plugins");
717
- const opencodePluginPath = path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH);
718
- await ensureDir(opencodePluginsDir);
719
- await writeFileSafe(opencodePluginPath, opencodePluginSource);
720
- await writeMergedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
721
- try {
722
- await fs.chmod(opencodePluginPath, 0o755);
723
- }
724
- catch {
725
- // chmod may fail on some filesystems
726
- }
727
- }
728
- for (const harness of harnesses) {
729
- if (harness === "claude") {
730
- const dir = path.join(projectRoot, ".claude/hooks");
731
- await ensureDir(dir);
732
- await writeMergedHookJson(projectRoot, path.join(dir, "hooks.json"), claudeHooksJson());
733
- }
734
- else if (harness === "cursor") {
735
- const cursorDir = path.join(projectRoot, ".cursor");
736
- await ensureDir(cursorDir);
737
- await writeMergedHookJson(projectRoot, path.join(cursorDir, "hooks.json"), cursorHooksJson());
738
- }
739
- else if (harness === "codex") {
740
- // Codex CLI lifecycle hooks live at `.codex/hooks.json`, gated by
741
- // `[features] codex_hooks = true` in `~/.codex/config.toml`. cclaw
742
- // always writes the file so the moment the flag flips on, cclaw
743
- // hooks start firing. PreToolUse/PostToolUse remain Bash-only on
744
- // Codex; if the feature flag is off, hooks stay inert until the
745
- // user enables `codex_hooks` in `~/.codex/config.toml`.
746
- const codexDir = path.join(projectRoot, ".codex");
747
- await ensureDir(codexDir);
748
- await writeMergedHookJson(projectRoot, path.join(codexDir, "hooks.json"), codexHooksJson());
749
- }
750
- // OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
751
- }
752
- }
753
- async function canonicalHookScripts() {
754
- const hookRuntimeOptions = {};
755
- const bundledHookRuntime = await readBundledRunHookRuntimeScript(hookRuntimeOptions);
756
- return {
757
- "stage-complete.mjs": stageCompleteScript(),
758
- "start-flow.mjs": startFlowScript(),
759
- "cancel-run.mjs": cancelRunScript(),
760
- "run-hook.mjs": bundledHookRuntime ?? nodeHookRuntimeScript(hookRuntimeOptions),
761
- "run-hook.cmd": runHookCmdScript(),
762
- "delegation-record.mjs": delegationRecordScript(),
763
- "slice-commit.mjs": sliceCommitScript(),
764
- "opencode-plugin.mjs": opencodePluginJs()
765
- };
766
- }
767
- async function checkManagedHookDrift(projectRoot) {
768
- const hooksDir = runtimePath(projectRoot, "hooks");
769
- const canonical = await canonicalHookScripts();
770
- const findings = [];
771
- for (const [fileName, expectedSource] of Object.entries(canonical)) {
772
- const targetPath = path.join(hooksDir, fileName);
773
- let actual;
774
- try {
775
- actual = await fs.readFile(targetPath);
776
- }
777
- catch {
778
- findings.push({ file: fileName, reason: "missing" });
779
- continue;
780
- }
781
- const expected = Buffer.from(expectedSource, "utf8");
782
- if (!actual.equals(expected)) {
783
- findings.push({ file: fileName, reason: "content_mismatch" });
784
- }
785
- }
786
- return findings;
787
- }
788
- function formatManagedHookDriftError(findings) {
789
- const details = findings
790
- .map((finding) => `- .cclaw/hooks/${finding.file}: ${finding.reason === "missing" ? "missing" : "content differs from canonical renderer"}`)
791
- .join("\n");
792
- return ("[sync --check] Managed hook drift detected.\n" +
793
- `${details}\n` +
794
- "Re-run `npx cclaw-cli sync` to rewrite managed hooks.");
795
- }
796
- async function ensureKnowledgeStore(projectRoot) {
797
- const storePath = runtimePath(projectRoot, "knowledge.jsonl");
798
- if (!(await exists(storePath))) {
799
- await writeFileSafe(storePath, "", { mode: 0o600 });
800
- }
801
- const legacyMdPath = runtimePath(projectRoot, "knowledge.md");
802
- if (await exists(legacyMdPath)) {
803
- await fs.rm(legacyMdPath, { force: true });
804
- }
805
- }
806
- async function writeRulebook(projectRoot) {
807
- await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
808
- await writeFileSafe(runtimePath(projectRoot, "rules", "rules.json"), `${JSON.stringify(buildRulesJson(), null, 2)}\n`);
809
- }
810
- async function writeCursorWorkflowRule(projectRoot, harnesses) {
811
- const rulePath = path.join(projectRoot, CURSOR_RULE_REL_PATH);
812
- const guidelinesPath = path.join(projectRoot, CURSOR_GUIDELINES_REL_PATH);
813
- if (!harnesses.includes("cursor")) {
814
- for (const target of [rulePath, guidelinesPath]) {
815
- try {
816
- await fs.rm(target, { force: true });
817
- }
818
- catch {
819
- // best-effort cleanup
820
- }
821
- }
822
- return;
823
- }
824
- await ensureDir(path.dirname(rulePath));
825
- await writeFileSafe(rulePath, CURSOR_WORKFLOW_RULE_MDC);
826
- await ensureDir(path.dirname(guidelinesPath));
827
- await writeFileSafe(guidelinesPath, CURSOR_GUIDELINES_RULE_MDC);
828
- }
829
- async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
830
- const enabled = new Set(harnesses);
831
- const managedHookFiles = [
832
- { harness: "claude", hookPath: path.join(projectRoot, ".claude/hooks/hooks.json") },
833
- { harness: "cursor", hookPath: path.join(projectRoot, ".cursor/hooks.json") },
834
- { harness: "codex", hookPath: path.join(projectRoot, ".codex/hooks.json") }
835
- ];
836
- for (const entry of managedHookFiles) {
837
- if (enabled.has(entry.harness))
838
- continue;
839
- await removeManagedHookEntries(entry.hookPath, { failOnParseError: true });
840
- }
841
- if (!enabled.has("opencode")) {
842
- try {
843
- await fs.rm(path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH), { force: true });
844
- }
845
- catch {
846
- // best-effort cleanup
847
- }
848
- try {
849
- await fs.rm(path.join(projectRoot, ".opencode/agents"), { recursive: true, force: true });
850
- }
851
- catch {
852
- // best-effort cleanup
853
- }
854
- await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
855
- }
856
- if (!enabled.has("codex")) {
857
- try {
858
- await fs.rm(path.join(projectRoot, ".codex/agents"), { recursive: true, force: true });
859
- }
860
- catch {
861
- // best-effort cleanup
184
+ };
185
+ await writeFileSafe(path.join(projectRoot, dir, fileName), `${JSON.stringify(hooksJson, null, 2)}\n`);
862
186
  }
863
187
  }
864
188
  }
865
- async function writeState(projectRoot, config, forceReset = false) {
866
- void config;
867
- // Fresh init no longer materializes flow-state.json. The first managed
868
- // `/cc <idea>` start-flow call creates the state file.
869
- if (!forceReset) {
870
- return;
871
- }
872
- const statePath = runtimePath(projectRoot, "state", "flow-state.json");
873
- if (await exists(statePath)) {
874
- return;
875
- }
876
- const state = createInitialFlowState({ track: "standard" });
877
- await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
878
- }
879
- async function cleanLegacyArtifacts(projectRoot) {
880
- for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
881
- await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
882
- }
883
- for (const legacyFolder of DEPRECATED_STAGE_SKILL_FOLDERS) {
884
- await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
885
- }
886
- for (const legacyFolder of DEPRECATED_SKILL_FOLDERS_FULL) {
887
- await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
888
- }
889
- for (const legacyAgentFile of DEPRECATED_AGENT_FILES) {
890
- await removeBestEffort(runtimePath(projectRoot, "agents", legacyAgentFile));
891
- }
892
- for (const legacyPlugin of [
893
- path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
894
- path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
895
- path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
896
- ]) {
897
- await removeBestEffort(legacyPlugin);
898
- }
899
- for (const legacyRuntimeFile of [
900
- ...DEPRECATED_COMMAND_FILES.map((file) => runtimePath(projectRoot, "commands", file)),
901
- ...DEPRECATED_SKILL_FILES.map((segments) => runtimePath(projectRoot, "skills", ...segments)),
902
- ...DEPRECATED_STATE_FILES.map((file) => runtimePath(projectRoot, "state", file)),
903
- ...DEPRECATED_ARTIFACT_FILES.map((file) => runtimePath(projectRoot, "artifacts", file)),
904
- ...DEPRECATED_RUNTIME_ROOT_FILES.map((file) => runtimePath(projectRoot, file)),
905
- ...DEPRECATED_HOOK_FILES.map((file) => runtimePath(projectRoot, "hooks", file))
906
- ]) {
907
- await removeBestEffort(legacyRuntimeFile);
908
- }
909
- // Runtime simplification cleanup: these folders were generated in older
910
- // releases and are now intentionally removed from user projects.
911
- for (const legacyRuntimeDir of DEPRECATED_RUNTIME_DIRS) {
912
- await removeBestEffort(runtimePath(projectRoot, legacyRuntimeDir), true);
913
- }
914
- // Archive storage migration: `.cclaw/runs` is legacy and no longer a valid
915
- // archive root. Remove only when empty; otherwise keep it so users can
916
- // manually migrate or inspect old data.
917
- const legacyRunsDir = runtimePath(projectRoot, "runs");
918
- try {
919
- const entries = await fs.readdir(legacyRunsDir);
920
- if (entries.length === 0) {
921
- await fs.rm(legacyRunsDir, { recursive: true, force: true });
922
- }
923
- }
924
- catch {
925
- // missing or unreadable legacy dir; keep best-effort behavior
926
- }
927
- // D-4 terminology migration: rename historical ideation artifact prefixes to
928
- // the canonical idea-* naming without deleting user-authored content.
929
- const legacyIdeaArtifactPattern = /^ideation-(.+\.md)$/u;
930
- const artifactsDir = runtimePath(projectRoot, "artifacts");
931
- try {
932
- const entries = await fs.readdir(artifactsDir);
933
- for (const entry of entries) {
934
- const match = legacyIdeaArtifactPattern.exec(entry);
935
- if (!match)
936
- continue;
937
- const nextName = `idea-${match[1]}`;
938
- const from = path.join(artifactsDir, entry);
939
- const to = path.join(artifactsDir, nextName);
940
- if (await exists(to)) {
941
- continue;
942
- }
943
- await fs.rename(from, to);
944
- }
945
- }
946
- catch {
947
- // no artifacts directory yet (fresh init) or read-only FS
948
- }
189
+ async function writeConfig(projectRoot, config) {
190
+ const configPath = path.join(projectRoot, RUNTIME_ROOT, "config.yaml");
191
+ await writeFileSafe(configPath, renderConfig(config));
192
+ return configPath;
949
193
  }
950
- async function cleanStaleFiles(projectRoot) {
951
- const expectedShimFiles = new Set(harnessShimFileNames());
952
- const expectedShimSkills = new Set(harnessShimFileNames().map((fileName) => fileName.replace(/\.md$/u, "")));
953
- for (const adapter of Object.values(HARNESS_ADAPTERS)) {
954
- const commandDir = path.join(projectRoot, adapter.commandDir);
955
- if (!(await exists(commandDir)))
956
- continue;
957
- let entries = [];
958
- try {
959
- entries = await fs.readdir(commandDir);
960
- }
961
- catch {
962
- entries = [];
963
- }
964
- if (adapter.shimKind === "skill") {
965
- for (const entry of entries) {
966
- if (!/^cc(?:-.*)?$/u.test(entry))
967
- continue;
968
- if (expectedShimSkills.has(entry))
969
- continue;
970
- await fs.rm(path.join(commandDir, entry), { recursive: true, force: true });
971
- }
972
- continue;
973
- }
974
- for (const entry of entries) {
975
- if (!/^cc(?:-.*)?\.md$/u.test(entry))
976
- continue;
977
- if (expectedShimFiles.has(entry))
978
- continue;
979
- await fs.rm(path.join(commandDir, entry), { force: true });
980
- }
194
+ async function resolveHarnesses(projectRoot, fromOptions, fromConfig) {
195
+ if (fromOptions && fromOptions.length > 0)
196
+ return fromOptions;
197
+ if (fromConfig && fromConfig.length > 0)
198
+ return fromConfig;
199
+ const detected = await detectHarnesses(projectRoot);
200
+ if (detected.length === 0) {
201
+ throw new Error(NO_HARNESS_DETECTED_MESSAGE);
981
202
  }
982
- // Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
983
- // Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
203
+ return detected;
984
204
  }
985
- async function assertExpectedHarnessShims(projectRoot, harnesses) {
986
- const expectedFiles = harnessShimFileNames();
987
- const expectedSkillFolders = harnessShimSkillNames();
205
+ export async function syncCclaw(options) {
206
+ const projectRoot = options.cwd;
207
+ await ensureRuntimeRoot(projectRoot);
208
+ const existing = await readConfig(projectRoot);
209
+ const harnesses = await resolveHarnesses(projectRoot, options.harnesses, existing?.harnesses);
988
210
  for (const harness of harnesses) {
989
- const adapter = HARNESS_ADAPTERS[harness];
990
- const base = path.join(projectRoot, adapter.commandDir);
991
- for (const fileName of expectedFiles) {
992
- const target = adapter.shimKind === "skill"
993
- ? path.join(base, fileName.replace(/\.md$/u, ""), "SKILL.md")
994
- : path.join(base, fileName);
995
- if (!(await exists(target))) {
996
- throw new Error(`[sync fail-fast] Harness shim drift detected for ${harness}: missing ${target}. ` +
997
- `Run \`npx cclaw-cli sync\` again; if the file is still missing, inspect harness permissions/paths.`);
998
- }
999
- }
1000
- if (adapter.shimKind === "skill") {
1001
- for (const folder of expectedSkillFolders) {
1002
- const skillPath = path.join(base, folder, "SKILL.md");
1003
- if (!(await exists(skillPath))) {
1004
- throw new Error(`[sync fail-fast] Harness skill shim drift detected for ${harness}: missing ${skillPath}. ` +
1005
- `Run \`npx cclaw-cli sync\` again; if the issue persists, inspect generated .agents/skills surfaces.`);
1006
- }
1007
- }
1008
- }
1009
- }
1010
- }
1011
- async function maybeLogParallelWaveDispatchHint(projectRoot) {
1012
- const flowPath = runtimePath(projectRoot, "state", "flow-state.json");
1013
- if (!(await exists(flowPath)))
1014
- return;
1015
- try {
1016
- const planPath = runtimePath(projectRoot, "artifacts", "05-plan.md");
1017
- if (!(await exists(planPath)))
1018
- return;
1019
- const planRaw = await fs.readFile(planPath, "utf8");
1020
- const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planRaw), await parseWavePlanDirectory(runtimePath(projectRoot, "artifacts")));
1021
- const hint = formatNextParallelWaveSyncHint(merged);
1022
- if (hint) {
1023
- process.stdout.write(`cclaw: ${hint}\n`);
1024
- }
1025
- }
1026
- catch {
1027
- // best-effort note only
1028
- }
1029
- }
1030
- async function materializeRuntime(projectRoot, config, forceStateReset, operation = "sync") {
1031
- await warnStaleInitSentinel(projectRoot, operation);
1032
- const sentinelPath = await writeInitSentinel(projectRoot, operation);
1033
- const managedSession = await ManagedResourceSession.create({ projectRoot, operation });
1034
- setActiveManagedResourceSession(managedSession);
1035
- try {
1036
- const harnesses = config.harnesses;
1037
- await ensureStructure(projectRoot);
1038
- await cleanLegacyArtifacts(projectRoot);
1039
- await cleanStaleFiles(projectRoot);
1040
- await Promise.all([
1041
- writeEntryCommands(projectRoot),
1042
- writeSkills(projectRoot, config),
1043
- writeArtifactTemplates(projectRoot),
1044
- writeWavePlansScaffold(projectRoot),
1045
- writeRulebook(projectRoot)
1046
- ]);
1047
- await writeState(projectRoot, config, forceStateReset);
1048
- try {
1049
- await ensureRunSystem(projectRoot, { createIfMissing: false });
1050
- }
1051
- catch (error) {
1052
- if (error instanceof CorruptFlowStateError) {
1053
- throw new Error(`[sync fail-fast] Corrupt flow state detected: ${error.message} ` +
1054
- `Resolve the quarantined flow-state file and re-run \`npx cclaw-cli sync\`.`);
1055
- }
1056
- throw error;
1057
- }
1058
- await ensureKnowledgeStore(projectRoot);
1059
- await writeHooks(projectRoot, config);
1060
- await syncDisabledHarnessArtifacts(projectRoot, harnesses);
1061
- await cleanupLegacyManagedGitHookRelays(projectRoot);
1062
- await syncHarnessShims(projectRoot, harnesses);
1063
- await assertExpectedHarnessShims(projectRoot, harnesses);
1064
- await writeCursorWorkflowRule(projectRoot, harnesses);
1065
- await ensureGitignore(projectRoot);
1066
- if (operation === "sync" || operation === "upgrade") {
1067
- await maybeLogParallelWaveDispatchHint(projectRoot);
1068
- }
1069
- await managedSession.commit();
1070
- await fs.unlink(sentinelPath).catch(() => undefined);
1071
- }
1072
- catch (error) {
1073
- // Leave the sentinel in place so the interrupted run is visible.
1074
- throw error;
1075
- }
1076
- finally {
1077
- setActiveManagedResourceSession(null);
1078
- }
1079
- }
1080
- async function warnCodexHooksFeatureFlagIfDisabled(harnesses) {
1081
- if (!harnesses.includes("codex"))
1082
- return;
1083
- const codexTomlPath = codexConfigPath();
1084
- let existing;
1085
- try {
1086
- existing = await readCodexConfig(codexTomlPath);
1087
- }
1088
- catch (error) {
1089
- process.stderr.write(`cclaw: could not read ${codexTomlPath} to validate codex_hooks flag: ${error instanceof Error ? error.message : String(error)}\n`);
1090
- return;
211
+ if (!HARNESS_IDS.includes(harness)) {
212
+ throw new Error(`Unknown harness: ${harness}. Supported: ${HARNESS_IDS.join(", ")}`);
213
+ }
214
+ }
215
+ const config = existing
216
+ ? { ...existing, version: CCLAW_VERSION, flowVersion: "8", harnesses }
217
+ : createDefaultConfig(harnesses);
218
+ await ensureRunSystem(projectRoot);
219
+ await writeAgentFiles(projectRoot);
220
+ for (const hook of NODE_HOOKS) {
221
+ await writeHookFile(projectRoot, hook);
222
+ }
223
+ await writeRuntimeSkills(projectRoot);
224
+ await writeMetaSkill(projectRoot);
225
+ await writeTemplates(projectRoot);
226
+ await writeStageRunbooks(projectRoot);
227
+ await writeReferencePatterns(projectRoot);
228
+ await writeResearchPlaybooks(projectRoot);
229
+ await writeRecoveryPlaybooks(projectRoot);
230
+ await writeExamples(projectRoot);
231
+ await writeAntipatterns(projectRoot);
232
+ await writeDecisionProtocol(projectRoot);
233
+ await writeIdeasSeed(projectRoot);
234
+ for (const harness of harnesses) {
235
+ await writeHarnessAssets(projectRoot, HARNESS_LAYOUTS[harness]);
1091
236
  }
1092
- if (classifyCodexHooksFlag(existing) === "enabled")
1093
- return;
1094
- process.stderr.write(`cclaw: Codex hooks file written, but [features] codex_hooks is not true in ${codexTomlPath} — hooks are inert until you enable it.\n`);
237
+ await ensureGitignorePatterns(projectRoot);
238
+ const configPath = await writeConfig(projectRoot, config);
239
+ return { installedHarnesses: harnesses, configPath };
1095
240
  }
1096
241
  export async function initCclaw(options) {
1097
- if (options.harnesses !== undefined && options.harnesses.length === 0) {
1098
- throw new Error("Select at least one harness.");
1099
- }
1100
- const config = createDefaultConfig(options.harnesses, options.track);
1101
- await writeConfig(options.projectRoot, config, { mode: "minimal" });
1102
- // Init should scaffold runtime surfaces but leave flow-state creation to the
1103
- // first managed start-flow invocation.
1104
- await materializeRuntime(options.projectRoot, config, false, "init");
1105
- }
1106
- export async function syncCclaw(projectRoot, options = {}) {
1107
- if (options.harnesses !== undefined && options.harnesses.length === 0) {
1108
- throw new Error("Select at least one harness.");
1109
- }
1110
- if (options.check === true) {
1111
- const drift = await checkManagedHookDrift(projectRoot);
1112
- if (drift.length > 0) {
1113
- throw new Error(formatManagedHookDriftError(drift));
1114
- }
1115
- return;
1116
- }
1117
- const configExists = await exists(configPath(projectRoot));
1118
- let config = await readConfig(projectRoot);
1119
- if (!configExists) {
1120
- // Prefer detected harness markers over the hardcoded default list.
1121
- // Without this, a user running `cclaw sync` in a `.claude`-only
1122
- // project ends up with a config that also enables cursor/opencode/
1123
- // codex, which then creates invalid harness expectations.
1124
- // Fall back to the previous default (config.harnesses) if no markers
1125
- // are found so brand-new projects still bootstrap cleanly.
1126
- const detected = await detectHarnesses(projectRoot);
1127
- const harnesses = options.harnesses ?? (detected.length > 0 ? detected : config.harnesses);
1128
- const defaultConfig = createDefaultConfig(harnesses);
1129
- await writeConfig(projectRoot, defaultConfig);
1130
- config = defaultConfig;
1131
- }
1132
- else if (options.harnesses !== undefined) {
1133
- config = {
1134
- ...config,
1135
- harnesses: options.harnesses
1136
- };
1137
- await writeConfig(projectRoot, config, {
1138
- mode: "minimal",
1139
- advancedKeysPresent: await detectAdvancedKeys(projectRoot)
1140
- });
1141
- }
1142
- await materializeRuntime(projectRoot, config, false, "sync");
1143
- await warnCodexHooksFeatureFlagIfDisabled(config.harnesses);
1144
- }
1145
- /**
1146
- * Refresh generated files in `.cclaw/` without touching user-authored
1147
- * artifacts or state. Config remains harness-only with managed version
1148
- * stamps.
1149
- */
1150
- export async function upgradeCclaw(projectRoot) {
1151
- const configExists = await exists(configPath(projectRoot));
1152
- const advancedKeysPresent = await detectAdvancedKeys(projectRoot);
1153
- const detectedHarnesses = configExists ? [] : await detectHarnesses(projectRoot);
1154
- const existing = configExists
1155
- ? await readConfig(projectRoot)
1156
- : createDefaultConfig(detectedHarnesses.length > 0 ? detectedHarnesses : undefined);
1157
- const upgraded = {
1158
- ...existing,
1159
- version: CCLAW_VERSION,
1160
- flowVersion: FLOW_VERSION
1161
- };
1162
- await writeConfig(projectRoot, upgraded, {
1163
- mode: "minimal",
1164
- advancedKeysPresent
1165
- });
1166
- await materializeRuntime(projectRoot, upgraded, false, "upgrade");
242
+ return syncCclaw(options);
1167
243
  }
1168
- function stripManagedHookCommands(value) {
1169
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1170
- return { updated: value, changed: false };
1171
- }
1172
- const root = { ...value };
1173
- const hooks = root.hooks;
1174
- if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
1175
- return { updated: root, changed: false };
1176
- }
1177
- let changed = false;
1178
- const cleanedHooks = {};
1179
- for (const [eventName, entries] of Object.entries(hooks)) {
1180
- if (!Array.isArray(entries)) {
1181
- cleanedHooks[eventName] = entries;
1182
- continue;
244
+ export async function uninstallCclaw(options) {
245
+ const projectRoot = options.cwd;
246
+ const config = await readConfig(projectRoot);
247
+ const harnesses = config?.harnesses ?? HARNESS_IDS;
248
+ await removePath(path.join(projectRoot, RUNTIME_ROOT));
249
+ for (const harness of harnesses) {
250
+ const layout = HARNESS_LAYOUTS[harness];
251
+ for (const filename of ["cc.md", "cc-cancel.md", "cc-idea.md"]) {
252
+ await removePath(path.join(projectRoot, layout.commandsDir, filename));
1183
253
  }
1184
- const cleanedEntries = entries.flatMap((entry) => {
1185
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1186
- return [entry];
1187
- }
1188
- const obj = entry;
1189
- if (typeof obj.command === "string" && isManagedRuntimeHookCommand(obj.command)) {
1190
- changed = true;
1191
- return [];
1192
- }
1193
- if (Array.isArray(obj.hooks)) {
1194
- const nested = obj.hooks.filter((nestedHook) => {
1195
- if (!nestedHook || typeof nestedHook !== "object" || Array.isArray(nestedHook))
1196
- return true;
1197
- const nestedObj = nestedHook;
1198
- return !(typeof nestedObj.command === "string" && isManagedRuntimeHookCommand(nestedObj.command));
1199
- });
1200
- if (nested.length !== obj.hooks.length) {
1201
- changed = true;
1202
- }
1203
- if (nested.length === 0) {
1204
- changed = true;
1205
- return [];
1206
- }
1207
- return [{ ...obj, hooks: nested }];
1208
- }
1209
- return [entry];
1210
- });
1211
- if (cleanedEntries.length > 0) {
1212
- cleanedHooks[eventName] = cleanedEntries;
254
+ for (const agent of CORE_AGENTS) {
255
+ await removePath(path.join(projectRoot, layout.agentsDir, `${agent.id}.md`));
1213
256
  }
1214
- else if (entries.length > 0) {
1215
- changed = true;
257
+ await removePath(path.join(projectRoot, layout.skillsDir));
258
+ if (layout.hooksConfig) {
259
+ await removePath(path.join(projectRoot, layout.hooksConfig.dir, layout.hooksConfig.fileName));
1216
260
  }
1217
- }
1218
- if (!changed) {
1219
- return { updated: root, changed: false };
1220
- }
1221
- root.hooks = cleanedHooks;
1222
- return { updated: root, changed: true };
1223
- }
1224
- function isManagedRuntimeHookCommand(command) {
1225
- // Normalize whitespace and collapse any Windows-style backslash path
1226
- // separators to forward slashes so user-edited hook configs on Windows
1227
- // (e.g. `node .cclaw\hooks\run-hook.mjs ...`) still round-trip through
1228
- // sync without being duplicated alongside freshly generated entries.
1229
- const normalized = command.trim().replace(/\s+/gu, " ").replace(/\\/gu, "/");
1230
- if (/(^|\s)(?:node\s+)?(?:"|')?(?:\.\/)?\.cclaw\/hooks\/run-hook\.(?:mjs|cmd)(?:"|')?\s+(?:session-start|stop-handoff|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|pre-tool-pipeline|prompt-pipeline|context-monitor|verify-current-state)(?:\s|$)/u.test(normalized)) {
1231
- return true;
1232
- }
1233
- // Codex UserPromptSubmit non-blocking state nudge.
1234
- return /internal verify-current-state(?:\s|$)/u.test(normalized);
1235
- }
1236
- async function removeManagedHookEntries(hookFilePath, options = {}) {
1237
- if (!(await exists(hookFilePath)))
1238
- return;
1239
- let parsed = null;
1240
- try {
1241
- const raw = await fs.readFile(hookFilePath, "utf8");
1242
- const recovered = tryParseHookDocument(raw);
1243
- if (recovered === null) {
1244
- if (options.failOnParseError === true) {
1245
- throw new Error(`[sync fail-fast] Cannot strip managed hook entries from ${hookFilePath} — JSON is unparseable. ` +
1246
- `Run \`rm ${hookFilePath}\` and rerun \`npx cclaw-cli sync\`.`);
1247
- }
1248
- return;
261
+ if (await exists(path.join(projectRoot, layout.commandsDir))) {
262
+ const remaining = await fs.readdir(path.join(projectRoot, layout.commandsDir));
263
+ if (remaining.length === 0)
264
+ await removePath(path.join(projectRoot, layout.commandsDir));
1249
265
  }
1250
- parsed = recovered.parsed;
1251
- }
1252
- catch (error) {
1253
- if (options.failOnParseError === true) {
1254
- throw new Error(`[sync fail-fast] Cannot strip managed hook entries from ${hookFilePath} — ${error instanceof Error ? error.message : String(error)}. Run \`rm ${hookFilePath}\` and rerun \`npx cclaw-cli sync\`.`);
1255
- }
1256
- return;
1257
- }
1258
- const { updated, changed } = stripManagedHookCommands(parsed);
1259
- if (!changed)
1260
- return;
1261
- const root = updated;
1262
- const hooks = root.hooks;
1263
- const hasHooks = typeof hooks === "object" &&
1264
- hooks !== null &&
1265
- !Array.isArray(hooks) &&
1266
- Object.keys(hooks).length > 0;
1267
- if (!hasHooks) {
1268
- const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version" || key === "cclawHookSchemaVersion");
1269
- if (onlyHooksShell) {
1270
- await fs.rm(hookFilePath, { force: true });
1271
- return;
266
+ if (await exists(path.join(projectRoot, layout.agentsDir))) {
267
+ const remaining = await fs.readdir(path.join(projectRoot, layout.agentsDir));
268
+ if (remaining.length === 0)
269
+ await removePath(path.join(projectRoot, layout.agentsDir));
1272
270
  }
1273
- root.hooks = {};
1274
271
  }
1275
- await writeFileSafe(hookFilePath, `${JSON.stringify(root, null, 2)}\n`);
272
+ await removeGitignorePatterns(projectRoot);
1276
273
  }
1277
- async function removeIfEmpty(dirPath) {
1278
- try {
1279
- const entries = await fs.readdir(dirPath);
1280
- if (entries.length === 0) {
1281
- await fs.rmdir(dirPath);
1282
- }
1283
- }
1284
- catch {
1285
- // directory not present or not removable
1286
- }
274
+ export async function upgradeCclaw(options) {
275
+ return syncCclaw(options);
1287
276
  }
1288
- export async function uninstallCclaw(projectRoot) {
1289
- const fullRuntimePath = path.join(projectRoot, RUNTIME_ROOT);
1290
- try {
1291
- await fs.rm(fullRuntimePath, { recursive: true, force: true });
1292
- }
1293
- catch {
1294
- // path not present
1295
- }
1296
- await removeCclawFromAgentsMd(projectRoot);
1297
- await removeGitignorePatterns(projectRoot);
1298
- await cleanupLegacyManagedGitHookRelays(projectRoot);
1299
- const hookFiles = [
1300
- ".claude/hooks/hooks.json",
1301
- ".cursor/hooks.json",
1302
- ".codex/hooks.json"
1303
- ];
1304
- for (const hf of hookFiles) {
1305
- await removeManagedHookEntries(path.join(projectRoot, hf));
1306
- }
1307
- const commandDirs = [
1308
- ".claude/commands",
1309
- ".cursor/commands",
1310
- ".opencode/commands",
1311
- ".codex/commands"
1312
- ];
1313
- for (const relDir of commandDirs) {
1314
- const fullDir = path.join(projectRoot, relDir);
1315
- try {
1316
- const entries = await fs.readdir(fullDir);
1317
- for (const entry of entries) {
1318
- if (/^(?:viby|cc)(?:-.*)?\.md$/u.test(entry)) {
1319
- await fs.rm(path.join(fullDir, entry), { force: true });
1320
- }
1321
- }
1322
- }
1323
- catch {
1324
- // directory not present
1325
- }
1326
- }
1327
- // Codex shims live at `.agents/skills/cc*/SKILL.md`, matching Codex's
1328
- // `/use cc` prompt verbatim. Older layouts (`.codex/commands/cc*.md` and
1329
- // `.agents/skills/cclaw-cc*/SKILL.md`) are purged on uninstall so orphans
1330
- // can't linger. We only touch cclaw-owned folder names — other tools
1331
- // share `.agents/skills/` with us.
1332
- const codexSkillsRoot = path.join(projectRoot, ".agents/skills");
1333
- try {
1334
- const entries = await fs.readdir(codexSkillsRoot);
1335
- for (const entry of entries) {
1336
- if (/^(?:cclaw-)?cc(?:-(?:next|view|finish|cancel|ops|idea|brainstorm|scope|design|spec|plan|tdd|review|ship))?$/u.test(entry)) {
1337
- await fs.rm(path.join(codexSkillsRoot, entry), { recursive: true, force: true });
1338
- }
1339
- }
1340
- }
1341
- catch {
1342
- // directory not present
1343
- }
1344
- await removeIfEmpty(codexSkillsRoot);
1345
- await removeIfEmpty(path.join(projectRoot, ".agents"));
1346
- const managedAgentNames = CCLAW_AGENTS.map((agent) => agent.name);
1347
- for (const agentName of managedAgentNames) {
1348
- await removeBestEffort(path.join(projectRoot, ".opencode/agents", `${agentName}.md`));
1349
- await removeBestEffort(path.join(projectRoot, ".codex/agents", `${agentName}.toml`));
1350
- }
1351
- for (const pluginPath of [
1352
- path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
1353
- path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
1354
- path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
1355
- ]) {
1356
- try {
1357
- await fs.rm(pluginPath, { force: true });
1358
- }
1359
- catch {
1360
- // best-effort cleanup
1361
- }
1362
- }
1363
- await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
1364
- for (const target of [
1365
- path.join(projectRoot, CURSOR_RULE_REL_PATH),
1366
- path.join(projectRoot, CURSOR_GUIDELINES_REL_PATH)
1367
- ]) {
1368
- try {
1369
- await fs.rm(target, { force: true });
1370
- }
1371
- catch {
1372
- // best-effort cleanup
1373
- }
1374
- }
1375
- const managedDirs = [
1376
- ".claude/hooks",
1377
- ".claude/commands",
1378
- ".claude",
1379
- ".cursor/rules",
1380
- ".cursor/commands",
1381
- ".cursor",
1382
- ".codex/agents",
1383
- ".codex/commands",
1384
- ".codex",
1385
- ".opencode/agents",
1386
- ".opencode/plugins",
1387
- ".opencode/commands",
1388
- ".opencode"
1389
- ];
1390
- for (const relDir of managedDirs) {
1391
- await removeIfEmpty(path.join(projectRoot, relDir));
1392
- }
277
+ export function planSeedForSlug(slug) {
278
+ return planTemplateForSlug(slug);
1393
279
  }
280
+ export const HARNESS_LAYOUT_TABLE = HARNESS_LAYOUTS;
281
+ export const COMMIT_HELPER_HOOK = COMMIT_HELPER_HOOK_SPEC;