knodin 0.5.1 → 0.7.3

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 (70) hide show
  1. package/README.md +190 -535
  2. package/dist/bin/cli.js +173 -17
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +4 -16
  5. package/dist/src/artifact-refresh.js +1 -1
  6. package/dist/src/cli-args.js +8 -1
  7. package/dist/src/cli-model.js +35 -1
  8. package/dist/src/codeflow-replay.js +80 -0
  9. package/dist/src/competitive-cold-mcp.js +40 -0
  10. package/dist/src/competitive-manifest.js +106 -25
  11. package/dist/src/competitive-runner.js +37 -3
  12. package/dist/src/competitive-sandbox.js +4 -4
  13. package/dist/src/context-export.js +1 -1
  14. package/dist/src/diagnostics.js +449 -0
  15. package/dist/src/docs-sections.js +5 -5
  16. package/dist/src/engine/ann-hnsw.js +5 -5
  17. package/dist/src/engine/embeddings.js +4 -4
  18. package/dist/src/engine/git-history.js +289 -0
  19. package/dist/src/engine/index.js +450 -103
  20. package/dist/src/engine/prune.js +2 -2
  21. package/dist/src/engine/scip-import.js +408 -0
  22. package/dist/src/engine/symbol-delete.js +1 -1
  23. package/dist/src/execution-profile.js +203 -0
  24. package/dist/src/failure-diagnosis.js +69 -10
  25. package/dist/src/hook-manager-integration.js +156 -0
  26. package/dist/src/index-activity.js +1 -1
  27. package/dist/src/init-progress-worker.js +1 -1
  28. package/dist/src/init.js +361 -92
  29. package/dist/src/lifecycle-health.js +51 -14
  30. package/dist/src/lsp-readonly.js +1 -1
  31. package/dist/src/output-compression.js +1 -1
  32. package/dist/src/output-telemetry.js +8 -4
  33. package/dist/src/pr-triage.js +2 -2
  34. package/dist/src/progressive-evidence.js +473 -0
  35. package/dist/src/pure-compression-cli.js +101 -0
  36. package/dist/src/release-preflight.js +510 -0
  37. package/dist/src/repair-progress-worker.js +1 -1
  38. package/dist/src/repository-init-process.js +1 -1
  39. package/dist/src/repository-management.js +145 -3
  40. package/dist/src/response-budget.js +11 -1
  41. package/dist/src/server.js +23 -3
  42. package/dist/src/structural-fast-path.js +303 -0
  43. package/dist/src/structural-snapshot.js +33 -0
  44. package/dist/src/system-config.js +7 -7
  45. package/dist/src/tools/knodin-tools.js +106 -15
  46. package/dist/src/update-ceremony.js +158 -0
  47. package/dist/src/update-policy.js +4 -4
  48. package/dist/src/wait-for-fresh.js +2 -2
  49. package/dist/src/worktree-lifecycle.js +2 -2
  50. package/docs/CLI.md +22 -0
  51. package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
  52. package/docs/CONTAINED-EXECUTION.md +77 -0
  53. package/docs/DIAGNOSTICS.md +45 -0
  54. package/docs/DOCTOR-AND-UPDATES.md +7 -4
  55. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  56. package/docs/INSTALLATION.md +1 -1
  57. package/docs/MCP.md +15 -0
  58. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  59. package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
  60. package/docs/SCIP-IMPORT.md +57 -0
  61. package/docs/SIGNED-UPDATES.md +5 -0
  62. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
  63. package/docs/TELEMETRY.md +6 -2
  64. package/docs/releases/0.6.0.md +18 -0
  65. package/docs/releases/0.7.0.md +24 -0
  66. package/docs/releases/0.7.1.md +21 -0
  67. package/docs/releases/0.7.2.md +21 -0
  68. package/docs/releases/0.7.3.md +23 -0
  69. package/package.json +34 -3
  70. package/dist/src/tools/reckon-tools.js +0 -5
package/dist/bin/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * knodin explain <symbol> [minimal] edit-ready source + direct call paths + blast radius
6
6
  * knodin review [base] risk-scored context with explicit git diff scopes
7
7
  * knodin map subsystems + confidence-tagged edges
8
- * knodin wiki [--force] write .reckon/wiki/ (index.md + per-community pages)
8
+ * knodin wiki [--force] write .knodin/wiki/ (index.md + per-community pages)
9
9
  * knodin serve run the MCP server on stdio
10
10
  *
11
11
  * The same engine backs both the CLI and the MCP server, so any assistant and
@@ -22,6 +22,7 @@ import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, pla
22
22
  import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
