@astrosheep/keiyaku 2.9.8 → 2.9.10

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 (230) hide show
  1. package/README.md +50 -182
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +10 -7
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +242 -79
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +65 -29
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +13 -35
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +45 -69
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +3 -1
  90. package/build/config/env.js +24 -1
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +13 -6
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +29 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +35 -337
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +125 -127
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { parseResolvedAkumaLaunchSnapshot, } from "../agents/launch-snapshot/model.js";
5
5
  import { resolvePersistedSubagentSession } from "../agents/session.js";
6
+ import { OutcomeErrorValidationError, parseCurrentOutcomeError, } from "../agents/harness/index.js";
6
7
  import { isErrnoException } from "../errno.js";
7
8
  import { FlowError, requireText } from "../flow-error.js";
8
9
  import { getCurrentBranch } from "../git/branches.js";
@@ -19,6 +20,7 @@ export { isResponseArtifactId, RESPONSE_ARTIFACT_ID_RE } from "./response-artifa
19
20
  const RESPONSE_HISTORY_SLUG_MAX_LENGTH = 24;
20
21
  const RESPONSE_HISTORY_SESSION_KEY = "session";
21
22
  const RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY = "launch_snapshot";
23
+ const RESPONSE_HISTORY_TERMINAL_FAILURE_KEY = "terminal_failure";
22
24
  const RESPONSE_HISTORY_PROVENANCE_KEY = "provenance";
23
25
  const REVIEW_HISTORY_FILE_RE = /^\d{8}-\d{9}_REVIEW_(\d+)\.md$/;
24
26
  /** Artifact-owner rendering of seal receipt/diffstat facts into the durable Diff section. */
@@ -99,6 +101,7 @@ function buildMarkdown(input, iso, provenance) {
99
101
  `${RESPONSE_HISTORY_PROVENANCE_KEY}: ${yamlQuote(JSON.stringify(provenance))}`,
100
102
  ...(input.session ? [`${RESPONSE_HISTORY_SESSION_KEY}: ${yamlQuote(JSON.stringify(input.session))}`] : []),
101
103
  ...(input.launchSnapshot ? [`${RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY}: ${yamlQuote(JSON.stringify(input.launchSnapshot))}`] : []),
104
+ ...(input.tool === "call" && input.terminalFailure ? [`${RESPONSE_HISTORY_TERMINAL_FAILURE_KEY}: ${yamlQuote(JSON.stringify(input.terminalFailure))}`] : []),
102
105
  ...(input.subagentName?.trim() ? [`subagent_name: ${yamlQuote(input.subagentName.trim())}`] : []),
103
106
  ...(input.effectivePolicy ? [`effective_policy: ${yamlQuote(JSON.stringify(input.effectivePolicy))}`] : []),
104
107
  `time: ${yamlQuote(iso)}`,
@@ -172,6 +175,10 @@ export async function readResponseArtifact(input) {
172
175
  throw new FlowError("INVALID_RESPONSE_PATH", `Response artifact not found: '${locator}'.`);
173
176
  throw error;
174
177
  }
178
+ return parseResponseArtifactDocument(input, locator, absolutePath, document);
179
+ }
180
+ /** Parse one already-read response document without a second filesystem read. */
181
+ async function parseResponseArtifactDocument(input, locator, absolutePath, document) {
175
182
  const ast = parseToAST(document);
176
183
  const frontmatter = ast.frontmatter?.entries;
177
184
  if (!frontmatter)
@@ -215,9 +222,94 @@ export async function readResponseArtifact(input) {
215
222
  throw artifactError(locator, RESPONSE_HISTORY_LAUNCH_SNAPSHOT_KEY, `is invalid${error instanceof Error ? ` (${error.message})` : ""}`);
216
223
  }
217
224
  }
225
+ let terminalFailure;
226
+ const rawTerminalFailure = frontmatter[RESPONSE_HISTORY_TERMINAL_FAILURE_KEY];
227
+ if (rawTerminalFailure !== undefined) {
228
+ let terminalFailureValue;
229
+ try {
230
+ terminalFailureValue = JSON.parse(rawTerminalFailure);
231
+ }
232
+ catch (error) {
233
+ throw artifactError(locator, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY, `contains malformed JSON${error instanceof Error ? ` (${error.message})` : ""}`);
234
+ }
235
+ try {
236
+ terminalFailure = parseCurrentOutcomeError(terminalFailureValue, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY);
237
+ }
238
+ catch (error) {
239
+ if (error instanceof OutcomeErrorValidationError) {
240
+ throw artifactError(locator, RESPONSE_HISTORY_TERMINAL_FAILURE_KEY, `is invalid (${error.message})`);
241
+ }
242
+ throw error;
243
+ }
244
+ }
218
245
  const subagentName = frontmatter.subagent_name?.trim() || undefined;
