@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,10 +1,10 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
2
  import { assertGitObservation, requireGitRepository } from "../../git/branches.js";
3
- import { withLockHeldTaskBoard } from "./task-git-store.js";
3
+ import { assertTaskBoardGeneration, withLockHeldTaskBoard, } from "./task-git-store.js";
4
4
  import { prepareTaskDocumentReplacements, taskBoardFromSourceBoard, taskRepositoryFromLockedBoard } from "./task-store-repository.js";
5
- import { serializeTaskDocument } from "./document.js";
6
5
  import { buildTaskSourceBoard } from "./source-board.js";
7
- import { composeLandingTree, containsCommit, createLandingCommit, createTaskBoardCommit, findAcceptedLandingCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSettlementOwnershipAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
6
+ import { formatTaskCoordinate } from "./coordinate.js";
7
+ import { createTaskBoardCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, } from "./settlement-git.js";
8
8
  import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
9
9
  import { stableRepoRoot } from "../worktree-path.js";
10
10
  /**
@@ -38,7 +38,7 @@ function ownedTaskPaths(board, taskIds) {
38
38
  for (const taskId of taskIds) {
39
39
  let current = board.byId.get(taskId);
40
40
  while (current) {
41
- paths.add(taskPath(current.id));
41
+ paths.add(taskPath(current.coordinate));
42
42
  current = current.parent === null ? undefined : board.byId.get(current.parent);
43
43
  }
44
44
  }
@@ -83,18 +83,18 @@ function repositoryForSettlement(transaction, sources) {
83
83
  async function clearAttemptedPointers(input) {
84
84
  return input.repository.mutate((board) => {
85
85
  const replacements = input.tasks.flatMap((task) => {
86
- const current = requireSettledTask(board, task.id);
86
+ const current = requireSettledTask(board, task.coordinate);
87
87
  return current.contractId === input.contractId
88
88
  ? [contractPointerDocument(current, undefined, input.updatedAt)]
89
89
  : [];
90
90
  });
91
- return { replacements, select: () => replacements.map((replacement) => replacement.id) };
91
+ return { replacements, select: () => replacements.map((replacement) => replacement.coordinate ?? replacement.id) };
92
92
  });
93
93
  }
94
94
  async function replaceTaskDocuments(repository, replacements) {
95
95
  return repository.mutate(() => ({
96
96
  replacements,
97
- select: (resulting) => replacements.map((replacement) => requireSettledTask(resulting, replacement.id)),
97
+ select: (resulting) => replacements.map((replacement) => requireSettledTask(resulting, replacement.coordinate ?? replacement.id)),
98
98
  }));
99
99
  }
100
100
  async function restorePreAdmissionTaskMutations(input) {
@@ -149,9 +149,9 @@ export async function settleTaskBoardForBind(input, deps = {}) {
149
149
  if (taskBound && selectedTasks.length > 0) {
150
150
  await replaceTaskDocuments(repository, selectedTasks.map((task) => contractPointerDocument(task, contractId, updatedAt)));
151
151
  }
152
- const taskIds = taskBound ? selectedTasks.map((t) => t.id) : undefined;
152
+ const taskIds = taskBound ? selectedTasks.map((t) => t.coordinate) : undefined;
153
153
  const selectedPaths = ownedTaskPaths(await repository.read(), taskIds);
154
- const mutationPaths = taskBound ? selectedTasks.map((t) => taskPath(t.id)) : [];
154
+ const mutationPaths = taskBound ? selectedTasks.map((t) => taskPath(t.coordinate)) : [];
155
155
  const selectCurrentSettlement = async () => selectSettlementSources({
156
156
  root: transaction.location.root,
157
157
  sources: repository.sources(),
@@ -166,7 +166,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
166
166
  try {
167
167
  const taskLabel = taskBound
168
168
  ? selectedTasks.length === 1
169
- ? `task ${selectedTasks[0].id}`
169
+ ? `task ${selectedTasks[0].coordinate}`
170
170
  : `${selectedTasks.length} tasks`
171
171
  : "";
172
172
  firstCommit = await makeBoardCommit({
@@ -182,7 +182,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
182
182
  }
183
183
  catch (error) {
184
184
  if (taskBound) {
185
- await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.id), previousSources });
185
+ await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.coordinate), previousSources });
186
186
  }
187
187
  throw error;
188
188
  }
@@ -192,7 +192,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
192
192
  catch (error) {
193
193
  if (taskBound) {
194
194
  const concurrentHead = await refHead(hubCwd, branch.ref);
195
- const concurrentMatches = await Promise.all(selectedTasks.map(async (task) => (await taskContractAtCommit(hubCwd, concurrentHead, task.id)) === contractId));
195
+ const concurrentMatches = await Promise.all(selectedTasks.map(async (task) => (await taskContractAtCommit(hubCwd, concurrentHead, task.coordinate)) === contractId));
196
196
  if (concurrentMatches.every(Boolean)) {
197
197
  await rollForwardClearAttemptedPointer({
198
198
  repository, selected: selectedTasks, contractId, hubCwd,
@@ -202,7 +202,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
202
202
  });
203
203
  }
204
204
  else {
205
- await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.id), previousSources });
205
+ await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.coordinate), previousSources });
206
206
  }
207
207
  }
208
208
  throw error;
@@ -228,7 +228,7 @@ export async function settleTaskBoardForBind(input, deps = {}) {
228
228
  boardCommit: firstCommit,
229
229
  contractId,
230
230
  ledger,
231
- ...(taskBound && selectedTasks.length > 0 ? { tasks: selectedTasks.map((t) => requireSettledTask(boardAfter, t.id)) } : {}),
231
+ ...(taskBound && selectedTasks.length > 0 ? { tasks: selectedTasks.map((t) => requireSettledTask(boardAfter, t.coordinate)) } : {}),
232
232
  evidence: selection.evidence,
233
233
  };
234
234
  }
@@ -256,207 +256,116 @@ export async function reconcileTaskForfeit(input) {
256
256
  }
257
257
  throw new FlowError("NOT_GIT_REPO", "task forfeit requires Git task storage");
258
258
  }
259
+ assertTaskBoardGeneration(transaction, input.expectedGeneration);
259
260
  const repository = taskRepositoryFromLockedBoard(transaction);
260
- const matched = tasksForContract(await repository.read(), input.contractId);
261
- const ledger = await input.publishLedger();
262
- if (matched.length === 0)
263
- return { ledger };
261
+ const branch = await inspectCurrentBranch(transaction.location.root);
262
+ const trackedPaths = await trackedTaskPaths(transaction.location.root, branch.head);
263
+ const branchSources = await taskSourcesAtCommit(transaction.location.root, transaction.location.root, branch.head, trackedPaths);
264
+ if (!branchSources)
265
+ throw new FlowError("INTERNAL_STATE", `task branch ${branch.head} lost tracked task sources`);
266
+ const branchBoard = requireHealthySettlementBoard(taskBoardFromSourceBoard(buildTaskSourceBoard(branchSources)));
267
+ const matched = tasksForContract(branchBoard, input.contractId);
268
+ const worktreeMatched = tasksForContract(await repository.read(), input.contractId);
269
+ if (matched.length === 0 && worktreeMatched.length === 0) {
270
+ input.observeEffect?.({ effect: "task-source-reconciliation", status: "succeeded", taskCount: 0, detail: "current" });
271
+ input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit: branch.head, detail: "current" });
272
+ input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit: branch.head });
273
+ return { ledger: await input.publishLedger() };
274
+ }
264
275
  const reopenedDocuments = matched.map((task) => reopenedTaskDocument(task, updatedAt));
265
- const reopened = await replaceTaskDocuments(repository, reopenedDocuments);
266
- const taskLabel = matched.length === 1 ? `task ${matched[0].id}` : `${matched.length} tasks`;
276
+ const prepared = prepareTaskDocumentReplacements(branchSources, reopenedDocuments);
277
+ const projectionIds = matched.length > 0 ? matched.map((task) => task.coordinate) : worktreeMatched.map((task) => task.coordinate);
278
+ const branchSourcesByPath = new Map(branchSources.map((source) => [sourcePath(transaction.location.root, source), source]));
279
+ const writes = matched.length > 0
280
+ ? prepared.writes
281
+ : taskWritesFromSources(projectionIds.map((id) => {
282
+ const source = branchSourcesByPath.get(taskPath(id));
283
+ if (!source)
284
+ throw new FlowError("INTERNAL_STATE", `task ${id} is missing from authoritative task branch ${branch.head}`);
285
+ return source;
286
+ }));
287
+ const reopened = projectionIds.map((id) => requireSettledTask(prepared.board, id));
288
+ const taskLabel = projectionIds.length === 1 ? `task ${projectionIds[0]}` : `${projectionIds.length} tasks`;
289
+ let indexSucceeded = false;
267
290
  await publishTaskMutationCommit({
268
291
  cwd: transaction.location.root,
269
292
  gitDir: transaction.location.gitDir,
270
- writes: reopenedDocuments.map((document) => ({ id: document.id, text: serializeTaskDocument(document) })),
293
+ writes,
271
294
  subject: `Reopen ${taskLabel} after forfeit of ${input.contractId}`,
295
+ observeEffect: (effect) => {
296
+ input.observeEffect?.(effect);
297
+ if (effect.effect === "task-index-reconciliation" && effect.status === "succeeded")
298
+ indexSucceeded = true;
299
+ },
272
300
  });
301
+ if (!indexSucceeded)
302
+ throw new FlowError("INTERNAL_STATE", "task index reconciliation failed before forfeit ledger publication");
303
+ try {
304
+ await transaction.replace(writes);
305
+ input.observeEffect?.({ effect: "task-source-reconciliation", status: "succeeded", taskCount: reopened.length, detail: "materialized" });
306
+ }
307
+ catch (error) {
308
+ input.observeEffect?.({ effect: "task-source-reconciliation", status: "failed", error });
309
+ throw error;
310
+ }
311
+ const ledger = await input.publishLedger();
273
312
  return { ledger, tasks: reopened };
274
313
  });
275
314
  }
276
- /**
277
- * Claim owns the sole task-board settlement after target CAS. Contract branch
278
- * task trees are fossils: the candidate is the code-only three-way merge plus
279
- * one complete snapshot of the common hub board held under its repository lock.
280
- */
281
- export async function settleTaskBoardForLanding(input, deps = {}) {
282
- const updatedAt = new Date().toISOString();
283
- await requireGitRepository(input.cwd, "task landing");
315
+ /** Replay the claim's folded task selection after target+ledger authority commits. */
316
+ export async function replayClaimTasks(input) {
317
+ await requireGitRepository(input.cwd, "task claim replay");
284
318
  const hubRoot = await stableRepoRoot(input.cwd);
285
- const ownership = await taskSettlementOwnershipAtCommit(input.cwd, input.baseCommit, input.contractId);
286
- const reconcileIndex = deps.reconcileTaskPathsetIndex ?? reconcileTaskPathsetIndex;
287
- let lastTargetHead = input.targetHead;
288
- let lastPostCasError;
289
- let acceptedMovement;
290
- for (let attempt = 0; attempt < 4; attempt += 1) {
291
- const result = await withLockHeldTaskBoard(hubRoot, async (transaction) => {
292
- if (transaction.location.mode !== "git") {
293
- if (transaction.location.capability === "unavailable") {
294
- assertGitObservation({ capability: "unavailable" }, "task landing");
295
- }
296
- throw new FlowError("NOT_GIT_REPO", "task landing requires Git task storage");
319
+ return await withLockHeldTaskBoard(hubRoot, async (transaction) => {
320
+ if (transaction.location.mode !== "git") {
321
+ if (transaction.location.capability === "unavailable") {
322
+ assertGitObservation({ capability: "unavailable" }, "task claim replay");
297
323
  }
298
- // Each attempt reads fresh target and board facts while holding the
299
- // common task lock. A losing CAS exits before the next attempt.
324
+ throw new FlowError("NOT_GIT_REPO", "task claim replay requires Git task storage");
325
+ }
326
+ const hubBranch = await inspectCurrentBranch(transaction.location.root);
327
+ const materializeHub = hubBranch.ref === input.targetRef;
328
+ const selectedPaths = input.taskIds.map(taskPath);
329
+ for (let attempt = 0; attempt < 4; attempt += 1) {
300
330
  const targetHead = await refHead(input.cwd, input.targetRef);
301
- lastTargetHead = targetHead;
302
- const selectedPaths = ownership.paths;
303
- const acceptedLanding = acceptedMovement?.mergeCommit === targetHead
304
- ? targetHead
305
- : await findAcceptedLandingCommit({
306
- cwd: input.cwd,
307
- gitDir: transaction.location.gitDir,
308
- root: transaction.location.root,
309
- base: input.baseCommit,
310
- target: targetHead,
311
- delivery: input.deliveryHead,
312
- contractId: input.contractId,
313
- targetBranch: input.targetBranch,
314
- ownedPaths: selectedPaths,
315
- settledPaths: ownership.taskIds.map(taskPath),
316
- });
317
- const deliveryAlreadyAccepted = ownership.taskIds.length === 0
318
- && await containsCommit(input.cwd, input.deliveryHead, targetHead);
319
- if (acceptedLanding !== undefined || deliveryAlreadyAccepted) {
320
- const mergeCommit = acceptedLanding ?? targetHead;
321
- const movement = acceptedMovement?.mergeCommit === targetHead ? acceptedMovement : undefined;
322
- const authoritativeSources = selectedPaths.length > 0
323
- ? await taskSourcesAtCommit(input.cwd, transaction.location.root, targetHead, selectedPaths)
324
- : [];
325
- if (!authoritativeSources) {
326
- throw new FlowError("INTERNAL_STATE", `accepted landing ${mergeCommit} lost an owned task path`);
331
+ const paths = await trackedTaskPaths(input.cwd, targetHead);
332
+ const sources = await taskSourcesAtCommit(input.cwd, transaction.location.root, targetHead, paths);
333
+ if (!sources)
334
+ throw new FlowError("INTERNAL_STATE", `claim target ${targetHead} lost tracked task sources`);
335
+ const board = requireHealthySettlementBoard(taskBoardFromSourceBoard(buildTaskSourceBoard(sources)));
336
+ const replacements = doneTaskReplacements(board, input.contractId, input.taskIds, input.updatedAt);
337
+ if (replacements.length === 0) {
338
+ const selected = new Set(selectedPaths);
339
+ const writes = taskWritesFromSources(sources.filter((source) => selected.has(sourcePath(transaction.location.root, source))));
340
+ if (materializeHub) {
341
+ if (writes.length > 0)
342
+ await transaction.replace(writes);
343
+ await reconcileTaskPathsetIndex(transaction.location.root, targetHead, selectedPaths);
327
344
  }
328
- const selection = selectSettlementSources({
329
- root: transaction.location.root,
330
- sources: authoritativeSources,
331
- trackedPaths: await trackedTaskPaths(input.cwd, targetHead),
332
- ownedPaths: selectedPaths,
333
- mutationPaths: selectedPaths,
334
- });
335
- const currentText = new Map(transaction.sources.map((source) => [sourcePath(transaction.location.root, source), source.text]));
336
- const writes = authoritativeSources.filter((source) => currentText.get(sourcePath(transaction.location.root, source)) !== source.text);
337
- if (writes.length > 0)
338
- await transaction.replace(taskWritesFromSources(writes));
339
- try {
340
- await reconcileIndex(transaction.location.root, targetHead, selectedPaths);
341
- }
342
- catch (error) {
343
- lastPostCasError = error;
344
- return { kind: "retry" };
345
- }
346
- try {
347
- await input.publishLedger?.(mergeCommit);
348
- }
349
- catch (error) {
350
- lastPostCasError = error;
351
- return { kind: "retry" };
352
- }
353
- const reconcileWarnings = movement
354
- ? await input.afterCas?.(mergeCommit, movement.oldHead)
355
- : undefined;
356
- return {
357
- kind: movement ? "accepted" : "idempotent",
358
- mergeCommit,
359
- ...(movement ? { oldHead: movement.oldHead } : {}),
360
- ...(reconcileWarnings && reconcileWarnings.length > 0 ? { warnings: reconcileWarnings } : {}),
361
- evidence: selection.evidence,
362
- };
363
- }
364
- // No durable receipt exists, so this remains a first-publication attempt:
365
- // validate mutable hub authority before composing or publishing anything.
366
- const repository = repositoryForSettlement(transaction, transaction.sources);
367
- const board = requireHealthySettlementBoard(await repository.read());
368
- const replacements = doneTaskReplacements(board, input.contractId, ownership.taskIds, updatedAt);
369
- const prepared = replacements.length > 0
370
- ? prepareTaskDocumentReplacements(transaction.sources, replacements)
371
- : { sources: transaction.sources, board };
372
- const finalSources = prepared.sources;
373
- requireHealthySettlementBoard(prepared.board);
374
- const selection = selectSettlementSources({
375
- root: transaction.location.root,
376
- sources: finalSources,
377
- trackedPaths: await trackedTaskPaths(input.cwd, targetHead),
378
- ownedPaths: selectedPaths,
379
- // Bind-base ownership, unlike incidental hub bytes, authorizes a path
380
- // even when a concurrent target commit removed it.
381
- mutationPaths: selectedPaths,
382
- });
383
- const selected = new Set(selection.evidence.selectedPaths);
384
- const missingOwnedPaths = selectedPaths.filter((ownedPath) => !selected.has(ownedPath));
385
- if (missingOwnedPaths.length > 0) {
386
- throw new FlowError("INTERNAL_STATE", `hub board lost bind-base-owned task paths: ${missingOwnedPaths.join(", ")}`);
345
+ return { taskCount: input.taskIds.length, commit: targetHead, worktree: materializeHub ? "materialized" : "ref-only" };
387
346
  }
388
- const candidateTree = await composeLandingTree({
347
+ const prepared = prepareTaskDocumentReplacements(sources, replacements);
348
+ const commit = await createTaskBoardCommit({
389
349
  cwd: input.cwd,
390
350
  gitDir: transaction.location.gitDir,
391
351
  root: transaction.location.root,
392
- base: input.baseCommit,
393
- target: targetHead,
394
- delivery: input.deliveryHead,
395
- sources: selection.sources,
352
+ parent: targetHead,
353
+ sources: prepared.sources,
354
+ subject: `Complete ${replacements.length === 1 ? `task ${replacements[0].coordinate ?? formatTaskCoordinate(replacements[0].namespace ?? null, replacements[0].id)}` : `${replacements.length} tasks`} after claim of ${input.contractId}`,
396
355
  });
397
- const mergeCommit = await createLandingCommit({
398
- cwd: input.cwd,
399
- target: targetHead,
400
- delivery: input.deliveryHead,
401
- tree: candidateTree,
402
- contractId: input.contractId,
403
- targetBranch: input.targetBranch,
404
- });
405
- await input.beforeCas?.(mergeCommit, targetHead);
406
- let movement;
407
- try {
408
- await updateTargetRef(input.cwd, input.targetRef, mergeCommit, targetHead);
409
- movement = { mergeCommit, oldHead: targetHead };
410
- acceptedMovement = movement;
411
- }
412
- catch {
413
- return { kind: "retry" };
414
- }
415
- const warnings = [];
416
- if (replacements.length > 0) {
417
- await transaction.replace(taskWritesFromSources(selection.sources));
418
- }
419
- try {
420
- await reconcileIndex(transaction.location.root, mergeCommit, selectedPaths);
421
- }
422
- catch (error) {
423
- lastPostCasError = error;
424
- return { kind: "retry" };
425
- }
426
356
  try {
427
- await input.publishLedger?.(mergeCommit);
357
+ await updateBranch({ cwd: input.cwd, ref: input.targetRef, next: commit, expected: targetHead }, {});
358
+ if (materializeHub) {
359
+ await transaction.replace(prepared.writes);
360
+ await reconcileTaskPathsetIndex(transaction.location.root, commit, selectedPaths);
361
+ }
362
+ return { taskCount: input.taskIds.length, commit, worktree: materializeHub ? "materialized" : "ref-only" };
428
363
  }
429
364
  catch (error) {
430
- lastPostCasError = error;
431
- return { kind: "retry" };
365
+ if (attempt === 3)
366
+ throw error;
432
367
  }
433
- const reconcileWarnings = await input.afterCas?.(mergeCommit, movement.oldHead);
434
- warnings.push(...(reconcileWarnings ?? []));
435
- return {
436
- kind: "accepted",
437
- mergeCommit,
438
- oldHead: movement.oldHead,
439
- ...(warnings.length > 0 ? { warnings } : {}),
440
- evidence: selection.evidence,
441
- };
442
- });
443
- if (result.kind === "retry")
444
- continue;
445
- if (result.kind === "idempotent") {
446
- return {
447
- mergeCommit: result.mergeCommit,
448
- ...(result.warnings ? { warnings: result.warnings } : {}),
449
- ...(result.evidence ? { evidence: result.evidence } : {}),
450
- };
451
368
  }
452
- return {
453
- mergeCommit: result.mergeCommit,
454
- oldHead: result.oldHead,
455
- ...(result.warnings ? { warnings: result.warnings } : {}),
456
- ...(result.evidence ? { evidence: result.evidence } : {}),
457
- };
458
- }
459
- if (lastPostCasError !== undefined)
460
- throw lastPostCasError;
461
- throw new FlowError("CONCURRENT_MODIFICATION", `target branch kept moving while claiming ${input.contractId}; last observed ${lastTargetHead}`);
369
+ throw new FlowError("CONCURRENT_MODIFICATION", `claim target ${input.targetRef} kept moving during task replay`);
370
+ });
462
371
  }
@@ -0,0 +1,171 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { FlowError } from "../../flow-error.js";
5
+ import { syncDirectorySync } from "../../fs/durability.js";
6
+ import { isTaskCoordinate } from "./coordinate.js";
7
+ export function atomicReplaceTaskFile(filePath, text, _deps) {
8
+ const directory = path.dirname(filePath);
9
+ fs.mkdirSync(directory, { recursive: true });
10
+ const temporary = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`);
11
+ let descriptor;
12
+ try {
13
+ descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
14
+ fs.writeFileSync(descriptor, text, { encoding: "utf8" });
15
+ fs.fsyncSync(descriptor);
16
+ fs.closeSync(descriptor);
17
+ descriptor = undefined;
18
+ fs.renameSync(temporary, filePath);
19
+ syncDirectorySync(directory);
20
+ }
21
+ catch (error) {
22
+ if (descriptor !== undefined) {
23
+ try {
24
+ fs.closeSync(descriptor);
25
+ }
26
+ catch { /* preserve original failure */ }
27
+ }
28
+ try {
29
+ fs.unlinkSync(temporary);
30
+ }
31
+ catch { /* absent after rename or already cleaned */ }
32
+ throw error;
33
+ }
34
+ }
35
+ function lstatIfPresent(target) {
36
+ try {
37
+ return fs.lstatSync(target);
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return undefined;
42
+ throw error;
43
+ }
44
+ }
45
+ export function validateTaskMutationWrites(writes) {
46
+ if (writes.length === 0) {
47
+ throw new FlowError("INTERNAL_STATE", "task mutation must contain at least one write");
48
+ }
49
+ const targets = new Set();
50
+ for (const write of writes) {
51
+ const target = `${write.id}.md`;
52
+ if (!isTaskCoordinate(write.id)) {
53
+ throw new FlowError("INTERNAL_STATE", `invalid task mutation coordinate: ${write.id}`);
54
+ }
55
+ if (targets.has(target))
56
+ throw new FlowError("INTERNAL_STATE", `duplicate task mutation target: ${target}`);
57
+ targets.add(target);
58
+ }
59
+ }
60
+ function mutationTarget(write) {
61
+ return `${write.id}.md`;
62
+ }
63
+ function capturePreviousFiles(directory, writes) {
64
+ return writes.map((write) => {
65
+ const destination = path.join(directory, mutationTarget(write));
66
+ const stat = lstatIfPresent(destination);
67
+ if (!stat)
68
+ return { path: destination };
69
+ if (stat.isSymbolicLink() || !stat.isFile()) {
70
+ throw new FlowError("INTERNAL_STATE", `task destination is not a regular file: ${destination}`);
71
+ }
72
+ return { path: destination, text: fs.readFileSync(destination, "utf8") };
73
+ });
74
+ }
75
+ export function sameSources(left, right) {
76
+ return left.length === right.length && left.every((source, index) => {
77
+ const candidate = right[index];
78
+ return candidate !== undefined && source.path === candidate.path && source.text === candidate.text;
79
+ });
80
+ }
81
+ export function generationFor(location, sources) {
82
+ const digest = createHash("sha256").update("keiyaku-task-board-v1\0");
83
+ for (const source of [...sources].sort((left, right) => Buffer.from(left.path).compare(Buffer.from(right.path)))) {
84
+ const relativePath = path.relative(location.mode === "git" ? location.surfaceRoot : location.root, source.path).split(path.sep).join("/");
85
+ digest.update(String(Buffer.byteLength(relativePath))).update(":").update(relativePath);
86
+ digest.update(String(Buffer.byteLength(source.text))).update(":").update(source.text);
87
+ }
88
+ return { root: location.root, fingerprint: digest.digest("hex") };
89
+ }
90
+ export function taskBoardGeneration(transaction) {
91
+ return generationFor(transaction.location, transaction.sources);
92
+ }
93
+ export function assertTaskBoardGeneration(transaction, expected) {
94
+ const current = taskBoardGeneration(transaction);
95
+ if (current.root === expected.root && current.fingerprint === expected.fingerprint)
96
+ return;
97
+ throw new FlowError("CONCURRENT_MODIFICATION", "task board generation changed during lifecycle preparation", {
98
+ facts: { kind: "task_board_generation_changed", expected, current },
99
+ });
100
+ }
101
+ function removeTaskFile(filePath) {
102
+ try {
103
+ fs.unlinkSync(filePath);
104
+ syncDirectorySync(path.dirname(filePath));
105
+ }
106
+ catch (error) {
107
+ if (error.code !== "ENOENT")
108
+ throw error;
109
+ }
110
+ }
111
+ function restoreFiles(previous, deps) {
112
+ for (const entry of previous) {
113
+ if (entry.text === undefined)
114
+ removeTaskFile(entry.path);
115
+ else
116
+ (deps.atomicReplace ?? atomicReplaceTaskFile)(entry.path, entry.text);
117
+ }
118
+ }
119
+ function writeTaskFile(directory, write, deps) {
120
+ const destination = path.join(directory, mutationTarget(write));
121
+ (deps.atomicReplace ?? atomicReplaceTaskFile)(destination, write.text);
122
+ }
123
+ function assertTaskDirectorySafety(directory) {
124
+ const stat = lstatIfPresent(directory);
125
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
126
+ throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
127
+ }
128
+ }
129
+ export function createTaskFileTransaction(input) {
130
+ return {
131
+ location: input.location,
132
+ get sources() { return input.getSources(); },
133
+ async replace(writes) {
134
+ validateTaskMutationWrites(writes);
135
+ input.assertBoardSafety();
136
+ assertTaskDirectorySafety(input.directory);
137
+ const currentSources = await input.readSources();
138
+ if (!sameSources(input.getSources(), currentSources)) {
139
+ throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while applying the mutation");
140
+ }
141
+ input.assertBoardSafety();
142
+ assertTaskDirectorySafety(input.directory);
143
+ const previous = capturePreviousFiles(input.directory, writes);
144
+ let applied = 0;
145
+ try {
146
+ for (const write of writes) {
147
+ writeTaskFile(input.directory, write, input.deps);
148
+ applied += 1;
149
+ }
150
+ }
151
+ catch (error) {
152
+ try {
153
+ restoreFiles(previous.slice(0, applied + 1), input.deps);
154
+ }
155
+ catch { /* the original replacement failure remains authoritative */ }
156
+ throw error;
157
+ }
158
+ input.setSources(await input.readSources());
159
+ },
160
+ async restore(writes, previousSources) {
161
+ validateTaskMutationWrites(writes);
162
+ const currentSources = await input.readSources();
163
+ if (!sameSources(input.getSources(), currentSources)) {
164
+ throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while rolling back a failed publication");
165
+ }
166
+ const previousById = new Map(previousSources.map((source) => [path.relative(input.directory, source.path).slice(0, -3).split(path.sep).join("/"), source.text]));
167
+ restoreFiles(writes.map((write) => ({ path: path.join(input.directory, mutationTarget(write)), text: previousById.get(write.id) })), input.deps);
168
+ input.setSources(await input.readSources());
169
+ },
170
+ };
171
+ }