iranti 0.2.6 → 0.2.8

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
@@ -354,6 +354,19 @@ Use `--agent`, `--provider`, and `--model` to pin the session identity and model
354
354
  The chat surface now includes slash commands for fact history, relationships, conflict-resolution handoff, and confidence updates in addition to memory search/write operations.
355
355
  Guide: [`docs/guides/chat.md`](docs/guides/chat.md)
356
356
 
357
+ ### Manual Attendant Inspection
358
+
359
+ For debugging and operator visibility, Iranti also exposes manual Attendant commands:
360
+
361
+ ```bash
362
+ iranti handshake --task "Working on ProofScript repo"
363
+ iranti attend "What did we decide about the parser?" --context-file transcript.txt
364
+ ```
365
+
366
+ Both commands accept `--json`.
367
+ They are useful for verifying what the Attendant would load or inject for a given agent and project binding.
368
+ They are not a replacement for Claude Code hooks or MCP tools in normal use.
369
+
357
370
  ---
358
371
 
359
372
  ## Install Strategy (Double Layer)
@@ -16,9 +16,11 @@ const net_1 = __importDefault(require("net"));
16
16
  const client_1 = require("../src/library/client");
17
17
  const apiKeys_1 = require("../src/security/apiKeys");
18
18
  const escalationPaths_1 = require("../src/lib/escalationPaths");
19
+ const runtimeEnv_1 = require("../src/lib/runtimeEnv");
19
20
  const resolutionist_1 = require("../src/resolutionist");
20
21
  const chat_1 = require("../src/chat");
21
22
  const backends_1 = require("../src/library/backends");
23
+ const sdk_1 = require("../src/sdk");
22
24
  const PROVIDER_ENV_KEYS = {
23
25
  mock: null,
24
26
  ollama: null,
@@ -650,28 +652,216 @@ function makeIrantiMcpServerConfig() {
650
652
  args: ['mcp'],
651
653
  };
652
654
  }
