continuous-improvement 3.22.1 → 3.24.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 +1 -1
- package/CHANGELOG.md +23 -0
- package/QUICKSTART.md +19 -20
- package/README.md +72 -19
- package/SKILL.md +4 -0
- package/bin/companion-preference-status.mjs +2 -5
- package/bin/generate-plugin-manifests.mjs +21 -2
- package/bin/harvest-friction.mjs +10 -8
- package/bin/install.mjs +208 -33
- package/bin/mcp-server.mjs +4 -9
- package/bin/observe.mjs +3 -3
- package/bin/reconcile-instinct-hashes.mjs +226 -0
- package/commands/discipline.md +5 -2
- package/commands/reconcile.md +1 -1
- package/commands/ship.md +5 -49
- package/commands/superpowers.md +1 -1
- package/commands/verify-install.md +8 -3
- package/hooks/companion-preference.mjs +2 -5
- package/hooks/config-guard.mjs +94 -0
- package/hooks/gateguard.mjs +39 -39
- package/hooks/goal-drift-stop.mjs +2 -2
- package/hooks/query-cost-nudge.mjs +2 -2
- package/hooks/recall-briefing.mjs +2 -2
- package/hooks/route-prompt.mjs +2 -5
- package/hooks/session.mjs +2 -2
- package/hooks/workflow-distill.mjs +2 -2
- package/lib/config-guard-gate.mjs +243 -0
- package/lib/destructive-bash.mjs +216 -0
- package/lib/gateguard-state.mjs +5 -1
- package/lib/plugin-metadata.mjs +12 -1
- package/lib/skill-catalog.mjs +169 -0
- package/llms.txt +11 -0
- package/package.json +1 -1
- package/plugins/beginner.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/README.md +1 -2
- package/plugins/continuous-improvement/bin/mcp-server.mjs +4 -9
- package/plugins/continuous-improvement/bin/observe.mjs +3 -3
- package/plugins/continuous-improvement/commands/discipline.md +5 -2
- package/plugins/continuous-improvement/commands/reconcile.md +1 -1
- package/plugins/continuous-improvement/commands/ship.md +5 -49
- package/plugins/continuous-improvement/commands/superpowers.md +1 -1
- package/plugins/continuous-improvement/commands/verify-install.md +8 -3
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +2 -5
- package/plugins/continuous-improvement/hooks/config-guard.mjs +94 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +39 -39
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +2 -2
- package/plugins/continuous-improvement/hooks/hooks.json +10 -0
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +2 -2
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +2 -2
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +2 -5
- package/plugins/continuous-improvement/hooks/session.mjs +2 -2
- package/plugins/continuous-improvement/hooks/workflow-distill.mjs +2 -2
- package/plugins/continuous-improvement/lib/config-guard-gate.mjs +243 -0
- package/plugins/continuous-improvement/lib/destructive-bash.mjs +216 -0
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +5 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +12 -1
- package/plugins/continuous-improvement/skills/README.md +1 -1
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +4 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +17 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/ship/SKILL.md +139 -0
- package/plugins/expert.json +1 -1
- package/skills/README.md +2 -2
- package/skills/deploy-receipt.md +1 -1
- package/skills/gateguard.md +17 -2
- package/skills/reconcile.md +0 -1
- package/skills/ship.md +139 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +0 -77
- package/skills/safety-guard.md +0 -77
package/bin/install.mjs
CHANGED
|
@@ -9,18 +9,18 @@
|
|
|
9
9
|
* npx continuous-improvement install --target gemini,codex # skill into other agents' rules files
|
|
10
10
|
* npx continuous-improvement install --uninstall # remove everything
|
|
11
11
|
*/
|
|
12
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
12
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
14
13
|
import { homedir } from "node:os";
|
|
15
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
16
15
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
16
|
import { PACKAGE_NAME, VERSION, getToolNames } from "../lib/plugin-metadata.mjs";
|
|
17
|
+
import { hashProjectRoot, resolveProjectRoot } from "../lib/gateguard-state.mjs";
|
|
19
18
|
import { TARGET_IDS, planTargetWrites, resolveTargets } from "../lib/install-targets.mjs";
|
|
20
19
|
import { evaluateUpdateCheck, fetchLatestNpmVersion, isThrottled, pendingNotice, } from "../lib/version-check.mjs";
|
|
21
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
21
|
const __dirname = dirname(__filename);
|
|
23
22
|
const SKILL_SOURCE = join(__dirname, "..", "SKILL.md");
|
|
23
|
+
const SHIP_SKILL_SOURCE = join(__dirname, "..", "skills", "ship.md");
|
|
24
24
|
const SKILL_NAME = "continuous-improvement";
|
|
25
25
|
const REPO_ROOT = join(__dirname, "..");
|
|
26
26
|
const COMMAND_FILES = [
|
|
@@ -124,17 +124,169 @@ const modeIndex = rawArgs.indexOf("--mode");
|
|
|
124
124
|
const requestedMode = modeIndex !== -1 ? rawArgs[modeIndex + 1] : undefined;
|
|
125
125
|
const INSTALL_MODE = isInstallMode(requestedMode) ? requestedMode : "beginner";
|
|
126
126
|
const SKILL_DIR = join(getHomeDir(), ".claude", "skills", SKILL_NAME);
|
|
127
|
+
const SHIP_SKILL_DIR = join(getHomeDir(), ".claude", "skills", "ship");
|
|
128
|
+
const SHIP_SKILLS_DIR = dirname(SHIP_SKILL_DIR);
|
|
129
|
+
const SHIP_STAGING_ROOT = join(getHomeDir(), ".claude", ".continuous-improvement-staging");
|
|
130
|
+
const SHIP_SKILL_OWNER_FILE = join(SHIP_SKILL_DIR, ".continuous-improvement-owner");
|
|
131
|
+
const SHIP_SKILL_OWNER = `${PACKAGE_NAME}\n`;
|
|
132
|
+
function pathEntryExists(path) {
|
|
133
|
+
try {
|
|
134
|
+
lstatSync(path);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === "ENOENT")
|
|
139
|
+
return false;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isOwnedShipSkill() {
|
|
144
|
+
try {
|
|
145
|
+
const dir = lstatSync(SHIP_SKILL_DIR);
|
|
146
|
+
const owner = lstatSync(SHIP_SKILL_OWNER_FILE);
|
|
147
|
+
if (!dir.isDirectory() || dir.isSymbolicLink())
|
|
148
|
+
return false;
|
|
149
|
+
if (!owner.isFile() || owner.isSymbolicLink() || owner.nlink !== 1)
|
|
150
|
+
return false;
|
|
151
|
+
return readFileSync(SHIP_SKILL_OWNER_FILE, "utf8") === SHIP_SKILL_OWNER;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function isSameOrChildPath(parent, candidate) {
|
|
158
|
+
const relativePath = relative(parent, candidate);
|
|
159
|
+
return (relativePath === "" ||
|
|
160
|
+
(relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)));
|
|
161
|
+
}
|
|
162
|
+
function getSafeShipStagingRoot() {
|
|
163
|
+
mkdirSync(dirname(SHIP_STAGING_ROOT), { recursive: true });
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(SHIP_STAGING_ROOT);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code !== "EEXIST")
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
const stagingEntry = lstatSync(SHIP_STAGING_ROOT);
|
|
172
|
+
if (!stagingEntry.isDirectory() || stagingEntry.isSymbolicLink()) {
|
|
173
|
+
throw new Error(`Ship staging root must be a plain directory, not a link or junction: ${SHIP_STAGING_ROOT}`);
|
|
174
|
+
}
|
|
175
|
+
const stagingRoot = realpathSync(SHIP_STAGING_ROOT);
|
|
176
|
+
const skillsRoot = realpathSync(SHIP_SKILLS_DIR);
|
|
177
|
+
if (isSameOrChildPath(skillsRoot, stagingRoot)) {
|
|
178
|
+
throw new Error(`Ship staging root resolves inside skill discovery: ${stagingRoot}`);
|
|
179
|
+
}
|
|
180
|
+
return { stagingRoot, skillsRoot };
|
|
181
|
+
}
|
|
182
|
+
function stageShipSkill() {
|
|
183
|
+
mkdirSync(SHIP_SKILLS_DIR, { recursive: true });
|
|
184
|
+
const { stagingRoot, skillsRoot } = getSafeShipStagingRoot();
|
|
185
|
+
const createdStagingDir = mkdtempSync(join(stagingRoot, "ship-"));
|
|
186
|
+
try {
|
|
187
|
+
const stagingEntry = lstatSync(createdStagingDir);
|
|
188
|
+
const stagingDir = realpathSync(createdStagingDir);
|
|
189
|
+
if (!stagingEntry.isDirectory() ||
|
|
190
|
+
stagingEntry.isSymbolicLink() ||
|
|
191
|
+
!isSameOrChildPath(stagingRoot, stagingDir) ||
|
|
192
|
+
isSameOrChildPath(skillsRoot, stagingDir)) {
|
|
193
|
+
throw new Error(`Ship staging directory escaped its safe root: ${stagingDir}`);
|
|
194
|
+
}
|
|
195
|
+
copyFileSync(SHIP_SKILL_SOURCE, join(stagingDir, "SKILL.md"));
|
|
196
|
+
writeFileSync(join(stagingDir, ".continuous-improvement-owner"), SHIP_SKILL_OWNER);
|
|
197
|
+
return stagingDir;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
rmSync(createdStagingDir, { recursive: true, force: true });
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function installShipSkill() {
|
|
205
|
+
const shipSkillExists = pathEntryExists(SHIP_SKILL_DIR);
|
|
206
|
+
if (shipSkillExists && !isOwnedShipSkill()) {
|
|
207
|
+
console.warn(` ! Preserved existing unowned ship skill at ${SHIP_SKILL_DIR}`);
|
|
208
|
+
return "preserved";
|
|
209
|
+
}
|
|
210
|
+
let stagingDir = "";
|
|
211
|
+
let preserveStagingDir = false;
|
|
212
|
+
try {
|
|
213
|
+
stagingDir = stageShipSkill();
|
|
214
|
+
if (shipSkillExists) {
|
|
215
|
+
const skillPath = join(SHIP_SKILL_DIR, "SKILL.md");
|
|
216
|
+
const previousSkillPath = join(stagingDir, "previous-SKILL.md");
|
|
217
|
+
let previousSkillMoved = false;
|
|
218
|
+
if (pathEntryExists(skillPath)) {
|
|
219
|
+
const skillEntry = lstatSync(skillPath);
|
|
220
|
+
if (skillEntry.isDirectory()) {
|
|
221
|
+
throw new Error(`Refusing to replace directory at ${skillPath}`);
|
|
222
|
+
}
|
|
223
|
+
renameSync(skillPath, previousSkillPath);
|
|
224
|
+
previousSkillMoved = true;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
renameSync(join(stagingDir, "SKILL.md"), skillPath);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (previousSkillMoved) {
|
|
231
|
+
try {
|
|
232
|
+
renameSync(previousSkillPath, skillPath);
|
|
233
|
+
}
|
|
234
|
+
catch (restoreError) {
|
|
235
|
+
preserveStagingDir = true;
|
|
236
|
+
let recoveryRenameError = "";
|
|
237
|
+
const stagedSkillPath = join(stagingDir, "SKILL.md");
|
|
238
|
+
if (pathEntryExists(stagedSkillPath)) {
|
|
239
|
+
try {
|
|
240
|
+
renameSync(stagedSkillPath, join(stagingDir, "new-SKILL.md"));
|
|
241
|
+
}
|
|
242
|
+
catch (recoveryError) {
|
|
243
|
+
recoveryRenameError = ` Recovery artifact rename also failed (${getErrorMessage(recoveryError)}).`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Ship skill replacement failed (${getErrorMessage(error)}) and rollback failed (${getErrorMessage(restoreError)}).${recoveryRenameError} Recovery files retained outside skill discovery at ${stagingDir}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
rmSync(stagingDir, { recursive: true });
|
|
252
|
+
stagingDir = "";
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
renameSync(stagingDir, SHIP_SKILL_DIR);
|
|
256
|
+
stagingDir = "";
|
|
257
|
+
}
|
|
258
|
+
console.log(` ✓ Global ship skill → ${SHIP_SKILL_DIR}/SKILL.md`);
|
|
259
|
+
return "installed";
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.error(` ✗ Global ship skill install failed: ${getErrorMessage(error)}`);
|
|
263
|
+
return "failed";
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (stagingDir && !preserveStagingDir) {
|
|
267
|
+
try {
|
|
268
|
+
if (pathEntryExists(stagingDir)) {
|
|
269
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (cleanupError) {
|
|
273
|
+
console.error(` ! Staged ship skill cleanup failed at ${stagingDir}: ${getErrorMessage(cleanupError)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
127
278
|
function installSkill() {
|
|
128
279
|
try {
|
|
129
280
|
mkdirSync(SKILL_DIR, { recursive: true });
|
|
130
281
|
copyFileSync(SKILL_SOURCE, join(SKILL_DIR, "SKILL.md"));
|
|
131
282
|
console.log(` ✓ Claude Code skill → ${SKILL_DIR}/SKILL.md`);
|
|
283
|
+
const shipOutcome = installShipSkill();
|
|
132
284
|
setupMulahazah();
|
|
133
|
-
return
|
|
285
|
+
return shipOutcome;
|
|
134
286
|
}
|
|
135
287
|
catch (error) {
|
|
136
288
|
console.error(` ✗ Install failed: ${getErrorMessage(error)}`);
|
|
137
|
-
return
|
|
289
|
+
return "failed";
|
|
138
290
|
}
|
|
139
291
|
}
|
|
140
292
|
function setupMulahazah() {
|
|
@@ -326,9 +478,10 @@ function patchClaudeSettings(observePath) {
|
|
|
326
478
|
}
|
|
327
479
|
}
|
|
328
480
|
function uninstallAll() {
|
|
329
|
-
console.log("\nUninstalling continuous-improvement
|
|
481
|
+
console.log("\nUninstalling continuous-improvement skills...\n");
|
|
330
482
|
const home = getHomeDir();
|
|
331
483
|
let removed = 0;
|
|
484
|
+
let failed = false;
|
|
332
485
|
if (existsSync(SKILL_DIR)) {
|
|
333
486
|
try {
|
|
334
487
|
rmSync(SKILL_DIR, { recursive: true });
|
|
@@ -337,6 +490,23 @@ function uninstallAll() {
|
|
|
337
490
|
}
|
|
338
491
|
catch (error) {
|
|
339
492
|
console.error(` ✗ Skill removal failed: ${getErrorMessage(error)}`);
|
|
493
|
+
failed = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (existsSync(SHIP_SKILL_DIR)) {
|
|
497
|
+
if (!isOwnedShipSkill()) {
|
|
498
|
+
console.warn(` ! Preserved existing unowned ship skill at ${SHIP_SKILL_DIR}`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
try {
|
|
502
|
+
rmSync(SHIP_SKILL_DIR, { recursive: true });
|
|
503
|
+
console.log(" ✓ Removed global ship skill");
|
|
504
|
+
removed++;
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
console.error(` ✗ Global ship skill removal failed: ${getErrorMessage(error)}`);
|
|
508
|
+
failed = true;
|
|
509
|
+
}
|
|
340
510
|
}
|
|
341
511
|
}
|
|
342
512
|
for (const commandName of COMMAND_FILES) {
|
|
@@ -350,6 +520,7 @@ function uninstallAll() {
|
|
|
350
520
|
}
|
|
351
521
|
catch (error) {
|
|
352
522
|
console.error(` ✗ ${commandName}: ${getErrorMessage(error)}`);
|
|
523
|
+
failed = true;
|
|
353
524
|
}
|
|
354
525
|
}
|
|
355
526
|
for (const hookFile of ["observe.sh", "session.sh", "session.mjs"]) {
|
|
@@ -363,6 +534,7 @@ function uninstallAll() {
|
|
|
363
534
|
}
|
|
364
535
|
catch (error) {
|
|
365
536
|
console.error(` ✗ ${hookFile}: ${getErrorMessage(error)}`);
|
|
537
|
+
failed = true;
|
|
366
538
|
}
|
|
367
539
|
}
|
|
368
540
|
// Remove the Node observer artifacts deployed alongside observe.sh.
|
|
@@ -379,6 +551,7 @@ function uninstallAll() {
|
|
|
379
551
|
}
|
|
380
552
|
catch (error) {
|
|
381
553
|
console.error(` ✗ ${observerFile}: ${getErrorMessage(error)}`);
|
|
554
|
+
failed = true;
|
|
382
555
|
}
|
|
383
556
|
}
|
|
384
557
|
const settingsPath = join(home, ".claude", "settings.json");
|
|
@@ -435,12 +608,17 @@ function uninstallAll() {
|
|
|
435
608
|
}
|
|
436
609
|
else {
|
|
437
610
|
console.warn(" ! Could not clean settings.json — remove hooks manually");
|
|
611
|
+
failed = true;
|
|
438
612
|
}
|
|
439
613
|
}
|
|
440
614
|
const desktopConfigPath = join(home, ".claude", "claude_desktop_config.json");
|
|
441
615
|
if (existsSync(desktopConfigPath)) {
|
|
442
616
|
const desktopConfig = readJsonFile(desktopConfigPath);
|
|
443
|
-
if (desktopConfig
|
|
617
|
+
if (!desktopConfig) {
|
|
618
|
+
console.warn(" ! Could not clean claude_desktop_config.json — remove the MCP server manually");
|
|
619
|
+
failed = true;
|
|
620
|
+
}
|
|
621
|
+
else if (desktopConfig.mcpServers?.["continuous-improvement"]) {
|
|
444
622
|
delete desktopConfig.mcpServers["continuous-improvement"];
|
|
445
623
|
writeFileSync(desktopConfigPath, JSON.stringify(desktopConfig, null, 2) + "\n");
|
|
446
624
|
console.log(" ✓ Removed MCP server from Claude Desktop config");
|
|
@@ -451,6 +629,7 @@ function uninstallAll() {
|
|
|
451
629
|
}
|
|
452
630
|
console.log("\n Note: Instinct data in ~/.claude/instincts/ was preserved.\n" +
|
|
453
631
|
" To remove learned data too: rm -rf ~/.claude/instincts/\n");
|
|
632
|
+
return !failed;
|
|
454
633
|
}
|
|
455
634
|
function printUsage() {
|
|
456
635
|
console.log(`
|
|
@@ -463,11 +642,11 @@ Subcommands:
|
|
|
463
642
|
|
|
464
643
|
Options for 'install':
|
|
465
644
|
--mode <mode> Installation mode:
|
|
466
|
-
beginner — hooks +
|
|
645
|
+
beginner — hooks + skills + commands (default)
|
|
467
646
|
expert — beginner + MCP server + session hooks
|
|
468
647
|
--pack <name> Load a starter instinct pack (react, python, go, meta)
|
|
469
648
|
--target <names> Comma-separated platform list (default: claude):
|
|
470
|
-
claude — full install: hooks +
|
|
649
|
+
claude — full install: hooks + skills + commands
|
|
471
650
|
gemini — GEMINI.md (Gemini CLI)
|
|
472
651
|
codex — AGENTS.md (Codex CLI / agents.md standard)
|
|
473
652
|
cursor — .cursor/rules/continuous-improvement.mdc
|
|
@@ -494,19 +673,10 @@ Examples:
|
|
|
494
673
|
`);
|
|
495
674
|
}
|
|
496
675
|
function getProjectHashSync() {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// file literally named /dev/null); ignore stderr via stdio instead.
|
|
500
|
-
const root = execSync("git rev-parse --show-toplevel", {
|
|
501
|
-
encoding: "utf8",
|
|
502
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
503
|
-
}).trim();
|
|
504
|
-
const hash = createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
505
|
-
return { root, hash };
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
676
|
+
const root = resolveProjectRoot();
|
|
677
|
+
if (root === "global")
|
|
508
678
|
return { root: "global", hash: "global" };
|
|
509
|
-
}
|
|
679
|
+
return { root, hash: hashProjectRoot(root) };
|
|
510
680
|
}
|
|
511
681
|
const args = process.argv.slice(2);
|
|
512
682
|
const command = args[0];
|
|
@@ -534,8 +704,7 @@ if (command === "backfill") {
|
|
|
534
704
|
process.exit(0);
|
|
535
705
|
}
|
|
536
706
|
if (args.includes("--uninstall")) {
|
|
537
|
-
uninstallAll();
|
|
538
|
-
process.exit(0);
|
|
707
|
+
process.exit(uninstallAll() ? 0 : 1);
|
|
539
708
|
}
|
|
540
709
|
// --target <names>: comma-separated platform list, default claude-only.
|
|
541
710
|
// Non-claude targets receive the skill text in their platform's rules file
|
|
@@ -597,7 +766,9 @@ Research → Plan → Execute → Verify → Reflect → Learn → Iterate
|
|
|
597
766
|
`);
|
|
598
767
|
warnOnMarketplaceCollision();
|
|
599
768
|
console.log("Installing to Claude Code...\n");
|
|
600
|
-
const
|
|
769
|
+
const installOutcome = installSkill();
|
|
770
|
+
if (installOutcome === "failed")
|
|
771
|
+
process.exitCode = 1;
|
|
601
772
|
const modeInfo = {
|
|
602
773
|
beginner: "Hooks are capturing silently. System auto-levels as you use it.",
|
|
603
774
|
expert: `Full plugin active: hooks + MCP server + session hooks. ${getToolNames("expert").length} tools available.`,
|
|
@@ -651,19 +822,23 @@ if (packIndex !== -1 && rawArgs[packIndex + 1]) {
|
|
|
651
822
|
console.error(` ✗ Unknown pack: ${packName}. Available: ${available.join(", ")}`);
|
|
652
823
|
}
|
|
653
824
|
}
|
|
654
|
-
|
|
655
|
-
${installed === 1 ? "Done." : "Failed."}
|
|
656
|
-
${modeInfo[INSTALL_MODE]}
|
|
825
|
+
const installedNextSteps = `${modeInfo[INSTALL_MODE]}
|
|
657
826
|
|
|
658
827
|
Next steps:
|
|
659
828
|
1. Start a new Claude Code session
|
|
660
829
|
2. Say: "Use the continuous-improvement framework to [your task]"
|
|
661
|
-
3.
|
|
662
|
-
4.
|
|
663
|
-
5.
|
|
830
|
+
3. For one defect, run: /ship [one-line defect description]
|
|
831
|
+
4. If a task needs persistent planning, run: /planning-with-files
|
|
832
|
+
5. After your first task, run: /continuous-improvement
|
|
833
|
+
6. Try: /discipline for quick reference, /dashboard for instinct health
|
|
664
834
|
${INSTALL_MODE === "expert" ? `\nMCP tools available (${getToolNames("expert").length}): ${getToolNames("expert").join(", ")}` : ""}
|
|
665
|
-
Available instinct packs: npx continuous-improvement install --pack react|python|go|meta
|
|
666
|
-
|
|
835
|
+
Available instinct packs: npx continuous-improvement install --pack react|python|go|meta`;
|
|
836
|
+
const installSummary = installOutcome === "installed"
|
|
837
|
+
? `Done.\n${installedNextSteps}`
|
|
838
|
+
: installOutcome === "preserved"
|
|
839
|
+
? `Done with warning.\n${modeInfo[INSTALL_MODE]}\n\nPackage /ship was not installed because an existing unowned ship skill was preserved. Resolve that collision, then reinstall before using the package workflow.`
|
|
840
|
+
: "Failed.\nInstallation incomplete. Fix the errors above and rerun the installer. Do not assume hooks, skills, or commands are ready.";
|
|
841
|
+
console.log(`\n${installSummary}\n`);
|
|
667
842
|
// Update-available nudge for the npm/CLI install path (the marketplace path is
|
|
668
843
|
// covered by Claude Code's native plugin auto-update). Reads the public npm
|
|
669
844
|
// registry only — no telemetry. Throttled, default-on, off via
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -16,12 +16,11 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { basename, dirname, join, resolve } from "node:path";
|
|
17
17
|
import { createInterface } from "node:readline";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
19
|
import { PACKAGE_NAME, VERSION, getToolDefinitions, isPluginMode, } from "../lib/plugin-metadata.mjs";
|
|
21
20
|
import { formatDriftReport, parseGoalFromPlan, scoreObservations, } from "../lib/goal-state.mjs";
|
|
22
21
|
import { buildIndex, formatRecallHits, parseSince, query as queryRecall, } from "../lib/recall-index.mjs";
|
|
23
22
|
import { draftFromCandidate, draftFromWorkflowRun, extractTrajectories, findCandidates, formatCandidates, serializeDraft, workflowRunFromObservations, } from "../lib/skill-distill.mjs";
|
|
24
|
-
import { MAX_CLEARED_FILES, canonicalizeFileKey, clearFiles, resolveInstinctsRoot, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
23
|
+
import { MAX_CLEARED_FILES, canonicalizeFileKey, clearFiles, hashProjectRoot, resolveInstinctsRoot, resolveProjectRoot, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
25
24
|
function getHomeDir() {
|
|
26
25
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
27
26
|
}
|
|
@@ -93,14 +92,10 @@ const modeIndex = args.indexOf("--mode");
|
|
|
93
92
|
const requestedMode = args[modeIndex + 1];
|
|
94
93
|
const MODE = isPluginMode(requestedMode) ? requestedMode : "beginner";
|
|
95
94
|
function getProjectHash() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const hash = createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
99
|
-
return { root, hash, name: basename(root) };
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
95
|
+
const root = resolveProjectRoot();
|
|
96
|
+
if (root === "global")
|
|
102
97
|
return { root: "global", hash: "global", name: "global" };
|
|
103
|
-
}
|
|
98
|
+
return { root, hash: hashProjectRoot(root), name: basename(root) };
|
|
104
99
|
}
|
|
105
100
|
function readInstincts(projectHash) {
|
|
106
101
|
const instincts = [];
|
package/bin/observe.mjs
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
//
|
|
17
17
|
// See docs/plans/2026-05-05-node-observer-rich-schema.md for the full design.
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
19
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
20
|
import { homedir } from "node:os";
|
|
22
21
|
import { basename, join } from "node:path";
|
|
22
|
+
import { canonicalizeProjectRoot, hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
23
23
|
import { parseHookPayload, summariseInput, summariseOutput } from "../lib/observe-event.mjs";
|
|
24
24
|
const ROTATION_LINE_THRESHOLD = 10_000;
|
|
25
25
|
const ARCHIVE_RETENTION = 10;
|
|
@@ -40,8 +40,8 @@ function runObserver() {
|
|
|
40
40
|
const payload = parseHookPayload(raw);
|
|
41
41
|
if (!payload)
|
|
42
42
|
return;
|
|
43
|
-
const projectRoot = resolveProjectRoot();
|
|
44
|
-
const projectHash =
|
|
43
|
+
const projectRoot = canonicalizeProjectRoot(resolveProjectRoot());
|
|
44
|
+
const projectHash = hashProjectRoot(projectRoot);
|
|
45
45
|
const projectName = basename(projectRoot.replace(/\.git$/, ""));
|
|
46
46
|
const instinctsDir = join(getHomeDir(), ".claude", "instincts");
|
|
47
47
|
const projectDir = join(instinctsDir, projectHash);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Merge instinct buckets that are the same project under different path
|
|
4
|
+
* spellings (C:/ vs c:/ vs C:\). observe/gateguard now hash the canonical
|
|
5
|
+
* root; this CLI copies history into that hash and leaves an alias marker
|
|
6
|
+
* on the old dir. Idempotent. Never deletes a directory.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/reconcile-instinct-hashes.mjs --dry-run
|
|
10
|
+
* node bin/reconcile-instinct-hashes.mjs --apply
|
|
11
|
+
* node bin/reconcile-instinct-hashes.mjs --apply --only 3ef4426c6e15 --only 137f2f54ec70
|
|
12
|
+
*/
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { argv, exit } from "node:process";
|
|
16
|
+
import { canonicalizeProjectRoot, hashProjectRoot, resolveInstinctsRoot } from "../lib/gateguard-state.mjs";
|
|
17
|
+
function countJsonl(path) {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return 0;
|
|
20
|
+
return readFileSync(path, "utf8").split(/\n/).filter((line) => line.trim() !== "").length;
|
|
21
|
+
}
|
|
22
|
+
function isoNow() {
|
|
23
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
24
|
+
}
|
|
25
|
+
export function discoverGroups(instinctsRoot) {
|
|
26
|
+
const groups = new Map();
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(instinctsRoot);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
for (const name of entries) {
|
|
35
|
+
if (name === "global")
|
|
36
|
+
continue;
|
|
37
|
+
const dir = join(instinctsRoot, name);
|
|
38
|
+
try {
|
|
39
|
+
if (!statSync(dir).isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (existsSync(join(dir, "alias.json")) && !existsSync(join(dir, "observations.jsonl"))) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const projectPath = join(dir, "project.json");
|
|
49
|
+
if (!existsSync(projectPath))
|
|
50
|
+
continue;
|
|
51
|
+
let project;
|
|
52
|
+
try {
|
|
53
|
+
project = JSON.parse(readFileSync(projectPath, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!project.root)
|
|
59
|
+
continue;
|
|
60
|
+
const canonicalRoot = canonicalizeProjectRoot(project.root);
|
|
61
|
+
const canonicalHash = hashProjectRoot(canonicalRoot);
|
|
62
|
+
const bucket = {
|
|
63
|
+
hash: name,
|
|
64
|
+
root: project.root,
|
|
65
|
+
name: project.name ?? name,
|
|
66
|
+
rows: countJsonl(join(dir, "observations.jsonl")),
|
|
67
|
+
};
|
|
68
|
+
const existing = groups.get(canonicalHash);
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.members.push(bucket);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
groups.set(canonicalHash, {
|
|
74
|
+
canonicalHash,
|
|
75
|
+
canonicalRoot,
|
|
76
|
+
name: project.name ?? name,
|
|
77
|
+
members: [bucket],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...groups.values()].filter((group) => group.members.some((member) => member.hash !== group.canonicalHash && member.rows > 0));
|
|
82
|
+
}
|
|
83
|
+
function parseTs(line) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(line);
|
|
86
|
+
const value = Date.parse(parsed.ts ?? "");
|
|
87
|
+
return Number.isFinite(value) ? value : 0;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function mergeJsonl(sources) {
|
|
94
|
+
const rows = [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const file of sources) {
|
|
97
|
+
if (!existsSync(file))
|
|
98
|
+
continue;
|
|
99
|
+
for (const line of readFileSync(file, "utf8").split(/\n/)) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed || seen.has(trimmed))
|
|
102
|
+
continue;
|
|
103
|
+
seen.add(trimmed);
|
|
104
|
+
rows.push(trimmed);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
rows.sort((a, b) => parseTs(a) - parseTs(b) || a.localeCompare(b));
|
|
108
|
+
return rows.length === 0 ? "" : `${rows.join("\n")}\n`;
|
|
109
|
+
}
|
|
110
|
+
export function selectGroups(groups, only) {
|
|
111
|
+
if (only.length === 0)
|
|
112
|
+
return groups;
|
|
113
|
+
const wanted = new Set(only);
|
|
114
|
+
return groups.filter((group) => wanted.has(group.canonicalHash) || group.members.some((member) => wanted.has(member.hash)));
|
|
115
|
+
}
|
|
116
|
+
export function applyGroup(instinctsRoot, group) {
|
|
117
|
+
const destDir = join(instinctsRoot, group.canonicalHash);
|
|
118
|
+
mkdirSync(destDir, { recursive: true });
|
|
119
|
+
const destObs = join(destDir, "observations.jsonl");
|
|
120
|
+
const sources = group.members
|
|
121
|
+
.map((member) => join(instinctsRoot, member.hash, "observations.jsonl"))
|
|
122
|
+
.filter((path) => existsSync(path));
|
|
123
|
+
const merged = mergeJsonl(sources);
|
|
124
|
+
writeFileSync(destObs, merged, "utf8");
|
|
125
|
+
let createdAt = isoNow();
|
|
126
|
+
const destProject = join(destDir, "project.json");
|
|
127
|
+
if (existsSync(destProject)) {
|
|
128
|
+
try {
|
|
129
|
+
const existing = JSON.parse(readFileSync(destProject, "utf8"));
|
|
130
|
+
if (existing.created_at)
|
|
131
|
+
createdAt = existing.created_at;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// rewrite below
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
writeFileSync(destProject, `${JSON.stringify({
|
|
138
|
+
id: group.canonicalHash,
|
|
139
|
+
name: group.name,
|
|
140
|
+
root: group.canonicalRoot,
|
|
141
|
+
created_at: createdAt,
|
|
142
|
+
})}\n`, "utf8");
|
|
143
|
+
const aliases = [];
|
|
144
|
+
for (const member of group.members) {
|
|
145
|
+
if (member.hash === group.canonicalHash)
|
|
146
|
+
continue;
|
|
147
|
+
const srcDir = join(instinctsRoot, member.hash);
|
|
148
|
+
try {
|
|
149
|
+
for (const file of readdirSync(srcDir)) {
|
|
150
|
+
if (!file.endsWith(".yaml"))
|
|
151
|
+
continue;
|
|
152
|
+
const dest = join(destDir, file);
|
|
153
|
+
if (!existsSync(dest))
|
|
154
|
+
copyFileSync(join(srcDir, file), dest);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// missing dir is non-fatal
|
|
159
|
+
}
|
|
160
|
+
const srcObs = join(srcDir, "observations.jsonl");
|
|
161
|
+
if (existsSync(srcObs)) {
|
|
162
|
+
renameSync(srcObs, join(srcDir, `observations.migrated-to-${group.canonicalHash}.jsonl`));
|
|
163
|
+
}
|
|
164
|
+
writeFileSync(join(srcDir, "alias.json"), `${JSON.stringify({
|
|
165
|
+
canonical: group.canonicalHash,
|
|
166
|
+
canonical_root: group.canonicalRoot,
|
|
167
|
+
migrated_at: isoNow(),
|
|
168
|
+
rows: member.rows,
|
|
169
|
+
})}\n`, "utf8");
|
|
170
|
+
aliases.push(member.hash);
|
|
171
|
+
}
|
|
172
|
+
const copiedRows = merged === "" ? 0 : merged.trim().split("\n").length;
|
|
173
|
+
return { copiedRows, aliases };
|
|
174
|
+
}
|
|
175
|
+
function printPlan(groups) {
|
|
176
|
+
if (groups.length === 0) {
|
|
177
|
+
console.log("No alias observation buckets to merge.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(`Alias groups: ${groups.length}`);
|
|
181
|
+
for (const group of groups) {
|
|
182
|
+
console.log(` ${group.name} → ${group.canonicalHash} (${group.canonicalRoot})`);
|
|
183
|
+
for (const member of group.members) {
|
|
184
|
+
const mark = member.hash === group.canonicalHash ? "canonical" : "alias";
|
|
185
|
+
console.log(` ${member.hash} ${member.rows} rows ${mark} root=${member.root}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function parseOnly(args) {
|
|
190
|
+
const only = [];
|
|
191
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
192
|
+
if (args[i] === "--only" && args[i + 1]) {
|
|
193
|
+
only.push(args[i + 1]);
|
|
194
|
+
i += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return only;
|
|
198
|
+
}
|
|
199
|
+
function main() {
|
|
200
|
+
const args = argv.slice(2);
|
|
201
|
+
const apply = args.includes("--apply");
|
|
202
|
+
const dryRun = args.includes("--dry-run") || !apply;
|
|
203
|
+
const instinctsRoot = resolveInstinctsRoot();
|
|
204
|
+
const groups = selectGroups(discoverGroups(instinctsRoot), parseOnly(args));
|
|
205
|
+
printPlan(groups);
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
if (groups.length > 0)
|
|
208
|
+
console.log("\nRe-run with --apply to merge.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const group of groups) {
|
|
212
|
+
const result = applyGroup(instinctsRoot, group);
|
|
213
|
+
console.log(`merged ${result.copiedRows} rows into ${group.canonicalHash}; aliases ${result.aliases.join(",") || "(none)"}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const invokedDirectly = argv[1]?.endsWith("reconcile-instinct-hashes.mjs");
|
|
217
|
+
if (invokedDirectly) {
|
|
218
|
+
try {
|
|
219
|
+
main();
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
console.error(message);
|
|
224
|
+
exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/commands/discipline.md
CHANGED
|
@@ -19,6 +19,8 @@ Print this card and check yourself against each law.
|
|
|
19
19
|
| 6 | **Iterate One Change** | Am I changing one thing at a time? | "And also..." |
|
|
20
20
|
| 7 | **Learn From Every Session** | Did I capture this as an instinct? | "Next time I'll..." |
|
|
21
21
|
|
|
22
|
+
Read the seven as three moments around every act. **Before** (Laws 1, 2): set the terms. **During** (Laws 3, 6): watch yourself. **After** (Laws 4, 5, 7): settle the account and carry it forward. Every check is an audit you run on yourself; every red flag is you hoping instead. The sentence this comes from, and its sources: `docs/philosophy.md`.
|
|
23
|
+
|
|
22
24
|
## Operator Stakes
|
|
23
25
|
|
|
24
26
|
The Laws above are the *how*. These five principles are the *why*: code ships from your account, the incident lands on your pager, the bill hits your budget. Each one pairs with the Law that prevents it from going wrong.
|
|
@@ -31,7 +33,7 @@ The Laws above are the *how*. These five principles are the *why*: code ships fr
|
|
|
31
33
|
| 4 | **Problem framing** | Builds the websocket chat the ticket asked for | Finds out users wanted faster support replies, not chat | 1 |
|
|
32
34
|
| 5 | **Constraints management** | Calls the $0.02/image model on every upload | Does the math, adds client-side validation + caching + cheaper triage model | 2 |
|
|
33
35
|
|
|
34
|
-
Code is a liability, not an asset. Speed without these five turns into someone else's incident at 3am — except the someone is you.
|
|
36
|
+
Code is a liability, not an asset. Speed without these five turns into someone else's incident at 3am — except the someone is you. The other half of the why is not fear: the session ends and the context is gone, so the only work that survives is what you verified and wrote down for the one who comes after, whether that is tomorrow's session or the engineer who inherits the repo.
|
|
35
37
|
|
|
36
38
|
## Goal-Driven Execution maps onto the Laws
|
|
37
39
|
|
|
@@ -61,5 +63,6 @@ Before saying "Done", verify ALL:
|
|
|
61
63
|
- [ ] I checked the **actual** result (not assumed)
|
|
62
64
|
- [ ] Build passes
|
|
63
65
|
- [ ] I can explain the change in one sentence
|
|
66
|
+
- [ ] For each item above I checked, not hoped
|
|
64
67
|
|
|
65
|
-
If you're skipping a step, that's the step you need most.
|
|
68
|
+
If you're skipping a step, that's the step you need most. The step you skip is the one you are hoping through.
|