@astrosheep/keiyaku 2.9.12 → 2.9.13

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 (212) hide show
  1. package/README.md +22 -18
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +2 -2
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -0,0 +1,208 @@
1
+ import { launchDetachedProcess } from "./detached-process.js";
2
+ import { terminateProcessGroup } from "./process-tree.js";
3
+ import { acquireRunnerLeaseTransferTarget, observeRunnerLease, } from "./runner-lease.js";
4
+ export const RUN_CONTROL_BOOTSTRAP_ENV = "KEIYAKU_RUN_CONTROL_BOOTSTRAP";
5
+ function detail(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ function send(child, message) {
9
+ return new Promise((resolve, reject) => {
10
+ if (!child.send || !child.connected)
11
+ return reject(new Error("bootstrap channel is unavailable"));
12
+ child.send(message, (error) => error ? reject(error) : resolve());
13
+ });
14
+ }
15
+ function waitForMessage(child, coordinate, expected, deadline, now, setTimer, clearTimer) {
16
+ return new Promise((resolve, reject) => {
17
+ const cleanup = () => {
18
+ child.off("message", onMessage);
19
+ child.off("error", onError);
20
+ child.off("exit", onExit);
21
+ clearTimer(timer);
22
+ };
23
+ const onMessage = (value) => {
24
+ const message = value;
25
+ if (message.coordinate !== coordinate || message.kind !== expected)
26
+ return;
27
+ cleanup();
28
+ resolve();
29
+ };
30
+ const onError = (error) => { cleanup(); reject(error); };
31
+ const onExit = () => onError(new Error("child exited during bootstrap"));
32
+ const remaining = deadline - now();
33
+ const timer = setTimer(() => onError(new Error(`bootstrap deadline passed before ${expected}`)), Math.max(0, remaining));
34
+ child.on("message", onMessage);
35
+ child.on("error", onError);
36
+ child.on("exit", onExit);
37
+ });
38
+ }
39
+ async function stop(child, prefix, terminate) {
40
+ try {
41
+ await terminate(child);
42
+ return { status: "failed", stage: "transfer", detail: prefix };
43
+ }
44
+ catch (error) {
45
+ return { status: "failed", stage: "cleanup", detail: `${prefix}; termination could not be verified: ${detail(error)}` };
46
+ }
47
+ }
48
+ export async function openDetachedLeaseBootstrap(input) {
49
+ const terminate = input.terminate ?? terminateProcessGroup;
50
+ const now = input.now ?? Date.now;
51
+ let child;
52
+ try {
53
+ child = launchDetachedProcess(input.launch, input.spawn);
54
+ }
55
+ catch (error) {
56
+ try {
57
+ input.parentLease.close();
58
+ return { status: "failed", stage: "spawn", detail: detail(error) };
59
+ }
60
+ catch (closeError) {
61
+ return { status: "failed", stage: "cleanup", detail: `${detail(error)}; parent lease release failed: ${detail(closeError)}` };
62
+ }
63
+ }
64
+ const ready = waitForMessage(child, input.coordinate, "transfer-ready", input.deadline, now, setTimeout, clearTimeout);
65
+ const acquired = waitForMessage(child, input.coordinate, "ownership-acquired", input.deadline, now, setTimeout, clearTimeout);
66
+ void acquired.catch(() => { });
67
+ try {
68
+ await ready;
69
+ }
70
+ catch (error) {
71
+ let failure = detail(error);
72
+ try {
73
+ input.parentLease.close();
74
+ }
75
+ catch (closeError) {
76
+ failure += `; parent lease release failed: ${detail(closeError)}`;
77
+ }
78
+ return stop(child, failure, terminate);
79
+ }
80
+ let decision;
81
+ let result;
82
+ return {
83
+ status: "ready",
84
+ bootstrap: {
85
+ child,
86
+ async authorize() {
87
+ if (decision === "reject")
88
+ return { status: "failed", stage: "transfer", detail: "bootstrap was already rejected" };
89
+ if (decision)
90
+ return result;
91
+ decision = "authorize";
92
+ result = (async () => {
93
+ let transferFailure;
94
+ let transferMayBeOwnedByChild = false;
95
+ try {
96
+ input.parentLease.releaseReservationForTransfer();
97
+ await send(child, { kind: "promote", coordinate: input.coordinate });
98
+ transferMayBeOwnedByChild = true;
99
+ try {
100
+ input.parentLease.close();
101
+ }
102
+ catch (error) {
103
+ transferFailure = error;
104
+ }
105
+ try {
106
+ await acquired;
107
+ }
108
+ catch (error) {
109
+ transferFailure ??= error;
110
+ }
111
+ if (transferFailure && observeRunnerLease(input.leasePath).state !== "held")
112
+ throw transferFailure;
113
+ await send(child, { kind: "authorize", coordinate: input.coordinate });
114
+ return { status: "authorized", child };
115
+ }
116
+ catch (error) {
117
+ try {
118
+ input.parentLease.close();
119
+ }
120
+ catch { /* cleanup detail is secondary to transfer failure */ }
121
+ if (transferMayBeOwnedByChild && observeRunnerLease(input.leasePath).state === "held") {
122
+ try {
123
+ await send(child, { kind: "authorize", coordinate: input.coordinate });
124
+ }
125
+ catch { /* held lease already proves authority */ }
126
+ return { status: "authorized", child };
127
+ }
128
+ return stop(child, detail(error), terminate);
129
+ }
130
+ })();
131
+ return result;
132
+ },
133
+ async reject() {
134
+ if (decision === "authorize")
135
+ return { status: "failed", stage: "transfer", detail: "bootstrap was already authorized" };
136
+ if (decision)
137
+ return result;
138
+ decision = "reject";
139
+ result = (async () => {
140
+ try {
141
+ await send(child, { kind: "reject", coordinate: input.coordinate });
142
+ }
143
+ catch { /* termination proves rejection */ }
144
+ let failure = "launch rejected";
145
+ try {
146
+ input.parentLease.close();
147
+ }
148
+ catch (error) {
149
+ failure += `; parent lease release failed: ${detail(error)}`;
150
+ }
151
+ const stopped = await stop(child, failure, terminate);
152
+ return stopped.stage === "cleanup" ? stopped : { status: "rejected" };
153
+ })();
154
+ return result;
155
+ },
156
+ },
157
+ };
158
+ }
159
+ function sendUpstream(message) {
160
+ return new Promise((resolve, reject) => {
161
+ if (!process.send || !process.connected)
162
+ return reject(new Error("bootstrap channel is unavailable"));
163
+ process.send(message, (error) => error ? reject(error) : resolve());
164
+ });
165
+ }
166
+ function receive(coordinate, expected) {
167
+ return new Promise((resolve, reject) => {
168
+ const cleanup = () => {
169
+ process.off("message", onMessage);
170
+ process.off("disconnect", onDisconnect);
171
+ };
172
+ const onDisconnect = () => { cleanup(); reject(new Error("bootstrap channel disconnected")); };
173
+ const onMessage = (value) => {
174
+ const message = value;
175
+ if (message.coordinate !== coordinate || (message.kind !== expected && message.kind !== "reject"))
176
+ return;
177
+ cleanup();
178
+ resolve(message);
179
+ };
180
+ process.on("message", onMessage);
181
+ process.on("disconnect", onDisconnect);
182
+ });
183
+ }
184
+ export async function receiveDetachedLease(input) {
185
+ if (process.env[RUN_CONTROL_BOOTSTRAP_ENV] !== "1")
186
+ throw new Error("process lacks bootstrap authority");
187
+ let target;
188
+ let lease;
189
+ try {
190
+ target = acquireRunnerLeaseTransferTarget(input.leasePath);
191
+ await sendUpstream({ kind: "transfer-ready", coordinate: input.coordinate });
192
+ if ((await receive(input.coordinate, "promote")).kind === "reject")
193
+ return { status: "rejected" };
194
+ lease = target.promote();
195
+ target = undefined;
196
+ await sendUpstream({ kind: "ownership-acquired", coordinate: input.coordinate });
197
+ if ((await receive(input.coordinate, "authorize")).kind === "reject")
198
+ return { status: "rejected" };
199
+ process.disconnect?.();
200
+ const owned = lease;
201
+ lease = undefined;
202
+ return { status: "authorized", lease: owned };
203
+ }
204
+ finally {
205
+ lease?.close();
206
+ target?.close();
207
+ }
208
+ }
@@ -0,0 +1,14 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ /** Launches a process in its own POSIX process group when the platform supports it. */
3
+ export function launchDetachedProcess(input, spawnProcess = nodeSpawn) {
4
+ const child = spawnProcess(input.command, input.args, {
5
+ ...input.options,
6
+ detached: process.platform !== "win32",
7
+ });
8
+ if (child.pid === undefined) {
9
+ throw new Error("detached process did not expose a process id");
10
+ }
11
+ if (input.unref)
12
+ child.unref();
13
+ return child;
14
+ }
@@ -0,0 +1,6 @@
1
+ export { PROCESS_GROUP_KILL_GRACE_MS, ProcessTreeTerminationError, ownedProcessIdentity, ownedProcessIsLive, terminateOwnedProcess, terminateProcessGroup, } from "./process-tree.js";
2
+ export { launchDetachedProcess, } from "./detached-process.js";
3
+ export { openDetachedLeaseBootstrap, receiveDetachedLease, RUN_CONTROL_BOOTSTRAP_ENV, } from "./detached-lease-bootstrap.js";
4
+ export { initializeRunnerLeaseDatabase, acquireRunnerLease, acquireRunnerLeaseTransferTarget, acquireRunnerLostProof, observeRunnerLease, RunnerLeaseUnavailableError, } from "./runner-lease.js";
5
+ export { beginConnectionBoundRunnerClose, } from "./connection-bound-close.js";
6
+ export { acquireSqliteProcessLifetimeLock, initializeSqliteProcessLifetimeLockDatabase, isSqliteProcessLifetimeLockContention, openSqliteProcessLifetimeLockTransaction, SqliteProcessLifetimeLockError, sqliteProcessLifetimeLockErrorDetail, } from "./sqlite-process-lifetime-lock.js";
@@ -88,7 +88,7 @@ async function terminateIdentity(identity, options) {
88
88
  throw new Error(`owned process group ${identity.pid} remained after SIGKILL`);
89
89
  }
90
90
  }
