@astrosheep/keiyaku 2.9.2 → 2.9.4

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 (238) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +13 -94
  3. package/build/agents/harness/activity-values.js +16 -0
  4. package/build/agents/harness/event-persistence.js +218 -0
  5. package/build/agents/harness/execution-handle.js +25 -222
  6. package/build/agents/harness/index.js +3 -1
  7. package/build/agents/harness/outcome.js +47 -4
  8. package/build/agents/harness/projection.js +42 -59
  9. package/build/agents/harness/provider-adapter.js +58 -0
  10. package/build/agents/harness/router.js +11 -187
  11. package/build/agents/harness/runtime.js +17 -4
  12. package/build/agents/harness/session.js +15 -0
  13. package/build/agents/launch-snapshot/model.js +15 -236
  14. package/build/agents/launch-snapshot/parse-helpers.js +114 -0
  15. package/build/agents/launch-snapshot/resolve.js +17 -43
  16. package/build/agents/opencode-sdk.js +45 -12
  17. package/build/agents/providers/claude-agent-sdk/adapter.js +6 -17
  18. package/build/agents/providers/claude-agent-sdk/registration.js +214 -0
  19. package/build/agents/providers/claude-agent-sdk/session-schema.js +2 -0
  20. package/build/agents/providers/codex-app-server/adapter.js +50 -53
  21. package/build/agents/providers/codex-app-server/events.js +67 -11
  22. package/build/agents/providers/codex-app-server/identity.js +1 -0
  23. package/build/agents/providers/codex-app-server/policy.js +29 -0
  24. package/build/agents/providers/codex-app-server/registration.js +248 -0
  25. package/build/agents/providers/codex-app-server/session-schema.js +6 -0
  26. package/build/agents/providers/codex-app-server/session.js +23 -17
  27. package/build/agents/providers/codex-app-server/terms.js +1 -0
  28. package/build/agents/providers/opencode-sdk/adapter.js +94 -50
  29. package/build/agents/providers/opencode-sdk/events.js +8 -4
  30. package/build/agents/providers/opencode-sdk/registration.js +135 -0
  31. package/build/agents/providers/opencode-sdk/session-schema.js +2 -0
  32. package/build/agents/providers/opencode-sdk/session.js +21 -14
  33. package/build/agents/providers/opencode-sdk/terms.js +1 -0
  34. package/build/agents/providers/pi/adapter.js +36 -30
  35. package/build/agents/providers/pi/registration.js +145 -0
  36. package/build/agents/providers/pi/session-schema.js +6 -0
  37. package/build/agents/providers/pi/terms.js +1 -0
  38. package/build/agents/providers/registration.js +7 -0
  39. package/build/agents/providers/registry.js +76 -0
  40. package/build/agents/selector.js +2 -29
  41. package/build/agents/types.js +8 -0
  42. package/build/artifact-directories.js +22 -0
  43. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  44. package/build/cli/commands/akuma/view/handler.js +5 -4
  45. package/build/cli/commands/akuma.js +6 -0
  46. package/build/cli/commands/contract/amend/handler.js +8 -2
  47. package/build/cli/commands/contract/arc/handler.js +7 -1
  48. package/build/cli/commands/contract/bind/handler.js +4 -3
  49. package/build/cli/commands/contract/bind/meta.js +34 -7
  50. package/build/cli/commands/contract/forfeit/handler.js +4 -4
  51. package/build/cli/commands/contract/log/handler.js +3 -3
  52. package/build/cli/commands/contract/petition/handler.js +4 -6
  53. package/build/cli/commands/contract/renew/handler.js +20 -6
  54. package/build/cli/commands/contract/renew/meta.js +5 -4
  55. package/build/cli/commands/metadata.js +58 -0
  56. package/build/cli/commands/projection/call/handler.js +6 -5
  57. package/build/cli/commands/projection/kill/handler.js +16 -32
  58. package/build/cli/commands/projection/shared.js +28 -0
  59. package/build/cli/commands/projection/status/handler.js +1 -1
  60. package/build/cli/commands/projection/tell/handler.js +14 -30
  61. package/build/cli/commands/projection/wait/handler.js +45 -22
  62. package/build/cli/commands/projection/wait/meta.js +7 -2
  63. package/build/cli/commands/registry.js +2 -54
  64. package/build/cli/commands/shared.js +10 -7
  65. package/build/cli/commands/skills/install/handler.js +2 -5
  66. package/build/cli/commands/skills/skills/handler.js +1 -1
  67. package/build/cli/commands/task/add/handler.js +5 -13
  68. package/build/cli/commands/task/catalog.js +2 -2
  69. package/build/cli/commands/task/done/handler.js +4 -8
  70. package/build/cli/commands/task/drop/handler.js +3 -6
  71. package/build/cli/commands/task/ls/handler.js +3 -5
  72. package/build/cli/commands/task/shared.js +14 -4
  73. package/build/cli/commands/task/{view → show}/handler.js +7 -8
  74. package/build/cli/commands/task/show/meta.js +8 -0
  75. package/build/cli/commands/task/start/handler.js +3 -6
  76. package/build/cli/commands/task/stop/handler.js +4 -8
  77. package/build/cli/commands/task/task/handler.js +1 -1
  78. package/build/cli/commands/task/update/handler.js +4 -8
  79. package/build/cli/completion.js +1 -1
  80. package/build/cli/flags.js +19 -3
  81. package/build/cli/help.js +4 -11
  82. package/build/cli/index.js +57 -83
  83. package/build/cli/missing-subcommand.js +7 -0
  84. package/build/cli/parse-flags.js +14 -5
  85. package/build/cli/parse-metadata.js +3 -3
  86. package/build/cli/parse-selectors.js +1 -1
  87. package/build/cli/parse.js +21 -0
  88. package/build/cli/projection-address.js +7 -12
  89. package/build/cli/render/address.js +41 -37
  90. package/build/cli/render/arc.js +24 -12
  91. package/build/cli/render/call.js +11 -5
  92. package/build/cli/render/errors.js +36 -1
  93. package/build/cli/render/format.js +4 -0
  94. package/build/cli/render/kanshi.js +6 -12
  95. package/build/cli/render/kill.js +9 -0
  96. package/build/cli/render/misc.js +55 -12
  97. package/build/cli/render/petition.js +20 -15
  98. package/build/cli/render/projection-activity.js +3 -0
  99. package/build/cli/render/shared.js +60 -21
  100. package/build/cli/render/skills.js +10 -0
  101. package/build/cli/render/status.js +37 -14
  102. package/build/cli/render/success-response.js +77 -0
  103. package/build/cli/render/task.js +54 -14
  104. package/build/cli/render/tell.js +5 -0
  105. package/build/cli/render/terminal-failure.js +58 -0
  106. package/build/cli/render/types.js +1 -0
  107. package/build/cli/render/wait.js +38 -36
  108. package/build/cli/types.js +1 -1
  109. package/build/config/akuma-loader.js +0 -5
  110. package/build/config/env-keys.js +1 -1
  111. package/build/config/provider-policy-ownership.js +3 -32
  112. package/build/config/settings/disease.js +8 -1
  113. package/build/config/settings/loader.js +11 -3
  114. package/build/config/settings/provider-instance-fields.js +51 -0
  115. package/build/config/settings/schema.js +6 -176
  116. package/build/core/addressing.js +66 -16
  117. package/build/core/amend.js +135 -18
  118. package/build/core/arc.js +19 -67
  119. package/build/core/bind.js +82 -35
  120. package/build/core/call/call.js +15 -9
  121. package/build/core/call/context.js +6 -51
  122. package/build/core/call/execution.js +21 -22
  123. package/build/core/call/prompt.js +14 -1
  124. package/build/core/command-markdown.js +45 -0
  125. package/build/core/context.js +13 -7
  126. package/build/core/contract-view.js +20 -3
  127. package/build/core/draft.js +123 -16
  128. package/build/core/forfeit.js +5 -2
  129. package/build/core/hints.js +1 -75
  130. package/build/core/identifier-slug.js +66 -0
  131. package/build/core/ids.js +25 -11
  132. package/build/core/ledger.js +20 -68
  133. package/build/core/markdown/parser.js +0 -68
  134. package/build/core/markdown/render.js +68 -0
  135. package/build/core/markdown/sections.js +2 -1
  136. package/build/core/outcome-base.js +4 -0
  137. package/build/core/projection/generation/database.js +2 -38
  138. package/build/core/projection/generation/model.js +42 -1
  139. package/build/core/{projection-generation-continuation.js → projection/generation/projection-generation-continuation.js} +3 -3
  140. package/build/core/{projection-generation-execution.js → projection/generation/projection-generation-execution.js} +19 -49
  141. package/build/core/{projection-generation-identity.js → projection/generation/projection-generation-identity.js} +2 -2
  142. package/build/core/{projection-generation-launcher.js → projection/generation/projection-generation-launcher.js} +5 -3
  143. package/build/core/{projection-generation-process.js → projection/generation/projection-generation-process.js} +2 -1
  144. package/build/core/{projection-generation-runner.js → projection/generation/projection-generation-runner.js} +18 -3
  145. package/build/core/projection/generation/store.js +2 -2
  146. package/build/core/projection/generation/transitions.js +1 -1
  147. package/build/core/projection/index.js +27 -0
  148. package/build/core/{projection-activity.js → projection/projection-activity.js} +12 -0
  149. package/build/core/{projection-alias.js → projection/projection-alias.js} +14 -19
  150. package/build/core/projection/projection-coordinate.js +61 -0
  151. package/build/core/{projection-core.js → projection/projection-core.js} +9 -9
  152. package/build/core/{projection-execution-observer.js → projection/projection-execution-observer.js} +2 -57
  153. package/build/core/{projection-identity.js → projection/projection-identity.js} +1 -1
  154. package/build/core/{projection-kill.js → projection/projection-kill.js} +1 -1
  155. package/build/core/{projection-life-observer.js → projection/projection-life-observer.js} +1 -1
  156. package/build/core/{projection-mint.js → projection/projection-mint.js} +10 -10
  157. package/build/core/{projection-status.js → projection/projection-status.js} +98 -25
  158. package/build/core/projection/projection-terminal-failure.js +104 -0
  159. package/build/core/projection/projection-wait.js +278 -0
  160. package/build/core/projection/projection-wake.js +203 -0
  161. package/build/core/provider-loader-ports.js +4 -0
  162. package/build/core/ref-log.js +65 -40
  163. package/build/core/registry.js +63 -10
  164. package/build/core/renew-session.js +128 -0
  165. package/build/core/renew.js +426 -99
  166. package/build/core/scope.js +143 -66
  167. package/build/core/seal.js +139 -35
  168. package/build/core/{claim-delivery.js → settlement/claim-delivery.js} +9 -14
  169. package/build/core/{claim.js → settlement/claim.js} +32 -8
  170. package/build/core/settlement/index.js +19 -0
  171. package/build/core/{petition-claim-gates.js → settlement/petition-claim-gates.js} +22 -18
  172. package/build/core/{petition-forfeit.js → settlement/petition-forfeit.js} +2 -2
  173. package/build/core/{petition-head-guard.js → settlement/petition-head-guard.js} +2 -2
  174. package/build/core/settlement/petition-io.js +1 -0
  175. package/build/core/{petition-preview.js → settlement/petition-preview.js} +1 -1
  176. package/build/core/{petition.js → settlement/petition.js} +12 -10
  177. package/build/core/settlement/queue-read-model.js +143 -0
  178. package/build/core/settlement/queue-seat-allocation.js +156 -0
  179. package/build/core/settlement/queue.js +6 -0
  180. package/build/core/settlement/settlement.js +295 -0
  181. package/build/core/{verdict.js → settlement/verdict.js} +9 -37
  182. package/build/core/status/board.js +24 -23
  183. package/build/core/status/ledger-batch.js +158 -0
  184. package/build/core/status/reconciliation.js +10 -4
  185. package/build/core/stored-agent-event.js +3 -0
  186. package/build/core/task/board.js +40 -4
  187. package/build/core/task/commands.js +30 -21
  188. package/build/core/task/index.js +12 -0
  189. package/build/core/task/settlement-git.js +157 -65
  190. package/build/core/task/settlement-policy.js +54 -24
  191. package/build/core/task/task-contract.js +370 -0
  192. package/build/core/{task-doctor.js → task/task-doctor.js} +1 -1
  193. package/build/core/{task-git-actor.js → task/task-git-actor.js} +5 -10
  194. package/build/core/{task-git-runtime.js → task/task-git-runtime.js} +3 -3
  195. package/build/core/{task-git-store.js → task/task-git-store.js} +92 -29
  196. package/build/core/task/task-repository.js +1 -0
  197. package/build/core/{task-store-repository.js → task/task-store-repository.js} +5 -4
  198. package/build/core/task/task-store.js +1 -0
  199. package/build/core/{task-worktree.js → task/task-worktree.js} +2 -1
  200. package/build/core/task/task.js +5 -0
  201. package/build/core/task/tree.js +37 -0
  202. package/build/core/task/validation.js +5 -2
  203. package/build/core/transcripts.js +15 -3
  204. package/build/core/worktree-path.js +43 -28
  205. package/build/flow-error.js +85 -39
  206. package/build/fs/durability.js +27 -0
  207. package/build/fs/path-coordinate.js +16 -0
  208. package/build/generated/version.js +3 -2
  209. package/build/git/branches.js +2 -1
  210. package/build/git/core.js +106 -2
  211. package/build/git/path-coordinate.js +17 -0
  212. package/build/git/refs.js +57 -0
  213. package/build/git/streaming-batch.js +301 -0
  214. package/build/git/worktree.js +65 -0
  215. package/build/index.js +3 -3
  216. package/build/keiyaku.js +2 -36
  217. package/package.json +4 -10
  218. package/skills/keiyaku/SKILL.md +3 -2
  219. package/skills/keiyaku-akuma/SKILL.md +10 -6
  220. package/skills/keiyaku-task/SKILL.md +1 -1
  221. package/skills/keiyaku-workflow/SKILL.md +25 -4
  222. package/build/agents/providers/codex-sdk.js +0 -329
  223. package/build/cli/commands/task/view/meta.js +0 -8
  224. package/build/core/command-io.js +0 -174
  225. package/build/core/path-coordinate.js +0 -27
  226. package/build/core/petition-claim.js +0 -117
  227. package/build/core/petition-run.js +0 -83
  228. package/build/core/projection-coordinate.js +0 -63
  229. package/build/core/projection-wait.js +0 -165
  230. package/build/core/projection-wake.js +0 -136
  231. package/build/core/queue.js +0 -343
  232. package/build/core/task-contract.js +0 -237
  233. package/build/core/task.js +0 -4
  234. /package/build/{core/task-repository.js → agents/launch-snapshot/types.js} +0 -0
  235. /package/build/{core/task-store.js → agents/providers/claude-agent-sdk/terms.js} +0 -0
  236. /package/build/core/{projection-generation-runtime.js → projection/generation/projection-generation-runtime.js} +0 -0
  237. /package/build/core/{projection-life-protocol.js → projection/projection-life-protocol.js} +0 -0
  238. /package/build/core/{projection-runner-lock.js → projection/projection-runner-lock.js} +0 -0
