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
package/dist/bin/cli.js CHANGED
@@ -22,10 +22,11 @@ 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
+ import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
26
26
  import { getDocSection, listDocTopics } from "../src/docs-sections.js";
27
27
  import { diagnoseInstallation } from "../src/doctor.js";
28
28
  import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
29
+ import { resolveDbPath } from "../src/engine/state-paths.js";
29
30
  import { diagnoseFailure, } from "../src/failure-diagnosis.js";
30
31
  import { gitExecutable } from "../src/git-executable.js";
31
32
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
@@ -33,6 +34,7 @@ import { createIndexActivityReporter } from "../src/index-activity.js";
33
34
  import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
34
35
  import { createInitProgressRenderer } from "../src/init-progress.js";
35
36
  import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
37
+ import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
36
38
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
37
39
  import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
38
40
  import { auditPullRequests } from "../src/pr-triage.js";
@@ -41,10 +43,10 @@ import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, res
41
43
  import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
42
44
  import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
43
45
  import { applyResponseBudget } from "../src/response-budget.js";
44
- import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
46
+ import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
45
47
  import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
46
48
  import { KNODIN_VERSION } from "../src/version.js";
47
- import { writeVisualization } from "../src/visualization.js";
49
+ import { writeVisualization, } from "../src/visualization.js";
48
50
  import { waitForFresh } from "../src/wait-for-fresh.js";
49
51
  import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../src/worktree-lifecycle.js";