653
- function makeClaudeHookSettings(projectEnvPath) {
654
- const hookArgs = (event) => {
655
- const args = ['claude-hook', '--event', event];
656
- if (projectEnvPath) {
657
- args.push('--project-env', projectEnvPath);
658
- }
659
- return args;
655
+ function applyEnvMap(vars) {
656
+ for (const [key, value] of Object.entries(vars)) {
657
+ process.env[key] = value;
658
+ }
659
+ }
660
+ async function resolveAttendantCliTarget(args) {
661
+ const explicitAgent = getFlag(args, 'agent')?.trim();
662
+ const explicitProjectEnv = getFlag(args, 'project-env');
663
+ const instanceName = getFlag(args, 'instance');
664
+ let envSource = 'project';
665
+ let envFile = null;
666
+ let projectEnvFile;
667
+ let instanceEnvFile;
668
+ if (instanceName) {
669
+ const scope = normalizeScope(getFlag(args, 'scope'));
670
+ const root = resolveInstallRoot(args, scope);
671
+ const loaded = await loadInstanceEnv(root, instanceName);
672
+ applyEnvMap(loaded.env);
673
+ envSource = `instance:${instanceName}`;
674
+ envFile = loaded.envFile;
675
+ instanceEnvFile = loaded.envFile;
676
+ }
677
+ else {
678
+ const cwd = path_1.default.resolve(getFlag(args, 'project') ?? process.cwd());
679
+ const loaded = (0, runtimeEnv_1.loadRuntimeEnv)({
680
+ cwd,
681
+ projectEnvFile: explicitProjectEnv ? path_1.default.resolve(explicitProjectEnv) : undefined,
682
+ });
683
+ envSource = loaded.projectEnvFile ? 'project-binding' : 'environment';
684
+ envFile = loaded.projectEnvFile ?? loaded.instanceEnvFile ?? null;
685
+ projectEnvFile = loaded.projectEnvFile;
686
+ instanceEnvFile = loaded.instanceEnvFile;
687
+ }
688
+ const connectionString = process.env.DATABASE_URL?.trim();
689
+ if (!connectionString) {
690
+ throw new Error('DATABASE_URL is required. Run from a bound project, pass --instance <name>, or set DATABASE_URL first.');
691
+ }
692
+ const agentId = explicitAgent
693
+ || process.env.IRANTI_AGENT_ID?.trim()
694
+ || process.env.IRANTI_CLAUDE_AGENT_ID?.trim()
695
+ || 'iranti_cli';
696
+ return {
697
+ envSource,
698
+ envFile,
699
+ projectEnvFile,
700
+ instanceEnvFile,
701
+ agentId,
702
+ iranti: new sdk_1.Iranti({ connectionString }),
660
703
  };
704
+ }
705
+ function truncateText(value, limit) {
706
+ return value.length <= limit ? value : `${value.slice(0, limit - 3)}...`;
707
+ }
708
+ function resolveRecentMessages(args) {
709
+ const inline = getFlag(args, 'recent');
710
+ const file = getFlag(args, 'recent-file');
711
+ if (inline) {
712
+ return inline
713
+ .split('||')
714
+ .map((item) => item.trim())
715
+ .filter(Boolean);
716
+ }
717
+ if (file) {
718
+ const content = fs_1.default.readFileSync(path_1.default.resolve(file), 'utf-8');
719
+ return content
720
+ .split(/\r?\n/)
721
+ .map((line) => line.trim())
722
+ .filter(Boolean);
723
+ }
724
+ return [];
725
+ }
726
+ function parsePositiveInteger(raw, label) {
727
+ if (!raw)
728
+ return undefined;
729
+ const parsed = Number.parseInt(raw, 10);
730
+ if (!Number.isFinite(parsed) || parsed <= 0) {
731
+ throw new Error(`${label} must be a positive integer.`);
732
+ }
733
+ return parsed;
734
+ }
735
+ function resolveContextText(args) {
736
+ const inline = getFlag(args, 'context');
737
+ if (inline)
738
+ return inline;
739
+ const file = getFlag(args, 'context-file');
740
+ if (file) {
741
+ return fs_1.default.readFileSync(path_1.default.resolve(file), 'utf-8');
742
+ }
743
+ return '';
744
+ }
745
+ function resolveAttendMessage(args) {
746
+ const fromFlag = getFlag(args, 'message');
747
+ if (fromFlag?.trim())
748
+ return fromFlag.trim();
749
+ const fromPositionals = args.positionals.join(' ').trim();
750
+ if (fromPositionals)
751
+ return fromPositionals;
752
+ throw new Error('Missing latest message. Usage: iranti attend [message] [--message <text>] [--context <text>] [--json]');
753
+ }
754
+ function printHandshakeResult(target, task, result) {
755
+ console.log(bold('Iranti handshake'));
756
+ console.log(` agent ${target.agentId}`);
757
+ console.log(` env source ${target.envSource}`);
758
+ if (target.envFile)
759
+ console.log(` env file ${target.envFile}`);
760
+ console.log(` task ${task}`);
761
+ console.log(` inferred task ${result.inferredTaskType}`);
762
+ console.log(` memory facts ${result.workingMemory.length}`);
763
+ console.log(` generated ${result.briefGeneratedAt}`);
764
+ console.log('');
765
+ console.log(`Rules: ${truncateText(result.operatingRules, 160)}`);
766
+ if (result.workingMemory.length === 0) {
767
+ console.log('');
768
+ console.log('No working memory entries loaded.');
769
+ return;
770
+ }
771
+ console.log('');
772
+ for (const entry of result.workingMemory) {
773
+ console.log(`- ${entry.entityKey} | ${entry.summary} | ${entry.confidence} | ${entry.source}`);
774
+ }
775
+ }
776
+ function printAttendResult(target, latestMessage, result) {
777
+ console.log(bold('Iranti attend'));
778
+ console.log(` agent ${target.agentId}`);
779
+ console.log(` env source ${target.envSource}`);
780
+ if (target.envFile)
781
+ console.log(` env file ${target.envFile}`);
782
+ console.log(` message ${truncateText(latestMessage, 120)}`);
783
+ console.log(` inject ${result.shouldInject ? 'yes' : 'no'}`);
784
+ console.log(` reason ${result.reason}`);
785
+ console.log(` method ${result.decision.method}`);
786
+ console.log(` confidence ${result.decision.confidence}`);
787
+ console.log(` explanation ${result.decision.explanation}`);
788
+ console.log(` facts ${result.facts.length}`);
789
+ if (result.facts.length === 0) {
790
+ console.log('');
791
+ console.log('No facts selected for injection.');
792
+ return;
793
+ }
794
+ console.log('');
795
+ for (const fact of result.facts) {
796
+ console.log(`- ${fact.entityKey} | ${fact.summary} | ${fact.confidence} | ${fact.source}`);
797
+ }
798
+ }
799
+ function quoteClaudeHookArg(value) {
800
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
801
+ return value;
802
+ }
803
+ return `"${value.replace(/(["\\])/g, '\\$1')}"`;
804
+ }
805
+ function makeClaudeHookCommand(event, projectEnvPath) {
806
+ const parts = ['iranti', 'claude-hook', '--event', event];
807
+ if (projectEnvPath) {
808
+ parts.push('--project-env', quoteClaudeHookArg(projectEnvPath));
809
+ }
810
+ return parts.join(' ');
811
+ }
812
+ function makeClaudeHookEntry(event, projectEnvPath) {
813
+ return {
814
+ matcher: '',
815
+ hooks: [
816
+ {
817
+ type: 'command',
818
+ command: makeClaudeHookCommand(event, projectEnvPath),
819
+ },
820
+ ],
821
+ };
822
+ }
823
+ function isClaudeHooksObject(value) {
824
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
825
+ }
826
+ function isLegacyIrantiClaudeHookEntry(value) {
827
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
828
+ return false;
829
+ }
830
+ const entry = value;
831
+ return entry.command === 'iranti'
832
+ && Array.isArray(entry.args)
833
+ && entry.args[0] === 'claude-hook';
834
+ }
835
+ function needsClaudeHookSettingsUpgrade(value) {
836
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
837
+ return false;
838
+ }
839
+ const settings = value;
840
+ const hooks = isClaudeHooksObject(settings.hooks) ? settings.hooks : null;
841
+ if (!hooks) {
842
+ return false;
843
+ }
844
+ for (const event of ['SessionStart', 'UserPromptSubmit']) {
845
+ const entries = hooks[event];
846
+ if (!Array.isArray(entries) || entries.length === 0) {
847
+ continue;
848
+ }
849
+ if (entries.some((entry) => isLegacyIrantiClaudeHookEntry(entry))) {
850
+ return true;
851
+ }
852
+ }
853
+ return false;
854
+ }
855
+ function makeClaudeHookSettings(projectEnvPath, existing) {
856
+ const existingHooks = existing && isClaudeHooksObject(existing.hooks)
857
+ ? existing.hooks
858
+ : {};
661
859
  return {
860
+ ...(existing ?? {}),
662
861
  hooks: {
663
- SessionStart: [
664
- {
665
- command: 'iranti',
666
- args: hookArgs('SessionStart'),
667
- },
668
- ],
669
- UserPromptSubmit: [
670
- {
671
- command: 'iranti',
672
- args: hookArgs('UserPromptSubmit'),
673
- },
674
- ],
862
+ ...existingHooks,
863
+ SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
864
+ UserPromptSubmit: [makeClaudeHookEntry('UserPromptSubmit', projectEnvPath)],
675
865
  },
676
866
  };
677
867
  }
