continuous-improvement 3.9.2 → 3.12.3

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 (160) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/CHANGELOG.md +95 -0
  3. package/LICENSE +21 -21
  4. package/QUICKSTART.md +13 -8
  5. package/README.md +94 -162
  6. package/SKILL.md +1 -1
  7. package/bin/analyze.sh +161 -161
  8. package/bin/backfill.mjs +19 -19
  9. package/bin/check-docs-substrings.mjs +73 -0
  10. package/bin/check-scripts-citation-drift.mjs +210 -0
  11. package/bin/check-skill-count.mjs +110 -0
  12. package/bin/check-test-imports-only.mjs +126 -0
  13. package/bin/check-tool-count.mjs +129 -0
  14. package/bin/companion-preference-status.mjs +231 -0
  15. package/bin/gateguard-clear.mjs +53 -0
  16. package/bin/generate-plugin-manifests.mjs +8 -1
  17. package/bin/harvest-friction.mjs +1 -1
  18. package/bin/hook-stats.mjs +21 -21
  19. package/bin/install.mjs +185 -28
  20. package/bin/mcp-server.mjs +216 -3
  21. package/bin/refresh-third-party.mjs +315 -313
  22. package/commands/audit.md +34 -0
  23. package/commands/companion-preference.md +58 -0
  24. package/commands/continuous-improvement.md +115 -115
  25. package/commands/dashboard.md +56 -56
  26. package/commands/discipline.md +14 -0
  27. package/commands/distill.md +39 -0
  28. package/commands/goal-check.md +53 -0
  29. package/commands/grill-me.md +40 -0
  30. package/commands/grill-with-docs.md +38 -0
  31. package/commands/handoff.md +42 -0
  32. package/commands/harvest.md +1 -1
  33. package/commands/proceed-with-the-recommendation.md +20 -0
  34. package/commands/recall.md +49 -0
  35. package/commands/reconcile.md +47 -0
  36. package/commands/seven-laws.md +16 -16
  37. package/commands/superpowers.md +3 -3
  38. package/commands/verify-install.md +55 -0
  39. package/commands/workspace-surface-audit.md +77 -77
  40. package/hooks/companion-preference.mjs +190 -0
  41. package/hooks/gateguard.mjs +72 -25
  42. package/hooks/goal-drift-stop.mjs +183 -0
  43. package/hooks/observe.sh +15 -1
  44. package/hooks/recall-briefing.mjs +167 -0
  45. package/hooks/route-prompt.mjs +180 -0
  46. package/hooks/route-table.json +35 -0
  47. package/hooks/session.sh +106 -106
  48. package/hooks/three-section-close.mjs +134 -132
  49. package/instinct-packs/go.json +58 -58
  50. package/instinct-packs/meta.json +16 -16
  51. package/instinct-packs/python.json +58 -58
  52. package/instinct-packs/react.json +58 -58
  53. package/lib/gateguard-state.mjs +54 -2
  54. package/lib/goal-drift-gate.mjs +50 -0
  55. package/lib/goal-state.mjs +285 -0
  56. package/lib/plugin-metadata.mjs +134 -15
  57. package/lib/recall-briefing.mjs +57 -0
  58. package/lib/recall-index.mjs +175 -0
  59. package/lib/skill-distill.mjs +222 -0
  60. package/llms.txt +2 -2
  61. package/package.json +12 -7
  62. package/plugins/beginner.json +9 -4
  63. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  64. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  65. package/plugins/continuous-improvement/LICENSE +21 -21
  66. package/plugins/continuous-improvement/agents/README.md +3 -3
  67. package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
  68. package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
  69. package/plugins/continuous-improvement/bin/mcp-server.mjs +216 -3
  70. package/plugins/continuous-improvement/commands/audit.md +34 -0
  71. package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
  72. package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
  73. package/plugins/continuous-improvement/commands/dashboard.md +56 -56
  74. package/plugins/continuous-improvement/commands/discipline.md +14 -0
  75. package/plugins/continuous-improvement/commands/distill.md +39 -0
  76. package/plugins/continuous-improvement/commands/goal-check.md +53 -0
  77. package/plugins/continuous-improvement/commands/grill-me.md +40 -0
  78. package/plugins/continuous-improvement/commands/grill-with-docs.md +38 -0
  79. package/plugins/continuous-improvement/commands/handoff.md +42 -0
  80. package/plugins/continuous-improvement/commands/harvest.md +1 -1
  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/seven-laws.md +16 -16
  85. package/plugins/continuous-improvement/commands/superpowers.md +3 -3
  86. package/plugins/continuous-improvement/commands/verify-install.md +55 -0
  87. package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
  88. package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
  89. package/plugins/continuous-improvement/hooks/gateguard.mjs +72 -25
  90. package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
  91. package/plugins/continuous-improvement/hooks/hooks.json +23 -2
  92. package/plugins/continuous-improvement/hooks/observe.sh +15 -1
  93. package/plugins/continuous-improvement/hooks/recall-briefing.mjs +167 -0
  94. package/plugins/continuous-improvement/hooks/route-prompt.mjs +180 -0
  95. package/plugins/continuous-improvement/hooks/route-table.json +35 -0
  96. package/plugins/continuous-improvement/hooks/session.sh +106 -106
  97. package/plugins/continuous-improvement/hooks/three-section-close.mjs +134 -132
  98. package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
  99. package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
  100. package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
  101. package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
  102. package/plugins/continuous-improvement/lib/gateguard-state.mjs +137 -0
  103. package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +50 -0
  104. package/plugins/continuous-improvement/lib/goal-state.mjs +285 -0
  105. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +134 -15
  106. package/plugins/continuous-improvement/lib/recall-briefing.mjs +57 -0
  107. package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
  108. package/plugins/continuous-improvement/lib/skill-distill.mjs +222 -0
  109. package/plugins/continuous-improvement/skills/README.md +8 -0
  110. package/plugins/continuous-improvement/skills/audit/SKILL.md +73 -0
  111. package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +1 -1
  112. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +13 -17
  113. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +36 -17
  114. package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +81 -0
  115. package/plugins/continuous-improvement/skills/grill-me/SKILL.md +66 -0
  116. package/plugins/continuous-improvement/skills/grill-with-docs/SKILL.md +252 -0
  117. package/plugins/continuous-improvement/skills/handoff/SKILL.md +59 -0
  118. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +1 -1
  119. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +82 -6
  120. package/plugins/continuous-improvement/skills/ralph/SKILL.md +1 -0
  121. package/plugins/continuous-improvement/skills/recall/SKILL.md +50 -0
  122. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +80 -0
  123. package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +1 -0
  124. package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +1 -0
  125. package/plugins/continuous-improvement/skills/skill-distillation/SKILL.md +57 -0
  126. package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +1 -0
  127. package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +1 -0
  128. package/plugins/continuous-improvement/skills/superpowers/SKILL.md +77 -3
  129. package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
  130. package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +1 -1
  131. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +28 -20
  132. package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +1 -0
  133. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
  134. package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +3 -2
  135. package/plugins/expert.json +28 -3
  136. package/skills/README.md +3 -0
  137. package/skills/audit.md +73 -0
  138. package/skills/deploy-receipt.md +13 -17
  139. package/skills/gateguard.md +36 -17
  140. package/skills/goal-monitor.md +81 -0
  141. package/skills/grill-me.md +66 -0
  142. package/skills/grill-with-docs.md +252 -0
  143. package/skills/handoff.md +59 -0
  144. package/skills/para-memory-files.md +1 -1
  145. package/skills/proceed-with-the-recommendation.md +82 -6
  146. package/skills/ralph.md +1 -0
  147. package/skills/recall.md +50 -0
  148. package/skills/reconcile.md +80 -0
  149. package/skills/recovery-classification.md +1 -0
  150. package/skills/safety-guard.md +1 -0
  151. package/skills/skill-distillation.md +57 -0
  152. package/skills/state-reconciliation.md +1 -0
  153. package/skills/strategic-compact.md +1 -0
  154. package/skills/superpowers.md +77 -3
  155. package/skills/tdd-workflow.md +411 -411
  156. package/skills/token-budget-advisor.md +1 -1
  157. package/skills/verification-loop.md +28 -20
  158. package/skills/wild-risa-balance.md +1 -0
  159. package/skills/workspace-surface-audit.md +1 -1
  160. package/skills/worktree-safety.md +3 -2
