@astrosheep/keiyaku 4.5.11 → 4.5.13

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 (272) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +135 -377
  2. package/build/integrations/pi/keiyaku.ts +11 -116
  3. package/build/src/akuma/akuma-errors.d.ts +1 -0
  4. package/build/src/akuma/akuma-errors.js +1 -0
  5. package/build/src/akuma/akuma-handle.d.ts +1 -0
  6. package/build/src/akuma/akuma-handle.js +6 -7
  7. package/build/src/akuma/akuma-product.d.ts +6 -8
  8. package/build/src/akuma/akuma-product.js +141 -57
  9. package/build/src/akuma/akuma.d.ts +5 -7
  10. package/build/src/akuma/akuma.js +1 -22
  11. package/build/src/akuma/archetype.d.ts +6 -5
  12. package/build/src/akuma/archetype.js +137 -38
  13. package/build/src/akuma/body.d.ts +5 -5
  14. package/build/src/akuma/body.js +223 -63
  15. package/build/src/akuma/call-input.d.ts +4 -0
  16. package/build/src/akuma/call-input.js +21 -0
  17. package/build/src/akuma/call-request.d.ts +5 -8
  18. package/build/src/akuma/call-request.js +28 -73
  19. package/build/src/akuma/fleet-execution.d.ts +26 -0
  20. package/build/src/akuma/fleet-execution.js +109 -0
  21. package/build/src/{library/fleet-result.d.ts → akuma/fleet-observation.d.ts} +13 -57
  22. package/build/src/{library/fleet-result.js → akuma/fleet-observation.js} +4 -25
  23. package/build/src/akuma/fleet-request.d.ts +122 -0
  24. package/build/src/akuma/fleet-request.js +168 -0
  25. package/build/src/akuma/heart/facts.d.ts +23 -0
  26. package/build/src/akuma/heart/facts.js +7 -0
  27. package/build/src/akuma/heart/index.d.ts +43 -4
  28. package/build/src/akuma/heart/index.js +107 -7
  29. package/build/src/akuma/heart/request-authority.d.ts +2 -0
  30. package/build/src/akuma/heart/request-authority.js +28 -3
  31. package/build/src/akuma/heart/request-rows.d.ts +2 -0
  32. package/build/src/akuma/heart/request-rows.js +24 -5
  33. package/build/src/akuma/heart/rows.d.ts +4 -1
  34. package/build/src/akuma/heart/rows.js +46 -19
  35. package/build/src/akuma/heart/schema.d.ts +1 -1
  36. package/build/src/akuma/heart/schema.js +28 -6
  37. package/build/src/akuma/heart/storage.d.ts +1 -1
  38. package/build/src/akuma/heart/tells.d.ts +29 -0
  39. package/build/src/akuma/heart/tells.js +136 -5
  40. package/build/src/akuma/heart/timeline.js +9 -3
  41. package/build/src/akuma/projection-read.js +36 -3
  42. package/build/src/akuma/projection.js +3 -4
  43. package/build/src/akuma/provider.d.ts +4 -0
  44. package/build/src/akuma/provider.js +56 -3
  45. package/build/src/akuma/providers/claude/index.js +3 -0
  46. package/build/src/akuma/providers/codex-app-server/index.js +14 -12
  47. package/build/src/akuma/providers/pi/index.js +7 -1
  48. package/build/src/akuma/publication.js +65 -49
  49. package/build/src/akuma/request-lifecycle.d.ts +5 -4
  50. package/build/src/akuma/request-lifecycle.js +29 -16
  51. package/build/src/akuma/request-rendezvous.d.ts +15 -12
  52. package/build/src/akuma/request-rendezvous.js +80 -51
  53. package/build/src/akuma/request-serve.d.ts +1 -2
  54. package/build/src/akuma/request-serve.js +94 -39
  55. package/build/src/akuma/request-wire.d.ts +57 -26
  56. package/build/src/akuma/request-wire.js +64 -25
  57. package/build/src/akuma/turn-drive.d.ts +5 -4
  58. package/build/src/akuma/turn-drive.js +27 -11
  59. package/build/src/akuma-body.d.ts +4 -6
  60. package/build/src/akuma-body.js +19 -17
  61. package/build/src/bounded-list.d.ts +8 -0
  62. package/build/src/bounded-list.js +14 -0
  63. package/build/src/cli/accepted.d.ts +1 -1
  64. package/build/src/cli/accepted.js +1 -0
  65. package/build/src/cli/commands/akuma-invoke.js +1 -25
  66. package/build/src/cli/commands/contract-help.d.ts +5 -2
  67. package/build/src/cli/commands/contract-help.js +62 -12
  68. package/build/src/cli/commands/contract-invoke.d.ts +5 -0
  69. package/build/src/cli/commands/contract-invoke.js +26 -1
  70. package/build/src/cli/commands/contract.js +50 -18
  71. package/build/src/cli/commands/task-invoke.js +10 -2
  72. package/build/src/cli/commands/task-query.js +3 -4
  73. package/build/src/cli/coordinates.d.ts +13 -0
  74. package/build/src/cli/coordinates.js +38 -24
  75. package/build/src/cli/draft.d.ts +1 -1
  76. package/build/src/cli/invoke.js +14 -1
  77. package/build/src/cli/render/akuma-activity.d.ts +0 -1
  78. package/build/src/cli/render/akuma-activity.js +5 -38
  79. package/build/src/cli/render/akuma.js +4 -1
  80. package/build/src/cli/render/audit.js +4 -1
  81. package/build/src/cli/render/catalog.js +13 -39
  82. package/build/src/cli/render/contract.js +4 -14
  83. package/build/src/cli/render/kanshi-akuma.js +3 -8
  84. package/build/src/cli/render/kanshi.js +8 -34
  85. package/build/src/cli/render/nuke.js +7 -3
  86. package/build/src/cli/render/receipt.d.ts +1 -0
  87. package/build/src/cli/render/receipt.js +8 -0
  88. package/build/src/cli/render/refusal.js +4 -3
  89. package/build/src/cli/render/task.js +4 -37
  90. package/build/src/cli/render/terminal.d.ts +2 -6
  91. package/build/src/cli/render/terminal.js +4 -9
  92. package/build/src/cli/result.d.ts +2 -1
  93. package/build/src/contract-guidance.js +13 -3
  94. package/build/src/contract-worktree.d.ts +1 -0
  95. package/build/src/contract-worktree.js +26 -2
  96. package/build/src/core/facts/codec.d.ts +2 -1
  97. package/build/src/core/facts/codec.js +3 -0
  98. package/build/src/core/facts/gate.d.ts +2 -1
  99. package/build/src/core/facts/gate.js +63 -0
  100. package/build/src/core/verbs/abandon.d.ts +2 -1
  101. package/build/src/core/verbs/abandon.js +16 -0
  102. package/build/src/core/verbs/amend.d.ts +2 -1
  103. package/build/src/core/verbs/amend.js +20 -0
  104. package/build/src/core/verbs/arc.d.ts +2 -1
  105. package/build/src/core/verbs/arc.js +16 -0
  106. package/build/src/core/verbs/attestation.d.ts +2 -1
  107. package/build/src/core/verbs/attestation.js +16 -0
  108. package/build/src/core/verbs/bind.d.ts +2 -1
  109. package/build/src/core/verbs/bind.js +16 -0
  110. package/build/src/core/verbs/deliver.d.ts +2 -1
  111. package/build/src/core/verbs/deliver.js +16 -0
  112. package/build/src/core/verbs/placement.d.ts +2 -1
  113. package/build/src/core/verbs/placement.js +58 -1
  114. package/build/src/dispatch/association.d.ts +13 -0
  115. package/build/src/dispatch/association.js +20 -0
  116. package/build/src/dispatch/index.d.ts +6 -0
  117. package/build/src/dispatch/index.js +27 -3
  118. package/build/src/git/hooks.d.ts +9 -1
  119. package/build/src/git/hooks.js +79 -1
  120. package/build/src/git/nuke.d.ts +2 -1
  121. package/build/src/git/nuke.js +6 -11
  122. package/build/src/git/private-state-seat.d.ts +26 -1
  123. package/build/src/git/private-state-seat.js +78 -13
  124. package/build/src/git/process.d.ts +2 -0
  125. package/build/src/git/reconcile.d.ts +1 -2
  126. package/build/src/git/reconcile.js +0 -6
  127. package/build/src/git/repository.d.ts +0 -2
  128. package/build/src/git/repository.js +27 -25
  129. package/build/src/git/result-codec.d.ts +29 -0
  130. package/build/src/git/result-codec.js +306 -0
  131. package/build/src/git/target-placement.js +2 -8
  132. package/build/src/git/workspace.d.ts +2 -0
  133. package/build/src/git/workspace.js +4 -0
  134. package/build/src/index.d.ts +1 -0
  135. package/build/src/kanshi/fleet.js +1 -1
  136. package/build/src/kanshi/read.js +55 -28
  137. package/build/src/kanshi/report.d.ts +4 -3
  138. package/build/src/kanshi/select.js +14 -1
  139. package/build/src/library/address.d.ts +1 -0
  140. package/build/src/library/address.js +4 -7
  141. package/build/src/library/akuma-creation.d.ts +5 -3
  142. package/build/src/library/akuma-creation.js +90 -33
  143. package/build/src/library/audit.js +2 -1
  144. package/build/src/library/catalog.d.ts +10 -2
  145. package/build/src/library/catalog.js +65 -25
  146. package/build/src/library/composition.d.ts +1 -0
  147. package/build/src/library/composition.js +1 -0
  148. package/build/src/library/configuration.d.ts +4 -21
  149. package/build/src/library/configuration.js +2 -158
  150. package/build/src/library/continuation.d.ts +20 -1
  151. package/build/src/library/continuation.js +36 -0
  152. package/build/src/library/contract-bind.d.ts +1 -0
  153. package/build/src/library/contract-bind.js +16 -5
  154. package/build/src/library/contract-forwarding.d.ts +2 -2
  155. package/build/src/library/contract-forwarding.js +18 -4
  156. package/build/src/library/contract-handle.js +4 -3
  157. package/build/src/library/contract-operations.d.ts +5 -9
  158. package/build/src/library/contract-operations.js +34 -47
  159. package/build/src/library/contract.d.ts +11 -2
  160. package/build/src/library/contract.js +21 -1
  161. package/build/src/library/delivery.d.ts +4 -1
  162. package/build/src/library/delivery.js +47 -0
  163. package/build/src/library/fleet.d.ts +4 -136
  164. package/build/src/library/fleet.js +31 -315
  165. package/build/src/library/keiyaku.d.ts +7 -3
  166. package/build/src/library/keiyaku.js +1 -0
  167. package/build/src/library/mutation.d.ts +431 -3
  168. package/build/src/library/mutation.js +192 -19
  169. package/build/src/library/nuke.d.ts +7 -1
  170. package/build/src/library/nuke.js +18 -4
  171. package/build/src/library/reconcile.d.ts +14 -0
  172. package/build/src/library/reconcile.js +22 -8
  173. package/build/src/library/refusal.d.ts +9 -1
  174. package/build/src/library/refusal.js +76 -0
  175. package/build/src/library/region.d.ts +0 -8
  176. package/build/src/library/region.js +1 -29
  177. package/build/src/library/result-codec.d.ts +2 -0
  178. package/build/src/library/result-codec.js +12 -0
  179. package/build/src/markdown/lex.js +9 -2
  180. package/build/src/markdown/types.d.ts +2 -1
  181. package/build/src/markdown/types.js +3 -1
  182. package/build/src/plugin/akuma-signals.d.ts +12 -0
  183. package/build/src/plugin/akuma-signals.js +14 -0
  184. package/build/src/plugin/public.d.ts +51 -0
  185. package/build/src/plugin/public.js +1 -0
  186. package/build/src/plugin/runtime.d.ts +14 -0
  187. package/build/src/plugin/runtime.js +357 -0
  188. package/build/src/protocol/amend.js +3 -3
  189. package/build/src/protocol/attempt.d.ts +2 -1
  190. package/build/src/protocol/attempt.js +1 -1
  191. package/build/src/protocol/audit.d.ts +1 -1
  192. package/build/src/protocol/audit.js +1 -1
  193. package/build/src/protocol/bind.d.ts +3 -1
  194. package/build/src/protocol/bind.js +26 -0
  195. package/build/src/protocol/completion.js +0 -1
  196. package/build/src/protocol/deliver.d.ts +1 -1
  197. package/build/src/protocol/deliver.js +136 -57
  198. package/build/src/protocol/intent.d.ts +0 -1
  199. package/build/src/protocol/intent.js +0 -1
  200. package/build/src/protocol/operations.d.ts +9 -2
  201. package/build/src/protocol/operations.js +16 -1
  202. package/build/src/protocol/outcome.d.ts +3 -0
  203. package/build/src/protocol/outcome.js +17 -1
  204. package/build/src/protocol/placement.js +10 -3
  205. package/build/src/protocol/read/status.d.ts +8 -0
  206. package/build/src/protocol/read/status.js +49 -0
  207. package/build/src/protocol/reintegrate.js +10 -3
  208. package/build/src/protocol/result-codec.d.ts +22 -0
  209. package/build/src/protocol/result-codec.js +442 -0
  210. package/build/src/protocol/review.d.ts +1 -1
  211. package/build/src/protocol/review.js +81 -46
  212. package/build/src/protocol/run.d.ts +22 -2
  213. package/build/src/protocol/run.js +119 -29
  214. package/build/src/runtime/proc/lifecycle.js +1 -1
  215. package/build/src/runtime/proc/line-rpc.d.ts +7 -1
  216. package/build/src/runtime/proc/line-rpc.js +49 -9
  217. package/build/src/runtime/proc/run.d.ts +0 -1
  218. package/build/src/runtime/proc/run.js +1 -11
  219. package/build/src/runtime/proc/termination.d.ts +1 -0
  220. package/build/src/runtime/proc/termination.js +63 -18
  221. package/build/src/runtime/proc/windows-launch.exe +0 -0
  222. package/build/src/settings.d.ts +10 -0
  223. package/build/src/settings.js +88 -2
  224. package/build/src/settlement/result-codec.d.ts +2 -0
  225. package/build/src/settlement/result-codec.js +56 -0
  226. package/build/src/settlement/settle.d.ts +5 -12
  227. package/build/src/settlement/settle.js +60 -55
  228. package/build/src/task/board.d.ts +3 -0
  229. package/build/src/task/board.js +22 -4
  230. package/build/src/task/catalog.d.ts +13 -2
  231. package/build/src/task/catalog.js +59 -5
  232. package/build/src/task/compose.d.ts +4 -2
  233. package/build/src/task/compose.js +56 -30
  234. package/build/src/task/created-observation.d.ts +39 -0
  235. package/build/src/task/created-observation.js +26 -0
  236. package/build/src/task/identity.d.ts +6 -0
  237. package/build/src/task/identity.js +48 -7
  238. package/build/src/task/index.d.ts +4 -3
  239. package/build/src/task/index.js +19 -16
  240. package/build/src/task/input.d.ts +3 -1
  241. package/build/src/task/input.js +7 -4
  242. package/build/src/task/lifecycle-operations.d.ts +6 -0
  243. package/build/src/task/lifecycle-operations.js +116 -0
  244. package/build/src/task/mutation-result.d.ts +762 -20
  245. package/build/src/task/mutation-result.js +34 -9
  246. package/build/src/task/mutation.d.ts +4 -9
  247. package/build/src/task/mutation.js +17 -52
  248. package/build/src/task/operation-types.d.ts +114 -0
  249. package/build/src/task/operation-types.js +1 -0
  250. package/build/src/task/operations.d.ts +6 -113
  251. package/build/src/task/operations.js +75 -117
  252. package/build/src/task/query.d.ts +8 -11
  253. package/build/src/task/query.js +7 -14
  254. package/build/src/task/store.d.ts +11 -1
  255. package/build/src/task/store.js +169 -55
  256. package/build/src/task/view.d.ts +4 -0
  257. package/build/src/task/view.js +7 -0
  258. package/build/src/verification/declaration.d.ts +1 -0
  259. package/build/src/verification/declaration.js +18 -1
  260. package/build/src/verification/execution.d.ts +0 -1
  261. package/build/src/verification/execution.js +5 -5
  262. package/build/src/world.d.ts +0 -2
  263. package/build/src/world.js +3 -7
  264. package/package.json +17 -5
  265. package/build/src/cli/square-edge.d.ts +0 -7
  266. package/build/src/cli/square-edge.js +0 -159
  267. package/build/src/git/ref-migration.d.ts +0 -22
  268. package/build/src/git/ref-migration.js +0 -55
  269. package/build/src/library/contract-forwarding-reconciliation-result.d.ts +0 -102
  270. package/build/src/library/contract-forwarding-reconciliation-result.js +0 -92
  271. package/build/src/library/contract-forwarding-result.d.ts +0 -4270
  272. package/build/src/library/contract-forwarding-result.js +0 -521