@@ -725,9 +915,18 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
725
915
  await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
726
916
  settingsStatus = 'created';
727
917
  }
728
- else if (force) {
729
- await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
730
- settingsStatus = 'updated';
918
+ else {
919
+ const existingSettings = readJsonFile(settingsFile);
920
+ if (existingSettings && typeof existingSettings === 'object' && !Array.isArray(existingSettings)) {
921
+ if (force || needsClaudeHookSettingsUpgrade(existingSettings)) {
922
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, existingSettings), null, 2)}\n`);
923
+ settingsStatus = 'updated';
924
+ }
925
+ }
926
+ else if (force) {
927
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
928
+ settingsStatus = 'updated';
929
+ }
731
930
  }
732
931
  return {
733
932
  mcp: mcpStatus,
@@ -1189,21 +1388,96 @@ function isPathInside(parentDir, childDir) {
1189
1388
  const child = normalizePathForCompare(childDir);
1190
1389
  return child === parent || child.startsWith(`${parent}/`);
1191
1390
  }
1192
- function resolveSpawnExecutable(executable) {
1193
- if (process.platform !== 'win32')
1194
- return executable;
1195
- if (executable === 'npm')
1196
- return 'npm.cmd';
1197
- if (executable === 'npx')
1198
- return 'npx.cmd';
1199
- return executable;
1391
+ function hasCommandInstalled(command) {
1392
+ try {
1393
+ const proc = process.platform === 'win32'
1394
+ ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', `${command} --version`], { stdio: 'ignore' })
1395
+ : (0, child_process_1.spawnSync)(command, ['--version'], { stdio: 'ignore' });
1396
+ return proc.status === 0;
1397
+ }
1398
+ catch {
1399
+ return false;
1400
+ }
1200
1401
  }
1201
- function runCommandCapture(executable, args, cwd) {
1202
- const proc = (0, child_process_1.spawnSync)(resolveSpawnExecutable(executable), args, {
1203
- cwd,
1204
- encoding: 'utf8',
1205
- stdio: ['ignore', 'pipe', 'pipe'],
1402
+ async function canConnectTcp(port, host = '127.0.0.1', timeoutMs = 800) {
1403
+ return await new Promise((resolve) => {
1404
+ const socket = net_1.default.createConnection({ port, host });
1405
+ let settled = false;
1406
+ const finish = (value) => {
1407
+ if (settled)
1408
+ return;
1409
+ settled = true;
1410
+ socket.destroy();
1411
+ resolve(value);
1412
+ };
1413
+ socket.setTimeout(timeoutMs);
1414
+ socket.on('connect', () => finish(true));
1415
+ socket.on('timeout', () => finish(false));
1416
+ socket.on('error', () => finish(false));
1206
1417
  });
1418
+ }
1419
+ async function collectDependencyChecks() {
1420
+ const docker = hasDockerInstalled();
1421
+ const psql = hasCommandInstalled('psql');
1422
+ const pgIsReady = hasCommandInstalled('pg_isready');
1423
+ const postgresPort = await canConnectTcp(5432);
1424
+ const checks = [
1425
+ {
1426
+ name: 'docker',
1427
+ status: docker ? 'pass' : 'warn',
1428
+ detail: docker ? 'Docker is installed.' : 'Docker is not installed or not on PATH.',
1429
+ },
1430
+ {
1431
+ name: 'psql',
1432
+ status: psql ? 'pass' : 'warn',
1433
+ detail: psql ? 'psql is installed.' : 'psql is not installed or not on PATH.',
1434
+ },
1435
+ {
1436
+ name: 'pg_isready',
1437
+ status: pgIsReady ? 'pass' : 'warn',
1438
+ detail: pgIsReady ? 'pg_isready is installed.' : 'pg_isready is not installed or not on PATH.',
1439
+ },
1440
+ {
1441
+ name: 'localhost:5432',
1442
+ status: postgresPort ? 'pass' : 'warn',
1443
+ detail: postgresPort ? 'A PostgreSQL listener appears reachable on localhost:5432.' : 'Nothing is reachable on localhost:5432 right now.',
1444
+ },
1445
+ ];
1446
+ return checks;
1447
+ }
1448
+ function printDependencyChecks(checks) {
1449
+ console.log(bold('Dependency check'));
1450
+ for (const check of checks) {
1451
+ const marker = check.status === 'pass' ? okLabel('PASS') : warnLabel('WARN');
1452
+ console.log(`${marker} ${check.name} — ${check.detail}`);
1453
+ }
1454
+ if (checks.every((check) => check.status === 'warn')) {
1455
+ console.log(`${warnLabel()} No PostgreSQL tooling or reachable local Postgres was detected. You can still continue if you plan to use managed Postgres, but local setup will be rough until you install PostgreSQL tooling or Docker.`);
1456
+ }
1457
+ }
1458
+ function quoteForCmd(arg) {
1459
+ if (arg.length === 0)
1460
+ return '""';
1461
+ if (!/[ \t"&()<>|^]/.test(arg))
1462
+ return arg;
1463
+ return `"${arg.replace(/"/g, '\\"')}"`;
1464
+ }
1465
+ function runCommandCapture(executable, args, cwd) {
1466
+ const proc = process.platform === 'win32'
1467
+ ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', [
1468
+ '/d',
1469
+ '/c',
1470
+ [executable, ...args].map(quoteForCmd).join(' '),
1471
+ ], {
1472
+ cwd,
1473
+ encoding: 'utf8',
1474
+ stdio: ['ignore', 'pipe', 'pipe'],
1475
+ })
1476
+ : (0, child_process_1.spawnSync)(executable, args, {
1477
+ cwd,
1478
+ encoding: 'utf8',
1479
+ stdio: ['ignore', 'pipe', 'pipe'],
1480
+ });
1207
1481
  return {
1208
1482
  status: proc.status,
1209
1483
  stdout: proc.stdout ?? '',
@@ -1211,10 +1485,19 @@ function runCommandCapture(executable, args, cwd) {
1211
1485
  };
1212
1486
  }
