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
@@ -24,6 +24,7 @@ import { diagnoseFailure, } from "../failure-diagnosis.js";
24
24
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
25
25
  import { inspectRepositoryIntegrationStatus } from "../init.js";
26
26
  import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
27
+ import { readManagerUpdateState, readUpdateJournal, updateAttention } from "../manager-update.js";
27
28
  import { listMirrors } from "../mirror.js";
28
29
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
29
30
  import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
@@ -158,6 +159,13 @@ const GRAPH_INDEPENDENT_QUERY_PATTERNS = new Set([
158
159
  "lsp_implementations",
159
160
  "file_metrics",
160
161
  ]);
162
+ const BATCH_QUERY_PATTERNS = new Set([
163
+ "callers_of",
164
+ "callees_of",
165
+ "imports_of",
166
+ "importers_of",
167
+ "file_summary",
168
+ ]);
161
169
  function buildDocumentedKnodinTools() {
162
170
  return [
163
171
  {
@@ -514,8 +522,14 @@ function buildDocumentedKnodinTools() {
514
522
  repositories: { type: "array", items: { type: "string" } },
515
523
  scope: { type: "string", enum: ["personal", "team"] },
516
524
  distributed: { type: "boolean" },
517
- repositoryIds: { type: "object", additionalProperties: { type: "string" } },
518
- components: { type: "object", additionalProperties: { type: "object" } },
525
+ repositoryIds: {
526
+ type: "object",
527
+ additionalProperties: { type: "string" },
528
+ },
529
+ components: {
530
+ type: "object",
531
+ additionalProperties: { type: "object" },
532
+ },
519
533
  evidence: { type: "string" },
520
534
  relationshipType: { type: "string", enum: RELATIONSHIP_TYPES },
521
535
  requireComplete: { type: "boolean" },
@@ -814,7 +828,11 @@ async function handleRepositoriesOperation(options) {
814
828
  dryRun,
815
829
  manifestPath,
816
830
  indexMode: (target) => indexModeForPath(loadSystemConfiguration(target), target),
817
- isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command, memoryLimitBytes }),
831
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({
832
+ repository: target,
833
+ command,
834
+ memoryLimitBytes,
835
+ }),
818
836
  });
819
837
  const { repositories } = await inventory();
820
838
  return bounded({ schemaVersion: 1, ...summary, repositories }, "repositories:init");
@@ -1376,7 +1394,7 @@ async function handleDiagnosticsOperation(repo, args, bounded) {
1376
1394
  async function dispatchKnodinTool(args) {
1377
1395
  const startedAt = performance.now();
1378
1396
  const parsedArgs = (args ?? {});
1379
- const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositories, scope, distributed, repositoryIds, components, evidence, relationshipType, requireComplete, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, planDigest, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
1397
+ const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, queries, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositories, scope, distributed, repositoryIds, components, evidence, relationshipType, requireComplete, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, planDigest, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
1380
1398
  const repo = repoPath ?? process.cwd();
1381
1399
  const selector = {
1382
1400
  identity: expandCompactIdentity(identity),
@@ -1469,7 +1487,10 @@ async function dispatchKnodinTool(args) {
1469
1487
  "search",
1470
1488
  ].includes(rootOperation);
1471
1489
  const enriched = includeSharedIndex && result && typeof result === "object" && !Array.isArray(result)
1472
- ? { ...result, sharedIndex: sharedIndexEnvelope() }
1490
+ ? {
1491
+ ...result,
1492
+ sharedIndex: sharedIndexEnvelope(),
1493
+ }
1473
1494
  : result;
1474
1495
  const key = `${op}${minimal ? ":minimal" : ""}`;
1475
1496
  const output = applyResponseBudget(enriched, op, budget, RESPONSE_DEFAULTS[key] ?? RESPONSE_DEFAULTS.default);
@@ -1527,11 +1548,16 @@ async function dispatchKnodinTool(args) {
1527
1548
  return engine.status(repo, { audit: statusAudit }).then((result) => bounded({
1528
1549
  ...attachLifecycleHealth(repo, result),
1529
1550
  integration: inspectRepositoryIntegrationStatus(repo),
1530
- update: trustedUpdateStatus({
1531
- currentVersion: result.version,
1532
- installMethod: "unknown",
1533
- env: process.env,
1534
- }),
1551
+ update: {
1552
+ ...trustedUpdateStatus({
1553
+ currentVersion: result.version,
1554
+ installMethod: "unknown",
1555
+ env: process.env,
1556
+ }),
1557
+ managerState: readManagerUpdateState(),
1558
+ attention: updateAttention(readManagerUpdateState()),
1559
+ journal: readUpdateJournal(),
1560
+ },
1535
1561
  }, "status"));
1536
1562
  case "wait":
1537
1563
  return bounded(await waitForFresh(engine, repo, timeoutMs), "wait");
@@ -1719,6 +1745,39 @@ async function dispatchKnodinTool(args) {
1719
1745
  expectedLogin,
1720
1746
  }), "prs");
1721
1747
  }
1748
+ case "query_batch": {
1749
+ if (!process.env.KNODIN_UPDATE_VERIFICATION_TOKEN)
1750
+ throw new Error("knodin query_batch is restricted to the isolated update verifier");
1751
+ if (!Array.isArray(queries) || queries.length < 1 || queries.length > 32)
1752
+ throw new Error("knodin query_batch requires 1-32 `queries`");
1753
+ for (const item of queries) {
1754
+ if (!item ||
1755
+ typeof item.symbol !== "string" ||
1756
+ item.symbol.length === 0 ||
1757
+ !BATCH_QUERY_PATTERNS.has(item.pattern))
1758
+ throw new Error("knodin query_batch accepts only non-empty callers/callees/imports/importers/file-summary queries");
1759
+ }
1760
+ const queryHealth = await inspectGatewayGraphHealth(repo);
1761
+ if (!queryHealth.available)
1762
+ return bounded(queryHealth, "query_batch");
1763
+ const outputs = [];
1764
+ for (const item of queries) {
1765
+ outputs.push({
1766
+ pattern: item.pattern,
1767
+ symbol: item.symbol,
1768
+ output: await engine.query(item.pattern, item.symbol, repo, undefined, Math.min(limit ?? itemBudget ?? 100, itemBudget ?? 1000)),
1769
+ });
1770
+ }
1771
+ const verifiedQueryHealth = await inspectGatewayGraphHealth(repo);
1772
+ if (!verifiedQueryHealth.available)
1773
+ return bounded(verifiedQueryHealth, "query_batch");
1774
+ return bounded({
1775
+ results: outputs.map((item) => ({
1776
+ ...item,
1777
+ output: decorateGraphQueryResult(item.output, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness),
1778
+ })),
1779
+ }, "query_batch");
1780
+ }
1722
1781
  case "query": {
1723
1782
  if (!pattern)
1724
1783
  throw new Error("knodin query requires `pattern`");
@@ -40,7 +40,7 @@ export function validateRootCeremonyManifest(value) {
40
40
  manifest.productionActivation !== false)
41
41
  throw new Error("invalid fail-closed ceremony header");
42
42
  if (!Array.isArray(manifest.custodians) ||
43
- manifest.custodians.length < 3 ||
43
+ manifest.custodians.length < 1 ||
44
44
  new Set(manifest.custodians.map((c) => c.id)).size !== manifest.custodians.length ||
45
45
  manifest.custodians.some((c) => {
46
46
  exactKeys(c, c.role === "root-signer"
@@ -54,7 +54,7 @@ export function validateRootCeremonyManifest(value) {
54
54
  !Array.isArray(c.recoveryKeyIds) ||
55
55
  c.recoveryKeyIds.some((id) => !SHA256.test(id)))));
56
56
  }))
57
- throw new Error("at least three distinct named custodians are required");
57
+ throw new Error("at least one named maintainer custodian is required");
58
58
  const roles = ["root", "targets", "snapshot", "timestamp"];
59
59
  exactKeys(manifest.roleKeyIds, roles, "roleKeyIds");
60
60
  exactKeys(manifest.thresholds, roles, "thresholds");
@@ -68,14 +68,11 @@ export function validateRootCeremonyManifest(value) {
68
68
  throw new Error("role keys must be valid and disjoint");
69
69
  for (const id of ids)
70
70
  all.add(id);
71
- if (!Number.isInteger(threshold) ||
72
- threshold < (role === "root" || role === "targets" ? 2 : 1) ||
73
- threshold > ids.length)
71
+ if (!Number.isInteger(threshold) || threshold < 1 || threshold > ids.length)
74
72
  throw new Error(`invalid ${role} threshold`);
75
73
  }
76
- if (manifest.custodians.filter((c) => c.role === "root-signer").length < manifest.thresholds.root ||
77
- !manifest.custodians.some((c) => c.role === "recovery"))
78
- throw new Error("custodian roles do not satisfy threshold and recovery separation");
74
+ if (manifest.custodians.filter((c) => c.role === "root-signer").length < manifest.thresholds.root)
75
+ throw new Error("maintainer custodians do not satisfy the root threshold");
79
76
  const assigned = manifest.custodians
80
77
  .filter((c) => c.role === "root-signer")
81
78
  .map((c) => c.publicKeyId)
@@ -85,15 +82,9 @@ export function validateRootCeremonyManifest(value) {
85
82
  if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort(compareBytes).join("\0"))
86
83
  throw new Error("root signer assignments must exactly match root role keys");
87
84
  const recovery = manifest.custodians.filter((c) => c.role === "recovery");
88
- if (recovery.length < manifest.thresholds.root ||
89
- new Set(recovery.map((c) => c.evidenceId)).size !== recovery.length ||
90
- recovery.some((c) => !c.recoveryKeyIds?.length ||
91
- new Set(c.recoveryKeyIds).size !== c.recoveryKeyIds.length ||
92
- c.recoveryKeyIds.length >= manifest.thresholds.root))
93
- throw new Error("recovery sets must preserve root threshold independence");
94
- const recovered = new Set(recovery.flatMap((c) => c.recoveryKeyIds ?? []));
95
- if (manifest.roleKeyIds.root.some((id) => !recovered.has(id)))
96
- throw new Error("recovery assignments must cover every root role key");
85
+ if (new Set(recovery.map((c) => c.evidenceId)).size !== recovery.length ||
86
+ recovery.some((c) => !c.recoveryKeyIds?.length || new Set(c.recoveryKeyIds).size !== c.recoveryKeyIds.length))
87
+ throw new Error("recovery assignments must be unique and non-empty when declared");
97
88
  return manifest;
98
89
  }
99
90
  export function validateRootPinReviewReceipt(value, manifest) {
@@ -115,20 +106,21 @@ export function validateRootPinReviewReceipt(value, manifest) {
115
106
  receipt.ceremonyId !== manifest.ceremonyId ||
116
107
  !SHA256.test(receipt.rootEnvelopeSha256) ||
117
108
  !ID.test(receipt.reviewerId) ||
118
- manifest.custodians.some((c) => c.id === receipt.reviewerId) ||
119
109
  receipt.decision !== "approved" ||
120
- receipt.authentication !== "rehearsal-unsigned" ||
110
+ (manifest.purpose === "rehearsal"
111
+ ? receipt.authentication !== "rehearsal-unsigned" ||
112
+ manifest.custodians.some((c) => c.id === receipt.reviewerId)
113
+ : receipt.authentication !== "maintainer-attested" ||
114
+ !manifest.custodians.some((c) => c.id === receipt.reviewerId)) ||
121
115
  receipt.productionActivation !== false ||
122
116
  !Number.isFinite(Date.parse(receipt.reviewedAt)) ||
123
117
  new Date(receipt.reviewedAt).toISOString() !== receipt.reviewedAt)
124
- throw new Error("invalid or non-independent pin-review receipt");
118
+ throw new Error("invalid root-pin attestation receipt");
125
119
  return receipt;
126
120
  }
127
121
  export function preparePublicRootImport(envelope, pin, manifestValue, receiptValue, now = new Date()) {
128
122
  rejectPrivateMaterial(envelope, "rootEnvelope");
129
123
  const manifest = validateRootCeremonyManifest(manifestValue);
130
- if (manifest.purpose === "production")
131
- throw new Error("production import requires externally authenticated review and remains blocked");
132
124
  const receipt = validateRootPinReviewReceipt(receiptValue, manifest);
133
125
  if (Date.parse(receipt.reviewedAt) > now.getTime())
134
126
  throw new Error("pin-review receipt cannot be future-dated");
@@ -156,7 +148,11 @@ export function preparePublicRootImport(envelope, pin, manifestValue, receiptVal
156
148
  }
157
149
  const digest = createHash("sha256").update(canonicalizeUpdateMetadata(envelope)).digest("hex");
158
150
  if (pin !== digest || receipt.rootEnvelopeSha256 !== digest)
159
- throw new Error("root envelope, pin, and independent review receipt disagree");
151
+ throw new Error("root envelope, pin, and maintainer attestation disagree");
160
152
  verifyUpdateRootChain(envelope, pin, [], now);
161
- return { rootEnvelope: envelope, rootEnvelopeSha256: digest, productionActivation: false };
153
+ return {
154
+ rootEnvelope: envelope,
155
+ rootEnvelopeSha256: digest,
156
+ productionActivation: false,
157
+ };
162
158
  }
@@ -0,0 +1,158 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { readUpdateJournal } from "./manager-update.js";
7
+ function coordinationRoot(stateHome) {
8
+ const root = stateHome ?? process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state");
9
+ return path.join(root, "knodin", "coordination");
10
+ }
11
+ function processStartToken(pid) {
12
+ try {
13
+ if (process.platform === "win32") {
14
+ return execFileSync("powershell.exe", ["-NoProfile", "-Command", `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`], { encoding: "utf8", timeout: 2_000 }).trim();
15
+ }
16
+ return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
17
+ encoding: "utf8",
18
+ timeout: 2_000,
19
+ }).trim();
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function readLease(file) {
26
+ try {
27
+ const stat = fs.lstatSync(file);
28
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
29
+ return null;
30
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
31
+ return value.schemaVersion === 1 && typeof value.nonce === "string" ? value : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function live(record) {
38
+ return Boolean(record && processStartToken(record.pid) === record.processStartToken);
39
+ }
40
+ function writeExclusive(file, record) {
41
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
42
+ const descriptor = fs.openSync(file, "wx", 0o600);
43
+ try {
44
+ fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
45
+ fs.fsyncSync(descriptor);
46
+ }
47
+ finally {
48
+ fs.closeSync(descriptor);
49
+ }
50
+ }
51
+ function releaseOwned(file, nonce) {
52
+ const current = readLease(file);
53
+ if (current?.nonce !== nonce)
54
+ return;
55
+ try {
56
+ fs.unlinkSync(file);
57
+ }
58
+ catch (error) {
59
+ if (error.code !== "ENOENT")
60
+ throw error;
61
+ }
62
+ }
63
+ function updateLeaseFile(stateHome) {
64
+ return path.join(coordinationRoot(stateHome), "update.lock");
65
+ }
66
+ function lifecycleDirectory(stateHome) {
67
+ return path.join(coordinationRoot(stateHome), "lifecycle");
68
+ }
69
+ function tokenHash(token) {
70
+ return createHash("sha256").update(token).digest("hex");
71
+ }
72
+ function activeLifecycleLeases(stateHome) {
73
+ const directory = lifecycleDirectory(stateHome);
74
+ let names = [];
75
+ try {
76
+ names = fs.readdirSync(directory).slice(0, 4096);
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ const active = [];
82
+ for (const name of names) {
83
+ const file = path.join(directory, name);
84
+ const record = readLease(file);
85
+ if (live(record))
86
+ active.push(record);
87
+ else
88
+ releaseOwned(file, record?.nonce ?? "");
89
+ }
90
+ return active;
91
+ }
92
+ export function acquireLifecycleCoordination(options) {
93
+ const updateFile = updateLeaseFile(options.stateHome);
94
+ const update = readLease(updateFile);
95
+ if (live(update)) {
96
+ const permitted = options.verificationToken &&
97
+ update?.verificationTokenHash === tokenHash(options.verificationToken);
98
+ if (!permitted)
99
+ throw new Error("an executable update is active; retry lifecycle work after it completes");
100
+ }
101
+ else if (update) {
102
+ const journal = readUpdateJournal(options.stateHome);
103
+ if (journal && !["candidate-verified", "rollback-verified"].includes(journal.state)) {
104
+ throw new Error(`an interrupted update is ${journal.state}; run \`knodin update status\` to recover it`);
105
+ }
106
+ releaseOwned(updateFile, update.nonce);
107
+ }
108
+ const nonce = randomUUID();
109
+ const record = {
110
+ schemaVersion: 1,
111
+ kind: "lifecycle",
112
+ nonce,
113
+ pid: process.pid,
114
+ processStartToken: processStartToken(process.pid) ?? "unavailable",
115
+ command: options.command,
116
+ repository: fs.realpathSync(options.repository),
117
+ startedAt: new Date().toISOString(),
118
+ };
119
+ const file = path.join(lifecycleDirectory(options.stateHome), `${process.pid}-${nonce}.json`);
120
+ writeExclusive(file, record);
121
+ return { record, release: () => releaseOwned(file, nonce) };
122
+ }
123
+ export function acquireExclusiveUpdateCoordination(options) {
124
+ const active = activeLifecycleLeases(options.stateHome);
125
+ if (active.length > 0) {
126
+ throw new Error(`executable replacement deferred while ${active.length} lifecycle operation(s) are active`);
127
+ }
128
+ const file = updateLeaseFile(options.stateHome);
129
+ const existing = readLease(file);
130
+ if (live(existing))
131
+ throw new Error("another executable update is active");
132
+ if (existing) {
133
+ const journal = readUpdateJournal(options.stateHome);
134
+ if (journal &&
135
+ !["candidate-verified", "rollback-verified"].includes(journal.state) &&
136
+ !options.recovery) {
137
+ throw new Error(`interrupted update recovery required from journal state ${journal.state}`);
138
+ }
139
+ releaseOwned(file, existing.nonce);
140
+ }
141
+ const nonce = randomUUID();
142
+ const record = {
143
+ schemaVersion: 1,
144
+ kind: "update",
145
+ nonce,
146
+ pid: process.pid,
147
+ processStartToken: processStartToken(process.pid) ?? "unavailable",
148
+ command: "update",
149
+ startedAt: new Date().toISOString(),
150
+ verificationTokenHash: tokenHash(options.verificationToken),
151
+ };
152
+ writeExclusive(file, record);
153
+ return { record, release: () => releaseOwned(file, nonce) };
154
+ }
155
+ export function coordinationStatus(stateHome) {
156
+ const update = readLease(updateLeaseFile(stateHome));
157
+ return { update: live(update) ? update : null, lifecycle: activeLifecycleLeases(stateHome) };
158
+ }