cclaw-cli 0.1.1 → 0.2.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/dist/artifact-linter.d.ts +20 -0
- package/dist/artifact-linter.js +368 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -2
- package/dist/config.d.ts +4 -4
- package/dist/config.js +56 -5
- package/dist/constants.d.ts +4 -4
- package/dist/constants.js +6 -3
- package/dist/content/autoplan.js +51 -4
- package/dist/content/contexts.d.ts +9 -0
- package/dist/content/contexts.js +65 -0
- package/dist/content/hooks.d.ts +6 -2
- package/dist/content/hooks.js +448 -16
- package/dist/content/meta-skill.js +26 -0
- package/dist/content/next-command.d.ts +9 -0
- package/dist/content/next-command.js +138 -0
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +506 -24
- package/dist/content/skills.js +126 -0
- package/dist/content/stage-schema.d.ts +7 -0
- package/dist/content/stage-schema.js +70 -12
- package/dist/content/subagents.js +33 -0
- package/dist/content/templates.d.ts +1 -0
- package/dist/content/templates.js +182 -77
- package/dist/content/utility-skills.d.ts +5 -1
- package/dist/content/utility-skills.js +208 -2
- package/dist/delegation.d.ts +21 -0
- package/dist/delegation.js +94 -0
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +274 -23
- package/dist/fs-utils.d.ts +10 -0
- package/dist/fs-utils.js +47 -0
- package/dist/gate-evidence.d.ts +26 -0
- package/dist/gate-evidence.js +157 -0
- package/dist/harness-adapters.js +2 -0
- package/dist/hook-schema.d.ts +6 -0
- package/dist/hook-schema.js +45 -0
- package/dist/hook-schemas/claude-hooks.v1.json +12 -0
- package/dist/hook-schemas/codex-hooks.v1.json +12 -0
- package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
- package/dist/install.js +431 -16
- package/dist/policy.d.ts +5 -1
- package/dist/policy.js +52 -1
- package/dist/runs.js +8 -3
- package/dist/trace-matrix.d.ts +13 -0
- package/dist/trace-matrix.js +182 -0
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -1,26 +1,171 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
3
6
|
import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT, UTILITY_COMMANDS } from "./constants.js";
|
|
4
7
|
import { writeConfig, createDefaultConfig, readConfig, configPath } from "./config.js";
|
|
5
8
|
import { commandContract } from "./content/contracts.js";
|
|
6
9
|
import { autoplanSkillMarkdown, autoplanCommandContract } from "./content/autoplan.js";
|
|
10
|
+
import { contextModeFiles, createInitialContextModeState } from "./content/contexts.js";
|
|
7
11
|
import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
|
|
12
|
+
import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
|
|
8
13
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
9
14
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
10
15
|
import { sessionStartScript, stopCheckpointScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
|
|
11
|
-
import { contextMonitorScript, observeScript, promptGuardScript, summarizeObservationsRuntimeModule, summarizeObservationsScript } from "./content/observe.js";
|
|
16
|
+
import { contextMonitorScript, observeScript, promptGuardScript, workflowGuardScript, summarizeObservationsRuntimeModule, summarizeObservationsScript } from "./content/observe.js";
|
|
12
17
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
13
|
-
import { ARTIFACT_TEMPLATES, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
18
|
+
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
14
19
|
import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
15
20
|
import { UTILITY_SKILL_FOLDERS, UTILITY_SKILL_MAP } from "./content/utility-skills.js";
|
|
16
21
|
import { createInitialFlowState } from "./flow-state.js";
|
|
17
22
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
18
23
|
import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
19
24
|
import { HARNESS_ADAPTERS, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
|
|
25
|
+
import { validateHookDocument } from "./hook-schema.js";
|
|
20
26
|
import { ensureRunSystem, readFlowState } from "./runs.js";
|
|
27
|
+
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
28
|
+
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
29
|
+
const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
30
|
+
const GIT_HOOK_RUNTIME_REL_DIR = `${RUNTIME_ROOT}/hooks/git`;
|
|
31
|
+
const execFileAsync = promisify(execFile);
|
|
21
32
|
function runtimePath(projectRoot, ...segments) {
|
|
22
33
|
return path.join(projectRoot, RUNTIME_ROOT, ...segments);
|
|
23
34
|
}
|
|
35
|
+
function resolveGlobalLearningsPath(projectRoot, config) {
|
|
36
|
+
if (config.globalLearnings !== true) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const raw = config.globalLearningsPath?.trim() ?? "";
|
|
40
|
+
if (raw.length === 0) {
|
|
41
|
+
return path.join(os.homedir(), ".cclaw-global-learnings.jsonl");
|
|
42
|
+
}
|
|
43
|
+
if (raw.startsWith("~/")) {
|
|
44
|
+
return path.join(os.homedir(), raw.slice(2));
|
|
45
|
+
}
|
|
46
|
+
if (path.isAbsolute(raw)) {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
return path.join(projectRoot, raw);
|
|
50
|
+
}
|
|
51
|
+
async function resolveGitHooksDir(projectRoot) {
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", "hooks"], {
|
|
54
|
+
cwd: projectRoot
|
|
55
|
+
});
|
|
56
|
+
const rel = stdout.trim();
|
|
57
|
+
if (rel.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(projectRoot, rel);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function managedGitRuntimeScript(hookName) {
|
|
67
|
+
const rangeExpression = hookName === "pre-commit"
|
|
68
|
+
? 'git diff --cached --name-only'
|
|
69
|
+
: 'git diff --name-only @{upstream}...HEAD || git diff --name-only HEAD~1...HEAD';
|
|
70
|
+
return `#!/usr/bin/env bash
|
|
71
|
+
# ${GIT_HOOK_MANAGED_MARKER}: runtime ${hookName}
|
|
72
|
+
set -euo pipefail
|
|
73
|
+
|
|
74
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
75
|
+
GUARD_SCRIPT="$ROOT/${RUNTIME_ROOT}/hooks/prompt-guard.sh"
|
|
76
|
+
[ -x "$GUARD_SCRIPT" ] || exit 0
|
|
77
|
+
|
|
78
|
+
FILES=$(${rangeExpression} 2>/dev/null || true)
|
|
79
|
+
[ -n "$FILES" ] || exit 0
|
|
80
|
+
|
|
81
|
+
printf '%s\n' "$FILES" | bash "$GUARD_SCRIPT"
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
function managedGitRelayHook(hookName) {
|
|
85
|
+
return `#!/usr/bin/env bash
|
|
86
|
+
# ${GIT_HOOK_MANAGED_MARKER}: relay ${hookName}
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
|
|
89
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
90
|
+
RUNTIME_HOOK="$ROOT/${GIT_HOOK_RUNTIME_REL_DIR}/${hookName}.sh"
|
|
91
|
+
[ -x "$RUNTIME_HOOK" ] || exit 0
|
|
92
|
+
exec bash "$RUNTIME_HOOK" "$@"
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
async function removeManagedGitHookRelays(projectRoot) {
|
|
96
|
+
const hooksDir = await resolveGitHooksDir(projectRoot);
|
|
97
|
+
if (!hooksDir) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const hookName of ["pre-commit", "pre-push"]) {
|
|
101
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
102
|
+
if (!(await exists(hookPath)))
|
|
103
|
+
continue;
|
|
104
|
+
let content = "";
|
|
105
|
+
try {
|
|
106
|
+
content = await fs.readFile(hookPath, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
content = "";
|
|
110
|
+
}
|
|
111
|
+
if (!content.includes(GIT_HOOK_MANAGED_MARKER)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
await fs.rm(hookPath, { force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function syncManagedGitHooks(projectRoot, config) {
|
|
118
|
+
const hooksDir = await resolveGitHooksDir(projectRoot);
|
|
119
|
+
if (!hooksDir) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (config.gitHookGuards !== true) {
|
|
123
|
+
await removeManagedGitHookRelays(projectRoot);
|
|
124
|
+
try {
|
|
125
|
+
await fs.rm(path.join(projectRoot, GIT_HOOK_RUNTIME_REL_DIR), { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort cleanup
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const runtimeGitHooksDir = path.join(projectRoot, GIT_HOOK_RUNTIME_REL_DIR);
|
|
133
|
+
await ensureDir(runtimeGitHooksDir);
|
|
134
|
+
for (const hookName of ["pre-commit", "pre-push"]) {
|
|
135
|
+
const runtimePathForHook = path.join(runtimeGitHooksDir, `${hookName}.sh`);
|
|
136
|
+
await writeFileSafe(runtimePathForHook, managedGitRuntimeScript(hookName));
|
|
137
|
+
try {
|
|
138
|
+
await fs.chmod(runtimePathForHook, 0o755);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// best effort on constrained filesystems
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await ensureDir(hooksDir);
|
|
145
|
+
for (const hookName of ["pre-commit", "pre-push"]) {
|
|
146
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
147
|
+
let canWriteRelay = true;
|
|
148
|
+
if (await exists(hookPath)) {
|
|
149
|
+
try {
|
|
150
|
+
const existing = await fs.readFile(hookPath, "utf8");
|
|
151
|
+
canWriteRelay = existing.includes(GIT_HOOK_MANAGED_MARKER);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
canWriteRelay = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!canWriteRelay) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
await writeFileSafe(hookPath, managedGitRelayHook(hookName));
|
|
161
|
+
try {
|
|
162
|
+
await fs.chmod(hookPath, 0o755);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// best effort on constrained filesystems
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
24
169
|
async function ensureStructure(projectRoot) {
|
|
25
170
|
for (const dir of REQUIRED_DIRS) {
|
|
26
171
|
await ensureDir(path.join(projectRoot, dir));
|
|
@@ -48,6 +193,7 @@ async function writeSkills(projectRoot) {
|
|
|
48
193
|
// Utility skills (not flow stages)
|
|
49
194
|
await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
|
|
50
195
|
await writeFileSafe(runtimePath(projectRoot, "skills", "autoplan", "SKILL.md"), autoplanSkillMarkdown());
|
|
196
|
+
await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
|
|
51
197
|
await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
|
|
52
198
|
await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
|
|
53
199
|
await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
|
|
@@ -60,6 +206,7 @@ async function writeSkills(projectRoot) {
|
|
|
60
206
|
async function writeUtilityCommands(projectRoot) {
|
|
61
207
|
await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
|
|
62
208
|
await writeFileSafe(runtimePath(projectRoot, "commands", "autoplan.md"), autoplanCommandContract());
|
|
209
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
|
|
63
210
|
}
|
|
64
211
|
function toObject(value) {
|
|
65
212
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -133,11 +280,120 @@ function tryParseHookDocument(raw) {
|
|
|
133
280
|
return null;
|
|
134
281
|
}
|
|
135
282
|
}
|
|
283
|
+
function opencodeConfigCandidates(projectRoot) {
|
|
284
|
+
return [
|
|
285
|
+
path.join(projectRoot, "opencode.json"),
|
|
286
|
+
path.join(projectRoot, "opencode.jsonc"),
|
|
287
|
+
path.join(projectRoot, ".opencode", "opencode.json"),
|
|
288
|
+
path.join(projectRoot, ".opencode", "opencode.jsonc")
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
function normalizeOpenCodePluginEntry(entry) {
|
|
292
|
+
if (typeof entry === "string" && entry.trim().length > 0) {
|
|
293
|
+
return entry.trim();
|
|
294
|
+
}
|
|
295
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const obj = entry;
|
|
299
|
+
for (const key of ["path", "src", "plugin"]) {
|
|
300
|
+
const value = obj[key];
|
|
301
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
302
|
+
return value.trim();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
function mergeOpenCodePluginConfig(existingDoc, pluginRelPath) {
|
|
308
|
+
const root = toObject(existingDoc) ?? {};
|
|
309
|
+
const pluginsRaw = Array.isArray(root.plugin) ? [...root.plugin] : [];
|
|
310
|
+
const normalized = new Set(pluginsRaw.map((entry) => normalizeOpenCodePluginEntry(entry)).filter(Boolean));
|
|
311
|
+
if (!normalized.has(pluginRelPath)) {
|
|
312
|
+
pluginsRaw.push(pluginRelPath);
|
|
313
|
+
}
|
|
314
|
+
const changed = !normalized.has(pluginRelPath) || !Array.isArray(root.plugin);
|
|
315
|
+
return {
|
|
316
|
+
merged: {
|
|
317
|
+
...root,
|
|
318
|
+
plugin: pluginsRaw
|
|
319
|
+
},
|
|
320
|
+
changed
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async function resolveOpenCodeConfigPath(projectRoot) {
|
|
324
|
+
for (const candidate of opencodeConfigCandidates(projectRoot)) {
|
|
325
|
+
if (await exists(candidate)) {
|
|
326
|
+
return candidate;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return path.join(projectRoot, "opencode.json");
|
|
330
|
+
}
|
|
331
|
+
async function writeMergedOpenCodePluginConfig(projectRoot, pluginRelPath) {
|
|
332
|
+
const configPath = await resolveOpenCodeConfigPath(projectRoot);
|
|
333
|
+
await ensureDir(path.dirname(configPath));
|
|
334
|
+
let existingDoc = {};
|
|
335
|
+
if (await exists(configPath)) {
|
|
336
|
+
try {
|
|
337
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
338
|
+
const parsed = tryParseHookDocument(raw);
|
|
339
|
+
existingDoc = parsed?.parsed ?? {};
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
existingDoc = {};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const { merged, changed } = mergeOpenCodePluginConfig(existingDoc, pluginRelPath);
|
|
346
|
+
if (changed || !(await exists(configPath))) {
|
|
347
|
+
await writeFileSafe(configPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function removeManagedOpenCodePluginConfig(projectRoot, pluginRelPath) {
|
|
351
|
+
for (const configPath of opencodeConfigCandidates(projectRoot)) {
|
|
352
|
+
if (!(await exists(configPath)))
|
|
353
|
+
continue;
|
|
354
|
+
let parsed = null;
|
|
355
|
+
try {
|
|
356
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
357
|
+
parsed = tryParseHookDocument(raw)?.parsed ?? null;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
parsed = null;
|
|
361
|
+
}
|
|
362
|
+
const root = toObject(parsed);
|
|
363
|
+
if (!root || !Array.isArray(root.plugin))
|
|
364
|
+
continue;
|
|
365
|
+
const filtered = root.plugin.filter((entry) => normalizeOpenCodePluginEntry(entry) !== pluginRelPath);
|
|
366
|
+
if (filtered.length === root.plugin.length) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
root.plugin = filtered;
|
|
370
|
+
const remainingKeys = Object.keys(root).filter((k) => k !== "plugin" || filtered.length > 0);
|
|
371
|
+
if (remainingKeys.length === 0 || (remainingKeys.length === 1 && remainingKeys[0] === "plugin" && filtered.length === 0)) {
|
|
372
|
+
await fs.rm(configPath, { force: true });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
if (filtered.length === 0) {
|
|
376
|
+
delete root.plugin;
|
|
377
|
+
}
|
|
378
|
+
await writeFileSafe(configPath, `${JSON.stringify(root, null, 2)}\n`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
136
382
|
function backupFileNameForHook(projectRoot, hookFilePath) {
|
|
137
383
|
const rel = path.relative(projectRoot, hookFilePath).replace(/[\\/]/gu, "__");
|
|
138
384
|
const ts = new Date().toISOString().replace(/[:.]/gu, "-");
|
|
139
385
|
return `${rel}.${ts}.bak`;
|
|
140
386
|
}
|
|
387
|
+
function harnessForHookFile(projectRoot, hookFilePath) {
|
|
388
|
+
const rel = path.relative(projectRoot, hookFilePath).replace(/\\/gu, "/");
|
|
389
|
+
if (rel === ".claude/hooks/hooks.json")
|
|
390
|
+
return "claude";
|
|
391
|
+
if (rel === ".cursor/hooks.json")
|
|
392
|
+
return "cursor";
|
|
393
|
+
if (rel === ".codex/hooks.json")
|
|
394
|
+
return "codex";
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
141
397
|
async function pruneOldHookBackups(backupsDir, maxBackups = 20) {
|
|
142
398
|
let entries = [];
|
|
143
399
|
try {
|
|
@@ -225,25 +481,50 @@ async function writeMergedHookJson(projectRoot, hookFilePath, generatedJson) {
|
|
|
225
481
|
}
|
|
226
482
|
}
|
|
227
483
|
const generatedDoc = JSON.parse(generatedJson);
|
|
484
|
+
const harness = harnessForHookFile(projectRoot, hookFilePath);
|
|
485
|
+
if (harness) {
|
|
486
|
+
const generatedSchema = validateHookDocument(harness, generatedDoc);
|
|
487
|
+
if (!generatedSchema.ok) {
|
|
488
|
+
throw new Error(`Generated ${harness} hook document failed schema validation: ${generatedSchema.errors.join("; ")}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
228
491
|
const mergedDoc = mergeHookDocuments(existingDoc, generatedDoc);
|
|
492
|
+
if (harness) {
|
|
493
|
+
const mergedSchema = validateHookDocument(harness, mergedDoc);
|
|
494
|
+
if (!mergedSchema.ok) {
|
|
495
|
+
throw new Error(`Merged ${harness} hook document failed schema validation: ${mergedSchema.errors.join("; ")}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
229
498
|
await writeFileSafe(hookFilePath, `${JSON.stringify(mergedDoc, null, 2)}\n`);
|
|
230
499
|
}
|
|
231
|
-
async function writeHooks(projectRoot,
|
|
500
|
+
async function writeHooks(projectRoot, config) {
|
|
501
|
+
const harnesses = config.harnesses;
|
|
232
502
|
const hooksDir = runtimePath(projectRoot, "hooks");
|
|
233
503
|
await ensureDir(hooksDir);
|
|
234
|
-
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript(
|
|
504
|
+
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript({
|
|
505
|
+
globalLearningsEnabled: config.globalLearnings === true,
|
|
506
|
+
globalLearningsPath: config.globalLearningsPath
|
|
507
|
+
}));
|
|
235
508
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
236
|
-
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript(
|
|
509
|
+
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
|
|
510
|
+
strictMode: config.promptGuardMode === "strict"
|
|
511
|
+
}));
|
|
512
|
+
await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript());
|
|
237
513
|
await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
|
|
238
514
|
await writeFileSafe(path.join(hooksDir, "observe.sh"), observeScript());
|
|
239
515
|
await writeFileSafe(path.join(hooksDir, "summarize-observations.sh"), summarizeObservationsScript());
|
|
240
516
|
await writeFileSafe(path.join(hooksDir, "summarize-observations.mjs"), summarizeObservationsRuntimeModule());
|
|
241
|
-
|
|
517
|
+
const opencodePluginSource = opencodePluginJs({
|
|
518
|
+
globalLearningsEnabled: config.globalLearnings === true,
|
|
519
|
+
globalLearningsPath: config.globalLearningsPath
|
|
520
|
+
});
|
|
521
|
+
await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
|
|
242
522
|
try {
|
|
243
523
|
for (const script of [
|
|
244
524
|
"session-start.sh",
|
|
245
525
|
"stop-checkpoint.sh",
|
|
246
526
|
"prompt-guard.sh",
|
|
527
|
+
"workflow-guard.sh",
|
|
247
528
|
"context-monitor.sh",
|
|
248
529
|
"observe.sh",
|
|
249
530
|
"summarize-observations.sh",
|
|
@@ -256,6 +537,19 @@ async function writeHooks(projectRoot, harnesses) {
|
|
|
256
537
|
catch {
|
|
257
538
|
// chmod may fail on some filesystems
|
|
258
539
|
}
|
|
540
|
+
if (harnesses.includes("opencode")) {
|
|
541
|
+
const opencodePluginsDir = path.join(projectRoot, ".opencode/plugins");
|
|
542
|
+
const opencodePluginPath = path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
543
|
+
await ensureDir(opencodePluginsDir);
|
|
544
|
+
await writeFileSafe(opencodePluginPath, opencodePluginSource);
|
|
545
|
+
await writeMergedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
546
|
+
try {
|
|
547
|
+
await fs.chmod(opencodePluginPath, 0o755);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// chmod may fail on some filesystems
|
|
551
|
+
}
|
|
552
|
+
}
|
|
259
553
|
for (const harness of harnesses) {
|
|
260
554
|
if (harness === "claude") {
|
|
261
555
|
const dir = path.join(projectRoot, ".claude/hooks");
|
|
@@ -272,7 +566,7 @@ async function writeHooks(projectRoot, harnesses) {
|
|
|
272
566
|
await ensureDir(dir);
|
|
273
567
|
await writeMergedHookJson(projectRoot, path.join(dir, "hooks.json"), codexHooksJson());
|
|
274
568
|
}
|
|
275
|
-
// OpenCode
|
|
569
|
+
// OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
|
|
276
570
|
}
|
|
277
571
|
}
|
|
278
572
|
async function ensureLearningsStore(projectRoot) {
|
|
@@ -281,6 +575,16 @@ async function ensureLearningsStore(projectRoot) {
|
|
|
281
575
|
await writeFileSafe(storePath, "");
|
|
282
576
|
}
|
|
283
577
|
}
|
|
578
|
+
async function ensureGlobalLearningsStore(projectRoot, config) {
|
|
579
|
+
const globalPath = resolveGlobalLearningsPath(projectRoot, config);
|
|
580
|
+
if (!globalPath) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
await ensureDir(path.dirname(globalPath));
|
|
584
|
+
if (!(await exists(globalPath))) {
|
|
585
|
+
await writeFileSafe(globalPath, "");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
284
588
|
async function ensureSessionStateFiles(projectRoot) {
|
|
285
589
|
const stateDir = runtimePath(projectRoot, "state");
|
|
286
590
|
await ensureDir(stateDir);
|
|
@@ -312,11 +616,56 @@ async function ensureSessionStateFiles(projectRoot) {
|
|
|
312
616
|
};
|
|
313
617
|
await writeFileSafe(suggestionMemoryPath, `${JSON.stringify(suggestionMemory, null, 2)}\n`);
|
|
314
618
|
}
|
|
619
|
+
const contextModePath = path.join(stateDir, "context-mode.json");
|
|
620
|
+
if (!(await exists(contextModePath))) {
|
|
621
|
+
await writeFileSafe(contextModePath, `${JSON.stringify(createInitialContextModeState(), null, 2)}\n`);
|
|
622
|
+
}
|
|
315
623
|
}
|
|
316
624
|
async function writeRulebook(projectRoot) {
|
|
317
625
|
await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
|
|
318
626
|
await writeFileSafe(runtimePath(projectRoot, "rules", "rules.json"), `${JSON.stringify(buildRulesJson(), null, 2)}\n`);
|
|
319
627
|
}
|
|
628
|
+
async function writeContextModes(projectRoot) {
|
|
629
|
+
for (const [mode, content] of Object.entries(contextModeFiles())) {
|
|
630
|
+
await writeFileSafe(runtimePath(projectRoot, "contexts", `${mode}.md`), content);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function writeCursorWorkflowRule(projectRoot, harnesses) {
|
|
634
|
+
const rulePath = path.join(projectRoot, CURSOR_RULE_REL_PATH);
|
|
635
|
+
if (!harnesses.includes("cursor")) {
|
|
636
|
+
try {
|
|
637
|
+
await fs.rm(rulePath, { force: true });
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
// best-effort cleanup
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
await ensureDir(path.dirname(rulePath));
|
|
645
|
+
await writeFileSafe(rulePath, CURSOR_WORKFLOW_RULE_MDC);
|
|
646
|
+
}
|
|
647
|
+
async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
|
|
648
|
+
const enabled = new Set(harnesses);
|
|
649
|
+
const managedHookFiles = [
|
|
650
|
+
{ harness: "claude", hookPath: path.join(projectRoot, ".claude/hooks/hooks.json") },
|
|
651
|
+
{ harness: "cursor", hookPath: path.join(projectRoot, ".cursor/hooks.json") },
|
|
652
|
+
{ harness: "codex", hookPath: path.join(projectRoot, ".codex/hooks.json") }
|
|
653
|
+
];
|
|
654
|
+
for (const entry of managedHookFiles) {
|
|
655
|
+
if (enabled.has(entry.harness))
|
|
656
|
+
continue;
|
|
657
|
+
await removeManagedHookEntries(entry.hookPath);
|
|
658
|
+
}
|
|
659
|
+
if (!enabled.has("opencode")) {
|
|
660
|
+
try {
|
|
661
|
+
await fs.rm(path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH), { force: true });
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// best-effort cleanup
|
|
665
|
+
}
|
|
666
|
+
await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
320
669
|
async function writeState(projectRoot, forceReset = false) {
|
|
321
670
|
const statePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
322
671
|
if (!forceReset && (await exists(statePath))) {
|
|
@@ -363,9 +712,23 @@ async function cleanLegacyArtifacts(projectRoot) {
|
|
|
363
712
|
catch {
|
|
364
713
|
// best-effort cleanup
|
|
365
714
|
}
|
|
715
|
+
for (const legacyPlugin of [
|
|
716
|
+
path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
|
|
717
|
+
path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
|
|
718
|
+
path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
|
|
719
|
+
]) {
|
|
720
|
+
try {
|
|
721
|
+
await fs.rm(legacyPlugin, { force: true });
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// best-effort cleanup
|
|
725
|
+
}
|
|
726
|
+
}
|
|
366
727
|
}
|
|
367
728
|
async function cleanStaleFiles(projectRoot) {
|
|
368
729
|
const expectedShimFiles = new Set([
|
|
730
|
+
...COMMAND_FILE_ORDER.map((stage) => `viby-${stage}.md`),
|
|
731
|
+
...UTILITY_COMMANDS.map((cmd) => `viby-${cmd}.md`),
|
|
369
732
|
...COMMAND_FILE_ORDER.map((stage) => `cc-${stage}.md`),
|
|
370
733
|
...UTILITY_COMMANDS.map((cmd) => `cc-${cmd}.md`)
|
|
371
734
|
]);
|
|
@@ -391,13 +754,15 @@ async function cleanStaleFiles(projectRoot) {
|
|
|
391
754
|
// Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
|
|
392
755
|
// Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
|
|
393
756
|
}
|
|
394
|
-
async function materializeRuntime(projectRoot,
|
|
757
|
+
async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
758
|
+
const harnesses = config.harnesses;
|
|
395
759
|
await ensureStructure(projectRoot);
|
|
396
760
|
await cleanLegacyArtifacts(projectRoot);
|
|
397
761
|
await cleanStaleFiles(projectRoot);
|
|
398
762
|
await writeCommandContracts(projectRoot);
|
|
399
763
|
await writeUtilityCommands(projectRoot);
|
|
400
764
|
await writeSkills(projectRoot);
|
|
765
|
+
await writeContextModes(projectRoot);
|
|
401
766
|
await writeArtifactTemplates(projectRoot);
|
|
402
767
|
await writeRulebook(projectRoot);
|
|
403
768
|
await writeState(projectRoot, forceStateReset);
|
|
@@ -405,27 +770,31 @@ async function materializeRuntime(projectRoot, harnesses, forceStateReset) {
|
|
|
405
770
|
await ensureSessionStateFiles(projectRoot);
|
|
406
771
|
await writeAdapterManifest(projectRoot, harnesses);
|
|
407
772
|
await ensureLearningsStore(projectRoot);
|
|
408
|
-
await
|
|
773
|
+
await ensureGlobalLearningsStore(projectRoot, config);
|
|
774
|
+
await writeHooks(projectRoot, config);
|
|
775
|
+
await syncDisabledHarnessArtifacts(projectRoot, harnesses);
|
|
776
|
+
await syncManagedGitHooks(projectRoot, config);
|
|
409
777
|
await syncHarnessShims(projectRoot, harnesses);
|
|
778
|
+
await writeCursorWorkflowRule(projectRoot, harnesses);
|
|
410
779
|
await ensureGitignore(projectRoot);
|
|
411
780
|
}
|
|
412
781
|
export async function initCclaw(options) {
|
|
413
782
|
const config = createDefaultConfig(options.harnesses);
|
|
414
783
|
await writeConfig(options.projectRoot, config);
|
|
415
|
-
await materializeRuntime(options.projectRoot, config
|
|
784
|
+
await materializeRuntime(options.projectRoot, config, true);
|
|
416
785
|
}
|
|
417
786
|
export async function syncCclaw(projectRoot) {
|
|
418
787
|
const config = await readConfig(projectRoot);
|
|
419
788
|
if (!(await exists(configPath(projectRoot)))) {
|
|
420
789
|
await writeConfig(projectRoot, createDefaultConfig(config.harnesses));
|
|
421
790
|
}
|
|
422
|
-
await materializeRuntime(projectRoot, config
|
|
791
|
+
await materializeRuntime(projectRoot, config, false);
|
|
423
792
|
}
|
|
424
793
|
export async function upgradeCclaw(projectRoot) {
|
|
425
794
|
const config = await readConfig(projectRoot);
|
|
426
795
|
const upgradedConfig = createDefaultConfig(config.harnesses);
|
|
427
796
|
await writeConfig(projectRoot, upgradedConfig);
|
|
428
|
-
await materializeRuntime(projectRoot, upgradedConfig
|
|
797
|
+
await materializeRuntime(projectRoot, upgradedConfig, false);
|
|
429
798
|
}
|
|
430
799
|
function stripManagedHookCommands(value) {
|
|
431
800
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -485,7 +854,7 @@ function stripManagedHookCommands(value) {
|
|
|
485
854
|
}
|
|
486
855
|
function isManagedRuntimeHookCommand(command) {
|
|
487
856
|
const normalized = command.trim().replace(/\s+/gu, " ");
|
|
488
|
-
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|context-monitor|observe|summarize-observations)\.sh(?:\s|$)/u.test(normalized);
|
|
857
|
+
return /(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|prompt-guard|workflow-guard|context-monitor|observe|summarize-observations)\.sh(?:\s|$)/u.test(normalized);
|
|
489
858
|
}
|
|
490
859
|
async function removeManagedHookEntries(hookFilePath) {
|
|
491
860
|
if (!(await exists(hookFilePath)))
|
|
@@ -511,7 +880,7 @@ async function removeManagedHookEntries(hookFilePath) {
|
|
|
511
880
|
!Array.isArray(hooks) &&
|
|
512
881
|
Object.keys(hooks).length > 0;
|
|
513
882
|
if (!hasHooks) {
|
|
514
|
-
const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version");
|
|
883
|
+
const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version" || key === "cclawHookSchemaVersion");
|
|
515
884
|
if (onlyHooksShell) {
|
|
516
885
|
await fs.rm(hookFilePath, { force: true });
|
|
517
886
|
return;
|
|
@@ -520,6 +889,17 @@ async function removeManagedHookEntries(hookFilePath) {
|
|
|
520
889
|
}
|
|
521
890
|
await writeFileSafe(hookFilePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
522
891
|
}
|
|
892
|
+
async function removeIfEmpty(dirPath) {
|
|
893
|
+
try {
|
|
894
|
+
const entries = await fs.readdir(dirPath);
|
|
895
|
+
if (entries.length === 0) {
|
|
896
|
+
await fs.rmdir(dirPath);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
// directory not present or not removable
|
|
901
|
+
}
|
|
902
|
+
}
|
|
523
903
|
export async function uninstallCclaw(projectRoot) {
|
|
524
904
|
const fullRuntimePath = path.join(projectRoot, RUNTIME_ROOT);
|
|
525
905
|
try {
|
|
@@ -530,7 +910,7 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
530
910
|
}
|
|
531
911
|
await removeCclawFromAgentsMd(projectRoot);
|
|
532
912
|
await removeGitignorePatterns(projectRoot);
|
|
533
|
-
|
|
913
|
+
await removeManagedGitHookRelays(projectRoot);
|
|
534
914
|
const hookFiles = [
|
|
535
915
|
".claude/hooks/hooks.json",
|
|
536
916
|
".cursor/hooks.json",
|
|
@@ -550,7 +930,7 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
550
930
|
try {
|
|
551
931
|
const entries = await fs.readdir(fullDir);
|
|
552
932
|
for (const entry of entries) {
|
|
553
|
-
if (/^cc-.*\.md$/u.test(entry)) {
|
|
933
|
+
if (/^(?:viby|cc)-.*\.md$/u.test(entry)) {
|
|
554
934
|
await fs.rm(path.join(fullDir, entry), { force: true });
|
|
555
935
|
}
|
|
556
936
|
}
|
|
@@ -559,4 +939,39 @@ export async function uninstallCclaw(projectRoot) {
|
|
|
559
939
|
// directory not present
|
|
560
940
|
}
|
|
561
941
|
}
|
|
942
|
+
for (const pluginPath of [
|
|
943
|
+
path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
|
|
944
|
+
path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
|
|
945
|
+
path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
|
|
946
|
+
]) {
|
|
947
|
+
try {
|
|
948
|
+
await fs.rm(pluginPath, { force: true });
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
// best-effort cleanup
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
|
|
955
|
+
try {
|
|
956
|
+
await fs.rm(path.join(projectRoot, CURSOR_RULE_REL_PATH), { force: true });
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
// best-effort cleanup
|
|
960
|
+
}
|
|
961
|
+
const managedDirs = [
|
|
962
|
+
".claude/hooks",
|
|
963
|
+
".claude/commands",
|
|
964
|
+
".claude",
|
|
965
|
+
".cursor/rules",
|
|
966
|
+
".cursor/commands",
|
|
967
|
+
".cursor",
|
|
968
|
+
".codex/commands",
|
|
969
|
+
".codex",
|
|
970
|
+
".opencode/plugins",
|
|
971
|
+
".opencode/commands",
|
|
972
|
+
".opencode"
|
|
973
|
+
];
|
|
974
|
+
for (const relDir of managedDirs) {
|
|
975
|
+
await removeIfEmpty(path.join(projectRoot, relDir));
|
|
976
|
+
}
|
|
562
977
|
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { HarnessId } from "./types.js";
|
|
1
2
|
export interface PolicyCheck {
|
|
2
3
|
name: string;
|
|
3
4
|
ok: boolean;
|
|
4
5
|
details: string;
|
|
5
6
|
}
|
|
6
|
-
export
|
|
7
|
+
export interface PolicyOptions {
|
|
8
|
+
harnesses?: HarnessId[];
|
|
9
|
+
}
|
|
10
|
+
export declare function policyChecks(projectRoot: string, options?: PolicyOptions): Promise<PolicyCheck[]>;
|