@astrosheep/keiyaku 4.5.7 → 4.5.9

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 (244) hide show
  1. package/README.md +10 -0
  2. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/opencode.js +3 -2
  5. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +12 -11
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +33 -12
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-babysit/SKILL.md +35 -96
  9. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +28 -49
  10. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +21 -15
  11. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +148 -89
  12. package/build/src/akuma/abort.d.ts +2 -1
  13. package/build/src/akuma/abort.js +6 -23
  14. package/build/src/akuma/akuma-handle.js +4 -2
  15. package/build/src/akuma/akuma-observe.d.ts +2 -0
  16. package/build/src/akuma/akuma-observe.js +1 -0
  17. package/build/src/akuma/akuma-product.js +9 -7
  18. package/build/src/akuma/akuma.d.ts +392 -9
  19. package/build/src/akuma/akuma.js +31 -1
  20. package/build/src/akuma/allowed.d.ts +1 -1
  21. package/build/src/akuma/allowed.js +1 -0
  22. package/build/src/akuma/archetype.js +1 -1
  23. package/build/src/akuma/body.d.ts +8 -5
  24. package/build/src/akuma/body.js +12 -5
  25. package/build/src/akuma/call-request.d.ts +80 -0
  26. package/build/src/akuma/call-request.js +211 -0
  27. package/build/src/akuma/heart/facts.d.ts +16 -78
  28. package/build/src/akuma/heart/facts.js +11 -0
  29. package/build/src/akuma/heart/index.d.ts +3 -2
  30. package/build/src/akuma/heart/index.js +4 -1
  31. package/build/src/akuma/heart/request-authority.d.ts +5 -4
  32. package/build/src/akuma/heart/request-authority.js +20 -34
  33. package/build/src/akuma/heart/request-rows.js +12 -23
  34. package/build/src/akuma/heart/schema.d.ts +1 -1
  35. package/build/src/akuma/heart/schema.js +5 -13
  36. package/build/src/akuma/index.d.ts +1 -1
  37. package/build/src/akuma/index.js +1 -1
  38. package/build/src/akuma/projection-read.d.ts +1 -0
  39. package/build/src/akuma/projection-read.js +8 -1
  40. package/build/src/akuma/projection.d.ts +907 -137
  41. package/build/src/akuma/projection.js +193 -9
  42. package/build/src/akuma/provider.d.ts +30 -3
  43. package/build/src/akuma/provider.js +111 -0
  44. package/build/src/akuma/providers/acp/core.d.ts +2 -2
  45. package/build/src/akuma/providers/acp/core.js +13 -6
  46. package/build/src/akuma/providers/acp/index.js +5 -4
  47. package/build/src/akuma/providers/claude/events.d.ts +2 -2
  48. package/build/src/akuma/providers/claude/events.js +16 -3
  49. package/build/src/akuma/providers/claude/index.d.ts +3 -3
  50. package/build/src/akuma/providers/claude/index.js +16 -5
  51. package/build/src/akuma/providers/codex-app-server/events.d.ts +2 -2
  52. package/build/src/akuma/providers/codex-app-server/events.js +25 -4
  53. package/build/src/akuma/providers/codex-app-server/index.js +18 -6
  54. package/build/src/akuma/providers/grok-build/index.js +5 -4
  55. package/build/src/akuma/providers/opencode-sdk/index.js +66 -25
  56. package/build/src/akuma/providers/opencode-sdk/session.d.ts +3 -2
  57. package/build/src/akuma/providers/opencode-sdk/session.js +28 -11
  58. package/build/src/akuma/providers/pi/index.js +72 -58
  59. package/build/src/akuma/request-lifecycle.d.ts +4 -10
  60. package/build/src/akuma/request-lifecycle.js +7 -5
  61. package/build/src/akuma/request-rendezvous.d.ts +26 -0
  62. package/build/src/akuma/request-rendezvous.js +100 -0
  63. package/build/src/akuma/request-serve.d.ts +6 -66
  64. package/build/src/akuma/request-serve.js +83 -210
  65. package/build/src/akuma/request-wire.d.ts +109 -115
  66. package/build/src/akuma/request-wire.js +63 -318
  67. package/build/src/akuma/requests.d.ts +19 -47
  68. package/build/src/akuma/requests.js +51 -107
  69. package/build/src/akuma/turn-drive.d.ts +5 -3
  70. package/build/src/akuma/turn-drive.js +39 -11
  71. package/build/src/akuma-body.d.ts +7 -2
  72. package/build/src/akuma-body.js +68 -46
  73. package/build/src/body/amend.js +18 -19
  74. package/build/src/cli/accepted.d.ts +1 -1
  75. package/build/src/cli/accepted.js +2 -2
  76. package/build/src/cli/commands/akuma-invoke.d.ts +2 -0
  77. package/build/src/cli/commands/akuma-invoke.js +17 -9
  78. package/build/src/cli/commands/akuma.js +8 -8
  79. package/build/src/cli/commands/contract-invoke.d.ts +3 -2
  80. package/build/src/cli/commands/contract-invoke.js +42 -30
  81. package/build/src/cli/commands/contract.d.ts +16 -20
  82. package/build/src/cli/commands/contract.js +58 -25
  83. package/build/src/cli/commands/install.d.ts +2 -1
  84. package/build/src/cli/commands/install.js +3 -2
  85. package/build/src/cli/commands/task-invoke.d.ts +3 -0
  86. package/build/src/cli/commands/task-invoke.js +23 -140
  87. package/build/src/cli/commands/task.js +54 -20
  88. package/build/src/cli/invoke.js +39 -25
  89. package/build/src/cli/main.js +2 -1
  90. package/build/src/cli/parse.d.ts +2 -2
  91. package/build/src/cli/parse.js +56 -27
  92. package/build/src/cli/render/akuma-activity.d.ts +3 -0
  93. package/build/src/cli/render/akuma-activity.js +45 -29
  94. package/build/src/cli/render/akuma-tool.d.ts +5 -1
  95. package/build/src/cli/render/akuma.js +4 -8
  96. package/build/src/cli/render/audit.js +1 -1
  97. package/build/src/cli/render/catalog.js +80 -21
  98. package/build/src/cli/render/contract-observation.d.ts +2 -0
  99. package/build/src/cli/render/contract-observation.js +11 -0
  100. package/build/src/cli/render/contract.js +35 -62
  101. package/build/src/cli/render/kanshi-akuma.d.ts +8 -0
  102. package/build/src/cli/render/kanshi-akuma.js +132 -0
  103. package/build/src/cli/render/kanshi.js +131 -213
  104. package/build/src/cli/render/receipt.d.ts +1 -2
  105. package/build/src/cli/render/receipt.js +76 -26
  106. package/build/src/cli/render/refusal.js +2 -10
  107. package/build/src/cli/render/region.js +5 -2
  108. package/build/src/cli/render/settings.d.ts +1 -1
  109. package/build/src/cli/render/settings.js +27 -8
  110. package/build/src/cli/render/task.js +22 -5
  111. package/build/src/cli/render/terminal.d.ts +32 -1
  112. package/build/src/cli/render/terminal.js +77 -1
  113. package/build/src/cli/result.d.ts +3 -3
  114. package/build/src/cli/runtime.js +4 -2
  115. package/build/src/cli/selectors.d.ts +2 -1
  116. package/build/src/cli/selectors.js +4 -3
  117. package/build/src/cli/square-edge.js +120 -36
  118. package/build/src/cli/usage.js +2 -1
  119. package/build/src/contract-guidance.d.ts +5 -0
  120. package/build/src/contract-guidance.js +121 -0
  121. package/build/src/contract-worktree.d.ts +0 -1
  122. package/build/src/contract-worktree.js +32 -83
  123. package/build/src/coordination/sqlite-transaction-lock.d.ts +1 -0
  124. package/build/src/coordination/sqlite-transaction-lock.js +5 -0
  125. package/build/src/core/facts/codec.d.ts +2 -0
  126. package/build/src/core/facts/codec.js +4 -0
  127. package/build/src/core/verbs/placement.d.ts +6 -1
  128. package/build/src/core/verbs/placement.js +5 -3
  129. package/build/src/dispatch/index.d.ts +0 -2
  130. package/build/src/dispatch/index.js +54 -55
  131. package/build/src/git/admission.js +2 -2
  132. package/build/src/git/hooks.d.ts +9 -15
  133. package/build/src/git/hooks.js +23 -317
  134. package/build/src/git/nuke.d.ts +2 -1
  135. package/build/src/git/nuke.js +39 -21
  136. package/build/src/git/private-state-seat.d.ts +12 -0
  137. package/build/src/git/private-state-seat.js +39 -0
  138. package/build/src/git/process.d.ts +6 -0
  139. package/build/src/git/process.js +26 -11
  140. package/build/src/git/read-observation.js +29 -20
  141. package/build/src/git/reconcile.d.ts +13 -1
  142. package/build/src/git/reconcile.js +70 -11
  143. package/build/src/git/ref-migration.d.ts +22 -0
  144. package/build/src/git/ref-migration.js +55 -0
  145. package/build/src/git/repository.d.ts +10 -6
  146. package/build/src/git/repository.js +16 -8
  147. package/build/src/git/target-placement.d.ts +1 -0
  148. package/build/src/git/target-placement.js +5 -0
  149. package/build/src/git/tender.d.ts +1 -0
  150. package/build/src/git/terminal-reconcile.d.ts +1 -1
  151. package/build/src/git/terminal-reconcile.js +22 -20
  152. package/build/src/git/workspace.d.ts +14 -0
  153. package/build/src/git/workspace.js +44 -0
  154. package/build/src/kanshi/index.d.ts +1 -1
  155. package/build/src/kanshi/read.js +16 -16
  156. package/build/src/kanshi/report.d.ts +2 -2
  157. package/build/src/library/address.d.ts +9 -3
  158. package/build/src/library/address.js +3 -2
  159. package/build/src/library/akuma-creation.d.ts +4 -3
  160. package/build/src/library/akuma-creation.js +19 -13
  161. package/build/src/library/audit.d.ts +14 -6
  162. package/build/src/library/audit.js +13 -23
  163. package/build/src/library/catalog.d.ts +3 -1
  164. package/build/src/library/catalog.js +20 -6
  165. package/build/src/library/composition.d.ts +385 -0
  166. package/build/src/library/composition.js +28 -0
  167. package/build/src/library/configuration.js +8 -2
  168. package/build/src/library/continuation.js +66 -30
  169. package/build/src/library/contract-bind.js +10 -2
  170. package/build/src/library/contract-forwarding-reconciliation-result.d.ts +102 -0
  171. package/build/src/library/contract-forwarding-reconciliation-result.js +92 -0
  172. package/build/src/library/contract-forwarding-result.d.ts +4464 -0
  173. package/build/src/library/contract-forwarding-result.js +521 -0
  174. package/build/src/library/contract-forwarding.d.ts +71 -0
  175. package/build/src/library/contract-forwarding.js +114 -0
  176. package/build/src/library/contract-handle.d.ts +38 -0
  177. package/build/src/library/contract-handle.js +314 -0
  178. package/build/src/library/contract-operations.d.ts +214 -69
  179. package/build/src/library/contract-operations.js +189 -118
  180. package/build/src/library/contract-types.d.ts +7 -3
  181. package/build/src/library/contract.d.ts +21 -34
  182. package/build/src/library/contract.js +29 -287
  183. package/build/src/library/fleet-result.d.ts +1580 -0
  184. package/build/src/library/fleet-result.js +98 -0
  185. package/build/src/library/fleet.d.ts +112 -54
  186. package/build/src/library/fleet.js +211 -71
  187. package/build/src/library/input.d.ts +3 -1
  188. package/build/src/library/input.js +11 -1
  189. package/build/src/library/keiyaku.d.ts +388 -20
  190. package/build/src/library/keiyaku.js +13 -21
  191. package/build/src/library/mutation.d.ts +3 -3
  192. package/build/src/library/mutation.js +5 -9
  193. package/build/src/library/nuke.d.ts +1 -1
  194. package/build/src/library/nuke.js +4 -3
  195. package/build/src/library/reconcile.d.ts +4 -0
  196. package/build/src/library/reconcile.js +2 -0
  197. package/build/src/library/refusal.d.ts +0 -9
  198. package/build/src/library/refusal.js +0 -7
  199. package/build/src/protocol/amend.js +56 -50
  200. package/build/src/protocol/attempt.d.ts +2 -0
  201. package/build/src/protocol/attempt.js +8 -1
  202. package/build/src/protocol/completion.js +7 -0
  203. package/build/src/protocol/deliver.js +6 -2
  204. package/build/src/protocol/operations.d.ts +4 -1
  205. package/build/src/protocol/operations.js +5 -2
  206. package/build/src/protocol/placement.d.ts +1 -0
  207. package/build/src/protocol/placement.js +52 -38
  208. package/build/src/protocol/read/observation.d.ts +1 -4
  209. package/build/src/protocol/read/observation.js +2 -15
  210. package/build/src/protocol/reintegrate.js +62 -58
  211. package/build/src/protocol/review.js +5 -0
  212. package/build/src/protocol/run.js +17 -13
  213. package/build/src/runtime/proc/run.d.ts +9 -0
  214. package/build/src/runtime/proc/run.js +51 -4
  215. package/build/src/runtime/proc/termination.js +26 -22
  216. package/build/src/runtime/proc/windows-launch.exe +0 -0
  217. package/build/src/settlement/holder.d.ts +2 -1
  218. package/build/src/settlement/holder.js +15 -4
  219. package/build/src/settlement/settle.js +99 -33
  220. package/build/src/task/board.d.ts +68 -27
  221. package/build/src/task/board.js +31 -1
  222. package/build/src/task/catalog.d.ts +4 -0
  223. package/build/src/task/catalog.js +23 -0
  224. package/build/src/task/compose-language.js +33 -7
  225. package/build/src/task/compose-parser.d.ts +1 -1
  226. package/build/src/task/compose-parser.js +1 -1
  227. package/build/src/task/compose.js +2 -0
  228. package/build/src/task/index.d.ts +12 -4
  229. package/build/src/task/index.js +97 -16
  230. package/build/src/task/mutation-result.d.ts +923 -0
  231. package/build/src/task/mutation-result.js +140 -0
  232. package/build/src/task/mutation.d.ts +552 -28
  233. package/build/src/task/mutation.js +304 -97
  234. package/build/src/task/operations.d.ts +1 -4
  235. package/build/src/task/operations.js +0 -6
  236. package/build/src/task/public.d.ts +2 -0
  237. package/build/src/task/public.js +1 -0
  238. package/build/src/workspace-place.d.ts +7 -2
  239. package/build/src/workspace-place.js +34 -24
  240. package/build/src/world.d.ts +1 -0
  241. package/build/src/world.js +11 -0
  242. package/package.json +8 -6
  243. package/build/src/akuma/request-execution.d.ts +0 -8
  244. package/build/src/akuma/request-execution.js +0 -98
