@astrosheep/keiyaku 2.8.4 → 2.9.0

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 (229) hide show
  1. package/README.md +35 -15
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
  4. package/build/agents/harness/control-types.js +1 -0
  5. package/build/agents/harness/execution-handle.js +23 -3
  6. package/build/agents/harness/index.js +6 -607
  7. package/build/agents/harness/outcome.js +141 -0
  8. package/build/agents/harness/projection.js +310 -0
  9. package/build/agents/harness/router.js +201 -0
  10. package/build/agents/harness/runtime.js +2 -1
  11. package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
  12. package/build/agents/launch-snapshot/resolve.js +72 -0
  13. package/build/agents/opencode-sdk.js +24 -3
  14. package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
  15. package/build/agents/providers/claude-agent-sdk/events.js +218 -0
  16. package/build/agents/providers/claude-agent-sdk/session.js +158 -0
  17. package/build/agents/providers/codex-app-server/adapter.js +347 -0
  18. package/build/agents/providers/codex-app-server/events.js +352 -0
  19. package/build/agents/providers/codex-app-server/session.js +266 -0
  20. package/build/agents/providers/codex-sdk.js +23 -5
  21. package/build/agents/providers/opencode-sdk/adapter.js +344 -0
  22. package/build/agents/providers/opencode-sdk/events.js +354 -0
  23. package/build/agents/providers/opencode-sdk/session.js +180 -0
  24. package/build/agents/providers/pi/adapter.js +269 -0
  25. package/build/agents/providers/pi/checkpoint.js +41 -0
  26. package/build/agents/providers/pi/events.js +282 -0
  27. package/build/agents/{selector_v2.js → selector.js} +8 -6
  28. package/build/cli/commands/akuma/akuma/handler.js +4 -0
  29. package/build/cli/commands/akuma/akuma/meta.js +16 -0
  30. package/build/cli/commands/akuma/catalog.js +8 -0
  31. package/build/cli/commands/akuma/list/handler.js +15 -0
  32. package/build/cli/commands/akuma/list/meta-ls.js +9 -0
  33. package/build/cli/commands/akuma/view/handler.js +34 -0
  34. package/build/cli/commands/akuma/view/meta.js +8 -0
  35. package/build/cli/commands/akuma.js +1 -1
  36. package/build/cli/commands/contract/amend/handler.js +10 -0
  37. package/build/cli/commands/contract/amend/meta.js +12 -0
  38. package/build/cli/commands/contract/arc/handler.js +9 -0
  39. package/build/cli/commands/contract/arc/meta.js +26 -0
  40. package/build/cli/commands/contract/bind/handler.js +23 -0
  41. package/build/cli/commands/contract/bind/meta.js +30 -0
  42. package/build/cli/commands/contract/catalog.js +16 -0
  43. package/build/cli/commands/contract/forfeit/handler.js +13 -0
  44. package/build/cli/commands/contract/forfeit/meta.js +12 -0
  45. package/build/cli/commands/contract/log/handler.js +12 -0
  46. package/build/cli/commands/contract/log/meta.js +12 -0
  47. package/build/cli/commands/contract/petition/handler.js +26 -0
  48. package/build/cli/commands/contract/petition/meta.js +23 -0
  49. package/build/cli/commands/contract/renew/handler.js +10 -0
  50. package/build/cli/commands/contract/renew/meta.js +17 -0
  51. package/build/cli/commands/projection/call/handler.js +39 -0
  52. package/build/cli/commands/projection/call/meta.js +9 -0
  53. package/build/cli/commands/projection/catalog.js +14 -0
  54. package/build/cli/commands/projection/kill/handler.js +35 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +53 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +23 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +34 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +21 -0
  63. package/build/cli/commands/projection/wait/meta.js +11 -0
  64. package/build/cli/commands/registry.js +100 -0
  65. package/build/cli/commands/shared.js +71 -0
  66. package/build/cli/commands/skills/catalog.js +6 -0
  67. package/build/cli/commands/skills/install/handler.js +10 -0
  68. package/build/cli/commands/skills/install/meta.js +9 -0
  69. package/build/cli/commands/skills/skills/handler.js +4 -0
  70. package/build/cli/commands/skills/skills/meta.js +10 -0
  71. package/build/cli/commands/system/catalog.js +8 -0
  72. package/build/cli/commands/system/completion/handler.js +10 -0
  73. package/build/cli/commands/system/completion/meta.js +9 -0
  74. package/build/cli/commands/system/dump-env/handler.js +3 -0
  75. package/build/cli/commands/system/dump-env/meta.js +10 -0
  76. package/build/cli/commands/system/guide/handler.js +4 -0
  77. package/build/cli/commands/system/guide/meta.js +9 -0
  78. package/build/cli/commands/task/add/handler.js +21 -0
  79. package/build/cli/commands/task/add/meta.js +8 -0
  80. package/build/cli/commands/task/catalog.js +22 -0
  81. package/build/cli/commands/task/doctor/handler.js +4 -0
  82. package/build/cli/commands/task/doctor/meta.js +8 -0
  83. package/build/cli/commands/task/done/handler.js +17 -0
  84. package/build/cli/commands/task/done/meta.js +8 -0
  85. package/build/cli/commands/task/drop/handler.js +6 -0
  86. package/build/cli/commands/task/drop/meta.js +8 -0
  87. package/build/cli/commands/task/ls/handler.js +13 -0
  88. package/build/cli/commands/task/ls/meta.js +8 -0
  89. package/build/cli/commands/task/shared.js +131 -0
  90. package/build/cli/commands/task/start/handler.js +6 -0
  91. package/build/cli/commands/task/start/meta.js +8 -0
  92. package/build/cli/commands/task/stop/handler.js +17 -0
  93. package/build/cli/commands/task/stop/meta.js +8 -0
  94. package/build/cli/commands/task/task/handler.js +5 -0
  95. package/build/cli/commands/task/task/meta.js +11 -0
  96. package/build/cli/commands/task/update/handler.js +18 -0
  97. package/build/cli/commands/task/update/meta.js +8 -0
  98. package/build/cli/commands/task/view/handler.js +14 -0
  99. package/build/cli/commands/task/view/meta.js +8 -0
  100. package/build/cli/completion.js +26 -27
  101. package/build/cli/flags.js +79 -2
  102. package/build/cli/help.js +27 -49
  103. package/build/cli/index.js +121 -374
  104. package/build/cli/output-stream-error-policy.js +17 -0
  105. package/build/cli/parse.js +159 -72
  106. package/build/cli/projection-address.js +5 -0
  107. package/build/cli/render/arc.js +1 -1
  108. package/build/cli/render/errors.js +4 -3
  109. package/build/cli/render/format.js +14 -0
  110. package/build/cli/render/kanshi.js +106 -0
  111. package/build/cli/render/petition.js +24 -1
  112. package/build/cli/render/projection-activity.js +5 -1
  113. package/build/cli/render/shared.js +2 -2
  114. package/build/cli/render/status.js +79 -13
  115. package/build/cli/render/task.js +113 -0
  116. package/build/cli/render/wait.js +23 -3
  117. package/build/cli/types.js +1 -1
  118. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
  119. package/build/config/env.js +9 -4
  120. package/build/config/settings/disease.js +26 -0
  121. package/build/config/settings/knobs.js +8 -0
  122. package/build/config/settings/loader.js +244 -0
  123. package/build/config/settings/schema.js +276 -0
  124. package/build/core/addressing.js +1 -1
  125. package/build/core/amend.js +3 -3
  126. package/build/core/arc.js +5 -4
  127. package/build/core/bind-workspace.js +123 -0
  128. package/build/core/bind.js +75 -155
  129. package/build/core/call/call.js +224 -0
  130. package/build/core/call/context.js +238 -0
  131. package/build/core/call/execution.js +217 -0
  132. package/build/core/call/prompt.js +42 -0
  133. package/build/core/call/reference.js +24 -0
  134. package/build/core/call-persist.js +1 -1
  135. package/build/core/claim-delivery.js +247 -0
  136. package/build/core/claim.js +39 -277
  137. package/build/core/context.js +2 -1
  138. package/build/core/{render.js → contract-view.js} +12 -140
  139. package/build/core/draft.js +107 -9
  140. package/build/core/entry.js +9 -1
  141. package/build/core/execution-coordinate.js +4 -20
  142. package/build/core/execution-pact.js +16 -31
  143. package/build/core/forfeit.js +35 -22
  144. package/build/core/hints.js +2 -2
  145. package/build/core/log.js +2 -2
  146. package/build/core/petition-claim-gates.js +8 -6
  147. package/build/core/petition-preview.js +97 -0
  148. package/build/core/petition-run.js +3 -2
  149. package/build/core/petition.js +39 -26
  150. package/build/core/projection/generation/database.js +306 -0
  151. package/build/core/projection/generation/identity.js +84 -0
  152. package/build/core/projection/generation/model.js +201 -0
  153. package/build/core/projection/generation/store.js +73 -0
  154. package/build/core/projection/generation/transitions.js +258 -0
  155. package/build/core/projection/heart.js +0 -307
  156. package/build/core/projection/tell/database.js +109 -0
  157. package/build/core/projection/tell/model.js +97 -0
  158. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  159. package/build/core/projection-activity.js +23 -2
  160. package/build/core/projection-core.js +2 -1
  161. package/build/core/projection-execution-observer.js +259 -0
  162. package/build/core/projection-generation-continuation.js +1 -1
  163. package/build/core/projection-generation-execution.js +90 -20
  164. package/build/core/projection-generation-launcher.js +1 -1
  165. package/build/core/projection-generation-runner.js +198 -4
  166. package/build/core/projection-identity.js +1 -1
  167. package/build/core/projection-kill.js +50 -17
  168. package/build/core/projection-life-observer.js +1 -1
  169. package/build/core/projection-life-protocol.js +10 -0
  170. package/build/core/projection-mint.js +1 -1
  171. package/build/core/projection-status.js +45 -32
  172. package/build/core/projection-wait.js +12 -2
  173. package/build/core/projection-wake.js +5 -2
  174. package/build/core/{queue_v2.js → queue.js} +2 -1
  175. package/build/core/registry.js +1 -1
  176. package/build/core/renew.js +7 -7
  177. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  178. package/build/core/response-artifact-catalog.js +7 -0
  179. package/build/core/seal.js +44 -16
  180. package/build/core/status/board.js +286 -0
  181. package/build/core/status/drift.js +99 -0
  182. package/build/core/status/lifecycle.js +84 -0
  183. package/build/core/status/reconciliation.js +131 -0
  184. package/build/core/stored-agent-event.js +28 -4
  185. package/build/core/target-ref.js +10 -0
  186. package/build/core/task/board.js +175 -0
  187. package/build/core/task/commands.js +211 -0
  188. package/build/core/task/document.js +189 -0
  189. package/build/core/task/model.js +1 -0
  190. package/build/core/task/settlement-git.js +207 -0
  191. package/build/core/task/settlement-policy.js +82 -0
  192. package/build/core/task/source-board.js +84 -0
  193. package/build/core/task/source-model.js +1 -0
  194. package/build/core/task/validation.js +44 -0
  195. package/build/core/task-contract.js +224 -0
  196. package/build/core/task-doctor.js +14 -0
  197. package/build/core/task-git-actor.js +38 -0
  198. package/build/core/task-git-runtime.js +34 -0
  199. package/build/core/task-git-store.js +375 -0
  200. package/build/core/task-repository.js +1 -0
  201. package/build/core/task-store-repository.js +86 -0
  202. package/build/core/task-store.js +1 -0
  203. package/build/core/task-worktree.js +51 -0
  204. package/build/core/task.js +4 -0
  205. package/build/core/transcripts.js +138 -56
  206. package/build/core/validation.js +17 -0
  207. package/build/core/verdict.js +2 -2
  208. package/build/core/worktree-path.js +24 -6
  209. package/build/flow-error.js +3 -2
  210. package/build/generated/version.js +1 -1
  211. package/build/git/commits.js +3 -0
  212. package/build/git/diff/parsers.js +0 -94
  213. package/build/index.js +2 -0
  214. package/build/keiyaku-constants.js +2 -0
  215. package/build/keiyaku.js +4 -3
  216. package/package.json +3 -2
  217. package/skills/keiyaku/SKILL.md +1 -1
  218. package/skills/keiyaku-akuma/SKILL.md +11 -6
  219. package/build/agents/harness/pump.js +0 -158
  220. package/build/agents/providers/codex-app-server.js +0 -864
  221. package/build/agents/providers/opencode-sdk.js +0 -363
  222. package/build/agents/providers/pi.js +0 -557
  223. package/build/config/settings.js +0 -533
  224. package/build/core/call.js +0 -686
  225. package/build/core/projection/generation.js +0 -412
  226. package/build/core/projection/identity.js +0 -81
  227. package/build/core/projection/mint.js +0 -174
  228. package/build/core/projection-generation-store.js +0 -707
  229. package/build/core/status.js +0 -539