1213
1487
  function runCommandInteractive(step) {
1214
- const proc = (0, child_process_1.spawnSync)(resolveSpawnExecutable(step.executable), step.args, {
1215
- cwd: step.cwd,
1216
- stdio: 'inherit',
1217
- });
1488
+ const proc = process.platform === 'win32'
1489
+ ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', [
1490
+ '/d',
1491
+ '/c',
1492
+ [step.executable, ...step.args].map(quoteForCmd).join(' '),
1493
+ ], {
1494
+ cwd: step.cwd,
1495
+ stdio: 'inherit',
1496
+ })
1497
+ : (0, child_process_1.spawnSync)(step.executable, step.args, {
1498
+ cwd: step.cwd,
1499
+ stdio: 'inherit',
1500
+ });
1218
1501
  return proc.status;
1219
1502
  }
1220
1503
  function detectPythonLauncher() {
@@ -1350,8 +1633,8 @@ function detectUpgradeContext(args) {
1350
1633
  const repoCheckout = fs_1.default.existsSync(path_1.default.join(packageRootPath, '.git'));
1351
1634
  const repoDirty = repoCheckout ? repoIsDirty(packageRootPath) : false;
1352
1635
  const globalNpmRoot = detectGlobalNpmRoot();
1353
- const globalNpmInstall = globalNpmRoot !== null && isPathInside(globalNpmRoot, packageRootPath);
1354
- const globalNpmVersion = globalNpmInstall ? detectGlobalNpmInstalledVersion() : null;
1636
+ const globalNpmVersion = detectGlobalNpmInstalledVersion();
1637
+ const globalNpmInstall = globalNpmVersion !== null;
1355
1638
  const python = detectPythonLauncher();
1356
1639
  const pythonVersion = detectPythonInstalledVersion(python);
1357
1640
  const availableTargets = [];
@@ -1715,6 +1998,9 @@ async function setupCommand(args) {
1715
1998
  throw new Error('Use either --config <file> or --defaults, not both.');
1716
1999
  }
1717
2000
  if (configPath || useDefaults) {
2001
+ const dependencyChecks = await collectDependencyChecks();
2002
+ printDependencyChecks(dependencyChecks);
2003
+ console.log('');
1718
2004
  const plan = configPath ? parseSetupConfig(configPath) : defaultsSetupPlan(args);
1719
2005
  const result = await executeSetupPlan(plan);
1720
2006
  console.log(bold('Setup complete'));
@@ -1746,6 +2032,8 @@ async function setupCommand(args) {
1746
2032
  console.log(bold('Iranti setup'));
1747
2033
  console.log('This wizard will get Iranti set up: install a runtime, create or update an instance, connect provider keys, create a usable Iranti API key, and optionally bind one or more project folders.');
1748
2034
  console.log('');
2035
+ printDependencyChecks(await collectDependencyChecks());
2036
+ console.log('');
1749
2037
  let result = null;
1750
2038
  await withPromptSession(async (prompt) => {
1751
2039
  let setupMode = 'shared';
@@ -2351,6 +2639,7 @@ async function upgradeCommand(args) {
2351
2639
  async function installCommand(args) {
2352
2640
  const scope = normalizeScope(getFlag(args, 'scope'));
2353
2641
  const root = resolveInstallRoot(args, scope);
2642
+ const dependencyChecks = await collectDependencyChecks();
2354
2643
  await ensureDir(root);
2355
2644
  await ensureDir(path_1.default.join(root, 'instances'));
2356
2645
  await ensureDir(path_1.default.join(root, 'logs'));
@@ -2365,6 +2654,8 @@ async function installCommand(args) {
2365
2654
  console.log(`${okLabel()} Iranti runtime initialized`);
2366
2655
  console.log(` scope: ${scope}`);
2367
2656
  console.log(` root : ${root}`);
2657
+ console.log('');
2658
+ printDependencyChecks(dependencyChecks);
2368
2659
  console.log(`Next: iranti instance create local --port 3001`);
2369
2660
  }
2370
2661
  async function createInstanceCommand(args) {
@@ -2792,6 +3083,67 @@ async function resolveCommand(args) {
2792
3083
  const escalationDir = explicitDir ? path_1.default.resolve(explicitDir) : (0, escalationPaths_1.getEscalationPaths)().root;
2793
3084
  await (0, resolutionist_1.resolveInteractive)(escalationDir);
2794
3085
  }
3086
+ async function handshakeCommand(args) {
3087
+ const json = hasFlag(args, 'json');
3088
+ const target = await resolveAttendantCliTarget(args);
3089
+ const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
3090
+ const recentMessages = resolveRecentMessages(args);
3091
+ const result = await target.iranti.handshake({
3092
+ agent: target.agentId,
3093
+ task,
3094
+ recentMessages,
3095
+ });
3096
+ if (json) {
3097
+ console.log(JSON.stringify({
3098
+ agent: target.agentId,
3099
+ envSource: target.envSource,
3100
+ envFile: target.envFile,
3101
+ task,
3102
+ recentMessages,
3103
+ result,
3104
+ }, null, 2));
3105
+ return;
3106
+ }
3107
+ printHandshakeResult(target, task, result);
3108
+ console.log('');
3109
+ console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
3110
+ }
3111
+ async function attendCommand(args) {
3112
+ const json = hasFlag(args, 'json');
3113
+ const target = await resolveAttendantCliTarget(args);
3114
+ const latestMessage = resolveAttendMessage(args);
3115
+ const currentContext = resolveContextText(args);
3116
+ const maxFacts = parsePositiveInteger(getFlag(args, 'max-facts'), 'max-facts');
3117
+ const entityHint = getFlag(args, 'entity-hint')?.trim();
3118
+ if (entityHint && !entityHint.includes('/')) {
3119
+ throw new Error('entity-hint must use entityType/entityId format.');
3120
+ }
3121
+ const result = await target.iranti.attend({
3122
+ agent: target.agentId,
3123
+ currentContext,
3124
+ latestMessage,
3125
+ forceInject: hasFlag(args, 'force'),
3126
+ maxFacts,
3127
+ entityHints: entityHint ? [entityHint] : undefined,
3128
+ });
3129
+ if (json) {
3130
+ console.log(JSON.stringify({
3131
+ agent: target.agentId,
3132
+ envSource: target.envSource,
3133
+ envFile: target.envFile,
3134
+ latestMessage,
3135
+ currentContext,
3136
+ maxFacts: maxFacts ?? null,
3137
+ entityHints: entityHint ? [entityHint] : [],
3138
+ forceInject: hasFlag(args, 'force'),
3139
+ result,
3140
+ }, null, 2));
3141
+ return;
3142
+ }
3143
+ printAttendResult(target, latestMessage, result);
3144
+ console.log('');
3145
+ console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
3146
+ }
2795
3147
  function printClaudeSetupHelp() {
2796
3148
  console.log([
2797
3149
  'Scaffold Claude Code MCP and hook files for the current project.',
@@ -2984,6 +3336,8 @@ Project-level:
2984
3336
  iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]
2985
3337
  iranti status [--scope user|system] [--json]
2986
3338
  iranti upgrade [--check] [--dry-run] [--yes] [--all] [--target auto|npm-global|npm-repo|python[,python]] [--json]
3339
+ iranti handshake [--instance <name> | --project-env <file>] [--agent <id>] [--task <text>] [--recent <msg1||msg2>] [--recent-file <path>] [--json]
3340
+ iranti attend [message] [--instance <name> | --project-env <file>] [--agent <id>] [--context <text> | --context-file <path>] [--entity-hint <entity>] [--force] [--max-facts <n>] [--json]
2987
3341
  iranti chat [--agent <agent-id>] [--provider <provider>] [--model <model>]
2988
3342
  iranti resolve [--dir <escalation-dir>]
2989
3343
 
@@ -3089,6 +3443,14 @@ async function main() {
3089
3443
  await upgradeCommand(args);
3090
3444
  return;
3091
3445
  }
3446
+ if (args.command === 'handshake') {
3447
+ await handshakeCommand(args);
3448
+ return;
3449
+ }
3450
+ if (args.command === 'attend') {
3451
+ await attendCommand(args);
3452
+ return;
3453
+ }
3092
3454
  if (args.command === 'chat') {
3093
3455
  await chatCommand(args);
3094
3456
  return;
@@ -144,7 +144,7 @@ async function main() {
144
144
  await ensureDefaultAgent(iranti);
145
145
  const server = new mcp_js_1.McpServer({
146
146
  name: 'iranti-mcp',
147
- version: '0.2.6',
147
+ version: '0.2.8',
148
148
  });
149
149
  server.registerTool('iranti_handshake', {
150
150
  description: `Initialize or refresh an agent's working-memory brief for the current task.
@@ -15,7 +15,7 @@ const STAFF_ENTRIES = [
15
15
  entityId: 'librarian',
16
16
  key: 'operating_rules',
17
17
  valueRaw: {
18
- version: '0.2.6',
18
+ version: '0.2.8',
19
19
  rules: [
20
20
  'All writes from external agents go through the Librarian — never directly to the database',
21
21
  'Check for existing entries before every write',
@@ -39,7 +39,7 @@ const STAFF_ENTRIES = [
39
39
  entityId: 'attendant',
40
40
  key: 'operating_rules',
41
41
  valueRaw: {
42
- version: '0.2.6',
42
+ version: '0.2.8',
43
43
  rules: [
44
44
  'Assigned one-per-external-agent — serve the agent, not the user',
45
45
  'On handshake: read AGENTS.md and MCP config, query Librarian for relevant rules and task context',
@@ -61,7 +61,7 @@ const STAFF_ENTRIES = [
61
61
  entityId: 'archivist',
62
62
  key: 'operating_rules',
63
63
  valueRaw: {
64
- version: '0.2.6',
64
+ version: '0.2.8',
65
65
  rules: [
66
66
  'Run on schedule or when conflict flags exceed threshold — not on every write',
67
67
  'Scan for expired, low-confidence, flagged, and duplicate entries',
@@ -82,7 +82,7 @@ const STAFF_ENTRIES = [
82
82
  entityType: 'system',
83
83
  entityId: 'library',
84
84
  key: 'schema_version',
85
- valueRaw: { version: '0.2.6' },
85
+ valueRaw: { version: '0.2.8' },
86
86
  valueSummary: 'Current Library schema version.',
87
87
  confidence: 100,
88
88
  source: 'seed',
@@ -95,7 +95,7 @@ const STAFF_ENTRIES = [
95
95
  key: 'initialization_log',
96
96
  valueRaw: {
97
97
  initializedAt: new Date().toISOString(),
98
- seedVersion: '0.2.6',
98
+ seedVersion: '0.2.8',
99
99
  },
100
100
  valueSummary: 'Record of when and how this Library was initialized.',
101
101
  confidence: 100,
@@ -108,7 +108,7 @@ const STAFF_ENTRIES = [
108
108
  entityId: 'ontology',
109
109
  key: 'core_schema',
110
110
  valueRaw: {
111
- version: '0.2.6',
111
+ version: '0.2.8',
112
112
  states: ['candidate', 'provisional', 'canonical'],
113
113
  coreEntityTypes: [
114
114
  'person',
@@ -156,7 +156,7 @@ const STAFF_ENTRIES = [
156
156
  entityId: 'ontology',
157
157
  key: 'extension_registry',
158
158
  valueRaw: {
159
- version: '0.2.6',
159
+ version: '0.2.8',
160
160
  namespaces: {
161
161
  education: {
162
162
  status: 'provisional',
@@ -187,7 +187,7 @@ const STAFF_ENTRIES = [
187
187
  entityId: 'ontology',
188
188
  key: 'candidate_terms',
189
189
  valueRaw: {
190
- version: '0.2.6',
190
+ version: '0.2.8',
191
191
  terms: [],
192
192
  },
193
193
  valueSummary: 'Staging area for ontology terms detected repeatedly but not yet promoted.',
@@ -201,7 +201,7 @@ const STAFF_ENTRIES = [
201
201
  entityId: 'ontology',
202
202
  key: 'promotion_policy',
203
203
  valueRaw: {
204
- version: '0.2.6',
204
+ version: '0.2.8',
205
205
  candidateToProvisional: {
206
206
  minSeenCount: 3,
207
207
  minDistinctAgents: 2,
@@ -237,7 +237,7 @@ const STAFF_ENTRIES = [
237
237
  entityId: 'ontology',
238
238
  key: 'change_log',
239
239
  valueRaw: {
240
- version: '0.2.6',
240
+ version: '0.2.8',
241
241
  events: [
242
242
  {
243
243
  at: new Date().toISOString(),
@@ -69,7 +69,7 @@ app.use(express_1.default.json({ limit: process.env.IRANTI_MAX_BODY_BYTES ?? '25
69
69
  app.get(ROUTES.health, (_req, res) => {
70
70
  res.json({
71
71
  status: 'ok',
72
- version: '0.2.6',
72
+ version: '0.2.8',
73
73
  provider: process.env.LLM_PROVIDER ?? 'mock',
74
74
  });
75
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iranti",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [