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
@@ -5,15 +5,6 @@ import path from "node:path";
5
5
  import { compareBytes } from "./compare.js";
6
6
  import { validateRootCeremonyManifest } from "./update-ceremony.js";
7
7
  import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
8
- export const RELEASE_PREFLIGHT_DEPENDENCIES = [
9
- "C60",
10
- "C62",
11
- "C63",
12
- "C64",
13
- "C65",
14
- "C66",
15
- "C75",
16
- ];
17
8
  export const RELEASE_PREFLIGHT_REPOSITORY_COMMANDS = [
18
9
  "npm ci --ignore-scripts",
19
10
  "npm run lint",
@@ -47,6 +38,20 @@ const COMMAND_CATEGORIES = [
47
38
  "attestation",
48
39
  ];
49
40
  const TARGET_PLATFORMS = ["macOS", "Linux", "Windows"];
41
+ const VERIFIER_COVERAGE = [
42
+ "typescript",
43
+ "javascript",
44
+ "python",
45
+ "java",
46
+ "csharp",
47
+ "apex",
48
+ "sql",
49
+ "prisma",
50
+ "xml",
51
+ "salesforce",
52
+ "terraform",
53
+ "dbt",
54
+ ];
50
55
  const MAX_MANIFEST_BYTES = 1 * 1_024 * 1_024;
51
56
  const MAX_EVIDENCE_BYTES = 16 * 1_024 * 1_024;
52
57
  const MAX_TOTAL_EVIDENCE_BYTES = 128 * 1_024 * 1_024;
@@ -54,10 +59,8 @@ const COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
54
59
  const SHA256 = /^[a-f0-9]{64}$/;
55
60
  const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
56
61
  const REPOSITORY_PATH = /^[A-Za-z0-9._+/-]+$/;
57
- const IMMUTABLE_EVIDENCE_PATH = /^(?:docs\/evidence|benchmarks\/evaluations)\//;
58
62
  const RUNTIME_NAME = /^[a-z][a-z0-9._-]{0,31}$/;
59
63
  const RUNTIME_VERSION = /^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/;
60
- const TERMINAL_STATUS = /^(?:implemented(?: \([^()\n]+\)| — [^\n]+)?|evaluated — [^\n]+|certified(?: with [^\n]+| — [^\n]+)?|closed(?:; [^\n]+| — [^\n]+)?|done(?: — [^\n]+)?)$/;
61
64
  function fail(message) {
62
65
  throw new Error(`release preflight failed: ${message}`);
63
66
  }
@@ -110,7 +113,7 @@ function parsePlan(value) {
110
113
  "trustCeremonyManifestPath",
111
114
  "trustReviewReceiptPath",
112
115
  "verificationCommands",
113
- "dependencyEvidence",
116
+ "technicalEvidence",
114
117
  "evidenceDirectory",
115
118
  ], "release plan");
116
119
  if (plan.schemaVersion !== 1)
@@ -151,9 +154,8 @@ function parsePlan(value) {
151
154
  !runtimeNames.includes("npm"))
152
155
  fail("runtimes must uniquely include node and npm");
153
156
  const certifiedTargetPlatforms = boundedStringArray(plan.certifiedTargetPlatforms, "certified target platforms", { maximumItems: 3, maximumLength: 16 });
154
- if (certifiedTargetPlatforms.length !== TARGET_PLATFORMS.length ||
155
- TARGET_PLATFORMS.some((platform) => !certifiedTargetPlatforms.includes(platform)))
156
- fail("certified target platforms must contain exactly macOS, Linux, and Windows");
157
+ if (certifiedTargetPlatforms.some((platform) => !TARGET_PLATFORMS.includes(platform)))
158
+ fail("certified target platforms may contain only macOS, Linux, or Windows");
157
159
  const trustRootSha256 = text(plan.trustRootSha256, "trust-root digest", 64);
158
160
  if (!SHA256.test(trustRootSha256))