23
23
  import { buildKnodinContext } from "../src/context.js";
24
24
  import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/context-export.js";
25
+ import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, recordDiagnosticFailure, } from "../src/diagnostics.js";
25
26
  import { getDocSection, listDocTopics } from "../src/docs-sections.js";
26
27
  import { diagnoseInstallation } from "../src/doctor.js";
27
28
  import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
@@ -31,13 +32,14 @@ import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-
31
32
  import { createIndexActivityReporter } from "../src/index-activity.js";
32
33
  import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
33
34
  import { createInitProgressRenderer } from "../src/init-progress.js";
34
- import { attachLifecycleHealth } from "../src/lifecycle-health.js";
35
+ import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
35
36
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
36
37
  import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
37
38
  import { auditPullRequests } from "../src/pr-triage.js";
39
+ import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
38
40
  import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
39
41
  import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
40
- import { discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, searchRepositories, } from "../src/repository-management.js";
42
+ import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
41
43
  import { applyResponseBudget } from "../src/response-budget.js";
42
44
  import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
43
45
  import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
@@ -96,7 +98,8 @@ async function chooseInitScope(args, repo) {
96
98
  }
97
99
  function integrationAgents(repo) {
98
100
  const previous = readRepositoryIntegrationConfig(repo)?.agents ?? [];
99
- return [...new Set([...detectSupportedAgents(), ...previous])];
101
+ const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
102
+ return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
100
103
  }
