@astrosheep/keiyaku 2.8.4 → 2.9.0

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 (229) hide show
  1. package/README.md +35 -15
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
  4. package/build/agents/harness/control-types.js +1 -0
  5. package/build/agents/harness/execution-handle.js +23 -3
  6. package/build/agents/harness/index.js +6 -607
  7. package/build/agents/harness/outcome.js +141 -0
  8. package/build/agents/harness/projection.js +310 -0
  9. package/build/agents/harness/router.js +201 -0
  10. package/build/agents/harness/runtime.js +2 -1
  11. package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
  12. package/build/agents/launch-snapshot/resolve.js +72 -0
  13. package/build/agents/opencode-sdk.js +24 -3
  14. package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
  15. package/build/agents/providers/claude-agent-sdk/events.js +218 -0
  16. package/build/agents/providers/claude-agent-sdk/session.js +158 -0
  17. package/build/agents/providers/codex-app-server/adapter.js +347 -0
  18. package/build/agents/providers/codex-app-server/events.js +352 -0
  19. package/build/agents/providers/codex-app-server/session.js +266 -0
  20. package/build/agents/providers/codex-sdk.js +23 -5
  21. package/build/agents/providers/opencode-sdk/adapter.js +344 -0
  22. package/build/agents/providers/opencode-sdk/events.js +354 -0
  23. package/build/agents/providers/opencode-sdk/session.js +180 -0
  24. package/build/agents/providers/pi/adapter.js +269 -0
  25. package/build/agents/providers/pi/checkpoint.js +41 -0
  26. package/build/agents/providers/pi/events.js +282 -0
  27. package/build/agents/{selector_v2.js → selector.js} +8 -6
  28. package/build/cli/commands/akuma/akuma/handler.js +4 -0
  29. package/build/cli/commands/akuma/akuma/meta.js +16 -0
  30. package/build/cli/commands/akuma/catalog.js +8 -0
  31. package/build/cli/commands/akuma/list/handler.js +15 -0
  32. package/build/cli/commands/akuma/list/meta-ls.js +9 -0
  33. package/build/cli/commands/akuma/view/handler.js +34 -0
  34. package/build/cli/commands/akuma/view/meta.js +8 -0
  35. package/build/cli/commands/akuma.js +1 -1
  36. package/build/cli/commands/contract/amend/handler.js +10 -0
  37. package/build/cli/commands/contract/amend/meta.js +12 -0
  38. package/build/cli/commands/contract/arc/handler.js +9 -0
  39. package/build/cli/commands/contract/arc/meta.js +26 -0
  40. package/build/cli/commands/contract/bind/handler.js +23 -0
  41. package/build/cli/commands/contract/bind/meta.js +30 -0
  42. package/build/cli/commands/contract/catalog.js +16 -0
  43. package/build/cli/commands/contract/forfeit/handler.js +13 -0
  44. package/build/cli/commands/contract/forfeit/meta.js +12 -0
  45. package/build/cli/commands/contract/log/handler.js +12 -0
  46. package/build/cli/commands/contract/log/meta.js +12 -0
  47. package/build/cli/commands/contract/petition/handler.js +26 -0
  48. package/build/cli/commands/contract/petition/meta.js +23 -0
  49. package/build/cli/commands/contract/renew/handler.js +10 -0
  50. package/build/cli/commands/contract/renew/meta.js +17 -0
  51. package/build/cli/commands/projection/call/handler.js +39 -0
  52. package/build/cli/commands/projection/call/meta.js +9 -0
  53. package/build/cli/commands/projection/catalog.js +14 -0
  54. package/build/cli/commands/projection/kill/handler.js +35 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +53 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +23 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +34 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +21 -0
  63. package/build/cli/commands/projection/wait/meta.js +11 -0
  64. package/build/cli/commands/registry.js +100 -0
  65. package/build/cli/commands/shared.js +71 -0
  66. package/build/cli/commands/skills/catalog.js +6 -0
  67. package/build/cli/commands/skills/install/handler.js +10 -0
  68. package/build/cli/commands/skills/install/meta.js +9 -0
  69. package/build/cli/commands/skills/skills/handler.js +4 -0
  70. package/build/cli/commands/skills/skills/meta.js +10 -0
  71. package/build/cli/commands/system/catalog.js +8 -0
  72. package/build/cli/commands/system/completion/handler.js +10 -0
  73. package/build/cli/commands/system/completion/meta.js +9 -0
  74. package/build/cli/commands/system/dump-env/handler.js +3 -0
  75. package/build/cli/commands/system/dump-env/meta.js +10 -0
  76. package/build/cli/commands/system/guide/handler.js +4 -0
  77. package/build/cli/commands/system/guide/meta.js +9 -0
  78. package/build/cli/commands/task/add/handler.js +21 -0
  79. package/build/cli/commands/task/add/meta.js +8 -0
  80. package/build/cli/commands/task/catalog.js +22 -0
  81. package/build/cli/commands/task/doctor/handler.js +4 -0
  82. package/build/cli/commands/task/doctor/meta.js +8 -0
  83. package/build/cli/commands/task/done/handler.js +17 -0
  84. package/build/cli/commands/task/done/meta.js +8 -0
  85. package/build/cli/commands/task/drop/handler.js +6 -0
  86. package/build/cli/commands/task/drop/meta.js +8 -0
  87. package/build/cli/commands/task/ls/handler.js +13 -0
  88. package/build/cli/commands/task/ls/meta.js +8 -0
  89. package/build/cli/commands/task/shared.js +131 -0
  90. package/build/cli/commands/task/start/handler.js +6 -0
  91. package/build/cli/commands/task/start/meta.js +8 -0
  92. package/build/cli/commands/task/stop/handler.js +17 -0
  93. package/build/cli/commands/task/stop/meta.js +8 -0
  94. package/build/cli/commands/task/task/handler.js +5 -0
  95. package/build/cli/commands/task/task/meta.js +11 -0
  96. package/build/cli/commands/task/update/handler.js +18 -0
  97. package/build/cli/commands/task/update/meta.js +8 -0
  98. package/build/cli/commands/task/view/handler.js +14 -0
  99. package/build/cli/commands/task/view/meta.js +8 -0
  100. package/build/cli/completion.js +26 -27
  101. package/build/cli/flags.js +79 -2
  102. package/build/cli/help.js +27 -49
  103. package/build/cli/index.js +121 -374
  104. package/build/cli/output-stream-error-policy.js +17 -0
  105. package/build/cli/parse.js +159 -72
  106. package/build/cli/projection-address.js +5 -0
  107. package/build/cli/render/arc.js +1 -1
  108. package/build/cli/render/errors.js +4 -3
  109. package/build/cli/render/format.js +14 -0
  110. package/build/cli/render/kanshi.js +106 -0
  111. package/build/cli/render/petition.js +24 -1
  112. package/build/cli/render/projection-activity.js +5 -1
  113. package/build/cli/render/shared.js +2 -2
  114. package/build/cli/render/status.js +79 -13
  115. package/build/cli/render/task.js +113 -0
  116. package/build/cli/render/wait.js +23 -3
  117. package/build/cli/types.js +1 -1
  118. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
  119. package/build/config/env.js +9 -4
  120. package/build/config/settings/disease.js +26 -0
  121. package/build/config/settings/knobs.js +8 -0
  122. package/build/config/settings/loader.js +244 -0
  123. package/build/config/settings/schema.js +276 -0
  124. package/build/core/addressing.js +1 -1
  125. package/build/core/amend.js +3 -3
  126. package/build/core/arc.js +5 -4
  127. package/build/core/bind-workspace.js +123 -0
  128. package/build/core/bind.js +75 -155
  129. package/build/core/call/call.js +224 -0
  130. package/build/core/call/context.js +238 -0
  131. package/build/core/call/execution.js +217 -0
  132. package/build/core/call/prompt.js +42 -0
  133. package/build/core/call/reference.js +24 -0
  134. package/build/core/call-persist.js +1 -1
  135. package/build/core/claim-delivery.js +247 -0
  136. package/build/core/claim.js +39 -277
  137. package/build/core/context.js +2 -1
  138. package/build/core/{render.js → contract-view.js} +12 -140
  139. package/build/core/draft.js +107 -9
  140. package/build/core/entry.js +9 -1
  141. package/build/core/execution-coordinate.js +4 -20
  142. package/build/core/execution-pact.js +16 -31
  143. package/build/core/forfeit.js +35 -22
  144. package/build/core/hints.js +2 -2
  145. package/build/core/log.js +2 -2
  146. package/build/core/petition-claim-gates.js +8 -6
  147. package/build/core/petition-preview.js +97 -0
  148. package/build/core/petition-run.js +3 -2
  149. package/build/core/petition.js +39 -26
  150. package/build/core/projection/generation/database.js +306 -0
  151. package/build/core/projection/generation/identity.js +84 -0
  152. package/build/core/projection/generation/model.js +201 -0
  153. package/build/core/projection/generation/store.js +73 -0
  154. package/build/core/projection/generation/transitions.js +258 -0
  155. package/build/core/projection/heart.js +0 -307
  156. package/build/core/projection/tell/database.js +109 -0
  157. package/build/core/projection/tell/model.js +97 -0
  158. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  159. package/build/core/projection-activity.js +23 -2
  160. package/build/core/projection-core.js +2 -1
  161. package/build/core/projection-execution-observer.js +259 -0
  162. package/build/core/projection-generation-continuation.js +1 -1
  163. package/build/core/projection-generation-execution.js +90 -20
  164. package/build/core/projection-generation-launcher.js +1 -1
  165. package/build/core/projection-generation-runner.js +198 -4
  166. package/build/core/projection-identity.js +1 -1
  167. package/build/core/projection-kill.js +50 -17
  168. package/build/core/projection-life-observer.js +1 -1
  169. package/build/core/projection-life-protocol.js +10 -0
  170. package/build/core/projection-mint.js +1 -1
  171. package/build/core/projection-status.js +45 -32
  172. package/build/core/projection-wait.js +12 -2
  173. package/build/core/projection-wake.js +5 -2
  174. package/build/core/{queue_v2.js → queue.js} +2 -1
  175. package/build/core/registry.js +1 -1
  176. package/build/core/renew.js +7 -7
  177. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  178. package/build/core/response-artifact-catalog.js +7 -0
  179. package/build/core/seal.js +44 -16
  180. package/build/core/status/board.js +286 -0
  181. package/build/core/status/drift.js +99 -0
  182. package/build/core/status/lifecycle.js +84 -0
  183. package/build/core/status/reconciliation.js +131 -0
  184. package/build/core/stored-agent-event.js +28 -4
  185. package/build/core/target-ref.js +10 -0
  186. package/build/core/task/board.js +175 -0
  187. package/build/core/task/commands.js +211 -0
  188. package/build/core/task/document.js +189 -0
  189. package/build/core/task/model.js +1 -0
  190. package/build/core/task/settlement-git.js +207 -0
  191. package/build/core/task/settlement-policy.js +82 -0
  192. package/build/core/task/source-board.js +84 -0
  193. package/build/core/task/source-model.js +1 -0
  194. package/build/core/task/validation.js +44 -0
  195. package/build/core/task-contract.js +224 -0
  196. package/build/core/task-doctor.js +14 -0
  197. package/build/core/task-git-actor.js +38 -0
  198. package/build/core/task-git-runtime.js +34 -0
  199. package/build/core/task-git-store.js +375 -0
  200. package/build/core/task-repository.js +1 -0
  201. package/build/core/task-store-repository.js +86 -0
  202. package/build/core/task-store.js +1 -0
  203. package/build/core/task-worktree.js +51 -0
  204. package/build/core/task.js +4 -0
  205. package/build/core/transcripts.js +138 -56
  206. package/build/core/validation.js +17 -0
  207. package/build/core/verdict.js +2 -2
  208. package/build/core/worktree-path.js +24 -6
  209. package/build/flow-error.js +3 -2
  210. package/build/generated/version.js +1 -1
  211. package/build/git/commits.js +3 -0
  212. package/build/git/diff/parsers.js +0 -94
  213. package/build/index.js +2 -0
  214. package/build/keiyaku-constants.js +2 -0
  215. package/build/keiyaku.js +4 -3
  216. package/package.json +3 -2
  217. package/skills/keiyaku/SKILL.md +1 -1
  218. package/skills/keiyaku-akuma/SKILL.md +11 -6
  219. package/build/agents/harness/pump.js +0 -158
  220. package/build/agents/providers/codex-app-server.js +0 -864
  221. package/build/agents/providers/opencode-sdk.js +0 -363
  222. package/build/agents/providers/pi.js +0 -557
  223. package/build/config/settings.js +0 -533
  224. package/build/core/call.js +0 -686
  225. package/build/core/projection/generation.js +0 -412
  226. package/build/core/projection/identity.js +0 -81
  227. package/build/core/projection/mint.js +0 -174
  228. package/build/core/projection-generation-store.js +0 -707
  229. package/build/core/status.js +0 -539
