@astrosheep/keiyaku 4.0.0 → 4.0.1

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 (241) hide show
  1. package/README.md +2 -9
  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 +11 -3
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +9 -6
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +94 -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 +162 -45
  12. package/build/src/akuma/akuma.d.ts +33 -25
  13. package/build/src/akuma/akuma.js +93 -61
  14. package/build/src/akuma/archetype.d.ts +8 -0
  15. package/build/src/akuma/archetype.js +28 -9
  16. package/build/src/akuma/body.js +196 -84
  17. package/build/src/akuma/coordinate.d.ts +9 -0
  18. package/build/src/akuma/coordinate.js +22 -0
  19. package/build/src/akuma/heart/facts.d.ts +67 -15
  20. package/build/src/akuma/heart/facts.js +3 -3
  21. package/build/src/akuma/heart/index.d.ts +41 -29
  22. package/build/src/akuma/heart/index.js +72 -193
  23. package/build/src/akuma/heart/rows.d.ts +30 -53
  24. package/build/src/akuma/heart/rows.js +91 -111
  25. package/build/src/akuma/heart/schema.d.ts +1 -1
  26. package/build/src/akuma/heart/schema.js +55 -8
  27. package/build/src/akuma/heart/storage.d.ts +33 -0
  28. package/build/src/akuma/heart/storage.js +164 -0
  29. package/build/src/akuma/heart/tells.d.ts +12 -0
  30. package/build/src/akuma/heart/tells.js +79 -0
  31. package/build/src/akuma/heart/timeline.d.ts +15 -0
  32. package/build/src/akuma/heart/timeline.js +86 -0
  33. package/build/src/akuma/index.d.ts +4 -2
  34. package/build/src/akuma/index.js +1 -1
  35. package/build/src/akuma/projection.d.ts +97 -0
  36. package/build/src/akuma/projection.js +145 -0
  37. package/build/src/akuma/provider.d.ts +57 -11
  38. package/build/src/akuma/provider.js +57 -37
  39. package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
  40. package/build/src/akuma/providers/claude/events.js +185 -0
  41. package/build/src/akuma/providers/claude/index.d.ts +46 -0
  42. package/build/src/akuma/providers/claude/index.js +265 -0
  43. package/build/src/akuma/providers/claude/input.d.ts +13 -0
  44. package/build/src/akuma/providers/claude/input.js +109 -0
  45. package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
  46. package/build/src/akuma/providers/codex-app-server/events.js +12 -12
  47. package/build/src/akuma/providers/codex-app-server/index.d.ts +13 -11
  48. package/build/src/akuma/providers/codex-app-server/index.js +73 -22
  49. package/build/src/akuma/providers/index.js +22 -8
  50. package/build/src/akuma/providers/opencode-sdk/events.d.ts +48 -0
  51. package/build/src/akuma/providers/opencode-sdk/events.js +116 -0
  52. package/build/src/akuma/providers/opencode-sdk/index.d.ts +32 -0
  53. package/build/src/akuma/providers/opencode-sdk/index.js +203 -0
  54. package/build/src/akuma/providers/opencode-sdk/session.d.ts +41 -0
  55. package/build/src/akuma/providers/opencode-sdk/session.js +62 -0
  56. package/build/src/akuma/providers/pi/events.d.ts +33 -0
  57. package/build/src/akuma/providers/pi/events.js +128 -0
  58. package/build/src/akuma/providers/pi/index.d.ts +11 -0
  59. package/build/src/akuma/providers/pi/index.js +162 -0
  60. package/build/src/akuma/requests.d.ts +1 -0
  61. package/build/src/akuma/requests.js +5 -2
  62. package/build/src/alias/index.d.ts +4 -3
  63. package/build/src/alias/index.js +2 -3
  64. package/build/src/body/render.js +2 -1
  65. package/build/src/body/shape.js +7 -1
  66. package/build/src/body/verification.js +19 -4
  67. package/build/src/cli/accepted.d.ts +2 -0
  68. package/build/src/cli/accepted.js +2 -1
  69. package/build/src/cli/commands/akuma-invoke.d.ts +42 -18
  70. package/build/src/cli/commands/akuma-invoke.js +94 -50
  71. package/build/src/cli/commands/akuma.d.ts +15 -7
  72. package/build/src/cli/commands/akuma.js +94 -56
  73. package/build/src/cli/commands/contract.d.ts +142 -0
  74. package/build/src/cli/commands/contract.js +14 -0
  75. package/build/src/cli/commands/install.js +2 -0
  76. package/build/src/cli/commands/task-invoke.d.ts +3 -1
  77. package/build/src/cli/commands/task-invoke.js +27 -2
  78. package/build/src/cli/commands/task.js +2 -2
  79. package/build/src/cli/invoke.d.ts +6 -1
  80. package/build/src/cli/invoke.js +241 -43
  81. package/build/src/cli/main.js +20 -1
  82. package/build/src/cli/parse.d.ts +13 -116
  83. package/build/src/cli/parse.js +51 -78
  84. package/build/src/cli/render/akuma-tool.d.ts +2 -0
  85. package/build/src/cli/render/akuma-tool.js +6 -2
  86. package/build/src/cli/render/akuma.d.ts +2 -1
  87. package/build/src/cli/render/akuma.js +157 -217
  88. package/build/src/cli/render/catalog.d.ts +2 -0
  89. package/build/src/cli/render/catalog.js +16 -0
  90. package/build/src/cli/render/contract.d.ts +1 -1
  91. package/build/src/cli/render/contract.js +38 -5
  92. package/build/src/cli/render/kanshi.js +53 -5
  93. package/build/src/cli/render/refusal.js +17 -0
  94. package/build/src/cli/render/terminal.d.ts +13 -0
  95. package/build/src/cli/render/terminal.js +75 -0
  96. package/build/src/cli/render/text.js +5 -0
  97. package/build/src/cli/result.d.ts +15 -3
  98. package/build/src/contract-worktree.d.ts +43 -0
  99. package/build/src/contract-worktree.js +213 -0
  100. package/build/src/coordination/durable-file.d.ts +3 -0
  101. package/build/src/coordination/durable-file.js +44 -1
  102. package/build/src/core/facts/codec.js +23 -4
  103. package/build/src/core/facts/eligibility.d.ts +1 -10
  104. package/build/src/core/facts/eligibility.js +0 -49
  105. package/build/src/core/facts/observation.d.ts +3 -0
  106. package/build/src/core/facts/observation.js +12 -0
  107. package/build/src/core/facts/types.d.ts +10 -3
  108. package/build/src/core/subject.js +2 -2
  109. package/build/src/core/verbs/amend.js +12 -16
  110. package/build/src/core/verbs/bind.js +2 -16
  111. package/build/src/core/verbs/deliver.js +20 -4
  112. package/build/src/core/verbs/placement.js +3 -8
  113. package/build/src/dispatch/index.d.ts +3 -1
  114. package/build/src/dispatch/index.js +14 -5
  115. package/build/src/duration.d.ts +10 -0
  116. package/build/src/duration.js +22 -0
  117. package/build/src/git/admission.d.ts +2 -2
  118. package/build/src/git/admission.js +26 -16
  119. package/build/src/git/hooks.d.ts +15 -0
  120. package/build/src/git/hooks.js +57 -5
  121. package/build/src/git/identity.d.ts +5 -1
  122. package/build/src/git/identity.js +23 -4
  123. package/build/src/git/integration.d.ts +49 -0
  124. package/build/src/git/integration.js +214 -0
  125. package/build/src/git/observe.d.ts +25 -13
  126. package/build/src/git/observe.js +175 -72
  127. package/build/src/git/read-observation.d.ts +41 -0
  128. package/build/src/git/read-observation.js +361 -0
  129. package/build/src/git/reconcile.d.ts +15 -5
  130. package/build/src/git/reconcile.js +238 -85
  131. package/build/src/git/repository.d.ts +19 -4
  132. package/build/src/git/repository.js +77 -43
  133. package/build/src/git/scratch.d.ts +13 -0
  134. package/build/src/git/scratch.js +66 -0
  135. package/build/src/git/target-placement.d.ts +56 -0
  136. package/build/src/git/target-placement.js +255 -0
  137. package/build/src/git/tender.d.ts +48 -0
  138. package/build/src/git/tender.js +82 -0
  139. package/build/src/git/terminal-seal.d.ts +24 -0
  140. package/build/src/git/terminal-seal.js +87 -0
  141. package/build/src/git/tree.d.ts +2 -2
  142. package/build/src/git/tree.js +4 -0
  143. package/build/src/git/workspace.d.ts +21 -0
  144. package/build/src/git/workspace.js +104 -0
  145. package/build/src/identity/selector.d.ts +6 -0
  146. package/build/src/identity/selector.js +19 -0
  147. package/build/src/index.d.ts +4 -2
  148. package/build/src/index.js +2 -1
  149. package/build/src/kanshi/index.d.ts +1 -1
  150. package/build/src/kanshi/read.d.ts +5 -2
  151. package/build/src/kanshi/read.js +154 -45
  152. package/build/src/kanshi/report.d.ts +31 -6
  153. package/build/src/kanshi/select.js +1 -1
  154. package/build/src/library/address.d.ts +48 -0
  155. package/build/src/library/address.js +134 -0
  156. package/build/src/library/akuma-creation.d.ts +10 -6
  157. package/build/src/library/akuma-creation.js +12 -11
  158. package/build/src/library/bind.d.ts +21 -0
  159. package/build/src/library/bind.js +56 -0
  160. package/build/src/library/catalog.d.ts +58 -0
  161. package/build/src/library/catalog.js +72 -0
  162. package/build/src/library/configuration.d.ts +6 -15
  163. package/build/src/library/configuration.js +14 -17
  164. package/build/src/library/contract.d.ts +21 -46
  165. package/build/src/library/contract.js +193 -183
  166. package/build/src/library/delivery.d.ts +23 -0
  167. package/build/src/library/delivery.js +21 -0
  168. package/build/src/library/fleet.d.ts +65 -0
  169. package/build/src/library/fleet.js +189 -0
  170. package/build/src/library/input.d.ts +3 -0
  171. package/build/src/library/input.js +19 -0
  172. package/build/src/library/keiyaku.d.ts +16 -2
  173. package/build/src/library/keiyaku.js +11 -1
  174. package/build/src/library/mutation.d.ts +38 -0
  175. package/build/src/library/mutation.js +39 -0
  176. package/build/src/library/refusal.d.ts +21 -0
  177. package/build/src/library/refusal.js +29 -0
  178. package/build/src/library/region.d.ts +2 -1
  179. package/build/src/library/region.js +3 -3
  180. package/build/src/library/repo.d.ts +4 -1
  181. package/build/src/library/repo.js +31 -16
  182. package/build/src/protocol/attempt.d.ts +16 -3
  183. package/build/src/protocol/attempt.js +39 -8
  184. package/build/src/protocol/bind.d.ts +9 -2
  185. package/build/src/protocol/bind.js +47 -39
  186. package/build/src/protocol/intent.d.ts +42 -18
  187. package/build/src/protocol/intent.js +36 -75
  188. package/build/src/protocol/operations.d.ts +93 -35
  189. package/build/src/protocol/operations.js +235 -123
  190. package/build/src/protocol/outcome.d.ts +7 -2
  191. package/build/src/protocol/outcome.js +9 -3
  192. package/build/src/protocol/placement.d.ts +30 -0
  193. package/build/src/protocol/placement.js +114 -0
  194. package/build/src/protocol/read/audit.d.ts +9 -3
  195. package/build/src/protocol/read/audit.js +25 -9
  196. package/build/src/protocol/read/documents.d.ts +2 -2
  197. package/build/src/protocol/read/documents.js +4 -4
  198. package/build/src/protocol/read/status.d.ts +11 -5
  199. package/build/src/protocol/read/status.js +28 -15
  200. package/build/src/protocol/run.d.ts +49 -7
  201. package/build/src/protocol/run.js +53 -25
  202. package/build/src/runtime/proc/line-rpc.d.ts +1 -0
  203. package/build/src/runtime/proc/line-rpc.js +21 -0
  204. package/build/src/runtime/proc/run.d.ts +18 -2
  205. package/build/src/runtime/proc/run.js +34 -8
  206. package/build/src/settings.d.ts +6 -0
  207. package/build/src/settings.js +29 -14
  208. package/build/src/settlement/fence.d.ts +9 -0
  209. package/build/src/settlement/fence.js +23 -0
  210. package/build/src/settlement/holder.d.ts +25 -4
  211. package/build/src/settlement/holder.js +102 -28
  212. package/build/src/settlement/settle.d.ts +8 -0
  213. package/build/src/settlement/settle.js +102 -28
  214. package/build/src/task/board.d.ts +20 -2
  215. package/build/src/task/board.js +11 -1
  216. package/build/src/task/compose.d.ts +2 -2
  217. package/build/src/task/compose.js +2 -2
  218. package/build/src/task/context.js +2 -7
  219. package/build/src/task/index.d.ts +9 -9
  220. package/build/src/task/index.js +15 -12
  221. package/build/src/task/operations.d.ts +31 -12
  222. package/build/src/task/operations.js +8 -4
  223. package/build/src/task/store.d.ts +4 -6
  224. package/build/src/task/store.js +3 -3
  225. package/build/src/verification/declaration.d.ts +1 -0
  226. package/build/src/verification/execution.d.ts +49 -0
  227. package/build/src/verification/execution.js +104 -0
  228. package/build/src/world.d.ts +13 -0
  229. package/build/src/world.js +103 -0
  230. package/package.json +7 -4
  231. package/build/src/akuma/activity.d.ts +0 -66
  232. package/build/src/akuma/activity.js +0 -139
  233. package/build/src/akuma/providers/claude.js +0 -305
  234. package/build/src/context-root.d.ts +0 -4
  235. package/build/src/context-root.js +0 -15
  236. package/build/src/git/delivery.d.ts +0 -20
  237. package/build/src/git/delivery.js +0 -109
  238. package/build/src/git/verification.d.ts +0 -14
  239. package/build/src/git/verification.js +0 -56
  240. package/build/src/verification/producer.d.ts +0 -23
  241. package/build/src/verification/producer.js +0 -54
