knodin 0.8.7 → 0.9.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 (44) hide show
  1. package/dist/bin/cli.js +307 -85
  2. package/dist/fixtures/update-verification/config/application.xml +4 -0
  3. package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
  4. package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
  5. package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
  6. package/dist/fixtures/update-verification/fixture.json +147 -0
  7. package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
  8. package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
  9. package/dist/fixtures/update-verification/package.json +5 -0
  10. package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
  11. package/dist/fixtures/update-verification/sql/functions.sql +16 -0
  12. package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
  13. package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
  14. package/dist/fixtures/update-verification/src/javascript.js +7 -0
  15. package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
  16. package/dist/fixtures/update-verification/src/typescript.ts +7 -0
  17. package/dist/fixtures/update-verification/terraform/main.tf +7 -0
  18. package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
  19. package/dist/src/agent-integration.js +26 -3
  20. package/dist/src/backup-retention.js +811 -0
  21. package/dist/src/cli-model.js +35 -8
  22. package/dist/src/docs-sections.js +1 -0
  23. package/dist/src/doctor.js +142 -12
  24. package/dist/src/engine/embeddings.js +8 -1
  25. package/dist/src/engine/index.js +355 -292
  26. package/dist/src/init.js +222 -179
  27. package/dist/src/manager-update.js +455 -0
  28. package/dist/src/release-preflight.js +106 -95
  29. package/dist/src/repair-lease.js +77 -8
  30. package/dist/src/response-budget.js +5 -1
  31. package/dist/src/server.js +18 -5
  32. package/dist/src/shared-index/compatibility.js +4 -2
  33. package/dist/src/tools/knodin-tools.js +69 -10
  34. package/dist/src/update-ceremony.js +20 -24
  35. package/dist/src/update-coordination.js +158 -0
  36. package/dist/src/update-executor.js +336 -0
  37. package/dist/src/update-verifier.js +358 -0
  38. package/docs/BACKUP-RETENTION.md +54 -0
  39. package/docs/CLI.md +6 -0
  40. package/docs/DOCTOR-AND-UPDATES.md +44 -12
  41. package/docs/SIGNED-UPDATES.md +29 -22
  42. package/docs/releases/0.9.0.md +43 -0
  43. package/package.json +7 -1
  44. package/roadmap/competitive-roadmap.md +8 -0
@@ -175,6 +175,29 @@ function addGraphCommands(program, capture) {
175
175
  .option("--apply", "apply the verified edit")
176
176
  .option("--no-verify", "skip post-apply typecheck");
177
177
  }
