iranti 0.3.11 → 0.3.13

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/README.md CHANGED
@@ -1,19 +1,21 @@
1
1
  # Iranti
2
2
 
3
3
  [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
4
+ [![MCP Server](https://img.shields.io/badge/MCP-server-purple.svg)](https://modelcontextprotocol.io)
5
+ [![npm](https://img.shields.io/badge/npm-iranti-red.svg)](https://www.npmjs.com/package/iranti)
4
6
  [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
5
7
  [![TypeScript](https://img.shields.io/badge/typescript-5.0+-blue.svg)](https://www.typescriptlang.org/)
6
8
  [![CrewAI Compatible](https://img.shields.io/badge/CrewAI-compatible-green.svg)](https://www.crewai.com/)
7
9
 
8
- **Memory infrastructure for multi-agent AI systems.**
10
+ **Memory infrastructure for multi-agent AI systems — with a built-in MCP server.**
9
11
 
10
12
  Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
11
13
 
12
- **Repo version:** `0.3.4`
14
+ **Repo version:** `0.3.11`
13
15
  Published packages:
14
- - npm `iranti@0.3.4`
15
- - npm `@iranti/sdk@0.3.4`
16
- - PyPI `iranti==0.3.4`
16
+ - npm `iranti@0.3.11`
17
+ - npm `@iranti/sdk@0.3.11`
18
+ - PyPI `iranti==0.3.11`
17
19
 
18
20
  ---
19
21
 
@@ -23,6 +25,32 @@ Iranti is a knowledge base for multi-agent systems. The primary read path is ide
23
25
 
24
26
  ---
25
27
 
28
+ ## MCP Server
29
+
30
+ Iranti ships a stdio MCP server compatible with Claude Code, GitHub Copilot, Codex, and any MCP-compliant client:
31
+
32
+ ```bash
33
+ # Fast path for Claude Code
34
+ iranti claude-setup
35
+
36
+ # Fast path for Codex
37
+ iranti codex-setup
38
+
39
+ # Fast path for GitHub Copilot
40
+ iranti copilot-setup
41
+
42
+ # Manual / other MCP clients
43
+ iranti mcp
44
+ ```
45
+
46
+ MCP tools exposed: `iranti_handshake`, `iranti_attend`, `iranti_write`, `iranti_query`, `iranti_search`, `iranti_checkpoint`, `iranti_ingest`, `iranti_relate`, `iranti_related`, `iranti_related_deep`, `iranti_history`, `iranti_who_knows`, `iranti_observe`, and more.
47
+
48
+ Full setup guides:
49
+ - [Claude Code](docs/guides/claude-code.md)
50
+ - [Codex](docs/guides/codex.md)
51
+
52
+ ---
53
+
26
54
  ## Runtime Roles
27
55
 
28
56
  - **User**: Person who interacts with an app or chatbot built on Iranti.
@@ -315,7 +315,9 @@ function emitHookContext(event, additionalContext) {
315
315
  additionalContext,
316
316
  },
317
317
  };
318
- process.stdout.write(`${JSON.stringify(payload)}\n`);
318
+ // Use synchronous write to fd 1 to avoid libuv UV_HANDLE_CLOSING assertion
319
+ // on Windows when Node exits while an async stdout write is still pending.
320
+ require('fs').writeSync(1, `${JSON.stringify(payload)}\n`);
319
321
  }
320
322
  function shouldFetchMemory(prompt) {
321
323
  const normalized = prompt.trim();
@@ -364,6 +364,147 @@ function writeWorkspaceVsCodeMcpFile(projectEnv, options) {
364
364
  }, null, 2)}\n`, 'utf8');
365
365
  return { filePath: mcpFile, status: 'updated' };
366
366
  }
367
+ /**
368
+ * Protocol reminder hook script content for Codex UserPromptSubmit.
369
+ * Same content as the Claude Code version — fires before every user prompt.
370
+ */
371
+ function buildProtocolReminderHookScript() {
372
+ return [
373
+ '#!/usr/bin/env node',
374
+ "'use strict';",
375
+ '// Iranti protocol reminder hook — fires on UserPromptSubmit for any Iranti project.',
376
+ '// Cross-platform: runs on Windows, macOS, Linux via Node.js.',
377
+ '// Exits cleanly with no output for non-Iranti projects.',
378
+ "const fs = require('fs');",
379
+ "const path = require('path');",
380
+ '',
381
+ "const envFile = path.join(process.cwd(), '.env.iranti');",
382
+ 'if (!fs.existsSync(envFile)) process.exit(0);',
383
+ '',
384
+ 'const content = [',
385
+ " 'IRANTI PROTOCOL (required this turn):',",
386
+ " '1. iranti_attend(phase=pre-response) BEFORE replying',",
387
+ " '2. iranti_attend BEFORE each Read / Grep / Glob / Bash / WebSearch / WebFetch',",
388
+ " '3. iranti_write AFTER each Edit or Write:',",
389
+ " ' entity: project/[id]/file/[filename] -- not the broad project entity',",
390
+ " ' value must include: absolutePath, lines, before, after, verify, why',",
391
+ " '4. iranti_write AFTER each Bash that reveals system state (build, errors, ports, env)',",
392
+ " '5. iranti_write AFTER each WebSearch/WebFetch -- write findings AND dead ends / 404s',",
393
+ " '6. iranti_attend(phase=post-response) AFTER every response without exception',",
394
+ "].join('\\n') + '\\n';",
395
+ "require('fs').writeSync(1, content);",
396
+ '',
397
+ ].join('\n');
398
+ }
399
+ /**
400
+ * Write the protocol-reminder hook script into the project's .codex/ directory
401
+ * and return a status result matching the workspace file pattern.
402
+ */
403
+ function writeProtocolReminderHook(projectEnv) {
404
+ const projectPath = node_path_1.default.dirname(projectEnv);
405
+ const codexDir = node_path_1.default.join(projectPath, '.codex');
406
+ const hookFile = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
407
+ const hookContent = buildProtocolReminderHookScript();
408
+ node_fs_1.default.mkdirSync(codexDir, { recursive: true });
409
+ if (!node_fs_1.default.existsSync(hookFile)) {
410
+ node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
411
+ return { filePath: hookFile, status: 'created' };
412
+ }
413
+ const existing = node_fs_1.default.readFileSync(hookFile, 'utf8');
414
+ if (existing !== hookContent) {
415
+ node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
416
+ return { filePath: hookFile, status: 'updated' };
417
+ }
418
+ return { filePath: hookFile, status: 'unchanged' };
419
+ }
420
+ /**
421
+ * Write a .codex/hooks.json referencing the protocol-reminder hook.
422
+ * This fires on UserPromptSubmit when the codex_hooks feature is enabled.
423
+ */
424
+ function writeCodexHooksConfig(projectEnv) {
425
+ const projectPath = node_path_1.default.dirname(projectEnv);
426
+ const codexDir = node_path_1.default.join(projectPath, '.codex');
427
+ const hooksConfigFile = node_path_1.default.join(codexDir, 'hooks.json');
428
+ const hookScriptPath = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
429
+ const hooksConfig = {
430
+ hooks: {
431
+ UserPromptSubmit: [
432
+ {
433
+ matcher: '',
434
+ hooks: [
435
+ {
436
+ type: 'command',
437
+ command: `node ${hookScriptPath.replace(/\\/g, '/')}`,
438
+ },
439
+ ],
440
+ },
441
+ ],
442
+ },
443
+ };
444
+ node_fs_1.default.mkdirSync(codexDir, { recursive: true });
445
+ const nextContent = `${JSON.stringify(hooksConfig, null, 2)}\n`;
446
+ if (!node_fs_1.default.existsSync(hooksConfigFile)) {
447
+ node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
448
+ return { filePath: hooksConfigFile, status: 'created' };
449
+ }
450
+ const existing = node_fs_1.default.readFileSync(hooksConfigFile, 'utf8');
451
+ if (existing === nextContent) {
452
+ return { filePath: hooksConfigFile, status: 'unchanged' };
453
+ }
454
+ // Merge: keep existing hooks, add/replace UserPromptSubmit from iranti.
455
+ try {
456
+ const parsed = JSON.parse(existing);
457
+ const existingHooks = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)
458
+ ? parsed.hooks
459
+ : {};
460
+ const merged = {
461
+ ...parsed,
462
+ hooks: {
463
+ ...existingHooks,
464
+ UserPromptSubmit: hooksConfig.hooks.UserPromptSubmit,
465
+ },
466
+ };
467
+ const mergedContent = `${JSON.stringify(merged, null, 2)}\n`;
468
+ if (mergedContent === existing) {
469
+ return { filePath: hooksConfigFile, status: 'unchanged' };
470
+ }
471
+ node_fs_1.default.writeFileSync(hooksConfigFile, mergedContent, 'utf8');
472
+ return { filePath: hooksConfigFile, status: 'updated' };
473
+ }
474
+ catch {
475
+ // Can't parse existing — overwrite
476
+ node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
477
+ return { filePath: hooksConfigFile, status: 'updated' };
478
+ }
479
+ }
480
+ /**
481
+ * Check if the codex_hooks feature is enabled globally.
482
+ */
483
+ function isCodexHooksFeatureEnabled(repoRoot) {
484
+ try {
485
+ const output = run('codex', ['features', 'list'], repoRoot);
486
+ const match = output.match(/codex_hooks\s+\S+\s+(true|false)/);
487
+ return match?.[1] === 'true';
488
+ }
489
+ catch {
490
+ return false;
491
+ }
492
+ }
493
+ /**
494
+ * Enable the codex_hooks feature flag if not already enabled.
495
+ */
496
+ function ensureCodexHooksFeature(repoRoot) {
497
+ if (isCodexHooksFeatureEnabled(repoRoot)) {
498
+ return true;
499
+ }
500
+ try {
501
+ run('codex', ['features', 'enable', 'codex_hooks'], repoRoot);
502
+ return true;
503
+ }
504
+ catch {
505
+ return false;
506
+ }
507
+ }
367
508
  function canUseInstalledIranti(repoRoot) {
368
509
  try {
369
510
  run('iranti', ['mcp', '--help'], repoRoot);
@@ -428,8 +569,12 @@ async function main() {
428
569
  vscode: writeWorkspaceVsCodeMcpFile(workspaceProjectEnv, options),
429
570
  agents: writeWorkspaceAgentsFile(workspaceProjectEnv),
430
571
  irantiMd: writeWorkspaceIrantiMdFile(workspaceProjectEnv),
572
+ protocolHook: writeProtocolReminderHook(workspaceProjectEnv),
573
+ hooksConfig: writeCodexHooksConfig(workspaceProjectEnv),
431
574
  }
432
575
  : null;
576
+ // Enable the codex_hooks feature flag so the UserPromptSubmit hook fires.
577
+ const hooksFeatureEnabled = ensureCodexHooksFeature(repoRoot);
433
578
  const registered = run('codex', ['mcp', 'get', options.name], repoRoot);
434
579
  console.log(registered);
435
580
  console.log('');
@@ -468,6 +613,9 @@ async function main() {
468
613
  console.log(`Workspace .vscode/mcp.json: ${workspaceFilesResult.vscode.status} (${workspaceFilesResult.vscode.filePath})`);
469
614
  console.log(`Workspace AGENTS.md: ${workspaceFilesResult.agents.status} (${workspaceFilesResult.agents.filePath})`);
470
615
  console.log(`Workspace IRANTI.md: ${workspaceFilesResult.irantiMd.status} (${workspaceFilesResult.irantiMd.filePath})`);
616
+ console.log(`Protocol hook: ${workspaceFilesResult.protocolHook.status} (${workspaceFilesResult.protocolHook.filePath})`);
617
+ console.log(`Hooks config: ${workspaceFilesResult.hooksConfig.status} (${workspaceFilesResult.hooksConfig.filePath})`);
618
+ console.log(`codex_hooks feature: ${hooksFeatureEnabled ? 'enabled' : 'not enabled (UserPromptSubmit hook requires codex_hooks feature)'}`);
471
619
  const closeout = await (0, scaffoldCloseout_1.writeProjectScaffoldCloseout)({
472
620
  tool: 'codex',
473
621
  projectPath: node_path_1.default.dirname(boundProjectEnv),
@@ -477,6 +625,8 @@ async function main() {
477
625
  { path: workspaceFilesResult.vscode.filePath, status: workspaceFilesResult.vscode.status },
478
626
  { path: workspaceFilesResult.agents.filePath, status: workspaceFilesResult.agents.status },
479
627
  { path: workspaceFilesResult.irantiMd.filePath, status: workspaceFilesResult.irantiMd.status },
628
+ { path: workspaceFilesResult.protocolHook.filePath, status: workspaceFilesResult.protocolHook.status },
629
+ { path: workspaceFilesResult.hooksConfig.filePath, status: workspaceFilesResult.hooksConfig.status },
480
630
  ],
481
631
  agentId: options.agent || 'codex_code',
482
632
  });
@@ -2231,6 +2231,218 @@ function makeClaudeHookEntry(event, projectEnvPath) {
2231
2231
  ],
2232
2232
  };
2233
2233
  }
2234
+ function makeClaudeWriteGuardHookEntry(hookScriptDir) {
2235
+ return {
2236
+ matcher: 'Edit|Write',
2237
+ hooks: [
2238
+ {
2239
+ type: 'command',
2240
+ command: `node ${quoteClaudeHookArg(path_1.default.join(hookScriptDir, 'iranti-write-guard-hook.js'))}`,
2241
+ },
2242
+ ],
2243
+ };
2244
+ }
2245
+ function makeClaudeEditTrackerHookEntry(hookScriptDir) {
2246
+ return {
2247
+ matcher: 'Edit|Write',
2248
+ hooks: [
2249
+ {
2250
+ type: 'command',
2251
+ command: `node ${quoteClaudeHookArg(path_1.default.join(hookScriptDir, 'iranti-edit-tracker-hook.js'))}`,
2252
+ },
2253
+ ],
2254
+ };
2255
+ }
2256
+ /**
2257
+ * Embedded JS content for the write-guard hook (PreToolUse).
2258
+ * Blocks Edit/Write if the agent has pending unwritten edits in .iranti-write-debt.
2259
+ */
2260
+ function buildWriteGuardHookScript() {
2261
+ return `#!/usr/bin/env node
2262
+ 'use strict';
2263
+ // Iranti write-guard hook — fires on PreToolUse for Edit/Write.
2264
+ // Checks whether the agent has pending unwritten edits by querying the
2265
+ // .iranti-write-debt signal file. If the agent edited files without calling
2266
+ // iranti_write, this hook DENIES the next edit and injects a reminder.
2267
+ //
2268
+ // Falls through silently (allows) for non-Iranti projects or if check fails.
2269
+ const fs = require('fs');
2270
+ const path = require('path');
2271
+
2272
+ const envFile = path.join(process.cwd(), '.env.iranti');
2273
+ if (!fs.existsSync(envFile)) {
2274
+ process.exit(0);
2275
+ }
2276
+
2277
+ let input = '';
2278
+ try {
2279
+ input = fs.readFileSync(0, 'utf8');
2280
+ } catch {
2281
+ process.exit(0);
2282
+ }
2283
+
2284
+ let hookData;
2285
+ try {
2286
+ hookData = JSON.parse(input);
2287
+ } catch {
2288
+ process.exit(0);
2289
+ }
2290
+
2291
+ const toolName = hookData.tool_name || hookData.toolName || '';
2292
+ if (toolName !== 'Edit' && toolName !== 'Write') {
2293
+ process.exit(0);
2294
+ }
2295
+
2296
+ const signalFile = path.join(process.cwd(), '.iranti-write-debt');
2297
+ if (!fs.existsSync(signalFile)) {
2298
+ process.exit(0);
2299
+ }
2300
+
2301
+ let debt;
2302
+ try {
2303
+ debt = JSON.parse(fs.readFileSync(signalFile, 'utf8'));
2304
+ } catch {
2305
+ process.exit(0);
2306
+ }
2307
+
2308
+ const pendingEdits = debt.pendingEdits || 0;
2309
+ const lastEditAt = debt.lastEditAt || null;
2310
+
2311
+ if (pendingEdits < 1) {
2312
+ process.exit(0);
2313
+ }
2314
+
2315
+ const output = JSON.stringify({
2316
+ hookEventName: 'PreToolUse',
2317
+ permissionDecision: 'deny',
2318
+ permissionDecisionReason: \`Iranti write-guard: \${pendingEdits} file edit(s) since last iranti_write (last edit: \${lastEditAt || 'unknown'}). Call iranti_write for each pending edit before making new changes.\`,
2319
+ additionalContext: [
2320
+ 'IRANTI WRITE-GUARD BLOCKED THIS EDIT.',
2321
+ \`You have \${pendingEdits} pending file edit(s) that have not been written to Iranti shared memory.\`,
2322
+ 'Before making any more edits, you MUST call iranti_write for each pending edit.',
2323
+ 'Include: entity (project/[id]/file/[filename]), absolutePath, lines changed, what changed and why.',
2324
+ 'After writing all pending edits, this guard will allow new edits.',
2325
+ ].join('\\n'),
2326
+ });
2327
+
2328
+ require('fs').writeSync(1, output);
2329
+ `;
2330
+ }
2331
+ /**
2332
+ * Embedded JS content for the edit-tracker hook (PostToolUse).
2333
+ * Increments the .iranti-write-debt counter after each Edit/Write.
2334
+ */
2335
+ function buildEditTrackerHookScript() {
2336
+ return `#!/usr/bin/env node
2337
+ 'use strict';
2338
+ // Iranti edit-tracker hook — fires on PostToolUse for Edit/Write.
2339
+ // Increments the write-debt counter so the PreToolUse write-guard knows
2340
+ // the agent has pending unwritten edits.
2341
+ //
2342
+ // The write-debt file (.iranti-write-debt) is cleared by the MCP server
2343
+ // when iranti_write is called, completing the enforcement loop.
2344
+ //
2345
+ // Falls through silently for non-Iranti projects.
2346
+ const fs = require('fs');
2347
+ const path = require('path');
2348
+
2349
+ const envFile = path.join(process.cwd(), '.env.iranti');
2350
+ if (!fs.existsSync(envFile)) {
2351
+ process.exit(0);
2352
+ }
2353
+
2354
+ let input = '';
2355
+ try {
2356
+ input = fs.readFileSync(0, 'utf8');
2357
+ } catch {
2358
+ process.exit(0);
2359
+ }
2360
+
2361
+ let hookData;
2362
+ try {
2363
+ hookData = JSON.parse(input);
2364
+ } catch {
2365
+ process.exit(0);
2366
+ }
2367
+
2368
+ const toolName = hookData.tool_name || hookData.toolName || '';
2369
+ if (toolName !== 'Edit' && toolName !== 'Write') {
2370
+ process.exit(0);
2371
+ }
2372
+
2373
+ const signalFile = path.join(process.cwd(), '.iranti-write-debt');
2374
+ let debt = { pendingEdits: 0, edits: [] };
2375
+
2376
+ try {
2377
+ if (fs.existsSync(signalFile)) {
2378
+ debt = JSON.parse(fs.readFileSync(signalFile, 'utf8'));
2379
+ }
2380
+ } catch {
2381
+ debt = { pendingEdits: 0, edits: [] };
2382
+ }
2383
+
2384
+ const editedFile = hookData.tool_input?.file_path
2385
+ || hookData.input?.file_path
2386
+ || hookData.tool_input?.filePath
2387
+ || hookData.input?.filePath
2388
+ || 'unknown';
2389
+
2390
+ debt.pendingEdits = (debt.pendingEdits || 0) + 1;
2391
+ debt.lastEditAt = new Date().toISOString();
2392
+ debt.edits = debt.edits || [];
2393
+ debt.edits.push({
2394
+ file: editedFile,
2395
+ at: debt.lastEditAt,
2396
+ });
2397
+
2398
+ if (debt.edits.length > 20) {
2399
+ debt.edits = debt.edits.slice(-20);
2400
+ }
2401
+
2402
+ try {
2403
+ fs.writeFileSync(signalFile, JSON.stringify(debt, null, 2), 'utf8');
2404
+ } catch {
2405
+ // Can't write signal — that's okay, guard just won't fire
2406
+ }
2407
+
2408
+ const output = JSON.stringify({
2409
+ hookEventName: 'PostToolUse',
2410
+ additionalContext: \`Iranti: file edited (\${path.basename(editedFile)}). \${debt.pendingEdits} pending edit(s) awaiting iranti_write. Write before making more edits.\`,
2411
+ });
2412
+
2413
+ require('fs').writeSync(1, output);
2414
+ `;
2415
+ }
2416
+ /**
2417
+ * Embedded JS content for the Codex/generic protocol reminder hook (UserPromptSubmit).
2418
+ * Injects a protocol reminder before every user prompt in Codex sessions.
2419
+ */
2420
+ function buildProtocolReminderHookScript() {
2421
+ return `#!/usr/bin/env node
2422
+ 'use strict';
2423
+ // Iranti protocol reminder hook — fires on UserPromptSubmit for any Iranti project.
2424
+ // Cross-platform: runs on Windows, macOS, Linux via Node.js.
2425
+ // Exits cleanly with no output for non-Iranti projects.
2426
+ const fs = require('fs');
2427
+ const path = require('path');
2428
+
2429
+ const envFile = path.join(process.cwd(), '.env.iranti');
2430
+ if (!fs.existsSync(envFile)) process.exit(0);
2431
+
2432
+ const content = [
2433
+ 'IRANTI PROTOCOL (required this turn):',
2434
+ '1. iranti_attend(phase=pre-response) BEFORE replying',
2435
+ '2. iranti_attend BEFORE each Read / Grep / Glob / Bash / WebSearch / WebFetch',
2436
+ '3. iranti_write AFTER each Edit or Write:',
2437
+ ' entity: project/[id]/file/[filename] -- not the broad project entity',
2438
+ ' value must include: absolutePath, lines, before, after, verify, why',
2439
+ '4. iranti_write AFTER each Bash that reveals system state (build, errors, ports, env)',
2440
+ '5. iranti_write AFTER each WebSearch/WebFetch -- write findings AND dead ends / 404s',
2441
+ '6. iranti_attend(phase=post-response) AFTER every response without exception',
2442
+ ].join('\\n') + '\\n';
2443
+ require('fs').writeSync(1, content);
2444
+ `;
2445
+ }
2234
2446
  const IRANTI_CLAUDE_ALLOWED_TOOLS = [
2235
2447
  'mcp__iranti__iranti_handshake',
2236
2448
  'mcp__iranti__iranti_attend',
@@ -2267,6 +2479,7 @@ function needsClaudeHookSettingsUpgrade(value) {
2267
2479
  if (!hooks) {
2268
2480
  return false;
2269
2481
  }
2482
+ // Legacy hook format detection — upgrade needed if old-style entries exist.
2270
2483
  for (const event of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
2271
2484
  const entries = hooks[event];
2272
2485
  if (!Array.isArray(entries) || entries.length === 0) {
@@ -2276,6 +2489,13 @@ function needsClaudeHookSettingsUpgrade(value) {
2276
2489
  return true;
2277
2490
  }
2278
2491
  }
2492
+ // Write-guard hooks missing — upgrade needed to add PreToolUse/PostToolUse.
2493
+ if (!Array.isArray(hooks.PreToolUse) || hooks.PreToolUse.length === 0) {
2494
+ return true;
2495
+ }
2496
+ if (!Array.isArray(hooks.PostToolUse) || hooks.PostToolUse.length === 0) {
2497
+ return true;
2498
+ }
2279
2499
  return false;
2280
2500
  }
2281
2501
  function mergeClaudePermissionAllowList(existing) {
@@ -2295,7 +2515,7 @@ function mergeClaudePermissionAllowList(existing) {
2295
2515
  allow,
2296
2516
  };
2297
2517
  }
2298
- function makeClaudeHookSettings(projectEnvPath, existing) {
2518
+ function makeClaudeHookSettings(projectEnvPath, existing, hookScriptDir) {
2299
2519
  const existingHooks = existing && isClaudeHooksObject(existing.hooks)
2300
2520
  ? existing.hooks
2301
2521
  : {};
@@ -2307,6 +2527,17 @@ function makeClaudeHookSettings(projectEnvPath, existing) {
2307
2527
  .map((value) => String(value))
2308
2528
  .filter((value) => value !== 'iranti')
2309
2529
  : undefined;
2530
+ const hooks = {
2531
+ ...existingHooks,
2532
+ SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
2533
+ UserPromptSubmit: [makeClaudeHookEntry('UserPromptSubmit', projectEnvPath)],
2534
+ Stop: [makeClaudeHookEntry('Stop', projectEnvPath)],
2535
+ };
2536
+ // Write-guard hooks: block edits until prior edits are written to Iranti.
2537
+ if (hookScriptDir) {
2538
+ hooks.PreToolUse = [makeClaudeWriteGuardHookEntry(hookScriptDir)];
2539
+ hooks.PostToolUse = [makeClaudeEditTrackerHookEntry(hookScriptDir)];
2540
+ }
2310
2541
  return {
2311
2542
  ...(existing ?? {}),
2312
2543
  enableAllProjectMcpServers: false,
@@ -2318,12 +2549,7 @@ function makeClaudeHookSettings(projectEnvPath, existing) {
2318
2549
  ...(existingEnabledMcpjsonServers
2319
2550
  ? { enabledMcpjsonServers: existingEnabledMcpjsonServers }
2320
2551
  : {}),
2321
- hooks: {
2322
- ...existingHooks,
2323
- SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
2324
- UserPromptSubmit: [makeClaudeHookEntry('UserPromptSubmit', projectEnvPath)],
2325
- Stop: [makeClaudeHookEntry('Stop', projectEnvPath)],
2326
- },
2552
+ hooks,
2327
2553
  };
2328
2554
  }
2329
2555
  async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force = false) {
@@ -2416,22 +2642,65 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
2416
2642
  }
2417
2643
  const claudeDir = path_1.default.join(projectPath, '.claude');
2418
2644
  await ensureDir(claudeDir);
2645
+ // Write hook scripts into .claude/ directory.
2646
+ const writeGuardHookFile = path_1.default.join(claudeDir, 'iranti-write-guard-hook.js');
2647
+ let writeGuardHookStatus = 'unchanged';
2648
+ const writeGuardContent = buildWriteGuardHookScript();
2649
+ if (!fs_1.default.existsSync(writeGuardHookFile)) {
2650
+ await writeText(writeGuardHookFile, writeGuardContent);
2651
+ writeGuardHookStatus = 'created';
2652
+ }
2653
+ else {
2654
+ const existing = fs_1.default.readFileSync(writeGuardHookFile, 'utf8');
2655
+ if (existing !== writeGuardContent) {
2656
+ await writeText(writeGuardHookFile, writeGuardContent);
2657
+ writeGuardHookStatus = 'updated';
2658
+ }
2659
+ }
2660
+ const editTrackerHookFile = path_1.default.join(claudeDir, 'iranti-edit-tracker-hook.js');
2661
+ let editTrackerHookStatus = 'unchanged';
2662
+ const editTrackerContent = buildEditTrackerHookScript();
2663
+ if (!fs_1.default.existsSync(editTrackerHookFile)) {
2664
+ await writeText(editTrackerHookFile, editTrackerContent);
2665
+ editTrackerHookStatus = 'created';
2666
+ }
2667
+ else {
2668
+ const existing = fs_1.default.readFileSync(editTrackerHookFile, 'utf8');
2669
+ if (existing !== editTrackerContent) {
2670
+ await writeText(editTrackerHookFile, editTrackerContent);
2671
+ editTrackerHookStatus = 'updated';
2672
+ }
2673
+ }
2674
+ const protocolReminderHookFile = path_1.default.join(claudeDir, 'iranti-protocol-hook.js');
2675
+ let protocolReminderHookStatus = 'unchanged';
2676
+ const protocolReminderContent = buildProtocolReminderHookScript();
2677
+ if (!fs_1.default.existsSync(protocolReminderHookFile)) {
2678
+ await writeText(protocolReminderHookFile, protocolReminderContent);
2679
+ protocolReminderHookStatus = 'created';
2680
+ }
2681
+ else {
2682
+ const existing = fs_1.default.readFileSync(protocolReminderHookFile, 'utf8');
2683
+ if (existing !== protocolReminderContent) {
2684
+ await writeText(protocolReminderHookFile, protocolReminderContent);
2685
+ protocolReminderHookStatus = 'updated';
2686
+ }
2687
+ }
2419
2688
  const settingsFile = path_1.default.join(claudeDir, 'settings.local.json');
2420
2689
  let settingsStatus = 'unchanged';
2421
2690
  if (!fs_1.default.existsSync(settingsFile)) {
2422
- await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
2691
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, undefined, claudeDir), null, 2)}\n`);
2423
2692
  settingsStatus = 'created';
2424
2693
  }
2425
2694
  else {
2426
2695
  const existingSettings = readJsonFile(settingsFile);
2427
2696
  if (existingSettings && typeof existingSettings === 'object' && !Array.isArray(existingSettings)) {
2428
2697
  if (force || needsClaudeHookSettingsUpgrade(existingSettings)) {
2429
- await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, existingSettings), null, 2)}\n`);
2698
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, existingSettings, claudeDir), null, 2)}\n`);
2430
2699
  settingsStatus = 'updated';
2431
2700
  }
2432
2701
  }
2433
2702
  else if (force) {
2434
- await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
2703
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, undefined, claudeDir), null, 2)}\n`);
2435
2704
  settingsStatus = 'updated';
2436
2705
  }
2437
2706
  }
@@ -2478,6 +2747,9 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
2478
2747
  files: [
2479
2748
  { path: mcpFile, status: mcpStatus },
2480
2749
  { path: vscodeMcpFile, status: vscodeMcpStatus },
2750
+ { path: writeGuardHookFile, status: writeGuardHookStatus },
2751
+ { path: editTrackerHookFile, status: editTrackerHookStatus },
2752
+ { path: protocolReminderHookFile, status: protocolReminderHookStatus },
2481
2753
  { path: settingsFile, status: settingsStatus },
2482
2754
  { path: claudeMdFile, status: claudeMdStatus },
2483
2755
  { path: irantiMdFile, status: irantiMdStatus },
@@ -2490,6 +2762,9 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
2490
2762
  settings: settingsStatus,
2491
2763
  claudeMd: claudeMdStatus,
2492
2764
  irantiMd: irantiMdStatus,
2765
+ writeGuardHook: writeGuardHookStatus,
2766
+ editTrackerHook: editTrackerHookStatus,
2767
+ protocolReminderHook: protocolReminderHookStatus,
2493
2768
  closeout,
2494
2769
  };
2495
2770
  }
@@ -8079,13 +8354,15 @@ async function claudeSetupCommand(args) {
8079
8354
  createdSettings += 1;
8080
8355
  if (result.settings === 'updated')
8081
8356
  updatedSettings += 1;
8082
- if (result.mcp === 'unchanged' && result.vscodeMcp === 'unchanged' && result.settings === 'unchanged' && result.irantiMd === 'unchanged')
8357
+ if (result.mcp === 'unchanged' && result.vscodeMcp === 'unchanged' && result.settings === 'unchanged' && result.irantiMd === 'unchanged' && result.writeGuardHook === 'unchanged' && result.editTrackerHook === 'unchanged')
8083
8358
  unchanged += 1;
8084
8359
  console.log(` ${projectPath}`);
8085
- console.log(` mcp ${result.mcp}`);
8086
- console.log(` vscode ${result.vscodeMcp}`);
8087
- console.log(` settings ${result.settings}`);
8088
- console.log(` iranti.md ${result.irantiMd}`);
8360
+ console.log(` mcp ${result.mcp}`);
8361
+ console.log(` vscode ${result.vscodeMcp}`);
8362
+ console.log(` settings ${result.settings}`);
8363
+ console.log(` iranti.md ${result.irantiMd}`);
8364
+ console.log(` write-guard ${result.writeGuardHook}`);
8365
+ console.log(` edit-tracker ${result.editTrackerHook}`);
8089
8366
  }
8090
8367
  console.log('');
8091
8368
  console.log('Summary:');
@@ -8114,19 +8391,25 @@ async function claudeSetupCommand(args) {
8114
8391
  }
8115
8392
  const result = await writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force);
8116
8393
  console.log(`${okLabel()} Claude Code integration scaffolded`);
8117
- console.log(` project ${projectPath}`);
8118
- console.log(` binding ${projectEnvPath}`);
8119
- console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
8120
- console.log(` vscode ${path_1.default.join(projectPath, '.vscode', 'mcp.json')}`);
8121
- console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
8122
- console.log(` claude.md ${path_1.default.join(projectPath, 'CLAUDE.md')}`);
8123
- console.log(` iranti.md ${path_1.default.join(projectPath, 'IRANTI.md')}`);
8124
- console.log(` mcp status ${result.mcp}`);
8125
- console.log(` vscode status ${result.vscodeMcp}`);
8126
- console.log(` settings status ${result.settings}`);
8127
- console.log(` claude.md status ${result.claudeMd}`);
8128
- console.log(` iranti.md status ${result.irantiMd}`);
8129
- console.log(` memory closeout ${result.closeout.status} (${result.closeout.detail})`);
8394
+ console.log(` project ${projectPath}`);
8395
+ console.log(` binding ${projectEnvPath}`);
8396
+ console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
8397
+ console.log(` vscode ${path_1.default.join(projectPath, '.vscode', 'mcp.json')}`);
8398
+ console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
8399
+ console.log(` claude.md ${path_1.default.join(projectPath, 'CLAUDE.md')}`);
8400
+ console.log(` iranti.md ${path_1.default.join(projectPath, 'IRANTI.md')}`);
8401
+ console.log(` write-guard ${path_1.default.join(projectPath, '.claude', 'iranti-write-guard-hook.js')}`);
8402
+ console.log(` edit-tracker ${path_1.default.join(projectPath, '.claude', 'iranti-edit-tracker-hook.js')}`);
8403
+ console.log(` protocol ${path_1.default.join(projectPath, '.claude', 'iranti-protocol-hook.js')}`);
8404
+ console.log(` mcp status ${result.mcp}`);
8405
+ console.log(` vscode status ${result.vscodeMcp}`);
8406
+ console.log(` settings status ${result.settings}`);
8407
+ console.log(` claude.md status ${result.claudeMd}`);
8408
+ console.log(` iranti.md status ${result.irantiMd}`);
8409
+ console.log(` write-guard status ${result.writeGuardHook}`);
8410
+ console.log(` edit-tracker status ${result.editTrackerHook}`);
8411
+ console.log(` protocol status ${result.protocolReminderHook}`);
8412
+ console.log(` memory closeout ${result.closeout.status} (${result.closeout.detail})`);
8130
8413
  console.log(`${infoLabel()} Next: open Claude Code in this project and verify Iranti tools are available.`);
8131
8414
  }
8132
8415
  async function chatCommand(args) {