iranti 0.2.51 → 0.3.2

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.
Files changed (163) hide show
  1. package/README.md +30 -17
  2. package/dist/scripts/api-key-create.js +1 -1
  3. package/dist/scripts/api-key-list.js +1 -1
  4. package/dist/scripts/api-key-revoke.js +1 -1
  5. package/dist/scripts/claude-code-memory-hook.js +116 -30
  6. package/dist/scripts/codex-setup.js +86 -4
  7. package/dist/scripts/iranti-cli.js +1359 -57
  8. package/dist/scripts/iranti-mcp.js +578 -75
  9. package/dist/scripts/seed.js +11 -6
  10. package/dist/scripts/setup.js +1 -1
  11. package/dist/src/api/healthChecks.d.ts +29 -0
  12. package/dist/src/api/healthChecks.d.ts.map +1 -0
  13. package/dist/src/api/healthChecks.js +72 -0
  14. package/dist/src/api/healthChecks.js.map +1 -0
  15. package/dist/src/api/middleware/validation.d.ts +22 -0
  16. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  17. package/dist/src/api/middleware/validation.js +93 -3
  18. package/dist/src/api/middleware/validation.js.map +1 -1
  19. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  20. package/dist/src/api/routes/knowledge.js +53 -0
  21. package/dist/src/api/routes/knowledge.js.map +1 -1
  22. package/dist/src/api/routes/memory.d.ts.map +1 -1
  23. package/dist/src/api/routes/memory.js +73 -9
  24. package/dist/src/api/routes/memory.js.map +1 -1
  25. package/dist/src/api/server.js +38 -43
  26. package/dist/src/api/server.js.map +1 -1
  27. package/dist/src/attendant/AttendantInstance.d.ts +135 -2
  28. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  29. package/dist/src/attendant/AttendantInstance.js +1836 -93
  30. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  31. package/dist/src/attendant/index.d.ts +1 -1
  32. package/dist/src/attendant/index.d.ts.map +1 -1
  33. package/dist/src/attendant/index.js +1 -1
  34. package/dist/src/attendant/index.js.map +1 -1
  35. package/dist/src/attendant/registry.d.ts.map +1 -1
  36. package/dist/src/attendant/registry.js +2 -0
  37. package/dist/src/attendant/registry.js.map +1 -1
  38. package/dist/src/chat/index.d.ts +23 -0
  39. package/dist/src/chat/index.d.ts.map +1 -1
  40. package/dist/src/chat/index.js +111 -22
  41. package/dist/src/chat/index.js.map +1 -1
  42. package/dist/src/generated/prisma/browser.d.ts +5 -0
  43. package/dist/src/generated/prisma/browser.d.ts.map +1 -1
  44. package/dist/src/generated/prisma/client.d.ts +5 -0
  45. package/dist/src/generated/prisma/client.d.ts.map +1 -1
  46. package/dist/src/generated/prisma/commonInputTypes.d.ts +48 -0
  47. package/dist/src/generated/prisma/commonInputTypes.d.ts.map +1 -1
  48. package/dist/src/generated/prisma/internal/class.d.ts +11 -0
  49. package/dist/src/generated/prisma/internal/class.d.ts.map +1 -1
  50. package/dist/src/generated/prisma/internal/class.js +4 -4
  51. package/dist/src/generated/prisma/internal/class.js.map +1 -1
  52. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +92 -1
  53. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
  54. package/dist/src/generated/prisma/internal/prismaNamespace.js +17 -2
  55. package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
  56. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +16 -0
  57. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  58. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +17 -2
  59. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
  60. package/dist/src/generated/prisma/models/StaffEvent.d.ts +1184 -0
  61. package/dist/src/generated/prisma/models/StaffEvent.d.ts.map +1 -0
  62. package/dist/src/generated/prisma/models/StaffEvent.js +3 -0
  63. package/dist/src/generated/prisma/models/StaffEvent.js.map +1 -0
  64. package/dist/src/generated/prisma/models.d.ts +1 -0
  65. package/dist/src/generated/prisma/models.d.ts.map +1 -1
  66. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  67. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  68. package/dist/src/lib/assistantCheckpoint.js +143 -0
  69. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  70. package/dist/src/lib/autoRemember.d.ts +15 -0
  71. package/dist/src/lib/autoRemember.d.ts.map +1 -1
  72. package/dist/src/lib/autoRemember.js +433 -71
  73. package/dist/src/lib/autoRemember.js.map +1 -1
  74. package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
  75. package/dist/src/lib/cliHelpCatalog.js +23 -11
  76. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  77. package/dist/src/lib/cliHelpRenderer.d.ts +1 -0
  78. package/dist/src/lib/cliHelpRenderer.d.ts.map +1 -1
  79. package/dist/src/lib/cliHelpRenderer.js +4 -0
  80. package/dist/src/lib/cliHelpRenderer.js.map +1 -1
  81. package/dist/src/lib/commandErrors.d.ts +5 -1
  82. package/dist/src/lib/commandErrors.d.ts.map +1 -1
  83. package/dist/src/lib/commandErrors.js +250 -17
  84. package/dist/src/lib/commandErrors.js.map +1 -1
  85. package/dist/src/lib/createFirstPartyIranti.d.ts.map +1 -1
  86. package/dist/src/lib/createFirstPartyIranti.js +1 -0
  87. package/dist/src/lib/createFirstPartyIranti.js.map +1 -1
  88. package/dist/src/lib/dbStaffEventEmitter.d.ts +2 -0
  89. package/dist/src/lib/dbStaffEventEmitter.d.ts.map +1 -1
  90. package/dist/src/lib/dbStaffEventEmitter.js +15 -0
  91. package/dist/src/lib/dbStaffEventEmitter.js.map +1 -1
  92. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  93. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  94. package/dist/src/lib/hostMemoryFormatting.js +55 -0
  95. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  96. package/dist/src/lib/issueFacts.d.ts +37 -0
  97. package/dist/src/lib/issueFacts.d.ts.map +1 -0
  98. package/dist/src/lib/issueFacts.js +72 -0
  99. package/dist/src/lib/issueFacts.js.map +1 -0
  100. package/dist/src/lib/llm.d.ts +8 -0
  101. package/dist/src/lib/llm.d.ts.map +1 -1
  102. package/dist/src/lib/llm.js +33 -0
  103. package/dist/src/lib/llm.js.map +1 -1
  104. package/dist/src/lib/packageRoot.d.ts +2 -0
  105. package/dist/src/lib/packageRoot.d.ts.map +1 -0
  106. package/dist/src/lib/packageRoot.js +22 -0
  107. package/dist/src/lib/packageRoot.js.map +1 -0
  108. package/dist/src/lib/projectLearning.d.ts +21 -0
  109. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  110. package/dist/src/lib/projectLearning.js +357 -0
  111. package/dist/src/lib/projectLearning.js.map +1 -0
  112. package/dist/src/lib/protocolEnforcement.d.ts +29 -0
  113. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -0
  114. package/dist/src/lib/protocolEnforcement.js +124 -0
  115. package/dist/src/lib/protocolEnforcement.js.map +1 -0
  116. package/dist/src/lib/providers/claude.js +1 -1
  117. package/dist/src/lib/providers/claude.js.map +1 -1
  118. package/dist/src/lib/router.js +1 -1
  119. package/dist/src/lib/router.js.map +1 -1
  120. package/dist/src/lib/runtimeEnv.d.ts.map +1 -1
  121. package/dist/src/lib/runtimeEnv.js +8 -3
  122. package/dist/src/lib/runtimeEnv.js.map +1 -1
  123. package/dist/src/lib/scaffoldCloseout.d.ts +27 -0
  124. package/dist/src/lib/scaffoldCloseout.d.ts.map +1 -0
  125. package/dist/src/lib/scaffoldCloseout.js +139 -0
  126. package/dist/src/lib/scaffoldCloseout.js.map +1 -0
  127. package/dist/src/lib/semanticFactTags.d.ts +10 -0
  128. package/dist/src/lib/semanticFactTags.d.ts.map +1 -0
  129. package/dist/src/lib/semanticFactTags.js +166 -0
  130. package/dist/src/lib/semanticFactTags.js.map +1 -0
  131. package/dist/src/lib/sessionLedger.d.ts +94 -0
  132. package/dist/src/lib/sessionLedger.d.ts.map +1 -0
  133. package/dist/src/lib/sessionLedger.js +997 -0
  134. package/dist/src/lib/sessionLedger.js.map +1 -0
  135. package/dist/src/lib/sharedStateInvalidation.d.ts +10 -0
  136. package/dist/src/lib/sharedStateInvalidation.d.ts.map +1 -0
  137. package/dist/src/lib/sharedStateInvalidation.js +184 -0
  138. package/dist/src/lib/sharedStateInvalidation.js.map +1 -0
  139. package/dist/src/lib/staffEventsTable.d.ts +3 -0
  140. package/dist/src/lib/staffEventsTable.d.ts.map +1 -0
  141. package/dist/src/lib/staffEventsTable.js +58 -0
  142. package/dist/src/lib/staffEventsTable.js.map +1 -0
  143. package/dist/src/librarian/index.d.ts.map +1 -1
  144. package/dist/src/librarian/index.js +113 -2
  145. package/dist/src/librarian/index.js.map +1 -1
  146. package/dist/src/library/client.d.ts +6 -1
  147. package/dist/src/library/client.d.ts.map +1 -1
  148. package/dist/src/library/client.js +21 -7
  149. package/dist/src/library/client.js.map +1 -1
  150. package/dist/src/library/embeddings.d.ts +9 -1
  151. package/dist/src/library/embeddings.d.ts.map +1 -1
  152. package/dist/src/library/embeddings.js +28 -3
  153. package/dist/src/library/embeddings.js.map +1 -1
  154. package/dist/src/library/queries.d.ts.map +1 -1
  155. package/dist/src/library/queries.js +263 -46
  156. package/dist/src/library/queries.js.map +1 -1
  157. package/dist/src/sdk/index.d.ts +52 -1
  158. package/dist/src/sdk/index.d.ts.map +1 -1
  159. package/dist/src/sdk/index.js +546 -98
  160. package/dist/src/sdk/index.js.map +1 -1
  161. package/package.json +24 -3
  162. package/prisma/migrations/20260331101500_add_staff_events_ledger/migration.sql +24 -0
  163. package/prisma/schema.prisma +22 -0
@@ -13,6 +13,7 @@ const https_1 = __importDefault(require("https"));
13
13
  const promises_2 = __importDefault(require("readline/promises"));
14
14
  const stream_1 = require("stream");
15
15
  const net_1 = __importDefault(require("net"));
16
+ const crypto_1 = require("crypto");
16
17
  const client_1 = require("../src/library/client");
17
18
  const apiKeys_1 = require("../src/security/apiKeys");
18
19
  const cliHelpRenderer_1 = require("../src/lib/cliHelpRenderer");
@@ -30,7 +31,10 @@ const backends_1 = require("../src/library/backends");
30
31
  const runtimeLifecycle_1 = require("../src/lib/runtimeLifecycle");
31
32
  const queries_1 = require("../src/library/queries");
32
33
  const autoRemember_1 = require("../src/lib/autoRemember");
