@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,6 +1,7 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { isAbsolute, resolve } from "node:path";
3
3
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
4
+ import { consumeProcessStdout } from "../runtime/proc/run.js";
4
5
  import { gitObjectId } from "./identity.js";
5
6
  import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
6
7
  export const GIT_REF = "refs/heads/keiyaku-state";
@@ -38,6 +39,7 @@ export class GitPlumbingError extends Error {
38
39
  this.pid = input.pid ?? null;
39
40
  }
40
41
  }
42
+ const GIT_STDERR_BYTES = 16 * 1024;
41
43
  function worktreesFromPorcelain(output) {
42
44
  const fields = output.toString("utf8").split("\0");
43
45
  const worktrees = [];
@@ -71,11 +73,13 @@ function worktreesFromPorcelain(output) {
71
73
  throw new Error("Git worktree porcelain output has no worktrees");
72
74
  return worktrees;
73
75
  }
74
- export function registeredWorktrees(repository) {
75
- return worktreesFromPorcelain(runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
76
+ export async function registeredWorktrees(repository) {
77
+ return worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
76
78
  }
77
- export function registeredWorktreePaths(repository) { return registeredWorktrees(repository).map((worktree) => worktree.path); }
78
- export function repositoryAt(cwd) {
79
+ export async function registeredWorktreePaths(repository) {
80
+ return (await registeredWorktrees(repository)).map((worktree) => worktree.path);
81
+ }
82
+ export async function repositoryAt(cwd) {
79
83
  if (typeof cwd !== "string" || cwd.length === 0) {
80
84
  throw new Error("repository path must be a nonempty string");
81
85
  }
@@ -88,8 +92,8 @@ export function repositoryAt(cwd) {
88
92
  let primaryWorktree;
89
93
  let commonDirectory;
90
94
  try {
91
- primaryWorktree = registeredWorktreePaths(provisional)[0];
92
- commonDirectory = absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
95
+ primaryWorktree = (await registeredWorktreePaths(provisional))[0];
96
+ commonDirectory = await absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
93
97
  }
94
98
  catch (error) {
95
99
  if (error instanceof GitPlumbingError && error.status === 128) {
@@ -99,8 +103,8 @@ export function repositoryAt(cwd) {
99
103
  }
100
104
  return { effectiveCwd, primaryWorktree, commonDirectory };
101
105
  }
102
- function absoluteGitPath(repository, args, label) {
103
- const value = runGit(repository, ["rev-parse", "--path-format=absolute", ...args]).toString("utf8").trim();
106
+ async function absoluteGitPath(repository, args, label) {
107
+ const value = (await runGit(repository, ["rev-parse", "--path-format=absolute", ...args])).toString("utf8").trim();
104
108
  if (value.length === 0)
105
109
  throw new Error(`${label} is empty`);
106
110
  return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
@@ -108,12 +112,12 @@ function absoluteGitPath(repository, args, label) {
108
112
  export function commonGitDirectory(repository) {
109
113
  return repository.commonDirectory;
110
114
  }
111
- export function worktreeGitDirectory(repository, worktree) {
115
+ export async function worktreeGitDirectory(repository, worktree) {
112
116
  const scoped = { ...repository, effectiveCwd: worktree };
113
- return absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
117
+ return await absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
114
118
  }
115
- export function worktreeRoot(repository) {
116
- return absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
119
+ export async function worktreeRoot(repository) {
120
+ return await absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
117
121
  }
118
122
  function commandError(command, error) {
119
123
  const candidate = error;
@@ -128,32 +132,69 @@ function commandError(command, error) {
128
132
  pid: candidate.pid ?? null,
129
133
  });
130
134
  }
131
- export function runGit(repository, args, input) {
132
- try {
133
- const output = execFileSync("git", [...args], {
134
- cwd: repository.effectiveCwd,
135
- input,
136
- stdio: ["pipe", "pipe", "pipe"],
137
- });
138
- return Buffer.isBuffer(output) ? output : Buffer.from(output);
139
- }
140
- catch (error) {
141
- throw commandError(args, error);
142
- }
135
+ async function executeGit(repository, args, input, environment) {
136
+ const stdout = [];
137
+ const stderr = [];
138
+ let stderrBytes = 0;
139
+ let inputError;
140
+ const child = spawn("git", [...args], {
141
+ cwd: repository.effectiveCwd,
142
+ ...(environment === undefined ? {} : { env: { ...process.env, ...environment } }),
143
+ stdio: ["pipe", "pipe", "pipe"],
144
+ windowsHide: true,
145
+ });
146
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
147
+ child.stderr.on("data", (chunk) => {
148
+ if (stderrBytes >= GIT_STDERR_BYTES)
149
+ return;
150
+ const retained = chunk.subarray(0, GIT_STDERR_BYTES - stderrBytes);
151
+ stderr.push(retained);
152
+ stderrBytes += retained.length;
153
+ });
154
+ child.stdin.on("error", (error) => { inputError = error; });
155
+ child.stdin.end(input);
156
+ const terminal = await new Promise((resolveTerminal) => {
157
+ child.once("error", (error) => resolveTerminal({ code: null, error }));
158
+ child.once("close", (code) => resolveTerminal({ code }));
159
+ });
160
+ const output = Buffer.concat(stdout);
161
+ if (terminal.code === 0 && inputError === undefined)
162
+ return output;
163
+ throw commandError(args, {
164
+ message: terminal.error?.message ?? inputError?.message ?? `git exited with status ${terminal.code ?? "unknown"}`,
165
+ stdout: output,
166
+ stderr: Buffer.concat(stderr),
167
+ status: terminal.code,
168
+ pid: child.pid,
169
+ });
143
170
  }
144
- export function runGitWithEnvironment(repository, args, input, environment) {
145
- try {
146
- const output = execFileSync("git", [...args], {
147
- cwd: repository.effectiveCwd,
148
- input,
149
- env: { ...process.env, ...environment },
150
- stdio: ["pipe", "pipe", "pipe"],
151
- });
152
- return Buffer.isBuffer(output) ? output : Buffer.from(output);
153
- }
154
- catch (error) {
155
- throw commandError(args, error);
156
- }
171
+ export async function runGit(repository, args, input) {
172
+ return await executeGit(repository, args, input);
173
+ }
174
+ export async function consumeGitStdout(repository, args, consume) {
175
+ const result = await consumeProcessStdout({
176
+ argv: ["git", ...args],
177
+ cwd: repository.effectiveCwd,
178
+ }, consume);
179
+ const { outcome } = result;
180
+ if (outcome.kind === "terminal" && outcome.code === 0)
181
+ return;
182
+ const status = outcome.kind === "terminal" ? outcome.code : null;
183
+ const stderr = outcome.kind === "terminal" ? outcome.stderr : "";
184
+ const message = outcome.kind === "terminal"
185
+ ? `git exited with status ${outcome.code}`
186
+ : outcome.kind === "spawn-error" || outcome.kind === "stream-error"
187
+ ? outcome.diagnostic
188
+ : `git process ended with ${outcome.kind}`;
189
+ throw commandError(args, {
190
+ message,
191
+ stderr,
192
+ status,
193
+ pid: result.pid ?? undefined,
194
+ });
195
+ }
196
+ export async function runGitWithEnvironment(repository, args, input, environment) {
197
+ return await executeGit(repository, args, input, environment);
157
198
  }
158
199
  function assertOid(oid, label) {
159
200
  gitObjectId(oid, label);
@@ -167,10 +208,10 @@ function assertAssertionRef(ref) {
167
208
  if (ref !== "HEAD")
168
209
  assertRef(ref);
169
210
  }
170
- export function readRef(repository, ref) {
211
+ export async function readRef(repository, ref) {
171
212
  assertAssertionRef(ref);
172
213
  try {
173
- const oid = runGit(repository, ["rev-parse", "--verify", "--quiet", ref]).toString("utf8").trim();
214
+ const oid = (await runGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
174
215
  if (oid.length === 0)
175
216
  return null;
176
217
  assertOid(oid, `ref ${ref}`);
@@ -182,9 +223,9 @@ export function readRef(repository, ref) {
182
223
  throw error;
183
224
  }
184
225
  }
185
- function readTreeForCommit(repository, commit) {
226
+ async function readTreeForCommit(repository, commit) {
186
227
  assertOid(commit, "Git commit");
187
- const tree = runGit(repository, ["show", "-s", "--format=%T", commit]).toString("utf8").trim();
228
+ const tree = (await runGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
188
229
  assertOid(tree, "Git tree");
189
230
  return tree;
190
231
  }
@@ -206,30 +247,30 @@ function parseTreeEntries(output) {
206
247
  }
207
248
  return entries;
208
249
  }
209
- export function readTreeEntries(repository, tree) {
250
+ export async function readTreeEntries(repository, tree) {
210
251
  assertOid(tree, "tree");
211
- const output = runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
252
+ const output = await runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
212
253
  return parseTreeEntries(output);
213
254
  }
214
- export function readGit(repository) {
215
- const commit = readRef(repository, GIT_REF);
255
+ export async function readGit(repository) {
256
+ const commit = await readRef(repository, GIT_REF);
216
257
  if (commit === null) {
217
258
  return { commit: null, tree: null, paths: new Map() };
218
259
  }
219
- const tree = readTreeForCommit(repository, commit);
220
- const paths = readTreeEntries(repository, tree);
221
- validateGitFormat(repository, paths);
260
+ const tree = await readTreeForCommit(repository, commit);
261
+ const paths = await readTreeEntries(repository, tree);
262
+ await validateGitFormat(repository, paths);
222
263
  return { commit, tree, paths };
223
264
  }
224
265
  /** Read only the requested private Git paths from one immutable Git tree. */
225
- export function readGitPaths(repository, requestedPaths) {
266
+ export async function readGitPaths(repository, requestedPaths) {
226
267
  for (const path of requestedPaths)
227
268
  validPath(path);
228
- const commit = readRef(repository, GIT_REF);
269
+ const commit = await readRef(repository, GIT_REF);
229
270
  if (commit === null)
230
271
  return { commit: null, tree: null, paths: new Map() };
231
- const tree = readTreeForCommit(repository, commit);
232
- const paths = parseTreeEntries(runGit(repository, [
272
+ const tree = await readTreeForCommit(repository, commit);
273
+ const paths = parseTreeEntries(await runGit(repository, [
233
274
  "ls-tree",
234
275
  "-z",
235
276
  "--full-tree",
@@ -238,16 +279,16 @@ export function readGitPaths(repository, requestedPaths) {
238
279
  GIT_FORMAT_PATH,
239
280
  ...requestedPaths,
240
281
  ]));
241
- validateGitFormat(repository, paths);
282
+ await validateGitFormat(repository, paths);
242
283
  return { commit, tree, paths };
243
284
  }
244
285
  /** Add paths from the same immutable Git tree without rereading its ref. */
245
- export function extendGitPaths(repository, snapshot, requestedPaths) {
286
+ export async function extendGitPaths(repository, snapshot, requestedPaths) {
246
287
  for (const path of requestedPaths)
247
288
  validPath(path);
248
289
  if (snapshot.tree === null || requestedPaths.length === 0)
249
290
  return snapshot;
250
- const additions = parseTreeEntries(runGit(repository, [
291
+ const additions = parseTreeEntries(await runGit(repository, [
251
292
  "ls-tree",
252
293
  "-z",
253
294
  "--full-tree",
@@ -258,19 +299,19 @@ export function extendGitPaths(repository, snapshot, requestedPaths) {
258
299
  return { ...snapshot, paths: new Map([...snapshot.paths, ...additions]) };
259
300
  }
260
301
  /** Expand a targeted immutable snapshot to its complete already-pinned tree. */
261
- export function expandGitSnapshot(repository, snapshot) {
302
+ export async function expandGitSnapshot(repository, snapshot) {
262
303
  if (snapshot.tree === null)
263
304
  return snapshot;
264
- return { ...snapshot, paths: readTreeEntries(repository, snapshot.tree) };
305
+ return { ...snapshot, paths: await readTreeEntries(repository, snapshot.tree) };
265
306
  }
266
- export function writeBlob(repository, bytes) {
267
- const oid = runGit(repository, ["hash-object", "-w", "--stdin"], bytes).toString("utf8").trim();
307
+ export async function writeBlob(repository, bytes) {
308
+ const oid = (await runGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
268
309
  assertOid(oid, "written blob");
269
310
  return oid;
270
311
  }
271
- export function readBlob(repository, oid) {
312
+ export async function readBlob(repository, oid) {
272
313
  assertOid(oid, "blob");
273
- return runGit(repository, ["cat-file", "blob", oid]);
314
+ return await runGit(repository, ["cat-file", "blob", oid]);
274
315
  }
275
316
  function malformedBatchOutput(detail) {
276
317
  throw new GitPlumbingError({
@@ -279,13 +320,13 @@ function malformedBatchOutput(detail) {
279
320
  message: `malformed cat-file --batch output: ${detail}`,
280
321
  });
281
322
  }
282
- function readObjects(repository, oids) {
323
+ async function readObjects(repository, oids) {
283
324
  const unique = [...new Set(oids)];
284
325
  for (const oid of unique)
285
326
  assertOid(oid, "Git object");
286
327
  if (unique.length === 0)
287
328
  return new Map();
288
- const output = runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
329
+ const output = await runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
289
330
  const objects = new Map();
290
331
  let offset = 0;
291
332
  for (const oid of unique) {
@@ -321,22 +362,22 @@ function readObjects(repository, oids) {
321
362
  malformedBatchOutput("trailing bytes");
322
363
  return objects;
323
364
  }
324
- function validateGitFormat(repository, paths) {
365
+ async function validateGitFormat(repository, paths) {
325
366
  const format = paths.get(GIT_FORMAT_PATH);
326
367
  if (format === undefined)
327
368
  throw new AuthorityCorruptionError(`Git is missing ${GIT_FORMAT_PATH}`);
328
369
  if (format.type !== "blob")
329
370
  throw new AuthorityCorruptionError(`Git format is not a blob: ${GIT_FORMAT_PATH}`);
330
- const bytes = readBlob(repository, format.oid);
371
+ const bytes = await readBlob(repository, format.oid);
331
372
  if (bytes.toString("utf8") !== GIT_FORMAT_BYTES) {
332
373
  throw new AuthorityCorruptionError(`Git format is not current: ${GIT_FORMAT_PATH}`);
333
374
  }
334
375
  }
335
- function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
376
+ async function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
336
377
  const byPath = new Map();
337
378
  if (baseTree === null)
338
379
  return byPath;
339
- const ancestry = parseTreeEntries(runGit(repository, [
380
+ const ancestry = parseTreeEntries(await runGit(repository, [
340
381
  "ls-tree",
341
382
  "-z",
342
383
  "-t",
@@ -357,7 +398,7 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
357
398
  throw new Error(`Git path is both file and directory: ${path}`);
358
399
  treeByPath.set(path, entry.oid);
359
400
  }
360
- const objects = readObjects(repository, [...treeByPath.values()]);
401
+ const objects = await readObjects(repository, [...treeByPath.values()]);
361
402
  const oidBytes = baseTree.length / 2;
362
403
  for (const [path, oid] of treeByPath) {
363
404
  const object = objects.get(oid);
@@ -368,8 +409,8 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
368
409
  }
369
410
  return byPath;
370
411
  }
371
- function writePreparedTrees(repository, prepared) {
372
- const output = runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
412
+ async function writePreparedTrees(repository, prepared) {
413
+ const output = await runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
373
414
  const written = output.toString("ascii").trimEnd().split("\n");
374
415
  if (written.length !== prepared.length) {
375
416
  throw new GitPlumbingError({ stderr: output, status: null, message: "malformed mktree --batch output" });
@@ -384,18 +425,18 @@ function writePreparedTrees(repository, prepared) {
384
425
  }
385
426
  }
386
427
  /** Copy only the changed paths' tree ancestors, preserving untouched subtrees by object ID. */
387
- export function updateGitTree(repository, baseTree, changes) {
428
+ export async function updateGitTree(repository, baseTree, changes) {
388
429
  const update = treeUpdate(changes);
389
- const bases = baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
430
+ const bases = await baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
390
431
  const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
391
432
  if (representativeOid === undefined)
392
433
  throw new Error("cannot write an empty Git tree without an object format");
393
434
  const prepared = prepareTreeUpdate({ update, bases, oidBytes: representativeOid.length / 2 });
394
- writePreparedTrees(repository, prepared.trees);
435
+ await writePreparedTrees(repository, prepared.trees);
395
436
  return prepared.root;
396
437
  }
397
438
  /** Write a tree update from directory entries frozen with the admitted base tree. */
398
- export function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
439
+ export async function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
399
440
  const update = treeUpdate(changes);
400
441
  const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
401
442
  if (representativeOid === undefined)
@@ -405,10 +446,10 @@ export function updateGitTreeFromFrozenDirectories(repository, baseTree, directo
405
446
  bases: directories,
406
447
  oidBytes: representativeOid.length / 2,
407
448
  });
408
- writePreparedTrees(repository, prepared.trees);
449
+ await writePreparedTrees(repository, prepared.trees);
409
450
  return prepared.root;
410
451
  }
411
- function writeCommitObject(input) {
452
+ async function writeCommitObject(input) {
412
453
  const { repository, tree, parents, message, actor, email, at } = input;
413
454
  assertOid(tree, "commit tree");
414
455
  for (const parent of parents)
@@ -416,20 +457,20 @@ function writeCommitObject(input) {
416
457
  const args = ["commit-tree", tree];
417
458
  for (const parent of parents)
418
459
  args.push("-p", parent);
419
- const commit = runGitWithEnvironment(repository, args, `${message}\n`, {
460
+ const commit = (await runGitWithEnvironment(repository, args, `${message}\n`, {
420
461
  GIT_AUTHOR_NAME: actor,
421
462
  GIT_AUTHOR_EMAIL: email,
422
463
  GIT_COMMITTER_NAME: actor,
423
464
  GIT_COMMITTER_EMAIL: email,
424
465
  ...(at === undefined ? {} : { GIT_AUTHOR_DATE: at, GIT_COMMITTER_DATE: at }),
425
- })
466
+ }))
426
467
  .toString("utf8")
427
468
  .trim();
428
469
  assertOid(commit, "written Git commit");
429
470
  return commit;
430
471
  }
431
- export function writeCommit(input) {
432
- return writeCommitObject({
472
+ export async function writeCommit(input) {
473
+ return await writeCommitObject({
433
474
  repository: input.repository,
434
475
  tree: input.tree,
435
476
  parents: input.parent === null ? [] : [input.parent],
@@ -455,7 +496,7 @@ function refUpdateLine(update, index) {
455
496
  assertOid(update.expectedOid, `expected oid for ${update.ref}`);
456
497
  return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
457
498
  }
458
- export function updateRefsAtomically(repository, updates, assertions = []) {
499
+ export async function updateRefsAtomically(repository, updates, assertions = []) {
459
500
  if (updates.length === 0)
460
501
  throw new Error("an atomic ref transaction needs a Git update");
461
502
  if (updates.length > 2)
@@ -473,7 +514,7 @@ export function updateRefsAtomically(repository, updates, assertions = []) {
473
514
  "",
474
515
  ];
475
516
  try {
476
- runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
517
+ await runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
477
518
  return { kind: "published" };
478
519
  }
479
520
  catch (error) {
@@ -488,3 +529,34 @@ export function updateRefsAtomically(repository, updates, assertions = []) {
488
529
  throw error;
489
530
  }
490
531
  }
532
+ export function decodeGitNumstat(output) {
533
+ const fields = output.toString("utf8").split("\0");
534
+ if (fields.at(-1) !== "")
535
+ throw new Error("Git numstat output is not NUL terminated");
536
+ let filesChanged = 0;
537
+ let insertions = 0;
538
+ let deletions = 0;
539
+ for (let index = 0; index < fields.length - 1; index += 1) {
540
+ const field = fields[index];
541
+ const [added, deleted, path, extra] = field.split("\t");
542
+ if (added === undefined || deleted === undefined || path === undefined || extra !== undefined) {
543
+ throw new Error("Git numstat output is malformed");
544
+ }
545
+ if (path.length === 0) {
546
+ if (fields[index + 1] === undefined || fields[index + 2] === undefined) {
547
+ throw new Error("Git rename numstat output is missing paths");
548
+ }
549
+ index += 2;
550
+ }
551
+ filesChanged += 1;
552
+ insertions += added === "-" ? 0 : Number.parseInt(added, 10);
553
+ deletions += deleted === "-" ? 0 : Number.parseInt(deleted, 10);
554
+ }
555
+ return { filesChanged, insertions, deletions };
556
+ }
557
+ export function decodeGitNameOnly(output) {
558
+ const names = output.toString("utf8").split("\0");
559
+ if (names.at(-1) !== "")
560
+ throw new Error("Git name-only output is not NUL terminated");
561
+ return names.slice(0, -1);
562
+ }
@@ -2,12 +2,16 @@ import type { SnapshotId } from "../core/facts/types.js";
2
2
  import { type GitRepository } from "./repository.js";
3
3
  export type MaterializedScratchCandidate = Readonly<{
4
4
  cwd: string;
5
- dispose: () => WorktreeLeak | null;
5
+ dispose: () => Promise<WorktreeLeak | null>;
6
+ }>;
7
+ export type CollectableScratchWorktree = Readonly<{
8
+ path: string;
9
+ release(): void;
6
10
  }>;
7
11
  export type WorktreeLeak = Readonly<{
8
12
  path: string;
9
13
  diagnostic: string;
10
14
  }>;
11
- export declare function orphanedScratchWorktrees(paths: Iterable<string>): readonly string[];
15
+ export declare function collectableScratchWorktrees(paths: Iterable<string>): Promise<readonly CollectableScratchWorktree[]>;
12
16
  /** Materialize a disposable candidate worktree and own only its removal. */
13
- export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): MaterializedScratchCandidate;
17
+ export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): Promise<MaterializedScratchCandidate>;
@@ -1,58 +1,62 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { realpathSync } from "node:fs";
2
+ import { realpath } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { tryAcquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
5
6
  import { gitObjectIdForSnapshot } from "./identity.js";
6
7
  import { runGit } from "./repository.js";
7
- import { currentProcessIdentity, probeProcessIdentity, } from "../runtime/proc/run.js";
8
8
  const SCRATCH_PREFIX = "keiyaku-v4-verify-";
9
- const SCRATCH_ROOT = realpathSync(tmpdir());
9
+ const SCRATCH_ROOT = await realpath(tmpdir());
10
+ const SCRATCH_PATTERN = /^keiyaku-v4-verify-([0-9a-f]{24})$/;
10
11
  function scratchPath() {
11
- const owner = currentProcessIdentity();
12
- const identity = Buffer.from(owner.spawnedAt, "utf8").toString("base64url");
13
- return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${owner.pid}-${identity}-${randomBytes(12).toString("hex")}`);
12
+ return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${randomBytes(12).toString("hex")}`);
14
13
  }
15
- function scratchOwner(path) {
16
- const prefix = join(SCRATCH_ROOT, SCRATCH_PREFIX);
17
- if (!path.startsWith(prefix))
14
+ function ownershipLockPath(path) {
15
+ if (dirname(path) !== SCRATCH_ROOT)
18
16
  return null;
19
- const suffix = path.slice(prefix.length);
20
- const match = /^([1-9][0-9]*)-([A-Za-z0-9_-]+)-([0-9a-f]{24})$/.exec(suffix);
21
- if (match === null)
22
- return null;
23
- const pid = Number(match[1]);
24
- if (!Number.isSafeInteger(pid))
25
- return null;
26
- let spawnedAt;
27
- try {
28
- spawnedAt = Buffer.from(match[2], "base64url").toString("utf8");
29
- }
30
- catch {
31
- return null;
32
- }
33
- return spawnedAt.length === 0 ? null : { pid, spawnedAt };
17
+ const match = SCRATCH_PATTERN.exec(basename(path));
18
+ return match === null ? null : join(SCRATCH_ROOT, `.${SCRATCH_PREFIX}${match[1]}.owner.sqlite`);
34
19
  }
35
- export function orphanedScratchWorktrees(paths) {
36
- const orphaned = [];
20
+ export async function collectableScratchWorktrees(paths) {
21
+ const collectable = [];
37
22
  for (const path of paths) {
38
- const owner = scratchOwner(path);
39
- if (owner === null)
23
+ const lockPath = ownershipLockPath(path);
24
+ if (lockPath === null)
40
25
  continue;
41
- const state = probeProcessIdentity(owner);
42
- if (state.kind === "gone" || state.kind === "replaced")
43
- orphaned.push(path);
26
+ const lock = await tryAcquireSqliteTransactionLock({ path: lockPath, mode: "exclusive" });
27
+ if (lock !== null)
28
+ collectable.push({ path, release: () => lock.close() });
44
29
  }
45
- return orphaned;
30
+ return collectable;
46
31
  }
47
32
  /** Materialize a disposable candidate worktree and own only its removal. */
48
- export function materializeScratchCandidate(repository, candidate) {
33
+ export async function materializeScratchCandidate(repository, candidate) {
49
34
  const cwd = scratchPath();
50
- runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
35
+ const lockPath = ownershipLockPath(cwd);
36
+ if (lockPath === null)
37
+ throw new Error("scratch ownership lock path is invalid");
38
+ const ownership = await tryAcquireSqliteTransactionLock({
39
+ path: lockPath,
40
+ mode: "exclusive",
41
+ });
42
+ if (ownership === null)
43
+ throw new Error("new scratch ownership lock is unexpectedly held");
44
+ try {
45
+ await runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
46
+ }
47
+ catch (error) {
48
+ ownership.close();
49
+ throw error;
50
+ }
51
+ let disposed = false;
51
52
  return {
52
53
  cwd,
53
- dispose: () => {
54
+ dispose: async () => {
55
+ if (disposed)
56
+ return null;
57
+ disposed = true;
54
58
  try {
55
- runGit(repository, ["worktree", "remove", "--force", cwd]);
59
+ await runGit(repository, ["worktree", "remove", "--force", cwd]);
56
60
  return null;
57
61
  }
58
62
  catch (error) {
@@ -61,6 +65,9 @@ export function materializeScratchCandidate(repository, candidate) {
61
65
  diagnostic: error instanceof Error ? error.message : String(error),
62
66
  };
63
67
  }
68
+ finally {
69
+ ownership.close();
70
+ }
64
71
  },
65
72
  };
66
73
  }
@@ -48,9 +48,45 @@ export type TargetPlacementPreparation = Readonly<{
48
48
  kind: "refused";
49
49
  refusal: TargetPlacementRefusal;
50
50
  }>;
51
- export declare function observeTargetHead(repository: GitRepository, target: string): SnapshotId | null;
51
+ export declare function observeTargetHead(repository: GitRepository, target: string): Promise<SnapshotId | null>;
52
52
  export declare function acquireTargetPlacementFence(repository: GitRepository, target: string): Promise<HeldSqliteTransactionLock>;
53
- export declare function prepareTargetPlacement(repository: GitRepository, state: ContractState, target: RefOperation): TargetPlacementPreparation;
54
- export declare function followTargetPlacement(repository: GitRepository, prepared: PreparedTargetPlacement): TargetPlacementPhysicalResult;
55
- export declare function recoverTargetPlacement(repository: GitRepository, state: ContractState): TargetPlacementPhysicalResult;
53
+ export type TargetedContractCoordinates = ContractState["coordinates"] & Readonly<{
54
+ target: string;
55
+ }>;
56
+ export type TargetPlacementObservationInput = Readonly<{
57
+ contractId: ContractId;
58
+ coordinates: TargetedContractCoordinates;
59
+ predecessor: SnapshotId;
60
+ candidate: SnapshotId;
61
+ }>;
62
+ export type TargetPlacementObservation = Readonly<{
63
+ kind: "ready";
64
+ arms: readonly FollowArm[];
65
+ }> | Readonly<{
66
+ kind: "refused";
67
+ refusal: TargetPlacementRefusal;
68
+ }>;
69
+ /** Observe whether registered target checkouts can follow a predecessor-to-candidate movement. */
70
+ export declare function observeTargetPlacement(repository: GitRepository, input: TargetPlacementObservationInput): Promise<TargetPlacementObservation>;
71
+ export type AuditTargetAnswer = Readonly<{
72
+ kind: "placeable";
73
+ ref: string;
74
+ head: SnapshotId;
75
+ }> | Readonly<{
76
+ kind: "moved";
77
+ ref: string;
78
+ expected: SnapshotId;
79
+ observed: SnapshotId | null;
80
+ }> | Readonly<{
81
+ kind: "refused";
82
+ refusal: TargetPlacementRefusal;
83
+ }> | Readonly<{
84
+ kind: "failed";
85
+ diagnostic: string;
86
+ }>;
87
+ /** Adjudicate the complete post-Verification audit target answer without placing. */
88
+ export declare function adjudicateAuditTarget(repository: GitRepository, input: TargetPlacementObservationInput): Promise<AuditTargetAnswer>;
89
+ export declare function prepareTargetPlacement(repository: GitRepository, state: ContractState, target: RefOperation): Promise<TargetPlacementPreparation>;
90
+ export declare function followTargetPlacement(repository: GitRepository, prepared: PreparedTargetPlacement): Promise<TargetPlacementPhysicalResult>;
91
+ export declare function recoverTargetPlacement(repository: GitRepository, state: ContractState): Promise<TargetPlacementPhysicalResult>;
56
92
  export {};