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.
- package/.claude-plugin/marketplace.json +3 -3
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -21
- package/QUICKSTART.md +13 -8
- package/README.md +94 -162
- package/SKILL.md +1 -1
- package/bin/analyze.sh +161 -161
- package/bin/backfill.mjs +19 -19
- package/bin/check-docs-substrings.mjs +73 -0
- package/bin/check-scripts-citation-drift.mjs +210 -0
- package/bin/check-skill-count.mjs +110 -0
- package/bin/check-test-imports-only.mjs +126 -0
- package/bin/check-tool-count.mjs +129 -0
- package/bin/companion-preference-status.mjs +231 -0
- package/bin/gateguard-clear.mjs +53 -0
- package/bin/generate-plugin-manifests.mjs +8 -1
- package/bin/harvest-friction.mjs +1 -1
- package/bin/hook-stats.mjs +21 -21
- package/bin/install.mjs +185 -28
- package/bin/mcp-server.mjs +216 -3
- package/bin/refresh-third-party.mjs +315 -313
- package/commands/audit.md +34 -0
- package/commands/companion-preference.md +58 -0
- package/commands/continuous-improvement.md +115 -115
- package/commands/dashboard.md +56 -56
- package/commands/discipline.md +14 -0
- package/commands/distill.md +39 -0
- package/commands/goal-check.md +53 -0
- package/commands/grill-me.md +40 -0
- package/commands/grill-with-docs.md +38 -0
- package/commands/handoff.md +42 -0
- package/commands/harvest.md +1 -1
- package/commands/proceed-with-the-recommendation.md +20 -0
- package/commands/recall.md +49 -0
- package/commands/reconcile.md +47 -0
- package/commands/seven-laws.md +16 -16
- package/commands/superpowers.md +3 -3
- package/commands/verify-install.md +55 -0
- package/commands/workspace-surface-audit.md +77 -77
- package/hooks/companion-preference.mjs +190 -0
- package/hooks/gateguard.mjs +72 -25
- package/hooks/goal-drift-stop.mjs +183 -0
- package/hooks/observe.sh +15 -1
- package/hooks/recall-briefing.mjs +167 -0
- package/hooks/route-prompt.mjs +180 -0
- package/hooks/route-table.json +35 -0
- package/hooks/session.sh +106 -106
- package/hooks/three-section-close.mjs +134 -132
- package/instinct-packs/go.json +58 -58
- package/instinct-packs/meta.json +16 -16
- package/instinct-packs/python.json +58 -58
- package/instinct-packs/react.json +58 -58
- package/lib/gateguard-state.mjs +54 -2
- package/lib/goal-drift-gate.mjs +50 -0
- package/lib/goal-state.mjs +285 -0
- package/lib/plugin-metadata.mjs +134 -15
- package/lib/recall-briefing.mjs +57 -0
- package/lib/recall-index.mjs +175 -0
- package/lib/skill-distill.mjs +222 -0
- package/llms.txt +2 -2
- package/package.json +12 -7
- package/plugins/beginner.json +9 -4
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/LICENSE +21 -21
- package/plugins/continuous-improvement/agents/README.md +3 -3
- package/plugins/continuous-improvement/bin/backfill.mjs +19 -19
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +216 -3
- package/plugins/continuous-improvement/commands/audit.md +34 -0
- package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
- package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -115
- package/plugins/continuous-improvement/commands/dashboard.md +56 -56
- package/plugins/continuous-improvement/commands/discipline.md +14 -0
- package/plugins/continuous-improvement/commands/distill.md +39 -0
- package/plugins/continuous-improvement/commands/goal-check.md +53 -0
- package/plugins/continuous-improvement/commands/grill-me.md +40 -0
- package/plugins/continuous-improvement/commands/grill-with-docs.md +38 -0
- package/plugins/continuous-improvement/commands/handoff.md +42 -0
- package/plugins/continuous-improvement/commands/harvest.md +1 -1
- package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +20 -0
- package/plugins/continuous-improvement/commands/recall.md +49 -0
- package/plugins/continuous-improvement/commands/reconcile.md +47 -0
- package/plugins/continuous-improvement/commands/seven-laws.md +16 -16
- package/plugins/continuous-improvement/commands/superpowers.md +3 -3
- package/plugins/continuous-improvement/commands/verify-install.md +55 -0
- package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -77
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +72 -25
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
- package/plugins/continuous-improvement/hooks/hooks.json +23 -2
- package/plugins/continuous-improvement/hooks/observe.sh +15 -1
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +167 -0
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +180 -0
- package/plugins/continuous-improvement/hooks/route-table.json +35 -0
- package/plugins/continuous-improvement/hooks/session.sh +106 -106
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +134 -132
- package/plugins/continuous-improvement/instinct-packs/go.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/meta.json +16 -16
- package/plugins/continuous-improvement/instinct-packs/python.json +58 -58
- package/plugins/continuous-improvement/instinct-packs/react.json +58 -58
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +137 -0
- package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +50 -0
- package/plugins/continuous-improvement/lib/goal-state.mjs +285 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +134 -15
- package/plugins/continuous-improvement/lib/recall-briefing.mjs +57 -0
- package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
- package/plugins/continuous-improvement/lib/skill-distill.mjs +222 -0
- package/plugins/continuous-improvement/skills/README.md +8 -0
- package/plugins/continuous-improvement/skills/audit/SKILL.md +73 -0
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +13 -17
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +36 -17
- package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +81 -0
- package/plugins/continuous-improvement/skills/grill-me/SKILL.md +66 -0
- package/plugins/continuous-improvement/skills/grill-with-docs/SKILL.md +252 -0
- package/plugins/continuous-improvement/skills/handoff/SKILL.md +59 -0
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +82 -6
- package/plugins/continuous-improvement/skills/ralph/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/recall/SKILL.md +50 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +80 -0
- package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/skill-distillation/SKILL.md +57 -0
- package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +77 -3
- package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -411
- package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +28 -20
- package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +3 -2
- package/plugins/expert.json +28 -3
- package/skills/README.md +3 -0
- package/skills/audit.md +73 -0
- package/skills/deploy-receipt.md +13 -17
- package/skills/gateguard.md +36 -17
- package/skills/goal-monitor.md +81 -0
- package/skills/grill-me.md +66 -0
- package/skills/grill-with-docs.md +252 -0
- package/skills/handoff.md +59 -0
- package/skills/para-memory-files.md +1 -1
- package/skills/proceed-with-the-recommendation.md +82 -6
- package/skills/ralph.md +1 -0
- package/skills/recall.md +50 -0
- package/skills/reconcile.md +80 -0
- package/skills/recovery-classification.md +1 -0
- package/skills/safety-guard.md +1 -0
- package/skills/skill-distillation.md +57 -0
- package/skills/state-reconciliation.md +1 -0
- package/skills/strategic-compact.md +1 -0
- package/skills/superpowers.md +77 -3
- package/skills/tdd-workflow.md +411 -411
- package/skills/token-budget-advisor.md +1 -1
- package/skills/verification-loop.md +28 -20
- package/skills/wild-risa-balance.md +1 -0
- package/skills/workspace-surface-audit.md +1 -1
- 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,
|
|
173
|
-
return entries.some((entry) =>
|
|
174
|
-
entry.hooks
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
202
|
-
hookEntries.push(
|
|
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
|
|
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,
|
|
218
|
-
hookEntries.push(
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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:
|
|
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
|
-
|
|
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" ?
|
|
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
|
`);
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -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 (
|
|
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(-
|
|
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");
|