@astrosheep/keiyaku 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +13 -94
  3. package/build/agents/harness/activity-values.js +16 -0
  4. package/build/agents/harness/event-persistence.js +218 -0
  5. package/build/agents/harness/execution-handle.js +25 -222
  6. package/build/agents/harness/index.js +3 -1
  7. package/build/agents/harness/outcome.js +47 -4
  8. package/build/agents/harness/projection.js +42 -59
  9. package/build/agents/harness/provider-adapter.js +58 -0
  10. package/build/agents/harness/router.js +11 -187
  11. package/build/agents/harness/runtime.js +17 -4
  12. package/build/agents/harness/session.js +15 -0
  13. package/build/agents/launch-snapshot/model.js +15 -236
  14. package/build/agents/launch-snapshot/parse-helpers.js +114 -0
  15. package/build/agents/launch-snapshot/resolve.js +17 -43
  16. package/build/agents/opencode-sdk.js +45 -12
  17. package/build/agents/providers/claude-agent-sdk/adapter.js +6 -17
  18. package/build/agents/providers/claude-agent-sdk/registration.js +214 -0
  19. package/build/agents/providers/claude-agent-sdk/session-schema.js +2 -0
  20. package/build/agents/providers/codex-app-server/adapter.js +50 -53
  21. package/build/agents/providers/codex-app-server/events.js +67 -11
  22. package/build/agents/providers/codex-app-server/identity.js +1 -0
  23. package/build/agents/providers/codex-app-server/policy.js +29 -0
  24. package/build/agents/providers/codex-app-server/registration.js +248 -0
  25. package/build/agents/providers/codex-app-server/session-schema.js +6 -0
  26. package/build/agents/providers/codex-app-server/session.js +23 -17
  27. package/build/agents/providers/codex-app-server/terms.js +1 -0
  28. package/build/agents/providers/opencode-sdk/adapter.js +94 -50
  29. package/build/agents/providers/opencode-sdk/events.js +8 -4
  30. package/build/agents/providers/opencode-sdk/registration.js +135 -0
  31. package/build/agents/providers/opencode-sdk/session-schema.js +2 -0
  32. package/build/agents/providers/opencode-sdk/session.js +21 -14
  33. package/build/agents/providers/opencode-sdk/terms.js +1 -0
  34. package/build/agents/providers/pi/adapter.js +36 -30
  35. package/build/agents/providers/pi/registration.js +145 -0
  36. package/build/agents/providers/pi/session-schema.js +6 -0
  37. package/build/agents/providers/pi/terms.js +1 -0
  38. package/build/agents/providers/registration.js +7 -0
  39. package/build/agents/providers/registry.js +76 -0
  40. package/build/agents/selector.js +2 -29
  41. package/build/agents/types.js +8 -0
  42. package/build/artifact-directories.js +22 -0
  43. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  44. package/build/cli/commands/akuma/view/handler.js +5 -4
  45. package/build/cli/commands/akuma.js +6 -0
  46. package/build/cli/commands/contract/amend/handler.js +8 -2
  47. package/build/cli/commands/contract/arc/handler.js +7 -1
  48. package/build/cli/commands/contract/bind/handler.js +4 -3
  49. package/build/cli/commands/contract/bind/meta.js +34 -7
  50. package/build/cli/commands/contract/forfeit/handler.js +4 -4
  51. package/build/cli/commands/contract/log/handler.js +3 -3
  52. package/build/cli/commands/contract/petition/handler.js +4 -6
  53. package/build/cli/commands/contract/renew/handler.js +20 -6
  54. package/build/cli/commands/contract/renew/meta.js +5 -4
  55. package/build/cli/commands/metadata.js +58 -0
  56. package/build/cli/commands/projection/call/handler.js +6 -5
  57. package/build/cli/commands/projection/kill/handler.js +16 -32
  58. package/build/cli/commands/projection/shared.js +28 -0
  59. package/build/cli/commands/projection/status/handler.js +1 -1
  60. package/build/cli/commands/projection/tell/handler.js +14 -30
  61. package/build/cli/commands/projection/wait/handler.js +45 -22
  62. package/build/cli/commands/projection/wait/meta.js +7 -2
  63. package/build/cli/commands/registry.js +2 -54
  64. package/build/cli/commands/shared.js +10 -7
  65. package/build/cli/commands/skills/install/handler.js +2 -5
  66. package/build/cli/commands/skills/skills/handler.js +1 -1
  67. package/build/cli/commands/task/add/handler.js +5 -13
  68. package/build/cli/commands/task/catalog.js +2 -2
  69. package/build/cli/commands/task/done/handler.js +4 -8
  70. package/build/cli/commands/task/drop/handler.js +3 -6
  71. package/build/cli/commands/task/ls/handler.js +3 -5
  72. package/build/cli/commands/task/shared.js +14 -4
  73. package/build/cli/commands/task/{view → show}/handler.js +7 -8
  74. package/build/cli/commands/task/show/meta.js +8 -0
  75. package/build/cli/commands/task/start/handler.js +3 -6
  76. package/build/cli/commands/task/stop/handler.js +4 -8
  77. package/build/cli/commands/task/task/handler.js +1 -1
  78. package/build/cli/commands/task/update/handler.js +4 -8
  79. package/build/cli/completion.js +1 -1
  80. package/build/cli/flags.js +19 -3
  81. package/build/cli/help.js +4 -11
  82. package/build/cli/index.js +57 -83
  83. package/build/cli/missing-subcommand.js +7 -0
  84. package/build/cli/parse-flags.js +14 -5
  85. package/build/cli/parse-metadata.js +3 -3
  86. package/build/cli/parse-selectors.js +1 -1
  87. package/build/cli/parse.js +21 -0
  88. package/build/cli/projection-address.js +7 -12
  89. package/build/cli/render/address.js +41 -37
  90. package/build/cli/render/arc.js +24 -12
  91. package/build/cli/render/call.js +11 -5
  92. package/build/cli/render/errors.js +36 -1
  93. package/build/cli/render/format.js +4 -0
  94. package/build/cli/render/kanshi.js +6 -12
  95. package/build/cli/render/kill.js +9 -0
  96. package/build/cli/render/misc.js +55 -12
  97. package/build/cli/render/petition.js +20 -15
  98. package/build/cli/render/projection-activity.js +3 -0
  99. package/build/cli/render/shared.js +60 -21
  100. package/build/cli/render/skills.js +10 -0
  101. package/build/cli/render/status.js +37 -14
  102. package/build/cli/render/success-response.js +77 -0
  103. package/build/cli/render/task.js +54 -14
  104. package/build/cli/render/tell.js +5 -0
  105. package/build/cli/render/terminal-failure.js +58 -0
  106. package/build/cli/render/types.js +1 -0
  107. package/build/cli/render/wait.js +38 -36
  108. package/build/cli/types.js +1 -1
  109. package/build/config/akuma-loader.js +0 -5
  110. package/build/config/env-keys.js +1 -1
  111. package/build/config/provider-policy-ownership.js +3 -32
  112. package/build/config/settings/disease.js +8 -1
  113. package/build/config/settings/loader.js +11 -3
  114. package/build/config/settings/provider-instance-fields.js +51 -0
  115. package/build/config/settings/schema.js +6 -176
  116. package/build/core/addressing.js +66 -16
  117. package/build/core/amend.js +135 -18
  118. package/build/core/arc.js +19 -67
  119. package/build/core/bind.js +82 -35
  120. package/build/core/call/call.js +15 -9
  121. package/build/core/call/context.js +6 -51
  122. package/build/core/call/execution.js +21 -22
  123. package/build/core/call/prompt.js +14 -1
  124. package/build/core/command-markdown.js +45 -0
  125. package/build/core/context.js +13 -7
  126. package/build/core/contract-view.js +20 -3
  127. package/build/core/draft.js +123 -16
  128. package/build/core/forfeit.js +5 -2
  129. package/build/core/hints.js +1 -75
  130. package/build/core/identifier-slug.js +66 -0
  131. package/build/core/ids.js +25 -11
  132. package/build/core/ledger.js +20 -68
  133. package/build/core/markdown/parser.js +0 -68
  134. package/build/core/markdown/render.js +68 -0
  135. package/build/core/markdown/sections.js +2 -1
  136. package/build/core/outcome-base.js +4 -0
  137. package/build/core/projection/generation/database.js +2 -38
  138. package/build/core/projection/generation/model.js +42 -1
  139. package/build/core/{projection-generation-continuation.js → projection/generation/projection-generation-continuation.js} +3 -3
  140. package/build/core/{projection-generation-execution.js → projection/generation/projection-generation-execution.js} +19 -49
  141. package/build/core/{projection-generation-identity.js → projection/generation/projection-generation-identity.js} +2 -2
  142. package/build/core/{projection-generation-launcher.js → projection/generation/projection-generation-launcher.js} +5 -3
  143. package/build/core/{projection-generation-process.js → projection/generation/projection-generation-process.js} +2 -1
  144. package/build/core/{projection-generation-runner.js → projection/generation/projection-generation-runner.js} +18 -3
  145. package/build/core/projection/generation/store.js +2 -2
  146. package/build/core/projection/generation/transitions.js +1 -1
  147. package/build/core/projection/index.js +27 -0
  148. package/build/core/{projection-activity.js → projection/projection-activity.js} +12 -0
  149. package/build/core/{projection-alias.js → projection/projection-alias.js} +14 -19
  150. package/build/core/projection/projection-coordinate.js +61 -0
  151. package/build/core/{projection-core.js → projection/projection-core.js} +9 -9
  152. package/build/core/{projection-execution-observer.js → projection/projection-execution-observer.js} +2 -57
  153. package/build/core/{projection-identity.js → projection/projection-identity.js} +1 -1
  154. package/build/core/{projection-kill.js → projection/projection-kill.js} +1 -1
  155. package/build/core/{projection-life-observer.js → projection/projection-life-observer.js} +1 -1
  156. package/build/core/{projection-mint.js → projection/projection-mint.js} +10 -10
  157. package/build/core/{projection-status.js → projection/projection-status.js} +98 -25
  158. package/build/core/projection/projection-terminal-failure.js +104 -0
  159. package/build/core/projection/projection-wait.js +278 -0
  160. package/build/core/projection/projection-wake.js +203 -0
  161. package/build/core/provider-loader-ports.js +4 -0
  162. package/build/core/ref-log.js +65 -40
  163. package/build/core/registry.js +63 -10
  164. package/build/core/renew-session.js +128 -0
  165. package/build/core/renew.js +426 -99
  166. package/build/core/scope.js +143 -66
  167. package/build/core/seal.js +139 -35
  168. package/build/core/{claim-delivery.js → settlement/claim-delivery.js} +9 -14
  169. package/build/core/{claim.js → settlement/claim.js} +32 -8
  170. package/build/core/settlement/index.js +19 -0
  171. package/build/core/{petition-claim-gates.js → settlement/petition-claim-gates.js} +22 -18
  172. package/build/core/{petition-forfeit.js → settlement/petition-forfeit.js} +2 -2
  173. package/build/core/{petition-head-guard.js → settlement/petition-head-guard.js} +2 -2
  174. package/build/core/settlement/petition-io.js +1 -0
  175. package/build/core/{petition-preview.js → settlement/petition-preview.js} +1 -1
  176. package/build/core/{petition.js → settlement/petition.js} +12 -10
  177. package/build/core/settlement/queue-read-model.js +143 -0
  178. package/build/core/settlement/queue-seat-allocation.js +156 -0
  179. package/build/core/settlement/queue.js +6 -0
  180. package/build/core/settlement/settlement.js +295 -0
  181. package/build/core/{verdict.js → settlement/verdict.js} +9 -37
  182. package/build/core/status/board.js +24 -23
  183. package/build/core/status/ledger-batch.js +158 -0
  184. package/build/core/status/reconciliation.js +10 -4
  185. package/build/core/stored-agent-event.js +3 -0
  186. package/build/core/task/board.js +40 -4
  187. package/build/core/task/commands.js +30 -21
  188. package/build/core/task/index.js +12 -0
  189. package/build/core/task/settlement-git.js +157 -65
  190. package/build/core/task/settlement-policy.js +54 -24
  191. package/build/core/task/task-contract.js +370 -0
  192. package/build/core/{task-doctor.js → task/task-doctor.js} +1 -1
  193. package/build/core/{task-git-actor.js → task/task-git-actor.js} +5 -10
  194. package/build/core/{task-git-runtime.js → task/task-git-runtime.js} +3 -3
  195. package/build/core/{task-git-store.js → task/task-git-store.js} +92 -29
  196. package/build/core/task/task-repository.js +1 -0
  197. package/build/core/{task-store-repository.js → task/task-store-repository.js} +5 -4
  198. package/build/core/task/task-store.js +1 -0
  199. package/build/core/{task-worktree.js → task/task-worktree.js} +2 -1
  200. package/build/core/task/task.js +5 -0
  201. package/build/core/task/tree.js +37 -0
  202. package/build/core/task/validation.js +5 -2
  203. package/build/core/transcripts.js +15 -3
  204. package/build/core/worktree-path.js +43 -28
  205. package/build/flow-error.js +85 -39
  206. package/build/fs/durability.js +27 -0
  207. package/build/fs/path-coordinate.js +16 -0
  208. package/build/generated/version.js +3 -2
  209. package/build/git/branches.js +2 -1
  210. package/build/git/core.js +106 -2
  211. package/build/git/path-coordinate.js +17 -0
  212. package/build/git/refs.js +57 -0
  213. package/build/git/streaming-batch.js +301 -0
  214. package/build/git/worktree.js +65 -0
  215. package/build/index.js +3 -3
  216. package/build/keiyaku.js +2 -36
  217. package/package.json +4 -10
  218. package/skills/keiyaku/SKILL.md +3 -2
  219. package/skills/keiyaku-akuma/SKILL.md +10 -6
  220. package/skills/keiyaku-task/SKILL.md +1 -1
  221. package/skills/keiyaku-workflow/SKILL.md +25 -4
  222. package/build/agents/providers/codex-sdk.js +0 -329
  223. package/build/cli/commands/task/view/meta.js +0 -8
  224. package/build/core/command-io.js +0 -174
  225. package/build/core/path-coordinate.js +0 -27
  226. package/build/core/petition-claim.js +0 -117
  227. package/build/core/petition-run.js +0 -83
  228. package/build/core/projection-coordinate.js +0 -63
  229. package/build/core/projection-wait.js +0 -165
  230. package/build/core/projection-wake.js +0 -136
  231. package/build/core/queue.js +0 -343
  232. package/build/core/task-contract.js +0 -237
  233. package/build/core/task.js +0 -4
  234. /package/build/{core/task-repository.js → agents/launch-snapshot/types.js} +0 -0
  235. /package/build/{core/task-store.js → agents/providers/claude-agent-sdk/terms.js} +0 -0
  236. /package/build/core/{projection-generation-runtime.js → projection/generation/projection-generation-runtime.js} +0 -0
  237. /package/build/core/{projection-life-protocol.js → projection/projection-life-protocol.js} +0 -0
  238. /package/build/core/{projection-runner-lock.js → projection/projection-runner-lock.js} +0 -0
