@astrosheep/keiyaku 4.0.1 → 4.0.2

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 (245) hide show
  1. package/README.md +137 -5
  2. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +3 -3
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -35
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +98 -58
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +152 -53
  9. package/build/src/akuma/abort.d.ts +2 -0
  10. package/build/src/akuma/abort.js +50 -0
  11. package/build/src/akuma/akuma.d.ts +23 -14
  12. package/build/src/akuma/akuma.js +120 -104
  13. package/build/src/akuma/archetype.d.ts +7 -6
  14. package/build/src/akuma/archetype.js +68 -41
  15. package/build/src/akuma/body.d.ts +3 -5
  16. package/build/src/akuma/body.js +408 -213
  17. package/build/src/akuma/heart/facts.d.ts +25 -34
  18. package/build/src/akuma/heart/facts.js +18 -7
  19. package/build/src/akuma/heart/index.d.ts +39 -42
  20. package/build/src/akuma/heart/index.js +134 -93
  21. package/build/src/akuma/heart/rows.d.ts +9 -31
  22. package/build/src/akuma/heart/rows.js +26 -44
  23. package/build/src/akuma/heart/schema.d.ts +1 -1
  24. package/build/src/akuma/heart/schema.js +7 -15
  25. package/build/src/akuma/heart/soul.d.ts +8 -0
  26. package/build/src/akuma/heart/soul.js +136 -0
  27. package/build/src/akuma/heart/storage.d.ts +28 -14
  28. package/build/src/akuma/heart/storage.js +86 -26
  29. package/build/src/akuma/identity.d.ts +2 -2
  30. package/build/src/akuma/identity.js +9 -9
  31. package/build/src/akuma/index.d.ts +6 -1
  32. package/build/src/akuma/index.js +6 -0
  33. package/build/src/akuma/projection.d.ts +117 -38
  34. package/build/src/akuma/projection.js +208 -97
  35. package/build/src/akuma/provider-recipe.d.ts +24 -0
  36. package/build/src/akuma/provider-recipe.js +100 -0
  37. package/build/src/akuma/provider.d.ts +12 -4
  38. package/build/src/akuma/provider.js +117 -70
  39. package/build/src/akuma/providers/acp/core.d.ts +19 -0
  40. package/build/src/akuma/providers/acp/core.js +159 -0
  41. package/build/src/akuma/providers/acp/events.d.ts +20 -0
  42. package/build/src/akuma/providers/acp/events.js +73 -0
  43. package/build/src/akuma/providers/acp/index.d.ts +12 -0
  44. package/build/src/akuma/providers/acp/index.js +94 -0
  45. package/build/src/akuma/providers/claude/events.js +66 -13
  46. package/build/src/akuma/providers/claude/index.d.ts +2 -1
  47. package/build/src/akuma/providers/claude/index.js +35 -21
  48. package/build/src/akuma/providers/codex-app-server/events.js +4 -1
  49. package/build/src/akuma/providers/codex-app-server/index.d.ts +3 -3
  50. package/build/src/akuma/providers/codex-app-server/index.js +24 -31
  51. package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
  52. package/build/src/akuma/providers/grok-build/index.js +70 -0
  53. package/build/src/akuma/providers/index.d.ts +5 -3
  54. package/build/src/akuma/providers/index.js +24 -59
  55. package/build/src/akuma/providers/opencode-sdk/events.d.ts +40 -34
  56. package/build/src/akuma/providers/opencode-sdk/events.js +220 -101
  57. package/build/src/akuma/providers/opencode-sdk/index.d.ts +3 -2
  58. package/build/src/akuma/providers/opencode-sdk/index.js +233 -128
  59. package/build/src/akuma/providers/opencode-sdk/session.d.ts +11 -26
  60. package/build/src/akuma/providers/opencode-sdk/session.js +10 -9
  61. package/build/src/akuma/providers/pi/events.js +49 -11
  62. package/build/src/akuma/providers/pi/index.d.ts +1 -1
  63. package/build/src/akuma/providers/pi/index.js +50 -21
  64. package/build/src/akuma/publication.d.ts +3 -3
  65. package/build/src/akuma/publication.js +28 -40
  66. package/build/src/akuma/requests.d.ts +9 -7
  67. package/build/src/akuma/requests.js +92 -65
  68. package/build/src/alias/index.d.ts +2 -2
  69. package/build/src/alias/index.js +10 -10
  70. package/build/src/body/decode.js +7 -1
  71. package/build/src/body/region.d.ts +2 -0
  72. package/build/src/body/region.js +77 -7
  73. package/build/src/cli/accepted.d.ts +17 -13
  74. package/build/src/cli/accepted.js +72 -34
  75. package/build/src/cli/actor.js +3 -3
  76. package/build/src/cli/commands/akuma-invoke.d.ts +4 -3
  77. package/build/src/cli/commands/akuma-invoke.js +14 -9
  78. package/build/src/cli/commands/akuma.d.ts +13 -3
  79. package/build/src/cli/commands/akuma.js +67 -34
  80. package/build/src/cli/commands/contract.d.ts +18 -5
  81. package/build/src/cli/commands/contract.js +9 -3
  82. package/build/src/cli/commands/task-invoke.d.ts +17 -4
  83. package/build/src/cli/commands/task-invoke.js +43 -20
  84. package/build/src/cli/commands/task-query.d.ts +5 -0
  85. package/build/src/cli/commands/task-query.js +247 -0
  86. package/build/src/cli/commands/task.d.ts +3 -1
  87. package/build/src/cli/commands/task.js +51 -13
  88. package/build/src/cli/coordinates.d.ts +18 -0
  89. package/build/src/cli/coordinates.js +120 -0
  90. package/build/src/cli/draft.d.ts +8 -0
  91. package/build/src/cli/draft.js +96 -0
  92. package/build/src/cli/invoke.d.ts +4 -3
  93. package/build/src/cli/invoke.js +160 -179
  94. package/build/src/cli/main.js +34 -6
  95. package/build/src/cli/parse.d.ts +9 -2
  96. package/build/src/cli/parse.js +29 -7
  97. package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
  98. package/build/src/cli/render/akuma-tool-command.js +144 -0
  99. package/build/src/cli/render/akuma-tool.d.ts +2 -2
  100. package/build/src/cli/render/akuma-tool.js +30 -4
  101. package/build/src/cli/render/akuma.d.ts +1 -0
  102. package/build/src/cli/render/akuma.js +211 -116
  103. package/build/src/cli/render/audit.d.ts +3 -0
  104. package/build/src/cli/render/audit.js +104 -0
  105. package/build/src/cli/render/contract.d.ts +3 -3
  106. package/build/src/cli/render/contract.js +192 -79
  107. package/build/src/cli/render/kanshi.js +263 -168
  108. package/build/src/cli/render/receipt.d.ts +19 -0
  109. package/build/src/cli/render/receipt.js +106 -0
  110. package/build/src/cli/render/refusal.d.ts +16 -2
  111. package/build/src/cli/render/refusal.js +88 -20
  112. package/build/src/cli/render/region.d.ts +2 -0
  113. package/build/src/cli/render/region.js +17 -0
  114. package/build/src/cli/render/task.d.ts +2 -1
  115. package/build/src/cli/render/task.js +223 -60
  116. package/build/src/cli/render/terminal.d.ts +7 -0
  117. package/build/src/cli/render/terminal.js +46 -0
  118. package/build/src/cli/render/text.js +6 -3
  119. package/build/src/cli/result.d.ts +115 -20
  120. package/build/src/cli/usage.d.ts +1 -0
  121. package/build/src/cli/usage.js +3 -0
  122. package/build/src/contract-worktree.d.ts +6 -5
  123. package/build/src/contract-worktree.js +95 -57
  124. package/build/src/coordination/durable-file.d.ts +4 -3
  125. package/build/src/coordination/durable-file.js +29 -25
  126. package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
  127. package/build/src/coordination/sqlite-transaction-lock.js +22 -5
  128. package/build/src/core/facts/fold.js +1 -6
  129. package/build/src/core/facts/gate.d.ts +1 -0
  130. package/build/src/core/facts/gate.js +1 -0
  131. package/build/src/core/verbs/amend.d.ts +1 -1
  132. package/build/src/core/verbs/amend.js +0 -11
  133. package/build/src/core/verbs/deliver.d.ts +1 -1
  134. package/build/src/core/verbs/deliver.js +1 -7
  135. package/build/src/core/verbs/placement.d.ts +1 -1
  136. package/build/src/core/verbs/placement.js +4 -1
  137. package/build/src/dispatch/index.d.ts +2 -2
  138. package/build/src/dispatch/index.js +17 -17
  139. package/build/src/git/admission.d.ts +1 -1
  140. package/build/src/git/admission.js +11 -11
  141. package/build/src/git/hooks.js +13 -13
  142. package/build/src/git/integration.d.ts +14 -15
  143. package/build/src/git/integration.js +80 -44
  144. package/build/src/git/observe.d.ts +5 -3
  145. package/build/src/git/observe.js +18 -12
  146. package/build/src/git/read-observation.js +9 -9
  147. package/build/src/git/reconcile.d.ts +2 -0
  148. package/build/src/git/reconcile.js +138 -95
  149. package/build/src/git/repository.d.ts +27 -19
  150. package/build/src/git/repository.js +155 -83
  151. package/build/src/git/scratch.d.ts +7 -3
  152. package/build/src/git/scratch.js +44 -37
  153. package/build/src/git/target-placement.d.ts +40 -4
  154. package/build/src/git/target-placement.js +221 -101
  155. package/build/src/git/tender.d.ts +6 -5
  156. package/build/src/git/tender.js +24 -43
  157. package/build/src/git/terminal-seal.d.ts +1 -1
  158. package/build/src/git/terminal-seal.js +7 -5
  159. package/build/src/git/workspace.d.ts +36 -4
  160. package/build/src/git/workspace.js +53 -18
  161. package/build/src/identity/selector.js +1 -1
  162. package/build/src/index.d.ts +3 -3
  163. package/build/src/index.js +1 -1
  164. package/build/src/kanshi/index.d.ts +2 -2
  165. package/build/src/kanshi/index.js +1 -1
  166. package/build/src/kanshi/read.d.ts +2 -1
  167. package/build/src/kanshi/read.js +118 -27
  168. package/build/src/kanshi/report.d.ts +50 -0
  169. package/build/src/kanshi/select.d.ts +5 -0
  170. package/build/src/kanshi/select.js +32 -0
  171. package/build/src/library/address.d.ts +12 -3
  172. package/build/src/library/address.js +77 -46
  173. package/build/src/library/akuma-creation.d.ts +6 -1
  174. package/build/src/library/akuma-creation.js +86 -13
  175. package/build/src/library/audit.d.ts +18 -0
  176. package/build/src/library/audit.js +44 -0
  177. package/build/src/library/bind.js +4 -4
  178. package/build/src/library/catalog.d.ts +1 -1
  179. package/build/src/library/catalog.js +5 -8
  180. package/build/src/library/contract.d.ts +5 -6
  181. package/build/src/library/contract.js +11 -35
  182. package/build/src/library/delivery.d.ts +2 -1
  183. package/build/src/library/fleet.d.ts +2 -2
  184. package/build/src/library/fleet.js +35 -71
  185. package/build/src/library/keiyaku.d.ts +3 -2
  186. package/build/src/library/keiyaku.js +1 -0
  187. package/build/src/library/mutation.d.ts +7 -6
  188. package/build/src/library/mutation.js +23 -25
  189. package/build/src/library/reconcile.d.ts +29 -0
  190. package/build/src/library/reconcile.js +179 -0
  191. package/build/src/library/region.d.ts +12 -0
  192. package/build/src/library/region.js +15 -1
  193. package/build/src/library/repo.d.ts +4 -16
  194. package/build/src/library/repo.js +13 -38
  195. package/build/src/protocol/attempt.js +6 -6
  196. package/build/src/protocol/bind.js +4 -4
  197. package/build/src/protocol/intent.d.ts +15 -2
  198. package/build/src/protocol/intent.js +21 -6
  199. package/build/src/protocol/operations.d.ts +73 -22
  200. package/build/src/protocol/operations.js +292 -88
  201. package/build/src/protocol/outcome.d.ts +9 -3
  202. package/build/src/protocol/outcome.js +3 -1
  203. package/build/src/protocol/placement.d.ts +15 -4
  204. package/build/src/protocol/placement.js +58 -25
  205. package/build/src/protocol/read/status.d.ts +8 -1
  206. package/build/src/protocol/read/status.js +61 -14
  207. package/build/src/runtime/proc/line-rpc.d.ts +3 -8
  208. package/build/src/runtime/proc/line-rpc.js +16 -43
  209. package/build/src/runtime/proc/run.d.ts +15 -29
  210. package/build/src/runtime/proc/run.js +113 -105
  211. package/build/src/runtime/proc/stdio.d.ts +18 -0
  212. package/build/src/runtime/proc/stdio.js +58 -0
  213. package/build/src/settings.d.ts +2 -2
  214. package/build/src/settings.js +8 -8
  215. package/build/src/settlement/fence.d.ts +0 -4
  216. package/build/src/settlement/fence.js +0 -9
  217. package/build/src/settlement/holder.d.ts +11 -1
  218. package/build/src/settlement/holder.js +25 -0
  219. package/build/src/settlement/settle.d.ts +1 -1
  220. package/build/src/settlement/settle.js +52 -89
  221. package/build/src/task/board.d.ts +3 -3
  222. package/build/src/task/board.js +6 -9
  223. package/build/src/task/compose.d.ts +1 -1
  224. package/build/src/task/compose.js +18 -10
  225. package/build/src/task/context.d.ts +3 -3
  226. package/build/src/task/context.js +14 -14
  227. package/build/src/task/document.d.ts +2 -1
  228. package/build/src/task/document.js +15 -5
  229. package/build/src/task/index.d.ts +19 -7
  230. package/build/src/task/index.js +42 -23
  231. package/build/src/task/operations.d.ts +15 -9
  232. package/build/src/task/operations.js +78 -48
  233. package/build/src/task/query.d.ts +67 -0
  234. package/build/src/task/query.js +279 -0
  235. package/build/src/task/store.d.ts +2 -2
  236. package/build/src/task/store.js +40 -25
  237. package/build/src/verification/execution.d.ts +4 -2
  238. package/build/src/verification/execution.js +9 -5
  239. package/build/src/workspace-place.d.ts +41 -0
  240. package/build/src/workspace-place.js +386 -0
  241. package/build/src/world.d.ts +13 -2
  242. package/build/src/world.js +81 -48
  243. package/package.json +3 -2
  244. package/build/src/protocol/read/audit.d.ts +0 -31
  245. package/build/src/protocol/read/audit.js +0 -55