@@ -1,8 +1,19 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { join } from "node:path";
2
- import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
3
+ import { acquireSqliteTransactionLock, SqliteTransactionLockError } from "../coordination/sqlite-transaction-lock.js";
3
4
  const PRIVATE_STATE_SEAT = "private-state.sqlite";
5
+ const PRIVATE_STATE_SEAT_ACQUIRE_TIMEOUT_MS = 5_000;
4
6
  const privateStateSeat = Symbol("private-state-seat");
5
7
  const confirmedPublications = new WeakSet();
8
+ const heldPrivateStateSeats = new AsyncLocalStorage();
9
+ /** Timed-out wait for the private-state publication seat. The holder and lock are unchanged. */
10
+ export class GitPrivateStateSeatContentionError extends Error {
11
+ reason = "timeout";
12
+ constructor(message, options) {
13
+ super(message, options);
14
+ this.name = "GitPrivateStateSeatContentionError";
15
+ }
16
+ }
6
17
  /** Record a known or exact-read-back-proven private-root publication. */
7
18
  export function confirmPrivateStatePublication(seat) {
8
19
  confirmedPublications.add(seat);
@@ -10,30 +21,84 @@ export function confirmPrivateStatePublication(seat) {
10
21
  export function privateStatePublicationSeatPath(repository) {
11
22
  return join(repository.commonDirectory, "keiyaku", "locks", PRIVATE_STATE_SEAT);
12
23
  }
13
- /** Serialize cooperating writers of the one private Git state root. */
14
- export async function withPrivateStatePublicationSeat(repository, action) {
15
- const held = await acquireSqliteTransactionLock({
16
- path: privateStatePublicationSeatPath(repository),
17
- mode: "immediate",
18
- ...(repository.signal === undefined ? {} : { signal: repository.signal }),
19
- ...(repository.onPrivateStateSeatContention === undefined
20
- ? {}
21
- : { onContended: repository.onPrivateStateSeatContention }),
22
- });
24
+ export function privateStateSeatCloseLag(error) {
25
+ return {
26
+ kind: "private-state-seat-close-failed",
27
+ diagnostic: error instanceof Error ? error.message : String(error),
28
+ };
29
+ }
30
+ export function concatenatePrivateStateSeatClose(current, next) {
31
+ const lags = [...(current ?? []), ...(next ?? [])];
32
+ return lags.length === 0 ? undefined : lags;
33
+ }
34
+ export function appendPrivateStateSeatClose(current, closeLag) {
35
+ return current === undefined ? [closeLag] : [...current, closeLag];
36
+ }
37
+ /** Unwrap a seat outcome that has no typed lag surface; confirmed close lag throws. */
38
+ export function requireClosedPrivateStateSeat(outcome) {
39
+ if (outcome.closeLag !== undefined)
40
+ throw new Error(outcome.closeLag.diagnostic);
41
+ return outcome.value;
42
+ }
43
+ /** Unwrap a seat outcome through an owner-typed merge of confirmed close lag. */
44
+ export function mergePrivateStateSeatClose(outcome, merge) {
45
+ return outcome.closeLag === undefined ? outcome.value : merge(outcome.value, outcome.closeLag);
46
+ }
47
+ export function isPrivateStateSeatContention(error) {
48
+ return error instanceof GitPrivateStateSeatContentionError;
49
+ }
50
+ function acquireTimeoutMs(timeoutMs) {
51
+ return timeoutMs ?? PRIVATE_STATE_SEAT_ACQUIRE_TIMEOUT_MS;
52
+ }
53
+ async function acquirePrivateStateSeatLock(repository, path, timeoutMs) {
54
+ try {
55
+ return await acquireSqliteTransactionLock({
56
+ path,
57
+ mode: "immediate",
58
+ timeoutMs: acquireTimeoutMs(timeoutMs),
59
+ ...(repository.signal === undefined ? {} : { signal: repository.signal }),
60
+ ...(repository.onPrivateStateSeatContention === undefined
61
+ ? {}
62
+ : { onContended: repository.onPrivateStateSeatContention }),
63
+ });
64
+ }
65
+ catch (error) {
66
+ if (error instanceof SqliteTransactionLockError && error.reason === "timeout") {
67
+ throw new GitPrivateStateSeatContentionError(error.message, { cause: error });
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ async function runPrivateStateSeatAction(repository, held, action) {
23
73
  const seat = { [privateStateSeat]: true };
24
74
  let completed = false;
75
+ let value;
25
76
  try {
26
- const result = await action(seat);
77
+ value = await action(seat);
27
78
  completed = true;
28
- return result;
79
+ return { value };
29
80
  }
30
81
  finally {
31
82
  try {
32
83
  held.close();
84
+ repository.onPrivateStateSeatClose?.();
33
85
  }
34
86
  catch (error) {
35
87
  if (!(completed && confirmedPublications.has(seat)))
36
88
  throw error;
89
+ return { value: value, closeLag: privateStateSeatCloseLag(error) };
37
90
  }
38
91
  }
39
92
  }
93
+ /** Serialize cooperating writers of the one private Git state root. */
94
+ export async function withPrivateStatePublicationSeat(repository, action, options) {
95
+ const path = privateStatePublicationSeatPath(repository);
96
+ const heldPaths = heldPrivateStateSeats.getStore();
97
+ if (heldPaths?.has(path) === true) {
98
+ throw new Error(`private-state publication seat reentered: ${path}`);
99
+ }
100
+ const held = await acquirePrivateStateSeatLock(repository, path, options?.timeoutMs);
101
+ const nested = new Set(heldPaths);
102
+ nested.add(path);
103
+ return await heldPrivateStateSeats.run(nested, () => runPrivateStateSeatAction(repository, held, action));
104
+ }
@@ -13,6 +13,8 @@ export type GitRepository = Readonly<{
13
13
  readonly signal?: AbortSignal;
14
14
  /** Test-only observation that this capability is waiting for the private-state seat. */
15
15
  readonly onPrivateStateSeatContention?: () => void;
16
+ /** Test-only hook after the private-state seat close attempt. */
17
+ readonly onPrivateStateSeatClose?: () => void;
16
18
  }>;
17
19
  /** Give one caller's Git capability view its cancellation boundary. */
18
20
  export declare function withGitAbortSignal(repository: GitRepository, signal: AbortSignal | undefined): GitRepository;
@@ -4,7 +4,6 @@ import type { ContractId, ContractState, SnapshotId } from "../core/facts/types.
4
4
  import type { GitDecodeChannel } from "./read-observation.js";
5
5
  import { type TargetCheckoutEffect, type TargetCheckoutLag } from "./target-placement.js";
6
6
  import { type WorktreeHookLag, type WorktreeHooks } from "./hooks.js";
7
- import { type RefMigrationConflict } from "./ref-migration.js";
8
7
  import type { UnsealedBytes } from "./terminal-seal.js";
9
8
  export type Effect = Readonly<{
10
9
  kind: "worktree";
@@ -54,7 +53,7 @@ export type ReconcileFailure = Readonly<{
54
53
  stage: "observation" | "effect";
55
54
  diagnostic: string;
56
55
  }>;
57
- export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | RefMigrationConflict | ReconcileFailure;
56
+ export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | ReconcileFailure;
58
57
  export type ReconcileResult = Readonly<{
59
58
  effects: readonly Effect[];
60
59
  lag: readonly ReconcileLag[];
@@ -10,7 +10,6 @@ import { acquireTargetPlacementFence, recoverTargetPlacement, } from "./target-p
10
10
  import { runCreateHooks } from "./hooks.js";
11
11
  import { followDependentManagedWorktree, retireConflictHandoff, worktreePath } from "./workspace.js";
12
12
  import { removeCollectableScratchWorktrees } from "./scratch.js";
13
- import { migrateContractCustodyRefs } from "./ref-migration.js";
14
13
  import { reconcileTerminalManagedWorktree, removeRef, updateRef } from "./terminal-reconcile.js";
15
14
  const pathExists = (path) => access(path).then(() => true, () => false);
16
15
  function deliveryRefFor(contract) {
@@ -174,11 +173,6 @@ async function reconcileWithTopology(input, state, topology) {
174
173
  await removeCollectableScratch(repository, topology, effects, lag);
175
174
  if (!state)
176
175
  return complete(effects, lag);
177
- const migration = await migrateContractCustodyRefs(repository, state.id);
178
- effects.push(...migration.effects);
179
- lag.push(...migration.lag);
180
- if (migration.lag.length > 0)
181
- return complete(effects, lag);
182
176
  const targetCheckouts = await reconcileTargetCheckouts(repository, state);
183
177
  effects.push(...targetCheckouts.effects);
184
178
  lag.push(...targetCheckouts.lag);
@@ -5,8 +5,6 @@ export declare const GIT_REF = "refs/heads/keiyaku-state";
5
5
  export declare const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
6
6
  export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
7
7
  export declare const HANDOFF_RECEIPT_REF_NAMESPACE = "refs/keiyaku/handoff";
8
- export declare const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
9
- export declare const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
10
8
  export declare const GIT_FORMAT_PATH = "meta/format.json";
11
9
  export declare const GIT_FORMAT_BYTES = "{\"version\":4}\n";
12
10
  export declare const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
@@ -1,5 +1,5 @@
1
- /* eslint-disable max-lines */
2
1
  import { isAbsolute, resolve } from "node:path";
2
+ import { realpath } from "node:fs/promises";
3
3
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
4
4
  import { gitObjectId } from "./identity.js";
5
5
  import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
@@ -8,8 +8,6 @@ export const GIT_REF = "refs/heads/keiyaku-state";
8
8
  export const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
9
9
  export const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
10
10
  export const HANDOFF_RECEIPT_REF_NAMESPACE = "refs/keiyaku/handoff";
11
- export const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
12
- export const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
13
11
  export const GIT_FORMAT_PATH = "meta/format.json";
14
12
  const CURRENT_FORMAT_VERSION = 4;
15
13
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
@@ -21,11 +19,7 @@ export function isKeiyakuOwnedRef(ref) {
21
19
  ref === CANDIDATE_PIN_REF_NAMESPACE ||
22
20
  ref.startsWith(`${CANDIDATE_PIN_REF_NAMESPACE}/`) ||
23
21
  ref === HANDOFF_RECEIPT_REF_NAMESPACE ||
24
- ref.startsWith(`${HANDOFF_RECEIPT_REF_NAMESPACE}/`) ||
25
- ref === LEGACY_DELIVERY_REF_NAMESPACE ||
26
- ref.startsWith(`${LEGACY_DELIVERY_REF_NAMESPACE}/`) ||
27
- ref === LEGACY_CANDIDATE_PIN_REF_NAMESPACE ||
28
- ref.startsWith(`${LEGACY_CANDIDATE_PIN_REF_NAMESPACE}/`));
22
+ ref.startsWith(`${HANDOFF_RECEIPT_REF_NAMESPACE}/`));
29
23
  }
30
24
  export class NoGitWorldError extends Error {
31
25
  path;
@@ -53,7 +47,11 @@ function worktreesFromPorcelain(output) {
53
47
  const branch = branchField === undefined ? null : branchField.slice("branch ".length);
54
48
  if (branch !== null && !branch.startsWith("refs/"))
55
49
  throw new Error("Git worktree porcelain output has an invalid branch");
56
- worktrees.push({ path: resolve(path), branch });
50
+ worktrees.push({
51
+ path: resolve(path),
52
+ branch,
53
+ prunable: record.some((field) => field.startsWith("prunable ")),
54
+ });
57
55
  record = [];
58
56
  };
59
57
  for (const field of fields) {
@@ -69,7 +67,17 @@ function worktreesFromPorcelain(output) {
69
67
  return worktrees;
70
68
  }
71
69
  export async function registeredWorktrees(repository) {
72
- return worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
70
+ return await Promise.all(worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"])).map(async (worktree) => {
71
+ const { prunable, ...registered } = worktree;
72
+ try {
73
+ return { ...registered, path: await realpath(registered.path) };
74
+ }
75
+ catch (error) {
76
+ if (prunable || error.code === "ENOENT")
77
+ return registered;
78
+ throw error;
79
+ }
80
+ }));
73
81
  }
74
82
  export async function registeredWorktreePaths(repository) {
75
83
  return (await registeredWorktrees(repository)).map((worktree) => worktree.path);
@@ -106,7 +114,7 @@ async function absoluteGitPath(repository, args, label) {
106
114
  const value = (await runGit(repository, ["rev-parse", "--path-format=absolute", ...args])).toString("utf8").trim();
107
115
  if (value.length === 0)
108
116
  throw new Error(`${label} is empty`);
109
- return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
117
+ return await realpath(resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value)));
110
118
  }
111
119
  export function commonGitDirectory(repository) {
112
120
  return repository.commonDirectory;
@@ -329,10 +337,7 @@ async function writePreparedTrees(repository, prepared) {
329
337
  }
330
338
  }
331
339
  }
332
- /** Copy only the changed paths' tree ancestors, preserving untouched subtrees by object ID. */
333
- export async function updateGitTree(repository, baseTree, changes) {
334
- const update = treeUpdate(changes);
335
- const bases = await baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
340
+ async function writeTreeUpdate(repository, baseTree, changes, update, bases) {
336
341
  const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
337
342
  if (representativeOid === undefined)
338
343
  throw new Error("cannot write an empty Git tree without an object format");
@@ -340,19 +345,16 @@ export async function updateGitTree(repository, baseTree, changes) {
340
345
  await writePreparedTrees(repository, prepared.trees);
341
346
  return prepared.root;
342
347
  }
348
+ /** Copy only the changed paths' tree ancestors, preserving untouched subtrees by object ID. */
349
+ export async function updateGitTree(repository, baseTree, changes) {
350
+ const update = treeUpdate(changes);
351
+ const bases = await baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
352
+ return writeTreeUpdate(repository, baseTree, changes, update, bases);
353
+ }
343
354
  /** Write a tree update from directory entries frozen with the admitted base tree. */
344
355
  export async function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
345
356
  const update = treeUpdate(changes);
346
- const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
347
- if (representativeOid === undefined)
348
- throw new Error("cannot write an empty Git tree without an object format");
349
- const prepared = prepareTreeUpdate({
350
- update,
351
- bases: directories,
352
- oidBytes: representativeOid.length / 2,
353
- });
354
- await writePreparedTrees(repository, prepared.trees);
355
- return prepared.root;
357
+ return writeTreeUpdate(repository, baseTree, changes, update, directories);
356
358
  }
357
359
  async function writeCommitObject(input) {
358
360
  const { repository, tree, parents, message, actor, email, at } = input;
@@ -0,0 +1,29 @@
1
+ import { type ContractId } from "../core/facts/types.js";
2
+ import type { PublicationFailed } from "./admission.js";
3
+ import type { HookFailure } from "./hooks.js";
4
+ import type { IntegrationPreparationRefusal } from "./integration.js";
5
+ import type { PrivateStateSeatCloseLag } from "./private-state-seat.js";
6
+ import type { ReconcileLag } from "./reconcile.js";
7
+ import type { WorktreeLeak } from "./scratch.js";
8
+ import type { AuditTargetAnswer, CheckoutNotFollowableRefusal } from "./target-placement.js";
9
+ import type { DirtyWorkspaceRefusal, WorkspaceDirtyDelta, WorktreeMissingRefusal } from "./tender.js";
10
+ export declare function decodeHookFailure(value: unknown): HookFailure;
11
+ export declare function decodeWorktreeLeak(value: unknown): WorktreeLeak;
12
+ export declare function decodePrivateStateSeatCloseLag(value: unknown): PrivateStateSeatCloseLag;
13
+ export declare function decodePublicationFailed(value: unknown): PublicationFailed;
14
+ export declare function decodeWorktreeWorkspace(value: unknown): Readonly<{
15
+ kind: "worktree";
16
+ path: string;
17
+ }>;
18
+ export declare function decodeWorkspaceDirtyDelta(value: unknown): WorkspaceDirtyDelta;
19
+ export declare function decodeWorktreeMissingRefusal(value: unknown): WorktreeMissingRefusal;
20
+ export declare function decodeUnmergedPathsRefusal(value: unknown): Readonly<{
21
+ kind: "unmerged-paths";
22
+ contractId: ContractId;
23
+ paths: readonly string[];
24
+ }>;
25
+ export declare function decodeDirtyWorkspaceRefusal(value: unknown): DirtyWorkspaceRefusal;
26
+ export declare function decodeIntegrationPreparationRefusal(value: unknown): IntegrationPreparationRefusal;
27
+ export declare function decodeCheckoutNotFollowableRefusal(value: unknown): CheckoutNotFollowableRefusal;
28
+ export declare function decodeAuditTargetAnswer(value: unknown): AuditTargetAnswer;
29
+ export declare function decodeGitReconcileLag(value: unknown): ReconcileLag;
@@ -0,0 +1,306 @@
1
+ import { contractId, snapshotId } from "../core/facts/types.js";
2
+ import { mintSnapshotId } from "./identity.js";
3
+ function fail(message = "malformed git result") {
4
+ throw new Error(message);
5
+ }
6
+ function record(value, required, optional = []) {
7
+ if (value === null || typeof value !== "object" || Array.isArray(value))
8
+ fail();
9
+ const object = value;
10
+ const allowed = new Set([...required, ...optional]);
11
+ for (const key of Object.keys(object))
12
+ if (!allowed.has(key))
13
+ fail();
14
+ for (const key of required)
15
+ if (!(key in object))
16
+ fail();
17
+ return object;
18
+ }
19
+ function nonblank(value) {
20
+ if (typeof value !== "string" || value.trim() === "")
21
+ fail();
22
+ return value;
23
+ }
24
+ function integer(value) {
25
+ if (typeof value !== "number" || !Number.isInteger(value))
26
+ fail();
27
+ return value;
28
+ }
29
+ function nonnegativeInteger(value) {
30
+ const n = integer(value);
31
+ if (n < 0)
32
+ fail();
33
+ return n;
34
+ }
35
+ function boolean(value) {
36
+ if (typeof value !== "boolean")
37
+ fail();
38
+ return value;
39
+ }
40
+ function strings(value) {
41
+ if (!Array.isArray(value))
42
+ fail();
43
+ return value.map(nonblank);
44
+ }
45
+ function decodeContractId(value) {
46
+ try {
47
+ return contractId(nonblank(value));
48
+ }
49
+ catch {
50
+ fail();
51
+ }
52
+ }
53
+ function decodeSnapshotId(value) {
54
+ try {
55
+ return snapshotId(nonblank(value));
56
+ }
57
+ catch {
58
+ fail();
59
+ }
60
+ }
61
+ function decodeGitSnapshotId(value) {
62
+ try {
63
+ return mintSnapshotId(nonblank(value));
64
+ }
65
+ catch {
66
+ fail();
67
+ }
68
+ }
69
+ export function decodeHookFailure(value) {
70
+ const object = record(value, ["kind"], ["code", "stdout", "stderr", "truncated", "diagnostic"]);
71
+ if (object.kind === "timeout" || object.kind === "unknown-exit") {
72
+ if ("code" in object || "stdout" in object || "stderr" in object || "truncated" in object || "diagnostic" in object)
73
+ fail();
74
+ return { kind: object.kind };
75
+ }
76
+ if (object.kind === "spawn-error") {
77
+ if ("code" in object || "stdout" in object || "stderr" in object || "truncated" in object)
78
+ fail();
79
+ return { kind: "spawn-error", diagnostic: nonblank(object.diagnostic) };
80
+ }
81
+ if (object.kind !== "exit")
82
+ fail();
83
+ if ("diagnostic" in object)
84
+ fail();
85
+ return {
86
+ kind: "exit",
87
+ code: integer(object.code),
88
+ stdout: typeof object.stdout === "string" ? object.stdout : fail(),
89
+ stderr: typeof object.stderr === "string" ? object.stderr : fail(),
90
+ truncated: boolean(object.truncated),
91
+ };
92
+ }
93
+ export function decodeWorktreeLeak(value) {
94
+ const object = record(value, ["path", "diagnostic"]);
95
+ if (typeof object.path !== "string" || typeof object.diagnostic !== "string")
96
+ fail();
97
+ return { path: object.path, diagnostic: object.diagnostic };
98
+ }
99
+ export function decodePrivateStateSeatCloseLag(value) {
100
+ const object = record(value, ["kind", "diagnostic"]);
101
+ if (object.kind !== "private-state-seat-close-failed")
102
+ fail();
103
+ return { kind: "private-state-seat-close-failed", diagnostic: nonblank(object.diagnostic) };
104
+ }
105
+ export function decodePublicationFailed(value) {
106
+ const object = record(value, ["kind", "diagnostic"]);
107
+ if (object.kind !== "publication-failed")
108
+ fail();
109
+ if (typeof object.diagnostic !== "string")
110
+ fail();
111
+ return { kind: "publication-failed", diagnostic: object.diagnostic };
112
+ }
113
+ export function decodeWorktreeWorkspace(value) {
114
+ const object = record(value, ["kind", "path"]);
115
+ if (object.kind !== "worktree")
116
+ fail();
117
+ return { kind: "worktree", path: nonblank(object.path) };
118
+ }
119
+ export function decodeWorkspaceDirtyDelta(value) {
120
+ const object = record(value, ["staged", "unstaged", "untracked", "shortStat"]);
121
+ const shortStat = record(object.shortStat, ["filesChanged", "insertions", "deletions"]);
122
+ return {
123
+ staged: Array.isArray(object.staged)
124
+ ? object.staged.map((path) => (typeof path === "string" ? path : fail()))
125
+ : fail(),
126
+ unstaged: Array.isArray(object.unstaged)
127
+ ? object.unstaged.map((path) => (typeof path === "string" ? path : fail()))
128
+ : fail(),
129
+ untracked: Array.isArray(object.untracked)
130
+ ? object.untracked.map((path) => (typeof path === "string" ? path : fail()))
131
+ : fail(),
132
+ shortStat: {
133
+ filesChanged: integer(shortStat.filesChanged),
134
+ insertions: integer(shortStat.insertions),
135
+ deletions: integer(shortStat.deletions),
136
+ },
137
+ };
138
+ }
139
+ export function decodeWorktreeMissingRefusal(value) {
140
+ const object = record(value, ["kind", "contractId"]);
141
+ if (object.kind !== "worktree-missing")
142
+ fail();
143
+ return { kind: "worktree-missing", contractId: decodeContractId(object.contractId) };
144
+ }
145
+ export function decodeUnmergedPathsRefusal(value) {
146
+ const object = record(value, ["kind", "contractId", "paths"]);
147
+ if (object.kind !== "unmerged-paths")
148
+ fail();
149
+ return { kind: "unmerged-paths", contractId: decodeContractId(object.contractId), paths: strings(object.paths) };
150
+ }
151
+ export function decodeDirtyWorkspaceRefusal(value) {
152
+ const object = record(value, ["kind", "contractId", "staged", "unstaged", "untracked", "submodules", "shortStat"]);
153
+ if (object.kind !== "dirty-workspace")
154
+ fail();
155
+ const shortStat = record(object.shortStat, ["filesChanged", "insertions", "deletions"]);
156
+ return {
157
+ kind: "dirty-workspace",
158
+ contractId: decodeContractId(object.contractId),
159
+ staged: strings(object.staged),
160
+ unstaged: strings(object.unstaged),
161
+ untracked: strings(object.untracked),
162
+ submodules: strings(object.submodules),
163
+ shortStat: {
164
+ filesChanged: nonnegativeInteger(shortStat.filesChanged),
165
+ insertions: nonnegativeInteger(shortStat.insertions),
166
+ deletions: nonnegativeInteger(shortStat.deletions),
167
+ },
168
+ };
169
+ }
170
+ export function decodeIntegrationPreparationRefusal(value) {
171
+ const object = record(value, ["kind", "contractId"], ["reason", "targetHead", "conflictPaths", "requiredGit"]);
172
+ if (object.kind === "integration-unsupported") {
173
+ if (object.requiredGit !== "2.38")
174
+ fail();
175
+ if ("reason" in object || "targetHead" in object || "conflictPaths" in object)
176
+ fail();
177
+ return { kind: "integration-unsupported", contractId: decodeContractId(object.contractId), requiredGit: "2.38" };
178
+ }
179
+ if (object.kind !== "integration-failed")
180
+ fail();
181
+ if ("requiredGit" in object)
182
+ fail();
183
+ if (object.reason !== "not-based-on-target" &&
184
+ object.reason !== "unrelated-histories" &&
185
+ object.reason !== "conflict")
186
+ fail();
187
+ return {
188
+ kind: "integration-failed",
189
+ contractId: decodeContractId(object.contractId),
190
+ reason: object.reason,
191
+ targetHead: decodeSnapshotId(object.targetHead),
192
+ ...(object.conflictPaths === undefined ? {} : { conflictPaths: strings(object.conflictPaths) }),
193
+ };
194
+ }
195
+ export function decodeCheckoutNotFollowableRefusal(value) {
196
+ const object = record(value, ["kind", "contractId", "target", "path", "reason", "paths"]);
197
+ if (object.kind !== "checkout-not-followable")
198
+ fail();
199
+ if (object.reason !== "staged" && object.reason !== "conflict" && object.reason !== "untracked")
200
+ fail();
201
+ return {
202
+ kind: "checkout-not-followable",
203
+ contractId: decodeContractId(object.contractId),
204
+ target: nonblank(object.target),
205
+ path: nonblank(object.path),
206
+ reason: object.reason,
207
+ paths: strings(object.paths),
208
+ };
209
+ }
210
+ export function decodeAuditTargetAnswer(value) {
211
+ if (value === null || typeof value !== "object" || Array.isArray(value))
212
+ fail();
213
+ const kind = value.kind;
214
+ if (kind === "placeable") {
215
+ const object = record(value, ["kind", "ref", "head"]);
216
+ return { kind: "placeable", ref: nonblank(object.ref), head: decodeSnapshotId(object.head) };
217
+ }
218
+ if (kind === "moved") {
219
+ const object = record(value, ["kind", "ref", "expected", "observed"]);
220
+ return {
221
+ kind: "moved",
222
+ ref: nonblank(object.ref),
223
+ expected: decodeSnapshotId(object.expected),
224
+ observed: object.observed === null ? null : decodeSnapshotId(object.observed),
225
+ };
226
+ }
227
+ if (kind === "refused") {
228
+ const object = record(value, ["kind", "refusal"]);
229
+ return { kind: "refused", refusal: decodeCheckoutNotFollowableRefusal(object.refusal) };
230
+ }
231
+ const object = record(value, ["kind", "diagnostic"]);
232
+ if (object.kind !== "failed")
233
+ fail();
234
+ return { kind: "failed", diagnostic: nonblank(object.diagnostic) };
235
+ }
236
+ export function decodeGitReconcileLag(value) {
237
+ if (value === null || typeof value !== "object" || Array.isArray(value))
238
+ fail();
239
+ const kind = value.kind;
240
+ if (kind === "worktree-retained") {
241
+ const object = record(value, ["kind", "path"]);
242
+ return { kind: "worktree-retained", path: nonblank(object.path) };
243
+ }
244
+ if (kind === "worktree-follow-retained") {
245
+ const object = record(value, ["kind", "path", "tender", "head", "reason"], ["paths"]);
246
+ if (object.reason !== "head-moved" &&
247
+ object.reason !== "head-attached" &&
248
+ object.reason !== "operation-in-progress" &&
249
+ object.reason !== "unsupported-parent-shape")
250
+ fail();
251
+ const lag = {
252
+ kind: "worktree-follow-retained",
253
+ path: nonblank(object.path),
254
+ tender: decodeGitSnapshotId(object.tender),
255
+ head: decodeGitSnapshotId(object.head),
256
+ reason: object.reason,
257
+ ...(object.paths === undefined ? {} : { paths: strings(object.paths) }),
258
+ };
259
+ return lag;
260
+ }
261
+ if (kind === "unsealed-bytes") {
262
+ const object = record(value, ["kind", "path", "paths"], ["head"]);
263
+ const lag = {
264
+ kind: "unsealed-bytes",
265
+ path: nonblank(object.path),
266
+ paths: strings(object.paths),
267
+ ...(object.head === undefined ? {} : { head: decodeGitSnapshotId(object.head) }),
268
+ };
269
+ return lag;
270
+ }
271
+ if (kind === "target-checkout-retained") {
272
+ const object = record(value, ["kind", "path", "target", "diagnostic"]);
273
+ const lag = {
274
+ kind: "target-checkout-retained",
275
+ path: nonblank(object.path),
276
+ target: nonblank(object.target),
277
+ diagnostic: nonblank(object.diagnostic),
278
+ };
279
+ return lag;
280
+ }
281
+ if (kind === "reconcile-failed") {
282
+ const object = record(value, ["kind", "stage", "diagnostic"]);
283
+ if (object.stage !== "observation" && object.stage !== "effect")
284
+ fail();
285
+ const lag = {
286
+ kind: "reconcile-failed",
287
+ stage: object.stage,
288
+ diagnostic: nonblank(object.diagnostic),
289
+ };
290
+ return lag;
291
+ }
292
+ const object = record(value, ["kind", "phase", "path", "command", "name", "failure"]);
293
+ if (object.kind !== "worktree-hook-failed")
294
+ fail();
295
+ if (object.phase !== "create" && object.phase !== "destroy")
296
+ fail();
297
+ const lag = {
298
+ kind: "worktree-hook-failed",
299
+ phase: object.phase,
300
+ path: nonblank(object.path),
301
+ command: nonnegativeInteger(object.command),
302
+ name: nonblank(object.name),
303
+ failure: decodeHookFailure(object.failure),
304
+ };
305
+ return lag;
306
+ }
@@ -2,7 +2,7 @@ import { lstat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { acquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
4
4
  import { gitObjectId, gitObjectIdForSnapshot, gitRefLocator, mintSnapshotId } from "./identity.js";
5
- import { commonGitDirectory, readRef, registeredWorktrees } from "./repository.js";
5
+ import { commonGitDirectory, decodeGitNameOnly, readRef, registeredWorktrees } from "./repository.js";
6
6
  import { consumeGitStdout, GitPlumbingError, runGit } from "./process.js";
7
7
  import { captureWorkspaceTree } from "./workspace.js";
8
8
  export async function observeTargetHead(repository, target) {
@@ -12,14 +12,8 @@ export async function observeTargetHead(repository, target) {
12
12
  function diagnostic(error) {
13
13
  return error instanceof Error ? error.message : String(error);
14
14
  }
15
- function nulPaths(bytes) {
16
- const fields = bytes.toString("utf8").split("\0");
17
- if (fields.at(-1) !== "")
18
- throw new Error("Git path output is not NUL terminated");
19
- return [...new Set(fields.slice(0, -1))].sort();
20
- }
21
15
  async function gitPaths(repository, path, args) {
22
- return nulPaths(await runGit(repository, ["-C", path, ...args]));
16
+ return [...new Set(decodeGitNameOnly(await runGit(repository, ["-C", path, ...args])))].sort();
23
17
  }
24
18
  function literalPath(path) {
25
19
  return `:(literal)${path}`;
@@ -15,6 +15,8 @@ export type WorkspaceTree = Readonly<{
15
15
  changes: WorkspaceChanges;
16
16
  }>;
17
17
  export declare function worktreePath(repository: GitRepository, place: string): string;
18
+ /** Check out a captured candidate tree in the appointed detached worktree. */
19
+ export declare function checkoutDetachedSnapshot(repository: GitRepository, workspace: string, snapshot: SnapshotId): Promise<void>;
18
20
  export type DependentWorktreeFollow = Readonly<{
19
21
  kind: "followed";
20
22
  before: SnapshotId;
@@ -8,6 +8,10 @@ const WORKTREE_DIRECTORY = [".keiyaku", "wt"];
8
8
  export function worktreePath(repository, place) {
9
9
  return resolve(repository.primaryWorktree, ...WORKTREE_DIRECTORY, place);
10
10
  }
11
+ /** Check out a captured candidate tree in the appointed detached worktree. */
12
+ export async function checkoutDetachedSnapshot(repository, workspace, snapshot) {
13
+ await runGit(repository, ["-C", workspace, "checkout", "--quiet", "--force", "--detach", gitObjectId(snapshot)]);
14
+ }
11
15
  async function workspaceRevision(repository, workspace, revision) {
12
16
  try {
13
17
  const value = (await runGit(repository, ["-C", workspace, "rev-parse", "--verify", "--quiet", revision]))