159
161
  fail("trust-root digest must be a lowercase SHA-256 digest");
@@ -179,12 +181,23 @@ function parsePlan(value) {
179
181
  expected.some((command, index) => verificationCommands[category][index] !== command))
180
182
  fail(`${category} verification commands must equal the canonical ordered replay set`);
181
183
  }
182
- const evidenceRecord = record(plan.dependencyEvidence, "dependency evidence");
183
- exactKeys(evidenceRecord, RELEASE_PREFLIGHT_DEPENDENCIES, "dependency evidence");
184
- const dependencyEvidence = {};
185
- for (const dependency of RELEASE_PREFLIGHT_DEPENDENCIES) {
186
- dependencyEvidence[dependency] = boundedStringArray(evidenceRecord[dependency], `${dependency} evidence paths`, { maximumItems: 32, maximumLength: 512 }).map((candidate, index) => safeRelativePath(candidate, `${dependency} evidence paths[${index}]`));
187
- }
184
+ const technicalEvidenceRecord = record(plan.technicalEvidence, "technical evidence");
185
+ exactKeys(technicalEvidenceRecord, ["repository", "platforms"], "technical evidence");
186
+ const repositoryEvidencePath = safeRelativePath(technicalEvidenceRecord.repository, "repository technical evidence path");
187
+ if (!Array.isArray(technicalEvidenceRecord.platforms))
188
+ fail("platform technical evidence must be an array");
189
+ const platformEvidence = technicalEvidenceRecord.platforms.map((candidate, index) => {
190
+ const item = record(candidate, `platform technical evidence[${index}]`);
191
+ exactKeys(item, ["platform", "path"], `platform technical evidence[${index}]`);
192
+ return {
193
+ platform: text(item.platform, `platform technical evidence[${index}].platform`, 16),
194
+ path: safeRelativePath(item.path, `platform technical evidence[${index}].path`),
195
+ };
196
+ });
197
+ if (platformEvidence.length !== certifiedTargetPlatforms.length ||
198
+ new Set(platformEvidence.map(({ platform }) => platform)).size !== platformEvidence.length ||
199
+ certifiedTargetPlatforms.some((platform) => !platformEvidence.some((evidence) => evidence.platform === platform)))
200
+ fail("platform technical evidence must exactly cover certified target platforms");
188
201
  return {
189
202
  schemaVersion: 1,
190
203
  version,
@@ -198,7 +211,10 @@ function parsePlan(value) {
198
211
  trustCeremonyManifestPath,
199
212
  trustReviewReceiptPath,
200
213
  verificationCommands,
201
- dependencyEvidence,
214
+ technicalEvidence: {
215
+ repository: repositoryEvidencePath,
216
+ platforms: platformEvidence,
217
+ },
202
218
  evidenceDirectory: safeRelativePath(plan.evidenceDirectory, "evidence directory"),
203
219
  };
204
220
  }
@@ -318,42 +334,6 @@ function requireTaggedBytes(git, commit, relativePath, bytes, label) {
318
334
  if (!tagged.equals(bytes))
319
335
  fail(`${label} bytes do not equal the exact source-commit blob`);
320
336
  }
