@astrosheep/keiyaku 2.9.12 → 2.9.13

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 (212) hide show
  1. package/README.md +22 -18
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +2 -2
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { assertHostObjectId, deriveHostGitCoordinates, readCommitObject } from "./repository-ledger/git-object-store.js";
4
4
  import { runGitProcess, runGitProcessBytes } from "../git/core.js";
5
5
  import { stableRepoRoot } from "./worktree-path.js";
6
- const SCRATCH_VERSION = 1;
6
+ const SCRATCH_VERSION = 3;
7
7
  const SCRATCH_DIRECTORY = path.join("keiyaku", "renew-scratch");
8
8
  const SCRATCH_STATE_FILE = "state.json";
9
9
  const SCRATCH_FILES_DIRECTORY = "files";
@@ -136,11 +136,31 @@ async function mergeCommitTree(input) {
136
136
  await assertTree(input.cwd, input.coordinates, tree);
137
137
  if (result.status === 0)
138
138
  return { status: "clean", tree };
139
- const paths = Array.from(new Set(lines.flatMap((line) => {
140
- const match = /^\d{6} [0-9a-f]+ [123]\t(.+)$/.exec(line);
141
- return match ? [match[1]] : [];
142
- })));
143
- return { status: "conflict", tree, paths };
139
+ const index = lines.flatMap((line) => {
140
+ const match = /^(\d{6}) ([0-9a-f]+) ([123])\t(.+)$/.exec(line);
141
+ return match ? [{ mode: match[1], objectId: match[2], stage: Number(match[3]), path: match[4] }] : [];
142
+ });
143
+ const paths = Array.from(new Set(index.map((entry) => entry.path)));
144
+ return { status: "conflict", tree, paths, index };
145
+ }
146
+ /** Replay a resolved delivery tree from its captured target onto a later target. */
147
+ export async function replayResolvedDelivery(input) {
148
+ for (const commit of [input.capturedTarget, input.latestTarget]) {
149
+ assertHostObjectId(input.coordinates, commit);
150
+ await readCommitObject(input.cwd, input.coordinates, commit);
151
+ }
152
+ const resolvedCommit = await buildCommit(input.cwd, input.coordinates, input.identity, input.resolvedTree, input.capturedTarget);
153
+ const replay = await mergeCommitTree({
154
+ cwd: input.cwd,
155
+ coordinates: input.coordinates,
156
+ original: {
157
+ ...input.identity,
158
+ objectId: resolvedCommit,
159
+ parent: input.capturedTarget,
160
+ },
161
+ currentHead: input.latestTarget,
162
+ });
163
+ return { ...replay, resolvedCommit };
144
164
  }
145
165
  export async function buildCommit(cwd, coordinates, original, tree, parent) {
146
166
  await assertTree(cwd, coordinates, tree);
@@ -170,41 +190,64 @@ export function interrupted(signal) {
170
190
  });
171
191
  }
172
192
  }