@@ -0,0 +1,224 @@
1
+ import { FlowError } from "../flow-error.js";
2
+ import { withLockHeldTaskBoard } from "./task-git-store.js";
3
+ import { prepareTaskDocumentReplacements, taskRepositoryFromLockedBoard } from "./task-store-repository.js";
4
+ import { containsCommit, createLandingCommit, createTaskBoardCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, readableRefHead, reconcileTaskIndex, refHead, sameTaskSourceBytes, taskContractAtCommit, taskWritesFromSources, updateBranch, updateTargetRef, } from "./task/settlement-git.js";
5
+ import { contractPointerDocument, doneTaskReplacement, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTaskForBind, taskForContract, } from "./task/settlement-policy.js";
6
+ import { stableRepoRoot } from "./worktree-path.js";
7
+ async function clearAttemptedPointer(input) {
8
+ const current = requireSettledTask(await input.repository.read(), input.task.id);
9
+ if (current.contractId === input.contractId) {
10
+ await replaceTaskDocument(input.repository, contractPointerDocument(current, undefined));
11
+ }
12
+ }
13
+ async function replaceTaskDocument(repository, replacement) {
14
+ return repository.mutate(() => ({
15
+ replacements: [replacement],
16
+ select: (resulting) => requireSettledTask(resulting, replacement.id),
17
+ }));
18
+ }
19
+ export async function settleTaskBoardForBind(input, deps = {}) {
20
+ return withLockHeldTaskBoard(input.cwd, async (transaction) => {
21
+ if (transaction.location.mode !== "git") {
22
+ throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
23
+ }
24
+ const hubCwd = transaction.location.root;
25
+ const repository = taskRepositoryFromLockedBoard(transaction);
26
+ const branch = inspectCurrentBranch(hubCwd);
27
+ const board = requireHealthySettlementBoard(await repository.read());
28
+ const selected = input.taskId === undefined
29
+ ? undefined
30
+ : selectReadyTaskForBind(board, input.taskId);
31
+ if (selected) {
32
+ await replaceTaskDocument(repository, contractPointerDocument(selected, input.contractId));
33
+ }
34
+ const firstCommit = createTaskBoardCommit({
35
+ cwd: hubCwd,
36
+ gitDir: transaction.location.gitDir,
37
+ root: transaction.location.root,
38
+ parent: branch.head,
39
+ sources: transaction.sources,
40
+ subject: selected
41
+ ? `Bind task ${selected.id} to contract ${input.contractId}`
42
+ : `Flush task board for contract ${input.contractId}`,
43
+ });
44
+ try {
45
+ await updateBranch({ cwd: hubCwd, ref: branch.ref, next: firstCommit, expected: branch.head }, deps);
46
+ }
47
+ catch (error) {
48
+ if (selected) {
49
+ await clearAttemptedPointer({ repository, task: selected, contractId: input.contractId });
50
+ const concurrentHead = refHead(hubCwd, branch.ref);
51
+ if (taskContractAtCommit(hubCwd, concurrentHead, selected.id) === input.contractId) {
52
+ await publishForwardTaskBoardCommit({
53
+ cwd: hubCwd,
54
+ ref: branch.ref,
55
+ gitDir: transaction.location.gitDir,
56
+ root: transaction.location.root,
57
+ sources: transaction.sources,
58
+ subject: `Clear failed contract ${input.contractId} from task ${selected.id}`,
59
+ }, deps);
60
+ }
61
+ }
62
+ throw error;
63
+ }
64
+ try {
65
+ reconcileTaskIndex(hubCwd, firstCommit);
66
+ }
67
+ catch (indexError) {
68
+ if (selected) {
69
+ await clearAttemptedPointer({ repository, task: selected, contractId: input.contractId });
70
+ await publishForwardTaskBoardCommit({
71
+ cwd: hubCwd,
72
+ ref: branch.ref,
73
+ gitDir: transaction.location.gitDir,
74
+ root: transaction.location.root,
75
+ sources: transaction.sources,
76
+ subject: `Clear failed contract ${input.contractId} from task ${selected.id}`,
77
+ }, deps);
78
+ }
79
+ throw indexError;
80
+ }
81
+ try {
82
+ const ledger = await input.publishLedger(firstCommit);
83
+ return {
84
+ boardCommit: firstCommit,
85
+ ledger,
86
+ ...(selected ? { task: requireSettledTask(await repository.read(), selected.id) } : {}),
87
+ };
88
+ }
89
+ catch (publishError) {
90
+ if (!selected)
91
+ throw publishError;
92
+ await clearAttemptedPointer({ repository, task: selected, contractId: input.contractId });
93
+ await publishForwardTaskBoardCommit({
94
+ cwd: hubCwd,
95
+ ref: branch.ref,
96
+ gitDir: transaction.location.gitDir,
97
+ root: transaction.location.root,
98
+ sources: transaction.sources,
99
+ subject: `Clear failed contract ${input.contractId} from task ${selected.id}`,
100
+ }, deps);
101
+ throw publishError;
102
+ }
103
+ });
104
+ }
105
+ export async function reconcileTaskForfeit(input) {
106
+ const hubRoot = await stableRepoRoot(input.cwd);
107
+ return withLockHeldTaskBoard(hubRoot, async (transaction) => {
108
+ if (transaction.location.mode !== "git") {
109
+ throw new FlowError("NOT_GIT_REPO", "task forfeit requires Git task storage");
110
+ }
111
+ const repository = taskRepositoryFromLockedBoard(transaction);
112
+ const matched = taskForContract(await repository.read(), input.contractId);
113
+ const ledger = await input.publishLedger();
114
+ if (!matched)
115
+ return { ledger };
116
+ const task = await replaceTaskDocument(repository, reopenedTaskDocument(matched));
117
+ return {
118
+ ledger,
119
+ task,
120
+ };
121
+ });
122
+ }
123
+ /**
124
+ * Claim owns the sole task-board settlement after target CAS. Contract branch
125
+ * task trees are fossils: the candidate is the code-only three-way merge plus
126
+ * one complete snapshot of the common hub board held under its repository lock.
127
+ */
128
+ export async function settleTaskBoardForLanding(input) {
129
+ const hubRoot = await stableRepoRoot(input.cwd);
130
+ let lastTargetHead = input.targetHead;
131
+ let lastPublishError;
132
+ let acceptedMovement;
133
+ for (let attempt = 0; attempt < 4; attempt += 1) {
134
+ const result = await withLockHeldTaskBoard(hubRoot, async (transaction) => {
135
+ if (transaction.location.mode !== "git") {
136
+ throw new FlowError("NOT_GIT_REPO", "task landing requires Git task storage");
137
+ }
138
+ // Each attempt reads fresh target and board facts while holding the
139
+ // common task lock. A losing CAS exits before the next attempt.
140
+ const targetHead = refHead(input.cwd, input.targetRef);
141
+ lastTargetHead = targetHead;
142
+ const repository = taskRepositoryFromLockedBoard(transaction);
143
+ const board = requireHealthySettlementBoard(await repository.read());
144
+ const replacement = doneTaskReplacement(board, input.contractId);
145
+ const prepared = replacement
146
+ ? prepareTaskDocumentReplacements(transaction.sources, [replacement])
147
+ : { sources: transaction.sources, board };
148
+ const finalSources = prepared.sources;
149
+ requireHealthySettlementBoard(prepared.board);
150
+ let mergeCommit = targetHead;
151
+ let movement = acceptedMovement?.mergeCommit === targetHead ? acceptedMovement : undefined;
152
+ if (!containsCommit(input.cwd, input.deliveryHead, targetHead)) {
153
+ mergeCommit = createLandingCommit({
154
+ cwd: input.cwd,
155
+ gitDir: transaction.location.gitDir,
156
+ root: transaction.location.root,
157
+ base: input.baseCommit,
158
+ target: targetHead,
159
+ delivery: input.deliveryHead,
160
+ sources: finalSources,
161
+ contractId: input.contractId,
162
+ targetBranch: input.targetBranch,
163
+ });
164
+ await input.beforeCas?.(mergeCommit, targetHead);
165
+ try {
166
+ updateTargetRef(input.cwd, input.targetRef, mergeCommit, targetHead);
167
+ movement = { mergeCommit, oldHead: targetHead };
168
+ acceptedMovement = movement;
169
+ }
170
+ catch {
171
+ const current = readableRefHead(input.cwd, input.targetRef);
172
+ if (!current || !containsCommit(input.cwd, input.deliveryHead, current)) {
173
+ return { kind: "retry" };
174
+ }
175
+ mergeCommit = current;
176
+ movement = acceptedMovement?.mergeCommit === mergeCommit ? acceptedMovement : undefined;
177
+ }
178
+ }
179
+ const warnings = [];
180
+ if (!sameTaskSourceBytes(transaction.location.root, transaction.sources, finalSources)) {
181
+ await transaction.replace(taskWritesFromSources(finalSources));
182
+ }
183
+ try {
184
+ reconcileTaskIndex(transaction.location.root, mergeCommit);
185
+ }
186
+ catch (error) {
187
+ warnings.push(`Accepted landing ${mergeCommit}, but task index reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
188
+ }
189
+ try {
190
+ await input.publishLedger?.(mergeCommit);
191
+ }
192
+ catch (error) {
193
+ lastPublishError = error;
194
+ return { kind: "retry" };
195
+ }
196
+ const reconcileWarnings = movement
197
+ ? await input.afterCas?.(mergeCommit, movement.oldHead)
198
+ : undefined;
199
+ warnings.push(...(reconcileWarnings ?? []));
200
+ return {
201
+ kind: movement ? "accepted" : "idempotent",
202
+ mergeCommit,
203
+ ...(movement ? { oldHead: movement.oldHead } : {}),
204
+ ...(warnings.length > 0 ? { warnings } : {}),
205
+ };
206
+ });
207
+ if (result.kind === "retry")
208
+ continue;
209
+ if (result.kind === "idempotent") {
210
+ return {
211
+ mergeCommit: result.mergeCommit,
212
+ ...(result.warnings ? { warnings: result.warnings } : {}),
213
+ };
214
+ }
215
+ return {
216
+ mergeCommit: result.mergeCommit,
217
+ oldHead: result.oldHead,
218
+ ...(result.warnings ? { warnings: result.warnings } : {}),
219
+ };
220
+ }
221
+ if (lastPublishError !== undefined)
222
+ throw lastPublishError;
223
+ throw new FlowError("CONCURRENT_MODIFICATION", `target branch kept moving while claiming ${input.contractId}; last observed ${lastTargetHead}`);
224
+ }
@@ -0,0 +1,14 @@
1
+ import { rollbackOrphanTaskPointers } from "./task.js";
2
+ import { buildTaskSourceBoard } from "./task/source-board.js";
3
+ export async function inspectTaskDoctor(store, repository, facts) {
4
+ const orphans = facts.lockHeld ? [] : await rollbackOrphanTaskPointers(repository, facts.contractIds);
5
+ const sources = await store.read();
6
+ const board = buildTaskSourceBoard(sources);
7
+ return {
8
+ board,
9
+ sources,
10
+ orphans,
11
+ landingDiagnostics: facts.landingDiagnostics ?? [],
12
+ lockHeld: facts.lockHeld === true,
13
+ };
14
+ }
@@ -0,0 +1,38 @@
1
+ import { spawnSync } from "node:child_process";
2
+ const ANONYMOUS_TASK_CREATOR = "(anonymous)";
3
+ const TASK_ACTOR_ENV = "KEIYAKU_ACTOR";
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
+ });
12
+ }
13
+ function attribution(value) {
14
+ const trimmed = value?.trim();
15
+ return trimmed ? trimmed : undefined;
16
+ }
17
+ export function resolveTaskCreator(cwd, deps = {}) {
18
+ const sourceEnv = deps.env ?? process.env;
19
+ const environmentActor = attribution(sourceEnv[TASK_ACTOR_ENV]);
20
+ if (environmentActor)
21
+ return environmentActor;
22
+ const gitEnv = { ...sourceEnv, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
23
+ const runGit = deps.runGit ?? defaultRunGit;
24
+ for (const key of GIT_ACTOR_KEYS) {
25
+ try {
26
+ const result = runGit(cwd, ["config", "--get", key], gitEnv);
27
+ if (!result.error && result.status === 0) {
28
+ const configuredActor = attribution(result.stdout);
29
+ if (configuredActor)
30
+ return configuredActor;
31
+ }
32
+ }
33
+ catch {
34
+ // Creator attribution is advisory and must not prevent task creation.
35
+ }
36
+ }
37
+ return ANONYMOUS_TASK_CREATOR;
38
+ }
@@ -0,0 +1,34 @@
1
+ import { isGitRepo } from "../git/branches.js";
2
+ import { listContractIds } from "./ledger.js";
3
+ import { contractBoundByEngineWorktree, stableRepoRoot } from "./worktree-path.js";
4
+ import { inspectTaskLock, openTaskStore } from "./task-git-store.js";
5
+ import { taskRepositoryFromStore } from "./task-store-repository.js";
6
+ import { withTaskWorktreePolicy } from "./task-worktree.js";
7
+ import { inspectTaskDoctor } from "./task-doctor.js";
8
+ export async function taskCommandContractId(cwd) {
9
+ if (!(await isGitRepo(cwd)))
10
+ return undefined;
11
+ return contractBoundByEngineWorktree(cwd);
12
+ }
13
+ export async function openTaskCommandRepository(cwd) {
14
+ const store = await openTaskStore(cwd);
15
+ const repository = taskRepositoryFromStore(store);
16
+ if (store.mode !== "git")
17
+ return repository;
18
+ const contractId = await contractBoundByEngineWorktree(cwd);
19
+ return contractId ? withTaskWorktreePolicy(repository, contractId) : repository;
20
+ }
21
+ export async function taskContractIds(cwd) {
22
+ return new Set(await listContractIds(await stableRepoRoot(cwd)));
23
+ }
24
+ export async function readTaskDoctor(cwd) {
25
+ const store = await openTaskStore(cwd);
26
+ const repository = taskRepositoryFromStore(store);
27
+ const gitMode = store.mode === "git";
28
+ const lockHeld = (await inspectTaskLock(cwd)).state === "held";
29
+ return inspectTaskDoctor(store, repository, {
30
+ contractIds: gitMode ? await taskContractIds(cwd) : new Set(),
31
+ landingDiagnostics: [],
32
+ lockHeld,
33
+ });
34
+ }
@@ -0,0 +1,375 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { FlowError } from "../flow-error.js";
7
+ const TASK_ID = /^k-[0-9a-f]{12}$/;
8
+ const SQLITE_BUSY = 5;
9
+ const LOCK_FILE = "task-lock.sqlite";
10
+ const GIT_LOCK_FILE = "keiyaku-task-lock.sqlite";
11
+ export class TaskLockHeldError extends Error {
12
+ path;
13
+ code = "TASK_LOCK_HELD";
14
+ constructor(path, options) {
15
+ super(`task lock is already held: ${path}`, options);
16
+ this.path = path;
17
+ this.name = "TaskLockHeldError";
18
+ }
19
+ }
20
+ function errorText(error) {
21
+ return error instanceof Error ? error.message : String(error);
22
+ }
23
+ function isSqliteBusy(error) {
24
+ return typeof error === "object"
25
+ && error !== null
26
+ && "errcode" in error
27
+ && error.errcode === SQLITE_BUSY;
28
+ }
29
+ function defaultRunGit(cwd, args, input) {
30
+ return spawnSync("git", [...args], {
31
+ cwd,
32
+ input,
33
+ encoding: "utf8",
34
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ });
37
+ }
38
+ function runGit(cwd, args, input) {
39
+ const result = defaultRunGit(cwd, args, input);
40
+ if (result.error) {
41
+ throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
42
+ }
43
+ if (result.status !== 0) {
44
+ const detail = result.stderr.trim() || `exit ${String(result.status)}`;
45
+ throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${detail}`);
46
+ }
47
+ return result.stdout;
48
+ }
49
+ function nearestDirectoryBoard(cwd) {
50
+ const original = path.resolve(cwd);
51
+ let candidate = original;
52
+ while (true) {
53
+ if (lstatIfPresent(path.join(candidate, ".keiyaku")))
54
+ return candidate;
55
+ const parent = path.dirname(candidate);
56
+ if (parent === candidate)
57
+ return original;
58
+ candidate = parent;
59
+ }
60
+ }
61
+ export async function locateTaskBoard(cwd) {
62
+ const probe = defaultRunGit(cwd, ["rev-parse", "--is-inside-work-tree", "--is-bare-repository"]);
63
+ if (probe.error) {
64
+ throw new FlowError("INTERNAL_STATE", `cannot inspect task repository: ${probe.error.message}`, { cause: probe.error });
65
+ }
66
+ if (probe.status !== 0) {
67
+ return { mode: "directory", root: nearestDirectoryBoard(cwd) };
68
+ }
69
+ const [insideWorktree, bare] = probe.stdout.trim().split(/\r?\n/);
70
+ if (bare === "true" || insideWorktree !== "true") {
71
+ throw new FlowError("NOT_GIT_REPO", "bare repositories are not supported for task storage");
72
+ }
73
+ const facts = runGit(cwd, ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir"], undefined);
74
+ const [checkoutRoot, gitDir] = facts.trimEnd().split(/\r?\n/);
75
+ if (!checkoutRoot || !gitDir || !path.isAbsolute(checkoutRoot) || !path.isAbsolute(gitDir)) {
76
+ throw new FlowError("INTERNAL_STATE", "git returned incomplete task-board location facts");
77
+ }
78
+ return {
79
+ mode: "git",
80
+ root: path.dirname(gitDir),
81
+ gitDir,
82
+ };
83
+ }
84
+ function taskDirectory(board) {
85
+ return path.join(board.root, ".keiyaku", "tasks");
86
+ }
87
+ function assertKeiyakuDirectory(board) {
88
+ const keiyakuDirectory = path.join(board.root, ".keiyaku");
89
+ const stat = lstatIfPresent(keiyakuDirectory);
90
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
91
+ throw new FlowError("INTERNAL_STATE", `.keiyaku is not a regular directory: ${keiyakuDirectory}`);
92
+ }
93
+ }
94
+ function taskLockPath(board) {
95
+ return board.mode === "git"
96
+ ? path.join(board.gitDir, GIT_LOCK_FILE)
97
+ : path.join(board.root, ".keiyaku", LOCK_FILE);
98
+ }
99
+ function closeQuietly(database) {
100
+ try {
101
+ database.close();
102
+ }
103
+ catch {
104
+ // The original operation remains authoritative.
105
+ }
106
+ }
107
+ function acquireTaskLock(lockPath) {
108
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
109
+ let database;
110
+ try {
111
+ database = new DatabaseSync(lockPath);
112
+ }
113
+ catch (error) {
114
+ throw new FlowError("INTERNAL_STATE", `cannot open task lock: ${errorText(error)}`, { cause: error });
115
+ }
116
+ try {
117
+ database.exec("PRAGMA busy_timeout=0");
118
+ database.exec("PRAGMA journal_mode=DELETE");
119
+ database.exec("CREATE TABLE IF NOT EXISTS lock_anchor (id INTEGER PRIMARY KEY CHECK (id = 1))");
120
+ database.exec("BEGIN EXCLUSIVE");
121
+ database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
122
+ return database;
123
+ }
124
+ catch (error) {
125
+ closeQuietly(database);
126
+ if (isSqliteBusy(error))
127
+ throw new TaskLockHeldError(lockPath, { cause: error });
128
+ throw new FlowError("INTERNAL_STATE", `cannot acquire task lock: ${errorText(error)}`, { cause: error });
129
+ }
130
+ }
131
+ export async function inspectTaskLock(cwd) {
132
+ const lockPath = taskLockPath(await locateTaskBoard(cwd));
133
+ if (!fs.existsSync(lockPath))
134
+ return { state: "free", path: lockPath };
135
+ let database;
136
+ try {
137
+ database = new DatabaseSync(lockPath, { readOnly: true });
138
+ }
139
+ catch (error) {
140
+ throw new FlowError("INTERNAL_STATE", `cannot inspect task lock: ${errorText(error)}`, { cause: error });
141
+ }
142
+ try {
143
+ database.exec("PRAGMA busy_timeout=0");
144
+ database.exec("BEGIN DEFERRED");
145
+ database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
146
+ database.exec("ROLLBACK");
147
+ return { state: "free", path: lockPath };
148
+ }
149
+ catch (error) {
150
+ if (isSqliteBusy(error))
151
+ return { state: "held", path: lockPath };
152
+ throw new FlowError("INTERNAL_STATE", `cannot inspect task lock: ${errorText(error)}`, { cause: error });
153
+ }
154
+ finally {
155
+ closeQuietly(database);
156
+ }
157
+ }
158
+ export function atomicReplaceTaskFile(filePath, text, _deps) {
159
+ const directory = path.dirname(filePath);
160
+ fs.mkdirSync(directory, { recursive: true });
161
+ const temporary = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`);
162
+ let descriptor;
163
+ try {
164
+ descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
165
+ fs.writeFileSync(descriptor, text, { encoding: "utf8" });
166
+ fs.fsyncSync(descriptor);
167
+ fs.closeSync(descriptor);
168
+ descriptor = undefined;
169
+ fs.renameSync(temporary, filePath);
170
+ fsyncDirectory(directory);
171
+ }
172
+ catch (error) {
173
+ if (descriptor !== undefined) {
174
+ try {
175
+ fs.closeSync(descriptor);
176
+ }
177
+ catch { /* preserve original failure */ }
178
+ }
179
+ try {
180
+ fs.unlinkSync(temporary);
181
+ }
182
+ catch { /* absent after rename or already cleaned */ }
183
+ throw error;
184
+ }
185
+ }
186
+ function fsyncDirectory(directory) {
187
+ const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
188
+ try {
189
+ fs.fsyncSync(descriptor);
190
+ }
191
+ finally {
192
+ fs.closeSync(descriptor);
193
+ }
194
+ }
195
+ function lstatIfPresent(target) {
196
+ try {
197
+ return fs.lstatSync(target);
198
+ }
199
+ catch (error) {
200
+ if (error.code === "ENOENT")
201
+ return undefined;
202
+ throw error;
203
+ }
204
+ }
205
+ async function readTaskSources(directory) {
206
+ const directoryStat = lstatIfPresent(directory);
207
+ if (!directoryStat)
208
+ return [];
209
+ if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
210
+ throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
211
+ }
212
+ const names = fs.readdirSync(directory)
213
+ .filter((name) => name.endsWith(".md"))
214
+ .sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
215
+ return names.map((name) => {
216
+ const sourcePath = path.join(directory, name);
217
+ const sourceStat = fs.lstatSync(sourcePath);
218
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
219
+ throw new FlowError("INTERNAL_STATE", `task source is not a regular file: ${sourcePath}`);
220
+ }
221
+ return {
222
+ path: sourcePath,
223
+ text: fs.readFileSync(sourcePath, "utf8"),
224
+ };
225
+ });
226
+ }
227
+ function validatePlan(plan) {
228
+ validateWrites(plan.writes);
229
+ }
230
+ function validateWrites(writes) {
231
+ if (writes.length === 0) {
232
+ throw new FlowError("INTERNAL_STATE", "task mutation must contain at least one write");
233
+ }
234
+ const targets = new Set();
235
+ for (const write of writes) {
236
+ const target = `${write.id}.md`;
237
+ if (!TASK_ID.test(write.id)) {
238
+ throw new FlowError("INTERNAL_STATE", `invalid task mutation id: ${write.id}`);
239
+ }
240
+ if (targets.has(target))
241
+ throw new FlowError("INTERNAL_STATE", `duplicate task mutation target: ${target}`);
242
+ targets.add(target);
243
+ }
244
+ }
245
+ function mutationTarget(write) {
246
+ return `${write.id}.md`;
247
+ }
248
+ function capturePreviousFiles(directory, writes) {
249
+ return writes.map((write) => {
250
+ const destination = path.join(directory, mutationTarget(write));
251
+ const stat = lstatIfPresent(destination);
252
+ if (!stat)
253
+ return { path: destination };
254
+ if (stat.isSymbolicLink() || !stat.isFile()) {
255
+ throw new FlowError("INTERNAL_STATE", `task destination is not a regular file: ${destination}`);
256
+ }
257
+ return { path: destination, text: fs.readFileSync(destination, "utf8") };
258
+ });
259
+ }
260
+ function sameSources(left, right) {
261
+ return left.length === right.length && left.every((source, index) => {
262
+ const candidate = right[index];
263
+ return candidate !== undefined && source.path === candidate.path && source.text === candidate.text;
264
+ });
265
+ }
266
+ function removeTaskFile(filePath) {
267
+ try {
268
+ fs.unlinkSync(filePath);
269
+ fsyncDirectory(path.dirname(filePath));
270
+ }
271
+ catch (error) {
272
+ if (error.code !== "ENOENT")
273
+ throw error;
274
+ }
275
+ }
276
+ function restoreFiles(previous, deps) {
277
+ for (const entry of previous) {
278
+ if (entry.text === undefined)
279
+ removeTaskFile(entry.path);
280
+ else
281
+ (deps.atomicReplace ?? atomicReplaceTaskFile)(entry.path, entry.text);
282
+ }
283
+ }
284
+ function writeTaskFile(directory, write, deps) {
285
+ const destination = path.join(directory, mutationTarget(write));
286
+ (deps.atomicReplace ?? atomicReplaceTaskFile)(destination, write.text);
287
+ }
288
+ function releaseTaskLock(database, deps) {
289
+ try {
290
+ if (deps.releaseLock)
291
+ deps.releaseLock();
292
+ else
293
+ database.exec("COMMIT");
294
+ }
295
+ catch {
296
+ try {
297
+ database.exec("ROLLBACK");
298
+ }
299
+ catch { /* close releases the advisory lock */ }
300
+ }
301
+ finally {
302
+ closeQuietly(database);
303
+ }
304
+ }
305
+ function assertTaskDirectorySafety(directory) {
306
+ const stat = lstatIfPresent(directory);
307
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
308
+ throw new FlowError("INTERNAL_STATE", `task directory is not a regular directory: ${directory}`);
309
+ }
310
+ }
311
+ export async function openTaskStore(cwd, deps = {}) {
312
+ const board = await locateTaskBoard(cwd);
313
+ const directory = taskDirectory(board);
314
+ return {
315
+ mode: board.mode,
316
+ read: async () => {
317
+ assertKeiyakuDirectory(board);
318
+ return readTaskSources(directory);
319
+ },
320
+ async mutate(apply) {
321
+ return withLockHeldTaskBoard(cwd, async (transaction) => {
322
+ const plan = await apply(transaction.sources);
323
+ validatePlan(plan);
324
+ await transaction.replace(plan.writes);
325
+ return plan.value;
326
+ }, deps, board);
327
+ },
328
+ };
329
+ }
330
+ export async function withLockHeldTaskBoard(cwd, apply, deps = {}, locatedBoard) {
331
+ const board = locatedBoard ?? await locateTaskBoard(cwd);
332
+ const directory = taskDirectory(board);
333
+ assertKeiyakuDirectory(board);
334
+ const database = acquireTaskLock(taskLockPath(board));
335
+ try {
336
+ let sources = await readTaskSources(directory);
337
+ const transaction = {
338
+ location: board,
339
+ get sources() {
340
+ return sources;
341
+ },
342
+ async replace(writes) {
343
+ validateWrites(writes);
344
+ assertKeiyakuDirectory(board);
345
+ assertTaskDirectorySafety(directory);
346
+ const currentSources = await readTaskSources(directory);
347
+ if (!sameSources(sources, currentSources)) {
348
+ throw new FlowError("CONCURRENT_MODIFICATION", "task sources changed while applying the mutation");
349
+ }
350
+ assertKeiyakuDirectory(board);
351
+ assertTaskDirectorySafety(directory);
352
+ const previous = capturePreviousFiles(directory, writes);
353
+ let applied = 0;
354
+ try {
355
+ for (const write of writes) {
356
+ writeTaskFile(directory, write, deps);
357
+ applied += 1;
358
+ }
359
+ }
360
+ catch (error) {
361
+ try {
362
+ restoreFiles(previous.slice(0, applied + 1), deps);
363
+ }
364
+ catch { /* the original replacement failure remains authoritative */ }
365
+ throw error;
366
+ }
367
+ sources = await readTaskSources(directory);
368
+ },
369
+ };
370
+ return await apply(transaction);
371
+ }
372
+ finally {
373
+ releaseTaskLock(database, deps);
374
+ }
375
+ }
@@ -0,0 +1 @@
1
+ export {};