219
246
  const storageRoot = await fs.realpath(path.resolve(path.dirname(absolutePath), "..", ".."));
220
- return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(subagentName ? { subagentName } : {}) };
247
+ return { artifactId: provenance.artifactId, responsePath: path.relative(input.cwd, absolutePath), absolutePath, storageRoot, provenance, tool, ...(session ? { session } : {}), ...(launchSnapshot ? { launchSnapshot } : {}), ...(terminalFailure ? { terminalFailure } : {}), ...(subagentName ? { subagentName } : {}) };
248
+ }
249
+ function artifactIdFromDocument(document) {
250
+ const encoded = parseToAST(document).frontmatter?.entries?.[RESPONSE_HISTORY_PROVENANCE_KEY];
251
+ if (encoded === undefined)
252
+ return undefined;
253
+ try {
254
+ const provenance = JSON.parse(encoded);
255
+ return isPlainRecord(provenance) && typeof provenance.artifactId === "string" && isResponseArtifactId(provenance.artifactId)
256
+ ? provenance.artifactId
257
+ : undefined;
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ }
263
+ /** Read each response artifact file at most once to build a query-local lookup. */
264
+ export async function indexResponseArtifacts(input) {
265
+ let stableRoot;
266
+ try {
267
+ stableRoot = await fs.realpath(input.cwd);
268
+ }
269
+ catch (error) {
270
+ if (!isErrnoException(error) || error.code !== "ENOENT")
271
+ throw error;
272
+ stableRoot = path.resolve(input.cwd);
273
+ }
274
+ const historyDir = path.join(stableRoot, RESPONSE_HISTORY_DIR);
275
+ let names;
276
+ try {
277
+ names = await (input.readDirectory ?? fs.readdir)(historyDir);
278
+ }
279
+ catch (error) {
280
+ if (isErrnoException(error) && error.code === "ENOENT")
281
+ return { coverage: "complete", lookup: () => undefined };
282
+ throw error;
283
+ }
284
+ const entries = new Map();
285
+ let coverage = "complete";
286
+ for (const name of names.filter((candidate) => candidate.endsWith(".md")).sort()) {
287
+ const responsePath = path.join(RESPONSE_HISTORY_DIR, name);
288
+ let document;
289
+ try {
290
+ document = await fs.readFile(path.join(stableRoot, responsePath), "utf8");
291
+ const artifact = await parseResponseArtifactDocument({ cwd: stableRoot, responsePath }, responsePath, path.join(stableRoot, responsePath), document);
292
+ if (!artifact)
293
+ continue;
294
+ if (entries.has(artifact.artifactId))
295
+ entries.set(artifact.artifactId, { state: "incompatible" });
296
+ else
297
+ entries.set(artifact.artifactId, { state: "available", artifact });
298
+ }
299
+ catch (error) {
300
+ const artifactId = document === undefined ? undefined : artifactIdFromDocument(document);
301
+ if (isErrnoException(error)) {
302
+ if (artifactId && error.code !== "ENOENT")
303
+ entries.set(artifactId, { state: "unavailable" });
304
+ if (error.code !== "ENOENT")
305
+ coverage = "incomplete";
306
+ }
307
+ else if (artifactId) {
308
+ entries.set(artifactId, { state: "incompatible" });
309
+ }
310
+ }
311
+ }
312
+ return { coverage, lookup: (artifactId) => entries.get(artifactId) };
221
313
  }
222
314
  async function findResponseArtifactAtRoot(input) {
223
315
  const physicalHistoryDir = path.join(input.stableRoot, RESPONSE_HISTORY_DIR);
@@ -1,5 +1,6 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
- export const WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES = 2048;
2
+ import { terminateProcessGroup } from "./process-group.js";
3
+ export const WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES = 2048;
3
4
  export const WORKTREE_BOOTSTRAP_KILL_GRACE_MS = 10_000;
4
5
  function defaultWait(ms) {
5
6
  return new Promise((resolve) => {
@@ -8,16 +9,16 @@ function defaultWait(ms) {
8
9
  });
9
10
  }
10
11
  /** Retain the last 2048 UTF-8 bytes and decode lossily (partial leading code point → U+FFFD). */
11
- export function tailUtf8Bytes(bytes, maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES) {
12
+ export function tailUtf8Bytes(bytes, maxBytes = WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES) {
12
13
  const truncated = bytes.byteLength > maxBytes;
13
14
  const slice = truncated ? bytes.subarray(bytes.byteLength - maxBytes) : bytes;
14
15
  return { text: slice.toString("utf8"), truncated };
15
16
  }
16
17
  /**
17
- * Rolling stderr accumulator: holds at most `maxBytes` across every chunk and
18
- * remembers whether any earlier prefix was dropped. Never retains unbounded stderr.
18
+ * Rolling output accumulator: holds at most `maxBytes` across every chunk and
19
+ * remembers whether any earlier prefix was dropped.
19
20
  */
20
- function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES) {
21
+ function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_OUTPUT_TAIL_BYTES) {
21
22
  let tail = Buffer.alloc(0);
22
23
  let truncated = false;
23
24
  return {
@@ -56,17 +57,15 @@ function createRollingByteTail(maxBytes = WORKTREE_BOOTSTRAP_STDERR_TAIL_BYTES)
56
57
  },
57
58
  };
58
59
  }
59
- function observationFrom(result, durationMs, stderr) {
60
+ function observationFrom(result, durationMs, output) {
60
61
  const safeDuration = Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(durationMs)));
61
- if (!stderr || stderr.bytes.byteLength === 0) {
62
- return { result, durationMs: safeDuration };
63
- }
64
- // Buffer is already the bounded tail; decode lossily (partial leading code point → U+FFFD).
65
62
  return {
66
63
  result,
67
64
  durationMs: safeDuration,
68
- stderrTail: stderr.bytes.toString("utf8"),
69
- ...(stderr.truncated ? { stderrTruncated: true } : {}),
65
+ ...(output.stdout ? { stdoutTail: output.stdout.bytes.toString("utf8") } : {}),
66
+ ...(output.stdout?.truncated ? { stdoutTruncated: true } : {}),
67
+ ...(output.stderr ? { stderrTail: output.stderr.bytes.toString("utf8") } : {}),
68
+ ...(output.stderr?.truncated ? { stderrTruncated: true } : {}),
70
69
  };
71
70
  }
72
71
  function spawnFailureObservation(message, durationMs) {
@@ -78,17 +77,10 @@ function spawnFailureObservation(message, durationMs) {
78
77
  ...(tail.truncated ? { stderrTruncated: true } : {}),
79
78
  };
80
79
  }
81
- function killChild(child, signal) {
82
- try {
83
- child.kill(signal);
84
- }
85
- catch {
86
- // Process may have raced out between the liveness check and kill.
87
- }
88
- }
89
80
  /**
90
81
  * Run the project-declared worktree bootstrap once.
91
- * argv only (shell: false), cwd = new worktree, inherited env, stdin/stdout ignored, stderr piped.
82
+ * argv only (shell: false), cwd = new worktree, inherited env, stdin ignored.
83
+ * Output capture is opt-in so bind retains its established stdio and persistence behavior.
92
84
  */
93
85
  export async function runWorktreeBootstrap(input, dependencies = {}) {
94
86
  const spawn = dependencies.spawn ?? nodeSpawn;
@@ -102,6 +94,7 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
102
94
  };
103
95
  }