34
+ const semanticFactTags_1 = require("../src/lib/semanticFactTags");
33
35
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
36
+ const scaffoldCloseout_1 = require("../src/lib/scaffoldCloseout");
37
+ const projectLearning_1 = require("../src/lib/projectLearning");
34
38
  class CliError extends Error {
35
39
  constructor(code, message, hints = [], details) {
36
40
  super(message);
@@ -64,6 +68,7 @@ const ANSI = {
64
68
  };
65
69
  let CLI_DEBUG = process.argv.includes('--debug') || process.env.IRANTI_DEBUG === '1';
66
70
  let CLI_VERBOSE = CLI_DEBUG || process.argv.includes('--verbose') || process.env.IRANTI_VERBOSE === '1';
71
+ let ACTIVE_PARSED_ARGS = null;
67
72
  // H-7: Cleanup/rollback stack — LIFO handlers run on SIGINT/SIGTERM to undo partial multi-step operations
68
73
  const _cleanupStack = [];
69
74
  function pushCleanup(fn) {
@@ -146,6 +151,15 @@ function verboseLog(message, details) {
146
151
  function cliError(code, message, hints = [], details) {
147
152
  return new CliError(code, message, hints, details);
148
153
  }
154
+ function wantsJsonErrorEnvelope(args) {
155
+ return Boolean(args && hasFlag(args, 'json'));
156
+ }
157
+ function normalizeCliFailure(err) {
158
+ if (err instanceof CliError) {
159
+ return err;
160
+ }
161
+ return (0, commandErrors_1.rewriteCommandError)('iranti', err);
162
+ }
149
163
  function parseArgs(argv) {
150
164
  const flags = new Map();
151
165
  const positionals = [];
@@ -282,6 +296,25 @@ function findClosestAncestorRuntimeRoot(startDir) {
282
296
  }
283
297
  return null;
284
298
  }
299
+ function findClosestDoctorEnvTarget(startDir) {
300
+ for (const dir of walkAncestorPaths(startDir)) {
301
+ const repoEnv = path_1.default.join(dir, '.env');
302
+ if (fs_1.default.existsSync(repoEnv) && fs_1.default.statSync(repoEnv).isFile()) {
303
+ return {
304
+ envFile: repoEnv,
305
+ envSource: 'repo',
306
+ };
307
+ }
308
+ const projectEnv = path_1.default.join(dir, '.env.iranti');
309
+ if (fs_1.default.existsSync(projectEnv) && fs_1.default.statSync(projectEnv).isFile()) {
310
+ return {
311
+ envFile: projectEnv,
312
+ envSource: 'project-binding',
313
+ };
314
+ }
315
+ }
316
+ return { envFile: null, envSource: 'repo' };
317
+ }
285
318
  function resolveInstallRootDetails(args, scope) {
286
319
  const explicitFlag = getFlag(args, 'root');
287
320
  const explicit = explicitFlag ? stripWrappingQuotes(explicitFlag) : (process.env.IRANTI_HOME ? stripWrappingQuotes(process.env.IRANTI_HOME) : undefined);
@@ -621,6 +654,16 @@ async function readEnvFile(filePath) {
621
654
  }
622
655
  return out;
623
656
  }
657
+ async function readEnvFileIfExists(filePath) {
658
+ if (!filePath || !fs_1.default.existsSync(filePath))
659
+ return null;
660
+ try {
661
+ return await readEnvFile(filePath);
662
+ }
663
+ catch {
664
+ return null;
665
+ }
666
+ }
624
667
  async function readInstanceMetaFile(metaFile) {
625
668
  if (!fs_1.default.existsSync(metaFile))
626
669
  return null;
@@ -632,7 +675,11 @@ async function readInstanceMetaFile(metaFile) {
632
675
  return null;
633
676
  }
634
677
  }
635
- function makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir) {
678
+ function resolveInstanceApiKeyPepper(existingPepper) {
679
+ const trimmed = existingPepper?.trim() ?? '';
680
+ return trimmed || (0, crypto_1.randomBytes)(32).toString('hex');
681
+ }
682
+ function makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir, apiKeyPepper) {
636
683
  const lines = [
637
684
  '# Iranti instance env',
638
685
  `IRANTI_INSTANCE_NAME=${name}`,
@@ -645,6 +692,7 @@ function makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir) {
645
692
  'IRANTI_ARCHIVIST_DEBOUNCE_MS=60000',
646
693
  'IRANTI_ARCHIVIST_INTERVAL_MS=0',
647
694
  `IRANTI_API_KEY=${apiKey ?? 'replace_me_with_api_key'}`,
695
+ `IRANTI_API_KEY_PEPPER=${apiKeyPepper}`,
648
696
  '',
649
697
  ];
650
698
  return lines.join('\n');
@@ -1054,13 +1102,28 @@ async function ensureProjectGitignore(projectPath) {
1054
1102
  async function writeProjectBinding(projectPath, updates) {
1055
1103
  await ensureDir(projectPath);
1056
1104
  const outFile = path_1.default.join(projectPath, '.env.iranti');
1105
+ const previousBinding = fs_1.default.existsSync(outFile)
1106
+ ? await readEnvFile(outFile).catch(() => ({}))
1107
+ : {};
1108
+ const normalizedUpdates = {
1109
+ ...updates,
1110
+ IRANTI_CODEBASE_ENTITY: updates.IRANTI_CODEBASE_ENTITY ?? previousBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
1111
+ };
1057
1112
  if (!fs_1.default.existsSync(outFile)) {
1058
1113
  await writeText(outFile, '# Iranti project binding\n');
1059
1114
  }
1060
- await upsertEnvFile(outFile, updates);
1115
+ await upsertEnvFile(outFile, normalizedUpdates);
1061
1116
  await ensureProjectGitignore(projectPath);
1117
+ const writtenBinding = await readEnvFile(outFile).catch(() => ({}));
1118
+ await syncProjectBindingRegistry(projectPath, writtenBinding, previousBinding);
1062
1119
  return outFile;
1063
1120
  }
1121
+ function normalizeProjectRegistryPath(projectPath) {
1122
+ const normalized = path_1.default.resolve(projectPath);
1123
+ return process.platform === 'win32'
1124
+ ? normalized.toLowerCase()
1125
+ : normalized;
1126
+ }
1064
1127
  async function removeProjectPathFromRegistry(runtimeRoot, instanceName, projectPath) {
1065
1128
  const registryPath = path_1.default.join(runtimeRoot, 'instances', instanceName, 'projects.json');
1066
1129
  if (!fs_1.default.existsSync(registryPath))
@@ -1068,16 +1131,54 @@ async function removeProjectPathFromRegistry(runtimeRoot, instanceName, projectP
1068
1131
  const parsed = readJsonFile(registryPath);
1069
1132
  if (!parsed || !Array.isArray(parsed.projects))
1070
1133
  return false;
1134
+ const expectedProjectPath = normalizeProjectRegistryPath(projectPath);
1071
1135
  const nextProjects = parsed.projects.filter((entry) => {
1072
1136
  if (!entry || typeof entry !== 'object')
1073
1137
  return false;
1074
- return String(entry.projectPath ?? '') !== projectPath;
1138
+ return normalizeProjectRegistryPath(String(entry.projectPath ?? '')) !== expectedProjectPath;
1075
1139
  });
1076
1140
  if (nextProjects.length === parsed.projects.length)
1077
1141
  return false;
1078
1142
  await writeText(registryPath, `${JSON.stringify({ projects: nextProjects }, null, 2)}\n`);
1079
1143
  return true;
1080
1144
  }
1145
+ async function syncProjectBindingRegistry(projectPath, binding, previousBinding = {}) {
1146
+ const nextRuntimeRoot = runtimeRootFromInstanceEnv(binding.IRANTI_INSTANCE_ENV ?? '');
1147
+ const nextInstanceName = binding.IRANTI_INSTANCE?.trim() || null;
1148
+ const previousRuntimeRoot = runtimeRootFromInstanceEnv(previousBinding.IRANTI_INSTANCE_ENV ?? '');
1149
+ const previousInstanceName = previousBinding.IRANTI_INSTANCE?.trim() || null;
1150
+ if (previousRuntimeRoot && previousInstanceName && (previousRuntimeRoot !== nextRuntimeRoot
1151
+ || previousInstanceName !== nextInstanceName)) {
1152
+ await removeProjectPathFromRegistry(previousRuntimeRoot, previousInstanceName, projectPath);
1153
+ }
1154
+ if (!nextRuntimeRoot || !nextInstanceName)
1155
+ return;
1156
+ const registryPath = path_1.default.join(nextRuntimeRoot, 'instances', nextInstanceName, 'projects.json');
1157
+ const existingRegistry = fs_1.default.existsSync(registryPath)
1158
+ ? readJsonFile(registryPath)
1159
+ : null;
1160
+ const existingProjects = Array.isArray(existingRegistry?.projects) ? existingRegistry.projects : [];
1161
+ const normalizedProjectPath = normalizeProjectRegistryPath(projectPath);
1162
+ const previousEntry = existingProjects.find((entry) => entry
1163
+ && typeof entry === 'object'
1164
+ && normalizeProjectRegistryPath(String(entry.projectPath ?? '')) === normalizedProjectPath);
1165
+ const boundAt = typeof previousEntry?.boundAt === 'string' && previousEntry.boundAt.trim().length > 0
1166
+ ? previousEntry.boundAt
1167
+ : new Date().toISOString();
1168
+ const nextProjects = existingProjects
1169
+ .filter((entry) => !entry
1170
+ || typeof entry !== 'object'
1171
+ || normalizeProjectRegistryPath(String(entry.projectPath ?? '')) !== normalizedProjectPath)
1172
+ .concat({
1173
+ projectPath,
1174
+ agentId: binding.IRANTI_AGENT_ID?.trim() || 'project_main',
1175
+ memoryEntity: binding.IRANTI_MEMORY_ENTITY?.trim() || 'user/main',
1176
+ mode: binding.IRANTI_PROJECT_MODE?.trim() || 'isolated',
1177
+ boundAt,
1178
+ })
1179
+ .sort((a, b) => String(a.projectPath ?? '').localeCompare(String(b.projectPath ?? '')));
1180
+ await writeText(registryPath, `${JSON.stringify({ projects: nextProjects }, null, 2)}\n`);
1181
+ }
1081
1182
  async function cleanupProjectBindingRegistry(projectPath, binding) {
1082
1183
  const removedFrom = [];
1083
1184
  const runtimeRoot = runtimeRootFromInstanceEnv(binding.IRANTI_INSTANCE_ENV ?? '');
@@ -1089,6 +1190,24 @@ async function cleanupProjectBindingRegistry(projectPath, binding) {
1089
1190
  }
1090
1191
  return removedFrom;
1091
1192
  }
1193
+ function readInstanceProjectRegistry(root, instanceName) {
1194
+ const registryPath = path_1.default.join(root, 'instances', instanceName, 'projects.json');
1195
+ if (!fs_1.default.existsSync(registryPath))
1196
+ return [];
1197
+ const parsed = readJsonFile(registryPath);
1198
+ if (!parsed || !Array.isArray(parsed.projects))
1199
+ return [];
1200
+ return parsed.projects
1201
+ .filter((entry) => entry && typeof entry === 'object' && typeof entry.projectPath === 'string' && entry.projectPath.trim().length > 0)
1202
+ .map((entry) => ({
1203
+ projectPath: String(entry.projectPath),
1204
+ agentId: typeof entry.agentId === 'string' && entry.agentId.trim().length > 0 ? entry.agentId : 'project_main',
1205
+ memoryEntity: typeof entry.memoryEntity === 'string' && entry.memoryEntity.trim().length > 0 ? entry.memoryEntity : 'user/main',
1206
+ mode: typeof entry.mode === 'string' && entry.mode.trim().length > 0 ? entry.mode : 'isolated',
1207
+ boundAt: typeof entry.boundAt === 'string' && entry.boundAt.trim().length > 0 ? entry.boundAt : null,
1208
+ }))
1209
+ .sort((a, b) => a.projectPath.localeCompare(b.projectPath));
1210
+ }
1092
1211
  async function withPromptSession(run) {
1093
1212
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
1094
1213
  throw new Error('--interactive requires a real terminal session.');
@@ -1202,6 +1321,59 @@ function postgresDatabaseName(databaseUrl) {
1202
1321
  }
1203
1322
  return database;
1204
1323
  }
1324
+ function summarizeDatabaseTarget(databaseUrl) {
1325
+ const trimmed = databaseUrl?.trim() ?? '';
1326
+ if (!trimmed || detectPlaceholder(trimmed))
1327
+ return null;
1328
+ try {
1329
+ const parsed = parsePostgresConnectionString(trimmed);
1330
+ const database = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''));
1331
+ if (!database)
1332
+ return parsed.host || trimmed;
1333
+ return `${parsed.host}/${database}`;
1334
+ }
1335
+ catch {
1336
+ return trimmed;
1337
+ }
1338
+ }
1339
+ function summarizeActiveAuthority(envSource, bindingFile, boundInstanceEnvFile) {
1340
+ if (bindingFile && boundInstanceEnvFile)
1341
+ return 'project binding -> bound instance env';
1342
+ if (bindingFile)
1343
+ return 'project binding';
1344
+ if (envSource.startsWith('instance:'))
1345
+ return 'named instance env';
1346
+ if (envSource === 'explicit-env')
1347
+ return 'explicit env';
1348
+ if (envSource === 'repo')
1349
+ return 'repo env';
1350
+ if (envSource === 'environment')
1351
+ return 'environment';
1352
+ return envSource;
1353
+ }
1354
+ async function buildOperatorAuthoritySummary(options) {
1355
+ const repoEnv = await readEnvFileIfExists(options.repoEnvFile ?? null);
1356
+ const boundDatabaseUrl = options.boundInstanceEnv?.DATABASE_URL?.trim() || null;
1357
+ const nearbyBoundDatabaseUrl = options.nearbyBoundInstanceEnv?.DATABASE_URL?.trim() || null;
1358
+ const selectedDatabaseUrl = options.env?.DATABASE_URL?.trim() || null;
1359
+ const activeDatabaseUrl = boundDatabaseUrl || selectedDatabaseUrl;
1360
+ const repoDatabaseUrl = repoEnv?.DATABASE_URL?.trim() || null;
1361
+ return {
1362
+ activeAuthority: summarizeActiveAuthority(options.envSource, options.bindingFile ?? null, options.boundInstanceEnvFile ?? null),
1363
+ activeBindingSource: options.bindingFile ?? null,
1364
+ activeBoundInstanceEnv: options.boundInstanceEnvFile ?? null,
1365
+ activeDatabaseUrl,
1366
+ activeDatabaseTarget: summarizeDatabaseTarget(activeDatabaseUrl),
1367
+ repoDatabaseUrl,
1368
+ repoDatabaseTarget: summarizeDatabaseTarget(repoDatabaseUrl),
1369
+ repoDatabaseDiffers: Boolean(repoDatabaseUrl && activeDatabaseUrl && repoDatabaseUrl !== activeDatabaseUrl),
1370
+ nearbyBindingSource: options.nearbyBindingFile ?? null,
1371
+ nearbyBoundInstanceEnv: options.nearbyBoundInstanceEnvFile ?? null,
1372
+ nearbyBindingDatabaseUrl: nearbyBoundDatabaseUrl,
1373
+ nearbyBindingDatabaseTarget: summarizeDatabaseTarget(nearbyBoundDatabaseUrl),
1374
+ nearbyBindingDiffers: Boolean(nearbyBoundDatabaseUrl && activeDatabaseUrl && nearbyBoundDatabaseUrl !== activeDatabaseUrl),
1375
+ };
1376
+ }
1205
1377
  function isLocalPostgresHost(hostname) {
1206
1378
  const normalized = hostname.trim().toLowerCase();
1207
1379
  return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
@@ -1513,13 +1685,17 @@ async function ensureRuntimeInstalled(root, scope) {
1513
1685
  async function ensureInstanceConfigured(root, name, config) {
1514
1686
  const { instanceDir, envFile, metaFile } = instancePaths(root, name);
1515
1687
  const created = !fs_1.default.existsSync(envFile);
1688
+ const existingEnv = fs_1.default.existsSync(envFile)
1689
+ ? await readEnvFile(envFile).catch(() => ({}))
1690
+ : {};
1691
+ const apiKeyPepper = resolveInstanceApiKeyPepper(existingEnv.IRANTI_API_KEY_PEPPER);
1516
1692
  if (created) {
1517
1693
  await ensureDir(instanceDir);
1518
1694
  await ensureDir(path_1.default.join(instanceDir, 'logs'));
1519
1695
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'active'));
1520
1696
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'resolved'));
1521
1697
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'archived'));
1522
- await writeText(envFile, makeInstanceEnv(name, config.port, config.dbUrl, config.apiKey, instanceDir));
1698
+ await writeText(envFile, makeInstanceEnv(name, config.port, config.dbUrl, config.apiKey, instanceDir, apiKeyPepper));
1523
1699
  const meta = {
1524
1700
  name,
1525
1701
  createdAt: new Date().toISOString(),
@@ -1535,6 +1711,7 @@ async function ensureInstanceConfigured(root, name, config) {
1535
1711
  IRANTI_PORT: String(config.port),
1536
1712
  DATABASE_URL: config.dbUrl,
1537
1713
  IRANTI_API_KEY: config.apiKey,
1714
+ IRANTI_API_KEY_PEPPER: apiKeyPepper,
1538
1715
  LLM_PROVIDER: config.provider,
1539
1716
  ...config.providerKeys,
1540
1717
  });
@@ -1545,22 +1722,37 @@ async function ensureInstanceConfigured(root, name, config) {
1545
1722
  return { envFile, instanceDir, created };
1546
1723
  }
1547
1724
  function makeIrantiMcpServerConfig(projectEnvPath) {
1725
+ const env = {
1726
+ IRANTI_MCP_HOST: 'codex_cli',
1727
+ };
1728
+ if (projectEnvPath) {
1729
+ env.IRANTI_PROJECT_ENV = projectEnvPath;
1730
+ }
1548
1731
  return {
1549
1732
  command: 'iranti',
1550
1733
  args: ['mcp'],
1551
- ...(projectEnvPath
1552
- ? {
1553
- env: {
1554
- IRANTI_PROJECT_ENV: projectEnvPath,
1555
- },
1556
- }
1557
- : {}),
1734
+ env,
1735
+ };
1736
+ }
1737
+ function makeClaudeLocalMcpServerConfig(projectEnvPath) {
1738
+ const env = {
1739
+ IRANTI_MCP_HOST: 'claude_code',
1740
+ };
1741
+ if (projectEnvPath) {
1742
+ env.IRANTI_PROJECT_ENV = projectEnvPath;
1743
+ }
1744
+ return {
1745
+ command: 'iranti',
1746
+ args: ['mcp'],
1747
+ env,
1558
1748
  };
1559
1749
  }
1560
1750
  function makeVsCodeIrantiMcpServerConfig(projectPath, projectEnvPath) {
1561
1751
  const resolvedProjectEnvPath = projectEnvPath ? path_1.default.resolve(projectEnvPath) : undefined;
1562
1752
  const localProjectEnvPath = path_1.default.join(projectPath, '.env.iranti');
1563
- const env = {};
1753
+ const env = {
1754
+ IRANTI_MCP_HOST: 'codex_vscode',
1755
+ };
1564
1756
  if (resolvedProjectEnvPath && path_1.default.resolve(localProjectEnvPath) !== resolvedProjectEnvPath) {
1565
1757
  env.IRANTI_PROJECT_ENV = resolvedProjectEnvPath;
1566
1758
  }
@@ -1619,6 +1811,13 @@ async function resolveAttendantCliTarget(args) {
1619
1811
  cwd,
1620
1812
  projectEnvFile: explicitProjectEnv ? path_1.default.resolve(explicitProjectEnv) : undefined,
1621
1813
  });
1814
+ debugLog('Attendant CLI runtime env resolved.', {
1815
+ cwd,
1816
+ authorityMode: loaded.authorityMode,
1817
+ projectEnvFile: loaded.projectEnvFile ?? null,
1818
+ instanceEnvFile: loaded.instanceEnvFile ?? null,
1819
+ loadedFiles: loaded.loadedFiles.join(' | ') || '(none)',
1820
+ });
1622
1821
  envSource = loaded.projectEnvFile ? 'project-binding' : 'environment';
1623
1822
  envFile = loaded.projectEnvFile ?? loaded.instanceEnvFile ?? null;
1624
1823
  projectEnvFile = loaded.projectEnvFile;
@@ -1638,7 +1837,11 @@ async function resolveAttendantCliTarget(args) {
1638
1837
  projectEnvFile,
1639
1838
  instanceEnvFile,
1640
1839
  agentId,
1641
- iranti: (0, createFirstPartyIranti_1.createFirstPartyIranti)({ connectionString }),
1840
+ iranti: (0, createFirstPartyIranti_1.createFirstPartyIranti)({
1841
+ connectionString,
1842
+ sessionLedgerSource: 'cli',
1843
+ sessionLedgerHost: 'plain_cli',
1844
+ }),
1642
1845
  };
1643
1846
  }
1644
1847
  function truncateText(value, limit) {
@@ -1713,6 +1916,135 @@ function resolveTaskEntity(args) {
1713
1916
  }
1714
1917
  return entity;
1715
1918
  }
1919
+ function resolveIssueEntity(args) {
1920
+ const explicit = getFlag(args, 'entity')?.trim();
1921
+ if (explicit) {
1922
+ if (!explicit.includes('/')) {
1923
+ throw new Error('issue entity must use entityType/entityId format.');
1924
+ }
1925
+ return explicit;
1926
+ }
1927
+ const fromEnv = process.env.IRANTI_MEMORY_ENTITY?.trim();
1928
+ if (fromEnv?.includes('/')) {
1929
+ return fromEnv;
1930
+ }
1931
+ throw new Error('Missing issue entity. Pass --entity <entityType/entityId> or run from a bound project with IRANTI_MEMORY_ENTITY.');
1932
+ }
1933
+ function resolveIssueStatusFilter(args) {
1934
+ const raw = getFlag(args, 'status')?.trim().toLowerCase();
1935
+ if (!raw)
1936
+ return null;
1937
+ if (raw === 'open' || raw === 'resolved') {
1938
+ return raw;
1939
+ }
1940
+ throw new Error(`Invalid --status '${raw}'. Use open or resolved.`);
1941
+ }
1942
+ function parseIssueListFact(entry) {
1943
+ if (!entry.key.startsWith('issue_')) {
1944
+ return { item: null, invalid: null };
1945
+ }
1946
+ const value = typeof entry.value === 'object' && entry.value ? entry.value : null;
1947
+ if (!value) {
1948
+ return {
1949
+ item: null,
1950
+ invalid: {
1951
+ key: entry.key,
1952
+ summary: entry.summary,
1953
+ confidence: entry.confidence,
1954
+ source: entry.source,
1955
+ reason: 'issue_* fact value is not an object',
1956
+ },
1957
+ };
1958
+ }
1959
+ const rawStatus = typeof value?.status === 'string' ? value.status.toLowerCase() : '';
1960
+ if (rawStatus !== 'open' && rawStatus !== 'resolved') {
1961
+ return {
1962
+ item: null,
1963
+ invalid: {
1964
+ key: entry.key,
1965
+ summary: entry.summary,
1966
+ confidence: entry.confidence,
1967
+ source: entry.source,
1968
+ reason: 'issue_* fact is missing canonical open/resolved status',
1969
+ },
1970
+ };
1971
+ }
1972
+ return {
1973
+ item: {
1974
+ key: entry.key,
1975
+ issueId: typeof value?.issueId === 'string' && value.issueId.trim() ? value.issueId.trim() : entry.key.replace(/^issue_/, ''),
1976
+ title: typeof value?.title === 'string' && value.title.trim() ? value.title.trim() : entry.summary,
1977
+ status: rawStatus,
1978
+ severity: typeof value?.severity === 'string' && value.severity.trim() ? value.severity.trim() : 'medium',
1979
+ summary: entry.summary,
1980
+ confidence: entry.confidence,
1981
+ source: entry.source,
1982
+ discoveredAt: typeof value?.discoveredAt === 'string' && value.discoveredAt.trim() ? value.discoveredAt.trim() : null,
1983
+ resolvedAt: typeof value?.resolvedAt === 'string' && value.resolvedAt.trim() ? value.resolvedAt.trim() : null,
1984
+ resolution: typeof value?.resolution === 'string' && value.resolution.trim() ? value.resolution.trim() : null,
1985
+ tags: Array.isArray(value?.tags)
1986
+ ? value.tags.map((tag) => String(tag).trim()).filter(Boolean)
1987
+ : [],
1988
+ },
1989
+ invalid: null,
1990
+ };
1991
+ }
1992
+ function compareIssueListItems(a, b) {
1993
+ if (a.status !== b.status) {
1994
+ return a.status === 'open' ? -1 : 1;
1995
+ }
1996
+ const severityRank = new Map([
1997
+ ['critical', 0],
1998
+ ['high', 1],
1999
+ ['medium', 2],
2000
+ ['low', 3],
2001
+ ]);
2002
+ const severityDelta = (severityRank.get(a.severity) ?? 99) - (severityRank.get(b.severity) ?? 99);
2003
+ if (severityDelta !== 0)
2004
+ return severityDelta;
2005
+ return a.issueId.localeCompare(b.issueId);
2006
+ }
2007
+ function printIssuesResult(entity, items, statusFilter, inventoryCounts, invalidFacts) {
2008
+ console.log(sectionTitle('Issues Command'));
2009
+ console.log(`Entity: ${entity}`);
2010
+ console.log(`Filter: ${statusFilter ?? 'all'}`);
2011
+ console.log(`Total: ${items.length}`);
2012
+ console.log(`Canonical inventory: ${inventoryCounts.canonicalTotal} (${inventoryCounts.open} open, ${inventoryCounts.resolved} resolved)`);
2013
+ console.log(`Invalid issue_* facts: ${inventoryCounts.invalid}`);
2014
+ console.log('');
2015
+ if (invalidFacts.length > 0) {
2016
+ console.log('Warnings:');
2017
+ for (const invalid of invalidFacts) {
2018
+ console.log(` - ${invalid.key}: ${invalid.reason} [source=${invalid.source}, confidence=${invalid.confidence}]`);
2019
+ }
2020
+ console.log('');
2021
+ }
2022
+ if (items.length === 0) {
2023
+ console.log('No issue facts found.');
2024
+ return;
2025
+ }
2026
+ for (const item of items) {
2027
+ const header = `[${item.status.toUpperCase()}] ${item.issueId} (${item.severity})`;
2028
+ console.log(header);
2029
+ console.log(` Title: ${item.title}`);
2030
+ console.log(` Summary: ${item.summary}`);
2031
+ console.log(` Source: ${item.source}`);
2032
+ console.log(` Confidence: ${item.confidence}`);
2033
+ if (item.discoveredAt) {
2034
+ console.log(` Discovered: ${item.discoveredAt}`);
2035
+ }
2036
+ if (item.resolvedAt) {
2037
+ console.log(` Resolved: ${item.resolvedAt}`);
2038
+ }
2039
+ if (item.resolution) {
2040
+ console.log(` Resolution: ${item.resolution}`);
2041
+ }
2042
+ if (item.tags.length > 0) {
2043
+ console.log(` Tags: ${item.tags.join(', ')}`);
2044
+ }
2045
+ console.log('');
2046
+ }
2047
+ }
1716
2048
  function buildHandoffSummary(key, value) {
1717
2049
  switch (key) {
1718
2050
  case 'status': {
@@ -1805,6 +2137,11 @@ function printAttendResult(target, latestMessage, result) {
1805
2137
  console.log(` confidence ${result.decision.confidence}`);
1806
2138
  console.log(` explanation ${result.decision.explanation}`);
1807
2139
  console.log(` facts ${result.facts.length}`);
2140
+ if ((result.memoryAttributions?.length ?? 0) > 0) {
2141
+ const firstAttribution = result.memoryAttributions[0];
2142
+ console.log(` injection id ${firstAttribution.injectionId}`);
2143
+ console.log(` attribution ${firstAttribution.status} surfaced=${firstAttribution.surfaced} used=${firstAttribution.used} helpful=${firstAttribution.helpful}`);
2144
+ }
1808
2145
  if (result.facts.length === 0) {
1809
2146
  console.log('');
1810
2147
  console.log('No facts selected for injection.');
@@ -1839,6 +2176,21 @@ function makeClaudeHookEntry(event, projectEnvPath) {
1839
2176
  ],
1840
2177
  };
1841
2178
  }
2179
+ const IRANTI_CLAUDE_ALLOWED_TOOLS = [
2180
+ 'mcp__iranti__iranti_handshake',
2181
+ 'mcp__iranti__iranti_attend',
2182
+ 'mcp__iranti__iranti_observe',
2183
+ 'mcp__iranti__iranti_checkpoint',
2184
+ 'mcp__iranti__iranti_query',
2185
+ 'mcp__iranti__iranti_search',
2186
+ 'mcp__iranti__iranti_write',
2187
+ 'mcp__iranti__iranti_remember_response',
2188
+ 'mcp__iranti__iranti_ingest',
2189
+ 'mcp__iranti__iranti_relate',
2190
+ 'mcp__iranti__iranti_related',
2191
+ 'mcp__iranti__iranti_related_deep',
2192
+ 'mcp__iranti__iranti_who_knows',
2193
+ ];
1842
2194
  function isClaudeHooksObject(value) {
1843
2195
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1844
2196
  }
@@ -1871,12 +2223,46 @@ function needsClaudeHookSettingsUpgrade(value) {
1871
2223
  }
1872
2224
  return false;
1873
2225
  }
2226
+ function mergeClaudePermissionAllowList(existing) {
2227
+ const currentPermissions = existing && typeof existing.permissions === 'object' && existing.permissions !== null && !Array.isArray(existing.permissions)
2228
+ ? { ...existing.permissions }
2229
+ : {};
2230
+ const allow = Array.isArray(currentPermissions.allow)
2231
+ ? [...currentPermissions.allow.map((value) => String(value))]
2232
+ : [];
2233
+ for (const toolName of IRANTI_CLAUDE_ALLOWED_TOOLS) {
2234
+ if (!allow.includes(toolName)) {
2235
+ allow.push(toolName);
2236
+ }
2237
+ }
2238
+ return {
2239
+ ...currentPermissions,
2240
+ allow,
2241
+ };
2242
+ }
1874
2243
  function makeClaudeHookSettings(projectEnvPath, existing) {
1875
2244
  const existingHooks = existing && isClaudeHooksObject(existing.hooks)
1876
2245
  ? existing.hooks
1877
2246
  : {};
2247
+ const existingMcpServers = existing && existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers)
2248
+ ? existing.mcpServers
2249
+ : {};
2250
+ const existingEnabledMcpjsonServers = Array.isArray(existing?.enabledMcpjsonServers)
2251
+ ? existing.enabledMcpjsonServers
2252
+ .map((value) => String(value))
2253
+ .filter((value) => value !== 'iranti')
2254
+ : undefined;
1878
2255
  return {
1879
2256
  ...(existing ?? {}),
2257
+ enableAllProjectMcpServers: false,
2258
+ permissions: mergeClaudePermissionAllowList(existing),
2259
+ mcpServers: {
2260
+ ...existingMcpServers,
2261
+ iranti: makeClaudeLocalMcpServerConfig(projectEnvPath),
2262
+ },
2263
+ ...(existingEnabledMcpjsonServers
2264
+ ? { enabledMcpjsonServers: existingEnabledMcpjsonServers }
2265
+ : {}),
1880
2266
  hooks: {
1881
2267
  ...existingHooks,
1882
2268
  SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
@@ -1994,12 +2380,87 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
1994
2380
  settingsStatus = 'updated';
1995
2381
  }
1996
2382
  }
2383
+ const claudeMdFile = path_1.default.join(projectPath, 'CLAUDE.md');
2384
+ let claudeMdStatus = 'unchanged';
2385
+ const irantiMdBlock = buildIrantiClaudeMdBlock();
2386
+ if (!fs_1.default.existsSync(claudeMdFile)) {
2387
+ await writeText(claudeMdFile, irantiMdBlock);
2388
+ claudeMdStatus = 'created';
2389
+ }
2390
+ else {
2391
+ const existing = fs_1.default.readFileSync(claudeMdFile, 'utf8');
2392
+ if (!existing.includes('<!-- iranti-rules -->')) {
2393
+ await writeText(claudeMdFile, `${existing.trimEnd()}\n\n${irantiMdBlock}`);
2394
+ claudeMdStatus = 'updated';
2395
+ }
2396
+ else {
2397
+ const replaced = existing.replace(/<!-- iranti-rules -->[\s\S]*?<!-- \/iranti-rules -->/, irantiMdBlock.trim());
2398
+ if (replaced !== existing) {
2399
+ await writeText(claudeMdFile, replaced);
2400
+ claudeMdStatus = 'updated';
2401
+ }
2402
+ }
2403
+ }
2404
+ const closeout = await (0, scaffoldCloseout_1.writeProjectScaffoldCloseout)({
2405
+ tool: 'claude',
2406
+ projectPath,
2407
+ projectEnvFile: resolvedProjectEnvPath ?? projectEnvPath ?? null,
2408
+ files: [
2409
+ { path: mcpFile, status: mcpStatus },
2410
+ { path: vscodeMcpFile, status: vscodeMcpStatus },
2411
+ { path: settingsFile, status: settingsStatus },
2412
+ { path: claudeMdFile, status: claudeMdStatus },
2413
+ ],
2414
+ agentId: 'iranti_cli_scaffold',
2415
+ });
1997
2416
  return {
1998
2417
  mcp: mcpStatus,
1999
2418
  vscodeMcp: vscodeMcpStatus,
2000
2419
  settings: settingsStatus,
2420
+ claudeMd: claudeMdStatus,
2421
+ closeout,
2001
2422
  };
2002
2423
  }
2424
+ function buildIrantiClaudeMdBlock() {
2425
+ return [
2426
+ '<!-- iranti-rules -->',
2427
+ '# Iranti Memory Protocol',
2428
+ '',
2429
+ 'IMPORTANT: These rules OVERRIDE any default behavior and MUST be followed exactly. No work should begin until the session-start acknowledgment is stated.',
2430
+ '',
2431
+ '## Every turn',
2432
+ '1. Call `mcp__iranti__iranti_attend` before responding to the user message.',
2433
+ '2. Call `mcp__iranti__iranti_attend` before using any knowledge discovery tool — Read, Grep, Glob, WebSearch, WebFetch, and Bash commands used as factual basis for a decision.',
2434
+ '3. Call `mcp__iranti__iranti_attend` after knowledge discovery to check new findings against stored memory and decide what to inject, write, or checkpoint.',
2435
+ '4. Call `mcp__iranti__iranti_attend` after delivering any substantive response — plans, decisions, analysis, or structured recommendations are durable findings that must be assessed before the next turn.',
2436
+ '5. Call `mcp__iranti__iranti_write` after confirmed durable findings — decisions, blockers, next steps, validated environment details, what worked, what failed, what changed, and what happens next.',
2437
+ '6. Call `mcp__iranti__iranti_write` after every Edit or Write tool call — file changes are always durable. Record what changed, why, and what the file now does.',
2438
+ '7. Call `mcp__iranti__iranti_write` after any Bash command that reveals system state — build results, test outcomes, container state, environment facts, errors.',
2439
+ '8. Call `mcp__iranti__iranti_write` after any WebSearch or WebFetch that surfaces confirmed external facts.',
2440
+ '9. Call `mcp__iranti__iranti_write` after any Agent (subagent) tool completes — subagent findings are invisible to the hive mind otherwise.',
2441
+ '10. If a recall-style lookup returns no facts, try at least one alternative retrieval angle before concluding the fact is not stored.',
2442
+ '11. Call `mcp__iranti__iranti_attend` again when new knowledge should change what is loaded for the next step.',
2443
+ '',
2444
+ '## Session start',
2445
+ '- Call `mcp__iranti__iranti_handshake` with the current task before responding to the first user message.',
2446
+ '- After the handshake completes, you MUST explicitly state: "I acknowledge that Iranti is a hive mind. I have read the Iranti operating rules and the protocols for iranti_handshake, iranti_attend, iranti_write, iranti_checkpoint, and iranti_remember_response, and I pledge to use them to persist what I find, what works, what fails, what changes, and what happens next so later sessions do not have to rediscover it."',
2447
+ '- This acknowledgment is strictly mandatory. No work should begin until it is stated.',
2448
+ '',
2449
+ '## After context compaction',
2450
+ '- Call `mcp__iranti__iranti_handshake` before responding to the next user message.',
2451
+ '',
2452
+ '## Checkpointing',
2453
+ '- Call `mcp__iranti__iranti_checkpoint` when completing a task, when shifting to a new task mid-session, at any natural pause point, and before stepping away from long or interrupted work.',
2454
+ '- Record key actions in the checkpoint `actions` field so later sessions can see important commands, tests, searches, validations, and decisions without rerunning them blindly.',
2455
+ '- Do not rely on `mcp__iranti__iranti_write` alone — facts and checkpoints are separate stores. A checkpoint not written means the next handshake recovers from stale data.',
2456
+ '- Under-logged runs are non-compliant. Leave structured breadcrumbs for what you found, what worked, what failed, what changed, and what happens next instead of only a broad summary.',
2457
+ '',
2458
+ '## Host setup check',
2459
+ '- If this file was not present at session start, run `iranti claude-setup .` to complete integration.',
2460
+ '<!-- /iranti-rules -->',
2461
+ '',
2462
+ ].join('\n');
2463
+ }
2003
2464
  function hasCodexInstalled() {
2004
2465
  try {
2005
2466
  const proc = (0, commandInvocation_1.spawnSyncResolved)('codex', ['--version'], { stdio: 'ignore' });
@@ -2055,6 +2516,10 @@ async function isPortAvailable(port, host = '0.0.0.0') {
2055
2516
  });
2056
2517
  }
2057
2518
  function listPublishedDockerHostPorts() {
2519
+ const injected = process.env.IRANTI_FAKE_DOCKER_PORTS?.trim();
2520
+ if (injected) {
2521
+ return (0, dockerCliParsing_1.parsePublishedDockerHostPorts)(injected);
2522
+ }
2058
2523
  const docker = inspectDockerAvailability();
2059
2524
  if (!docker.daemonReachable)
2060
2525
  return new Set();
@@ -2305,12 +2770,21 @@ async function executeSetupPlan(plan) {
2305
2770
  IRANTI_INSTANCE: plan.instanceName,
2306
2771
  IRANTI_INSTANCE_ENV: configured.envFile,
2307
2772
  });
2773
+ const bindingEnv = await readEnvFile(written);
2774
+ const learningStatus = await (0, projectLearning_1.writeProjectLearningSnapshot)({
2775
+ projectPath,
2776
+ projectEnvFile: written,
2777
+ binding: bindingEnv,
2778
+ agentId: project.agentId,
2779
+ });
2308
2780
  bindings.push({
2309
2781
  projectPath,
2310
2782
  envFile: written,
2311
2783
  agentId: project.agentId,
2312
2784
  projectMode: project.projectMode,
2313
2785
  autoRemember: project.autoRemember,
2786
+ codebaseEntity: bindingEnv.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
2787
+ learningStatus,
2314
2788
  });
2315
2789
  if (project.claudeCode) {
2316
2790
  await writeClaudeCodeProjectFiles(projectPath);
@@ -2339,7 +2813,7 @@ async function executeSetupPlan(plan) {
2339
2813
  bindings,
2340
2814
  };
2341
2815
  }
2342
- function parseSetupConfig(filePath) {
2816
+ async function parseSetupConfig(filePath) {
2343
2817
  const resolved = path_1.default.resolve(filePath);
2344
2818
  if (!fs_1.default.existsSync(resolved)) {
2345
2819
  throw new Error(`Setup config file not found: ${resolved}`);
@@ -2361,7 +2835,7 @@ function parseSetupConfig(filePath) {
2361
2835
  : databaseModeRaw === 'existing' || databaseModeRaw === 'local' || databaseModeRaw.length === 0
2362
2836
  ? 'local'
2363
2837
  : (() => { throw new Error(`Unsupported databaseMode in setup config: ${databaseModeRaw}`); })();
2364
- const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2838
+ const databaseUrl = await resolveSetupDatabaseUrl(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2365
2839
  const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(String(raw?.databaseIntent ?? raw?.dbIntent ?? '').trim(), 'databaseIntent');
2366
2840
  const provider = normalizeProvider(String(raw?.provider ?? 'mock')) ?? 'mock';
2367
2841
  if (!isSupportedProvider(provider)) {
@@ -2423,7 +2897,7 @@ function parseSetupConfig(filePath) {
2423
2897
  }),
2424
2898
  };
2425
2899
  }
2426
- function defaultsSetupPlan(args) {
2900
+ async function defaultsSetupPlan(args) {
2427
2901
  const scope = normalizeScope(getFlag(args, 'scope'));
2428
2902
  const root = path_1.default.resolve(getFlag(args, 'root') ?? resolveInstallRoot(args, scope));
2429
2903
  const mode = getFlag(args, 'mode') === 'shared' ? 'shared' : 'isolated';
@@ -2450,7 +2924,7 @@ function defaultsSetupPlan(args) {
2450
2924
  const explicitDatabaseUrl = (getFlag(args, 'db-url')
2451
2925
  ?? (databaseMode === 'managed' ? process.env.DATABASE_URL : '')
2452
2926
  ?? '').trim();
2453
- const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, explicitDatabaseUrl);
2927
+ const databaseUrl = await resolveSetupDatabaseUrl(databaseMode, instanceName, explicitDatabaseUrl);
2454
2928
  const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(getFlag(args, 'db-intent'), '--db-intent');
2455
2929
  const provider = normalizeProvider(getFlag(args, 'provider') ?? process.env.LLM_PROVIDER ?? 'mock') ?? 'mock';
2456
2930
  if (!isSupportedProvider(provider)) {
@@ -2609,6 +3083,22 @@ function summarizeStatus(checks) {
2609
3083
  return 'warn';
2610
3084
  return 'pass';
2611
3085
  }
3086
+ async function resolveDatabaseAuthorityInfo(repoEnvFile, boundInstanceEnvFile) {
3087
+ const normalizedRepoEnvFile = repoEnvFile && fs_1.default.existsSync(repoEnvFile) ? repoEnvFile : null;
3088
+ const normalizedBoundEnvFile = boundInstanceEnvFile && fs_1.default.existsSync(boundInstanceEnvFile) ? boundInstanceEnvFile : null;
3089
+ const repoEnv = normalizedRepoEnvFile ? await readEnvFile(normalizedRepoEnvFile) : null;
3090
+ const boundEnv = normalizedBoundEnvFile ? await readEnvFile(normalizedBoundEnvFile) : null;
3091
+ const repoEnvDatabaseUrl = repoEnv?.DATABASE_URL?.trim() || null;
3092
+ const boundInstanceDatabaseUrl = boundEnv?.DATABASE_URL?.trim() || null;
3093
+ return {
3094
+ repoEnvFile: normalizedRepoEnvFile,
3095
+ repoEnvDatabaseUrl,
3096
+ boundInstanceDatabaseUrl,
3097
+ mismatch: Boolean(repoEnvDatabaseUrl
3098
+ && boundInstanceDatabaseUrl
3099
+ && repoEnvDatabaseUrl !== boundInstanceDatabaseUrl),
3100
+ };
3101
+ }
2612
3102
  function detectVsCodeMcpWorkspaceCheck(projectEnvPath) {
2613
3103
  const vscodeMcpPath = path_1.default.join(path_1.default.dirname(projectEnvPath), '.vscode', 'mcp.json');
2614
3104
  if (!fs_1.default.existsSync(vscodeMcpPath)) {
@@ -2680,6 +3170,9 @@ function collectDoctorRemediations(checks, envSource, envFile) {
2680
3170
  if (check.name === 'bound instance env' && check.status !== 'pass') {
2681
3171
  add('Run `iranti configure project` to refresh the project binding, or set IRANTI_INSTANCE_ENV in `.env.iranti` so doctor can inspect the bound local instance.');
2682
3172
  }
3173
+ if (check.name === 'runtime root selection' && check.status !== 'pass') {
3174
+ add('Rerun `iranti doctor` with `--root <runtime-root>` that matches the bound project instance, or refresh `.env.iranti` with `iranti configure project` if the binding is stale.');
3175
+ }
2683
3176
  if (check.name === 'vscode mcp workspace' && check.status !== 'pass') {
2684
3177
  add('Run `iranti codex-setup` from the project root to scaffold `.vscode/mcp.json`, or add an `iranti` server entry there manually for VS Code MCP clients.');
2685
3178
  }
@@ -2723,15 +3216,14 @@ function resolveDoctorEnvTarget(args) {
2723
3216
  envSource: `instance:${instanceName}`,
2724
3217
  };
2725
3218
  }
2726
- const repoEnv = path_1.default.join(cwd, '.env');
2727
- const projectEnv = path_1.default.join(cwd, '.env.iranti');
2728
- if (fs_1.default.existsSync(repoEnv)) {
2729
- debugLog('Doctor target resolved from repo env.', { envFile: repoEnv });
2730
- return { envFile: repoEnv, envSource: 'repo' };
3219
+ const discovered = findClosestDoctorEnvTarget(cwd);
3220
+ if (discovered.envFile && discovered.envSource === 'repo') {
3221
+ debugLog('Doctor target resolved from repo env.', { envFile: discovered.envFile });
3222
+ return discovered;
2731
3223
  }
2732
- if (fs_1.default.existsSync(projectEnv)) {
2733
- debugLog('Doctor target resolved from project binding.', { envFile: projectEnv });
2734
- return { envFile: projectEnv, envSource: 'project-binding' };
3224
+ if (discovered.envFile && discovered.envSource === 'project-binding') {
3225
+ debugLog('Doctor target resolved from project binding.', { envFile: discovered.envFile });
3226
+ return discovered;
2735
3227
  }
2736
3228
  debugLog('Doctor target resolution found no env file.', { cwd });
2737
3229
  return { envFile: null, envSource: 'repo' };
@@ -3022,6 +3514,23 @@ function deriveDatabaseUrlForMode(mode, instanceName, explicitDatabaseUrl) {
3022
3514
  }
3023
3515
  return `postgresql://${user}:${password}@${localDatabaseHost}:5432/iranti_${instanceName}`;
3024
3516
  }
3517
+ async function resolveSetupDatabaseUrl(mode, instanceName, explicitDatabaseUrl) {
3518
+ if (mode !== 'docker') {
3519
+ return deriveDatabaseUrlForMode(mode, instanceName, explicitDatabaseUrl);
3520
+ }
3521
+ const explicit = explicitDatabaseUrl?.trim() ?? '';
3522
+ if (explicit && !detectPlaceholder(explicit)) {
3523
+ return explicit;
3524
+ }
3525
+ const preferredPort = 5432;
3526
+ const dockerPublishedPorts = listPublishedDockerHostPorts();
3527
+ const selectedPort = await isPortUsable(preferredPort, '0.0.0.0', dockerPublishedPorts)
3528
+ ? preferredPort
3529
+ : await findNextAvailablePort(preferredPort + 1, '0.0.0.0', 50, dockerPublishedPorts);
3530
+ const user = encodeURIComponent((process.env.POSTGRES_USER ?? 'postgres').trim() || 'postgres');
3531
+ const password = encodeURIComponent((process.env.POSTGRES_PASSWORD ?? 'postgres').trim() || 'postgres');
3532
+ return `postgresql://${user}:${password}@${preferredLocalDatabaseHost()}:${selectedPort}/iranti_${instanceName}`;
3533
+ }
3025
3534
  function preferredLocalDatabaseHost() {
3026
3535
  return process.platform === 'win32' ? '127.0.0.1' : 'localhost';
3027
3536
  }
@@ -3906,6 +4415,15 @@ function removeIrantiClaudeHooksFromValue(value) {
3906
4415
  }
3907
4416
  return Object.keys(next).length === 0 ? null : next;
3908
4417
  }
4418
+ function removeIrantiAgentsBlockFromText(value) {
4419
+ if (!value.includes('<!-- iranti-rules -->'))
4420
+ return value;
4421
+ const next = value
4422
+ .replace(/<!-- iranti-rules -->[\s\S]*?<!-- \/iranti-rules -->/g, '')
4423
+ .replace(/\n{3,}/g, '\n\n')
4424
+ .trim();
4425
+ return next.length === 0 ? null : `${next}\n`;
4426
+ }
3909
4427
  async function cleanupProjectBindingIntegrations(projectPath) {
3910
4428
  const result = { removed: [], updated: [], warnings: [] };
3911
4429
  const candidates = [
@@ -3952,6 +4470,26 @@ async function cleanupProjectBindingIntegrations(projectPath) {
3952
4470
  }
3953
4471
  }
3954
4472
  }
4473
+ const agentsFile = path_1.default.join(projectPath, 'AGENTS.md');
4474
+ if (fs_1.default.existsSync(agentsFile)) {
4475
+ try {
4476
+ const existing = await promises_1.default.readFile(agentsFile, 'utf8');
4477
+ const next = removeIrantiAgentsBlockFromText(existing);
4478
+ if (next !== existing) {
4479
+ if (!next) {
4480
+ await promises_1.default.rm(agentsFile, { force: true });
4481
+ result.removed.push(agentsFile);
4482
+ }
4483
+ else {
4484
+ await writeText(agentsFile, next);
4485
+ result.updated.push(agentsFile);
4486
+ }
4487
+ }
4488
+ }
4489
+ catch {
4490
+ result.warnings.push(`Skipped unreadable Codex agents file ${agentsFile}`);
4491
+ }
4492
+ }
3955
4493
  return result;
3956
4494
  }
3957
4495
  async function cleanupProjectArtifacts(artifacts) {
@@ -4023,6 +4561,37 @@ async function cleanupProjectArtifacts(artifacts) {
4023
4561
  });
4024
4562
  }
4025
4563
  }
4564
+ if (artifact.agentsFile && fs_1.default.existsSync(artifact.agentsFile)) {
4565
+ try {
4566
+ const existing = await promises_1.default.readFile(artifact.agentsFile, 'utf8');
4567
+ const next = removeIrantiAgentsBlockFromText(existing);
4568
+ if (next !== existing) {
4569
+ if (!next) {
4570
+ await promises_1.default.rm(artifact.agentsFile, { force: true });
4571
+ results.push({
4572
+ label: 'project-agents',
4573
+ status: 'pass',
4574
+ detail: `Removed ${artifact.agentsFile}`,
4575
+ });
4576
+ }
4577
+ else {
4578
+ await writeText(artifact.agentsFile, next);
4579
+ results.push({
4580
+ label: 'project-agents',
4581
+ status: 'pass',
4582
+ detail: `Removed Iranti Codex block from ${artifact.agentsFile}`,
4583
+ });
4584
+ }
4585
+ }
4586
+ }
4587
+ catch {
4588
+ results.push({
4589
+ label: 'project-agents',
4590
+ status: 'warn',
4591
+ detail: `Skipped unreadable text file ${artifact.agentsFile}`,
4592
+ });
4593
+ }
4594
+ }
4026
4595
  }
4027
4596
  return results;
4028
4597
  }
@@ -4161,7 +4730,7 @@ async function uninstallCommand(args) {
4161
4730
  && !dryRun;
4162
4731
  if (execute && !dryRun) {
4163
4732
  if (requiresDetachedWindowsSelfUninstall) {
4164
- const artifactFiles = projectArtifacts.flatMap((artifact) => [artifact.bindingFile, artifact.mcpFile, artifact.claudeSettingsFile]
4733
+ const artifactFiles = projectArtifacts.flatMap((artifact) => [artifact.bindingFile, artifact.mcpFile, artifact.claudeSettingsFile, artifact.agentsFile]
4165
4734
  .filter((value) => Boolean(value)));
4166
4735
  const script = buildDetachedWindowsUninstallScript({
4167
4736
  parentPid: process.pid,
@@ -4473,7 +5042,7 @@ async function setupCommand(args) {
4473
5042
  const dependencyChecks = await collectDependencyChecks();
4474
5043
  printDependencyChecks(dependencyChecks);
4475
5044
  console.log('');
4476
- const plan = configPath ? parseSetupConfig(configPath) : defaultsSetupPlan(args);
5045
+ const plan = configPath ? await parseSetupConfig(configPath) : await defaultsSetupPlan(args);
4477
5046
  const result = await executeSetupPlan(plan);
4478
5047
  console.log(sectionTitle('Setup Complete'));
4479
5048
  console.log(` runtime root ${result.root}`);
@@ -4490,7 +5059,10 @@ async function setupCommand(args) {
4490
5059
  else {
4491
5060
  console.log(' projects');
4492
5061
  for (const binding of result.bindings) {
4493
- console.log(` - ${binding.projectPath} (${binding.agentId}, ${binding.projectMode})`);
5062
+ const learningSuffix = binding.learningStatus.status === 'written'
5063
+ ? `, learned=${binding.codebaseEntity}`
5064
+ : `, project-learning=${binding.learningStatus.status}`;
5065
+ console.log(` - ${binding.projectPath} (${binding.agentId}, ${binding.projectMode}${learningSuffix})`);
4494
5066
  }
4495
5067
  }
4496
5068
  printNextSteps([
@@ -4853,7 +5425,10 @@ async function setupCommand(args) {
4853
5425
  else {
4854
5426
  console.log(' projects');
4855
5427
  for (const binding of finalResult.bindings) {
4856
- console.log(` - ${binding.projectPath} (${binding.agentId}, ${binding.projectMode}, auto-remember=${binding.autoRemember ? 'true' : 'false'})`);
5428
+ const learningSuffix = binding.learningStatus.status === 'written'
5429
+ ? `, codebase=${binding.codebaseEntity}`
5430
+ : `, project-learning=${binding.learningStatus.status}`;
5431
+ console.log(` - ${binding.projectPath} (${binding.agentId}, ${binding.projectMode}, auto-remember=${binding.autoRemember ? 'true' : 'false'}${learningSuffix})`);
4857
5432
  }
4858
5433
  }
4859
5434
  const nextSteps = [
@@ -4868,8 +5443,37 @@ async function setupCommand(args) {
4868
5443
  }
4869
5444
  async function doctorCommand(args) {
4870
5445
  const json = hasFlag(args, 'json');
5446
+ const scope = normalizeScope(getFlag(args, 'scope'));
4871
5447
  const { envFile, envSource } = resolveDoctorEnvTarget(args);
5448
+ const repoEnvFile = findClosestAncestorFile(process.cwd(), '.env');
5449
+ const resolution = resolveInstallRootDetails(args, scope);
5450
+ const discovery = {
5451
+ selectedRuntimeRoot: resolution.root,
5452
+ selectionSource: resolution.source,
5453
+ selectionReason: describeRuntimeRootSource(resolution.source),
5454
+ boundRuntimeRoot: null,
5455
+ boundInstanceEnv: null,
5456
+ projectBindingFile: null,
5457
+ projectBindingSource: null,
5458
+ rootMismatch: false,
5459
+ otherRuntimeRoots: [],
5460
+ };
4872
5461
  const checks = [];
5462
+ let authority = {
5463
+ activeAuthority: summarizeActiveAuthority(envSource, null, null),
5464
+ activeBindingSource: null,
5465
+ activeBoundInstanceEnv: null,
5466
+ activeDatabaseUrl: null,
5467
+ activeDatabaseTarget: null,
5468
+ repoDatabaseUrl: null,
5469
+ repoDatabaseTarget: null,
5470
+ repoDatabaseDiffers: false,
5471
+ nearbyBindingSource: null,
5472
+ nearbyBoundInstanceEnv: null,
5473
+ nearbyBindingDatabaseUrl: null,
5474
+ nearbyBindingDatabaseTarget: null,
5475
+ nearbyBindingDiffers: false,
5476
+ };
4873
5477
  const version = getPackageVersion();
4874
5478
  const pushEnvironmentChecks = async (env, prefix = '') => {
4875
5479
  const databaseUrl = env.DATABASE_URL;
@@ -4902,7 +5506,7 @@ async function doctorCommand(args) {
4902
5506
  });
4903
5507
  try {
4904
5508
  if (!detectPlaceholder(databaseUrl)) {
4905
- (0, client_1.initDb)(databaseUrl);
5509
+ (0, client_1.initDb)(databaseUrl, { applicationName: 'iranti:cli:doctor' });
4906
5510
  databaseInitializedForDoctor = true;
4907
5511
  }
4908
5512
  const backendName = (0, backends_1.resolveVectorBackendName)({
@@ -4992,12 +5596,33 @@ async function doctorCommand(args) {
4992
5596
  const treatAsProjectBinding = envSource === 'project-binding'
4993
5597
  || path_1.default.basename(envFile).toLowerCase() === '.env.iranti'
4994
5598
  || (Boolean(env.IRANTI_URL?.trim()) && detectPlaceholder(env.DATABASE_URL));
5599
+ let linkedInstanceEnvFile = null;
5600
+ let linkedEnv = null;
4995
5601
  checks.push({
4996
5602
  name: 'environment file',
4997
5603
  status: 'pass',
4998
5604
  detail: `${envSource} env loaded from ${envFile}`,
4999
5605
  });
5000
5606
  if (treatAsProjectBinding) {
5607
+ const binding = await inspectProjectBinding(envFile);
5608
+ const boundRuntimeRoot = binding.runtimeRoot ? path_1.default.resolve(binding.runtimeRoot) : null;
5609
+ const selectedRuntimeRoot = path_1.default.resolve(resolution.root);
5610
+ const otherRuntimeRoots = Array.from(new Set([boundRuntimeRoot]
5611
+ .filter((candidate) => Boolean(candidate))
5612
+ .filter((candidate) => candidate !== selectedRuntimeRoot && fs_1.default.existsSync(candidate))));
5613
+ discovery.boundRuntimeRoot = boundRuntimeRoot;
5614
+ discovery.boundInstanceEnv = binding.instanceEnvFile;
5615
+ discovery.projectBindingFile = binding.bindingFile;
5616
+ discovery.projectBindingSource = 'doctor-target';
5617
+ discovery.rootMismatch = Boolean(boundRuntimeRoot && boundRuntimeRoot !== selectedRuntimeRoot);
5618
+ discovery.otherRuntimeRoots = otherRuntimeRoots;
5619
+ checks.push({
5620
+ name: 'runtime root selection',
5621
+ status: discovery.rootMismatch ? 'warn' : 'pass',
5622
+ detail: discovery.rootMismatch
5623
+ ? `Doctor selected runtime root ${selectedRuntimeRoot} (${describeRuntimeRootSource(resolution.source)}), but the project binding points at ${boundRuntimeRoot}.`
5624
+ : `Doctor selected runtime root ${selectedRuntimeRoot} (${describeRuntimeRootSource(resolution.source)}).`,
5625
+ });
5001
5626
  checks.push(detectPlaceholder(env.IRANTI_URL)
5002
5627
  ? {
5003
5628
  name: 'project binding url',
@@ -5023,28 +5648,28 @@ async function doctorCommand(args) {
5023
5648
  status: 'pass',
5024
5649
  detail: 'IRANTI_API_KEY is present in .env.iranti.',
5025
5650
  });
5026
- const linkedInstanceEnv = env.IRANTI_INSTANCE_ENV?.trim();
5027
- if (!linkedInstanceEnv) {
5651
+ linkedInstanceEnvFile = env.IRANTI_INSTANCE_ENV?.trim() || null;
5652
+ if (!linkedInstanceEnvFile) {
5028
5653
  checks.push({
5029
5654
  name: 'bound instance env',
5030
5655
  status: 'warn',
5031
5656
  detail: 'IRANTI_INSTANCE_ENV is not set in .env.iranti. Skipping database and provider checks for the bound instance.',
5032
5657
  });
5033
5658
  }
5034
- else if (!fs_1.default.existsSync(linkedInstanceEnv)) {
5659
+ else if (!fs_1.default.existsSync(linkedInstanceEnvFile)) {
5035
5660
  checks.push({
5036
5661
  name: 'bound instance env',
5037
5662
  status: 'warn',
5038
- detail: `Linked instance env not found: ${linkedInstanceEnv}. Skipping database and provider checks for the bound instance.`,
5663
+ detail: `Linked instance env not found: ${linkedInstanceEnvFile}. Skipping database and provider checks for the bound instance.`,
5039
5664
  });
5040
5665
  }
5041
5666
  else {
5042
5667
  checks.push({
5043
5668
  name: 'bound instance env',
5044
5669
  status: 'pass',
5045
- detail: `Using ${linkedInstanceEnv} for bound instance diagnostics.`,
5670
+ detail: `Using ${linkedInstanceEnvFile} for bound instance diagnostics.`,
5046
5671
  });
5047
- const linkedEnv = await readEnvFile(linkedInstanceEnv);
5672
+ linkedEnv = await readEnvFile(linkedInstanceEnvFile);
5048
5673
  await pushEnvironmentChecks(linkedEnv, 'bound instance ');
5049
5674
  }
5050
5675
  }
@@ -5062,11 +5687,55 @@ async function doctorCommand(args) {
5062
5687
  detail: 'IRANTI_API_KEY is present.',
5063
5688
  });
5064
5689
  }
5690
+ const nearbyProjectBindingFile = treatAsProjectBinding
5691
+ ? null
5692
+ : findClosestAncestorFile(path_1.default.dirname(envFile), '.env.iranti');
5693
+ const nearbyBindingEnv = await readEnvFileIfExists(nearbyProjectBindingFile);
5694
+ const nearbyBoundInstanceEnvFile = nearbyBindingEnv?.IRANTI_INSTANCE_ENV?.trim() || null;
5695
+ const nearbyBoundInstanceEnv = await readEnvFileIfExists(nearbyBoundInstanceEnvFile);
5696
+ authority = await buildOperatorAuthoritySummary({
5697
+ envSource,
5698
+ envFile,
5699
+ env,
5700
+ bindingFile: treatAsProjectBinding ? envFile : null,
5701
+ boundInstanceEnvFile: linkedInstanceEnvFile,
5702
+ boundInstanceEnv: linkedEnv,
5703
+ repoEnvFile,
5704
+ nearbyBindingFile: nearbyProjectBindingFile,
5705
+ nearbyBoundInstanceEnvFile,
5706
+ nearbyBoundInstanceEnv,
5707
+ });
5708
+ if (!treatAsProjectBinding && authority.nearbyBindingSource && authority.nearbyBindingDiffers) {
5709
+ checks.push({
5710
+ name: 'nearby project binding authority',
5711
+ status: 'warn',
5712
+ detail: `Repo env is the active doctor target, but nearby project binding ${authority.nearbyBindingSource} points at ${authority.nearbyBindingDatabaseTarget ?? 'an unknown database target'} via ${authority.nearbyBoundInstanceEnv ?? 'an unknown bound env'}. Direct DB checks against repo .env may miss facts stored in the bound instance DB.`,
5713
+ });
5714
+ }
5065
5715
  }
5716
+ debugLog('Doctor authority summary resolved.', {
5717
+ activeAuthority: authority.activeAuthority,
5718
+ activeBindingSource: authority.activeBindingSource ?? null,
5719
+ activeBoundInstanceEnv: authority.activeBoundInstanceEnv ?? null,
5720
+ activeDatabaseTarget: authority.activeDatabaseTarget ?? null,
5721
+ repoDatabaseTarget: authority.repoDatabaseTarget ?? null,
5722
+ repoDatabaseDiffers: authority.repoDatabaseDiffers,
5723
+ nearbyBindingSource: authority.nearbyBindingSource ?? null,
5724
+ nearbyBindingDatabaseTarget: authority.nearbyBindingDatabaseTarget ?? null,
5725
+ nearbyBindingDiffers: authority.nearbyBindingDiffers,
5726
+ });
5066
5727
  const result = {
5067
5728
  version,
5068
5729
  envSource,
5069
5730
  envFile,
5731
+ authority,
5732
+ selectedRuntimeRoot: discovery.selectedRuntimeRoot,
5733
+ selectedRuntimeRootSource: discovery.selectionSource,
5734
+ boundRuntimeRoot: discovery.boundRuntimeRoot,
5735
+ boundInstanceEnv: discovery.boundInstanceEnv,
5736
+ rootMismatch: discovery.rootMismatch,
5737
+ otherRuntimeRoots: discovery.otherRuntimeRoots,
5738
+ discovery,
5070
5739
  status: summarizeStatus(checks),
5071
5740
  checks,
5072
5741
  remediations: collectDoctorRemediations(checks, envSource, envFile),
@@ -5087,6 +5756,26 @@ async function doctorCommand(args) {
5087
5756
  : paint(result.status.toUpperCase(), 'red')}`);
5088
5757
  if (envFile)
5089
5758
  console.log(` env : ${envFile}`);
5759
+ console.log(` authority : ${authority.activeAuthority}`);
5760
+ if (authority.activeBindingSource)
5761
+ console.log(` binding : ${authority.activeBindingSource}`);
5762
+ if (authority.activeBoundInstanceEnv)
5763
+ console.log(` bound env : ${authority.activeBoundInstanceEnv}`);
5764
+ if (authority.activeDatabaseTarget)
5765
+ console.log(` active db : ${authority.activeDatabaseTarget}`);
5766
+ if (authority.activeDatabaseUrl)
5767
+ console.log(` active url: ${authority.activeDatabaseUrl}`);
5768
+ if (authority.repoDatabaseDiffers && authority.repoDatabaseTarget) {
5769
+ console.log(` repo db : ${authority.repoDatabaseTarget} (repo .env differs)`);
5770
+ }
5771
+ if (authority.nearbyBindingDiffers) {
5772
+ if (authority.nearbyBindingSource)
5773
+ console.log(` nearby binding : ${authority.nearbyBindingSource}`);
5774
+ if (authority.nearbyBoundInstanceEnv)
5775
+ console.log(` nearby bound env: ${authority.nearbyBoundInstanceEnv}`);
5776
+ if (authority.nearbyBindingDatabaseTarget)
5777
+ console.log(` nearby db : ${authority.nearbyBindingDatabaseTarget} (binding differs)`);
5778
+ }
5090
5779
  console.log('');
5091
5780
  for (const check of checks) {
5092
5781
  const marker = check.status === 'pass'
@@ -5120,6 +5809,24 @@ async function statusCommand(args) {
5120
5809
  const binding = projectEnv && fs_1.default.existsSync(projectEnv) ? await inspectProjectBinding(projectEnv) : null;
5121
5810
  const boundRuntimeRoot = binding?.runtimeRoot ?? null;
5122
5811
  const boundInstanceEnv = binding?.instanceEnvFile ?? null;
5812
+ const projectBindingEnv = await readEnvFileIfExists(projectEnv);
5813
+ const boundInstanceEnvMap = await readEnvFileIfExists(boundInstanceEnv);
5814
+ const activeStatusEnv = projectEnv && fs_1.default.existsSync(projectEnv)
5815
+ ? projectBindingEnv
5816
+ : await readEnvFileIfExists(repoEnv);
5817
+ const authority = await buildOperatorAuthoritySummary({
5818
+ envSource: projectEnv && fs_1.default.existsSync(projectEnv) ? 'project-binding' : repoEnv && fs_1.default.existsSync(repoEnv) ? 'repo' : 'environment',
5819
+ envFile: projectEnv && fs_1.default.existsSync(projectEnv)
5820
+ ? projectEnv
5821
+ : repoEnv && fs_1.default.existsSync(repoEnv)
5822
+ ? repoEnv
5823
+ : null,
5824
+ env: activeStatusEnv,
5825
+ bindingFile: projectEnv && fs_1.default.existsSync(projectEnv) ? projectEnv : null,
5826
+ boundInstanceEnvFile: boundInstanceEnv,
5827
+ boundInstanceEnv: boundInstanceEnvMap,
5828
+ repoEnvFile: repoEnv,
5829
+ });
5123
5830
  const rootMismatch = Boolean(boundRuntimeRoot && path_1.default.resolve(boundRuntimeRoot) !== path_1.default.resolve(root));
5124
5831
  const userInstallRuntimeRoot = fs_1.default.existsSync(path_1.default.join(resolution.userRoot, 'install.json')) ? resolution.userRoot : null;
5125
5832
  const systemInstallRuntimeRoot = fs_1.default.existsSync(path_1.default.join(resolution.systemRoot, 'install.json')) ? resolution.systemRoot : null;
@@ -5132,6 +5839,18 @@ async function statusCommand(args) {
5132
5839
  rows.push({ label: 'scope', value: scope });
5133
5840
  rows.push({ label: 'runtime_root', value: root });
5134
5841
  rows.push({ label: 'root_source', value: describeRuntimeRootSource(resolution.source) });
5842
+ rows.push({ label: 'authority', value: authority.activeAuthority });
5843
+ if (authority.activeBindingSource)
5844
+ rows.push({ label: 'binding_source', value: authority.activeBindingSource });
5845
+ if (authority.activeBoundInstanceEnv)
5846
+ rows.push({ label: 'bound_instance_env', value: authority.activeBoundInstanceEnv });
5847
+ if (authority.activeDatabaseTarget)
5848
+ rows.push({ label: 'active_db_target', value: authority.activeDatabaseTarget });
5849
+ if (authority.activeDatabaseUrl)
5850
+ rows.push({ label: 'active_db_url', value: authority.activeDatabaseUrl });
5851
+ if (authority.repoDatabaseDiffers && authority.repoDatabaseTarget) {
5852
+ rows.push({ label: 'repo_db_target', value: `${authority.repoDatabaseTarget} (repo .env differs)` });
5853
+ }
5135
5854
  if (boundRuntimeRoot)
5136
5855
  rows.push({ label: 'bound_root', value: boundRuntimeRoot });
5137
5856
  rows.push({ label: 'repo_env', value: repoEnv && fs_1.default.existsSync(repoEnv) ? repoEnv : '(missing)' });
@@ -5141,12 +5860,21 @@ async function statusCommand(args) {
5141
5860
  rows.push({ label: 'root_mismatch', value: 'project binding points at a different runtime root' });
5142
5861
  const instances = await collectRuntimeInstanceSummaries(root);
5143
5862
  const recommendedActions = Array.from(new Set(instances.flatMap((instance) => instance.repairHints)));
5863
+ debugLog('Status authority summary resolved.', {
5864
+ activeAuthority: authority.activeAuthority,
5865
+ activeBindingSource: authority.activeBindingSource ?? null,
5866
+ activeBoundInstanceEnv: authority.activeBoundInstanceEnv ?? null,
5867
+ activeDatabaseTarget: authority.activeDatabaseTarget ?? null,
5868
+ repoDatabaseTarget: authority.repoDatabaseTarget ?? null,
5869
+ repoDatabaseDiffers: authority.repoDatabaseDiffers,
5870
+ });
5144
5871
  if (json) {
5145
5872
  console.log(JSON.stringify({
5146
5873
  version: getPackageVersion(),
5147
5874
  scope,
5148
5875
  runtimeRoot: root,
5149
5876
  runtimeRootSource: resolution.source,
5877
+ authority,
5150
5878
  discovery: {
5151
5879
  selectedRuntimeRoot: root,
5152
5880
  selectionSource: resolution.source,
@@ -5196,6 +5924,15 @@ async function statusCommand(args) {
5196
5924
  console.log(` meta: ${instance.metaFile}`);
5197
5925
  console.log(` config: ${describeInstanceConfig(instance.config)}`);
5198
5926
  console.log(` runtime: ${describeInstanceRuntime(instance.runtime)}`);
5927
+ if (instance.projectCount === 0) {
5928
+ console.log(' projects: none bound');
5929
+ }
5930
+ else {
5931
+ console.log(` projects: ${instance.projectCount}`);
5932
+ for (const project of instance.boundProjects) {
5933
+ console.log(` - ${project.projectPath} (${project.agentId}, ${project.mode})`);
5934
+ }
5935
+ }
5199
5936
  if (instance.repairHints.length > 0) {
5200
5937
  console.log(' hints:');
5201
5938
  for (const hint of instance.repairHints) {
@@ -5215,6 +5952,108 @@ async function statusCommand(args) {
5215
5952
  }
5216
5953
  }
5217
5954
  }
5955
+ function handoffWriteProperties(key) {
5956
+ switch (key) {
5957
+ case 'status':
5958
+ return {
5959
+ memoryScope: 'project',
5960
+ capturePhase: 'manual',
5961
+ durableClass: 'handoff_status',
5962
+ canonicalKey: 'status',
5963
+ mergeStrategy: 'replace',
5964
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
5965
+ memoryScope: 'project',
5966
+ durableClass: 'handoff_status',
5967
+ mergeStrategy: 'replace',
5968
+ extraTags: ['handoff', 'task_memory'],
5969
+ }),
5970
+ };
5971
+ case 'next_step':
5972
+ return {
5973
+ memoryScope: 'project',
5974
+ capturePhase: 'manual',
5975
+ durableClass: 'next_step',
5976
+ canonicalKey: 'next_step',
5977
+ mergeStrategy: 'replace',
5978
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
5979
+ memoryScope: 'project',
5980
+ durableClass: 'next_step',
5981
+ mergeStrategy: 'replace',
5982
+ extraTags: ['handoff', 'task_memory'],
5983
+ }),
5984
+ };
5985
+ case 'current_owner':
5986
+ return {
5987
+ memoryScope: 'project',
5988
+ capturePhase: 'manual',
5989
+ durableClass: 'owner',
5990
+ canonicalKey: 'current_owner',
5991
+ mergeStrategy: 'replace',
5992
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
5993
+ memoryScope: 'project',
5994
+ durableClass: 'owner',
5995
+ mergeStrategy: 'replace',
5996
+ extraTags: ['handoff', 'task_memory'],
5997
+ }),
5998
+ };
5999
+ case 'blockers':
6000
+ return {
6001
+ memoryScope: 'project',
6002
+ capturePhase: 'manual',
6003
+ durableClass: 'open_risks',
6004
+ canonicalKey: 'blockers',
6005
+ mergeStrategy: 'replace',
6006
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
6007
+ memoryScope: 'project',
6008
+ durableClass: 'open_risks',
6009
+ mergeStrategy: 'replace',
6010
+ extraTags: ['handoff', 'task_memory', 'blockers'],
6011
+ }),
6012
+ };
6013
+ case 'artifacts':
6014
+ return {
6015
+ memoryScope: 'project',
6016
+ capturePhase: 'manual',
6017
+ durableClass: 'artifact',
6018
+ canonicalKey: 'artifacts',
6019
+ mergeStrategy: 'replace',
6020
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
6021
+ memoryScope: 'project',
6022
+ durableClass: 'artifact',
6023
+ mergeStrategy: 'replace',
6024
+ extraTags: ['handoff', 'task_memory'],
6025
+ }),
6026
+ };
6027
+ case 'active_handoff_task':
6028
+ return {
6029
+ memoryScope: 'project',
6030
+ capturePhase: 'manual',
6031
+ durableClass: 'handoff_task',
6032
+ canonicalKey: 'active_handoff_task',
6033
+ mergeStrategy: 'replace',
6034
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
6035
+ memoryScope: 'project',
6036
+ durableClass: 'handoff_task',
6037
+ mergeStrategy: 'replace',
6038
+ extraTags: ['handoff', 'project_memory'],
6039
+ }),
6040
+ };
6041
+ default:
6042
+ return {
6043
+ memoryScope: 'project',
6044
+ capturePhase: 'manual',
6045
+ durableClass: 'decision',
6046
+ canonicalKey: key,
6047
+ mergeStrategy: 'replace',
6048
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
6049
+ memoryScope: 'project',
6050
+ durableClass: 'decision',
6051
+ mergeStrategy: 'replace',
6052
+ extraTags: ['handoff', 'task_memory'],
6053
+ }),
6054
+ };
6055
+ }
6056
+ }
5218
6057
  async function collectRuntimeInstanceSummaries(root) {
5219
6058
  const instancesDir = path_1.default.join(root, 'instances');
5220
6059
  const instances = [];
@@ -5225,6 +6064,7 @@ async function collectRuntimeInstanceSummaries(root) {
5225
6064
  for (const entry of entries.filter((value) => value.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
5226
6065
  const { envFile, metaFile } = instancePaths(root, entry.name);
5227
6066
  const config = await inspectInstanceConfig(root, entry.name);
6067
+ const boundProjects = readInstanceProjectRegistry(root, entry.name);
5228
6068
  let port = '(unknown)';
5229
6069
  if (config.state.envPresent && config.state.envReadable) {
5230
6070
  try {
@@ -5241,6 +6081,8 @@ async function collectRuntimeInstanceSummaries(root) {
5241
6081
  port,
5242
6082
  envFile: config.state.envPresent ? envFile : '(missing)',
5243
6083
  metaFile: config.state.metaPresent ? metaFile : '(missing)',
6084
+ boundProjects,
6085
+ projectCount: boundProjects.length,
5244
6086
  config,
5245
6087
  runtime,
5246
6088
  repairHints: buildInstanceRepairHints(entry.name, config, runtime),
@@ -5514,6 +6356,10 @@ async function createInstanceCommand(args) {
5514
6356
  }
5515
6357
  const { instanceDir, envFile, metaFile } = instancePaths(root, name);
5516
6358
  const instanceAlreadyExisted = fs_1.default.existsSync(instanceDir);
6359
+ const existingEnv = fs_1.default.existsSync(envFile)
6360
+ ? await readEnvFile(envFile).catch(() => ({}))
6361
+ : {};
6362
+ const apiKeyPepper = resolveInstanceApiKeyPepper(existingEnv.IRANTI_API_KEY_PEPPER);
5517
6363
  if (instanceAlreadyExisted && !hasFlag(args, 'force')) {
5518
6364
  throw new Error(`Instance '${name}' already exists at ${instanceDir}. Use --force to overwrite.`);
5519
6365
  }
@@ -5538,17 +6384,25 @@ async function createInstanceCommand(args) {
5538
6384
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'active'));
5539
6385
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'resolved'));
5540
6386
  await ensureDir(path_1.default.join(instanceDir, 'escalation', 'archived'));
5541
- await writeText(envFile, makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir));
6387
+ await writeText(envFile, makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir, apiKeyPepper));
5542
6388
  await upsertEnvFile(envFile, {
6389
+ IRANTI_API_KEY_PEPPER: apiKeyPepper,
5543
6390
  LLM_PROVIDER: provider,
5544
6391
  ...(providerKey && providerKeyName ? { [providerKeyName]: providerKey } : {}),
5545
6392
  });
6393
+ const resolvedDatabaseIntent = resolveInstanceDatabaseIntent({
6394
+ instanceName: name,
6395
+ env: { DATABASE_URL: dbUrl },
6396
+ meta: null,
6397
+ dependencies: [],
6398
+ }).intent;
5546
6399
  const meta = {
5547
6400
  name,
5548
6401
  createdAt: new Date().toISOString(),
5549
6402
  port,
5550
6403
  envFile,
5551
6404
  instanceDir,
6405
+ ...(resolvedDatabaseIntent ? { databaseIntent: resolvedDatabaseIntent } : {}),
5552
6406
  };
5553
6407
  await writeJson(metaFile, meta);
5554
6408
  // Instance fully created — pop the rollback so it doesn't run on normal exit
@@ -5617,6 +6471,7 @@ async function showInstanceCommand(args) {
5617
6471
  : {};
5618
6472
  const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5619
6473
  const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
6474
+ const boundProjects = readInstanceProjectRegistry(root, name);
5620
6475
  const databaseIntent = resolveInstanceDatabaseIntent({
5621
6476
  instanceName: name,
5622
6477
  env,
@@ -5637,6 +6492,12 @@ async function showInstanceCommand(args) {
5637
6492
  if (dependencies.length > 0) {
5638
6493
  console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
5639
6494
  }
6495
+ if (boundProjects.length > 0) {
6496
+ console.log(' projects:');
6497
+ for (const project of boundProjects) {
6498
+ console.log(` - ${project.projectPath} (${project.agentId}, ${project.mode})`);
6499
+ }
6500
+ }
5640
6501
  console.log(` runtime: ${describeInstanceRuntime(runtime)}`);
5641
6502
  if (runtime.state?.healthUrl) {
5642
6503
  console.log(` health: ${runtime.state.healthUrl}`);
@@ -5742,10 +6603,21 @@ async function projectInitCommand(args) {
5742
6603
  IRANTI_INSTANCE: instanceName,
5743
6604
  IRANTI_INSTANCE_ENV: envFile,
5744
6605
  });
6606
+ const writtenBinding = await readEnvFile(outFile);
6607
+ const learningStatus = await (0, projectLearning_1.writeProjectLearningSnapshot)({
6608
+ projectPath,
6609
+ projectEnvFile: outFile,
6610
+ binding: writtenBinding,
6611
+ agentId,
6612
+ });
5745
6613
  console.log(sectionTitle('Project Initialized'));
5746
6614
  console.log(` status ${okLabel()}`);
5747
6615
  console.log(` wrote ${outFile}`);
5748
6616
  console.log(` mode ${projectMode}`);
6617
+ console.log(` codebase ${writtenBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath)}`);
6618
+ if (learningStatus.status !== 'written') {
6619
+ console.log(` learn ${paint(learningStatus.status, learningStatus.status === 'failed' ? 'red' : 'yellow')} (${learningStatus.detail})`);
6620
+ }
5749
6621
  printNextSteps([
5750
6622
  `iranti doctor --instance ${instanceName}`,
5751
6623
  'iranti chat',
@@ -5879,6 +6751,9 @@ async function configureInstanceCommand(args) {
5879
6751
  }
5880
6752
  updates[envKey] = undefined;
5881
6753
  }
6754
+ if (!env.IRANTI_API_KEY_PEPPER?.trim()) {
6755
+ updates.IRANTI_API_KEY_PEPPER = resolveInstanceApiKeyPepper();
6756
+ }
5882
6757
  let nextDependencies = currentDependencies;
5883
6758
  if (clearDockerContainer) {
5884
6759
  nextDependencies = currentDependencies.filter((dependency) => dependency.kind !== 'docker-container');
@@ -6031,7 +6906,7 @@ async function configureProjectCommand(args) {
6031
6906
  IRANTI_PERSONAL_MEMORY_ENTITY: explicitPersonalMemoryEntity ?? existing.IRANTI_PERSONAL_MEMORY_ENTITY ?? 'user/main',
6032
6907
  IRANTI_AUTO_REMEMBER: String(explicitAutoRemember ?? envFlagEnabled(existing.IRANTI_AUTO_REMEMBER)),
6033
6908
  IRANTI_PROJECT_MODE: normalizeProjectMode(explicitProjectMode, normalizeProjectMode(existing.IRANTI_PROJECT_MODE, inferProjectMode(projectPath, instanceEnvFile))),
6034
- IRANTI_INSTANCE: instanceName,
6909
+ IRANTI_INSTANCE: instanceName ?? existing.IRANTI_INSTANCE,
6035
6910
  IRANTI_INSTANCE_ENV: instanceEnvFile,
6036
6911
  };
6037
6912
  if (!updates.IRANTI_URL) {
@@ -6041,6 +6916,13 @@ async function configureProjectCommand(args) {
6041
6916
  throw new Error('Unable to determine IRANTI_API_KEY. Provide --api-key <token> or configure the instance first.');
6042
6917
  }
6043
6918
  const written = await writeProjectBinding(projectPath, updates);
6919
+ const writtenBinding = await readEnvFile(written);
6920
+ const learningStatus = await (0, projectLearning_1.writeProjectLearningSnapshot)({
6921
+ projectPath,
6922
+ projectEnvFile: written,
6923
+ binding: writtenBinding,
6924
+ agentId: updates.IRANTI_AGENT_ID,
6925
+ });
6044
6926
  const json = hasFlag(args, 'json');
6045
6927
  const result = {
6046
6928
  projectPath,
@@ -6050,6 +6932,8 @@ async function configureProjectCommand(args) {
6050
6932
  autoRemember: updates.IRANTI_AUTO_REMEMBER === 'true',
6051
6933
  projectMode: updates.IRANTI_PROJECT_MODE,
6052
6934
  instance: updates.IRANTI_INSTANCE ?? null,
6935
+ codebaseEntity: writtenBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
6936
+ projectLearning: learningStatus,
6053
6937
  };
6054
6938
  if (json) {
6055
6939
  console.log(JSON.stringify(result, null, 2));
@@ -6063,9 +6947,13 @@ async function configureProjectCommand(args) {
6063
6947
  console.log(` agent ${updates.IRANTI_AGENT_ID}`);
6064
6948
  console.log(` remember ${updates.IRANTI_AUTO_REMEMBER}`);
6065
6949
  console.log(` mode ${updates.IRANTI_PROJECT_MODE}`);
6950
+ console.log(` codebase ${writtenBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath)}`);
6066
6951
  if (updates.IRANTI_INSTANCE) {
6067
6952
  console.log(` instance ${updates.IRANTI_INSTANCE}`);
6068
6953
  }
6954
+ if (learningStatus.status !== 'written') {
6955
+ console.log(` learn ${paint(learningStatus.status, learningStatus.status === 'failed' ? 'red' : 'yellow')} (${learningStatus.detail})`);
6956
+ }
6069
6957
  printNextSteps([
6070
6958
  `iranti doctor${updates.IRANTI_INSTANCE ? ` --instance ${updates.IRANTI_INSTANCE}` : ''}`,
6071
6959
  ]);
@@ -6090,7 +6978,7 @@ async function authCreateKeyCommand(args) {
6090
6978
  throw new Error(`Instance '${instanceName}' still has a placeholder DATABASE_URL. Update ${envFile} first.`);
6091
6979
  }
6092
6980
  const scopes = scopesRaw.split(',').map((value) => value.trim()).filter(Boolean);
6093
- (0, client_1.initDb)(env.DATABASE_URL);
6981
+ (0, client_1.initDb)(env.DATABASE_URL, { applicationName: 'iranti:cli:auth_create' });
6094
6982
  const created = await (0, apiKeys_1.createOrRotateApiKey)({
6095
6983
  keyId,
6096
6984
  owner,
@@ -6104,7 +6992,7 @@ async function authCreateKeyCommand(args) {
6104
6992
  const resolvedProjectPath = path_1.default.resolve(projectPath);
6105
6993
  const existingBindingFile = path_1.default.join(resolvedProjectPath, '.env.iranti');
6106
6994
  const existingBinding = fs_1.default.existsSync(existingBindingFile) ? await readEnvFile(existingBindingFile) : {};
6107
- await writeProjectBinding(resolvedProjectPath, {
6995
+ const written = await writeProjectBinding(resolvedProjectPath, {
6108
6996
  IRANTI_URL: `http://localhost:${env.IRANTI_PORT ?? '3001'}`,
6109
6997
  IRANTI_API_KEY: created.token,
6110
6998
  IRANTI_AGENT_ID: agentId ?? existingBinding.IRANTI_AGENT_ID ?? 'my_agent',
@@ -6114,6 +7002,13 @@ async function authCreateKeyCommand(args) {
6114
7002
  IRANTI_INSTANCE: instanceName,
6115
7003
  IRANTI_INSTANCE_ENV: envFile,
6116
7004
  });
7005
+ const binding = await readEnvFile(written);
7006
+ await (0, projectLearning_1.writeProjectLearningSnapshot)({
7007
+ projectPath: resolvedProjectPath,
7008
+ projectEnvFile: written,
7009
+ binding,
7010
+ agentId: agentId ?? binding.IRANTI_AGENT_ID ?? 'my_agent',
7011
+ });
6117
7012
  }
6118
7013
  if (hasFlag(args, 'json')) {
6119
7014
  console.log(JSON.stringify({
@@ -6154,7 +7049,7 @@ async function authListKeysCommand(args) {
6154
7049
  if (detectPlaceholder(env.DATABASE_URL)) {
6155
7050
  throw new Error(`Instance '${instanceName}' still has a placeholder DATABASE_URL. Update ${envFile} first.`);
6156
7051
  }
6157
- (0, client_1.initDb)(env.DATABASE_URL);
7052
+ (0, client_1.initDb)(env.DATABASE_URL, { applicationName: 'iranti:cli:auth_list' });
6158
7053
  const keys = await (0, apiKeys_1.listApiKeys)();
6159
7054
  if (hasFlag(args, 'json')) {
6160
7055
  console.log(JSON.stringify({ instance: instanceName, keys }, null, 2));
@@ -6182,7 +7077,7 @@ async function authRevokeKeyCommand(args) {
6182
7077
  if (detectPlaceholder(env.DATABASE_URL)) {
6183
7078
  throw new Error(`Instance '${instanceName}' still has a placeholder DATABASE_URL. Update ${envFile} first.`);
6184
7079
  }
6185
- (0, client_1.initDb)(env.DATABASE_URL);
7080
+ (0, client_1.initDb)(env.DATABASE_URL, { applicationName: 'iranti:cli:auth_revoke' });
6186
7081
  const revoked = await (0, apiKeys_1.revokeApiKey)(keyId);
6187
7082
  if (!revoked) {
6188
7083
  throw new Error(`API key not found: ${keyId}`);
@@ -6315,6 +7210,52 @@ async function attendCommand(args) {
6315
7210
  await (0, client_1.disconnectDb)().catch(() => undefined);
6316
7211
  }
6317
7212
  }
7213
+ async function issuesCommand(args) {
7214
+ try {
7215
+ const json = hasFlag(args, 'json');
7216
+ const target = await resolveAttendantCliTarget(args);
7217
+ const entity = resolveIssueEntity(args);
7218
+ const statusFilter = resolveIssueStatusFilter(args);
7219
+ const allFacts = await target.iranti.queryAll(entity);
7220
+ const parsedIssueFacts = allFacts
7221
+ .map(parseIssueListFact)
7222
+ .filter((result) => result.item || result.invalid);
7223
+ const canonicalItems = parsedIssueFacts
7224
+ .map((result) => result.item)
7225
+ .filter((item) => Boolean(item));
7226
+ const invalidIssueLikeFacts = parsedIssueFacts
7227
+ .map((result) => result.invalid)
7228
+ .filter((item) => Boolean(item))
7229
+ .sort((a, b) => a.key.localeCompare(b.key));
7230
+ const inventoryCounts = {
7231
+ open: canonicalItems.filter((item) => item.status === 'open').length,
7232
+ resolved: canonicalItems.filter((item) => item.status === 'resolved').length,
7233
+ invalid: invalidIssueLikeFacts.length,
7234
+ canonicalTotal: canonicalItems.length,
7235
+ issueLikeTotal: canonicalItems.length + invalidIssueLikeFacts.length,
7236
+ };
7237
+ const items = canonicalItems
7238
+ .filter((item) => !statusFilter || item.status === statusFilter)
7239
+ .sort(compareIssueListItems);
7240
+ await (0, staffEventRegistry_1.flushStaffEventEmitter)().catch(() => undefined);
7241
+ if (json) {
7242
+ console.log(JSON.stringify({
7243
+ entity,
7244
+ status: statusFilter,
7245
+ total: items.length,
7246
+ counts: inventoryCounts,
7247
+ invalidIssueLikeFacts,
7248
+ items,
7249
+ }, null, 2));
7250
+ return;
7251
+ }
7252
+ printIssuesResult(entity, items, statusFilter, inventoryCounts, invalidIssueLikeFacts);
7253
+ }
7254
+ finally {
7255
+ await (0, staffEventRegistry_1.flushStaffEventEmitter)().catch(() => undefined);
7256
+ await (0, client_1.disconnectDb)().catch(() => undefined);
7257
+ }
7258
+ }
6318
7259
  async function handoffCommand(args) {
6319
7260
  try {
6320
7261
  const json = hasFlag(args, 'json');
@@ -6405,6 +7346,7 @@ async function handoffCommand(args) {
6405
7346
  confidence,
6406
7347
  source,
6407
7348
  agent: target.agentId,
7349
+ properties: handoffWriteProperties(write.key),
6408
7350
  });
6409
7351
  }
6410
7352
  await (0, staffEventRegistry_1.flushStaffEventEmitter)().catch(() => undefined);
@@ -6431,7 +7373,7 @@ async function handoffCommand(args) {
6431
7373
  function printClaudeSetupHelp() {
6432
7374
  console.log([
6433
7375
  'Scaffold Claude Code MCP and hook files for the current project.',
6434
- 'Use this when a bound repo should be ready for Claude Code without hand-editing `.mcp.json`, `.vscode/mcp.json`, or `.claude/settings.local.json`.',
7376
+ 'Use this when a bound repo should be ready for Claude Code without hand-editing `.mcp.json`, `.vscode/mcp.json`, `.claude/settings.local.json`, or `CLAUDE.md`.',
6435
7377
  '',
6436
7378
  'Usage:',
6437
7379
  ' iranti claude-setup [path] [--project-env <path>] [--force]',
@@ -6445,9 +7387,10 @@ function printClaudeSetupHelp() {
6445
7387
  '',
6446
7388
  'Notes:',
6447
7389
  ' - Expects a project binding at .env.iranti unless --project-env is supplied.',
6448
- ' - Writes .mcp.json, .vscode/mcp.json, and .claude/settings.local.json.',
7390
+ ' - Writes .mcp.json, .vscode/mcp.json, .claude/settings.local.json, and a local `CLAUDE.md` Iranti protocol block.',
6449
7391
  ' - Adds the Iranti MCP server to existing .mcp.json / .vscode/mcp.json files without removing other servers.',
6450
7392
  ' - Leaves existing Claude hook files untouched unless --force is supplied.',
7393
+ ' - The generated protocol block explicitly requires handshake at session start, attend before reply and before/after discovery, checkpointing at natural pauses/interrupted work, and durable writes after confirmed findings.',
6451
7394
  '',
6452
7395
  'Scan mode (--scan):',
6453
7396
  ' - Scans immediate subdirectories of the given dir by default.',
@@ -6570,6 +7513,7 @@ async function discoverProjectArtifacts(scanRoots) {
6570
7513
  const bindingFile = path_1.default.join(current, '.env.iranti');
6571
7514
  const mcpFile = path_1.default.join(current, '.mcp.json');
6572
7515
  const claudeSettingsFile = path_1.default.join(current, '.claude', 'settings.local.json');
7516
+ const agentsFile = path_1.default.join(current, 'AGENTS.md');
6573
7517
  const artifact = { projectPath: current };
6574
7518
  if (fs_1.default.existsSync(bindingFile))
6575
7519
  artifact.bindingFile = bindingFile;
@@ -6579,7 +7523,18 @@ async function discoverProjectArtifacts(scanRoots) {
6579
7523
  if (fs_1.default.existsSync(claudeSettingsFile) && hasIrantiClaudeHookSettings(readJsonFile(claudeSettingsFile))) {
6580
7524
  artifact.claudeSettingsFile = claudeSettingsFile;
6581
7525
  }
6582
- if (artifact.bindingFile || artifact.mcpFile || artifact.claudeSettingsFile) {
7526
+ if (fs_1.default.existsSync(agentsFile)) {
7527
+ try {
7528
+ const agentsText = await promises_1.default.readFile(agentsFile, 'utf8');
7529
+ if (agentsText.includes('<!-- iranti-rules -->')) {
7530
+ artifact.agentsFile = agentsFile;
7531
+ }
7532
+ }
7533
+ catch {
7534
+ // Ignore unreadable AGENTS files during discovery; cleanup will warn if selected.
7535
+ }
7536
+ }
7537
+ if (artifact.bindingFile || artifact.mcpFile || artifact.claudeSettingsFile || artifact.agentsFile) {
6583
7538
  projects.set(current, artifact);
6584
7539
  }
6585
7540
  for (const entry of entries) {
@@ -6674,9 +7629,12 @@ async function claudeSetupCommand(args) {
6674
7629
  console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
6675
7630
  console.log(` vscode ${path_1.default.join(projectPath, '.vscode', 'mcp.json')}`);
6676
7631
  console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
6677
- console.log(` mcp status ${result.mcp}`);
6678
- console.log(` vscode status ${result.vscodeMcp}`);
6679
- console.log(` settings status ${result.settings}`);
7632
+ console.log(` claude.md ${path_1.default.join(projectPath, 'CLAUDE.md')}`);
7633
+ console.log(` mcp status ${result.mcp}`);
7634
+ console.log(` vscode status ${result.vscodeMcp}`);
7635
+ console.log(` settings status ${result.settings}`);
7636
+ console.log(` claude.md status ${result.claudeMd}`);
7637
+ console.log(` memory closeout ${result.closeout.status} (${result.closeout.detail})`);
6680
7638
  console.log(`${infoLabel()} Next: open Claude Code in this project and verify Iranti tools are available.`);
6681
7639
  }
6682
7640
  async function chatCommand(args) {
@@ -6760,6 +7718,9 @@ function printHandshakeHelp() {
6760
7718
  function printAttendHelp() {
6761
7719
  (0, cliHelpRenderer_1.printAttendHelp)({ sectionTitle, commandText });
6762
7720
  }
7721
+ function printIssuesHelp() {
7722
+ (0, cliHelpRenderer_1.printIssuesHelp)({ sectionTitle, commandText });
7723
+ }
6763
7724
  function printHandoffHelp() {
6764
7725
  (0, cliHelpRenderer_1.printHandoffHelp)({ sectionTitle, commandText });
6765
7726
  }
@@ -6772,8 +7733,305 @@ function printResolveHelp() {
6772
7733
  function printProviderKeyHelp() {
6773
7734
  (0, cliHelpRenderer_1.printProviderKeyHelp)({ sectionTitle, commandText });
6774
7735
  }
7736
+ function printMcpHelp() {
7737
+ console.log([
7738
+ 'MCP server and maintenance commands.',
7739
+ 'Use this when the stdio MCP server should be started directly, or when you need to inspect and clean stale MCP wrapper/server pairs.',
7740
+ '',
7741
+ 'Usage:',
7742
+ ' iranti mcp',
7743
+ ' iranti mcp cleanup [--dry-run] [--json]',
7744
+ '',
7745
+ 'Notes:',
7746
+ ' - `iranti mcp` starts the stdio MCP server for Claude, Codex, or another MCP client.',
7747
+ ' - `iranti mcp cleanup` only removes stale launcher/server pairs that no longer have a live host ancestor.',
7748
+ ' - Active chains still rooted in `claude.exe` or `codex.exe` are reported but not killed.',
7749
+ ' - Current implementation is tuned for Windows process trees.',
7750
+ ].join('\n'));
7751
+ }
7752
+ function isMcpLauncherCommand(commandLine) {
7753
+ if (!commandLine)
7754
+ return false;
7755
+ const lower = commandLine.toLowerCase();
7756
+ return lower.includes('\\node_modules\\iranti\\bin\\iranti.js')
7757
+ && /\bmcp\b/.test(lower);
7758
+ }
7759
+ function isMcpChildCommand(commandLine) {
7760
+ if (!commandLine)
7761
+ return false;
7762
+ return commandLine.toLowerCase().includes('\\dist\\scripts\\iranti-mcp.js');
7763
+ }
7764
+ function collectWindowsProcessSnapshot() {
7765
+ const probe = runCommandCapture('powershell', [
7766
+ '-NoProfile',
7767
+ '-Command',
7768
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, Name, CommandLine | ConvertTo-Json -Compress',
7769
+ ]);
7770
+ if (probe.status !== 0) {
7771
+ throw cliError('IRANTI_MCP_CLEANUP_PROBE_FAILED', 'Failed to inspect Windows process state for MCP cleanup.', ['Retry with `--debug` to inspect the PowerShell probe output.'], { stderr: probe.stderr.trim() || null });
7772
+ }
7773
+ const payload = JSON.parse(probe.stdout);
7774
+ const rows = Array.isArray(payload) ? payload : [payload];
7775
+ return rows.map((row) => ({
7776
+ pid: Number(row.ProcessId ?? 0),
7777
+ parentPid: row.ParentProcessId === null || row.ParentProcessId === undefined
7778
+ ? null
7779
+ : Number(row.ParentProcessId),
7780
+ name: String(row.Name ?? ''),
7781
+ commandLine: String(row.CommandLine ?? ''),
7782
+ })).filter((row) => Number.isFinite(row.pid) && row.pid > 0);
7783
+ }
7784
+ function summarizeMcpCleanupCounts(candidates) {
7785
+ return {
7786
+ stale_no_host_ancestor: candidates.filter((candidate) => candidate.status === 'stale_no_host_ancestor').length,
7787
+ stale_shell_no_host: candidates.filter((candidate) => candidate.status === 'stale_shell_no_host').length,
7788
+ stale_child_parent_missing: candidates.filter((candidate) => candidate.status === 'stale_child_parent_missing').length,
7789
+ stale_launcher_only: candidates.filter((candidate) => candidate.status === 'stale_launcher_only').length,
7790
+ attached_claude: candidates.filter((candidate) => candidate.status === 'attached_claude').length,
7791
+ attached_codex: candidates.filter((candidate) => candidate.status === 'attached_codex').length,
7792
+ attached_or_uncertain: candidates.filter((candidate) => candidate.status === 'attached_or_uncertain').length,
7793
+ };
7794
+ }
7795
+ function buildMcpCleanupReport(rows) {
7796
+ const protectedPids = currentProcessFamilyPids();
7797
+ const index = new Map();
7798
+ for (const row of rows) {
7799
+ index.set(row.pid, row);
7800
+ }
7801
+ const warnings = [];
7802
+ const candidates = [];
7803
+ const matchedLaunchers = new Set();
7804
+ const childByLauncher = new Map();
7805
+ for (const row of rows) {
7806
+ if (row.name.toLowerCase() !== 'node.exe')
7807
+ continue;
7808
+ if (!isMcpChildCommand(row.commandLine))
7809
+ continue;
7810
+ if (protectedPids.has(row.pid))
7811
+ continue;
7812
+ const parent = row.parentPid ? index.get(row.parentPid) : undefined;
7813
+ if (!parent) {
7814
+ candidates.push({
7815
+ status: 'stale_child_parent_missing',
7816
+ childPid: row.pid,
7817
+ reason: 'MCP child process has no live launcher parent.',
7818
+ });
7819
+ continue;
7820
+ }
7821
+ if (!isMcpLauncherCommand(parent.commandLine)) {
7822
+ candidates.push({
7823
+ status: 'attached_or_uncertain',
7824
+ childPid: row.pid,
7825
+ reason: 'MCP child process is attached to a parent that is not a recognized Iranti MCP launcher.',
7826
+ });
7827
+ continue;
7828
+ }
7829
+ matchedLaunchers.add(parent.pid);
7830
+ childByLauncher.set(parent.pid, row);
7831
+ if (protectedPids.has(parent.pid)) {
7832
+ candidates.push({
7833
+ status: 'attached_or_uncertain',
7834
+ launcherPid: parent.pid,
7835
+ childPid: row.pid,
7836
+ reason: 'Skipping the current CLI process family.',
7837
+ });
7838
+ continue;
7839
+ }
7840
+ const grand = parent.parentPid ? index.get(parent.parentPid) : undefined;
7841
+ const great = grand?.parentPid ? index.get(grand.parentPid) : undefined;
7842
+ const grandName = grand?.name.toLowerCase() ?? '';
7843
+ const greatName = great?.name.toLowerCase() ?? '';
7844
+ if (!grand) {
7845
+ candidates.push({
7846
+ status: 'stale_no_host_ancestor',
7847
+ launcherPid: parent.pid,
7848
+ childPid: row.pid,
7849
+ reason: 'Launcher parent exists, but no live host ancestor remains.',
7850
+ });
7851
+ continue;
7852
+ }
7853
+ if (grandName === 'cmd.exe' && !great) {
7854
+ candidates.push({
7855
+ status: 'stale_shell_no_host',
7856
+ launcherPid: parent.pid,
7857
+ childPid: row.pid,
7858
+ reason: 'Launcher is still wrapped by cmd.exe, but the host process above cmd.exe is gone.',
7859
+ });
7860
+ continue;
7861
+ }
7862
+ if (grandName === 'cmd.exe' && greatName === 'claude.exe') {
7863
+ candidates.push({
7864
+ status: 'attached_claude',
7865
+ launcherPid: parent.pid,
7866
+ childPid: row.pid,
7867
+ host: 'claude',
7868
+ reason: 'Chain is still rooted in claude.exe.',
7869
+ });
7870
+ continue;
7871
+ }
7872
+ if (grandName === 'cmd.exe' && greatName === 'codex.exe') {
7873
+ candidates.push({
7874
+ status: 'attached_codex',
7875
+ launcherPid: parent.pid,
7876
+ childPid: row.pid,
7877
+ host: 'codex',
7878
+ reason: 'Chain is still rooted in codex.exe.',
7879
+ });
7880
+ continue;
7881
+ }
7882
+ candidates.push({
7883
+ status: 'attached_or_uncertain',
7884
+ launcherPid: parent.pid,
7885
+ childPid: row.pid,
7886
+ reason: 'Launcher/server pair still has a live ancestor that is not a known stale shape.',
7887
+ });
7888
+ }
7889
+ for (const row of rows) {
7890
+ if (row.name.toLowerCase() !== 'node.exe')
7891
+ continue;
7892
+ if (!isMcpLauncherCommand(row.commandLine))
7893
+ continue;
7894
+ if (matchedLaunchers.has(row.pid) || protectedPids.has(row.pid))
7895
+ continue;
7896
+ const grand = row.parentPid ? index.get(row.parentPid) : undefined;
7897
+ const great = grand?.parentPid ? index.get(grand.parentPid) : undefined;
7898
+ const grandName = grand?.name.toLowerCase() ?? '';
7899
+ const greatName = great?.name.toLowerCase() ?? '';
7900
+ if (!grand) {
7901
+ candidates.push({
7902
+ status: 'stale_launcher_only',
7903
+ launcherPid: row.pid,
7904
+ reason: 'Launcher remains alive without a child or live host ancestor.',
7905
+ });
7906
+ }
7907
+ else if (grandName === 'cmd.exe' && !great) {
7908
+ candidates.push({
7909
+ status: 'stale_launcher_only',
7910
+ launcherPid: row.pid,
7911
+ reason: 'Launcher is still wrapped by cmd.exe, but the host process above cmd.exe is gone.',
7912
+ });
7913
+ }
7914
+ }
7915
+ const safeStatuses = new Set([
7916
+ 'stale_no_host_ancestor',
7917
+ 'stale_shell_no_host',
7918
+ 'stale_child_parent_missing',
7919
+ 'stale_launcher_only',
7920
+ ]);
7921
+ const safeCandidates = candidates.filter((candidate) => safeStatuses.has(candidate.status));
7922
+ const skippedCandidates = candidates.filter((candidate) => !safeStatuses.has(candidate.status));
7923
+ if (safeCandidates.length === 0) {
7924
+ warnings.push('No stale MCP launcher/server pairs were found with the current safe cleanup rule.');
7925
+ }
7926
+ return {
7927
+ platform: process.platform,
7928
+ supported: process.platform === 'win32',
7929
+ dryRun: true,
7930
+ counts: summarizeMcpCleanupCounts(candidates),
7931
+ safeCandidates,
7932
+ skippedCandidates,
7933
+ cleaned: [],
7934
+ warnings,
7935
+ };
7936
+ }
7937
+ function printMcpCleanupReport(report) {
7938
+ console.log(sectionTitle('MCP Cleanup'));
7939
+ if (!report.supported) {
7940
+ console.log(`${warnLabel()} MCP cleanup is currently supported on Windows only.`);
7941
+ return;
7942
+ }
7943
+ console.log(` safe stale candidates ${report.safeCandidates.length}`);
7944
+ console.log(` attached claude ${report.counts.attached_claude}`);
7945
+ console.log(` attached codex ${report.counts.attached_codex}`);
7946
+ console.log(` other live / uncertain ${report.counts.attached_or_uncertain}`);
7947
+ console.log(` cleaned entries ${report.cleaned.length}`);
7948
+ console.log(` mode ${report.dryRun ? 'dry-run' : 'execute'}`);
7949
+ console.log('');
7950
+ if (report.safeCandidates.length > 0) {
7951
+ console.log(sectionTitle('Safe Targets'));
7952
+ for (const candidate of report.safeCandidates) {
7953
+ const ids = [
7954
+ candidate.launcherPid ? `launcher=${candidate.launcherPid}` : null,
7955
+ candidate.childPid ? `child=${candidate.childPid}` : null,
7956
+ ].filter((value) => Boolean(value));
7957
+ console.log(` ${commandText(candidate.status)} ${ids.join(' ')} - ${candidate.reason}`);
7958
+ }
7959
+ console.log('');
7960
+ }
7961
+ if (report.skippedCandidates.length > 0) {
7962
+ console.log(sectionTitle('Skipped Targets'));
7963
+ for (const candidate of report.skippedCandidates) {
7964
+ const ids = [
7965
+ candidate.launcherPid ? `launcher=${candidate.launcherPid}` : null,
7966
+ candidate.childPid ? `child=${candidate.childPid}` : null,
7967
+ ].filter((value) => Boolean(value));
7968
+ console.log(` ${commandText(candidate.status)} ${ids.join(' ')} - ${candidate.reason}`);
7969
+ }
7970
+ console.log('');
7971
+ }
7972
+ for (const warning of report.warnings) {
7973
+ console.log(`${warnLabel()} ${warning}`);
7974
+ }
7975
+ }
7976
+ async function mcpCleanupCommand(args) {
7977
+ const json = hasFlag(args, 'json');
7978
+ const dryRun = hasFlag(args, 'dry-run');
7979
+ if (process.platform !== 'win32') {
7980
+ const report = {
7981
+ platform: process.platform,
7982
+ supported: false,
7983
+ dryRun,
7984
+ counts: {
7985
+ stale_no_host_ancestor: 0,
7986
+ stale_shell_no_host: 0,
7987
+ stale_child_parent_missing: 0,
7988
+ stale_launcher_only: 0,
7989
+ attached_claude: 0,
7990
+ attached_codex: 0,
7991
+ attached_or_uncertain: 0,
7992
+ },
7993
+ safeCandidates: [],
7994
+ skippedCandidates: [],
7995
+ cleaned: [],
7996
+ warnings: ['MCP cleanup is currently implemented for Windows process trees only.'],
7997
+ };
7998
+ if (json) {
7999
+ console.log(JSON.stringify(report, null, 2));
8000
+ return;
8001
+ }
8002
+ printMcpCleanupReport(report);
8003
+ return;
8004
+ }
8005
+ const report = buildMcpCleanupReport(collectWindowsProcessSnapshot());
8006
+ report.dryRun = dryRun;
8007
+ if (!dryRun) {
8008
+ const cleaned = new Set();
8009
+ for (const candidate of report.safeCandidates) {
8010
+ if (candidate.childPid && !cleaned.has(`child:${candidate.childPid}`)) {
8011
+ const proc = runCommandCapture('powershell', ['-NoProfile', '-Command', `Stop-Process -Id ${candidate.childPid} -Force -ErrorAction SilentlyContinue`]);
8012
+ if (proc.status === 0) {
8013
+ report.cleaned.push({ pid: candidate.childPid, role: 'child' });
8014
+ cleaned.add(`child:${candidate.childPid}`);
8015
+ }
8016
+ }
8017
+ if (candidate.launcherPid && !cleaned.has(`launcher:${candidate.launcherPid}`)) {
8018
+ const proc = runCommandCapture('powershell', ['-NoProfile', '-Command', `Stop-Process -Id ${candidate.launcherPid} -Force -ErrorAction SilentlyContinue`]);
8019
+ if (proc.status === 0) {
8020
+ report.cleaned.push({ pid: candidate.launcherPid, role: 'launcher' });
8021
+ cleaned.add(`launcher:${candidate.launcherPid}`);
8022
+ }
8023
+ }
8024
+ }
8025
+ }
8026
+ if (json) {
8027
+ console.log(JSON.stringify(report, null, 2));
8028
+ return;
8029
+ }
8030
+ printMcpCleanupReport(report);
8031
+ }
6775
8032
  async function main() {
6776
8033
  const args = parseArgs(process.argv.slice(2));
8034
+ ACTIVE_PARSED_ARGS = args;
6777
8035
  setCliDebugFlags(args);
6778
8036
  debugLog('CLI invocation started.', {
6779
8037
  command: args.command,
@@ -7001,6 +8259,14 @@ async function main() {
7001
8259
  await attendCommand(args);
7002
8260
  return;
7003
8261
  }
8262
+ if (args.command === 'issues') {
8263
+ if (hasFlag(args, 'help')) {
8264
+ printIssuesHelp();
8265
+ return;
8266
+ }
8267
+ await issuesCommand(args);
8268
+ return;
8269
+ }
7004
8270
  if (args.command === 'handoff') {
7005
8271
  if (hasFlag(args, 'help')) {
7006
8272
  printHandoffHelp();
@@ -7026,6 +8292,26 @@ async function main() {
7026
8292
  return;
7027
8293
  }
7028
8294
  if (args.command === 'mcp') {
8295
+ if (!args.subcommand || args.subcommand === 'server') {
8296
+ if (hasFlag(args, 'help')) {
8297
+ printMcpHelp();
8298
+ return;
8299
+ }
8300
+ await handoffToScript('iranti-mcp', args.subcommand === 'server' ? process.argv.slice(4) : process.argv.slice(3));
8301
+ return;
8302
+ }
8303
+ if (args.subcommand === 'cleanup') {
8304
+ if (hasFlag(args, 'help')) {
8305
+ printMcpHelp();
8306
+ return;
8307
+ }
8308
+ await mcpCleanupCommand(args);
8309
+ return;
8310
+ }
8311
+ if (args.subcommand === 'help' || args.subcommand === '--help') {
8312
+ printMcpHelp();
8313
+ return;
8314
+ }
7029
8315
  await handoffToScript('iranti-mcp', process.argv.slice(3));
7030
8316
  return;
7031
8317
  }
@@ -7065,24 +8351,40 @@ main().then(async () => {
7065
8351
  await (0, staffEventRegistry_1.flushStaffEventEmitter)();
7066
8352
  }
7067
8353
  catch { }
7068
- const formattedError = (0, commandErrors_1.rewriteCommandError)('iranti', err);
7069
- const message = formattedError.message;
7070
- const code = err instanceof CliError ? err.code : null;
8354
+ const failure = normalizeCliFailure(err);
8355
+ const code = failure.code ?? 'IRANTI_COMMAND_FAILED';
8356
+ const message = failure.message;
8357
+ const hints = failure instanceof CliError ? failure.hints : (failure.hints ?? []);
8358
+ const details = failure instanceof CliError ? failure.details : undefined;
8359
+ if (wantsJsonErrorEnvelope(ACTIVE_PARSED_ARGS)) {
8360
+ const payload = {
8361
+ ok: false,
8362
+ error: {
8363
+ code,
8364
+ message,
8365
+ hints,
8366
+ ...(details && Object.keys(details).length > 0 ? { details } : {}),
8367
+ ...(CLI_DEBUG && failure.stack ? { stack: failure.stack } : {}),
8368
+ },
8369
+ };
8370
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
8371
+ process.exit(1);
8372
+ }
7071
8373
  console.error(`${failLabel('ERROR')}${code ? ` [${code}]` : ''} ${message}`);
7072
- if (err instanceof CliError && err.hints.length > 0) {
8374
+ if (hints.length > 0) {
7073
8375
  console.error('');
7074
8376
  console.error('Possible fixes:');
7075
- for (const hint of err.hints) {
8377
+ for (const hint of hints) {
7076
8378
  console.error(` - ${hint}`);
7077
8379
  }
7078
8380
  }
7079
- if (CLI_DEBUG && err instanceof CliError && err.details && Object.keys(err.details).length > 0) {
8381
+ if (CLI_DEBUG && details && Object.keys(details).length > 0) {
7080
8382
  console.error('');
7081
- console.error(`${paint('[DEBUG]', 'gray')} ${JSON.stringify(err.details, null, 2)}`);
8383
+ console.error(`${paint('[DEBUG]', 'gray')} ${JSON.stringify(details, null, 2)}`);
7082
8384
  }
7083
- if (CLI_DEBUG && formattedError.stack) {
8385
+ if (CLI_DEBUG && failure.stack) {
7084
8386
  console.error('');
7085
- console.error(formattedError.stack);
8387
+ console.error(failure.stack);
7086
8388
  }
7087
8389
  process.exit(1);
7088
8390
  });