@astrosheep/keiyaku 4.6.4 → 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 (191) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +43 -8
  2. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +7 -8
  3. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -3
  4. package/build/src/akuma/akuma-observe.d.ts +18 -15
  5. package/build/src/akuma/akuma-observe.js +75 -16
  6. package/build/src/akuma/akuma-wait.d.ts +4 -3
  7. package/build/src/akuma/akuma-wait.js +75 -53
  8. package/build/src/akuma/akuma.d.ts +6 -4
  9. package/build/src/akuma/akuma.js +4 -0
  10. package/build/src/akuma/call-initial-tell.d.ts +2 -2
  11. package/build/src/akuma/call-initial-tell.js +1 -1
  12. package/build/src/akuma/call-request.d.ts +2 -2
  13. package/build/src/akuma/heart/index.d.ts +7 -2
  14. package/build/src/akuma/heart/index.js +50 -5
  15. package/build/src/akuma/heart/tells.d.ts +33 -2
  16. package/build/src/akuma/heart/tells.js +24 -18
  17. package/build/src/akuma/heart/timeline.d.ts +23 -1
  18. package/build/src/akuma/heart/timeline.js +28 -21
  19. package/build/src/akuma/projection.d.ts +10 -10
  20. package/build/src/akuma/providers/pi/index.js +72 -39
  21. package/build/src/akuma/selection-observation.d.ts +20 -20
  22. package/build/src/akuma-body.js +1 -0
  23. package/build/src/alias/index.d.ts +0 -1
  24. package/build/src/alias/index.js +0 -4
  25. package/build/src/body/amend.d.ts +0 -4
  26. package/build/src/body/amend.js +0 -3
  27. package/build/src/body/region.d.ts +0 -1
  28. package/build/src/body/region.js +0 -3
  29. package/build/src/body/verification.d.ts +0 -1
  30. package/build/src/cli/commands/akuma-grammar.js +23 -66
  31. package/build/src/cli/commands/akuma.js +17 -30
  32. package/build/src/cli/commands/contract-grammar.d.ts +14 -1
  33. package/build/src/cli/commands/contract-grammar.js +54 -21
  34. package/build/src/cli/commands/contract-help.d.ts +9 -8
  35. package/build/src/cli/commands/contract-help.js +14 -7
  36. package/build/src/cli/commands/contract.js +57 -117
  37. package/build/src/cli/commands/install.d.ts +5 -5
  38. package/build/src/cli/commands/install.js +1 -4
  39. package/build/src/cli/commands/task-grammar.js +10 -66
  40. package/build/src/cli/commands/task.d.ts +0 -1
  41. package/build/src/cli/commands/task.js +34 -53
  42. package/build/src/cli/coordinates.d.ts +1 -2
  43. package/build/src/cli/coordinates.js +1 -4
  44. package/build/src/cli/parse.d.ts +2 -3
  45. package/build/src/cli/parse.js +24 -66
  46. package/build/src/cli/render/akuma.d.ts +3 -16
  47. package/build/src/cli/render/akuma.js +1 -13
  48. package/build/src/cli/render/audit.js +1 -1
  49. package/build/src/cli/render/contract.js +34 -32
  50. package/build/src/cli/render/kanshi-akuma.d.ts +8 -2
  51. package/build/src/cli/render/kanshi-akuma.js +14 -7
  52. package/build/src/cli/render/kanshi.d.ts +3 -1
  53. package/build/src/cli/render/kanshi.js +44 -32
  54. package/build/src/cli/render/receipt.js +6 -1
  55. package/build/src/cli/render/task.d.ts +1 -1
  56. package/build/src/cli/render/task.js +3 -27
  57. package/build/src/cli/render/terminal.d.ts +6 -2
  58. package/build/src/cli/render/terminal.js +8 -2
  59. package/build/src/cli/runtime.js +3 -16
  60. package/build/src/cli/scan.d.ts +26 -0
  61. package/build/src/cli/scan.js +82 -0
  62. package/build/src/cli/streams.d.ts +5 -0
  63. package/build/src/cli/streams.js +11 -0
  64. package/build/src/contract-worktree.js +38 -21
  65. package/build/src/core/facts/codec.d.ts +1 -2
  66. package/build/src/core/facts/codec.js +5 -11
  67. package/build/src/core/facts/types.d.ts +16 -2
  68. package/build/src/core/facts/types.js +15 -0
  69. package/build/src/core/verbs/abandon.js +3 -9
  70. package/build/src/core/verbs/amend.js +2 -9
  71. package/build/src/core/verbs/arc.js +6 -13
  72. package/build/src/core/verbs/attestation.js +2 -9
  73. package/build/src/core/verbs/bind.js +2 -9
  74. package/build/src/core/verbs/deliver.js +2 -9
  75. package/build/src/core/verbs/placement.d.ts +1 -1
  76. package/build/src/core/verbs/placement.js +2 -9
  77. package/build/src/core/verbs/reintegrate.d.ts +1 -1
  78. package/build/src/core/verbs/reintegrate.js +2 -9
  79. package/build/src/dispatch/index.d.ts +2 -2
  80. package/build/src/dispatch/index.js +33 -11
  81. package/build/src/git/hooks.d.ts +2 -4
  82. package/build/src/git/hooks.js +0 -12
  83. package/build/src/git/integration.d.ts +5 -3
  84. package/build/src/git/integration.js +36 -25
  85. package/build/src/git/nuke.js +4 -4
  86. package/build/src/git/observe.d.ts +4 -4
  87. package/build/src/git/observe.js +5 -5
  88. package/build/src/git/process.d.ts +1 -4
  89. package/build/src/git/process.js +1 -11
  90. package/build/src/git/read-observation.js +1 -1
  91. package/build/src/git/reconcile.d.ts +1 -1
  92. package/build/src/git/reconcile.js +15 -13
  93. package/build/src/git/repository.d.ts +2 -6
  94. package/build/src/git/repository.js +18 -31
  95. package/build/src/git/scratch.d.ts +6 -6
  96. package/build/src/git/scratch.js +110 -46
  97. package/build/src/git/target-placement.d.ts +5 -5
  98. package/build/src/git/target-placement.js +18 -33
  99. package/build/src/git/tender.d.ts +6 -2
  100. package/build/src/git/tender.js +19 -12
  101. package/build/src/git/terminal-reconcile.js +24 -13
  102. package/build/src/git/terminal-seal.d.ts +2 -5
  103. package/build/src/git/terminal-seal.js +5 -3
  104. package/build/src/git/workspace.d.ts +17 -13
  105. package/build/src/git/workspace.js +58 -41
  106. package/build/src/kanshi/index.d.ts +2 -2
  107. package/build/src/kanshi/index.js +1 -1
  108. package/build/src/kanshi/read.d.ts +8 -1
  109. package/build/src/kanshi/read.js +72 -7
  110. package/build/src/kanshi/report.d.ts +12 -0
  111. package/build/src/library/akuma-creation.d.ts +4 -4
  112. package/build/src/library/bind.d.ts +1 -63
  113. package/build/src/library/bind.js +3 -9
  114. package/build/src/library/contract-creation.js +2 -30
  115. package/build/src/library/contract-execution.d.ts +0 -1
  116. package/build/src/library/contract-execution.js +9 -34
  117. package/build/src/library/contract-operations.d.ts +5 -1
  118. package/build/src/library/contract-operations.js +7 -3
  119. package/build/src/library/contract-types.d.ts +1 -1
  120. package/build/src/library/delivery.d.ts +91 -93
  121. package/build/src/library/delivery.js +2 -1
  122. package/build/src/library/input.d.ts +0 -2
  123. package/build/src/library/input.js +0 -4
  124. package/build/src/library/invocation.d.ts +0 -1
  125. package/build/src/library/invocation.js +3 -25
  126. package/build/src/library/outcome.d.ts +651 -602
  127. package/build/src/library/outcome.js +4 -4
  128. package/build/src/library/reconcile.d.ts +6 -0
  129. package/build/src/library/reconcile.js +31 -1
  130. package/build/src/library/repo.d.ts +0 -1
  131. package/build/src/library/selection.d.ts +1 -10
  132. package/build/src/library/selection.js +14 -15
  133. package/build/src/protocol/abandon.d.ts +1 -1
  134. package/build/src/protocol/abandon.js +3 -10
  135. package/build/src/protocol/amend.js +2 -5
  136. package/build/src/protocol/arc.d.ts +1 -1
  137. package/build/src/protocol/arc.js +2 -9
  138. package/build/src/protocol/attempt.d.ts +3 -20
  139. package/build/src/protocol/attempt.js +6 -5
  140. package/build/src/protocol/audit.d.ts +1 -0
  141. package/build/src/protocol/audit.js +10 -11
  142. package/build/src/protocol/completion.d.ts +129 -55
  143. package/build/src/protocol/completion.js +62 -73
  144. package/build/src/protocol/deliver.d.ts +7 -5
  145. package/build/src/protocol/deliver.js +74 -44
  146. package/build/src/protocol/execution-observation.d.ts +2 -4
  147. package/build/src/protocol/execution-observation.js +3 -7
  148. package/build/src/protocol/intent.d.ts +1 -1
  149. package/build/src/protocol/intent.js +1 -0
  150. package/build/src/protocol/operations.d.ts +24 -12
  151. package/build/src/protocol/operations.js +36 -15
  152. package/build/src/protocol/placement.d.ts +2 -1
  153. package/build/src/protocol/placement.js +21 -5
  154. package/build/src/protocol/progress.d.ts +1 -1
  155. package/build/src/protocol/read/status.js +58 -41
  156. package/build/src/protocol/reintegrate.d.ts +1 -1
  157. package/build/src/protocol/reintegrate.js +10 -19
  158. package/build/src/protocol/review.d.ts +3 -28
  159. package/build/src/protocol/review.js +10 -37
  160. package/build/src/protocol/run.d.ts +19 -7
  161. package/build/src/protocol/run.js +19 -9
  162. package/build/src/runtime/proc/windows-launch.exe +0 -0
  163. package/build/src/settlement/holder.d.ts +2 -4
  164. package/build/src/settlement/holder.js +3 -6
  165. package/build/src/task/board.d.ts +6 -0
  166. package/build/src/task/board.js +30 -1
  167. package/build/src/task/catalog.js +1 -4
  168. package/build/src/task/compose.d.ts +0 -1
  169. package/build/src/task/compose.js +0 -1
  170. package/build/src/task/context.d.ts +0 -1
  171. package/build/src/task/context.js +0 -3
  172. package/build/src/task/created-observation.d.ts +5 -1
  173. package/build/src/task/created-observation.js +12 -5
  174. package/build/src/task/index.d.ts +1 -1
  175. package/build/src/task/index.js +7 -6
  176. package/build/src/task/input.d.ts +0 -6
  177. package/build/src/task/input.js +2 -14
  178. package/build/src/task/mutation.d.ts +3 -4
  179. package/build/src/task/mutation.js +11 -39
  180. package/build/src/task/operations.d.ts +2 -7
  181. package/build/src/task/operations.js +37 -49
  182. package/build/src/task/query.d.ts +0 -1
  183. package/build/src/task/query.js +1 -4
  184. package/build/src/task/store.d.ts +0 -2
  185. package/build/src/task/store.js +27 -11
  186. package/build/src/verification/declaration.js +3 -2
  187. package/build/src/verification/execution.d.ts +3 -2
  188. package/build/src/verification/execution.js +10 -6
  189. package/build/src/workspace-place.d.ts +0 -1
  190. package/build/src/workspace-place.js +0 -11
  191. package/package.json +7 -3
