@astrosheep/keiyaku 4.0.0 → 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 (308) hide show
  1. package/README.md +133 -8
  2. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +2 -2
  3. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +4 -4
  4. package/build/integrations/marketplace/plugins/keiyaku/opencode.js +2 -2
  5. package/build/integrations/marketplace/plugins/keiyaku/package.json +2 -2
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +13 -5
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -32
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +134 -0
  9. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/agents/openai.yaml +4 -0
  10. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +41 -6
  11. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +262 -46
  12. package/build/src/akuma/abort.d.ts +2 -0
  13. package/build/src/akuma/abort.js +50 -0
  14. package/build/src/akuma/akuma.d.ts +50 -33
  15. package/build/src/akuma/akuma.js +175 -127
  16. package/build/src/akuma/archetype.d.ts +14 -5
  17. package/build/src/akuma/archetype.js +85 -39
  18. package/build/src/akuma/body.d.ts +3 -5
  19. package/build/src/akuma/body.js +490 -183
  20. package/build/src/akuma/coordinate.d.ts +9 -0
  21. package/build/src/akuma/coordinate.js +22 -0
  22. package/build/src/akuma/heart/facts.d.ts +88 -45
  23. package/build/src/akuma/heart/facts.js +19 -8
  24. package/build/src/akuma/heart/index.d.ts +64 -55
  25. package/build/src/akuma/heart/index.js +172 -252
  26. package/build/src/akuma/heart/rows.d.ts +38 -83
  27. package/build/src/akuma/heart/rows.js +117 -155
  28. package/build/src/akuma/heart/schema.d.ts +1 -1
  29. package/build/src/akuma/heart/schema.js +61 -22
  30. package/build/src/akuma/heart/soul.d.ts +8 -0
  31. package/build/src/akuma/heart/soul.js +136 -0
  32. package/build/src/akuma/heart/storage.d.ts +47 -0
  33. package/build/src/akuma/heart/storage.js +224 -0
  34. package/build/src/akuma/heart/tells.d.ts +12 -0
  35. package/build/src/akuma/heart/tells.js +79 -0
  36. package/build/src/akuma/heart/timeline.d.ts +15 -0
  37. package/build/src/akuma/heart/timeline.js +86 -0
  38. package/build/src/akuma/identity.d.ts +2 -2
  39. package/build/src/akuma/identity.js +9 -9
  40. package/build/src/akuma/index.d.ts +10 -3
  41. package/build/src/akuma/index.js +7 -1
  42. package/build/src/akuma/projection.d.ts +176 -0
  43. package/build/src/akuma/projection.js +256 -0
  44. package/build/src/akuma/provider-recipe.d.ts +24 -0
  45. package/build/src/akuma/provider-recipe.js +100 -0
  46. package/build/src/akuma/provider.d.ts +67 -13
  47. package/build/src/akuma/provider.js +172 -105
  48. package/build/src/akuma/providers/acp/core.d.ts +19 -0
  49. package/build/src/akuma/providers/acp/core.js +159 -0
  50. package/build/src/akuma/providers/acp/events.d.ts +20 -0
  51. package/build/src/akuma/providers/acp/events.js +73 -0
  52. package/build/src/akuma/providers/acp/index.d.ts +12 -0
  53. package/build/src/akuma/providers/acp/index.js +94 -0
  54. package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
  55. package/build/src/akuma/providers/claude/events.js +238 -0
  56. package/build/src/akuma/providers/claude/index.d.ts +47 -0
  57. package/build/src/akuma/providers/claude/index.js +279 -0
  58. package/build/src/akuma/providers/claude/input.d.ts +13 -0
  59. package/build/src/akuma/providers/claude/input.js +109 -0
  60. package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
  61. package/build/src/akuma/providers/codex-app-server/events.js +15 -12
  62. package/build/src/akuma/providers/codex-app-server/index.d.ts +15 -13
  63. package/build/src/akuma/providers/codex-app-server/index.js +77 -33
  64. package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
  65. package/build/src/akuma/providers/grok-build/index.js +70 -0
  66. package/build/src/akuma/providers/index.d.ts +5 -3
  67. package/build/src/akuma/providers/index.js +35 -56
  68. package/build/src/akuma/providers/opencode-sdk/events.d.ts +54 -0
  69. package/build/src/akuma/providers/opencode-sdk/events.js +235 -0
  70. package/build/src/akuma/providers/opencode-sdk/index.d.ts +33 -0
  71. package/build/src/akuma/providers/opencode-sdk/index.js +308 -0
  72. package/build/src/akuma/providers/opencode-sdk/session.d.ts +26 -0
  73. package/build/src/akuma/providers/opencode-sdk/session.js +63 -0
  74. package/build/src/akuma/providers/pi/events.d.ts +33 -0
  75. package/build/src/akuma/providers/pi/events.js +166 -0
  76. package/build/src/akuma/providers/pi/index.d.ts +11 -0
  77. package/build/src/akuma/providers/pi/index.js +191 -0
  78. package/build/src/akuma/publication.d.ts +3 -3
  79. package/build/src/akuma/publication.js +28 -40
  80. package/build/src/akuma/requests.d.ts +9 -6
  81. package/build/src/akuma/requests.js +95 -65
  82. package/build/src/alias/index.d.ts +4 -3
  83. package/build/src/alias/index.js +12 -13
  84. package/build/src/body/decode.js +7 -1
  85. package/build/src/body/region.d.ts +2 -0
  86. package/build/src/body/region.js +77 -7
  87. package/build/src/body/render.js +2 -1
  88. package/build/src/body/shape.js +7 -1
  89. package/build/src/body/verification.js +19 -4
  90. package/build/src/cli/accepted.d.ts +18 -12
  91. package/build/src/cli/accepted.js +73 -34
  92. package/build/src/cli/actor.js +3 -3
  93. package/build/src/cli/commands/akuma-invoke.d.ts +44 -19
  94. package/build/src/cli/commands/akuma-invoke.js +101 -52
  95. package/build/src/cli/commands/akuma.d.ts +27 -9
  96. package/build/src/cli/commands/akuma.js +151 -80
  97. package/build/src/cli/commands/contract.d.ts +155 -0
  98. package/build/src/cli/commands/contract.js +20 -0
  99. package/build/src/cli/commands/install.js +2 -0
  100. package/build/src/cli/commands/task-invoke.d.ts +19 -4
  101. package/build/src/cli/commands/task-invoke.js +65 -17
  102. package/build/src/cli/commands/task-query.d.ts +5 -0
  103. package/build/src/cli/commands/task-query.js +247 -0
  104. package/build/src/cli/commands/task.d.ts +3 -1
  105. package/build/src/cli/commands/task.js +52 -14
  106. package/build/src/cli/coordinates.d.ts +18 -0
  107. package/build/src/cli/coordinates.js +120 -0
  108. package/build/src/cli/draft.d.ts +8 -0
  109. package/build/src/cli/draft.js +96 -0
  110. package/build/src/cli/invoke.d.ts +9 -3
  111. package/build/src/cli/invoke.js +287 -108
  112. package/build/src/cli/main.js +54 -7
  113. package/build/src/cli/parse.d.ts +21 -117
  114. package/build/src/cli/parse.js +79 -84
  115. package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
  116. package/build/src/cli/render/akuma-tool-command.js +144 -0
  117. package/build/src/cli/render/akuma-tool.d.ts +4 -2
  118. package/build/src/cli/render/akuma-tool.js +35 -5
  119. package/build/src/cli/render/akuma.d.ts +3 -1
  120. package/build/src/cli/render/akuma.js +247 -212
  121. package/build/src/cli/render/audit.d.ts +3 -0
  122. package/build/src/cli/render/audit.js +104 -0
  123. package/build/src/cli/render/catalog.d.ts +2 -0
  124. package/build/src/cli/render/catalog.js +16 -0
  125. package/build/src/cli/render/contract.d.ts +3 -3
  126. package/build/src/cli/render/contract.js +200 -54
  127. package/build/src/cli/render/kanshi.js +272 -129
  128. package/build/src/cli/render/receipt.d.ts +19 -0
  129. package/build/src/cli/render/receipt.js +106 -0
  130. package/build/src/cli/render/refusal.d.ts +16 -2
  131. package/build/src/cli/render/refusal.js +88 -3
  132. package/build/src/cli/render/region.d.ts +2 -0
  133. package/build/src/cli/render/region.js +17 -0
  134. package/build/src/cli/render/task.d.ts +2 -1
  135. package/build/src/cli/render/task.js +223 -60
  136. package/build/src/cli/render/terminal.d.ts +20 -0
  137. package/build/src/cli/render/terminal.js +121 -0
  138. package/build/src/cli/render/text.js +11 -3
  139. package/build/src/cli/result.d.ts +126 -19
  140. package/build/src/cli/usage.d.ts +1 -0
  141. package/build/src/cli/usage.js +3 -0
  142. package/build/src/contract-worktree.d.ts +44 -0
  143. package/build/src/contract-worktree.js +251 -0
  144. package/build/src/coordination/durable-file.d.ts +5 -1
  145. package/build/src/coordination/durable-file.js +57 -10
  146. package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
  147. package/build/src/coordination/sqlite-transaction-lock.js +22 -5
  148. package/build/src/core/facts/codec.js +23 -4
  149. package/build/src/core/facts/eligibility.d.ts +1 -10
  150. package/build/src/core/facts/eligibility.js +0 -49
  151. package/build/src/core/facts/fold.js +1 -6
  152. package/build/src/core/facts/gate.d.ts +1 -0
  153. package/build/src/core/facts/gate.js +1 -0
  154. package/build/src/core/facts/observation.d.ts +3 -0
  155. package/build/src/core/facts/observation.js +12 -0
  156. package/build/src/core/facts/types.d.ts +10 -3
  157. package/build/src/core/subject.js +2 -2
  158. package/build/src/core/verbs/amend.d.ts +1 -1
  159. package/build/src/core/verbs/amend.js +3 -18
  160. package/build/src/core/verbs/bind.js +2 -16
  161. package/build/src/core/verbs/deliver.d.ts +1 -1
  162. package/build/src/core/verbs/deliver.js +14 -4
  163. package/build/src/core/verbs/placement.d.ts +1 -1
  164. package/build/src/core/verbs/placement.js +7 -9
  165. package/build/src/dispatch/index.d.ts +5 -3
  166. package/build/src/dispatch/index.js +30 -21
  167. package/build/src/duration.d.ts +10 -0
  168. package/build/src/duration.js +22 -0
  169. package/build/src/git/admission.d.ts +2 -2
  170. package/build/src/git/admission.js +33 -23
  171. package/build/src/git/hooks.d.ts +15 -0
  172. package/build/src/git/hooks.js +70 -18
  173. package/build/src/git/identity.d.ts +5 -1
  174. package/build/src/git/identity.js +23 -4
  175. package/build/src/git/integration.d.ts +48 -0
  176. package/build/src/git/integration.js +250 -0
  177. package/build/src/git/observe.d.ts +29 -15
  178. package/build/src/git/observe.js +185 -76
  179. package/build/src/git/read-observation.d.ts +41 -0
  180. package/build/src/git/read-observation.js +361 -0
  181. package/build/src/git/reconcile.d.ts +17 -5
  182. package/build/src/git/reconcile.js +305 -109
  183. package/build/src/git/repository.d.ts +41 -18
  184. package/build/src/git/repository.js +220 -114
  185. package/build/src/git/scratch.d.ts +17 -0
  186. package/build/src/git/scratch.js +73 -0
  187. package/build/src/git/target-placement.d.ts +92 -0
  188. package/build/src/git/target-placement.js +375 -0
  189. package/build/src/git/tender.d.ts +49 -0
  190. package/build/src/git/tender.js +63 -0
  191. package/build/src/git/terminal-seal.d.ts +24 -0
  192. package/build/src/git/terminal-seal.js +89 -0
  193. package/build/src/git/tree.d.ts +2 -2
  194. package/build/src/git/tree.js +4 -0
  195. package/build/src/git/workspace.d.ts +53 -0
  196. package/build/src/git/workspace.js +139 -0
  197. package/build/src/identity/selector.d.ts +6 -0
  198. package/build/src/identity/selector.js +19 -0
  199. package/build/src/index.d.ts +4 -2
  200. package/build/src/index.js +2 -1
  201. package/build/src/kanshi/index.d.ts +2 -2
  202. package/build/src/kanshi/index.js +1 -1
  203. package/build/src/kanshi/read.d.ts +7 -3
  204. package/build/src/kanshi/read.js +245 -45
  205. package/build/src/kanshi/report.d.ts +81 -6
  206. package/build/src/kanshi/select.d.ts +5 -0
  207. package/build/src/kanshi/select.js +33 -1
  208. package/build/src/library/address.d.ts +57 -0
  209. package/build/src/library/address.js +165 -0
  210. package/build/src/library/akuma-creation.d.ts +16 -7
  211. package/build/src/library/akuma-creation.js +96 -22
  212. package/build/src/library/audit.d.ts +18 -0
  213. package/build/src/library/audit.js +44 -0
  214. package/build/src/library/bind.d.ts +21 -0
  215. package/build/src/library/bind.js +56 -0
  216. package/build/src/library/catalog.d.ts +58 -0
  217. package/build/src/library/catalog.js +69 -0
  218. package/build/src/library/configuration.d.ts +6 -15
  219. package/build/src/library/configuration.js +14 -17
  220. package/build/src/library/contract.d.ts +23 -49
  221. package/build/src/library/contract.js +171 -185
  222. package/build/src/library/delivery.d.ts +24 -0
  223. package/build/src/library/delivery.js +21 -0
  224. package/build/src/library/fleet.d.ts +65 -0
  225. package/build/src/library/fleet.js +153 -0
  226. package/build/src/library/input.d.ts +3 -0
  227. package/build/src/library/input.js +19 -0
  228. package/build/src/library/keiyaku.d.ts +18 -3
  229. package/build/src/library/keiyaku.js +12 -1
  230. package/build/src/library/mutation.d.ts +39 -0
  231. package/build/src/library/mutation.js +37 -0
  232. package/build/src/library/reconcile.d.ts +29 -0
  233. package/build/src/library/reconcile.js +179 -0
  234. package/build/src/library/refusal.d.ts +21 -0
  235. package/build/src/library/refusal.js +29 -0
  236. package/build/src/library/region.d.ts +14 -1
  237. package/build/src/library/region.js +18 -4
  238. package/build/src/library/repo.d.ts +4 -13
  239. package/build/src/library/repo.js +14 -24
  240. package/build/src/protocol/attempt.d.ts +16 -3
  241. package/build/src/protocol/attempt.js +41 -10
  242. package/build/src/protocol/bind.d.ts +9 -2
  243. package/build/src/protocol/bind.js +48 -40
  244. package/build/src/protocol/intent.d.ts +57 -20
  245. package/build/src/protocol/intent.js +55 -79
  246. package/build/src/protocol/operations.d.ts +152 -43
  247. package/build/src/protocol/operations.js +482 -166
  248. package/build/src/protocol/outcome.d.ts +14 -3
  249. package/build/src/protocol/outcome.js +11 -3
  250. package/build/src/protocol/placement.d.ts +41 -0
  251. package/build/src/protocol/placement.js +147 -0
  252. package/build/src/protocol/read/documents.d.ts +2 -2
  253. package/build/src/protocol/read/documents.js +4 -4
  254. package/build/src/protocol/read/status.d.ts +18 -5
  255. package/build/src/protocol/read/status.js +73 -13
  256. package/build/src/protocol/run.d.ts +49 -7
  257. package/build/src/protocol/run.js +53 -25
  258. package/build/src/runtime/proc/line-rpc.d.ts +4 -8
  259. package/build/src/runtime/proc/line-rpc.js +22 -28
  260. package/build/src/runtime/proc/run.d.ts +23 -21
  261. package/build/src/runtime/proc/run.js +123 -89
  262. package/build/src/runtime/proc/stdio.d.ts +18 -0
  263. package/build/src/runtime/proc/stdio.js +58 -0
  264. package/build/src/settings.d.ts +7 -1
  265. package/build/src/settings.js +32 -17
  266. package/build/src/settlement/fence.d.ts +5 -0
  267. package/build/src/settlement/fence.js +14 -0
  268. package/build/src/settlement/holder.d.ts +35 -4
  269. package/build/src/settlement/holder.js +127 -28
  270. package/build/src/settlement/settle.d.ts +9 -1
  271. package/build/src/settlement/settle.js +74 -37
  272. package/build/src/task/board.d.ts +23 -5
  273. package/build/src/task/board.js +17 -10
  274. package/build/src/task/compose.d.ts +2 -2
  275. package/build/src/task/compose.js +18 -10
  276. package/build/src/task/context.d.ts +3 -3
  277. package/build/src/task/context.js +15 -20
  278. package/build/src/task/document.d.ts +2 -1
  279. package/build/src/task/document.js +15 -5
  280. package/build/src/task/index.d.ts +27 -15
  281. package/build/src/task/index.js +57 -35
  282. package/build/src/task/operations.d.ts +38 -13
  283. package/build/src/task/operations.js +80 -46
  284. package/build/src/task/query.d.ts +67 -0
  285. package/build/src/task/query.js +279 -0
  286. package/build/src/task/store.d.ts +5 -7
  287. package/build/src/task/store.js +43 -28
  288. package/build/src/verification/declaration.d.ts +1 -0
  289. package/build/src/verification/execution.d.ts +51 -0
  290. package/build/src/verification/execution.js +108 -0
  291. package/build/src/workspace-place.d.ts +41 -0
  292. package/build/src/workspace-place.js +386 -0
  293. package/build/src/world.d.ts +24 -0
  294. package/build/src/world.js +136 -0
  295. package/package.json +8 -4
  296. package/build/src/akuma/activity.d.ts +0 -66
  297. package/build/src/akuma/activity.js +0 -139
  298. package/build/src/akuma/providers/claude.js +0 -305
  299. package/build/src/context-root.d.ts +0 -4
  300. package/build/src/context-root.js +0 -15
  301. package/build/src/git/delivery.d.ts +0 -20
  302. package/build/src/git/delivery.js +0 -109
  303. package/build/src/git/verification.d.ts +0 -14
  304. package/build/src/git/verification.js +0 -56
  305. package/build/src/protocol/read/audit.d.ts +0 -25
  306. package/build/src/protocol/read/audit.js +0 -39
  307. package/build/src/verification/producer.d.ts +0 -23
  308. package/build/src/verification/producer.js +0 -54
