iranti 0.3.0 → 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 (59) hide show
  1. package/README.md +843 -839
  2. package/bin/iranti.js +1 -1
  3. package/dist/scripts/claude-code-memory-hook.js +41 -153
  4. package/dist/scripts/iranti-cli.js +86 -9
  5. package/dist/scripts/iranti-mcp.js +141 -69
  6. package/dist/scripts/seed.js +1 -1
  7. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  8. package/dist/src/api/middleware/validation.js +13 -1
  9. package/dist/src/api/middleware/validation.js.map +1 -1
  10. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  11. package/dist/src/api/routes/knowledge.js +3 -0
  12. package/dist/src/api/routes/knowledge.js.map +1 -1
  13. package/dist/src/api/routes/memory.d.ts.map +1 -1
  14. package/dist/src/api/routes/memory.js +3 -0
  15. package/dist/src/api/routes/memory.js.map +1 -1
  16. package/dist/src/api/server.js +1 -1
  17. package/dist/src/archivist/index.js +9 -9
  18. package/dist/src/attendant/AttendantInstance.d.ts +42 -1
  19. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  20. package/dist/src/attendant/AttendantInstance.js +496 -90
  21. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  22. package/dist/src/attendant/index.d.ts +1 -1
  23. package/dist/src/attendant/index.d.ts.map +1 -1
  24. package/dist/src/attendant/index.js.map +1 -1
  25. package/dist/src/chat/index.d.ts +2 -0
  26. package/dist/src/chat/index.d.ts.map +1 -1
  27. package/dist/src/chat/index.js +56 -22
  28. package/dist/src/chat/index.js.map +1 -1
  29. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  30. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  31. package/dist/src/lib/assistantCheckpoint.js +143 -0
  32. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  33. package/dist/src/lib/cliHelpCatalog.js +2 -2
  34. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  35. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  36. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  37. package/dist/src/lib/hostMemoryFormatting.js +55 -0
  38. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  39. package/dist/src/lib/projectLearning.d.ts +21 -0
  40. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  41. package/dist/src/lib/projectLearning.js +357 -0
  42. package/dist/src/lib/projectLearning.js.map +1 -0
  43. package/dist/src/lib/protocolEnforcement.d.ts +3 -1
  44. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
  45. package/dist/src/lib/protocolEnforcement.js +28 -2
  46. package/dist/src/lib/protocolEnforcement.js.map +1 -1
  47. package/dist/src/lib/sessionLedger.d.ts +18 -0
  48. package/dist/src/lib/sessionLedger.d.ts.map +1 -1
  49. package/dist/src/lib/sessionLedger.js +78 -0
  50. package/dist/src/lib/sessionLedger.js.map +1 -1
  51. package/dist/src/librarian/index.d.ts.map +1 -1
  52. package/dist/src/librarian/index.js +102 -51
  53. package/dist/src/librarian/index.js.map +1 -1
  54. package/dist/src/library/queries.js +56 -56
  55. package/dist/src/sdk/index.d.ts +2 -0
  56. package/dist/src/sdk/index.d.ts.map +1 -1
  57. package/dist/src/sdk/index.js +39 -2
  58. package/dist/src/sdk/index.js.map +1 -1
  59. package/package.json +9 -5
package/bin/iranti.js CHANGED
@@ -21,4 +21,4 @@ try {
21
21
  console.error('Iranti CLI is not built. Run "npm run build" first.');
22
22
  console.error(message);
23
23
  process.exit(1);
24
- }
24
+ }
@@ -11,6 +11,8 @@ const runtimeEnv_1 = require("../src/lib/runtimeEnv");
11
11
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
12
12
  const client_1 = require("../src/library/client");
13
13
  const autoRemember_1 = require("../src/lib/autoRemember");