173
- async function materializeConflict(cwd, scratch, tree) {
174
- await fs.rm(scratch.files, { recursive: true, force: true });
175
- await fs.mkdir(scratch.files, { recursive: true });
176
- const listingArgs = ["ls-tree", "-r", "-z", "--full-tree", tree];
177
- const listing = await runGitProcessBytes(cwd, listingArgs);
178
- if (listing.status !== 0 || listing.error) {
179
- throw gitFailure(listingArgs, listing.stderr.toString("utf8").trim() || listing.error?.message || `exit ${String(listing.status)}`, listing.error);
180
- }
181
- for (const row of listing.stdout.toString("utf8").split("\0").filter(Boolean)) {
182
- const match = /^(\d{6}) (blob|commit) ([0-9a-f]+)\t([\s\S]+)$/.exec(row);
183
- if (!match)
184
- throw new RenewBuildError("GIT_RENEW_BUILD_FAILED", `invalid ls-tree row while materializing renew scratch`);
185
- const [, mode, type, objectId, relative] = match;
186
- const destination = path.resolve(scratch.files, relative);
187
- const withinScratch = path.relative(scratch.files, destination);
188
- if (withinScratch === "" || path.isAbsolute(withinScratch) || withinScratch === ".." || withinScratch.startsWith(`..${path.sep}`)) {
189
- throw new RenewBuildError("GIT_RENEW_BUILD_FAILED", `unsafe path in renew conflict tree: ${JSON.stringify(relative)}`);
190
- }
191
- await fs.mkdir(path.dirname(destination), { recursive: true });
192
- if (type === "commit") {
193
- await fs.writeFile(destination, `${objectId}\n`, "utf8");
194
- continue;
195
- }
196
- const blobArgs = ["cat-file", "blob", objectId];
197
- const blob = await runGitProcessBytes(cwd, blobArgs);
198
- if (blob.status !== 0 || blob.error) {
199
- throw gitFailure(blobArgs, blob.stderr.toString("utf8").trim() || blob.error?.message || `exit ${String(blob.status)}`, blob.error);
193
+ async function materializeConflict(cwd, scratch, state, create) {
194
+ if (create) {
195
+ await successfulGit(cwd, ["worktree", "add", "--detach", scratch.files, state.currentHead]);
196
+ }
197
+ else {
198
+ await validateScratchWorktree(cwd, scratch, state, { requireRecordedHead: false });
199
+ await successfulGit(scratch.files, ["reset", "--hard", state.currentHead]);
200
+ await successfulGit(scratch.files, ["clean", "-ffdx"]);
201
+ }
202
+ await successfulGit(scratch.files, ["read-tree", "--reset", "-u", state.conflictTree]);
203
+ await successfulGit(scratch.files, [
204
+ "update-index",
205
+ "--force-remove",
206
+ "--",
207
+ ...Array.from(new Set(state.conflictIndex.map((entry) => entry.path))),
208
+ ]);
209
+ await successfulGit(scratch.files, ["update-index", "--index-info"], { input: state.conflictIndex.map((entry) => `${entry.mode} ${entry.objectId} ${entry.stage}\t${entry.path}\n`).join("") });
210
+ }
211
+ async function scratchCommonDirectory(cwd) {
212
+ const commonDirectory = (await successfulGit(cwd, [
213
+ "rev-parse",
214
+ "--path-format=absolute",
215
+ "--git-common-dir",
216
+ ])).trim();
217
+ return await fs.realpath(commonDirectory);
218
+ }
219
+ async function validateScratchWorktree(cwd, scratch, state, options) {
220
+ let filesPath;
221
+ try {
222
+ filesPath = await fs.realpath(scratch.files);
223
+ }
224
+ catch (error) {
225
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} has no linked worktree`, { cause: error });
226
+ }
227
+ try {
228
+ const [topLevel, commonDirectory] = await Promise.all([
229
+ successfulGit(filesPath, ["rev-parse", "--show-toplevel"]),
230
+ scratchCommonDirectory(filesPath),
231
+ ]);
232
+ if (path.resolve(topLevel.trim()) !== filesPath || commonDirectory !== await scratchCommonDirectory(cwd)) {
233
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} is not the recorded linked worktree`);
200
234
  }
201
- if (mode === "120000") {
202
- await fs.symlink(blob.stdout.toString("utf8"), destination);
235
+ const symbolicHead = await runGitProcess(filesPath, ["symbolic-ref", "-q", "HEAD"]);
236
+ if (symbolicHead.status === 0 || symbolicHead.status !== 1 || symbolicHead.error) {
237
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} must retain a detached HEAD`);
203
238
  }
204
- else {
205
- await fs.writeFile(destination, blob.stdout, { mode: mode === "100755" ? 0o755 : 0o644 });
239
+ if (options.requireRecordedHead) {
240
+ const head = (await successfulGit(filesPath, ["rev-parse", "HEAD"])).trim();
241
+ if (head !== state.currentHead) {
242
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} HEAD no longer matches the recorded replay head`);
243
+ }
206
244
  }
207
245
  }