package/bin/install.mjs CHANGED
@@ -8,12 +8,13 @@
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";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { createHash } from "node:crypto";
17
+ import { getToolNames } from "../lib/plugin-metadata.mjs";
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
19
20
  const SKILL_SOURCE = join(__dirname, "..", "SKILL.md");
@@ -32,13 +33,40 @@ const COMMAND_FILES = [
32
33
  "seven-laws.md",
33
34
  "superpowers.md",
34
35
  "swarm.md",
36
+ "verify-install.md",
35
37
  "workspace-surface-audit.md",
36
38
  ];
37
39
  const HOOK_TYPES = ["PreToolUse", "PostToolUse"];
38
40
  const SESSION_HOOK_TYPES = ["SessionStart", "SessionEnd"];
41
+ // Iterated by install cleanup and uninstall cleanup. Deduped via Set so an
42
+ // accidental overlap between HOOK_TYPES and SESSION_HOOK_TYPES never causes
43
+ // a hook bucket to be processed twice.
44
+ const ALL_HOOK_TYPES = Array.from(new Set([...HOOK_TYPES, ...SESSION_HOOK_TYPES]));
45
+ // Our installer writes `bash ".../.claude/instincts/(observe|session).sh"`.
46
+ // Older Windows installs sometimes stored the same command with backslashes in
47
+ // the quoted path; those no-op because bash can't resolve them. We strip only
48
+ // that installer-owned command shape so foreign hooks that merely mention
49
+ // observe.sh/session.sh survive.
50
+ const INSTALLER_OBSERVE_SESSION_COMMAND_RE = /^bash ".*[\\/]\.claude[\\/]instincts[\\/](?:observe|session)\.sh"$/;
51
+ function isBrokenObserveOrSessionCommand(command) {
52
+ if (typeof command !== "string")
53
+ return false;
54
+ return command.includes("\\") && INSTALLER_OBSERVE_SESSION_COMMAND_RE.test(command);
55
+ }
56
+ // Any installer-owned observe.sh / session.sh hook command, broken or clean.
57
+ // Used by uninstall to drop both freshly-installed forward-slash hooks and any
58
+ // stale legacy entries. Pairs with isBrokenObserveOrSessionCommand above.
59
+ function isOurObserveOrSessionCommand(command) {
60
+ if (typeof command !== "string")
61
+ return false;
62
+ return INSTALLER_OBSERVE_SESSION_COMMAND_RE.test(command);
63
+ }
39
64
  function getHomeDir() {
40
65
  return process.env.HOME || process.env.USERPROFILE || homedir();
41
66
  }