91
- /** Terminate an orphaned runner identity retained by repository coordination. */
91
+ /** Terminate an orphaned process identity retained by caller-owned coordination. */
92
92
  export async function terminateOwnedProcess(identity, options = {}) {
93
93
  try {
94
94
  await terminateIdentity(identity, {
@@ -0,0 +1,172 @@
1
+ import { initializeSqliteProcessLifetimeLockDatabase, isSqliteProcessLifetimeLockContention, openSqliteProcessLifetimeLockTransaction, SqliteProcessLifetimeLockError, sqliteProcessLifetimeLockErrorDetail, } from "./sqlite-process-lifetime-lock.js";
2
+ export class RunnerLeaseUnavailableError extends Error {
3
+ code = "RUNNER_LEASE_UNAVAILABLE";
4
+ constructor(message, options) {
5
+ super(message, options);
6
+ this.name = "RunnerLeaseUnavailableError";
7
+ }
8
+ }
9
+ function errorDetail(error) {
10
+ return sqliteProcessLifetimeLockErrorDetail(error);
11
+ }
12
+ function isContention(error) {
13
+ return isSqliteProcessLifetimeLockContention(error)
14
+ || (error instanceof SqliteProcessLifetimeLockError && error.reason === "busy");
15
+ }
16
+ function openGuard(lockPath, busyTimeoutMs) {
17
+ return openSqliteProcessLifetimeLockTransaction({ path: lockPath, mode: "deferred", busyTimeoutMs });
18
+ }
19
+ function openReservation(lockPath, busyTimeoutMs) {
20
+ return openSqliteProcessLifetimeLockTransaction({ path: lockPath, mode: "immediate", busyTimeoutMs });
21
+ }
22
+ function closeTransaction(transaction) {
23
+ try {
24
+ transaction.close();
25
+ }
26
+ catch (error) {
27
+ return error;
28
+ }
29
+ }
30
+ function createHeldLock(lockPath, busyTimeoutMs, guard, initialReservation) {
31
+ let closed = false;
32
+ let reservation = initialReservation;
33
+ return {
34
+ releaseReservationForTransfer() {
35
+ if (closed || reservation === undefined)
36
+ return;
37
+ const released = reservation;
38
+ reservation = undefined;
39
+ const failure = closeTransaction(released);
40
+ if (failure !== undefined)
41
+ throw new RunnerLeaseUnavailableError(`runner lease reservation release failed: ${errorDetail(failure)}`, { cause: failure });
42
+ },
43
+ reclaimReservation() {
44
+ if (closed || reservation !== undefined)
45
+ return;
46
+ try {
47
+ reservation = openReservation(lockPath, busyTimeoutMs);
48
+ }
49
+ catch (error) {
50
+ throw new RunnerLeaseUnavailableError(`runner lease reservation reclaim failed: ${errorDetail(error)}`, { cause: error });
51
+ }
52
+ },
53
+ close() {
54
+ if (closed)
55
+ return;
56
+ closed = true;
57
+ let failure;
58
+ if (reservation !== undefined)
59
+ failure = closeTransaction(reservation);
60
+ failure ??= closeTransaction(guard);
61
+ if (failure !== undefined)
62
+ throw new RunnerLeaseUnavailableError(`runner lease release failed: ${errorDetail(failure)}`, { cause: failure });
63
+ },
64
+ };
65
+ }
66
+ function unavailable(operation, error) {
67
+ return new RunnerLeaseUnavailableError(`${operation}: ${errorDetail(error)}`, { cause: error });
68
+ }
69
+ export function initializeRunnerLeaseDatabase(leasePath) {
70
+ try {
71
+ return initializeSqliteProcessLifetimeLockDatabase(leasePath);
72
+ }
73
+ catch (error) {
74
+ throw unavailable("runner lease database could not be initialized", error);
75
+ }
76
+ }
77
+ export function acquireRunnerLease(lockPath, busyTimeoutMs = 0) {
78
+ let guard;
79
+ try {
80
+ guard = openGuard(lockPath, busyTimeoutMs);
81
+ }
82
+ catch (error) {
83
+ throw unavailable("runner lease database could not be opened", error);
84
+ }
85
+ try {
86
+ return createHeldLock(lockPath, busyTimeoutMs, guard, openReservation(lockPath, busyTimeoutMs));
87
+ }
88
+ catch (error) {
89
+ closeTransaction(guard);
90
+ if (isContention(error))
91
+ throw new RunnerLeaseUnavailableError("runner lease is already held", { cause: error });
92
+ throw unavailable("runner lease acquisition failed", error);
93
+ }
94
+ }
95
+ export function acquireRunnerLeaseTransferTarget(lockPath, busyTimeoutMs = 0) {
96
+ let guard;
97
+ try {
98
+ guard = openGuard(lockPath, busyTimeoutMs);
99
+ }
100
+ catch (error) {
101
+ throw unavailable("runner lease transfer acquisition failed", error);
102
+ }
103
+ let closed = false;
104
+ return {
105
+ promote() {
106
+ if (closed)
107
+ throw new RunnerLeaseUnavailableError("runner lease transfer target is closed");
108
+ try {
109
+ const reservation = openReservation(lockPath, busyTimeoutMs);
110
+ closed = true;
111
+ return createHeldLock(lockPath, busyTimeoutMs, guard, reservation);
112
+ }
113
+ catch (error) {
114
+ throw unavailable("runner lease transfer promotion failed", error);
115
+ }
116
+ },
117
+ close() {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ const failure = closeTransaction(guard);
122
+ if (failure !== undefined)
123
+ throw unavailable("runner lease transfer release failed", failure);
124
+ },
125
+ };
126
+ }
127
+ export function acquireRunnerLostProof(lockPath, busyTimeoutMs = 0) {
128
+ let transaction;
129
+ try {
130
+ transaction = openSqliteProcessLifetimeLockTransaction({ path: lockPath, mode: "exclusive", busyTimeoutMs });
131
+ }
132
+ catch (error) {
133
+ if (isContention(error))
134
+ throw new RunnerLeaseUnavailableError("runner lease is already held", { cause: error });
135
+ throw unavailable("runner lost proof acquisition failed", error);
136
+ }
137
+ let closed = false;
138
+ return {
139
+ close() {
140
+ if (closed)
141
+ return;
142
+ closed = true;
143
+ const failure = closeTransaction(transaction);
144
+ if (failure !== undefined)
145
+ throw unavailable("runner lost proof release failed", failure);
146
+ },
147
+ };
148
+ }
149
+ /** Positive death probe without creating a missing lock database. */
150
+ export function observeRunnerLease(lockPath) {
151
+ let probe;
152
+ try {
153
+ probe = openSqliteProcessLifetimeLockTransaction({ path: lockPath, mode: "exclusive", busyTimeoutMs: 0 });
154
+ return { state: "released" };
155
+ }
156
+ catch (error) {
157
+ if (isContention(error))
158
+ return { state: "held" };
159
+ const detail = errorDetail(error);
160
+ return {
161
+ state: "unknown",
162
+ reason: detail.includes("rollback-journal") ? "unsupported-journal-mode" : "probe-failed",
163
+ detail,
164
+ };
165
+ }
166
+ finally {
167
+ try {
168
+ probe?.close();
169
+ }
170
+ catch { /* no probe cleanup result is death evidence */ }
171
+ }
172
+ }
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { DatabaseSync } from "node:sqlite";
3
3
  const SQLITE_BUSY = 5;
4
- const SUPPORTED_PLATFORMS = new Set(["darwin", "linux", "win32"]);
5
4
  export class SqliteProcessLifetimeLockError extends Error {
6
5
  reason;
7
6
  code = "SQLITE_PROCESS_LIFETIME_LOCK_UNAVAILABLE";
@@ -11,16 +10,6 @@ export class SqliteProcessLifetimeLockError extends Error {
11
10
  this.name = "SqliteProcessLifetimeLockError";
12
11
  }
13
12
  }
14
- export class SqliteProcessLifetimeLockUnsupportedError extends Error {
15
- code = "SQLITE_PROCESS_LIFETIME_LOCK_UNSUPPORTED";
16
- constructor(platform) {
17
- super(`SQLite process-lifetime locks are unsupported on platform '${platform}'`);
18
- this.name = "SqliteProcessLifetimeLockUnsupportedError";
19
- }
20
- }
21
- export function isSqliteProcessLifetimeLockPlatformSupported(platform = process.platform) {
22
- return SUPPORTED_PLATFORMS.has(platform);
23
- }
24
13
  export function sqliteProcessLifetimeLockErrorDetail(error) {
25
14
  return error instanceof Error ? error.message : String(error);
26
15
  }
@@ -35,11 +24,6 @@ export function isSqliteProcessLifetimeLockContention(error) {
35
24
  return errcode === SQLITE_BUSY || code === "ERR_SQLITE_BUSY" || code === "ERR_SQLITE_LOCKED"
36
25
  || message.includes("database is locked") || message.includes("database is busy");
37
26
  }
38
- function assertPlatform(platform) {
39
- if (!isSqliteProcessLifetimeLockPlatformSupported(platform)) {
40
- throw new SqliteProcessLifetimeLockUnsupportedError(platform);
41
- }
42
- }
43
27
  function assertBusyTimeout(busyTimeoutMs) {
44
28
  if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0) {
45
29
  throw new SqliteProcessLifetimeLockError("SQLite process-lifetime lock busy timeout must be a non-negative safe integer", "invalid");
@@ -69,7 +53,7 @@ function closeTransaction(database) {
69
53
  return failure;
70
54
  }
71
55
  function throwLockError(prefix, error) {
72
- if (error instanceof SqliteProcessLifetimeLockError || error instanceof SqliteProcessLifetimeLockUnsupportedError)
56
+ if (error instanceof SqliteProcessLifetimeLockError)
73
57
  throw error;
74
58
  if (isSqliteProcessLifetimeLockContention(error)) {
75
59
  throw new SqliteProcessLifetimeLockError(`${prefix} is already held`, "busy", { cause: error });
@@ -78,10 +62,9 @@ function throwLockError(prefix, error) {
78
62
  }
79
63
  /**
80
64
  * Creates only a rollback-journal schema anchor. The empty database persists no
81
- * ownership facts; an open transaction is the complete ownership signal.
65
+ * ownership facts; an open transaction is the complete physical ownership signal.
82
66
  */
83
- export function initializeSqliteProcessLifetimeLockDatabase(lockPath, platform = process.platform) {
84
- assertPlatform(platform);
67
+ export function initializeSqliteProcessLifetimeLockDatabase(lockPath) {
85
68
  let database;
86
69
  try {
87
70
  database = new DatabaseSync(lockPath);
@@ -105,9 +88,7 @@ export function initializeSqliteProcessLifetimeLockDatabase(lockPath, platform =
105
88
  return lockPath;
106
89
  }
107
90
  export function openSqliteProcessLifetimeLockTransaction(input) {
108
- const platform = input.platform ?? process.platform;
109
91
  const busyTimeoutMs = input.busyTimeoutMs ?? 0;
110
- assertPlatform(platform);
111
92
  assertBusyTimeout(busyTimeoutMs);
112
93
  if (!input.create) {
113
94
  try {
@@ -130,7 +111,7 @@ export function openSqliteProcessLifetimeLockTransaction(input) {
130
111
  database.exec("CREATE TABLE IF NOT EXISTS lock_anchor (singleton INTEGER PRIMARY KEY CHECK (singleton = 1))");
131
112
  }
132
113
  database.exec(`BEGIN ${input.mode.toUpperCase()}`);
133
- // A DEFERRED transaction becomes the projection guard only after this read
114
+ // A DEFERRED transaction becomes a process-lifetime guard only after this read
134
115
  // acquires its SHARED lock. Keep this inside every transaction mode.
135
116
  assertSchema(database);
136
117
  if (journalMode(database) !== "delete") {
@@ -182,7 +163,6 @@ export async function acquireSqliteProcessLifetimeLock(input) {
182
163
  mode: "immediate",
183
164
  busyTimeoutMs: 0,
184
165
  create: true,
185
- platform: input.platform,
186
166
  });
187
167
  }
188
168
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { FlowError } from "../flow-error.js";
2
+ import { requireCommitCoordinate } from "../git/commits.js";
2
3
  import { createGit, parseRevListOutput, wrapGitError } from "../git/core.js";
3
4
  import { effectiveScope } from "./lifecycle-history.js";
4
5
  import { scopePathEvidence, } from "./scope.js";
@@ -42,13 +43,7 @@ export async function receiptsAfterFence(cwd, entries, head = "HEAD") {
42
43
  return revList(cwd, getFence(entries), await resolveCommit(cwd, head));
43
44
  }
44
45
  async function resolveCommit(cwd, revision) {
45
- const git = createGit(cwd);
46
- try {
47
- return (await git.raw(["rev-parse", "--verify", `${revision}^{commit}`])).trim();
48
- }
49
- catch (error) {
50
- throw wrapGitError(`rev-parse ${revision}`, error, cwd);
51
- }
46
+ return (await requireCommitCoordinate(cwd, revision)).commit;
52
47
  }
53
48
  async function revList(cwd, from, to) {
54
49
  const git = createGit(cwd);
@@ -120,15 +115,18 @@ async function auditScope(cwd, entries, paths) {
120
115
  ...await scopeViolations(cwd, paths, scope),
121
116
  ...taskNamespaceViolations(paths, findBind(entries).data.scope),
122
117
  ].filter((row, index, all) => all.findIndex((candidate) => candidate.path === row.path) === index);
123
- if (scopeViolationEvidence.length > 0) {
124
- throw new ScopeAuditError(findBind(entries).contract, scope, scopeViolationEvidence);
125
- }
126
118
  const violations = scopeViolationEvidence.map((violation) => violation.path);
127
119
  return {
128
120
  scopeAudit: { violations },
129
121
  scopeViolationEvidence,
130
122
  };
131
123
  }
124
+ function admitScope(contract, scope, capture, policy) {
125
+ if (policy === "refuse" && capture.scopeViolationEvidence.length > 0) {
126
+ throw new ScopeAuditError(contract, scope, capture.scopeViolationEvidence);
127
+ }
128
+ return capture;
129
+ }
132
130
  export class ScopeAuditError extends FlowError {
133
131
  scopeViolationEvidence;
134
132
  constructor(_contractId, scope, evidence) {
@@ -155,7 +153,7 @@ function currentFoldedSealArc(model) {
155
153
  return model.seal === null ? model.intent.arc : null;
156
154
  }
157
155
  /** Current-read admission capture: scope and fence come only from the folded model. */
158
- export async function captureFoldedSeal(cwd, model, head = "HEAD", scopeAppend = []) {
156
+ export async function captureFoldedSeal(cwd, model, head = "HEAD", scopeAppend = [], scopePolicy) {
159
157
  const arc = currentFoldedSealArc(model);
160
158
  if (arc === null)
161
159
  return null;
@@ -168,14 +166,18 @@ export async function captureFoldedSeal(cwd, model, head = "HEAD", scopeAppend =
168
166
  ]);
169
167
  const effectiveScope = [...model.effectiveScope, ...scopeAppend];
170
168
  const bind = model.history.find((row) => row.entry.kind === "bind")?.entry;
171
- const scopeViolationEvidence = [
172
- ...await scopeViolations(cwd, paths, effectiveScope),
173
- ...taskNamespaceViolations(paths, bind?.kind === "bind" ? bind.data.scope : []),
174
- ].filter((row, index, all) => all.findIndex((candidate) => candidate.path === row.path) === index);
175
- if (scopeViolationEvidence.length > 0) {
176
- throw new ScopeAuditError(model.contract, effectiveScope, scopeViolationEvidence);
169
+ const ordinaryViolations = await scopeViolations(cwd, paths, effectiveScope);
170
+ const namespaceViolations = taskNamespaceViolations(paths, bind?.kind === "bind" ? bind.data.scope : []);
171
+ if (namespaceViolations.length > 0) {
172
+ throw new ScopeAuditError(model.contract, effectiveScope, namespaceViolations);
177
173
  }
178
- return { arc, oldFence, newFence, receipts, diffstat: stat, scopeAudit: { violations: [] }, scopeViolationEvidence };
174
+ const scopeViolationEvidence = ordinaryViolations
175
+ .filter((row, index, all) => all.findIndex((candidate) => candidate.path === row.path) === index);
176
+ const scopeAudit = admitScope(model.contract, effectiveScope, {
177
+ scopeAudit: { violations: scopeViolationEvidence.map((violation) => violation.path) },
178
+ scopeViolationEvidence,
179
+ }, scopePolicy);
180
+ return { arc, oldFence, newFence, receipts, diffstat: stat, ...scopeAudit };
179
181
  }
180
182
  /** Current seal intent: open explicit arc, else derived a0 while no explicit arc has ever existed. */
181
183
  export function currentSealArc(entries) {
@@ -219,7 +221,14 @@ export async function previewSeal(cwd, entries, head = "HEAD") {
219
221
  };
220
222
  }
221
223
  export async function captureSeal(cwd, entries, head = "HEAD") {
222
- return (await previewSeal(cwd, entries, head)).capture;
224
+ const preview = await previewSeal(cwd, entries, head);
225
+ if (!preview.capture)
226
+ return null;
227
+ admitScope("direct seal", effectiveScope(entries), {
228
+ scopeAudit: preview.capture.scopeAudit,
229
+ scopeViolationEvidence: preview.capture.scopeViolationEvidence,
230
+ }, "refuse");
231
+ return preview.capture;
223
232
  }
224
233
  /** Folded renew capture: preserve receipt hashes while auditing rebased delivery content. */
225
234
  export async function captureFoldedRenewSeal(input) {
@@ -239,21 +248,24 @@ export async function captureFoldedRenewSeal(input) {
239
248
  ]);
240
249
  const scope = [...input.effectiveScope, ...input.pendingScopeOverlay];
241
250
  const bindScope = await acceptedBindScope(input.cwd, input.contract);
242
- const scopeViolationEvidence = [
243
- ...await scopeViolations(input.cwd, paths, scope),
244
- ...taskNamespaceViolations(paths, bindScope),
245
- ].filter((row, index, all) => all.findIndex((candidate) => candidate.path === row.path) === index);
246
- if (scopeViolationEvidence.length > 0) {
247
- throw new ScopeAuditError(input.contract, scope, scopeViolationEvidence);
251
+ const ordinaryViolations = await scopeViolations(input.cwd, paths, scope);
252
+ const namespaceViolations = taskNamespaceViolations(paths, bindScope);
253
+ if (namespaceViolations.length > 0) {
254
+ throw new ScopeAuditError(input.contract, scope, namespaceViolations);
248
255
  }
256
+ const scopeViolationEvidence = ordinaryViolations
257
+ .filter((row, index, all) => all.findIndex((candidate) => candidate.path === row.path) === index);
258
+ const scopeAudit = admitScope(input.contract, scope, {
259
+ scopeAudit: { violations: scopeViolationEvidence.map((violation) => violation.path) },
260
+ scopeViolationEvidence,
261
+ }, input.scopePolicy);
249
262
  return {
250
263
  arc,
251
264
  oldFence,
252
265
  newFence: receiptHead,
253
266
  receipts,
254
267
  diffstat: stat,
255
- scopeAudit: { violations: [] },
256
- scopeViolationEvidence,
268
+ ...scopeAudit,
257
269
  };
258
270
  }
259
271
  export async function sealCurrentArc(input) {
@@ -301,7 +313,7 @@ export async function sealCurrentArc(input) {
301
313
  prepare: async (admission) => {
302
314
  if (admission.business.existing)
303
315
  return { capture: admission.business.existing.capture, existing: { transactionObjectId: admission.business.existing.transactionObjectId } };
304
- return { capture: input.capture ?? await captureFoldedSeal(admission.business.targetRoot, admission.business.lifecycle, "HEAD") };
316
+ return { capture: input.capture ?? await captureFoldedSeal(admission.business.targetRoot, admission.business.lifecycle, "HEAD", [], "refuse") };
305
317
  },
306
318
  revalidate: ({ session, prepared }) => {
307
319
  assertOperable(input.contractId, session.lifecycle);
@@ -319,7 +331,7 @@ export async function sealCurrentArc(input) {
319
331
  effect: async ({ context, prepared }) => {
320
332
  const capture = prepared.capture;
321
333
  if (!capture)
322
- return { kind: "refused-before-effects", authoritativeHead: context.session.lifecycle.observedLedgerHead, refusal: { code: "NO_OPEN_ARC", message: "no current arc requires sealing" } };
334
+ return { kind: "refused-before-effects", authoritativeHead: context.session.pins.repositoryLedgerHead, refusal: { code: "NO_OPEN_ARC", message: "no current arc requires sealing" } };
323
335
  const at = new Date(input.nowMs).toISOString();
324
336
  {
325
337
  const { scopeViolationEvidence: _scopeViolationEvidence, ...durableCapture } = capture;
@@ -333,11 +345,11 @@ export async function sealCurrentArc(input) {
333
345
  };
334
346
  const result = prepared.existing
335
347
  ? await context.replayCommitted({ transactionObjectId: prepared.existing.transactionObjectId })
336
- : await context.publishAuthoritative({ verb: "seal", admittedLedgerHead: context.session.lifecycle.observedLedgerHead, acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId, entries: [entry] });
348
+ : await context.publishAuthoritative({ verb: "seal", admittedLedgerHead: context.session.pins.repositoryLedgerHead, acceptedLifecycleTail: context.session.lifecycle.contractHead, entries: [entry] });
337
349
  if (result.status === "indeterminate")
338
- return { kind: "precommit-effects-only", authoritativeHead: result.currentHead ?? context.session.lifecycle.observedLedgerHead };
350
+ return { kind: "precommit-effects-only", authoritativeHead: result.currentHead ?? context.session.pins.repositoryLedgerHead };
339
351
  if (result.status !== "committed")
340
- return { kind: "refused-before-effects", authoritativeHead: result.currentHead ?? context.session.lifecycle.observedLedgerHead, refusal: { code: "LEDGER_CAS_CONFLICT", message: `contract ${input.contractId} changed while sealing` } };
352
+ return { kind: "refused-before-effects", authoritativeHead: result.currentHead ?? context.session.pins.repositoryLedgerHead, refusal: { code: "LEDGER_CAS_CONFLICT", message: `contract ${input.contractId} changed while sealing` } };
341
353
  return { kind: "committed", commit: result, entryKind: "seal", value: {
342
354
  ...capture,
343
355
  sealedBy: input.sealedBy,