@@ -1,5 +1,5 @@
1
- import { spawn } from "node:child_process";
2
1
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
2
+ import { spawnCancellableProcess } from "../runtime/proc/run.js";
3
3
  import { gitObjectId } from "./identity.js";
4
4
  import { parseTreeObject, validPath } from "./tree.js";
5
5
  import { GIT_FORMAT_BYTES, GIT_FORMAT_PATH, GIT_REF, readRef } from "./repository.js";
@@ -36,11 +36,18 @@ function streamCursor(stream) {
36
36
  };
37
37
  return { line, exact };
38
38
  }
39
+ async function batchObjects(oids, object) {
40
+ const unique = [...new Set(oids)];
41
+ for (const oid of unique)
42
+ gitObjectId(oid, "Git object");
43
+ const entries = await Promise.all(unique.map(async (oid) => [oid, await object(oid)]));
44
+ return new Map(entries);
45
+ }
39
46
  function batchChild(repository) {
40
- return spawn(repository.gitPath, ["cat-file", "--batch"], {
47
+ return spawnCancellableProcess({
48
+ argv: [repository.gitPath, "cat-file", "--batch"],
41
49
  cwd: repository.effectiveCwd,
42
- stdio: ["pipe", "pipe", "pipe"],
43
- windowsHide: true,
50
+ ...(repository.signal === undefined ? {} : { signal: repository.signal }),
44
51
  });
45
52
  }
46
53
  function batchError(child, stderr, message, status) {
@@ -51,8 +58,18 @@ function batchError(child, stderr, message, status) {
51
58
  pid: child.pid ?? null,
52
59
  });
53
60
  }
61
+ async function closeBatchProcess(process, child, closed, spawnError, stderr) {
62
+ if (!child.stdin.destroyed)
63
+ child.stdin.end();
64
+ const outcome = await Promise.race([closed, process.terminationFailure]);
65
+ await process.waitTermination();
66
+ if (spawnError !== null || outcome.code !== 0) {
67
+ throw batchError(child, stderr, process.cancelled() ? "git process cancelled" : "git cat-file --batch did not close cleanly", outcome.code);
68
+ }
69
+ }
54
70
  function batchObjectReader(repository) {
55
- const child = batchChild(repository);
71
+ const process = batchChild(repository);
72
+ const child = process.child;
56
73
  const cursor = streamCursor(child.stdout);
57
74
  const cache = new Map();
58
75
  const stderr = [];
@@ -101,9 +118,10 @@ function batchObjectReader(repository) {
101
118
  catch (error) {
102
119
  if (failure !== null)
103
120
  throw failure;
104
- child.kill();
121
+ await process.terminate(true);
105
122
  await closed;
106
- failure = batchError(child, stderr, error instanceof Error ? error.message : String(error), child.exitCode);
123
+ await process.waitTermination();
124
+ failure = batchError(child, stderr, process.cancelled() ? "git process cancelled" : error instanceof Error ? error.message : String(error), process.cancelled() ? null : child.exitCode);
107
125
  throw failure;
108
126
  }
109
127
  };
@@ -116,23 +134,14 @@ function batchObjectReader(repository) {
116
134
  cache.set(oid, requested);
117
135
  return requested;
118
136
  };
119
- const objects = async (oids) => {
120
- const unique = [...new Set(oids)];
121
- for (const oid of unique)
122
- gitObjectId(oid, "Git object");
123
- const entries = await Promise.all(unique.map(async (oid) => [oid, await object(oid)]));
124
- return new Map(entries);
125
- };
137
+ const objects = (oids) => batchObjects(oids, object);
126
138
  const close = async () => {
127
139
  await tail.catch(() => undefined);
128
- if (failure !== null)
140
+ if (failure !== null) {
141
+ await process.waitTermination();
129
142
  return;
130
- if (!child.stdin.destroyed)
131
- child.stdin.end();
132
- const outcome = await closed;
133
- if (spawnError !== null || outcome.code !== 0) {
134
- throw batchError(child, stderr, "git cat-file --batch did not close cleanly", outcome.code);
135
143
  }
144
+ await closeBatchProcess(process, child, closed, spawnError, stderr);
136
145
  };
137
146
  return { objects, close };
138
147
  }
@@ -4,6 +4,7 @@ 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";
7
8
  import type { UnsealedBytes } from "./terminal-seal.js";
8
9
  export type Effect = Readonly<{
9
10
  kind: "worktree";
@@ -46,16 +47,21 @@ type WorktreeFollowRetained = Readonly<{
46
47
  tender: SnapshotId;
47
48
  head: SnapshotId;
48
49
  reason: "head-moved" | "head-attached" | "operation-in-progress" | "unsupported-parent-shape";
50
+ paths?: readonly string[];
49
51
  }>;
50
52
  export type ReconcileFailure = Readonly<{
51
53
  kind: "reconcile-failed";
52
54
  stage: "observation" | "effect";
53
55
  diagnostic: string;
54
56
  }>;
55
- export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | ReconcileFailure;
57
+ export type ReconcileLag = WorktreeRetained | WorktreeFollowRetained | UnsealedBytes | TargetCheckoutLag | WorktreeHookLag | RefMigrationConflict | ReconcileFailure;
56
58
  export type ReconcileResult = Readonly<{
57
59
  effects: readonly Effect[];
58
60
  lag: readonly ReconcileLag[];
61
+ hookRuns?: readonly {
62
+ phase: "create" | "destroy";
63
+ name: string;
64
+ }[];
59
65
  }>;
60
66
  type ReconcileBatchItem = Readonly<{
61
67
  contract: ContractId;
@@ -72,8 +78,14 @@ export type WorktreeTopology = Readonly<{
72
78
  export type ReconcileAccumulation = Readonly<{
73
79
  effects: Effect[];
74
80
  lag: ReconcileLag[];
81
+ hookRuns: {
82
+ phase: "create" | "destroy";
83
+ name: string;
84
+ }[];
75
85
  }>;
76
86
  export declare function reconcile(input: ReconcileInput): Promise<GitReconcileObservation>;
87
+ /** Reconcile one retained dependent worktree under its normal per-Contract lock. */
88
+ export declare function reconcileDependentWorktree(repository: GitRepository, contractId: ContractId, path: string, target: SnapshotId): Promise<ReconcileResult>;
77
89
  export declare function reconcileObservationFailure(error: unknown): ReconcileResult;
78
90
  export declare function reconcileEffectFailure(error: unknown, prior?: ReconcileResult): ReconcileResult;
79
91
  type ReconcileBatchOptions = Readonly<{
@@ -2,14 +2,15 @@ import { access, mkdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { acquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
4
4
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
5
- import { CANDIDATE_PIN_REF_NAMESPACE, commonGitDirectory, DELIVERY_REF_NAMESPACE, registeredWorktreePaths, worktreeGitDirectory, } from "./repository.js";
5
+ import { CANDIDATE_PIN_REF_NAMESPACE, commonGitDirectory, DELIVERY_REF_NAMESPACE, registeredWorktreePaths, } from "./repository.js";
6
6
  import { runGit } from "./process.js";
7
7
  import { contractLocator, contractPhysicalName, gitObjectIdForSnapshot } from "./identity.js";
8
8
  import { observeContractAt } from "./observe.js";
9
9
  import { acquireTargetPlacementFence, recoverTargetPlacement, } from "./target-placement.js";
10
10
  import { runCreateHooks } from "./hooks.js";
11
- import { followManagedWorktree, worktreePath } from "./workspace.js";
11
+ import { followDependentManagedWorktree, followManagedWorktree, worktreePath } from "./workspace.js";
12
12
  import { removeCollectableScratchWorktrees } from "./scratch.js";
13
+ import { migrateContractCustodyRefs } from "./ref-migration.js";
13
14
  import { reconcileTerminalManagedWorktree, removeRef, updateRef } from "./terminal-reconcile.js";
14
15
  const pathExists = (path) => access(path).then(() => true, () => false);
15
16
  function deliveryRefFor(contract) {
@@ -31,8 +32,8 @@ async function acquireWorktreeTopology(repository) {
31
32
  function diagnostic(error) {
32
33
  return error instanceof Error ? error.message : String(error);
33
34
  }
34
- function complete(effects = [], lag = []) {
35
- return { effects, lag };
35
+ function complete(effects = [], lag = [], hookRuns = []) {
36
+ return hookRuns.length === 0 ? { effects, lag } : { effects, lag, hookRuns };
36
37
  }
37
38
  function failed(stage, error, effects = [], lag = []) {
38
39
  if (error instanceof AuthorityCorruptionError || error instanceof TypeError)
@@ -103,7 +104,7 @@ async function reconcileTargetCheckouts(repository, state) {
103
104
  result = failed("effect", releaseFailure, result.effects, result.lag);
104
105
  return result;
105
106
  }
106
- async function reconcileActiveManagedWorktree({ repository, hooks, retryHooks, place }, state, topology, { effects, lag }) {
107
+ async function reconcileActiveManagedWorktree({ repository, hooks, retryHooks, place }, state, topology, { effects, lag, hookRuns }) {
107
108
  if (place === undefined)
108
109
  return complete(effects, [...lag, missingPlaceLag(repository)]);
109
110
  const path = worktreePath(repository, place);
@@ -131,29 +132,39 @@ async function reconcileActiveManagedWorktree({ repository, hooks, retryHooks, p
131
132
  else {
132
133
  effects.push(projection);
133
134
  }
134
- const hookLag = await runCreateHooks(path, await worktreeGitDirectory(repository, path), hooks, retryHooks);
135
- if (hookLag !== null)
136
- lag.push(hookLag);
135
+ if (projection.action === "created" || retryHooks) {
136
+ const hookRun = await runCreateHooks(path, hooks);
137
+ hookRuns.push(...hookRun.runs.map((name) => ({ phase: "create", name })));
138
+ if (hookRun.lag !== null)
139
+ lag.push(hookRun.lag);
140
+ }
137
141
  effects.push(await (state.delivery
138
142
  ? await updateRef(repository, candidatePinRefFor(state.id), state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot)
139
143
  : await removeRef(repository, candidatePinRefFor(state.id))));
140
- return complete(effects, lag);
144
+ return complete(effects, lag, hookRuns);
141
145
  }
142
146
  async function reconcileWithTopology(input, state, topology) {
143
147
  const { repository } = input;
144
148
  const effects = [];
145
149
  const lag = [];
150
+ const hookRuns = [];
146
151
  try {
147
152
  await removeCollectableScratch(repository, topology, effects, lag);
148
153
  if (!state)
149
154
  return complete(effects, lag);
155
+ const migration = await migrateContractCustodyRefs(repository, state.id);
156
+ effects.push(...migration.effects);
157
+ lag.push(...migration.lag);
158
+ if (migration.lag.length > 0)
159
+ return complete(effects, lag);
150
160
  const targetCheckouts = await reconcileTargetCheckouts(repository, state);
151
161
  effects.push(...targetCheckouts.effects);
152
162
  lag.push(...targetCheckouts.lag);
153
163
  if (state.terminal) {
154
- return await reconcileTerminalManagedWorktree(input, state, topology, { effects, lag }, { ref: deliveryRefFor(state.id), pin: candidatePinRefFor(state.id) });
164
+ return await reconcileTerminalManagedWorktree(input, state, topology, { effects, lag, hookRuns }, { ref: deliveryRefFor(state.id), pin: candidatePinRefFor(state.id) });
155
165
  }
156
- return await reconcileActiveManagedWorktree(input, state, topology, { effects, lag });
166
+ const result = await reconcileActiveManagedWorktree(input, state, topology, { effects, lag, hookRuns });
167
+ return hookRuns.length === 0 ? result : { ...result, hookRuns };
157
168
  }
158
169
  catch (error) {
159
170
  return failed("effect", error, effects, lag);
@@ -206,6 +217,54 @@ export async function reconcile(input) {
206
217
  throw new Error("reconcile produced no observation");
207
218
  return observation;
208
219
  }
220
+ /** Reconcile one retained dependent worktree under its normal per-Contract lock. */
221
+ export async function reconcileDependentWorktree(repository, contractId, path, target) {
222
+ let held;
223
+ try {
224
+ held = await acquireSqliteTransactionLock({ path: reconcileLockPath(repository, contractId), mode: "immediate" });
225
+ }
226
+ catch (error) {
227
+ return failed("effect", error);
228
+ }
229
+ let result;
230
+ try {
231
+ const follow = await followDependentManagedWorktree(repository, path, target);
232
+ if (follow.kind === "followed") {
233
+ result = {
234
+ effects: [{ kind: "worktree", path, action: "followed", before: follow.before, after: follow.after }],
235
+ lag: [],
236
+ };
237
+ }
238
+ else if (follow.kind === "unchanged") {
239
+ result = { effects: [], lag: [] };
240
+ }
241
+ else {
242
+ result = {
243
+ effects: [{ kind: "worktree", path, action: "unchanged" }],
244
+ lag: [
245
+ {
246
+ kind: "worktree-follow-retained",
247
+ path,
248
+ tender: target,
249
+ head: follow.head,
250
+ reason: follow.reason,
251
+ ...(follow.paths.length === 0 ? {} : { paths: follow.paths }),
252
+ },
253
+ ],
254
+ };
255
+ }
256
+ }
257
+ catch (error) {
258
+ result = failed("effect", error, [{ kind: "worktree", path, action: "unchanged" }]);
259
+ }
260
+ try {
261
+ held.close();
262
+ }
263
+ catch (error) {
264
+ result = failed("effect", error, result.effects, result.lag);
265
+ }
266
+ return result;
267
+ }
209
268
  export function reconcileObservationFailure(error) {
210
269
  return failed("observation", error);
211
270
  }
@@ -0,0 +1,22 @@
1
+ import type { ContractId } from "../core/facts/types.js";
2
+ import { type GitRepository } from "./process.js";
3
+ import { type GitOid } from "./repository.js";
4
+ export type RefMigrationEffect = Readonly<{
5
+ kind: "ref";
6
+ name: string;
7
+ before: GitOid | null;
8
+ after: GitOid | null;
9
+ action: "created" | "removed";
10
+ }>;
11
+ export type RefMigrationConflict = Readonly<{
12
+ kind: "ref-migration-conflict";
13
+ legacyRef: string;
14
+ legacyOid: GitOid;
15
+ currentRef: string;
16
+ currentOid: GitOid;
17
+ }>;
18
+ export type RefMigrationResult = Readonly<{
19
+ effects: readonly RefMigrationEffect[];
20
+ lag: readonly RefMigrationConflict[];
21
+ }>;
22
+ export declare function migrateContractCustodyRefs(repository: GitRepository, contract: ContractId): Promise<RefMigrationResult>;
@@ -0,0 +1,55 @@
1
+ import { contractPhysicalName } from "./identity.js";
2
+ import { runGit } from "./process.js";
3
+ import { CANDIDATE_PIN_REF_NAMESPACE, DELIVERY_REF_NAMESPACE, LEGACY_CANDIDATE_PIN_REF_NAMESPACE, LEGACY_DELIVERY_REF_NAMESPACE, readRef, } from "./repository.js";
4
+ export async function migrateContractCustodyRefs(repository, contract) {
5
+ const leaf = contractPhysicalName(contract);
6
+ const pairs = [
7
+ {
8
+ legacy: `${LEGACY_DELIVERY_REF_NAMESPACE}/${leaf}`,
9
+ current: `${DELIVERY_REF_NAMESPACE}/${leaf}`,
10
+ },
11
+ {
12
+ legacy: `${LEGACY_CANDIDATE_PIN_REF_NAMESPACE}/${leaf}`,
13
+ current: `${CANDIDATE_PIN_REF_NAMESPACE}/${leaf}`,
14
+ },
15
+ ];
16
+ const observed = await Promise.all(pairs.map(async (pair) => {
17
+ const [legacyOid, currentOid] = await Promise.all([
18
+ readRef(repository, pair.legacy),
19
+ readRef(repository, pair.current),
20
+ ]);
21
+ return { ...pair, legacyOid, currentOid };
22
+ }));
23
+ const lag = observed.flatMap((pair) => pair.legacyOid !== null && pair.currentOid !== null && pair.currentOid !== pair.legacyOid
24
+ ? [
25
+ {
26
+ kind: "ref-migration-conflict",
27
+ legacyRef: pair.legacy,
28
+ legacyOid: pair.legacyOid,
29
+ currentRef: pair.current,
30
+ currentOid: pair.currentOid,
31
+ },
32
+ ]
33
+ : []);
34
+ if (lag.length > 0)
35
+ return { effects: [], lag };
36
+ const migrating = observed.filter((pair) => pair.legacyOid !== null);
37
+ if (migrating.length === 0)
38
+ return { effects: [], lag: [] };
39
+ const lines = ["start"];
40
+ const effects = [];
41
+ for (const pair of migrating) {
42
+ if (pair.currentOid === null) {
43
+ lines.push(`create ${pair.current} ${pair.legacyOid}`);
44
+ effects.push({ kind: "ref", name: pair.current, before: null, after: pair.legacyOid, action: "created" });
45
+ }
46
+ else {
47
+ lines.push(`verify ${pair.current} ${pair.currentOid}`);
48
+ }
49
+ lines.push(`delete ${pair.legacy} ${pair.legacyOid}`);
50
+ effects.push({ kind: "ref", name: pair.legacy, before: pair.legacyOid, after: null, action: "removed" });
51
+ }
52
+ lines.push("prepare", "commit", "");
53
+ await runGit(repository, ["update-ref", "--stdin", "--no-deref"], lines.join("\n"));
54
+ return { effects, lag: [] };
55
+ }
@@ -2,12 +2,13 @@ import { type GitRepository } from "./process.js";
2
2
  import { type TreeChange, type TreeEntry } from "./tree.js";
3
3
  export type { TreeChange } from "./tree.js";
4
4
  export declare const GIT_REF = "refs/heads/keiyaku-state";
5
- export declare const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
6
- export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
7
- export declare const MIGRATION_DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
8
- export declare const MIGRATION_CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
5
+ export declare const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
6
+ export declare const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
7
+ export declare const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
+ export declare const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
9
  export declare const GIT_FORMAT_PATH = "meta/format.json";
10
10
  export declare const GIT_FORMAT_BYTES = "{\"version\":4}\n";
11
+ export declare const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
11
12
  export declare function isKeiyakuOwnedRef(ref: string): boolean;
12
13
  export type GitOid = string;
13
14
  export type RefPublication = Readonly<{
@@ -54,14 +55,17 @@ export declare function readBlob(repository: GitRepository, oid: GitOid): Promis
54
55
  export declare function updateGitTree(repository: GitRepository, baseTree: GitOid | null, changes: ReadonlyMap<string, TreeChange>): Promise<GitOid>;
55
56
  /** Write a tree update from directory entries frozen with the admitted base tree. */
56
57
  export declare function updateGitTreeFromFrozenDirectories(repository: GitRepository, baseTree: GitOid | null, directories: ReadonlyMap<string, ReadonlyMap<string, TreeEntry>>, changes: ReadonlyMap<string, TreeChange>): Promise<GitOid>;
57
- export declare function writeCommit(input: Readonly<{
58
+ type WriteCommitInput = Readonly<{
58
59
  repository: GitRepository;
59
60
  tree: GitOid;
60
61
  parent: GitOid | null;
61
62
  message?: string;
62
63
  actor?: string;
63
64
  at?: string;
64
- }>): Promise<GitOid>;
65
+ }>;
66
+ export declare function writeCommit(input: WriteCommitInput): Promise<GitOid>;
67
+ /** Write a commit that will become the canonical keiyaku-state tip. */
68
+ export declare function writeStateCommit(input: WriteCommitInput): Promise<GitOid>;
65
69
  export declare function updateRefsAtomically(repository: GitRepository, updates: readonly ({
66
70
  readonly ref: typeof GIT_REF;
67
71
  readonly newOid: GitOid;
@@ -1,26 +1,28 @@
1
+ /* eslint-disable max-lines */
1
2
  import { isAbsolute, resolve } from "node:path";
2
3
  import { AuthorityCorruptionError } from "../core/facts/errors.js";
3
4
  import { gitObjectId } from "./identity.js";
4
5
  import { GitPlumbingError, runGit, runGitWithEnvironment } from "./process.js";
5
6
  import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
6
7
  export const GIT_REF = "refs/heads/keiyaku-state";
7
- export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
8
- export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
9
- export const MIGRATION_DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
10
- export const MIGRATION_CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
8
+ export const DELIVERY_REF_NAMESPACE = "refs/keiyaku/delivery";
9
+ export const CANDIDATE_PIN_REF_NAMESPACE = "refs/keiyaku/candidate";
10
+ export const LEGACY_DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
11
+ export const LEGACY_CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
11
12
  export const GIT_FORMAT_PATH = "meta/format.json";
12
13
  const CURRENT_FORMAT_VERSION = 4;
13
14
  export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
15
+ export const STATE_COMMIT_SUBJECT = "keiyaku authority - do not delete or rewrite";
14
16
  export function isKeiyakuOwnedRef(ref) {
15
17
  return (ref === GIT_REF ||
16
18
  ref === DELIVERY_REF_NAMESPACE ||
17
19
  ref.startsWith(`${DELIVERY_REF_NAMESPACE}/`) ||
18
20
  ref === CANDIDATE_PIN_REF_NAMESPACE ||
19
21
  ref.startsWith(`${CANDIDATE_PIN_REF_NAMESPACE}/`) ||
20
- ref === MIGRATION_DELIVERY_REF_NAMESPACE ||
21
- ref.startsWith(`${MIGRATION_DELIVERY_REF_NAMESPACE}/`) ||
22
- ref === MIGRATION_CANDIDATE_PIN_REF_NAMESPACE ||
23
- ref.startsWith(`${MIGRATION_CANDIDATE_PIN_REF_NAMESPACE}/`));
22
+ ref === LEGACY_DELIVERY_REF_NAMESPACE ||
23
+ ref.startsWith(`${LEGACY_DELIVERY_REF_NAMESPACE}/`) ||
24
+ ref === LEGACY_CANDIDATE_PIN_REF_NAMESPACE ||
25
+ ref.startsWith(`${LEGACY_CANDIDATE_PIN_REF_NAMESPACE}/`));
24
26
  }
25
27
  export class NoGitWorldError extends Error {
26
28
  path;
@@ -380,6 +382,12 @@ export async function writeCommit(input) {
380
382
  ...(input.at === undefined ? {} : { at: input.at }),
381
383
  });
382
384
  }
385
+ /** Write a commit that will become the canonical keiyaku-state tip. */
386
+ export async function writeStateCommit(input) {
387
+ const detail = input.message;
388
+ const message = detail === undefined || detail.length === 0 ? STATE_COMMIT_SUBJECT : `${STATE_COMMIT_SUBJECT}\n\n${detail}`;
389
+ return await writeCommit({ ...input, message });
390
+ }
383
391
  function refAssertionLine(assertion) {
384
392
  assertAssertionRef(assertion.ref);
385
393
  gitObjectId(assertion.oid, `expected oid for ${assertion.ref}`);
@@ -43,6 +43,7 @@ export type TargetPlacementPreparation = Readonly<{
43
43
  refusal: TargetPlacementRefusal;
44
44
  }>;
45
45
  export declare function observeTargetHead(repository: GitRepository, target: string): Promise<SnapshotId | null>;
46
+ export declare function observedTreeEqualsCandidate(repository: GitRepository, observed: SnapshotId | null, candidate: SnapshotId): Promise<boolean>;
46
47
  export declare function acquireTargetPlacementFence(repository: GitRepository, target: string): Promise<HeldSqliteTransactionLock>;
47
48
  export type TargetedContractCoordinates = ContractState["coordinates"] & Readonly<{
48
49
  target: string;
@@ -27,6 +27,11 @@ function literalPath(path) {
27
27
  async function commitTree(repository, snapshot) {
28
28
  return gitObjectId((await runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)])).toString("utf8").trim(), "commit tree");
29
29
  }
30
+ export async function observedTreeEqualsCandidate(repository, observed, candidate) {
31
+ if (observed === null)
32
+ return false;
33
+ return (await commitTree(repository, observed)) === (await commitTree(repository, candidate));
34
+ }
30
35
  function checkoutRefusal(contractId, target, path, reason, paths) {
31
36
  return { kind: "checkout-not-followable", contractId, target: target.target, path, reason, paths };
32
37
  }
@@ -9,6 +9,7 @@ export type TenderCaptureCoordinates = Readonly<{
9
9
  place?: string;
10
10
  workspacePath?: string;
11
11
  captureMergeState?: boolean;
12
+ /** Opt-in real-index UU refusal. Omit or false when authorized capture must read current worktree bytes. */
12
13
  rejectUnmerged?: boolean;
13
14
  }>;
14
15
  export type WorktreeMissingRefusal = Readonly<{
@@ -17,5 +17,5 @@ type TerminalCustodyRefs = Readonly<{
17
17
  }>;
18
18
  export declare function updateRef(repository: GitRepository, ref: string, desired: SnapshotId): Promise<Effect>;
19
19
  export declare function removeRef(repository: GitRepository, ref: string): Promise<Effect>;
20
- export declare function reconcileTerminalManagedWorktree({ repository, channel, hooks, retryHooks, retainTerminalWorktree, place }: TerminalReconcileInput, state: ContractState, topology: WorktreeTopology, acc: ReconcileAccumulation, { ref, pin }: TerminalCustodyRefs): Promise<ReconcileResult>;
20
+ export declare function reconcileTerminalManagedWorktree({ repository, channel, hooks, retainTerminalWorktree, place }: TerminalReconcileInput, state: ContractState, topology: WorktreeTopology, acc: ReconcileAccumulation, { ref, pin }: TerminalCustodyRefs): Promise<ReconcileResult>;
21
21
  export {};
@@ -1,5 +1,5 @@
1
1
  import { access } from "node:fs/promises";
2
- import { isKeiyakuOwnedRef, readRef, writeCommit, worktreeGitDirectory } from "./repository.js";
2
+ 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";
@@ -9,10 +9,7 @@ const pathExists = (path) => access(path).then(() => true, () => false);
9
9
  async function removeTerminalWorktree(repository, topology, path) {
10
10
  const registered = topology.paths.has(path);
11
11
  if (!registered) {
12
- return {
13
- effect: { kind: "worktree", path, action: "unchanged" },
14
- retained: await pathExists(path),
15
- };
12
+ return { retained: await pathExists(path) };
16
13
  }
17
14
  if (!(await pathExists(path))) {
18
15
  await runGit(repository, ["worktree", "remove", path]);
@@ -105,20 +102,20 @@ async function snapshotCustodian(repository, snapshot) {
105
102
  function fromPrimaryWorktree(repository) {
106
103
  return { ...repository, effectiveCwd: repository.primaryWorktree };
107
104
  }
108
- function complete(effects = [], lag = []) {
109
- return { effects, lag };
105
+ function complete(effects = [], lag = [], hookRuns = []) {
106
+ return hookRuns.length === 0 ? { effects, lag } : { effects, lag, hookRuns };
110
107
  }
111
- function retainTerminalWorktree(path, retainedLag, { effects, lag }) {
108
+ function retainTerminalWorktree(path, retainedLag, { effects, lag, hookRuns }) {
112
109
  effects.push({ kind: "worktree", path, action: "unchanged" });
113
110
  lag.push(retainedLag);
114
- return complete(effects, lag);
111
+ return complete(effects, lag, hookRuns);
115
112
  }
116
113
  async function terminalSealExpectations(channel, state) {
117
114
  const snapshots = terminalSealSnapshots(state);
118
115
  const objects = await channel.readObjects(snapshots.map(gitObjectIdForSnapshot));
119
116
  return decodeTerminalSealExpectations(state, objects);
120
117
  }
121
- async function removeSealedTerminalWorktree({ repository, topology, path, state, expected, hooks, retryHooks, acc, }) {
118
+ async function removeSealedTerminalWorktree({ repository, topology, path, state, expected, hooks, acc, }) {
122
119
  if (topology.paths.has(path) && (await pathExists(path))) {
123
120
  const terminal = state.terminal;
124
121
  if (terminal === null)
@@ -156,9 +153,10 @@ async function removeSealedTerminalWorktree({ repository, topology, path, state,
156
153
  }
157
154
  recovery = await recordRecovery(beforeWorkspace);
158
155
  }
159
- const hookLag = await runDestroyHooks(path, await worktreeGitDirectory(repository, path), hooks, retryHooks);
160
- if (hookLag !== null)
161
- return retainTerminalWorktree(path, hookLag, acc);
156
+ const hookRun = await runDestroyHooks(path, hooks);
157
+ acc.hookRuns.push(...hookRun.runs.map((name) => ({ phase: "destroy", name })));
158
+ if (hookRun.lag !== null)
159
+ return retainTerminalWorktree(path, hookRun.lag, acc);
162
160
  const after = await observeTerminalWorkspace(repository, path, expected);
163
161
  const afterWorkspace = after.workspace;
164
162
  const afterHooks = after.unsealed;
@@ -180,7 +178,8 @@ async function removeSealedTerminalWorktree({ repository, topology, path, state,
180
178
  const removal = await removeTerminalWorktree(repository, topology, path);
181
179
  if (removal.retained)
182
180
  return retainTerminalWorktree(path, { kind: "worktree-retained", path }, acc);
183
- acc.effects.push(removal.effect);
181
+ if (removal.effect !== undefined)
182
+ acc.effects.push(removal.effect);
184
183
  return null;
185
184
  }
186
185
  async function targetCustodyForClaimedIntegration(repository, state, integration) {
@@ -244,23 +243,27 @@ async function releaseTerminalCustody({ repository, state, resolveSeal, ref, pin
244
243
  }
245
244
  }
246
245
  }
247
- export async function reconcileTerminalManagedWorktree({ repository, channel, hooks, retryHooks, retainTerminalWorktree, place }, state, topology, acc, { ref, pin }) {
246
+ export async function reconcileTerminalManagedWorktree({ repository, channel, hooks, retainTerminalWorktree, place }, state, topology, acc, { ref, pin }) {
248
247
  const primary = fromPrimaryWorktree(repository);
249
248
  const path = place === undefined ? undefined : worktreePath(repository, place);
250
249
  const resolveSeal = async () => await terminalSealExpectations(channel, state);
251
250
  if (path === undefined) {
252
251
  if (retainTerminalWorktree === true)
253
- return complete(acc.effects, acc.lag);
252
+ return complete(acc.effects, acc.lag, acc.hookRuns);
254
253
  await releaseTerminalCustody({ repository: primary, state, resolveSeal, ref, pin, acc });
255
- return complete(acc.effects, acc.lag);
254
+ return complete(acc.effects, acc.lag, acc.hookRuns);
256
255
  }
257
256
  const expected = await resolveSeal();
258
257
  acc.effects.push(await updateRef(primary, ref, state.delivery?.data.tenderSnapshot ?? state.coordinates.start));
259
258
  if (state.delivery !== null) {
260
259
  acc.effects.push(await updateRef(primary, pin, state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot));
261
260
  }
262
- if (retainTerminalWorktree === true)
261
+ if (retainTerminalWorktree === true) {
262
+ if (topology.paths.has(path) && (await pathExists(path))) {
263
+ acc.effects.push({ kind: "worktree", path, action: "unchanged" });
264
+ }
263
265
  return complete(acc.effects, acc.lag);
266
+ }
264
267
  const retained = await removeSealedTerminalWorktree({
265
268
  repository: primary,
266
269
  topology,
@@ -268,11 +271,10 @@ export async function reconcileTerminalManagedWorktree({ repository, channel, ho
268
271
  state,
269
272
  expected,
270
273
  hooks,
271
- retryHooks,
272
274
  acc,
273
275
  });
274
276
  if (retained !== null)
275
277
  return retained;
276
278
  await releaseTerminalCustody({ repository: primary, state, resolveSeal: async () => expected, ref, pin, acc });
277
- return complete(acc.effects, acc.lag);
279
+ return complete(acc.effects, acc.lag, acc.hookRuns);
278
280
  }
@@ -26,8 +26,22 @@ export type ManagedWorktreeFollow = Readonly<{
26
26
  head: SnapshotId;
27
27
  reason: "head-moved" | "head-attached" | "operation-in-progress" | "unsupported-parent-shape";
28
28
  }>;
29
+ export type DependentWorktreeFollow = Readonly<{
30
+ kind: "followed";
31
+ before: SnapshotId;
32
+ after: SnapshotId;
33
+ }> | Readonly<{
34
+ kind: "unchanged";
35
+ }> | Readonly<{
36
+ kind: "retained";
37
+ head: SnapshotId;
38
+ reason: "head-moved" | "head-attached" | "operation-in-progress";
39
+ paths: readonly string[];
40
+ }>;
29
41
  /** Follow an accepted tender only through the native index-and-HEAD transition. */
30
42
  export declare function followManagedWorktree(repository: GitRepository, workspace: string, tender: SnapshotId): Promise<ManagedWorktreeFollow>;
43
+ /** Follow a dependency baseline only through a clean, detached fast-forward. */
44
+ export declare function followDependentManagedWorktree(repository: GitRepository, workspace: string, target: SnapshotId): Promise<DependentWorktreeFollow>;
31
45
  export declare function withPrivateGitIndex<Value>(action: (environment: Readonly<{
32
46
  GIT_INDEX_FILE: string;
33
47
  }>) => Value | PromiseLike<Value>): Promise<Value>;