continuous-improvement 2.2.0 → 3.1.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/CHANGELOG.md +36 -0
- package/README.md +367 -32
- package/action.yml +33 -0
- package/bin/analyze.sh +27 -41
- package/bin/install.mjs +267 -40
- package/bin/lint-transcript.mjs +267 -0
- package/bin/mcp-server.mjs +663 -0
- package/commands/dashboard.md +56 -0
- package/commands/discipline.md +37 -0
- package/hooks/session.sh +106 -0
- package/instinct-packs/go.json +58 -0
- package/instinct-packs/python.json +58 -0
- package/instinct-packs/react.json +58 -0
- package/llms.txt +43 -0
- package/package.json +33 -10
- package/plugins/beginner.json +42 -0
- package/plugins/expert.json +70 -0
package/bin/install.mjs
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* continuous-improvement installer
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx continuous-improvement install
|
|
8
|
-
* npx continuous-improvement install --target claude
|
|
9
|
-
* npx continuous-improvement install --target openclaw
|
|
10
|
-
* npx continuous-improvement install --target cursor
|
|
11
|
-
* npx continuous-improvement install --target all
|
|
12
|
-
* npx continuous-improvement install --
|
|
7
|
+
* npx continuous-improvement install # auto-detect & install (beginner)
|
|
8
|
+
* npx continuous-improvement install --target claude # install to ~/.claude/skills/ + Mulahazah
|
|
9
|
+
* npx continuous-improvement install --target openclaw # install to ~/.openclaw/skills/
|
|
10
|
+
* npx continuous-improvement install --target cursor # install to ~/.cursor/skills/
|
|
11
|
+
* npx continuous-improvement install --target all # install to all detected targets
|
|
12
|
+
* npx continuous-improvement install --mode beginner # hooks only (default)
|
|
13
|
+
* npx continuous-improvement install --mode expert # hooks + MCP server + session hooks
|
|
14
|
+
* npx continuous-improvement install --mode mcp # MCP server only (any editor)
|
|
15
|
+
* npx continuous-improvement install --uninstall # remove from all targets
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import {
|
|
@@ -20,10 +23,12 @@ import {
|
|
|
20
23
|
writeFileSync,
|
|
21
24
|
rmSync,
|
|
22
25
|
chmodSync,
|
|
26
|
+
readdirSync,
|
|
23
27
|
} from "node:fs";
|
|
24
28
|
import { join, dirname } from "node:path";
|
|
25
29
|
import { homedir } from "node:os";
|
|
26
30
|
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { execSync } from "node:child_process";
|
|
27
32
|
|
|
28
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
34
|
const __dirname = dirname(__filename);
|
|
@@ -31,6 +36,11 @@ const SKILL_SOURCE = join(__dirname, "..", "SKILL.md");
|
|
|
31
36
|
const SKILL_NAME = "continuous-improvement";
|
|
32
37
|
const REPO_ROOT = join(__dirname, "..");
|
|
33
38
|
|
|
39
|
+
// Parse --mode flag
|
|
40
|
+
const _args = process.argv.slice(2);
|
|
41
|
+
const _modeIdx = _args.indexOf("--mode");
|
|
42
|
+
const INSTALL_MODE = _modeIdx !== -1 && _args[_modeIdx + 1] ? _args[_modeIdx + 1] : "beginner";
|
|
43
|
+
|
|
34
44
|
const TARGETS = {
|
|
35
45
|
claude: {
|
|
36
46
|
label: "Claude Code",
|
|
@@ -70,6 +80,17 @@ function installTo(key) {
|
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
try {
|
|
83
|
+
// MCP-only mode: skip skill file copy, just register MCP server
|
|
84
|
+
if (INSTALL_MODE === "mcp") {
|
|
85
|
+
if (key === "claude") {
|
|
86
|
+
setupMulahazah();
|
|
87
|
+
console.log(` ✓ ${target.label} → MCP server only`);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(` ⊘ ${target.label} — MCP mode only applies to Claude Code`);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
mkdirSync(target.dir, { recursive: true });
|
|
74
95
|
copyFileSync(SKILL_SOURCE, join(target.dir, "SKILL.md"));
|
|
75
96
|
console.log(` ✓ ${target.label} → ${target.dir}/SKILL.md`);
|
|
@@ -103,18 +124,88 @@ function setupMulahazah() {
|
|
|
103
124
|
console.log(` ✓ observe.sh → ${observeDest}`);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
|
-
// 3. Copy
|
|
127
|
+
// 3. Copy session.sh for expert mode
|
|
128
|
+
if (INSTALL_MODE === "expert") {
|
|
129
|
+
const sessionSrc = join(REPO_ROOT, "hooks", "session.sh");
|
|
130
|
+
const sessionDest = join(instinctsDir, "session.sh");
|
|
131
|
+
if (existsSync(sessionSrc)) {
|
|
132
|
+
copyFileSync(sessionSrc, sessionDest);
|
|
133
|
+
chmodSync(sessionDest, 0o755);
|
|
134
|
+
console.log(` ✓ session.sh → ${sessionDest}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 4. Copy all commands
|
|
107
139
|
const commandsDir = join(home, ".claude", "commands");
|
|
108
140
|
mkdirSync(commandsDir, { recursive: true });
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
const commandFiles = ["continuous-improvement.md", "discipline.md", "dashboard.md"];
|
|
142
|
+
for (const cmdFile of commandFiles) {
|
|
143
|
+
const cmdSrc = join(REPO_ROOT, "commands", cmdFile);
|
|
144
|
+
const cmdDest = join(commandsDir, cmdFile);
|
|
145
|
+
if (existsSync(cmdSrc)) {
|
|
146
|
+
copyFileSync(cmdSrc, cmdDest);
|
|
147
|
+
console.log(` ✓ /${cmdFile.replace(".md", "")} command → ${cmdDest}`);
|
|
148
|
+
}
|
|
114
149
|
}
|
|
115
150
|
|
|
116
|
-
//
|
|
151
|
+
// 5. Patch ~/.claude/settings.json with hooks
|
|
117
152
|
patchClaudeSettings(observeDest);
|
|
153
|
+
|
|
154
|
+
// 6. Setup MCP server for expert or mcp mode
|
|
155
|
+
if (INSTALL_MODE === "expert" || INSTALL_MODE === "mcp") {
|
|
156
|
+
setupMcpServer();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setupMcpServer() {
|
|
161
|
+
const home = homedir();
|
|
162
|
+
const mcpServerPath = join(REPO_ROOT, "bin", "mcp-server.mjs");
|
|
163
|
+
const mcpMode = INSTALL_MODE === "mcp" ? "beginner" : "expert";
|
|
164
|
+
|
|
165
|
+
// Patch Claude Code settings.json with MCP server config
|
|
166
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
167
|
+
let settings = {};
|
|
168
|
+
if (existsSync(settingsPath)) {
|
|
169
|
+
try {
|
|
170
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
171
|
+
} catch {
|
|
172
|
+
console.warn(` ! Could not parse settings.json — skipping MCP setup`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
178
|
+
|
|
179
|
+
const alreadySetup = settings.mcpServers["continuous-improvement"];
|
|
180
|
+
if (!alreadySetup) {
|
|
181
|
+
settings.mcpServers["continuous-improvement"] = {
|
|
182
|
+
command: "node",
|
|
183
|
+
args: [mcpServerPath, "--mode", mcpMode],
|
|
184
|
+
};
|
|
185
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
186
|
+
console.log(` ✓ MCP server registered (mode: ${mcpMode})`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` ✓ MCP server already registered — no change`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Also write claude_desktop_config.json if it exists
|
|
192
|
+
const desktopConfig = join(home, ".claude", "claude_desktop_config.json");
|
|
193
|
+
if (existsSync(desktopConfig)) {
|
|
194
|
+
try {
|
|
195
|
+
const config = JSON.parse(readFileSync(desktopConfig, "utf8"));
|
|
196
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
197
|
+
if (!config.mcpServers["continuous-improvement"]) {
|
|
198
|
+
config.mcpServers["continuous-improvement"] = {
|
|
199
|
+
command: "node",
|
|
200
|
+
args: [mcpServerPath, "--mode", mcpMode],
|
|
201
|
+
};
|
|
202
|
+
writeFileSync(desktopConfig, JSON.stringify(config, null, 2) + "\n");
|
|
203
|
+
console.log(` ✓ Claude Desktop MCP config updated`);
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// skip
|
|
207
|
+
}
|
|
208
|
+
}
|
|
118
209
|
}
|
|
119
210
|
|
|
120
211
|
function patchClaudeSettings(observePath) {
|
|
@@ -159,11 +250,40 @@ function patchClaudeSettings(observePath) {
|
|
|
159
250
|
}
|
|
160
251
|
}
|
|
161
252
|
|
|
253
|
+
// Expert mode: add session hooks
|
|
254
|
+
if (INSTALL_MODE === "expert") {
|
|
255
|
+
const sessionPath = join(homedir(), ".claude", "instincts", "session.sh");
|
|
256
|
+
const sessionHook = {
|
|
257
|
+
matcher: "",
|
|
258
|
+
hooks: [{ type: "command", command: `bash "${sessionPath}"` }],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Note: SessionStart/SessionEnd hooks may not be supported in all versions.
|
|
262
|
+
// We register them — they'll be silently ignored if unsupported.
|
|
263
|
+
for (const hookType of ["SessionStart", "SessionEnd"]) {
|
|
264
|
+
if (!Array.isArray(settings.hooks[hookType])) {
|
|
265
|
+
settings.hooks[hookType] = [];
|
|
266
|
+
}
|
|
267
|
+
const alreadyPatched = settings.hooks[hookType].some(
|
|
268
|
+
(h) =>
|
|
269
|
+
Array.isArray(h.hooks) &&
|
|
270
|
+
h.hooks.some((hh) => hh.command && hh.command.includes("session.sh"))
|
|
271
|
+
);
|
|
272
|
+
if (!alreadyPatched) {
|
|
273
|
+
settings.hooks[hookType].push(sessionHook);
|
|
274
|
+
changed = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
162
279
|
if (changed) {
|
|
163
280
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
164
|
-
|
|
281
|
+
const hookTypes = INSTALL_MODE === "expert"
|
|
282
|
+
? "PreToolUse/PostToolUse/SessionStart/SessionEnd"
|
|
283
|
+
: "PreToolUse/PostToolUse";
|
|
284
|
+
console.log(` ✓ Patched ~/.claude/settings.json with ${hookTypes} hooks`);
|
|
165
285
|
} else {
|
|
166
|
-
console.log(` ✓ settings.json already has
|
|
286
|
+
console.log(` ✓ settings.json already has hooks — no change needed`);
|
|
167
287
|
}
|
|
168
288
|
}
|
|
169
289
|
|
|
@@ -186,35 +306,41 @@ function uninstallAll() {
|
|
|
186
306
|
}
|
|
187
307
|
}
|
|
188
308
|
|
|
189
|
-
// 2. Remove
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
309
|
+
// 2. Remove all commands
|
|
310
|
+
for (const cmdName of ["continuous-improvement.md", "discipline.md", "dashboard.md"]) {
|
|
311
|
+
const cmdFile = join(home, ".claude", "commands", cmdName);
|
|
312
|
+
if (existsSync(cmdFile)) {
|
|
313
|
+
try {
|
|
314
|
+
rmSync(cmdFile);
|
|
315
|
+
console.log(` ✓ Removed /${cmdName.replace(".md", "")} command`);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(` ✗ ${cmdName}: ${err.message}`);
|
|
318
|
+
}
|
|
197
319
|
}
|
|
198
320
|
}
|
|
199
321
|
|
|
200
|
-
// 3. Remove observe.sh from instincts dir
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
322
|
+
// 3. Remove observe.sh and session.sh from instincts dir
|
|
323
|
+
for (const hookFile of ["observe.sh", "session.sh"]) {
|
|
324
|
+
const filePath = join(home, ".claude", "instincts", hookFile);
|
|
325
|
+
if (existsSync(filePath)) {
|
|
326
|
+
try {
|
|
327
|
+
rmSync(filePath);
|
|
328
|
+
console.log(` ✓ Removed ${hookFile}`);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(` ✗ ${hookFile}: ${err.message}`);
|
|
331
|
+
}
|
|
208
332
|
}
|
|
209
333
|
}
|
|
210
334
|
|
|
211
|
-
// 4. Remove hooks from settings.json
|
|
335
|
+
// 4. Remove hooks and MCP server from settings.json
|
|
212
336
|
const settingsPath = join(home, ".claude", "settings.json");
|
|
213
337
|
if (existsSync(settingsPath)) {
|
|
214
338
|
try {
|
|
215
339
|
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
216
340
|
let changed = false;
|
|
217
|
-
|
|
341
|
+
|
|
342
|
+
// Remove all hook types
|
|
343
|
+
for (const hookType of ["PreToolUse", "PostToolUse", "SessionStart", "SessionEnd"]) {
|
|
218
344
|
if (Array.isArray(settings.hooks?.[hookType])) {
|
|
219
345
|
const before = settings.hooks[hookType].length;
|
|
220
346
|
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
@@ -222,22 +348,44 @@ function uninstallAll() {
|
|
|
222
348
|
!(
|
|
223
349
|
Array.isArray(h.hooks) &&
|
|
224
350
|
h.hooks.some(
|
|
225
|
-
(hh) => hh.command && hh.command.includes("observe.sh")
|
|
351
|
+
(hh) => hh.command && (hh.command.includes("observe.sh") || hh.command.includes("session.sh"))
|
|
226
352
|
)
|
|
227
353
|
)
|
|
228
354
|
);
|
|
229
355
|
if (settings.hooks[hookType].length < before) changed = true;
|
|
230
356
|
}
|
|
231
357
|
}
|
|
358
|
+
|
|
359
|
+
// Remove MCP server
|
|
360
|
+
if (settings.mcpServers?.["continuous-improvement"]) {
|
|
361
|
+
delete settings.mcpServers["continuous-improvement"];
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
232
365
|
if (changed) {
|
|
233
366
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
234
|
-
console.log(` ✓ Removed hooks from settings.json`);
|
|
367
|
+
console.log(` ✓ Removed hooks and MCP server from settings.json`);
|
|
235
368
|
}
|
|
236
369
|
} catch {
|
|
237
370
|
console.warn(` ! Could not clean settings.json — remove hooks manually`);
|
|
238
371
|
}
|
|
239
372
|
}
|
|
240
373
|
|
|
374
|
+
// 5. Clean claude_desktop_config.json MCP entry
|
|
375
|
+
const desktopConfig = join(home, ".claude", "claude_desktop_config.json");
|
|
376
|
+
if (existsSync(desktopConfig)) {
|
|
377
|
+
try {
|
|
378
|
+
const config = JSON.parse(readFileSync(desktopConfig, "utf8"));
|
|
379
|
+
if (config.mcpServers?.["continuous-improvement"]) {
|
|
380
|
+
delete config.mcpServers["continuous-improvement"];
|
|
381
|
+
writeFileSync(desktopConfig, JSON.stringify(config, null, 2) + "\n");
|
|
382
|
+
console.log(` ✓ Removed MCP server from Claude Desktop config`);
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// skip
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
241
389
|
if (removed === 0) {
|
|
242
390
|
console.log(" No skill installations found.");
|
|
243
391
|
}
|
|
@@ -253,13 +401,30 @@ Usage: npx continuous-improvement install [options]
|
|
|
253
401
|
|
|
254
402
|
Options:
|
|
255
403
|
--target <name> Install to specific target (claude, openclaw, cursor, codex, all)
|
|
404
|
+
--mode <mode> Installation mode:
|
|
405
|
+
beginner — hooks only, no MCP server (default)
|
|
406
|
+
expert — hooks + MCP server + session hooks + all tools
|
|
407
|
+
mcp — MCP server only (works with any MCP client)
|
|
256
408
|
--uninstall Remove from all targets
|
|
257
409
|
--help Show this help
|
|
258
410
|
|
|
411
|
+
Modes explained:
|
|
412
|
+
BEGINNER (default) Just works. Hooks capture silently, instincts grow over time.
|
|
413
|
+
3 tools via /continuous-improvement command.
|
|
414
|
+
|
|
415
|
+
EXPERT Everything in beginner + MCP server with 8 tools:
|
|
416
|
+
import/export, manual instinct creation, observation viewer,
|
|
417
|
+
confidence tuning. Plus session start/end hooks.
|
|
418
|
+
|
|
419
|
+
MCP MCP server only — for editors that support MCP but not
|
|
420
|
+
Claude Code hooks (Cursor, Zed, Windsurf, VS Code).
|
|
421
|
+
|
|
259
422
|
Examples:
|
|
260
|
-
npx continuous-improvement install
|
|
261
|
-
npx continuous-improvement install --
|
|
262
|
-
npx continuous-improvement install --
|
|
423
|
+
npx continuous-improvement install # beginner (default)
|
|
424
|
+
npx continuous-improvement install --mode expert # full power
|
|
425
|
+
npx continuous-improvement install --mode mcp # MCP server only
|
|
426
|
+
npx continuous-improvement install --target all # install everywhere
|
|
427
|
+
npx continuous-improvement install --uninstall # remove all
|
|
263
428
|
`);
|
|
264
429
|
}
|
|
265
430
|
|
|
@@ -284,7 +449,7 @@ if (args.includes("--uninstall")) {
|
|
|
284
449
|
}
|
|
285
450
|
|
|
286
451
|
console.log(`
|
|
287
|
-
continuous-improvement
|
|
452
|
+
continuous-improvement v3.1 (mode: ${INSTALL_MODE})
|
|
288
453
|
Research → Plan → Execute → Verify → Reflect → Learn → Iterate
|
|
289
454
|
`);
|
|
290
455
|
|
|
@@ -321,11 +486,73 @@ for (const t of targets) {
|
|
|
321
486
|
|
|
322
487
|
const hasClaude = targets.includes("claude");
|
|
323
488
|
|
|
489
|
+
const modeInfo = {
|
|
490
|
+
beginner: "Hooks are capturing silently. System auto-levels as you use it.",
|
|
491
|
+
expert: "Full plugin active: hooks + MCP server + session hooks. 8 tools available.",
|
|
492
|
+
mcp: "MCP server registered. Connect from any MCP-compatible editor.",
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Handle --pack flag
|
|
496
|
+
const packIdx = _args.indexOf("--pack");
|
|
497
|
+
if (packIdx !== -1 && _args[packIdx + 1]) {
|
|
498
|
+
const packName = _args[packIdx + 1];
|
|
499
|
+
const packPath = join(REPO_ROOT, "instinct-packs", `${packName}.json`);
|
|
500
|
+
if (existsSync(packPath)) {
|
|
501
|
+
const project = getProjectHashSync();
|
|
502
|
+
const targetDir = join(homedir(), ".claude", "instincts", project.hash);
|
|
503
|
+
mkdirSync(targetDir, { recursive: true });
|
|
504
|
+
|
|
505
|
+
const instincts = JSON.parse(readFileSync(packPath, "utf8"));
|
|
506
|
+
let loaded = 0;
|
|
507
|
+
for (const inst of instincts) {
|
|
508
|
+
const instPath = join(targetDir, `${inst.id}.yaml`);
|
|
509
|
+
if (!existsSync(instPath)) {
|
|
510
|
+
const yaml = [
|
|
511
|
+
`id: ${inst.id}`,
|
|
512
|
+
`trigger: "${inst.trigger}"`,
|
|
513
|
+
`confidence: ${inst.confidence}`,
|
|
514
|
+
`domain: ${inst.domain || "workflow"}`,
|
|
515
|
+
`source: pack-${packName}`,
|
|
516
|
+
`scope: project`,
|
|
517
|
+
`project_id: ${project.hash}`,
|
|
518
|
+
`created: "${new Date().toISOString().split("T")[0]}"`,
|
|
519
|
+
`last_seen: "${new Date().toISOString().split("T")[0]}"`,
|
|
520
|
+
`observation_count: 0`,
|
|
521
|
+
"---",
|
|
522
|
+
inst.body,
|
|
523
|
+
].join("\n");
|
|
524
|
+
writeFileSync(instPath, yaml + "\n");
|
|
525
|
+
loaded++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
console.log(` ✓ Loaded ${loaded}/${instincts.length} instincts from ${packName} pack`);
|
|
529
|
+
} else {
|
|
530
|
+
const available = readdirSync(join(REPO_ROOT, "instinct-packs"))
|
|
531
|
+
.filter((f) => f.endsWith(".json"))
|
|
532
|
+
.map((f) => f.replace(".json", ""));
|
|
533
|
+
console.error(` ✗ Unknown pack: ${packName}. Available: ${available.join(", ")}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getProjectHashSync() {
|
|
538
|
+
try {
|
|
539
|
+
const root = execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf8" }).trim();
|
|
540
|
+
const hash = execSync(`printf '%s' "${root}" | sha256sum | cut -c1-12`, { encoding: "utf8", shell: "/bin/bash" }).trim();
|
|
541
|
+
return { root, hash };
|
|
542
|
+
} catch {
|
|
543
|
+
return { root: "global", hash: "global" };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
324
547
|
console.log(`
|
|
325
548
|
${installed > 0 ? "Done." : "Failed."} Installed to ${installed}/${targets.length} target(s).
|
|
326
|
-
${hasClaude ?
|
|
549
|
+
${hasClaude ? `\n${modeInfo[INSTALL_MODE] || modeInfo.beginner}` : ""}
|
|
327
550
|
Next steps:
|
|
328
551
|
1. Start a new Claude Code session
|
|
329
552
|
2. Say: "Use the continuous-improvement framework to [your task]"
|
|
330
553
|
3. After your first task, run: /continuous-improvement
|
|
554
|
+
4. Try: /discipline for quick reference, /dashboard for instinct health
|
|
555
|
+
${INSTALL_MODE === "expert" ? "\nMCP tools available: ci_status, ci_instincts, ci_reflect, ci_reinforce,\n ci_create_instinct, ci_observations, ci_export, ci_import, ci_dashboard, ci_load_pack" : ""}
|
|
556
|
+
${INSTALL_MODE === "mcp" ? "\nMCP tools available: ci_status, ci_instincts, ci_reflect" : ""}
|
|
557
|
+
Available instinct packs: npx continuous-improvement install --pack react|python|go
|
|
331
558
|
`);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Transcript Linter
|
|
5
|
+
*
|
|
6
|
+
* Analyzes AI agent transcripts/observations for compliance with
|
|
7
|
+
* the 7 Laws of AI Agent Discipline.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node bin/lint-transcript.mjs <file.jsonl> # Lint a transcript file
|
|
11
|
+
* cat observations.jsonl | node bin/lint-transcript.mjs --stdin # Pipe input
|
|
12
|
+
* node bin/lint-transcript.mjs --help
|
|
13
|
+
*
|
|
14
|
+
* Exit codes:
|
|
15
|
+
* 0 — no violations (or --strict not set)
|
|
16
|
+
* 1 — violations found (with --strict)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { createInterface } from "node:readline";
|
|
21
|
+
|
|
22
|
+
const LAWS = {
|
|
23
|
+
1: { name: "Research Before Executing", patterns: [] },
|
|
24
|
+
2: { name: "Plan Is Sacred", patterns: [] },
|
|
25
|
+
3: { name: "One Thing at a Time", patterns: [] },
|
|
26
|
+
4: { name: "Verify Before Reporting", patterns: [] },
|
|
27
|
+
5: { name: "Reflect After Sessions", patterns: [] },
|
|
28
|
+
6: { name: "Iterate One Change", patterns: [] },
|
|
29
|
+
7: { name: "Learn From Every Session", patterns: [] },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function analyzeTranscript(events) {
|
|
33
|
+
const violations = [];
|
|
34
|
+
const stats = {
|
|
35
|
+
totalEvents: events.length,
|
|
36
|
+
toolCalls: 0,
|
|
37
|
+
researchTools: 0, // Grep, Glob, Read
|
|
38
|
+
writeTools: 0, // Write, Edit, Bash
|
|
39
|
+
verifyTools: 0, // Bash (test/build commands)
|
|
40
|
+
sessions: new Set(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Classify events
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
stats.toolCalls++;
|
|
46
|
+
if (event.session_id) stats.sessions.add(event.session_id);
|
|
47
|
+
|
|
48
|
+
const tool = event.tool || event.tool_name || "";
|
|
49
|
+
const input = event.tool_input || event.input || {};
|
|
50
|
+
const command = input.command || "";
|
|
51
|
+
|
|
52
|
+
if (["Grep", "Glob", "Read"].includes(tool)) {
|
|
53
|
+
stats.researchTools++;
|
|
54
|
+
}
|
|
55
|
+
if (["Write", "Edit"].includes(tool)) {
|
|
56
|
+
stats.writeTools++;
|
|
57
|
+
}
|
|
58
|
+
if (tool === "Bash" && /\b(test|build|npm run|jest|pytest|go test|cargo test)\b/.test(command)) {
|
|
59
|
+
stats.verifyTools++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Law 1: Research Before Executing
|
|
64
|
+
// Check if writes happen without prior reads/greps
|
|
65
|
+
let lastResearchIdx = -1;
|
|
66
|
+
for (let i = 0; i < events.length; i++) {
|
|
67
|
+
const tool = events[i].tool || events[i].tool_name || "";
|
|
68
|
+
if (["Grep", "Glob", "Read"].includes(tool)) {
|
|
69
|
+
lastResearchIdx = i;
|
|
70
|
+
}
|
|
71
|
+
if (["Write", "Edit"].includes(tool) && lastResearchIdx === -1) {
|
|
72
|
+
violations.push({
|
|
73
|
+
law: 1,
|
|
74
|
+
severity: "high",
|
|
75
|
+
message: `Write/Edit at event ${i} without any prior research (Grep/Glob/Read)`,
|
|
76
|
+
event: i,
|
|
77
|
+
});
|
|
78
|
+
break; // Only report first occurrence
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Law 1 ratio check
|
|
83
|
+
if (stats.writeTools > 0 && stats.researchTools === 0) {
|
|
84
|
+
violations.push({
|
|
85
|
+
law: 1,
|
|
86
|
+
severity: "high",
|
|
87
|
+
message: `${stats.writeTools} writes with 0 research operations. Agent likely skipped research.`,
|
|
88
|
+
});
|
|
89
|
+
} else if (stats.writeTools > stats.researchTools * 3) {
|
|
90
|
+
violations.push({
|
|
91
|
+
law: 1,
|
|
92
|
+
severity: "medium",
|
|
93
|
+
message: `Write-to-research ratio is ${stats.writeTools}:${stats.researchTools}. Consider more research.`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Law 3: One Thing at a Time
|
|
98
|
+
// Detect rapid context switching (different files edited in quick succession without verification)
|
|
99
|
+
let consecutiveEdits = 0;
|
|
100
|
+
const editedFiles = new Set();
|
|
101
|
+
for (const event of events) {
|
|
102
|
+
const tool = event.tool || event.tool_name || "";
|
|
103
|
+
const input = event.tool_input || event.input || {};
|
|
104
|
+
|
|
105
|
+
if (["Write", "Edit"].includes(tool)) {
|
|
106
|
+
consecutiveEdits++;
|
|
107
|
+
if (input.file_path) editedFiles.add(input.file_path);
|
|
108
|
+
} else if (tool === "Bash") {
|
|
109
|
+
consecutiveEdits = 0; // Reset on verification
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (consecutiveEdits > 5) {
|
|
113
|
+
violations.push({
|
|
114
|
+
law: 3,
|
|
115
|
+
severity: "medium",
|
|
116
|
+
message: `${consecutiveEdits} consecutive edits without verification. Editing multiple files at once.`,
|
|
117
|
+
});
|
|
118
|
+
consecutiveEdits = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Law 4: Verify Before Reporting
|
|
123
|
+
if (stats.writeTools > 0 && stats.verifyTools === 0) {
|
|
124
|
+
violations.push({
|
|
125
|
+
law: 4,
|
|
126
|
+
severity: "high",
|
|
127
|
+
message: `${stats.writeTools} code changes with 0 verification commands (test/build). Agent may have declared done without verifying.`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Law 6: Iterate One Change
|
|
132
|
+
if (editedFiles.size > 8) {
|
|
133
|
+
violations.push({
|
|
134
|
+
law: 6,
|
|
135
|
+
severity: "medium",
|
|
136
|
+
message: `${editedFiles.size} different files modified. Consider smaller, focused iterations.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Calculate discipline score
|
|
141
|
+
const maxViolations = 7; // One per law
|
|
142
|
+
const violationWeight = violations.reduce((sum, v) => {
|
|
143
|
+
return sum + (v.severity === "high" ? 2 : 1);
|
|
144
|
+
}, 0);
|
|
145
|
+
const score = Math.max(0, Math.round(100 - (violationWeight / maxViolations) * 100));
|
|
146
|
+
|
|
147
|
+
return { violations, stats, score };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatReport(result) {
|
|
151
|
+
const { violations, stats, score } = result;
|
|
152
|
+
const lines = [];
|
|
153
|
+
|
|
154
|
+
lines.push("## Agent Discipline Report");
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(`**Score:** ${score}/100`);
|
|
157
|
+
lines.push(`**Events analyzed:** ${stats.totalEvents}`);
|
|
158
|
+
lines.push(`**Tool calls:** ${stats.toolCalls}`);
|
|
159
|
+
lines.push(`**Research ops:** ${stats.researchTools} | **Write ops:** ${stats.writeTools} | **Verify ops:** ${stats.verifyTools}`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
|
|
162
|
+
if (violations.length === 0) {
|
|
163
|
+
lines.push("No law violations detected. Good discipline!");
|
|
164
|
+
} else {
|
|
165
|
+
lines.push(`### Violations (${violations.length})`);
|
|
166
|
+
lines.push("");
|
|
167
|
+
for (const v of violations) {
|
|
168
|
+
const severity = v.severity === "high" ? "HIGH" : "MEDIUM";
|
|
169
|
+
lines.push(`- **[${severity}] Law ${v.law}: ${LAWS[v.law].name}**`);
|
|
170
|
+
lines.push(` ${v.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("---");
|
|
176
|
+
lines.push("*Generated by [continuous-improvement](https://github.com/naimkatiman/continuous-improvement) transcript linter*");
|
|
177
|
+
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Main ---
|
|
182
|
+
|
|
183
|
+
const args = process.argv.slice(2);
|
|
184
|
+
|
|
185
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
186
|
+
console.log(`
|
|
187
|
+
Agent Transcript Linter — The 7 Laws of AI Agent Discipline
|
|
188
|
+
|
|
189
|
+
Usage:
|
|
190
|
+
node bin/lint-transcript.mjs <file.jsonl> Lint a transcript file
|
|
191
|
+
cat obs.jsonl | node bin/lint-transcript.mjs --stdin Pipe input
|
|
192
|
+
node bin/lint-transcript.mjs --help Show help
|
|
193
|
+
|
|
194
|
+
Options:
|
|
195
|
+
--stdin Read from stdin
|
|
196
|
+
--strict Exit 1 if violations found
|
|
197
|
+
--json Output as JSON instead of markdown
|
|
198
|
+
`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const isStrict = args.includes("--strict");
|
|
203
|
+
const isJson = args.includes("--json");
|
|
204
|
+
const isStdin = args.includes("--stdin");
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
let lines;
|
|
208
|
+
|
|
209
|
+
if (isStdin) {
|
|
210
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
211
|
+
lines = [];
|
|
212
|
+
for await (const line of rl) {
|
|
213
|
+
if (line.trim()) lines.push(line.trim());
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
const filePath = args.find((a) => !a.startsWith("--"));
|
|
217
|
+
if (!filePath) {
|
|
218
|
+
console.error("Error: provide a file path or use --stdin");
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const content = readFileSync(filePath, "utf8");
|
|
222
|
+
lines = content.split("\n").filter((l) => l.trim());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const events = [];
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
try {
|
|
228
|
+
events.push(JSON.parse(line));
|
|
229
|
+
} catch {
|
|
230
|
+
// skip non-JSON lines
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (events.length === 0) {
|
|
235
|
+
console.log("No events found to analyze.");
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = analyzeTranscript(events);
|
|
240
|
+
|
|
241
|
+
if (isJson) {
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(formatReport(result));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// GitHub Actions output
|
|
248
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
249
|
+
const outputLines = [
|
|
250
|
+
`violations=${result.violations.length}`,
|
|
251
|
+
`score=${result.score}`,
|
|
252
|
+
];
|
|
253
|
+
const { appendFileSync } = await import("node:fs");
|
|
254
|
+
for (const line of outputLines) {
|
|
255
|
+
appendFileSync(process.env.GITHUB_OUTPUT, line + "\n");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isStrict && result.violations.length > 0) {
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
main().catch((err) => {
|
|
265
|
+
console.error(err.message);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
});
|