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,7 +12,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
14
  import { recordDiagnosticFailure } from "./diagnostics.js";
15
- import { getKnodinTools, handleKnodinTool } from "./tools/knodin-tools.js";
15
+ import { assertMcpMemoryHeadroom, classifyMcpFailure, createMcpRequestContext, recordMcpLifecycle, superviseMcpRequest, } from "./mcp-reliability.js";
16
+ import { WorkerSupervisor } from "./mcp-worker-supervisor.js";
17
+ import { attachMcpRequestSignal, getKnodinTools, handleKnodinTool } from "./tools/knodin-tools.js";
16
18
  import { KNODIN_VERSION } from "./version.js";
17
19
  const RETRY_SAFE_OPERATIONS = new Set([
18
20
  "architecture_overview",
@@ -25,11 +27,12 @@ const RETRY_SAFE_OPERATIONS = new Set([
25
27
  "search",
26
28
  "status",
27
29
  ]);
28
- class GatewayRequestQueue {
30
+ const RECOVERY_LANE_OPERATIONS = new Set(["docs"]);
31
+ class SafeGatewayScheduler {
29
32
  tail = Promise.resolve();
30
- pending = 0;
31
- async run(signal, task) {
32
- const position = this.pending++;
33
+ async run(operation, signal, task) {
34
+ if (RECOVERY_LANE_OPERATIONS.has(operation))
35
+ return task();
33
36
  const predecessor = this.tail;
34
37
  let release;
35
38
  this.tail = new Promise((resolve) => {
@@ -41,10 +44,9 @@ class GatewayRequestQueue {
41
44
  throw Object.assign(new Error("MCP request cancelled before dispatch"), {
42
45
  code: "KNODIN_REQUEST_CANCELLED",
43
46
  });
44
- return await task(position);
47
+ return await task();
45
48
  }
46
49
  finally {
47
- this.pending--;
48
50
  release();
49
51
  }
50
52
  }
@@ -65,16 +67,21 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
65
67
  phase: "dispatch",
66
68
  error,
67
69
  });
70
+ const message = error instanceof Error ? error.message : String(error);
68
71
  return diagnostic.recorded
69
- ? new Error(`${error instanceof Error ? error.message : String(error)} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
72
+ ? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
70
73
  : error;
71
74
  }
72
75
  export function createServer(dispatch = handleKnodinTool) {
73
- const queue = new GatewayRequestQueue();
76
+ const scheduler = new SafeGatewayScheduler();
77
+ const workers = new WorkerSupervisor();
74
78
  const server = new Server({ name: "knodin", version: KNODIN_VERSION }, {
75
79
  capabilities: { tools: {} },
76
80
  instructions: "Use knodin first for cold or unfamiliar codebase work: context to orient, explain for source-evidenced symbol context, query impact before meaningful edits, and review before handoff. Prefer direct reads for exact literals, non-code files, or files just edited this turn. If status reports repair-needed, follow its typed repair steps: repair graph damage or init displaced lifecycle routing before relying on graph evidence.",
77
81
  });
82
+ server.onclose = () => {
83
+ void workers.close();
84
+ };
78
85
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
79
86
  tools: getKnodinTools(),
80
87
  }));
@@ -88,53 +95,90 @@ export function createServer(dispatch = handleKnodinTool) {
88
95
  const progressToken = extra._meta?.progressToken;
89
96
  const requestId = String(extra.requestId);
90
97
  const startedAt = Date.now();
98
+ const repoPath = typeof values?.repoPath === "string" ? values.repoPath : process.cwd();
99
+ const context = createMcpRequestContext(requestId, operation, repoPath);
100
+ let progressSequence = 0;
91
101
  let heartbeat;
102
+ recordMcpLifecycle(context, "start");
92
103
  try {
93
- const result = await queue.run(extra.signal, async (position) => {
94
- const notify = async (message) => {
95
- if (progressToken === undefined)
96
- return;
97
- await extra.sendNotification({
98
- method: "notifications/progress",
99
- params: {
100
- progressToken,
101
- progress: Date.now() - startedAt,
102
- message,
103
- },
104
- });
104
+ assertMcpMemoryHeadroom();
105
+ const supervised = dispatch === handleKnodinTool && operation !== "docs";
106
+ const execution = supervised
107
+ ? await workers.worker(repoPath).execute(context, args, extra.signal, (sequence, phase) => {
108
+ progressSequence = sequence;
109
+ recordMcpLifecycle(context, "progress", { sequence });
110
+ if (progressToken !== undefined)
111
+ void extra
112
+ .sendNotification({
113
+ method: "notifications/progress",
114
+ params: { progressToken, progress: sequence, message: phase },
115
+ })
116
+ .catch(ignoreNotificationFailure);
117
+ })
118
+ : {
119
+ result: await scheduler.run(operation, extra.signal, () => superviseMcpRequest(context, extra.signal, async (operationSignal) => {
120
+ const notify = async (message) => {
121
+ if (progressToken === undefined)
122
+ return;
123
+ progressSequence++;
124
+ recordMcpLifecycle(context, "progress", { sequence: progressSequence });
125
+ await extra.sendNotification({
126
+ method: "notifications/progress",
127
+ params: {
128
+ progressToken,
129
+ progress: progressSequence,
130
+ message,
131
+ },
132
+ });
133
+ };
134
+ await notify(`Starting ${operation} (deadline ${context.deadlineMs}ms)`);
135
+ heartbeat = startHeartbeat(notify, operation, startedAt);
136
+ // Keep the production handler explicit: knodin's own source-evidence
137
+ // extractor maps this low-level MCP registration to its dispatcher.
138
+ if (dispatch === handleKnodinTool) {
139
+ attachMcpRequestSignal(args, operationSignal);
140
+ return await handleKnodinTool(args);
141
+ }
142
+ return await dispatch(args, { signal: operationSignal });
143
+ }, { cooperativeCancellation: operation === "repair" })),
105
144
  };
106
- await notify(position > 0
107
- ? `Starting ${operation} after waiting behind ${position} earlier request(s)`
108
- : `Starting ${operation}`);
109
- heartbeat = startHeartbeat(notify, operation, startedAt);
110
- // Keep the production handler explicit: knodin's own source-evidence
111
- // extractor maps this low-level MCP registration to its dispatcher.
112
- return dispatch === handleKnodinTool ? await handleKnodinTool(args) : await dispatch(args);
113
- });
145
+ const result = execution.result;
146
+ recordMcpLifecycle(context, "success", { elapsedMs: Date.now() - startedAt });
114
147
  return {
148
+ _meta: {
149
+ requestId: context.requestId,
150
+ traceId: context.traceId,
151
+ ...("predecessorTraceId" in execution
152
+ ? { predecessorTraceId: execution.predecessorTraceId }
153
+ : {}),
154
+ },
115
155
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
116
156
  };
117
157
  }
118
158
  catch (error) {
159
+ const failure = classifyMcpFailure(error);
160
+ recordMcpLifecycle(context, "failure", {
161
+ kind: failure.kind,
162
+ elapsedMs: Date.now() - startedAt,
163
+ });
119
164
  const recorded = recordMcpDiagnosticFailure(values, error);
120
165
  const message = recorded instanceof Error ? recorded.message : String(recorded);
121
- const traceId = /\[diagnostic ([a-f0-9]{16})\]/.exec(message)?.[1] ?? `mcp-${requestId}`;
122
166
  const retrySafe = RETRY_SAFE_OPERATIONS.has(operation);
123
167
  return {
124
168
  isError: true,
169
+ _meta: { requestId: context.requestId, traceId: context.traceId },
125
170
  content: [
126
171
  {
127
172
  type: "text",
128
173
  text: JSON.stringify({
129
- code: error?.code === "KNODIN_REQUEST_CANCELLED"
130
- ? "KNODIN_REQUEST_CANCELLED"
131
- : "KNODIN_OPERATION_FAILED",
174
+ code: failure.code,
175
+ kind: failure.kind,
132
176
  message,
133
- traceId,
177
+ traceId: context.traceId,
134
178
  requestId,
135
179
  operation,
136
- repository: typeof values?.repoPath === "string" ? values.repoPath : process.cwd(),
137
180
  phase: "dispatch",
181
+ deadlineMs: context.deadlineMs,
138
182
  retrySafe,
139
183
  diagnosticLog: ".knodin/diagnostics/events.jsonl (only when opt-in diagnostics are enabled)",
140
184
  recovery: retrySafe
@@ -148,6 +192,7 @@ export function createServer(dispatch = handleKnodinTool) {
148
192
  finally {
149
193
  if (heartbeat)
150
194
  clearInterval(heartbeat);
195
+ recordMcpLifecycle(context, "cleanup", { elapsedMs: Date.now() - startedAt });
151
196
  }
152
197
  });
153
198
  return server;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { compareBytes } from "./compare.js";
3
4
  import { contentFingerprint, readStructuralSnapshot, } from "./structural-snapshot.js";
4
5
  function exactFile(repo, file) {
5
6
  try {
@@ -124,7 +125,7 @@ function directSymbols(filePath, content) {
124
125
  }
125
126
  if (found) {
126
127
  const { kind, symbol } = found;
127
- const visibility = line.match(/\b(public|private|protected|internal)\b/)?.[1] ??
128
+ const visibility = /\b(public|private|protected|internal)\b/.exec(line)?.[1] ??
128
129
  (python && symbol.startsWith("_") ? "private" : "default");
129
130
  symbols.push({
130
131
  identity: null,
@@ -212,7 +213,9 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
212
213
  directories.set(directory, value);
213
214
  }
214
215
  rows = [...directories]
215
- .sort(([a], [b]) => a.localeCompare(b))
216
+ // Byte order: this sort decides which directories survive the
217
+ // `rows.slice(0, limit)` below, not merely how they read.
218
+ .sort(([a], [b]) => compareBytes(a, b))
216
219
  .map(([symbol, value]) => ({ symbol, kind: "directory", ...value }));
217
220
  fingerprint = contentFingerprint(JSON.stringify(matching.map(({ path: file, contentFingerprint: hash }) => [file, hash])));
218
221
  }
@@ -226,6 +229,13 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
226
229
  rows = files.flatMap((file) => file.symbols.map((symbol) => ({ ...symbol, file: file.path })));
227
230
  }
228
231
  const selected = rows.slice(0, limit);
232
+ let upgrade;
233
+ if (pattern === "file_summary")
234
+ upgrade = { operation: "explain", target };
235
+ else if (pattern === "project_overview")
236
+ upgrade = { operation: "query", pattern: "architecture_overview", target: "" };
237
+ else
238
+ upgrade = { operation: "query", pattern: "impact", target };
229
239
  return {
230
240
  pattern,
231
241
  target,
@@ -243,11 +253,7 @@ export function runStructuralFastPath(repo, pattern, target, limit = 100) {
243
253
  uniqueness: pattern === "project_overview" ? "not-applicable" : "file-scoped-only",
244
254
  evidenceGeneratedAt: snapshot?.generatedAt ?? new Date().toISOString(),
245
255
  },
246
- upgrade: pattern === "file_summary"
247
- ? { operation: "explain", target }
248
- : pattern === "project_overview"
249
- ? { operation: "query", pattern: "architecture_overview", target: "" }
250
- : { operation: "query", pattern: "impact", target },
256
+ upgrade,
251
257
  };
252
258
  }
253
259
  export function runExactSymbolFastPath(repo, symbol, filePath) {
@@ -306,7 +312,7 @@ export function runExactSymbolFastPath(repo, symbol, filePath) {
306
312
  upgrade: { operation: "explain", target: symbol },
307
313
  };
308
314
  }
309
- export function tryStructuralFastPath(argv, cwd = process.cwd()) {
315
+ export function tryStructuralFastPath(argv, cwd) {
310
316
  const valueAfter = (flag) => {
311
317
  const index = argv.indexOf(flag);
312
318
  return index >= 0
@@ -314,7 +320,7 @@ export function tryStructuralFastPath(argv, cwd = process.cwd()) {
314
320
  : argv.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
315
321
  };
316
322
  if (argv[0] === "explain" && argv[1] && valueAfter("--file")) {
317
- const repo = path.resolve(valueAfter("--repo") ?? cwd);
323
+ const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
318
324
  const result = runExactSymbolFastPath(repo, argv[1], valueAfter("--file"));
319
325
  if (!result)
320
326
  return false;
@@ -325,7 +331,7 @@ export function tryStructuralFastPath(argv, cwd = process.cwd()) {
325
331
  !["file_summary", "batch_outline", "project_overview"].includes(argv[1] ?? ""))
326
332
  return false;
327
333
  const pattern = argv[1];
328
- const repo = path.resolve(valueAfter("--repo") ?? cwd);
334
+ const repo = path.resolve(valueAfter("--repo") ?? cwd ?? process.cwd());
329
335
  const target = pattern === "project_overview" ? "" : (argv[2] ?? "");
330
336
  const limit = Number(valueAfter("--limit") ?? 100);
331
337
  if (!target && pattern !== "project_overview")
@@ -1,12 +1,14 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { compareBytes } from "./compare.js";
5
+ import { resolveStateDir } from "./engine/state-paths.js";
4
6
  export const STRUCTURAL_SNAPSHOT_VERSION = 1;
5
7
  export function contentFingerprint(content) {
6
8
  return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
7
9
  }
8
10
  export function structuralSnapshotPath(repo) {
9
- return path.join(path.resolve(repo), ".knodin", "structural-v1.json");
11
+ return path.join(resolveStateDir(repo), "structural-v1.json");
10
12
  }
11
13
  export function writeStructuralSnapshot(repo, files) {
12
14
  const destination = structuralSnapshotPath(repo);
@@ -14,7 +16,9 @@ export function writeStructuralSnapshot(repo, files) {
14
16
  const snapshot = {
15
17
  schemaVersion: STRUCTURAL_SNAPSHOT_VERSION,
16
18
  generatedAt: new Date().toISOString(),
17
- files: [...files].sort((left, right) => left.path.localeCompare(right.path)),
19
+ // Byte order, not collation: this array is written to disk and read back
20
+ // on any host, so the snapshot's bytes must not depend on the locale.
21
+ files: [...files].sort((left, right) => compareBytes(left.path, right.path)),
18
22
  };
19
23
  const temporary = `${destination}.${process.pid}.tmp`;
20
24
  fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600 });
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { parseDocument } from "yaml";
6
+ import { compareBytes } from "./compare.js";
6
7
  import { detectDuplicateCandidates, extractRepositoryRelationships, trackedRepositoryFiles, } from "./relationship-adapters.js";
7
8
  export const RELATIONSHIP_TYPES = [
8
9
  "depends_on",
@@ -52,6 +53,24 @@ function stringValue(value, field) {
52
53
  throw new Error(`${field} must be a string`);
53
54
  return value;
54
55
  }
56
+ function repositoryInitialization(value) {
57
+ if (value === undefined)
58
+ return undefined;
59
+ const record = asRecord(value);
60
+ if (record.memoryLimitMiB === undefined)
61
+ return {};
62
+ const mib = record.memoryLimitMiB;
63
+ if (typeof mib !== "number" ||
64
+ !Number.isSafeInteger(mib) ||
65
+ mib < 128 ||
66
+ !Number.isSafeInteger(mib * 1024 * 1024))
67
+ throw new Error("repositoryInitialization.memoryLimitMiB must be an integer of at least 128");
68
+ return { memoryLimitMiB: mib };
69
+ }
70
+ export function configuredRepositoryInitMemoryLimitBytes(config) {
71
+ const mib = config.repositoryInitialization?.memoryLimitMiB;
72
+ return mib === undefined ? undefined : mib * 1024 * 1024;
73
+ }
55
74
  function stableLegacyIdentity(portableReference) {
56
75
  let normalized = portableReference.replaceAll("\\", "/");
57
76
  while (normalized.endsWith("/"))
@@ -321,6 +340,7 @@ export function loadSystemConfiguration(repoPath, options = {}) {
321
340
  const repositories = (Array.isArray(team.repositories) ? team.repositories : []).map((value, index) => configuredRepository(value, index));
322
341
  const systems = (Array.isArray(team.systems) ? team.systems : []).map((value, index) => configuredSystem(value, index));
323
342
  const relationships = (Array.isArray(team.relationships) ? team.relationships : []).map((value, index) => declaredRelationship(value, index));
343
+ const initialization = repositoryInitialization(team.repositoryInitialization);
324
344
  const xdgConfigHome = options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
325
345
  applyPersonalMappings(repositories, xdgConfigHome);
326
346
  const legacyFederation = applyLegacyFederation(repo, repositories, relationships);
@@ -338,12 +358,15 @@ export function loadSystemConfiguration(repoPath, options = {}) {
338
358
  relationship,
339
359
  ])).values(),
340
360
  ];
341
- uniqueRelationships.sort((left, right) => `${left.from}\0${left.type}\0${left.to}`.localeCompare(`${right.from}\0${right.type}\0${right.to}`));
361
+ // Byte order over a NUL-joined composite key: collation may ignore the NUL
362
+ // separators, blurring the field boundaries the key depends on.
363
+ uniqueRelationships.sort((left, right) => compareBytes(`${left.from}\0${left.type}\0${left.to}`, `${right.from}\0${right.type}\0${right.to}`));
342
364
  return {
343
365
  schemaVersion: 1,
344
366
  repositories,
345
367
  systems,
346
368
  relationships: uniqueRelationships,
369
+ ...(initialization === undefined ? {} : { repositoryInitialization: initialization }),
347
370
  compatibility: { legacyFederation },
348
371
  };
349
372
  }
@@ -525,7 +548,7 @@ export async function enrichSystemRelationships(config) {
525
548
  return config;
526
549
  return {
527
550
  ...config,
528
- relationships: [...config.relationships, ...duplicates].sort((left, right) => `${left.from}\0${left.type}\0${left.to}\0${left.evidence.location}`.localeCompare(`${right.from}\0${right.type}\0${right.to}\0${right.evidence.location}`)),
551
+ relationships: [...config.relationships, ...duplicates].sort((left, right) => compareBytes(`${left.from}\0${left.type}\0${left.to}\0${left.evidence.location}`, `${right.from}\0${right.type}\0${right.to}\0${right.evidence.location}`)),
529
552
  };
530
553
  }
531
554
  function repositoryForIdentity(config, identity) {