knodin 0.7.5 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +18 -3
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +371 -66
  4. package/dist/bin/launcher.js +16 -1
  5. package/dist/src/agent-integration.js +82 -16
  6. package/dist/src/artifact-refresh.js +2 -1
  7. package/dist/src/cli-args.js +19 -1
  8. package/dist/src/cli-model.js +28 -2
  9. package/dist/src/codeflow-replay.js +2 -1
  10. package/dist/src/compare.js +39 -0
  11. package/dist/src/competitive-constraints.js +2 -1
  12. package/dist/src/competitive-runner.js +4 -4
  13. package/dist/src/context-export.js +3 -2
  14. package/dist/src/context.js +1 -1
  15. package/dist/src/deterministic-random.js +34 -0
  16. package/dist/src/diagnostics-write-helper.js +473 -0
  17. package/dist/src/diagnostics.js +1160 -133
  18. package/dist/src/doctor.js +3 -1
  19. package/dist/src/engine/ann-hnsw.js +2 -12
  20. package/dist/src/engine/file-walker.js +8 -2
  21. package/dist/src/engine/git-history.js +12 -12
  22. package/dist/src/engine/index.js +1174 -313
  23. package/dist/src/engine/sarif-import.js +341 -0
  24. package/dist/src/engine/scip-import.js +28 -13
  25. package/dist/src/engine/source-policy.js +16 -0
  26. package/dist/src/engine/state-paths.js +175 -0
  27. package/dist/src/execution-profile.js +15 -10
  28. package/dist/src/failure-diagnosis.js +7 -1
  29. package/dist/src/graph-layout.js +173 -0
  30. package/dist/src/index-activity.js +2 -1
  31. package/dist/src/init.js +86 -45
  32. package/dist/src/lifecycle-health.js +41 -9
  33. package/dist/src/mcp-graph-worker.js +69 -0
  34. package/dist/src/mcp-reliability.js +154 -0
  35. package/dist/src/mcp-worker-supervisor.js +350 -0
  36. package/dist/src/mirror.js +290 -0
  37. package/dist/src/node-runtime.js +157 -0
  38. package/dist/src/output-compression.js +2 -1
  39. package/dist/src/output-telemetry.js +16 -11
  40. package/dist/src/progressive-evidence.js +30 -26
  41. package/dist/src/pure-compression-cli.js +4 -3
  42. package/dist/src/relationship-adapters.js +15 -8
  43. package/dist/src/release-preflight.js +13 -10
  44. package/dist/src/repair-lease.js +85 -0
  45. package/dist/src/repository-init-process.js +13 -9
  46. package/dist/src/repository-management.js +34 -4
  47. package/dist/src/response-budget.js +8 -6
  48. package/dist/src/server.js +80 -35
  49. package/dist/src/structural-fast-path.js +16 -10
  50. package/dist/src/structural-snapshot.js +6 -2
  51. package/dist/src/system-config.js +25 -2
  52. package/dist/src/tools/knodin-tools.js +142 -31
  53. package/dist/src/update-ceremony.js +9 -5
  54. package/dist/src/update-trust.js +5 -4
  55. package/dist/src/visualization.js +372 -19
  56. package/dist/src/worktree-lifecycle.js +5 -2
  57. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  58. package/docs/CLI.md +20 -1
  59. package/docs/COMPARISON.md +403 -0
  60. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  61. package/docs/DIAGNOSTICS.md +46 -11
  62. package/docs/HANDOFF.md +180 -0
  63. package/docs/INSTALLATION.md +21 -2
  64. package/docs/MCP.md +59 -8
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
  66. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  67. package/docs/SCIP-IMPORT.md +5 -0
  68. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  69. package/docs/releases/0.5.1.md +4 -4
  70. package/docs/releases/0.8.0.md +74 -0
  71. package/docs/releases/0.8.2.md +34 -0
  72. package/package.json +17 -4
  73. package/roadmap/competitive-roadmap.md +3801 -0
  74. package/schemas/release-attestation-v1.schema.json +1 -1
  75. package/schemas/support-bundle-v2.schema.json +212 -0
@@ -12,6 +12,7 @@ import { resolveCliRuntimeCommand } from "../cli-args.js";
12
12
  import { compactExplainResult, compactQueryResult, compactSearchResult, expandCompactIdentity, } from "../compact-structural.js";