@@ -0,0 +1,247 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { FlowError } from "../flow-error.js";
4
+ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
+ import { readLedger } from "./ledger.js";
6
+ import { shortCommitHash } from "../git/commits.js";
7
+ import { commissionTarget, latestBase } from "./status/drift.js";
8
+ import { settleTaskBoardForLanding } from "./task-contract.js";
9
+ import { stableRepoRoot } from "./worktree-path.js";
10
+ const CLAIM_RECEIPTS_FILE = "claim-receipts.jsonl";
11
+ async function appendClaimReceipt(cwd, receipt, restoreIfMissing = "") {
12
+ const receiptPath = path.join(await stableRepoRoot(cwd), ".keiyaku", CLAIM_RECEIPTS_FILE);
13
+ let handle;
14
+ let created = false;
15
+ try {
16
+ handle = await fs.open(receiptPath, "ax");
17
+ created = true;
18
+ }
19
+ catch (error) {
20
+ if (error.code !== "EEXIST")
21
+ throw error;
22
+ handle = await fs.open(receiptPath, "a");
23
+ }
24
+ try {
25
+ await handle.writeFile(`${created ? restoreIfMissing : ""}${JSON.stringify(receipt)}\n`, "utf8");
26
+ await handle.sync();
27
+ }
28
+ finally {
29
+ await handle.close();
30
+ }
31
+ const directory = await fs.open(path.dirname(receiptPath), "r");
32
+ try {
33
+ await directory.sync();
34
+ }
35
+ finally {
36
+ await directory.close();
37
+ }
38
+ }
39
+ export async function resolveCommit(cwd, rev) {
40
+ const git = createGit(cwd);
41
+ try {
42
+ return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
43
+ }
44
+ catch (error) {
45
+ throw wrapGitError(`rev-parse ${rev}`, error, cwd);
46
+ }
47
+ }
48
+ export async function resolveCommitOrNull(cwd, rev) {
49
+ const git = createGit(cwd);
50
+ try {
51
+ return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
52
+ }
53
+ catch (error) {
54
+ if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
55
+ return null;
56
+ throw wrapGitError(`rev-parse ${rev}`, error, cwd);
57
+ }
58
+ }
59
+ /** Display path for preview cleanup effects; delivery owns path mechanics. */
60
+ export function relativeDisplayPath(from, to) {
61
+ return path.relative(from, to) || ".";
62
+ }
63
+ /**
64
+ * Find the worktree path that currently has `branch` checked out.
65
+ * Parses `git worktree list --porcelain -z` so linked worktrees are visible
66
+ * even when claim cwd is a different worktree (real Keiyaku topology).
67
+ */
68
+ async function findWorktreePathForBranch(cwd, branch) {
69
+ const git = createGit(cwd);
70
+ let output;
71
+ try {
72
+ output = await git.raw(["worktree", "list", "--porcelain", "-z"]);
73
+ }
74
+ catch (error) {
75
+ throw wrapGitError("worktree list --porcelain -z", error, cwd);
76
+ }
77
+ const expected = `refs/heads/${branch}`;
78
+ let currentPath;
79
+ for (const field of output.split("\0")) {
80
+ if (field.startsWith("worktree ")) {
81
+ currentPath = field.slice("worktree ".length);
82
+ continue;
83
+ }
84
+ if (field.startsWith("branch ") && field.slice("branch ".length) === expected) {
85
+ return currentPath;
86
+ }
87
+ }
88
+ return undefined;
89
+ }
90
+ async function listChangedPaths(cwd, oldHead, mergeCommit) {
91
+ const git = createGit(cwd);
92
+ try {
93
+ const output = await git.raw(["diff", "--name-only", "-z", oldHead, mergeCommit]);
94
+ return output.split("\0").filter((entry) => entry.length > 0);
95
+ }
96
+ catch (error) {
97
+ throw wrapGitError(`diff --name-only -z ${oldHead} ${mergeCommit}`, error, cwd);
98
+ }
99
+ }
100
+ /**
101
+ * Among candidate merge paths, return those that are dirty/untracked in the target worktree.
102
+ * Must run while HEAD/index still share oldHead (pre-CAS). Uses porcelain -z only — never prose.
103
+ */
104
+ async function listBlockingPaths(worktreePath, candidates) {
105
+ if (candidates.length === 0)
106
+ return [];
107
+ const candidateSet = new Set(candidates);
108
+ const git = createGit(worktreePath);
109
+ let output;
110
+ try {
111
+ output = await git.raw([
112
+ "status",
113
+ "--porcelain",
114
+ "-z",
115
+ "--untracked-files=all",
116
+ "--",
117
+ ...candidates,
118
+ ]);
119
+ }
120
+ catch (error) {
121
+ throw wrapGitError("status --porcelain -z --untracked-files=all", error, worktreePath);
122
+ }
123
+ const dirty = new Set();
124
+ for (const part of output.split("\0")) {
125
+ if (part.length < 3)
126
+ continue;
127
+ // porcelain -z: "XYpath" (no space) or legacy "XY path"
128
+ const pathEntry = part[2] === " " ? part.slice(3) : part.slice(2);
129
+ if (pathEntry.length > 0 && candidateSet.has(pathEntry)) {
130
+ dirty.add(pathEntry);
131
+ }
132
+ }
133
+ return candidates.filter((candidate) => dirty.has(candidate));
134
+ }
135
+ function buildReconcileWarning(input) {
136
+ const blocking = input.blockingPaths.length > 0
137
+ ? input.blockingPaths.map((entry) => `- ${entry}`).join("\n")
138
+ : "- (none identified)";
139
+ return [
140
+ `Target worktree ${input.worktreePath} remains behind after claim advanced '${input.targetBranch}' to ${shortCommitHash(input.mergeCommit)}.`,
141
+ "The branch ref advanced while that worktree was not reconciled.",
142
+ "Blocking paths:",
143
+ blocking,
144
+ ].join("\n");
145
+ }
146
+ /**
147
+ * Snapshot real blockers BEFORE CAS while target worktree HEAD/index still match oldHead.
148
+ * Post-CAS porcelain is useless: every merge path looks like ghost dirt.
149
+ */
150
+ async function snapshotPreCasBlockers(input) {
151
+ const worktreePath = await findWorktreePathForBranch(input.cwd, input.targetBranch);
152
+ if (!worktreePath)
153
+ return undefined;
154
+ // Claim reconciles the common task board itself after CAS. Task paths are
155
+ // therefore never target-worktree blockers and must not enter this snapshot.
156
+ const changedPaths = (await listChangedPaths(input.cwd, input.oldHead, input.mergeCommit))
157
+ .filter((entry) => !entry.startsWith(".keiyaku/tasks/"));
158
+ const blockingPaths = await listBlockingPaths(worktreePath, changedPaths);
159
+ return { worktreePath, blockingPaths };
160
+ }
161
+ /**
162
+ * After CAS, attempt two-tree read-tree on the pre-identified target worktree.
163
+ * - No snapshot (target not checked out) → ref-only, no warning.
164
+ * - read-tree success → no warnings.
165
+ * - read-tree reject → soft-fail using pre-CAS blocker snapshot; never throw/reset.
166
+ */
167
+ async function reconcileAfterCas(input) {
168
+ if (!input.snapshot)
169
+ return [];
170
+ const targetGit = createGit(input.snapshot.worktreePath);
171
+ try {
172
+ // The lock-held single-board settlement has already aligned task files and
173
+ // index entries to the accepted merge. Reconcile only the remaining tree.
174
+ await targetGit.raw(["read-tree", "-m", "-u", input.oldHead, input.mergeCommit]);
175
+ return [];
176
+ }
177
+ catch {
178
+ return [
179
+ buildReconcileWarning({
180
+ worktreePath: input.snapshot.worktreePath,
181
+ targetBranch: input.targetBranch,
182
+ mergeCommit: input.mergeCommit,
183
+ blockingPaths: input.snapshot.blockingPaths,
184
+ }),
185
+ ];
186
+ }
187
+ }
188
+ export async function defaultMergeDelivery(input) {
189
+ const ledger = await readLedger(input.cwd, input.contractId);
190
+ if (!ledger) {
191
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: ledger missing`);
192
+ }
193
+ const commission = commissionTarget(ledger.entries);
194
+ if (!commission) {
195
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: missing commission target`);
196
+ }
197
+ const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
198
+ if (!targetHead) {
199
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: commission target ${commission.ref} does not resolve`);
200
+ }
201
+ const branch = `keiyaku/${input.contractId}`;
202
+ const deliveryHead = await resolveCommit(input.cwd, branch);
203
+ const base = latestBase(ledger.entries);
204
+ if (!base)
205
+ throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: missing ledger base`);
206
+ let receiptStreamBeforeCas = "";
207
+ if (!input.writeClaimReceipt) {
208
+ const receiptPath = path.join(await stableRepoRoot(input.cwd), ".keiyaku", CLAIM_RECEIPTS_FILE);
209
+ try {
210
+ receiptStreamBeforeCas = await fs.readFile(receiptPath, "utf8");
211
+ }
212
+ catch (error) {
213
+ if (error.code !== "ENOENT")
214
+ throw error;
215
+ }
216
+ }
217
+ let preCasSnapshot;
218
+ const merged = await settleTaskBoardForLanding({
219
+ cwd: input.cwd,
220
+ contractId: input.contractId,
221
+ baseCommit: base,
222
+ targetRef: commission.ref,
223
+ targetBranch: commission.branch,
224
+ targetHead,
225
+ deliveryHead,
226
+ beforeCas: async (mergeCommit, oldHead) => {
227
+ preCasSnapshot = await snapshotPreCasBlockers({ cwd: input.cwd, targetBranch: commission.branch, oldHead, mergeCommit });
228
+ },
229
+ afterCas: async (mergeCommit, oldHead) => reconcileAfterCas({ contractId: input.contractId, targetBranch: commission.branch, oldHead, mergeCommit, snapshot: preCasSnapshot }),
230
+ publishLedger: input.publishClaim,
231
+ });
232
+ const receiptWarnings = [];
233
+ if (merged.oldHead !== undefined) {
234
+ try {
235
+ const receipt = { kind: "receipt", oldHead: merged.oldHead, newHead: merged.mergeCommit, contract: input.contractId };
236
+ if (input.writeClaimReceipt)
237
+ await input.writeClaimReceipt(input.cwd, receipt);
238
+ else
239
+ await appendClaimReceipt(input.cwd, receipt, receiptStreamBeforeCas);
240
+ }
241
+ catch {
242
+ receiptWarnings.push("Claim receipt was not recorded after the target branch update; this movement will appear as an unreceipted FACT until manually recorded.");
243
+ }
244
+ }
245
+ const warnings = [...receiptWarnings, ...(merged.warnings ?? [])];
246
+ return warnings.length > 0 ? { mergeCommit: merged.mergeCommit, warnings } : { mergeCommit: merged.mergeCommit };
247
+ }
@@ -1,32 +1,11 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
1
  import { FlowError } from "../flow-error.js";
