@astrosheep/keiyaku 4.6.3 → 4.6.6

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 (193) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +47 -11
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/references/automation.md +2 -2
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +8 -9
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +37 -25
  6. package/build/src/akuma/akuma-observe.d.ts +18 -15
  7. package/build/src/akuma/akuma-observe.js +75 -16
  8. package/build/src/akuma/akuma-wait.d.ts +4 -3
  9. package/build/src/akuma/akuma-wait.js +75 -53
  10. package/build/src/akuma/akuma.d.ts +6 -4
  11. package/build/src/akuma/akuma.js +4 -0
  12. package/build/src/akuma/call-initial-tell.d.ts +2 -2
  13. package/build/src/akuma/call-initial-tell.js +1 -1
  14. package/build/src/akuma/call-request.d.ts +2 -2
  15. package/build/src/akuma/heart/index.d.ts +7 -2
  16. package/build/src/akuma/heart/index.js +50 -5
  17. package/build/src/akuma/heart/tells.d.ts +33 -2
  18. package/build/src/akuma/heart/tells.js +24 -18
  19. package/build/src/akuma/heart/timeline.d.ts +23 -1
  20. package/build/src/akuma/heart/timeline.js +28 -21
  21. package/build/src/akuma/projection.d.ts +10 -10
  22. package/build/src/akuma/providers/pi/index.js +72 -39
  23. package/build/src/akuma/selection-observation.d.ts +20 -20
  24. package/build/src/akuma-body.js +1 -0
  25. package/build/src/alias/index.d.ts +0 -1
  26. package/build/src/alias/index.js +0 -4
  27. package/build/src/body/amend.d.ts +0 -4
  28. package/build/src/body/amend.js +0 -3
  29. package/build/src/body/region.d.ts +0 -1
  30. package/build/src/body/region.js +0 -3
  31. package/build/src/body/verification.d.ts +0 -1
  32. package/build/src/cli/commands/akuma-grammar.js +23 -66
  33. package/build/src/cli/commands/akuma.js +17 -30
  34. package/build/src/cli/commands/contract-grammar.d.ts +14 -1
  35. package/build/src/cli/commands/contract-grammar.js +54 -21
  36. package/build/src/cli/commands/contract-help.d.ts +9 -8
  37. package/build/src/cli/commands/contract-help.js +14 -7
  38. package/build/src/cli/commands/contract.js +57 -117
  39. package/build/src/cli/commands/install.d.ts +5 -5
  40. package/build/src/cli/commands/install.js +1 -4
  41. package/build/src/cli/commands/task-grammar.js +10 -66
  42. package/build/src/cli/commands/task.d.ts +0 -1
  43. package/build/src/cli/commands/task.js +34 -53
  44. package/build/src/cli/coordinates.d.ts +1 -2
  45. package/build/src/cli/coordinates.js +1 -4
  46. package/build/src/cli/parse.d.ts +2 -3
  47. package/build/src/cli/parse.js +24 -66
  48. package/build/src/cli/render/akuma.d.ts +3 -16
  49. package/build/src/cli/render/akuma.js +1 -13
  50. package/build/src/cli/render/audit.js +1 -1
  51. package/build/src/cli/render/contract.js +34 -32
  52. package/build/src/cli/render/kanshi-akuma.d.ts +8 -2
  53. package/build/src/cli/render/kanshi-akuma.js +14 -7
  54. package/build/src/cli/render/kanshi.d.ts +3 -1
  55. package/build/src/cli/render/kanshi.js +44 -32
  56. package/build/src/cli/render/receipt.js +6 -1
  57. package/build/src/cli/render/task.d.ts +1 -1
  58. package/build/src/cli/render/task.js +3 -27
  59. package/build/src/cli/render/terminal.d.ts +6 -2
  60. package/build/src/cli/render/terminal.js +8 -2
  61. package/build/src/cli/runtime.js +3 -16
  62. package/build/src/cli/scan.d.ts +26 -0
  63. package/build/src/cli/scan.js +82 -0
  64. package/build/src/cli/streams.d.ts +5 -0
  65. package/build/src/cli/streams.js +11 -0
  66. package/build/src/contract-worktree.js +38 -21
  67. package/build/src/core/facts/codec.d.ts +1 -2
  68. package/build/src/core/facts/codec.js +5 -11
  69. package/build/src/core/facts/types.d.ts +16 -2
  70. package/build/src/core/facts/types.js +15 -0
  71. package/build/src/core/verbs/abandon.js +3 -9
  72. package/build/src/core/verbs/amend.js +2 -9
  73. package/build/src/core/verbs/arc.js +6 -13
  74. package/build/src/core/verbs/attestation.js +2 -9
  75. package/build/src/core/verbs/bind.js +2 -9
  76. package/build/src/core/verbs/deliver.js +2 -9
  77. package/build/src/core/verbs/placement.d.ts +1 -1
  78. package/build/src/core/verbs/placement.js +2 -9
  79. package/build/src/core/verbs/reintegrate.d.ts +1 -1
  80. package/build/src/core/verbs/reintegrate.js +2 -9
  81. package/build/src/dispatch/index.d.ts +2 -2
  82. package/build/src/dispatch/index.js +33 -11
  83. package/build/src/git/hooks.d.ts +2 -4
  84. package/build/src/git/hooks.js +0 -12
  85. package/build/src/git/integration.d.ts +5 -3
  86. package/build/src/git/integration.js +36 -25
  87. package/build/src/git/nuke.js +4 -4
  88. package/build/src/git/observe.d.ts +4 -4
  89. package/build/src/git/observe.js +5 -5
  90. package/build/src/git/process.d.ts +1 -4
  91. package/build/src/git/process.js +1 -11
  92. package/build/src/git/read-observation.js +1 -1
  93. package/build/src/git/reconcile.d.ts +1 -1
  94. package/build/src/git/reconcile.js +15 -13
  95. package/build/src/git/repository.d.ts +2 -6
  96. package/build/src/git/repository.js +18 -31
  97. package/build/src/git/scratch.d.ts +6 -6
  98. package/build/src/git/scratch.js +110 -46
  99. package/build/src/git/target-placement.d.ts +5 -5
  100. package/build/src/git/target-placement.js +18 -33
  101. package/build/src/git/tender.d.ts +6 -2
  102. package/build/src/git/tender.js +19 -12
  103. package/build/src/git/terminal-reconcile.js +24 -13
  104. package/build/src/git/terminal-seal.d.ts +2 -5
  105. package/build/src/git/terminal-seal.js +5 -3
  106. package/build/src/git/workspace.d.ts +17 -13
  107. package/build/src/git/workspace.js +58 -41
  108. package/build/src/kanshi/index.d.ts +2 -2
  109. package/build/src/kanshi/index.js +1 -1
  110. package/build/src/kanshi/read.d.ts +8 -1
  111. package/build/src/kanshi/read.js +72 -7
  112. package/build/src/kanshi/report.d.ts +12 -0
  113. package/build/src/library/akuma-creation.d.ts +4 -4
  114. package/build/src/library/bind.d.ts +1 -63
  115. package/build/src/library/bind.js +3 -9
  116. package/build/src/library/contract-creation.js +2 -30
  117. package/build/src/library/contract-execution.d.ts +0 -1
  118. package/build/src/library/contract-execution.js +9 -34
  119. package/build/src/library/contract-operations.d.ts +5 -1
  120. package/build/src/library/contract-operations.js +7 -3
  121. package/build/src/library/contract-types.d.ts +1 -1
  122. package/build/src/library/delivery.d.ts +91 -93
  123. package/build/src/library/delivery.js +2 -1
  124. package/build/src/library/input.d.ts +0 -2
  125. package/build/src/library/input.js +0 -4
  126. package/build/src/library/invocation.d.ts +0 -1
  127. package/build/src/library/invocation.js +3 -25
  128. package/build/src/library/outcome.d.ts +651 -602
  129. package/build/src/library/outcome.js +4 -4
  130. package/build/src/library/reconcile.d.ts +6 -0
  131. package/build/src/library/reconcile.js +31 -1
  132. package/build/src/library/repo.d.ts +0 -1
  133. package/build/src/library/selection.d.ts +1 -10
  134. package/build/src/library/selection.js +14 -15
  135. package/build/src/protocol/abandon.d.ts +1 -1
  136. package/build/src/protocol/abandon.js +3 -10
  137. package/build/src/protocol/amend.js +2 -5
  138. package/build/src/protocol/arc.d.ts +1 -1
  139. package/build/src/protocol/arc.js +2 -9
  140. package/build/src/protocol/attempt.d.ts +3 -20
  141. package/build/src/protocol/attempt.js +6 -5
  142. package/build/src/protocol/audit.d.ts +1 -0
  143. package/build/src/protocol/audit.js +10 -11
  144. package/build/src/protocol/completion.d.ts +129 -55
  145. package/build/src/protocol/completion.js +62 -73
  146. package/build/src/protocol/deliver.d.ts +7 -5
  147. package/build/src/protocol/deliver.js +74 -44
  148. package/build/src/protocol/execution-observation.d.ts +2 -4
  149. package/build/src/protocol/execution-observation.js +3 -7
  150. package/build/src/protocol/intent.d.ts +1 -1
  151. package/build/src/protocol/intent.js +1 -0
  152. package/build/src/protocol/operations.d.ts +24 -12
  153. package/build/src/protocol/operations.js +36 -15
  154. package/build/src/protocol/placement.d.ts +2 -1
  155. package/build/src/protocol/placement.js +21 -5
  156. package/build/src/protocol/progress.d.ts +1 -1
  157. package/build/src/protocol/read/status.js +58 -41
  158. package/build/src/protocol/reintegrate.d.ts +1 -1
  159. package/build/src/protocol/reintegrate.js +10 -19
  160. package/build/src/protocol/review.d.ts +3 -28
  161. package/build/src/protocol/review.js +10 -37
  162. package/build/src/protocol/run.d.ts +19 -7
  163. package/build/src/protocol/run.js +19 -9
  164. package/build/src/runtime/proc/windows-launch.exe +0 -0
  165. package/build/src/settlement/holder.d.ts +2 -4
  166. package/build/src/settlement/holder.js +3 -6
  167. package/build/src/task/board.d.ts +6 -0
  168. package/build/src/task/board.js +30 -1
  169. package/build/src/task/catalog.js +1 -4
  170. package/build/src/task/compose.d.ts +0 -1
  171. package/build/src/task/compose.js +0 -1
  172. package/build/src/task/context.d.ts +0 -1
  173. package/build/src/task/context.js +0 -3
  174. package/build/src/task/created-observation.d.ts +5 -1
  175. package/build/src/task/created-observation.js +12 -5
  176. package/build/src/task/index.d.ts +1 -1
  177. package/build/src/task/index.js +7 -6
  178. package/build/src/task/input.d.ts +0 -6
  179. package/build/src/task/input.js +2 -14
  180. package/build/src/task/mutation.d.ts +3 -4
  181. package/build/src/task/mutation.js +11 -39
  182. package/build/src/task/operations.d.ts +2 -7
  183. package/build/src/task/operations.js +37 -49
  184. package/build/src/task/query.d.ts +0 -1
  185. package/build/src/task/query.js +1 -4
  186. package/build/src/task/store.d.ts +0 -2
  187. package/build/src/task/store.js +27 -11
  188. package/build/src/verification/declaration.js +3 -2
  189. package/build/src/verification/execution.d.ts +3 -2
  190. package/build/src/verification/execution.js +10 -6
  191. package/build/src/workspace-place.d.ts +0 -1
  192. package/build/src/workspace-place.js +0 -11
  193. package/package.json +7 -3
