continuous-improvement 3.9.2 → 3.11.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 (150) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +61 -0
  3. package/QUICKSTART.md +13 -8
  4. package/README.md +84 -153
  5. package/bin/backfill.mjs +0 -0
  6. package/bin/check-doc-runtime-claims.mjs +0 -0
  7. package/bin/check-docs-substrings.mjs +73 -0
  8. package/bin/check-everything-mirror.mjs +0 -0
  9. package/bin/check-routing-targets.mjs +0 -0
  10. package/bin/check-scripts-citation-drift.mjs +210 -0
  11. package/bin/check-skill-count.mjs +110 -0
  12. package/bin/check-skill-law-tag.mjs +0 -0
  13. package/bin/check-skill-mirror.mjs +0 -0
  14. package/bin/check-skill-tiers.mjs +0 -0
  15. package/bin/check-test-imports-only.mjs +126 -0
  16. package/bin/check-third-party-shape.mjs +0 -0
  17. package/bin/companion-preference-status.mjs +231 -0
  18. package/bin/gateguard-clear.mjs +53 -0
  19. package/bin/generate-plugin-manifests.mjs +7 -1
  20. package/bin/harvest-friction.mjs +0 -0
  21. package/bin/hook-stats.mjs +0 -0
  22. package/bin/install.mjs +182 -26
  23. package/bin/lint-transcript.mjs +0 -0
  24. package/bin/mcp-server.mjs +215 -2
  25. package/bin/observe.mjs +0 -0
  26. package/bin/pre-commit-block-strays.sh +0 -0
  27. package/bin/refresh-third-party.mjs +315 -313
  28. package/bin/unified-cli.mjs +0 -0
  29. package/commands/audit.md +34 -0
  30. package/commands/companion-preference.md +58 -0
  31. package/commands/discipline.md +14 -0
  32. package/commands/distill.md +39 -0
  33. package/commands/goal-check.md +53 -0
  34. package/commands/grill-me.md +40 -0
  35. package/commands/grill-with-docs.md +38 -0
  36. package/commands/handoff.md +42 -0
  37. package/commands/proceed-with-the-recommendation.md +20 -0
  38. package/commands/recall.md +49 -0
  39. package/commands/reconcile.md +47 -0
  40. package/commands/superpowers.md +2 -2
  41. package/commands/verify-install.md +55 -0
  42. package/hooks/companion-preference.mjs +190 -0
  43. package/hooks/gateguard.mjs +60 -25
  44. package/hooks/goal-drift-stop.mjs +183 -0
  45. package/hooks/observe.sh +15 -1
  46. package/hooks/route-prompt.mjs +180 -0
  47. package/hooks/route-table.json +35 -0
  48. package/hooks/three-section-close.mjs +134 -132
  49. package/lib/cli-anything.mjs +0 -0
  50. package/lib/compound-engineering.mjs +0 -0
  51. package/lib/gateguard-state.mjs +54 -2
  52. package/lib/goal-drift-gate.mjs +50 -0
  53. package/lib/goal-state.mjs +285 -0
  54. package/lib/observe-event.mjs +0 -0
  55. package/lib/plugin-metadata.mjs +126 -12
  56. package/lib/pm-marketplace.mjs +0 -0
  57. package/lib/pm-skills.mjs +0 -0
  58. package/lib/recall-index.mjs +175 -0
  59. package/lib/resolve-home-dir.mjs +0 -0
  60. package/lib/skill-distill.mjs +222 -0
  61. package/lib/skill-tiers.mjs +0 -0
  62. package/lib/unified-plugin.mjs +0 -0
  63. package/llms.txt +1 -1
  64. package/package.json +11 -7
  65. package/plugins/beginner.json +8 -3
  66. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  67. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  68. package/plugins/continuous-improvement/agents/README.md +2 -2
  69. package/plugins/continuous-improvement/bin/backfill.mjs +0 -0
  70. package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
  71. package/plugins/continuous-improvement/bin/mcp-server.mjs +215 -2
  72. package/plugins/continuous-improvement/bin/observe.mjs +0 -0
  73. package/plugins/continuous-improvement/commands/audit.md +34 -0
  74. package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
  75. package/plugins/continuous-improvement/commands/discipline.md +14 -0
  76. package/plugins/continuous-improvement/commands/distill.md +39 -0
  77. package/plugins/continuous-improvement/commands/goal-check.md +53 -0
  78. package/plugins/continuous-improvement/commands/grill-me.md +40 -0
  79. package/plugins/continuous-improvement/commands/grill-with-docs.md +38 -0
  80. package/plugins/continuous-improvement/commands/handoff.md +42 -0
  81. package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +20 -0
  82. package/plugins/continuous-improvement/commands/recall.md +49 -0
  83. package/plugins/continuous-improvement/commands/reconcile.md +47 -0
  84. package/plugins/continuous-improvement/commands/superpowers.md +2 -2
  85. package/plugins/continuous-improvement/commands/verify-install.md +55 -0
  86. package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
  87. package/plugins/continuous-improvement/hooks/gateguard.mjs +60 -25
  88. package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
  89. package/plugins/continuous-improvement/hooks/hooks.json +18 -2
  90. package/plugins/continuous-improvement/hooks/observe.sh +15 -1
  91. package/plugins/continuous-improvement/hooks/route-prompt.mjs +180 -0
  92. package/plugins/continuous-improvement/hooks/route-table.json +35 -0
  93. package/plugins/continuous-improvement/hooks/three-section-close.mjs +134 -132
  94. package/plugins/continuous-improvement/lib/gateguard-state.mjs +137 -0
  95. package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +50 -0
  96. package/plugins/continuous-improvement/lib/goal-state.mjs +285 -0
  97. package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
  98. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +126 -12
  99. package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
  100. package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
  101. package/plugins/continuous-improvement/lib/skill-distill.mjs +222 -0
  102. package/plugins/continuous-improvement/skills/README.md +8 -0
  103. package/plugins/continuous-improvement/skills/audit/SKILL.md +73 -0
  104. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +13 -17
  105. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +36 -17
  106. package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +81 -0
  107. package/plugins/continuous-improvement/skills/grill-me/SKILL.md +66 -0
  108. package/plugins/continuous-improvement/skills/grill-with-docs/SKILL.md +252 -0
  109. package/plugins/continuous-improvement/skills/handoff/SKILL.md +59 -0
  110. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +1 -1
  111. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +82 -6
  112. package/plugins/continuous-improvement/skills/ralph/SKILL.md +1 -0
  113. package/plugins/continuous-improvement/skills/recall/SKILL.md +50 -0
  114. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +80 -0
  115. package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +1 -0
  116. package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +1 -0
  117. package/plugins/continuous-improvement/skills/skill-distillation/SKILL.md +57 -0
  118. package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +1 -0
  119. package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +1 -0
  120. package/plugins/continuous-improvement/skills/superpowers/SKILL.md +76 -2
  121. package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +1 -1
  122. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +28 -20
  123. package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +1 -0
  124. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
  125. package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +3 -2
  126. package/plugins/expert.json +27 -2
  127. package/skills/README.md +3 -0
  128. package/skills/audit.md +73 -0
  129. package/skills/deploy-receipt.md +13 -17
  130. package/skills/gateguard.md +36 -17
  131. package/skills/goal-monitor.md +81 -0
  132. package/skills/grill-me.md +66 -0
  133. package/skills/grill-with-docs.md +252 -0
  134. package/skills/handoff.md +59 -0
  135. package/skills/para-memory-files.md +1 -1
  136. package/skills/proceed-with-the-recommendation.md +82 -6
  137. package/skills/ralph.md +1 -0
  138. package/skills/recall.md +50 -0
  139. package/skills/reconcile.md +80 -0
  140. package/skills/recovery-classification.md +1 -0
  141. package/skills/safety-guard.md +1 -0
  142. package/skills/skill-distillation.md +57 -0
  143. package/skills/state-reconciliation.md +1 -0
  144. package/skills/strategic-compact.md +1 -0
  145. package/skills/superpowers.md +76 -2
  146. package/skills/token-budget-advisor.md +1 -1
  147. package/skills/verification-loop.md +28 -20
  148. package/skills/wild-risa-balance.md +1 -0
  149. package/skills/workspace-surface-audit.md +1 -1
  150. package/skills/worktree-safety.md +3 -2
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gateguard-clear — record GateGuard clearance for files from the Bash route.
4
+ *
5
+ * The GateGuard PreToolUse hook blocks the first Edit/Write per file and prints
6
+ * the session-state path in its block reason. After presenting the required
7
+ * facts, run this to clear the gate so the retry is allowed. Bash is not gated
8
+ * (unless the command is destructive), so this surface always works — including
9
+ * where the ci_gateguard_clear MCP tool is not connected.
10
+ *
11
+ * Usage:
12
+ * node bin/gateguard-clear.mjs <file_path> [<file_path>...] [--state <path>]
13
+ *
14
+ * Default resolves the session dir canonically, the same way the hook does.
15
+ * --state <path> writes that exact gateguard-session.json — the path the block
16
+ * reason prints — bypassing resolution for any residual env mismatch.
17
+ */
18
+ import { dirname, join } from "node:path";
19
+ import { MAX_CLEARED_FILES, clearFiles, resolveSessionDir, } from "../lib/gateguard-state.mjs";
20
+ function main() {
21
+ const argv = process.argv.slice(2);
22
+ const files = [];
23
+ let stateFile = "";
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (arg === "--state") {
27
+ stateFile = argv[i + 1] ?? "";
28
+ i += 1;
29
+ }
30
+ else {
31
+ files.push(arg);
32
+ }
33
+ }
34
+ if (argv.includes("--state") && stateFile === "") {
35
+ process.stderr.write("--state requires a <gateguard-session.json> path argument\n");
36
+ process.exit(2);
37
+ }
38
+ if (files.length === 0) {
39
+ process.stderr.write("usage: gateguard-clear <file_path> [<file_path>...] [--state <gateguard-session.json>]\n");
40
+ process.exit(2);
41
+ }
42
+ // clearFiles writes <sessionDir>/gateguard-session.json. With --state the
43
+ // caller hands us the full state-file path the block reason printed, so the
44
+ // session dir is its parent.
45
+ const sessionDir = stateFile ? dirname(stateFile) : resolveSessionDir();
46
+ const { cleared, skippedForCap } = clearFiles(sessionDir, files);
47
+ process.stdout.write(`Cleared ${cleared.length}: ${cleared.length > 0 ? cleared.join(", ") : "(none — already cleared)"}\n`);
48
+ if (skippedForCap.length > 0) {
49
+ process.stdout.write(`Skipped — session cap of ${MAX_CLEARED_FILES} reached: ${skippedForCap.join(", ")}\n`);
50
+ }
51
+ process.stdout.write(`State: ${join(sessionDir, "gateguard-session.json")}\n`);
52
+ }
53
+ main();
@@ -35,7 +35,7 @@ async function writeBundledSkills() {
35
35
  const sourceSkillsDir = join(REPO_ROOT, "skills");
36
36
  const targetSkillsDir = join(PLUGIN_BUNDLE_DIR, "skills");
37
37
  const companionSkills = (await readdir(sourceSkillsDir))
38
- .filter((file) => /^[a-z][a-z0-9-]*\.md$/.test(file))
38
+ .filter((file) => file.endsWith(".md") && file !== "README.md")
39
39
  .sort();
40
40
  await mkdir(targetSkillsDir, { recursive: true });
41
41
  await copyFileTo(join(REPO_ROOT, "SKILL.md"), join(targetSkillsDir, PACKAGE_NAME, "SKILL.md"));
@@ -145,9 +145,15 @@ async function writePluginBundle() {
145
145
  copyFileTo(join(REPO_ROOT, "bin", "mcp-server.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "mcp-server.mjs")),
146
146
  copyFileTo(join(REPO_ROOT, "bin", "observe.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "observe.mjs")),
147
147
  copyFileTo(join(REPO_ROOT, "bin", "backfill.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "backfill.mjs")),
148
+ copyFileTo(join(REPO_ROOT, "bin", "gateguard-clear.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "gateguard-clear.mjs")),
149
+ copyFileTo(join(REPO_ROOT, "lib", "gateguard-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "gateguard-state.mjs")),
148
150
  copyFileTo(join(REPO_ROOT, "lib", "plugin-metadata.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "plugin-metadata.mjs")),
149
151
  copyFileTo(join(REPO_ROOT, "lib", "resolve-home-dir.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "resolve-home-dir.mjs")),
150
152
  copyFileTo(join(REPO_ROOT, "lib", "observe-event.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "observe-event.mjs")),
153
+ copyFileTo(join(REPO_ROOT, "lib", "goal-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-state.mjs")),
154
+ copyFileTo(join(REPO_ROOT, "lib", "goal-drift-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-drift-gate.mjs")),
155
+ copyFileTo(join(REPO_ROOT, "lib", "recall-index.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-index.mjs")),
156
+ copyFileTo(join(REPO_ROOT, "lib", "skill-distill.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "skill-distill.mjs")),
151
157
  copyFileTo(join(REPO_ROOT, "LICENSE"), join(PLUGIN_BUNDLE_DIR, "LICENSE")),
152
158
  writePluginBundleReadme(),
153
159
  ]);
File without changes
File without changes
package/bin/install.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * npx continuous-improvement install --pack react # load starter instinct pack
9
9
  * npx continuous-improvement install --uninstall # remove everything
10
10
  */
11
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
11
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
12
12
  import { execSync } from "node:child_process";
13
13
  import { homedir } from "node:os";
14
14
  import { dirname, join } from "node:path";
@@ -32,13 +32,40 @@ const COMMAND_FILES = [
32
32
  "seven-laws.md",
33
33
  "superpowers.md",
34
34
  "swarm.md",
35
+ "verify-install.md",
35
36
  "workspace-surface-audit.md",
36
37
  ];
37
38
  const HOOK_TYPES = ["PreToolUse", "PostToolUse"];
38
39
  const SESSION_HOOK_TYPES = ["SessionStart", "SessionEnd"];
40
+ // Iterated by install cleanup and uninstall cleanup. Deduped via Set so an
41
+ // accidental overlap between HOOK_TYPES and SESSION_HOOK_TYPES never causes
42
+ // a hook bucket to be processed twice.
43
+ const ALL_HOOK_TYPES = Array.from(new Set([...HOOK_TYPES, ...SESSION_HOOK_TYPES]));
44
+ // Our installer writes `bash ".../.claude/instincts/(observe|session).sh"`.
45
+ // Older Windows installs sometimes stored the same command with backslashes in
46
+ // the quoted path; those no-op because bash can't resolve them. We strip only
47
+ // that installer-owned command shape so foreign hooks that merely mention
48
+ // observe.sh/session.sh survive.
49
+ const INSTALLER_OBSERVE_SESSION_COMMAND_RE = /^bash ".*[\\/]\.claude[\\/]instincts[\\/](?:observe|session)\.sh"$/;
50
+ function isBrokenObserveOrSessionCommand(command) {
51
+ if (typeof command !== "string")
52
+ return false;
53
+ return command.includes("\\") && INSTALLER_OBSERVE_SESSION_COMMAND_RE.test(command);
54
+ }
55
+ // Any installer-owned observe.sh / session.sh hook command, broken or clean.
56
+ // Used by uninstall to drop both freshly-installed forward-slash hooks and any
57
+ // stale legacy entries. Pairs with isBrokenObserveOrSessionCommand above.
58
+ function isOurObserveOrSessionCommand(command) {
59
+ if (typeof command !== "string")
60
+ return false;
61
+ return INSTALLER_OBSERVE_SESSION_COMMAND_RE.test(command);
62
+ }
39
63
  function getHomeDir() {
40
64
  return process.env.HOME || process.env.USERPROFILE || homedir();
41
65
  }
66
+ function toBashPath(filePath) {
67
+ return filePath.replace(/\\/g, "/");
68
+ }
42
69
  function isInstallMode(value) {
43
70
  return value === "beginner" || value === "expert";
44
71
  }
@@ -53,6 +80,62 @@ function readJsonFile(filePath) {
53
80
  return null;
54
81
  }
55
82
  }
83
+ // The observation hooks (hooks/observe.sh, hooks/session.sh) are bash scripts.
84
+ // On Windows without Git Bash / WSL the hook commands written into settings.json
85
+ // silently no-op, so the user finishes install thinking observation capture is
86
+ // live when it never fires. Refuse the install with one actionable line instead.
87
+ function assertBashAvailableOnWindows() {
88
+ if (process.platform !== "win32")
89
+ return;
90
+ try {
91
+ execSync("bash --version", { stdio: "ignore" });
92
+ }
93
+ catch {
94
+ console.error(" ✗ Install refused: the observation hooks (hooks/observe.sh, hooks/session.sh) " +
95
+ "are bash scripts, but `bash --version` is not on PATH. Install Git Bash or WSL, " +
96
+ "reopen your shell, and re-run — see README → Troubleshooting install.");
97
+ process.exit(1);
98
+ }
99
+ }
100
+ // The marketplace `/plugin install` path and this npx installer both write into
101
+ // ~/.claude/. Running both duplicates hooks, commands, and skills. We cannot
102
+ // fully resolve Claude Code's marketplace layout from here, so this is a loud
103
+ // best-effort warning, not a hard refuse — a scan failure must never block install.
104
+ function warnOnMarketplaceCollision() {
105
+ const pluginsDir = join(getHomeDir(), ".claude", "plugins");
106
+ if (!existsSync(pluginsDir))
107
+ return;
108
+ let hit = false;
109
+ try {
110
+ for (const entry of readdirSync(pluginsDir)) {
111
+ if (entry.includes(SKILL_NAME)) {
112
+ hit = true;
113
+ break;
114
+ }
115
+ const sub = join(pluginsDir, entry);
116
+ try {
117
+ if (statSync(sub).isDirectory() &&
118
+ readdirSync(sub).some((e) => e.includes(SKILL_NAME))) {
119
+ hit = true;
120
+ break;
121
+ }
122
+ }
123
+ catch {
124
+ // unreadable entry — skip it
125
+ }
126
+ }
127
+ }
128
+ catch {
129
+ // best-effort — never block install on a scan failure
130
+ return;
131
+ }
132
+ if (hit) {
133
+ console.warn("\n ! Possible Beginner+Expert collision: a marketplace install of\n" +
134
+ " continuous-improvement looks present under ~/.claude/plugins/. Running the\n" +
135
+ " npx installer on top of it duplicates hooks/commands/skills in ~/.claude/.\n" +
136
+ " Pick ONE path — marketplace (/plugin install) or npx — not both.\n");
137
+ }
138
+ }
56
139
  const rawArgs = process.argv.slice(2);
57
140
  const modeIndex = rawArgs.indexOf("--mode");
58
141
  const requestedMode = modeIndex !== -1 ? rawArgs[modeIndex + 1] : undefined;
@@ -169,9 +252,10 @@ function setupMcpServer() {
169
252
  console.log(" ✓ Claude Desktop MCP config updated");
170
253
  }
171
254
  }
172
- function hookAlreadyPatched(entries, scriptName) {
173
- return entries.some((entry) => Array.isArray(entry.hooks) &&
174
- entry.hooks.some((hook) => hook.command.includes(scriptName)));
255
+ function hookAlreadyPatched(entries, expectedCommand) {
256
+ return entries.some((entry) => entry &&
257
+ Array.isArray(entry.hooks) &&
258
+ entry.hooks.some((hook) => typeof hook?.command === "string" && hook.command === expectedCommand));
175
259
  }
176
260
  function patchClaudeSettings(observePath) {
177
261
  const settingsPath = join(getHomeDir(), ".claude", "settings.json");
@@ -183,39 +267,75 @@ function patchClaudeSettings(observePath) {
183
267
  if (!settings.hooks) {
184
268
  settings.hooks = {};
185
269
  }
186
- const hookEntry = {
187
- matcher: "",
188
- hooks: [
189
- {
190
- type: "command",
191
- command: `bash "${observePath}"`,
192
- },
193
- ],
194
- };
270
+ // Strip broken legacy observe/session hooks at the hook level, not the entry
271
+ // level. A single entry may carry a foreign command alongside a broken hook;
272
+ // dropping the whole entry to remove the broken hook would also wipe the
273
+ // foreign one. Malformed rows (missing/non-array .hooks) are preserved verbatim
274
+ // — we don't know enough to filter them safely, and dropping risks data loss.
195
275
  let changed = false;
276
+ for (const hookType of ALL_HOOK_TYPES) {
277
+ const hookEntries = settings.hooks[hookType];
278
+ if (!Array.isArray(hookEntries))
279
+ continue;
280
+ let entryListChanged = false;
281
+ const cleanedEntries = [];
282
+ for (const entry of hookEntries) {
283
+ const entryHooks = entry?.hooks;
284
+ if (!Array.isArray(entryHooks)) {
285
+ cleanedEntries.push(entry);
286
+ continue;
287
+ }
288
+ const filteredHooks = entryHooks.filter((hook) => !isBrokenObserveOrSessionCommand(hook?.command));
289
+ if (filteredHooks.length === entryHooks.length) {
290
+ cleanedEntries.push(entry);
291
+ continue;
292
+ }
293
+ entryListChanged = true;
294
+ if (filteredHooks.length > 0) {
295
+ const cleanedEntry = {
296
+ ...entry,
297
+ hooks: filteredHooks,
298
+ };
299
+ cleanedEntries.push(cleanedEntry);
300
+ }
301
+ }
302
+ if (entryListChanged) {
303
+ settings.hooks[hookType] = cleanedEntries;
304
+ changed = true;
305
+ }
306
+ }
307
+ const observeCommand = `bash "${toBashPath(observePath)}"`;
196
308
  for (const hookType of HOOK_TYPES) {
197
309
  if (!Array.isArray(settings.hooks[hookType])) {
198
310
  settings.hooks[hookType] = [];
199
311
  }
200
312
  const hookEntries = settings.hooks[hookType];
201
- if (!hookAlreadyPatched(hookEntries, "observe.sh")) {
202
- hookEntries.push(hookEntry);
313
+ if (!hookAlreadyPatched(hookEntries, observeCommand)) {
314
+ hookEntries.push({
315
+ matcher: "",
316
+ hooks: [
317
+ {
318
+ type: "command",
319
+ command: observeCommand,
320
+ },
321
+ ],
322
+ });
203
323
  changed = true;
204
324
  }
205
325
  }
206
326
  if (INSTALL_MODE === "expert") {
207
327
  const sessionPath = join(getHomeDir(), ".claude", "instincts", "session.sh");
208
- const sessionHook = {
209
- matcher: "",
210
- hooks: [{ type: "command", command: `bash "${sessionPath}"` }],
211
- };
328
+ const sessionCommand = `bash "${toBashPath(sessionPath)}"`;
212
329
  for (const hookType of SESSION_HOOK_TYPES) {
213
330
  if (!Array.isArray(settings.hooks[hookType])) {
214
331
  settings.hooks[hookType] = [];
215
332
  }
216
333
  const hookEntries = settings.hooks[hookType];
217
- if (!hookAlreadyPatched(hookEntries, "session.sh")) {
218
- hookEntries.push(sessionHook);
334
+ if (!hookAlreadyPatched(hookEntries, sessionCommand)) {
335
+ hookEntries.push({
336
+ matcher: "",
337
+ hooks: [{ type: "command", command: sessionCommand }],
338
+ });
219
339
  changed = true;
220
340
  }
221
341
  }
@@ -292,16 +412,43 @@ function uninstallAll() {
292
412
  const settings = readJsonFile(settingsPath);
293
413
  if (settings) {
294
414
  let changed = false;
295
- for (const hookType of [...HOOK_TYPES, ...SESSION_HOOK_TYPES]) {
415
+ // Filter at the hook level, not the entry level. A foreign command may
416
+ // share an entry with one of ours; entry-level deletion would silently
417
+ // wipe the foreign hook. Malformed rows (missing/non-array .hooks) are
418
+ // preserved verbatim — we don't know enough to filter them safely.
419
+ // ALL_HOOK_TYPES dedupes any future overlap between HOOK_TYPES and
420
+ // SESSION_HOOK_TYPES so a bucket is never processed twice.
421
+ for (const hookType of ALL_HOOK_TYPES) {
296
422
  const hookEntries = settings.hooks?.[hookType];
297
423
  if (!Array.isArray(hookEntries)) {
298
424
  continue;
299
425
  }
300
- const nextEntries = hookEntries.filter((entry) => !entry.hooks.some((hook) => hook.command.includes("observe.sh") || hook.command.includes("session.sh")));
301
- if (nextEntries.length !== hookEntries.length) {
426
+ let entryListChanged = false;
427
+ const cleanedEntries = [];
428
+ for (const entry of hookEntries) {
429
+ const entryHooks = entry?.hooks;
430
+ if (!Array.isArray(entryHooks)) {
431
+ cleanedEntries.push(entry);
432
+ continue;
433
+ }
434
+ const filteredHooks = entryHooks.filter((hook) => !isOurObserveOrSessionCommand(hook?.command));
435
+ if (filteredHooks.length === entryHooks.length) {
436
+ cleanedEntries.push(entry);
437
+ continue;
438
+ }
439
+ entryListChanged = true;
440
+ if (filteredHooks.length > 0) {
441
+ const cleanedEntry = {
442
+ ...entry,
443
+ hooks: filteredHooks,
444
+ };
445
+ cleanedEntries.push(cleanedEntry);
446
+ }
447
+ }
448
+ if (entryListChanged) {
449
+ settings.hooks[hookType] = cleanedEntries;
302
450
  changed = true;
303
451
  }
304
- settings.hooks[hookType] = nextEntries;
305
452
  }
306
453
  if (settings.mcpServers?.["continuous-improvement"]) {
307
454
  delete settings.mcpServers["continuous-improvement"];
@@ -403,6 +550,8 @@ console.log(`
403
550
  continuous-improvement (mode: ${INSTALL_MODE})
404
551
  Research → Plan → Execute → Verify → Reflect → Learn → Iterate
405
552
  `);
553
+ assertBashAvailableOnWindows();
554
+ warnOnMarketplaceCollision();
406
555
  console.log("Installing to Claude Code...\n");
407
556
  const installed = installSkill() ? 1 : 0;
408
557
  const modeInfo = {
@@ -417,7 +566,14 @@ if (packIndex !== -1 && rawArgs[packIndex + 1]) {
417
566
  const project = getProjectHashSync();
418
567
  const targetDir = join(getHomeDir(), ".claude", "instincts", project.hash);
419
568
  mkdirSync(targetDir, { recursive: true });
420
- const instincts = JSON.parse(readFileSync(packPath, "utf8"));
569
+ let instincts = [];
570
+ try {
571
+ instincts = JSON.parse(readFileSync(packPath, "utf8"));
572
+ }
573
+ catch (error) {
574
+ console.error(` ✗ Pack "${packName}" could not be loaded: ${getErrorMessage(error)}\n` +
575
+ ` ${packPath} is not valid JSON. No instincts were written — fix or re-fetch the pack file.`);
576
+ }
421
577
  let loaded = 0;
422
578
  for (const instinct of instincts) {
423
579
  const instinctPath = join(targetDir, `${instinct.id}.yaml`);
File without changes
@@ -11,13 +11,17 @@
11
11
  * node bin/mcp-server.mjs --mode beginner # explicit beginner
12
12
  */
13
13
  import { execSync } from "node:child_process";
14
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
14
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
16
  import { basename, dirname, join } from "node:path";
17
17
  import { createInterface } from "node:readline";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { createHash } from "node:crypto";
20
20
  import { PACKAGE_NAME, VERSION, getToolDefinitions, isPluginMode, } from "../lib/plugin-metadata.mjs";
21
+ import { formatDriftReport, parseGoalFromPlan, scoreObservations, } from "../lib/goal-state.mjs";
22
+ import { buildIndex, formatRecallHits, parseSince, query as queryRecall, } from "../lib/recall-index.mjs";
23
+ import { draftFromCandidate, extractTrajectories, findCandidates, formatCandidates, serializeDraft, } from "../lib/skill-distill.mjs";
24
+ import { MAX_CLEARED_FILES, clearFiles, resolveSessionDir, } from "../lib/gateguard-state.mjs";
21
25
  function getHomeDir() {
22
26
  return process.env.HOME || process.env.USERPROFILE || homedir();
23
27
  }
@@ -32,6 +36,12 @@ function getString(value, fallback = "") {
32
36
  function getNumber(value, fallback = 0) {
33
37
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
34
38
  }
39
+ // Draft ids are derived from slugified tool n-grams (src/lib/skill-distill.mts)
40
+ // and are joined into a filesystem path, so a caller-supplied id must match the
41
+ // safe shape before it is used to read/write/delete a draft file.
42
+ function isSafeDraftId(id) {
43
+ return /^draft-[a-z0-9-]+$/.test(id);
44
+ }
35
45
  function getBoolean(value, fallback = false) {
36
46
  return typeof value === "boolean" ? value : fallback;
37
47
  }
@@ -174,6 +184,12 @@ function countObservations(projectHash) {
174
184
  }
175
185
  }
176
186
  function getRecentObservations(projectHash, limit = 50) {
187
+ // Clamp a non-positive or non-integer limit to the default. slice(-0) would
188
+ // otherwise return the ENTIRE history (the limit:0 boundary), and a negative
189
+ // limit would drop rows from the front instead of taking the tail. ci_goal_check
190
+ // pre-rejects bad limits with a clear error(); this guards ci_observations and
191
+ // any other caller that passes a user-supplied limit straight through.
192
+ const cappedLimit = Number.isInteger(limit) && limit > 0 ? limit : 50;
177
193
  const observationsFile = join(INSTINCTS_DIR, projectHash, "observations.jsonl");
178
194
  if (!existsSync(observationsFile)) {
179
195
  return [];
@@ -182,7 +198,7 @@ function getRecentObservations(projectHash, limit = 50) {
182
198
  const lines = readFileSync(observationsFile, "utf8")
183
199
  .split("\n")
184
200
  .filter((line) => line.trim().length > 0);
185
- return lines.slice(-limit).flatMap((line) => {
201
+ return lines.slice(-cappedLimit).flatMap((line) => {
186
202
  try {
187
203
  return [JSON.parse(line)];
188
204
  }
@@ -195,6 +211,16 @@ function getRecentObservations(projectHash, limit = 50) {
195
211
  return [];
196
212
  }
197
213
  }
214
+ function readDistillObservations(projectHash) {
215
+ return getRecentObservations(projectHash, 100000).map((observation) => ({
216
+ ts: getString(observation.ts),
217
+ session: getString(observation.session),
218
+ session_id: getString(observation.session_id),
219
+ tool: getString(observation.tool),
220
+ input_summary: getString(observation.input_summary),
221
+ output_summary: getString(observation.output_summary),
222
+ }));
223
+ }
198
224
  function detectLevel(projectHash) {
199
225
  const observationCount = countObservations(projectHash);
200
226
  const instincts = readInstincts(projectHash);
@@ -480,6 +506,33 @@ function handleTool(name, params) {
480
506
  ].join("\n");
481
507
  return text(reflection);
482
508
  }
509
+ case "ci_gateguard_clear": {
510
+ // Beginner-available on purpose: the GateGuard hook fires for every
511
+ // install, so the clearance action must too. Resolves the session dir via
512
+ // gateguard-state (canonical), the same way the hook does, so the marker
513
+ // lands where the hook looks regardless of how each process spelled the
514
+ // project root.
515
+ const rawList = Array.isArray(params.file_paths) ? params.file_paths : [];
516
+ const listPaths = rawList.filter((value) => typeof value === "string" && value.length > 0);
517
+ const single = getString(params.file_path).trim();
518
+ const paths = single ? [...listPaths, single] : listPaths;
519
+ if (paths.length === 0) {
520
+ return error("file_paths is required — pass the file path(s) named in the GateGuard block reason, e.g. { file_paths: [\"src/x.ts\"] }.");
521
+ }
522
+ const sessionDir = resolveSessionDir();
523
+ const { cleared, skippedForCap } = clearFiles(sessionDir, paths);
524
+ const lines = [
525
+ "## GateGuard clearance",
526
+ "",
527
+ `**State file:** ${join(sessionDir, "gateguard-session.json")}`,
528
+ `**Cleared (${cleared.length}):** ${cleared.length > 0 ? cleared.join(", ") : "(none — already cleared)"}`,
529
+ ];
530
+ if (skippedForCap.length > 0) {
531
+ lines.push(`**Skipped — session cap of ${MAX_CLEARED_FILES} reached (${skippedForCap.length}):** ${skippedForCap.join(", ")}`, "Start a new session to reset the gate.");
532
+ }
533
+ lines.push("", "Retry the Edit/Write now — it will pass.");
534
+ return text(lines.join("\n"));
535
+ }
483
536
  case "ci_reinforce": {
484
537
  if (MODE !== "expert") {
485
538
  return error("ci_reinforce requires expert mode. Start server with --mode expert");
@@ -665,6 +718,166 @@ function handleTool(name, params) {
665
718
  }
666
719
  return text(lines.join("\n"));
667
720
  }
721
+ case "ci_goal_check": {
722
+ if (MODE !== "expert") {
723
+ return error("ci_goal_check requires expert mode");
724
+ }
725
+ const limit = getNumber(params.limit, 30);
726
+ if (!Number.isInteger(limit) || limit <= 0) {
727
+ return error(`limit must be a positive integer; got ${limit}. Omit it to score the default 30 most recent observations.`);
728
+ }
729
+ const explicit = getString(params.goal_file).trim();
730
+ const workspaceRoot = getWorkspaceRoot();
731
+ const candidates = explicit
732
+ ? [explicit]
733
+ : [
734
+ join(workspaceRoot, PLANNING_FILES.taskPlan),
735
+ join(INSTINCTS_DIR, project.hash, "goal.md"),
736
+ ];
737
+ let goalContent = "";
738
+ let goalSource = "";
739
+ for (const candidate of candidates) {
740
+ if (!existsSync(candidate)) {
741
+ continue;
742
+ }
743
+ try {
744
+ goalContent = readFileSync(candidate, "utf8");
745
+ goalSource = candidate;
746
+ break;
747
+ }
748
+ catch {
749
+ // try the next candidate
750
+ }
751
+ }
752
+ if (!goalContent) {
753
+ return text(`No goal source found. Looked for: ${candidates.join(", ")}.\nRun ci_plan_init (or /planning-with-files) to create ${PLANNING_FILES.taskPlan} with a '## Goal' section.`);
754
+ }
755
+ const goal = parseGoalFromPlan(goalContent);
756
+ if (!goal) {
757
+ return text(`Found ${goalSource} but it has no '## Goal' section. Add one — ci_plan_init seeds it. Optional '## Goal Keywords' and '## Goal Scope' sections sharpen the drift signal.`);
758
+ }
759
+ const goalObservations = getRecentObservations(project.hash, limit).map((observation) => ({
760
+ ts: getString(observation.ts),
761
+ tool: getString(observation.tool),
762
+ input_summary: getString(observation.input_summary),
763
+ output_summary: getString(observation.output_summary),
764
+ }));
765
+ const report = scoreObservations(goalObservations, goal, { window: limit });
766
+ return text(`## Goal Check\n\n**Goal source:** ${goalSource}\n\n${formatDriftReport(report)}`);
767
+ }
768
+ case "ci_recall": {
769
+ if (MODE !== "expert") {
770
+ return error("ci_recall requires expert mode");
771
+ }
772
+ const queryString = getString(params.query).trim();
773
+ if (!queryString) {
774
+ return error("query is required");
775
+ }
776
+ const k = getNumber(params.k, 5);
777
+ const since = getString(params.since).trim();
778
+ if (since && parseSince(since, Date.now()) === null) {
779
+ return error(`Could not parse since="${since}". Use an ISO timestamp (e.g. 2026-05-01) or a relative window like 7d, 24h, or 30m.`);
780
+ }
781
+ // Recall searches the full history, not just the recent window.
782
+ const recallObservations = getRecentObservations(project.hash, 100000).map((observation) => ({
783
+ ts: getString(observation.ts),
784
+ session: getString(observation.session),
785
+ session_id: getString(observation.session_id),
786
+ tool: getString(observation.tool),
787
+ input_summary: getString(observation.input_summary),
788
+ output_summary: getString(observation.output_summary),
789
+ }));
790
+ if (recallObservations.length === 0) {
791
+ return text("No observations yet. Hooks capture tool calls automatically; recall searches that history.");
792
+ }
793
+ const index = buildIndex(recallObservations);
794
+ const hits = queryRecall(index, queryString, { k, since: since || undefined });
795
+ return text(formatRecallHits(hits, queryString));
796
+ }
797
+ case "ci_distill_candidates": {
798
+ if (MODE !== "expert") {
799
+ return error("ci_distill_candidates requires expert mode");
800
+ }
801
+ const distillObservations = readDistillObservations(project.hash);
802
+ const candidates = findCandidates(extractTrajectories(distillObservations));
803
+ return text(formatCandidates(candidates));
804
+ }
805
+ case "ci_distill_propose": {
806
+ if (MODE !== "expert") {
807
+ return error("ci_distill_propose requires expert mode");
808
+ }
809
+ const id = getString(params.id).trim();
810
+ if (!id) {
811
+ return error("id is required — run ci_distill_candidates to list current candidate ids");
812
+ }
813
+ if (!isSafeDraftId(id)) {
814
+ return error(`Invalid draft id "${id}". Draft ids look like draft-<slug> (lowercase letters, digits, hyphens).`);
815
+ }
816
+ const distillObservations = readDistillObservations(project.hash);
817
+ const candidate = findCandidates(extractTrajectories(distillObservations)).find((entry) => entry.id === id);
818
+ if (!candidate) {
819
+ return error(`No candidate "${id}". Run ci_distill_candidates to list current ids.`);
820
+ }
821
+ const draft = serializeDraft(draftFromCandidate(candidate));
822
+ const draftsDir = join(INSTINCTS_DIR, project.hash, "drafts");
823
+ mkdirSync(draftsDir, { recursive: true });
824
+ const draftPath = join(draftsDir, `${id}.yaml`);
825
+ writeFileSync(draftPath, draft);
826
+ return text([
827
+ "## Draft written",
828
+ "",
829
+ `**Path:** ${draftPath}`,
830
+ "",
831
+ "Edit the body to capture the real recipe (preconditions, concrete steps, gotchas), then promote with:",
832
+ "",
833
+ ` ci_distill_promote id=${id}`,
834
+ "",
835
+ "```yaml",
836
+ draft.trimEnd(),
837
+ "```",
838
+ ].join("\n"));
839
+ }
840
+ case "ci_distill_promote": {
841
+ if (MODE !== "expert") {
842
+ return error("ci_distill_promote requires expert mode");
843
+ }
844
+ const id = getString(params.id).trim();
845
+ if (!id) {
846
+ return error("id is required");
847
+ }
848
+ if (!isSafeDraftId(id)) {
849
+ return error(`Invalid draft id "${id}". Draft ids look like draft-<slug> (lowercase letters, digits, hyphens).`);
850
+ }
851
+ const draftPath = join(INSTINCTS_DIR, project.hash, "drafts", `${id}.yaml`);
852
+ if (!existsSync(draftPath)) {
853
+ return error(`No draft at ${draftPath}. Run ci_distill_propose id=${id} first.`);
854
+ }
855
+ let parsed = null;
856
+ try {
857
+ parsed = parseYamlInstinct(readFileSync(draftPath, "utf8"));
858
+ }
859
+ catch {
860
+ parsed = null;
861
+ }
862
+ if (!parsed) {
863
+ return error(`Draft ${draftPath} could not be parsed as an instinct. Check the YAML shape.`);
864
+ }
865
+ const promoted = {
866
+ ...parsed,
867
+ confidence: 0.5,
868
+ source: "distilled",
869
+ scope: "project",
870
+ observation_count: 1,
871
+ };
872
+ writeInstinct(project.hash, promoted);
873
+ try {
874
+ rmSync(draftPath);
875
+ }
876
+ catch {
877
+ // best-effort cleanup; the live instinct is already written
878
+ }
879
+ return text(`Promoted **${promoted.id}** to a project instinct at confidence ${promoted.confidence} (SUGGEST tier). The draft has been consumed.`);
880
+ }
668
881
  case "ci_dashboard": {
669
882
  if (MODE !== "expert") {
670
883
  return error("ci_dashboard requires expert mode");
package/bin/observe.mjs CHANGED
File without changes
File without changes