knodin 0.5.0

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,98 @@
1
+ import childProcess from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { attachLifecycleHealth, inspectLifecycleHealth } from "./lifecycle-health.js";
5
+ const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6
+ const PROCESSOR_RETRY_MS = 500;
7
+ function installedProcessorIsRunning(repo) {
8
+ try {
9
+ const pid = Number(fs.readFileSync(path.join(repo, ".reckon", "hooks", "refresh.lock", "pid"), "utf-8"));
10
+ if (!Number.isInteger(pid) || pid <= 0)
11
+ return false;
12
+ process.kill(pid, 0);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function launchInstalledLifecycleProcessor(repo) {
20
+ const processor = path.join(repo, ".reckon", "hooks", "background-index.sh");
21
+ if (fs.statSync(processor).mode & 0o111) {
22
+ const child = childProcess.spawn(processor, [], {
23
+ cwd: repo,
24
+ stdio: "ignore",
25
+ detached: process.platform !== "win32",
26
+ windowsHide: true,
27
+ });
28
+ child.once("error", () => undefined);
29
+ child.unref();
30
+ }
31
+ }
32
+ function requiresStructuralRepair(graph) {
33
+ if (Object.values(graph.orphaned).some((count) => count > 0))
34
+ return true;
35
+ const ordinary = [
36
+ "indexed file no longer exists: ",
37
+ "indexed file is no longer eligible: ",
38
+ "indexed snapshot differs from disk: ",
39
+ ];
40
+ return graph.missing.records.some((record) => !ordinary.some((prefix) => record.startsWith(prefix)));
41
+ }
42
+ function launchQueuedProcessorIfNeeded(repo, elapsed, timeoutMs, lastAttemptAt, processQueuedEvents) {
43
+ if (elapsed >= timeoutMs ||
44
+ inspectLifecycleHealth(repo).queuedEvents === 0 ||
45
+ installedProcessorIsRunning(repo) ||
46
+ elapsed - lastAttemptAt < PROCESSOR_RETRY_MS)
47
+ return lastAttemptAt;
48
+ try {
49
+ processQueuedEvents(repo);
50
+ }
51
+ catch {
52
+ // Lifecycle health retains the queued event and wait remains truthful.
53
+ }
54
+ return elapsed;
55
+ }
56
+ function completedStatus(graph) {
57
+ if (graph.status === "healthy" && graph.freshness.state === "fresh")
58
+ return "fresh";
59
+ if (graph.status === "repair-needed" && requiresStructuralRepair(graph))
60
+ return "repair-needed";
61
+ if (graph.freshness.state === "unknown")
62
+ return "unknown";
63
+ return null;
64
+ }
65
+ async function reconcileOrdinaryDrift(engine, repo, graph) {
66
+ const shouldReconcile = (graph.status === "stale" || graph.status === "repair-needed") &&
67
+ graph.freshness.state !== "queued";
68
+ if (!shouldReconcile)
69
+ return graph;
70
+ await engine.query("stats", "", repo);
71
+ return attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
72
+ }
73
+ function completedResult(status, startedAt, polls, graph) {
74
+ return { status, waitedMs: Date.now() - startedAt, polls, graph };
75
+ }
76
+ /** Wait for queued work and reconcile ordinary drift; structural repair stays explicit. */
77
+ export async function waitForFresh(engine, repo, timeoutMs = 30_000, processQueuedEvents = launchInstalledLifecycleProcessor) {
78
+ const startedAt = Date.now();
79
+ let polls = 0;
80
+ let lastProcessorAttemptAt = Number.NEGATIVE_INFINITY;
81
+ for (;;) {
82
+ polls++;
83
+ const beforeStatusElapsed = Date.now() - startedAt;
84
+ lastProcessorAttemptAt = launchQueuedProcessorIfNeeded(repo, beforeStatusElapsed, timeoutMs, lastProcessorAttemptAt, processQueuedEvents);
85
+ let graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "cached" }));
86
+ let status = completedStatus(graph);
87
+ if (status)
88
+ return completedResult(status, startedAt, polls, graph);
89
+ graph = await reconcileOrdinaryDrift(engine, repo, graph);
90
+ status = completedStatus(graph);
91
+ if (status)
92
+ return completedResult(status, startedAt, polls, graph);
93
+ const elapsed = Date.now() - startedAt;
94
+ if (elapsed >= timeoutMs)
95
+ return { status: "timeout", waitedMs: elapsed, polls, graph };
96
+ await delay(Math.min(100, timeoutMs - elapsed));
97
+ }
98
+ }
@@ -0,0 +1,231 @@
1
+ import childProcess from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ function git(repo, args) {
6
+ return childProcess.execFileSync("git", args, {
7
+ cwd: repo,
8
+ encoding: "utf-8",
9
+ stdio: ["ignore", "pipe", "pipe"],
10
+ });
11
+ }
12
+ function canonicalPath(target) {
13
+ try {
14
+ return fs.realpathSync(target);
15
+ }
16
+ catch {
17
+ return path.resolve(target);
18
+ }
19
+ }
20
+ function commonDirectory(repo) {
21
+ const value = git(repo, ["rev-parse", "--path-format=absolute", "--git-common-dir"]).trim();
22
+ return path.resolve(repo, value);
23
+ }
24
+ function registryPath(repo) {
25
+ return path.join(commonDirectory(repo), "knodin", "worktrees.json");
26
+ }
27
+ function emptyRegistry() {
28
+ return { schemaVersion: 1, graphState: "per-worktree", entries: [] };
29
+ }
30
+ function readRegistry(repo) {
31
+ try {
32
+ const parsed = JSON.parse(fs.readFileSync(registryPath(repo), "utf-8"));
33
+ return parsed.schemaVersion === 1 && Array.isArray(parsed.entries) ? parsed : emptyRegistry();
34
+ }
35
+ catch {
36
+ return emptyRegistry();
37
+ }
38
+ }
39
+ function writeRegistry(repo, registry) {
40
+ const target = registryPath(repo);
41
+ fs.mkdirSync(path.dirname(target), { recursive: true });
42
+ const temporary = `${target}.${process.pid}.tmp`;
43
+ fs.writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
44
+ fs.renameSync(temporary, target);
45
+ }
46
+ function identityFor(repo, worktreePath, classification) {
47
+ let administrativePath = classification;
48
+ try {
49
+ administrativePath = path.resolve(worktreePath, git(worktreePath, ["rev-parse", "--path-format=absolute", "--git-dir"]).trim());
50
+ }
51
+ catch {
52
+ // A removed worktree retains its registry identity.
53
+ }
54
+ return crypto
55
+ .createHash("sha256")
56
+ .update(`${commonDirectory(repo)}\0${administrativePath}`)
57
+ .digest("hex")
58
+ .slice(0, 24);
59
+ }
60
+ function listGitWorktrees(repo) {
61
+ const fields = git(repo, ["worktree", "list", "--porcelain", "-z"]).split("\0");
62
+ const records = [];
63
+ for (const field of fields) {
64
+ if (field.startsWith("worktree ")) {
65
+ records.push({
66
+ path: path.resolve(field.slice("worktree ".length)),
67
+ classification: records.length === 0 ? "main" : "linked",
68
+ head: null,
69
+ branch: null,
70
+ locked: false,
71
+ prunable: null,
72
+ });
73
+ continue;
74
+ }
75
+ const current = records.at(-1);
76
+ if (!current)
77
+ continue;
78
+ if (field.startsWith("HEAD "))
79
+ current.head = field.slice(5);
80
+ else if (field.startsWith("branch "))
81
+ current.branch = field.slice(7).replace(/^refs\/heads\//, "");
82
+ else if (field === "detached")
83
+ current.branch = null;
84
+ else if (field.startsWith("locked"))
85
+ current.locked = true;
86
+ else if (field.startsWith("prunable "))
87
+ current.prunable = field.slice(9);
88
+ }
89
+ return records;
90
+ }
91
+ /** Register one initialized checkout in Git-common metadata without sharing its graph database. */
92
+ export function registerInitializedWorktree(repoPath) {
93
+ const repo = path.resolve(repoPath);
94
+ const current = listGitWorktrees(repo).find((worktree) => canonicalPath(worktree.path) === canonicalPath(repo));
95
+ if (!current)
96
+ throw new Error(`knodin worktrees: ${repo} is not a registered Git worktree`);
97
+ const registry = readRegistry(repo);
98
+ const now = new Date().toISOString();
99
+ const identity = identityFor(repo, current.path, current.classification);
100
+ const existing = registry.entries.find((entry) => entry.identity === identity);
101
+ if (existing) {
102
+ existing.path = current.path;
103
+ existing.lastSeenAt = now;
104
+ delete existing.removedAt;
105
+ }
106
+ else {
107
+ registry.entries.push({
108
+ identity,
109
+ path: current.path,
110
+ classification: current.classification,
111
+ registeredAt: now,
112
+ lastSeenAt: now,
113
+ });
114
+ }
115
+ registry.entries.sort((left, right) => left.path.localeCompare(right.path));
116
+ writeRegistry(repo, registry);
117
+ }
118
+ export async function inspectWorktrees(repoPath, status) {
119
+ const repo = path.resolve(repoPath);
120
+ const current = listGitWorktrees(repo);
121
+ const registry = readRegistry(repo);
122
+ const livePaths = new Set(current.map((worktree) => worktree.path));
123
+ const worktrees = [];
124
+ for (const worktree of current) {
125
+ const identity = identityFor(repo, worktree.path, worktree.classification);
126
+ const registered = registry.entries.some((entry) => entry.identity === identity);
127
+ const graphInitialized = fs.existsSync(path.join(worktree.path, ".reckon", "db.sqlite"));
128
+ let indexedHead = null;
129
+ if (graphInitialized) {
130
+ try {
131
+ indexedHead = (await status(worktree.path)).lastIndexedHead || null;
132
+ }
133
+ catch {
134
+ indexedHead = null;
135
+ }
136
+ }
137
+ worktrees.push({
138
+ ...worktree,
139
+ identity,
140
+ registered,
141
+ graphInitialized,
142
+ indexedHead,
143
+ freshness: indexedHead === null || worktree.head === null
144
+ ? "unknown"
145
+ : indexedHead === worktree.head
146
+ ? "fresh"
147
+ : "stale",
148
+ removedAt: null,
149
+ });
150
+ }
151
+ for (const entry of registry.entries) {
152
+ if (livePaths.has(entry.path))
153
+ continue;
154
+ worktrees.push({
155
+ identity: entry.identity,
156
+ path: entry.path,
157
+ classification: entry.classification,
158
+ head: null,
159
+ branch: null,
160
+ locked: false,
161
+ prunable: "registered worktree is absent from Git's worktree inventory",
162
+ registered: true,
163
+ graphInitialized: fs.existsSync(path.join(entry.path, ".reckon", "db.sqlite")),
164
+ indexedHead: null,
165
+ freshness: "removed",
166
+ removedAt: entry.removedAt ?? null,
167
+ });
168
+ }
169
+ worktrees.sort((left, right) => left.path.localeCompare(right.path));
170
+ return { schemaVersion: 1, graphState: "per-worktree", worktrees };
171
+ }
172
+ /** Reconcile the durable registry with Git metadata; no checkout or graph is deleted. */
173
+ export function reconcileWorktrees(repoPath) {
174
+ const repo = path.resolve(repoPath);
175
+ const current = listGitWorktrees(repo);
176
+ const registry = readRegistry(repo);
177
+ const now = new Date().toISOString();
178
+ const seen = new Set();
179
+ const added = [];
180
+ const restored = [];
181
+ for (const worktree of current) {
182
+ const identity = identityFor(repo, worktree.path, worktree.classification);
183
+ seen.add(identity);
184
+ const existing = registry.entries.find((entry) => entry.identity === identity);
185
+ if (!existing) {
186
+ registry.entries.push({
187
+ identity,
188
+ path: worktree.path,
189
+ classification: worktree.classification,
190
+ registeredAt: now,
191
+ lastSeenAt: now,
192
+ });
193
+ added.push(worktree.path);
194
+ }
195
+ else {
196
+ if (existing.removedAt)
197
+ restored.push(worktree.path);
198
+ existing.path = worktree.path;
199
+ existing.lastSeenAt = now;
200
+ delete existing.removedAt;
201
+ }
202
+ }
203
+ const removed = [];
204
+ for (const entry of registry.entries) {
205
+ if (!seen.has(entry.identity) && !entry.removedAt) {
206
+ entry.removedAt = now;
207
+ removed.push(entry.path);
208
+ }
209
+ }
210
+ registry.entries.sort((left, right) => left.path.localeCompare(right.path));
211
+ writeRegistry(repo, registry);
212
+ return { schemaVersion: 1, added, restored, removed };
213
+ }
214
+ /** Remove exactly one linked worktree through Git, then retain removal evidence in the registry. */
215
+ export function removeManagedWorktree(repoPath, targetPath, dryRun) {
216
+ const repo = path.resolve(repoPath);
217
+ const target = path.resolve(targetPath);
218
+ const record = listGitWorktrees(repo).find((worktree) => canonicalPath(worktree.path) === canonicalPath(target));
219
+ if (!record)
220
+ throw new Error(`knodin worktrees remove: ${target} is not a registered Git worktree`);
221
+ if (record.classification === "main")
222
+ throw new Error("knodin worktrees remove: refusing to remove the main worktree");
223
+ if (dryRun)
224
+ return { schemaVersion: 1, target, dryRun: true, removed: false };
225
+ childProcess.execFileSync("git", ["worktree", "remove", target], {
226
+ cwd: repo,
227
+ stdio: ["ignore", "pipe", "pipe"],
228
+ });
229
+ reconcileWorktrees(repo);
230
+ return { schemaVersion: 1, target, dryRun: false, removed: true };
231
+ }
package/docs/CLI.md ADDED
@@ -0,0 +1,39 @@
1
+ # Declarative CLI contract
2
+
3
+ knodin's command grammar is defined once in `src/cli-model.ts` with Commander
4
+ 15.0.0. The model owns root and nested commands, positionals, global and local
5
+ options, numeric parsing, unknown-option rejection, and root or command help.
6
+ `bin/cli.ts` dispatches the validated invocation to the existing product
7
+ handlers so JSON output contracts and established commands remain compatible.
8
+
9
+ Run `knodin --help` for the root command inventory or append `--help` to any
10
+ declared command path, for example:
11
+
12
+ ```bash
13
+ knodin repos search --help
14
+ knodin update --help
15
+ ```
16
+
17
+ Help uses the active terminal width. Non-TTY output uses a deterministic
18
+ 100-column layout so snapshots, documentation, and automation do not depend on
19
+ the caller's environment. Narrow and wide formatting are covered by pure tests;
20
+ the Unix acceptance fixture also exercises a real pseudo-terminal when the
21
+ host provides one. Windows uses the same deterministic formatter and Commander
22
+ model without depending on a Unix `script` executable.
23
+
24
+ Commander is an exact production dependency rather than a floating range. Its
25
+ resolved integrity is pinned by both npm and Bun lockfiles and flows through
26
+ the normal SBOM, provenance, package-install, vulnerability, and signed-release
27
+ gates. It adds no transitive runtime dependencies. Updating it requires the
28
+ same lockfile review and full CLI compatibility suite as any parser change.
29
+
30
+ Command-specific semantic checks remain close to their product handlers (for
31
+ example repository existence, mutually exclusive configuration modes, and
32
+ graph-query target rules), but syntax cannot reach a handler unless the shared
33
+ declarative model accepts it first.
34
+
35
+ `knodin pack grep` evaluates user patterns with exact-pinned RE2 WebAssembly,
36
+ not Node's backtracking regular-expression engine. Backreferences and lookaround
37
+ are rejected because they cannot retain RE2's linear-time guarantee. Route
38
+ matching treats application path text literally and recognizes only complete
39
+ Express/NestJS-style `:name` or `{name}` path segments as parameters.
@@ -0,0 +1,194 @@
1
+ # Bounded diagnostic-output compression
2
+
3
+ knodin can deterministically compress already-produced build and test output
4
+ without running the command that created it. This capability is available from
5
+ the CLI and the single MCP gateway in post-0.3 development.
6
+
7
+ knodin deliberately does **not** expose `knodin run`. Portable command
8
+ execution remains excluded until the containment gate below can be proven.
9
+
10
+ ## Use it
11
+
12
+ Prefer a repository-relative file or stdin so the full log does not first cross
13
+ an agent's context boundary:
14
+
15
+ ```bash
16
+ build-command > build.log 2>&1
17
+ knodin compress build.log --exit-code 1 --lines 200
18
+
19
+ build-command 2>&1 | knodin compress - --exit-code 1 --json
20
+ ```
21
+
22
+ The same operation is available through the one-tool MCP gateway:
23
+
24
+ ```json
25
+ {
26
+ "operation": "compress",
27
+ "artifactPath": "build.log",
28
+ "exitCode": 1,
29
+ "lineBudget": 200,
30
+ "compressionByteBudget": 16384
31
+ }
32
+ ```
33
+
34
+ `artifactPath` is repository-bound and must be a regular, non-symlinked file.
35
+ Direct `text` and ordered `events` inputs support integrations and behavioral
36
+ parity, but they do not prove context savings if the raw value has already been
37
+ placed in the model's context.
38
+
39
+ The always-loaded MCP schema advertises the common path to stay within its
40
+ token ceiling. Advanced, still runtime-validated arguments are `events`,
41
+ `signal`, `strategy`, `adapter`, `contextLines`, `retain`, `redactSecrets`,
42
+ `maxInputBytes`, and `raw`. Use this canonical guide when composing them.
43
+
44
+ ## Contract
45
+
46
+ - Strategies: `smart`, `head-tail`, and `errors-only`.
47
+ - Adapters: generic fallback plus Vitest, Jest, pytest, Go test, Maven, Gradle,
48
+ .NET, and Cargo detection.
49
+ - Preserved metadata: original exit code and terminating signal.
50
+ - Preserved signals: detected errors, causes, stack frames, failing tests,
51
+ summaries, warnings, and bounded surrounding context.
52
+ - Hard limits: rendered content never exceeds the requested line count or
53
+ UTF-8 content-byte budget.
54
+ - Truthfulness: the status is `complete`, `compressed`, or
55
+ `insufficient-budget`; detected signals that cannot fit are listed rather
56
+ than silently described as preserved.
57
+ - Accounting: every omitted range has inclusive source-line bounds and its
58
+ exact source bytes, including original delimiters.
59
+ - Recovery: raw ordered events are retained locally by default and can be read
60
+ in bounded ranges without rerunning the command.
61
+
62
+ The default compact MCP result uses documented tuples:
63
+
64
+ - `exit`: `[exitCode, signal]`;
65
+ - `budget`: `[usedLines, lineLimit, usedUtf8Bytes, byteLimit]`;
66
+ - `fidelity`: `[detectedSignals, preservedSignals, unpreservedSignalIds]`;
67
+ - each `omitted` entry:
68
+ `[inclusiveStartLine, inclusiveEndLine, exactSourceBytes]`.
69
+
70
+ `artifactId` is the SHA-256 identity used with
71
+ `compressionAction: "read"`, `"diagnose"`, or `"delete"`. Request
72
+ `detailLevel: "standard"` for the fully named audit envelope.
73
+
74
+ CLI drill-down and cleanup are explicit:
75
+
76
+ ```bash
77
+ knodin compress read <artifact-id> --start 80 --end 140
78
+ knodin compress read <artifact-id> --raw
79
+ knodin compress delete <artifact-id>
80
+ ```
81
+
82
+ Human output prints an explicit fidelity warning when a budget cannot retain
83
+ every detected signal. `--json` emits the stable, fully named core result.
84
+
85
+ ## Connect a failure to code
86
+
87
+ After compression, diagnose the retained artifact without rerunning the failed
88
+ command:
89
+
90
+ ```bash
91
+ knodin compress diagnose <artifact-id> --limit 10 --context 2
92
+ knodin compress diagnose <artifact-id> --max-output-bytes 16384 --json
93
+ ```
94
+
95
+ The same graph read is available through the single MCP gateway:
96
+
97
+ ```json
98
+ {
99
+ "operation": "compress",
100
+ "compressionAction": "diagnose",
101
+ "artifactId": "<sha256>",
102
+ "limit": 10,
103
+ "contextLines": 2,
104
+ "compressionByteBudget": 16384,
105
+ "detailLevel": "standard"
106
+ }
107
+ ```
108
+
109
+ MCP callers may provide already-produced `text` instead of `artifactId`, but
110
+ must provide exactly one. Direct text is bounded and secret-redacted before
111
+ diagnosis. Retained artifacts are preferable because omitted regions remain
112
+ available locally and the raw log need not first enter the agent context.
113
+
114
+ Diagnosis recognizes source locations emitted by major JavaScript/TypeScript,
115
+ Python, Java, C#, Go, and Rust toolchains. Every candidate is resolved against
116
+ tracked files inside the current repository. Foreign CI checkout prefixes can
117
+ map by a unique tracked suffix; basename-only matches remain unresolved when
118
+ ambiguous. Traversal, symlinks outside the repository, untracked files, and
119
+ missing graph records are refused rather than guessed.
120
+
121
+ For each resolved location, the fully named result contains:
122
+
123
+ - the reported path, line, column, and original evidence line;
124
+ - the uniquely resolved tracked file and nearest package manifest;
125
+ - the smallest enclosing graph symbol, stable identity, signature, and source
126
+ span;
127
+ - related tests, upstream callers, and downstream dependencies;
128
+ - recent commits touching the file;
129
+ - a source snippet inside a hard aggregate UTF-8 byte budget; and
130
+ - the indexed commit, current commit, and freshness state.
131
+
132
+ The default compact MCP result preserves the same evidence with tuples:
133
+
134
+ - `package`: `[name, manifest, ecosystem]`;
135
+ - `owner`: `[identity, symbol, kind, startLine, endLine, signature]`;
136
+ - relation entries:
137
+ `[identity, symbol, file, kind, line, confidence]`;
138
+ - `recent`: `[shortCommit, authoredAt, subject]`; and
139
+ - `freshness`: `[state, indexedHead, currentHead]`.
140
+
141
+ `resolved` means every detected reference was mapped with complete input,
142
+ current graph evidence, and an untruncated context bundle. `partial` identifies
143
+ any omitted input, unresolved reference, stale graph, or exhausted context
144
+ budget. `unresolved` means no safe repository location was established.
145
+
146
+ The relationships are static diagnostic candidates, not proof of runtime
147
+ causality. Package ownership is evidenced by the nearest tracked
148
+ `package.json`, `pyproject.toml`, `pom.xml`, `Cargo.toml`, `go.mod`, or
149
+ `.csproj`; malformed or unnamed manifests remain explicit. Dynamic dispatch,
150
+ generated paths, source maps, and framework wiring can require additional
151
+ runtime evidence.
152
+
153
+ ## Local retention and redaction
154
+
155
+ Artifacts are stored beneath `.reckon/output/` with a private directory mode
156
+ and `0600` files. The default returned content and bounded reads redact common
157
+ credential forms and remove ANSI/control bytes. `--raw` is an explicit local
158
+ request and can reveal the original secrets. Redaction is defense in depth,
159
+ not a guarantee that every possible secret format is recognized.
160
+
161
+ Input is bounded to 16 MiB by default and cannot be configured above 64 MiB.
162
+ Oversized input is rejected rather than partially retained. Output artifacts
163
+ are content-addressed, reads are byte-bounded, deletion requires an exact
164
+ artifact identity, and symlinked storage paths are refused.
165
+
166
+ ## Command-execution security gate
167
+
168
+ The safe compressor processes data only; it does not invoke a shell or child
169
+ process. A future runner must address this threat model with executable,
170
+ platform-specific evidence:
171
+
172
+ | Threat | Required proof before shipping a runner |
173
+ |---|---|
174
+ | Shell injection | Argument-vector execution by default; no implicit shell |
175
+ | Repository prompt injection | Repository text cannot alter executable or policy authorization |
176
+ | Arbitrary filesystem access | Repository-root binding plus an enforceable filesystem sandbox |
177
+ | Network access and exfiltration | Deny-by-default network containment where claimed |
178
+ | Environment or credential leakage | Minimal allowlisted environment and no ambient credentials |
179
+ | Child-process escape | Complete descendant tracking and termination |
180
+ | Timeout | Process-tree termination, not only parent termination |
181
+ | CPU, memory, disk, output exhaustion | Enforced resource and output caps |
182
+ | Symlink or path traversal | Realpath containment and race-resistant file handling |
183
+ | Destructive commands | Allowlisted profiles or per-call approval with an explicit audit record |
184
+ | Cross-worktree effects | Authorized worktree binding and named affected roots |
185
+ | Telemetry leakage | No command text or raw output in default telemetry; redact before opt-in export |
186
+ | Platform differences | Separate verified guarantees and explicit unsupported cases |
187
+
188
+ Node's cross-platform child-process API alone cannot impose a hard memory limit
189
+ on an arbitrary executable, Windows job-object behavior differs from POSIX
190
+ process groups, and portable network/filesystem containment requires more than
191
+ an output buffer and timeout. Until a reviewed native or operating-system
192
+ containment adapter proves the table above, command execution is an intentional
193
+ safety non-goal. Existing trusted execution tools can write a log and pass that
194
+ artifact to knodin for bounded compression.
@@ -0,0 +1,27 @@
1
+ # Dead code and impact
2
+
3
+ `dead_code` reports static candidates, not deletion proof. A zero resolved
4
+ reference count means the current indexed static graph found no supported
5
+ reference. It does not prove that reflection, runtime dependency injection,
6
+ framework configuration, metadata, deployed infrastructure, or external
7
+ consumers cannot invoke the code.
8
+
9
+ Impact follows source-evidenced resolved relationships. High-confidence
10
+ incoming cross-repository relationships can make a dependency visible to
11
+ impact and dead-code analysis. Heuristic or unresolved relationships lower
12
+ deletion confidence; they must not falsely mark code live or safe.
13
+
14
+ Salesforce Apex, Flow, metadata, LWC/Aura/Visualforce, scheduled/platform
15
+ entrypoints, and deployed-org dependencies require platform corroboration.
16
+ Configuration-driven frameworks and generated registries have similar limits.
17
+
18
+ Before deletion:
19
+
20
+ 1. run `knodin query impact <symbol> --direction upstream`;
21
+ 2. inspect `tests_for`, relevant contracts, and system relationships;
22
+ 3. verify index health and freshness;
23
+ 4. investigate unresolved or heuristic incoming evidence;
24
+ 5. use platform/runtime evidence where static analysis is incomplete.
25
+
26
+ An unavailable, uninitialized, empty, unhealthy, or partially omitted
27
+ repository cannot produce an ordinary safe zero-result answer.
@@ -0,0 +1,84 @@
1
+ # Doctor and update awareness
2
+
3
+ `knodin doctor` reports:
4
+
5
+ - package, CLI, source, and MCP server version agreement;
6
+ - current Node runtime and npm prefix;
7
+ - manager ownership with confidence and evidence;
8
+ - resolved executable, symlink/shim chain, and duplicate candidates;
9
+ - supported agent configuration candidates;
10
+ - a real MCP initialize/tools-list result;
11
+ - Git-hook routing and refresh health;
12
+ - graph health;
13
+ - configured registry/channel;
14
+ - cached latest **trusted** release state; and
15
+ - the manager-safe action boundary when a signed update is known.
16
+
17
+ A manager is reported as known only from manager-owned environment evidence or
18
+ an identified shim. A path substring is labeled a medium-confidence heuristic,
19
+ not proof.
20
+
21
+ The former npm `latest` lookup has been removed from production paths. Doctor,
22
+ CLI status, and MCP status now consume the same signed-only state as:
23
+
24
+ ```bash
25
+ knodin update status
26
+ knodin update check
27
+ knodin update explain
28
+ knodin update apply
29
+ knodin update rollback
30
+ ```
31
+
32
+ Until the production root ceremony embeds an independently reviewed root and
33
+ pin, these commands report `trust-unconfigured` and make no request. A user
34
+ configuration file containing both a root path and its digest is deliberately
35
+ inert: accepting both from one mutable file would not be independent pinning.
36
+
37
+ After activation, an interactive ordinary command may atomically claim a due
38
+ background check. The foreground command does not wait for the network.
39
+ Metadata requests have a 512 KiB per-role cap, a 100 ms–10 s configured
40
+ timeout, no redirects, and remain within one approved HTTPS origin and path.
41
+ Disable checks or all network access with:
42
+
43
+ ```bash
44
+ RECKON_UPDATE_CHECK=0 knodin doctor
45
+ RECKON_OFFLINE=1 knodin update check
46
+ ```
47
+
48
+ Requests contain fixed metadata or signed target paths only. They never contain
49
+ a repository name, source path, symbol, command, username, or telemetry. A
50
+ failed check retains the last verified monotonic state but reports
51
+ `check-failed`; it never turns failure into `up-to-date`.
52
+
53
+ Apply is opt-in and fails closed. It verifies threshold-signed metadata,
54
+ release age/channel/version policy, artifact length and SHA-256, a signed
55
+ provenance digest, configured channel cross-checks, and both candidate and
56
+ rollback artifacts before any manager action. Artifacts are quarantined with
57
+ private permissions, fresh exclusive paths, and a symlink-rejecting quarantine
58
+ root. Every enterprise-only primary or cross-check origin must be explicitly
59
+ approved. Only a plain npm-owned install currently has an exact local-artifact
60
+ adapter; mise, Volta, nvm/fnm/asdf, Homebrew, and unknown ownership remain
61
+ `manager-action-required` until their rollback-safe adapters are certified. No
62
+ manager is ever asked to resolve a mutable version or tag, and a rollback is not
63
+ reported successful until the restored installation passes its health check.
64
+
65
+ Automatic activation also requires a certified sandbox smoke test. None is
66
+ enabled in the production CLI yet, so `apply-patch`/`apply-minor` policies stop
67
+ after verified quarantine instead of executing untrusted code. C64 supplies
68
+ the provenance/SBOM and five-channel attestation machinery; the production
69
+ release must populate it, and C65 must supply compromised-channel and recovery
70
+ evidence before this gate can open.
71
+
72
+ Run `knodin status --deep` for graph details and `knodin repair` for surgical
73
+ repair. Re-run `knodin init` after changing the executable owner or MCP path.
74
+
75
+ ## Query availability
76
+
77
+ Every graph read reports an explicit availability outcome. `no-match` means a
78
+ healthy graph was queried and produced no match. It is distinct from
79
+ `not-initialized`, `empty-repository`, `empty-index`, `repair-needed`,
80
+ `indexing`, and `unknown`; those states fail closed and include remediation
81
+ instead of returning an ordinary zero. An otherwise complete graph with broken
82
+ or displaced lifecycle hooks remains queryable but reports
83
+ `lifecycle-degraded`, so callers can use current evidence without mistaking its
84
+ refresh path for healthy.