@@ -2,7 +2,7 @@ import { isAbsolute, resolve } from "node:path";
2
2
  import { realpath } from "node:fs/promises";
3
3
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
4
4
  import { gitObjectId } from "./identity.js";
5
- import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
5
+ import { executeGit, GitPlumbingError } from "./process.js";
6
6
  import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
7
7
  export const GIT_REF = "refs/heads/keiyaku-state";
8
8
  export const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
@@ -11,7 +11,7 @@ export const HANDOFF_RECEIPT_REF_NAMESPACE = "refs/keiyaku/handoff";
11
11
  export const GIT_FORMAT_PATH = "meta/format.json";
12
12
  const CURRENT_FORMAT_VERSION = 4;
13
13
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
14
- export const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
14
+ const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
15
15
  export function isKeiyakuOwnedRef(ref) {
16
16
  return (ref === GIT_REF ||
17
17
  ref === DELIVERY_REF_NAMESPACE ||
@@ -67,7 +67,7 @@ function worktreesFromPorcelain(output) {
67
67
  return worktrees;
68
68
  }
69
69
  export async function registeredWorktrees(repository) {
70
- return await Promise.all(worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"])).map(async (worktree) => {
70
+ return await Promise.all(worktreesFromPorcelain(await executeGit(repository, ["worktree", "list", "--porcelain", "-z"])).map(async (worktree) => {
71
71
  const { prunable, ...registered } = worktree;
72
72
  try {
73
73
  return { ...registered, path: await realpath(registered.path) };
@@ -111,7 +111,9 @@ export async function repositoryAt(cwd, gitPath = "git") {
111
111
  return { gitPath, effectiveCwd, invocationWorktree, primaryWorktree, commonDirectory };
112
112
  }
113
113
  async function absoluteGitPath(repository, args, label) {
114
- const value = (await runGit(repository, ["rev-parse", "--path-format=absolute", ...args])).toString("utf8").trim();
114
+ const value = (await executeGit(repository, ["rev-parse", "--path-format=absolute", ...args]))
115
+ .toString("utf8")
116
+ .trim();
115
117
  if (value.length === 0)
116
118
  throw new Error(`${label} is empty`);
117
119
  return await realpath(resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value)));
@@ -123,7 +125,7 @@ export async function worktreeGitDirectory(repository, worktree) {
123
125
  const scoped = { ...repository, effectiveCwd: worktree };
124
126
  return await absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
125
127
  }
126
- export async function worktreeRoot(repository) {
128
+ async function worktreeRoot(repository) {
127
129
  return await absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
128
130
  }
129
131
  function assertRef(ref) {
@@ -138,7 +140,7 @@ function assertAssertionRef(ref) {
138
140
  export async function readRef(repository, ref) {
139
141
  assertAssertionRef(ref);
140
142
  try {
141
- const oid = (await runGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
143
+ const oid = (await executeGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
142
144
  return oid.length === 0 ? null : gitObjectId(oid, `ref ${ref}`);
143
145
  }
144
146
  catch (error) {
@@ -154,7 +156,7 @@ export async function readRefs(repository, refs) {
154
156
  assertAssertionRef(ref);
155
157
  if (refsToRead.length === 0)
156
158
  return new Map();
157
- const output = (await runGit(repository, ["cat-file", "--batch-check=%(objectname)"], `${refsToRead.join("\n")}\n`)).toString("utf8");
159
+ const output = (await executeGit(repository, ["cat-file", "--batch-check=%(objectname)"], `${refsToRead.join("\n")}\n`)).toString("utf8");
158
160
  const records = output.split("\n");
159
161
  if (records.pop() !== "" || records.length !== refsToRead.length)
160
162
  throw new Error("Git ref batch output is malformed");
@@ -168,7 +170,7 @@ export async function readRefs(repository, refs) {
168
170
  }
169
171
  async function readTreeForCommit(repository, commit) {
170
172
  gitObjectId(commit, "Git commit");
171
- const tree = (await runGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
173
+ const tree = (await executeGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
172
174
  gitObjectId(tree, "Git tree");
173
175
  return tree;
174
176
  }
@@ -190,21 +192,6 @@ function parseTreeEntries(output) {
190
192
  }
191
193
  return entries;
192
194
  }
193
- export async function readTreeEntries(repository, tree) {
194
- gitObjectId(tree, "tree");
195
- const output = await runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
196
- return parseTreeEntries(output);
197
- }
198
- export async function readGit(repository) {
199
- const commit = await readRef(repository, GIT_REF);
200
- if (commit === null) {
201
- return { commit: null, tree: null, paths: new Map() };
202
- }
203
- const tree = await readTreeForCommit(repository, commit);
204
- const paths = await readTreeEntries(repository, tree);
205
- await validateGitFormat(repository, paths);
206
- return { commit, tree, paths };
207
- }
208
195
  /** Read only the requested private Git paths from one immutable Git tree. */
209
196
  export async function readGitPaths(repository, requestedPaths) {
210
197
  for (const path of requestedPaths)
@@ -213,18 +200,18 @@ export async function readGitPaths(repository, requestedPaths) {
213
200
  if (commit === null)
214
201
  return { commit: null, tree: null, paths: new Map() };
215
202
  const tree = await readTreeForCommit(repository, commit);
216
- const paths = parseTreeEntries(await runGit(repository, ["ls-tree", "-z", "--full-tree", tree, "--", GIT_FORMAT_PATH, ...requestedPaths]));
203
+ const paths = parseTreeEntries(await executeGit(repository, ["ls-tree", "-z", "--full-tree", tree, "--", GIT_FORMAT_PATH, ...requestedPaths]));
217
204
  await validateGitFormat(repository, paths);
218
205
  return { commit, tree, paths };
219
206
  }
220
207
  export async function writeBlob(repository, bytes) {
221
- const oid = (await runGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
208
+ const oid = (await executeGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
222
209
  gitObjectId(oid, "written blob");
223
210
  return oid;
224
211
  }
225
212
  export async function readBlob(repository, oid) {
226
213
  gitObjectId(oid, "blob");
227
- return await runGit(repository, ["cat-file", "blob", oid]);
214
+ return await executeGit(repository, ["cat-file", "blob", oid]);
228
215
  }
229
216
  function malformedBatchOutput(detail) {
230
217
  throw new GitPlumbingError({
@@ -239,7 +226,7 @@ async function readObjects(repository, oids) {
239
226
  gitObjectId(oid, "Git object");
240
227
  if (unique.length === 0)
241
228
  return new Map();
242
- const output = await runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
229
+ const output = await executeGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
243
230
  const objects = new Map();
244
231
  let offset = 0;
245
232
  for (const oid of unique) {
@@ -290,7 +277,7 @@ async function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
290
277
  const byPath = new Map();
291
278
  if (baseTree === null)
292
279
  return byPath;
293
- const ancestry = parseTreeEntries(await runGit(repository, [
280
+ const ancestry = parseTreeEntries(await executeGit(repository, [
294
281
  "ls-tree",
295
282
  "-z",
296
283
  "-t",
@@ -323,7 +310,7 @@ async function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
323
310
  return byPath;
324
311
  }
325
312
  async function writePreparedTrees(repository, prepared) {
326
- const output = await runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
313
+ const output = await executeGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
327
314
  const written = output.toString("ascii").trimEnd().split("\n");
328
315
  if (written.length !== prepared.length) {
329
316
  throw new GitPlumbingError({ stderr: output, status: null, message: "malformed mktree --batch output" });
@@ -364,7 +351,7 @@ async function writeCommitObject(input) {
364
351
  const args = ["commit-tree", tree];
365
352
  for (const parent of parents)
366
353
  args.push("-p", parent);
367
- const commit = (await runGitWithEnvironment(repository, args, `${message}\n`, {
354
+ const commit = (await executeGit(repository, args, `${message}\n`, {
368
355
  GIT_AUTHOR_NAME: actor,
369
356
  GIT_AUTHOR_EMAIL: email,
370
357
  GIT_COMMITTER_NAME: actor,
@@ -420,7 +407,7 @@ export async function updateRefsAtomically(repository, updates, assertions = [])
420
407
  }
421
408
  const lines = ["start", ...assertions.map(refAssertionLine), ...updates.map(refUpdateLine), "prepare", "commit", ""];
422
409
  try {
423
- await runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
410
+ await executeGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
424
411
  return { kind: "published" };
425
412
  }
426
413
  catch (error) {
@@ -1,23 +1,23 @@
1
1
  import { z } from "zod";
2
- import type { SnapshotId } from "../core/facts/types.js";
2
+ import type { ContractId, SnapshotId } from "../core/facts/types.js";
3
3
  import { type GitRepository } from "./process.js";
4
4
  export type MaterializedScratchCandidate = Readonly<{
5
5
  cwd: string;
6
- dispose: () => Promise<WorktreeLeak | null>;
6
+ dispose: (retain?: boolean) => Promise<WorktreeLeak | null>;
7
7
  }>;
8
8
  export declare const worktreeLeakSchema: z.ZodObject<{
9
- path: z.ZodString;
9
+ path: z.ZodOptional<z.ZodString>;
10
10
  diagnostic: z.ZodString;
11
11
  }, z.core.$strict>;
12
12
  export type WorktreeLeak = z.infer<typeof worktreeLeakSchema>;
13
13
  type CollectableScratchRemoval = Readonly<{
14
14
  path: string;
15
- action: "removed" | "unchanged";
15
+ /** False only when Git removed the path and absence was confirmed; retained removals own a diagnostic. */
16
16
  retained: boolean;
17
17
  diagnostic?: string;
18
18
  }>;
19
19
  /** Remove only abandoned Verification scratch worktrees whose ownership locks are held. */
20
- export declare function removeCollectableScratchWorktrees(repository: GitRepository, paths: Set<string>): Promise<readonly CollectableScratchRemoval[]>;
20
+ export declare function removeCollectableScratchWorktrees(repository: GitRepository, paths: Set<string>, terminalContract?: ContractId): Promise<readonly CollectableScratchRemoval[]>;
21
21
  /** Materialize a disposable candidate worktree and own only its removal. */
22
- export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): Promise<MaterializedScratchCandidate>;
22
+ export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId, contract?: ContractId): Promise<MaterializedScratchCandidate>;
23
23
  export {};
@@ -1,18 +1,23 @@
1
1
  import { z } from "zod";
2
2
  import { randomBytes } from "node:crypto";
3
- import { access, realpath, unlink } from "node:fs/promises";
3
+ import { lstat, realpath, unlink } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
- import { tryAcquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
7
- import { gitObjectIdForSnapshot } from "./identity.js";
8
- import { runGit } from "./process.js";
6
+ import { acquireSqliteTransactionLock, tryAcquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
7
+ import { contractLocator, gitRefLocator, gitObjectIdForSnapshot } from "./identity.js";
8
+ import { executeGit } from "./process.js";
9
9
  const SCRATCH_PREFIX = "keiyaku-v4-verify-";
10
10
  const SCRATCH_ROOT = await realpath(tmpdir());
11
- const SCRATCH_PATTERN = /^keiyaku-v4-verify-([0-9a-f]{24})$/;
12
- export const worktreeLeakSchema = z.object({ path: z.string(), diagnostic: z.string() }).strict();
11
+ const SCRATCH_PATTERN = /^keiyaku-v4-verify-([0-9a-f]{24}|[0-9a-f]{64}-[0-9a-f]{64})$/;
12
+ /** One bounded removal attempt per scratch; expiry is reported, never retried inside this call. */
13
+ const SCRATCH_REMOVAL_TIMEOUT_MS = 120_000;
14
+ export const worktreeLeakSchema = z.object({ path: z.string().optional(), diagnostic: z.string() }).strict();
13
15
  function scratchPath() {
14
16
  return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${randomBytes(12).toString("hex")}`);
15
17
  }
18
+ function retainedScratchPath(repository, contract) {
19
+ return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${gitRefLocator(repository.commonDirectory)}-${contractLocator(contract)}`);
20
+ }
16
21
  function ownershipLockPath(path) {
17
22
  if (dirname(path) !== SCRATCH_ROOT)
18
23
  return null;
@@ -25,69 +30,132 @@ async function removeOwnershipLock(lockPath) {
25
30
  throw error;
26
31
  });
27
32
  }
28
- async function collectableScratchWorktrees(paths) {
33
+ async function collectableScratchWorktrees(paths, terminalScratch) {
29
34
  const collectable = [];
30
35
  for (const path of paths) {
31
36
  const lockPath = ownershipLockPath(path);
32
37
  if (lockPath === null)
33
38
  continue;
39
+ if (basename(path).length !== SCRATCH_PREFIX.length + 24 && path !== terminalScratch)
40
+ continue;
34
41
  const lock = await tryAcquireSqliteTransactionLock({ path: lockPath, mode: "exclusive" });
35
42
  if (lock !== null)
36
43
  collectable.push({ path, lockPath, release: () => lock.close() });
37
44
  }
38
45
  return collectable;
39
46
  }
40
- const pathExists = (path) => access(path).then(() => true, () => false);
47
+ /** Absence is ENOENT on the path entry itself: a dangling symlink is present residue, not a missing path. */
48
+ async function pathExists(path) {
49
+ try {
50
+ await lstat(path);
51
+ return true;
52
+ }
53
+ catch (error) {
54
+ if (error.code === "ENOENT")
55
+ return false;
56
+ throw error;
57
+ }
58
+ }
59
+ async function removeScratch(repository, path) {
60
+ const removal = AbortSignal.timeout(SCRATCH_REMOVAL_TIMEOUT_MS);
61
+ try {
62
+ const present = await pathExists(path);
63
+ await executeGit({ ...repository, signal: removal }, ["worktree", "remove", ...(present ? ["--force"] : []), path]);
64
+ return (await pathExists(path))
65
+ ? { path, diagnostic: `Git reported scratch worktree removal but the path remains: ${path}` }
66
+ : null;
67
+ }
68
+ catch (error) {
69
+ const original = error instanceof Error ? error.message : String(error);
70
+ return {
71
+ path,
72
+ diagnostic: removal.aborted
73
+ ? `scratch worktree removal timed out after ${SCRATCH_REMOVAL_TIMEOUT_MS}ms: ${original}`
74
+ : original,
75
+ };
76
+ }
77
+ }
41
78
  /** Remove only abandoned Verification scratch worktrees whose ownership locks are held. */
42
- export async function removeCollectableScratchWorktrees(repository, paths) {
79
+ export async function removeCollectableScratchWorktrees(repository, paths, terminalContract) {
43
80
  const removals = [];
44
- for (const scratch of await collectableScratchWorktrees(paths)) {
81
+ const terminalScratch = terminalContract === undefined ? undefined : retainedScratchPath(repository, terminalContract);
82
+ for (const scratch of await collectableScratchWorktrees(paths, terminalScratch)) {
45
83
  let removed = false;
46
84
  try {
47
- if (!(await pathExists(scratch.path))) {
48
- await runGit(repository, ["worktree", "remove", scratch.path]);
49
- paths.delete(scratch.path);
50
- removed = true;
51
- removals.push({ path: scratch.path, action: "removed", retained: false });
85
+ const leak = await removeScratch(repository, scratch.path);
86
+ if (leak !== null) {
87
+ removals.push({ path: scratch.path, retained: true, diagnostic: leak.diagnostic });
52
88
  continue;
53
89
  }
54
- try {
55
- await runGit(repository, ["worktree", "remove", "--force", scratch.path]);
56
- paths.delete(scratch.path);
57
- removed = true;
58
- removals.push({ path: scratch.path, action: "removed", retained: false });
59
- }
60
- catch (error) {
61
- removals.push({
62
- path: scratch.path,
63
- action: "unchanged",
64
- retained: true,
65
- diagnostic: error instanceof Error ? error.message : String(error),
66
- });
67
- }
90
+ paths.delete(scratch.path);
91
+ removed = true;
92
+ removals.push({ path: scratch.path, retained: false });
68
93
  }
69
94
  finally {
70
95
  scratch.release();
71
- if (removed)
96
+ if (removed && scratch.path !== terminalScratch)
72
97
  await removeOwnershipLock(scratch.lockPath);
73
98
  }
74
99
  }
75
100
  return removals;
76
101
  }
102
+ async function retainScratch(repository, cwd, contract) {
103
+ const retained = retainedScratchPath(repository, contract);
104
+ const lockPath = ownershipLockPath(retained);
105
+ if (lockPath === null)
106
+ throw new Error("scratch ownership lock path is invalid");
107
+ const { signal: _signal, ...retirementRepository } = repository;
108
+ const fallback = async (reason) => {
109
+ const ownLeak = await removeScratch(retirementRepository, cwd);
110
+ return {
111
+ ...(ownLeak === null ? {} : { path: ownLeak.path }),
112
+ diagnostic: `failed Verification scratch could not be retained: ${reason}${ownLeak === null ? "" : `; ${ownLeak.diagnostic}`}`,
113
+ };
114
+ };
115
+ let lock;
116
+ try {
117
+ lock = await acquireSqliteTransactionLock({
118
+ path: lockPath,
119
+ mode: "exclusive",
120
+ timeoutMs: SCRATCH_REMOVAL_TIMEOUT_MS,
121
+ });
122
+ }
123
+ catch (error) {
124
+ return await fallback(`retained scratch busy: ${error instanceof Error ? error.message : String(error)}`);
125
+ }
126
+ try {
127
+ const replaced = await pathExists(retained);
128
+ if (replaced) {
129
+ const leak = await removeScratch(retirementRepository, retained);
130
+ if (leak !== null)
131
+ return await fallback(leak.diagnostic);
132
+ }
133
+ await executeGit(retirementRepository, ["worktree", "move", cwd, retained]);
134
+ return {
135
+ path: retained,
136
+ diagnostic: `failed Verification scratch retained for diagnosis${replaced ? " (replaced the previous retained scratch)" : ""}`,
137
+ };
138
+ }
139
+ catch (error) {
140
+ return await fallback(error instanceof Error ? error.message : String(error));
141
+ }
142
+ finally {
143
+ lock.close();
144
+ }
145
+ }
77
146
  /** Materialize a disposable candidate worktree and own only its removal. */
78
- export async function materializeScratchCandidate(repository, candidate) {
147
+ export async function materializeScratchCandidate(repository, candidate, contract) {
79
148
  const cwd = scratchPath();
80
149
  const lockPath = ownershipLockPath(cwd);
81
150
  if (lockPath === null)
82
151
  throw new Error("scratch ownership lock path is invalid");
83
- const ownership = await tryAcquireSqliteTransactionLock({
152
+ const ownership = await acquireSqliteTransactionLock({
84
153
  path: lockPath,
85
154
  mode: "exclusive",
155
+ ...(repository.signal === undefined ? {} : { signal: repository.signal }),
86
156
  });
87
- if (ownership === null)
88
- throw new Error("new scratch ownership lock is unexpectedly held");
89
157
  try {
90
- await runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
158
+ await executeGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
91
159
  }
92
160
  catch (error) {
93
161
  ownership.close();
@@ -97,26 +165,22 @@ export async function materializeScratchCandidate(repository, candidate) {
97
165
  let disposed = false;
98
166
  return {
99
167
  cwd,
100
- dispose: async () => {
168
+ dispose: async (retain = false) => {
101
169
  if (disposed)
102
170
  return null;
103
171
  disposed = true;
104
- let leak = null;
105
172
  try {
106
- await runGit({ ...repository, signal: AbortSignal.timeout(5_000) }, ["worktree", "remove", "--force", cwd]);
107
- }
108
- catch (error) {
109
- leak = {
110
- path: cwd,
111
- diagnostic: error instanceof Error ? error.message : String(error),
112
- };
173
+ if (!retain)
174
+ return await removeScratch(repository, cwd);
175
+ if (contract === undefined)
176
+ return { path: cwd, diagnostic: "failed Verification scratch retained for diagnosis" };
177
+ return await retainScratch(repository, cwd, contract);
113
178
  }
114
179
  finally {
115
180
  ownership.close();
116
- if (leak === null)
181
+ if (!(await pathExists(cwd)))
117
182
  await removeOwnershipLock(lockPath);
118
183
  }
119
- return leak;
120
184
  },
121
185
  };
122
186
  }
@@ -30,7 +30,7 @@ export declare const targetCheckoutLagSchema: z.ZodObject<{
30
30
  diagnostic: z.ZodString;
31
31
  }, z.core.$strict>;
32
32
  type TargetCheckoutLag = z.infer<typeof targetCheckoutLagSchema>;
33
- export type TargetPlacementPhysicalResult = Readonly<{
33
+ type TargetPlacementPhysicalResult = Readonly<{
34
34
  effects: readonly TargetCheckoutEffect[];
35
35
  lag: readonly TargetCheckoutLag[];
36
36
  }>;
@@ -42,7 +42,7 @@ export type PreparedTargetPlacement = Readonly<{
42
42
  target: RefOperation;
43
43
  arms: readonly FollowArm[];
44
44
  }>;
45
- export type TargetPlacementPreparation = Readonly<{
45
+ type TargetPlacementPreparation = Readonly<{
46
46
  kind: "prepared";
47
47
  placement: PreparedTargetPlacement;
48
48
  }> | Readonly<{
@@ -63,16 +63,16 @@ export type TargetPlacementChanges = Readonly<{
63
63
  /** Only immutable commit contents are prepared outside the publication seat. */
64
64
  export declare function prepareTargetPlacementChanges(repository: GitRepository, target: RefOperation): Promise<TargetPlacementChanges>;
65
65
  export declare function acquireTargetPlacementFence(repository: GitRepository, target: string): Promise<HeldSqliteTransactionLock>;
66
- export type TargetedContractCoordinates = ContractState["coordinates"] & Readonly<{
66
+ type TargetedContractCoordinates = ContractState["coordinates"] & Readonly<{
67
67
  target: string;
68
68
  }>;
69
- export type TargetPlacementObservationInput = Readonly<{
69
+ type TargetPlacementObservationInput = Readonly<{
70
70
  contractId: ContractId;
71
71
  coordinates: TargetedContractCoordinates;
72
72
  predecessor: SnapshotId;
73
73
  candidate: SnapshotId;
74
74
  }>;
75
- export type TargetPlacementObservation = Readonly<{
75
+ type TargetPlacementObservation = Readonly<{
76
76
  kind: "ready";
77
77
  arms: readonly FollowArm[];
78
78
  }> | Readonly<{
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
6
6
  import { acquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
7
7
  import { gitObjectId, gitObjectIdForSnapshot, gitRefLocator, mintSnapshotId } from "./identity.js";
8
8
  import { commonGitDirectory, decodeGitNameOnly, readRef, registeredWorktrees } from "./repository.js";
9
- import { consumeGitStdout, GitPlumbingError, runGit } from "./process.js";
9
+ import { consumeGitStdout, GitPlumbingError, executeGit } from "./process.js";
10
10
  import { captureWorkspaceTree } from "./workspace.js";
11
11
  export const checkoutNotFollowableRefusalSchema = z
12
12
  .object({
@@ -42,13 +42,15 @@ function diagnostic(error) {
42
42
  return error instanceof Error ? error.message : String(error);
43
43
  }
44
44
  async function gitPaths(repository, path, args) {
45
- return [...new Set(decodeGitNameOnly(await runGit(repository, ["-C", path, ...args])))].sort();
45
+ return [...new Set(decodeGitNameOnly(await executeGit(repository, ["-C", path, ...args])))].sort();
46
46
  }
47
47
  function literalPath(path) {
48
48
  return `:(literal)${path}`;
49
49
  }
50
50
  async function commitTree(repository, snapshot) {
51
- return gitObjectId((await runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)])).toString("utf8").trim(), "commit tree");
51
+ return gitObjectId((await executeGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)]))
52
+ .toString("utf8")
53
+ .trim(), "commit tree");
52
54
  }
53
55
  export async function observedTreeEqualsCandidate(repository, observed, snapshot) {
54
56
  if (observed === null)
@@ -60,7 +62,7 @@ function checkoutRefusal(contractId, target, path, reason, paths) {
60
62
  }
61
63
  /** Only immutable commit contents are prepared outside the publication seat. */
62
64
  export async function prepareTargetPlacementChanges(repository, target) {
63
- const records = (await runGit(repository, [
65
+ const records = (await executeGit(repository, [
64
66
  "diff",
65
67
  "--raw",
66
68
  "--no-renames",
@@ -175,27 +177,10 @@ async function untrackedRefusalWithinScopes(input, scopes) {
175
177
  const first = directories.find((scope) => found.has(scope.path));
176
178
  return first === undefined ? null : checkoutRefusal(contractId, target, path, "untracked", [first.path]);
177
179
  }
178
- async function indexMatchesTreeOnPaths(repository, path, tree, paths) {
179
- return ((await gitPaths(repository, path, [
180
- "diff-index",
181
- "--cached",
182
- "--name-only",
183
- "-z",
184
- tree,
185
- "--",
186
- ...paths.map(literalPath),
187
- ])).length === 0);
188
- }
189
- async function workspaceMatchesTreeOnPaths(repository, path, tree, workspaceTree, paths) {
190
- return ((await gitPaths(repository, path, [
191
- "diff",
192
- "--name-only",
193
- "-z",
194
- tree,
195
- workspaceTree,
196
- "--",
197
- ...paths.map(literalPath),
198
- ])).length === 0);
180
+ /** Compare the index or a workspace tree against another tree over the given paths. */
181
+ async function matchesTreeOnPaths(repository, path, command, trees, paths) {
182
+ const args = [...command, "--name-only", "-z", ...trees, "--", ...paths.map(literalPath)];
183
+ return (await gitPaths(repository, path, args)).length === 0;
199
184
  }
200
185
  /**
201
186
  * A checkout that git cannot carry is judged by the physical follow, which records its own lag.
@@ -207,7 +192,7 @@ async function ordinaryPrecheck(repository, contractId, target, path, changes) {
207
192
  const candidate = gitObjectIdForSnapshot(target.newOid);
208
193
  const observation = { repository, contractId, target, path, predecessor, candidate, changes };
209
194
  try {
210
- await runGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
195
+ await executeGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
211
196
  }
212
197
  catch (error) {
213
198
  if (!(error instanceof GitPlumbingError) || repository.signal?.aborted === true)
@@ -311,7 +296,7 @@ export async function followTargetPlacement(repository, prepared, onPhysical) {
311
296
  for (const arm of prepared.arms) {
312
297
  const beforeEffects = effects.length, beforeLag = lag.length;
313
298
  try {
314
- await runGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
299
+ await executeGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
315
300
  effects.push({ kind: "target-checkout", path: arm.path, target: prepared.target.target, action: "followed" });
316
301
  }
317
302
  catch (error) {
@@ -337,15 +322,15 @@ async function classifyCheckout(input) {
337
322
  if (changedPaths.length === 0)
338
323
  return { kind: "complete" };
339
324
  const workspaceTree = (await captureWorkspaceTree(repository, path)).tree;
340
- const candidateIndex = await indexMatchesTreeOnPaths(repository, path, candidateTree, changedPaths);
341
- const candidateWorkspace = await workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
325
+ const candidateIndex = await matchesTreeOnPaths(repository, path, ["diff-index", "--cached"], [candidateTree], changedPaths);
326
+ const candidateWorkspace = await matchesTreeOnPaths(repository, path, ["diff"], [candidateTree, workspaceTree], changedPaths);
342
327
  if (candidateIndex && candidateWorkspace)
343
328
  return { kind: "complete" };
344
- const predecessorIndex = await indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
329
+ const predecessorIndex = await matchesTreeOnPaths(repository, path, ["diff-index", "--cached"], [predecessorTree], changedPaths);
345
330
  if (predecessorIndex && candidateWorkspace)
346
331
  return { kind: "recoverable", action: "index-merge" };
347
332
  return !predecessorIndex ||
348
- !(await workspaceMatchesTreeOnPaths(repository, path, predecessorTree, workspaceTree, changedPaths))
333
+ !(await matchesTreeOnPaths(repository, path, ["diff"], [predecessorTree, workspaceTree], changedPaths))
349
334
  ? { kind: "retained" }
350
335
  : { kind: "recoverable", action: "worktree-merge" };
351
336
  }
@@ -365,7 +350,7 @@ async function recoverCheckout(input) {
365
350
  return "retained";
366
351
  const { repository, path, predecessor, candidate } = input;
367
352
  if (classified.action === "index-merge") {
368
- await runGit(repository, [
353
+ await executeGit(repository, [
369
354
  "-C",
370
355
  path,
371
356
  "read-tree",
@@ -376,7 +361,7 @@ async function recoverCheckout(input) {
376
361
  ]);
377
362
  return "recovered";
378
363
  }
379
- await runGit(repository, [
364
+ await executeGit(repository, [
380
365
  "-C",
381
366
  path,
382
367
  "read-tree",
@@ -4,7 +4,7 @@ import type { ActorId, ContractCoordinates, ContractId, SnapshotId } from "../co
4
4
  import { type GitObjectId } from "./identity.js";
5
5
  import { type GitRepository } from "./process.js";
6
6
  import { captureWorkspaceTree } from "./workspace.js";
7
- export type TenderCaptureCoordinates = Readonly<{
7
+ type TenderCaptureCoordinates = Readonly<{
8
8
  contractId: ContractId;
9
9
  coordinates: ContractCoordinates;
10
10
  place?: string;
@@ -23,7 +23,7 @@ export declare const unmergedPathsRefusalSchema: z.ZodObject<{
23
23
  contractId: z.ZodPipe<z.ZodString, z.ZodTransform<ContractId, string>>;
24
24
  paths: z.ZodReadonly<z.ZodArray<z.ZodString>>;
25
25
  }, z.core.$strict>;
26
- export type TenderCaptureRefusal = WorktreeMissingRefusal | z.infer<typeof unmergedPathsRefusalSchema>;
26
+ type TenderCaptureRefusal = WorktreeMissingRefusal | z.infer<typeof unmergedPathsRefusalSchema>;
27
27
  export declare const workspaceDirtyDeltaSchema: z.ZodObject<{
28
28
  staged: z.ZodReadonly<z.ZodArray<z.ZodString>>;
29
29
  unstaged: z.ZodReadonly<z.ZodArray<z.ZodString>>;
@@ -81,6 +81,10 @@ export declare function prepareDeliveryCommitMetadata(repository: GitRepository,
81
81
  actor?: ActorId;
82
82
  message?: string;
83
83
  }>): Promise<DeliveryCommitMetadata>;
84
+ /** The Git identity environment for one delivery snapshot commit. */
85
+ export declare function deliveryEnvironment(commit: DeliveryCommitMetadata): NodeJS.ProcessEnv;
86
+ /** Materialize one delivery snapshot commit over the given parents. */
87
+ export declare function materializeSnapshotCommit(repository: GitRepository, tree: GitObjectId, parents: readonly SnapshotId[], message: string | Uint8Array, environment: NodeJS.ProcessEnv): Promise<SnapshotId>;
84
88
  /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
85
89
  export declare function materializeTenderSnapshot(repository: GitRepository, tender: TenderCapture, commit: DeliveryCommitMetadata): Promise<SnapshotId>;
86
90
  export {};