iranti 0.2.45 → 0.2.47

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.
@@ -29,6 +29,7 @@ const backends_1 = require("../src/library/backends");
29
29
  const runtimeLifecycle_1 = require("../src/lib/runtimeLifecycle");
30
30
  const sdk_1 = require("../src/sdk");
31
31
  const queries_1 = require("../src/library/queries");
32
+ const autoRemember_1 = require("../src/lib/autoRemember");
32
33
  class CliError extends Error {
33
34
  constructor(code, message, hints = [], details) {
34
35
  super(message);
@@ -766,12 +767,28 @@ async function inspectInstanceConfig(root, name) {
766
767
  let metaReadable = false;
767
768
  let envReadable = false;
768
769
  const ownershipIssues = [];
770
+ let rewrittenMeta = false;
769
771
  if (metaPresent) {
770
772
  try {
771
773
  const raw = await promises_1.default.readFile(metaFile, 'utf8');
772
774
  const parsed = JSON.parse(raw);
773
775
  metaReadable = typeof parsed.name === 'string' && parsed.name.trim().length > 0;
774
776
  if (metaReadable) {
777
+ const shouldRewriteOwnership = parsed.name?.trim() === name
778
+ && ((typeof parsed.instanceDir === 'string' && path_1.default.resolve(parsed.instanceDir) !== path_1.default.resolve(instanceDir))
779
+ || (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)));
780
+ if (shouldRewriteOwnership) {
781
+ const nextMeta = {
782
+ ...parsed,
783
+ name,
784
+ instanceDir,
785
+ envFile,
786
+ };
787
+ await writeText(metaFile, `${JSON.stringify(nextMeta, null, 2)}\n`);
788
+ rewrittenMeta = true;
789
+ parsed.instanceDir = instanceDir;
790
+ parsed.envFile = envFile;
791
+ }
775
792
  if (parsed.name?.trim() !== name) {
776
793
  ownershipIssues.push(`instance.json name is ${parsed.name}`);
777
794
  }
@@ -781,6 +798,10 @@ async function inspectInstanceConfig(root, name) {
781
798
  if (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)) {
782
799
  ownershipIssues.push(`instance.json envFile points to ${parsed.envFile}`);
783
800
  }
801
+ const databaseIntentValidation = parseInstanceDatabaseIntent(parsed.databaseIntent);
802
+ if (databaseIntentValidation.errors.length > 0) {
803
+ ownershipIssues.push(`instance.json databaseIntent invalid: ${databaseIntentValidation.errors.join(', ')}`);
804
+ }
784
805
  const dependencyValidation = (0, runtimeDependencies_1.parseInstanceDependencies)(parsed.dependencies);
785
806
  if (dependencyValidation.errors.length > 0) {
786
807
  ownershipIssues.push(`instance.json dependencies invalid: ${dependencyValidation.errors.join(', ')}`);
@@ -808,7 +829,9 @@ async function inspectInstanceConfig(root, name) {
808
829
  }
809
830
  else if (metaPresent && envPresent && metaReadable && envReadable) {
810
831
  classification = 'complete';
811
- detail = 'instance metadata and env are present';
832
+ detail = rewrittenMeta
833
+ ? 'instance metadata was repaired and env is present'
834
+ : 'instance metadata and env are present';
812
835
  }
813
836
  else if ((metaPresent && !metaReadable) || (envPresent && !envReadable)) {
814
837
  classification = 'invalid';
@@ -1142,6 +1165,160 @@ function isLocalPostgresHost(hostname) {
1142
1165
  const normalized = hostname.trim().toLowerCase();
1143
1166
  return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
1144
1167
  }
1168
+ function parseDatabaseIntentStrategy(raw) {
1169
+ const normalized = raw?.trim().toLowerCase() ?? '';
1170
+ if (!normalized)
1171
+ return null;
1172
+ if (normalized === 'dedicated' || normalized === 'dedicated-local')
1173
+ return 'dedicated-local';
1174
+ if (normalized === 'shared' || normalized === 'shared-local')
1175
+ return 'shared-local';
1176
+ if (normalized === 'external' || normalized === 'managed' || normalized === 'external-existing')
1177
+ return 'external-existing';
1178
+ return null;
1179
+ }
1180
+ function parseDatabaseIntentStrategyFlag(raw, label) {
1181
+ const trimmed = raw?.trim() ?? '';
1182
+ if (!trimmed)
1183
+ return null;
1184
+ const parsed = parseDatabaseIntentStrategy(trimmed);
1185
+ if (!parsed) {
1186
+ throw new Error(`Invalid ${label} '${raw}'. Use dedicated, shared, or external.`);
1187
+ }
1188
+ return parsed;
1189
+ }
1190
+ function databaseIntentPromptValue(strategy) {
1191
+ if (strategy === 'dedicated-local')
1192
+ return 'dedicated';
1193
+ if (strategy === 'shared-local')
1194
+ return 'shared';
1195
+ return 'external';
1196
+ }
1197
+ function describeDatabaseIntentStrategy(strategy) {
1198
+ switch (strategy) {
1199
+ case 'dedicated-local':
1200
+ return 'dedicated local database';
1201
+ case 'shared-local':
1202
+ return 'shared local database';
1203
+ case 'external-existing':
1204
+ default:
1205
+ return 'external existing database';
1206
+ }
1207
+ }
1208
+ function inferDatabaseProvisioning(databaseUrl, dependencies = []) {
1209
+ if (dependencies.some((dependency) => dependency.kind === 'docker-container')) {
1210
+ return 'docker';
1211
+ }
1212
+ const parsed = parsePostgresConnectionString(databaseUrl);
1213
+ return isLocalPostgresHost(parsed.hostname) ? 'local' : 'managed';
1214
+ }
1215
+ function inferDatabaseIntentStrategy(options) {
1216
+ const databaseName = postgresDatabaseName(options.databaseUrl);
1217
+ if (options.databaseMode === 'managed') {
1218
+ return 'external-existing';
1219
+ }
1220
+ return databaseName === `iranti_${options.instanceName}`
1221
+ ? 'dedicated-local'
1222
+ : 'shared-local';
1223
+ }
1224
+ function buildInstanceDatabaseIntent(options) {
1225
+ const parsed = parsePostgresConnectionString(options.databaseUrl);
1226
+ const strategy = options.strategy ?? inferDatabaseIntentStrategy({
1227
+ instanceName: options.instanceName,
1228
+ databaseMode: options.databaseMode,
1229
+ databaseUrl: options.databaseUrl,
1230
+ });
1231
+ const port = Number.parseInt(parsed.port || '5432', 10);
1232
+ return {
1233
+ strategy,
1234
+ provisioning: options.databaseMode,
1235
+ host: parsed.hostname,
1236
+ ...(Number.isFinite(port) && port > 0 ? { port } : {}),
1237
+ database: postgresDatabaseName(options.databaseUrl),
1238
+ ...(options.dockerContainerName ? { dockerContainerName: options.dockerContainerName } : {}),
1239
+ };
1240
+ }
1241
+ function parseInstanceDatabaseIntent(raw) {
1242
+ if (raw === undefined || raw === null) {
1243
+ return { intent: null, errors: [] };
1244
+ }
1245
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1246
+ return { intent: null, errors: ['databaseIntent must be an object'] };
1247
+ }
1248
+ const record = raw;
1249
+ const strategy = parseDatabaseIntentStrategy(typeof record.strategy === 'string' ? record.strategy : undefined);
1250
+ const provisioningRaw = typeof record.provisioning === 'string' ? record.provisioning.trim().toLowerCase() : '';
1251
+ const provisioning = provisioningRaw === 'local' || provisioningRaw === 'managed' || provisioningRaw === 'docker'
1252
+ ? provisioningRaw
1253
+ : null;
1254
+ const host = typeof record.host === 'string' ? record.host.trim() : '';
1255
+ const database = typeof record.database === 'string' ? record.database.trim() : '';
1256
+ const portRaw = typeof record.port === 'number'
1257
+ ? record.port
1258
+ : typeof record.port === 'string'
1259
+ ? Number.parseInt(record.port, 10)
1260
+ : undefined;
1261
+ const dockerContainerName = typeof record.dockerContainerName === 'string'
1262
+ ? record.dockerContainerName.trim()
1263
+ : undefined;
1264
+ const errors = [];
1265
+ if (!strategy) {
1266
+ errors.push('databaseIntent.strategy must be dedicated-local, shared-local, or external-existing');
1267
+ }
1268
+ if (!provisioning) {
1269
+ errors.push('databaseIntent.provisioning must be local, managed, or docker');
1270
+ }
1271
+ if (!host) {
1272
+ errors.push('databaseIntent.host is required');
1273
+ }
1274
+ if (!database) {
1275
+ errors.push('databaseIntent.database is required');
1276
+ }
1277
+ if (portRaw !== undefined && (!Number.isFinite(portRaw) || portRaw <= 0)) {
1278
+ errors.push('databaseIntent.port must be a positive integer');
1279
+ }
1280
+ if (errors.length > 0 || !strategy || !provisioning || !host || !database) {
1281
+ return { intent: null, errors };
1282
+ }
1283
+ return {
1284
+ intent: {
1285
+ strategy,
1286
+ provisioning,
1287
+ host,
1288
+ ...(portRaw !== undefined ? { port: portRaw } : {}),
1289
+ database,
1290
+ ...(dockerContainerName ? { dockerContainerName } : {}),
1291
+ },
1292
+ errors: [],
1293
+ };
1294
+ }
1295
+ function resolveInstanceDatabaseIntent(options) {
1296
+ const parsedMeta = parseInstanceDatabaseIntent(options.meta?.databaseIntent);
1297
+ if (parsedMeta.intent) {
1298
+ return { intent: parsedMeta.intent, source: 'meta', errors: parsedMeta.errors };
1299
+ }
1300
+ const dbUrl = options.env.DATABASE_URL?.trim();
1301
+ if (!dbUrl || detectPlaceholder(dbUrl)) {
1302
+ return { intent: null, source: 'none', errors: parsedMeta.errors };
1303
+ }
1304
+ const dockerDependency = options.dependencies.find((dependency) => dependency.kind === 'docker-container');
1305
+ return {
1306
+ intent: buildInstanceDatabaseIntent({
1307
+ instanceName: options.instanceName,
1308
+ databaseMode: inferDatabaseProvisioning(dbUrl, options.dependencies),
1309
+ databaseUrl: dbUrl,
1310
+ dockerContainerName: dockerDependency?.name,
1311
+ }),
1312
+ source: 'inferred',
1313
+ errors: parsedMeta.errors,
1314
+ };
1315
+ }
1316
+ function describeInstanceDatabaseIntent(intent, source = 'meta') {
1317
+ const target = `${intent.host}${intent.port ? `:${intent.port}` : ''}/${intent.database}`;
1318
+ const sourceSuffix = source === 'inferred' ? ' (inferred)' : '';
1319
+ const dockerSuffix = intent.dockerContainerName ? `, container ${intent.dockerContainerName}` : '';
1320
+ return `${describeDatabaseIntentStrategy(intent.strategy)} via ${intent.provisioning} -> ${target}${dockerSuffix}${sourceSuffix}`;
1321
+ }
1145
1322
  function sanitizeIdentifier(input, fallback) {
1146
1323
  const value = input.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
1147
1324
  if (!value && input.trim()) {
@@ -1286,6 +1463,7 @@ async function ensureInstanceConfigured(root, name, config) {
1286
1463
  port: config.port,
1287
1464
  envFile,
1288
1465
  instanceDir,
1466
+ databaseIntent: config.databaseIntent,
1289
1467
  ...(config.dependencies && config.dependencies.length > 0 ? { dependencies: config.dependencies } : {}),
1290
1468
  };
1291
1469
  await writeJson(metaFile, meta);
@@ -1298,6 +1476,7 @@ async function ensureInstanceConfigured(root, name, config) {
1298
1476
  ...config.providerKeys,
1299
1477
  });
1300
1478
  await syncInstanceMeta(root, name, config.port, {
1479
+ databaseIntent: config.databaseIntent,
1301
1480
  ...(config.dependencies !== undefined ? { dependencies: config.dependencies } : {}),
1302
1481
  });
1303
1482
  return { envFile, instanceDir, created };
@@ -1420,6 +1599,10 @@ function resolveRecentMessages(args) {
1420
1599
  }
1421
1600
  return [];
1422
1601
  }
1602
+ function resolveBackfillFile(args) {
1603
+ const backfill = getFlag(args, 'backfill')?.trim();
1604
+ return backfill ? path_1.default.resolve(backfill) : null;
1605
+ }
1423
1606
  function parsePositiveInteger(raw, label) {
1424
1607
  if (!raw)
1425
1608
  return undefined;
@@ -1529,7 +1712,10 @@ function printHandshakeResult(target, task, result) {
1529
1712
  console.log(` memory facts ${result.workingMemory.length}`);
1530
1713
  console.log(` generated ${result.briefGeneratedAt}`);
1531
1714
  console.log('');
1532
- console.log(`Rules: ${truncateText(result.operatingRules, 160)}`);
1715
+ console.log('Rules:');
1716
+ for (const line of result.operatingRules.split(/\r?\n/)) {
1717
+ console.log(line.trim().length > 0 ? ` ${line}` : '');
1718
+ }
1533
1719
  if (result.workingMemory.length === 0) {
1534
1720
  console.log('');
1535
1721
  console.log('No working memory entries loaded.');
@@ -1549,6 +1735,9 @@ function printAttendResult(target, latestMessage, result) {
1549
1735
  console.log(` message ${truncateText(latestMessage, 120)}`);
1550
1736
  console.log(` inject ${result.shouldInject ? 'yes' : 'no'}`);
1551
1737
  console.log(` reason ${result.reason}`);
1738
+ if (result.bootstrap?.handshakePerformed) {
1739
+ console.log(` bootstrap handshake auto-ran (${result.bootstrap.task})`);
1740
+ }
1552
1741
  console.log(` method ${result.decision.method}`);
1553
1742
  console.log(` confidence ${result.decision.confidence}`);
1554
1743
  console.log(` explanation ${result.decision.explanation}`);
@@ -1879,12 +2068,24 @@ async function syncInstanceMeta(root, name, port, options = {}) {
1879
2068
  const existing = await readInstanceMetaFile(metaFile);
1880
2069
  const existingCreatedAt = typeof existing?.createdAt === 'string' ? existing.createdAt : undefined;
1881
2070
  const existingDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(existing?.dependencies).dependencies;
2071
+ const currentDependencies = options.dependencies ?? existingDependencies;
2072
+ const env = fs_1.default.existsSync(envFile)
2073
+ ? await readEnvFile(envFile).catch(() => ({}))
2074
+ : {};
2075
+ const resolvedDatabaseIntent = options.databaseIntent
2076
+ ?? resolveInstanceDatabaseIntent({
2077
+ instanceName: name,
2078
+ env,
2079
+ meta: existing,
2080
+ dependencies: currentDependencies,
2081
+ }).intent;
1882
2082
  const meta = {
1883
2083
  name,
1884
2084
  createdAt: existingCreatedAt ?? new Date().toISOString(),
1885
2085
  port,
1886
2086
  envFile,
1887
2087
  instanceDir,
2088
+ ...(resolvedDatabaseIntent ? { databaseIntent: resolvedDatabaseIntent } : {}),
1888
2089
  ...(options.dependencies !== undefined
1889
2090
  ? { dependencies: options.dependencies }
1890
2091
  : existingDependencies.length > 0
@@ -1992,6 +2193,7 @@ async function executeSetupPlan(plan) {
1992
2193
  const configured = await ensureInstanceConfigured(plan.root, plan.instanceName, {
1993
2194
  port: plan.port,
1994
2195
  dbUrl: plan.databaseUrl,
2196
+ databaseIntent: plan.databaseIntent,
1995
2197
  provider: plan.provider,
1996
2198
  providerKeys: plan.providerKeys,
1997
2199
  apiKey: plan.apiKey,
@@ -2070,6 +2272,7 @@ async function executeSetupPlan(plan) {
2070
2272
  port: plan.port,
2071
2273
  mode: plan.mode,
2072
2274
  databaseMode: plan.databaseMode,
2275
+ databaseIntent: plan.databaseIntent,
2073
2276
  bindings,
2074
2277
  };
2075
2278
  }
@@ -2096,6 +2299,7 @@ function parseSetupConfig(filePath) {
2096
2299
  ? 'local'
2097
2300
  : (() => { throw new Error(`Unsupported databaseMode in setup config: ${databaseModeRaw}`); })();
2098
2301
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2302
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(String(raw?.databaseIntent ?? raw?.dbIntent ?? '').trim(), 'databaseIntent');
2099
2303
  const provider = normalizeProvider(String(raw?.provider ?? 'mock')) ?? 'mock';
2100
2304
  if (!isSupportedProvider(provider)) {
2101
2305
  throw new Error(`Unsupported provider in setup config: ${provider}`);
@@ -2134,6 +2338,13 @@ function parseSetupConfig(filePath) {
2134
2338
  port,
2135
2339
  databaseUrl,
2136
2340
  databaseMode,
2341
+ databaseIntent: buildInstanceDatabaseIntent({
2342
+ instanceName,
2343
+ databaseMode,
2344
+ databaseUrl,
2345
+ strategy: databaseIntentStrategy,
2346
+ dockerContainerName: typeof raw?.dockerContainerName === 'string' ? raw.dockerContainerName : undefined,
2347
+ }),
2137
2348
  provider,
2138
2349
  providerKeys,
2139
2350
  apiKey,
@@ -2174,6 +2385,7 @@ function defaultsSetupPlan(args) {
2174
2385
  throw new Error(`Invalid --db-mode '${explicit}'. Use local, managed, or docker.`);
2175
2386
  })();
2176
2387
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, (getFlag(args, 'db-url') ?? process.env.DATABASE_URL ?? '').trim());
2388
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(getFlag(args, 'db-intent'), '--db-intent');
2177
2389
  const provider = normalizeProvider(getFlag(args, 'provider') ?? process.env.LLM_PROVIDER ?? 'mock') ?? 'mock';
2178
2390
  if (!isSupportedProvider(provider)) {
2179
2391
  throw new Error(`Unsupported provider '${provider}' for --defaults.`);
@@ -2215,6 +2427,13 @@ function defaultsSetupPlan(args) {
2215
2427
  port,
2216
2428
  databaseUrl,
2217
2429
  databaseMode,
2430
+ databaseIntent: buildInstanceDatabaseIntent({
2431
+ instanceName,
2432
+ databaseMode,
2433
+ databaseUrl,
2434
+ strategy: databaseIntentStrategy,
2435
+ dockerContainerName: getFlag(args, 'docker-container-name'),
2436
+ }),
2218
2437
  provider,
2219
2438
  providerKeys,
2220
2439
  apiKey,
@@ -4137,6 +4356,7 @@ async function setupCommand(args) {
4137
4356
  console.log(` instance url http://localhost:${result.port}`);
4138
4357
  console.log(` memory mode ${result.mode}`);
4139
4358
  console.log(` database mode ${result.databaseMode}`);
4359
+ console.log(` db strategy ${describeInstanceDatabaseIntent(result.databaseIntent)}`);
4140
4360
  if (result.bindings.length === 0) {
4141
4361
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4142
4362
  }
@@ -4224,6 +4444,9 @@ async function setupCommand(args) {
4224
4444
  const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
4225
4445
  ? await loadInstanceEnv(finalRoot, instanceName)
4226
4446
  : null;
4447
+ const existingInstanceMeta = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).metaFile)
4448
+ ? await readInstanceMetaFile(instancePaths(finalRoot, instanceName).metaFile)
4449
+ : null;
4227
4450
  if (existingInstance) {
4228
4451
  console.log(`${infoLabel()} Found existing instance '${instanceName}'. Updating it.`);
4229
4452
  }
@@ -4244,6 +4467,7 @@ async function setupCommand(args) {
4244
4467
  let databaseProvisioned = false;
4245
4468
  let dockerContainerName;
4246
4469
  let databaseMode = recommendedDatabaseMode;
4470
+ let databaseIntentStrategy = null;
4247
4471
  printChoiceGuide('Database Mode Choices', [
4248
4472
  {
4249
4473
  choice: 'local',
@@ -4283,6 +4507,13 @@ async function setupCommand(args) {
4283
4507
  console.log(`${infoLabel()} Iranti will create the local database automatically if it does not already exist.`);
4284
4508
  }
4285
4509
  }
4510
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4511
+ instanceName,
4512
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4513
+ meta: existingInstanceMeta,
4514
+ dependencies: (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4515
+ }).intent?.strategy
4516
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4286
4517
  bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
4287
4518
  break;
4288
4519
  }
@@ -4298,6 +4529,15 @@ async function setupCommand(args) {
4298
4529
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
4299
4530
  dockerContainerName = containerName;
4300
4531
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
4532
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4533
+ instanceName,
4534
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4535
+ meta: existingInstanceMeta,
4536
+ dependencies: dockerContainerName
4537
+ ? [{ kind: 'docker-container', name: dockerContainerName, ...(dbHostPort > 0 ? { healthTcpPort: dbHostPort } : {}) }]
4538
+ : (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4539
+ }).intent?.strategy
4540
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4301
4541
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
4302
4542
  if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
4303
4543
  await runDockerPostgresContainer({
@@ -4442,6 +4682,13 @@ async function setupCommand(args) {
4442
4682
  port,
4443
4683
  databaseUrl: dbUrl,
4444
4684
  databaseMode,
4685
+ databaseIntent: buildInstanceDatabaseIntent({
4686
+ instanceName,
4687
+ databaseMode,
4688
+ databaseUrl: dbUrl,
4689
+ strategy: databaseIntentStrategy,
4690
+ dockerContainerName,
4691
+ }),
4445
4692
  provider,
4446
4693
  providerKeys,
4447
4694
  apiKey: defaultApiKey,
@@ -4472,6 +4719,7 @@ async function setupCommand(args) {
4472
4719
  console.log(` instance url http://localhost:${finalResult.port}`);
4473
4720
  console.log(` memory mode ${finalResult.mode}`);
4474
4721
  console.log(` database mode ${finalResult.databaseMode}`);
4722
+ console.log(` db strategy ${describeInstanceDatabaseIntent(finalResult.databaseIntent)}`);
4475
4723
  if (finalResult.bindings.length === 0) {
4476
4724
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4477
4725
  }
@@ -5236,6 +5484,12 @@ async function showInstanceCommand(args) {
5236
5484
  : {};
5237
5485
  const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5238
5486
  const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
5487
+ const databaseIntent = resolveInstanceDatabaseIntent({
5488
+ instanceName: name,
5489
+ env,
5490
+ meta,
5491
+ dependencies,
5492
+ });
5239
5493
  const runtime = await readInstanceRuntimeSummary(root, name);
5240
5494
  console.log(bold(`Instance: ${name}`));
5241
5495
  console.log(` dir : ${instanceDir}`);
@@ -5243,6 +5497,9 @@ async function showInstanceCommand(args) {
5243
5497
  console.log(` config: ${describeInstanceConfig(config)}`);
5244
5498
  console.log(` port: ${env.IRANTI_PORT ?? '3001'}`);
5245
5499
  console.log(` db : ${env.DATABASE_URL ?? '(missing)'}`);
5500
+ if (databaseIntent.intent) {
5501
+ console.log(` db strategy: ${describeInstanceDatabaseIntent(databaseIntent.intent, databaseIntent.source)}`);
5502
+ }
5246
5503
  console.log(` esc : ${env.IRANTI_ESCALATION_DIR ?? '(missing)'}`);
5247
5504
  if (dependencies.length > 0) {
5248
5505
  console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
@@ -5371,6 +5628,12 @@ async function configureInstanceCommand(args) {
5371
5628
  const { instanceDir, envFile, env, config } = await loadInstanceEnv(root, name, { allowRepair: true });
5372
5629
  const currentMeta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5373
5630
  const currentDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(currentMeta?.dependencies).dependencies;
5631
+ const currentDatabaseIntent = resolveInstanceDatabaseIntent({
5632
+ instanceName: name,
5633
+ env,
5634
+ meta: currentMeta,
5635
+ dependencies: currentDependencies,
5636
+ });
5374
5637
  const updates = {};
5375
5638
  let portRaw = getFlag(args, 'port');
5376
5639
  let dbUrl = getFlag(args, 'db-url');
@@ -5380,6 +5643,7 @@ async function configureInstanceCommand(args) {
5380
5643
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
5381
5644
  let dockerContainerName = getFlag(args, 'docker-container');
5382
5645
  let dockerHealthPortRaw = getFlag(args, 'docker-health-port');
5646
+ let dbIntentRaw = getFlag(args, 'db-intent');
5383
5647
  const clearDockerContainer = hasFlag(args, 'clear-docker-container');
5384
5648
  if (hasFlag(args, 'interactive')) {
5385
5649
  await withPromptSession(async (prompt) => {
@@ -5387,12 +5651,14 @@ async function configureInstanceCommand(args) {
5387
5651
  'This updates one existing instance in place.',
5388
5652
  'API port controls where the Iranti API listens.',
5389
5653
  'DATABASE_URL points at the PostgreSQL database for this instance.',
5654
+ 'Database intent tells Iranti whether this should be treated as a dedicated local DB, a shared local DB, or an external existing DB.',
5390
5655
  'LLM provider and provider key control which model backend Iranti uses.',
5391
5656
  'Iranti API key is the client credential other tools and project bindings use to authenticate.',
5392
5657
  'Optional Docker dependency settings let `iranti run --instance` start a recorded backing container before the API boots.',
5393
5658
  ]);
5394
5659
  portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
5395
5660
  dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
5661
+ dbIntentRaw = await prompt.line('Database intent (dedicated, shared, external)', dbIntentRaw ?? databaseIntentPromptValue(currentDatabaseIntent.intent?.strategy));
5396
5662
  providerInput = await prompt.line('LLM provider', providerInput ?? env.LLM_PROVIDER ?? 'mock');
5397
5663
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
5398
5664
  const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
@@ -5415,6 +5681,7 @@ async function configureInstanceCommand(args) {
5415
5681
  }
5416
5682
  if (dbUrl)
5417
5683
  updates.DATABASE_URL = dbUrl;
5684
+ const requestedDatabaseIntent = parseDatabaseIntentStrategyFlag(dbIntentRaw, '--db-intent');
5418
5685
  if (apiKey)
5419
5686
  updates.IRANTI_API_KEY = apiKey;
5420
5687
  const provider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
@@ -5467,8 +5734,9 @@ async function configureInstanceCommand(args) {
5467
5734
  }
5468
5735
  if (Object.keys(updates).length === 0) {
5469
5736
  const dependenciesUnchanged = JSON.stringify(nextDependencies) === JSON.stringify(currentDependencies);
5470
- if (dependenciesUnchanged) {
5471
- throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --port, or the Docker dependency flags.');
5737
+ const databaseIntentUnchanged = requestedDatabaseIntent === null;
5738
+ if (dependenciesUnchanged && databaseIntentUnchanged) {
5739
+ throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --db-intent, --port, or the Docker dependency flags.');
5472
5740
  }
5473
5741
  }
5474
5742
  const nextEnv = { ...env };
@@ -5488,9 +5756,19 @@ async function configureInstanceCommand(args) {
5488
5756
  if (!nextEnv.DATABASE_URL?.trim()) {
5489
5757
  throw cliError('IRANTI_INSTANCE_DATABASE_REQUIRED', `Instance '${name}' still needs DATABASE_URL before it can be considered repaired.`, ['Pass `--db-url <postgresql://...>` or rerun `iranti configure instance <name> --interactive`.'], { instance: name, envFile, config: config.classification });
5490
5758
  }
5759
+ const nextDatabaseIntent = buildInstanceDatabaseIntent({
5760
+ instanceName: name,
5761
+ databaseMode: inferDatabaseProvisioning(nextEnv.DATABASE_URL, nextDependencies),
5762
+ databaseUrl: nextEnv.DATABASE_URL,
5763
+ strategy: requestedDatabaseIntent ?? currentDatabaseIntent.intent?.strategy,
5764
+ dockerContainerName: nextDependencies.find((dependency) => dependency.kind === 'docker-container')?.name,
5765
+ });
5491
5766
  await ensureDir(instanceDir);
5492
5767
  await upsertEnvFile(envFile, updates);
5493
- await syncInstanceMeta(root, name, nextPort, { dependencies: nextDependencies });
5768
+ await syncInstanceMeta(root, name, nextPort, {
5769
+ dependencies: nextDependencies,
5770
+ databaseIntent: nextDatabaseIntent,
5771
+ });
5494
5772
  const json = hasFlag(args, 'json');
5495
5773
  const result = {
5496
5774
  instance: name,
@@ -5499,6 +5777,7 @@ async function configureInstanceCommand(args) {
5499
5777
  provider: updates.LLM_PROVIDER ?? env.LLM_PROVIDER ?? 'mock',
5500
5778
  apiKeyChanged: Boolean(apiKey),
5501
5779
  providerKeyChanged: Boolean(providerKey) || hasFlag(args, 'clear-provider-key'),
5780
+ databaseIntent: nextDatabaseIntent.strategy,
5502
5781
  dependencies: nextDependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)),
5503
5782
  };
5504
5783
  if (json) {
@@ -5509,6 +5788,7 @@ async function configureInstanceCommand(args) {
5509
5788
  console.log(` status ${okLabel()}`);
5510
5789
  console.log(` env ${envFile}`);
5511
5790
  console.log(` keys ${result.updatedKeys.join(', ')}`);
5791
+ console.log(` db ${describeInstanceDatabaseIntent(nextDatabaseIntent)}`);
5512
5792
  if (result.dependencies.length > 0) {
5513
5793
  console.log(` deps ${result.dependencies.join(', ')}`);
5514
5794
  }
@@ -5745,172 +6025,224 @@ async function resolveCommand(args) {
5745
6025
  await (0, resolutionist_1.resolveInteractive)(escalationDir);
5746
6026
  }
5747
6027
  async function handshakeCommand(args) {
5748
- const json = hasFlag(args, 'json');
5749
- const target = await resolveAttendantCliTarget(args);
5750
- const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
5751
- const recentMessages = resolveRecentMessages(args);
5752
- const result = await target.iranti.handshake({
5753
- agent: target.agentId,
5754
- task,
5755
- recentMessages,
5756
- });
5757
- if (json) {
5758
- console.log(JSON.stringify({
6028
+ try {
6029
+ const json = hasFlag(args, 'json');
6030
+ const target = await resolveAttendantCliTarget(args);
6031
+ const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
6032
+ let recentMessages = resolveRecentMessages(args);
6033
+ const backfillFile = resolveBackfillFile(args);
6034
+ let backfillResult = null;
6035
+ if (backfillFile) {
6036
+ const content = fs_1.default.readFileSync(backfillFile, 'utf-8');
6037
+ backfillResult = await (0, autoRemember_1.backfillChatHistory)({
6038
+ iranti: target.iranti,
6039
+ content,
6040
+ agent: target.agentId,
6041
+ source: 'CLIBackfill',
6042
+ });
6043
+ if (recentMessages.length === 0) {
6044
+ recentMessages = (0, autoRemember_1.parseBackfillChatTranscript)(content)
6045
+ .map((message) => message.text.trim())
6046
+ .filter(Boolean)
6047
+ .slice(-12);
6048
+ }
6049
+ }
6050
+ const result = await target.iranti.handshake({
5759
6051
  agent: target.agentId,
5760
- envSource: target.envSource,
5761
- envFile: target.envFile,
5762
6052
  task,
5763
6053
  recentMessages,
5764
- result,
5765
- }, null, 2));
5766
- return;
6054
+ });
6055
+ if (json) {
6056
+ console.log(JSON.stringify({
6057
+ agent: target.agentId,
6058
+ envSource: target.envSource,
6059
+ envFile: target.envFile,
6060
+ task,
6061
+ backfillFile,
6062
+ backfillResult,
6063
+ recentMessages,
6064
+ result,
6065
+ }, null, 2));
6066
+ return;
6067
+ }
6068
+ printHandshakeResult(target, task, result);
6069
+ if (backfillResult) {
6070
+ console.log('');
6071
+ console.log('Backfill:');
6072
+ console.log(` file ${backfillFile}`);
6073
+ console.log(` messages ${backfillResult.messagesParsed}`);
6074
+ console.log(` extracted ${backfillResult.extracted}`);
6075
+ console.log(` written ${backfillResult.written}`);
6076
+ if (backfillResult.skipped.length > 0) {
6077
+ console.log(` skipped ${backfillResult.skipped.length}`);
6078
+ }
6079
+ }
6080
+ if (result.backfillSuggestion?.suggested && !backfillFile) {
6081
+ console.log('');
6082
+ console.log('Backfill suggestion:');
6083
+ console.log(` reason ${result.backfillSuggestion.reason}`);
6084
+ console.log(` candidateFacts ${result.backfillSuggestion.candidateFacts}`);
6085
+ console.log(` sample keys ${result.backfillSuggestion.sampleKeys.join(', ')}`);
6086
+ console.log(` command ${result.backfillSuggestion.suggestedCommand}`);
6087
+ }
6088
+ console.log('');
6089
+ console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
6090
+ }
6091
+ finally {
6092
+ await (0, client_1.disconnectDb)().catch(() => undefined);
5767
6093
  }
5768
- printHandshakeResult(target, task, result);
5769
- console.log('');
5770
- console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
5771
6094
  }
5772
6095
  async function attendCommand(args) {
5773
- const json = hasFlag(args, 'json');
5774
- const target = await resolveAttendantCliTarget(args);
5775
- const latestMessage = resolveAttendMessage(args);
5776
- const currentContext = resolveContextText(args);
5777
- const maxFacts = parsePositiveInteger(getFlag(args, 'max-facts'), 'max-facts');
5778
- const entityHint = getFlag(args, 'entity-hint')?.trim();
5779
- if (entityHint && !entityHint.includes('/')) {
5780
- throw new Error('entity-hint must use entityType/entityId format.');
5781
- }
5782
- const result = await target.iranti.attend({
5783
- agent: target.agentId,
5784
- currentContext,
5785
- latestMessage,
5786
- forceInject: hasFlag(args, 'force'),
5787
- maxFacts,
5788
- entityHints: entityHint ? [entityHint] : undefined,
5789
- });
5790
- if (json) {
5791
- console.log(JSON.stringify({
6096
+ try {
6097
+ const json = hasFlag(args, 'json');
6098
+ const target = await resolveAttendantCliTarget(args);
6099
+ const latestMessage = resolveAttendMessage(args);
6100
+ const currentContext = resolveContextText(args);
6101
+ const maxFacts = parsePositiveInteger(getFlag(args, 'max-facts'), 'max-facts');
6102
+ const entityHint = getFlag(args, 'entity-hint')?.trim();
6103
+ if (entityHint && !entityHint.includes('/')) {
6104
+ throw new Error('entity-hint must use entityType/entityId format.');
6105
+ }
6106
+ const result = await target.iranti.attend({
5792
6107
  agent: target.agentId,
5793
- envSource: target.envSource,
5794
- envFile: target.envFile,
5795
- latestMessage,
5796
6108
  currentContext,
5797
- maxFacts: maxFacts ?? null,
5798
- entityHints: entityHint ? [entityHint] : [],
6109
+ latestMessage,
5799
6110
  forceInject: hasFlag(args, 'force'),
5800
- result,
5801
- }, null, 2));
5802
- return;
5803
- }
5804
- printAttendResult(target, latestMessage, result);
5805
- console.log('');
5806
- console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
5807
- }
5808
- async function handoffCommand(args) {
5809
- const json = hasFlag(args, 'json');
5810
- const target = await resolveAttendantCliTarget(args);
5811
- const taskEntity = resolveTaskEntity(args);
5812
- const projectEntity = getFlag(args, 'project-entity')?.trim();
5813
- if (projectEntity && !projectEntity.includes('/')) {
5814
- throw new Error('project-entity must use entityType/entityId format.');
5815
- }
5816
- const nextStep = getFlag(args, 'next-step')?.trim();
5817
- if (!nextStep) {
5818
- throw new Error('Missing --next-step. A standardized handoff must record the receiver action.');
5819
- }
5820
- const status = getFlag(args, 'status')?.trim() || 'ready_for_handoff';
5821
- const owner = getFlag(args, 'owner')?.trim();
5822
- const blockers = parseDelimitedList(getFlag(args, 'blockers'));
5823
- const artifacts = parseDelimitedList(getFlag(args, 'artifacts'));
5824
- const notes = getFlag(args, 'notes')?.trim();
5825
- const source = getFlag(args, 'source')?.trim() || 'CLIHandoff';
5826
- const confidence = parsePositiveInteger(getFlag(args, 'confidence'), 'confidence') ?? 95;
5827
- if (confidence > 100) {
5828
- throw new Error('confidence must be <= 100.');
5829
- }
5830
- const writes = [];
5831
- writes.push({
5832
- entity: taskEntity,
5833
- key: 'status',
5834
- value: { state: status },
5835
- summary: buildHandoffSummary('status', { state: status }),
5836
- });
5837
- writes.push({
5838
- entity: taskEntity,
5839
- key: 'next_step',
5840
- value: { instruction: nextStep },
5841
- summary: buildHandoffSummary('next_step', { instruction: nextStep }),
5842
- });
5843
- if (owner) {
5844
- writes.push({
5845
- entity: taskEntity,
5846
- key: 'current_owner',
5847
- value: { agentId: owner },
5848
- summary: buildHandoffSummary('current_owner', { agentId: owner }),
6111
+ maxFacts,
6112
+ entityHints: entityHint ? [entityHint] : undefined,
5849
6113
  });
6114
+ if (json) {
6115
+ console.log(JSON.stringify({
6116
+ agent: target.agentId,
6117
+ envSource: target.envSource,
6118
+ envFile: target.envFile,
6119
+ latestMessage,
6120
+ currentContext,
6121
+ maxFacts: maxFacts ?? null,
6122
+ entityHints: entityHint ? [entityHint] : [],
6123
+ forceInject: hasFlag(args, 'force'),
6124
+ result,
6125
+ }, null, 2));
6126
+ return;
6127
+ }
6128
+ printAttendResult(target, latestMessage, result);
6129
+ console.log('');
6130
+ console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
5850
6131
  }
5851
- if (blockers.length > 0) {
5852
- writes.push({
5853
- entity: taskEntity,
5854
- key: 'blockers',
5855
- value: { items: blockers },
5856
- summary: buildHandoffSummary('blockers', { items: blockers }),
5857
- });
6132
+ finally {
6133
+ await (0, client_1.disconnectDb)().catch(() => undefined);
5858
6134
  }
5859
- if (artifacts.length > 0) {
6135
+ }
6136
+ async function handoffCommand(args) {
6137
+ try {
6138
+ const json = hasFlag(args, 'json');
6139
+ const target = await resolveAttendantCliTarget(args);
6140
+ const taskEntity = resolveTaskEntity(args);
6141
+ const projectEntity = getFlag(args, 'project-entity')?.trim();
6142
+ if (projectEntity && !projectEntity.includes('/')) {
6143
+ throw new Error('project-entity must use entityType/entityId format.');
6144
+ }
6145
+ const nextStep = getFlag(args, 'next-step')?.trim();
6146
+ if (!nextStep) {
6147
+ throw new Error('Missing --next-step. A standardized handoff must record the receiver action.');
6148
+ }
6149
+ const status = getFlag(args, 'status')?.trim() || 'ready_for_handoff';
6150
+ const owner = getFlag(args, 'owner')?.trim();
6151
+ const blockers = parseDelimitedList(getFlag(args, 'blockers'));
6152
+ const artifacts = parseDelimitedList(getFlag(args, 'artifacts'));
6153
+ const notes = getFlag(args, 'notes')?.trim();
6154
+ const source = getFlag(args, 'source')?.trim() || 'CLIHandoff';
6155
+ const confidence = parsePositiveInteger(getFlag(args, 'confidence'), 'confidence') ?? 95;
6156
+ if (confidence > 100) {
6157
+ throw new Error('confidence must be <= 100.');
6158
+ }
6159
+ const writes = [];
5860
6160
  writes.push({
5861
6161
  entity: taskEntity,
5862
- key: 'artifacts',
5863
- value: { files: artifacts },
5864
- summary: buildHandoffSummary('artifacts', { files: artifacts }),
6162
+ key: 'status',
6163
+ value: { state: status },
6164
+ summary: buildHandoffSummary('status', { state: status }),
5865
6165
  });
5866
- }
5867
- if (notes) {
5868
6166
  writes.push({
5869
6167
  entity: taskEntity,
5870
- key: 'notes',
5871
- value: { text: notes },
5872
- summary: buildHandoffSummary('notes', { text: notes }),
5873
- });
5874
- }
5875
- if (projectEntity) {
5876
- writes.push({
5877
- entity: projectEntity,
5878
- key: 'active_handoff_task',
5879
- value: {
5880
- taskEntity,
5881
- owner: owner ?? null,
5882
- status,
5883
- updatedBy: target.agentId,
5884
- },
5885
- summary: buildHandoffSummary('active_handoff_task', { taskEntity }),
5886
- });
5887
- }
5888
- for (const write of writes) {
5889
- await target.iranti.write({
5890
- entity: write.entity,
5891
- key: write.key,
5892
- value: write.value,
5893
- summary: write.summary,
5894
- confidence,
5895
- source,
5896
- agent: target.agentId,
6168
+ key: 'next_step',
6169
+ value: { instruction: nextStep },
6170
+ summary: buildHandoffSummary('next_step', { instruction: nextStep }),
5897
6171
  });
6172
+ if (owner) {
6173
+ writes.push({
6174
+ entity: taskEntity,
6175
+ key: 'current_owner',
6176
+ value: { agentId: owner },
6177
+ summary: buildHandoffSummary('current_owner', { agentId: owner }),
6178
+ });
6179
+ }
6180
+ if (blockers.length > 0) {
6181
+ writes.push({
6182
+ entity: taskEntity,
6183
+ key: 'blockers',
6184
+ value: { items: blockers },
6185
+ summary: buildHandoffSummary('blockers', { items: blockers }),
6186
+ });
6187
+ }
6188
+ if (artifacts.length > 0) {
6189
+ writes.push({
6190
+ entity: taskEntity,
6191
+ key: 'artifacts',
6192
+ value: { files: artifacts },
6193
+ summary: buildHandoffSummary('artifacts', { files: artifacts }),
6194
+ });
6195
+ }
6196
+ if (notes) {
6197
+ writes.push({
6198
+ entity: taskEntity,
6199
+ key: 'notes',
6200
+ value: { text: notes },
6201
+ summary: buildHandoffSummary('notes', { text: notes }),
6202
+ });
6203
+ }
6204
+ if (projectEntity) {
6205
+ writes.push({
6206
+ entity: projectEntity,
6207
+ key: 'active_handoff_task',
6208
+ value: {
6209
+ taskEntity,
6210
+ owner: owner ?? null,
6211
+ status,
6212
+ updatedBy: target.agentId,
6213
+ },
6214
+ summary: buildHandoffSummary('active_handoff_task', { taskEntity }),
6215
+ });
6216
+ }
6217
+ for (const write of writes) {
6218
+ await target.iranti.write({
6219
+ entity: write.entity,
6220
+ key: write.key,
6221
+ value: write.value,
6222
+ summary: write.summary,
6223
+ confidence,
6224
+ source,
6225
+ agent: target.agentId,
6226
+ });
6227
+ }
6228
+ if (json) {
6229
+ console.log(JSON.stringify({
6230
+ agent: target.agentId,
6231
+ envSource: target.envSource,
6232
+ envFile: target.envFile,
6233
+ source,
6234
+ confidence,
6235
+ writes,
6236
+ }, null, 2));
6237
+ return;
6238
+ }
6239
+ printHandoffResult(target, taskEntity, writes);
6240
+ console.log('');
6241
+ console.log(`${infoLabel()} Handoffs are shared-memory facts. Pair this with checkpoint() if the sender also needs agent-local recovery.`);
5898
6242
  }
5899
- if (json) {
5900
- console.log(JSON.stringify({
5901
- agent: target.agentId,
5902
- envSource: target.envSource,
5903
- envFile: target.envFile,
5904
- source,
5905
- confidence,
5906
- writes,
5907
- }, null, 2));
5908
- process.exit(0);
6243
+ finally {
6244
+ await (0, client_1.disconnectDb)().catch(() => undefined);
5909
6245
  }
5910
- printHandoffResult(target, taskEntity, writes);
5911
- console.log('');
5912
- console.log(`${infoLabel()} Handoffs are shared-memory facts. Pair this with checkpoint() if the sender also needs agent-local recovery.`);
5913
- process.exit(0);
5914
6246
  }
5915
6247
  function printClaudeSetupHelp() {
5916
6248
  console.log([