cclaw-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +28 -0
  2. package/dist/artifact-linter.d.ts +20 -0
  3. package/dist/artifact-linter.js +368 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +22 -5
  6. package/dist/config.d.ts +4 -4
  7. package/dist/config.js +55 -3
  8. package/dist/constants.d.ts +4 -4
  9. package/dist/constants.js +6 -3
  10. package/dist/content/autoplan.js +51 -4
  11. package/dist/content/contexts.d.ts +9 -0
  12. package/dist/content/contexts.js +65 -0
  13. package/dist/content/hooks.d.ts +6 -2
  14. package/dist/content/hooks.js +448 -16
  15. package/dist/content/meta-skill.js +26 -0
  16. package/dist/content/next-command.d.ts +9 -0
  17. package/dist/content/next-command.js +138 -0
  18. package/dist/content/observe.d.ts +5 -1
  19. package/dist/content/observe.js +506 -24
  20. package/dist/content/skills.js +126 -0
  21. package/dist/content/stage-schema.d.ts +7 -0
  22. package/dist/content/stage-schema.js +70 -12
  23. package/dist/content/subagents.js +33 -0
  24. package/dist/content/templates.d.ts +1 -0
  25. package/dist/content/templates.js +182 -77
  26. package/dist/content/utility-skills.d.ts +5 -1
  27. package/dist/content/utility-skills.js +208 -2
  28. package/dist/delegation.d.ts +21 -0
  29. package/dist/delegation.js +94 -0
  30. package/dist/doctor.d.ts +5 -1
  31. package/dist/doctor.js +274 -29
  32. package/dist/fs-utils.d.ts +10 -0
  33. package/dist/fs-utils.js +47 -0
  34. package/dist/gate-evidence.d.ts +26 -0
  35. package/dist/gate-evidence.js +157 -0
  36. package/dist/harness-adapters.js +10 -26
  37. package/dist/hook-schema.d.ts +6 -0
  38. package/dist/hook-schema.js +45 -0
  39. package/dist/hook-schemas/claude-hooks.v1.json +12 -0
  40. package/dist/hook-schemas/codex-hooks.v1.json +12 -0
  41. package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
  42. package/dist/install.js +395 -15
  43. package/dist/policy.d.ts +5 -1
  44. package/dist/policy.js +52 -1
  45. package/dist/runs.js +8 -3
  46. package/dist/trace-matrix.d.ts +13 -0
  47. package/dist/trace-matrix.js +182 -0
  48. package/dist/types.d.ts +11 -1
  49. package/package.json +2 -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,111 @@ 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.plugins) ? [...root.plugins] : [];
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.plugins);
315
+ return {
316
+ merged: {
317
+ ...root,
318
+ plugins: 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.plugins))
364
+ continue;
365
+ const filtered = root.plugins.filter((entry) => normalizeOpenCodePluginEntry(entry) !== pluginRelPath);
366
+ if (filtered.length === root.plugins.length) {
367
+ continue;
368
+ }
369
+ root.plugins = filtered;
370
+ await writeFileSafe(configPath, `${JSON.stringify(root, null, 2)}\n`);
371
+ }
372
+ }
136
373
  function backupFileNameForHook(projectRoot, hookFilePath) {
137
374
  const rel = path.relative(projectRoot, hookFilePath).replace(/[\\/]/gu, "__");
138
375
  const ts = new Date().toISOString().replace(/[:.]/gu, "-");
139
376
  return `${rel}.${ts}.bak`;
140
377
  }
378
+ function harnessForHookFile(projectRoot, hookFilePath) {
379
+ const rel = path.relative(projectRoot, hookFilePath).replace(/\\/gu, "/");
380
+ if (rel === ".claude/hooks/hooks.json")
381
+ return "claude";
382
+ if (rel === ".cursor/hooks.json")
383
+ return "cursor";
384
+ if (rel === ".codex/hooks.json")
385
+ return "codex";
386
+ return null;
387
+ }
141
388
  async function pruneOldHookBackups(backupsDir, maxBackups = 20) {
142
389
  let entries = [];
143
390
  try {
@@ -225,25 +472,50 @@ async function writeMergedHookJson(projectRoot, hookFilePath, generatedJson) {
225
472
  }
226
473
  }
227
474
  const generatedDoc = JSON.parse(generatedJson);
475
+ const harness = harnessForHookFile(projectRoot, hookFilePath);
476
+ if (harness) {
477
+ const generatedSchema = validateHookDocument(harness, generatedDoc);
478
+ if (!generatedSchema.ok) {
479
+ throw new Error(`Generated ${harness} hook document failed schema validation: ${generatedSchema.errors.join("; ")}`);
480
+ }
481
+ }
228
482
  const mergedDoc = mergeHookDocuments(existingDoc, generatedDoc);
483
+ if (harness) {
484
+ const mergedSchema = validateHookDocument(harness, mergedDoc);
485
+ if (!mergedSchema.ok) {
486
+ throw new Error(`Merged ${harness} hook document failed schema validation: ${mergedSchema.errors.join("; ")}`);
487
+ }
488
+ }
229
489
  await writeFileSafe(hookFilePath, `${JSON.stringify(mergedDoc, null, 2)}\n`);
230
490
  }