@@ -7,7 +7,7 @@ export const GIT_REF = "refs/heads/keiyaku-state";
7
7
  export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
8
  export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
9
  export const GIT_FORMAT_PATH = "meta/format.json";
10
- const CURRENT_FORMAT_VERSION = 3;
10
+ const CURRENT_FORMAT_VERSION = 4;
11
11
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
12
12
  export function isKeiyakuOwnedRef(ref) {
13
13
  return ref === GIT_REF
@@ -25,20 +25,22 @@ export class NoGitWorldError extends Error {
25
25
  }
26
26
  }
27
27
  export class GitPlumbingError extends Error {
28
+ stdout;
28
29
  stderr;
29
30
  status;
30
31
  pid;
31
32
  constructor(input) {
32
33
  super(input.message);
33
34
  this.name = "GitPlumbingError";
35
+ this.stdout = Buffer.from(input.stdout ?? "");
34
36
  this.stderr = Buffer.from(input.stderr);
35
37
  this.status = input.status;
36
38
  this.pid = input.pid ?? null;
37
39
  }
38
40
  }
39
- function worktreePathsFromPorcelain(output) {
41
+ function worktreesFromPorcelain(output) {
40
42
  const fields = output.toString("utf8").split("\0");
41
- const paths = [];
43
+ const worktrees = [];
42
44
  let record = [];
43
45
  const finishRecord = () => {
44
46
  if (record.length === 0)
@@ -50,7 +52,11 @@ function worktreePathsFromPorcelain(output) {
50
52
  const path = worktree.slice("worktree ".length);
51
53
  if (path.length === 0)
52
54
  throw new Error("Git worktree porcelain output has an empty worktree path");
53
- paths.push(resolve(path));
55
+ const branchField = record.find((field) => field.startsWith("branch "));
56
+ const branch = branchField === undefined ? null : branchField.slice("branch ".length);
57
+ if (branch !== null && !branch.startsWith("refs/"))
58
+ throw new Error("Git worktree porcelain output has an invalid branch");
59
+ worktrees.push({ path: resolve(path), branch });
54
60
  record = [];
55
61
  };
56
62
  for (const field of fields) {
@@ -61,22 +67,29 @@ function worktreePathsFromPorcelain(output) {
61
67
  record.push(field);
62
68
  }
63
69
  finishRecord();
64
- if (paths.length === 0)
70
+ if (worktrees.length === 0)
65
71
  throw new Error("Git worktree porcelain output has no worktrees");
66
- return paths;
72
+ return worktrees;
67
73
  }
68
- export function registeredWorktreePaths(repository) {
69
- return worktreePathsFromPorcelain(runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
74
+ export function registeredWorktrees(repository) {
75
+ return worktreesFromPorcelain(runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
70
76
  }
77
+ export function registeredWorktreePaths(repository) { return registeredWorktrees(repository).map((worktree) => worktree.path); }
71
78
  export function repositoryAt(cwd) {
72
79
  if (typeof cwd !== "string" || cwd.length === 0) {
73
80
  throw new Error("repository path must be a nonempty string");
74
81
  }
75
82
  const effectiveCwd = resolve(cwd);
76
- const provisional = { effectiveCwd, primaryWorktree: effectiveCwd };
83
+ const provisional = {
84
+ effectiveCwd,
85
+ primaryWorktree: effectiveCwd,
86
+ commonDirectory: effectiveCwd,
87
+ };
77
88
  let primaryWorktree;
89
+ let commonDirectory;
78
90
  try {
79
91
  primaryWorktree = registeredWorktreePaths(provisional)[0];
92
+ commonDirectory = absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
80
93
  }
81
94
  catch (error) {
82
95
  if (error instanceof GitPlumbingError && error.status === 128) {
@@ -84,7 +97,7 @@ export function repositoryAt(cwd) {
84
97
  }
85
98
  throw error;
86
99
  }
87
- return { effectiveCwd, primaryWorktree };
100
+ return { effectiveCwd, primaryWorktree, commonDirectory };
88
101
  }
89
102
  function absoluteGitPath(repository, args, label) {
90
103
  const value = runGit(repository, ["rev-parse", "--path-format=absolute", ...args]).toString("utf8").trim();
@@ -93,17 +106,22 @@ function absoluteGitPath(repository, args, label) {
93
106
  return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
94
107
  }
95
108
  export function commonGitDirectory(repository) {
96
- return absoluteGitPath(repository, ["--git-common-dir"], "common Git directory");
109
+ return repository.commonDirectory;
97
110
  }
98
111
  export function worktreeGitDirectory(repository, worktree) {
99
112
  const scoped = { ...repository, effectiveCwd: worktree };
100
113
  return absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
101
114
  }
115
+ export function worktreeRoot(repository) {
116
+ return absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
117
+ }
102
118
  function commandError(command, error) {
103
119
  const candidate = error;
104
120
  const stderr = candidate.stderr === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stderr);
121
+ const stdout = candidate.stdout === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stdout);
105
122
  const detail = stderr.length === 0 ? candidate.message ?? "git command failed" : stderr.toString("utf8");
106
123
  return new GitPlumbingError({
124
+ stdout,
107
125
  stderr,
108
126
  status: candidate.status ?? null,
109
127
  message: `${command.join(" ")}: ${detail}`,
@@ -145,8 +163,12 @@ function assertRef(ref) {
145
163
  throw new Error(`invalid Git ref: ${ref}`);
146
164
  }
147
165
  }
166
+ function assertAssertionRef(ref) {
167
+ if (ref !== "HEAD")
168
+ assertRef(ref);
169
+ }
148
170
  export function readRef(repository, ref) {
149
- assertRef(ref);
171
+ assertAssertionRef(ref);
150
172
  try {
151
173
  const oid = runGit(repository, ["rev-parse", "--verify", "--quiet", ref]).toString("utf8").trim();
152
174
  if (oid.length === 0)
@@ -162,7 +184,7 @@ export function readRef(repository, ref) {
162
184
  }
163
185
  function readTreeForCommit(repository, commit) {
164
186
  assertOid(commit, "Git commit");
165
- const tree = runGit(repository, ["rev-parse", "--verify", `${commit}^{tree}`]).toString("utf8").trim();
187
+ const tree = runGit(repository, ["show", "-s", "--format=%T", commit]).toString("utf8").trim();
166
188
  assertOid(tree, "Git tree");
167
189
  return tree;
168
190
  }
@@ -184,7 +206,7 @@ function parseTreeEntries(output) {
184
206
  }
185
207
  return entries;
186
208
  }
187
- function readTreeEntries(repository, tree) {
209
+ export function readTreeEntries(repository, tree) {
188
210
  assertOid(tree, "tree");
189
211
  const output = runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
190
212
  return parseTreeEntries(output);
@@ -299,16 +321,6 @@ function readObjects(repository, oids) {
299
321
  malformedBatchOutput("trailing bytes");
300
322
  return objects;
301
323
  }
302
- /** Read immutable Git objects through one structured, length-delimited batch. */
303
- export function readBlobs(repository, oids) {
304
- const blobs = new Map();
305
- for (const [oid, object] of readObjects(repository, oids)) {
306
- if (object.type !== "blob")
307
- malformedBatchOutput(`expected blob for ${oid}, found ${object.type}`);
308
- blobs.set(oid, object.bytes);
309
- }
310
- return blobs;
311
- }
312
324
  function validateGitFormat(repository, paths) {
313
325
  const format = paths.get(GIT_FORMAT_PATH);
314
326
  if (format === undefined)
@@ -375,13 +387,27 @@ function writePreparedTrees(repository, prepared) {
375
387
  export function updateGitTree(repository, baseTree, changes) {
376
388
  const update = treeUpdate(changes);
377
389
  const bases = baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
378
- const representativeOid = baseTree ?? changes.values().next().value?.oid;
390
+ const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
379
391
  if (representativeOid === undefined)
380
392
  throw new Error("cannot write an empty Git tree without an object format");
381
393
  const prepared = prepareTreeUpdate({ update, bases, oidBytes: representativeOid.length / 2 });
382
394
  writePreparedTrees(repository, prepared.trees);
383
395
  return prepared.root;
384
396
  }
397
+ /** Write a tree update from directory entries frozen with the admitted base tree. */
398
+ export function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
399
+ const update = treeUpdate(changes);
400
+ const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
401
+ if (representativeOid === undefined)
402
+ throw new Error("cannot write an empty Git tree without an object format");
403
+ const prepared = prepareTreeUpdate({
404
+ update,
405
+ bases: directories,
406
+ oidBytes: representativeOid.length / 2,
407
+ });
408
+ writePreparedTrees(repository, prepared.trees);
409
+ return prepared.root;
410
+ }
385
411
  function writeCommitObject(input) {
386
412
  const { repository, tree, parents, message, actor, email, at } = input;
387
413
  assertOid(tree, "commit tree");
@@ -413,7 +439,23 @@ export function writeCommit(input) {
413
439
  ...(input.at === undefined ? {} : { at: input.at }),
414
440
  });
415
441
  }
416
- export function updateRefsAtomically(repository, updates) {
442
+ function refAssertionLine(assertion) {
443
+ assertAssertionRef(assertion.ref);
444
+ assertOid(assertion.oid, `expected oid for ${assertion.ref}`);
445
+ return `${assertion.ref === "HEAD" ? "" : "option no-deref\n"}verify ${assertion.ref} ${assertion.oid}`;
446
+ }
447
+ function refUpdateLine(update, index) {
448
+ assertRef(update.ref);
449
+ if (index > 0 && update.ref === GIT_REF)
450
+ throw new Error(`duplicate ref update: ${update.ref}`);
451
+ if (index > 0 && update.expectedOid === null)
452
+ throw new Error("target ref updates require non-null OIDs");
453
+ assertOid(update.newOid, `new oid for ${update.ref}`);
454
+ if (update.expectedOid !== null)
455
+ assertOid(update.expectedOid, `expected oid for ${update.ref}`);
456
+ return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
457
+ }
458
+ export function updateRefsAtomically(repository, updates, assertions = []) {
417
459
  if (updates.length === 0)
418
460
  throw new Error("an atomic ref transaction needs a Git update");
419
461
  if (updates.length > 2)
@@ -422,24 +464,16 @@ export function updateRefsAtomically(repository, updates) {
422
464
  if (git === undefined || git.ref !== GIT_REF) {
423
465
  throw new Error(`the first atomic update must be the Git ref: ${GIT_REF}`);
424
466
  }
425
- const lines = ["start"];
426
- for (const [index, update] of updates.entries()) {
427
- assertRef(update.ref);
428
- if (index > 0 && update.ref === GIT_REF) {
429
- throw new Error(`duplicate ref update: ${update.ref}`);
430
- }
431
- if (index > 0 && (update.newOid === null || update.expectedOid === null)) {
432
- throw new Error("target ref updates require non-null OIDs");
433
- }
434
- assertOid(update.newOid, `new oid for ${update.ref}`);
435
- if (update.expectedOid !== null)
436
- assertOid(update.expectedOid, `expected oid for ${update.ref}`);
437
- const expectedOid = update.expectedOid ?? "0".repeat(update.newOid.length);
438
- lines.push(`update ${update.ref} ${update.newOid} ${expectedOid}`);
439
- }
440
- lines.push("prepare", "commit", "");
467
+ const lines = [
468
+ "start",
469
+ ...assertions.map(refAssertionLine),
470
+ ...updates.map(refUpdateLine),
471
+ "prepare",
472
+ "commit",
473
+ "",
474
+ ];
441
475
  try {
442
- runGit(repository, ["update-ref", "--stdin", "--no-deref"], lines.join("\n"));
476
+ runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
443
477
  return { kind: "published" };
444
478
  }
445
479
  catch (error) {
@@ -0,0 +1,13 @@
1
+ import type { SnapshotId } from "../core/facts/types.js";
2
+ import { type GitRepository } from "./repository.js";
3
+ export type MaterializedScratchCandidate = Readonly<{
4
+ cwd: string;
5
+ dispose: () => WorktreeLeak | null;
6
+ }>;
7
+ export type WorktreeLeak = Readonly<{
8
+ path: string;
9
+ diagnostic: string;
10
+ }>;
11
+ export declare function orphanedScratchWorktrees(paths: Iterable<string>): readonly string[];
12
+ /** Materialize a disposable candidate worktree and own only its removal. */
13
+ export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): MaterializedScratchCandidate;
@@ -0,0 +1,66 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { gitObjectIdForSnapshot } from "./identity.js";
6
+ import { runGit } from "./repository.js";
7
+ import { currentProcessIdentity, probeProcessIdentity, } from "../runtime/proc/run.js";
8
+ const SCRATCH_PREFIX = "keiyaku-v4-verify-";
9
+ const SCRATCH_ROOT = realpathSync(tmpdir());
10
+ 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")}`);
14
+ }
15
+ function scratchOwner(path) {
16
+ const prefix = join(SCRATCH_ROOT, SCRATCH_PREFIX);
17
+ if (!path.startsWith(prefix))
18
+ 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 };
34
+ }
35
+ export function orphanedScratchWorktrees(paths) {
36
+ const orphaned = [];
37
+ for (const path of paths) {
38
+ const owner = scratchOwner(path);
39
+ if (owner === null)
40
+ continue;
41
+ const state = probeProcessIdentity(owner);
42
+ if (state.kind === "gone" || state.kind === "replaced")
43
+ orphaned.push(path);
44
+ }
45
+ return orphaned;
46
+ }
47
+ /** Materialize a disposable candidate worktree and own only its removal. */
48
+ export function materializeScratchCandidate(repository, candidate) {
49
+ const cwd = scratchPath();
50
+ runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
51
+ return {
52
+ cwd,
53
+ dispose: () => {
54
+ try {
55
+ runGit(repository, ["worktree", "remove", "--force", cwd]);
56
+ return null;
57
+ }
58
+ catch (error) {
59
+ return {
60
+ path: cwd,
61
+ diagnostic: error instanceof Error ? error.message : String(error),
62
+ };
63
+ }
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,56 @@
1
+ import { type HeldSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
2
+ import type { ContractId, ContractState, SnapshotId } from "../core/facts/types.js";
3
+ import type { RefOperation } from "../core/facts/offer.js";
4
+ import { type GitRepository } from "./repository.js";
5
+ export type WorkspaceNotOnTargetRefusal = Readonly<{
6
+ kind: "workspace-not-on-target";
7
+ contractId: ContractId;
8
+ target: string;
9
+ branch: string | null;
10
+ }>;
11
+ export type CheckoutNotFollowableRefusal = Readonly<{
12
+ kind: "checkout-not-followable";
13
+ contractId: ContractId;
14
+ target: string;
15
+ path: string;
16
+ reason: "staged" | "conflict" | "untracked";
17
+ paths: readonly string[];
18
+ }>;
19
+ export type TargetPlacementRefusal = CheckoutNotFollowableRefusal | WorkspaceNotOnTargetRefusal;
20
+ export type TargetCheckoutEffect = Readonly<{
21
+ kind: "target-checkout";
22
+ path: string;
23
+ target: string;
24
+ action: "followed" | "recovered";
25
+ }>;
26
+ export type TargetCheckoutLag = Readonly<{
27
+ kind: "target-checkout-retained";
28
+ path: string;
29
+ target: string;
30
+ diagnostic: string;
31
+ }>;
32
+ export type TargetPlacementPhysicalResult = Readonly<{
33
+ effects: readonly TargetCheckoutEffect[];
34
+ lag: readonly TargetCheckoutLag[];
35
+ }>;
36
+ type FollowArm = Readonly<{
37
+ kind: "ordinary" | "here";
38
+ path: string;
39
+ }>;
40
+ export type PreparedTargetPlacement = Readonly<{
41
+ target: RefOperation;
42
+ arms: readonly FollowArm[];
43
+ }>;
44
+ export type TargetPlacementPreparation = Readonly<{
45
+ kind: "prepared";
46
+ placement: PreparedTargetPlacement;
47
+ }> | Readonly<{
48
+ kind: "refused";
49
+ refusal: TargetPlacementRefusal;
50
+ }>;
51
+ export declare function observeTargetHead(repository: GitRepository, target: string): SnapshotId | null;
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;
56
+ export {};
@@ -0,0 +1,255 @@
1
+ import { resolve } from "node:path";
2
+ import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
3
+ import { gitObjectId, gitObjectIdForSnapshot, gitRefLocator, mintSnapshotId } from "./identity.js";
4
+ import { currentBranch } from "./observe.js";
5
+ import { commonGitDirectory, GitPlumbingError, readRef, registeredWorktrees, runGit, } from "./repository.js";
6
+ import { captureWorkspaceTree, withPrivateGitIndex } from "./workspace.js";
7
+ export function observeTargetHead(repository, target) {
8
+ const value = readRef(repository, target);
9
+ return value === null ? null : mintSnapshotId(value);
10
+ }
11
+ function diagnostic(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+ function nulPaths(bytes) {
15
+ const fields = bytes.toString("utf8").split("\0");
16
+ if (fields.at(-1) !== "")
17
+ throw new Error("Git path output is not NUL terminated");
18
+ return [...new Set(fields.slice(0, -1))].sort();
19
+ }
20
+ function gitPaths(repository, path, args) {
21
+ return nulPaths(runGit(repository, ["-C", path, ...args]));
22
+ }
23
+ function commitTree(repository, snapshot) {
24
+ return gitObjectId(runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)])
25
+ .toString("utf8")
26
+ .trim(), "commit tree");
27
+ }
28
+ function pathsOverlap(left, right) {
29
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
30
+ }
31
+ function intersection(left, right) {
32
+ return left.filter((candidate) => right.some((path) => pathsOverlap(candidate, path))).sort();
33
+ }
34
+ function failurePaths(error) {
35
+ if (!(error instanceof GitPlumbingError))
36
+ return [];
37
+ const paths = new Set();
38
+ for (const match of error.stderr.toString("utf8").matchAll(/(?:Entry|Untracked working tree file) '([^']+)'/gu)) {
39
+ paths.add(match[1]);
40
+ }
41
+ return [...paths].sort();
42
+ }
43
+ function checkoutRefusal(contractId, target, path, reason, error) {
44
+ const paths = failurePaths(error);
45
+ if (paths.length === 0)
46
+ throw error;
47
+ return { kind: "checkout-not-followable", contractId, target: target.target, path, reason, paths };
48
+ }
49
+ function indexMatchesTreeOnPaths(repository, path, tree, paths) {
50
+ return gitPaths(repository, path, ["diff-index", "--cached", "--name-only", "-z", tree, "--", ...paths]).length === 0;
51
+ }
52
+ function workspaceMatchesTreeOnPaths(repository, path, tree, workspaceTree, paths) {
53
+ return gitPaths(repository, path, ["diff", "--name-only", "-z", tree, workspaceTree, "--", ...paths]).length === 0;
54
+ }
55
+ function sourceWorktree(repository) {
56
+ return resolve(runGit(repository, ["rev-parse", "--show-toplevel"]).toString("utf8").trim());
57
+ }
58
+ function ordinaryPrecheck(repository, contractId, target, path) {
59
+ const predecessor = gitObjectIdForSnapshot(target.expectedOid);
60
+ const candidate = gitObjectIdForSnapshot(target.newOid);
61
+ try {
62
+ withPrivateGitIndex((environment) => {
63
+ runGit(repository, ["-C", path, "read-tree", "-i", `--index-output=${environment.GIT_INDEX_FILE}`, "-m", predecessor, candidate]);
64
+ });
65
+ }
66
+ catch (error) {
67
+ return checkoutRefusal(contractId, target, path, "staged", error);
68
+ }
69
+ const changed = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessor, candidate]);
70
+ const dirty = gitPaths(repository, path, ["diff-files", "--name-only", "-z"]);
71
+ const conflicts = intersection(changed, dirty);
72
+ if (conflicts.length > 0) {
73
+ return {
74
+ kind: "checkout-not-followable",
75
+ contractId,
76
+ target: target.target,
77
+ path,
78
+ reason: "conflict",
79
+ paths: conflicts,
80
+ };
81
+ }
82
+ const additions = gitPaths(repository, path, ["diff", "--name-only", "--diff-filter=A", "-z", predecessor, candidate]);
83
+ const untracked = gitPaths(repository, path, ["ls-files", "--others", "-z"]);
84
+ const collisions = intersection(additions, untracked);
85
+ if (collisions.length > 0) {
86
+ return {
87
+ kind: "checkout-not-followable",
88
+ contractId,
89
+ target: target.target,
90
+ path,
91
+ reason: "untracked",
92
+ paths: collisions,
93
+ };
94
+ }
95
+ try {
96
+ runGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
97
+ }
98
+ catch (error) {
99
+ const reason = error instanceof GitPlumbingError && /untracked working tree file/iu.test(error.stderr.toString("utf8"))
100
+ ? "untracked"
101
+ : "conflict";
102
+ return checkoutRefusal(contractId, target, path, reason, error);
103
+ }
104
+ return null;
105
+ }
106
+ export async function acquireTargetPlacementFence(repository, target) {
107
+ const locator = gitRefLocator(target);
108
+ return acquireSqliteTransactionLock({
109
+ path: resolve(commonGitDirectory(repository), "keiyaku", "locks", "target-placement", `${locator}.sqlite`),
110
+ mode: "immediate",
111
+ });
112
+ }
113
+ export function prepareTargetPlacement(repository, state, target) {
114
+ if (state.coordinates.target !== target.target || state.delivery?.data.integration.snapshot !== target.newOid) {
115
+ throw new Error("placement state does not match its offered target movement");
116
+ }
117
+ const worktrees = registeredWorktrees(repository)
118
+ .filter((worktree) => worktree.branch === target.target)
119
+ .sort((left, right) => left.path.localeCompare(right.path));
120
+ const hereSource = state.coordinates.workspace === "here" ? sourceWorktree(repository) : null;
121
+ if (hereSource !== null) {
122
+ const branch = currentBranch(repository, hereSource);
123
+ if (branch !== target.target) {
124
+ return {
125
+ kind: "refused",
126
+ refusal: {
127
+ kind: "workspace-not-on-target",
128
+ contractId: state.id,
129
+ target: target.target,
130
+ branch,
131
+ },
132
+ };
133
+ }
134
+ }
135
+ const arms = [];
136
+ for (const worktree of worktrees) {
137
+ const kind = hereSource !== null && resolve(worktree.path) === hereSource ? "here" : "ordinary";
138
+ if (kind === "ordinary") {
139
+ const refusal = ordinaryPrecheck(repository, state.id, target, worktree.path);
140
+ if (refusal !== null)
141
+ return { kind: "refused", refusal };
142
+ }
143
+ arms.push({ kind, path: worktree.path });
144
+ }
145
+ if (hereSource !== null && !arms.some((arm) => arm.kind === "here")) {
146
+ throw new Error("targeted here workspace is not a registered checkout of its target");
147
+ }
148
+ return { kind: "prepared", placement: { target, arms } };
149
+ }
150
+ export function followTargetPlacement(repository, prepared) {
151
+ const effects = [];
152
+ const lag = [];
153
+ const predecessor = gitObjectIdForSnapshot(prepared.target.expectedOid);
154
+ const candidate = gitObjectIdForSnapshot(prepared.target.newOid);
155
+ for (const arm of prepared.arms) {
156
+ try {
157
+ if (arm.kind === "here")
158
+ runGit(repository, ["-C", arm.path, "read-tree", candidate]);
159
+ else
160
+ runGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
161
+ effects.push({ kind: "target-checkout", path: arm.path, target: prepared.target.target, action: "followed" });
162
+ }
163
+ catch (error) {
164
+ lag.push({
165
+ kind: "target-checkout-retained",
166
+ path: arm.path,
167
+ target: prepared.target.target,
168
+ diagnostic: diagnostic(error),
169
+ });
170
+ }
171
+ }
172
+ return { effects, lag };
173
+ }
174
+ function recoveryLag(path, target, detail) {
175
+ return { kind: "target-checkout-retained", path, target, diagnostic: detail };
176
+ }
177
+ function recoverCheckout(input) {
178
+ const { repository, path, predecessor, candidate, predecessorTree, candidateTree } = input;
179
+ const changedPaths = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessorTree, candidateTree]);
180
+ if (changedPaths.length === 0)
181
+ return "complete";
182
+ const workspaceTree = captureWorkspaceTree(repository, path).tree;
183
+ const candidateIndex = indexMatchesTreeOnPaths(repository, path, candidateTree, changedPaths);
184
+ const candidateWorkspace = workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
185
+ if (candidateIndex && candidateWorkspace)
186
+ return "complete";
187
+ if (workspaceTree === candidateTree) {
188
+ runGit(repository, ["-C", path, "read-tree", gitObjectIdForSnapshot(candidate)]);
189
+ return "recovered";
190
+ }
191
+ const predecessorIndex = indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
192
+ if (predecessorIndex && candidateWorkspace) {
193
+ runGit(repository, [
194
+ "-C",
195
+ path,
196
+ "read-tree",
197
+ "-i",
198
+ "-m",
199
+ gitObjectIdForSnapshot(predecessor),
200
+ gitObjectIdForSnapshot(candidate),
201
+ ]);
202
+ return "recovered";
203
+ }
204
+ const predecessorWorkspace = workspaceMatchesTreeOnPaths(repository, path, predecessorTree, workspaceTree, changedPaths);
205
+ if (!predecessorIndex || !predecessorWorkspace)
206
+ return "retained";
207
+ runGit(repository, [
208
+ "-C",
209
+ path,
210
+ "read-tree",
211
+ "-m",
212
+ "-u",
213
+ gitObjectIdForSnapshot(predecessor),
214
+ gitObjectIdForSnapshot(candidate),
215
+ ]);
216
+ return "recovered";
217
+ }
218
+ export function recoverTargetPlacement(repository, state) {
219
+ const target = state.coordinates.target;
220
+ const delivery = state.delivery;
221
+ if (state.terminal?.kind !== "claimed" || target === undefined || delivery === null)
222
+ return { effects: [], lag: [] };
223
+ if (readRef(repository, target) !== delivery.data.integration.snapshot)
224
+ return { effects: [], lag: [] };
225
+ const candidateTree = commitTree(repository, delivery.data.integration.snapshot);
226
+ const predecessorTree = commitTree(repository, delivery.data.integration.predecessor);
227
+ const worktrees = registeredWorktrees(repository)
228
+ .filter((worktree) => worktree.branch === target)
229
+ .sort((left, right) => left.path.localeCompare(right.path));
230
+ const effects = [];
231
+ const lag = [];
232
+ for (const worktree of worktrees) {
233
+ try {
234
+ const recovery = recoverCheckout({
235
+ repository,
236
+ path: worktree.path,
237
+ predecessor: delivery.data.integration.predecessor,
238
+ candidate: delivery.data.integration.snapshot,
239
+ predecessorTree,
240
+ candidateTree,
241
+ });
242
+ if (recovery === "retained") {
243
+ lag.push(recoveryLag(worktree.path, target, "target checkout entries are neither predecessor nor candidate"));
244
+ continue;
245
+ }
246
+ if (recovery === "recovered") {
247
+ effects.push({ kind: "target-checkout", path: worktree.path, target, action: "recovered" });
248
+ }
249
+ }
250
+ catch (error) {
251
+ lag.push(recoveryLag(worktree.path, target, diagnostic(error)));
252
+ }
253
+ }
254
+ return { effects, lag };
255
+ }