knodin 0.6.0 → 0.7.4

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 (49) hide show
  1. package/README.md +26 -3
  2. package/dist/bin/cli.js +168 -12
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +3 -1
  5. package/dist/src/cli-args.js +8 -1
  6. package/dist/src/cli-model.js +35 -1
  7. package/dist/src/codeflow-replay.js +80 -0
  8. package/dist/src/competitive-cold-mcp.js +40 -0
  9. package/dist/src/competitive-manifest.js +106 -25
  10. package/dist/src/competitive-runner.js +37 -3
  11. package/dist/src/competitive-sandbox.js +1 -1
  12. package/dist/src/diagnostics.js +449 -0
  13. package/dist/src/engine/git-history.js +289 -0
  14. package/dist/src/engine/index.js +417 -70
  15. package/dist/src/engine/scip-import.js +408 -0
  16. package/dist/src/execution-profile.js +203 -0
  17. package/dist/src/failure-diagnosis.js +69 -10
  18. package/dist/src/hook-manager-integration.js +156 -0
  19. package/dist/src/init.js +319 -33
  20. package/dist/src/lifecycle-health.js +42 -4
  21. package/dist/src/output-telemetry.js +4 -0
  22. package/dist/src/progressive-evidence.js +473 -0
  23. package/dist/src/pure-compression-cli.js +101 -0
  24. package/dist/src/release-preflight.js +510 -0
  25. package/dist/src/repository-management.js +142 -0
  26. package/dist/src/response-budget.js +11 -1
  27. package/dist/src/server.js +22 -2
  28. package/dist/src/structural-fast-path.js +338 -0
  29. package/dist/src/structural-snapshot.js +33 -0
  30. package/dist/src/tools/knodin-tools.js +105 -14
  31. package/dist/src/update-ceremony.js +158 -0
  32. package/docs/CLI.md +22 -0
  33. package/docs/COMMAND-OUTPUT-COMPRESSION.md +31 -15
  34. package/docs/CONTAINED-EXECUTION.md +77 -0
  35. package/docs/DIAGNOSTICS.md +45 -0
  36. package/docs/DOCTOR-AND-UPDATES.md +5 -2
  37. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  38. package/docs/MCP.md +15 -0
  39. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  40. package/docs/REPOSITORIES-AND-WORKTREES.md +30 -0
  41. package/docs/SCIP-IMPORT.md +57 -0
  42. package/docs/SIGNED-UPDATES.md +5 -0
  43. package/docs/TELEMETRY.md +4 -0
  44. package/docs/releases/0.7.0.md +24 -0
  45. package/docs/releases/0.7.1.md +21 -0
  46. package/docs/releases/0.7.2.md +21 -0
  47. package/docs/releases/0.7.3.md +23 -0
  48. package/docs/releases/0.7.4.md +17 -0
  49. package/package.json +34 -2
@@ -16,13 +16,15 @@ import { getDocSection, listDocTopics } from "../docs-sections.js";
16
16
  import { diagnoseInstallation } from "../doctor.js";
17
17
  import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
18
18
  import { measurePerfPhaseSync } from "../engine/perf.js";
19
+ import { runExecutionProfile } from "../execution-profile.js";
19
20
  import { diagnoseFailure, } from "../failure-diagnosis.js";
20
21
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
21
22
  import { inspectRepositoryIntegrationStatus } from "../init.js";
22
- import { attachLifecycleHealth } from "../lifecycle-health.js";
23
+ import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
23
24
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
24
25
  import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
25
26
  import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
27
+ import { deliverProgressiveEvidence, } from "../progressive-evidence.js";
26
28
  import { createRepairPlan } from "../repair-progress.js";
27
29
  import { runRepositoryInitializationProcess } from "../repository-init-process.js";
28
30
  import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
@@ -31,7 +33,18 @@ import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeFor
31
33
  import { trustedUpdateStatus } from "../update-policy.js";
32
34
  import { waitForFresh } from "../wait-for-fresh.js";
33
35
  import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../worktree-lifecycle.js";