@@ -1,5 +1,7 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { closeSync, openSync } from "node:fs";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { open } from "node:fs/promises";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { promisify } from "node:util";
3
5
  const TERMINATION_GRACE_MS = 250;
4
6
  const STREAM_TAIL_BYTES = 16 * 1024;
5
7
  function tailCapture(limit) {
@@ -31,25 +33,21 @@ function tailCapture(limit) {
31
33
  },
32
34
  };
33
35
  }
34
- function wait(milliseconds) {
35
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
36
- }
37
36
  function ignoreMissingProcess(error) {
38
37
  if (error.code !== "ESRCH")
39
38
  throw error;
40
39
  }
41
- function terminateWindowsTree(pid) {
42
- return new Promise((resolve) => {
43
- const taskkill = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
44
- stdio: "ignore",
45
- windowsHide: true,
46
- });
47
- taskkill.once("error", resolve);
48
- taskkill.once("close", resolve);
40
+ const WINDOWS_TERMINATION_TIMEOUT_MS = 1_000;
41
+ const execFileAsync = promisify(execFile);
42
+ async function terminateWindowsTree(pid) {
43
+ await execFileAsync("taskkill", ["/PID", String(pid), "/T", "/F"], {
44
+ timeout: WINDOWS_TERMINATION_TIMEOUT_MS,
45
+ windowsHide: true,
49
46
  });
50
47
  }
51
- export async function terminateProcessTree(pid, force = false) {
52
- if (pid === undefined)
48
+ export async function terminateOwnedProcess(child, force = false) {
49
+ const pid = child.pid;
50
+ if (pid === undefined || child.exitCode !== null || child.signalCode !== null)
53
51
  return;
54
52
  if (process.platform === "win32") {
55
53
  await terminateWindowsTree(pid);
@@ -64,6 +62,11 @@ export async function terminateProcessTree(pid, force = false) {
64
62
  }
65
63
  return;
66
64
  }
65
+ let exited = false;
66
+ const exit = new Promise((resolve) => {
67
+ child.once("exit", () => { exited = true; resolve(); });
68
+ child.once("close", () => { exited = true; resolve(); });
69
+ });
67
70
  try {
68
71
  process.kill(-pid, "SIGTERM");
69
72
  }
@@ -71,7 +74,9 @@ export async function terminateProcessTree(pid, force = false) {
71
74
  ignoreMissingProcess(error);
72
75
  return;
73
76
  }
74
- await wait(TERMINATION_GRACE_MS);
77
+ await Promise.race([exit, delay(TERMINATION_GRACE_MS)]);
78
+ if (exited || child.exitCode !== null || child.signalCode !== null)
79
+ return;
75
80
  try {
76
81
  process.kill(-pid, "SIGKILL");
77
82
  }
@@ -79,49 +84,14 @@ export async function terminateProcessTree(pid, force = false) {
79
84
  ignoreMissingProcess(error);
80
85
  }
81
86
  }
82
- function processStartToken(pid) {
83
- const command = process.platform === "win32"
84
- ? ["powershell.exe", "-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CreationDate`]
85
- : ["ps", "-o", "lstart=", "-p", String(pid)];
86
- const result = spawnSync(command[0], command.slice(1), { encoding: "utf8", windowsHide: true });
87
- const token = result.status === 0 ? result.stdout.trim() : "";
88
- return token.length === 0 ? null : token;
89
- }
90
- export function currentProcessCollar() {
91
- const identity = currentProcessIdentity();
92
- return { ...identity, processGroup: identity.pid };
93
- }
94
- export function currentProcessIdentity() {
95
- const spawnedAt = processStartToken(process.pid);
96
- if (spawnedAt === null)
97
- throw new Error(`cannot read process start identity for ${process.pid}`);
98
- return { pid: process.pid, spawnedAt };
99
- }
100
- export function probeProcessIdentity(identity) {
101
- try {
102
- process.kill(identity.pid, 0);
103
- }
104
- catch (error) {
105
- const code = error.code;
106
- if (code === "ESRCH")
107
- return { kind: "gone" };
108
- return { kind: "unverifiable", diagnostic: error instanceof Error ? error.message : String(error) };
109
- }
110
- const current = processStartToken(identity.pid);
111
- if (current === null)
112
- return { kind: "unverifiable", diagnostic: `cannot read process start identity for ${identity.pid}` };
113
- if (current === identity.spawnedAt)
114
- return { kind: "alive" };
115
- return { kind: "replaced" };
116
- }
117
87
  export async function spawnDetachedProcess(input) {
118
- const log = openSync(input.log, "a");
88
+ const log = await open(input.log, "a");
119
89
  try {
120
90
  const child = spawn(input.argv[0], input.argv.slice(1), {
121
91
  cwd: input.cwd,
122
92
  env: input.env,
123
93
  detached: true,
124
- stdio: ["ignore", log, log],
94
+ stdio: ["ignore", log.fd, log.fd],
125
95
  windowsHide: true,
126
96
  });
127
97
  await new Promise((resolve, reject) => {
@@ -130,48 +100,47 @@ export async function spawnDetachedProcess(input) {
130
100
  });
131
101
  if (child.pid === undefined)
132
102
  throw new Error("detached process spawned without a pid");
133
- const spawnedAt = processStartToken(child.pid);
134
- if (spawnedAt === null)
135
- throw new Error(`cannot read process start identity for ${child.pid}`);
136
- child.unref();
137
- return { pid: child.pid, processGroup: child.pid, spawnedAt };
103
+ const pid = child.pid;
104
+ let state = "active";
105
+ let termination;
106
+ const invalidate = () => { state = "inert"; };
107
+ child.once("exit", invalidate);
108
+ child.once("close", invalidate);
109
+ return {
110
+ pid,
111
+ terminate(force = false) {
112
+ if (state === "inert")
113
+ return Promise.resolve();
114
+ if (termination !== undefined)
115
+ return termination;
116
+ state = "terminating";
117
+ termination = terminateOwnedProcess(child, force).finally(invalidate);
118
+ return termination;
119
+ },
120
+ release: () => {
121
+ if (state === "inert")
122
+ return;
123
+ state = "inert";
124
+ child.unref();
125
+ },
126
+ };
138
127
  }
139
128
  finally {
140
- closeSync(log);
141
- }
142
- }
143
- export function probeProcessTree(collar) {
144
- try {
145
- process.kill(process.platform === "win32" ? collar.pid : -collar.processGroup, 0);
146
- }
147
- catch (error) {
148
- const code = error.code;
149
- if (code === "ESRCH")
150
- return { kind: "gone" };
151
- return { kind: "unverifiable", diagnostic: error instanceof Error ? error.message : String(error) };
129
+ await log.close();
152
130
  }
153
- const current = processStartToken(collar.pid);
154
- if (current === null && process.platform !== "win32")
155
- return { kind: "alive" };
156
- if (current === collar.spawnedAt)
157
- return { kind: "alive" };
158
- return { kind: "unverifiable", diagnostic: `process ${collar.pid} no longer matches its recorded start identity` };
159
131
  }
160
- export async function putDownProcessTree(collar) {
161
- const before = probeProcessTree(collar);
162
- if (before.kind === "gone")
163
- return "already-dead";
164
- if (before.kind === "unverifiable")
165
- return "unavailable";
166
- await terminateProcessTree(collar.pid);
167
- const after = probeProcessTree(collar);
168
- if (after.kind === "gone")
169
- return "killed";
170
- return after.kind === "alive" ? "alive-after-sigkill" : "unavailable";
132
+ function terminalOutcome(code, stdout, stderr, consuming) {
133
+ return {
134
+ kind: "terminal",
135
+ code,
136
+ stdout: consuming ? "" : stdout.text,
137
+ stderr: stderr.text,
138
+ truncated: (consuming ? false : stdout.truncated) || stderr.truncated,
139
+ };
171
140
  }
172
- async function executeProcess(input, timeoutMs) {
141
+ async function executeProcess(input, timeoutMs, consumeStdout) {
173
142
  if (input.signal?.aborted === true)
174
- return { kind: "cancelled" };
143
+ return { outcome: { kind: "cancelled" }, pid: null };
175
144
  const child = spawn(input.argv[0], input.argv.slice(1), {
176
145
  cwd: input.cwd,
177
146
  env: input.env,
@@ -181,16 +150,38 @@ async function executeProcess(input, timeoutMs) {
181
150
  });
182
151
  const stdout = tailCapture(STREAM_TAIL_BYTES);
183
152
  const stderr = tailCapture(STREAM_TAIL_BYTES);
184
- child.stdout.on("data", (chunk) => stdout.append(chunk));
185
- child.stderr.on("data", (chunk) => stderr.append(chunk));
186
153
  let stop;
154
+ let streamError;
187
155
  let termination;
188
156
  const requestStop = (reason) => {
189
157
  if (stop !== undefined)
190
158
  return;
191
159
  stop = reason;
192
- termination = terminateProcessTree(child.pid);
160
+ termination = terminateOwnedProcess(child);
161
+ };
162
+ const failStream = (error) => {
163
+ if (streamError !== undefined)
164
+ return;
165
+ streamError = error;
166
+ requestStop("cancelled");
193
167
  };
168
+ child.stdout.on("data", (chunk) => {
169
+ if (consumeStdout === undefined) {
170
+ stdout.append(chunk);
171
+ return;
172
+ }
173
+ try {
174
+ consumeStdout(chunk);
175
+ }
176
+ catch (error) {
177
+ failStream(error);
178
+ }
179
+ });
180
+ child.stderr.on("data", (chunk) => stderr.append(chunk));
181
+ if (consumeStdout !== undefined) {
182
+ child.stdout.once("error", failStream);
183
+ child.stderr.once("error", failStream);
184
+ }
194
185
  const cancel = () => requestStop("cancelled");
195
186
  input.signal?.addEventListener("abort", cancel, { once: true });
196
187
  const timeout = timeoutMs === undefined ? undefined : setTimeout(() => requestStop("timeout"), timeoutMs);
@@ -202,25 +193,42 @@ async function executeProcess(input, timeoutMs) {
202
193
  clearTimeout(timeout);
203
194
  input.signal?.removeEventListener("abort", cancel);
204
195
  await termination;
205
- if (terminal.kind === "spawn-error")
206
- return { kind: "spawn-error", diagnostic: terminal.error.message };
196
+ const pid = child.pid ?? null;
197
+ if (terminal.kind === "spawn-error") {
198
+ return { outcome: { kind: "spawn-error", diagnostic: terminal.error.message }, pid };
199
+ }
200
+ if (streamError !== undefined) {
201
+ return {
202
+ outcome: {
203
+ kind: "stream-error",
204
+ diagnostic: streamError instanceof Error ? streamError.message : String(streamError),
205
+ },
206
+ pid,
207
+ };
208
+ }
207
209
  if (stop !== undefined)
208
- return { kind: stop };
210
+ return { outcome: { kind: stop }, pid };
209
211
  if (terminal.code === null)
210
- return { kind: "unknown-exit" };
212
+ return { outcome: { kind: "unknown-exit" }, pid };
211
213
  const capturedStdout = stdout.result();
212
214
  const capturedStderr = stderr.result();
213
215
  return {
214
- kind: "terminal",
215
- code: terminal.code,
216
- stdout: capturedStdout.text,
217
- stderr: capturedStderr.text,
218
- truncated: capturedStdout.truncated || capturedStderr.truncated,
216
+ outcome: terminalOutcome(terminal.code, capturedStdout, capturedStderr, consumeStdout !== undefined),
217
+ pid,
219
218
  };
220
219
  }
221
- export function runProcess(input) {
222
- return executeProcess(input, input.timeoutMs);
220
+ export async function runProcess(input) {
221
+ const result = await executeProcess(input, input.timeoutMs);
222
+ if (result.outcome.kind === "stream-error")
223
+ throw new Error("buffered process produced a stream consumer error");
224
+ return result.outcome;
225
+ }
226
+ export async function runProcessToExit(input) {
227
+ const result = await executeProcess(input, undefined);
228
+ if (result.outcome.kind === "stream-error")
229
+ throw new Error("buffered process produced a stream consumer error");
230
+ return result.outcome;
223
231
  }
224
- export function runProcessToExit(input) {
225
- return executeProcess(input, undefined);
232
+ export function consumeProcessStdout(input, consume) {
233
+ return executeProcess(input, input.timeoutMs, consume);
226
234
  }
@@ -0,0 +1,18 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export type StdioProcessExit = Readonly<{
3
+ code: number | null;
4
+ signal: NodeJS.Signals | null;
5
+ stderr: string;
6
+ }>;
7
+ export type StdioProcess = Readonly<{
8
+ input: Writable;
9
+ output: Readable;
10
+ exited: Promise<StdioProcessExit>;
11
+ endInputAndDrain(timeoutMs?: number): Promise<void>;
12
+ close(force?: boolean): Promise<void>;
13
+ }>;
14
+ export declare function spawnStdioProcess(input: Readonly<{
15
+ argv: readonly [string, ...string[]];
16
+ cwd: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ }>): StdioProcess;
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { terminateOwnedProcess } from "./run.js";
4
+ const DEFAULT_DRAIN_TIMEOUT_MS = 1_000;
5
+ export function spawnStdioProcess(input) {
6
+ const child = spawn(input.argv[0], input.argv.slice(1), {
7
+ cwd: input.cwd,
8
+ env: input.env ?? process.env,
9
+ detached: true,
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ windowsHide: true,
12
+ });
13
+ let stderr = "";
14
+ let inputEnded = false;
15
+ let forced;
16
+ child.stderr.setEncoding("utf8");
17
+ child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-4_000); });
18
+ const exited = new Promise((resolve) => {
19
+ child.once("close", (code, signal) => resolve({ code, signal, stderr: stderr.trim() }));
20
+ child.once("error", (error) => resolve({ code: null, signal: null, stderr: error.message }));
21
+ });
22
+ const endInput = () => {
23
+ if (inputEnded)
24
+ return;
25
+ inputEnded = true;
26
+ child.stdin.end();
27
+ };
28
+ const forceClose = async () => {
29
+ forced ??= (async () => {
30
+ await terminateOwnedProcess(child, true);
31
+ await exited;
32
+ })();
33
+ await forced;
34
+ };
35
+ const endInputAndDrain = async (timeoutMs = DEFAULT_DRAIN_TIMEOUT_MS) => {
36
+ endInput();
37
+ if (forced !== undefined) {
38
+ await forced;
39
+ return;
40
+ }
41
+ const timedOut = await Promise.race([
42
+ exited.then(() => false),
43
+ delay(timeoutMs, true, { ref: false }),
44
+ ]);
45
+ if (timedOut)
46
+ await forceClose();
47
+ };
48
+ const close = async (force = false) => {
49
+ endInput();
50
+ if (force) {
51
+ await forceClose();
52
+ return;
53
+ }
54
+ await terminateOwnedProcess(child, false);
55
+ await exited;
56
+ };
57
+ return { input: child.stdin, output: child.stdout, exited, endInputAndDrain, close };
58
+ }
@@ -46,6 +46,6 @@ export declare class SettingsError extends Error {
46
46
  readonly kind = "settings";
47
47
  constructor(message: string);
48
48
  }
49
- export declare function settings(input?: SettingsInput): Settings;
49
+ export declare function settings(input?: SettingsInput): Promise<Settings>;
50
50
  /** Read only the project scope owned by a materialized candidate snapshot. */
51
- export declare function projectSettings(root: string): Settings;
51
+ export declare function projectSettings(root: string): Promise<Settings>;
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  export class SettingsError extends Error {
@@ -27,10 +27,10 @@ function freezeJson(value) {
27
27
  function bounded(error) {
28
28
  return (error instanceof Error ? error.message : String(error)).slice(0, 500);
29
29
  }
30
- function readScope(path) {
30
+ async function readScope(path) {
31
31
  let bytes;
32
32
  try {
33
- bytes = readFileSync(path, "utf8");
33
+ bytes = await readFile(path, "utf8");
34
34
  }
35
35
  catch (error) {
36
36
  if (error.code === "ENOENT")
@@ -91,7 +91,7 @@ function settingsFromScopes(project, user) {
91
91
  },
92
92
  });
93
93
  }
94
- export function settings(input = {}) {
94
+ export async function settings(input = {}) {
95
95
  if (!object(input))
96
96
  throw new TypeError("settings input must be an object");
97
97
  if (input.root !== undefined && (typeof input.root !== "string" || input.root.trim().length === 0)) {
@@ -102,13 +102,13 @@ export function settings(input = {}) {
102
102
  }
103
103
  const project = input.root === undefined
104
104
  ? { state: { kind: "absent" } }
105
- : readScope(join(resolve(input.root), ".keiyaku", "settings.json"));
106
- const user = readScope(join(resolve(input.home ?? join(homedir(), ".keiyaku")), "settings.json"));
105
+ : await readScope(join(resolve(input.root), ".keiyaku", "settings.json"));
106
+ const user = await readScope(join(resolve(input.home ?? join(homedir(), ".keiyaku")), "settings.json"));
107
107
  return settingsFromScopes(project, user);
108
108
  }
109
109
  /** Read only the project scope owned by a materialized candidate snapshot. */
110
- export function projectSettings(root) {
110
+ export async function projectSettings(root) {
111
111
  if (typeof root !== "string" || root.trim().length === 0)
112
112
  throw new TypeError("settings root must be a nonblank string");
113
- return settingsFromScopes(readScope(join(resolve(root), ".keiyaku", "settings.json")), { state: { kind: "absent" } });
113
+ return settingsFromScopes(await readScope(join(resolve(root), ".keiyaku", "settings.json")), { state: { kind: "absent" } });
114
114
  }
@@ -3,7 +3,3 @@ import { type GitRepository } from "../git/repository.js";
3
3
  import type { TaskId } from "../task/identity.js";
4
4
  export declare function settlementFencePath(repository: GitRepository, taskId: TaskId): string;
5
5
  export declare function acquireTaskSettlementFence(repository: GitRepository, taskId: TaskId): Promise<HeldSqliteTransactionLock>;
6
- export declare function withTaskSettlementFence<T>(input: Readonly<{
7
- repository: GitRepository;
8
- taskId: TaskId;
9
- }>, action: () => T | Promise<T>): Promise<T>;
@@ -12,12 +12,3 @@ export function acquireTaskSettlementFence(repository, taskId) {
12
12
  mode: "immediate",
13
13
  });
14
14
  }
15
- export async function withTaskSettlementFence(input, action) {
16
- const held = await acquireTaskSettlementFence(input.repository, input.taskId);
17
- try {
18
- return await action();
19
- }
20
- finally {
21
- held.close();
22
- }
23
- }
@@ -1,7 +1,7 @@
1
1
  import type { TreeUpdate } from "../core/facts/offer.js";
2
2
  import { type ContractId } from "../core/facts/types.js";
3
3
  import type { GitDecisionObservation } from "../git/observe.js";
4
- import type { GitRepository } from "../git/repository.js";
4
+ import { type GitRepository } from "../git/repository.js";
5
5
  import { type GitDecodeChannel, type GitReadObservation, type GitTreeSelection } from "../git/read-observation.js";
6
6
  import { type TaskId } from "../task/identity.js";
7
7
  export declare function taskHolderObservationSelection(): GitTreeSelection;
@@ -28,6 +28,16 @@ export type TaskHolderAdmission<T extends AdmissionOutcome> = Readonly<{
28
28
  export declare function finishTaskHolderAdmission<T extends AdmissionOutcome>(taskId: TaskId, result: T, close: () => void): TaskHolderAdmission<T>;
29
29
  export declare function claimTaskHolderWithFence<T extends AdmissionOutcome>(repository: GitRepository, taskId: TaskId, action: () => T | Promise<T>): Promise<TaskHolderAdmission<T>>;
30
30
  export declare function releaseTaskHolder(channel: GitDecodeChannel, observation: GitDecisionObservation, owner: ContractId): Promise<TreeUpdate | null>;
31
+ export type TaskHolderReleasePublication = Readonly<{
32
+ kind: "released";
33
+ }> | Readonly<{
34
+ kind: "not-held";
35
+ }> | Readonly<{
36
+ kind: "non-published";
37
+ diagnostic: string;
38
+ }>;
39
+ /** Publish the released holder against the same frozen observation that decided the release. */
40
+ export declare function publishTaskHolderRelease(repository: GitRepository, channel: GitDecodeChannel, observation: GitDecisionObservation, owner: ContractId): Promise<TaskHolderReleasePublication>;
31
41
  export declare function readTaskHolderProjectionFromDecision(channel: GitDecodeChannel, observation: GitDecisionObservation): Promise<TaskHolderProjection>;
32
42
  export declare function observeTaskHolderProjection(repository: GitRepository, channel: GitDecodeChannel): Promise<TaskHolderProjection>;
33
43
  export declare function releaseTaskHolderWithFence<T extends AdmissionOutcome>(repository: GitRepository, channel: GitDecodeChannel, owner: ContractId, action: () => T | Promise<T>): Promise<TaskHolderAdmission<T>>;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
3
3
  import { contractId } from "../core/facts/types.js";
4
+ import { GIT_REF, updateGitTree, updateRefsAtomically, writeBlob, writeCommit, } from "../git/repository.js";
4
5
  import { withGitTargetedReadObservation, } from "../git/read-observation.js";
5
6
  import { parseTaskId } from "../task/identity.js";
6
7
  import { acquireTaskSettlementFence } from "./fence.js";
@@ -120,6 +121,30 @@ export async function releaseTaskHolder(channel, observation, owner) {
120
121
  ? null
121
122
  : update({ ...current, disposition: "released" });
122
123
  }
124
+ /** Publish the released holder against the same frozen observation that decided the release. */
125
+ export async function publishTaskHolderRelease(repository, channel, observation, owner) {
126
+ const release = await releaseTaskHolder(channel, observation, owner);
127
+ if (release === null)
128
+ return { kind: "not-held" };
129
+ const tree = await updateGitTree(repository, observation.admission.snapshot.tree, new Map([[release.path, { oid: await writeBlob(repository, release.bytes) }]]));
130
+ const commit = await writeCommit({
131
+ repository,
132
+ tree,
133
+ parent: observation.admission.snapshot.commit,
134
+ message: `release held Task completion: ${owner}`,
135
+ });
136
+ const publication = await updateRefsAtomically(repository, [{
137
+ ref: GIT_REF,
138
+ newOid: commit,
139
+ expectedOid: observation.admission.snapshot.commit,
140
+ }]);
141
+ if (publication.kind === "published")
142
+ return { kind: "released" };
143
+ return {
144
+ kind: "non-published",
145
+ diagnostic: publication.kind === "non-published" ? detail(publication.error) : "Task holder release publication state is unknown",
146
+ };
147
+ }
123
148
  export async function readTaskHolderProjectionFromDecision(channel, observation) {
124
149
  const read = {
125
150
  snapshot: observation.admission.snapshot,
@@ -6,7 +6,7 @@ import type { TaskId } from "../task/identity.js";
6
6
  export type SettlementAction = Readonly<{
7
7
  kind: "task";
8
8
  taskId: TaskId;
9
- action: "done" | "reopened";
9
+ action: "done";
10
10
  }> | Readonly<{
11
11
  kind: "namespace-context";
12
12
  path: string;