101
104
  function formatInitHuman(result) {
102
105
  const agents = result.paths.scope === "cli-only"
@@ -117,6 +120,8 @@ function formatConfigureStatusHuman(result) {
117
120
  return "Agent integration: unconfigured.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal`.\n";
118
121
  if (result.scope === "cli-only")
119
122
  return "Agent integration: CLI-only.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal` or `--scope team` to enable them.\n";
123
+ if (result.scope === "repository-detected")
124
+ return `Agent integration: repository-detected${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\nLocal scope receipt: missing; managed repository integration is present.\n`;
120
125
  return `Agent integration: ${result.scope}${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\n`;
121
126
  }
122
127
  function formatConfigureHuman(result) {
@@ -133,7 +138,13 @@ function formatConfigureHuman(result) {
133
138
  const refresh = result.paths.lifecycleRefresh.state === "fresh"
134
139
  ? "fresh"
135
140
  : `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s))`;
136
- return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
141
+ const filesystemMutationLines = result.paths.filesystemChanges
142
+ .map(({ path: changedPath, action }) => `\nFilesystem ${action}: ${changedPath}`)
143
+ .join("");
144
+ const externalOutcomeLines = result.paths.externalConfigurationOutcomes
145
+ .map(({ system, state }) => `\nExternal configuration outcome: ${system} ${state} (external mutation not locally observable)`)
146
+ .join("");
147
+ return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}${filesystemMutationLines}${externalOutcomeLines}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
137
148
  }
138
149
  function formatRepairHuman(result) {
139
150
  const coverage = result.after.coverage;
@@ -141,6 +152,8 @@ function formatRepairHuman(result) {
141
152
  return `Repair paused: ${result.remaining ?? 0} file(s) remaining. Run \`knodin repair\` again to finish.\n`;
142
153
  }
143
154
  if (result.verified) {
155
+ if (result.lifecycle?.status === "degraded")
156
+ return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols), but lifecycle routing is degraded. Run \`knodin init\`, then \`knodin status\`.\n`;
144
157
  return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols).\n`;
145
158
  }
146
159
  return `Repair finished with remaining issues. Run \`knodin status --deep\` for details.\n`;
@@ -313,7 +326,7 @@ function createProgressWorkerRenderer(workerName, startMessage) {
313
326
  : [workerPath];
314
327
  const worker = spawn(process.execPath, workerArgs, {
315
328
  stdio: ["pipe", "ignore", "inherit"],
316
- env: { ...process.env, RECKON_FORCE_PROGRESS_TTY: "1" },
329
+ env: { ...process.env, KNODIN_FORCE_PROGRESS_TTY: "1" },
317
330
  });
318
331
  let failed = false;
319
332
  const closed = new Promise((resolve) => {
@@ -547,11 +560,15 @@ async function main() {
547
560
  const engine = createEngine();
548
561
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
549
562
  const repositories = [];
550
- for (const record of discovery.repositories) {
551
- repositories.push(await inventoryRepository(record, {
563
+ const discoveryRecords = plan.signals
564
+ ? discovery.repositories.slice(0, repositorySignalInspectionLimit(plan))
565
+ : discovery.repositories;
566
+ for (const record of discoveryRecords) {
567
+ const inventory = await inventoryRepository(record, {
552
568
  status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
553
569
  systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
554
- }));
570
+ });
571
+ repositories.push(withRepositorySignals(inventory, plan.signals, plan.signals ? await detectRepositorySignals(record.path) : {}));
555
572
  }
556
573
  await engine.close();
557
574
  const output = {
@@ -561,7 +578,10 @@ async function main() {
561
578
  ...discovery,
562
579
  repositories,
563
580
  };
564
- process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
581
+ const boundedOutput = plan.signals
582
+ ? applyResponseBudget(output, "repositories:discover", { bytes: plan.byteBudget, tokens: plan.tokenBudget, items: plan.itemBudget }, { bytes: 65_536, tokens: 16_384, items: 100 })
583
+ : output;
584
+ process.stdout.write(`${JSON.stringify(boundedOutput, null, plan.json ? 0 : 2)}\n`);
565
585
  process.exitCode = discovery.issues.length > 0 ? 1 : 0;
566
586
  return;
567
587
  }
@@ -788,7 +808,7 @@ async function main() {
788
808
  output = await checkTrustedUpdate(options);
789
809
  }
790
810
  finally {
791
- if (process.env.RECKON_UPDATE_BACKGROUND === "1")
811
+ if (process.env.KNODIN_UPDATE_BACKGROUND === "1")
792
812
  releaseScheduledUpdateCheck();
793
813
  }
794
814
  }
@@ -812,7 +832,7 @@ async function main() {
812
832
  const background = spawn(executable, [...arguments_, "update", "check", "--json"], {
813
833
  detached: true,
814
834
  stdio: "ignore",
815
- env: { ...process.env, RECKON_UPDATE_BACKGROUND: "1" },
835
+ env: { ...process.env, KNODIN_UPDATE_BACKGROUND: "1" },
816
836
  });
817
837
  background.unref();
818
838
  }
@@ -856,6 +876,7 @@ async function main() {
856
876
  "--start",
857
877
  "--end",
858
878
  "--limit",
879
+ "--offset",
859
880
  ]);
860
881
  const positionals = rest.filter((argument, index) => !argument.startsWith("--") && !valueFlags.has(rest[index - 1]));
861
882
  const integer = (flag, fallback, minimum, maximum) => {
@@ -881,6 +902,7 @@ async function main() {
881
902
  compressionResult = await diagnoseFailure(diagnosisEngine, repo, {
882
903
  artifactId: artifactId ?? "",
883
904
  maxDiagnostics: integer("--limit", 10, 1, 50),
905
+ diagnosticOffset: integer("--offset", 0, 0, 1_000_000),
884
906
  contextLines: integer("--context", 2, 0, 10),
885
907
  contextByteBudget: integer("--max-output-bytes", 16_384, 256, 128 * 1024),
886
908
  });
@@ -945,6 +967,12 @@ async function main() {
945
967
  ? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
946
968
  : compressOutputFile(repo, input, request);
947
969
  }
970
+ if (action === "diagnose" &&
971
+ (responseBudget.bytes !== undefined ||
972
+ responseBudget.tokens !== undefined ||
973
+ responseBudget.items !== undefined)) {
974
+ compressionResult = applyResponseBudget(compressionResult, "compress:diagnose", responseBudget, { bytes: 65_536, tokens: 16_384, items: 50 });
975
+ }
948
976
  if (jsonOutput)
949
977
  process.stdout.write(`${JSON.stringify(compressionResult)}\n`);
950
978
  else if (action === "diagnose" && !diagnosisUnavailable)
@@ -1120,7 +1148,7 @@ async function main() {
1120
1148
  }
1121
1149
  case "configure": {
1122
1150
  if (rawRest.includes("--status")) {
1123
- result = readRepositoryIntegrationConfig(repo) ?? {
1151
+ result = inspectRepositoryIntegrationStatus(repo) ?? {
1124
1152
  scope: "unconfigured",
1125
1153
  agents: [],
1126
1154
  warning: "AI agents are not configured by knodin. Run `knodin configure --scope personal`.",
@@ -1130,7 +1158,7 @@ async function main() {
1130
1158
  const scope = explicitScope(rawRest);
1131
1159
  if (!scope)
1132
1160
  throw new Error("knodin configure: missing --scope");
1133
- if (!fs.existsSync(path.join(repo, ".reckon", "db.sqlite"))) {
1161
+ if (!fs.existsSync(path.join(repo, ".knodin", "db.sqlite"))) {
1134
1162
  throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
1135
1163
  }
1136
1164
  const agents = scope === "team" ? [] : integrationAgents(repo);
@@ -1140,6 +1168,7 @@ async function main() {
1140
1168
  scope,
1141
1169
  agents,
1142
1170
  allowTrackedTransition: true,
1171
+ auditConfigurationChanges: true,
1143
1172
  });
1144
1173
  result = {
1145
1174
  status: "success",
@@ -1162,6 +1191,7 @@ async function main() {
1162
1191
  process.exit(1);
1163
1192
  }
1164
1193
  const clean = rest.includes("--clean") || rest.includes("--force");
1194
+ const scipPath = selectorValue("--scip");
1165
1195
  const renderer = createInitRenderer("index");
1166
1196
  const activity = createIndexActivityReporter(plan.repo);
1167
1197
  activity.start();
@@ -1169,6 +1199,7 @@ async function main() {
1169
1199
  let indexResult;
1170
1200
  try {
1171
1201
  indexResult = await engine.index(plan.repo, plan.files, clean, {
1202
+ scip: scipPath ? { path: scipPath } : undefined,
1172
1203
  onProgress: (event) => {
1173
1204
  activity.update(event);
1174
1205
  renderer.onProgress(event);
@@ -1271,10 +1302,10 @@ async function main() {
1271
1302
  process.once("SIGINT", abortRepair);
1272
1303
  renderer.start();
1273
1304
  try {
1274
- result = await engine.repair(repo, {
1305
+ result = attachRepairLifecycle(repo, await engine.repair(repo, {
1275
1306
  signal: controller.signal,
1276
1307
  onProgress: (event) => renderer.onProgress(event),
1277
- });
1308
+ }));
1278
1309
  }
1279
1310
  finally {
1280
1311
  process.removeListener("SIGINT", abortRepair);
@@ -1436,6 +1467,28 @@ async function main() {
1436
1467
  }
1437
1468
  break;
1438
1469
  }
1470
+ case "evidence": {
1471
+ const level = rest[0];
1472
+ const file = rest[1];
1473
+ if (!file || !["locate", "outline", "evidence", "expand"].includes(level))
1474
+ throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");
1475
+ result = deliverProgressiveEvidence({
1476
+ repo,
1477
+ file,
1478
+ level,
1479
+ continuation: selectorValue("--continuation"),
1480
+ baselineHash: selectorValue("--baseline-hash"),
1481
+ baselineBytes: selectorValue("--baseline-bytes")
1482
+ ? Number(selectorValue("--baseline-bytes"))
1483
+ : undefined,
1484
+ startLine: selectorValue("--start") ? Number(selectorValue("--start")) : undefined,
1485
+ endLine: selectorValue("--end") ? Number(selectorValue("--end")) : undefined,
1486
+ byteLimit: responseBudget.bytes,
1487
+ tokenLimit: responseBudget.tokens,
1488
+ itemLimit: responseBudget.items,
1489
+ });
1490
+ break;
1491
+ }
1439
1492
  case "query": {
1440
1493
  const pattern = rest[0];
1441
1494
  const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
@@ -1634,6 +1687,61 @@ async function main() {
1634
1687
  throw new Error("knodin telemetry requires status, report, export, or clear");
1635
1688
  break;
1636
1689
  }
1690
+ case "diagnostics": {
1691
+ const [action, bundlePath] = invocation.positionals;
1692
+ const rawRetention = selectorValue("--retention-days");
1693
+ const sinceOption = selectorValue("--since");
1694
+ const outputOption = selectorValue("--output");
1695
+ const retentionDays = rawRetention === undefined ? 14 : Number(rawRetention);
1696
+ if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365)
1697
+ throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");
1698
+ if (bundlePath && action !== "inspect")
1699
+ throw new Error(`knodin diagnostics ${action}: unexpected bundle argument`);
1700
+ if (rawRetention !== undefined && action !== "enable")
1701
+ throw new Error(`knodin diagnostics ${action}: --retention-days applies only to enable`);
1702
+ if (sinceOption !== undefined && action !== "collect")
1703
+ throw new Error(`knodin diagnostics ${action}: --since applies only to collect`);
1704
+ if (outputOption !== undefined && action !== "collect")
1705
+ throw new Error(`knodin diagnostics ${action}: --output applies only to collect`);
1706
+ if (action === "enable")
1707
+ result = enableDiagnostics(repo, retentionDays);
1708
+ else if (action === "status")
1709
+ result = diagnosticsStatus(repo);
1710
+ else if (action === "disable")
1711
+ result = disableDiagnostics(repo);
1712
+ else if (action === "clear")
1713
+ result = clearDiagnostics(repo);
1714
+ else if (action === "inspect") {
1715
+ if (!bundlePath)
1716
+ throw new Error("knodin diagnostics inspect requires <bundle>");
1717
+ result = inspectDiagnosticsBundle(repo, bundlePath);
1718
+ }
1719
+ else if (action === "collect") {
1720
+ const since = sinceOption ?? "24h";
1721
+ const match = /^(\d+)(h|d)$/.exec(since);
1722
+ if (!match)
1723
+ throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");
1724
+ const sinceHours = Number(match[1]) * (match[2] === "d" ? 24 : 1);
1725
+ const graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
1726
+ const doctor = await diagnoseInstallation(repo, {
1727
+ currentVersion: KNODIN_VERSION,
1728
+ runtimeCommand: [...runtimeCommand, "serve"],
1729
+ graph,
1730
+ });
1731
+ result = collectDiagnostics(repo, {
1732
+ sinceHours,
1733
+ outputPath: outputOption,
1734
+ doctor,
1735
+ graph,
1736
+ telemetry: readTelemetryRecords(repo, undefined, Math.max(1, Math.ceil(sinceHours / 24))),
1737
+ knodinVersion: KNODIN_VERSION,
1738
+ });
1739
+ }
1740
+ else {
1741
+ throw new Error("knodin diagnostics requires enable, status, collect, inspect, clear, or disable");
1742
+ }
1743
+ break;
1744
+ }
1637
1745
  case "context": {
1638
1746
  // knodin context "<task>" [base]
1639
1747
  const task = rest[0];
@@ -1699,6 +1807,54 @@ async function main() {
1699
1807
  process.exitCode = finalExitCode;
1700
1808
  }
1701
1809
  main().catch((err) => {
1702
- console.error(err instanceof Error ? err.message : String(err));
1810
+ const argv = process.argv.slice(2);
1811
+ const repoIndex = argv.indexOf("--repo");
1812
+ const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
1813
+ const candidate = repoIndex >= 0 && argv[repoIndex + 1]
1814
+ ? argv[repoIndex + 1]
1815
+ : equalsRepo
1816
+ ? equalsRepo.slice("--repo=".length)
1817
+ : process.cwd();
1818
+ const command = argv.find((argument, index) => {
1819
+ if (argument.startsWith("-"))
1820
+ return false;
1821
+ return !(index > 0 && argv[index - 1] === "--repo") && argument !== candidate;
1822
+ });
1823
+ const knownCommands = new Set([
1824
+ "init",
1825
+ "configure",
1826
+ "index",
1827
+ "doctor",
1828
+ "status",
1829
+ "wait",
1830
+ "repair",
1831
+ "serve",
1832
+ "context",
1833
+ "explain",
1834
+ "review",
1835
+ "map",
1836
+ "search",
1837
+ "query",
1838
+ "rename",
1839
+ "wiki",
1840
+ "visualize",
1841
+ "pack",
1842
+ "compress",
1843
+ "prs",
1844
+ "worktrees",
1845
+ "telemetry",
1846
+ "diagnostics",
1847
+ "system",
1848
+ "repos",
1849
+ "update",
1850
+ ]);
1851
+ const diagnostic = recordDiagnosticFailure(candidate, {
1852
+ surface: "cli",
1853
+ operation: command && knownCommands.has(command) ? command : "unknown",
1854
+ phase: "dispatch",
1855
+ error: err,
1856
+ });
1857
+ const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
1858
+ console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
1703
1859
  process.exit(1);
1704
1860
  });
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { tryStructuralFastPath } from "../src/structural-fast-path.js";
3
+ const argv = process.argv.slice(2);
4
+ const fast = tryStructuralFastPath(argv);
5
+ if (!fast) {
6
+ const { tryPureCompressionFastPath } = await import("../src/pure-compression-cli.js");
7
+ if (!(await tryPureCompressionFastPath(argv)))
8
+ await import("./cli.js");
9
+ }
10
+ if (process.env.KNODIN_BENCH_PROCESS_UPTIME === "1")
11
+ process.stderr.write(`KNODIN_PROCESS_UPTIME_MS=${(process.uptime() * 1000).toFixed(3)}\n`);
@@ -7,11 +7,8 @@ export function parseInitScope(value) {
7
7
  throw new Error("knodin: --scope must be personal, team, or cli-only");
8
8
  }
9
9
  const MCP_NAME = "knodin";
10
- const LEGACY_MCP_NAME = "reckon-graph";
11
10
  const TOML_START = "# knodin:start";
12
11
  const TOML_END = "# knodin:end";
13
- const LEGACY_TOML_START = "# reckon-graph:start";
14
- const LEGACY_TOML_END = "# reckon-graph:end";
15
12
  const TOML_BLOCK = `${TOML_START}
16
13
  [mcp_servers."${MCP_NAME}"]
17
14
  command = "knodin"
@@ -70,16 +67,6 @@ export function configurePersonalAgents(options) {
70
67
  for (const agent of options.detected) {
71
68
  if (agent !== "claude")
72
69
  continue;
73
- const removeLegacy = {
74
- executable: "claude",
75
- args: ["mcp", "remove", LEGACY_MCP_NAME, "--scope", "local"],
76
- };
77
- try {
78
- run(removeLegacy);
79
- }
80
- catch {
81
- // A missing legacy registration is already the desired state.
82
- }
83
70
  const command = options.remove
84
71
  ? {
85
72
  executable: "claude",
@@ -94,7 +81,9 @@ export function configurePersonalAgents(options) {
94
81
  (options.remove ? removed : configured).push(agent);
95
82
  }
96
83
  catch (error) {
97
- // Removing a missing registration is already the desired state.
84
+ // Claude reports a missing local registration as a remove failure. Keep
85
+ // this backward-compatible result conservative at the presentation layer:
86
+ // it means removal was attempted, not that absence was verified.
98
87
  if (options.remove)
99
88
  removed.push(agent);
100
89
  else
@@ -143,7 +132,6 @@ function updateJsonMcp(filePath, remove) {
143
132
  const servers = typeof existing === "object" && existing !== null && !Array.isArray(existing)
144
133
  ? { ...existing }
145
134
  : {};
146
- delete servers[LEGACY_MCP_NAME];
147
135
  if (remove)
148
136
  delete servers[MCP_NAME];
149
137
  else
@@ -176,7 +164,7 @@ function updateTomlMcp(filePath, remove) {
176
164
  ? `${content.slice(0, start)}${content.slice(end + endMarker.length)}`.trim()
177
165
  : content.trim();
178
166
  }
179
- const without = removeManagedBlock(removeManagedBlock(existing, LEGACY_TOML_START, LEGACY_TOML_END), TOML_START, TOML_END);
167
+ const without = removeManagedBlock(existing, TOML_START, TOML_END);
180
168
  const next = remove ? without : [without, TOML_BLOCK].filter(Boolean).join("\n\n");
181
169
  if (!next) {
182
170
  fs.rmSync(filePath, { force: true });
@@ -75,7 +75,7 @@ export function refreshExternalGraphArtifacts(repoPath, event, runtime = default
75
75
  }
76
76
  /** Persist an audit record locally; no source content or tool output is retained. */
77
77
  export function writeArtifactRefreshRecord(repoPath, result) {
78
- const knodinDir = path.join(path.resolve(repoPath), ".reckon");
78
+ const knodinDir = path.join(path.resolve(repoPath), ".knodin");
79
79
  fs.mkdirSync(knodinDir, { recursive: true });
80
80
  fs.appendFileSync(path.join(knodinDir, "artifact-refresh.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), ...result })}\n`, "utf8");
81
81
  }
@@ -240,7 +240,14 @@ export function parseReviewArgs(args) {
240
240
  if (rawScope && !scopes.includes(rawScope)) {
241
241
  throw new Error(`invalid review scope: ${rawScope}`);
242
242
  }
243
- const optionValues = new Set([...GLOBAL_VALUE_FLAGS, "--scope", "--from", "--to", "--files"]);
243
+ const optionValues = new Set([
244
+ ...GLOBAL_VALUE_FLAGS,
245
+ "--scope",
246
+ "--from",
247
+ "--to",
248
+ "--files",
249
+ "--scip",
250
+ ]);
244
251
  const positionals = args.filter((arg, index) => {
245
252
  if (arg.startsWith("--"))
246
253
  return false;
@@ -66,6 +66,14 @@ function addRepositoryCommands(program, capture) {
66
66
  .option("--manifest <path>", "write or resume a portfolio manifest")
67
67
  .option("--dry-run", "report actions without changing repositories");
68
68
  }
69
+ if (action === "discover") {
70
+ command
71
+ .option("--signals", "include bounded repository applicability signals")
72
+ .addOption(option("--items <count>", "bound returned repositories and signal arrays", "integer"))
73
+ .addOption(option("--bytes <count>", "bound serialized response bytes", "integer"))
74
+ .addOption(option("--tokens <count>", "bound estimated response tokens", "integer"))
75
+ .addHelpText("after", "\n--signals returns hookManager, markerFiles, sanitized remotes, ciProviders, agentConfigs, and aidevTrackReferenced when known; every returned repository has signals, including {}. Inspection uses fixed documented marker/hook allowlists, reads only bounded hook configuration, and performs no network, credential, or marker-content reads. Linked worktrees inspect their own checkout when --linked-worktrees include is selected. See docs/REPOSITORIES-AND-WORKTREES.md for omissions and limits.\n");
76
+ }
69
77
  }
70
78
  leaf(repos, "search <query>", "search selected repositories sequentially", capture)
71
79
  .addOption(option("--root <path>", "portfolio root", "collect"))
@@ -129,6 +137,12 @@ function addGraphCommands(program, capture) {
129
137
  .option("--no-verify", "skip post-apply typecheck");
130
138
  }
131
139
  function addArtifactCommands(program, capture) {
140
+ leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
141
+ .option("--continuation <handle>", "resume an exact prior response")
142
+ .option("--baseline-hash <sha256>", "complete baseline SHA-256")
143
+ .addOption(option("--baseline-bytes <count>", "complete baseline UTF-8 bytes", "integer"))
144
+ .addOption(option("--start <line>", "first source line", "integer"))
145
+ .addOption(option("--end <line>", "last source line", "integer"));
132
146
  const pack = program.command("pack").description("export bounded portable context");
133
147
  pack
134
148
  .argument("[input]")
@@ -173,6 +187,7 @@ function addArtifactCommands(program, capture) {
173
187
  .addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
174
188
  .addOption(option("--context <count>", "diagnostic context lines", "integer"))
175
189
  .addOption(option("--limit <count>", "diagnostic result limit", "integer"))
190
+ .addOption(option("--offset <count>", "resume retained diagnostics at offset", "integer"))
176
191
  .option("--raw", "return unredacted retained bytes");
177
192
  }
178
193
  leaf(compress, "delete <artifact>", "delete a retained output artifact", capture);
@@ -196,7 +211,8 @@ function createCliProgram(capture = () => { }) {
196
211
  .option("--status", "inspect configuration without changing it");
197
212
  leaf(program, "index [files...]", "index a repository or selected files", capture)
198
213
  .option("--clean", "rebuild selected index state")
199
- .option("--force", "force clean indexing");
214
+ .option("--force", "force clean indexing")
215
+ .option("--scip <file>", "opt in to a bounded local SCIP protobuf import");
200
216
  leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
201
217
  leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
202
218
  .option("--deep", "run a full graph audit")
@@ -250,6 +266,24 @@ function createCliProgram(capture = () => { }) {
250
266
  .option("--output <path>", "dashboard or evidence-bundle output path")
251
267
  .addOption(option("--retention-days <count>", "retention window in days", "integer"))
252
268
  .action((...values) => capture(values.at(-1)));
269
+ const diagnostics = program
270
+ .command("diagnostics")
271
+ .description("manage local troubleshooting diagnostics and support bundles")
272
+ .allowExcessArguments(false)
273
+ .addArgument(new Argument("<action>").choices([
274
+ "enable",
275
+ "status",
276
+ "collect",
277
+ "inspect",
278
+ "clear",
279
+ "disable",
280
+ ]))
281
+ .addArgument(new Argument("[bundle]"));
282
+ diagnostics
283
+ .addOption(option("--retention-days <count>", "local event retention in days", "integer"))
284
+ .option("--since <duration>", "collection window, such as 24h or 7d")
285
+ .option("--output <path>", "repository-contained .json.gz bundle path")
286
+ .action((...values) => capture(values.at(-1)));
253
287
  return program;
254
288
  }
255
289
  function flattenPositionals(values) {
@@ -0,0 +1,80 @@
1
+ export const EXPECTED_RELATIONSHIPS = [
2
+ "packages/api/route.ts->packages/auth/service.ts",
3
+ "packages/api/route.ts->packages/audit/store.ts",
4
+ "packages/auth/service.ts->packages/data/users.ts",
5
+ ];
6
+ export function normalizeCodeFlowRelationships(connections) {
7
+ return connections.map((value) => {
8
+ const edge = value;
9
+ return {
10
+ // CodeFlow exports callee/definition as source and caller as target.
11
+ from: String(edge.target ?? ""),
12
+ to: String(edge.source ?? ""),
13
+ kind: typeof edge.kind === "string" ? edge.kind : null,
14
+ extractor: typeof edge.extractor === "string" ? edge.extractor : null,
15
+ confidence: edge.confidence === "exact" || edge.confidence === "heuristic" ? edge.confidence : null,
16
+ sourceFile: typeof edge.sourceFile === "string" ? edge.sourceFile : null,
17
+ sourceLine: typeof edge.sourceLine === "number" ? edge.sourceLine : null,
18
+ evidence: typeof edge.evidence === "string" ? edge.evidence : null,
19
+ raw: value,
20
+ };
21
+ });
22
+ }
23
+ export function scoreRelationships(relationships) {
24
+ const actual = new Set(relationships.map((edge) => `${edge.from}->${edge.to}`));
25
+ const expected = new Set(EXPECTED_RELATIONSHIPS);
26
+ const truePositives = [...expected].filter((edge) => actual.has(edge));
27
+ const falseNegatives = [...expected].filter((edge) => !actual.has(edge));
28
+ const falsePositives = [...actual].filter((edge) => !expected.has(edge));
29
+ const malformed = relationships.filter((edge) => !edge.from || !edge.to);
30
+ return {
31
+ truePositives,
32
+ falsePositives,
33
+ falseNegatives,
34
+ precision: actual.size === 0 ? 0 : truePositives.length / actual.size,
35
+ recall: truePositives.length / expected.size,
36
+ unsupportedEdgeRate: falseNegatives.length / expected.size,
37
+ malformedRelationships: malformed.length,
38
+ };
39
+ }
40
+ export function blastRadiusCompleteness(relationships) {
41
+ const origin = "packages/data/users.ts";
42
+ const expected = new Set(["packages/auth/service.ts", "packages/api/route.ts"]);
43
+ const adjacency = new Map();
44
+ for (const edge of relationships) {
45
+ if (!adjacency.has(edge.to))
46
+ adjacency.set(edge.to, new Set());
47
+ adjacency.get(edge.to)?.add(edge.from);
48
+ }
49
+ const found = new Set();
50
+ const queue = [origin];
51
+ while (queue.length) {
52
+ const current = queue.shift();
53
+ for (const dependent of adjacency.get(current) ?? []) {
54
+ if (dependent === origin || found.has(dependent))
55
+ continue;
56
+ found.add(dependent);
57
+ queue.push(dependent);
58
+ }
59
+ }
60
+ return {
61
+ origin,
62
+ expected: [...expected],
63
+ found: [...found].sort(),
64
+ missing: [...expected].filter((file) => !found.has(file)),
65
+ completeness: [...expected].filter((file) => found.has(file)).length / expected.size,
66
+ };
67
+ }
68
+ export function provenanceCoverage(relationships) {
69
+ const count = relationships.length;
70
+ const rate = (predicate) => count === 0 ? 0 : relationships.filter(predicate).length / count;
71
+ return {
72
+ relationships: count,
73
+ relationKind: rate((edge) => edge.kind !== null),
74
+ extractorIdentity: rate((edge) => edge.extractor !== null),
75
+ exactOrHeuristicConfidence: rate((edge) => edge.confidence !== null),
76
+ sourceFile: rate((edge) => edge.sourceFile !== null),
77
+ sourceLine: rate((edge) => edge.sourceLine !== null),
78
+ boundedSourceEvidence: rate((edge) => edge.evidence !== null),
79
+ };
80
+ }
@@ -0,0 +1,40 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { sampleOperation } from "./competitive-measurement.js";
5
+ function textOf(value) {
6
+ return (value.content ?? [])
7
+ .filter((item) => item.type === "text")
8
+ .map((item) => item.text ?? "")
9
+ .join("\n");
10
+ }
11
+ /** Measure five cold calls, rebuilding the MCP process before every recorded call. */
12
+ export async function measureColdMcp(command, args, cwd, side, clientName) {
13
+ let last;
14
+ let error;
15
+ const { measurement } = await sampleOperation(async () => {
16
+ const transport = new StdioClientTransport({ command, args, cwd, stderr: "pipe" });
17
+ const client = new Client({ name: clientName, version: "1" }, { capabilities: {} });
18
+ try {
19
+ await client.connect(transport);
20
+ last = await client.callTool({ name: side.tool, arguments: side.args });
21
+ error = undefined;
22
+ }
23
+ catch (cause) {
24
+ error = cause instanceof Error ? cause.message : String(cause);
25
+ }
26
+ finally {
27
+ await transport.close().catch(() => undefined);
28
+ }
29
+ return last;
30
+ }, { mode: "cold" });
31
+ const response = last ? textOf(last) : "";
32
+ return {
33
+ ok: !error && !last?.isError,
34
+ error,
35
+ ...measurement,
36
+ responseBytes: Buffer.byteLength(response),
37
+ responseSha256: createHash("sha256").update(response).digest("hex"),
38
+ response,
39
+ };
40
+ }