@@ -0,0 +1,370 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { assertGitObservation, requireGitRepository } from "../../git/branches.js";
3
+ import { withLockHeldTaskBoard } from "./task-git-store.js";
4
+ import { prepareTaskDocumentReplacements, taskBoardFromSourceBoard, taskRepositoryFromLockedBoard } from "./task-store-repository.js";
5
+ import { serializeTaskDocument } from "./document.js";
6
+ import { buildTaskSourceBoard } from "./source-board.js";
7
+ import { containsCommit, createLandingCommit, createTaskBoardCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, readableRefHead, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
8
+ import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
9
+ import { stableRepoRoot } from "../worktree-path.js";
10
+ function sourcePath(root, source) {
11
+ return source.path.slice(root.length + 1).split("\\").join("/");
12
+ }
13
+ function taskPath(id) {
14
+ return `.keiyaku/tasks/${id}.md`;
15
+ }
16
+ function ownedTaskPaths(board, taskIds) {
17
+ if (!taskIds || taskIds.length === 0)
18
+ return [];
19
+ const paths = new Set();
20
+ for (const taskId of taskIds) {
21
+ let current = board.byId.get(taskId);
22
+ while (current) {
23
+ paths.add(taskPath(current.id));
24
+ current = current.parent === null ? undefined : board.byId.get(current.parent);
25
+ }
26
+ }
27
+ return [...paths].sort();
28
+ }
29
+ function selectSettlementSources(input) {
30
+ const tracked = new Set(input.trackedPaths);
31
+ const sourceByPath = new Map(input.sources.map((source) => [sourcePath(input.root, source), source]));
32
+ // An ancestry path is eligible only when it was already tracked. A mutation is
33
+ // the sole explicit adoption authority for an untracked task document.
34
+ const selected = new Set([
35
+ ...input.ownedPaths.filter((taskSourcePath) => tracked.has(taskSourcePath)),
36
+ ...input.mutationPaths,
37
+ ].filter((taskSourcePath) => sourceByPath.has(taskSourcePath)));
38
+ const selectedPaths = [...selected].sort();
39
+ const adoptedPaths = selectedPaths.filter((taskSourcePath) => !tracked.has(taskSourcePath));
40
+ const observedPaths = new Set([...tracked, ...sourceByPath.keys()]);
41
+ return {
42
+ sources: selectedPaths.map((taskSourcePath) => sourceByPath.get(taskSourcePath)),
43
+ evidence: {
44
+ selectedPaths,
45
+ rejectedPaths: [...observedPaths].filter((taskSourcePath) => !selected.has(taskSourcePath)).sort(),
46
+ adoptedPaths,
47
+ },
48
+ };
49
+ }
50
+ function repositoryForSettlement(transaction, sources) {
51
+ let currentSources = sources;
52
+ return {
53
+ read: async () => taskBoardFromSourceBoard(buildTaskSourceBoard(currentSources)),
54
+ sources: () => currentSources,
55
+ mutate: async (apply) => {
56
+ const board = taskBoardFromSourceBoard(buildTaskSourceBoard(currentSources));
57
+ const mutation = await apply(board);
58
+ const prepared = prepareTaskDocumentReplacements(currentSources, mutation.replacements);
59
+ await transaction.replace(prepared.writes);
60
+ currentSources = prepared.sources;
61
+ return mutation.select(prepared.board);
62
+ },
63
+ };
64
+ }
65
+ async function clearAttemptedPointers(input) {
66
+ return input.repository.mutate((board) => {
67
+ const replacements = input.tasks.flatMap((task) => {
68
+ const current = requireSettledTask(board, task.id);
69
+ return current.contractId === input.contractId ? [contractPointerDocument(current, undefined)] : [];
70
+ });
71
+ return { replacements, select: () => replacements.map((replacement) => replacement.id) };
72
+ });
73
+ }
74
+ async function replaceTaskDocuments(repository, replacements) {
75
+ return repository.mutate(() => ({
76
+ replacements,
77
+ select: (resulting) => replacements.map((replacement) => requireSettledTask(resulting, replacement.id)),
78
+ }));
79
+ }
80
+ async function restorePreAdmissionTaskMutations(input) {
81
+ // Write text is ignored by restore; the id selects the path whose prior bytes return.
82
+ await input.transaction.restore(input.taskIds.map((id) => ({ id, text: "" })), input.previousSources);
83
+ }
84
+ async function rollForwardClearAttemptedPointer(input) {
85
+ const clearedIds = await clearAttemptedPointers({
86
+ repository: input.repository,
87
+ tasks: input.selected,
88
+ contractId: input.contractId,
89
+ });
90
+ if (clearedIds.length === 0)
91
+ return;
92
+ const rollbackSelection = selectSettlementSources({
93
+ root: input.root,
94
+ sources: input.repository.sources(),
95
+ trackedPaths: await trackedTaskPaths(input.hubCwd, input.trackedHead),
96
+ ownedPaths: input.selectedPaths,
97
+ mutationPaths: clearedIds.map(taskPath),
98
+ });
99
+ await publishForwardTaskBoardCommit({
100
+ cwd: input.hubCwd,
101
+ ref: input.branchRef,
102
+ gitDir: input.gitDir,
103
+ root: input.root,
104
+ sources: rollbackSelection.sources,
105
+ paths: rollbackSelection.evidence.selectedPaths,
106
+ subject: `Clear failed contract ${input.contractId} from ${clearedIds.length === 1 ? `task ${clearedIds[0]}` : `${clearedIds.length} tasks`}`,
107
+ }, input.deps);
108
+ }
109
+ export async function settleTaskBoardForBind(input, deps = {}) {
110
+ await requireGitRepository(input.cwd, "task bind");
111
+ return withLockHeldTaskBoard(input.cwd, async (transaction) => {
112
+ if (transaction.location.mode !== "git") {
113
+ if (transaction.location.capability === "unavailable") {
114
+ assertGitObservation({ capability: "unavailable" }, "task bind");
115
+ }
116
+ throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
117
+ }
118
+ const hubCwd = transaction.location.root;
119
+ const branch = await inspectCurrentBranch(hubCwd);
120
+ const previousSources = transaction.sources;
121
+ const repository = repositoryForSettlement(transaction, transaction.sources);
122
+ const board = requireHealthySettlementBoard(await repository.read());
123
+ const taskBound = "taskIds" in input;
124
+ const selectedTasks = taskBound ? selectReadyTasksForBind(board, input.taskIds) : [];
125
+ const prepared = taskBound ? await input.prepare(selectedTasks) : input;
126
+ const contractId = prepared.contractId;
127
+ if (taskBound && selectedTasks.length > 0) {
128
+ await replaceTaskDocuments(repository, selectedTasks.map((task) => contractPointerDocument(task, contractId)));
129
+ }
130
+ const taskIds = taskBound ? selectedTasks.map((t) => t.id) : undefined;
131
+ const selectedPaths = ownedTaskPaths(await repository.read(), taskIds);
132
+ const mutationPaths = taskBound ? selectedTasks.map((t) => taskPath(t.id)) : [];
133
+ const selectCurrentSettlement = async () => selectSettlementSources({
134
+ root: transaction.location.root,
135
+ sources: repository.sources(),
136
+ trackedPaths: await trackedTaskPaths(hubCwd, branch.head),
137
+ ownedPaths: selectedPaths,
138
+ mutationPaths,
139
+ });
140
+ const selection = await selectCurrentSettlement();
141
+ const makeBoardCommit = deps.createTaskBoardCommit ?? createTaskBoardCommit;
142
+ const reconcileIndex = deps.reconcileTaskPathsetIndex ?? reconcileTaskPathsetIndex;
143
+ let firstCommit;
144
+ try {
145
+ const taskLabel = taskBound
146
+ ? selectedTasks.length === 1
147
+ ? `task ${selectedTasks[0].id}`
148
+ : `${selectedTasks.length} tasks`
149
+ : "";
150
+ firstCommit = await makeBoardCommit({
151
+ cwd: hubCwd,
152
+ gitDir: transaction.location.gitDir,
153
+ root: transaction.location.root,
154
+ parent: branch.head,
155
+ sources: selection.sources,
156
+ subject: taskBound
157
+ ? `Bind ${taskLabel} to contract ${contractId}`
158
+ : `Flush task board for contract ${contractId}`,
159
+ });
160
+ }
161
+ catch (error) {
162
+ if (taskBound) {
163
+ await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.id), previousSources });
164
+ }
165
+ throw error;
166
+ }
167
+ try {
168
+ await updateBranch({ cwd: hubCwd, ref: branch.ref, next: firstCommit, expected: branch.head }, deps);
169
+ }
170
+ catch (error) {
171
+ if (taskBound) {
172
+ const concurrentHead = await refHead(hubCwd, branch.ref);
173
+ const concurrentMatches = await Promise.all(selectedTasks.map(async (task) => (await taskContractAtCommit(hubCwd, concurrentHead, task.id)) === contractId));
174
+ if (concurrentMatches.every(Boolean)) {
175
+ await rollForwardClearAttemptedPointer({
176
+ repository, selected: selectedTasks, contractId, hubCwd,
177
+ branchRef: branch.ref, trackedHead: branch.head,
178
+ gitDir: transaction.location.gitDir, root: transaction.location.root,
179
+ selectedPaths, deps,
180
+ });
181
+ }
182
+ else {
183
+ await restorePreAdmissionTaskMutations({ transaction, taskIds: selectedTasks.map((task) => task.id), previousSources });
184
+ }
185
+ }
186
+ throw error;
187
+ }
188
+ try {
189
+ await reconcileIndex(hubCwd, firstCommit, selection.evidence.selectedPaths);
190
+ }
191
+ catch (indexError) {
192
+ if (taskBound) {
193
+ await rollForwardClearAttemptedPointer({
194
+ repository, selected: selectedTasks, contractId, hubCwd,
195
+ branchRef: branch.ref, trackedHead: branch.head,
196
+ gitDir: transaction.location.gitDir, root: transaction.location.root,
197
+ selectedPaths, deps,
198
+ });
199
+ }
200
+ throw indexError;
201
+ }
202
+ try {
203
+ const ledger = await prepared.publishLedger(firstCommit);
204
+ const boardAfter = requireHealthySettlementBoard(await repository.read());
205
+ return {
206
+ boardCommit: firstCommit,
207
+ contractId,
208
+ ledger,
209
+ ...(taskBound && selectedTasks.length > 0 ? { tasks: selectedTasks.map((t) => requireSettledTask(boardAfter, t.id)) } : {}),
210
+ evidence: selection.evidence,
211
+ };
212
+ }
213
+ catch (publishError) {
214
+ if (!taskBound)
215
+ throw publishError;
216
+ await rollForwardClearAttemptedPointer({
217
+ repository, selected: selectedTasks, contractId, hubCwd,
218
+ branchRef: branch.ref, trackedHead: branch.head,
219
+ gitDir: transaction.location.gitDir, root: transaction.location.root,
220
+ selectedPaths, deps,
221
+ });
222
+ throw publishError;
223
+ }
224
+ }, { signal: input.signal });
225
+ }
226
+ export async function reconcileTaskForfeit(input) {
227
+ await requireGitRepository(input.cwd, "task forfeit");
228
+ const hubRoot = await stableRepoRoot(input.cwd);
229
+ return withLockHeldTaskBoard(hubRoot, async (transaction) => {
230
+ if (transaction.location.mode !== "git") {
231
+ if (transaction.location.capability === "unavailable") {
232
+ assertGitObservation({ capability: "unavailable" }, "task forfeit");
233
+ }
234
+ throw new FlowError("NOT_GIT_REPO", "task forfeit requires Git task storage");
235
+ }
236
+ const repository = taskRepositoryFromLockedBoard(transaction);
237
+ const matched = tasksForContract(await repository.read(), input.contractId);
238
+ const ledger = await input.publishLedger();
239
+ if (matched.length === 0)
240
+ return { ledger };
241
+ const reopenedDocuments = matched.map(reopenedTaskDocument);
242
+ const reopened = await replaceTaskDocuments(repository, reopenedDocuments);
243
+ const taskLabel = matched.length === 1 ? `task ${matched[0].id}` : `${matched.length} tasks`;
244
+ await publishTaskMutationCommit({
245
+ cwd: transaction.location.root,
246
+ gitDir: transaction.location.gitDir,
247
+ writes: reopenedDocuments.map((document) => ({ id: document.id, text: serializeTaskDocument(document) })),
248
+ subject: `Reopen ${taskLabel} after forfeit of ${input.contractId}`,
249
+ });
250
+ return { ledger, tasks: reopened };
251
+ });
252
+ }
253
+ /**
254
+ * Claim owns the sole task-board settlement after target CAS. Contract branch
255
+ * task trees are fossils: the candidate is the code-only three-way merge plus
256
+ * one complete snapshot of the common hub board held under its repository lock.
257
+ */
258
+ export async function settleTaskBoardForLanding(input) {
259
+ await requireGitRepository(input.cwd, "task landing");
260
+ const hubRoot = await stableRepoRoot(input.cwd);
261
+ let lastTargetHead = input.targetHead;
262
+ let lastPublishError;
263
+ let acceptedMovement;
264
+ for (let attempt = 0; attempt < 4; attempt += 1) {
265
+ const result = await withLockHeldTaskBoard(hubRoot, async (transaction) => {
266
+ if (transaction.location.mode !== "git") {
267
+ if (transaction.location.capability === "unavailable") {
268
+ assertGitObservation({ capability: "unavailable" }, "task landing");
269
+ }
270
+ throw new FlowError("NOT_GIT_REPO", "task landing requires Git task storage");
271
+ }
272
+ // Each attempt reads fresh target and board facts while holding the
273
+ // common task lock. A losing CAS exits before the next attempt.
274
+ const targetHead = await refHead(input.cwd, input.targetRef);
275
+ lastTargetHead = targetHead;
276
+ const repository = repositoryForSettlement(transaction, transaction.sources);
277
+ const board = requireHealthySettlementBoard(await repository.read());
278
+ const replacements = doneTaskReplacements(board, input.contractId);
279
+ const prepared = replacements.length > 0
280
+ ? prepareTaskDocumentReplacements(transaction.sources, replacements)
281
+ : { sources: transaction.sources, board };
282
+ const finalSources = prepared.sources;
283
+ requireHealthySettlementBoard(prepared.board);
284
+ const replacementIds = replacements.map((r) => r.id);
285
+ const selectedPaths = ownedTaskPaths(prepared.board, replacementIds.length > 0 ? replacementIds : undefined);
286
+ const selection = selectSettlementSources({
287
+ root: transaction.location.root,
288
+ sources: finalSources,
289
+ trackedPaths: await trackedTaskPaths(input.cwd, targetHead),
290
+ ownedPaths: selectedPaths,
291
+ mutationPaths: replacements.map((r) => taskPath(r.id)),
292
+ });
293
+ let mergeCommit = targetHead;
294
+ let movement = acceptedMovement?.mergeCommit === targetHead ? acceptedMovement : undefined;
295
+ if (!(await containsCommit(input.cwd, input.deliveryHead, targetHead))) {
296
+ mergeCommit = await createLandingCommit({
297
+ cwd: input.cwd,
298
+ gitDir: transaction.location.gitDir,
299
+ root: transaction.location.root,
300
+ base: input.baseCommit,
301
+ target: targetHead,
302
+ delivery: input.deliveryHead,
303
+ sources: selection.sources,
304
+ contractId: input.contractId,
305
+ targetBranch: input.targetBranch,
306
+ });
307
+ await input.beforeCas?.(mergeCommit, targetHead);
308
+ try {
309
+ await updateTargetRef(input.cwd, input.targetRef, mergeCommit, targetHead);
310
+ movement = { mergeCommit, oldHead: targetHead };
311
+ acceptedMovement = movement;
312
+ }
313
+ catch {
314
+ const current = await readableRefHead(input.cwd, input.targetRef);
315
+ if (!current || !(await containsCommit(input.cwd, input.deliveryHead, current))) {
316
+ return { kind: "retry" };
317
+ }
318
+ mergeCommit = current;
319
+ movement = acceptedMovement?.mergeCommit === mergeCommit ? acceptedMovement : undefined;
320
+ }
321
+ }
322
+ const warnings = [];
323
+ if (replacements.length > 0) {
324
+ await transaction.replace(taskWritesFromSources(selection.sources));
325
+ }
326
+ try {
327
+ await reconcileTaskPathsetIndex(transaction.location.root, mergeCommit, selection.evidence.selectedPaths);
328
+ }
329
+ catch (error) {
330
+ warnings.push(`Accepted landing ${mergeCommit}, but task index reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
331
+ }
332
+ try {
333
+ await input.publishLedger?.(mergeCommit);
334
+ }
335
+ catch (error) {
336
+ lastPublishError = error;
337
+ return { kind: "retry" };
338
+ }
339
+ const reconcileWarnings = movement
340
+ ? await input.afterCas?.(mergeCommit, movement.oldHead)
341
+ : undefined;
342
+ warnings.push(...(reconcileWarnings ?? []));
343
+ return {
344
+ kind: movement ? "accepted" : "idempotent",
345
+ mergeCommit,
346
+ ...(movement ? { oldHead: movement.oldHead } : {}),
347
+ ...(warnings.length > 0 ? { warnings } : {}),
348
+ evidence: selection.evidence,
349
+ };
350
+ });
351
+ if (result.kind === "retry")
352
+ continue;
353
+ if (result.kind === "idempotent") {
354
+ return {
355
+ mergeCommit: result.mergeCommit,
356
+ ...(result.warnings ? { warnings: result.warnings } : {}),
357
+ ...(result.evidence ? { evidence: result.evidence } : {}),
358
+ };
359
+ }
360
+ return {
361
+ mergeCommit: result.mergeCommit,
362
+ oldHead: result.oldHead,
363
+ ...(result.warnings ? { warnings: result.warnings } : {}),
364
+ ...(result.evidence ? { evidence: result.evidence } : {}),
365
+ };
366
+ }
367
+ if (lastPublishError !== undefined)
368
+ throw lastPublishError;
369
+ throw new FlowError("CONCURRENT_MODIFICATION", `target branch kept moving while claiming ${input.contractId}; last observed ${lastTargetHead}`);
370
+ }
@@ -1,5 +1,5 @@
1
1
  import { rollbackOrphanTaskPointers } from "./task.js";
2
- import { buildTaskSourceBoard } from "./task/source-board.js";
2
+ import { buildTaskSourceBoard } from "./source-board.js";
3
3
  export async function inspectTaskDoctor(store, repository, facts) {
4
4
  const orphans = facts.lockHeld ? [] : await rollbackOrphanTaskPointers(repository, facts.contractIds);
5
5
  const sources = await store.read();
@@ -1,20 +1,15 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { runGitProcess } from "../../git/core.js";
2
2
  const ANONYMOUS_TASK_CREATOR = "(anonymous)";
3
3
  const TASK_ACTOR_ENV = "KEIYAKU_ACTOR";
4
4
  const GIT_ACTOR_KEYS = ["keiyaku.actor", "user.name"];
5
- function defaultRunGit(cwd, args, env) {
6
- return spawnSync("git", [...args], {
7
- cwd,
8
- encoding: "utf8",
9
- env,
10
- stdio: ["ignore", "pipe", "pipe"],
11
- });
5
+ async function defaultRunGit(cwd, args, env) {
6
+ return await runGitProcess(cwd, args, { env: { ...env } });
12
7
  }
13
8
  function attribution(value) {
14
9
  const trimmed = value?.trim();
15
10
  return trimmed ? trimmed : undefined;
16
11
  }
17
- export function resolveTaskCreator(cwd, deps = {}) {
12
+ export async function resolveTaskCreator(cwd, deps = {}) {
18
13
  const sourceEnv = deps.env ?? process.env;
19
14
  const environmentActor = attribution(sourceEnv[TASK_ACTOR_ENV]);
20
15
  if (environmentActor)
@@ -23,7 +18,7 @@ export function resolveTaskCreator(cwd, deps = {}) {
23
18
  const runGit = deps.runGit ?? defaultRunGit;
24
19
  for (const key of GIT_ACTOR_KEYS) {
25
20
  try {
26
- const result = runGit(cwd, ["config", "--get", key], gitEnv);
21
+ const result = await runGit(cwd, ["config", "--get", key], gitEnv);
27
22
  if (!result.error && result.status === 0) {
28
23
  const configuredActor = attribution(result.stdout);
29
24
  if (configuredActor)
@@ -1,6 +1,6 @@
1
- import { observeGitRepository } from "../git/branches.js";
2
- import { listContractIds } from "./ledger.js";
3
- import { contractBoundByEngineWorktree, stableRepoRoot } from "./worktree-path.js";
1
+ import { observeGitRepository } from "../../git/branches.js";
2
+ import { listContractIds } from "../ledger.js";
3
+ import { contractBoundByEngineWorktree, stableRepoRoot } from "../worktree-path.js";
4
4
  import { inspectTaskLock, openTaskStore } from "./task-git-store.js";
5
5
  import { taskRepositoryFromStore } from "./task-store-repository.js";
6
6
  import { withTaskWorktreePolicy } from "./task-worktree.js";
@@ -1,15 +1,21 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { spawnSync } from "node:child_process";
3
2
  import * as fs from "node:fs";
4
3
  import * as path from "node:path";
5
4
  import { DatabaseSync } from "node:sqlite";
6
- import { FlowError } from "../flow-error.js";
7
- import { isGitExecutableUnavailable, NOT_GIT_REPOSITORY_PATTERNS } from "../git/core.js";
8
- import { gitCommonRootPath, normalizeGitPath } from "./path-coordinate.js";
9
- const TASK_ID = /^k-[0-9a-f]{12}$/;
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { FlowError } from "../../flow-error.js";
7
+ import { syncDirectorySync } from "../../fs/durability.js";
8
+ import { normalizeFilesystemCoordinate } from "../../fs/path-coordinate.js";
9
+ import { isGitExecutableUnavailable, NOT_GIT_REPOSITORY_PATTERNS, runGitProcess } from "../../git/core.js";
10
+ import { gitCommonRootPath, normalizeGitPath } from "../../git/path-coordinate.js";
11
+ import { inspectCurrentBranch, publishTaskMutationCommit } from "./settlement-git.js";
12
+ import { isTaskId } from "./validation.js";
10
13
  const SQLITE_BUSY = 5;
11
14
  const LOCK_FILE = "task-lock.sqlite";
12
15
  const GIT_LOCK_FILE = "keiyaku-task-lock.sqlite";
16
+ export const TASK_LOCK_ADMISSION_TIMEOUT_MS = 4_000;
17
+ export const TASK_LOCK_ADMISSION_INITIAL_DELAY_MS = 10;
18
+ export const TASK_LOCK_ADMISSION_MAX_DELAY_MS = 100;
13
19
  export class TaskLockHeldError extends Error {
14
20
  path;
15
21
  code = "TASK_LOCK_HELD";
@@ -19,6 +25,16 @@ export class TaskLockHeldError extends Error {
19
25
  this.name = "TaskLockHeldError";
20
26
  }
21
27
  }
28
+ export class TaskLockAdmissionTimeoutError extends FlowError {
29
+ path;
30
+ timeoutMs;
31
+ constructor(path, timeoutMs, options) {
32
+ super("TASK_LOCK_ADMISSION_TIMEOUT", `task lock admission timed out after ${timeoutMs}ms: ${path}`, { cause: options?.cause });
33
+ this.path = path;
34
+ this.timeoutMs = timeoutMs;
35
+ this.name = "TaskLockAdmissionTimeoutError";
36
+ }
37
+ }
22
38
  function errorText(error) {
23
39
  return error instanceof Error ? error.message : String(error);
24
40
  }
@@ -28,17 +44,11 @@ function isSqliteBusy(error) {
28
44
  && "errcode" in error
29
45
  && error.errcode === SQLITE_BUSY;
30
46
  }
31
- function defaultRunGit(cwd, args, input) {
32
- return spawnSync("git", [...args], {
33
- cwd,
34
- input,
35
- encoding: "utf8",
36
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
37
- stdio: ["pipe", "pipe", "pipe"],
38
- });
47
+ async function defaultRunGit(cwd, args, input) {
48
+ return await runGitProcess(cwd, args, { input });
39
49
  }
40
50
  function nearestDirectoryBoard(cwd) {
41
- const original = path.resolve(cwd);
51
+ const original = path.resolve(normalizeFilesystemCoordinate(cwd));
42
52
  let candidate = original;
43
53
  while (true) {
44
54
  if (lstatIfPresent(path.join(candidate, ".keiyaku")))
@@ -53,7 +63,8 @@ export function taskBoardRootFromGitDir(rawGitDir, platform = process.platform)
53
63
  return gitCommonRootPath(rawGitDir, platform);
54
64
  }
55
65
  export async function locateTaskBoard(cwd, runGitProbe = defaultRunGit) {
56
- const probe = runGitProbe(cwd, ["rev-parse", "--is-inside-work-tree", "--is-bare-repository"]);
66
+ cwd = normalizeFilesystemCoordinate(cwd);
67
+ const probe = await runGitProbe(cwd, ["rev-parse", "--is-inside-work-tree", "--is-bare-repository"]);
57
68
  if (probe.error && !isGitExecutableUnavailable(probe.error)) {
58
69
  throw new FlowError("INTERNAL_STATE", `cannot inspect task repository: ${probe.error.message}`, { cause: probe.error });
59
70
  }
@@ -71,7 +82,7 @@ export async function locateTaskBoard(cwd, runGitProbe = defaultRunGit) {
71
82
  if (bare === "true" || insideWorktree !== "true") {
72
83
  throw new FlowError("NOT_GIT_REPO", "bare repositories are not supported for task storage");
73
84
  }
74
- const facts = runGitProbe(cwd, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"]);
85
+ const facts = await runGitProbe(cwd, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"]);
75
86
  if (facts.error) {
76
87
  throw new FlowError("INTERNAL_STATE", `git rev-parse failed: ${facts.error.message}`, { cause: facts.error });
77
88
  }
@@ -138,6 +149,31 @@ function acquireTaskLock(lockPath) {
138
149
  throw new FlowError("INTERNAL_STATE", `cannot acquire task lock: ${errorText(error)}`, { cause: error });
139
150
  }
140
151
  }
152
+ async function acquireTaskLockWithAdmission(lockPath, deps) {
153
+ const now = deps.now ?? Date.now;
154
+ const random = deps.random ?? Math.random;
155
+ const sleep = deps.sleep ?? ((milliseconds, signal) => (delay(milliseconds, undefined, signal ? { signal } : undefined)));
156
+ const deadline = now() + TASK_LOCK_ADMISSION_TIMEOUT_MS;
157
+ let delayCapMs = TASK_LOCK_ADMISSION_INITIAL_DELAY_MS;
158
+ for (;;) {
159
+ deps.signal?.throwIfAborted();
160
+ try {
161
+ return acquireTaskLock(lockPath);
162
+ }
163
+ catch (error) {
164
+ if (!(error instanceof TaskLockHeldError))
165
+ throw error;
166
+ const observedAt = now();
167
+ if (observedAt >= deadline) {
168
+ throw new TaskLockAdmissionTimeoutError(lockPath, TASK_LOCK_ADMISSION_TIMEOUT_MS, { cause: error });
169
+ }
170
+ const unit = Math.max(0, Math.min(1, random()));
171
+ const jitteredDelayMs = Math.ceil((delayCapMs / 2) + ((delayCapMs / 2) * unit));
172
+ await sleep(Math.min(jitteredDelayMs, deadline - observedAt), deps.signal);
173
+ delayCapMs = Math.min(TASK_LOCK_ADMISSION_MAX_DELAY_MS, delayCapMs * 2);
174
+ }
175
+ }
176
+ }
141
177
  export async function inspectTaskLock(cwd) {
142
178
  const lockPath = taskLockPath(await locateTaskBoard(cwd));
143
179
  if (!fs.existsSync(lockPath))
@@ -177,7 +213,7 @@ export function atomicReplaceTaskFile(filePath, text, _deps) {
177
213
  fs.closeSync(descriptor);
178
214
  descriptor = undefined;
179
215
  fs.renameSync(temporary, filePath);
180
- fsyncDirectory(directory);
216
+ syncDirectorySync(directory);
181
217
  }
182
218
  catch (error) {
183
219
  if (descriptor !== undefined) {
@@ -193,15 +229,6 @@ export function atomicReplaceTaskFile(filePath, text, _deps) {
193
229
  throw error;
194
230
  }
195
231
  }
196
- function fsyncDirectory(directory) {
197
- const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
198
- try {
199
- fs.fsyncSync(descriptor);
200
- }
201
- finally {
202
- fs.closeSync(descriptor);
203
- }
204
- }
205
232
  function lstatIfPresent(target) {
206
233
  try {
207
234
  return fs.lstatSync(target);
@@ -244,7 +271,7 @@ function validateWrites(writes) {
244
271
  const targets = new Set();
245
272
  for (const write of writes) {
246
273
  const target = `${write.id}.md`;
247
- if (!TASK_ID.test(write.id)) {
274
+ if (!isTaskId(write.id)) {
248
275
  throw new FlowError("INTERNAL_STATE", `invalid task mutation id: ${write.id}`);
249
276
  }
250
277
  if (targets.has(target))
@@ -276,7 +303,7 @@ function sameSources(left, right) {
276
303
  function removeTaskFile(filePath) {
277
304
  try {
278
305
  fs.unlinkSync(filePath);
279
- fsyncDirectory(path.dirname(filePath));
306
+ syncDirectorySync(path.dirname(filePath));
280
307
  }
281
308
  catch (error) {
282
309
  if (error.code !== "ENOENT")
@@ -329,9 +356,32 @@ export async function openTaskStore(cwd, deps = {}) {
329
356
  },
330
357
  async mutate(apply) {
331
358
  return withLockHeldTaskBoard(cwd, async (transaction) => {
359
+ const previousSources = transaction.sources;
332
360
  const plan = await apply(transaction.sources);
333
361
  validatePlan(plan);
362
+ let subject;
363
+ if (transaction.location.mode === "git") {
364
+ subject = plan.subject?.trim();
365
+ if (!subject) {
366
+ throw new FlowError("INTERNAL_STATE", "git task mutation requires a micro-commit subject");
367
+ }
368
+ await inspectCurrentBranch(transaction.location.root);
369
+ }
334
370
  await transaction.replace(plan.writes);
371
+ if (transaction.location.mode === "git") {
372
+ try {
373
+ await (deps.publishMutation ?? publishTaskMutationCommit)({
374
+ cwd: transaction.location.root,
375
+ gitDir: transaction.location.gitDir,
376
+ writes: plan.writes,
377
+ subject: subject,
378
+ });
379
+ }
380
+ catch (error) {
381
+ await transaction.restore(plan.writes, previousSources);
382
+ throw error;
383
+ }
384
+ }
335
385
  return plan.value;
336
386
  }, deps, board);
337
387
  },
@@ -341,7 +391,7 @@ export async function withLockHeldTaskBoard(cwd, apply, deps = {}, locatedBoard)
341
391
  const board = locatedBoard ?? await locateTaskBoard(cwd, deps.runGit);
342
392
  const directory = taskDirectory(board);
343
393
  assertKeiyakuDirectory(board);
344
- const database = acquireTaskLock(taskLockPath(board));
394
+ const database = await acquireTaskLockWithAdmission(taskLockPath(board), deps);
345
395
  try {
346
396
  let sources = await readTaskSources(directory);
347
397
  const transaction = {
@@ -376,6 +426,19 @@ export async function withLockHeldTaskBoard(cwd, apply, deps = {}, locatedBoard)
376
426
  }
377
427
  sources = await readTaskSources(directory);
378
428
  },
429
+ async restore(writes, previousSources) {
430
+ validateWrites(writes);
431
+ const currentSources = await readTaskSources(directory);
432
+ if (!sameSources(sources, currentSources)) {
433
+ throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while rolling back a failed publication");
434
+ }
435
+ const previousById = new Map(previousSources.map((source) => [path.basename(source.path, ".md"), source.text]));
436
+ restoreFiles(writes.map((write) => ({
437
+ path: path.join(directory, mutationTarget(write)),
438
+ text: previousById.get(write.id),
439
+ })), deps);
440
+ sources = await readTaskSources(directory);
441
+ },
379
442
  };
380
443
  return await apply(transaction);
381
444
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
- import { requireHealthy } from "./task/board.js";
2
- import { parseTaskDocument, serializeTaskDocument } from "./task/document.js";
3
- import { buildTaskSourceBoard } from "./task/source-board.js";
4
- function taskBoardFromSourceBoard(sourceBoard) {
1
+ import { requireHealthy } from "./board.js";
2
+ import { parseTaskDocument, serializeTaskDocument } from "./document.js";
3
+ import { buildTaskSourceBoard } from "./source-board.js";
4
+ export function taskBoardFromSourceBoard(sourceBoard) {
5
5
  const tasks = sourceBoard.tasks.map((task) => ({
6
6
  id: task.id,
7
7
  title: task.title,
@@ -66,6 +66,7 @@ function mutationPlan(sources, mutation) {
66
66
  return {
67
67
  writes: prepared.writes,
68
68
  value: mutation.select(prepared.board),
69
+ ...(mutation.subject === undefined ? {} : { subject: mutation.subject }),
69
70
  };
70
71
  }
71
72
  export function taskRepositoryFromStore(store) {
@@ -0,0 +1 @@
1
+ export {};