knodin 0.8.3 → 0.8.5

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 (42) hide show
  1. package/README.md +7 -1
  2. package/dist/bin/cli.js +489 -78
  3. package/dist/src/agent-events.js +128 -0
  4. package/dist/src/agent-hooks.js +156 -0
  5. package/dist/src/cli-model.js +42 -1
  6. package/dist/src/engine/candidate-database.js +167 -0
  7. package/dist/src/engine/embeddings.js +8 -5
  8. package/dist/src/engine/index.js +930 -262
  9. package/dist/src/engine/prune.js +8 -0
  10. package/dist/src/file-metrics.js +153 -0
  11. package/dist/src/output-telemetry.js +8 -3
  12. package/dist/src/repair-lease.js +8 -5
  13. package/dist/src/response-budget.js +2 -1
  14. package/dist/src/session-telemetry.js +163 -0
  15. package/dist/src/shared-index/artifact.js +65 -0
  16. package/dist/src/shared-index/cache.js +267 -0
  17. package/dist/src/shared-index/compatibility.js +70 -0
  18. package/dist/src/shared-index/config.js +224 -0
  19. package/dist/src/shared-index/contract.js +117 -0
  20. package/dist/src/shared-index/manifest.js +71 -0
  21. package/dist/src/shared-index/opportunistic.js +37 -0
  22. package/dist/src/shared-index/overlay.js +98 -0
  23. package/dist/src/shared-index/provenance.js +149 -0
  24. package/dist/src/shared-index/publisher.js +198 -0
  25. package/dist/src/shared-index/restore.js +209 -0
  26. package/dist/src/shared-index/s3-client.js +124 -0
  27. package/dist/src/shared-index/schemas.js +86 -0
  28. package/dist/src/shared-index/selection.js +111 -0
  29. package/dist/src/tools/knodin-tools.js +70 -3
  30. package/docs/CLI.md +56 -0
  31. package/docs/INSTALLATION.md +11 -0
  32. package/docs/MCP.md +12 -0
  33. package/docs/SHARED-INDEX-CONTRACT.md +132 -0
  34. package/docs/TELEMETRY.md +19 -1
  35. package/docs/releases/0.8.4.md +22 -0
  36. package/docs/releases/0.8.5.md +25 -0
  37. package/package.json +12 -3
  38. package/roadmap/competitive-roadmap.md +20 -0
  39. package/schemas/shared-index-branch-pointer-v1.schema.json +66 -0
  40. package/schemas/shared-index-config-v1.schema.json +97 -0
  41. package/schemas/shared-index-manifest-v1.schema.json +104 -0
  42. package/schemas/shared-index-provenance-v1.schema.json +100 -0
package/dist/bin/cli.js CHANGED
@@ -16,6 +16,8 @@ import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import readline from "node:readline/promises";
18
18
  import { fileURLToPath } from "node:url";
19
+ import { canUseSessionContextCache, parseClaudeHookPayload, readSessionContextCache, recordClaudeLifecycleEvent, renderSessionContext, SESSION_CONTEXT_BUDGET_LINE, sessionStartHookOutput, writeSessionContextCache, } from "../src/agent-events.js";
20
+ import { inspectClaudeAgentHooks, installClaudeAgentHooks, uninstallClaudeAgentHooks, } from "../src/agent-hooks.js";
19
21
  import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
20
22
  import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
21
23
  import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
@@ -43,6 +45,7 @@ import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, res
43
45
  import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
44
46
  import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
45
47
  import { applyResponseBudget } from "../src/response-budget.js";
48
+ import { appendSessionEvent, clearSessionTelemetry, disableSessionTelemetry, enableSessionTelemetry, readSessionEvents, sessionTelemetryStatus, } from "../src/session-telemetry.js";
46
49
  import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
47
50
  import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
48
51
  import { KNODIN_VERSION } from "../src/version.js";
