cclaw-cli 7.7.1 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -134
- package/dist/artifact-frontmatter.d.ts +51 -0
- package/dist/artifact-frontmatter.js +131 -0
- package/dist/artifact-paths.d.ts +7 -27
- package/dist/artifact-paths.js +20 -249
- package/dist/cancel.d.ts +16 -0
- package/dist/cancel.js +66 -0
- package/dist/cli.d.ts +2 -27
- package/dist/cli.js +107 -511
- package/dist/compound.d.ts +26 -0
- package/dist/compound.js +96 -0
- package/dist/config.d.ts +14 -51
- package/dist/config.js +23 -359
- package/dist/constants.d.ts +11 -18
- package/dist/constants.js +19 -106
- package/dist/content/antipatterns.d.ts +1 -0
- package/dist/content/antipatterns.js +109 -0
- package/dist/content/artifact-templates.d.ts +10 -0
- package/dist/content/artifact-templates.js +550 -0
- package/dist/content/cancel-command.d.ts +2 -2
- package/dist/content/cancel-command.js +25 -17
- package/dist/content/core-agents.d.ts +9 -233
- package/dist/content/core-agents.js +39 -768
- package/dist/content/decision-protocol.d.ts +1 -12
- package/dist/content/decision-protocol.js +27 -20
- package/dist/content/examples.d.ts +8 -42
- package/dist/content/examples.js +293 -425
- package/dist/content/idea-command.d.ts +2 -0
- package/dist/content/idea-command.js +38 -0
- package/dist/content/iron-laws.d.ts +4 -138
- package/dist/content/iron-laws.js +18 -197
- package/dist/content/meta-skill.d.ts +1 -3
- package/dist/content/meta-skill.js +57 -134
- package/dist/content/node-hooks.d.ts +12 -8
- package/dist/content/node-hooks.js +188 -838
- package/dist/content/recovery.d.ts +8 -0
- package/dist/content/recovery.js +179 -0
- package/dist/content/reference-patterns.d.ts +4 -13
- package/dist/content/reference-patterns.js +260 -389
- package/dist/content/research-playbooks.d.ts +8 -8
- package/dist/content/research-playbooks.js +108 -121
- package/dist/content/review-loop.d.ts +6 -192
- package/dist/content/review-loop.js +29 -731
- package/dist/content/skills.d.ts +8 -38
- package/dist/content/skills.js +681 -732
- package/dist/content/specialist-prompts/architect.d.ts +1 -0
- package/dist/content/specialist-prompts/architect.js +225 -0
- package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
- package/dist/content/specialist-prompts/brainstormer.js +168 -0
- package/dist/content/specialist-prompts/index.d.ts +2 -0
- package/dist/content/specialist-prompts/index.js +14 -0
- package/dist/content/specialist-prompts/planner.d.ts +1 -0
- package/dist/content/specialist-prompts/planner.js +182 -0
- package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/reviewer.js +193 -0
- package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/security-reviewer.js +133 -0
- package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
- package/dist/content/specialist-prompts/slice-builder.js +232 -0
- package/dist/content/stage-playbooks.d.ts +8 -0
- package/dist/content/stage-playbooks.js +404 -0
- package/dist/content/start-command.d.ts +2 -12
- package/dist/content/start-command.js +221 -207
- package/dist/flow-state.d.ts +21 -178
- package/dist/flow-state.js +67 -170
- package/dist/fs-utils.d.ts +6 -26
- package/dist/fs-utils.js +29 -162
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.js +51 -34
- package/dist/harness-detect.d.ts +10 -0
- package/dist/harness-detect.js +29 -0
- package/dist/harness-prompt.d.ts +26 -0
- package/dist/harness-prompt.js +142 -0
- package/dist/install.d.ts +35 -15
- package/dist/install.js +238 -1347
- package/dist/knowledge-store.d.ts +19 -163
- package/dist/knowledge-store.js +56 -590
- package/dist/logger.d.ts +8 -3
- package/dist/logger.js +13 -4
- package/dist/orchestrator-routing.d.ts +29 -0
- package/dist/orchestrator-routing.js +156 -0
- package/dist/run-persistence.d.ts +7 -118
- package/dist/run-persistence.js +29 -845
- package/dist/runtime/run-hook.entry.d.ts +1 -3
- package/dist/runtime/run-hook.entry.js +19 -4
- package/dist/runtime/run-hook.mjs +13 -1024
- package/dist/types.d.ts +25 -261
- package/dist/types.js +8 -36
- package/package.json +6 -3
- package/dist/artifact-linter/brainstorm.d.ts +0 -2
- package/dist/artifact-linter/brainstorm.js +0 -353
- package/dist/artifact-linter/design.d.ts +0 -18
- package/dist/artifact-linter/design.js +0 -444
- package/dist/artifact-linter/findings-dedup.d.ts +0 -56
- package/dist/artifact-linter/findings-dedup.js +0 -232
- package/dist/artifact-linter/plan.d.ts +0 -2
- package/dist/artifact-linter/plan.js +0 -826
- package/dist/artifact-linter/review-army.d.ts +0 -49
- package/dist/artifact-linter/review-army.js +0 -520
- package/dist/artifact-linter/review.d.ts +0 -2
- package/dist/artifact-linter/review.js +0 -113
- package/dist/artifact-linter/scope.d.ts +0 -2
- package/dist/artifact-linter/scope.js +0 -158
- package/dist/artifact-linter/shared.d.ts +0 -637
- package/dist/artifact-linter/shared.js +0 -2163
- package/dist/artifact-linter/ship.d.ts +0 -2
- package/dist/artifact-linter/ship.js +0 -250
- package/dist/artifact-linter/spec.d.ts +0 -2
- package/dist/artifact-linter/spec.js +0 -176
- package/dist/artifact-linter/tdd.d.ts +0 -118
- package/dist/artifact-linter/tdd.js +0 -1404
- package/dist/artifact-linter.d.ts +0 -15
- package/dist/artifact-linter.js +0 -517
- package/dist/codex-feature-flag.d.ts +0 -58
- package/dist/codex-feature-flag.js +0 -193
- package/dist/content/closeout-guidance.d.ts +0 -14
- package/dist/content/closeout-guidance.js +0 -44
- package/dist/content/diff-command.d.ts +0 -1
- package/dist/content/diff-command.js +0 -43
- package/dist/content/harness-doc.d.ts +0 -1
- package/dist/content/harness-doc.js +0 -65
- package/dist/content/hook-events.d.ts +0 -9
- package/dist/content/hook-events.js +0 -23
- package/dist/content/hook-manifest.d.ts +0 -81
- package/dist/content/hook-manifest.js +0 -156
- package/dist/content/hooks.d.ts +0 -11
- package/dist/content/hooks.js +0 -1972
- package/dist/content/idea.d.ts +0 -60
- package/dist/content/idea.js +0 -416
- package/dist/content/language-policy.d.ts +0 -2
- package/dist/content/language-policy.js +0 -13
- package/dist/content/learnings.d.ts +0 -6
- package/dist/content/learnings.js +0 -141
- package/dist/content/observe.d.ts +0 -19
- package/dist/content/observe.js +0 -86
- package/dist/content/opencode-plugin.d.ts +0 -1
- package/dist/content/opencode-plugin.js +0 -635
- package/dist/content/review-prompts.d.ts +0 -1
- package/dist/content/review-prompts.js +0 -104
- package/dist/content/runtime-shared-snippets.d.ts +0 -8
- package/dist/content/runtime-shared-snippets.js +0 -80
- package/dist/content/session-hooks.d.ts +0 -7
- package/dist/content/session-hooks.js +0 -107
- package/dist/content/skills-elicitation.d.ts +0 -1
- package/dist/content/skills-elicitation.js +0 -167
- package/dist/content/stage-command.d.ts +0 -2
- package/dist/content/stage-command.js +0 -17
- package/dist/content/stage-schema.d.ts +0 -117
- package/dist/content/stage-schema.js +0 -955
- package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
- package/dist/content/stages/_lint-metadata/index.js +0 -97
- package/dist/content/stages/brainstorm.d.ts +0 -2
- package/dist/content/stages/brainstorm.js +0 -184
- package/dist/content/stages/design.d.ts +0 -2
- package/dist/content/stages/design.js +0 -288
- package/dist/content/stages/index.d.ts +0 -8
- package/dist/content/stages/index.js +0 -11
- package/dist/content/stages/plan.d.ts +0 -2
- package/dist/content/stages/plan.js +0 -191
- package/dist/content/stages/review.d.ts +0 -2
- package/dist/content/stages/review.js +0 -240
- package/dist/content/stages/schema-types.d.ts +0 -203
- package/dist/content/stages/schema-types.js +0 -1
- package/dist/content/stages/scope.d.ts +0 -2
- package/dist/content/stages/scope.js +0 -254
- package/dist/content/stages/ship.d.ts +0 -2
- package/dist/content/stages/ship.js +0 -159
- package/dist/content/stages/spec.d.ts +0 -2
- package/dist/content/stages/spec.js +0 -170
- package/dist/content/stages/tdd.d.ts +0 -4
- package/dist/content/stages/tdd.js +0 -273
- package/dist/content/state-contracts.d.ts +0 -1
- package/dist/content/state-contracts.js +0 -63
- package/dist/content/status-command.d.ts +0 -4
- package/dist/content/status-command.js +0 -109
- package/dist/content/subagent-context-skills.d.ts +0 -4
- package/dist/content/subagent-context-skills.js +0 -279
- package/dist/content/subagents.d.ts +0 -3
- package/dist/content/subagents.js +0 -997
- package/dist/content/templates.d.ts +0 -26
- package/dist/content/templates.js +0 -1692
- package/dist/content/track-render-context.d.ts +0 -18
- package/dist/content/track-render-context.js +0 -53
- package/dist/content/tree-command.d.ts +0 -1
- package/dist/content/tree-command.js +0 -64
- package/dist/content/utility-skills.d.ts +0 -30
- package/dist/content/utility-skills.js +0 -160
- package/dist/content/view-command.d.ts +0 -2
- package/dist/content/view-command.js +0 -92
- package/dist/delegation.d.ts +0 -649
- package/dist/delegation.js +0 -1539
- package/dist/early-loop.d.ts +0 -70
- package/dist/early-loop.js +0 -302
- package/dist/execution-topology.d.ts +0 -44
- package/dist/execution-topology.js +0 -95
- package/dist/gate-evidence.d.ts +0 -85
- package/dist/gate-evidence.js +0 -631
- package/dist/harness-adapters.d.ts +0 -151
- package/dist/harness-adapters.js +0 -756
- package/dist/harness-selection.d.ts +0 -31
- package/dist/harness-selection.js +0 -214
- package/dist/hook-schema.d.ts +0 -6
- package/dist/hook-schema.js +0 -114
- package/dist/hook-schemas/claude-hooks.v1.json +0 -10
- package/dist/hook-schemas/codex-hooks.v1.json +0 -10
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
- package/dist/init-detect.d.ts +0 -2
- package/dist/init-detect.js +0 -50
- package/dist/internal/advance-stage/advance.d.ts +0 -89
- package/dist/internal/advance-stage/advance.js +0 -655
- package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
- package/dist/internal/advance-stage/cancel-run.js +0 -19
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
- package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
- package/dist/internal/advance-stage/helpers.d.ts +0 -14
- package/dist/internal/advance-stage/helpers.js +0 -145
- package/dist/internal/advance-stage/hook.d.ts +0 -8
- package/dist/internal/advance-stage/hook.js +0 -40
- package/dist/internal/advance-stage/parsers.d.ts +0 -72
- package/dist/internal/advance-stage/parsers.js +0 -357
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
- package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
- package/dist/internal/advance-stage/review-loop.d.ts +0 -16
- package/dist/internal/advance-stage/review-loop.js +0 -199
- package/dist/internal/advance-stage/rewind.d.ts +0 -14
- package/dist/internal/advance-stage/rewind.js +0 -108
- package/dist/internal/advance-stage/start-flow.d.ts +0 -13
- package/dist/internal/advance-stage/start-flow.js +0 -241
- package/dist/internal/advance-stage/verify.d.ts +0 -21
- package/dist/internal/advance-stage/verify.js +0 -185
- package/dist/internal/advance-stage.d.ts +0 -7
- package/dist/internal/advance-stage.js +0 -138
- package/dist/internal/cohesion-contract-stub.d.ts +0 -24
- package/dist/internal/cohesion-contract-stub.js +0 -148
- package/dist/internal/compound-readiness.d.ts +0 -23
- package/dist/internal/compound-readiness.js +0 -102
- package/dist/internal/detect-public-api-changes.d.ts +0 -5
- package/dist/internal/detect-public-api-changes.js +0 -45
- package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
- package/dist/internal/detect-supply-chain-changes.js +0 -138
- package/dist/internal/early-loop-status.d.ts +0 -7
- package/dist/internal/early-loop-status.js +0 -93
- package/dist/internal/envelope-validate.d.ts +0 -7
- package/dist/internal/envelope-validate.js +0 -66
- package/dist/internal/flow-state-repair.d.ts +0 -20
- package/dist/internal/flow-state-repair.js +0 -104
- package/dist/internal/plan-split-waves.d.ts +0 -190
- package/dist/internal/plan-split-waves.js +0 -764
- package/dist/internal/runtime-integrity.d.ts +0 -7
- package/dist/internal/runtime-integrity.js +0 -268
- package/dist/internal/slice-commit.d.ts +0 -7
- package/dist/internal/slice-commit.js +0 -619
- package/dist/internal/tdd-loop-status.d.ts +0 -14
- package/dist/internal/tdd-loop-status.js +0 -68
- package/dist/internal/tdd-red-evidence.d.ts +0 -7
- package/dist/internal/tdd-red-evidence.js +0 -153
- package/dist/internal/waiver-grant.d.ts +0 -62
- package/dist/internal/waiver-grant.js +0 -294
- package/dist/internal/wave-status.d.ts +0 -74
- package/dist/internal/wave-status.js +0 -506
- package/dist/managed-resources.d.ts +0 -53
- package/dist/managed-resources.js +0 -313
- package/dist/policy.d.ts +0 -10
- package/dist/policy.js +0 -167
- package/dist/retro-gate.d.ts +0 -9
- package/dist/retro-gate.js +0 -47
- package/dist/run-archive.d.ts +0 -61
- package/dist/run-archive.js +0 -391
- package/dist/runs.d.ts +0 -2
- package/dist/runs.js +0 -2
- package/dist/stack-detection.d.ts +0 -116
- package/dist/stack-detection.js +0 -489
- package/dist/streaming/event-stream.d.ts +0 -31
- package/dist/streaming/event-stream.js +0 -114
- package/dist/tdd-cycle.d.ts +0 -107
- package/dist/tdd-cycle.js +0 -289
- package/dist/tdd-verification-evidence.d.ts +0 -17
- package/dist/tdd-verification-evidence.js +0 -122
- package/dist/track-heuristics.d.ts +0 -27
- package/dist/track-heuristics.js +0 -154
- package/dist/util/slice-id.d.ts +0 -58
- package/dist/util/slice-id.js +0 -89
- package/dist/worktree-manager.d.ts +0 -20
- package/dist/worktree-manager.js +0 -108
package/dist/install.js
CHANGED
|
@@ -1,1393 +1,284 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 { isInteractive, runPicker } from "./harness-prompt.js";
|
|
25
|
+
import { HARNESS_IDS } from "./types.js";
|
|
26
|
+
import { ironLawsMarkdown } from "./content/iron-laws.js";
|
|
27
|
+
const HARNESS_LAYOUTS = {
|
|
28
|
+
claude: {
|
|
29
|
+
id: "claude",
|
|
30
|
+
commandsDir: ".claude/commands",
|
|
31
|
+
agentsDir: ".claude/agents",
|
|
32
|
+
skillsDir: ".claude/skills/cclaw",
|
|
33
|
+
hooksConfig: { dir: ".claude/hooks", fileName: "hooks.json" }
|
|
34
|
+
},
|
|
35
|
+
cursor: {
|
|
36
|
+
id: "cursor",
|
|
37
|
+
commandsDir: ".cursor/commands",
|
|
38
|
+
agentsDir: ".cursor/agents",
|
|
39
|
+
skillsDir: ".cursor/skills/cclaw",
|
|
40
|
+
hooksConfig: { dir: ".cursor", fileName: "hooks.json" }
|
|
41
|
+
},
|
|
42
|
+
opencode: {
|
|
43
|
+
id: "opencode",
|
|
44
|
+
commandsDir: ".opencode/commands",
|
|
45
|
+
agentsDir: ".opencode/agents",
|
|
46
|
+
skillsDir: ".opencode/skills/cclaw",
|
|
47
|
+
hooksConfig: { dir: ".opencode/plugins", fileName: "cclaw-plugin.mjs" }
|
|
48
|
+
},
|
|
49
|
+
codex: {
|
|
50
|
+
id: "codex",
|
|
51
|
+
commandsDir: ".codex/commands",
|
|
52
|
+
agentsDir: ".codex/agents",
|
|
53
|
+
skillsDir: ".codex/skills/cclaw",
|
|
54
|
+
hooksConfig: { dir: ".codex", fileName: "hooks.json" }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export async function ensureRuntimeRoot(projectRoot) {
|
|
58
|
+
const root = path.join(projectRoot, RUNTIME_ROOT);
|
|
59
|
+
for (const dir of [
|
|
60
|
+
STATE_REL_PATH,
|
|
61
|
+
HOOKS_REL_PATH,
|
|
62
|
+
FLOWS_ROOT,
|
|
63
|
+
SHIPPED_DIR_REL_PATH,
|
|
64
|
+
CANCELLED_DIR_REL_PATH,
|
|
65
|
+
LIB_ROOT,
|
|
66
|
+
path.join(LIB_ROOT, "agents"),
|
|
67
|
+
path.join(LIB_ROOT, "skills"),
|
|
68
|
+
path.join(LIB_ROOT, "templates"),
|
|
69
|
+
path.join(LIB_ROOT, "runbooks"),
|
|
70
|
+
path.join(LIB_ROOT, "patterns"),
|
|
71
|
+
path.join(LIB_ROOT, "research"),
|
|
72
|
+
path.join(LIB_ROOT, "recovery"),
|
|
73
|
+
path.join(LIB_ROOT, "examples")
|
|
74
|
+
]) {
|
|
243
75
|
await ensureDir(path.join(projectRoot, dir));
|
|
244
76
|
}
|
|
77
|
+
await writeFileSafe(path.join(root, ".gitkeep"), "cclaw runtime root. Generated by cclaw-cli; safe to keep in version control.\n");
|
|
245
78
|
}
|
|
246
|
-
async function
|
|
247
|
-
|
|
248
|
-
|
|
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"), "");
|
|
79
|
+
async function writeHookFile(projectRoot, hook) {
|
|
80
|
+
const hookPath = path.join(projectRoot, HOOKS_REL_PATH, hook.fileName);
|
|
81
|
+
await writeFileSafe(hookPath, hook.body);
|
|
82
|
+
await fs.chmod(hookPath, 0o755);
|
|
256
83
|
}
|
|
257
|
-
async function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
}
|
|
84
|
+
async function writeAgentFiles(projectRoot) {
|
|
85
|
+
for (const agent of CORE_AGENTS) {
|
|
86
|
+
const agentPath = path.join(projectRoot, LIB_ROOT, "agents", `${agent.id}.md`);
|
|
87
|
+
await writeFileSafe(agentPath, renderAgentMarkdown(agent));
|
|
295
88
|
}
|
|
296
89
|
}
|
|
297
|
-
async function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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));
|
|
90
|
+
async function writeRuntimeSkills(projectRoot) {
|
|
91
|
+
for (const skill of AUTO_TRIGGER_SKILLS) {
|
|
92
|
+
const target = path.join(projectRoot, LIB_ROOT, "skills", skill.fileName);
|
|
93
|
+
await writeFileSafe(target, skill.body);
|
|
304
94
|
}
|
|
305
95
|
}
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
96
|
+
async function writeTemplates(projectRoot) {
|
|
97
|
+
for (const template of ARTIFACT_TEMPLATES) {
|
|
98
|
+
const target = path.join(projectRoot, LIB_ROOT, "templates", template.fileName);
|
|
99
|
+
await writeFileSafe(target, template.body);
|
|
309
100
|
}
|
|
310
|
-
|
|
101
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "templates", "iron-laws.md"), ironLawsMarkdown());
|
|
311
102
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
358
|
-
}
|
|
359
|
-
return out;
|
|
360
|
-
}
|
|
361
|
-
function normalizeJsonLike(raw) {
|
|
362
|
-
return stripJsonCommentsOutsideStrings(raw).replace(/,\s*([}\]])/gu, "$1");
|
|
103
|
+
async function writeIdeasSeed(projectRoot) {
|
|
104
|
+
const target = path.join(projectRoot, RUNTIME_ROOT, "ideas.md");
|
|
105
|
+
if (await exists(target))
|
|
106
|
+
return;
|
|
107
|
+
await writeFileSafe(target, templateBody("ideas"));
|
|
363
108
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
catch {
|
|
369
|
-
// continue with relaxed parse
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
return { parsed: JSON.parse(normalizeJsonLike(raw)), recovered: true };
|
|
109
|
+
async function writeStageRunbooks(projectRoot) {
|
|
110
|
+
const dir = path.join(projectRoot, LIB_ROOT, "runbooks");
|
|
111
|
+
for (const playbook of STAGE_PLAYBOOKS) {
|
|
112
|
+
await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
|
|
373
113
|
}
|
|
374
|
-
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
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
|
-
];
|
|
114
|
+
await writeFileSafe(path.join(dir, "index.md"), STAGE_PLAYBOOKS_INDEX);
|
|
385
115
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
116
|
+
async function writeReferencePatterns(projectRoot) {
|
|
117
|
+
const dir = path.join(projectRoot, LIB_ROOT, "patterns");
|
|
118
|
+
for (const pattern of REFERENCE_PATTERNS) {
|
|
119
|
+
await writeFileSafe(path.join(dir, pattern.fileName), pattern.body);
|
|
389
120
|
}
|
|
390
|
-
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
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;
|
|
121
|
+
await writeFileSafe(path.join(dir, "index.md"), REFERENCE_PATTERNS_INDEX);
|
|
401
122
|
}
|
|
402
|
-
function
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
if (!normalized.has(pluginRelPath)) {
|
|
407
|
-
pluginsRaw.push(pluginRelPath);
|
|
123
|
+
async function writeResearchPlaybooks(projectRoot) {
|
|
124
|
+
const dir = path.join(projectRoot, LIB_ROOT, "research");
|
|
125
|
+
for (const playbook of RESEARCH_PLAYBOOKS) {
|
|
126
|
+
await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
|
|
408
127
|
}
|
|
409
|
-
|
|
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
|
-
};
|
|
128
|
+
await writeFileSafe(path.join(dir, "index.md"), RESEARCH_PLAYBOOKS_INDEX);
|
|
426
129
|
}
|
|
427
|
-
async function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
130
|
+
async function writeRecoveryPlaybooks(projectRoot) {
|
|
131
|
+
const dir = path.join(projectRoot, LIB_ROOT, "recovery");
|
|
132
|
+
for (const playbook of RECOVERY_PLAYBOOKS) {
|
|
133
|
+
await writeFileSafe(path.join(dir, playbook.fileName), playbook.body);
|
|
432
134
|
}
|
|
433
|
-
|
|
135
|
+
await writeFileSafe(path.join(dir, "index.md"), RECOVERY_INDEX);
|
|
434
136
|
}
|
|
435
|
-
async function
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
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`);
|
|
137
|
+
async function writeExamples(projectRoot) {
|
|
138
|
+
const dir = path.join(projectRoot, LIB_ROOT, "examples");
|
|
139
|
+
for (const example of EXAMPLES) {
|
|
140
|
+
await writeFileSafe(path.join(dir, example.fileName), example.body);
|
|
452
141
|
}
|
|
142
|
+
await writeFileSafe(path.join(dir, "index.md"), EXAMPLES_INDEX);
|
|
453
143
|
}
|
|
454
|
-
async function
|
|
455
|
-
|
|
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
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
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
|
-
}));
|
|
144
|
+
async function writeAntipatterns(projectRoot) {
|
|
145
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "antipatterns.md"), ANTIPATTERNS);
|
|
521
146
|
}
|
|
522
|
-
async function
|
|
523
|
-
|
|
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;
|
|
147
|
+
async function writeDecisionProtocol(projectRoot) {
|
|
148
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "decision-protocol.md"), DECISION_PROTOCOL);
|
|
530
149
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
150
|
+
async function writeMetaSkill(projectRoot) {
|
|
151
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "skills", "cclaw-meta.md"), META_SKILL);
|
|
533
152
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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;
|
|
153
|
+
async function writeHarnessAssets(projectRoot, layout) {
|
|
154
|
+
await ensureDir(path.join(projectRoot, layout.commandsDir));
|
|
155
|
+
await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc.md"), renderStartCommand());
|
|
156
|
+
await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc-cancel.md"), renderCancelCommand());
|
|
157
|
+
await writeFileSafe(path.join(projectRoot, layout.commandsDir, "cc-idea.md"), renderIdeaCommand());
|
|
158
|
+
await ensureDir(path.join(projectRoot, layout.agentsDir));
|
|
159
|
+
for (const agent of CORE_AGENTS) {
|
|
160
|
+
await writeFileSafe(path.join(projectRoot, layout.agentsDir, `${agent.id}.md`), renderAgentMarkdown(agent));
|
|
565
161
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
}
|
|
162
|
+
await ensureDir(path.join(projectRoot, layout.skillsDir));
|
|
163
|
+
for (const skill of AUTO_TRIGGER_SKILLS) {
|
|
164
|
+
await writeFileSafe(path.join(projectRoot, layout.skillsDir, skill.fileName), skill.body);
|
|
576
165
|
}
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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;
|
|
166
|
+
if (layout.hooksConfig) {
|
|
167
|
+
const { dir, fileName } = layout.hooksConfig;
|
|
168
|
+
await ensureDir(path.join(projectRoot, dir));
|
|
169
|
+
if (fileName.endsWith(".mjs")) {
|
|
170
|
+
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`;
|
|
171
|
+
await writeFileSafe(path.join(projectRoot, dir, fileName), moduleBody);
|
|
596
172
|
}
|
|
597
173
|
else {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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);
|
|
174
|
+
const hooksJson = {
|
|
175
|
+
version: 1,
|
|
176
|
+
generatedBy: `cclaw-cli@${CCLAW_VERSION}`,
|
|
177
|
+
events: {
|
|
178
|
+
"session.start": [
|
|
179
|
+
{ command: "node", args: [`./${HOOKS_REL_PATH}/${SESSION_START_HOOK_SPEC.fileName}`] }
|
|
180
|
+
],
|
|
181
|
+
"session.stop": [
|
|
182
|
+
{ command: "node", args: [`./${HOOKS_REL_PATH}/${STOP_HANDOFF_HOOK_SPEC.fileName}`] }
|
|
183
|
+
]
|
|
624
184
|
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
await
|
|
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
|
-
}
|
|
185
|
+
};
|
|
186
|
+
await writeFileSafe(path.join(projectRoot, dir, fileName), `${JSON.stringify(hooksJson, null, 2)}\n`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function writeConfig(projectRoot, config) {
|
|
191
|
+
const configPath = path.join(projectRoot, RUNTIME_ROOT, "config.yaml");
|
|
192
|
+
await writeFileSafe(configPath, renderConfig(config));
|
|
193
|
+
return configPath;
|
|
194
|
+
}
|
|
195
|
+
async function resolveHarnesses(projectRoot, fromOptions, fromConfig, interactive) {
|
|
196
|
+
if (fromOptions && fromOptions.length > 0)
|
|
197
|
+
return fromOptions;
|
|
198
|
+
if (fromConfig && fromConfig.length > 0)
|
|
199
|
+
return fromConfig;
|
|
200
|
+
const detected = await detectHarnesses(projectRoot);
|
|
201
|
+
if (interactive && isInteractive()) {
|
|
202
|
+
return runPicker({ detected });
|
|
203
|
+
}
|
|
204
|
+
if (detected.length > 0)
|
|
205
|
+
return detected;
|
|
206
|
+
throw new Error(NO_HARNESS_DETECTED_MESSAGE);
|
|
207
|
+
}
|
|
208
|
+
export async function syncCclaw(options) {
|
|
209
|
+
const projectRoot = options.cwd;
|
|
210
|
+
await ensureRuntimeRoot(projectRoot);
|
|
211
|
+
const existing = await readConfig(projectRoot);
|
|
212
|
+
const harnesses = await resolveHarnesses(projectRoot, options.harnesses, existing?.harnesses, options.interactive ?? false);
|
|
728
213
|
for (const harness of harnesses) {
|
|
729
|
-
if (harness
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
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
|
-
}
|
|
949
|
-
}
|
|
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
|
-
}
|
|
981
|
-
}
|
|
982
|
-
// Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
|
|
983
|
-
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
984
|
-
}
|
|
985
|
-
async function assertExpectedHarnessShims(projectRoot, harnesses) {
|
|
986
|
-
const expectedFiles = harnessShimFileNames();
|
|
987
|
-
const expectedSkillFolders = harnessShimSkillNames();
|
|
214
|
+
if (!HARNESS_IDS.includes(harness)) {
|
|
215
|
+
throw new Error(`Unknown harness: ${harness}. Supported: ${HARNESS_IDS.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const config = existing
|
|
219
|
+
? { ...existing, version: CCLAW_VERSION, flowVersion: "8", harnesses }
|
|
220
|
+
: createDefaultConfig(harnesses);
|
|
221
|
+
await ensureRunSystem(projectRoot);
|
|
222
|
+
await writeAgentFiles(projectRoot);
|
|
223
|
+
for (const hook of NODE_HOOKS) {
|
|
224
|
+
await writeHookFile(projectRoot, hook);
|
|
225
|
+
}
|
|
226
|
+
await writeRuntimeSkills(projectRoot);
|
|
227
|
+
await writeMetaSkill(projectRoot);
|
|
228
|
+
await writeTemplates(projectRoot);
|
|
229
|
+
await writeStageRunbooks(projectRoot);
|
|
230
|
+
await writeReferencePatterns(projectRoot);
|
|
231
|
+
await writeResearchPlaybooks(projectRoot);
|
|
232
|
+
await writeRecoveryPlaybooks(projectRoot);
|
|
233
|
+
await writeExamples(projectRoot);
|
|
234
|
+
await writeAntipatterns(projectRoot);
|
|
235
|
+
await writeDecisionProtocol(projectRoot);
|
|
236
|
+
await writeIdeasSeed(projectRoot);
|
|
988
237
|
for (const harness of harnesses) {
|
|
989
|
-
|
|
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;
|
|
238
|
+
await writeHarnessAssets(projectRoot, HARNESS_LAYOUTS[harness]);
|
|
1091
239
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
240
|
+
await ensureGitignorePatterns(projectRoot);
|
|
241
|
+
const configPath = await writeConfig(projectRoot, config);
|
|
242
|
+
return { installedHarnesses: harnesses, configPath };
|
|
1095
243
|
}
|
|
1096
244
|
export async function initCclaw(options) {
|
|
1097
|
-
|
|
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");
|
|
245
|
+
return syncCclaw(options);
|
|
1167
246
|
}
|
|
1168
|
-
function
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
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;
|
|
247
|
+
export async function uninstallCclaw(options) {
|
|
248
|
+
const projectRoot = options.cwd;
|
|
249
|
+
const config = await readConfig(projectRoot);
|
|
250
|
+
const harnesses = config?.harnesses ?? HARNESS_IDS;
|
|
251
|
+
await removePath(path.join(projectRoot, RUNTIME_ROOT));
|
|
252
|
+
for (const harness of harnesses) {
|
|
253
|
+
const layout = HARNESS_LAYOUTS[harness];
|
|
254
|
+
for (const filename of ["cc.md", "cc-cancel.md", "cc-idea.md"]) {
|
|
255
|
+
await removePath(path.join(projectRoot, layout.commandsDir, filename));
|
|
1183
256
|
}
|
|
1184
|
-
const
|
|
1185
|
-
|
|
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;
|
|
257
|
+
for (const agent of CORE_AGENTS) {
|
|
258
|
+
await removePath(path.join(projectRoot, layout.agentsDir, `${agent.id}.md`));
|
|
1213
259
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
260
|
+
await removePath(path.join(projectRoot, layout.skillsDir));
|
|
261
|
+
if (layout.hooksConfig) {
|
|
262
|
+
await removePath(path.join(projectRoot, layout.hooksConfig.dir, layout.hooksConfig.fileName));
|
|
1216
263
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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;
|
|
264
|
+
if (await exists(path.join(projectRoot, layout.commandsDir))) {
|
|
265
|
+
const remaining = await fs.readdir(path.join(projectRoot, layout.commandsDir));
|
|
266
|
+
if (remaining.length === 0)
|
|
267
|
+
await removePath(path.join(projectRoot, layout.commandsDir));
|
|
1249
268
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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\`.`);
|
|
269
|
+
if (await exists(path.join(projectRoot, layout.agentsDir))) {
|
|
270
|
+
const remaining = await fs.readdir(path.join(projectRoot, layout.agentsDir));
|
|
271
|
+
if (remaining.length === 0)
|
|
272
|
+
await removePath(path.join(projectRoot, layout.agentsDir));
|
|
1255
273
|
}
|
|
1256
|
-
return;
|
|
1257
274
|
}
|
|
1258
|
-
|
|
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;
|
|
1272
|
-
}
|
|
1273
|
-
root.hooks = {};
|
|
1274
|
-
}
|
|
1275
|
-
await writeFileSafe(hookFilePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
275
|
+
await removeGitignorePatterns(projectRoot);
|
|
1276
276
|
}
|
|
1277
|
-
async function
|
|
1278
|
-
|
|
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
|
-
}
|
|
277
|
+
export async function upgradeCclaw(options) {
|
|
278
|
+
return syncCclaw(options);
|
|
1287
279
|
}
|
|
1288
|
-
export
|
|
1289
|
-
|
|
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
|
-
}
|
|
280
|
+
export function planSeedForSlug(slug) {
|
|
281
|
+
return planTemplateForSlug(slug);
|
|
1393
282
|
}
|
|
283
|
+
export const HARNESS_LAYOUT_TABLE = HARNESS_LAYOUTS;
|
|
284
|
+
export const COMMIT_HELPER_HOOK = COMMIT_HELPER_HOOK_SPEC;
|