50
52
  function explicitScope(args) {
@@ -102,11 +104,11 @@ function integrationAgents(repo) {
102
104
  return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
103
105
  }
104
106
  function formatInitHuman(result) {
105
- const agents = result.paths.scope === "cli-only"
106
- ? "CLI-only — AI agents are not configured to discover knodin"
107
- : result.paths.agentIntegration.configured.length > 0
108
- ? `${result.paths.scope} — ${result.paths.agentIntegration.configured.join(", ")}`
109
- : `${result.paths.scope} — no supported coding agents detected`;
107
+ let agents = `${result.paths.scope} no supported coding agents detected`;
108
+ if (result.paths.scope === "cli-only")
109
+ agents = "CLI-only — AI agents are not configured to discover knodin";
110
+ else if (result.paths.agentIntegration.configured.length > 0)
111
+ agents = `${result.paths.scope} — ${result.paths.agentIntegration.configured.join(", ")}`;
110
112
  const failures = result.paths.agentIntegration.failed
111
113
  .map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
112
114
  .join("");
@@ -174,8 +176,220 @@ function formatIndexVerificationError(result) {
174
176
  const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
175
177
  return `knodin index: requested work completed, but ${result.verification.issueCount.toLocaleString()} graph issue(s) remain.${detail} Run \`knodin repair\`.\n`;
176
178
  }
179
+ /**
180
+ * The hooks/lifecycle line of `status`.
181
+ *
182
+ * A mirror has no hooks by design, so neither "installed" nor "degraded" is true
183
+ * of it: claiming they are "installed and executable" would describe a directory
184
+ * containing none, and reporting "degraded" would demand a `knodin init` that
185
+ * must never run there.
186
+ */
187
+ function formatLifecycleLine(lifecycle, isMirror) {
188
+ if (!lifecycle)
189
+ return "";
190
+ if (isMirror)
191
+ return "Hooks: not applicable; a mirror is refreshed explicitly, not by Git events.\n";
192
+ if (lifecycle.status === "healthy")
193
+ return "Hooks: installed and executable.\n";
194
+ const issue = lifecycle.issues[0] ?? "refresh capability is not verified";
195
+ return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin init\`.\n`;
196
+ }
197
+ /** Renders the top extensions of a tally as `.md 180, .yml 74, +3 more`. */
198
+ function formatTally(counts, limit = 4) {
199
+ const entries = Object.entries(counts).sort((left, right) => right[1] - left[1]);
200
+ const shown = entries.slice(0, limit).map(([ext, count]) => `${ext} ${count}`);
201
+ const remaining = entries.length - shown.length;
202
+ if (remaining > 0)
203
+ shown.push(`+${remaining} more`);
204
+ return shown.join(", ");
205
+ }
206
+ /**
207
+ * One clause naming what the graph does not cover. Silence here would let a
208
+ * language gap read as an absence of facts, so this is appended to the coverage
209
+ * line whenever anything was skipped.
210
+ */
211
+ function formatCoverageGaps(skipped) {
212
+ if (!skipped)
213
+ return "";
214
+ // A zero total is only reportable as "no gaps" when the tally was actually
215
+ // read. If it wasn't, staying silent would state full coverage on the
216
+ // strength of a measurement that never happened.
217
+ if (skipped.total === 0 && !skipped.unparsedUnknown)
218
+ return "";
219
+ const clauses = [];
220
+ const byExtension = formatTally(skipped.byExtension);
221
+ if (byExtension)
222
+ clauses.push(`not indexed: ${byExtension}`);
223
+ const unparsed = formatTally(skipped.unparsedByExtension);
224
+ if (unparsed)
225
+ clauses.push(`parsed but empty: ${unparsed}`);
226
+ if (skipped.unparsedUnknown)
227
+ clauses.push("parsed-but-empty tally unavailable — run a full index; this is a lower bound");
228
+ const count = skipped.unparsedUnknown ? `${skipped.total}+` : `${skipped.total}`;
229
+ return `. Not in graph: ${count} files (${clauses.join("; ")})`;
230
+ }
231
+ /**
232
+ * Recursive on-disk size of a directory.
233
+ *
234
+ * Reports unreadable entries rather than treating them as zero. The listing
235
+ * exists so acquired disk is auditable; a mirror that cannot be measured showing
236
+ * a confident `0 B` invites exactly the wrong conclusion — that it is empty and
237
+ * safe to remove — which is the same absence-as-fact error this whole feature
238
+ * is built to avoid.
239
+ */
240
+ function directorySizeBytes(target) {
241
+ let bytes = 0;
242
+ let partial = false;
243
+ let entries;
244
+ try {
245
+ entries = fs.readdirSync(target, { withFileTypes: true });
246
+ }
247
+ catch {
248
+ return { bytes: 0, partial: true };
249
+ }
250
+ for (const entry of entries) {
251
+ const child = path.join(target, entry.name);
252
+ if (entry.isDirectory()) {
253
+ const nested = directorySizeBytes(child);
254
+ bytes += nested.bytes;
255
+ partial = partial || nested.partial;
256
+ }
257
+ else if (entry.isFile()) {
258
+ try {
259
+ bytes += fs.statSync(child).size;
260
+ }
261
+ catch {
262
+ partial = true;
263
+ }
264
+ }
265
+ }
266
+ return { bytes, partial };
267
+ }
268
+ function formatBytes(bytes) {
269
+ if (bytes < 1024)
270
+ return `${bytes} B`;
271
+ const units = ["KiB", "MiB", "GiB"];
272
+ let value = bytes / 1024;
273
+ let unit = 0;
274
+ while (value >= 1024 && unit < units.length - 1) {
275
+ value /= 1024;
276
+ unit++;
277
+ }
278
+ return `${value.toFixed(1)} ${units[unit]}`;
279
+ }
280
+ /**
281
+ * `knodin remote <add|list|remove|refresh>`.
282
+ *
283
+ * Mirrors are reported as snapshots, never as `fresh`: the clone is pinned to
284
+ * the commit it was fetched at, and nothing watches the remote for changes. Size
285
+ * and fetch time are shown for every mirror so acquired disk is auditable rather
286
+ * than silently accumulating.
287
+ */
288
+ function writeMirrorListing() {
289
+ const mirrors = listMirrors();
290
+ if (mirrors.length === 0) {
291
+ process.stdout.write("No mirrors acquired. Add one with `knodin remote add <url>`.\n");
292
+ return;
293
+ }
294
+ let total = 0;
295
+ let anyPartial = false;
296
+ for (const mirror of mirrors) {
297
+ const size = directorySizeBytes(path.dirname(mirror.path));
298
+ total += size.bytes;
299
+ anyPartial = anyPartial || size.partial;
300
+ // "at least" rather than a bare figure when something was unreadable: a
301
+ // mirror reported as 0 B reads as empty, and the obvious next action on an
302
+ // empty mirror is to delete it.
303
+ const shown = size.partial ? `at least ${formatBytes(size.bytes)}` : formatBytes(size.bytes);
304
+ process.stdout.write(`${mirror.identity} ${mirror.url}\n snapshot ${mirror.sha.slice(0, 12)}, fetched ${mirror.fetchedAt}, ${shown}\n ${mirror.path}\n`);
305
+ }
306
+ const totalShown = anyPartial ? `at least ${formatBytes(total)}` : formatBytes(total);
307
+ process.stdout.write(`${mirrors.length} mirror(s), ${totalShown} on disk. Remove one with \`knodin remote remove <identity>\`.\n`);
308
+ }
309
+ async function remoteAdd(url, options) {
310
+ const { record, alreadyPresent } = addMirror(url);
311
+ process.stdout.write(alreadyPresent
312
+ ? `Mirror already present: ${record.identity} (snapshot ${record.sha.slice(0, 12)}). Use \`knodin remote refresh ${record.identity}\` to update it.\n`
313
+ : `Acquired ${record.identity} at snapshot ${record.sha.slice(0, 12)} into ${record.path}\n`);
314
+ if (options.skipIndex || alreadyPresent)
315
+ return;
316
+ await indexMirror(record.path, options.deferSemantic);
317
+ }
318
+ async function remoteRefresh(identity, options) {
319
+ const record = refreshMirror(identity);
320
+ process.stdout.write(`Refreshed ${record.identity} to snapshot ${record.sha.slice(0, 12)} (${record.fetchedAt})\n`);
321
+ if (options.skipIndex)
322
+ return;
323
+ await indexMirror(record.path, options.deferSemantic);
324
+ }
325
+ async function runRemoteCommand(args) {
326
+ const [action, target] = args;
327
+ const options = {
328
+ skipIndex: args.includes("--no-index"),
329
+ deferSemantic: args.includes("--defer-semantic"),
330
+ };
331
+ if (action === "list")
332
+ return writeMirrorListing();
333
+ if (action === "remove") {
334
+ if (!target)
335
+ throw new Error("knodin remote remove requires an <identity>");
336
+ process.stdout.write(removeMirror(target) ? `Removed ${target}\n` : `No mirror named ${target}\n`);
337
+ return;
338
+ }
339
+ if (action === "add") {
340
+ if (!target)
341
+ throw new Error("knodin remote add requires a <url>");
342
+ return remoteAdd(target, options);
343
+ }
344
+ if (action === "refresh") {
345
+ if (!target)
346
+ throw new Error("knodin remote refresh requires an <identity>");
347
+ return remoteRefresh(target, options);
348
+ }
349
+ throw new Error("knodin remote <add|list|remove|refresh>");
350
+ }
351
+ /**
352
+ * Builds a mirror's graph structure-first, then fills embeddings.
353
+ *
354
+ * The embedding pass dominates indexing cost, so structural answers (`explain`,
355
+ * `callers_of`, blast radius) become available in a fraction of the total. The
356
+ * gap is reported rather than hidden: until embeddings land, `search` silently
357
+ * under-returns, so saying nothing here would hand back exactly the confident
358
+ * empty result this whole feature exists to avoid.
359
+ */
360
+ async function indexMirror(source, deferSemantic) {
361
+ const engine = createEngine();
362
+ try {
363
+ await engine.index(source, undefined, true, { skipEmbeddings: true });
364
+ const structural = await engine.status(source, { audit: "deep" });
365
+ process.stdout.write(`Indexed ${structural.coverage.indexedFiles} file(s), ${structural.coverage.filesWithSymbols} with symbols${formatCoverageGaps(structural.coverage.skipped)}\n`);
366
+ process.stdout.write("Structural queries (explain, query, map) are ready now; semantic search is not.\n");
367
+ if (deferSemantic) {
368
+ process.stdout.write("Semantic coverage deferred. Run `knodin index --repo <mirror>` to build it; until then `search` will under-return.\n");
369
+ return;
370
+ }
371
+ process.stdout.write("Building semantic index...\n");
372
+ await engine.index(source, undefined, false);
373
+ const full = await engine.status(source, { audit: "deep" });
374
+ process.stdout.write(`Semantic coverage: ${full.semanticReadiness ?? "unknown"}\n`);
375
+ }
376
+ finally {
377
+ await engine.close();
378
+ }
379
+ }
177
380
  function formatStatusHuman(result) {
178
- const coverage = `${result.coverage.sourceFiles} source files, ${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols`;
381
+ // A mirror's `freshness` describes the graph against its local clone and can
382
+ // legitimately read `fresh`. Saying so without also saying the clone is a
383
+ // snapshot would imply knodin is tracking the remote, which it is not.
384
+ const mirrorNote = result.mirror
385
+ ? ` Mirror of ${result.mirror.url}: snapshot ${result.mirror.sha.slice(0, 12)}, fetched ${result.mirror.fetchedAt}; the remote is not watched.`
386
+ : "";
387
+ // Semantic search drops symbols that have no embedding, so an incomplete pass
388
+ // silently shortens results. Say so rather than let it read as "no matches".
389
+ const semanticNote = result.semanticReadiness && result.semanticReadiness !== "ready"
390
+ ? ` Semantic search coverage is ${result.semanticReadiness}: \`search\` will under-return until embedding completes (\`knodin index\`).`
391
+ : "";
392
+ const coverage = `${result.coverage.sourceFiles} source files, ${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
179
393
  if (result.status === "indexing" && result.activity) {
180
394
  const count = result.activity.phaseTotal === undefined
181
395
  ? ""
@@ -184,14 +398,14 @@ function formatStatusHuman(result) {
184
398
  return `Graph update in progress: ${result.activity.phase}${count} — ${result.activity.message} (${elapsed}s elapsed; ${coverage}).\n`;
185
399
  }
186
400
  const integration = result.integration;
401
+ const integrationAgents = integration && integration.agents.length > 0 ? ` (${integration.agents.join(", ")})` : "";
187
402
  const integrationLine = integration
188
- ? `Agent integration: ${integration.scope}${integration.agents.length > 0 ? ` (${integration.agents.join(", ")})` : ""}.\n`
403
+ ? `Agent integration: ${integration.scope}${integrationAgents}.\n`
189
404
  : "Agent integration: unconfigured. AI agents will not discover knodin automatically; run `knodin configure --scope personal`.\n";
190
- const lifecycleLine = result.lifecycle
191
- ? result.lifecycle.status === "healthy"
192
- ? "Hooks: installed and executable.\n"
193
- : `Lifecycle refresh: ${result.lifecycle.status}; ${result.lifecycle.issues[0] ?? "refresh capability is not verified"}. Run \`knodin init\`.\n`
194
- : "";
405
+ // Replaces main's inline form: a mirror installs no hooks by design, so the
406
+ // inline version reported "needs repair — run knodin init" on every mirror
407
+ // status call, an instruction that must never be followed there.
408
+ const lifecycleLine = formatLifecycleLine(result.lifecycle, result.mirror !== undefined);
195
409
  const head = (value) => value?.slice(0, 12) ?? "unknown";
196
410
  const distance = result.freshness.commitDistance === null
197
411
  ? ""
@@ -219,7 +433,8 @@ function humanLabel(key) {
219
433
  /** Render bounded CLI data for a terminal without turning it back into JSON. */
220
434
  function formatHumanValue(value, indent = "", label) {
221
435
  if (value === null || typeof value !== "object") {
222
- return [`${indent}${label ? `${humanLabel(label)}: ` : ""}${String(value)}`];
436
+ const labelPrefix = label ? `${humanLabel(label)}: ` : "";
437
+ return [`${indent}${labelPrefix}${String(value)}`];
223
438
  }
224
439
  if (Array.isArray(value)) {
225
440
  if (value.length === 0)
@@ -245,7 +460,8 @@ function formatHumanValue(value, indent = "", label) {
245
460
  return lines;
246
461
  }
247
462
  function formatGenericHuman(cmd, result) {
248
- return `${[`${humanLabel(cmd)}:`, ...formatHumanValue(result, " ")].join("\n")}\n`;
463
+ const lines = [`${humanLabel(cmd)}:`, ...formatHumanValue(result, " ")];
464
+ return `${lines.join("\n")}\n`;
249
465
  }
250
466
  function formatCompressionHuman(result) {
251
467
  const omitted = result.omittedRanges.reduce((total, range) => total + range.lineCount, 0);
@@ -318,6 +534,22 @@ async function readBoundedStdin(maxBytes) {
318
534
  }
319
535
  const CLEAR_TERMINAL_LINE = "\r\x1b[2K";
320
536
  const PROGRESS_WORKER_CLOSE_TIMEOUT_MS = 2_000;
537
+ /** Resolve true when `closed` settles first, false when the deadline wins. */
538
+ function raceWorkerClose(closed, timeoutMs) {
539
+ return new Promise((resolve) => {
540
+ let settled = false;
541
+ const finish = (closedCleanly) => {
542
+ if (settled)
543
+ return;
544
+ settled = true;
545
+ clearTimeout(timer);
546
+ resolve(closedCleanly);
547
+ };
548
+ const timer = setTimeout(() => finish(false), timeoutMs);
549
+ timer.unref();
550
+ void closed.then(() => finish(true));
551
+ });
552
+ }
321
553
  function createProgressWorkerRenderer(workerName, startMessage) {
322
554
  const extension = fileURLToPath(import.meta.url).endsWith(".ts") ? ".ts" : ".js";
323
555
  const workerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), `../src/${workerName}${extension}`);
@@ -355,19 +587,7 @@ function createProgressWorkerRenderer(workerName, startMessage) {
355
587
  stop: async () => {
356
588
  send({ type: "stop" });
357
589
  worker.stdin.end();
358
- const waitForClose = (timeoutMs) => new Promise((resolve) => {
359
- let settled = false;
360
- const finish = (closedCleanly) => {
361
- if (settled)
362
- return;
363
- settled = true;
364
- clearTimeout(timer);
365
- resolve(closedCleanly);
366
- };
367
- const timer = setTimeout(() => finish(false), timeoutMs);
368
- timer.unref();
369
- void closed.then(() => finish(true));
370
- });
590
+ const waitForClose = (timeoutMs) => raceWorkerClose(closed, timeoutMs);
371
591
  if (!(await waitForClose(PROGRESS_WORKER_CLOSE_TIMEOUT_MS))) {
372
592
  // Rendering is observational. A wedged terminal renderer must never
373
593
  // hold graph work or its completion summary hostage.
@@ -531,6 +751,7 @@ async function main() {
531
751
  }
532
752
  const engine = createEngine();
533
753
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
754
+ const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
534
755
  const summary = await initializeRepositories(plan.roots, {
535
756
  command: runtimeCommand,
536
757
  depth: plan.depth,
@@ -540,13 +761,21 @@ async function main() {
540
761
  status: (target) => engine.status(target),
541
762
  agents: detectSupportedAgents(),
542
763
  indexMode: (target) => indexModeForPath(systemConfig, target),
543
- isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command: runtimeCommand }),
764
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({
765
+ repository: target,
766
+ command: runtimeCommand,
767
+ memoryLimitBytes,
768
+ }),
544
769
  });
545
770
  await engine.close();
546
771
  process.stdout.write(plan.json ? `${JSON.stringify(summary)}\n` : formatRepositoryHuman(summary));
547
772
  process.exitCode = summary.exitCode;
548
773
  return;
549
774
  }
775
+ if (cmd === "remote") {
776
+ await runRemoteCommand(rawRest);
777
+ return;
778
+ }
550
779
  if (cmd === "repos") {
551
780
  if (repoFlag !== undefined) {
552
781
  throw new Error("knodin repos accepts discovery roots, not --repo");
@@ -587,6 +816,7 @@ async function main() {
587
816
  }
588
817
  if (plan.command === "init") {
589
818
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
819
+ const memoryLimitBytes = plan.memoryLimitBytes ?? configuredRepositoryInitMemoryLimitBytes(systemConfig);
590
820
  if (plan.dryRun) {
591
821
  const summary = await initializeRepositories(plan.roots, {
592
822
  command: runtimeCommand,
@@ -636,7 +866,11 @@ async function main() {
636
866
  status: (target) => engine.status(target),
637
867
  agents: detectSupportedAgents(),
638
868
  indexMode: (target) => indexModeForPath(systemConfig, target),
639
- isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command: runtimeCommand }),
869
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({
870
+ repository: target,
871
+ command: runtimeCommand,
872
+ memoryLimitBytes,
873
+ }),
640
874
  });
641
875
  const discovery = await discoverRepositories(plan.roots, {
642
876
  depth: plan.depth,
@@ -1010,8 +1244,9 @@ async function main() {
1010
1244
  switch (cmd) {
1011
1245
  case "doctor": {
1012
1246
  const client = selectorValue("--client");
1013
- if (client !== undefined && !["claude", "codex", "gemini", "antigravity"].includes(client))
1014
- throw new Error("knodin doctor: --client must be claude, codex, gemini, or antigravity");
1247
+ if (client !== undefined &&
1248
+ !["claude", "codex", "gemini", "copilot", "antigravity"].includes(client))
1249
+ throw new Error("knodin doctor: --client must be claude, codex, gemini, copilot, or antigravity");
1015
1250
  const unsupported = rest.filter((argument, index) => argument !== "--client" && rest[index - 1] !== "--client");
1016
1251
  if (unsupported.length > 0)
1017
1252
  throw new Error(`knodin doctor: unknown option ${unsupported[0]}`);
@@ -1158,7 +1393,7 @@ async function main() {
1158
1393
  const scope = explicitScope(rawRest);
1159
1394
  if (!scope)
1160
1395
  throw new Error("knodin configure: missing --scope");
1161
- if (!fs.existsSync(path.join(repo, ".knodin", "db.sqlite"))) {
1396
+ if (!fs.existsSync(resolveDbPath(repo))) {
1162
1397
  throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
1163
1398
  }
1164
1399
  const agents = scope === "team" ? [] : integrationAgents(repo);
@@ -1192,6 +1427,28 @@ async function main() {
1192
1427
  }
1193
1428
  const clean = rest.includes("--clean") || rest.includes("--force");
1194
1429
  const scipPath = selectorValue("--scip");
1430
+ const sarifPath = selectorValue("--sarif");
1431
+ // A bound the operator cannot move is just a failure, so every import
1432
+ // ceiling is overridable. Reject junk here rather than letting NaN
1433
+ // silently disable a limit downstream.
1434
+ const importLimit = (flag) => {
1435
+ const raw = selectorValue(flag);
1436
+ if (raw === undefined)
1437
+ return undefined;
1438
+ const parsed = Number(raw);
1439
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1440
+ process.stderr.write(`knodin index: ${flag} must be a positive integer\n`);
1441
+ process.exit(1);
1442
+ }
1443
+ return parsed;
1444
+ };
1445
+ const sarifMaxBytes = importLimit("--sarif-max-bytes");
1446
+ const sarifMaxFindings = importLimit("--sarif-max-findings");
1447
+ const sarifTimeoutMs = importLimit("--sarif-timeout-ms");
1448
+ const scipMaxBytes = importLimit("--scip-max-bytes");
1449
+ const scipMaxFiles = importLimit("--scip-max-files");
1450
+ const scipMaxFacts = importLimit("--scip-max-facts");
1451
+ const scipTimeoutMs = importLimit("--scip-timeout-ms");
1195
1452
  const renderer = createInitRenderer("index");
1196
1453
  const activity = createIndexActivityReporter(plan.repo);
1197
1454
  activity.start();
@@ -1199,7 +1456,25 @@ async function main() {
1199
1456
  let indexResult;
1200
1457
  try {
1201
1458
  indexResult = await engine.index(plan.repo, plan.files, clean, {
1202
- scip: scipPath ? { path: scipPath } : undefined,
1459
+ scip: scipPath
1460
+ ? {
1461
+ path: scipPath,
1462
+ // Only override a default when the operator actually asked.
1463
+ ...(scipMaxBytes !== undefined ? { maxBytes: scipMaxBytes } : {}),
1464
+ ...(scipMaxFiles !== undefined ? { maxFiles: scipMaxFiles } : {}),
1465
+ ...(scipMaxFacts !== undefined ? { maxFacts: scipMaxFacts } : {}),
1466
+ ...(scipTimeoutMs !== undefined ? { timeoutMs: scipTimeoutMs } : {}),
1467
+ }
1468
+ : undefined,
1469
+ sarif: sarifPath
1470
+ ? {
1471
+ path: sarifPath,
1472
+ // Only override a default when the operator actually asked.
1473
+ ...(sarifMaxBytes !== undefined ? { maxBytes: sarifMaxBytes } : {}),
1474
+ ...(sarifMaxFindings !== undefined ? { maxFindings: sarifMaxFindings } : {}),
1475
+ ...(sarifTimeoutMs !== undefined ? { timeoutMs: sarifTimeoutMs } : {}),
1476
+ }
1477
+ : undefined,
1203
1478
  onProgress: (event) => {
1204
1479
  activity.update(event);
1205
1480
  renderer.onProgress(event);
@@ -1376,13 +1651,18 @@ async function main() {
1376
1651
  case "visualize": {
1377
1652
  const entry = rest.find((value, index) => !value.startsWith("--") && !rest[index - 1]?.startsWith("--"));
1378
1653
  const outputPath = selectorValue("--output");
1379
- if (!entry)
1654
+ const scope = selectorValue("--scope");
1655
+ const granularity = selectorValue("--granularity");
1656
+ // Repo scope draws the whole graph, so it takes no entry selector.
1657
+ if (!entry && scope !== "repo")
1380
1658
  throw new Error("knodin visualize requires an <entry> selector");
1381
1659
  if (!outputPath)
1382
1660
  throw new Error("knodin visualize requires --output <path.html>");
1383
1661
  result = await graphRead(() => writeVisualization(engine, repo, {
1384
1662
  entry,
1385
1663
  outputPath,
1664
+ scope,
1665
+ granularity,
1386
1666
  depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
1387
1667
  byteBudget: selectorValue("--max-bytes")
1388
1668
  ? Number(selectorValue("--max-bytes"))
@@ -1405,16 +1685,17 @@ async function main() {
1405
1685
  const limit = Number(selectorValue("--limit") ?? positionalLimit ?? 5);
1406
1686
  if (!Number.isInteger(limit) || limit < 1)
1407
1687
  throw new Error("knodin search: limit must be a positive integer");
1688
+ let testScope = "all";
1689
+ if (rest.includes("--tests-only"))
1690
+ testScope = "test";
1691
+ else if (rest.includes("--production-only"))
1692
+ testScope = "production";
1408
1693
  result = await graphRead(() => engine.search(query, repo, limit, {
1409
1694
  languages: selectorValue("--languages")?.split(",").filter(Boolean),
1410
1695
  extensions: selectorValue("--extensions")?.split(",").filter(Boolean),
1411
1696
  kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
1412
1697
  path: selectorValue("--path"),
1413
- testScope: rest.includes("--tests-only")
1414
- ? "test"
1415
- : rest.includes("--production-only")
1416
- ? "production"
1417
- : "all",
1698
+ testScope,
1418
1699
  includeSource: !rest.includes("--no-source"),
1419
1700
  offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : 0,
1420
1701
  }));
@@ -1692,6 +1973,7 @@ async function main() {
1692
1973
  const rawRetention = selectorValue("--retention-days");
1693
1974
  const sinceOption = selectorValue("--since");
1694
1975
  const outputOption = selectorValue("--output");
1976
+ const previewId = selectorValue("--preview-id");
1695
1977
  const retentionDays = rawRetention === undefined ? 14 : Number(rawRetention);
1696
1978
  if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365)
1697
1979
  throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");
@@ -1699,10 +1981,12 @@ async function main() {
1699
1981
  throw new Error(`knodin diagnostics ${action}: unexpected bundle argument`);
1700
1982
  if (rawRetention !== undefined && action !== "enable")
1701
1983
  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`);
1984
+ if (sinceOption !== undefined && !["preview", "archive", "collect"].includes(action ?? ""))
1985
+ throw new Error(`knodin diagnostics ${action}: --since applies only to preview or archive`);
1986
+ if (outputOption !== undefined && !["archive", "collect"].includes(action ?? ""))
1987
+ throw new Error(`knodin diagnostics ${action}: --output applies only to archive`);
1988
+ if (previewId !== undefined && !["archive", "collect"].includes(action ?? ""))
1989
+ throw new Error(`knodin diagnostics ${action}: --preview-id applies only to archive`);
1706
1990
  if (action === "enable")
1707
1991
  result = enableDiagnostics(repo, retentionDays);
1708
1992
  else if (action === "status")
@@ -1716,29 +2000,45 @@ async function main() {
1716
2000
  throw new Error("knodin diagnostics inspect requires <bundle>");
1717
2001
  result = inspectDiagnosticsBundle(repo, bundlePath);
1718
2002
  }
1719
- else if (action === "collect") {
2003
+ else if (["preview", "archive", "collect"].includes(action ?? "")) {
1720
2004
  const since = sinceOption ?? "24h";
1721
- const match = /^(\d+)(h|d)$/.exec(since);
2005
+ const match = /^(\d+)([hd])$/.exec(since);
1722
2006
  if (!match)
1723
2007
  throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");
1724
2008
  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, {
2009
+ let graph;
2010
+ let doctor;
2011
+ try {
2012
+ graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
2013
+ }
2014
+ catch {
2015
+ // Support evidence remains available with an explicit unavailable graph section.
2016
+ }
2017
+ if (graph) {
2018
+ try {
2019
+ doctor = await diagnoseInstallation(repo, {
2020
+ currentVersion: KNODIN_VERSION,
2021
+ runtimeCommand: [...runtimeCommand, "serve"],
2022
+ graph,
2023
+ });
2024
+ }
2025
+ catch {
2026
+ // Installation diagnosis is represented as unavailable in the manifest.
2027
+ }
2028
+ }
2029
+ const options = {
1732
2030
  sinceHours,
1733
- outputPath: outputOption,
1734
2031
  doctor,
1735
2032
  graph,
1736
- telemetry: readTelemetryRecords(repo, undefined, Math.max(1, Math.ceil(sinceHours / 24))),
1737
2033
  knodinVersion: KNODIN_VERSION,
1738
- });
2034
+ };
2035
+ result =
2036
+ action === "preview"
2037
+ ? persistDiagnosticsPreview(repo, options)
2038
+ : collectDiagnostics(repo, { ...options, outputPath: outputOption, previewId });
1739
2039
  }
1740
2040
  else {
1741
- throw new Error("knodin diagnostics requires enable, status, collect, inspect, clear, or disable");
2041
+ throw new Error("knodin diagnostics requires enable, status, preview, archive, inspect, clear, or disable");
1742
2042
  }
1743
2043
  break;
1744
2044
  }
@@ -1806,15 +2106,19 @@ async function main() {
1806
2106
  : `${JSON.stringify(boundedResult)}\n`);
1807
2107
  process.exitCode = finalExitCode;
1808
2108
  }
1809
- main().catch((err) => {
2109
+ try {
2110
+ await main();
2111
+ }
2112
+ catch (err) {
1810
2113
  const argv = process.argv.slice(2);
1811
2114
  const repoIndex = argv.indexOf("--repo");
1812
2115
  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();
2116
+ const explicitRepo = repoIndex >= 0 ? argv[repoIndex + 1] : undefined;
2117
+ let candidate = process.cwd();
2118
+ if (explicitRepo)
2119
+ candidate = explicitRepo;
2120
+ else if (equalsRepo)
2121
+ candidate = equalsRepo.slice("--repo=".length);
1818
2122
  const command = argv.find((argument, index) => {
1819
2123
  if (argument.startsWith("-"))
1820
2124
  return false;
@@ -1846,6 +2150,7 @@ main().catch((err) => {
1846
2150
  "diagnostics",
1847
2151
  "system",
1848
2152
  "repos",
2153
+ "remote",
1849
2154
  "update",
1850
2155
  ]);
1851
2156
  const diagnostic = recordDiagnosticFailure(candidate, {
@@ -1857,4 +2162,4 @@ main().catch((err) => {
1857
2162
  const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
1858
2163
  console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
1859
2164
  process.exit(1);
1860
- });
2165
+ }