104
96
  const startedAt = nowMs();
97
+ const stdoutTail = input.captureOutput ? createRollingByteTail() : null;
105
98
  const stderrTail = createRollingByteTail();
106
99
  let child;
107
100
  try {
@@ -109,7 +102,8 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
109
102
  cwd: input.cwd,
110
103
  env: process.env,
111
104
  shell: false,
112
- stdio: ["ignore", "ignore", "pipe"],
105
+ detached: process.platform !== "win32",
106
+ stdio: ["ignore", input.captureOutput ? "pipe" : "ignore", "pipe"],
113
107
  });
114
108
  }
115
109
  catch (error) {
@@ -129,6 +123,9 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
129
123
  clearTimeout(timeoutHandle);
130
124
  resolve(observation);
131
125
  };
126
+ child.stdout?.on("data", (chunk) => {
127
+ stdoutTail?.append(chunk);
128
+ });
132
129
  child.stderr?.on("data", (chunk) => {
133
130
  stderrTail.append(chunk);
134
131
  });
@@ -139,35 +136,45 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
139
136
  child.on("close", (code, signal) => {
140
137
  exited = true;
141
138
  const durationMs = Math.max(0, nowMs() - startedAt);
142
- const snap = stderrTail.snapshot();
143
- const stderr = snap.bytes ? { bytes: snap.bytes, truncated: snap.truncated } : null;
139
+ const stdoutSnapshot = stdoutTail?.snapshot();
140
+ const stderrSnapshot = stderrTail.snapshot();
141
+ const capturedOutput = {
142
+ stdout: stdoutSnapshot?.bytes ? { bytes: stdoutSnapshot.bytes, truncated: stdoutSnapshot.truncated } : null,
143
+ stderr: stderrSnapshot.bytes ? { bytes: stderrSnapshot.bytes, truncated: stderrSnapshot.truncated } : null,
144
+ };
145
+ const failureOutput = { stdout: capturedOutput.stdout, stderr: capturedOutput.stderr };
144
146
  if (timedOut) {
145
- finish(observationFrom({ kind: "timeout" }, durationMs, stderr));
147
+ finish(observationFrom({ kind: "timeout" }, durationMs, failureOutput));
146
148
  return;
147
149
  }
148
150
  if (signal) {
149
- finish(observationFrom({ kind: "signal", signal }, durationMs, stderr));
151
+ finish(observationFrom({ kind: "signal", signal }, durationMs, failureOutput));
150
152
  return;
151
153
  }
152
154
  const exitCode = typeof code === "number" ? code : 1;
153
155
  const normalized = Math.max(0, Math.min(255, exitCode));
154
156
  if (normalized === 0) {
155
- // Success discards stderr but still clamps durationMs.
156
- finish(observationFrom({ kind: "exit", code: 0 }, durationMs, null));
157
+ const successOutput = input.captureOutput ? capturedOutput : { stdout: null, stderr: null };
158
+ finish(observationFrom({ kind: "exit", code: 0 }, durationMs, successOutput));
157
159
  return;
158
160
  }
159
- finish(observationFrom({ kind: "exit", code: normalized }, durationMs, stderr));
161
+ finish(observationFrom({ kind: "exit", code: normalized }, durationMs, failureOutput));
160
162
  });