4
- import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
2
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
6
- import { assertClaimQueueHead, readQueueReadModel } from "./queue_v2.js";
3
+ import { assertClaimQueueHead, readQueueReadModel } from "./queue.js";
7
4
  import { releaseScope } from "./registry.js";
8
- import { shortCommitHash } from "./render.js";
9
- import { commissionTarget, deriveContractState, latestBase } from "./status.js";
5
+ import { deriveContractState } from "./status/lifecycle.js";
6
+ import { commissionTarget, latestBase } from "./status/drift.js";
10
7
  import { cleanupContractWorkspace, findContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
11
- const CLAIM_RECEIPTS_FILE = "claim-receipts.jsonl";
12
- async function appendClaimReceipt(cwd, receipt) {
13
- const receiptPath = path.join(await stableRepoRoot(cwd), ".keiyaku", CLAIM_RECEIPTS_FILE);
14
- const handle = await fs.open(receiptPath, "a");
15
- try {
16
- await handle.writeFile(`${JSON.stringify(receipt)}\n`, "utf8");
17
- await handle.sync();
18
- }
19
- finally {
20
- await handle.close();
21
- }
22
- const directory = await fs.open(path.dirname(receiptPath), "r");
23
- try {
24
- await directory.sync();
25
- }
26
- finally {
27
- await directory.close();
28
- }
29
- }
8
+ import { defaultMergeDelivery, relativeDisplayPath, resolveCommit, resolveCommitOrNull, } from "./claim-delivery.js";
30
9
  async function cleanupClaim(cwd, contractId, actor, at) {
31
10
  const stableRoot = await stableRepoRoot(cwd);
32
11
  await releaseScope(stableRoot, { contractId, actor, at });
@@ -35,240 +14,6 @@ async function cleanupClaim(cwd, contractId, actor, at) {
35
14
  function findClaim(entries) {
36
15
  return entries.find((entry) => entry.kind === "claim");
37
16
  }
38
- async function resolveCommit(cwd, rev) {
39
- const git = createGit(cwd);
40
- try {
41
- return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
42
- }
43
- catch (error) {
44
- throw wrapGitError(`rev-parse ${rev}`, error, cwd);
45
- }
46
- }
47
- async function resolveCommitOrNull(cwd, rev) {
48
- const git = createGit(cwd);
49
- try {
50
- return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
51
- }
52
- catch (error) {
53
- if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
54
- return null;
55
- throw wrapGitError(`rev-parse ${rev}`, error, cwd);
56
- }
57
- }
58
- async function isAncestor(cwd, ancestor, descendant) {
59
- const git = createGit(cwd);
60
- try {
61
- return (await git.raw(["merge-base", ancestor, descendant])).trim() === ancestor;
62
- }
63
- catch {
64
- return false;
65
- }
66
- }
67
- /**
68
- * Find the worktree path that currently has `branch` checked out.
69
- * Parses `git worktree list --porcelain -z` so linked worktrees are visible
70
- * even when claim cwd is a different worktree (real Keiyaku topology).
71
- */
72
- async function findWorktreePathForBranch(cwd, branch) {
73
- const git = createGit(cwd);
74
- let output;
75
- try {
76
- output = await git.raw(["worktree", "list", "--porcelain", "-z"]);
77
- }
78
- catch (error) {
79
- throw wrapGitError("worktree list --porcelain -z", error, cwd);
80
- }
81
- const expected = `refs/heads/${branch}`;
82
- let currentPath;
83
- for (const field of output.split("\0")) {
84
- if (field.startsWith("worktree ")) {
85
- currentPath = field.slice("worktree ".length);
86
- continue;
87
- }
88
- if (field.startsWith("branch ") && field.slice("branch ".length) === expected) {
89
- return currentPath;
90
- }
91
- }
92
- return undefined;
93
- }
94
- async function listChangedPaths(cwd, oldHead, mergeCommit) {
95
- const git = createGit(cwd);
96
- try {
97
- const output = await git.raw(["diff", "--name-only", "-z", oldHead, mergeCommit]);
98
- return output.split("\0").filter((entry) => entry.length > 0);
99
- }
100
- catch (error) {
101
- throw wrapGitError(`diff --name-only -z ${oldHead} ${mergeCommit}`, error, cwd);
102
- }
103
- }
104
- /**
105
- * Among candidate merge paths, return those that are dirty/untracked in the target worktree.
106
- * Must run while HEAD/index still share oldHead (pre-CAS). Uses porcelain -z only — never prose.
107
- */
108
- async function listBlockingPaths(worktreePath, candidates) {
109
- if (candidates.length === 0)
110
- return [];
111
- const candidateSet = new Set(candidates);
112
- const git = createGit(worktreePath);
113
- let output;
114
- try {
115
- output = await git.raw([
116
- "status",
117
- "--porcelain",
118
- "-z",
119
- "--untracked-files=all",
120
- "--",
121
- ...candidates,
122
- ]);
123
- }
124
- catch (error) {
125
- throw wrapGitError("status --porcelain -z --untracked-files=all", error, worktreePath);
126
- }
127
- const dirty = new Set();
128
- for (const part of output.split("\0")) {
129
- if (part.length < 3)
130
- continue;
131
- // porcelain -z: "XYpath" (no space) or legacy "XY path"
132
- const pathEntry = part[2] === " " ? part.slice(3) : part.slice(2);
133
- if (pathEntry.length > 0 && candidateSet.has(pathEntry)) {
134
- dirty.add(pathEntry);
135
- }
136
- }
137
- return candidates.filter((candidate) => dirty.has(candidate));
138
- }
139
- function buildReconcileWarning(input) {
140
- const blocking = input.blockingPaths.length > 0
141
- ? input.blockingPaths.map((entry) => `- ${entry}`).join("\n")
142
- : "- (none identified)";
143
- return [
144
- `Target worktree ${input.worktreePath} remains behind after claim advanced '${input.targetBranch}' to ${shortCommitHash(input.mergeCommit)}.`,
145
- "The branch ref advanced while that worktree was not reconciled.",
146
- "Blocking paths:",
147
- blocking,
148
- ].join("\n");
149
- }
150
- /**
151
- * Snapshot real blockers BEFORE CAS while target worktree HEAD/index still match oldHead.
152
- * Post-CAS porcelain is useless: every merge path looks like ghost dirt.
153
- */
154
- async function snapshotPreCasBlockers(input) {
155
- const worktreePath = await findWorktreePathForBranch(input.cwd, input.targetBranch);
156
- if (!worktreePath)
157
- return undefined;
158
- const changedPaths = await listChangedPaths(input.cwd, input.oldHead, input.mergeCommit);
159
- const blockingPaths = await listBlockingPaths(worktreePath, changedPaths);
160
- return { worktreePath, blockingPaths };
161
- }
162
- /**
163
- * After CAS, attempt two-tree read-tree on the pre-identified target worktree.
164
- * - No snapshot (target not checked out) → ref-only, no warning.
165
- * - read-tree success → no warnings.
166
- * - read-tree reject → soft-fail using pre-CAS blocker snapshot; never throw/reset.
167
- */
168
- async function reconcileAfterCas(input) {
169
- if (!input.snapshot)
170
- return [];
171
- const targetGit = createGit(input.snapshot.worktreePath);
172
- try {
173
- await targetGit.raw(["read-tree", "-m", "-u", input.oldHead, input.mergeCommit]);
174
- return [];
175
- }
176
- catch {
177
- return [
178
- buildReconcileWarning({
179
- worktreePath: input.snapshot.worktreePath,
180
- targetBranch: input.targetBranch,
181
- mergeCommit: input.mergeCommit,
182
- blockingPaths: input.snapshot.blockingPaths,
183
- }),
184
- ];
185
- }
186
- }
187
- async function defaultMergeDelivery(input) {
188
- const ledger = await readLedger(input.cwd, input.contractId);
189
- if (!ledger) {
190
- throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: ledger missing`);
191
- }
192
- const commission = commissionTarget(ledger.entries);
193
- if (!commission) {
194
- throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: missing commission target`);
195
- }
196
- const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
197
- if (!targetHead) {
198
- throw new FlowError("INTERNAL_STATE", `cannot claim ${input.contractId}: commission target ${commission.ref} does not resolve`);
199
- }
200
- const branch = `keiyaku/${input.contractId}`;
201
- const deliveryHead = await resolveCommit(input.cwd, branch);
202
- if (deliveryHead !== targetHead && await isAncestor(input.cwd, deliveryHead, targetHead)) {
203
- return { mergeCommit: targetHead };
204
- }
205
- const git = createGit(input.cwd);
206
- let tree;
207
- try {
208
- tree = (await git.raw(["merge-tree", "--write-tree", targetHead, deliveryHead])).trim();
209
- }
210
- catch (error) {
211
- throw new FlowError("CLOSE_MERGE_CONFLICT", `claim merge conflict for ${input.contractId} into ${commission.branch}`, { cause: error });
212
- }
213
- let mergeCommit;
214
- try {
215
- mergeCommit = (await git.raw([
216
- "commit-tree",
217
- tree,
218
- "-p",
219
- targetHead,
220
- "-p",
221
- deliveryHead,
222
- "-m",
223
- `keiyaku: claim ${input.contractId}`,
224
- "-m",
225
- `Merge contract ${input.contractId} into ${commission.branch}.`,
226
- ])).trim();
227
- }
228
- catch (error) {
229
- throw wrapGitError(`commit-tree claim ${input.contractId}`, error, input.cwd);
230
- }
231
- // Pre-CAS: locate target worktree and snapshot real blockers while HEAD/index still share oldHead.
232
- const preCasSnapshot = await snapshotPreCasBlockers({
233
- cwd: input.cwd,
234
- targetBranch: commission.branch,
235
- oldHead: targetHead,
236
- mergeCommit,
237
- });
238
- try {
239
- await git.raw(["update-ref", commission.ref, mergeCommit, targetHead]);
240
- }
241
- catch (error) {
242
- const currentHead = await resolveCommit(input.cwd, commission.ref);
243
- if (await isAncestor(input.cwd, deliveryHead, currentHead)) {
244
- return { mergeCommit: currentHead };
245
- }
246
- throw new FlowError("CONCURRENT_MODIFICATION", `target branch ${commission.branch} moved while claiming ${input.contractId}`, { cause: error });
247
- }
248
- // The target ref is authoritative. Receipt failure cannot undo its CAS.
249
- const receiptWarnings = [];
250
- try {
251
- await (input.writeClaimReceipt ?? appendClaimReceipt)(input.cwd, {
252
- kind: "receipt",
253
- oldHead: targetHead,
254
- newHead: mergeCommit,
255
- contract: input.contractId,
256
- });
257
- }
258
- catch {
259
- receiptWarnings.push("Claim receipt was not recorded after the target branch update; this movement will appear as an unreceipted FACT until manually recorded.");
260
- }
261
- const warnings = await reconcileAfterCas({
262
- contractId: input.contractId,
263
- targetBranch: commission.branch,
264
- oldHead: targetHead,
265
- mergeCommit,
266
- snapshot: preCasSnapshot,
267
- });
268
- return [...receiptWarnings, ...warnings].length > 0
269
- ? { mergeCommit, warnings: [...receiptWarnings, ...warnings] }
270
- : { mergeCommit };
271
- }
272
17
  function assertClaimableFromLedger(contractId, ledger) {
273
18
  if (!ledger) {
274
19
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found`);
@@ -284,6 +29,24 @@ function assertClaimableFromLedger(contractId, ledger) {
284
29
  }
285
30
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim from state ${state.state}`);
286
31
  }
32
+ async function publishClaimLedger(input) {
33
+ const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
34
+ if (entries.some((entry) => entry.kind === "claim"))
35
+ return null;
36
+ const entry = {
37
+ v: 1,
38
+ kind: "claim",
39
+ contract: input.contractId,
40
+ at: input.at,
41
+ actor: input.actor,
42
+ data: { mergeCommit: input.mergeCommit },
43
+ };
44
+ return entry;
45
+ });
46
+ if (!result.ok && result.reason !== "aborted") {
47
+ throw new FlowError("INTERNAL_STATE", `failed to append claim for ${input.contractId}: ${result.reason}`);
48
+ }
49
+ }
287
50
  export async function previewClaimContract(input) {
288
51
  const model = await readQueueReadModel(input.cwd);
289
52
  const membership = assertClaimQueueHead(model, input.contractId);
@@ -325,7 +88,7 @@ export async function previewClaimContract(input) {
325
88
  ...(deliveryHead ? { deliveryHead } : {}),
326
89
  cleanupEffects: [
327
90
  `release registry scope for ${input.contractId}`,
328
- cleanupPath ? `remove worktree ${path.relative(input.cwd, cleanupPath) || "."} when present` : "no linked worktree to remove",
91
+ cleanupPath ? `remove worktree ${relativeDisplayPath(input.cwd, cleanupPath)} when present` : "no linked worktree to remove",
329
92
  `delete branch keiyaku/${input.contractId} when present`,
330
93
  ],
331
94
  };
@@ -350,32 +113,31 @@ export async function claimContract(input) {
350
113
  let mergeCommit = existingClaim?.data.mergeCommit;
351
114
  let warnings;
352
115
  if (!mergeCommit) {
116
+ const usesDefaultMerger = input.mergeDelivery === undefined;
353
117
  const mergeDelivery = input.mergeDelivery ?? defaultMergeDelivery;
118
+ const publishClaim = async (acceptedMergeCommit) => publishClaimLedger({
119
+ cwd: input.cwd,
120
+ contractId: input.contractId,
121
+ actor: input.actor,
122
+ at,
123
+ mergeCommit: acceptedMergeCommit,
124
+ });
354
125
  // mergeDelivery must be idempotent: if a prior attempt merged delivery but
355
126
  // crashed before appending claim, a retry must return the existing merge
356
127
  // receipt without creating a second merge effect.
357
- const merged = await mergeDelivery({ cwd: input.cwd, contractId: input.contractId, writeClaimReceipt: input.writeClaimReceipt });
128
+ const merged = await mergeDelivery({
129
+ cwd: input.cwd,
130
+ contractId: input.contractId,
131
+ writeClaimReceipt: input.writeClaimReceipt,
132
+ ...(usesDefaultMerger ? { publishClaim } : {}),
133
+ });
358
134
  await input.afterMerge?.(merged);
359
135
  mergeCommit = merged.mergeCommit;
360
136
  if (merged.warnings && merged.warnings.length > 0) {
361
137
  warnings = [...merged.warnings];
362
138
  }
363
- const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
364
- if (entries.some((entry) => entry.kind === "claim"))
365
- return null;
366
- const entry = {
367
- v: 1,
368
- kind: "claim",
369
- contract: input.contractId,
370
- at,
371
- actor: input.actor,
372
- data: { mergeCommit: mergeCommit },
373
- };
374
- return entry;
375
- });
376
- if (!result.ok && result.reason !== "aborted") {
377
- throw new FlowError("INTERNAL_STATE", `failed to append claim for ${input.contractId}: ${result.reason}`);
378
- }
139
+ if (!usesDefaultMerger)
140
+ await publishClaim(mergeCommit);
379
141
  }
380
142
  // Claim entry naturally removes membership; no dequeue record.
381
143
  const cleanup = await cleanupClaim(input.cwd, input.contractId, input.actor, at);
@@ -5,7 +5,8 @@ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError
5
5
  import { getCurrentBranch, listLocalBranches } from "../git/branches.js";
6
6
  import { readLedger } from "./ledger.js";
7
7
  import { listContractIds } from "./ledger.js";
8
- import { deriveContractState, isTerminalState, latestBase } from "./status.js";
8
+ import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
9
+ import { latestBase } from "./status/drift.js";
9
10
  import { activeContractIdForWorktreeDir } from "./worktree-path.js";
10
11
  function isOperableLedger(entries) {
11
12
  const state = deriveContractState(entries);