@astrosheep/keiyaku 2.9.9 → 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 +2 -1
  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 +6 -1
  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 +3 -2
  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 +9 -16
  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 +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  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 +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -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 +21 -358
  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 +114 -119
  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
@@ -1,174 +1,256 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
2
  import { assertCleanTrackedWorkingTree } from "../../git/worktree.js";
3
- import { activeLedgerRef, appendWithRetry, readLedger } from "../ledger.js";
3
+ import { runLifecycle } from "../lifecycle-runner.js";
4
+ import { isJsonObject } from "../outcome-base.js";
5
+ import { terminalCleanupFacts } from "../worktree-path.js";
4
6
  import { assertClaimQueueHead, readQueueReadModel } from "./queue.js";
5
- import { releaseScope } from "../registry.js";
6
- import { deriveContractState } from "../status/lifecycle.js";
7
- import { commissionTarget, latestBase } from "../status/drift.js";
8
- import { cleanupContractWorkspace, findContractWorktreePath, stableRepoRoot } from "../worktree-path.js";
9
- import { defaultMergeDelivery, relativeDisplayPath, resolveCommit, resolveCommitOrNull, } from "./claim-delivery.js";
10
- async function cleanupClaim(cwd, contractId, actor, at) {
11
- const stableRoot = await stableRepoRoot(cwd);
12
- await releaseScope(stableRoot, { contractId, actor, at });
13
- return await cleanupContractWorkspace(stableRoot, contractId);
14
- }
15
- function findClaim(entries) {
16
- return entries.find((entry) => entry.kind === "claim");
7
+ import { buildClaimLanding, relativeDisplayPath, } from "./claim-delivery.js";
8
+ export function claimDeliveryCoordinateMismatch(published, delivery) {
9
+ const assertion = published.observations.find((observation) => (observation.ref === delivery.ref
10
+ && observation.expectedOldOid === delivery.head));
11
+ if (!assertion || assertion.observed.status === "unknown")
12
+ return undefined;
13
+ if (assertion.observed.status === "present" && assertion.observed.objectId === delivery.head)
14
+ return undefined;
15
+ return {
16
+ code: "CONCURRENT_MODIFICATION",
17
+ message: "claim delivery coordinate changed before atomic publication",
18
+ facts: {
19
+ delivery: {
20
+ expected: { ref: delivery.ref, head: delivery.head },
21
+ observed: assertion.observed,
22
+ },
23
+ },
24
+ };
17
25
  }
18
- /** Verdict belonging to the open petition window (after last petition, before claim). */
19
- function latestCurrentPetitionVerdict(entries) {
20
- for (let index = entries.length - 1; index >= 0; index -= 1) {
21
- const entry = entries[index];
22
- if (entry.kind === "verdict")
23
- return entry;
24
- if (entry.kind === "petition")
25
- return undefined;
26
- }
27
- return undefined;
26
+ function acceptedClaim(model) {
27
+ const row = [...model.history].reverse().find(({ entry }) => entry.kind === "claim");
28
+ return row?.entry.kind === "claim" ? { objectId: row.objectId, entry: row.entry } : undefined;
28
29
  }
29
- /**
30
- * Delivery requires an approved verdict for the current petition.
31
- * Already-claimed cleanup retry remains legal without re-proving judgment.
32
- */
33
- function assertClaimableFromLedger(contractId, ledger) {
34
- if (!ledger) {
35
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found`);
36
- }
37
- const state = deriveContractState(ledger.entries);
38
- const claim = findClaim(ledger.entries);
39
- // Already-claimed cleanup retry: legal head even if a prior crash left debris.
40
- if (claim && state.state === "claimed")
41
- return claim;
42
- if (state.state === "petitioned") {
43
- const verdict = latestCurrentPetitionVerdict(ledger.entries);
44
- if (verdict?.data.result !== "approved") {
45
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim without approved verdict for current petition`);
46
- }
30
+ function assertClaimable(contractId, model) {
31
+ const claim = acceptedClaim(model);
32
+ if (model.phase === "claimed" && claim)
47
33
  return claim;
34
+ if (model.phase === "approved" && model.petition.state === "approved" && model.petition.intent === "claim")
35
+ return undefined;
36
+ if (model.phase === "forfeited") {
37
+ throw new FlowError("CONTRACT_TERMINAL", `contract ${contractId} is terminal (${model.phase})`);
48
38
  }
49
- if (state.state === "forfeited") {
50
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} forfeited`);
51
- }
52
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim from state ${state.state}`);
53
- }
54
- async function publishClaimLedger(input) {
55
- const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
56
- if (entries.some((entry) => entry.kind === "claim"))
57
- return null;
58
- const entry = {
59
- v: 1,
60
- kind: "claim",
61
- contract: input.contractId,
62
- at: input.at,
63
- actor: input.actor,
64
- data: { mergeCommit: input.mergeCommit },
65
- };
66
- return entry;
67
- });
68
- if (!result.ok && result.reason !== "aborted") {
69
- throw new FlowError("INTERNAL_STATE", `failed to append claim for ${input.contractId}: ${result.reason}`);
39
+ if (model.phase === "awaiting-verdict") {
40
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim without approved verdict for current petition`);
70
41
  }
42
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot claim from state ${model.phase}`);
71
43
  }