161
163
  timeoutHandle = setTimeout(() => {
162
164
  void (async () => {
163
165
  if (settled || exited)
164
166
  return;
165
167
  timedOut = true;
166
- killChild(child, "SIGTERM");
167
- await wait(WORKTREE_BOOTSTRAP_KILL_GRACE_MS);
168
- // SIGKILL only if the child has not reached a terminal close yet.
169
- if (!exited && !settled) {
170
- killChild(child, "SIGKILL");
168
+ try {
169
+ await terminateProcessGroup(child, { graceMs: WORKTREE_BOOTSTRAP_KILL_GRACE_MS, wait });
170
+ }
171
+ catch {
172
+ const stdoutSnapshot = stdoutTail?.snapshot();
173
+ const stderrSnapshot = stderrTail.snapshot();
174
+ finish(observationFrom({ kind: "timeout" }, Math.max(0, nowMs() - startedAt), {
175
+ stdout: stdoutSnapshot?.bytes ? { bytes: stdoutSnapshot.bytes, truncated: stdoutSnapshot.truncated } : null,
176
+ stderr: stderrSnapshot.bytes ? { bytes: stderrSnapshot.bytes, truncated: stderrSnapshot.truncated } : null,
177
+ }));
171
178
  }
172
179
  })();
173
180
  }, input.plan.timeoutMs);
@@ -175,11 +182,12 @@ export async function runWorktreeBootstrap(input, dependencies = {}) {
175
182
  });
176
183
  }
177
184
  export function bootstrapEntryDataFromObservation(observation) {
185
+ const retainFailureStderr = !isBootstrapSuccess(observation);
178
186
  return {
179
187
  result: observation.result,
180
188
  durationMs: observation.durationMs,
181
- ...(observation.stderrTail !== undefined ? { stderrTail: observation.stderrTail } : {}),
182
- ...(observation.stderrTruncated ? { stderrTruncated: true } : {}),
189
+ ...(retainFailureStderr && observation.stderrTail !== undefined ? { stderrTail: observation.stderrTail } : {}),
190
+ ...(retainFailureStderr && observation.stderrTruncated ? { stderrTruncated: true } : {}),
183
191
  };
184
192
  }
