knodin 0.6.0 → 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 (48) hide show
  1. package/README.md +26 -3
  2. package/dist/bin/cli.js +168 -12
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +3 -1
  5. package/dist/src/cli-args.js +8 -1
  6. package/dist/src/cli-model.js +35 -1
  7. package/dist/src/codeflow-replay.js +80 -0
  8. package/dist/src/competitive-cold-mcp.js +40 -0
  9. package/dist/src/competitive-manifest.js +106 -25
  10. package/dist/src/competitive-runner.js +37 -3
  11. package/dist/src/competitive-sandbox.js +1 -1
  12. package/dist/src/diagnostics.js +449 -0
  13. package/dist/src/engine/git-history.js +289 -0
  14. package/dist/src/engine/index.js +417 -70
  15. package/dist/src/engine/scip-import.js +408 -0
  16. package/dist/src/execution-profile.js +203 -0
  17. package/dist/src/failure-diagnosis.js +69 -10
  18. package/dist/src/hook-manager-integration.js +156 -0
  19. package/dist/src/init.js +319 -33
  20. package/dist/src/lifecycle-health.js +42 -4
  21. package/dist/src/output-telemetry.js +4 -0
  22. package/dist/src/progressive-evidence.js +473 -0
  23. package/dist/src/pure-compression-cli.js +101 -0
  24. package/dist/src/release-preflight.js +510 -0
  25. package/dist/src/repository-management.js +142 -0
  26. package/dist/src/response-budget.js +11 -1
  27. package/dist/src/server.js +22 -2
  28. package/dist/src/structural-fast-path.js +303 -0
  29. package/dist/src/structural-snapshot.js +33 -0
  30. package/dist/src/tools/knodin-tools.js +105 -14
  31. package/dist/src/update-ceremony.js +158 -0
  32. package/docs/CLI.md +22 -0
  33. package/docs/COMMAND-OUTPUT-COMPRESSION.md +31 -15
  34. package/docs/CONTAINED-EXECUTION.md +77 -0
  35. package/docs/DIAGNOSTICS.md +45 -0
  36. package/docs/DOCTOR-AND-UPDATES.md +5 -2
  37. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  38. package/docs/MCP.md +15 -0
  39. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  40. package/docs/REPOSITORIES-AND-WORKTREES.md +30 -0
  41. package/docs/SCIP-IMPORT.md +57 -0
  42. package/docs/SIGNED-UPDATES.md +5 -0
  43. package/docs/TELEMETRY.md +4 -0
  44. package/docs/releases/0.7.0.md +24 -0
  45. package/docs/releases/0.7.1.md +21 -0
  46. package/docs/releases/0.7.2.md +21 -0
  47. package/docs/releases/0.7.3.md +23 -0
  48. package/package.json +33 -2
package/README.md CHANGED
@@ -147,9 +147,10 @@ knodin repair
147
147
  |---|---|
148
148
  | `knodin context` | Compact repository orientation and next-operation hint |
149
149
  | `knodin explain <symbol>` | Source, identity, callers, callees, and blast radius |
150
- | `knodin review` | Diff-aware risks, changed symbols, affected flows, and test gaps |
150
+ | `knodin review` | Itemized graph impact, test gaps, centrality, and bounded Git-history evidence |
151
151
  | `knodin query …` | Impact, paths, callers, callees, tests, flows, and structured graph queries |
152
- | `knodin search <query>` | Hybrid semantic and keyword symbol search |
152
+ | `knodin search <query>` | Explainable structural-first search with a bounded local embedding fallback |
153
+ | `knodin index --scip <file>` | Opt in to a bounded local SCIP snapshot while preserving native and LSIF facts |
153
154
  | `knodin map` | Subsystems, communities, hubs, bridges, and boundaries |
154
155
  | `knodin pack …` | Deterministic source context under hard budgets |
155
156
  | `knodin compress …` | Bounded diagnostic output with recoverable local detail |
@@ -158,6 +159,7 @@ knodin repair
158
159
  | `knodin visualize` | Self-contained local graph visualization |
159
160
  | `knodin status --deep` | Index freshness, health, hooks, and integration status |
160
161
  | `knodin repair` | Repair or rebuild unhealthy local graph state |
162
+ | `knodin diagnostics …` | Retain local failure evidence and create a redacted support bundle |
161
163
 
162
164
  Run `knodin --help` or read the [CLI reference](docs/CLI.md) for complete syntax.
163
165
 
@@ -186,7 +188,8 @@ repository, and lifecycle capabilities. See the [MCP guide](docs/MCP.md).
186
188
  Each checkout stores its graph and lifecycle state in `.knodin/`. Shared model
187
189
  files live in the user cache rather than being duplicated per repository.
188
190
  Source and graph data stay local. Telemetry is metadata-only, disabled by
189
- default, and never sent by knodin.
191
+ default, and never sent by knodin. Troubleshooting diagnostics are also
192
+ explicitly enabled, local-only, bounded, and never uploaded automatically.
190
193
 
191
194
  The optional `prs` command invokes the user's authenticated `gh` CLI. Network
192
195
  access may also occur when installing packages, downloading the configured
@@ -206,9 +209,29 @@ productivity percentage or superiority over every competing product.
206
209
  - [Competitive roadmap and limitations](roadmap/competitive-roadmap.md)
207
210
  - [Systems and relationships](docs/SYSTEMS-AND-RELATIONSHIPS.md)
208
211
  - [Command-output compression](docs/COMMAND-OUTPUT-COMPRESSION.md)
212
+ - [Local troubleshooting diagnostics](docs/DIAGNOSTICS.md)
209
213
 
210
214
  ## Develop
211
215
 
216
+ ### Internal GHES mirror
217
+
218
+ GitHub SaaS `knodin/knodin` is the authoritative review and release repository.
219
+ The internal `github.docusignhq.com/Enterprise-Apps/knodin` repository is a
220
+ read-only mirror for colleagues who need GHES access; work must not be opened
221
+ or reviewed there.
222
+
223
+ After a pull fast-forwards local `main`, Lefthook automatically fast-forwards
224
+ the mirror when this maintainer-local remote exists:
225
+
226
+ ```bash
227
+ git remote add ghes git@github.docusignhq.com:Enterprise-Apps/knodin.git
228
+ ```
229
+
230
+ The hook silently no-ops off `main`, on CI, or without that remote. It never
231
+ forces and never breaks the surrounding merge when GHES is unavailable. Run
232
+ `npm run mirror:sync` for manual recovery. A divergence is reported for
233
+ investigation instead of being overwritten.
234
+
212
235
  ```bash
213
236
  npm ci
214
237
  npm run build
package/dist/bin/cli.js CHANGED
@@ -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`;
@@ -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
  }
@@ -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`.",
@@ -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`);
@@ -81,7 +81,9 @@ export function configurePersonalAgents(options) {
81
81
  (options.remove ? removed : configured).push(agent);
82
82
  }
83
83
  catch (error) {
84
- // 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.
85
87
  if (options.remove)
86
88
  removed.push(agent);
87
89
  else
@@ -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
+ }