@@ -1,5 +1,4 @@
1
- import { spawn } from "node:child_process";
2
- import { terminateProcessTree } from "./run.js";
1
+ import { spawnStdioProcess } from "./stdio.js";
3
2
  function object(value) {
4
3
  return value !== null && typeof value === "object" && !Array.isArray(value)
5
4
  ? value
@@ -9,37 +8,25 @@ function text(value) {
9
8
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
10
9
  }
11
10
  export class LineRpcProcess {
12
- child;
11
+ process;
13
12
  pending = new Map();
14
13
  notifications = new Set();
15
14
  serverRequests = new Set();
16
- exited;
17
15
  nextId = 1;
18
16
  stdout = "";
19
- stderr = "";
20
17
  closed = false;
21
18
  constructor(input) {
22
- this.child = spawn(input.argv[0], input.argv.slice(1), {
23
- cwd: input.cwd,
24
- env: input.env ?? process.env,
25
- detached: true,
26
- stdio: ["pipe", "pipe", "pipe"],
27
- windowsHide: true,
28
- });
29
- this.child.stdout.setEncoding("utf8");
30
- this.child.stderr.setEncoding("utf8");
31
- this.child.stdout.on("data", (chunk) => this.consume(chunk));
32
- this.child.stderr.on("data", (chunk) => { this.stderr = `${this.stderr}${chunk}`.slice(-4_000); });
33
- this.child.on("error", (error) => this.fail(error));
34
- this.exited = new Promise((resolve) => this.child.once("close", (code, signal) => {
19
+ this.process = spawnStdioProcess(input);
20
+ this.process.output.setEncoding("utf8");
21
+ this.process.output.on("data", (chunk) => this.consume(chunk));
22
+ void this.process.exited.then((exit) => {
35
23
  this.closed = true;
36
- this.fail(new Error(`line RPC process exited${code === null ? "" : ` with code ${code}`}`));
37
- resolve({ code, signal, stderr: this.stderr.trim() });
38
- }));
24
+ this.fail(new Error(`line RPC process exited${exit.code === null ? "" : ` with code ${exit.code}`}`));
25
+ });
39
26
  }
40
27
  onNotification(listener) { this.notifications.add(listener); }
41
28
  onServerRequest(listener) { this.serverRequests.add(listener); }
42
- onExit(listener) { void this.exited.then(listener); }
29
+ onExit(listener) { void this.process.exited.then(listener); }
43
30
  consume(chunk) {
44
31
  this.stdout += chunk;
45
32
  for (;;) {
@@ -98,7 +85,7 @@ export class LineRpcProcess {
98
85
  const id = this.nextId++;
99
86
  return new Promise((resolve, reject) => {
100
87
  this.pending.set(id, { resolve, reject });
101
- this.child.stdin.write(`${JSON.stringify({ id, method, ...(params === undefined ? {} : { params }) })}\n`, (error) => {
88
+ this.process.input.write(`${JSON.stringify({ id, method, ...(params === undefined ? {} : { params }) })}\n`, (error) => {
102
89
  if (error === null || error === undefined)
103
90
  return;
104
91
  this.pending.delete(id);
@@ -108,17 +95,24 @@ export class LineRpcProcess {
108
95
  }
109
96
  notify(method) {
110
97
  if (!this.closed)
111
- this.child.stdin.write(`${JSON.stringify({ method })}\n`);
98
+ this.process.input.write(`${JSON.stringify({ method })}\n`);
99
+ }
100
+ async endInputAndDrain(timeoutMs) {
101
+ if (this.closed) {
102
+ await this.process.exited;
103
+ return;
104
+ }
105
+ this.closed = true;
106
+ this.fail(new Error("line RPC process is closed"));
107
+ await this.process.endInputAndDrain(timeoutMs);
112
108
  }
113
109
  async close(force = false) {
114
110
  if (this.closed) {
115
- await this.exited;
111
+ await this.process.exited;
116
112
  return;
117
113
  }
118
114
  this.closed = true;
119
115
  this.fail(new Error("line RPC process is closed"));
120
- this.child.stdin.end();
121
- await terminateProcessTree(this.child.pid, force);
122
- await this.exited;
116
+ await this.process.close(force);
123
117
  }
124
118
  }
@@ -1,10 +1,12 @@
1
+ import { type ChildProcess } from "node:child_process";
1
2
  type ProcessLaunch = Readonly<{
2
3
  readonly argv: readonly string[];
3
4
  readonly cwd?: string;
4
5
  readonly env?: NodeJS.ProcessEnv;
6
+ readonly signal?: AbortSignal;
5
7
  }>;
6
8
  export type ProcessInput = ProcessLaunch & Readonly<{
7
- readonly timeoutMs: number;
9
+ readonly timeoutMs?: number;
8
10
  }>;
9
11
  type ProcessTerminal = Readonly<{
10
12
  readonly kind: "terminal";
@@ -23,32 +25,32 @@ type ProcessSpawnError = Readonly<{
23
25
  type ProcessUnknownExit = Readonly<{
24
26
  readonly kind: "unknown-exit";
25
27
  }>;
26
- export type ProcessOutcome = ProcessTerminal | ProcessTimeout | ProcessSpawnError | ProcessUnknownExit;
27
- export type ProcessCollar = Readonly<{
28
- pid: number;
29
- processGroup: number;
30
- spawnedAt: string;
31
- }>;
32
- export type ProcessTreeProbe = Readonly<{
33
- kind: "gone";
34
- }> | Readonly<{
35
- kind: "alive";
36
- }> | Readonly<{
37
- kind: "unverifiable";
38
- diagnostic: string;
39
- }>;
40
- export type PutDownEvidence = "killed" | "already-dead" | "alive-after-sigkill" | "unavailable";
28
+ type ProcessCancelled = Readonly<{
29
+ readonly kind: "cancelled";
30
+ }>;
31
+ export type ProcessOutcome = ProcessTerminal | ProcessTimeout | ProcessSpawnError | ProcessUnknownExit | ProcessCancelled;
32
+ type ProcessStreamError = Readonly<{
33
+ readonly kind: "stream-error";
34
+ readonly diagnostic: string;
35
+ }>;
36
+ export type ProcessConsumption = Readonly<{
37
+ readonly outcome: ProcessOutcome | ProcessStreamError;
38
+ readonly pid: number | null;
39
+ }>;
41
40
  export type DetachedProcessInput = Readonly<{
42
41
  argv: readonly string[];
43
42
  cwd: string;
44
43
  env?: NodeJS.ProcessEnv;
45
44
  log: string;
46
45
  }>;
47
- export declare function terminateProcessTree(pid: number | undefined, force?: boolean): Promise<void>;
48
- export declare function currentProcessCollar(): ProcessCollar;
49
- export declare function spawnDetachedProcess(input: DetachedProcessInput): Promise<ProcessCollar>;
50
- export declare function probeProcessTree(collar: ProcessCollar): ProcessTreeProbe;
51
- export declare function putDownProcessTree(collar: ProcessCollar): Promise<PutDownEvidence>;
46
+ export type OwnedProcess = Readonly<{
47
+ pid: number;
48
+ terminate(force?: boolean): Promise<void>;
49
+ release(): void;
50
+ }>;
51
+ export declare function terminateOwnedProcess(child: ChildProcess, force?: boolean): Promise<void>;
52
+ export declare function spawnDetachedProcess(input: DetachedProcessInput): Promise<OwnedProcess>;
52
53
  export declare function runProcess(input: ProcessInput): Promise<ProcessOutcome>;
53
54
  export declare function runProcessToExit(input: ProcessLaunch): Promise<ProcessOutcome>;
55
+ export declare function consumeProcessStdout(input: ProcessInput, consume: (chunk: Buffer) => void): Promise<ProcessConsumption>;
54
56
  export {};
@@ -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,28 +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 spawnedAt = processStartToken(process.pid);
92
- if (spawnedAt === null)
93
- throw new Error(`cannot read process start identity for ${process.pid}`);
94
- return { pid: process.pid, processGroup: process.pid, spawnedAt };
95
- }
96
87
  export async function spawnDetachedProcess(input) {
97
- const log = openSync(input.log, "a");
88
+ const log = await open(input.log, "a");
98
89
  try {
99
90
  const child = spawn(input.argv[0], input.argv.slice(1), {
100
91
  cwd: input.cwd,
101
92
  env: input.env,
102
93
  detached: true,
103
- stdio: ["ignore", log, log],
94
+ stdio: ["ignore", log.fd, log.fd],
104
95
  windowsHide: true,
105
96
  });
106
97
  await new Promise((resolve, reject) => {
@@ -109,46 +100,47 @@ export async function spawnDetachedProcess(input) {
109
100
  });
110
101
  if (child.pid === undefined)
111
102
  throw new Error("detached process spawned without a pid");
112
- const spawnedAt = processStartToken(child.pid);
113
- if (spawnedAt === null)
114
- throw new Error(`cannot read process start identity for ${child.pid}`);
115
- child.unref();
116
- 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
+ };
117
127
  }
118
128
  finally {
119
- closeSync(log);
129
+ await log.close();
120
130
  }
121
131
  }
122
- export function probeProcessTree(collar) {
123
- try {
124
- process.kill(process.platform === "win32" ? collar.pid : -collar.processGroup, 0);
125
- }
126
- catch (error) {
127
- const code = error.code;
128
- if (code === "ESRCH")
129
- return { kind: "gone" };
130
- return { kind: "unverifiable", diagnostic: error instanceof Error ? error.message : String(error) };
131
- }
132
- const current = processStartToken(collar.pid);
133
- if (current === null && process.platform !== "win32")
134
- return { kind: "alive" };
135
- if (current === collar.spawnedAt)
136
- return { kind: "alive" };
137
- return { kind: "unverifiable", diagnostic: `process ${collar.pid} no longer matches its recorded start identity` };
138
- }
139
- export async function putDownProcessTree(collar) {
140
- const before = probeProcessTree(collar);
141
- if (before.kind === "gone")
142
- return "already-dead";
143
- if (before.kind === "unverifiable")
144
- return "unavailable";
145
- await terminateProcessTree(collar.pid);
146
- const after = probeProcessTree(collar);
147
- if (after.kind === "gone")
148
- return "killed";
149
- 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
+ };
150
140
  }
151
- async function executeProcess(input, timeoutMs) {
141
+ async function executeProcess(input, timeoutMs, consumeStdout) {
142
+ if (input.signal?.aborted === true)
143
+ return { outcome: { kind: "cancelled" }, pid: null };
152
144
  const child = spawn(input.argv[0], input.argv.slice(1), {
153
145
  cwd: input.cwd,
154
146
  env: input.env,
@@ -158,43 +150,85 @@ async function executeProcess(input, timeoutMs) {
158
150
  });
159
151
  const stdout = tailCapture(STREAM_TAIL_BYTES);
160
152
  const stderr = tailCapture(STREAM_TAIL_BYTES);
161
- child.stdout.on("data", (chunk) => stdout.append(chunk));
162
- child.stderr.on("data", (chunk) => stderr.append(chunk));
163
- let timedOut = false;
153
+ let stop;
154
+ let streamError;
164
155
  let termination;
165
- const requestStop = () => {
166
- if (timedOut)
156
+ const requestStop = (reason) => {
157
+ if (stop !== undefined)
158
+ return;
159
+ stop = reason;
160
+ termination = terminateOwnedProcess(child);
161
+ };
162
+ const failStream = (error) => {
163
+ if (streamError !== undefined)
167
164
  return;
168
- timedOut = true;
169
- termination = terminateProcessTree(child.pid);
165
+ streamError = error;
166
+ requestStop("cancelled");
170
167
  };
171
- const timeout = timeoutMs === undefined ? undefined : setTimeout(requestStop, timeoutMs);
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
+ }
185
+ const cancel = () => requestStop("cancelled");
186
+ input.signal?.addEventListener("abort", cancel, { once: true });
187
+ const timeout = timeoutMs === undefined ? undefined : setTimeout(() => requestStop("timeout"), timeoutMs);
172
188
  const terminal = await new Promise((resolve) => {
173
189
  child.once("close", (code) => resolve({ kind: "closed", code }));
174
190
  child.once("error", (error) => resolve({ kind: "spawn-error", error }));
175
191
  });
176
192
  if (timeout !== undefined)
177
193
  clearTimeout(timeout);
194
+ input.signal?.removeEventListener("abort", cancel);
178
195
  await termination;
179
- if (terminal.kind === "spawn-error")
180
- return { kind: "spawn-error", diagnostic: terminal.error.message };
181
- if (timedOut)
182
- return { kind: "timeout" };
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
+ }
209
+ if (stop !== undefined)
210
+ return { outcome: { kind: stop }, pid };
183
211
  if (terminal.code === null)
184
- return { kind: "unknown-exit" };
212
+ return { outcome: { kind: "unknown-exit" }, pid };
185
213
  const capturedStdout = stdout.result();
186
214
  const capturedStderr = stderr.result();
187
215
  return {
188
- kind: "terminal",
189
- code: terminal.code,
190
- stdout: capturedStdout.text,
191
- stderr: capturedStderr.text,
192
- truncated: capturedStdout.truncated || capturedStderr.truncated,
216
+ outcome: terminalOutcome(terminal.code, capturedStdout, capturedStderr, consumeStdout !== undefined),
217
+ pid,
193
218
  };
194
219
  }
195
- export function runProcess(input) {
196
- 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;
197
231
  }
198
- export function runProcessToExit(input) {
199
- return executeProcess(input, undefined);
232
+ export function consumeProcessStdout(input, consume) {
233
+ return executeProcess(input, input.timeoutMs, consume);
200
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
+ }
@@ -42,4 +42,10 @@ export type SettingsInput = Readonly<{
42
42
  root?: string;
43
43
  home?: string;
44
44
  }>;
45
- export declare function settings(input?: SettingsInput): Settings;
45
+ export declare class SettingsError extends Error {
46
+ readonly kind = "settings";
47
+ constructor(message: string);
48
+ }
49
+ export declare function settings(input?: SettingsInput): Promise<Settings>;
50
+ /** Read only the project scope owned by a materialized candidate snapshot. */
51
+ export declare function projectSettings(root: string): Promise<Settings>;
@@ -1,6 +1,13 @@
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
+ export class SettingsError extends Error {
5
+ kind = "settings";
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "SettingsError";
9
+ }
10
+ }
4
11
  function object(value) {
5
12
  return value !== null && typeof value === "object" && !Array.isArray(value);
6
13
  }
@@ -20,10 +27,10 @@ function freezeJson(value) {
20
27
  function bounded(error) {
21
28
  return (error instanceof Error ? error.message : String(error)).slice(0, 500);
22
29
  }
23
- function readScope(path) {
30
+ async function readScope(path) {
24
31
  let bytes;
25
32
  try {
26
- bytes = readFileSync(path, "utf8");
33
+ bytes = await readFile(path, "utf8");
27
34
  }
28
35
  catch (error) {
29
36
  if (error.code === "ENOENT")
@@ -60,20 +67,7 @@ function namespaceEntries(name, scope, loaded) {
60
67
  })),
61
68
  };
62
69
  }
63
- export function settings(input = {}) {
64
- if (!object(input))
65
- throw new TypeError("settings input must be an object");
66
- if (input.root !== undefined && (typeof input.root !== "string" || input.root.trim().length === 0)) {
67
- throw new TypeError("settings root must be a nonblank string");
68
- }
69
- if (input.home !== undefined && (typeof input.home !== "string" || input.home.trim().length === 0)) {
70
- throw new TypeError("settings home must be a nonblank string");
71
- }
72
- const home = resolve(input.home ?? join(homedir(), ".keiyaku"));
73
- const user = readScope(join(home, "settings.json"));
74
- const project = input.root === undefined
75
- ? { state: { kind: "absent" } }
76
- : readScope(join(resolve(input.root), ".keiyaku", "settings.json"));
70
+ function settingsFromScopes(project, user) {
77
71
  return Object.freeze({
78
72
  scopes: Object.freeze({ project: project.state, user: user.state }),
79
73
  namespace(name) {
@@ -97,3 +91,24 @@ export function settings(input = {}) {
97
91
  },
98
92
  });
99
93
  }
94
+ export async function settings(input = {}) {
95
+ if (!object(input))
96
+ throw new TypeError("settings input must be an object");
97
+ if (input.root !== undefined && (typeof input.root !== "string" || input.root.trim().length === 0)) {
98
+ throw new TypeError("settings root must be a nonblank string");
99
+ }
100
+ if (input.home !== undefined && (typeof input.home !== "string" || input.home.trim().length === 0)) {
101
+ throw new TypeError("settings home must be a nonblank string");
102
+ }
103
+ const project = input.root === undefined
104
+ ? { state: { kind: "absent" } }
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
+ return settingsFromScopes(project, user);
108
+ }
109
+ /** Read only the project scope owned by a materialized candidate snapshot. */
110
+ export async function projectSettings(root) {
111
+ if (typeof root !== "string" || root.trim().length === 0)
112
+ throw new TypeError("settings root must be a nonblank string");
113
+ return settingsFromScopes(await readScope(join(resolve(root), ".keiyaku", "settings.json")), { state: { kind: "absent" } });
114
+ }
@@ -0,0 +1,5 @@
1
+ import { type HeldSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
2
+ import { type GitRepository } from "../git/repository.js";
3
+ import type { TaskId } from "../task/identity.js";
4
+ export declare function settlementFencePath(repository: GitRepository, taskId: TaskId): string;
5
+ export declare function acquireTaskSettlementFence(repository: GitRepository, taskId: TaskId): Promise<HeldSqliteTransactionLock>;
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { acquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
4
+ import { commonGitDirectory } from "../git/repository.js";
5
+ export function settlementFencePath(repository, taskId) {
6
+ const locator = createHash("sha256").update(taskId).digest("hex");
7
+ return resolve(commonGitDirectory(repository), "keiyaku", "locks", "settlement", `${locator}.sqlite`);
8
+ }
9
+ export function acquireTaskSettlementFence(repository, taskId) {
10
+ return acquireSqliteTransactionLock({
11
+ path: settlementFencePath(repository, taskId),
12
+ mode: "immediate",
13
+ });
14
+ }