14
+ const assistantCheckpoint_1 = require("../src/lib/assistantCheckpoint");
15
+ const hostMemoryFormatting_1 = require("../src/lib/hostMemoryFormatting");
14
16
  const MEMORY_NEED_POSITIVE_PATTERNS = [
15
17
  /\bwhat(?:'s| is| was)?\s+my\b/i,
16
18
  /\bdo you remember\b/i,
@@ -41,7 +43,7 @@ function printHelp() {
41
43
  '',
42
44
  'Reads Claude Code hook JSON from stdin and returns hookSpecificOutput.additionalContext on stdout.',
43
45
  'This helper retrieves working memory; durable KB writes still require explicit iranti_write/ingest calls.',
44
- 'Set IRANTI_AUTO_REMEMBER=true to auto-save narrow personal facts to IRANTI_PERSONAL_MEMORY_ENTITY/user/main, project summaries to IRANTI_MEMORY_ENTITY, and shared checkpoint breadcrumbs for resumable work.',
46
+ 'Set IRANTI_AUTO_REMEMBER=true to auto-save prompt-side personal/project facts. Assistant-response continuity facts and shared checkpoints are captured on Stop regardless.',
45
47
  ].join('\n'));
46
48
  }
47
49
  function parseArgs(argv) {
@@ -229,19 +231,28 @@ function getMaxFacts() {
229
231
  return Math.min(12, Math.trunc(raw));
230
232
  }
231
233
  function formatSessionContext(facts, cwd) {
232
- const limited = facts.slice(0, getMaxFacts());
234
+ const limited = (0, hostMemoryFormatting_1.assignStructuredFactIds)(facts.slice(0, getMaxFacts()).map((fact) => ({
235
+ ...fact,
236
+ entityKey: `${fact.entity}/${fact.key}`,
237
+ })));
233
238
  const lines = [
234
239
  '[Iranti Session Memory]',
235
240
  `Project: ${path_1.default.basename(cwd)}`,
236
241
  'REQUIRED: Call mcp__iranti__iranti_handshake before responding to the first user message.',
237
- 'REQUIRED: Call mcp__iranti__iranti_attend before every subsequent turn.',
242
+ 'REQUIRED: Call mcp__iranti__iranti_attend(phase=\'pre-response\') before every reply and before factual discovery.',
243
+ 'REQUIRED: After every response, call mcp__iranti__iranti_attend(phase=\'post-response\').',
244
+ 'REQUIRED: Prefer injected Iranti facts before re-inferring project state.',
238
245
  ];
239
- if (limited.length > 0) {
240
- lines.push('Relevant memory:');
241
- for (const fact of limited) {
242
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
243
- }
244
- }
246
+ const block = (0, hostMemoryFormatting_1.formatStructuredFactBlock)(limited, {
247
+ title: 'Iranti Session Facts',
248
+ introLines: [
249
+ 'Use these loaded facts as the starting working-memory frame for this session.',
250
+ 'Prefer them before re-inferring project state.',
251
+ 'Fact IDs are stable only within this block.',
252
+ ],
253
+ });
254
+ if (block)
255
+ lines.push(block);
245
256
  return lines.join('\n');
246
257
  }
247
258
  function formatPreCompactContext() {
@@ -270,154 +281,22 @@ function extractSelfMemoryQueryKey(prompt) {
270
281
  function formatPromptContext(facts, prompt) {
271
282
  if (facts.length === 0)
272
283
  return '';
273
- const lines = ['[Iranti Retrieved Memory]'];
284
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(facts.map((fact) => ({
285
+ ...fact,
286
+ entityKey: `${fact.entity}/${fact.key}`,
287
+ })));
288
+ const lines = [];
274
289
  const targetKey = prompt ? extractSelfMemoryQueryKey(prompt) : null;
275
290
  if (targetKey) {
276
- const answerCandidate = facts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.key) === targetKey);
291
+ const answerCandidate = structuredFacts.find((fact) => (0, autoRemember_1.canonicalizeMemoryKey)(fact.entityKey.split('/').slice(2).join('/')) === targetKey);
277
292
  if (answerCandidate) {
278
- lines.push(`Direct answer: ${answerCandidate.summary}.`);
279
- lines.push('Use the direct answer above when it fully answers the user question.');
293
+ lines.push(`[Iranti Direct Answer]`);
294
+ lines.push(`Use ${answerCandidate.factId} directly if it fully answers the user question: ${answerCandidate.summary}.`);
280
295
  }
281
296
  }
282
- for (const fact of facts) {
283
- lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
284
- }
297
+ lines.push((0, hostMemoryFormatting_1.formatStructuredFactBlock)(structuredFacts, { title: 'Iranti Retrieved Memory' }));
285
298
  return lines.join('\n');
286
299
  }
287
- function readTextField(value, preferredKey) {
288
- if (typeof value !== 'object' || value === null)
289
- return undefined;
290
- const record = value;
291
- const raw = record[preferredKey];
292
- return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
293
- }
294
- function readItems(value) {
295
- if (typeof value !== 'object' || value === null)
296
- return [];
297
- const record = value;
298
- const items = Array.isArray(record.items) ? record.items : [];
299
- return items.map((item) => String(item ?? '').trim()).filter(Boolean);
300
- }
301
- function readFileChangeOutputs(value) {
302
- if (typeof value !== 'object' || value === null)
303
- return [];
304
- const record = value;
305
- const items = Array.isArray(record.items) ? record.items : [];
306
- return items.map((item) => {
307
- if (typeof item !== 'object' || item === null)
308
- return '';
309
- const change = item;
310
- const action = String(change.action ?? 'updated').trim();
311
- const targetPath = String(change.path ?? '').trim();
312
- const toPath = String(change.toPath ?? '').trim();
313
- if (!targetPath)
314
- return '';
315
- return toPath ? `${action} ${targetPath} -> ${toPath}` : `${action} ${targetPath}`;
316
- }).filter(Boolean);
317
- }
318
- function readCheckpointActions(value) {
319
- if (typeof value !== 'object' || value === null)
320
- return [];
321
- const record = value;
322
- const items = Array.isArray(record.items) ? record.items : [];
323
- return items
324
- .map((item) => {
325
- if (typeof item !== 'object' || item === null)
326
- return null;
327
- const action = item;
328
- const summary = String(action.summary ?? '').trim();
329
- if (!summary)
330
- return null;
331
- const kind = String(action.kind ?? 'action').trim() || 'action';
332
- return {
333
- kind,
334
- summary,
335
- ...(typeof action.status === 'string' && action.status.trim() ? { status: action.status.trim() } : {}),
336
- ...(typeof action.target === 'string' && action.target.trim() ? { target: action.target.trim() } : {}),
337
- ...(typeof action.detail === 'string' && action.detail.trim() ? { detail: action.detail.trim() } : {}),
338
- };
339
- })
340
- .filter((item) => Boolean(item));
341
- }
342
- function readActionOutputs(value) {
343
- if (typeof value !== 'object' || value === null)
344
- return [];
345
- const record = value;
346
- const items = Array.isArray(record.items) ? record.items : [];
347
- return items.map((item) => {
348
- if (typeof item !== 'object' || item === null)
349
- return '';
350
- const action = item;
351
- const kind = String(action.kind ?? 'action').trim() || 'action';
352
- const summary = String(action.summary ?? '').trim();
353
- const status = String(action.status ?? '').trim();
354
- if (!summary)
355
- return '';
356
- return status ? `[${status}] ${kind}: ${summary}` : `${kind}: ${summary}`;
357
- }).filter(Boolean);
358
- }
359
- function extractHookCheckpointPayload(response) {
360
- const facts = (0, autoRemember_1.extractExplicitAssistantMemory)(response).filter((fact) => fact.scope === 'project');
361
- if (facts.length === 0) {
362
- return null;
363
- }
364
- const checkpoint = {};
365
- const outputs = [];
366
- for (const fact of facts) {
367
- if (fact.key === 'current_step') {
368
- checkpoint.currentStep = readTextField(fact.value, 'text');
369
- continue;
370
- }
371
- if (fact.key === 'next_step') {
372
- checkpoint.nextStep = readTextField(fact.value, 'instruction') ?? readTextField(fact.value, 'text');
373
- continue;
374
- }
375
- if (fact.key === 'open_risks') {
376
- checkpoint.openRisks = readItems(fact.value);
377
- continue;
378
- }
379
- if (fact.key === 'important_artifacts') {
380
- outputs.push(...readItems(fact.value));
381
- continue;
382
- }
383
- if (fact.key === 'recent_file_changes') {
384
- const fileChanges = typeof fact.value === 'object' && fact.value !== null && Array.isArray(fact.value.items)
385
- ? fact.value.items
386
- .filter((item) => item && typeof item === 'object')
387
- .map((item) => ({
388
- action: String(item.action ?? 'updated').trim() || 'updated',
389
- path: String(item.path ?? '').trim(),
390
- ...(typeof item.toPath === 'string' && item.toPath.trim() ? { toPath: String(item.toPath).trim() } : {}),
391
- ...(typeof item.purpose === 'string' && item.purpose.trim() ? { purpose: String(item.purpose).trim() } : {}),
392
- }))
393
- .filter((item) => item.path)
394
- : [];
395
- if (fileChanges.length > 0) {
396
- checkpoint.fileChanges = [...(checkpoint.fileChanges ?? []), ...fileChanges];
397
- }
398
- outputs.push(...readFileChangeOutputs(fact.value));
399
- continue;
400
- }
401
- if (fact.key === 'recent_actions') {
402
- const actions = readCheckpointActions(fact.value);
403
- if (actions.length > 0) {
404
- checkpoint.actions = [...(checkpoint.actions ?? []), ...actions];
405
- }
406
- outputs.push(...readActionOutputs(fact.value));
407
- }
408
- }
409
- if (outputs.length > 0) {
410
- checkpoint.recentOutputs = outputs;
411
- }
412
- return checkpoint.currentStep
413
- || checkpoint.nextStep
414
- || (checkpoint.openRisks && checkpoint.openRisks.length > 0)
415
- || (checkpoint.recentOutputs && checkpoint.recentOutputs.length > 0)
416
- || (checkpoint.actions && checkpoint.actions.length > 0)
417
- || (checkpoint.fileChanges && checkpoint.fileChanges.length > 0)
418
- ? checkpoint
419
- : null;
420
- }
421
300
  function emitHookContext(event, additionalContext) {
422
301
  const payload = {
423
302
  hookSpecificOutput: {
@@ -523,8 +402,8 @@ async function buildHookAdditionalContext(options) {
523
402
  }
524
403
  if (event === 'Stop') {
525
404
  const response = getLastAssistantMessage(payload);
526
- if (response && (0, autoRemember_1.isAutoRememberEnabled)()) {
527
- await (0, autoRemember_1.autoRememberAssistantFacts)({
405
+ if (response) {
406
+ await (0, autoRemember_1.rememberAssistantResponseFacts)({
528
407
  iranti,
529
408
  response,
530
409
  agent,
@@ -534,7 +413,7 @@ async function buildHookAdditionalContext(options) {
534
413
  host: 'claude_code',
535
414
  },
536
415
  });
537
- const checkpoint = extractHookCheckpointPayload(response);
416
+ const checkpoint = (0, assistantCheckpoint_1.extractAssistantCheckpointPayload)(response);
538
417
  const projectEntity = (0, autoRemember_1.getProjectMemoryEntity)();
539
418
  if (checkpoint && projectEntity && typeof iranti.checkpoint === 'function') {
540
419
  await iranti.checkpoint({
@@ -547,6 +426,14 @@ async function buildHookAdditionalContext(options) {
547
426
  },
548
427
  });
549
428
  }
429
+ await iranti.attend({
430
+ agent,
431
+ latestMessage: response,
432
+ currentContext: response,
433
+ entityHints,
434
+ maxFacts: getMaxFacts(),
435
+ phase: 'post-response',
436
+ });
550
437
  }
551
438
  return '';
552
439
  }
@@ -575,6 +462,7 @@ async function buildHookAdditionalContext(options) {
575
462
  currentContext: buildCurrentContext(payload, prompt),
576
463
  entityHints,
577
464
  maxFacts: getMaxFacts(),
465
+ phase: 'pre-response',
578
466
  });
579
467
  const facts = attend.facts.map((fact) => ({
580
468
  entity: fact.entityKey.split('/').slice(0, 2).join('/'),
@@ -34,6 +34,7 @@ const autoRemember_1 = require("../src/lib/autoRemember");
34
34
  const semanticFactTags_1 = require("../src/lib/semanticFactTags");
35
35
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
36
36
  const scaffoldCloseout_1 = require("../src/lib/scaffoldCloseout");
37
+ const projectLearning_1 = require("../src/lib/projectLearning");
37
38
  class CliError extends Error {
38
39
  constructor(code, message, hints = [], details) {
39
40
  super(message);
@@ -1104,10 +1105,14 @@ async function writeProjectBinding(projectPath, updates) {
1104
1105
  const previousBinding = fs_1.default.existsSync(outFile)
1105
1106
  ? await readEnvFile(outFile).catch(() => ({}))
1106
1107
  : {};
1108
+ const normalizedUpdates = {
1109
+ ...updates,
1110
+ IRANTI_CODEBASE_ENTITY: updates.IRANTI_CODEBASE_ENTITY ?? previousBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
1111
+ };
1107
1112
  if (!fs_1.default.existsSync(outFile)) {
1108
1113
  await writeText(outFile, '# Iranti project binding\n');
1109
1114
  }
1110
- await upsertEnvFile(outFile, updates);
1115
+ await upsertEnvFile(outFile, normalizedUpdates);
1111
1116
  await ensureProjectGitignore(projectPath);
1112
1117
  const writtenBinding = await readEnvFile(outFile).catch(() => ({}));
1113
1118
  await syncProjectBindingRegistry(projectPath, writtenBinding, previousBinding);
@@ -2132,6 +2137,11 @@ function printAttendResult(target, latestMessage, result) {
2132
2137
  console.log(` confidence ${result.decision.confidence}`);
2133
2138
  console.log(` explanation ${result.decision.explanation}`);
2134
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
+ }
2135
2145
  if (result.facts.length === 0) {
2136
2146
  console.log('');
2137
2147
  console.log('No facts selected for injection.');
@@ -2506,6 +2516,10 @@ async function isPortAvailable(port, host = '0.0.0.0') {
2506
2516
  });
2507
2517
  }
2508
2518
  function listPublishedDockerHostPorts() {
2519
+ const injected = process.env.IRANTI_FAKE_DOCKER_PORTS?.trim();
2520
+ if (injected) {
2521
+ return (0, dockerCliParsing_1.parsePublishedDockerHostPorts)(injected);
2522
+ }
2509
2523
  const docker = inspectDockerAvailability();
2510
2524
  if (!docker.daemonReachable)
2511
2525
  return new Set();
@@ -2756,12 +2770,21 @@ async function executeSetupPlan(plan) {
2756
2770
  IRANTI_INSTANCE: plan.instanceName,
2757
2771
  IRANTI_INSTANCE_ENV: configured.envFile,
2758
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
+ });
2759
2780
  bindings.push({
2760
2781
  projectPath,
2761
2782
  envFile: written,
2762
2783
  agentId: project.agentId,
2763
2784
  projectMode: project.projectMode,
2764
2785
  autoRemember: project.autoRemember,
2786
+ codebaseEntity: bindingEnv.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
2787
+ learningStatus,
2765
2788
  });
2766
2789
  if (project.claudeCode) {
2767
2790
  await writeClaudeCodeProjectFiles(projectPath);
@@ -2790,7 +2813,7 @@ async function executeSetupPlan(plan) {
2790
2813
  bindings,
2791
2814
  };
2792
2815
  }
2793
- function parseSetupConfig(filePath) {
2816
+ async function parseSetupConfig(filePath) {
2794
2817
  const resolved = path_1.default.resolve(filePath);
2795
2818
  if (!fs_1.default.existsSync(resolved)) {
2796
2819
  throw new Error(`Setup config file not found: ${resolved}`);
@@ -2812,7 +2835,7 @@ function parseSetupConfig(filePath) {
2812
2835
  : databaseModeRaw === 'existing' || databaseModeRaw === 'local' || databaseModeRaw.length === 0
2813
2836
  ? 'local'
2814
2837
  : (() => { throw new Error(`Unsupported databaseMode in setup config: ${databaseModeRaw}`); })();
2815
- const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2838
+ const databaseUrl = await resolveSetupDatabaseUrl(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2816
2839
  const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(String(raw?.databaseIntent ?? raw?.dbIntent ?? '').trim(), 'databaseIntent');
2817
2840
  const provider = normalizeProvider(String(raw?.provider ?? 'mock')) ?? 'mock';
2818
2841
  if (!isSupportedProvider(provider)) {
@@ -2874,7 +2897,7 @@ function parseSetupConfig(filePath) {
2874
2897
  }),
2875
2898
  };
2876
2899
  }
2877
- function defaultsSetupPlan(args) {
2900
+ async function defaultsSetupPlan(args) {
2878
2901
  const scope = normalizeScope(getFlag(args, 'scope'));
2879
2902
  const root = path_1.default.resolve(getFlag(args, 'root') ?? resolveInstallRoot(args, scope));
2880
2903
  const mode = getFlag(args, 'mode') === 'shared' ? 'shared' : 'isolated';
@@ -2901,7 +2924,7 @@ function defaultsSetupPlan(args) {
2901
2924
  const explicitDatabaseUrl = (getFlag(args, 'db-url')
2902
2925
  ?? (databaseMode === 'managed' ? process.env.DATABASE_URL : '')
2903
2926
  ?? '').trim();
2904
- const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, explicitDatabaseUrl);
2927
+ const databaseUrl = await resolveSetupDatabaseUrl(databaseMode, instanceName, explicitDatabaseUrl);
2905
2928
  const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(getFlag(args, 'db-intent'), '--db-intent');
2906
2929
  const provider = normalizeProvider(getFlag(args, 'provider') ?? process.env.LLM_PROVIDER ?? 'mock') ?? 'mock';
2907
2930
  if (!isSupportedProvider(provider)) {
@@ -3491,6 +3514,23 @@ function deriveDatabaseUrlForMode(mode, instanceName, explicitDatabaseUrl) {
3491
3514
  }
3492
3515
  return `postgresql://${user}:${password}@${localDatabaseHost}:5432/iranti_${instanceName}`;
3493
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
+ }
3494
3534
  function preferredLocalDatabaseHost() {
3495
3535
  return process.platform === 'win32' ? '127.0.0.1' : 'localhost';
3496
3536
  }
@@ -5002,7 +5042,7 @@ async function setupCommand(args) {
5002
5042
  const dependencyChecks = await collectDependencyChecks();
5003
5043
  printDependencyChecks(dependencyChecks);
5004
5044
  console.log('');
5005
- const plan = configPath ? parseSetupConfig(configPath) : defaultsSetupPlan(args);
5045
+ const plan = configPath ? await parseSetupConfig(configPath) : await defaultsSetupPlan(args);
5006
5046
  const result = await executeSetupPlan(plan);
5007
5047
  console.log(sectionTitle('Setup Complete'));
5008
5048
  console.log(` runtime root ${result.root}`);
@@ -5019,7 +5059,10 @@ async function setupCommand(args) {
5019
5059
  else {
5020
5060
  console.log(' projects');
5021
5061
  for (const binding of result.bindings) {
5022
- 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})`);
5023
5066
  }
5024
5067
  }
5025
5068
  printNextSteps([
@@ -5382,7 +5425,10 @@ async function setupCommand(args) {
5382
5425
  else {
5383
5426
  console.log(' projects');
5384
5427
  for (const binding of finalResult.bindings) {
5385
- 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})`);
5386
5432
  }
5387
5433
  }
5388
5434
  const nextSteps = [
@@ -6557,10 +6603,21 @@ async function projectInitCommand(args) {
6557
6603
  IRANTI_INSTANCE: instanceName,
6558
6604
  IRANTI_INSTANCE_ENV: envFile,
6559
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
+ });
6560
6613
  console.log(sectionTitle('Project Initialized'));
6561
6614
  console.log(` status ${okLabel()}`);
6562
6615
  console.log(` wrote ${outFile}`);
6563
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
+ }
6564
6621
  printNextSteps([
6565
6622
  `iranti doctor --instance ${instanceName}`,
6566
6623
  'iranti chat',
@@ -6859,6 +6916,13 @@ async function configureProjectCommand(args) {
6859
6916
  throw new Error('Unable to determine IRANTI_API_KEY. Provide --api-key <token> or configure the instance first.');
6860
6917
  }
6861
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
+ });
6862
6926
  const json = hasFlag(args, 'json');
6863
6927
  const result = {
6864
6928
  projectPath,
@@ -6868,6 +6932,8 @@ async function configureProjectCommand(args) {
6868
6932
  autoRemember: updates.IRANTI_AUTO_REMEMBER === 'true',
6869
6933
  projectMode: updates.IRANTI_PROJECT_MODE,
6870
6934
  instance: updates.IRANTI_INSTANCE ?? null,
6935
+ codebaseEntity: writtenBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath),
6936
+ projectLearning: learningStatus,
6871
6937
  };
6872
6938
  if (json) {
6873
6939
  console.log(JSON.stringify(result, null, 2));
@@ -6881,9 +6947,13 @@ async function configureProjectCommand(args) {
6881
6947
  console.log(` agent ${updates.IRANTI_AGENT_ID}`);
6882
6948
  console.log(` remember ${updates.IRANTI_AUTO_REMEMBER}`);
6883
6949
  console.log(` mode ${updates.IRANTI_PROJECT_MODE}`);
6950
+ console.log(` codebase ${writtenBinding.IRANTI_CODEBASE_ENTITY ?? (0, projectLearning_1.deriveProjectCodebaseEntity)(projectPath)}`);
6884
6951
  if (updates.IRANTI_INSTANCE) {
6885
6952
  console.log(` instance ${updates.IRANTI_INSTANCE}`);
6886
6953
  }
6954
+ if (learningStatus.status !== 'written') {
6955
+ console.log(` learn ${paint(learningStatus.status, learningStatus.status === 'failed' ? 'red' : 'yellow')} (${learningStatus.detail})`);
6956
+ }
6887
6957
  printNextSteps([
6888
6958
  `iranti doctor${updates.IRANTI_INSTANCE ? ` --instance ${updates.IRANTI_INSTANCE}` : ''}`,
6889
6959
  ]);
@@ -6922,7 +6992,7 @@ async function authCreateKeyCommand(args) {
6922
6992
  const resolvedProjectPath = path_1.default.resolve(projectPath);
6923
6993
  const existingBindingFile = path_1.default.join(resolvedProjectPath, '.env.iranti');
6924
6994
  const existingBinding = fs_1.default.existsSync(existingBindingFile) ? await readEnvFile(existingBindingFile) : {};
6925
- await writeProjectBinding(resolvedProjectPath, {
6995
+ const written = await writeProjectBinding(resolvedProjectPath, {
6926
6996
  IRANTI_URL: `http://localhost:${env.IRANTI_PORT ?? '3001'}`,
6927
6997
  IRANTI_API_KEY: created.token,
6928
6998
  IRANTI_AGENT_ID: agentId ?? existingBinding.IRANTI_AGENT_ID ?? 'my_agent',
@@ -6932,6 +7002,13 @@ async function authCreateKeyCommand(args) {
6932
7002
  IRANTI_INSTANCE: instanceName,
6933
7003
  IRANTI_INSTANCE_ENV: envFile,
6934
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
+ });
6935
7012
  }
6936
7013
  if (hasFlag(args, 'json')) {
6937
7014
  console.log(JSON.stringify({