34
- const engine = createEngine();
36
+ let initializedEngine = null;
37
+ function gatewayEngine() {
38
+ initializedEngine ??= createEngine();
39
+ return initializedEngine;
40
+ }
41
+ /** Lazy engine facade: schema/docs/compression startup does not open graph state. */
42
+ const engine = new Proxy({}, {
43
+ get(_target, property) {
44
+ const value = gatewayEngine()[property];
45
+ return typeof value === "function" ? value.bind(gatewayEngine()) : value;
46
+ },
47
+ });
35
48
  function gatewayCliCommand() {
36
49
  const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
37
50
  const entry = nodePath.resolve(nodePath.dirname(fileURLToPath(import.meta.url)), `../../bin/cli.${extension}`);
@@ -44,7 +57,9 @@ function gatewayCliCommand() {
44
57
  }
45
58
  /** Release the gateway engine's local watcher and database resources. */
46
59
  export async function closeKnodinToolEngine() {
47
- await engine.close();
60
+ if (initializedEngine)
61
+ await initializedEngine.close();
62
+ initializedEngine = null;
48
63
  compactReadyRepos.clear();
49
64
  }
50
65
  const localTelemetry = [];
@@ -154,7 +169,9 @@ function buildDocumentedKnodinTools() {
154
169
  "docs",
155
170
  "doctor",
156
171
  "pack",
172
+ "evidence",
157
173
  "compress",
174
+ "execute",
158
175
  "status",
159
176
  "wait",
160
177
  "worktrees",
@@ -163,7 +180,12 @@ function buildDocumentedKnodinTools() {
163
180
  "system",
164
181
  "telemetry",
165
182
  ],
166
- description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
183
+ description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
184
+ },
185
+ profile: {
186
+ type: "string",
187
+ pattern: "^[a-z][a-z0-9_-]{0,63}$",
188
+ description: "execute only: immutable repository-defined profile identity. The caller cannot supply executable, argv, cwd, environment, or limits.",
167
189
  },
168
190
  symbol: {
169
191
  type: "string",
@@ -177,6 +199,24 @@ function buildDocumentedKnodinTools() {
177
199
  type: "string",
178
200
  description: "Repo-relative definition file selector.",
179
201
  },
202
+ evidenceLevel: {
203
+ type: "string",
204
+ enum: ["locate", "outline", "evidence", "expand"],
205
+ description: "evidence only: deterministic progressive-delivery level.",
206
+ },
207
+ continuation: {
208
+ type: "string",
209
+ description: "evidence only: integrity-checked continuation returned by the prior level/page.",
210
+ },
211
+ baselineHash: {
212
+ type: "string",
213
+ description: "evidence only: SHA-256 of the complete baseline; omission requires an exact current match.",
214
+ },
215
+ baselineBytes: {
216
+ type: "number",
217
+ minimum: 0,
218
+ description: "evidence only: complete baseline UTF-8 byte length; required with baselineHash.",
219
+ },
180
220
  kind: {
181
221
  type: "string",
182
222
  description: "Definition kind selector (function, class, method, etc.).",
@@ -622,16 +662,16 @@ function buildDocumentedKnodinTools() {
622
662
  export function getDocumentedKnodinToolsForSchemaProof() {
623
663
  return buildDocumentedKnodinTools();
624
664
  }
625
- const COMPACT_TOOL_DESCRIPTION = "Local code intelligence with freshness evidence. Start with context; use docs for guidance.";
665
+ const COMPACT_TOOL_DESCRIPTION = "Local code intelligence.";
626
666
  const COMPACT_PARAMETER_DESCRIPTIONS = {
627
- operation: "Capability to run. Use docs for complete operation guidance.",
628
- symbol: "Target symbol or file; exact meaning depends on operation.",
629
- pattern: "Query pattern; applies only when operation is query.",
630
- impactMode: "Impact by stable symbol identity or explicitly labeled files.",
631
- apply: "Apply rename_preview edits; false returns a dry-run.",
632
- diffScope: "Review scope: unstaged, staged, all, or compare.",
633
- section: "Docs section; quickstart includes the complete parameter reference.",
634
- persistTelemetry: "Persist metadata-only telemetry locally; never source.",
667
+ operation: "Capability.",
668
+ symbol: "Target.",
669
+ pattern: "Query pattern.",
670
+ impactMode: "Symbol or file impact.",
671
+ apply: "Apply verified edits.",
672
+ diffScope: "Review diff scope.",
673
+ section: "Documentation section.",
674
+ persistTelemetry: "Persist metadata telemetry.",
635
675
  };
636
676
  function compactParameterDescription(name, description) {
637
677
  void description;
@@ -881,6 +921,23 @@ function compactExplainSource(result) {
881
921
  staleness: result.staleness,
882
922
  };
883
923
  }
924
+ function handleProgressiveEvidenceOperation(repo, args, observe) {
925
+ if (!args.file || !args.evidenceLevel)
926
+ throw new Error("knodin evidence requires `file` and `evidenceLevel`");
927
+ return observe(deliverProgressiveEvidence({
928
+ repo,
929
+ file: args.file,
930
+ level: args.evidenceLevel,
931
+ continuation: args.continuation,
932
+ baselineHash: args.baselineHash,
933
+ baselineBytes: args.baselineBytes,
934
+ startLine: args.startLine,
935
+ endLine: args.endLine,
936
+ byteLimit: args.byteBudget,
937
+ tokenLimit: args.tokenBudget,
938
+ itemLimit: args.itemBudget,
939
+ }), "evidence");
940
+ }
884
941
  function compactCompression(result) {
885
942
  return {
886
943
  status: result.status,
@@ -958,6 +1015,9 @@ function compactFailureDiagnosis(result) {
958
1015
  unresolved: result.unresolved,
959
1016
  context: result.contextBundle,
960
1017
  freshness: [result.freshness.state, result.freshness.indexedHead, result.freshness.currentHead],
1018
+ confidence: result.confidence,
1019
+ omissions: result.omissions,
1020
+ telemetry: result.telemetry,
961
1021
  limitations: result.limitations,
962
1022
  };
963
1023
  }
@@ -973,6 +1033,7 @@ async function handleCompressionDiagnosis(repo, args, engine, graphRead) {
973
1033
  artifactId: args.artifactId,
974
1034
  text: args.text,
975
1035
  maxDiagnostics: args.limit ?? args.itemBudget,
1036
+ diagnosticOffset: args.startLine,
976
1037
  contextLines: args.contextLines,
977
1038
  contextByteBudget: args.compressionByteBudget,
978
1039
  });
@@ -1042,6 +1103,29 @@ async function handleCompressionOperation(repo, args, engine, observe, bounded,
1042
1103
  throw new Error("knodin compress: invalid compressionAction");
1043
1104
  }
1044
1105
  }
1106
+ async function handleExecutionOperation(repo, profile, engine, bounded) {
1107
+ if (!profile)
1108
+ throw new Error("knodin execute requires `profile`");
1109
+ const execution = await runExecutionProfile({ repo, profile });
1110
+ let diagnosis = null;
1111
+ if (execution.output?.artifact.retained && execution.status !== "completed") {
1112
+ try {
1113
+ diagnosis = await diagnoseFailure(engine, repo, {
1114
+ artifactId: execution.output.artifact.id,
1115
+ maxDiagnostics: 10,
1116
+ contextLines: 2,
1117
+ contextByteBudget: 16_384,
1118
+ });
1119
+ }
1120
+ catch (error) {
1121
+ diagnosis = {
1122
+ status: "unavailable",
1123
+ reason: error instanceof Error ? error.message : String(error),
1124
+ };
1125
+ }
1126
+ }
1127
+ return bounded({ execution, diagnosis }, "execute", true);
1128
+ }
1045
1129
  async function runCompactExplain(symbol, repo, selector, file, byteBudget) {
1046
1130
  const result = await engine.explain(symbol, repo, "minimal", selector);
1047
1131
  let sourceFile = file;
@@ -1159,6 +1243,9 @@ export async function handleKnodinTool(args) {
1159
1243
  root.telemetry?.truncated === true,
1160
1244
  schemaTokens: gatewaySchemaTokens(),
1161
1245
  });
1246
+ if (op === "execute" && parsedArgs.profile) {
1247
+ telemetry.executionProfile = parsedArgs.profile;
1248
+ }
1162
1249
  telemetry.temperature = localTelemetry.some(({ operation, repositoryId }) => operation === op && repositoryId === telemetry.repositoryId)
1163
1250
  ? "warm"
1164
1251
  : "cold";
@@ -1237,7 +1324,7 @@ export async function handleKnodinTool(args) {
1237
1324
  case "repair":
1238
1325
  if (repairPlan)
1239
1326
  return bounded(createRepairPlan(await engine.status(repo, { audit: "deep" })), "repair:plan");
1240
- return engine.repair(repo).then((result) => bounded(result, "repair"));
1327
+ return bounded(attachRepairLifecycle(repo, await engine.repair(repo)), "repair");
1241
1328
  case "repositories": {
1242
1329
  if (!repositoryAction)
1243
1330
  throw new Error("knodin repositories requires `repositoryAction`");
@@ -1273,6 +1360,10 @@ export async function handleKnodinTool(args) {
1273
1360
  }
1274
1361
  case "compress":
1275
1362
  return handleCompressionOperation(repo, parsedArgs, engine, observe, bounded, graphRead);
1363
+ case "execute":
1364
+ return handleExecutionOperation(repo, parsedArgs.profile, engine, bounded);
1365
+ case "evidence":
1366
+ return handleProgressiveEvidenceOperation(repo, parsedArgs, observe);
1276
1367
  case "pack": {
1277
1368
  if (packAction !== undefined && !["export", "read", "grep"].includes(packAction))
1278
1369
  throw new Error("knodin pack: invalid packAction");
@@ -0,0 +1,158 @@
1
+ import { createHash, createPublicKey } from "node:crypto";
2
+ import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
3
+ const SHA256 = /^[0-9a-f]{64}$/;
4
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$/;
5
+ function exactKeys(value, expected, label) {
6
+ const actual = Object.keys(value).sort();
7
+ const wanted = [...expected].sort();
8
+ if (actual.join("\0") !== wanted.join("\0"))
9
+ throw new Error(`${label} contains missing or unknown fields`);
10
+ }
11
+ function rejectPrivateMaterial(value, path = "input") {
12
+ if (typeof value === "string" && /PRIVATE KEY|recovery secret|seed phrase/i.test(value))
13
+ throw new Error(`${path} contains private material`);
14
+ if (!value || typeof value !== "object")
15
+ return;
16
+ for (const [key, child] of Object.entries(value)) {
17
+ if (/private|secret|seed|mnemonic|password|token/i.test(key))
18
+ throw new Error(`${path}.${key} is forbidden`);
19
+ rejectPrivateMaterial(child, `${path}.${key}`);
20
+ }
21
+ }
22
+ export function validateRootCeremonyManifest(value) {
23
+ rejectPrivateMaterial(value);
24
+ if (!value || typeof value !== "object" || Array.isArray(value))
25
+ throw new Error("manifest must be an object");
26
+ const manifest = value;
27
+ exactKeys(manifest, [
28
+ "schemaVersion",
29
+ "ceremonyId",
30
+ "purpose",
31
+ "productionActivation",
32
+ "custodians",
33
+ "roleKeyIds",
34
+ "thresholds",
35
+ ], "manifest");
36
+ if (manifest.schemaVersion !== 1 ||
37
+ !ID.test(manifest.ceremonyId) ||
38
+ !["rehearsal", "production"].includes(manifest.purpose) ||
39
+ manifest.productionActivation !== false)
40
+ throw new Error("invalid fail-closed ceremony header");
41
+ if (!Array.isArray(manifest.custodians) ||
42
+ manifest.custodians.length < 3 ||
43
+ new Set(manifest.custodians.map((c) => c.id)).size !== manifest.custodians.length ||
44
+ manifest.custodians.some((c) => {
45
+ exactKeys(c, c.role === "root-signer"
46
+ ? ["id", "role", "publicKeyId"]
47
+ : ["id", "role", "recoveryKeyIds", "evidenceId"], "custodian");
48
+ return (!ID.test(c.id) ||
49
+ !["root-signer", "recovery"].includes(c.role) ||
50
+ (c.role === "root-signer" && !SHA256.test(c.publicKeyId ?? "")) ||
51
+ (c.role === "recovery" &&
52
+ (!ID.test(c.evidenceId ?? "") ||
53
+ !Array.isArray(c.recoveryKeyIds) ||
54
+ c.recoveryKeyIds.some((id) => !SHA256.test(id)))));
55
+ }))
56
+ throw new Error("at least three distinct named custodians are required");
57
+ const roles = ["root", "targets", "snapshot", "timestamp"];
58
+ exactKeys(manifest.roleKeyIds, roles, "roleKeyIds");
59
+ exactKeys(manifest.thresholds, roles, "thresholds");
60
+ const all = new Set();
61
+ for (const role of roles) {
62
+ const ids = manifest.roleKeyIds?.[role];
63
+ const threshold = manifest.thresholds?.[role];
64
+ if (!Array.isArray(ids) ||
65
+ ids.length === 0 ||
66
+ ids.some((id) => !SHA256.test(id) || all.has(id)))
67
+ throw new Error("role keys must be valid and disjoint");
68
+ for (const id of ids)
69
+ all.add(id);
70
+ if (!Number.isInteger(threshold) ||
71
+ threshold < (role === "root" || role === "targets" ? 2 : 1) ||
72
+ threshold > ids.length)
73
+ throw new Error(`invalid ${role} threshold`);
74
+ }
75
+ if (manifest.custodians.filter((c) => c.role === "root-signer").length < manifest.thresholds.root ||
76
+ !manifest.custodians.some((c) => c.role === "recovery"))
77
+ throw new Error("custodian roles do not satisfy threshold and recovery separation");
78
+ const assigned = manifest.custodians
79
+ .filter((c) => c.role === "root-signer")
80
+ .map((c) => c.publicKeyId)
81
+ .sort();
82
+ if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort().join("\0"))
83
+ throw new Error("root signer assignments must exactly match root role keys");
84
+ const recovery = manifest.custodians.filter((c) => c.role === "recovery");
85
+ if (recovery.length < manifest.thresholds.root ||
86
+ new Set(recovery.map((c) => c.evidenceId)).size !== recovery.length ||
87
+ recovery.some((c) => !c.recoveryKeyIds?.length ||
88
+ new Set(c.recoveryKeyIds).size !== c.recoveryKeyIds.length ||
89
+ c.recoveryKeyIds.length >= manifest.thresholds.root))
90
+ throw new Error("recovery sets must preserve root threshold independence");
91
+ const recovered = new Set(recovery.flatMap((c) => c.recoveryKeyIds ?? []));
92
+ if (manifest.roleKeyIds.root.some((id) => !recovered.has(id)))
93
+ throw new Error("recovery assignments must cover every root role key");
94
+ return manifest;
95
+ }
96
+ export function validateRootPinReviewReceipt(value, manifest) {
97
+ rejectPrivateMaterial(value);
98
+ if (!value || typeof value !== "object" || Array.isArray(value))
99
+ throw new Error("receipt must be an object");
100
+ const receipt = value;
101
+ exactKeys(receipt, [
102
+ "schemaVersion",
103
+ "ceremonyId",
104
+ "rootEnvelopeSha256",
105
+ "reviewerId",
106
+ "reviewedAt",
107
+ "decision",
108
+ "authentication",
109
+ "productionActivation",
110
+ ], "receipt");
111
+ if (receipt.schemaVersion !== 1 ||
112
+ receipt.ceremonyId !== manifest.ceremonyId ||
113
+ !SHA256.test(receipt.rootEnvelopeSha256) ||
114
+ !ID.test(receipt.reviewerId) ||
115
+ manifest.custodians.some((c) => c.id === receipt.reviewerId) ||
116
+ receipt.decision !== "approved" ||
117
+ receipt.authentication !== "rehearsal-unsigned" ||
118
+ receipt.productionActivation !== false ||
119
+ !Number.isFinite(Date.parse(receipt.reviewedAt)) ||
120
+ new Date(receipt.reviewedAt).toISOString() !== receipt.reviewedAt)
121
+ throw new Error("invalid or non-independent pin-review receipt");
122
+ return receipt;
123
+ }
124
+ export function preparePublicRootImport(envelope, pin, manifestValue, receiptValue, now = new Date()) {
125
+ rejectPrivateMaterial(envelope, "rootEnvelope");
126
+ const manifest = validateRootCeremonyManifest(manifestValue);
127
+ if (manifest.purpose === "production")
128
+ throw new Error("production import requires externally authenticated review and remains blocked");
129
+ const receipt = validateRootPinReviewReceipt(receiptValue, manifest);
130
+ if (Date.parse(receipt.reviewedAt) > now.getTime())
131
+ throw new Error("pin-review receipt cannot be future-dated");
132
+ const roles = ["root", "targets", "snapshot", "timestamp"];
133
+ for (const role of roles) {
134
+ const expected = manifest.roleKeyIds[role];
135
+ const actual = envelope.signed.roles[role];
136
+ if (actual.threshold !== manifest.thresholds[role] ||
137
+ [...actual.keyids].sort().join("\0") !== [...expected].sort().join("\0"))
138
+ throw new Error(`manifest ${role} role does not match root envelope`);
139
+ }
140
+ for (const key of Object.values(envelope.signed.keys)) {
141
+ let canonical = "";
142
+ try {
143
+ canonical = createPublicKey(key.keyval.public)
144
+ .export({ type: "spki", format: "pem" })
145
+ .toString();
146
+ }
147
+ catch {
148
+ /* fail below */
149
+ }
150
+ if (canonical !== key.keyval.public)
151
+ throw new Error("root envelope keys must use canonical public SPKI PEM only");
152
+ }
153
+ const digest = createHash("sha256").update(canonicalizeUpdateMetadata(envelope)).digest("hex");
154
+ if (pin !== digest || receipt.rootEnvelopeSha256 !== digest)
155
+ throw new Error("root envelope, pin, and independent review receipt disagree");
156
+ verifyUpdateRootChain(envelope, pin, [], now);
157
+ return { rootEnvelope: envelope, rootEnvelopeSha256: digest, productionActivation: false };
158
+ }
package/docs/CLI.md CHANGED
@@ -32,6 +32,28 @@ example repository existence, mutually exclusive configuration modes, and
32
32
  graph-query target rules), but syntax cannot reach a handler unless the shared
33
33
  declarative model accepts it first.
34
34
 
35
+ For support evidence, `knodin diagnostics enable|status|collect|inspect|clear|disable`
36
+ manages an explicit local failure journal and redacted gzip JSON bundles.
37
+ Collection does not upload data. See `docs/DIAGNOSTICS.md` for retention,
38
+ redaction, inspection, and known bounds.
39
+
40
+ `knodin index --scip <file>` explicitly imports a bounded local SCIP protobuf
41
+ snapshot. The flag is never implied by `init` or ordinary indexing, and no
42
+ producer, daemon, account, credential, or network service is started. See
43
+ `docs/SCIP-IMPORT.md` for the fact, freshness, path, resource, conflict, and
44
+ known producer/language bounds.
45
+
46
+ `knodin evidence <locate|outline|evidence|expand> <file>` progressively returns
47
+ exact local source and stable continuations. A returned evidence handle plus
48
+ `--baseline-hash` and `--baseline-bytes` permits omission only on an exact
49
+ current-file match; all other baseline states return the required source. See
50
+ `docs/PROGRESSIVE-EVIDENCE.md` for the protocol and limitations.
51
+
52
+ `knodin review` returns separately itemized graph impact, test gaps, structural
53
+ centrality, churn, co-change, and coupling evidence. Git history has explicit
54
+ commit/file/time bounds and truthful unavailable or truncation states; see
55
+ `docs/GIT-HISTORY-REVIEW.md`.
56
+
35
57
  `knodin pack grep` evaluates user patterns with exact-pinned RE2 WebAssembly,
36
58
  not Node's backtracking regular-expression engine. Backreferences and lookaround
37
59
  are rejected because they cannot retain RE2's linear-time guarantee. Route
@@ -4,8 +4,9 @@ knodin can deterministically compress already-produced build and test output
4
4
  without running the command that created it. This capability is available from
5
5
  the CLI and the single MCP gateway in post-0.3 development.
6
6
 
7
- knodin deliberately does **not** expose `knodin run`. Portable command
8
- execution remains excluded until the containment gate below can be proven.
7
+ knodin deliberately does **not** expose `knodin run` or accept command strings.
8
+ The separate `execute` MCP operation is limited to doubly enabled immutable
9
+ profiles and the certified platform boundary documented below.
9
10
 
10
11
  ## Use it
11
12
 
@@ -90,6 +91,7 @@ command:
90
91
  ```bash
91
92
  knodin compress diagnose <artifact-id> --limit 10 --context 2
92
93
  knodin compress diagnose <artifact-id> --max-output-bytes 16384 --json
94
+ knodin compress diagnose <artifact-id> --offset 10 --limit 10 --json
93
95
  ```
94
96
 
95
97
  The same graph read is available through the single MCP gateway:
@@ -100,6 +102,7 @@ The same graph read is available through the single MCP gateway:
100
102
  "compressionAction": "diagnose",
101
103
  "artifactId": "<sha256>",
102
104
  "limit": 10,
105
+ "startLine": 10,
103
106
  "contextLines": 2,
104
107
  "compressionByteBudget": 16384,
105
108
  "detailLevel": "standard"
@@ -143,8 +146,18 @@ current graph evidence, and an untruncated context bundle. `partial` identifies
143
146
  any omitted input, unresolved reference, stale graph, or exhausted context
144
147
  budget. `unresolved` means no safe repository location was established.
145
148
 
146
- The relationships are static diagnostic candidates, not proof of runtime
147
- causality. Package ownership is evidenced by the nearest tracked
149
+ The versioned JSON schema also reports `confidence`, exact `omissions`, and
150
+ `telemetry`. When the diagnostic item cap omits references from a retained
151
+ artifact, `omissions.continuation` identifies the artifact and next diagnostic
152
+ offset for recoverable local drill-down. `telemetry.commandRerun` is always
153
+ false. Outer MCP response budgets add hard byte/token/item accounting without
154
+ changing the diagnosis evidence contract.
155
+
156
+ For the compact single-tool schema, MCP diagnose continuation uses the existing
157
+ bounded `startLine` integer as its diagnostic offset; CLI uses `--offset`.
158
+
159
+ The relationships are bounded source-evidenced diagnostic candidates, not
160
+ proof of runtime causality, guaranteed root causes, or automatic fixes. Package ownership is evidenced by the nearest tracked
148
161
  `package.json`, `pyproject.toml`, `pom.xml`, `Cargo.toml`, `go.mod`, or
149
162
  `.csproj`; malformed or unnamed manifests remain explicit. Dynamic dispatch,
150
163
  generated paths, source maps, and framework wiring can require additional
@@ -163,11 +176,15 @@ Oversized input is rejected rather than partially retained. Output artifacts
163
176
  are content-addressed, reads are byte-bounded, deletion requires an exact
164
177
  artifact identity, and symlinked storage paths are refused.
165
178
 
166
- ## Command-execution security gate
179
+ ## Profile-based execution boundary
180
+
181
+ The compressor remains independently usable on already-produced output. The
182
+ optional `execute` operation now accepts only an immutable repository profile
183
+ identity and composes captured output through this compressor. The detailed
184
+ configuration and certified boundary are documented in
185
+ [Profile-based contained execution](CONTAINED-EXECUTION.md).
167
186
 
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:
187
+ The shipping runner is still held to this threat model:
171
188
 
172
189
  | Threat | Required proof before shipping a runner |
173
190
  |---|---|
@@ -185,10 +202,9 @@ platform-specific evidence:
185
202
  | Telemetry leakage | No command text or raw output in default telemetry; redact before opt-in export |
186
203
  | Platform differences | Separate verified guarantees and explicit unsupported cases |
187
204
 
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.
205
+ The original C58 prototype remains rejected: cwd/realpath checks and polling
206
+ were not containment. C88 reopens the capability under a narrower contract.
207
+ The certified macOS adapter adds enforceable network/user-data boundaries and
208
+ hard fork denial; Linux and Windows fail closed as unavailable. No portable
209
+ claim is inferred, no unrestricted command surface exists, and repository code
210
+ can still modify the authorized checkout.
@@ -0,0 +1,77 @@
1
+ # Profile-based contained execution
2
+
3
+ `knodin` can execute one immutable repository-defined profile through the
4
+ single MCP gateway. It never accepts a command string, executable, argv, cwd,
5
+ environment, or limit from an MCP caller.
6
+
7
+ Execution is disabled by two independent defaults. Enable it globally in
8
+ `$XDG_CONFIG_HOME/knodin/execution.json` (or
9
+ `~/.config/knodin/execution.json`):
10
+
11
+ ```json
12
+ {"execution":{"enabled":true}}
13
+ ```
14
+
15
+ Then define and enable exact profiles in `.knodin/execution.json`:
16
+
17
+ ```json
18
+ {
19
+ "execution": {
20
+ "enabled": true,
21
+ "profiles": {
22
+ "test": {
23
+ "executable": "/opt/homebrew/bin/bun",
24
+ "args": ["run", "test"],
25
+ "network": "deny",
26
+ "timeoutMs": 120000,
27
+ "maxOutputBytes": 2097152,
28
+ "maxProcesses": 1,
29
+ "containment": "fully-contained"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ An agent may call `{ "operation": "execute", "profile": "test" }`. Profile
37
+ names are bounded identifiers. Shell strings and caller-supplied argument
38
+ suffixes are not part of the schema.
39
+
40
+ ## Containment contract
41
+
42
+ The initial certified adapter is macOS `sandbox-exec`. It runs with a minimal
43
+ environment (`PATH`, `LANG`, `LC_ALL`, and repository-local `TMPDIR`), closed
44
+ stdin, no shell, repository cwd, a detached process group, hard timeout and
45
+ combined-output caps, and Seatbelt rules that:
46
+
47
+ - deny all network operations;
48
+ - deny process forks, making `maxProcesses: 1` a hard contract;
49
+ - deny all writes outside the repository;
50
+ - deny reads under user homes, mounted volumes, network mounts, and shared
51
+ temporary roots outside the repository; and
52
+ - allow repository reads/writes and read-only system runtime paths required to
53
+ load the approved executable.
54
+
55
+ Profiles needing child processes are rejected by this first contract. Define
56
+ the profile around a single-process test/lint/typecheck/build executable. The
57
+ macOS adapter reports `fully-contained` only when every configured boundary is
58
+ active. It also reports that `sandbox-exec` is deprecated; this certification
59
+ is tied to the checked-in adversarial tests, not a claim that Seatbelt is a
60
+ supported public Apple API.
61
+
62
+ Linux and Windows currently report `containment-unavailable` and do not spawn
63
+ the profile. A future Bubblewrap or Windows Job Object/restricted-token adapter
64
+ requires its own native evidence before it can change that result.
65
+
66
+ ## Output, diagnosis, and audit
67
+
68
+ Captured stdout/stderr goes directly through recoverable compression. Failed,
69
+ timed-out, and output-limited MCP executions then request source-evidenced
70
+ failure diagnosis from the existing graph. Raw command text, argv, environment,
71
+ and output are never written to execution telemetry. The private repository
72
+ audit `.knodin/execution-audit.jsonl` records only timestamp, profile identity,
73
+ status, exit/signal, reported containment, and retained output-artifact identity.
74
+
75
+ Repository code can still be destructive inside the authorized repository.
76
+ This is a constrained trusted-repository workflow, not a guarantee that an
77
+ approved test/build profile cannot modify its own checkout.
@@ -0,0 +1,45 @@
1
+ # Local troubleshooting diagnostics
2
+
3
+ knodin can retain a small, local diagnostic journal and build a redacted bundle
4
+ for a user to inspect and share with support. Diagnostics are disabled until the
5
+ user enables them, never upload automatically, and require no account or hosted
6
+ service.
7
+
8
+ ```bash
9
+ knodin diagnostics enable --retention-days 14
10
+ knodin diagnostics status
11
+ knodin diagnostics collect --since 24h
12
+ knodin diagnostics inspect .knodin/diagnostics/knodin-diagnostics-….json.gz
13
+ knodin diagnostics clear
14
+ knodin diagnostics disable
15
+ ```
16
+
17
+ `enable` records bounded failure envelopes from the CLI and MCP gateway. Each
18
+ record contains a timestamp, random correlation ID, surface, operation, phase,
19
+ safe error name/code, a message fingerprint, and sanitized stack shape. It does
20
+ not retain the command arguments, query, error message, source, raw output,
21
+ environment, username, repository path, or source paths. Records are stored in
22
+ `.knodin/diagnostics/events.jsonl`, mode `0600`, capped at 500 events, and
23
+ pruned to the configured 1–365 day retention window.
24
+
25
+ `collect` runs local installation and deep graph diagnostics and combines their
26
+ redacted results with recent failure envelopes, metadata-only telemetry when
27
+ present, and at most 64 KiB/200 lines from the lifecycle indexer log. The
28
+ default collection window is 24 hours; `--since` accepts hours or days. Bundle
29
+ output must remain inside the repository and cannot traverse a symlink.
30
+
31
+ The bundle is gzip-compressed JSON, written mode `0600`, and includes a privacy
32
+ manifest, explicit omissions, and a redaction count. `inspect` validates the
33
+ manifest and a 10 MiB compressed-size ceiling before returning its contents.
34
+ Users should inspect the bundle before attaching it to Jira, GitHub, email, or
35
+ another support channel. knodin does not transmit it.
36
+
37
+ `clear` removes only the event journal. It does not delete previously created
38
+ bundles or ROI telemetry. `disable` stops future failure recording but preserves
39
+ existing events so disabling never silently destroys troubleshooting evidence.
40
+ Run `clear` when deletion is intended.
41
+
42
+ Diagnostics are evidence, not runtime-causality proof. Message fingerprints can
43
+ group identical sanitized failures but cannot reconstruct the original message.
44
+ Redaction is defense in depth; users remain responsible for inspecting an
45
+ artifact before sharing it outside their organization.
@@ -69,8 +69,11 @@ the provenance/SBOM and five-channel attestation machinery; the production
69
69
  release must populate it, and C65 must supply compromised-channel and recovery
70
70
  evidence before this gate can open.
71
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.
72
+ Run `knodin status --deep` for graph details and follow its typed repair steps:
73
+ `knodin repair` repairs graph data, while `knodin init` restores lifecycle
74
+ routing. Re-run `knodin init` after changing the executable owner, MCP path, or
75
+ hook-manager installation. The complete refresh-path matrix is in
76
+ [`FRESHNESS-AND-LIFECYCLE.md`](FRESHNESS-AND-LIFECYCLE.md).
74
77
 
75
78
  ## Query availability
76
79