178
+ function addBackupCommands(program, capture) {
179
+ const backups = program.command("backups").description("inspect and prune local graph backups");
180
+ leaf(backups, "list [roots...]", "list exact completed backup snapshots", capture)
181
+ .addOption(option("--depth <count>", "maximum discovery depth", "integer"))
182
+ .option("--jsonl", "stream one record per line");
183
+ leaf(backups, "prune [roots...]", "preview or apply safe backup pruning", capture)
184
+ .option("--apply", "delete exact preview candidates after revalidation")
185
+ .addOption(option("--retention-days <count>", "retention window in days", "integer"))
186
+ .addOption(option("--keep-newest <count>", "newest backups retained per repository", "integer"))
187
+ .addOption(option("--depth <count>", "maximum discovery depth", "integer"))
188
+ .option("--jsonl", "stream one record per line");
189
+ const retention = backups.command("retention").description("manage opt-in ongoing retention");
190
+ leaf(retention, "install [roots...]", "install daily and opportunistic retention", capture)
191
+ .addOption(option("--retention-days <count>", "retention window in days", "integer"))
192
+ .addOption(option("--keep-newest <count>", "newest backups retained per repository", "integer"))
193
+ .addOption(option("--depth <count>", "maximum discovery depth", "integer"))
194
+ .option("--schedule <time>", "daily local HH:MM schedule")
195
+ .option("--dry-run", "install a policy that logs without deleting")
196
+ .option("--jsonl", "stream one record per line");
197
+ for (const action of ["status", "doctor", "remove"])
198
+ leaf(retention, action, `${action} installed backup retention`, capture);
199
+ leaf(retention, "run", "run the installed retention policy", capture);
200
+ }
178
201
  function addArtifactCommands(program, capture) {
179
202
  leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
180
203
  .option("--continuation <handle>", "resume an exact prior response")
@@ -241,8 +264,8 @@ function createCliProgram(capture = () => { }) {
241
264
  .exitOverride();
242
265
  program.configureOutput({ writeErr: () => { } });
243
266
  addGlobalOptions(program);
244
- program.addHelpText("after", "\nMCP callers use the `prs` operation and corresponding operations on the single `knodin` gateway.\nknodin update status|check|explain|apply|rollback consumes only threshold-signed metadata.\n");
245
- leaf(program, "init", "initialize graph, lifecycle hooks, and agent integration", capture)
267
+ program.addHelpText("after", "\nMCP callers use the `prs` operation and corresponding operations on the single `knodin` gateway.\nLegacy update status|check|explain|apply|rollback consumes only threshold-signed metadata. Manager-aware available|upgrade|pin|unpin|acknowledge|auto binds actions to the proven active owner.\n");
268
+ leaf(program, "init", "initialize graph and lifecycle hooks; configure agents only with --scope", capture)
246
269
  .option("--scope <scope>", "personal, team, or cli-only")
247
270
  .addHelpText("after", "\nUsage: knodin init [--scope personal|team|cli-only] [--json]\n\npersonal recommended; local/excluded agent adapters and a clean Git status\nteam commit-ready shared agent configuration\ncli-only no agent discovery; AI agents will not know to invoke knodin\n\nChange later with `knodin configure --scope <scope>`.\nTracked files are never added to Git exclude files.\n");
248
271
  leaf(program, "configure", "change or inspect agent integration", capture)
@@ -286,6 +309,7 @@ function createCliProgram(capture = () => { }) {
286
309
  leaf(program, "agent-event <event>", "internal coding-agent lifecycle event", capture);
287
310
  leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
288
311
  addRepositoryCommands(program, capture);
312
+ addBackupCommands(program, capture);
289
313
  addRemoteCommands(program, capture);
290
314
  addSharedIndexCommands(program, capture);
291
315
  addGraphCommands(program, capture);
@@ -327,12 +351,15 @@ function createCliProgram(capture = () => { }) {
327
351
  .option("--scope <scope>", "personal, team, or cli-only")
328
352
  .addOption(option("--client <client>", "codex, claude, or gemini", "collect"))
329
353
  .option("--dry-run", "preview skill changes without writing");
330
- const update = program
331
- .command("update")
332
- .description("inspect or apply threshold-signed updates")
333
- .allowExcessArguments(false)
334
- .addArgument(new Argument("<action>").choices(["status", "check", "explain", "apply", "rollback"]));
335
- update.action((...values) => capture(values.at(-1)));
354
+ const update = program.command("update").description("inspect or apply verified updates");
355
+ for (const action of ["status", "check", "explain", "apply", "rollback"])
356
+ leaf(update, action, `${action} threshold-signed updates`, capture);
357
+ leaf(update, "available", "query the active owner's release channel", capture);
358
+ leaf(update, "upgrade [version]", "plan or apply an exact verified upgrade", capture).option("--apply", "execute the owning package manager");
359
+ leaf(update, "pin <version>", "pin an exact version", capture).option("--apply", "install and verify the pinned version");
360
+ leaf(update, "unpin", "remove the active update pin", capture);
361
+ leaf(update, "acknowledge", "acknowledge the current rollback notice", capture);
362
+ leaf(update, "auto <action>", "status, enable, disable, or run automatic updates", capture);
336
363
  leaf(program, "docs <topic>", "read canonical product documentation", capture);
337
364
  leaf(program, "prs [action]", "audit pull requests, reviews, and checks", capture)
338
365
  .option("--state <state>", "open, merged, closed, or all")
@@ -112,6 +112,7 @@ const DOC_TOPIC_FILES = {
112
112
  provenance: "INDEXING-POLICY-AND-PROVENANCE.md",
113
113
  "dead-code": "DEAD-CODE-AND-IMPACT.md",
114
114
  doctor: "DOCTOR-AND-UPDATES.md",
115
+ backups: "BACKUP-RETENTION.md",
115
116
  compression: "COMMAND-OUTPUT-COMPRESSION.md",
116
117
  };
117
118
  function packageRoot() {
@@ -3,7 +3,9 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { detectSupportedAgents } from "./agent-integration.js";
6
+ import { listBackups, pruneBackups, retentionDoctor } from "./backup-retention.js";
6
7
  import { inspectLifecycleHealth } from "./lifecycle-health.js";
8
+ import { readManagerUpdateState, readUpdateJournal, resolveManagerOwnership, updateAttention, } from "./manager-update.js";
7
9
  import { trustedUpdateStatus } from "./update-policy.js";
8
10
  function defaultRun(command, args, options = {}) {
9
11
  const result = child_process.spawnSync(command, args, {
@@ -141,12 +143,83 @@ function sanitizeRegistry(raw) {
141
143
  return "unknown";
142
144
  }
143
145
  }
146
+ const MANAGED_REPOSITORY_PATHS = [
147
+ "AGENTS.md",
148
+ "GEMINI.md",
149
+ ".mcp.json",
150
+ ".codex/config.toml",
151
+ ".gemini/settings.json",
152
+ ".vscode/mcp.json",
153
+ ".agents/mcp_config.json",
154
+ ];
155
+ function managedConfigurationRemediation(repo, run) {
156
+ const paths = [...MANAGED_REPOSITORY_PATHS];
157
+ for (const root of [".agents/skills", ".claude/skills", ".gemini/skills"]) {
158
+ const absolute = path.join(repo, root);
159
+ let entries = [];
160
+ try {
161
+ entries = fs.readdirSync(absolute, { withFileTypes: true }).slice(0, 64);
162
+ }
163
+ catch {
164
+ continue;
165
+ }
166
+ for (const entry of entries) {
167
+ if (!entry.isDirectory())
168
+ continue;
169
+ const relative = path.posix.join(root, entry.name);
170
+ if (fs.existsSync(path.join(repo, relative, ".knodin-managed.json")))
171
+ paths.push(relative);
172
+ }
173
+ }
174
+ return paths
175
+ .map((relative) => {
176
+ const absolute = path.join(repo, relative);
177
+ const present = fs.existsSync(absolute);
178
+ const trackedResult = run("git", ["-C", repo, "ls-files", "--error-unmatch", "--", relative]);
179
+ const tracked = trackedResult.status === 0;
180
+ const diffResult = tracked
181
+ ? run("git", ["-C", repo, "diff", "--no-ext-diff", "--unified=3", "--", relative])
182
+ : { status: 0, stdout: "", stderr: "" };
183
+ const receipted = fs.existsSync(path.join(absolute, ".knodin-managed.json"));
184
+ let entryOwned = false;
185
+ if (present && !receipted) {
186
+ try {
187
+ entryOwned = fs.readFileSync(absolute, "utf8").includes("knodin");
188
+ }
189
+ catch {
190
+ entryOwned = false;
191
+ }
192
+ }
193
+ const instructions = [];
194
+ if (tracked && diffResult.stdout)
195
+ instructions.push(`Review the diff, then restore manually with: git restore --source=HEAD -- ${relative}`);
196
+ if (!tracked && receipted)
197
+ instructions.push(`Review the Knodin receipt, then remove manually if unwanted: ${relative}`);
198
+ if (!tracked && present && (receipted || entryOwned))
199
+ instructions.push(`To retain it locally without committing, add ${relative} to .git/info/exclude manually.`);
200
+ return {
201
+ path: relative,
202
+ tracked,
203
+ present,
204
+ ownership: receipted ? "knodin-receipt" : entryOwned ? "knodin-entry" : "none",
205
+ diff: diffResult.stdout ? diffResult.stdout.slice(0, 32_768) : null,
206
+ instructions,
207
+ };
208
+ })
209
+ .filter((entry) => entry.present || entry.tracked || entry.diff !== null || entry.ownership !== "none");
210
+ }
144
211
  function readAgentConfig(agent, file) {
145
212
  try {
146
213
  const content = fs.readFileSync(file, "utf-8");
147
214
  if (agent === "claude" && path.basename(file) === ".claude.json") {
148
215
  const present = content.includes('"knodin"');
149
- return { agent, file, present, command: present ? "knodin" : null, args: ["serve"] };
216
+ return {
217
+ agent,
218
+ file,
219
+ present,
220
+ command: present ? "knodin" : null,
221
+ args: ["serve"],
222
+ };
150
223
  }
151
224
  if (file.endsWith(".toml")) {
152
225
  const sectionStart = content.indexOf('[mcp_servers."knodin"]');
@@ -273,15 +346,27 @@ function mcpProbe(command, run) {
273
346
  clientInfo: { name: "knodin-doctor", version: "1" },
274
347
  },
275
348
  }),
276
- JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }),
349
+ JSON.stringify({
350
+ jsonrpc: "2.0",
351
+ method: "notifications/initialized",
352
+ params: {},
353
+ }),
277
354
  JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
278
355
  "",
279
356
  ].join("\n");
280
357
  const [executable, ...args] = command;
281
358
  if (!executable)
282
- return { initialize: "failed", toolsList: "failed", toolNames: [], stderr: "no command" };
359
+ return {
360
+ initialize: "failed",
361
+ toolsList: "failed",
362
+ toolNames: [],
363
+ stderr: "no command",
364
+ };
283
365
  const result = run(executable, args, { input });
284
- return { ...parseMcp(result.stdout), stderr: result.status === 0 ? undefined : result.stderr };
366
+ return {
367
+ ...parseMcp(result.stdout),
368
+ stderr: result.status === 0 ? undefined : result.stderr,
369
+ };
285
370
  }
286
371
  export async function diagnoseInstallation(repoPath, options) {
287
372
  const repo = path.resolve(repoPath);
@@ -298,6 +383,7 @@ export async function diagnoseInstallation(repoPath, options) {
298
383
  const detectedAgents = detectSupportedAgents();
299
384
  const repositoryCommands = configuredAgentCommands(repo);
300
385
  const personalCommands = personalAgentCommands(options.homeDir ?? os.homedir());
386
+ const configurationRemediation = managedConfigurationRemediation(repo, run);
301
387
  const clients = repositoryCommands
302
388
  .filter(({ agent }) => options.client === undefined || options.client === agent)
303
389
  .map((repository) => {
@@ -323,7 +409,16 @@ export async function diagnoseInstallation(repoPath, options) {
323
409
  };
324
410
  });
325
411
  const manager = managerEvidence(chain.at(-1) ?? executable, env);
412
+ const owner = resolveManagerOwnership({
413
+ runtimeCommand: options.runtimeCommand,
414
+ executablePath: chain.at(-1) ?? executable,
415
+ env,
416
+ });
417
+ const stateHome = env.XDG_STATE_HOME;
418
+ const managerState = readManagerUpdateState(stateHome);
326
419
  const packageVersion = installedPackageVersion(chain.at(-1) ?? executable);
420
+ const backupInventory = listBackups([repo], { depth: 0 });
421
+ const backupPrunePreview = pruneBackups([repo], { depth: 0 });
327
422
  return {
328
423
  schemaVersion: 1,
329
424
  status: options.graph.status === "healthy" &&
@@ -360,19 +455,54 @@ export async function diagnoseInstallation(repoPath, options) {
360
455
  configuredCommands: repositoryCommands,
361
456
  clients,
362
457
  },
458
+ configurationRemediation,
363
459
  mcp,
364
460
  lifecycle,
365
461
  graph: options.graph,
366
- update: trustedUpdateStatus({
367
- currentVersion: options.currentVersion,
368
- stateHome: options.cacheHome,
369
- installMethod: manager.name,
370
- env,
371
- }),
462
+ backups: {
463
+ inventory: backupInventory,
464
+ reclaimableBytes: backupPrunePreview.reclaimableBytes,
465
+ skippedRepositories: backupPrunePreview.skipped,
466
+ retention: retentionDoctor(),
467
+ remediation: "Preview with `knodin backups prune`; deletion requires `--apply` or installed retention.",
468
+ },
469
+ update: {
470
+ ...trustedUpdateStatus({
471
+ currentVersion: options.currentVersion,
472
+ stateHome: options.cacheHome,
473
+ installMethod: manager.name,
474
+ env,
475
+ }),
476
+ owner: {
477
+ method: owner.method,
478
+ status: owner.status,
479
+ executable: owner.executable,
480
+ evidence: owner.evidence,
481
+ },
482
+ pin: managerState.pin ?? null,
483
+ auto: {
484
+ enabled: managerState.autoEnabled,
485
+ platformEligible: managerState.platformAutoEligible,
486
+ },
487
+ attention: updateAttention(managerState),
488
+ journal: readUpdateJournal(stateHome),
489
+ },
372
490
  remediation: {
373
491
  manager: managerRemediation(manager.name, options.currentVersion),
374
- duplicates: candidates.length > 1
375
- ? "Remove only the unintended manager-owned installation, then rerun `knodin doctor`."
492
+ duplicates: owner.duplicates.length > 0 || owner.staleLinks.length > 0 || owner.pathShadowing
493
+ ? {
494
+ instructions: [
495
+ ...owner.duplicates.map((entry) => `Use the package manager that owns ${entry.path} to remove only that competing installation.`),
496
+ ...owner.staleLinks.map((entry) => `Inspect and manually remove the stale executable link ${entry}.`),
497
+ ...(owner.pathShadowing
498
+ ? [
499
+ "Correct PATH ordering so the intended manager-owned executable resolves first.",
500
+ ]
501
+ : []),
502
+ "Rerun `knodin doctor`; Knodin will not remove another installation automatically.",
503
+ ],
504
+ ownershipEvidence: owner.evidence,
505
+ }
376
506
  : null,
377
507
  graph: options.graph.status === "healthy"
378
508
  ? null
@@ -18,6 +18,7 @@ let embedderReady = false;
18
18
  export const EMBEDDING_DIMENSIONS = 384;
19
19
  export const EMBEDDING_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
20
20
  export const EMBEDDING_MODEL_REVISION = "751bff37182d3f1213fa05d7196b954e230abad9";
21
+ export const EMBEDDING_MODEL_DTYPE = "q8";
21
22
  const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
22
23
  /**
23
24
  * One machine-level model cache shared by every repository and every installed
@@ -137,6 +138,9 @@ export function getEmbedder(onProgress) {
137
138
  notify({ status: "loading", name: EMBEDDING_MODEL_ID });
138
139
  const pipe = await pipeline("feature-extraction", EMBEDDING_MODEL_ID, {
139
140
  revision: EMBEDDING_MODEL_REVISION,
141
+ // q8 preserves the model's 384-dimensional normalized-vector contract
142
+ // while avoiding the 86 MiB FP32 graph load on every cold CLI process.
143
+ dtype: EMBEDDING_MODEL_DTYPE,
140
144
  progress_callback: notify,
141
145
  });
142
146
  realEmbedderLoadCount++;
@@ -162,7 +166,10 @@ export async function generateEmbeddings(texts, onModelProgress) {
162
166
  if (texts.length === 0)
163
167
  return [];
164
168
  const extractor = await getEmbedder(onModelProgress);
165
- const result = await extractor([...texts], { pooling: "mean", normalize: true });
169
+ const result = await extractor([...texts], {
170
+ pooling: "mean",
171
+ normalize: true,
172
+ });
166
173
  const data = result.data instanceof Float32Array ? result.data : Float32Array.from(result.data);
167
174
  const dims = result.dims;
168
175
  if (dims?.length !== 2 || dims[0] !== texts.length || dims[1] !== EMBEDDING_DIMENSIONS) {