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.
- package/README.md +210 -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 +90 -508
- 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/install.d.ts +27 -15
- package/dist/install.js +230 -1342
- 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,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 {
|
|
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
|
-
|
|
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
|
-
// 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
|
|
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"), "");
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
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
|
-
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
127
|
+
await writeFileSafe(path.join(dir, "index.md"), RESEARCH_PLAYBOOKS_INDEX);
|
|
434
128
|
}
|
|
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`);
|
|
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
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
487
|
-
|
|
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
|
|
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;
|
|
146
|
+
async function writeDecisionProtocol(projectRoot) {
|
|
147
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "decision-protocol.md"), DECISION_PROTOCOL);
|
|
530
148
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
149
|
+
async function writeMetaSkill(projectRoot) {
|
|
150
|
+
await writeFileSafe(path.join(projectRoot, LIB_ROOT, "skills", "cclaw-meta.md"), META_SKILL);
|
|
533
151
|
}
|
|
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;
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
983
|
-
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
203
|
+
return detected;
|
|
984
204
|
}
|
|
985
|
-
async function
|
|
986
|
-
const
|
|
987
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1013
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
|
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;
|
|
254
|
+
for (const agent of CORE_AGENTS) {
|
|
255
|
+
await removePath(path.join(projectRoot, layout.agentsDir, `${agent.id}.md`));
|
|
1213
256
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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\`.`);
|
|
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
|
|
272
|
+
await removeGitignorePatterns(projectRoot);
|
|
1276
273
|
}
|
|
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
|
-
}
|
|
274
|
+
export async function upgradeCclaw(options) {
|
|
275
|
+
return syncCclaw(options);
|
|
1287
276
|
}
|
|
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
|
-
}
|
|
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;
|