13
13
  import { buildKnodinContext } from "../context.js";
14
14
  import { exportContext, grepPackedArtifact, readPackedArtifact } from "../context-export.js";
15
+ import { collectDiagnostics, persistDiagnosticsPreview } from "../diagnostics.js";
15
16
  import { getDocSection, listDocTopics } from "../docs-sections.js";
16
17
  import { diagnoseInstallation } from "../doctor.js";
17
18
  import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
@@ -21,6 +22,7 @@ import { diagnoseFailure, } from "../failure-diagnosis.js";
21
22
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
22
23
  import { inspectRepositoryIntegrationStatus } from "../init.js";
23
24
  import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
25
+ import { listMirrors } from "../mirror.js";
24
26
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
25
27
  import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
26
28
  import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
@@ -29,7 +31,7 @@ import { createRepairPlan } from "../repair-progress.js";
29
31
  import { runRepositoryInitializationProcess } from "../repository-init-process.js";
30
32
  import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
31
33
  import { applyResponseBudget } from "../response-budget.js";
32
- import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
34
+ import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
33
35
  import { trustedUpdateStatus } from "../update-policy.js";
34
36
  import { waitForFresh } from "../wait-for-fresh.js";
35
37
  import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../worktree-lifecycle.js";
@@ -177,10 +179,12 @@ function buildDocumentedKnodinTools() {
177
179
  "worktrees",
178
180
  "repair",
179
181
  "repositories",
182
+ "remote",
180
183
  "system",
181
184
  "telemetry",
185
+ "diagnostics",
182
186
  ],
183
- description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
187
+ description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP. remote: list read-only mirrors of repositories that are reachable but not checked out locally, so you can run explain/query/map/search against one by passing its `path` as `repoPath`. Each mirror is a snapshot pinned at a commit; the remote is not watched. Acquiring, refreshing, and removing mirrors is CLI-only (`knodin remote add|refresh|remove`) because it clones another repository's source onto this machine and consumes disk nothing reclaims automatically — the user's decision to make, not an agent's.",
184
188
  },
