@astrosheep/keiyaku 2.9.9 → 2.9.11

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 +93 -174
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +2 -1
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +6 -1
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +3 -2
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +9 -16
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +21 -358
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +114 -119
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -0,0 +1,193 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { runGitProcess } from "../../git/core.js";
3
+ import { assertGitObservation, requireGitRepository } from "../../git/branches.js";
4
+ import { inspectCurrentBranch, createTaskBoardCommit, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskSourcesAtCommit, taskWritesFromSources, } from "./settlement-git.js";
5
+ import { taskCoordinateFromSourcePath } from "./coordinate.js";
6
+ import { advancedTaskUpdatedAt } from "./model.js";
7
+ import { requireHealthySettlementBoard, selectReadyTasksForBind } from "./settlement-policy.js";
8
+ import { buildTaskSourceBoard } from "./source-board.js";
9
+ import { prepareTaskDocumentReplacements, taskBoardFromSourceBoard } from "./task-store-repository.js";
10
+ import { withLockHeldTaskBoard, } from "./task-git-store.js";
11
+ function linkedTaskDocument(task, contractId, at) {
12
+ return {
13
+ id: task.id,
14
+ namespace: task.namespace,
15
+ coordinate: task.coordinate,
16
+ title: task.title,
17
+ state: "open",
18
+ pri: task.pri,
19
+ needs: [...task.needs],
20
+ parent: task.parent,
21
+ from: [...task.from],
22
+ supersedes: [...task.supersedes],
23
+ relatesTo: [...task.relatesTo],
24
+ createdAt: task.createdAt,
25
+ updatedAt: advancedTaskUpdatedAt(task, at),
26
+ creator: task.creator,
27
+ contractId,
28
+ body: task.body,
29
+ };
30
+ }
31
+ function selectedEvidence(board, selected) {
32
+ return selected.map((task) => ({
33
+ taskId: task.coordinate,
34
+ needs: task.needs.map((taskId) => {
35
+ const state = board.byId.get(taskId)?.state;
36
+ if (state !== "done" && state !== "drop") {
37
+ throw new FlowError("INTERNAL_STATE", `ready task ${task.coordinate} has unsatisfied need ${taskId}`);
38
+ }
39
+ return { taskId, state };
40
+ }),
41
+ }));
42
+ }
43
+ async function prepareLockedTaskBind(transaction, input, deps) {
44
+ if (transaction.location.mode !== "git") {
45
+ if (transaction.location.capability === "unavailable") {
46
+ assertGitObservation({ capability: "unavailable" }, "task bind");
47
+ }
48
+ throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
49
+ }
50
+ const board = requireHealthySettlementBoard(taskBoardFromSourceBoard(buildTaskSourceBoard(transaction.sources)));
51
+ const selected = selectReadyTasksForBind(board, input.taskIds);
52
+ if (selected.length === 0) {
53
+ throw new FlowError("INVALID_TARGET", "task bind requires at least one selected task");
54
+ }
55
+ const attempt = await input.prepare(selected);
56
+ const evidence = selectedEvidence(board, selected);
57
+ const linked = prepareTaskDocumentReplacements(transaction.sources, selected.map((task) => linkedTaskDocument(task, attempt.contractId, attempt.at)));
58
+ const linkedIds = new Set(selected.map((task) => task.coordinate));
59
+ const linkedSources = linked.sources.filter((source) => linkedIds.has(taskCoordinateFromSourcePath(source.path).coordinate));
60
+ const originalSources = transaction.sources.filter((source) => linkedIds.has(taskCoordinateFromSourcePath(source.path).coordinate));
61
+ if (linkedSources.length !== selected.length) {
62
+ throw new FlowError("INTERNAL_STATE", "task bind preparation did not resolve the exact selected task pathset");
63
+ }
64
+ if (originalSources.length !== selected.length) {
65
+ throw new FlowError("INTERNAL_STATE", "task bind preparation did not retain the exact original task pathset");
66
+ }
67
+ const branch = await inspectCurrentBranch(transaction.location.root);
68
+ const newHead = await (deps.createTaskBoardCommit ?? createTaskBoardCommit)({
69
+ cwd: transaction.location.root,
70
+ gitDir: transaction.location.gitDir,
71
+ root: transaction.location.root,
72
+ parent: branch.head,
73
+ sources: linkedSources,
74
+ subject: selected.length === 1
75
+ ? `Bind task ${selected[0].coordinate} to contract ${attempt.contractId}`
76
+ : `Bind ${selected.length} tasks to contract ${attempt.contractId}`,
77
+ });
78
+ const taskBoard = { ref: branch.ref, expectedHead: branch.head, head: newHead };
79
+ return {
80
+ boardRef: taskBoard.ref,
81
+ expectedHead: taskBoard.expectedHead,
82
+ newHead,
83
+ entryBase: newHead,
84
+ taskIds: selected.map((task) => task.coordinate),
85
+ selected: evidence,
86
+ taskBoard,
87
+ replay: {
88
+ paths: selected.map((task) => `.keiyaku/tasks/${task.coordinate}.md`).sort(),
89
+ linkedSources,
90
+ originalSources,
91
+ },
92
+ };
93
+ }
94
+ /** Hold the repository task lock across build and the caller-owned ref transaction. */
95
+ export async function withPreparedTaskBind(input, deps = {}) {
96
+ await requireGitRepository(input.cwd, "task bind");
97
+ return await withLockHeldTaskBoard(input.cwd, async (transaction) => {
98
+ const preparation = await prepareLockedTaskBind(transaction, input, deps);
99
+ const result = await input.transact(preparation, transaction);
100
+ return { preparation, result };
101
+ }, { signal: input.signal });
102
+ }
103
+ /** Complete the prepared task projection while its authority transaction still owns the task lock. */
104
+ export async function replayPreparedTaskBindLocked(transaction, prepared) {
105
+ if (transaction.location.mode !== "git")
106
+ throw new FlowError("NOT_GIT_REPO", "task bind replay requires Git task storage");
107
+ const currentHead = await refHead(transaction.location.root, prepared.boardRef);
108
+ const sources = await taskSourcesAtCommit(transaction.location.root, transaction.location.root, currentHead, prepared.replay.paths);
109
+ if (!sources)
110
+ throw new FlowError("INTERNAL_STATE", "committed task bind paths are missing from board authority");
111
+ await transaction.replace(taskWritesFromSources(sources));
112
+ await reconcileTaskPathsetIndex(transaction.location.root, currentHead, prepared.replay.paths);
113
+ }
114
+ export async function replayPreparedTaskBind(cwd, prepared) {
115
+ await withLockHeldTaskBoard(cwd, async (transaction) => {
116
+ await replayPreparedTaskBindLocked(transaction, prepared);
117
+ });
118
+ }
119
+ async function projectionMatchesCommit(cwd, commit, paths) {
120
+ const cached = await runGitProcess(cwd, ["diff", "--quiet", "--cached", commit, "--", ...paths]);
121
+ if (cached.error || (cached.status !== 0 && cached.status !== 1))
122
+ return false;
123
+ if (cached.status !== 0)
124
+ return false;
125
+ const worktree = await runGitProcess(cwd, ["diff", "--quiet", "--", ...paths]);
126
+ return !worktree.error && worktree.status === 0;
127
+ }
128
+ /** Rebuild the selected task working-tree projection from the authoritative bind base. */
129
+ export async function replayCommittedTaskBind(cwd, commit, taskIds) {
130
+ const paths = taskIds.map((taskId) => `.keiyaku/tasks/${taskId}.md`).sort();
131
+ if (await projectionMatchesCommit(cwd, commit, paths))
132
+ return "already-present";
133
+ return await withLockHeldTaskBoard(cwd, async (transaction) => {
134
+ if (transaction.location.mode !== "git")
135
+ throw new FlowError("NOT_GIT_REPO", "task bind replay requires Git task storage");
136
+ const sources = await taskSourcesAtCommit(transaction.location.root, transaction.location.root, commit, paths);
137
+ if (!sources)
138
+ throw new FlowError("INTERNAL_STATE", "committed task bind paths are missing from board authority");
139
+ const current = new Map(transaction.sources.map((source) => [source.path, source.text]));
140
+ const alreadyPresent = sources.every((source) => current.get(source.path) === source.text);
141
+ if (!alreadyPresent)
142
+ await transaction.replace(taskWritesFromSources(sources));
143
+ await reconcileTaskPathsetIndex(transaction.location.root, commit, paths);
144
+ return alreadyPresent ? "already-present" : "created";
145
+ });
146
+ }
147
+ /** Reopen task projections from the authoritative parent of a task-selected bind base. */
148
+ export async function replayCommittedTaskBindForfeit(cwd, commit, boardRef, taskIds) {
149
+ return await withLockHeldTaskBoard(cwd, async (transaction) => {
150
+ if (transaction.location.mode !== "git")
151
+ throw new FlowError("NOT_GIT_REPO", "task forfeit replay requires Git task storage");
152
+ const parentResult = await runGitProcess(transaction.location.root, ["rev-parse", "--verify", `${commit}^`]);
153
+ if (parentResult.status !== 0 || parentResult.error) {
154
+ throw new FlowError("INTERNAL_STATE", `committed task bind base ${commit} has no readable parent`, { cause: parentResult.error });
155
+ }
156
+ const parent = parentResult.stdout.trim();
157
+ const paths = taskIds.map((taskId) => `.keiyaku/tasks/${taskId}.md`).sort();
158
+ const originalSources = await taskSourcesAtCommit(transaction.location.root, transaction.location.root, parent, paths);
159
+ if (!originalSources)
160
+ throw new FlowError("INTERNAL_STATE", "committed task bind parent is missing selected task paths");
161
+ const current = new Map(transaction.sources.map((source) => [source.path, source.text]));
162
+ const alreadyPresent = originalSources.every((source) => current.get(source.path) === source.text);
163
+ if (alreadyPresent)
164
+ return "already-present";
165
+ await transaction.replace(taskWritesFromSources(originalSources));
166
+ await publishForwardTaskBoardCommit({
167
+ cwd: transaction.location.root,
168
+ ref: boardRef,
169
+ gitDir: transaction.location.gitDir,
170
+ root: transaction.location.root,
171
+ sources: originalSources,
172
+ paths,
173
+ subject: `Reopen ${taskIds.length === 1 ? `task ${taskIds[0]}` : `${taskIds.length} tasks`} after bind failure`,
174
+ }, {});
175
+ return "created";
176
+ });
177
+ }
178
+ export async function replayPreparedTaskBindForfeit(cwd, prepared) {
179
+ await withLockHeldTaskBoard(cwd, async (transaction) => {
180
+ if (transaction.location.mode !== "git")
181
+ throw new FlowError("NOT_GIT_REPO", "task forfeit replay requires Git task storage");
182
+ await transaction.replace(taskWritesFromSources(prepared.replay.originalSources));
183
+ await publishForwardTaskBoardCommit({
184
+ cwd: transaction.location.root,
185
+ ref: prepared.boardRef,
186
+ gitDir: transaction.location.gitDir,
187
+ root: transaction.location.root,
188
+ sources: prepared.replay.originalSources,
189
+ paths: prepared.replay.paths,
190
+ subject: `Reopen ${prepared.taskIds.length === 1 ? `task ${prepared.taskIds[0]}` : `${prepared.taskIds.length} tasks`} after bind failure`,
191
+ }, {});
192
+ });
193
+ }
@@ -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
  }