@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
@@ -1,566 +1,341 @@
1
- import { asMessage, FlowError } from "../flow-error.js";
2
- import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, parseRevListOutput, runGitProcess, wrapGitError } from "../git/core.js";
3
- import { readLedger } from "./ledger.js";
4
- import { hasStrandableProjectionGeneration } from "./projection/index.js";
5
- import { refreshContractView } from "./contract-view.js";
6
- import { appendRenewSettlement, appendScopeTerms, captureRenewSeal, captureSeal, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "./seal.js";
7
- import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
8
- import { commissionTarget, latestBase } from "./status/drift.js";
9
- import { findContractWorktreePath, findRegisteredContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
1
+ import { FlowError } from "../flow-error.js";
10
2
  import { getCurrentBranch, requireGitRepository } from "../git/branches.js";
11
- import { assertCleanTrackedWorkingTree, getUnmergedPaths, hasActiveRebase } from "../git/worktree.js";
12
- import { createRecoveryRef, deleteRecoveryRef } from "../git/refs.js";
13
- import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
14
3
  import { ACTOR_IDENTITY } from "../keiyaku.js";
4
+ import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
5
+ import { withContractCarrierSession } from "./contract-carrier.js";
6
+ import { runLifecycle } from "./lifecycle-runner.js";
7
+ import { committedLifecycleFacts, derivedCleanupFacts, isCompleteLifecycleOutcome, } from "./outcome-base.js";
8
+ import { abortDetachedRenewChain, readDetachedRenewScratch, renewScratchName, } from "./renew-rewrite.js";
15
9
  import { persistResponseHistory } from "./transcripts.js";
16
- import { convergeRenewRecoverySession, assertNoRenewRecoverySession, removeRenewRecoverySession, writeRenewRecoverySession, } from "./renew-session.js";
17
- function assertOperable(contractId, entries) {
18
- const state = deriveContractState(entries);
19
- if (state.state === "corrupt") {
20
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} is corrupt at entry ${state.at}: ${state.reason}`);
21
- }
22
- if (isTerminalState(state) || state.state === "petitioned") {
23
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot renew from state ${state.state}`);
24
- }
25
- }
26
- async function resolveCommit(cwd, rev) {
27
- const git = createGit(cwd);
28
- try {
29
- return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
30
- }
31
- catch (error) {
32
- throw wrapGitError(`rev-parse ${rev}`, error, cwd);
33
- }
34
- }
35
- async function resolveCommitOrNull(cwd, rev) {
36
- const git = createGit(cwd);
37
- try {
38
- return (await git.raw(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
39
- }
40
- catch (error) {
41
- if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
42
- return null;
43
- throw wrapGitError(`rev-parse ${rev}`, error, cwd);
10
+ import { admitRenew, checkCancellation, prepareRenew, sameAdmission, scopeEntry } from "./renew-plan.js";
11
+ async function persistRenewResponse(journal, facts, persist) {
12
+ if (!persist)
13
+ return;
14
+ let response;
15
+ await journal.run({
16
+ effect: "response-history-persistence",
17
+ idempotencyKey: `${facts.contractId}:${facts.ledgerHead}:response-history-persistence`,
18
+ run: async () => { response = await persist(facts); return response; },
19
+ });
20
+ if (response) {
21
+ facts.responsePath = response.responsePath;
22
+ facts.artifactId = response.artifactId;
44
23
  }
45
24
  }
46
- const RENEW_RECOVERY_REF_PREFIX = "refs/keiyaku/recovery/renew";
47
- function renewRecoveryRef(contractId, oldHead) {
48
- return `${RENEW_RECOVERY_REF_PREFIX}/${contractId}/${oldHead}`;
49
- }
50
- export async function rewriteRenewBranch(input) {
51
- const { cwd, contractId, oldHead, recoveryRef, newBase, oldBase, branch, preserveConflict } = input;
52
- const git = createGit(cwd);
53
- const originalBranch = branch ? await getCurrentBranch(cwd) : undefined;
54
- const args = branch
55
- ? ["rebase", "--onto", newBase, oldBase, branch]
56
- : ["rebase", "--onto", newBase, oldBase];
57
- try {
58
- await git.raw(args);
59
- }
60
- catch (error) {
61
- const isConflict = errorContainsAnyPattern(error, ["CONFLICT", "could not apply", "Resolve all conflicts manually"]);
62
- let conflictPaths = [];
63
- let conflictPathDiagnostic;
64
- if (isConflict) {
65
- try {
66
- conflictPaths = await getUnmergedPaths(cwd);
25
+ async function executeRenew(input, continuation) {
26
+ const execution = await runLifecycle({
27
+ cwd: input.cwd,
28
+ contractId: input.contractId,
29
+ mutation: {
30
+ verb: "renew",
31
+ matchesAccepted: (transaction) => {
32
+ const accepted = transaction.entries.at(-1);
33
+ const expectedAmend = scopeEntry(input);
34
+ if (accepted?.kind === "amend") {
35
+ return transaction.entries.length === 1
36
+ && expectedAmend !== null
37
+ && accepted.data.amendment === expectedAmend.data.amendment
38
+ && JSON.stringify(accepted.data.scopeDelta ?? null) === JSON.stringify(expectedAmend.data.scopeDelta ?? null);
39
+ }
40
+ if (accepted?.kind !== "renew")
41
+ return false;
42
+ const acceptedAmend = transaction.entries.find((entry) => entry.kind === "amend");
43
+ return JSON.stringify(acceptedAmend?.kind === "amend" ? acceptedAmend.data.scopeDelta ?? null : null)
44
+ === JSON.stringify(expectedAmend?.data.scopeDelta ?? null);
45
+ },
46
+ },
47
+ signal: input.signal,
48
+ ...(input.renewDeliveryProjector ? { renewDeliveryProjector: input.renewDeliveryProjector } : {}),
49
+ admit: (session, replay) => admitRenew(session, input, replay, continuation?.scratch),
50
+ prepare: (admission) => prepareRenew(admission.business, input, continuation),
51
+ revalidate: async ({ session, admission }) => {
52
+ const current = await admitRenew(session, input, admission.replay, continuation?.scratch);
53
+ if (!sameAdmission(current, admission.business)) {
54
+ return { code: "RENEW_COORDINATES_CHANGED", message: "renew coordinates changed during detached construction" };
67
55
  }
68
- catch (captureError) {
69
- conflictPathDiagnostic = asMessage(captureError);
56
+ },
57
+ effect: async ({ context, admission, prepared }) => {
58
+ if (admission.business.acceptedScopeReplay) {
59
+ const accepted = admission.business.acceptedScopeReplay;
60
+ const replayed = await context.replayCommitted({ transactionObjectId: accepted.transactionObjectId });
61
+ const value = {
62
+ ...committedLifecycleFacts({ head: accepted.transactionObjectId, entryKind: "amend", contractId: input.contractId, carrier: replayed.carrier }),
63
+ contractId: input.contractId,
64
+ oldBase: admission.business.oldBase,
65
+ newBase: admission.business.newBase,
66
+ oldHead: admission.business.oldHead,
67
+ newHead: admission.business.oldHead,
68
+ ledgerHead: accepted.transactionObjectId,
69
+ alreadyFresh: true,
70
+ sealOid: null,
71
+ renewOid: null,
72
+ sealed: null,
73
+ carrier: replayed.carrier,
74
+ targetRoot: admission.business.targetRoot,
75
+ };
76
+ await persistRenewResponse(context.journal, value, input.persistResponse);
77
+ return { kind: "committed", commit: replayed, entryKind: "amend", value };
70
78
  }
71
- }
72
- if (isConflict && preserveConflict) {
73
- throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session is open for ${contractId}`, {
74
- cause: error,
75
- facts: { kind: "renew_session", commission: contractId, nextAction: "resolve-conflicts" },
76
- });
77
- }
78
- try {
79
- await git.raw(["rebase", "--abort"]);
80
- }
81
- catch (abortError) {
82
- throw wrapGitError("rebase --abort", abortError, cwd);
83
- }
84
- if (originalBranch && originalBranch !== branch) {
85
- try {
86
- await git.raw(["switch", originalBranch]);
79
+ if (admission.business.acceptedReplay) {
80
+ const accepted = admission.business.acceptedReplay;
81
+ const replayed = await context.replayCommitted({ transactionObjectId: accepted.transactionObjectId });
82
+ const sealed = { ...accepted.seal.data, scopeViolationEvidence: [], ledgerHead: accepted.sealObjectId };
83
+ const value = {
84
+ ...committedLifecycleFacts({ head: accepted.transactionObjectId, entryKind: "renew", contractId: input.contractId, carrier: replayed.carrier }),
85
+ contractId: input.contractId,
86
+ oldBase: accepted.entry.data.oldBase,
87
+ newBase: accepted.entry.data.newBase,
88
+ oldHead: accepted.entry.data.oldHead,
89
+ newHead: accepted.entry.data.newHead,
90
+ ledgerHead: accepted.transactionObjectId,
91
+ alreadyFresh: false,
92
+ sealOid: accepted.sealObjectId,
93
+ renewOid: accepted.renewObjectId,
94
+ sealed,
95
+ carrier: replayed.carrier,
96
+ targetRoot: admission.business.targetRoot,
97
+ };
98
+ if (continuation) {
99
+ await context.journal.run({
100
+ effect: "renew-scratch-cleanup",
101
+ idempotencyKey: `${input.contractId}:${accepted.transactionObjectId}:renew-scratch-cleanup`,
102
+ run: async () => abortDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName }),
103
+ });
104
+ }
105
+ await persistRenewResponse(context.journal, value, input.persistResponse);
106
+ return { kind: "committed", commit: replayed, entryKind: "renew", value };
87
107
  }
88
- catch (switchError) {
89
- throw wrapGitError(`switch ${originalBranch}`, switchError, cwd);
108
+ if (prepared.build?.status === "conflict") {
109
+ const conflict = prepared.build;
110
+ context.beginEffects();
111
+ context.journal.record({
112
+ effect: "renew-conflict-scratch",
113
+ idempotencyKey: `${input.contractId}:${admission.ledgerHead}:renew-conflict-scratch:${conflict.scratchName}`,
114
+ status: "failed",
115
+ cause: {
116
+ code: "CLOSE_MERGE_CONFLICT",
117
+ message: `renew conflict for ${input.contractId}`,
118
+ facts: {
119
+ kind: "renew_conflict_scratch",
120
+ contractId: input.contractId,
121
+ scratchName: conflict.scratchName,
122
+ scratchPath: conflict.scratchPath,
123
+ observedLedgerHead: admission.ledgerHead,
124
+ deliveryRef: admission.business.deliveryRef,
125
+ targetRef: admission.business.targetRef,
126
+ oldBase: admission.business.oldBase,
127
+ oldHead: admission.business.oldHead,
128
+ newBase: admission.business.newBase,
129
+ effectiveScope: admission.business.effectiveScope,
130
+ conflictCommit: conflict.conflictCommit,
131
+ conflictPaths: [...conflict.conflictPaths],
132
+ nextAction: "continue-or-abort",
133
+ authoritativeRefsChanged: false,
134
+ },
135
+ },
136
+ });
137
+ return { kind: "precommit-effects-only", authoritativeHead: admission.ledgerHead };
90
138
  }
91
- }
92
- if (isConflict) {
93
- const restoredHead = await resolveCommit(cwd, `keiyaku/${contractId}`);
94
- if (restoredHead !== oldHead) {
95
- throw new FlowError("INTERNAL_STATE", `renew abort did not restore keiyaku/${contractId}: expected ${oldHead}, found ${restoredHead}`);
139
+ if (admission.business.oldBase === admission.business.newBase) {
140
+ if (!admission.business.amendment) {
141
+ const transactionObjectId = admission.replay.state === "verb-committed"
142
+ ? admission.replay.transaction.transactionObjectId
143
+ : admission.ledgerHead;
144
+ const replayed = await context.replayCommitted({ transactionObjectId });
145
+ const accepted = replayed.transaction.entries.at(-1);
146
+ if (!accepted)
147
+ throw new FlowError("INTERNAL_STATE", `transaction ${transactionObjectId} has no accepted lifecycle entry`);
148
+ const value = {
149
+ ...committedLifecycleFacts({ head: transactionObjectId, entryKind: accepted.kind, contractId: input.contractId, carrier: replayed.carrier }),
150
+ contractId: input.contractId,
151
+ oldBase: admission.business.oldBase,
152
+ newBase: admission.business.newBase,
153
+ oldHead: admission.business.oldHead,
154
+ newHead: admission.business.oldHead,
155
+ ledgerHead: transactionObjectId,
156
+ alreadyFresh: true,
157
+ sealOid: null,
158
+ renewOid: null,
159
+ sealed: null,
160
+ carrier: replayed.carrier,
161
+ targetRoot: admission.business.targetRoot,
162
+ };
163
+ await persistRenewResponse(context.journal, value, input.persistResponse);
164
+ return { kind: "committed", commit: replayed, entryKind: accepted.kind, value };
165
+ }
166
+ const published = await context.publishAuthoritative({
167
+ verb: "renew",
168
+ admittedLedgerHead: admission.ledgerHead,
169
+ acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
170
+ entries: [admission.business.amendment],
171
+ });
172
+ if (published.status === "refused-before-effects")
173
+ return { kind: "refused-before-effects", authoritativeHead: admission.ledgerHead, refusal: { code: "CONCURRENT_MODIFICATION", message: "repository ledger changed before scope amendment" } };
174
+ if (published.status === "indeterminate")
175
+ return { kind: "precommit-effects-only", authoritativeHead: admission.ledgerHead };
176
+ const value = {
177
+ ...committedLifecycleFacts({ head: published.head, entryKind: "amend", contractId: input.contractId, carrier: published.carrier }),
178
+ contractId: input.contractId,
179
+ oldBase: admission.business.oldBase,
180
+ newBase: admission.business.newBase,
181
+ oldHead: admission.business.oldHead,
182
+ newHead: admission.business.oldHead,
183
+ ledgerHead: published.head,
184
+ alreadyFresh: true,
185
+ sealOid: null,
186
+ renewOid: null,
187
+ sealed: null,
188
+ carrier: published.carrier,
189
+ targetRoot: admission.business.targetRoot,
190
+ };
191
+ await persistRenewResponse(context.journal, value, input.persistResponse);
192
+ return { kind: "committed", commit: published, entryKind: "amend", value };
96
193
  }
97
- await assertCleanTrackedWorkingTree(cwd);
98
- throw new FlowError("CLOSE_MERGE_CONFLICT", "renew conflict", {
99
- cause: error,
100
- facts: {
101
- kind: "renew_conflict",
102
- contractId,
103
- oldHead,
104
- recoveryRef,
105
- conflictPaths,
106
- conflictPathCapture: conflictPathDiagnostic
107
- ? { status: "failed", diagnostic: conflictPathDiagnostic }
108
- : { status: "captured" },
109
- workspaceRestored: true,
110
- ledgerWrites: false,
194
+ if (!prepared.entry || prepared.build?.status !== "clean" || !prepared.capture)
195
+ throw new FlowError("INTERNAL_STATE", "renew detached build is incomplete");
196
+ const entries = prepared.prefix.length === 0
197
+ ? [prepared.entry]
198
+ : prepared.prefix[0].kind === "seal"
199
+ ? [prepared.prefix[0], prepared.entry]
200
+ : prepared.prefix.length === 1
201
+ ? [prepared.entry]
202
+ : [prepared.prefix[0], prepared.prefix[1], prepared.entry];
203
+ const published = await context.publishAuthoritative({
204
+ verb: "renew",
205
+ admittedLedgerHead: admission.ledgerHead,
206
+ acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
207
+ entries,
208
+ delivery: {
209
+ ref: admission.business.deliveryRef,
210
+ expectedOid: prepared.entry.data.oldHead,
111
211
  },
112
212
  });
113
- }
114
- throw wrapGitError(`renew ${branch ?? "current branch"}`, error, cwd);
115
- }
116
- }
117
- async function assertNoInFlightProjection(repositoryRoot, contractId, deliveryWorktree, nowMs) {
118
- if (!await hasStrandableProjectionGeneration({
119
- cwd: repositoryRoot,
120
- commissionId: contractId,
121
- deliveryWorktree,
122
- nowMs,
123
- }))
124
- return;
125
- throw new FlowError("INTERNAL_STATE", `akuma still out on ${contractId}'s worktree; rewriting under it would strand its work`, { facts: { kind: "projection_in_flight", contractId } });
126
- }
127
- function looseCommitError(contractId, receipts) {
128
- return new FlowError("NO_OPEN_ARC", `${receipts.length} commits after fence have no arc to receipt them`, { facts: { kind: "loose_commits", contractId, nextVerb: "renew" } });
129
- }
130
- async function mergeBase(cwd, left, right) {
131
- const git = createGit(cwd);
132
- try {
133
- return (await git.raw(["merge-base", left, right])).trim();
134
- }
135
- catch (error) {
136
- throw wrapGitError(`merge-base ${left} ${right}`, error, cwd);
137
- }
138
- }
139
- async function assertRenewCoordinates(input) {
140
- const fence = getFence(input.entries);
141
- let branchBase;
142
- let fenceBase;
143
- try {
144
- [branchBase, fenceBase] = await Promise.all([
145
- mergeBase(input.cwd, input.oldHead, input.targetHead),
146
- mergeBase(input.cwd, fence, input.oldHead),
147
- ]);
148
- }
149
- catch (error) {
150
- throw new FlowError("RENEW_COORDINATE_DRIFT", `renew coordinates for ${input.contractId} are detached from the ledger`, {
151
- cause: error,
152
- facts: { kind: "renew_coordinate_drift", commission: input.contractId, oldBase: input.oldBase, oldHead: input.oldHead, fence, targetHead: input.targetHead },
153
- });
154
- }
155
- if (branchBase === input.oldBase && fenceBase === fence)
156
- return;
157
- throw new FlowError("RENEW_COORDINATE_DRIFT", `renew coordinates for ${input.contractId} drifted from the ledger before scope audit`, {
158
- facts: { kind: "renew_coordinate_drift", commission: input.contractId, oldBase: input.oldBase, oldHead: input.oldHead, fence, targetHead: input.targetHead },
159
- });
160
- }
161
- async function finishRenew(input) {
162
- const capture = input.capture === undefined
163
- ? await captureSeal(input.repositoryRoot, input.entries, input.oldHead)
164
- : input.capture;
165
- const settlement = await appendRenewSettlement({
166
- cwd: input.repositoryRoot,
167
- contractId: input.contractId,
168
- expectedLedgerHead: input.ledgerHead,
169
- capture,
170
- oldBase: input.oldBase,
171
- newBase: input.newBase,
172
- oldHead: input.oldHead,
173
- newHead: input.newHead,
174
- actor: input.actor,
175
- nowMs: input.nowMs,
176
- });
177
- await input.afterLedgerCas?.();
178
- await refreshContractView({
179
- ledgerCwd: input.repositoryRoot,
180
- renderCwd: input.rewriteCwd,
181
- contractId: input.contractId,
213
+ if (published.status === "refused-before-effects")
214
+ return { kind: "refused-before-effects", authoritativeHead: admission.ledgerHead, refusal: { code: "CONCURRENT_MODIFICATION", message: "repository ledger or delivery ref changed before renew transaction" } };
215
+ if (published.status === "indeterminate")
216
+ return { kind: "precommit-effects-only", authoritativeHead: admission.ledgerHead };
217
+ const sealIndex = published.transaction.entries.findIndex((entry) => entry.kind === "seal");
218
+ const renewIndex = published.transaction.entries.findIndex((entry) => entry.kind === "renew");
219
+ const sealOid = sealIndex < 0 ? null : published.transaction.entryObjectIds[sealIndex];
220
+ const renewOid = renewIndex < 0 ? null : published.transaction.entryObjectIds[renewIndex];
221
+ const sealed = {
222
+ ...prepared.capture,
223
+ newFence: prepared.build.newHead,
224
+ sealedBy: "renew",
225
+ ledgerHead: sealOid,
226
+ };
227
+ const value = {
228
+ ...committedLifecycleFacts({ head: published.head, entryKind: "renew", contractId: input.contractId, carrier: published.carrier }),
229
+ contractId: input.contractId,
230
+ oldBase: admission.business.oldBase,
231
+ newBase: admission.business.newBase,
232
+ oldHead: admission.business.oldHead,
233
+ newHead: prepared.build.newHead,
234
+ ledgerHead: published.head,
235
+ alreadyFresh: false,
236
+ sealOid,
237
+ renewOid,
238
+ sealed,
239
+ carrier: published.carrier,
240
+ targetRoot: admission.business.targetRoot,
241
+ };
242
+ if (continuation) {
243
+ await context.journal.run({
244
+ effect: "renew-scratch-cleanup",
245
+ idempotencyKey: `${input.contractId}:${published.head}:renew-scratch-cleanup`,
246
+ run: async () => abortDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName }),
247
+ });
248
+ }
249
+ await persistRenewResponse(context.journal, value, input.persistResponse);
250
+ return { kind: "committed", commit: published, entryKind: "renew", value };
251
+ },
182
252
  });
183
- const recoveryCleanup = await deleteRecoveryRef(input.repositoryRoot, input.recoveryRef, input.oldHead);
184
- await removeRenewRecoverySession(input.repositoryRoot, input.contractId);
185
- return {
186
- contractId: input.contractId,
187
- oldBase: input.oldBase,
188
- newBase: input.newBase,
189
- oldHead: input.oldHead,
190
- newHead: input.newHead,
191
- ledgerHead: settlement.ledgerHead,
192
- alreadyFresh: false,
193
- sealed: settlement.sealed,
194
- recoveryCleanup,
195
- };
253
+ if (execution.kind === "noncommitted")
254
+ return execution.outcome;
255
+ if (execution.kind === "contained")
256
+ return { ...execution.outcome, contractId: input.contractId };
257
+ return { ...execution.payload, ...execution.outcome };
196
258
  }
197
259
  export async function renewContract(input) {
198
- const repositoryRoot = await stableRepoRoot(input.cwd);
199
- const worktreePath = await findContractWorktreePath(repositoryRoot, input.contractId);
200
- const rewriteCwd = worktreePath ?? input.cwd;
201
- await assertNoRenewRecoverySession(repositoryRoot, input.contractId);
202
- if (input.resolveConflicts && !worktreePath) {
203
- throw new FlowError("INTERNAL_STATE", `renew --resolve requires ${input.contractId}'s registered contract worktree`);
204
- }
205
- await assertCleanTrackedWorkingTree(rewriteCwd);
206
- const ledger = await readLedger(input.cwd, input.contractId);
207
- if (!ledger) {
208
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
209
- }
210
- assertOperable(input.contractId, ledger.entries);
211
- const commission = commissionTarget(ledger.entries);
212
- if (!commission) {
213
- throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: missing commission target`);
214
- }
215
- const targetHead = await resolveCommitOrNull(input.cwd, commission.ref);
216
- if (!targetHead) {
217
- throw new FlowError("INTERNAL_STATE", `cannot renew ${input.contractId}: commission target ${commission.ref} does not resolve`);
218
- }
219
- const oldBase = latestBase(ledger.entries);
220
- if (!oldBase)
221
- throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
222
- const branch = `keiyaku/${input.contractId}`;
223
- const oldHead = await resolveCommit(input.cwd, branch);
224
- await assertRenewCoordinates({
225
- cwd: repositoryRoot,
226
- contractId: input.contractId,
227
- oldBase,
228
- oldHead,
229
- targetHead,
230
- entries: ledger.entries,
231
- });
232
- if (getMaxArc(ledger.entries) > 0 && !findOpenArc(ledger.entries)) {
233
- const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
234
- if (loose.length > 0)
235
- throw looseCommitError(input.contractId, loose);
236
- }
237
- if (oldBase !== targetHead) {
238
- const deliveryWorktree = worktreePath ?? repositoryRoot;
239
- await assertNoInFlightProjection(repositoryRoot, input.contractId, deliveryWorktree, input.nowMs);
240
- }
241
- await appendScopeTerms({
242
- cwd: repositoryRoot,
243
- renderCwd: rewriteCwd,
244
- contractId: input.contractId,
245
- patterns: input.appendScope,
246
- actor: input.actor,
247
- nowMs: input.nowMs,
248
- });
249
- const currentLedger = input.appendScope?.length ? await readLedger(repositoryRoot, input.contractId) : ledger;
250
- if (!currentLedger)
251
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} disappeared while appending scope`);
252
- const capture = await captureSeal(input.cwd, currentLedger.entries, oldHead);
253
- if (oldBase === targetHead) {
254
- return {
255
- contractId: input.contractId,
256
- oldBase,
257
- newBase: targetHead,
258
- oldHead,
259
- newHead: oldHead,
260
- ledgerHead: currentLedger.head,
261
- alreadyFresh: true,
262
- sealed: null,
263
- recoveryCleanup: null,
264
- };
265
- }
266
- const recoveryRef = renewRecoveryRef(input.contractId, oldHead);
267
- await createRecoveryRef(input.cwd, recoveryRef, oldHead);
268
- if (input.resolveConflicts) {
269
- await writeRenewRecoverySession(repositoryRoot, {
270
- commission: input.contractId,
271
- oldBase,
272
- oldHead,
273
- newBase: targetHead,
274
- recoveryRef,
275
- });
276
- }
277
- try {
278
- await rewriteRenewBranch({
279
- cwd: rewriteCwd,
280
- contractId: input.contractId,
281
- oldHead,
282
- recoveryRef,
283
- newBase: targetHead,
284
- oldBase,
285
- branch: worktreePath ? undefined : branch,
286
- preserveConflict: input.resolveConflicts,
287
- });
288
- }
289
- catch (error) {
290
- if (input.resolveConflicts && !(error instanceof FlowError && error.code === "RENEW_SESSION_OPEN")) {
291
- await removeRenewRecoverySession(repositoryRoot, input.contractId);
292
- }
293
- throw error;
294
- }
295
- const newHead = await resolveCommit(input.cwd, branch);
296
- return await finishRenew({
297
- repositoryRoot,
298
- rewriteCwd,
299
- ledgerHead: currentLedger.head,
300
- entries: currentLedger.entries,
301
- contractId: input.contractId,
302
- oldBase,
303
- oldHead,
304
- newBase: targetHead,
305
- newHead,
306
- recoveryRef,
307
- actor: input.actor,
308
- nowMs: input.nowMs,
309
- capture,
310
- afterLedgerCas: input.afterLedgerCas,
311
- });
260
+ return executeRenew(input);
312
261
  }
313
- async function revListRange(cwd, range) {
314
- const git = createGit(cwd);
262
+ export async function continueRenewContract(input) {
263
+ const scratchName = renewScratchName(input.contractId);
264
+ let scratch;
315
265
  try {
316
- return parseRevListOutput(await git.raw(["rev-list", "--reverse", range]));
266
+ scratch = await readDetachedRenewScratch({ cwd: input.cwd, scratchName });
317
267
  }
318
268
  catch (error) {
319
- throw wrapGitError(`rev-list ${range}`, error, cwd);
320
- }
321
- }
322
- async function assertRecoveryFacts(repositoryRoot, contractId, session) {
323
- if (session.commission !== contractId) {
324
- throw new FlowError("INTERNAL_STATE", `renew recovery session belongs to ${session.commission}, not ${contractId}`);
325
- }
326
- const rewriteCwd = await findRegisteredContractWorktreePath(repositoryRoot, contractId);
327
- if (!rewriteCwd)
328
- throw new FlowError("INTERNAL_STATE", `registered contract worktree disappeared for ${contractId}`);
329
- const ledger = await readLedger(repositoryRoot, contractId);
330
- if (!ledger)
331
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found or not active`);
332
- const savedHead = await resolveCommit(repositoryRoot, session.recoveryRef);
333
- if (savedHead !== session.oldHead || latestBase(ledger.entries) !== session.oldBase) {
334
- throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session facts no longer match the owned rewrite for ${contractId}`, {
335
- facts: { kind: "renew_session", commission: contractId, nextAction: "abort" },
336
- });
269
+ throw new FlowError("RENEW_SCRATCH_NOT_FOUND", `no renew conflict scratch is open for ${input.contractId}`, { cause: error });
337
270
  }
338
- return { ledger, rewriteCwd };
339
- }
340
- async function resultFromSpentSession(repositoryRoot, spent) {
341
- const seal = spent.seal;
342
- const sealed = seal ? {
343
- ...seal.data,
344
- scopeViolationEvidence: [],
345
- ledgerHead: await resolveCommit(repositoryRoot, `${spent.ledger.head}^`),
346
- } : null;
347
- return {
348
- contractId: spent.session.commission,
349
- ...spent.renew.data,
350
- ledgerHead: spent.ledger.head,
351
- alreadyFresh: false,
352
- sealed,
353
- recoveryCleanup: spent.recoveryCleanup,
354
- };
355
- }
356
- export async function continueRenewContract(input) {
357
- const repositoryRoot = await stableRepoRoot(input.cwd);
358
- const sessionState = await convergeRenewRecoverySession(repositoryRoot, input.contractId);
359
- if (sessionState.state === "spent")
360
- return await resultFromSpentSession(repositoryRoot, sessionState);
361
- if (sessionState.state === "none") {
362
- throw new FlowError("RENEW_SESSION_NOT_FOUND", `no renew recovery session is open for ${input.contractId}`);
363
- }
364
- const session = sessionState.session;
365
- const { ledger, rewriteCwd } = await assertRecoveryFacts(repositoryRoot, input.contractId, session);
366
- const delivery = await revListRange(repositoryRoot, `${session.oldBase}..${session.oldHead}`);
367
- if (await hasActiveRebase(rewriteCwd)) {
368
- const stoppedAt = await resolveCommitOrNull(rewriteCwd, "REBASE_HEAD");
369
- if (!stoppedAt) {
370
- throw new FlowError("RENEW_SESSION_OPEN", `owned rebase state has no stopped receipt for ${input.contractId}`, {
371
- facts: { kind: "renew_session", commission: input.contractId, nextAction: "abort" },
372
- });
373
- }
374
- const stoppedIndex = delivery.indexOf(stoppedAt);
375
- const rewrittenPrefix = await revListRange(rewriteCwd, `${session.newBase}..HEAD`);
376
- if (stoppedIndex < 0 || rewrittenPrefix.length !== stoppedIndex) {
377
- throw new FlowError("RENEW_SESSION_OPEN", `loose commits do not belong to the owned renew rebase for ${input.contractId}`, {
378
- facts: { kind: "renew_session", commission: input.contractId, nextAction: "continue-or-abort" },
379
- });
380
- }
381
- const continued = await runGitProcess(rewriteCwd, ["rebase", "--continue"], {
382
- env: { GIT_EDITOR: "true" },
383
- });
384
- if (continued.status !== 0) {
385
- const error = continued.error ?? { message: "git rebase --continue failed", stderr: continued.stderr, stdout: continued.stdout };
386
- if (errorContainsAnyPattern(error, ["CONFLICT", "could not apply", "Resolve all conflicts manually"])) {
387
- throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session is open for ${input.contractId}`, {
388
- cause: error,
389
- facts: { kind: "renew_session", commission: input.contractId, nextAction: "resolve-conflicts" },
390
- });
391
- }
392
- throw wrapGitError("rebase --continue", error, rewriteCwd);
393
- }
394
- }
395
- const newHead = await resolveCommit(repositoryRoot, `keiyaku/${input.contractId}`);
396
- const rewritten = await revListRange(repositoryRoot, `${session.newBase}..${newHead}`);
397
- if (await mergeBase(repositoryRoot, session.newBase, newHead) !== session.newBase || rewritten.length !== delivery.length) {
398
- throw new FlowError("RENEW_SESSION_OPEN", `rewritten delivery no longer corresponds to the saved renew receipts for ${input.contractId}`, {
399
- facts: { kind: "renew_session", commission: input.contractId, nextAction: "abort" },
400
- });
401
- }
402
- await appendScopeTerms({
403
- cwd: repositoryRoot,
404
- renderCwd: rewriteCwd,
405
- contractId: input.contractId,
406
- patterns: input.appendScope,
407
- actor: input.actor,
408
- nowMs: input.nowMs,
409
- });
410
- const currentLedger = input.appendScope?.length ? await readLedger(repositoryRoot, input.contractId) : ledger;
411
- if (!currentLedger)
412
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} disappeared while appending scope`);
413
- const capture = await captureRenewSeal({
414
- cwd: repositoryRoot,
415
- entries: currentLedger.entries,
416
- receiptHead: session.oldHead,
417
- contentBase: session.newBase,
418
- contentHead: newHead,
419
- });
420
- return await finishRenew({
421
- repositoryRoot,
422
- rewriteCwd,
423
- ledgerHead: currentLedger.head,
424
- entries: currentLedger.entries,
425
- contractId: input.contractId,
426
- oldBase: session.oldBase,
427
- oldHead: session.oldHead,
428
- newBase: session.newBase,
429
- newHead,
430
- recoveryRef: session.recoveryRef,
431
- actor: input.actor,
432
- nowMs: input.nowMs,
433
- capture,
434
- afterLedgerCas: input.afterLedgerCas,
435
- });
271
+ return executeRenew(input, { scratch, resolution: input.resolution ?? { kind: "scratch-files" } });
436
272
  }
437
273
  export async function abortRenewContract(input) {
438
- const repositoryRoot = await stableRepoRoot(input.cwd);
439
- const sessionState = await convergeRenewRecoverySession(repositoryRoot, input.contractId);
440
- if (sessionState.state !== "live") {
441
- throw new FlowError("RENEW_SESSION_NOT_FOUND", `no renew recovery session is open for ${input.contractId}`);
442
- }
443
- const session = sessionState.session;
444
- const { rewriteCwd } = await assertRecoveryFacts(repositoryRoot, input.contractId, session);
445
- const git = createGit(rewriteCwd);
446
- if (await hasActiveRebase(rewriteCwd)) {
274
+ checkCancellation(input.signal);
275
+ return withContractCarrierSession({ cwd: input.cwd, contractId: input.contractId, mode: "observe-only", signal: input.signal }, async (session) => {
276
+ const scratchName = renewScratchName(input.contractId);
277
+ let scratch;
447
278
  try {
448
- await git.raw(["rebase", "--abort"]);
279
+ scratch = await readDetachedRenewScratch({ cwd: input.cwd, scratchName });
449
280
  }
450
281
  catch (error) {
451
- throw wrapGitError("rebase --abort", error, rewriteCwd);
282
+ throw new FlowError("RENEW_SCRATCH_NOT_FOUND", `no renew conflict scratch is open for ${input.contractId}`, { cause: error });
452
283
  }
453
- }
454
- let branch;
455
- try {
456
- branch = (await git.raw(["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
457
- }
458
- catch {
459
- branch = undefined;
460
- }
461
- const expectedBranch = `keiyaku/${input.contractId}`;
462
- if (branch !== expectedBranch) {
463
- try {
464
- await git.raw(["switch", expectedBranch]);
465
- }
466
- catch (error) {
467
- throw wrapGitError(`switch ${expectedBranch}`, error, rewriteCwd);
468
- }
469
- }
470
- try {
471
- await git.raw(["reset", "--hard", session.oldHead]);
472
- }
473
- catch (error) {
474
- throw wrapGitError(`reset --hard ${session.recoveryRef}`, error, rewriteCwd);
475
- }
476
- await assertCleanTrackedWorkingTree(rewriteCwd);
477
- await removeRenewRecoverySession(repositoryRoot, input.contractId);
478
- const recoveryCleanup = await deleteRecoveryRef(repositoryRoot, session.recoveryRef, session.oldHead);
479
- return { contractId: input.contractId, oldHead: session.oldHead, recoveryCleanup };
284
+ checkCancellation(input.signal);
285
+ const cleanup = await abortDetachedRenewChain({ cwd: input.cwd, scratchName });
286
+ return {
287
+ ...derivedCleanupFacts({
288
+ contractId: input.contractId,
289
+ authoritativeHead: session.lifecycle.observedLedgerHead,
290
+ effect: "renew-conflict-scratch-cleanup",
291
+ receipt: { status: cleanup.status, scratchName, oldHead: scratch.oldHead },
292
+ }),
293
+ contractId: input.contractId,
294
+ oldHead: scratch.oldHead,
295
+ scratchName,
296
+ carrier: session.initialCarrier,
297
+ };
298
+ });
480
299
  }
481
300
  export async function renewKeiyaku(input) {
482
301
  await requireGitRepository(input.cwd, "renew");
483
302
  const address = input.address ?? await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
484
- return await withResolvedAddress(address, async () => {
485
- const contractId = address.binding.commissionId;
486
- const renewed = await renewContract({
487
- cwd: input.cwd,
488
- contractId,
489
- actor: ACTOR_IDENTITY,
490
- nowMs: Date.now(),
491
- appendScope: input.appendScope,
492
- resolveConflicts: input.resolveConflicts,
493
- });
494
- return await buildRenewOutcome(input.cwd, address, renewed);
303
+ return withResolvedAddress(address, async () => {
304
+ const result = await renewContract({ cwd: input.cwd, contractId: address.binding.commissionId, actor: ACTOR_IDENTITY, nowMs: Date.now(), appendScope: input.appendScope, signal: input.signal, persistResponse: renewResponsePersistence(input.cwd, address) });
305
+ return isCompleteLifecycleOutcome(result) ? buildRenewOutcome(address, result) : result;
495
306
  });
496
307
  }
497
- async function buildRenewOutcome(cwd, address, renewed) {
498
- const contractId = address.binding.commissionId;
499
- const currentBranch = await getCurrentBranch(cwd);
500
- const baseBranch = address.target.branch.slice("refs/heads/".length);
501
- const summary = renewed.alreadyFresh
502
- ? `Contract ${contractId} is already fresh; nothing touched.`
503
- : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
504
- const cleanupWarnings = renewed.recoveryCleanup?.status === "retained"
505
- ? [`Renew succeeded, but recovery ref ${renewed.recoveryCleanup.ref} was retained: ${renewed.recoveryCleanup.diagnostic}`]
506
- : [];
507
- let responsePath;
508
- let artifactId;
509
- try {
510
- const persisted = await persistResponseHistory({
511
- cwd,
512
- tool: "renew",
513
- provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: cwd },
514
- title: contractId,
515
- oldBase: renewed.oldBase,
516
- newBase: renewed.newBase,
517
- oldHead: renewed.oldHead,
518
- newHead: renewed.newHead,
519
- ledgerCommit: renewed.ledgerHead,
520
- summary,
521
- emptyDiffLabel: "nothing sealed",
522
- ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
523
- });
524
- responsePath = persisted.responsePath;
525
- artifactId = persisted.artifactId;
526
- }
527
- catch {
528
- responsePath = undefined;
529
- }
308
+ function renewResponsePersistence(cwd, address) {
309
+ return async (facts) => {
310
+ const contractId = address.binding.commissionId;
311
+ const summary = facts.alreadyFresh ? `Contract ${contractId} is already fresh; nothing touched.` : `Contract ${contractId} renewed onto ${facts.newBase}.`;
312
+ return persistResponseHistory({ cwd, tool: "renew", provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: cwd }, title: contractId, oldBase: facts.oldBase, newBase: facts.newBase, oldHead: facts.oldHead, newHead: facts.newHead, ledgerCommit: facts.ledgerHead, summary, emptyDiffLabel: "nothing sealed", ...(facts.sealed ? { sealed: facts.sealed } : {}) });
313
+ };
314
+ }
315
+ async function buildRenewOutcome(address, renewed) {
316
+ const { targetRoot, ...facts } = renewed;
530
317
  return {
531
318
  status: "success",
532
- ...renewed,
533
- currentBranch,
534
- baseBranch,
535
- // CLI/render synthesizes public face bytes from sealed typed facts.
319
+ ...facts,
320
+ currentBranch: await getCurrentBranch(targetRoot),
321
+ baseBranch: address.target.branch.slice("refs/heads/".length),
536
322
  diff: "",
537
323
  sealed: renewed.sealed ?? undefined,
538
- summary,
539
- ...(cleanupWarnings.length > 0 ? { meta: { warnings: cleanupWarnings } } : {}),
540
- responsePath,
541
- artifactId,
324
+ summary: renewed.alreadyFresh ? `Contract ${renewed.contractId} is already fresh; nothing touched.` : `Contract ${renewed.contractId} renewed onto ${renewed.newBase}.`,
542
325
  address,
543
326
  };
544
327
  }
545
328
  export async function continueRenewKeiyaku(input) {
546
329
  await requireGitRepository(input.cwd, "renew --continue");
547
- return await withResolvedAddress(input.address, async () => buildRenewOutcome(input.cwd, input.address, await continueRenewContract({
548
- cwd: input.cwd,
549
- contractId: input.address.binding.commissionId,
550
- actor: ACTOR_IDENTITY,
551
- nowMs: Date.now(),
552
- appendScope: input.appendScope,
553
- })));
330
+ return withResolvedAddress(input.address, async () => {
331
+ const result = await continueRenewContract({ cwd: input.cwd, contractId: input.address.binding.commissionId, actor: ACTOR_IDENTITY, nowMs: Date.now(), appendScope: input.appendScope, signal: input.signal, persistResponse: renewResponsePersistence(input.cwd, input.address) });
332
+ return isCompleteLifecycleOutcome(result) ? buildRenewOutcome(input.address, result) : result;
333
+ });
554
334
  }
555
335
  export async function abortRenewKeiyaku(input) {
556
336
  await requireGitRepository(input.cwd, "renew --abort");
557
- return await withResolvedAddress(input.address, async () => {
558
- const result = await abortRenewContract({ cwd: input.cwd, contractId: input.address.binding.commissionId });
559
- return {
560
- status: "success",
561
- ...result,
562
- address: input.address,
563
- summary: `Renew recovery for ${result.contractId} aborted; delivery restored to ${result.oldHead}.`,
564
- };
337
+ return withResolvedAddress(input.address, async () => {
338
+ const result = await abortRenewContract({ cwd: input.cwd, contractId: input.address.binding.commissionId, signal: input.signal });
339
+ return { status: "success", ...result, address: input.address, summary: `Renew conflict scratch for ${result.contractId} aborted; authoritative refs were untouched.` };
565
340
  });
566
341
  }