185
193
  export function isBootstrapSuccess(observation) {
@@ -1,14 +1,61 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs/promises";
3
3
  import { FlowError } from "../flow-error.js";
4
- import { createGit, wrapGitError } from "../git/core.js";
5
- import { readLedger } from "./ledger.js";
6
- import { readLedgerSnapshot } from "./ledger-batch.js";
7
- import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
4
+ import { createGit, runGitProcess, wrapGitError } from "../git/core.js";
5
+ import { resolveLocalBranchHead } from "../git/refs.js";
6
+ import { readCurrentContractLifecycle } from "./repository-ledger/read-model.js";
8
7
  import { normalizeFilesystemCoordinate } from "../fs/path-coordinate.js";
9
8
  import { gitCommonRootPath, resolveGitCommonRoot } from "../git/path-coordinate.js";
10
- export function findBindEntry(entries) {
11
- return entries.find((entry) => entry.kind === "bind");
9
+ function record(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value)
11
+ ? value
12
+ : undefined;
13
+ }
14
+ /** Reconstruct the public terminal-cleanup fact from its independent replay receipts. */
15
+ export function terminalCleanupFacts(worktreeValue, deliveryRefValue) {
16
+ const worktree = record(worktreeValue);
17
+ const delivery = record(deliveryRefValue);
18
+ if (!worktree || !Array.isArray(worktree.absent) || !delivery)
19
+ return undefined;
20
+ const absent = worktree.absent.filter((item) => item === "worktree");
21
+ const base = {
22
+ absent,
23
+ ...(typeof worktree.removedWorktreePath === "string" ? { removedWorktreePath: worktree.removedWorktreePath } : {}),
24
+ };
25
+ const failure = record(worktree.worktreeRemoveFailure);
26
+ if (failure && typeof failure.path === "string" && typeof failure.reason === "string") {
27
+ base.worktreeRemoveFailure = { path: failure.path, reason: failure.reason };
28
+ }
29
+ if (delivery.status === "aligned"
30
+ && typeof delivery.ref === "string"
31
+ && delivery.expected === "absent"
32
+ && delivery.observed === "absent") {
33
+ return { ...base, deliveryRef: { status: "aligned", ref: delivery.ref, expected: "absent", observed: "absent" } };
34
+ }
35
+ const mismatch = record(delivery.mismatch);
36
+ const observed = record(mismatch?.observed);
37
+ if (delivery.status === "mismatch"
38
+ && mismatch?.kind === "projected-surface-mismatch"
39
+ && mismatch.surface === "delivery-ref"
40
+ && typeof mismatch.ref === "string"
41
+ && mismatch.expected === "absent"
42
+ && observed?.state === "present"
43
+ && typeof observed.head === "string") {
44
+ return {
45
+ ...base,
46
+ deliveryRef: {
47
+ status: "mismatch",
48
+ mismatch: {
49
+ kind: "projected-surface-mismatch",
50
+ surface: "delivery-ref",
51
+ ref: mismatch.ref,
52
+ expected: "absent",
53
+ observed: { state: "present", head: observed.head },
54
+ },
55
+ },
56
+ };
57
+ }
58
+ return undefined;
12
59
  }
13
60
  function parseWorktreeListPorcelainZ(output) {
14
61
  const listings = [];
@@ -28,7 +75,16 @@ function parseWorktreeListPorcelainZ(output) {
28
75
  listings.push(current);
29
76
  return listings;
30
77
  }
31
- async function listWorktrees(cwd) {
78
+ async function listWorktrees(cwd, signal) {
79
+ if (signal) {
80
+ const result = await runGitProcess(cwd, ["worktree", "list", "--porcelain", "-z"], { signal });
81
+ if (result.error || result.status !== 0) {
82
+ if (result.error?.name === "AbortError")
83
+ throw result.error;
84
+ throw wrapGitError("worktree list --porcelain -z", result.error ?? new Error(result.stderr || `exit ${String(result.status)}`), cwd);
85
+ }
86
+ return parseWorktreeListPorcelainZ(result.stdout);
87
+ }
32
88
  const git = createGit(cwd);
33
89
  try {
34
90
  return parseWorktreeListPorcelainZ(await git.raw(["worktree", "list", "--porcelain", "-z"]));
@@ -80,16 +136,19 @@ function contractWorktreePathFromStableRoot(stableRoot, contractId, bind) {
80
136
  return path.join(stableRoot, ".keiyaku", "wt", bind?.data.place ?? contractId);
81
137
  }
82
138
  export async function contractWorktreePath(cwd, contractId) {
83
- const ledger = await readLedger(cwd, contractId);
84
- const bind = ledger ? findBindEntry(ledger.entries) : undefined;
85
- return await contractWorktreePathFromBind(cwd, contractId, bind);
139
+ const stableRoot = await stableRepoRoot(cwd);
140
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
141
+ return path.join(stableRoot, ".keiyaku", "wt", outcome.status === "healthy" || outcome.status === "diseased"
142
+ ? outcome.model.resources.worktree?.place ?? contractId
143
+ : contractId);
86
144
  }
87
145
  export async function requireContractWorktreePath(cwd, contractId) {
88
- const ledger = await readLedger(cwd, contractId);
89
- if (!ledger) {
146
+ const stableRoot = await stableRepoRoot(cwd);
147
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
148
+ if (outcome.status !== "healthy" || !outcome.model.resources.worktree) {
90
149
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found`);
91
150
  }
92
- return await contractWorktreePathFromBind(cwd, contractId, findBindEntry(ledger.entries));
151
+ return path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place);
93
152
  }
94
153
  async function findContractWorktree(cwd, contractId) {
95
154
  const expectedBranch = `refs/heads/keiyaku/${contractId}`;
@@ -108,21 +167,53 @@ export async function findContractWorktreePath(cwd, contractId) {
108
167
  }
109
168
  export async function findRegisteredContractWorktreePath(cwd, contractId) {
110
169
  const stableRoot = await stableRepoRoot(cwd);
111
- const ledger = await readLedger(stableRoot, contractId);
112
- const bind = ledger ? findBindEntry(ledger.entries) : undefined;
113
- if (!bind || bind.data.workspace !== "worktree")
170
+ const outcome = await readCurrentContractLifecycle(stableRoot, contractId);
171
+ if (outcome.status !== "healthy" || outcome.model.resources.worktree?.state !== "held")
114
172
  return undefined;
115
- let expected;
116
- try {
117
- expected = await fs.realpath(contractWorktreePathFromStableRoot(stableRoot, contractId, bind));
118
- }
119
- catch {
173
+ return findRegisteredContractWorktreePathFromModel(stableRoot, outcome.model);
174
+ }
175
+ export async function findRegisteredContractWorktreePathFromModel(stableRoot, model) {
176
+ const worktree = model.resources.worktree;
177
+ if (!worktree)
120
178
  return undefined;
179
+ const expected = path.join(stableRoot, ".keiyaku", "wt", worktree.place);
180
+ for (const registered of await listWorktrees(stableRoot)) {
181
+ if (registered.branch !== model.resources.deliveryRef)
182
+ continue;
183
+ try {
184
+ if (normalizeFilesystemCoordinate(await fs.realpath(registered.path)) === normalizeFilesystemCoordinate(await fs.realpath(expected))) {
185
+ return await fs.realpath(registered.path);
186
+ }
187
+ }
188
+ catch { /* stale registration is not lifecycle evidence */ }
121
189
  }
190
+ return undefined;
191
+ }
192
+ async function findRegisteredContractWorktreePathFromBind(stableRoot, contractId, _bind) {
193
+ const expectedBranch = `refs/heads/keiyaku/${contractId}`;
122
194
  for (const worktree of await listWorktrees(stableRoot)) {
195
+ let ownsBranch = worktree.branch === expectedBranch;
196
+ if (!ownsBranch && !worktree.branch) {
197
+ for (const statePath of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
198
+ try {
199
+ const gitPath = (await createGit(worktree.path).raw(["rev-parse", "--git-path", statePath])).trim();
200
+ if ((await fs.readFile(gitPath, "utf8")).trim() === expectedBranch) {
201
+ ownsBranch = true;
202
+ break;
203
+ }
204
+ }
205
+ catch {
206
+ // No owned rebase coordinate at this registered worktree.
207
+ }
208
+ }
209
+ }
210
+ if (!ownsBranch)
211
+ continue;
123
212
  try {
124
- if (await fs.realpath(worktree.path) === expected)
125
- return worktree.path;
213
+ const dotGit = await fs.stat(path.join(worktree.path, ".git"));
214
+ if (dotGit.isDirectory())
215
+ continue;
216
+ return await fs.realpath(worktree.path);
126
217
  }
127
218
  catch {
128
219
  // A stale Git registration is not an engine-owned worktree fact.
@@ -130,12 +221,21 @@ export async function findRegisteredContractWorktreePath(cwd, contractId) {
130
221
  }
131
222
  return undefined;
132
223
  }
133
- /**
134
- * Delivery coordinate for dirty admission and KEIYAKU.md render: the linked
135
- * contract worktree when Git registers one, otherwise cwd (workspace here).
136
- */
137
- export async function resolveContractDeliveryCwd(cwd, contractId) {
138
- return (await findContractWorktreePath(cwd, contractId)) ?? cwd;
224
+ /** Prove the sole carrier target from stable-root, bind, and Git registration facts. */
225
+ export async function resolveContractCarrierTarget(stableRoot, contractId, bind) {
226
+ if (bind.data.workspace === "here") {
227
+ const root = await fs.realpath(stableRoot);
228
+ return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "here" };
229
+ }
230
+ const root = await findRegisteredContractWorktreePathFromBind(stableRoot, contractId, bind);
231
+ if (!root) {
232
+ return {
233
+ status: "unavailable",
234
+ workspace: "worktree",
235
+ reason: `contract ${contractId} has no verified registered engine worktree`,
236
+ };
237
+ }
238
+ return { status: "available", root, path: path.join(root, ".keiyaku", "KEIYAKU.md"), workspace: "worktree" };
139
239
  }
140
240
  /**
141
241
  * Resolve the contract id for an engine-owned bound worktree containing cwd.
@@ -144,10 +244,29 @@ export async function resolveContractDeliveryCwd(cwd, contractId) {
144
244
  */
145
245
  async function contractIdForEngineOwnedWorktree(cwd, options) {
146
246
  const stableRoot = options.stableRoot ?? await stableRepoRoot(cwd);
247
+ const candidate = await contractIdentityFromWorktreeWitness(cwd, stableRoot);
248
+ if (!candidate)
249
+ return undefined;
250
+ const outcome = await readCurrentContractLifecycle(stableRoot, candidate.contractId);
251
+ if (outcome.status !== "healthy" || !outcome.model.resources.worktree)
252
+ return undefined;
253
+ if (options.requireActive && ["claimed", "forfeited"].includes(outcome.model.phase))
254
+ return undefined;
255
+ let expectedWorktree;
256
+ try {
257
+ expectedWorktree = await fs.realpath(path.join(stableRoot, ".keiyaku", "wt", outcome.model.resources.worktree.place));
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ return candidate.path === expectedWorktree ? candidate.contractId : undefined;
263
+ }
264
+ /** Resolve only Git's local registered-worktree witness; lifecycle facts must verify it separately. */
265
+ export async function contractIdentityFromWorktreeWitness(cwd, stableRoot, signal) {
147
266
  const canonicalCwd = await fs.realpath(cwd);
267
+ const branchPrefix = "refs/heads/keiyaku/";
148
268
  const candidates = [];
149
- for (const worktree of await listWorktrees(stableRoot)) {
150
- const branchPrefix = "refs/heads/keiyaku/";
269
+ for (const worktree of await listWorktrees(stableRoot, signal)) {
151
270
  if (!worktree.branch?.startsWith(branchPrefix))
152
271
  continue;
153
272
  let canonicalWorktree;
@@ -162,27 +281,10 @@ async function contractIdForEngineOwnedWorktree(cwd, options) {
162
281
  candidates.push({
163
282
  contractId: worktree.branch.slice(branchPrefix.length),
164
283
  path: canonicalWorktree,
284
+ branch: worktree.branch,
165
285
  });
166
286
  }
167
- const candidate = candidates.sort((left, right) => right.path.length - left.path.length)[0];
168
- if (!candidate)
169
- return undefined;
170
- const ledger = await readLedgerSnapshot(stableRoot, candidate.contractId);
171
- if (!ledger)
172
- return undefined;
173
- if (options.requireActive && isTerminalState(deriveContractState(ledger.entries)))
174
- return undefined;
175
- const bind = findBindEntry(ledger.entries);
176
- if (!bind || bind.data.workspace !== "worktree")
177
- return undefined;
178
- let expectedWorktree;
179
- try {
180
- expectedWorktree = await fs.realpath(contractWorktreePathFromStableRoot(stableRoot, candidate.contractId, bind));
181
- }
182
- catch {
183
- return undefined;
184
- }
185
- return candidate.path === expectedWorktree ? candidate.contractId : undefined;
287
+ return candidates.sort((left, right) => right.path.length - left.path.length || left.contractId.localeCompare(right.contractId))[0];
186
288
  }
187
289
  /**
188
290
  * Resolve the contract bound by an engine-owned worktree containing cwd,
@@ -196,12 +298,48 @@ export async function contractBoundByEngineWorktree(cwd, stableRoot) {
196
298
  export async function activeContractBoundByEngineWorktree(cwd, stableRoot) {
197
299
  return contractIdForEngineOwnedWorktree(cwd, { requireActive: true, stableRoot });
198
300
  }
199
- export async function cleanupContractWorkspace(cwd, contractId) {
301
+ /**
302
+ * Verify a worktree binding against a folded current-ledger model. Git's
303
+ * registration is only a physical witness here; it cannot create lifecycle
304
+ * state or select a contract absent from the supplied read model.
305
+ */
306
+ export async function currentContractBoundByWorktreeWitness(input) {
307
+ let canonicalCwd;
308
+ try {
309
+ canonicalCwd = await fs.realpath(input.cwd);
310
+ }
311
+ catch {
312
+ return undefined;
313
+ }
314
+ const registeredWorktrees = await listWorktrees(input.stableRoot);
315
+ const candidates = [];
316
+ for (const model of input.contracts) {
317
+ const worktree = model.resources.worktree;
318
+ if (!worktree || worktree.state !== "held")
319
+ continue;
320
+ const expectedPath = path.join(input.stableRoot, ".keiyaku", "wt", worktree.place);
321
+ let canonicalExpectedPath;
322
+ try {
323
+ canonicalExpectedPath = await fs.realpath(expectedPath);
324
+ }
325
+ catch {
326
+ continue;
327
+ }
328
+ if (!worktreeContainsCoordinate(canonicalExpectedPath, canonicalCwd))
329
+ continue;
330
+ const registered = registeredWorktrees.some((candidate) => candidate.branch === model.resources.deliveryRef &&
331
+ normalizeFilesystemCoordinate(candidate.path) === normalizeFilesystemCoordinate(canonicalExpectedPath));
332
+ if (registered)
333
+ candidates.push({ contract: model.contract, path: canonicalExpectedPath });
334
+ }
335
+ candidates.sort((left, right) => right.path.length - left.path.length || left.contract.localeCompare(right.contract));
336
+ return candidates[0]?.contract;
337
+ }
338
+ /** Remove only the derived linked worktree. Authoritative refs are outside this adapter. */
339
+ export async function cleanupContractWorkspace(cwd, lifecycle) {
200
340
  const stableRoot = await stableRepoRoot(cwd);
201
341
  const git = createGit(stableRoot);
202
- const branch = `keiyaku/${contractId}`;
203
- const worktree = await findContractWorktree(stableRoot, contractId);
204
- const worktreePath = worktree.path;
342
+ const worktreePath = await findRegisteredContractWorktreePathFromModel(stableRoot, lifecycle);
205
343
  const facts = { absent: [] };
206
344
  if (worktreePath) {
207
345
  try {
@@ -215,28 +353,23 @@ export async function cleanupContractWorkspace(cwd, contractId) {
215
353
  else {
216
354
  facts.absent.push("worktree");
217
355
  }
218
- if (worktree.primary) {
219
- facts.absent.push("branch");
220
- return facts;
221
- }
222
- let branchRef;
223
- try {
224
- branchRef = (await git.raw(["for-each-ref", "--format=%(refname)", `refs/heads/${branch}`])).trim();
225
- }
226
- catch (error) {
227
- throw wrapGitError(`for-each-ref refs/heads/${branch}`, error, stableRoot);
228
- }
229
- if (branchRef !== `refs/heads/${branch}`) {
230
- facts.absent.push("branch");
231
- }
232
- else {
233
- try {
234
- await git.raw(["branch", "-D", branch]);
235
- facts.deletedBranch = branch;
236
- }
237
- catch (error) {
238
- facts.branchDeleteFailure = { name: branch, reason: wrapGitError(`branch -D ${branch}`, error, stableRoot).message };
239
- }
240
- }
241
356
  return facts;
242
357
  }
358
+ /** Observe the terminal transaction's expected delivery-ref absence without reconciling it. */
359
+ export async function observeTerminalDeliveryRef(cwd, contractId) {
360
+ const stableRoot = await stableRepoRoot(cwd);
361
+ const ref = `refs/heads/keiyaku/${contractId}`;
362
+ const observed = await resolveLocalBranchHead(stableRoot, ref);
363
+ if (!observed)
364
+ return { status: "aligned", ref, expected: "absent", observed: "absent" };
365
+ return {
366
+ status: "mismatch",
367
+ mismatch: {
368
+ kind: "projected-surface-mismatch",
369
+ surface: "delivery-ref",
370
+ ref,
371
+ expected: "absent",
372
+ observed: { state: "present", head: observed.head },
373
+ },
374
+ };
375
+ }