231
- async function writeHooks(projectRoot, harnesses) {
491
+ async function writeHooks(projectRoot, config) {
492
+ const harnesses = config.harnesses;
232
493
  const hooksDir = runtimePath(projectRoot, "hooks");
233
494
  await ensureDir(hooksDir);
234
- await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
495
+ await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript({
496
+ globalLearningsEnabled: config.globalLearnings === true,
497
+ globalLearningsPath: config.globalLearningsPath
498
+ }));
235
499
  await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
236
- await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript());
500
+ await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
501
+ strictMode: config.promptGuardMode === "strict"
502
+ }));
503
+ await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript());
237
504
  await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
238
505
  await writeFileSafe(path.join(hooksDir, "observe.sh"), observeScript());
239
506
  await writeFileSafe(path.join(hooksDir, "summarize-observations.sh"), summarizeObservationsScript());
240
507
  await writeFileSafe(path.join(hooksDir, "summarize-observations.mjs"), summarizeObservationsRuntimeModule());
241
- await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginJs());
508
+ const opencodePluginSource = opencodePluginJs({
509
+ globalLearningsEnabled: config.globalLearnings === true,
510
+ globalLearningsPath: config.globalLearningsPath
511
+ });
512
+ await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
242
513
  try {
243
514
  for (const script of [
244
515
  "session-start.sh",
245
516
  "stop-checkpoint.sh",
246
517
  "prompt-guard.sh",
518
+ "workflow-guard.sh",
247
519
  "context-monitor.sh",
248
520
  "observe.sh",
249
521
  "summarize-observations.sh",
@@ -256,6 +528,19 @@ async function writeHooks(projectRoot, harnesses) {
256
528
  catch {
257
529
  // chmod may fail on some filesystems
258
530
  }
531
+ if (harnesses.includes("opencode")) {
532
+ const opencodePluginsDir = path.join(projectRoot, ".opencode/plugins");
533
+ const opencodePluginPath = path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH);
534
+ await ensureDir(opencodePluginsDir);
535
+ await writeFileSafe(opencodePluginPath, opencodePluginSource);
536
+ await writeMergedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
537
+ try {
538
+ await fs.chmod(opencodePluginPath, 0o755);
539
+ }
540
+ catch {
541
+ // chmod may fail on some filesystems
542
+ }
543
+ }
259
544
  for (const harness of harnesses) {
260
545
  if (harness === "claude") {
261
546
  const dir = path.join(projectRoot, ".claude/hooks");
@@ -272,7 +557,7 @@ async function writeHooks(projectRoot, harnesses) {
272
557
  await ensureDir(dir);
273
558
  await writeMergedHookJson(projectRoot, path.join(dir, "hooks.json"), codexHooksJson());
274
559
  }
275
- // OpenCode: plugin.mjs is in .cclaw/hooks/ — user registers in opencode.json
560
+ // OpenCode registration is auto-managed via opencode.json/opencode.jsonc.
276
561
  }
277
562
  }
278
563
  async function ensureLearningsStore(projectRoot) {
@@ -281,6 +566,16 @@ async function ensureLearningsStore(projectRoot) {
281
566
  await writeFileSafe(storePath, "");
282
567
  }
283
568
  }
569
+ async function ensureGlobalLearningsStore(projectRoot, config) {
570
+ const globalPath = resolveGlobalLearningsPath(projectRoot, config);
571
+ if (!globalPath) {
572
+ return;
573
+ }
574
+ await ensureDir(path.dirname(globalPath));
575
+ if (!(await exists(globalPath))) {
576
+ await writeFileSafe(globalPath, "");
577
+ }
578
+ }
284
579
  async function ensureSessionStateFiles(projectRoot) {
285
580
  const stateDir = runtimePath(projectRoot, "state");
286
581
  await ensureDir(stateDir);
@@ -312,11 +607,56 @@ async function ensureSessionStateFiles(projectRoot) {
312
607
  };
313
608
  await writeFileSafe(suggestionMemoryPath, `${JSON.stringify(suggestionMemory, null, 2)}\n`);
314
609
  }
610
+ const contextModePath = path.join(stateDir, "context-mode.json");
611
+ if (!(await exists(contextModePath))) {
612
+ await writeFileSafe(contextModePath, `${JSON.stringify(createInitialContextModeState(), null, 2)}\n`);
613
+ }
315
614
  }
316
615
  async function writeRulebook(projectRoot) {
317
616
  await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
318
617
  await writeFileSafe(runtimePath(projectRoot, "rules", "rules.json"), `${JSON.stringify(buildRulesJson(), null, 2)}\n`);
319
618
  }
619
+ async function writeContextModes(projectRoot) {
620
+ for (const [mode, content] of Object.entries(contextModeFiles())) {
621
+ await writeFileSafe(runtimePath(projectRoot, "contexts", `${mode}.md`), content);
622
+ }
623
+ }
624
+ async function writeCursorWorkflowRule(projectRoot, harnesses) {
625
+ const rulePath = path.join(projectRoot, CURSOR_RULE_REL_PATH);
626
+ if (!harnesses.includes("cursor")) {
627
+ try {
628
+ await fs.rm(rulePath, { force: true });
629
+ }
630
+ catch {
631
+ // best-effort cleanup
632
+ }
633
+ return;
634
+ }
635
+ await ensureDir(path.dirname(rulePath));
636
+ await writeFileSafe(rulePath, CURSOR_WORKFLOW_RULE_MDC);
637
+ }
638
+ async function syncDisabledHarnessArtifacts(projectRoot, harnesses) {
639
+ const enabled = new Set(harnesses);
640
+ const managedHookFiles = [
641
+ { harness: "claude", hookPath: path.join(projectRoot, ".claude/hooks/hooks.json") },
642
+ { harness: "cursor", hookPath: path.join(projectRoot, ".cursor/hooks.json") },
643
+ { harness: "codex", hookPath: path.join(projectRoot, ".codex/hooks.json") }
644
+ ];
645
+ for (const entry of managedHookFiles) {
646
+ if (enabled.has(entry.harness))
647
+ continue;
648
+ await removeManagedHookEntries(entry.hookPath);
649
+ }
650
+ if (!enabled.has("opencode")) {
651
+ try {
652
+ await fs.rm(path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH), { force: true });
653
+ }
654
+ catch {
655
+ // best-effort cleanup
656
+ }
657
+ await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
658
+ }
659
+ }
320
660
  async function writeState(projectRoot, forceReset = false) {
321
661
  const statePath = runtimePath(projectRoot, "state", "flow-state.json");
322
662
  if (!forceReset && (await exists(statePath))) {
@@ -363,9 +703,23 @@ async function cleanLegacyArtifacts(projectRoot) {
363
703
  catch {
364
704
  // best-effort cleanup
365
705
  }
706
+ for (const legacyPlugin of [
707
+ path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
708
+ path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
709
+ path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
710
+ ]) {
711
+ try {
712
+ await fs.rm(legacyPlugin, { force: true });
713
+ }
714
+ catch {
715
+ // best-effort cleanup
716
+ }
717
+ }
366
718
  }
367
719
  async function cleanStaleFiles(projectRoot) {
368
720
  const expectedShimFiles = new Set([
721
+ ...COMMAND_FILE_ORDER.map((stage) => `viby-${stage}.md`),
722
+ ...UTILITY_COMMANDS.map((cmd) => `viby-${cmd}.md`),
369
723
  ...COMMAND_FILE_ORDER.map((stage) => `cc-${stage}.md`),
370
724
  ...UTILITY_COMMANDS.map((cmd) => `cc-${cmd}.md`)
371
725
  ]);
@@ -391,13 +745,15 @@ async function cleanStaleFiles(projectRoot) {
391
745
  // Keep user-owned custom assets under .cclaw/agents and .cclaw/skills.
392
746
  // Legacy managed removals happen in cleanLegacyArtifacts() with explicit paths.
393
747
  }
394
- async function materializeRuntime(projectRoot, harnesses, forceStateReset) {
748
+ async function materializeRuntime(projectRoot, config, forceStateReset) {
749
+ const harnesses = config.harnesses;
395
750
  await ensureStructure(projectRoot);
396
751
  await cleanLegacyArtifacts(projectRoot);
397
752
  await cleanStaleFiles(projectRoot);
398
753
  await writeCommandContracts(projectRoot);
399
754
  await writeUtilityCommands(projectRoot);
400
755
  await writeSkills(projectRoot);
756
+ await writeContextModes(projectRoot);
401
757
  await writeArtifactTemplates(projectRoot);
402
758
  await writeRulebook(projectRoot);
403
759
  await writeState(projectRoot, forceStateReset);
@@ -405,27 +761,31 @@ async function materializeRuntime(projectRoot, harnesses, forceStateReset) {
405
761
  await ensureSessionStateFiles(projectRoot);
406
762
  await writeAdapterManifest(projectRoot, harnesses);
407
763
  await ensureLearningsStore(projectRoot);
408
- await writeHooks(projectRoot, harnesses);
764
+ await ensureGlobalLearningsStore(projectRoot, config);
765
+ await writeHooks(projectRoot, config);
766
+ await syncDisabledHarnessArtifacts(projectRoot, harnesses);
767
+ await syncManagedGitHooks(projectRoot, config);
409
768
  await syncHarnessShims(projectRoot, harnesses);
769
+ await writeCursorWorkflowRule(projectRoot, harnesses);
410
770
  await ensureGitignore(projectRoot);
411
771
  }
412
772
  export async function initCclaw(options) {
413
773
  const config = createDefaultConfig(options.harnesses);
414
774
  await writeConfig(options.projectRoot, config);
415
- await materializeRuntime(options.projectRoot, config.harnesses, true);
775
+ await materializeRuntime(options.projectRoot, config, true);
416
776
  }
417
777
  export async function syncCclaw(projectRoot) {
418
778
  const config = await readConfig(projectRoot);
419
779
  if (!(await exists(configPath(projectRoot)))) {
420
780
  await writeConfig(projectRoot, createDefaultConfig(config.harnesses));
421
781
  }
422
- await materializeRuntime(projectRoot, config.harnesses, false);
782
+ await materializeRuntime(projectRoot, config, false);
423
783
  }
424
784
  export async function upgradeCclaw(projectRoot) {
425
785
  const config = await readConfig(projectRoot);
426
786
  const upgradedConfig = createDefaultConfig(config.harnesses);
427
787
  await writeConfig(projectRoot, upgradedConfig);
428
- await materializeRuntime(projectRoot, upgradedConfig.harnesses, false);
788
+ await materializeRuntime(projectRoot, upgradedConfig, false);
429
789
  }
430
790
  function stripManagedHookCommands(value) {
431
791
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -485,7 +845,7 @@ function stripManagedHookCommands(value) {
485
845
  }
486
846
  function isManagedRuntimeHookCommand(command) {
487
847
  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);
848
+ 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
849
  }
490
850
  async function removeManagedHookEntries(hookFilePath) {
491
851
  if (!(await exists(hookFilePath)))
@@ -511,7 +871,7 @@ async function removeManagedHookEntries(hookFilePath) {
511
871
  !Array.isArray(hooks) &&
512
872
  Object.keys(hooks).length > 0;
513
873
  if (!hasHooks) {
514
- const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version");
874
+ const onlyHooksShell = Object.keys(root).every((key) => key === "hooks" || key === "version" || key === "cclawHookSchemaVersion");
515
875
  if (onlyHooksShell) {
516
876
  await fs.rm(hookFilePath, { force: true });
517
877
  return;
@@ -530,6 +890,7 @@ export async function uninstallCclaw(projectRoot) {
530
890
  }
531
891
  await removeCclawFromAgentsMd(projectRoot);
532
892
  await removeGitignorePatterns(projectRoot);
893
+ await removeManagedGitHookRelays(projectRoot);
533
894
  // Clean hook files
534
895
  const hookFiles = [
535
896
  ".claude/hooks/hooks.json",
@@ -550,7 +911,7 @@ export async function uninstallCclaw(projectRoot) {
550
911
  try {
551
912
  const entries = await fs.readdir(fullDir);
552
913
  for (const entry of entries) {
553
- if (/^cc-.*\.md$/u.test(entry)) {
914
+ if (/^(?:viby|cc)-.*\.md$/u.test(entry)) {
554
915
  await fs.rm(path.join(fullDir, entry), { force: true });
555
916
  }
556
917
  }
@@ -559,4 +920,23 @@ export async function uninstallCclaw(projectRoot) {
559
920
  // directory not present
560
921
  }
561
922
  }
923
+ for (const pluginPath of [
924
+ path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
925
+ path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
926
+ path.join(projectRoot, OPENCODE_PLUGIN_REL_PATH)
927
+ ]) {
928
+ try {
929
+ await fs.rm(pluginPath, { force: true });
930
+ }
931
+ catch {
932
+ // best-effort cleanup
933
+ }
934
+ }
935
+ await removeManagedOpenCodePluginConfig(projectRoot, OPENCODE_PLUGIN_REL_PATH);
936
+ try {
937
+ await fs.rm(path.join(projectRoot, CURSOR_RULE_REL_PATH), { force: true });
938
+ }
939
+ catch {
940
+ // best-effort cleanup
941
+ }
562
942
  }
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 declare function policyChecks(projectRoot: string): Promise<PolicyCheck[]>;
7
+ export interface PolicyOptions {
8
+ harnesses?: HarnessId[];
9
+ }
10
+ export declare function policyChecks(projectRoot: string, options?: PolicyOptions): Promise<PolicyCheck[]>;