67
+ function toBashPath(filePath) {
68
+ return filePath.replace(/\\/g, "/");
69
+ }
42
70
  function isInstallMode(value) {
43
71
  return value === "beginner" || value === "expert";
44
72
  }
@@ -53,6 +81,62 @@ function readJsonFile(filePath) {
53
81
  return null;
54
82
  }
55
83
  }
84
+ // The observation hooks (hooks/observe.sh, hooks/session.sh) are bash scripts.
85
+ // On Windows without Git Bash / WSL the hook commands written into settings.json
86
+ // silently no-op, so the user finishes install thinking observation capture is
87
+ // live when it never fires. Refuse the install with one actionable line instead.
88
+ function assertBashAvailableOnWindows() {
89
+ if (process.platform !== "win32")
90
+ return;
91
+ try {
92
+ execSync("bash --version", { stdio: "ignore" });
93
+ }
94
+ catch {
95
+ console.error(" ✗ Install refused: the observation hooks (hooks/observe.sh, hooks/session.sh) " +
96
+ "are bash scripts, but `bash --version` is not on PATH. Install Git Bash or WSL, " +
97
+ "reopen your shell, and re-run — see README → Troubleshooting install.");
98
+ process.exit(1);
99
+ }
100
+ }
101
+ // The marketplace `/plugin install` path and this npx installer both write into
102
+ // ~/.claude/. Running both duplicates hooks, commands, and skills. We cannot
103
+ // fully resolve Claude Code's marketplace layout from here, so this is a loud
104
+ // best-effort warning, not a hard refuse — a scan failure must never block install.
105
+ function warnOnMarketplaceCollision() {
106
+ const pluginsDir = join(getHomeDir(), ".claude", "plugins");
107
+ if (!existsSync(pluginsDir))
108
+ return;
109
+ let hit = false;
110
+ try {
111
+ for (const entry of readdirSync(pluginsDir)) {
112
+ if (entry.includes(SKILL_NAME)) {
113
+ hit = true;
114
+ break;
115
+ }
116
+ const sub = join(pluginsDir, entry);
117
+ try {
118
+ if (statSync(sub).isDirectory() &&
119
+ readdirSync(sub).some((e) => e.includes(SKILL_NAME))) {
120
+ hit = true;
121
+ break;
122
+ }
123
+ }
124
+ catch {
125
+ // unreadable entry — skip it
126
+ }
127
+ }
128
+ }
129
+ catch {
130
+ // best-effort — never block install on a scan failure
131
+ return;
132
+ }
133
+ if (hit) {
134
+ console.warn("\n ! Possible Beginner+Expert collision: a marketplace install of\n" +
135
+ " continuous-improvement looks present under ~/.claude/plugins/. Running the\n" +
136
+ " npx installer on top of it duplicates hooks/commands/skills in ~/.claude/.\n" +
137
+ " Pick ONE path — marketplace (/plugin install) or npx — not both.\n");
138
+ }
139
+ }
56
140
  const rawArgs = process.argv.slice(2);