@@ -1,19 +1,19 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { FlowError } from "../flow-error.js";
4
- import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
5
- import { KEIYAKU_FILE } from "../keiyaku.js";
6
- import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
- import { readProjectionStatusBoard } from "./projection-status.js";
8
- import { renderContractView } from "./contract-view.js";
9
- import { captureSeal, findOpenArc, getMaxArc, receiptsAfterFence, sealCurrentArc } from "./seal.js";
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, captureRenewSeal, captureSeal, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "./seal.js";
10
7
  import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
11
8
  import { commissionTarget, latestBase } from "./status/drift.js";
12
- import { findContractWorktreePath } from "./worktree-path.js";
9
+ import { findContractWorktreePath, findRegisteredContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
13
10
  import { getCurrentBranch, requireGitRepository } from "../git/branches.js";
11
+ import { assertCleanTrackedWorkingTree, getUnmergedPaths, hasActiveRebase } from "../git/worktree.js";
12
+ import { createRecoveryRef, deleteRecoveryRef } from "../git/refs.js";
14
13
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
15
14
  import { ACTOR_IDENTITY } from "../keiyaku.js";
16
15
  import { persistResponseHistory } from "./transcripts.js";
16
+ import { convergeRenewRecoverySession, readRenewRecoverySession, removeRenewRecoverySession, writeRenewRecoverySession, } from "./renew-session.js";
17
17
  function assertOperable(contractId, entries) {
18
18
  const state = deriveContractState(entries);
19
19
  if (state.state === "corrupt") {
@@ -43,15 +43,14 @@ async function resolveCommitOrNull(cwd, rev) {
43
43
  throw wrapGitError(`rev-parse ${rev}`, error, cwd);
44
44
  }
45
45
  }
46
- async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
47
- const ledger = await readLedger(ledgerCwd, contractId);
48
- if (!ledger) {
49
- throw new FlowError("INTERNAL_STATE", `contract ${contractId} active ledger disappeared before render`);
50
- }
51
- await fs.writeFile(path.join(renderCwd, KEIYAKU_FILE), renderContractView({ id: contractId, entries: ledger.entries }), "utf8");
46
+ const RENEW_RECOVERY_REF_PREFIX = "refs/keiyaku/recovery/renew";
47
+ function renewRecoveryRef(contractId, oldHead) {
48
+ return `${RENEW_RECOVERY_REF_PREFIX}/${contractId}/${oldHead}`;
52
49
  }
53
- async function rewriteBranch(cwd, newBase, oldBase, branch) {
50
+ async function rewriteBranch(input) {
51
+ const { cwd, contractId, oldHead, recoveryRef, newBase, oldBase, branch, preserveConflict } = input;
54
52
  const git = createGit(cwd);
53
+ const originalBranch = branch ? await getCurrentBranch(cwd) : undefined;
55
54
  const args = branch
56
55
  ? ["rebase", "--onto", newBase, oldBase, branch]
57
56
  : ["rebase", "--onto", newBase, oldBase];
@@ -59,31 +58,155 @@ async function rewriteBranch(cwd, newBase, oldBase, branch) {
59
58
  await git.raw(args);
60
59
  }
61
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);
67
+ }
68
+ catch (captureError) {
69
+ conflictPathDiagnostic = asMessage(captureError);
70
+ }
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
+ }
62
78
  try {
63
79
  await git.raw(["rebase", "--abort"]);
64
80
  }
65
- catch {
66
- // Preserve the rewrite failure after best-effort restoration.
81
+ catch (abortError) {
82
+ throw wrapGitError("rebase --abort", abortError, cwd);
67
83
  }
68
- if (errorContainsAnyPattern(error, ["CONFLICT", "could not apply", "Resolve all conflicts manually"])) {
69
- throw new FlowError("CLOSE_MERGE_CONFLICT", "renew conflict; working tree restored", { cause: error });
84
+ if (originalBranch && originalBranch !== branch) {
85
+ try {
86
+ await git.raw(["switch", originalBranch]);
87
+ }
88
+ catch (switchError) {
89
+ throw wrapGitError(`switch ${originalBranch}`, switchError, cwd);
90
+ }
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}`);
96
+ }
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,
111
+ },
112
+ });
70
113
  }
71
114
  throw wrapGitError(`renew ${branch ?? "current branch"}`, error, cwd);
72
115
  }
73
116
  }
74
- function assertNoInFlightProjection(cwd, contractId, nowMs) {
75
- const board = readProjectionStatusBoard(cwd, nowMs);
76
- const unsafe = new Set(["out", "minting", "startup-timeout", "unknown"]);
77
- if (!board?.rows.some((row) => unsafe.has(row.state)))
117
+ async function assertNoInFlightProjection(repositoryRoot, contractId, deliveryWorktree, nowMs) {
118
+ if (!await hasStrandableProjectionGeneration({
119
+ cwd: repositoryRoot,
120
+ commissionId: contractId,
121
+ deliveryWorktree,
122
+ nowMs,
123
+ }))
78
124
  return;
79
- throw new FlowError("INTERNAL_STATE", `akuma still out on ${contractId}'s worktree; rewriting under it would strand its work`, { hints: ["keiyaku status — watch for the akuma to land, then renew"] });
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 } });
80
126
  }