185
189
  profile: {
186
190
  type: "string",
@@ -442,7 +446,7 @@ function buildDocumentedKnodinTools() {
442
446
  telemetryAction: {
443
447
  type: "string",
444
448
  enum: ["records", "status", "report", "export", "clear"],
445
- description: "telemetry action.",
449
+ description: "telemetry action; diagnostics uses report (preview) or export (archive).",
446
450
  },
447
451
  telemetryRetentionDays: {
448
452
  type: "integer",
@@ -631,17 +635,23 @@ function buildDocumentedKnodinTools() {
631
635
  statusAudit: {
632
636
  type: "string",
633
637
  enum: ["cached", "deep"],
634
- description: "status only: cached (default) reuses a warm deep audit only after a bounded freshness probe; deep forces a complete filesystem/schema/orphan audit.",
638
+ description: "status: cached (default) or complete deep audit.",
635
639
  },
636
640
  timeoutMs: {
637
- type: "number",
641
+ type: "integer",
638
642
  minimum: 0,
639
- maximum: 300000,
643
+ maximum: 300_000,
640
644
  description: "wait: maximum milliseconds to wait for freshness.",
641
645
  },
646
+ sinceHours: {
647
+ type: "integer",
648
+ minimum: 1,
649
+ maximum: 8_760,
650
+ description: "diagnostics: history window in hours (default 24).",
651
+ },
642
652
  client: {
643
653
  type: "string",
644
- enum: ["claude", "codex", "gemini", "antigravity"],
654
+ enum: ["claude", "codex", "gemini", "copilot", "antigravity"],
645
655
  description: "doctor: optionally return one client-specific integration diagnosis.",
646
656
  },
647
657
  repairPlan: {
@@ -662,26 +672,32 @@ function buildDocumentedKnodinTools() {
662
672
  export function getDocumentedKnodinToolsForSchemaProof() {
663
673
  return buildDocumentedKnodinTools();
664
674
  }
665
- const COMPACT_TOOL_DESCRIPTION = "Local code intelligence.";
666
675
  const COMPACT_PARAMETER_DESCRIPTIONS = {
667
676
  operation: "Capability.",
668
677
  symbol: "Target.",
669
678
  pattern: "Query pattern.",
670
- impactMode: "Symbol or file impact.",
671
- apply: "Apply verified edits.",
672
- diffScope: "Review diff scope.",
673
- section: "Documentation section.",
674
- persistTelemetry: "Persist metadata telemetry.",
679
+ impactMode: "Symbol or file.",
680
+ apply: "Apply edits.",
681
+ // Shortened from "Review diff scope." / "Documentation section." on main: the
682
+ // `remote` operation added to the enum costs tokens, and this surface has a
683
+ // hard ceiling enforced by tool-schema-budget.spec.ts.
684
+ diffScope: "Diff scope.",
685
+ section: "Docs section.",
686
+ persistTelemetry: "Persist telemetry.",
675
687
  };
676
- function compactParameterDescription(name, description) {
677
- void description;
688
+ /**
689
+ * The documented description is deliberately discarded: the compact surface
690
+ * carries only the curated one-liners, and falling back to full prose would
691
+ * reintroduce the per-tool context cost this surface exists to avoid.
692
+ */
693
+ function compactParameterDescription(name, _description) {
678
694
  return COMPACT_PARAMETER_DESCRIPTIONS[name];
679
695
  }
680
696
  /** The compact, always-on MCP surface. Full prose remains available via docs. */
681
697
  export function getKnodinTools() {
682
698
  const tools = buildDocumentedKnodinTools();
683
699
  for (const tool of tools) {
684
- tool.description = COMPACT_TOOL_DESCRIPTION;
700
+ tool.description = undefined;
685
701
  const properties = tool.inputSchema.properties;
686
702
  for (const [name, property] of Object.entries(properties)) {
687
703
  property.description = compactParameterDescription(name, property.description);
@@ -703,6 +719,8 @@ const RESPONSE_DEFAULTS = {
703
719
  "explain:minimal": { bytes: 16_384, tokens: 4_096, items: 25 },
704
720
  map: { bytes: 131_072, tokens: 32_768, items: 200 },
705
721
  "map:minimal": { bytes: 32_768, tokens: 8_192, items: 15 },
722
+ "diagnostics:preview": { bytes: 524_288, tokens: 131_072, items: 1_000 },
723
+ "diagnostics:archive": { bytes: 65_536, tokens: 16_384, items: 100 },
706
724
  default: { bytes: 65_536, tokens: 16_384, items: 100 },
707
725
  };
708
726
  async function populateRepositoryDiagnoses(records, repositories, configRoot) {
@@ -757,6 +775,7 @@ async function handleRepositoriesOperation(options) {
757
775
  if (repositoryAction === "init") {
758
776
  const command = gatewayCliCommand();
759
777
  const systemConfig = loadSystemConfiguration(discoveryRoots[0] ?? repo);
778
+ const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
760
779
  const summary = await initializeRepositories(discoveryRoots, {
761
780
  command,
762
781
  index: (target) => engine.index(target),
@@ -766,7 +785,7 @@ async function handleRepositoriesOperation(options) {
766
785
  dryRun,
767
786
  manifestPath,
768
787
  indexMode: (target) => indexModeForPath(systemConfig, target),
769
- isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command }),
788
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command, memoryLimitBytes }),
770
789
  });
771
790
  const { repositories } = await inventory();
772
791
  return bounded({ schemaVersion: 1, ...summary, repositories }, "repositories:init");
@@ -898,8 +917,19 @@ function validateKnodinArgs(args) {
898
917
  if (args.timeoutMs !== undefined &&
899
918
  (!Number.isInteger(args.timeoutMs) || args.timeoutMs < 0 || args.timeoutMs > 300_000))
900
919
  throw new Error("knodin: timeoutMs must be an integer between 0 and 300000");
920
+ if (args.operation === "diagnostics" &&
921
+ args.telemetryAction !== undefined &&
922
+ !["report", "export"].includes(args.telemetryAction))
923
+ throw new Error("knodin: diagnostics telemetryAction must be report or export");
924
+ if (args.sinceHours !== undefined &&
925
+ (!Number.isInteger(args.sinceHours) || args.sinceHours < 1 || args.sinceHours > 8760))
926
+ throw new Error("knodin: sinceHours must be an integer from 1 through 8760");
927
+ if (args.operation === "diagnostics" &&
928
+ args.artifactId !== undefined &&
929
+ !/^[a-f0-9]{32}$/.test(args.artifactId))
930
+ throw new Error("knodin: diagnostics artifactId must be a preview ID");
901
931
  if (args.client !== undefined &&
902
- !["claude", "codex", "gemini", "antigravity"].includes(args.client))
932
+ !["claude", "codex", "gemini", "copilot", "antigravity"].includes(args.client))
903
933
  throw new Error("knodin: invalid client");
904
934
  }
905
935
  function compactExplainSource(result) {
@@ -1209,7 +1239,53 @@ async function tryCompactStructural(args, repo, selector, bounded) {
1209
1239
  (await runCompactQuery(compactPattern, symbol ?? "", repo, Math.min(args.limit ?? args.itemBudget ?? 100, args.itemBudget ?? 1000), args.depth, selector, byteBudget)),
1210
1240
  };
1211
1241
  }
1212
- export async function handleKnodinTool(args) {
1242
+ const mcpRequestSignals = new WeakMap();
1243
+ export function attachMcpRequestSignal(args, signal) {
1244
+ if (args && typeof args === "object")
1245
+ mcpRequestSignals.set(args, signal);
1246
+ return args;
1247
+ }
1248
+ async function repairLifecycleForRequest(repo, args) {
1249
+ const signal = args && typeof args === "object" ? mcpRequestSignals.get(args) : undefined;
1250
+ return attachRepairLifecycle(repo, await engine.repair(repo, { signal }));
1251
+ }
1252
+ async function handleDiagnosticsOperation(repo, args, bounded) {
1253
+ let graph;
1254
+ let doctor;
1255
+ try {
1256
+ graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
1257
+ }
1258
+ catch {
1259
+ // A support bundle must remain available when graph diagnostics fail.
1260
+ }
1261
+ if (graph) {
1262
+ try {
1263
+ doctor = await diagnoseInstallation(repo, {
1264
+ currentVersion: graph.version,
1265
+ runtimeCommand: [process.execPath, process.argv[1] ?? "", "serve"],
1266
+ graph,
1267
+ client: args.client,
1268
+ });
1269
+ }
1270
+ catch {
1271
+ // The allowlisted health section reports installation as unavailable.
1272
+ }
1273
+ }
1274
+ const options = {
1275
+ sinceHours: args.sinceHours ?? 24,
1276
+ doctor,
1277
+ graph,
1278
+ knodinVersion: graph?.version,
1279
+ };
1280
+ return bounded(args.telemetryAction === "export"
1281
+ ? collectDiagnostics(repo, {
1282
+ ...options,
1283
+ outputPath: args.outputPath,
1284
+ previewId: args.artifactId,
1285
+ })
1286
+ : persistDiagnosticsPreview(repo, options), `diagnostics:${args.telemetryAction === "export" ? "archive" : "preview"}`);
1287
+ }
1288
+ async function dispatchKnodinTool(args) {
1213
1289
  const startedAt = performance.now();
1214
1290
  const parsedArgs = (args ?? {});
1215
1291
  const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
@@ -1230,9 +1306,8 @@ export async function handleKnodinTool(args) {
1230
1306
  const observe = (output, op, minimal = false) => {
1231
1307
  const root = output;
1232
1308
  const budgetMeta = root.responseBudget;
1233
- if (op === "review") {
1309
+ if (op === "review")
1234
1310
  measurePerfPhaseSync("review_serialization", () => JSON.stringify(output));
1235
- }
1236
1311
  const telemetry = measureOutput({
1237
1312
  repo,
1238
1313
  operation: op,
@@ -1243,18 +1318,16 @@ export async function handleKnodinTool(args) {
1243
1318
  root.telemetry?.truncated === true,
1244
1319
  schemaTokens: gatewaySchemaTokens(),
1245
1320
  });
1246
- if (op === "execute" && parsedArgs.profile) {
1321
+ if (op === "execute" && parsedArgs.profile)
1247
1322
  telemetry.executionProfile = parsedArgs.profile;
1248
- }
1249
1323
  telemetry.temperature = localTelemetry.some(({ operation, repositoryId }) => operation === op && repositoryId === telemetry.repositoryId)
1250
1324
  ? "warm"
1251
1325
  : "cold";
1252
1326
  localTelemetry.push(telemetry);
1253
1327
  if (localTelemetry.length > 1000)
1254
1328
  localTelemetry.shift();
1255
- if (persistTelemetry === true) {
1329
+ if (persistTelemetry === true)
1256
1330
  appendTelemetryRecord(repo, telemetry, telemetryRetentionDays);
1257
- }
1258
1331
  return root;
1259
1332
  };
1260
1333
  const bounded = (result, op = String(operation), minimal = false) => {
@@ -1279,6 +1352,8 @@ export async function handleKnodinTool(args) {
1279
1352
  if (detailLevel === "compact")
1280
1353
  throw new Error("knodin: compact detail is unsupported here");
1281
1354
  switch (operation) {
1355
+ case "diagnostics":
1356
+ return handleDiagnosticsOperation(repo, parsedArgs, bounded);
1282
1357
  case "telemetry": {
1283
1358
  const action = telemetryAction ?? "records";
1284
1359
  if (action === "records")
@@ -1324,7 +1399,29 @@ export async function handleKnodinTool(args) {
1324
1399
  case "repair":
1325
1400
  if (repairPlan)
1326
1401
  return bounded(createRepairPlan(await engine.status(repo, { audit: "deep" })), "repair:plan");
1327
- return bounded(attachRepairLifecycle(repo, await engine.repair(repo)), "repair");
1402
+ return bounded(await repairLifecycleForRequest(repo, args), "repair");
1403
+ case "remote": {
1404
+ // Read-only over MCP by design. Acquiring a mirror clones another
1405
+ // repository's source onto this machine and consumes disk that nothing
1406
+ // reclaims automatically; refresh discards local state and remove
1407
+ // deletes a graph that cost minutes to build. Those are the user's
1408
+ // calls, made explicitly at the CLI, not side effects of an agent's
1409
+ // investigation. Listing is safe and is what makes a mirror usable:
1410
+ // pass a listed `path` as `repoPath` to explain/query/map/search it.
1411
+ const mirrors = listMirrors().map((mirror) => ({
1412
+ identity: mirror.identity,
1413
+ url: mirror.url,
1414
+ path: mirror.path,
1415
+ snapshot: mirror.sha,
1416
+ fetchedAt: mirror.fetchedAt,
1417
+ readOnly: true,
1418
+ }));
1419
+ return bounded({
1420
+ mirrors,
1421
+ acquisition: "CLI only: `knodin remote add <url>` / `refresh <identity>` / `remove <identity>`.",
1422
+ note: "Each mirror is a snapshot pinned at `snapshot`; the remote is not watched, so results describe that commit and no later one.",
1423
+ }, "remote:list");
1424
+ }
1328
1425
  case "repositories": {
1329
1426
  if (!repositoryAction)
1330
1427
  throw new Error("knodin repositories requires `repositoryAction`");
@@ -1517,13 +1614,27 @@ export async function handleKnodinTool(args) {
1517
1614
  const result = pattern === "impact" || pattern === "dead_code"
1518
1615
  ? incorporateSystemQueryEvidence(await enrichSystemRelationships(loadSystemConfiguration(repo)), repo, pattern, symbol ?? "", queryResult)
1519
1616
  : queryResult;
1520
- return bounded(queryHealth?.available
1521
- ? decorateGraphQueryResult(result, verifiedQueryHealth?.available ? verifiedQueryHealth.state : queryHealth.state, verifiedQueryHealth?.available
1522
- ? verifiedQueryHealth.graph.freshness
1523
- : queryHealth.graph.freshness)
1524
- : result, `query:${pattern}`);
1617
+ let decorated = result;
1618
+ if (queryHealth?.available) {
1619
+ // A verified probe supersedes the unverified one for both fields, so
1620
+ // state and freshness always come from the same observation.
1621
+ const authoritative = verifiedQueryHealth?.available ? verifiedQueryHealth : queryHealth;
1622
+ decorated = decorateGraphQueryResult(result, authoritative.state, authoritative.graph.freshness);
1623
+ }
1624
+ return bounded(decorated, `query:${pattern}`);
1525
1625
  }
1526
1626
  default:
1527
1627
  throw new Error(`unknown knodin operation: ${String(operation)}`);
1528
1628
  }
1529
1629
  }
1630
+ let responseTestMutator;
1631
+ /** @internal Test-only response seam. Production callers must never register a mutator. */
1632
+ export function setKnodinResponseTestMutator(mutator) {
1633
+ if (process.env.KNODIN_C95_TEST_MODE !== "offline-contract-replay")
1634
+ throw new Error("knodin: C95 response mutation seam is unavailable outside its test replay");
1635
+ responseTestMutator = mutator;
1636
+ }
1637
+ export async function handleKnodinTool(args) {
1638
+ const response = await dispatchKnodinTool(args);
1639
+ return responseTestMutator ? responseTestMutator(args, response) : response;
1640
+ }
@@ -1,10 +1,11 @@
1
1
  import { createHash, createPublicKey } from "node:crypto";
2
+ import { compareBytes } from "./compare.js";
2
3
  import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
3
4
  const SHA256 = /^[0-9a-f]{64}$/;
4
5
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$/;
5
6
  function exactKeys(value, expected, label) {
6
- const actual = Object.keys(value).sort();
7
- const wanted = [...expected].sort();
7
+ const actual = Object.keys(value).sort(compareBytes);
8
+ const wanted = [...expected].sort(compareBytes);
8
9
  if (actual.join("\0") !== wanted.join("\0"))
9
10
  throw new Error(`${label} contains missing or unknown fields`);
10
11
  }
@@ -78,8 +79,10 @@ export function validateRootCeremonyManifest(value) {
78
79
  const assigned = manifest.custodians
79
80
  .filter((c) => c.role === "root-signer")
80
81
  .map((c) => c.publicKeyId)
81
- .sort();
82
- if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort().join("\0"))
82
+ // `publicKeyId` is optional on the type but validated present for every
83
+ // root-signer above; `sort` also never passes undefined to a comparator.
84
+ .sort((left, right) => compareBytes(left ?? "", right ?? ""));
85
+ if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort(compareBytes).join("\0"))
83
86
  throw new Error("root signer assignments must exactly match root role keys");
84
87
  const recovery = manifest.custodians.filter((c) => c.role === "recovery");
85
88
  if (recovery.length < manifest.thresholds.root ||
@@ -134,7 +137,8 @@ export function preparePublicRootImport(envelope, pin, manifestValue, receiptVal
134
137
  const expected = manifest.roleKeyIds[role];
135
138
  const actual = envelope.signed.roles[role];
136
139
  if (actual.threshold !== manifest.thresholds[role] ||
137
- [...actual.keyids].sort().join("\0") !== [...expected].sort().join("\0"))
140
+ [...actual.keyids].sort(compareBytes).join("\0") !==
141
+ [...expected].sort(compareBytes).join("\0"))
138
142
  throw new Error(`manifest ${role} role does not match root envelope`);
139
143
  }
140
144
  for (const key of Object.values(envelope.signed.keys)) {
@@ -1,4 +1,5 @@
1
1
  import { createHash, createPublicKey, verify as cryptoVerify, timingSafeEqual, } from "node:crypto";
2
+ import { compareBytes } from "./compare.js";
2
3
  export class UpdateTrustError extends Error {
3
4
  code;
4
5
  role;
@@ -53,10 +54,10 @@ function canonicalize(value, seen) {
53
54
  seen.add(value);
54
55
  const object = value;
55
56
  const serialized = `{${Object.keys(object)
56
- // ECMAScript's default code-unit ordering is locale-independent, which is
57
- // required because these bytes are signed on one host and verified on another.
58
- // A localeCompare comparator would make trust bytes depend on host ICU data.
59
- .sort() // NOSONAR: cryptographic canonicalization must not use locale collation.
57
+ // Code-unit ordering is locale-independent, which is required because these
58
+ // bytes are signed on one host and verified on another. A localeCompare
59
+ // comparator would make trust bytes depend on host ICU data.
60
+ .sort(compareBytes)
60
61
  .map((key) => {
61
62
  if (object[key] === undefined)
62
63
  invalid(`metadata field ${key} must not be undefined`);