57
141
  const modeIndex = rawArgs.indexOf("--mode");
58
142
  const requestedMode = modeIndex !== -1 ? rawArgs[modeIndex + 1] : undefined;
@@ -169,9 +253,10 @@ function setupMcpServer() {
169
253
  console.log(" ✓ Claude Desktop MCP config updated");
170
254
  }
171
255
  }
172
- function hookAlreadyPatched(entries, scriptName) {
173
- return entries.some((entry) => Array.isArray(entry.hooks) &&
174
- entry.hooks.some((hook) => hook.command.includes(scriptName)));
256
+ function hookAlreadyPatched(entries, expectedCommand) {
257
+ return entries.some((entry) => entry &&
258
+ Array.isArray(entry.hooks) &&
259
+ entry.hooks.some((hook) => typeof hook?.command === "string" && hook.command === expectedCommand));
175
260
  }
176
261
  function patchClaudeSettings(observePath) {
177
262
  const settingsPath = join(getHomeDir(), ".claude", "settings.json");
@@ -183,39 +268,75 @@ function patchClaudeSettings(observePath) {
183
268
  if (!settings.hooks) {
184
269
  settings.hooks = {};
185
270
  }
186
- const hookEntry = {
187
- matcher: "",
188
- hooks: [
189
- {
190
- type: "command",
191
- command: `bash "${observePath}"`,
192
- },
193
- ],
194
- };
271
+ // Strip broken legacy observe/session hooks at the hook level, not the entry
272
+ // level. A single entry may carry a foreign command alongside a broken hook;
273
+ // dropping the whole entry to remove the broken hook would also wipe the
274
+ // foreign one. Malformed rows (missing/non-array .hooks) are preserved verbatim
275
+ // — we don't know enough to filter them safely, and dropping risks data loss.
195
276
  let changed = false;
277
+ for (const hookType of ALL_HOOK_TYPES) {
278
+ const hookEntries = settings.hooks[hookType];
279
+ if (!Array.isArray(hookEntries))
280
+ continue;
281
+ let entryListChanged = false;
282
+ const cleanedEntries = [];
283
+ for (const entry of hookEntries) {
284
+ const entryHooks = entry?.hooks;
285
+ if (!Array.isArray(entryHooks)) {
286
+ cleanedEntries.push(entry);
287
+ continue;
288
+ }
289
+ const filteredHooks = entryHooks.filter((hook) => !isBrokenObserveOrSessionCommand(hook?.command));
290
+ if (filteredHooks.length === entryHooks.length) {
291
+ cleanedEntries.push(entry);
292
+ continue;
293
+ }
294
+ entryListChanged = true;
295
+ if (filteredHooks.length > 0) {
296
+ const cleanedEntry = {
297
+ ...entry,
298
+ hooks: filteredHooks,
299
+ };
300
+ cleanedEntries.push(cleanedEntry);
301
+ }
302
+ }
303
+ if (entryListChanged) {
304
+ settings.hooks[hookType] = cleanedEntries;
305
+ changed = true;
306
+ }
307
+ }
308
+ const observeCommand = `bash "${toBashPath(observePath)}"`;
196
309
  for (const hookType of HOOK_TYPES) {
197
310
  if (!Array.isArray(settings.hooks[hookType])) {
198
311
  settings.hooks[hookType] = [];
199
312
  }
200
313
  const hookEntries = settings.hooks[hookType];
201
- if (!hookAlreadyPatched(hookEntries, "observe.sh")) {
202
- hookEntries.push(hookEntry);
314
+ if (!hookAlreadyPatched(hookEntries, observeCommand)) {
315
+ hookEntries.push({
316
+ matcher: "",
317
+ hooks: [
318
+ {
319
+ type: "command",
320
+ command: observeCommand,
321
+ },
322
+ ],
323
+ });
203
324
  changed = true;
204
325
  }
205
326
  }
206
327
  if (INSTALL_MODE === "expert") {
207
328
  const sessionPath = join(getHomeDir(), ".claude", "instincts", "session.sh");
208
- const sessionHook = {
209
- matcher: "",
210
- hooks: [{ type: "command", command: `bash "${sessionPath}"` }],
211
- };
329
+ const sessionCommand = `bash "${toBashPath(sessionPath)}"`;
212
330
  for (const hookType of SESSION_HOOK_TYPES) {
213
331
  if (!Array.isArray(settings.hooks[hookType])) {
214
332
  settings.hooks[hookType] = [];
215
333
  }
216
334
  const hookEntries = settings.hooks[hookType];
217
- if (!hookAlreadyPatched(hookEntries, "session.sh")) {
218
- hookEntries.push(sessionHook);
335
+ if (!hookAlreadyPatched(hookEntries, sessionCommand)) {
336
+ hookEntries.push({
337
+ matcher: "",
338
+ hooks: [{ type: "command", command: sessionCommand }],
339
+ });
219
340
  changed = true;
220
341
  }
221
342
  }
@@ -292,16 +413,43 @@ function uninstallAll() {
292
413
  const settings = readJsonFile(settingsPath);
293
414
  if (settings) {
294
415
  let changed = false;
295
- for (const hookType of [...HOOK_TYPES, ...SESSION_HOOK_TYPES]) {
416
+ // Filter at the hook level, not the entry level. A foreign command may
417
+ // share an entry with one of ours; entry-level deletion would silently
418
+ // wipe the foreign hook. Malformed rows (missing/non-array .hooks) are
419
+ // preserved verbatim — we don't know enough to filter them safely.
420
+ // ALL_HOOK_TYPES dedupes any future overlap between HOOK_TYPES and
421
+ // SESSION_HOOK_TYPES so a bucket is never processed twice.
422
+ for (const hookType of ALL_HOOK_TYPES) {
296
423
  const hookEntries = settings.hooks?.[hookType];
297
424
  if (!Array.isArray(hookEntries)) {
298
425
  continue;
299
426
  }
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) {
427
+ let entryListChanged = false;
428
+ const cleanedEntries = [];
429
+ for (const entry of hookEntries) {
430
+ const entryHooks = entry?.hooks;
431
+ if (!Array.isArray(entryHooks)) {
432
+ cleanedEntries.push(entry);
433
+ continue;
434
+ }
435
+ const filteredHooks = entryHooks.filter((hook) => !isOurObserveOrSessionCommand(hook?.command));
436
+ if (filteredHooks.length === entryHooks.length) {
437
+ cleanedEntries.push(entry);
438
+ continue;
439
+ }
440
+ entryListChanged = true;
441
+ if (filteredHooks.length > 0) {
442
+ const cleanedEntry = {
443
+ ...entry,
444
+ hooks: filteredHooks,
445
+ };
446
+ cleanedEntries.push(cleanedEntry);
447
+ }
448
+ }
449
+ if (entryListChanged) {
450
+ settings.hooks[hookType] = cleanedEntries;
302
451
  changed = true;
303
452
  }
304
- settings.hooks[hookType] = nextEntries;
305
453
  }
306
454
  if (settings.mcpServers?.["continuous-improvement"]) {
307
455
  delete settings.mcpServers["continuous-improvement"];
@@ -403,11 +551,13 @@ console.log(`
403
551
  continuous-improvement (mode: ${INSTALL_MODE})
404
552
  Research → Plan → Execute → Verify → Reflect → Learn → Iterate
405
553
  `);
554
+ assertBashAvailableOnWindows();
555
+ warnOnMarketplaceCollision();
406
556
  console.log("Installing to Claude Code...\n");
407
557
  const installed = installSkill() ? 1 : 0;
408
558
  const modeInfo = {
409
559
  beginner: "Hooks are capturing silently. System auto-levels as you use it.",
410
- expert: "Full plugin active: hooks + MCP server + session hooks. 12 tools available.",
560
+ expert: `Full plugin active: hooks + MCP server + session hooks. ${getToolNames("expert").length} tools available.`,
411
561
  };
412
562
  const packIndex = rawArgs.indexOf("--pack");
413
563
  if (packIndex !== -1 && rawArgs[packIndex + 1]) {
@@ -417,7 +567,14 @@ if (packIndex !== -1 && rawArgs[packIndex + 1]) {
417
567
  const project = getProjectHashSync();
418
568
  const targetDir = join(getHomeDir(), ".claude", "instincts", project.hash);
419
569
  mkdirSync(targetDir, { recursive: true });
420
- const instincts = JSON.parse(readFileSync(packPath, "utf8"));
570
+ let instincts = [];
571
+ try {
572
+ instincts = JSON.parse(readFileSync(packPath, "utf8"));
573
+ }
574
+ catch (error) {
575
+ console.error(` ✗ Pack "${packName}" could not be loaded: ${getErrorMessage(error)}\n` +
576
+ ` ${packPath} is not valid JSON. No instincts were written — fix or re-fetch the pack file.`);
577
+ }
421
578
  let loaded = 0;
422
579
  for (const instinct of instincts) {
423
580
  const instinctPath = join(targetDir, `${instinct.id}.yaml`);
@@ -461,6 +618,6 @@ Next steps:
461
618
  3. If a task needs persistent planning, run: /planning-with-files
462
619
  4. After your first task, run: /continuous-improvement
463
620
  5. Try: /discipline for quick reference, /dashboard for instinct health
464
- ${INSTALL_MODE === "expert" ? "\nMCP tools available: ci_status, ci_instincts, ci_reflect, ci_reinforce,\n ci_create_instinct, ci_observations, ci_export, ci_import, ci_plan_init,\n ci_plan_status, ci_dashboard, ci_load_pack" : ""}
621
+ ${INSTALL_MODE === "expert" ? `\nMCP tools available (${getToolNames("expert").length}): ${getToolNames("expert").join(", ")}` : ""}
465
622
  Available instinct packs: npx continuous-improvement install --pack react|python|go|meta
466
623
  `);
@@ -3,7 +3,7 @@
3
3
  * continuous-improvement MCP Server
4
4
  *
5
5
  * Exposes instincts, observations, and reflection as MCP tools + resources.
6
- * Two modes: beginner (3 tools) and expert (all tools).
6
+ * Two modes: beginner (4 tools) and expert (all tools).
7
7
  *
8
8
  * Usage:
9
9
  * node bin/mcp-server.mjs # default: beginner mode
@@ -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");