@@ -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 {};
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { access } from "node:fs/promises";
4
4
  import { gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
5
5
  import { decodeGitNumstat, registeredWorktreePaths } from "./repository.js";
6
- import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
6
+ import { executeGit, GitPlumbingError } from "./process.js";
7
7
  import { captureWorkspaceTree, unmergedWorkspacePaths, workspaceMergeHead, worktreePath } from "./workspace.js";
8
8
  export const worktreeMissingRefusalSchema = z
9
9
  .object({ kind: z.literal("worktree-missing"), contractId: contractIdSchema })
@@ -69,7 +69,7 @@ export async function captureTender(repository, input) {
69
69
  };
70
70
  }
71
71
  async function dirtyShortStat(repository, tender) {
72
- return decodeGitNumstat(await runGit(repository, ["diff", "--numstat", "-z", gitObjectIdForSnapshot(tender.head), tender.tree]));
72
+ return decodeGitNumstat(await executeGit(repository, ["diff", "--numstat", "-z", gitObjectIdForSnapshot(tender.head), tender.tree]));
73
73
  }
74
74
  export async function dirtyTenderRefusal(repository, contractId, tender) {
75
75
  return {
@@ -87,7 +87,7 @@ export async function dirtyTenderDelta(repository, tender) {
87
87
  }
88
88
  async function effectiveConfig(repository, key) {
89
89
  try {
90
- const value = (await runGit(repository, ["config", "--get", key])).toString("utf8").trim();
90
+ const value = (await executeGit(repository, ["config", "--get", key])).toString("utf8").trim();
91
91
  return value.length === 0 ? undefined : value;
92
92
  }
93
93
  catch (error) {
@@ -119,22 +119,29 @@ export async function prepareDeliveryCommitMetadata(repository, input) {
119
119
  identity,
120
120
  };
121
121
  }
122
- /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
123
- export async function materializeTenderSnapshot(repository, tender, commit) {
124
- if (!tender.dirty && tender.mergeHead === undefined)
125
- return tender.head;
126
- const parents = ["-p", gitObjectIdForSnapshot(tender.head)];
127
- if (tender.mergeHead !== undefined)
128
- parents.push("-p", gitObjectIdForSnapshot(tender.mergeHead));
129
- const oid = (await runGitWithEnvironment(repository, ["commit-tree", tender.tree, ...parents], commit.message, {
122
+ /** The Git identity environment for one delivery snapshot commit. */
123
+ export function deliveryEnvironment(commit) {
124
+ return {
130
125
  GIT_AUTHOR_NAME: commit.identity.name,
131
126
  GIT_AUTHOR_EMAIL: commit.identity.email,
132
127
  GIT_COMMITTER_NAME: commit.identity.name,
133
128
  GIT_COMMITTER_EMAIL: commit.identity.email,
134
129
  GIT_AUTHOR_DATE: commit.at,
135
130
  GIT_COMMITTER_DATE: commit.at,
136
- }))
131
+ };
132
+ }
133
+ /** Materialize one delivery snapshot commit over the given parents. */
134
+ export async function materializeSnapshotCommit(repository, tree, parents, message, environment) {
135
+ const args = parents.flatMap((parent) => ["-p", gitObjectIdForSnapshot(parent)]);
136
+ const oid = (await executeGit(repository, ["commit-tree", tree, ...args], message, environment))
137
137
  .toString("utf8")
138
138
  .trim();
139
139
  return mintSnapshotId(oid);
140
140
  }
141
+ /** Materialize the tender tree when it contains admitted dirty workspace bytes. */
142
+ export async function materializeTenderSnapshot(repository, tender, commit) {
143
+ if (!tender.dirty && tender.mergeHead === undefined)
144
+ return tender.head;
145
+ const parents = tender.mergeHead === undefined ? [tender.head] : [tender.head, tender.mergeHead];
146
+ return await materializeSnapshotCommit(repository, tender.tree, parents, commit.message, deliveryEnvironment(commit));
147
+ }
@@ -3,7 +3,7 @@ import { isKeiyakuOwnedRef, readRef, writeCommit } from "./repository.js";
3
3
  import { gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
4
4
  import { runDestroyHooks } from "./hooks.js";
5
5
  import { terminalSealExpectations as decodeTerminalSealExpectations, terminalSealSnapshots, observeTerminalWorkspace, } from "./terminal-seal.js";
6
- import { GitPlumbingError, runGit } from "./process.js";
6
+ import { GitPlumbingError, executeGit } from "./process.js";
7
7
  import { worktreePath } from "./workspace.js";
8
8
  async function pathExists(path) {
9
9
  try {
@@ -16,29 +16,38 @@ async function pathExists(path) {
16
16
  throw error;
17
17
  }
18
18
  }
19
+ /** Attempt removal once. Retention is a lag carrying its reason; no effect claims the worktree state after it. */
19
20
  async function removeTerminalWorktree(repository, topology, path) {
20
21
  const registered = topology.paths.has(path);
21
22
  if (!registered) {
22
- return { retained: await pathExists(path) };
23
+ return (await pathExists(path)) ? { retainedLag: { kind: "worktree-retained", path } } : {};
23
24
  }
24
25
  const present = await pathExists(path);
25
26
  try {
26
- await runGit(repository, ["worktree", "remove", ...(present ? ["--force"] : []), path]);
27
+ await executeGit(repository, ["worktree", "remove", ...(present ? ["--force"] : []), path]);
27
28
  }
28
29
  catch (error) {
29
30
  if (!(error instanceof GitPlumbingError))
30
31
  throw error;
31
- return { effect: { kind: "worktree", path, action: "unchanged" }, retained: true };
32
+ return { retainedLag: { kind: "worktree-retained", path, diagnostic: error.message } };
32
33
  }
33
34
  topology.paths.delete(path);
34
- const retained = await pathExists(path);
35
- return { effect: { kind: "worktree", path, action: retained ? "unchanged" : "removed" }, retained };
35
+ if (await pathExists(path)) {
36
+ return {
37
+ retainedLag: {
38
+ kind: "worktree-retained",
39
+ path,
40
+ diagnostic: `Git reported worktree removal but the path remains: ${path}`,
41
+ },
42
+ };
43
+ }
44
+ return { effect: { kind: "worktree", path, action: "removed" } };
36
45
  }
37
46
  export async function updateRef(repository, ref, desired) {
38
47
  const before = await readRef(repository, ref);
39
48
  if (before === desired)
40
49
  return { kind: "ref", name: ref, action: "unchanged", before, after: desired };
41
- await runGit(repository, [
50
+ await executeGit(repository, [
42
51
  "update-ref",
43
52
  "--no-deref",
44
53
  ref,
@@ -51,7 +60,7 @@ export async function removeRef(repository, ref) {
51
60
  const before = await readRef(repository, ref);
52
61
  if (before === null)
53
62
  return { kind: "ref", name: ref, action: "unchanged", before: null, after: null };
54
- await runGit(repository, ["update-ref", "--no-deref", "-d", ref, before]);
63
+ await executeGit(repository, ["update-ref", "--no-deref", "-d", ref, before]);
55
64
  return { kind: "ref", name: ref, action: "removed", before, after: null };
56
65
  }
57
66
  async function removeRefWithCustody(repository, ref, custodian, expectedCustodian) {
@@ -61,7 +70,7 @@ async function removeRefWithCustody(repository, ref, custodian, expectedCustodia
61
70
  if ((await readRef(repository, custodian)) !== expectedCustodian) {
62
71
  return { kind: "ref", name: ref, action: "unchanged", before, after: before };
63
72
  }
64
- await runGit(repository, ["update-ref", "--stdin", "--no-deref"], [
73
+ await executeGit(repository, ["update-ref", "--stdin", "--no-deref"], [
65
74
  "start",
66
75
  `verify ${custodian} ${gitObjectIdForSnapshot(expectedCustodian)}`,
67
76
  `delete ${ref} ${before}`,
@@ -75,7 +84,7 @@ function retainedRefEffect(ref, snapshot) {
75
84
  return { kind: "ref", name: ref, action: "unchanged", before: snapshot, after: snapshot };
76
85
  }
77
86
  async function refRows(repository) {
78
- const output = (await runGit(repository, ["for-each-ref", "--format=%(refname)%00%(objectname)"])).toString("utf8");
87
+ const output = (await executeGit(repository, ["for-each-ref", "--format=%(refname)%00%(objectname)"])).toString("utf8");
79
88
  if (output.length === 0)
80
89
  return [];
81
90
  return output
@@ -90,7 +99,7 @@ async function refRows(repository) {
90
99
  }
91
100
  async function commitIsAncestor(repository, ancestor, descendant) {
92
101
  try {
93
- await runGit(repository, ["merge-base", "--is-ancestor", gitObjectIdForSnapshot(ancestor), descendant]);
102
+ await executeGit(repository, ["merge-base", "--is-ancestor", gitObjectIdForSnapshot(ancestor), descendant]);
94
103
  return true;
95
104
  }
96
105
  catch (error) {
@@ -181,8 +190,10 @@ async function removeSealedTerminalWorktree({ repository, topology, path, state,
181
190
  }
182
191
  }
183
192
  const removal = await removeTerminalWorktree(repository, topology, path);
184
- if (removal.retained)
185
- return retainTerminalWorktree(path, { kind: "worktree-retained", path }, acc);
193
+ if (removal.retainedLag !== undefined) {
194
+ acc.recordLag(removal.retainedLag);
195
+ return complete(acc.effects, acc.lag, acc.hookRuns);
196
+ }
186
197
  if (removal.effect !== undefined)
187
198
  acc.recordEffect(removal.effect);
188
199
  return null;
@@ -3,7 +3,6 @@ import type { ContractState, SnapshotId } from "../core/facts/types.js";
3
3
  import { type GitObjectId } from "./identity.js";
4
4
  import type { GitOid } from "./repository.js";
5
5
  import { type GitRepository } from "./process.js";
6
- import { captureWorkspaceTree } from "./workspace.js";
7
6
  export declare const unsealedBytesSchema: z.ZodObject<{
8
7
  kind: z.ZodLiteral<"unsealed-bytes">;
9
8
  path: z.ZodString;
@@ -16,25 +15,23 @@ export type TerminalSealExpectations = Readonly<{
16
15
  trees: readonly GitObjectId[];
17
16
  treeBySnapshot: ReadonlyMap<SnapshotId, GitObjectId>;
18
17
  }>;
19
- export type TerminalSealObject = Readonly<{
18
+ type TerminalSealObject = Readonly<{
20
19
  kind: "present";
21
20
  type: string;
22
21
  bytes: Buffer;
23
22
  }> | Readonly<{
24
23
  kind: "missing";
25
24
  }>;
26
- type WorkspaceTree = Awaited<ReturnType<typeof captureWorkspaceTree>>;
27
25
  export type TerminalWorkspace = Readonly<{
28
26
  tree: GitObjectId;
29
27
  head: SnapshotId;
30
28
  submodules: readonly string[];
31
29
  }>;
32
- export type TerminalWorkspaceObservation = Readonly<{
30
+ type TerminalWorkspaceObservation = Readonly<{
33
31
  workspace: TerminalWorkspace;
34
32
  unsealed: UnsealedBytes | null;
35
33
  }>;
36
34
  export declare function terminalSealSnapshots(state: ContractState): readonly SnapshotId[];
37
35
  export declare function terminalSealExpectations(state: ContractState, objects: ReadonlyMap<GitOid, TerminalSealObject>): TerminalSealExpectations;
38
- export declare function unsealedWorkspaceBytes(repository: GitRepository, path: string, expected: TerminalSealExpectations, workspace: WorkspaceTree): Promise<UnsealedBytes | null>;
39
36
  export declare function observeTerminalWorkspace(repository: GitRepository, path: string, expected: TerminalSealExpectations): Promise<TerminalWorkspaceObservation>;
40
37
  export {};
@@ -1,7 +1,7 @@
1
1
  import { mintedSnapshotInputSchema } from "./identity.js";
2
2
  import { z } from "zod";
3
3
  import { gitObjectId, gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
4
- import { runGit } from "./process.js";
4
+ import { executeGit } from "./process.js";
5
5
  import { captureWorkspaceTree } from "./workspace.js";
6
6
  export const unsealedBytesSchema = z
7
7
  .object({
@@ -71,12 +71,14 @@ export function terminalSealExpectations(state, objects) {
71
71
  };
72
72
  }
73
73
  async function changedPaths(repository, left, right) {
74
- const fields = (await runGit(repository, ["diff", "--name-only", "-z", left, right])).toString("utf8").split("\0");
74
+ const fields = (await executeGit(repository, ["diff", "--name-only", "-z", left, right]))
75
+ .toString("utf8")
76
+ .split("\0");
75
77
  if (fields.at(-1) !== "")
76
78
  throw new Error("Git tree diff paths are not NUL terminated");
77
79
  return fields.slice(0, -1).sort();
78
80
  }
79
- export async function unsealedWorkspaceBytes(repository, path, expected, workspace) {
81
+ async function unsealedWorkspaceBytes(repository, path, expected, workspace) {
80
82
  const headIsSealed = expected.heads.includes(workspace.head);
81
83
  if (workspace.changes.submodules.length > 0) {
82
84
  return {