81
127
  function looseCommitError(contractId, receipts) {
82
- return new FlowError("NO_OPEN_ARC", `${receipts.length} commits after fence have no arc to receipt them`, {
83
- hints: [`keiyaku @${contractId} arc — opens a new arc to receipt them, then renew`],
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, input.waivePaths)
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,
84
182
  });
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
+ };
85
196
  }
86
197
  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
+ if (await readRenewRecoverySession(repositoryRoot, input.contractId)) {
202
+ throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session is open for ${input.contractId}`, {
203
+ facts: { kind: "renew_session", commission: input.contractId, nextAction: "continue-or-abort" },
204
+ });
205
+ }
206
+ if (input.resolveConflicts && !worktreePath) {
207
+ throw new FlowError("INTERNAL_STATE", `renew --resolve requires ${input.contractId}'s registered contract worktree`);
208
+ }
209
+ await assertCleanTrackedWorkingTree(rewriteCwd);
87
210
  const ledger = await readLedger(input.cwd, input.contractId);
88
211
  if (!ledger) {
89
212
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
@@ -102,6 +225,14 @@ export async function renewContract(input) {
102
225
  throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
103
226
  const branch = `keiyaku/${input.contractId}`;
104
227
  const oldHead = await resolveCommit(input.cwd, branch);
228
+ await assertRenewCoordinates({
229
+ cwd: repositoryRoot,
230
+ contractId: input.contractId,
231
+ oldBase,
232
+ oldHead,
233
+ targetHead,
234
+ entries: ledger.entries,
235
+ });
105
236
  const capture = await captureSeal(input.cwd, ledger.entries, oldHead, input.waivePaths);
106
237
  if (getMaxArc(ledger.entries) > 0 && !findOpenArc(ledger.entries)) {
107
238
  const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
@@ -118,51 +249,216 @@ export async function renewContract(input) {
118
249
  ledgerHead: ledger.head,
119
250
  alreadyFresh: true,
120
251
  sealed: null,
252
+ recoveryCleanup: null,
121
253
  };
122
254
  }
123
- const worktreePath = await findContractWorktreePath(input.cwd, input.contractId);
124
- const rewriteCwd = worktreePath ?? input.cwd;
125
- assertNoInFlightProjection(rewriteCwd, input.contractId, input.nowMs);
126
- await rewriteBranch(rewriteCwd, targetHead, oldBase, worktreePath ? undefined : branch);
127
- const newHead = await resolveCommit(input.cwd, branch);
128
- const sealed = await sealCurrentArc({
129
- cwd: input.cwd,
130
- renderCwd: rewriteCwd,
131
- contractId: input.contractId,
132
- sealedBy: "renew",
133
- nowMs: input.nowMs,
134
- capture: capture ? { ...capture, newFence: newHead } : null,
135
- });
136
- const at = new Date(input.nowMs).toISOString();
137
- const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
138
- assertOperable(input.contractId, entries);
139
- if (latestBase(entries) !== oldBase) {
140
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} changed while renewing`);
255
+ const deliveryWorktree = worktreePath ?? repositoryRoot;
256
+ await assertNoInFlightProjection(repositoryRoot, input.contractId, deliveryWorktree, input.nowMs);
257
+ const recoveryRef = renewRecoveryRef(input.contractId, oldHead);
258
+ await createRecoveryRef(input.cwd, recoveryRef, oldHead);
259
+ if (input.resolveConflicts) {
260
+ await writeRenewRecoverySession(repositoryRoot, {
261
+ commission: input.contractId,
262
+ oldBase,
263
+ oldHead,
264
+ newBase: targetHead,
265
+ recoveryRef,
266
+ });
267
+ }
268
+ try {
269
+ await rewriteBranch({
270
+ cwd: rewriteCwd,
271
+ contractId: input.contractId,
272
+ oldHead,
273
+ recoveryRef,
274
+ newBase: targetHead,
275
+ oldBase,
276
+ branch: worktreePath ? undefined : branch,
277
+ preserveConflict: input.resolveConflicts,
278
+ });
279
+ }
280
+ catch (error) {
281
+ if (input.resolveConflicts && !(error instanceof FlowError && error.code === "RENEW_SESSION_OPEN")) {
282
+ await removeRenewRecoverySession(repositoryRoot, input.contractId);
141
283
  }
142
- return {
143
- v: 1,
144
- kind: "renew",
145
- contract: input.contractId,
146
- at,
147
- actor: input.actor,
148
- data: { oldBase, newBase: targetHead, oldHead, newHead },
149
- };
150
- });
151
- if (!result.ok) {
152
- throw new FlowError("INTERNAL_STATE", `failed to append renew for ${input.contractId}: ${result.reason}`);
284
+ throw error;
153
285
  }
154
- await renderUpdatedContract(input.cwd, rewriteCwd, input.contractId);
155
- return {
286
+ const newHead = await resolveCommit(input.cwd, branch);
287
+ return await finishRenew({
288
+ repositoryRoot,
289
+ rewriteCwd,
290
+ ledgerHead: ledger.head,
291
+ entries: ledger.entries,
156
292
  contractId: input.contractId,
157
293
  oldBase,
158
- newBase: targetHead,
159
294
  oldHead,
295
+ newBase: targetHead,
160
296
  newHead,
161
- ledgerHead: result.head,
297
+ recoveryRef,
298
+ actor: input.actor,
299
+ nowMs: input.nowMs,
300
+ capture,
301
+ afterLedgerCas: input.afterLedgerCas,
302
+ });
303
+ }
304
+ async function revListRange(cwd, range) {
305
+ const git = createGit(cwd);
306
+ try {
307
+ return parseRevListOutput(await git.raw(["rev-list", "--reverse", range]));
308
+ }
309
+ catch (error) {
310
+ throw wrapGitError(`rev-list ${range}`, error, cwd);
311
+ }
312
+ }
313
+ async function assertRecoveryFacts(repositoryRoot, contractId, session) {
314
+ if (session.commission !== contractId) {
315
+ throw new FlowError("INTERNAL_STATE", `renew recovery session belongs to ${session.commission}, not ${contractId}`);
316
+ }
317
+ const rewriteCwd = await findRegisteredContractWorktreePath(repositoryRoot, contractId);
318
+ if (!rewriteCwd)
319
+ throw new FlowError("INTERNAL_STATE", `registered contract worktree disappeared for ${contractId}`);
320
+ const ledger = await readLedger(repositoryRoot, contractId);
321
+ if (!ledger)
322
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found or not active`);
323
+ const savedHead = await resolveCommit(repositoryRoot, session.recoveryRef);
324
+ if (savedHead !== session.oldHead || latestBase(ledger.entries) !== session.oldBase) {
325
+ throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session facts no longer match the owned rewrite for ${contractId}`, {
326
+ facts: { kind: "renew_session", commission: contractId, nextAction: "abort" },
327
+ });
328
+ }
329
+ return { ledger, rewriteCwd };
330
+ }
331
+ async function resultFromSpentSession(repositoryRoot, spent) {
332
+ const seal = spent.seal;
333
+ const sealed = seal ? {
334
+ ...seal.data,
335
+ scopeViolationEvidence: [],
336
+ ledgerHead: await resolveCommit(repositoryRoot, `${spent.ledger.head}^`),
337
+ } : null;
338
+ return {
339
+ contractId: spent.session.commission,
340
+ ...spent.renew.data,
341
+ ledgerHead: spent.ledger.head,
162
342
  alreadyFresh: false,
163
343
  sealed,
344
+ recoveryCleanup: spent.recoveryCleanup,
164
345
  };
165
346
  }
347
+ export async function continueRenewContract(input) {
348
+ const repositoryRoot = await stableRepoRoot(input.cwd);
349
+ const sessionState = await convergeRenewRecoverySession(repositoryRoot, input.contractId);
350
+ if (sessionState.state === "spent")
351
+ return await resultFromSpentSession(repositoryRoot, sessionState);
352
+ if (sessionState.state === "none") {
353
+ throw new FlowError("RENEW_SESSION_NOT_FOUND", `no renew recovery session is open for ${input.contractId}`);
354
+ }
355
+ const session = sessionState.session;
356
+ const { ledger, rewriteCwd } = await assertRecoveryFacts(repositoryRoot, input.contractId, session);
357
+ const delivery = await revListRange(repositoryRoot, `${session.oldBase}..${session.oldHead}`);
358
+ if (await hasActiveRebase(rewriteCwd)) {
359
+ const stoppedAt = await resolveCommitOrNull(rewriteCwd, "REBASE_HEAD");
360
+ if (!stoppedAt) {
361
+ throw new FlowError("RENEW_SESSION_OPEN", `owned rebase state has no stopped receipt for ${input.contractId}`, {
362
+ facts: { kind: "renew_session", commission: input.contractId, nextAction: "abort" },
363
+ });
364
+ }
365
+ const stoppedIndex = delivery.indexOf(stoppedAt);
366
+ const rewrittenPrefix = await revListRange(rewriteCwd, `${session.newBase}..HEAD`);
367
+ if (stoppedIndex < 0 || rewrittenPrefix.length !== stoppedIndex) {
368
+ throw new FlowError("RENEW_SESSION_OPEN", `loose commits do not belong to the owned renew rebase for ${input.contractId}`, {
369
+ facts: { kind: "renew_session", commission: input.contractId, nextAction: "continue-or-abort" },
370
+ });
371
+ }
372
+ const continued = await runGitProcess(rewriteCwd, ["rebase", "--continue"], {
373
+ env: { GIT_EDITOR: "true" },
374
+ });
375
+ if (continued.status !== 0) {
376
+ const error = continued.error ?? { message: "git rebase --continue failed", stderr: continued.stderr, stdout: continued.stdout };
377
+ if (errorContainsAnyPattern(error, ["CONFLICT", "could not apply", "Resolve all conflicts manually"])) {
378
+ throw new FlowError("RENEW_SESSION_OPEN", `renew recovery session is open for ${input.contractId}`, {
379
+ cause: error,
380
+ facts: { kind: "renew_session", commission: input.contractId, nextAction: "resolve-conflicts" },
381
+ });
382
+ }
383
+ throw wrapGitError("rebase --continue", error, rewriteCwd);
384
+ }
385
+ }
386
+ const newHead = await resolveCommit(repositoryRoot, `keiyaku/${input.contractId}`);
387
+ const rewritten = await revListRange(repositoryRoot, `${session.newBase}..${newHead}`);
388
+ if (await mergeBase(repositoryRoot, session.newBase, newHead) !== session.newBase || rewritten.length !== delivery.length) {
389
+ throw new FlowError("RENEW_SESSION_OPEN", `rewritten delivery no longer corresponds to the saved renew receipts for ${input.contractId}`, {
390
+ facts: { kind: "renew_session", commission: input.contractId, nextAction: "abort" },
391
+ });
392
+ }
393
+ const capture = await captureRenewSeal({
394
+ cwd: repositoryRoot,
395
+ entries: ledger.entries,
396
+ receiptHead: session.oldHead,
397
+ contentBase: session.newBase,
398
+ contentHead: newHead,
399
+ waivePaths: input.waivePaths,
400
+ });
401
+ return await finishRenew({
402
+ repositoryRoot,
403
+ rewriteCwd,
404
+ ledgerHead: ledger.head,
405
+ entries: ledger.entries,
406
+ contractId: input.contractId,
407
+ oldBase: session.oldBase,
408
+ oldHead: session.oldHead,
409
+ newBase: session.newBase,
410
+ newHead,
411
+ recoveryRef: session.recoveryRef,
412
+ actor: input.actor,
413
+ nowMs: input.nowMs,
414
+ capture,
415
+ afterLedgerCas: input.afterLedgerCas,
416
+ });
417
+ }
418
+ export async function abortRenewContract(input) {
419
+ const repositoryRoot = await stableRepoRoot(input.cwd);
420
+ const sessionState = await convergeRenewRecoverySession(repositoryRoot, input.contractId);
421
+ if (sessionState.state !== "live") {
422
+ throw new FlowError("RENEW_SESSION_NOT_FOUND", `no renew recovery session is open for ${input.contractId}`);
423
+ }
424
+ const session = sessionState.session;
425
+ const { rewriteCwd } = await assertRecoveryFacts(repositoryRoot, input.contractId, session);
426
+ const git = createGit(rewriteCwd);
427
+ if (await hasActiveRebase(rewriteCwd)) {
428
+ try {
429
+ await git.raw(["rebase", "--abort"]);
430
+ }
431
+ catch (error) {
432
+ throw wrapGitError("rebase --abort", error, rewriteCwd);
433
+ }
434
+ }
435
+ let branch;
436
+ try {
437
+ branch = (await git.raw(["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
438
+ }
439
+ catch {
440
+ branch = undefined;
441
+ }
442
+ const expectedBranch = `keiyaku/${input.contractId}`;
443
+ if (branch !== expectedBranch) {
444
+ try {
445
+ await git.raw(["switch", expectedBranch]);
446
+ }
447
+ catch (error) {
448
+ throw wrapGitError(`switch ${expectedBranch}`, error, rewriteCwd);
449
+ }
450
+ }
451
+ try {
452
+ await git.raw(["reset", "--hard", session.oldHead]);
453
+ }
454
+ catch (error) {
455
+ throw wrapGitError(`reset --hard ${session.recoveryRef}`, error, rewriteCwd);
456
+ }
457
+ await assertCleanTrackedWorkingTree(rewriteCwd);
458
+ await removeRenewRecoverySession(repositoryRoot, input.contractId);
459
+ const recoveryCleanup = await deleteRecoveryRef(repositoryRoot, session.recoveryRef, session.oldHead);
460
+ return { contractId: input.contractId, oldHead: session.oldHead, recoveryCleanup };
461
+ }
166
462
  export async function renewKeiyaku(input) {
167
463
  await requireGitRepository(input.cwd, "renew");
168
464
  const address = input.address ?? await resolveContractAddress(input.cwd, input.contractId, input.contractAddressSource);
@@ -174,47 +470,78 @@ export async function renewKeiyaku(input) {
174
470
  actor: ACTOR_IDENTITY,
175
471
  nowMs: Date.now(),
176
472
  waivePaths: input.waivePaths,
473
+ resolveConflicts: input.resolveConflicts,
177
474
  });
178
- const currentBranch = await getCurrentBranch(input.cwd);
179
- const baseBranch = address.target.branch.slice("refs/heads/".length);
180
- const diff = renewed.sealed?.incrementalDiff ?? "nothing sealed";
181
- const summary = renewed.alreadyFresh
182
- ? `Contract ${contractId} is already fresh; nothing touched.`
183
- : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
184
- let responsePath;
185
- let artifactId;
186
- try {
187
- const persisted = await persistResponseHistory({
188
- cwd: input.cwd,
189
- tool: "renew",
190
- provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: input.cwd },
191
- title: contractId,
192
- oldBase: renewed.oldBase,
193
- newBase: renewed.newBase,
194
- oldHead: renewed.oldHead,
195
- newHead: renewed.newHead,
196
- ledgerCommit: renewed.ledgerHead,
197
- summary,
198
- diff,
199
- ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
200
- });
201
- responsePath = persisted.responsePath;
202
- artifactId = persisted.artifactId;
203
- }
204
- catch {
205
- responsePath = undefined;
206
- }
475
+ return await buildRenewOutcome(input.cwd, address, renewed);
476
+ });
477
+ }
478
+ async function buildRenewOutcome(cwd, address, renewed) {
479
+ const contractId = address.binding.commissionId;
480
+ const currentBranch = await getCurrentBranch(cwd);
481
+ const baseBranch = address.target.branch.slice("refs/heads/".length);
482
+ const summary = renewed.alreadyFresh
483
+ ? `Contract ${contractId} is already fresh; nothing touched.`
484
+ : `Contract ${contractId} renewed onto ${renewed.newBase}.`;
485
+ const cleanupWarnings = renewed.recoveryCleanup?.status === "retained"
486
+ ? [`Renew succeeded, but recovery ref ${renewed.recoveryCleanup.ref} was retained: ${renewed.recoveryCleanup.diagnostic}`]
487
+ : [];
488
+ let responsePath;
489
+ let artifactId;
490
+ try {
491
+ const persisted = await persistResponseHistory({
492
+ cwd,
493
+ tool: "renew",
494
+ provenance: { authority: { kind: "repository" }, binding: address.binding, evidenceCwd: cwd },
495
+ title: contractId,
496
+ oldBase: renewed.oldBase,
497
+ newBase: renewed.newBase,
498
+ oldHead: renewed.oldHead,
499
+ newHead: renewed.newHead,
500
+ ledgerCommit: renewed.ledgerHead,
501
+ summary,
502
+ emptyDiffLabel: "nothing sealed",
503
+ ...(renewed.sealed ? { sealed: renewed.sealed } : {}),
504
+ });
505
+ responsePath = persisted.responsePath;
506
+ artifactId = persisted.artifactId;
507
+ }
508
+ catch {
509
+ responsePath = undefined;
510
+ }
511
+ return {
512
+ status: "success",
513
+ ...renewed,
514
+ currentBranch,
515
+ baseBranch,
516
+ // CLI/render synthesizes public face bytes from sealed typed facts.
517
+ diff: "",
518
+ sealed: renewed.sealed ?? undefined,
519
+ summary,
520
+ ...(cleanupWarnings.length > 0 ? { meta: { warnings: cleanupWarnings } } : {}),
521
+ responsePath,
522
+ artifactId,
523
+ address,
524
+ };
525
+ }
526
+ export async function continueRenewKeiyaku(input) {
527
+ await requireGitRepository(input.cwd, "renew --continue");
528
+ return await withResolvedAddress(input.address, async () => buildRenewOutcome(input.cwd, input.address, await continueRenewContract({
529
+ cwd: input.cwd,
530
+ contractId: input.address.binding.commissionId,
531
+ actor: ACTOR_IDENTITY,
532
+ nowMs: Date.now(),
533
+ waivePaths: input.waivePaths,
534
+ })));
535
+ }
536
+ export async function abortRenewKeiyaku(input) {
537
+ await requireGitRepository(input.cwd, "renew --abort");
538
+ return await withResolvedAddress(input.address, async () => {
539
+ const result = await abortRenewContract({ cwd: input.cwd, contractId: input.address.binding.commissionId });
207
540
  return {
208
541
  status: "success",
209
- ...renewed,
210
- currentBranch,
211
- baseBranch,
212
- diff,
213
- sealed: renewed.sealed ?? undefined,
214
- summary,
215
- responsePath,
216
- artifactId,
217
- address,
542
+ ...result,
543
+ address: input.address,
544
+ summary: `Renew recovery for ${result.contractId} aborted; delivery restored to ${result.oldHead}.`,
218
545
  };
219
546
  });
220
547
  }