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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +61 -0
- package/QUICKSTART.md +13 -8
- package/README.md +84 -153
- package/bin/backfill.mjs +0 -0
- package/bin/check-doc-runtime-claims.mjs +0 -0
- package/bin/check-docs-substrings.mjs +73 -0
- package/bin/check-everything-mirror.mjs +0 -0
- package/bin/check-routing-targets.mjs +0 -0
- package/bin/check-scripts-citation-drift.mjs +210 -0
- package/bin/check-skill-count.mjs +110 -0
- package/bin/check-skill-law-tag.mjs +0 -0
- package/bin/check-skill-mirror.mjs +0 -0
- package/bin/check-skill-tiers.mjs +0 -0
- package/bin/check-test-imports-only.mjs +126 -0
- package/bin/check-third-party-shape.mjs +0 -0
- package/bin/companion-preference-status.mjs +231 -0
- package/bin/gateguard-clear.mjs +53 -0
- package/bin/generate-plugin-manifests.mjs +7 -1
- package/bin/harvest-friction.mjs +0 -0
- package/bin/hook-stats.mjs +0 -0
- package/bin/install.mjs +182 -26
- package/bin/lint-transcript.mjs +0 -0
- package/bin/mcp-server.mjs +215 -2
- package/bin/observe.mjs +0 -0
- package/bin/pre-commit-block-strays.sh +0 -0
- package/bin/refresh-third-party.mjs +315 -313
- package/bin/unified-cli.mjs +0 -0
- package/commands/audit.md +34 -0
- package/commands/companion-preference.md +58 -0
- 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/proceed-with-the-recommendation.md +20 -0
- package/commands/recall.md +49 -0
- package/commands/reconcile.md +47 -0
- package/commands/superpowers.md +2 -2
- package/commands/verify-install.md +55 -0
- package/hooks/companion-preference.mjs +190 -0
- package/hooks/gateguard.mjs +60 -25
- package/hooks/goal-drift-stop.mjs +183 -0
- package/hooks/observe.sh +15 -1
- package/hooks/route-prompt.mjs +180 -0
- package/hooks/route-table.json +35 -0
- package/hooks/three-section-close.mjs +134 -132
- package/lib/cli-anything.mjs +0 -0
- package/lib/compound-engineering.mjs +0 -0
- package/lib/gateguard-state.mjs +54 -2
- package/lib/goal-drift-gate.mjs +50 -0
- package/lib/goal-state.mjs +285 -0
- package/lib/observe-event.mjs +0 -0
- package/lib/plugin-metadata.mjs +126 -12
- package/lib/pm-marketplace.mjs +0 -0
- package/lib/pm-skills.mjs +0 -0
- package/lib/recall-index.mjs +175 -0
- package/lib/resolve-home-dir.mjs +0 -0
- package/lib/skill-distill.mjs +222 -0
- package/lib/skill-tiers.mjs +0 -0
- package/lib/unified-plugin.mjs +0 -0
- package/llms.txt +1 -1
- package/package.json +11 -7
- package/plugins/beginner.json +8 -3
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/agents/README.md +2 -2
- package/plugins/continuous-improvement/bin/backfill.mjs +0 -0
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +215 -2
- package/plugins/continuous-improvement/bin/observe.mjs +0 -0
- package/plugins/continuous-improvement/commands/audit.md +34 -0
- package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
- 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/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/superpowers.md +2 -2
- package/plugins/continuous-improvement/commands/verify-install.md +55 -0
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +60 -25
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
- package/plugins/continuous-improvement/hooks/hooks.json +18 -2
- package/plugins/continuous-improvement/hooks/observe.sh +15 -1
- 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/three-section-close.mjs +134 -132
- 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/observe-event.mjs +0 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +126 -12
- package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -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/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 +76 -2
- 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 +27 -2
- 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 +76 -2
- 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
|
@@ -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) =>
|
|
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
|
]);
|
package/bin/harvest-friction.mjs
CHANGED
|
File without changes
|
package/bin/hook-stats.mjs
CHANGED
|
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,
|
|
173
|
-
return entries.some((entry) =>
|
|
174
|
-
entry.hooks
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
202
|
-
hookEntries.push(
|
|
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
|
|
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,
|
|
218
|
-
hookEntries.push(
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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`);
|
package/bin/lint-transcript.mjs
CHANGED
|
File without changes
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -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");
|
package/bin/observe.mjs
CHANGED
|
File without changes
|
|
File without changes
|