246
+ catch (error) {
247
+ if (error instanceof RenewBuildError)
248
+ throw error;
249
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} has invalid linked worktree metadata`, { cause: error });
250
+ }
208
251
  }
209
252
  async function writeScratchState(scratch, state) {
210
253
  const temporary = `${scratch.state}.next`;
@@ -212,9 +255,40 @@ async function writeScratchState(scratch, state) {
212
255
  await fs.writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
213
256
  await fs.rename(temporary, scratch.state);
214
257
  }
258
+ async function hasRegisteredScratchWorktree(cwd, scratch) {
259
+ const worktrees = await successfulGit(cwd, ["worktree", "list", "--porcelain", "-z"]);
260
+ const expected = path.resolve(scratch.files);
261
+ return worktrees.split("\0").some((field) => field.startsWith("worktree ") && path.resolve(field.slice("worktree ".length)) === expected);
262
+ }
263
+ async function scratchRootExists(scratch) {
264
+ try {
265
+ await fs.lstat(scratch.root);
266
+ return true;
267
+ }
268
+ catch (error) {
269
+ if (error.code === "ENOENT")
270
+ return false;
271
+ throw error;
272
+ }
273
+ }
274
+ /** Remove only the deterministic scratch root and its exact Git registration. */
275
+ export async function cleanupRenewScratch(input) {
276
+ const [registered, rootExists] = await Promise.all([
277
+ hasRegisteredScratchWorktree(input.cwd, input.scratch),
278
+ scratchRootExists(input.scratch),
279
+ ]);
280
+ if (!registered && !rootExists)
281
+ return { status: "absent" };
282
+ if (registered) {
283
+ await successfulGit(input.cwd, ["worktree", "remove", "--force", input.scratch.files]);
284
+ }
285
+ await fs.rm(input.scratch.root, { recursive: true, force: true });
286
+ return { status: "deleted" };
287
+ }
215
288
  export function evidenceResult(state) {
216
289
  return {
217
290
  observedLedgerHead: state.observedLedgerHead,
291
+ lifecycleHead: state.lifecycleHead,
218
292
  deliveryRef: state.deliveryRef,
219
293
  targetRef: state.targetRef,
220
294
  effectiveScope: [...state.effectiveScope],
@@ -225,8 +299,9 @@ export function evidenceResult(state) {
225
299
  evidenceMap: { ...state.evidenceMap },
226
300
  };
227
301
  }
228
- async function persistConflict(cwd, scratchBase, state, create) {
302
+ export async function persistConflict(cwd, scratchBase, state, create) {
229
303
  const scratch = scratchPaths(scratchBase, state.scratchName);
304
+ let previous;
230
305
  if (create) {
231
306
  await fs.mkdir(path.dirname(scratch.root), { recursive: true });
232
307
  try {
@@ -239,8 +314,27 @@ async function persistConflict(cwd, scratchBase, state, create) {
239
314
  throw error;
240
315
  }
241
316
  }
242
- await materializeConflict(cwd, scratch, state.conflictTree);
243
- await writeScratchState(scratch, state);
317
+ else {
318
+ previous = await readScratchState(scratch);
319
+ }
320
+ try {
321
+ await materializeConflict(cwd, scratch, state, create);
322
+ await writeScratchState(scratch, state);
323
+ }
324
+ catch (error) {
325
+ if (create) {
326
+ await cleanupRenewScratch({ cwd, scratch });
327
+ }
328
+ else if (previous) {
329
+ try {
330
+ await materializeConflict(cwd, scratch, previous, false);
331
+ }
332
+ catch (restoreError) {
333
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${state.scratchName} could not restore its prior conflict state`, { cause: restoreError instanceof Error ? restoreError : undefined });
334
+ }
335
+ }
336
+ throw error;
337
+ }
244
338
  return {
245
339
  status: "conflict",
246
340
  ...evidenceResult(state),
@@ -270,6 +364,7 @@ export async function replayFrom(input) {
270
364
  conflictCommit: original.objectId,
271
365
  conflictTree: merged.tree,
272
366
  conflictPaths: merged.paths,
367
+ conflictIndex: merged.index,
273
368
  };
274
369
  return await persistConflict(input.cwd, input.scratchBase, state, input.createScratch);
275
370
  }