72
- export async function previewClaimContract(input) {
73
- const model = await readQueueReadModel(input.cwd);
74
- const membership = assertClaimQueueHead(model, input.contractId);
75
- const ledger = await readLedger(input.cwd, input.contractId);
76
- assertClaimableFromLedger(input.contractId, ledger);
77
- const entries = ledger.entries;
78
- const cleanupPath = await findContractWorktreePath(input.cwd, input.contractId);
79
- const state = deriveContractState(entries);
80
- const base = latestBase(entries);
81
- const commission = commissionTarget(entries);
82
- if (!commission) {
83
- throw new FlowError("INTERNAL_STATE", `cannot preview claim ${input.contractId}: missing commission target`);
44
+ function freezeClaimCoordinates(contractId, model, pins) {
45
+ if (model.petition.state !== "approved" || model.petition.intent !== "claim" || !model.petition.targetHead) {
46
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} has no approved claim coordinates`);
84
47
  }
85
- const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
86
- if (!targetHead) {
87
- throw new FlowError("INTERNAL_STATE", `cannot preview claim ${input.contractId}: commission target ${commission.ref} does not resolve`);
88
- }
89
- const freshness = base
90
- ? base === targetHead ? "fresh" : "stale"
91
- : "unknown";
92
- const deliveryBranch = `keiyaku/${input.contractId}`;
93
- let deliveryHead;
94
- try {
95
- deliveryHead = await resolveCommit(input.cwd, deliveryBranch);
96
- }
97
- catch {
98
- deliveryHead = undefined;
48
+ if (pins.target.ref !== model.petition.target ||
49
+ pins.target.head !== model.petition.targetHead ||
50
+ pins.delivery.head !== model.pins.delivery) {
51
+ throw new FlowError("CONCURRENT_MODIFICATION", `claim coordinates for ${contractId} no longer match approved petition`);
99
52
  }
100
53
  return {
101
- contractId: input.contractId,
102
- queueHead: membership.queueHead,
103
- queuePosition: membership.position,
104
- state: state.state,
105
- freshness,
106
- ...(base ? { base } : {}),
107
- targetBranch: commission.branch,
108
- targetHead,
109
- deliveryBranch,
110
- ...(deliveryHead ? { deliveryHead } : {}),
111
- cleanupEffects: [
112
- `release registry scope for ${input.contractId}`,
113
- cleanupPath ? `remove worktree ${relativeDisplayPath(input.cwd, cleanupPath)} when present` : "no linked worktree to remove",
114
- `delete branch keiyaku/${input.contractId} when present`,
115
- ],
54
+ base: model.pins.base,
55
+ target: model.petition.target,
56
+ targetHead: model.petition.targetHead,
57
+ deliveryHead: model.pins.delivery,
116
58
  };
117
59
  }
118
- export async function claimContract(input) {
119
- const at = new Date(input.nowMs).toISOString();
120
- const deliveryWorktree = await findContractWorktreePath(input.cwd, input.contractId);
121
- await assertCleanTrackedWorkingTree(deliveryWorktree ?? input.cwd);
122
- const existing = await readLedger(input.cwd, input.contractId);
123
- const existingClaim = assertClaimableFromLedger(input.contractId, existing);
124
- const existingState = existing ? deriveContractState(existing.entries) : undefined;
125
- // Already-claimed retry: skip queue-head gating and merge; finish cleanup.
126
- if (existingClaim && existingState?.state === "claimed") {
127
- const cleanup = await cleanupClaim(input.cwd, input.contractId, input.actor, at);
60
+ export async function previewClaimContract(input) {
61
+ const { withContractCarrierSession } = await import("../contract-carrier.js");
62
+ return withContractCarrierSession({ cwd: input.cwd, contractId: input.contractId, mode: "observe-only", signal: input.signal }, async (session) => {
63
+ const queue = await readQueueReadModel(session.stableRoot);
64
+ const membership = assertClaimQueueHead(queue, input.contractId);
65
+ assertClaimable(input.contractId, session.lifecycle);
66
+ const targetHead = session.pins.target.head;
67
+ const base = session.lifecycle.pins.base;
68
+ const deliveryBranch = `keiyaku/${input.contractId}`;
69
+ const cleanupPath = session.target.status === "available" ? session.target.root : undefined;
128
70
  return {
129
71
  contractId: input.contractId,
130
- mergeCommit: existingClaim.data.mergeCommit,
131
- cleanup,
72
+ queueHead: membership.queueHead,
73
+ queuePosition: membership.position,
74
+ state: session.lifecycle.phase,
75
+ freshness: targetHead ? (base === targetHead ? "fresh" : "stale") : "unknown",
76
+ base,
77
+ targetBranch: session.lifecycle.pins.target.ref.slice("refs/heads/".length),
78
+ ...(targetHead ? { targetHead } : {}),
79
+ deliveryBranch,
80
+ ...(session.pins.delivery.head ? { deliveryHead: session.pins.delivery.head } : {}),
81
+ cleanupEffects: [
82
+ `release registry scope for ${input.contractId}`,
83
+ cleanupPath ? `remove worktree ${relativeDisplayPath(input.cwd, cleanupPath)} when present` : "no linked worktree to remove",
84
+ `delete branch ${deliveryBranch} when present`,
85
+ ],
132
86
  };
133
- }
134
- // Still petitioned: prove target-local head before merge.
135
- const model = await readQueueReadModel(input.cwd);
136
- assertClaimQueueHead(model, input.contractId);
137
- let mergeCommit = existingClaim?.data.mergeCommit;
138
- let warnings;
139
- if (!mergeCommit) {
140
- const usesDefaultMerger = input.mergeDelivery === undefined;
141
- const mergeDelivery = input.mergeDelivery ?? defaultMergeDelivery;
142
- const publishClaim = async (acceptedMergeCommit) => publishClaimLedger({
143
- cwd: input.cwd,
144
- contractId: input.contractId,
145
- actor: input.actor,
146
- at,
147
- mergeCommit: acceptedMergeCommit,
148
- });
149
- // mergeDelivery must be idempotent: if a prior attempt merged delivery but
150
- // crashed before appending claim, a retry must return the existing merge
151
- // receipt without creating a second merge effect.
152
- const merged = await mergeDelivery({
153
- cwd: input.cwd,
154
- contractId: input.contractId,
155
- writeClaimReceipt: input.writeClaimReceipt,
156
- ...(usesDefaultMerger ? { publishClaim } : {}),
157
- });
158
- await input.afterMerge?.(merged);
159
- mergeCommit = merged.mergeCommit;
160
- if (merged.warnings && merged.warnings.length > 0) {
161
- warnings = [...merged.warnings];
162
- }
163
- if (!usesDefaultMerger)
164
- await publishClaim(mergeCommit);
165
- }
166
- // Claim entry naturally removes membership; no dequeue record.
167
- const cleanup = await cleanupClaim(input.cwd, input.contractId, input.actor, at);
87
+ });
88
+ }
89
+ export async function claimContract(input) {
90
+ const at = new Date(input.nowMs).toISOString();
91
+ const execution = await runLifecycle({
92
+ cwd: input.cwd,
93
+ contractId: input.contractId,
94
+ mutation: {
95
+ verb: "claim",
96
+ coordinateChangeRefusal: {
97
+ code: "CONCURRENT_MODIFICATION",
98
+ message: "claim target or repository ledger changed during atomic publication preparation",
99
+ },
100
+ },
101
+ signal: input.signal,
102
+ admit: async (session) => {
103
+ const existing = assertClaimable(input.contractId, session.lifecycle);
104
+ if (existing) {
105
+ return {
106
+ existing,
107
+ coordinates: null,
108
+ closedDelivery: null,
109
+ queuePin: null,
110
+ taskIds: session.lifecycle.resources.tasks.map(({ taskId }) => taskId),
111
+ };
112
+ }
113
+ if (session.target.status !== "available")
114
+ throw new FlowError("INTERNAL_STATE", session.target.reason);
115
+ await assertCleanTrackedWorkingTree(session.target.root);
116
+ const queue = await readQueueReadModel(session.stableRoot);
117
+ assertClaimQueueHead(queue, input.contractId);
118
+ const coordinates = freezeClaimCoordinates(input.contractId, session.lifecycle, session.pins);
119
+ return {
120
+ coordinates,
121
+ closedDelivery: { ref: session.pins.delivery.ref, head: coordinates.deliveryHead },
122
+ targetRoot: session.target.root,
123
+ queuePin: JSON.stringify(queue),
124
+ taskIds: session.lifecycle.resources.tasks.map(({ taskId }) => taskId),
125
+ };
126
+ },
127
+ prepare: async (admission) => {
128
+ if (admission.business.coordinates === null)
129
+ return null;
130
+ const landing = await buildClaimLanding({
131
+ cwd: input.cwd,
132
+ contractId: input.contractId,
133
+ actor: input.actor,
134
+ at,
135
+ coordinates: admission.business.coordinates,
136
+ });
137
+ await input.beforePublication?.({
138
+ cwd: input.cwd,
139
+ contractId: input.contractId,
140
+ repositoryLedgerHead: admission.ledgerHead,
141
+ coordinates: Object.freeze({ ...admission.business.coordinates }),
142
+ landing: Object.freeze({ mergeCommit: landing.mergeCommit, mergeTree: landing.mergeTree }),
143
+ });
144
+ return landing;
145
+ },
146
+ revalidate: async ({ session, admission }) => {
147
+ if (admission.business.existing) {
148
+ const current = acceptedClaim(session.lifecycle);
149
+ return current?.objectId === admission.business.existing.objectId
150
+ ? undefined
151
+ : { code: "CLAIM_STATE_CHANGED", message: "accepted claim changed during replay admission" };
152
+ }
153
+ if (session.target.status !== "available" || session.target.root !== admission.business.targetRoot) {
154
+ return { code: "DELIVERY_WORKTREE_CHANGED", message: "registered delivery worktree changed during claim preparation" };
155
+ }
156
+ await assertCleanTrackedWorkingTree(session.target.root);
157
+ const currentCoordinates = freezeClaimCoordinates(input.contractId, session.lifecycle, session.pins);
158
+ if (JSON.stringify(currentCoordinates) !== JSON.stringify(admission.business.coordinates)) {
159
+ return { code: "CLAIM_COORDINATES_CHANGED", message: "claim coordinates changed during preparation" };
160
+ }
161
+ const queue = await readQueueReadModel(session.stableRoot);
162
+ try {
163
+ assertClaimQueueHead(queue, input.contractId);
164
+ }
165
+ catch (error) {
166
+ return { code: "QUEUE_HEAD_CHANGED", message: error instanceof Error ? error.message : String(error) };
167
+ }
168
+ return JSON.stringify(queue) === admission.business.queuePin
169
+ ? undefined
170
+ : { code: "QUEUE_CHANGED", message: "settlement queue changed during claim preparation" };
171
+ },
172
+ effect: async ({ context, admission, prepared }) => {
173
+ const existing = admission.business.existing;
174
+ const mergeCommit = existing?.entry.data.mergeCommit ?? prepared?.mergeCommit;
175
+ if (!mergeCommit)
176
+ throw new FlowError("INTERNAL_STATE", `claim ${input.contractId} has no merge commit`);
177
+ const published = existing
178
+ ? await context.replayCommitted({ transactionObjectId: admission.replay.state === "verb-committed" ? admission.replay.transaction.transactionObjectId : (() => { throw new FlowError("INTERNAL_STATE", "claim replay has no accepted transaction"); })(), carrier: "retire" })
179
+ : await context.publishAuthoritative({
180
+ verb: "claim",
181
+ admittedLedgerHead: admission.ledgerHead,
182
+ acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
183
+ entries: [{
184
+ v: 2,
185
+ kind: "claim",
186
+ contract: input.contractId,
187
+ at,
188
+ actor: input.actor,
189
+ data: {
190
+ ...admission.business.coordinates,
191
+ mergeCommit,
192
+ },
193
+ }],
194
+ target: {
195
+ ref: admission.business.coordinates.target,
196
+ expectedOid: admission.business.coordinates.targetHead,
197
+ },
198
+ delivery: {
199
+ ref: admission.business.closedDelivery.ref,
200
+ expectedOid: admission.business.closedDelivery.head,
201
+ },
202
+ });
203
+ if (published.status !== "committed") {
204
+ const deliveryMismatch = published.status === "refused-before-effects"
205
+ ? claimDeliveryCoordinateMismatch(published, admission.business.closedDelivery)
206
+ : undefined;
207
+ return {
208
+ kind: "refused-before-effects",
209
+ authoritativeHead: published.currentHead ?? admission.ledgerHead,
210
+ refusal: deliveryMismatch ?? { code: "CONCURRENT_MODIFICATION", message: "claim target or repository ledger changed before atomic publication" },
211
+ };
212
+ }
213
+ await context.journal.run({
214
+ effect: "after-landing-hook",
215
+ ...(context.journal.all().some((row) => row.effect === "ledger-cas" && row.status === "succeeded")
216
+ ? { dependency: `${input.contractId}:${published.head}:ledger-cas` }
217
+ : {}),
218
+ run: async () => {
219
+ await input.afterPublication?.({ mergeCommit });
220
+ return { mergeCommit };
221
+ },
222
+ });
223
+ await context.session.completeReplay("retire");
224
+ const taskRow = context.journal.all().find((row) => row.effect === "task-board-projection" && row.status === "succeeded");
225
+ const taskFacts = taskRow?.status === "succeeded" ? taskRow.receipt?.facts : undefined;
226
+ const warnings = isJsonObject(taskFacts) && Array.isArray(taskFacts.warnings)
227
+ ? taskFacts.warnings.filter((warning) => typeof warning === "string")
228
+ : [];
229
+ const cleanupRow = context.journal.all().find((row) => row.effect === "worktree-projection" && row.status === "succeeded");
230
+ const cleanupFacts = cleanupRow?.status === "succeeded" ? cleanupRow.receipt?.facts : undefined;
231
+ const deliveryRefRow = context.journal.all().find((row) => row.effect === "delivery-ref-projection" && row.status === "succeeded");
232
+ const deliveryRefFacts = deliveryRefRow?.status === "succeeded" ? deliveryRefRow.receipt?.facts : undefined;
233
+ const cleanup = terminalCleanupFacts(cleanupFacts, deliveryRefFacts);
234
+ if (!cleanup)
235
+ throw new FlowError("INTERNAL_STATE", "terminal replay completed without typed cleanup facts");
236
+ return {
237
+ kind: "committed",
238
+ commit: published,
239
+ entryKind: "claim",
240
+ value: { mergeCommit, ...(warnings.length > 0 ? { warnings } : {}), ...(cleanup ? { cleanup } : {}) },
241
+ };
242
+ },
243
+ });
244
+ if (execution.kind === "noncommitted")
245
+ return execution.outcome;
246
+ if (execution.kind === "contained")
247
+ return { ...execution.outcome, contractId: input.contractId };
168
248
  return {
249
+ ...execution.outcome,
169
250
  contractId: input.contractId,
170
- mergeCommit: mergeCommit,
171
- cleanup,
172
- ...(warnings && warnings.length > 0 ? { warnings } : {}),
251
+ mergeCommit: execution.payload.mergeCommit,
252
+ ...(execution.payload.warnings ? { warnings: execution.payload.warnings } : {}),
253
+ ...(execution.payload.cleanup ? { cleanup: execution.payload.cleanup } : {}),
254
+ carrier: execution.carrier,
173
255
  };
174
256
  }
@@ -7,13 +7,13 @@
7
7
  * Foundations (queue, claim-delivery) may be depended on downward by stage modules.
8
8
  * Stage modules (petition, verdict, claim) do not import one another.
9
9
  */
10
- export { claimKeiyaku, petitionKeiyaku, } from "./settlement.js";
11
- export { buildPetitionClaimInput, petitionContract, loadPetitionPreflightContext, assertPetitionFresh, assertPetitionArcReady, assertPetitionable, } from "./petition.js";
10
+ export { claimKeiyaku, petitionKeiyaku, reconcilePetitionRetry, } from "./settlement.js";
11
+ export { buildPetitionClaimInput, petitionContract, } from "./petition.js";
12
12
  export { previewPetition, PETITION_PREVIEW_GATE_NAMES, } from "./petition-preview.js";
13
13
  export { verdictContract, } from "./verdict.js";
14
14
  export { claimContract, previewClaimContract, } from "./claim.js";
15
- export { defaultMergeDelivery, relativeDisplayPath, resolveCommit, resolveCommitOrNull, } from "./claim-delivery.js";
16
- export { allocateSettlementSeat, assertClaimQueueHead, deriveQueueReadModel, queueMembership, readQueueReadModel, seatAllocatorRef, } from "./queue.js";
15
+ export { relativeDisplayPath, resolveCommit, resolveCommitOrNull, } from "./claim-delivery.js";
16
+ export { assertClaimQueueHead, deriveQueueReadModel, queueMembership, readQueueReadModel, } from "./queue.js";
17
17
  export { buildClaimGatePipeline, createAgentReviewGate, createOathGate, } from "./petition-claim-gates.js";
18
18
  export { assertHeadUnchanged } from "./petition-head-guard.js";
19
19
  export { forfeitKeiyaku } from "./petition-forfeit.js";
@@ -8,13 +8,14 @@ import { getDiffStats } from "../../git/diff/preview.js";
8
8
  import { readDiff } from "../../git/diff/read.js";
9
9
  import { getCurrentBranch } from "../../git/branches.js";
10
10
  import { getLatestCommitHash } from "../../git/staging.js";
11
+ import { createGit } from "../../git/core.js";
11
12
  import { DIFF_PREVIEW_TEXT_MAX_CHARS } from "../../keiyaku.js";
12
13
  import { FlowError } from "../../flow-error.js";
13
14
  import { persistResponseHistory } from "../transcripts.js";
14
15
  import { logInfo } from "../../telemetry/logger.js";
15
16
  import { execSubagent } from "../call/call.js";
16
17
  import { readContractLog } from "../log.js";
17
- import { runVerificationCommand } from "./verification.js";
18
+ import { runVerificationPlan } from "./verification.js";
18
19
  const CLAIM_VERIFICATION_OUTPUT_MAX_CHARS = 1200;
19
20
  const REVIEW_PASS_TOKEN = "PASS";
20
21
  const REVIEW_REJECTION_MESSAGE = "CLAIM rejected by independent reviewer.";
@@ -29,7 +30,6 @@ const REVIEW_PROMPT_LINES = [
29
30
  "You may use read-only repository inspection commands and tools as needed.",
30
31
  "",
31
32
  "## Workflow Context",
32
- "- KEIYAKU.md is a workflow protocol file that will be removed before merge. Ignore it.",
33
33
  "- Commit messages in this branch are intermediate; the branch will be squash-merged. Do not evaluate commit hygiene.",
34
34
  "",
35
35
  "## Output Rules",
@@ -100,25 +100,41 @@ export function createOathGate() {
100
100
  export function createCommandVerificationGate() {
101
101
  return {
102
102
  name: "command",
103
- async evaluate({ cwd, verificationCommands }) {
104
- for (const verification of verificationCommands) {
105
- logInfo(`[CLAIM] Running verification: ${verification.label}`, { cwd, section: "script", progressOnly: true });
106
- const result = await runVerificationCommand(cwd, verification);
107
- if (result.code !== 0) {
108
- return {
109
- pass: false,
110
- code: "CLOSE_QUALITY_GATE_FAILED",
111
- message: `CLAIM blocked: verification failed at '${verification.label}' (exit ${result.code}).`,
112
- facts: {
113
- kind: "verification_failure",
114
- label: verification.label,
115
- stdout: clipCommandOutput(result.stdout.tail),
116
- stderr: clipCommandOutput(result.stderr.tail),
117
- },
118
- };
119
- }
103
+ async evaluate({ cwd, coordinationRoot, verificationPlan, verificationCandidate, input }) {
104
+ const repositoryRoot = coordinationRoot ?? cwd;
105
+ for (const verification of verificationPlan) {
106
+ logInfo(`[CLAIM] Running ${verification.source}: ${verification.label}`, { cwd, section: "script", progressOnly: true });
120
107
  }
121
- return { pass: true };
108
+ const evidence = await runVerificationPlan({ cwd, candidate: verificationCandidate, plan: verificationPlan, signal: input.signal, coordinationKey: JSON.stringify({ repository: repositoryRoot, candidate: verificationCandidate, plan: verificationPlan }), coordinationRoot: repositoryRoot });
109
+ const runnerFailure = evidence.find((result) => result.execution.status === "runner_failed");
110
+ if (runnerFailure?.execution.status === "runner_failed") {
111
+ return {
112
+ pass: false,
113
+ code: "CLOSE_QUALITY_GATE_FAILED",
114
+ message: `CLAIM blocked: verification runner uncertainty at ${runnerFailure.source} '${runnerFailure.label}' (${runnerFailure.execution.cause.code}: ${runnerFailure.execution.cause.message}).`,
115
+ facts: {
116
+ kind: "verification_failure",
117
+ label: `${runnerFailure.source} ${runnerFailure.label}`,
118
+ stdout: clipCommandOutput(runnerFailure.execution.stdout.tail),
119
+ stderr: clipCommandOutput(runnerFailure.execution.stderr.tail),
120
+ },
121
+ };
122
+ }
123
+ const commandFailure = evidence.find((result) => result.execution.status === "executed" && result.execution.code !== 0);
124
+ if (commandFailure?.execution.status === "executed") {
125
+ return {
126
+ pass: false,
127
+ code: "CLOSE_QUALITY_GATE_FAILED",
128
+ message: `CLAIM blocked: verification failed at ${commandFailure.source} '${commandFailure.label}' (exit ${commandFailure.execution.code}).`,
129
+ facts: {
130
+ kind: "verification_failure",
131
+ label: `${commandFailure.source} ${commandFailure.label}`,
132
+ stdout: clipCommandOutput(commandFailure.execution.stdout.tail),
133
+ stderr: clipCommandOutput(commandFailure.execution.stderr.tail),
134
+ },
135
+ };
136
+ }
137
+ return { pass: true, verification: [...evidence] };
122
138
  },
123
139
  };
124
140
  }
@@ -177,25 +193,13 @@ export function createAgentReviewGate() {
177
193
  authority: { kind: "repository" },
178
194
  binding: { kind: "commission", commissionId: contractId },
179
195
  });
180
- const reviewHeadBeforeWrite = await getLatestCommitHash(cwd);
196
+ const reviewHeadBeforeWrite = (await createGit(cwd).revparse(["--verify", "HEAD^{commit}"])).trim();
181
197
  const raw = subagentResult.finalMessage.trim();
182
198
  const passed = raw.startsWith(REVIEW_PASS_TOKEN);
183
199
  const observations = passed ? raw.slice(REVIEW_PASS_TOKEN.length).trim() || undefined : undefined;
184
- const persisted = await persistResponseHistory({
185
- cwd,
186
- tool: "review",
187
- provenance: { authority: { kind: "repository" }, binding: { kind: "commission", commissionId: contractId }, evidenceCwd: cwd },
188
- title: String(reviewNumber),
189
- prompt,
190
- response: subagentResult.finalMessage,
191
- session: subagentResult.session,
192
- subagentName: selectedReviewerAgentName,
193
- launchSnapshot: subagentResult.launchSnapshot,
194
- });
195
200
  const review = {
196
201
  review: reviewNumber,
197
202
  result: passed ? "passed" : "rejected",
198
- historyPath: persisted.responsePath,
199
203
  findings: passed ? observations : raw || REVIEW_RESPONSE_FALLBACK,
200
204
  };
201
205
  if (review.result === "passed") {
@@ -204,6 +208,21 @@ export function createAgentReviewGate() {
204
208
  review,
205
209
  reviewHeadBeforeWrite,
206
210
  reviewerActor: selectedReviewerAgentName,
211
+ reviewPersistence: {
212
+ reviewNumber,
213
+ reviewedHead: reviewHeadBeforeWrite,
214
+ persist: () => persistResponseHistory({
215
+ cwd,
216
+ tool: "review",
217
+ provenance: { authority: { kind: "repository" }, binding: { kind: "commission", commissionId: contractId }, evidenceCwd: cwd },
218
+ title: String(reviewNumber),
219
+ prompt,
220
+ response: subagentResult.finalMessage,
221
+ session: subagentResult.session,
222
+ subagentName: selectedReviewerAgentName,
223
+ launchSnapshot: subagentResult.launchSnapshot,
224
+ }),
225
+ },
207
226
  };
208
227
  }
209
228
  return {
@@ -1,5 +1,6 @@
1
1
  import { forfeitContract } from "../forfeit.js";
2
2
  import { readContractLog } from "../log.js";
3
+ import { isCompleteLifecycleOutcome } from "../outcome-base.js";
3
4
  function maxArc(entries) {
4
5
  return entries.filter((entry) => entry.kind === "arc").length;
5
6
  }
@@ -8,32 +9,38 @@ function cleanupWarnings(cleanup) {
8
9
  ...(cleanup.worktreeRemoveFailure
9
10
  ? [`Could not remove contract worktree ${cleanup.worktreeRemoveFailure.path}: ${cleanup.worktreeRemoveFailure.reason}`]
10
11
  : []),
11
- ...(cleanup.branchDeleteFailure
12
- ? [`Could not delete contract branch ${cleanup.branchDeleteFailure.name}: ${cleanup.branchDeleteFailure.reason}`]
12
+ ...(cleanup.deliveryRef.status === "mismatch"
13
+ ? [`Delivery ref ${cleanup.deliveryRef.mismatch.ref} remains present at ${cleanup.deliveryRef.mismatch.observed.head}; terminal replay preserved it as a projected-surface mismatch.`]
13
14
  : []),
14
15
  ];
15
16
  }
16
- export async function forfeitKeiyaku(context) {
17
+ export async function forfeitKeiyaku(context, signal) {
17
18
  const { cwd, baseBranch, contractId } = context;
18
- const log = await readContractLog({ cwd, contractId });
19
+ const log = await readContractLog({ cwd, contractId, signal });
19
20
  const forfeited = await forfeitContract({
20
21
  cwd,
21
22
  contractId,
22
23
  actor: "keiyaku",
23
24
  nowMs: Date.now(),
24
25
  reason: "manual",
26
+ signal,
25
27
  });
28
+ if (!isCompleteLifecycleOutcome(forfeited))
29
+ return forfeited;
30
+ if (!forfeited.cleanup)
31
+ return forfeited;
26
32
  return {
33
+ ...forfeited,
27
34
  status: "success",
28
35
  result: "dropped",
29
36
  arc: maxArc(log.entries),
30
37
  currentBranch: baseBranch,
31
38
  baseBranch,
32
- ...(forfeited.cleanup.deletedBranch ? { deletedBranch: forfeited.cleanup.deletedBranch } : {}),
33
39
  cleanup: forfeited.cleanup,
34
40
  ledgerView: log.markdown,
35
41
  diff: "Forfeited without merge.",
36
42
  droppedChanges: [],
43
+ carrier: forfeited.carrier,
37
44
  ...(cleanupWarnings(forfeited.cleanup).length > 0 ? { meta: { warnings: cleanupWarnings(forfeited.cleanup) } } : {}),
38
45
  };
39
46
  }
@@ -1,10 +1,13 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
- import { getLatestCommitHash } from "../../git/staging.js";
2
+ import { createGit } from "../../git/core.js";
3
3
  const CONCURRENT_MODIFICATION_CODE = "CONCURRENT_MODIFICATION";
4
4
  export async function assertHeadUnchanged(cwd, expectedHead, operation) {
5
- const currentHead = await getLatestCommitHash(cwd);
6
- if (currentHead === expectedHead) {
5
+ const git = createGit(cwd);
6
+ const [expectedCommit, currentHead] = await Promise.all([
7
+ git.raw(["rev-parse", "--verify", `${expectedHead}^{commit}`]),
8
+ git.raw(["rev-parse", "--verify", "HEAD^{commit}"]),
9
+ ]);
10
+ if (currentHead.trim() === expectedCommit.trim())
7
11
  return;
8
- }
9
- throw new FlowError(CONCURRENT_MODIFICATION_CODE, `concurrent ${operation} modification detected: repository HEAD changed from ${expectedHead} to ${currentHead} while the ${operation} was running`);
12
+ throw new FlowError(CONCURRENT_MODIFICATION_CODE, `concurrent ${operation} modification detected: repository HEAD changed from ${expectedHead} to ${currentHead.trim()} while the ${operation} was running`);
10
13
  }