@@ -362,7 +365,7 @@ async function runRemoteCommand(args) {
362
365
  * empty result this whole feature exists to avoid.
363
366
  */
364
367
  async function indexMirror(source, deferSemantic) {
365
- const engine = createEngine();
368
+ const engine = createEngine({ watcher: "disabled" });
366
369
  try {
367
370
  await engine.index(source, undefined, true, { skipEmbeddings: true });
368
371
  const structural = await engine.status(source, { audit: "deep" });
@@ -417,7 +420,8 @@ function formatStatusHuman(result) {
417
420
  const freshnessLine = `Freshness: ${result.freshness.state}; indexed ${head(result.freshness.indexedHead)}, ` +
418
421
  `current ${head(result.freshness.currentHead)} (${result.freshness.commitRelation}${distance}); ` +
419
422
  `${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
420
- `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n`;
423
+ `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n` +
424
+ `Freshness mechanism: ${result.freshnessMechanism.strategy}; watcher ${result.freshnessMechanism.watcher}${result.freshnessMechanism.diagnostic ? ` (${result.freshnessMechanism.diagnostic})` : ""}.\n`;
421
425
  if (result.status === "healthy")
422
426
  return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
423
427
  if (result.status === "stale")
@@ -631,7 +635,7 @@ async function main() {
631
635
  }, 50);
632
636
  interval.unref();
633
637
  process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
634
- const engine = createEngine();
638
+ const engine = createEngine({ watcher: "disabled" });
635
639
  try {
636
640
  const systemConfig = loadSystemConfiguration(repository);
637
641
  const summary = await initializeRepositories([repository], {
@@ -684,9 +688,13 @@ async function main() {
684
688
  };
685
689
  const repoFlag = selectorValue("--repo");
686
690
  const runtimeCommand = resolveCliRuntimeCommand(process);
687
- // hook-refresh is an internal machine-to-machine command. Keep it JSON even
691
+ // Internal machine-to-machine commands stay JSON regardless of TTY state.
688
692
  // when an older installed hook predates the explicit --json argument.
689
- const jsonOutput = invocation.options.json === true || cmd === "hook-refresh";
693
+ const jsonOutput = invocation.options.json === true ||
694
+ invocation.options.format === "json" ||
695
+ cmd === "hook-refresh" ||
696
+ cmd === "agent-event" ||
697
+ (cmd === "shared" && rawRest.includes("--jsonl"));
690
698
  // `--json` is a shared output flag. Repair owns its richer --json/--jsonl
691
699
  // parser; all other commands receive their original arguments minus it.
692
700
  const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
@@ -753,7 +761,7 @@ async function main() {
753
761
  if (!plan.json) {
754
762
  process.stderr.write("warning: `knodin fleet init` is deprecated; use `knodin repos init --linked-worktrees=skip|include` (alias retained for two minor releases)\n");
755
763
  }
756
- const engine = createEngine();
764
+ const engine = createEngine({ watcher: "disabled" });
757
765
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
758
766
  const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
759
767
  const summary = await initializeRepositories(plan.roots, {
@@ -790,7 +798,7 @@ async function main() {
790
798
  depth: plan.depth,
791
799
  linkedWorktrees: plan.linkedWorktrees,
792
800
  });
793
- const engine = createEngine();
801
+ const engine = createEngine({ watcher: "disabled" });
794
802
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
795
803
  const repositories = [];
796
804
  const discoveryRecords = plan.signals
@@ -857,7 +865,7 @@ async function main() {
857
865
  process.exitCode = summary.exitCode;
858
866
  return;
859
867
  }
860
- const engine = createEngine();
868
+ const engine = createEngine({ watcher: "disabled" });
861
869
  const summary = await initializeRepositories(plan.roots, {
862
870
  command: runtimeCommand,
863
871
  depth: plan.depth,
@@ -895,7 +903,7 @@ async function main() {
895
903
  return;
896
904
  }
897
905
  if (plan.command === "search") {
898
- const engine = createEngine();
906
+ const engine = createEngine({ watcher: "disabled" });
899
907
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
900
908
  const output = await searchRepositories(plan.roots, plan.query ?? "", {
901
909
  depth: plan.depth,
@@ -924,7 +932,7 @@ async function main() {
924
932
  depth: plan.depth,
925
933
  linkedWorktrees: plan.linkedWorktrees,
926
934
  });
927
- const engine = createEngine();
935
+ const engine = createEngine({ watcher: "disabled" });
928
936
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
929
937
  const repositories = [];
930
938
  for (const repository of discovery.repositories) {
@@ -1075,12 +1083,43 @@ async function main() {
1075
1083
  background.unref();
1076
1084
  }
1077
1085
  }
1086
+ if (cmd === "agent-hooks") {
1087
+ const action = invocation.commandPath[1];
1088
+ const client = selectorValue("--client") ?? "claude";
1089
+ if (client !== "claude")
1090
+ throw new Error("knodin agent-hooks: only --client claude is supported");
1091
+ if (invocation.options.dryRun === true && action !== "install")
1092
+ throw new Error("knodin agent-hooks: --dry-run applies only to install");
1093
+ let hookResult;
1094
+ if (action === "install") {
1095
+ const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
1096
+ hookResult = installClaudeAgentHooks({
1097
+ command: runtimeCommand.map(quote).join(" "),
1098
+ dryRun: invocation.options.dryRun === true,
1099
+ });
1100
+ }
1101
+ else if (action === "status")
1102
+ hookResult = inspectClaudeAgentHooks();
1103
+ else if (action === "uninstall")
1104
+ hookResult = uninstallClaudeAgentHooks();
1105
+ else
1106
+ throw new Error("knodin agent-hooks requires install, status, or uninstall");
1107
+ process.stdout.write(jsonOutput
1108
+ ? `${JSON.stringify(hookResult)}\n`
1109
+ : formatGenericHuman("agent-hooks", hookResult));
1110
+ return;
1111
+ }
1078
1112
  const resolved = resolveRepo(repoFlag, process.cwd());
1079
1113
  if (!resolved.ok) {
1114
+ // Global agent hooks also run outside repositories and must fail open.
1115
+ if (cmd === "agent-event")
1116
+ return;
1080
1117
  process.stderr.write(`${resolved.error}\n`);
1081
1118
  process.exit(1);
1082
1119
  }
1083
1120
  const repo = resolved.repo;
1121
+ if (cmd === "agent-event" && !fs.existsSync(resolveDbPath(repo)))
1122
+ return;
1084
1123
  if (cmd === "doctor") {
1085
1124
  const gitProbe = spawnSync(gitExecutable(), ["rev-parse", "--is-inside-work-tree"], {
1086
1125
  cwd: repo,
@@ -1133,7 +1172,7 @@ async function main() {
1133
1172
  let compressionResult;
1134
1173
  let diagnosisUnavailable = false;
1135
1174
  if (action === "diagnose") {
1136
- const diagnosisEngine = createEngine();
1175
+ const diagnosisEngine = createEngine({ watcher: "disabled" });
1137
1176
  try {
1138
1177
  const health = await inspectGraphQueryHealth(repo, (target) => diagnosisEngine.status(target, { audit: "cached" }));
1139
1178
  if (health.available) {
@@ -1225,12 +1264,19 @@ async function main() {
1225
1264
  process.stdout.write(formatGenericHuman("compress delete", compressionResult));
1226
1265
  return;
1227
1266
  }
1228
- const engine = createEngine();
1267
+ const engine = createEngine({ watcher: "disabled" });
1268
+ const opportunisticSharedRestore = async () => {
1269
+ if (!fs.existsSync(path.join(repo, ".knodin", "shared-index.yaml")))
1270
+ return null;
1271
+ const { attemptOpportunisticSharedRestore } = await import("../src/shared-index/opportunistic.js");
1272
+ return attemptOpportunisticSharedRestore(repo, engine);
1273
+ };
1229
1274
  let result;
1230
1275
  let repairOutput;
1231
1276
  let repairWasPlan = false;
1232
1277
  let repairExitCode = 0;
1233
1278
  let statusWasWatched = false;
1279
+ let agentEventOutput = false;
1234
1280
  const graphRead = async (run) => {
1235
1281
  const health = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1236
1282
  if (!health.available) {
@@ -1246,6 +1292,310 @@ async function main() {
1246
1292
  return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
1247
1293
  };
1248
1294
  switch (cmd) {
1295
+ case "shared": {
1296
+ const [cacheModule, compatibilityModule, configModule, manifestModule, provenanceModule, publisherModule, restoreModule, s3Module, selectionModule,] = await Promise.all([
1297
+ import("../src/shared-index/cache.js"),
1298
+ import("../src/shared-index/compatibility.js"),
1299
+ import("../src/shared-index/config.js"),
1300
+ import("../src/shared-index/manifest.js"),
1301
+ import("../src/shared-index/provenance.js"),
1302
+ import("../src/shared-index/publisher.js"),
1303
+ import("../src/shared-index/restore.js"),
1304
+ import("../src/shared-index/s3-client.js"),
1305
+ import("../src/shared-index/selection.js"),
1306
+ ]);
1307
+ const { SharedIndexCache } = cacheModule;
1308
+ const { currentSharedIndexCompatibility } = compatibilityModule;
1309
+ const { loadSharedIndexConfig, publicKeyFingerprint, writeSharedIndexConfig } = configModule;
1310
+ const { branchPointerKey } = manifestModule;
1311
+ const { readSharedIndexProvenance, readSharedRestoreAttempt } = provenanceModule;
1312
+ const { createSharedPublisherBundle, finalizeSharedPublisherBundle, guardPublisherPointer, prepareSharedPublisherDatabase, } = publisherModule;
1313
+ const { restoreSharedIndex } = restoreModule;
1314
+ const { createSharedObjectStore, SharedIndexError } = s3Module;
1315
+ const { configuredBranchCandidates } = selectionModule;
1316
+ const action = invocation.commandPath[1];
1317
+ if (action === "configure") {
1318
+ const existing = loadSharedIndexConfig(repo);
1319
+ if (invocation.options.disable === true) {
1320
+ if (existing.state !== "configured")
1321
+ throw new Error("knodin shared configure --disable requires a valid existing configuration");
1322
+ writeSharedIndexConfig(repo, { ...existing.config, enabled: false });
1323
+ result = { state: "configured", config: { ...existing.config, enabled: false } };
1324
+ break;
1325
+ }
1326
+ const required = (name) => {
1327
+ const value = invocation.options[name];
1328
+ if (typeof value !== "string" || !value)
1329
+ throw new Error(`knodin shared configure requires --${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`);
1330
+ return value;
1331
+ };
1332
+ const publicKeyPath = path.resolve(process.cwd(), required("publicKey"));
1333
+ const publicKeyPem = fs.readFileSync(publicKeyPath, "utf8");
1334
+ const branches = Array.isArray(invocation.options.branch)
1335
+ ? invocation.options.branch.filter((value) => typeof value === "string")
1336
+ : [];
1337
+ if (branches.length === 0)
1338
+ throw new Error("knodin shared configure requires --branch");
1339
+ const config = {
1340
+ schemaVersion: 1,
1341
+ enabled: true,
1342
+ registrationId: required("registrationId"),
1343
+ aws: { region: required("region"), bucket: required("bucket") },
1344
+ repository: {
1345
+ host: required("host"),
1346
+ owner: required("owner"),
1347
+ name: required("name"),
1348
+ },
1349
+ branches,
1350
+ signing: {
1351
+ algorithm: "RSASSA_PSS_SHA_256",
1352
+ keyId: required("keyId"),
1353
+ publicKeySha256: publicKeyFingerprint(publicKeyPem),
1354
+ publicKeyPem,
1355
+ },
1356
+ };
1357
+ const target = writeSharedIndexConfig(repo, config);
1358
+ result = { state: "configured", path: target, config: loadSharedIndexConfig(repo) };
1359
+ break;
1360
+ }
1361
+ if (action === "publisher-metadata") {
1362
+ const configuration = loadSharedIndexConfig(repo);
1363
+ if (configuration.state !== "configured")
1364
+ throw new Error("knodin shared publisher-metadata requires valid reviewed configuration");
1365
+ result = prepareSharedPublisherDatabase(repo);
1366
+ break;
1367
+ }
1368
+ if (action === "publisher-bundle") {
1369
+ const configuration = loadSharedIndexConfig(repo);
1370
+ if (configuration.state !== "configured")
1371
+ throw new Error("knodin shared publisher-bundle requires valid reviewed configuration");
1372
+ const branch = invocation.positionals[0];
1373
+ const output = invocation.options.output;
1374
+ if (!branch || typeof output !== "string")
1375
+ throw new Error("knodin shared publisher-bundle requires <branch> --output <path>");
1376
+ result = await createSharedPublisherBundle({
1377
+ repo,
1378
+ config: configuration.config,
1379
+ branch,
1380
+ outputDirectory: path.resolve(process.cwd(), output),
1381
+ });
1382
+ break;
1383
+ }
1384
+ if (action === "publisher-finalize") {
1385
+ const configuration = loadSharedIndexConfig(repo);
1386
+ if (configuration.state !== "configured")
1387
+ throw new Error("knodin shared publisher-finalize requires valid reviewed configuration");
1388
+ const bundlePath = invocation.positionals[0];
1389
+ if (!bundlePath)
1390
+ throw new Error("knodin shared publisher-finalize requires <bundle>");
1391
+ const bundle = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), bundlePath), "utf8"));
1392
+ const pointer = finalizeSharedPublisherBundle({ config: configuration.config, bundle });
1393
+ result = { bundle, pointer: JSON.parse(pointer.toString("utf8")) };
1394
+ break;
1395
+ }
1396
+ if (action === "publisher-guard") {
1397
+ const configuration = loadSharedIndexConfig(repo);
1398
+ if (configuration.state !== "configured")
1399
+ throw new Error("knodin shared publisher-guard requires valid reviewed configuration");
1400
+ const [pointerPath, branch, commit] = invocation.positionals;
1401
+ if (!pointerPath || !branch || !commit)
1402
+ throw new Error("knodin shared publisher-guard requires <pointer> <branch> <commit>");
1403
+ guardPublisherPointer({
1404
+ repo,
1405
+ config: configuration.config,
1406
+ branch,
1407
+ newCommit: commit,
1408
+ existingPointerBytes: fs.readFileSync(path.resolve(process.cwd(), pointerPath)),
1409
+ });
1410
+ result = { allowed: true, branch, commit };
1411
+ break;
1412
+ }
1413
+ if (action === "status") {
1414
+ const configuration = loadSharedIndexConfig(repo);
1415
+ const graph = await engine.status(repo, { audit: "cached" });
1416
+ let credentialProbe = "not-requested";
1417
+ if (invocation.options.probe === true) {
1418
+ if (configuration.state !== "configured" || !configuration.config.enabled) {
1419
+ credentialProbe = { state: "unavailable", category: "configuration" };
1420
+ }
1421
+ else {
1422
+ const store = createSharedObjectStore(configuration.config);
1423
+ try {
1424
+ const current = spawnSync(gitExecutable(), ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim();
1425
+ const branch = configuredBranchCandidates(configuration.config, current || undefined)[0];
1426
+ if (!store || !branch)
1427
+ throw new SharedIndexError("configuration", "shared-index probe has no configured local branch candidate");
1428
+ await store.head(branchPointerKey(configuration.config, branch));
1429
+ credentialProbe = { state: "available" };
1430
+ }
1431
+ catch (error) {
1432
+ credentialProbe = {
1433
+ state: "unavailable",
1434
+ category: error instanceof SharedIndexError ? error.category : "unavailable",
1435
+ };
1436
+ }
1437
+ }
1438
+ }
1439
+ result = {
1440
+ configuration,
1441
+ provenance: readSharedIndexProvenance(repo),
1442
+ lastRestoreAttempt: readSharedRestoreAttempt(repo),
1443
+ credentialProbe,
1444
+ freshnessMechanism: graph.freshnessMechanism,
1445
+ };
1446
+ break;
1447
+ }
1448
+ if (action === "pull") {
1449
+ const configuration = loadSharedIndexConfig(repo);
1450
+ if (configuration.state !== "configured")
1451
+ throw new Error("knodin shared pull requires valid .knodin/shared-index.yaml");
1452
+ const store = createSharedObjectStore(configuration.config);
1453
+ if (!store)
1454
+ throw new Error("knodin shared pull: shared-index access is disabled");
1455
+ const progressMode = invocation.options.jsonl === true
1456
+ ? "jsonl"
1457
+ : typeof invocation.options.progress === "string"
1458
+ ? invocation.options.progress
1459
+ : process.stderr.isTTY
1460
+ ? "plain"
1461
+ : "none";
1462
+ if (!["plain", "jsonl", "none"].includes(progressMode))
1463
+ throw new Error("knodin shared pull: --progress must be plain, jsonl, or none");
1464
+ let lastHumanPhase = "";
1465
+ let lastHumanWrite = 0;
1466
+ result = await restoreSharedIndex({
1467
+ repo,
1468
+ config: configuration.config,
1469
+ store,
1470
+ cache: new SharedIndexCache(),
1471
+ engine,
1472
+ clientCompatibility: currentSharedIndexCompatibility(),
1473
+ onProgress: (event) => {
1474
+ if (progressMode === "jsonl")
1475
+ process.stdout.write(`${JSON.stringify({ type: "progress", ...event })}\n`);
1476
+ else if (progressMode === "plain") {
1477
+ const now = Date.now();
1478
+ if (event.phase !== lastHumanPhase ||
1479
+ event.phase === "completed" ||
1480
+ event.phase === "failed" ||
1481
+ now - lastHumanWrite >= 125) {
1482
+ lastHumanPhase = event.phase;
1483
+ lastHumanWrite = now;
1484
+ const count = event.completed !== undefined && event.total !== undefined
1485
+ ? ` ${event.completed.toLocaleString()}/${event.total.toLocaleString()}`
1486
+ : "";
1487
+ const rate = event.ratePerSecond
1488
+ ? ` · ${Math.round(event.ratePerSecond).toLocaleString()}/s`
1489
+ : "";
1490
+ const eta = event.etaSeconds ? ` · ETA ${Math.ceil(event.etaSeconds)}s` : "";
1491
+ process.stderr.write(`[shared:${event.phase}]${count}${rate}${eta} ${event.message}\n`);
1492
+ }
1493
+ }
1494
+ },
1495
+ });
1496
+ if (!result.restored)
1497
+ repairExitCode = 1;
1498
+ break;
1499
+ }
1500
+ throw new Error("knodin shared: unknown action");
1501
+ }
1502
+ case "agent-event": {
1503
+ agentEventOutput = true;
1504
+ const event = invocation.positionals[0];
1505
+ if (!event)
1506
+ throw new Error("knodin agent-event requires an event");
1507
+ let payload;
1508
+ try {
1509
+ payload = parseClaudeHookPayload(await readBoundedStdin(65_536));
1510
+ }
1511
+ catch {
1512
+ result = null;
1513
+ break;
1514
+ }
1515
+ if (event !== "session-start") {
1516
+ try {
1517
+ recordClaudeLifecycleEvent(repo, event, payload);
1518
+ }
1519
+ catch {
1520
+ // Optional telemetry must never interrupt the agent.
1521
+ }
1522
+ result = null;
1523
+ break;
1524
+ }
1525
+ const startedAt = performance.now();
1526
+ let status;
1527
+ try {
1528
+ status = await engine.status(repo, { audit: "cached" });
1529
+ }
1530
+ catch {
1531
+ if (payload.session_id)
1532
+ try {
1533
+ appendSessionEvent(repo, {
1534
+ event: "session_start",
1535
+ sessionId: payload.session_id,
1536
+ context: "unavailable",
1537
+ freshness: "unavailable",
1538
+ latencyMs: Math.round((performance.now() - startedAt) * 100) / 100,
1539
+ cacheHit: false,
1540
+ });
1541
+ }
1542
+ catch {
1543
+ // Best-effort local recording remains fail-open.
1544
+ }
1545
+ result = sessionStartHookOutput(`## knodin session context\n${SESSION_CONTEXT_BUDGET_LINE}\nContext generation was unavailable. Run knodin status before relying on graph evidence.`);
1546
+ break;
1547
+ }
1548
+ const freshness = status.freshness?.state ?? "unknown";
1549
+ const cacheKey = JSON.stringify({
1550
+ schemaVersion: 1,
1551
+ head: status.freshness?.currentHead,
1552
+ fingerprint: status.freshness?.workingTree?.indexedFingerprint,
1553
+ generation: status.indexGeneration,
1554
+ budgets: [600, 8192, 12],
1555
+ });
1556
+ const cacheEligible = canUseSessionContextCache(status.status, freshness);
1557
+ let text = cacheEligible ? readSessionContextCache(repo, cacheKey) : null;
1558
+ let cacheHit = text !== null;
1559
+ let contextState = "delivered";
1560
+ if (!cacheEligible) {
1561
+ contextState = "degraded";
1562
+ text = `## knodin session context\n${SESSION_CONTEXT_BUDGET_LINE}\nGraph evidence is ${freshness}. Run knodin status and follow its repair or reindex guidance before relying on graph evidence.`;
1563
+ }
1564
+ else if (!text) {
1565
+ cacheHit = false;
1566
+ try {
1567
+ const context = await buildKnodinContext(engine, "Orient this coding session", repo, undefined, []);
1568
+ text = renderSessionContext(context, freshness);
1569
+ writeSessionContextCache(repo, cacheKey, text);
1570
+ }
1571
+ catch {
1572
+ contextState = "unavailable";
1573
+ text = `## knodin session context\n${SESSION_CONTEXT_BUDGET_LINE}\nContext generation was unavailable. Run knodin status, then call the connected knodin context operation when needed.`;
1574
+ }
1575
+ }
1576
+ if (payload.session_id)
1577
+ try {
1578
+ appendSessionEvent(repo, {
1579
+ event: "session_start",
1580
+ sessionId: payload.session_id,
1581
+ context: contextState,
1582
+ freshness: freshness === "fresh"
1583
+ ? "fresh"
1584
+ : status.status === "repair-needed"
1585
+ ? "repair-needed"
1586
+ : freshness === "unknown"
1587
+ ? "unavailable"
1588
+ : "stale",
1589
+ latencyMs: Math.round((performance.now() - startedAt) * 100) / 100,
1590
+ cacheHit,
1591
+ });
1592
+ }
1593
+ catch {
1594
+ // Best-effort local recording remains fail-open.
1595
+ }
1596
+ result = sessionStartHookOutput(text);
1597
+ break;
1598
+ }
1249
1599
  case "doctor": {
1250
1600
  const client = selectorValue("--client");
1251
1601
  if (client !== undefined &&
@@ -1340,8 +1690,11 @@ async function main() {
1340
1690
  else {
1341
1691
  throw new Error("knodin hook-refresh: invalid lifecycle event");
1342
1692
  }
1343
- const indexed = await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1344
- result = { indexed };
1693
+ const shared = await opportunisticSharedRestore();
1694
+ const indexed = shared?.restored
1695
+ ? []
1696
+ : await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1697
+ result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
1345
1698
  break;
1346
1699
  }
1347
1700
  case "init": {
@@ -1354,6 +1707,11 @@ async function main() {
1354
1707
  let paths;
1355
1708
  try {
1356
1709
  try {
1710
+ if (!fs.existsSync(resolveDbPath(repo))) {
1711
+ const shared = await opportunisticSharedRestore();
1712
+ if (shared?.restored)
1713
+ process.stderr.write("[init:shared] Restored and verified shared snapshot\n");
1714
+ }
1357
1715
  paths = await initializeRepository(repo, {
1358
1716
  command: runtimeCommand,
1359
1717
  index: (target, options) => engine.index(target, undefined, false, options),
@@ -1785,7 +2143,7 @@ async function main() {
1785
2143
  ? ""
1786
2144
  : (rest[1] ?? "");
1787
2145
  if (!pattern) {
1788
- process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|batch_outline|project_overview|shortest_path|cross_substrate_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|resource_reachability|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
2146
+ process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|file_metrics|batch_outline|project_overview|shortest_path|cross_substrate_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|resource_reachability|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
1789
2147
  process.exit(1);
1790
2148
  }
1791
2149
  const directionValue = selectorValue("--direction");
@@ -1794,6 +2152,16 @@ async function main() {
1794
2152
  process.exit(1);
1795
2153
  }
1796
2154
  const facetsValue = selectorValue("--facets")?.split(",").filter(Boolean);
2155
+ const testScopeValue = selectorValue("--test-scope");
2156
+ const queryFormat = selectorValue("--format");
2157
+ if (queryFormat && (pattern !== "file_metrics" || queryFormat !== "json")) {
2158
+ process.stderr.write("knodin query: --format is supported only as file_metrics --format json\n");
2159
+ process.exit(1);
2160
+ }
2161
+ if (testScopeValue && !["all", "test", "production"].includes(testScopeValue)) {
2162
+ process.stderr.write("knodin query: --test-scope must be all, test, or production\n");
2163
+ process.exit(1);
2164
+ }
1797
2165
  if (facetsValue?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet))) {
1798
2166
  process.stderr.write("knodin query: --facets contains an unknown architecture facet\n");
1799
2167
  process.exit(1);
@@ -1838,7 +2206,7 @@ async function main() {
1838
2206
  process.stderr.write("knodin query: --limit must be a positive integer\n");
1839
2207
  process.exit(1);
1840
2208
  }
1841
- const queryHealth = pattern.startsWith("lsp_")
2209
+ const queryHealth = pattern.startsWith("lsp_") || pattern === "file_metrics"
1842
2210
  ? null
1843
2211
  : await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1844
2212
  if (queryHealth && !queryHealth.available) {
@@ -1864,6 +2232,10 @@ async function main() {
1864
2232
  }
1865
2233
  : undefined, {
1866
2234
  minLines: selectorValue("--min-lines") ? Number(selectorValue("--min-lines")) : undefined,
2235
+ base: pattern === "file_metrics" ? selectorValue("--base") : undefined,
2236
+ testScope: pattern === "file_metrics"
2237
+ ? testScopeValue
2238
+ : undefined,
1867
2239
  minComplexity: selectorValue("--min-complexity")
1868
2240
  ? Number(selectorValue("--min-complexity"))
1869
2241
  : undefined,
@@ -1888,6 +2260,8 @@ async function main() {
1888
2260
  : selectorValue("--relations")?.split(",").filter(Boolean),
1889
2261
  detailLevel,
1890
2262
  });
2263
+ if (pattern === "file_metrics")
2264
+ result = result.fileMetrics;
1891
2265
  if (pattern === "impact" || pattern === "dead_code") {
1892
2266
  const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
1893
2267
  result = incorporateSystemQueryEvidence(config, repo, pattern, target, result);
@@ -1973,16 +2347,23 @@ async function main() {
1973
2347
  if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 3650)
1974
2348
  throw new Error("knodin telemetry: --retention-days must be an integer from 1 to 3650");
1975
2349
  const input = selectorValue("--input");
1976
- if (action === "status")
1977
- result = telemetryStatus(repo, input, retentionDays);
2350
+ if (action === "enable")
2351
+ result = enableSessionTelemetry(repo);
2352
+ else if (action === "disable")
2353
+ result = disableSessionTelemetry(repo);
2354
+ else if (action === "status")
2355
+ result = {
2356
+ ...telemetryStatus(repo, input, retentionDays),
2357
+ session: sessionTelemetryStatus(repo, retentionDays),
2358
+ };
1978
2359
  else if (action === "report")
1979
- result = writeTelemetryReport(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
2360
+ result = writeTelemetryReport(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"), readSessionEvents(repo, retentionDays));
1980
2361
  else if (action === "export")
1981
- result = exportTelemetry(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
2362
+ result = exportTelemetry(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"), readSessionEvents(repo, retentionDays));
1982
2363
  else if (action === "clear")
1983
- result = clearTelemetry(repo, input);
2364
+ result = { ...clearTelemetry(repo, input), session: clearSessionTelemetry(repo) };
1984
2365
  else
1985
- throw new Error("knodin telemetry requires status, report, export, or clear");
2366
+ throw new Error("knodin telemetry requires enable, disable, status, report, export, or clear");
1986
2367
  break;
1987
2368
  }
1988
2369
  case "diagnostics": {
@@ -2074,13 +2455,17 @@ async function main() {
2074
2455
  process.exit(1);
2075
2456
  }
2076
2457
  await engine.close();
2458
+ if (agentEventOutput) {
2459
+ if (result !== null && result !== undefined)
2460
+ process.stdout.write(`${JSON.stringify(result)}\n`);
2461
+ return;
2462
+ }
2077
2463
  if (statusWasWatched)
2078
2464
  return;
2079
- const boundedResult = applyResponseBudget(result, cmd, responseBudget, {
2080
- bytes: 65_536,
2081
- tokens: 16_384,
2082
- items: 100,
2083
- });
2465
+ const fileMetricsOutput = cmd === "query" && rest[0] === "file_metrics";
2466
+ const boundedResult = applyResponseBudget(result, cmd, responseBudget, fileMetricsOutput
2467
+ ? { bytes: 16_777_216, tokens: 4_194_304, items: 100_000 }
2468
+ : { bytes: 65_536, tokens: 16_384, items: 100 });
2084
2469
  const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
2085
2470
  if (cmd === "init" && !jsonOutput) {
2086
2471
  process.stdout.write(formatInitHuman(boundedResult));
@@ -2128,55 +2513,81 @@ try {
2128
2513
  }
2129
2514
  catch (err) {
2130
2515
  const argv = process.argv.slice(2);
2131
- const repoIndex = argv.indexOf("--repo");
2132
- const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
2133
- const explicitRepo = repoIndex >= 0 ? argv[repoIndex + 1] : undefined;
2134
- let candidate = process.cwd();
2135
- if (explicitRepo)
2136
- candidate = explicitRepo;
2137
- else if (equalsRepo)
2138
- candidate = equalsRepo.slice("--repo=".length);
2139
- const command = argv.find((argument, index) => {
2140
- if (argument.startsWith("-"))
2141
- return false;
2142
- return !(index > 0 && argv[index - 1] === "--repo") && argument !== candidate;
2143
- });
2144
- const knownCommands = new Set([
2145
- "init",
2146
- "configure",
2147
- "index",
2148
- "doctor",
2149
- "status",
2150
- "wait",
2151
- "repair",
2152
- "serve",
2153
- "context",
2154
- "explain",
2155
- "review",
2156
- "map",
2157
- "search",
2158
- "query",
2159
- "rename",
2160
- "wiki",
2161
- "visualize",
2162
- "pack",
2163
- "compress",
2164
- "prs",
2165
- "worktrees",
2166
- "telemetry",
2167
- "diagnostics",
2168
- "system",
2169
- "repos",
2170
- "remote",
2171
- "update",
2172
- ]);
2173
- const diagnostic = recordDiagnosticFailure(candidate, {
2174
- surface: "cli",
2175
- operation: command && knownCommands.has(command) ? command : "unknown",
2176
- phase: "dispatch",
2177
- error: err,
2178
- });
2179
- const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
2180
- console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
2181
- process.exit(1);
2516
+ let invokedCommand;
2517
+ for (let index = 0; index < argv.length; index += 1) {
2518
+ const argument = argv[index];
2519
+ if (argument === "--repo") {
2520
+ index += 1;
2521
+ continue;
2522
+ }
2523
+ if (argument?.startsWith("--repo=") || argument === "--json")
2524
+ continue;
2525
+ if (!argument?.startsWith("-")) {
2526
+ invokedCommand = argument;
2527
+ break;
2528
+ }
2529
+ }
2530
+ if (invokedCommand === "agent-event") {
2531
+ const eventIndex = argv.indexOf("agent-event");
2532
+ if (argv[eventIndex + 1] === "session-start") {
2533
+ process.stdout.write(`${JSON.stringify(sessionStartHookOutput(`## knodin session context\n${SESSION_CONTEXT_BUDGET_LINE}\nContext generation was unavailable. Run knodin status before relying on graph evidence.`))}\n`);
2534
+ }
2535
+ process.exitCode = 0;
2536
+ }
2537
+ else {
2538
+ const repoIndex = argv.indexOf("--repo");
2539
+ const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
2540
+ const explicitRepo = repoIndex >= 0 ? argv[repoIndex + 1] : undefined;
2541
+ let candidate = process.cwd();
2542
+ if (explicitRepo)
2543
+ candidate = explicitRepo;
2544
+ else if (equalsRepo)
2545
+ candidate = equalsRepo.slice("--repo=".length);
2546
+ const command = argv.find((argument, index) => {
2547
+ if (argument.startsWith("-"))
2548
+ return false;
2549
+ return !(index > 0 && argv[index - 1] === "--repo") && argument !== candidate;
2550
+ });
2551
+ const knownCommands = new Set([
2552
+ "init",
2553
+ "configure",
2554
+ "agent-hooks",
2555
+ "agent-event",
2556
+ "index",
2557
+ "doctor",
2558
+ "status",
2559
+ "wait",
2560
+ "repair",
2561
+ "serve",
2562
+ "context",
2563
+ "explain",
2564
+ "review",
2565
+ "map",
2566
+ "search",
2567
+ "query",
2568
+ "rename",
2569
+ "wiki",
2570
+ "visualize",
2571
+ "pack",
2572
+ "compress",
2573
+ "prs",
2574
+ "worktrees",
2575
+ "telemetry",
2576
+ "diagnostics",
2577
+ "system",
2578
+ "repos",
2579
+ "remote",
2580
+ "shared",
2581
+ "update",
2582
+ ]);
2583
+ const diagnostic = recordDiagnosticFailure(candidate, {
2584
+ surface: "cli",
2585
+ operation: command && knownCommands.has(command) ? command : "unknown",
2586
+ phase: "dispatch",
2587
+ error: err,
2588
+ });
2589
+ const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
2590
+ console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
2591
+ process.exit(1);
2592
+ }
2182
2593
  }