@@ -292,38 +387,54 @@ export async function buildDetachedRenewChain(input) {
292
387
  const coordinates = await deriveHostGitCoordinates(repositoryRoot);
293
388
  const commits = await validateCoordinates({ ...input, cwd: repositoryRoot, coordinates });
294
389
  const existingScratch = scratchPaths(scratchBase, input.scratchName);
295
- try {
296
- await fs.lstat(existingScratch.root);
297
- const state = await readScratchState(existingScratch);
298
- if (state.observedLedgerHead !== input.observedLedgerHead
299
- || state.deliveryRef !== input.deliveryRef
300
- || state.targetRef !== input.targetRef
301
- || state.oldBase !== input.oldBase
302
- || state.oldHead !== input.oldHead
303
- || state.newBase !== input.newBase
304
- || JSON.stringify(state.effectiveScope) !== JSON.stringify(input.effectiveScope)
305
- || JSON.stringify(state.deliveryCommits) !== JSON.stringify(input.deliveryCommits)) {
306
- throw new RenewBuildError("RENEW_SCRATCH_EXISTS", `renew scratch ${input.scratchName} belongs to different coordinates`);
390
+ const hasExistingScratch = await Promise.all([
391
+ scratchRootExists(existingScratch),
392
+ hasRegisteredScratchWorktree(repositoryRoot, existingScratch),
393
+ ]).then(([rootExists, registered]) => rootExists || registered);
394
+ if (hasExistingScratch) {
395
+ let state;
396
+ try {
397
+ state = await readScratchState(existingScratch);
398
+ }
399
+ catch (error) {
400
+ if (!(error instanceof RenewBuildError)
401
+ || (error.code !== "RENEW_SCRATCH_NOT_FOUND" && error.code !== "INVALID_RENEW_SCRATCH")) {
402
+ throw error;
403
+ }
404
+ await cleanupRenewScratch({ cwd: repositoryRoot, scratch: existingScratch });
405
+ }
406
+ if (state) {
407
+ if (state.observedLedgerHead !== input.observedLedgerHead
408
+ || state.lifecycleHead !== input.lifecycleHead
409
+ || state.deliveryRef !== input.deliveryRef
410
+ || state.targetRef !== input.targetRef
411
+ || state.oldBase !== input.oldBase
412
+ || state.oldHead !== input.oldHead
413
+ || state.newBase !== input.newBase
414
+ || JSON.stringify(state.effectiveScope) !== JSON.stringify(input.effectiveScope)
415
+ || JSON.stringify(state.deliveryCommits) !== JSON.stringify(input.deliveryCommits)) {
416
+ await cleanupRenewScratch({ cwd: repositoryRoot, scratch: existingScratch });
417
+ }
418
+ else {
419
+ await validateScratchProgress(repositoryRoot, coordinates, state, commits);
420
+ await validateScratchWorktree(repositoryRoot, existingScratch, state, { requireRecordedHead: true });
421
+ return {
422
+ status: "conflict",
423
+ ...evidenceResult(state),
424
+ scratchName: state.scratchName,
425
+ scratchPath: existingScratch.root,
426
+ filesPath: existingScratch.files,
427
+ conflictCommit: state.conflictCommit,
428
+ conflictPaths: [...state.conflictPaths],
429
+ };
430
+ }
307
431
  }
308
- await validateScratchProgress(repositoryRoot, coordinates, state, commits);
309
- return {
310
- status: "conflict",
311
- ...evidenceResult(state),
312
- scratchName: state.scratchName,
313
- scratchPath: existingScratch.root,
314
- filesPath: existingScratch.files,
315
- conflictCommit: state.conflictCommit,
316
- conflictPaths: [...state.conflictPaths],
317
- };
318
- }
319
- catch (error) {
320
- if (error.code !== "ENOENT")
321
- throw error;
322
432
  }
323
433
  const state = {
324
434
  version: SCRATCH_VERSION,
325
435
  scratchName: input.scratchName,
326
436
  observedLedgerHead: input.observedLedgerHead,
437
+ lifecycleHead: input.lifecycleHead,
327
438
  deliveryRef: input.deliveryRef,
328
439
  targetRef: input.targetRef,
329
440
  effectiveScope: [...input.effectiveScope],
@@ -337,6 +448,7 @@ export async function buildDetachedRenewChain(input) {
337
448
  conflictCommit: "",
338
449
  conflictTree: "",
339
450
  conflictPaths: [],
451
+ conflictIndex: [],
340
452
  };
341
453
  return await replayFrom({
342
454
  cwd: repositoryRoot,
@@ -366,6 +478,7 @@ export async function readScratchState(scratch) {
366
478
  || parsed.version !== SCRATCH_VERSION
367
479
  || parsed.scratchName !== path.basename(scratch.root)
368
480
  || typeof parsed.observedLedgerHead !== "string"
481
+ || typeof parsed.lifecycleHead !== "string"
369
482
  || typeof parsed.deliveryRef !== "string"
370
483
  || typeof parsed.targetRef !== "string"
371
484
  || !Array.isArray(parsed.effectiveScope)
@@ -382,30 +495,47 @@ export async function readScratchState(scratch) {
382
495
  || typeof parsed.conflictCommit !== "string"
383
496
  || typeof parsed.conflictTree !== "string"
384
497
  || !Array.isArray(parsed.conflictPaths)
385
- || parsed.conflictPaths.some((value) => typeof value !== "string")) {
498
+ || parsed.conflictPaths.some((value) => typeof value !== "string")
499
+ || !Array.isArray(parsed.conflictIndex)
500
+ || parsed.conflictIndex.some((entry) => !isRecord(entry)
501
+ || !/^\d{6}$/.test(String(entry.mode))
502
+ || !/^[0-9a-f]+$/.test(String(entry.objectId))
503
+ || ![1, 2, 3].includes(entry.stage)
504
+ || typeof entry.path !== "string")) {
386
505
  throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${path.basename(scratch.root)} has invalid state`);
387
506
  }
388
507
  return parsed;
389
508
  }
390
509
  export async function treeFromScratchFiles(cwd, coordinates, scratch, state) {
391
- const index = path.join(scratch.root, "continue.index");
392
- await fs.rm(index, { force: true });
393
- const env = { GIT_INDEX_FILE: index, GIT_WORK_TREE: scratch.files };
510
+ await validateScratchWorktree(cwd, scratch, state, { requireRecordedHead: true });
511
+ const indexPath = (await successfulGit(scratch.files, [
512
+ "rev-parse",
513
+ "--path-format=absolute",
514
+ "--git-path",
515
+ "index",
516
+ ])).trim();
517
+ const temporaryDirectory = await fs.mkdtemp(path.join(scratch.root, "tree-index-"));
518
+ const temporaryIndex = path.join(temporaryDirectory, "index");
394
519
  try {
395
- await successfulGit(cwd, ["read-tree", state.conflictTree], { env });
396
- await successfulGit(cwd, ["add", "--all", "--", "."], { env });
397
- const tree = (await successfulGit(cwd, ["write-tree"], { env })).trim();
520
+ // Candidate construction must not stage or otherwise alter the retryable scratch.
521
+ await fs.copyFile(indexPath, temporaryIndex);
522
+ const env = { ...process.env, GIT_INDEX_FILE: temporaryIndex };
523
+ await successfulGit(scratch.files, ["add", "--all", "--", "."], { env });
524
+ const tree = (await successfulGit(scratch.files, ["write-tree"], { env })).trim();
398
525
  await assertTree(cwd, coordinates, tree);
399
526
  return tree;
400
527
  }
401
528
  finally {
402
- await fs.rm(index, { force: true });
529
+ await fs.rm(temporaryDirectory, { recursive: true, force: true });
403
530
  }
404
531
  }
405
532
  export async function validateScratchProgress(cwd, coordinates, state, commits) {
406
533
  if (state.nextIndex < 0 || state.nextIndex >= commits.length || commits[state.nextIndex]?.objectId !== state.conflictCommit) {
407
534
  throw new RenewBuildError("INVALID_RENEW_SCRATCH", "renew scratch does not identify the stopped delivery commit");
408
535
  }
536
+ if (state.conflictPaths.length === 0 || state.conflictIndex.length === 0) {
537
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", "renew scratch has no captured conflict index");
538
+ }
409
539
  let parent = state.newBase;
410
540
  for (let index = 0; index < state.nextIndex; index += 1) {
411
541
  const original = commits[index].objectId;
@@ -2,6 +2,7 @@ import { FlowError } from "../flow-error.js";
2
2
  import { currentLifecycleState } from "./repository-ledger/read-model.js";
3
3
  import { buildDetachedRenewChain, continueDetachedRenewChain, renewScratchName } from "./renew-rewrite.js";
4
4
  import { hasStrandableProjectionGeneration } from "./projection/index.js";
5
+ import { readCommitCoordinate } from "../git/commits.js";
5
6
  import { runGitProcess } from "../git/core.js";
6
7
  import { assertCleanTrackedWorkingTree } from "../git/worktree.js";
7
8
  import { normalizeScopePattern } from "./scope.js";
@@ -28,7 +29,7 @@ async function deliveryChain(cwd, oldBase, oldHead) {
28
29
  export function scopeEntry(input) {
29
30
  const patterns = (input.appendScope ?? []).map(normalizeScopePattern);
30
31
  return patterns.length === 0 ? null : {
31
- v: 1,
32
+ v: 2,
32
33
  kind: "amend",
33
34
  contract: input.contractId,
34
35
  at: new Date(input.nowMs).toISOString(),
@@ -40,8 +41,8 @@ function effectiveScope(model, amendment) {
40
41
  return [...model.effectiveScope, ...(amendment?.data.scopeDelta?.add ?? [])];
41
42
  }
42
43
  async function gitTree(cwd, revision) {
43
- const result = await runGitProcess(cwd, ["rev-parse", "--verify", `${revision}^{tree}`]);
44
- return result.status === 0 && !result.error ? result.stdout.trim() : null;
44
+ const read = await readCommitCoordinate(cwd, revision);
45
+ return read.status === "ok" ? read.coordinate.tree : null;
45
46
  }
46
47
  async function acceptedRenewIsReplayable(targetRoot, entry) {
47
48
  const [index, oldTree, newTree, worktree] = await Promise.all([
@@ -103,11 +104,16 @@ export async function admitRenew(session, input, replay, expected) {
103
104
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} carrier pins disagree with the lifecycle read model`);
104
105
  }
105
106
  const amendment = scopeEntry(input);
107
+ const observedLedgerHead = session.repository.observedLedgerHead;
108
+ if (!observedLedgerHead)
109
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has no observed repository ledger head`);
110
+ const lifecycleHead = model.contractHead;
106
111
  const acceptedReplay = await acceptedRenewFromReplay(model, session.target.root, oldHead, newBase, replay);
107
112
  if (acceptedReplay) {
108
113
  return {
109
114
  model,
110
115
  repository: session.repository,
116
+ observedLedgerHead,
111
117
  targetRoot: session.target.root,
112
118
  oldBase: acceptedReplay.entry.data.oldBase,
113
119
  oldFence: acceptedReplay.seal.data.oldFence,
@@ -117,6 +123,7 @@ export async function admitRenew(session, input, replay, expected) {
117
123
  targetRef: model.pins.target.ref,
118
124
  effectiveScope: [...model.effectiveScope],
119
125
  amendment: null,
126
+ lifecycleHead,
120
127
  acceptedReplay,
121
128
  acceptedScopeReplay: null,
122
129
  };
@@ -126,6 +133,7 @@ export async function admitRenew(session, input, replay, expected) {
126
133
  return {
127
134
  model,
128
135
  repository: session.repository,
136
+ observedLedgerHead,
129
137
  targetRoot: session.target.root,
130
138
  oldBase: model.pins.base,
131
139
  oldFence: model.pins.delivery,
@@ -135,6 +143,7 @@ export async function admitRenew(session, input, replay, expected) {
135
143
  targetRef: model.pins.target.ref,
136
144
  effectiveScope: [...model.effectiveScope],
137
145
  amendment: null,
146
+ lifecycleHead,
138
147
  acceptedReplay: null,
139
148
  acceptedScopeReplay,
140
149
  };
@@ -147,6 +156,7 @@ export async function admitRenew(session, input, replay, expected) {
147
156
  const admission = {
148
157
  model,
149
158
  repository: session.repository,
159
+ observedLedgerHead,
150
160
  targetRoot: session.target.root,
151
161
  oldBase: model.pins.base,
152
162
  oldFence: model.pins.delivery,
@@ -156,16 +166,17 @@ export async function admitRenew(session, input, replay, expected) {
156
166
  targetRef: model.pins.target.ref,
157
167
  effectiveScope: scope,
158
168
  amendment,
169
+ lifecycleHead,
159
170
  acceptedReplay: null,
160
171
  acceptedScopeReplay: null,
161
172
  };
162
- if (expected && (expected.observedLedgerHead !== model.observedLedgerHead ||
173
+ if (expected && (expected.lifecycleHead !== lifecycleHead ||
163
174
  expected.deliveryRef !== admission.deliveryRef ||
164
175
  expected.targetRef !== admission.targetRef ||
165
176
  expected.oldBase !== admission.oldBase ||
166
177
  expected.oldHead !== admission.oldHead ||
167
- expected.newBase !== admission.newBase ||
168
- JSON.stringify(expected.effectiveScope) !== JSON.stringify(scope))) {
178
+ (JSON.stringify(expected.effectiveScope) !== JSON.stringify(model.effectiveScope)
179
+ && JSON.stringify(expected.effectiveScope) !== JSON.stringify(scope)))) {
169
180
  throw new FlowError("CONCURRENT_MODIFICATION", `renew scratch coordinates no longer match ${input.contractId}`);
170
181
  }
171
182
  return admission;
@@ -177,11 +188,23 @@ export async function prepareRenew(admission, input, continuation) {
177
188
  return { build: null, capture: null, prefix: admission.amendment ? [admission.amendment] : [], entry: null };
178
189
  }
179
190
  const build = continuation
180
- ? await continueDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName, resolution: continuation.resolution, signal: input.signal })
191
+ ? await continueDetachedRenewChain({
192
+ cwd: input.cwd,
193
+ scratchName: continuation.scratch.scratchName,
194
+ resolution: continuation.resolution,
195
+ refresh: {
196
+ latestTarget: admission.newBase,
197
+ observedLedgerHead: admission.observedLedgerHead,
198
+ effectiveScope: admission.effectiveScope,
199
+ lifecycleHead: admission.lifecycleHead,
200
+ },
201
+ signal: input.signal,
202
+ })
181
203
  : await buildDetachedRenewChain({
182
204
  cwd: input.cwd,
183
205
  scratchName: renewScratchName(input.contractId),
184
- observedLedgerHead: admission.model.observedLedgerHead,
206
+ observedLedgerHead: admission.observedLedgerHead,
207
+ lifecycleHead: admission.lifecycleHead,
185
208
  deliveryRef: admission.deliveryRef,
186
209
  targetRef: admission.targetRef,
187
210
  effectiveScope: admission.effectiveScope,
@@ -204,6 +227,7 @@ export async function prepareRenew(admission, input, continuation) {
204
227
  receiptHead: admission.oldHead,
205
228
  contentBase: admission.newBase,
206
229
  contentHead: build.newHead,
230
+ scopePolicy: "warn",
207
231
  });
208
232
  if (!capture)
209
233
  throw new FlowError("NO_OPEN_ARC", `contract ${input.contractId} has no active folded receipt segment to renew`);
@@ -228,11 +252,12 @@ export async function prepareRenew(admission, input, continuation) {
228
252
  return { build, capture, prefix, entry };
229
253
  }
230
254
  export function sameAdmission(current, pinned) {
231
- return current.model.observedLedgerHead === pinned.model.observedLedgerHead &&
255
+ return current.observedLedgerHead === pinned.observedLedgerHead &&
232
256
  current.oldBase === pinned.oldBase && current.oldFence === pinned.oldFence &&
233
257
  current.oldHead === pinned.oldHead && current.newBase === pinned.newBase &&
234
258
  current.deliveryRef === pinned.deliveryRef && current.targetRef === pinned.targetRef &&
235
259
  current.targetRoot === pinned.targetRoot &&
260
+ current.lifecycleHead === pinned.lifecycleHead &&
236
261
  JSON.stringify(current.effectiveScope) === JSON.stringify(pinned.effectiveScope) &&
237
262
  current.acceptedReplay?.renewObjectId === pinned.acceptedReplay?.renewObjectId &&
238
263
  current.acceptedScopeReplay?.transactionObjectId === pinned.acceptedScopeReplay?.transactionObjectId;
@@ -1,8 +1,8 @@
1
- import fs from "node:fs/promises";
2
1
  import { createHash } from "node:crypto";
2
+ import { requireCommitCoordinate } from "../git/commits.js";
3
3
  import { deriveHostGitCoordinates, readCommitObject } from "./repository-ledger/git-object-store.js";
4
4
  import { stableRepoRoot } from "./worktree-path.js";
5
- import { assertTree, buildCommit, evidenceResult, interrupted, readScratchState, renewScratchBase, replayFrom, scratchPaths, treeFromScratchFiles, validateCoordinates, validateScratchProgress, RenewBuildError } from "./renew-build.js";
5
+ import { assertTree, buildCommit, cleanupRenewScratch, evidenceResult, interrupted, persistConflict, readScratchState, renewScratchBase, replayFrom, replayResolvedDelivery, scratchPaths, treeFromScratchFiles, validateCoordinates, validateScratchProgress, RenewBuildError } from "./renew-build.js";
6
6
  export * from "./renew-build.js";
7
7
  export function renewScratchName(contractId) {
8
8
  return `renew-${createHash("sha256").update(contractId).digest("hex").slice(0, 24)}`;
@@ -11,6 +11,8 @@ export async function readDetachedRenewScratch(input) {
11
11
  const repositoryRoot = await stableRepoRoot(input.cwd);
12
12
  const scratch = scratchPaths(await renewScratchBase(repositoryRoot), input.scratchName);
13
13
  const state = await readScratchState(scratch);
14
+ const coordinates = await deriveHostGitCoordinates(repositoryRoot);
15
+ await assertTree(repositoryRoot, coordinates, state.conflictTree);
14
16
  return {
15
17
  status: "conflict",
16
18
  ...evidenceResult(state),
@@ -27,14 +29,17 @@ export async function continueDetachedRenewChain(input) {
27
29
  const scratch = scratchPaths(scratchBase, input.scratchName);
28
30
  const state = await readScratchState(scratch);
29
31
  const coordinates = await deriveHostGitCoordinates(repositoryRoot);
32
+ await assertTree(repositoryRoot, coordinates, state.conflictTree);
30
33
  const commits = await validateCoordinates({ ...state, cwd: repositoryRoot, coordinates });
31
34
  await validateScratchProgress(repositoryRoot, coordinates, state, commits);
32
35
  interrupted(input.signal);
33
36
  const original = commits[state.nextIndex];
34
37
  let resolvedHead;
38
+ let resolvedTree;
35
39
  if (input.resolution.kind === "tree") {
36
40
  await assertTree(repositoryRoot, coordinates, input.resolution.tree);
37
- resolvedHead = await buildCommit(repositoryRoot, coordinates, original, input.resolution.tree, state.currentHead);
41
+ resolvedTree = input.resolution.tree;
42
+ resolvedHead = await buildCommit(repositoryRoot, coordinates, original, resolvedTree, state.currentHead);
38
43
  }
39
44
  else if (input.resolution.kind === "commit") {
40
45
  const resolved = await readCommitObject(repositoryRoot, coordinates, input.resolution.commit);
@@ -42,10 +47,54 @@ export async function continueDetachedRenewChain(input) {
42
47
  throw new RenewBuildError("INVALID_RENEW_RESOLUTION", `resolved commit ${input.resolution.commit} must have exactly parent ${state.currentHead}`);
43
48
  }
44
49
  resolvedHead = input.resolution.commit;
50
+ resolvedTree = (await requireCommitCoordinate(repositoryRoot, resolvedHead)).tree;
51
+ await assertTree(repositoryRoot, coordinates, resolvedTree);
45
52
  }
46
53
  else {
47
- const tree = await treeFromScratchFiles(repositoryRoot, coordinates, scratch, state);
48
- resolvedHead = await buildCommit(repositoryRoot, coordinates, original, tree, state.currentHead);
54
+ resolvedTree = await treeFromScratchFiles(repositoryRoot, coordinates, scratch, state);
55
+ resolvedHead = await buildCommit(repositoryRoot, coordinates, original, resolvedTree, state.currentHead);
56
+ }
57
+ if (input.refresh) {
58
+ state.observedLedgerHead = input.refresh.observedLedgerHead;
59
+ state.effectiveScope = [...input.refresh.effectiveScope];
60
+ state.lifecycleHead = input.refresh.lifecycleHead;
61
+ if (input.refresh.latestTarget !== state.newBase) {
62
+ const resolvedPrefix = commits.slice(0, state.nextIndex + 1).map((commit, index) => ({
63
+ original: commit,
64
+ resolvedHead: index === state.nextIndex ? resolvedHead : state.evidenceMap[commit.objectId],
65
+ }));
66
+ state.newBase = input.refresh.latestTarget;
67
+ state.currentHead = input.refresh.latestTarget;
68
+ state.evidenceMap = {};
69
+ for (let index = 0; index < resolvedPrefix.length; index += 1) {
70
+ const resolved = resolvedPrefix[index];
71
+ const commit = await readCommitObject(repositoryRoot, coordinates, resolved.resolvedHead);
72
+ if (commit.parents.length !== 1) {
73
+ throw new RenewBuildError("INVALID_RENEW_SCRATCH", `resolved commit ${resolved.resolvedHead} must have exactly one parent`);
74
+ }
75
+ const tree = (await requireCommitCoordinate(repositoryRoot, resolved.resolvedHead)).tree;
76
+ const replay = await replayResolvedDelivery({
77
+ cwd: repositoryRoot,
78
+ coordinates,
79
+ capturedTarget: commit.parents[0],
80
+ latestTarget: state.currentHead,
81
+ resolvedTree: tree,
82
+ identity: resolved.original,
83
+ });
84
+ if (replay.status === "conflict") {
85
+ state.nextIndex = index;
86
+ state.conflictTree = replay.tree;
87
+ state.conflictPaths = [...replay.paths];
88
+ state.conflictIndex = [...replay.index];
89
+ state.conflictCommit = resolved.original.objectId;
90
+ return await persistConflict(repositoryRoot, scratchBase, state, false);
91
+ }
92
+ const refreshedHead = await buildCommit(repositoryRoot, coordinates, resolved.original, replay.tree, state.currentHead);
93
+ state.evidenceMap[resolved.original.objectId] = refreshedHead;
94
+ state.currentHead = refreshedHead;
95
+ }
96
+ resolvedHead = state.currentHead;
97
+ }
49
98
  }
50
99
  state.evidenceMap[original.objectId] = resolvedHead;
51
100
  state.currentHead = resolvedHead;
@@ -64,14 +113,6 @@ export async function continueDetachedRenewChain(input) {
64
113
  export async function abortDetachedRenewChain(input) {
65
114
  const repositoryRoot = await stableRepoRoot(input.cwd);
66
115
  const scratch = scratchPaths(await renewScratchBase(repositoryRoot), input.scratchName);
67
- try {
68
- await fs.lstat(scratch.root);
69
- }
70
- catch (error) {
71
- if (error.code === "ENOENT")
72
- return { status: "absent", scratchName: input.scratchName };
73
- throw error;
74
- }
75
- await fs.rm(scratch.root, { recursive: true, force: true });
76
- return { status: "deleted", scratchName: input.scratchName };
116
+ const cleanup = await cleanupRenewScratch({ cwd: repositoryRoot, scratch });
117
+ return { ...cleanup, scratchName: input.scratchName };
77
118
  }