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.
Files changed (48) hide show
  1. package/dist/artifact-linter.d.ts +20 -0
  2. package/dist/artifact-linter.js +368 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +8 -2
  5. package/dist/config.d.ts +4 -4
  6. package/dist/config.js +56 -5
  7. package/dist/constants.d.ts +4 -4
  8. package/dist/constants.js +6 -3
  9. package/dist/content/autoplan.js +51 -4
  10. package/dist/content/contexts.d.ts +9 -0
  11. package/dist/content/contexts.js +65 -0
  12. package/dist/content/hooks.d.ts +6 -2
  13. package/dist/content/hooks.js +448 -16
  14. package/dist/content/meta-skill.js +26 -0
  15. package/dist/content/next-command.d.ts +9 -0
  16. package/dist/content/next-command.js +138 -0
  17. package/dist/content/observe.d.ts +5 -1
  18. package/dist/content/observe.js +506 -24
  19. package/dist/content/skills.js +126 -0
  20. package/dist/content/stage-schema.d.ts +7 -0
  21. package/dist/content/stage-schema.js +70 -12
  22. package/dist/content/subagents.js +33 -0
  23. package/dist/content/templates.d.ts +1 -0
  24. package/dist/content/templates.js +182 -77
  25. package/dist/content/utility-skills.d.ts +5 -1
  26. package/dist/content/utility-skills.js +208 -2
  27. package/dist/delegation.d.ts +21 -0
  28. package/dist/delegation.js +94 -0
  29. package/dist/doctor.d.ts +5 -1
  30. package/dist/doctor.js +274 -23
  31. package/dist/fs-utils.d.ts +10 -0
  32. package/dist/fs-utils.js +47 -0
  33. package/dist/gate-evidence.d.ts +26 -0
  34. package/dist/gate-evidence.js +157 -0
  35. package/dist/harness-adapters.js +2 -0
  36. package/dist/hook-schema.d.ts +6 -0
  37. package/dist/hook-schema.js +45 -0
  38. package/dist/hook-schemas/claude-hooks.v1.json +12 -0
  39. package/dist/hook-schemas/codex-hooks.v1.json +12 -0
  40. package/dist/hook-schemas/cursor-hooks.v1.json +15 -0
  41. package/dist/install.js +431 -16
  42. package/dist/policy.d.ts +5 -1
  43. package/dist/policy.js +52 -1
  44. package/dist/runs.js +8 -3
  45. package/dist/trace-matrix.d.ts +13 -0
  46. package/dist/trace-matrix.js +182 -0
  47. package/dist/types.d.ts +11 -1
  48. 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, harnesses) {
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
- await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginJs());
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: plugin.mjs is in .cclaw/hooks/ — user registers in opencode.json
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, harnesses, forceStateReset) {
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 writeHooks(projectRoot, harnesses);
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.harnesses, true);
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.harnesses, false);
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.harnesses, false);
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
- // Clean hook files
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 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[]>;