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/bin/install.mjs CHANGED
@@ -4,12 +4,15 @@
4
4
  * continuous-improvement installer
5
5
  *
6
6
  * Usage:
7
- * npx continuous-improvement install # auto-detect & install
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 --uninstall # remove from all targets
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 /continuous-improvement command
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 cmdSrc = join(REPO_ROOT, "commands", "continuous-improvement.md");
110
- const cmdDest = join(commandsDir, "continuous-improvement.md");
111
- if (existsSync(cmdSrc)) {
112
- copyFileSync(cmdSrc, cmdDest);
113
- console.log(` ✓ /continuous-improvement command → ${cmdDest}`);
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
- // 4. Patch ~/.claude/settings.json with hooks
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
- console.log(` ✓ Patched ~/.claude/settings.json with PreToolUse/PostToolUse hooks`);
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 observe.sh hooks — no change needed`);
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 /continuous-improvement command
190
- const cmdFile = join(home, ".claude", "commands", "continuous-improvement.md");
191
- if (existsSync(cmdFile)) {
192
- try {
193
- rmSync(cmdFile);
194
- console.log(` ✓ Removed /continuous-improvement command`);
195
- } catch (err) {
196
- console.error(` ✗ Command file: ${err.message}`);
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 observeFile = join(home, ".claude", "instincts", "observe.sh");
202
- if (existsSync(observeFile)) {
203
- try {
204
- rmSync(observeFile);
205
- console.log(` ✓ Removed observe.sh`);
206
- } catch (err) {
207
- console.error(` ✗ observe.sh: ${err.message}`);
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
- for (const hookType of ["PreToolUse", "PostToolUse"]) {
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 # auto-detect & install
261
- npx continuous-improvement install --target all # install everywhere
262
- npx continuous-improvement install --uninstall # remove all
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 v2.1
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 ? "\nHooks are capturing. System auto-levels as you use it." : ""}
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
+ });