321
- function verifyRoadmap(roadmap) {
322
- const activeLedger = /### Track A — Active local-work ledger\n[\s\S]*?\n\| ID \| State \| Previous state \| Depends on \| Exit evidence \|\n\|---\|---\|---\|---\|---\|\n((?:\| C[^\n]+\n)*)\n### Track B — Parked external-evidence ledger/.exec(roadmap);
323
- const parkedLedger = /### Track B — Parked external-evidence ledger\n[\s\S]*?\n\| ID \| State \| Previous state \| Depends on \| Exit evidence \|\n\|---\|---\|---\|---\|---\|\n((?:\| C[^\n]+\n)*)\n### Goal-loop ordering/.exec(roadmap);
324
- if (!activeLedger || !parkedLedger)
325
- fail("two-track roadmap ledger is missing or malformed");
326
- const activeIds = new Set([...activeLedger[1].matchAll(/^\| (C\d+) \|/gm)].map((match) => match[1]));
327
- const parkedIds = new Set([...parkedLedger[1].matchAll(/^\| (C\d+) \|/gm)].map((match) => match[1]));
328
- if (parkedIds.size > 0)
329
- fail(`Track B remains parked (${parkedIds.size} external item${parkedIds.size === 1 ? "" : "s"}: ${[...parkedIds].join(", ")})`);
330
- const evidencePaths = new Map();
331
- for (const dependency of RELEASE_PREFLIGHT_DEPENDENCIES) {
332
- if (activeIds.has(dependency))
333
- fail(`${dependency} remains in Track A`);
334
- const header = dependency === "C75" ? "C69–C75" : dependency;
335
- const section = new RegExp(String.raw `(^#{2,3} ${header} —[^\n]*\n[\s\S]*?)(?=^#{2,3} C(?:\d+|\d+–C\d+) —|(?![\s\S]))`, "m").exec(roadmap);
336
- if (!section)
337
- fail(`${dependency} has no authoritative item status`);
338
- const statusMatch = /^- Status: ([^\n]+)$/m.exec(section[1]);
339
- if (!statusMatch)
340
- fail(`${dependency} has no authoritative item status`);
341
- const status = statusMatch[1].trim();
342
- if (!TERMINAL_STATUS.test(status))
343
- fail(`${dependency} has nonterminal status ${JSON.stringify(status)}`);
344
- const evidenceLabel = dependency === "C75" ? "C75 Evidence" : "Evidence";
345
- const evidenceField = new RegExp(String.raw `^- ${evidenceLabel}: ([\s\S]*?)(?=\n\n|\n- [A-Z][^\n]*:|(?![\s\S]))`, "m").exec(section[1]);
346
- if (!evidenceField)
347
- fail(`${dependency} has no explicit authoritative evidence field`);
348
- const paths = [...evidenceField[1].matchAll(/`([^`]+)`/g)].map((match) => match[1]);
349
- if (paths.length === 0 || new Set(paths).size !== paths.length)
350
- fail(`${dependency} authoritative evidence field must name distinct paths`);
351
- if (!paths.some((candidate) => IMMUTABLE_EVIDENCE_PATH.test(candidate)))
352
- fail(`${dependency} must declare at least one immutable evidence or evaluation path`);
353
- evidencePaths.set(dependency, paths);
354
- }
355
- return evidencePaths;
356
- }
357
337
  function verifyFreshEvidenceDirectory(repositoryRoot, plan) {
358
338
  const expected = `docs/evidence/releases/${plan.version}`;
359
339
  if (plan.evidenceDirectory !== expected)
@@ -381,9 +361,8 @@ function verifyFreshEvidenceDirectory(repositoryRoot, plan) {
381
361
  }
382
362
  function verifyProductionTrust(plan, evidence) {
383
363
  const paths = [plan.trustRootPath, plan.trustCeremonyManifestPath, plan.trustReviewReceiptPath];
384
- if (new Set(paths).size !== paths.length ||
385
- paths.some((candidate) => !plan.dependencyEvidence.C62.includes(candidate)))
386
- fail("production trust artifacts must be three distinct C62 dependency-evidence paths");
364
+ if (new Set(paths).size !== paths.length)
365
+ fail("production trust artifacts must be three distinct paths");
387
366
  const rootBytes = evidence.get(plan.trustRootPath);
388
367
  const manifestBytes = evidence.get(plan.trustCeremonyManifestPath);
389
368
  const receiptBytes = evidence.get(plan.trustReviewReceiptPath);
@@ -416,7 +395,7 @@ function verifyProductionTrust(plan, evidence) {
416
395
  fail(`production trust root must be a valid self-threshold-signed root envelope: ${error instanceof Error ? error.message : "invalid root"}`);
417
396
  }
418
397
  if (digest !== plan.trustRootSha256)
419
- fail("trust-root digest does not match the canonical C62 root envelope");
398
+ fail("trust-root digest does not match the canonical root envelope");
420
399
  const receipt = record(parseJson(receiptBytes, "production root review receipt"), "production root review receipt");
421
400
  exactKeys(receipt, [
422
401
  "schemaVersion",
@@ -434,14 +413,58 @@ function verifyProductionTrust(plan, evidence) {
434
413
  receipt.ceremonyId !== manifest.ceremonyId ||
435
414
  receipt.rootEnvelopeSha256 !== digest ||
436
415
  typeof receipt.reviewerId !== "string" ||
437
- manifest.custodians.some(({ id }) => id === receipt.reviewerId) ||
416
+ !manifest.custodians.some(({ id }) => id === receipt.reviewerId) ||
438
417
  receipt.decision !== "approved" ||
439
- receipt.authentication !== "externally-authenticated" ||
418
+ receipt.authentication !== "maintainer-attested" ||
440
419
  receipt.productionActivation !== false ||
441
420
  !Number.isFinite(reviewedTime.getTime()) ||
442
421
  reviewedTime.toISOString() !== reviewedAt ||
443
422
  reviewedTime.getTime() > Date.now())
444
- fail("production root review receipt is not independent, authenticated, and digest-bound");
423
+ fail("production root attestation is not maintainer-owned, timestamped, and digest-bound");
424
+ }
425
+ function verifyTechnicalEvidence(plan, evidence) {
426
+ const repository = record(parseJson(evidence.get(plan.technicalEvidence.repository) ?? Buffer.alloc(0), "repository technical evidence"), "repository technical evidence");
427
+ if (repository.schemaVersion !== 1 ||
428
+ repository.sourceCommit !== plan.sourceCommit ||
429
+ repository.passed !== true ||
430
+ repository.sonarQualityGate !== "passed")
431
+ fail("repository technical evidence must be source-bound with passing gates and Sonar");
432
+ const commandResults = repository.commands;
433
+ const expectedCommands = COMMAND_CATEGORIES.flatMap((category) => plan.verificationCommands[category].map((command) => ({ category, command })));
434
+ if (!Array.isArray(commandResults) ||
435
+ commandResults.length !== expectedCommands.length ||
436
+ expectedCommands.some((expected, index) => {
437
+ const actual = commandResults[index];
438
+ return (!actual ||
439
+ typeof actual !== "object" ||
440
+ actual.category !== expected.category ||
441
+ actual.command !== expected.command ||
442
+ actual.exitCode !== 0);
443
+ }))
444
+ fail("repository technical evidence must record every canonical command with exit code zero");
445
+ for (const declared of plan.technicalEvidence.platforms) {
446
+ const platform = record(parseJson(evidence.get(declared.path) ?? Buffer.alloc(0), `${declared.platform} technical evidence`), `${declared.platform} technical evidence`);
447
+ const runs = platform.runsMs;
448
+ const coverage = platform.coverage;
449
+ if (platform.schemaVersion !== 1 ||
450
+ platform.sourceCommit !== plan.sourceCommit ||
451
+ platform.platform !== declared.platform ||
452
+ platform.passed !== true ||
453
+ platform.fixtureVersion !== "2026-08-14.2" ||
454
+ platform.silentEmptyControl !== "failed-as-expected" ||
455
+ !Array.isArray(runs) ||
456
+ runs.length !== 5 ||
457
+ runs.some((run) => typeof run !== "number" || run < 0 || run > 60_000) ||
458
+ typeof platform.verifierP95Ms !== "number" ||
459
+ platform.verifierP95Ms < 0 ||
460
+ platform.verifierP95Ms > 20_000 ||
461
+ platform.verifierP95Ms !== Math.max(...runs) ||
462
+ platform.verifierHardLimitMs !== 60_000 ||
463
+ !coverage ||
464
+ typeof coverage !== "object" ||
465
+ VERIFIER_COVERAGE.some((language) => coverage[language] !== "passed"))
466
+ fail(`${declared.platform} technical evidence does not satisfy the verifier budget`);
467
+ }
445
468
  }
446
469
  export function evaluateReleasePreflight(options) {
447
470
  const repositoryRoot = fs.realpathSync(options.repositoryRoot);
@@ -467,39 +490,28 @@ export function evaluateReleasePreflight(options) {
467
490
  if (!git.isTracked(workflowFile.relativePath))
468
491
  fail("C64 candidate workflow must be checked in");
469
492
  requireTaggedBytes(git, plan.sourceCommit, workflowFile.relativePath, workflowFile.bytes, "C64 candidate workflow");
470
- const roadmapFile = readBoundedRegularFile(repositoryRoot, path.join(repositoryRoot, "roadmap/competitive-roadmap.md"), "authoritative roadmap", MAX_EVIDENCE_BYTES);
471
- if (!git.isTracked(roadmapFile.relativePath))
472
- fail("authoritative roadmap must be checked in");
473
- requireTaggedBytes(git, plan.sourceCommit, roadmapFile.relativePath, roadmapFile.bytes, "authoritative roadmap");
474
- const roadmapEvidence = verifyRoadmap(roadmapFile.bytes.toString("utf8"));
475
493
  let totalEvidenceBytes = 0;
476
- const uniquePaths = new Set();
477
494
  const evidenceBytes = new Map();
478
- for (const dependency of RELEASE_PREFLIGHT_DEPENDENCIES) {
479
- const declared = plan.dependencyEvidence[dependency];
480
- const authoritative = roadmapEvidence.get(dependency) ?? [];
481
- if (declared.length !== authoritative.length ||
482
- [...declared]
483
- .sort(compareBytes)
484
- .some((candidate, index) => candidate !== [...authoritative].sort(compareBytes)[index]))
485
- fail(`${dependency} evidence paths must equal its complete authoritative Evidence field`);
486
- for (const relativePath of plan.dependencyEvidence[dependency]) {
487
- if (uniquePaths.has(relativePath))
488
- fail(`evidence path is declared more than once: ${relativePath}`);
489
- uniquePaths.add(relativePath);
490
- const evidence = readBoundedRegularFile(repositoryRoot, path.join(repositoryRoot, relativePath), `${dependency} evidence ${relativePath}`, MAX_EVIDENCE_BYTES);
491
- if (evidence.relativePath !== relativePath)
492
- fail(`${dependency} evidence path must be normalized`);
493
- if (!git.isTracked(relativePath))
494
- fail(`${dependency} evidence must be checked in: ${relativePath}`);
495
- requireTaggedBytes(git, plan.sourceCommit, relativePath, evidence.bytes, `${dependency} evidence ${relativePath}`);
496
- evidenceBytes.set(relativePath, evidence.bytes);
497
- totalEvidenceBytes += evidence.size;
498
- if (totalEvidenceBytes > MAX_TOTAL_EVIDENCE_BYTES)
499
- fail("dependency evidence exceeds the aggregate byte budget");
500
- }
495
+ for (const relativePath of [
496
+ plan.trustRootPath,
497
+ plan.trustCeremonyManifestPath,
498
+ plan.trustReviewReceiptPath,
499
+ plan.technicalEvidence.repository,
500
+ ...plan.technicalEvidence.platforms.map(({ path: evidencePath }) => evidencePath),
501
+ ]) {
502
+ const evidence = readBoundedRegularFile(repositoryRoot, path.join(repositoryRoot, relativePath), `production trust artifact ${relativePath}`, MAX_EVIDENCE_BYTES);
503
+ if (evidence.relativePath !== relativePath)
504
+ fail("production trust artifact path must be normalized");
505
+ if (!git.isTracked(relativePath))
506
+ fail(`production trust artifact must be checked in: ${relativePath}`);
507
+ requireTaggedBytes(git, plan.sourceCommit, relativePath, evidence.bytes, `production trust artifact ${relativePath}`);
508
+ evidenceBytes.set(relativePath, evidence.bytes);
509
+ totalEvidenceBytes += evidence.size;
510
+ if (totalEvidenceBytes > MAX_TOTAL_EVIDENCE_BYTES)
511
+ fail("production trust artifacts exceed the aggregate byte budget");
501
512
  }
502
513
  verifyProductionTrust(plan, evidenceBytes);
514
+ verifyTechnicalEvidence(plan, evidenceBytes);
503
515
  verifyFreshEvidenceDirectory(repositoryRoot, plan);
504
516
  return {
505
517
  ok: true,
@@ -508,6 +520,5 @@ export function evaluateReleasePreflight(options) {
508
520
  sourceCommit: plan.sourceCommit,
509
521
  workflowCommit: plan.workflow.commit,
510
522
  evidenceDirectory: plan.evidenceDirectory,
511
- dependencies: [...RELEASE_PREFLIGHT_DEPENDENCIES],
512
523
  };
513
524
  }
@@ -1,10 +1,24 @@
1
+ import child_process from "node:child_process";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
5
+ import { acquireLifecycleCoordination } from "./update-coordination.js";
4
6
  const MAX_LEASE_BYTES = 4_096;
5
7
  function leasePath(repo) {
6
8
  return path.join(path.resolve(repo), ".knodin", "repair.lock");
7
9
  }
10
+ function activityPath(repo) {
11
+ return path.join(path.resolve(repo), ".knodin", "lifecycle-activity.json");
12
+ }
13
+ function writeActivity(repo, record, finishedAt) {
14
+ const target = activityPath(repo);
15
+ const temporary = `${target}.${record.nonce}.tmp`;
16
+ fs.writeFileSync(temporary, `${JSON.stringify({ ...record, finishedAt: finishedAt ?? null })}\n`, {
17
+ encoding: "utf8",
18
+ mode: 0o600,
19
+ });
20
+ fs.renameSync(temporary, target);
21
+ }
8
22
  function processIsAlive(pid) {
9
23
  try {
10
24
  process.kill(pid, 0);
@@ -14,17 +28,36 @@ function processIsAlive(pid) {
14
28
  return false;
15
29
  }
16
30
  }
31
+ function processStartToken(pid) {
32
+ try {
33
+ return (child_process
34
+ .execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ })
38
+ .trim() || null);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
17
44
  function readLease(target) {
18
45
  try {
19
46
  const stat = fs.statSync(target);
20
47
  if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_LEASE_BYTES)
21
48
  return null;
22
49
  const value = JSON.parse(fs.readFileSync(target, "utf8"));
23
- if (value.schemaVersion !== 1 ||
50
+ if (value.schemaVersion !== 2 ||
24
51
  !Number.isInteger(value.pid) ||
25
52
  (value.pid ?? 0) <= 0 ||
26
53
  typeof value.startedAt !== "string" ||
27
54
  !Number.isFinite(Date.parse(value.startedAt)) ||
55
+ typeof value.processStartToken !== "string" ||
56
+ !value.processStartToken ||
57
+ typeof value.command !== "string" ||
58
+ !value.command ||
59
+ typeof value.repository !== "string" ||
60
+ !path.isAbsolute(value.repository) ||
28
61
  typeof value.nonce !== "string" ||
29
62
  !value.nonce)
30
63
  return null;
@@ -43,7 +76,9 @@ function writeLease(target, record) {
43
76
  }
44
77
  function removeStaleLeaseOrThrow(target, operation) {
45
78
  const current = readLease(target);
46
- if (current && processIsAlive(current.pid)) {
79
+ if (current &&
80
+ processIsAlive(current.pid) &&
81
+ processStartToken(current.pid) === current.processStartToken) {
47
82
  const contention = operation === "repair"
48
83
  ? "another repair is already in progress"
49
84
  : "another lifecycle mutation is already in progress";
@@ -61,28 +96,62 @@ function removeStaleLeaseOrThrow(target, operation) {
61
96
  export function acquireRepairLease(repo, operation = "repair") {
62
97
  const target = leasePath(repo);
63
98
  fs.mkdirSync(path.dirname(target), { recursive: true });
99
+ const lifecycleCommand = [
100
+ "repair",
101
+ "init",
102
+ "configure",
103
+ "index",
104
+ "lifecycle-hook",
105
+ ].includes(operation)
106
+ ? operation
107
+ : "repair";
108
+ const coordination = acquireLifecycleCoordination({
109
+ command: lifecycleCommand,
110
+ repository: repo,
111
+ verificationToken: process.env.KNODIN_UPDATE_VERIFICATION_TOKEN,
112
+ });
64
113
  const record = {
65
- schemaVersion: 1,
114
+ schemaVersion: 2,
66
115
  pid: process.pid,
67
116
  startedAt: new Date().toISOString(),
117
+ processStartToken: processStartToken(process.pid) ?? `node-start-${Date.now() - process.uptime() * 1000}`,
118
+ command: operation,
119
+ repository: path.resolve(repo),
68
120
  nonce: crypto.randomUUID(),
69
121
  };
70
122
  for (let attempt = 0; attempt < 2; attempt += 1) {
71
123
  try {
72
124
  writeLease(target, record);
125
+ writeActivity(repo, record);
73
126
  return {
74
127
  release() {
75
- const current = readLease(target);
76
- if (current?.nonce === record.nonce)
77
- fs.rmSync(target, { force: true });
128
+ try {
129
+ const current = readLease(target);
130
+ if (current?.nonce === record.nonce) {
131
+ writeActivity(repo, record, new Date().toISOString());
132
+ fs.rmSync(target, { force: true });
133
+ }
134
+ }
135
+ finally {
136
+ coordination.release();
137
+ }
78
138
  },
79
139
  };
80
140
  }
81
141
  catch (error) {
82
- if (error.code !== "EEXIST")
142
+ if (error.code !== "EEXIST") {
143
+ coordination.release();
83
144
  throw error;
84
- removeStaleLeaseOrThrow(target, operation);
145
+ }
146
+ try {
147
+ removeStaleLeaseOrThrow(target, operation);
148
+ }
149
+ catch (error_) {
150
+ coordination.release();
151
+ throw error_;
152
+ }
85
153
  }
86
154
  }
155
+ coordination.release();
87
156
  throw new Error(`knodin ${operation}: could not acquire the repository lifecycle lease; retry shortly`);
88
157
  }
@@ -42,9 +42,11 @@ const PROTECTED_CONTRACT_KEYS = new Set([
42
42
  "operation",
43
43
  "kind",
44
44
  "name",
45
+ "client",
45
46
  "target",
46
47
  "to",
47
48
  ]);
49
+ const PROTECTED_CONTRACT_ARRAY_PATHS = new Set(["$/agents/clients"]);
48
50
  function isTruncatableDetail(entry) {
49
51
  return (!entry.path.startsWith("$/sharedIndex/") &&
50
52
  (typeof entry.key !== "string" || !PROTECTED_CONTRACT_KEYS.has(entry.key)));
@@ -105,7 +107,9 @@ function truncatePayloadToBytes(root, byteLimit) {
105
107
  }
106
108
  while (bytes(root) > byteLimit) {
107
109
  const candidate = collectArrays(root)
108
- .filter((entry) => !entry.path.startsWith("$/responseBudget") && entry.value.length > 0)
110
+ .filter((entry) => !entry.path.startsWith("$/responseBudget") &&
111
+ !PROTECTED_CONTRACT_ARRAY_PATHS.has(entry.path) &&
112
+ entry.value.length > 0)
109
113
  .sort((a, b) => bytes(b.value) - bytes(a.value))[0];
110
114
  if (!candidate)
111
115
  break;
@@ -23,6 +23,7 @@ const RETRY_SAFE_OPERATIONS = new Set([
23
23
  "explain",
24
24
  "map",
25
25
  "query",
26
+ "query_batch",
26
27
  "review",
27
28
  "search",
28
29
  "status",
@@ -69,7 +70,9 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
69
70
  });
70
71
  const message = error instanceof Error ? error.message : String(error);
71
72
  return diagnostic.recorded
72
- ? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
73
+ ? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, {
74
+ cause: error,
75
+ })
73
76
  : error;
74
77
  }
75
78
  export function createServer(dispatch = handleKnodinTool) {
@@ -111,7 +114,11 @@ export function createServer(dispatch = handleKnodinTool) {
111
114
  void extra
112
115
  .sendNotification({
113
116
  method: "notifications/progress",
114
- params: { progressToken, progress: sequence, message: phase },
117
+ params: {
118
+ progressToken,
119
+ progress: sequence,
120
+ message: phase,
121
+ },
115
122
  })
116
123
  .catch(ignoreNotificationFailure);
117
124
  })
@@ -121,7 +128,9 @@ export function createServer(dispatch = handleKnodinTool) {
121
128
  if (progressToken === undefined)
122
129
  return;
123
130
  progressSequence++;
124
- recordMcpLifecycle(context, "progress", { sequence: progressSequence });
131
+ recordMcpLifecycle(context, "progress", {
132
+ sequence: progressSequence,
133
+ });
125
134
  await extra.sendNotification({
126
135
  method: "notifications/progress",
127
136
  params: {
@@ -143,7 +152,9 @@ export function createServer(dispatch = handleKnodinTool) {
143
152
  }, { cooperativeCancellation: operation === "repair" })),
144
153
  };
145
154
  const result = execution.result;
146
- recordMcpLifecycle(context, "success", { elapsedMs: Date.now() - startedAt });
155
+ recordMcpLifecycle(context, "success", {
156
+ elapsedMs: Date.now() - startedAt,
157
+ });
147
158
  return {
148
159
  _meta: {
149
160
  requestId: context.requestId,
@@ -192,7 +203,9 @@ export function createServer(dispatch = handleKnodinTool) {
192
203
  finally {
193
204
  if (heartbeat)
194
205
  clearInterval(heartbeat);
195
- recordMcpLifecycle(context, "cleanup", { elapsedMs: Date.now() - startedAt });
206
+ recordMcpLifecycle(context, "cleanup", {
207
+ elapsedMs: Date.now() - startedAt,
208
+ });
196
209
  }
197
210
  });
198
211
  return server;
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
5
+ import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_DTYPE, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
6
6
  import { KNODIN_SCHEMA_VERSION } from "../engine/index.js";
7
7
  import { prunePolicyDescriptor } from "../engine/prune.js";
8
8
  import { KNODIN_VERSION } from "../version.js";
@@ -44,6 +44,7 @@ export function currentSharedIndexCompatibility() {
44
44
  id: EMBEDDING_MODEL_ID,
45
45
  revision: EMBEDDING_MODEL_REVISION,
46
46
  dimensions: EMBEDDING_DIMENSIONS,
47
+ dtype: EMBEDDING_MODEL_DTYPE,
47
48
  runtime: `@huggingface/transformers@${packageVersion("@huggingface/transformers")}`,
48
49
  };
49
50
  const parsers = {
@@ -61,9 +62,10 @@ export function currentSharedIndexCompatibility() {
61
62
  modelDigest: digest(model),
62
63
  parserBundleDigest: digest(parsers),
63
64
  indexConfigDigest: digest({
64
- policyVersion: 1,
65
+ policyVersion: 2,
65
66
  databaseSchemaVersion: KNODIN_SCHEMA_VERSION,
66
67
  embeddingDimensions: EMBEDDING_DIMENSIONS,
68
+ embeddingDtype: EMBEDDING_MODEL_DTYPE,
67
69
  }),
68
70
  prunePolicyDigest: digest(prunePolicyDescriptor()),
69
71
  };