@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
@@ -1,8 +1,7 @@
1
- import { FlowError } from "../flow-error.js";
2
- import { claimContract } from "./claim.js";
3
- import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
4
- import { assertClaimQueueHead, queueMembership, readQueueReadModel } from "./queue.js";
5
- import { deriveContractState } from "./status/lifecycle.js";
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { activeLedgerRef, appendWithRetry, readLedger } from "../ledger.js";
3
+ import { assertClaimQueueHead, readQueueReadModel } from "./queue.js";
4
+ import { deriveContractState } from "../status/lifecycle.js";
6
5
  function latestVerdict(entries) {
7
6
  for (let index = entries.length - 1; index >= 0; index -= 1) {
8
7
  const entry = entries[index];
@@ -19,6 +18,10 @@ function assertPetitioned(contractId, entries) {
19
18
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot verdict from state ${state.state}`);
20
19
  }
21
20
  }
21
+ /**
22
+ * Append a verdict entry and return typed judgment facts.
23
+ * Does not claim, clean up, or otherwise execute delivery.
24
+ */
22
25
  export async function verdictContract(input) {
23
26
  const at = new Date(input.nowMs).toISOString();
24
27
  const current = await readLedger(input.cwd, input.contractId);
@@ -69,40 +72,9 @@ export async function verdictContract(input) {
69
72
  if (verdict.data.result !== input.result) {
70
73
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} already has ${verdict.data.result} verdict`);
71
74
  }
72
- if (verdict.data.result === "rejected") {
73
- // Rejected verdict naturally returns state to active and clears membership.
74
- return {
75
- contractId: input.contractId,
76
- result: "rejected",
77
- summary: verdict.data.summary,
78
- queuePosition: null,
79
- };
80
- }
81
- const claimOnApproval = input.claimOnApproval ?? true;
82
- if (claimOnApproval) {
83
- const claimed = await claimContract({
84
- cwd: input.cwd,
85
- contractId: input.contractId,
86
- actor: "keiyaku",
87
- nowMs: input.nowMs,
88
- mergeDelivery: input.mergeDelivery,
89
- afterMerge: input.afterMerge,
90
- });
91
- return {
92
- contractId: input.contractId,
93
- result: "approved",
94
- summary: verdict.data.summary,
95
- claimed,
96
- queuePosition: null,
97
- };
98
- }
99
- // claimOnApproval false remains petitioned at target-local position 1.
100
- const model = await readQueueReadModel(input.cwd);
101
- const membership = queueMembership(model, input.contractId);
102
75
  return {
103
76
  contractId: input.contractId,
104
- result: "approved",
77
+ result: verdict.data.result,
105
78
  summary: verdict.data.summary,
106
- queuePosition: membership?.position ?? 1,
107
79
  };
108
80
  }
@@ -1,15 +1,13 @@
1
1
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
2
2
  import { observeGitRepository } from "../../git/branches.js";
3
- import { listContractIds, readLedger } from "../ledger.js";
4
- import { readProjectionStatusBoard } from "../projection-status.js";
5
- import { deriveQueueReadModel, queueMembership, } from "../queue.js";
3
+ import { readProjectionStatusBoard } from "../projection/index.js";
4
+ import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
6
5
  import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
7
- import { readTaskBoard } from "../task.js";
8
- import { openTaskStore } from "../task-git-store.js";
9
- import { taskRepositoryFromStore } from "../task-store-repository.js";
6
+ import { openTaskStore, readTaskBoard, taskRepositoryFromStore, } from "../task/index.js";
10
7
  import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerdict, isTerminalState, } from "./lifecycle.js";
11
8
  import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
12
9
  import { computeClaimReconciliation, } from "./reconciliation.js";
10
+ import { readStatusLedgers } from "./ledger-batch.js";
13
11
  function compareTaskStatusRows(left, right) {
14
12
  if (left.pri !== right.pri)
15
13
  return left.pri - right.pri;
@@ -32,7 +30,15 @@ async function readTaskStatusSummary(cwd) {
32
30
  const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
33
31
  if (board.tasks.length === 0 && board.diseases.length === 0)
34
32
  return undefined;
35
- return { total: board.tasks.length, inProgress, ready, blocked, topTasks, diseases: board.diseases };
33
+ return {
34
+ total: board.tasks.length,
35
+ inProgress,
36
+ ready,
37
+ blocked,
38
+ omitted: inProgress + ready - topTasks.length,
39
+ topTasks,
40
+ diseases: board.diseases,
41
+ };
36
42
  }
37
43
  catch (error) {
38
44
  return {
@@ -40,6 +46,7 @@ async function readTaskStatusSummary(cwd) {
40
46
  inProgress: 0,
41
47
  ready: 0,
42
48
  blocked: 0,
49
+ omitted: 0,
43
50
  topTasks: [],
44
51
  diseases: [],
45
52
  diagnostic: error instanceof Error ? error.message : String(error),
@@ -148,7 +155,7 @@ export async function readKanshiBoard(cwd, now, opts) {
148
155
  if (repository.capability !== "available" || repository.coordinate !== "repository") {
149
156
  return {
150
157
  contracts: [],
151
- projections: readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
158
+ projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
152
159
  queueLength: 0,
153
160
  diseases: loadedSettings.diseases,
154
161
  claimReconciliation: null,
@@ -165,22 +172,14 @@ export async function readKanshiBoard(cwd, now, opts) {
165
172
  catch {
166
173
  defaultBranch = null;
167
174
  }
168
- const contractIds = await listContractIds(cwd);
169
175
  const terminalById = new Map();
170
- const ledgers = [];
176
+ const ledgers = await readStatusLedgers(cwd);
171
177
  const readableLedgers = [];
172
- for (const id of contractIds) {
173
- try {
174
- const ledger = await readLedger(cwd, id);
175
- if (ledger) {
176
- ledgers.push(ledger);
177
- readableLedgers.push(ledger);
178
- terminalById.set(id, isTerminalState(deriveContractState(ledger.entries)));
179
- }
180
- }
181
- catch (error) {
182
- ledgers.push({ id, error: error instanceof Error ? error : new Error(String(error)) });
183
- }
178
+ for (const ledger of ledgers) {
179
+ if ("error" in ledger)
180
+ continue;
181
+ readableLedgers.push(ledger);
182
+ terminalById.set(ledger.contractId, isTerminalState(deriveContractState(ledger.entries)));
184
183
  }
185
184
  const queueModel = deriveQueueReadModel(readableLedgers);
186
185
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
@@ -194,6 +193,7 @@ export async function readKanshiBoard(cwd, now, opts) {
194
193
  contracts.push({
195
194
  id: item.id,
196
195
  slug: contractSlugFromId(item.id),
196
+ title: contractSlugFromId(item.id),
197
197
  state: { state: "corrupt", at: 0, reason: item.error.message },
198
198
  queueHeadStalled: false,
199
199
  arcs: 0,
@@ -245,6 +245,7 @@ export async function readKanshiBoard(cwd, now, opts) {
245
245
  contracts.push({
246
246
  id: item.contractId,
247
247
  slug: contractSlugFromId(item.contractId),
248
+ title: bind?.data.name ?? contractSlugFromId(item.contractId),
248
249
  state,
249
250
  queuePosition,
250
251
  queueHeadStalled: isQueueHeadStall({
@@ -277,7 +278,7 @@ export async function readKanshiBoard(cwd, now, opts) {
277
278
  const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
278
279
  return {
279
280
  contracts: filteredContracts,
280
- projections: readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
281
+ projections: await readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
281
282
  queueLength,
282
283
  defaultBranch: defaultBranch?.branch,
283
284
  defaultHead: defaultBranch?.head,
@@ -0,0 +1,158 @@
1
+ import { wrapGitError, runGitProcess } from "../../git/core.js";
2
+ import { GitStreamingBatchFramingError, GitStreamingBatchResponseEndedError, openGitStreamingBatch, } from "../../git/streaming-batch.js";
3
+ import { parseLedgerEntryCommitMessage } from "../entry.js";
4
+ import { ACTIVE_LEDGER_REF_PREFIX, } from "../ledger.js";
5
+ const MAX_LEDGER_COMMIT_BYTES = 1024 * 1024;
6
+ async function runStatusGit(cwd, args) {
7
+ const result = await runGitProcess(cwd, args);
8
+ if (result.error || result.status !== 0) {
9
+ throw wrapGitError(args.join(" "), {
10
+ message: result.error?.message
11
+ ?? `git exited with status ${result.status ?? "unknown"}`,
12
+ stderr: result.stderr,
13
+ stdout: result.stdout,
14
+ }, cwd);
15
+ }
16
+ return Buffer.from(result.stdout, "utf8");
17
+ }
18
+ async function queryBatchObject(batch, expression) {
19
+ const record = await batch.request(expression, {
20
+ maxContentBytes: MAX_LEDGER_COMMIT_BYTES,
21
+ });
22
+ if (record.kind === "missing" || record.kind === "ambiguous") {
23
+ throw new Error(`[git cat-file ${expression}] object is ${record.kind === "missing" ? "missing" : "ambiguous"}`);
24
+ }
25
+ return {
26
+ oid: record.record.oid,
27
+ type: record.record.type,
28
+ content: record.record.content,
29
+ };
30
+ }
31
+ function parseCommitObject(object) {
32
+ if (object.type !== "commit") {
33
+ throw new Error(`git cat-file expected commit ${object.oid}, received ${object.type}`);
34
+ }
35
+ const content = object.content.toString("utf8");
36
+ const messageStart = content.indexOf("\n\n");
37
+ if (messageStart < 0)
38
+ throw new Error(`git commit ${object.oid} has no message separator`);
39
+ const parents = [];
40
+ for (const line of content.slice(0, messageStart).split("\n")) {
41
+ if (!line.startsWith("parent "))
42
+ continue;
43
+ const parent = line.slice("parent ".length);
44
+ if (!/^[0-9a-f]{40,64}$/.test(parent)) {
45
+ throw new Error(`git commit ${object.oid} has an invalid parent header`);
46
+ }
47
+ parents.push(parent);
48
+ }
49
+ return { oid: object.oid, parents, message: content.slice(messageStart + 2) };
50
+ }
51
+ function parseLedgerRefs(output) {
52
+ return output
53
+ .toString("utf8")
54
+ .split(/\r?\n/)
55
+ .filter((ref) => ref.startsWith(ACTIVE_LEDGER_REF_PREFIX))
56
+ .map((ref) => ({ contractId: ref.slice(ACTIVE_LEDGER_REF_PREFIX.length), ref }));
57
+ }
58
+ async function readLedgerFromBatch(batch, ref, commits) {
59
+ const headObject = await queryBatchObject(batch, `${ref.ref}^{commit}`);
60
+ const head = parseCommitObject(headObject);
61
+ commits.set(head.oid, Promise.resolve(head));
62
+ const ordered = [];
63
+ const visited = new Set();
64
+ const visiting = new Set();
65
+ const visit = async (oid) => {
66
+ if (visited.has(oid))
67
+ return;
68
+ if (visiting.has(oid))
69
+ throw new Error(`git ledger ancestry contains a cycle at ${oid}`);
70
+ visiting.add(oid);
71
+ let pending = commits.get(oid);
72
+ if (!pending) {
73
+ pending = queryBatchObject(batch, oid).then(parseCommitObject);
74
+ commits.set(oid, pending);
75
+ }
76
+ const commit = await pending;
77
+ for (const parent of commit.parents)
78
+ await visit(parent);
79
+ visiting.delete(oid);
80
+ visited.add(oid);
81
+ ordered.push(commit);
82
+ };
83
+ await visit(head.oid);
84
+ return {
85
+ contractId: ref.contractId,
86
+ ref: ref.ref,
87
+ head: head.oid,
88
+ entries: ordered.map((commit) => parseLedgerEntryCommitMessage(commit.message)),
89
+ };
90
+ }
91
+ /**
92
+ * Read the aggregate status ledger snapshot with at most two Git processes:
93
+ * ref-name discovery and one framed, streaming object session.
94
+ */
95
+ export async function readStatusLedgers(cwd) {
96
+ const refOutput = await runStatusGit(cwd, [
97
+ "for-each-ref",
98
+ "--sort=refname",
99
+ "--format=%(refname)",
100
+ ACTIVE_LEDGER_REF_PREFIX,
101
+ ]);
102
+ const refs = parseLedgerRefs(refOutput);
103
+ if (refs.length === 0)
104
+ return [];
105
+ const batch = openGitStreamingBatch(cwd);
106
+ const commits = new Map();
107
+ const results = [];
108
+ let readError;
109
+ try {
110
+ for (let index = 0; index < refs.length; index += 1) {
111
+ const ref = refs[index];
112
+ try {
113
+ results.push(await readLedgerFromBatch(batch, ref, commits));
114
+ }
115
+ catch (error) {
116
+ const diagnostic = error instanceof Error ? error : new Error(String(error));
117
+ results.push({ id: ref.contractId, error: diagnostic });
118
+ // Framing and mid-frame EOF both destroy the batch channel; isolate remaining refs.
119
+ if (error instanceof GitStreamingBatchFramingError
120
+ || error instanceof GitStreamingBatchResponseEndedError) {
121
+ const isolationReason = error instanceof GitStreamingBatchResponseEndedError
122
+ ? "response ended"
123
+ : "malformed frame";
124
+ for (const pending of refs.slice(index + 1)) {
125
+ results.push({
126
+ id: pending.contractId,
127
+ error: new Error(`git cat-file batch terminated after ${isolationReason}: ${diagnostic.message}`),
128
+ });
129
+ }
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ catch (error) {
136
+ readError = error;
137
+ }
138
+ try {
139
+ const close = await batch.close();
140
+ if (!close.aborted && (close.timedOut || close.signal || close.code !== 0)) {
141
+ throw wrapGitError("cat-file --batch", {
142
+ message: close.timedOut
143
+ ? "git timed out"
144
+ : close.signal
145
+ ? `git exited from signal ${close.signal}`
146
+ : `git exited with status ${close.code ?? "unknown"}`,
147
+ stderr: close.stderr.toString("utf8"),
148
+ }, cwd);
149
+ }
150
+ }
151
+ catch (error) {
152
+ if (!readError)
153
+ readError = error;
154
+ }
155
+ if (readError)
156
+ throw readError;
157
+ return results;
158
+ }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { createGit } from "../../git/core.js";
3
+ import { createGit, parseRevListOutput, wrapGitError } from "../../git/core.js";
4
4
  import { stableRepoRoot } from "../worktree-path.js";
5
5
  function isClaimReceipt(value) {
6
6
  if (!value || typeof value !== "object")
@@ -51,7 +51,13 @@ export async function computeClaimReconciliation(cwd, defaultBranch) {
51
51
  if (receiptLines.length === 0)
52
52
  return null;
53
53
  const git = createGit(cwd);
54
- const firstParentHistory = (await git.raw(["rev-list", "--first-parent", defaultBranch.head])).trim().split("\n").filter(Boolean);
54
+ let firstParentHistory;
55
+ try {
56
+ firstParentHistory = parseRevListOutput(await git.raw(["rev-list", "--first-parent", defaultBranch.head]));
57
+ }
58
+ catch (error) {
59
+ throw wrapGitError(`rev-list --first-parent ${defaultBranch.head}`, error, cwd);
60
+ }
55
61
  const firstParentSet = new Set(firstParentHistory);
56
62
  let baselineIndex = -1;
57
63
  for (let index = receiptLines.length - 1; index >= 0; index -= 1) {
@@ -70,10 +76,10 @@ export async function computeClaimReconciliation(cwd, defaultBranch) {
70
76
  return line.record?.kind === "baseline" ? [{ line: line.line, reason: "non-chain" }] : [];
71
77
  });
72
78
  // Only records after the latest valid baseline belong to this fence window.
79
+ // Intentional degrade: rev-list failure yields empty segments plus non-chain receipt issues.
73
80
  let revList;
74
81
  try {
75
- const output = await git.raw(["rev-list", "--first-parent", "--reverse", `${baseline.head}..${defaultBranch.head}`]);
76
- revList = output.trim().split("\n").filter((h) => h.length > 0);
82
+ revList = parseRevListOutput(await git.raw(["rev-list", "--first-parent", "--reverse", `${baseline.head}..${defaultBranch.head}`]));
77
83
  }
78
84
  catch {
79
85
  return {
@@ -8,6 +8,9 @@ const optionalString = (value, key) => value[key] === undefined || typeof value[
8
8
  const nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
9
9
  const safeIntegerOrNull = (value) => value === null || (typeof value === "number" && Number.isSafeInteger(value));
10
10
  function isValidStoredWarning(warning) {
11
+ if (warning.reason === "rate-limit") {
12
+ return warning.state === "exhausted" && Object.keys(warning).length === 2;
13
+ }
11
14
  if (warning.reason === "retry") {
12
15
  return nonNegativeSafeInteger(warning.attempt)
13
16
  && nonNegativeSafeInteger(warning.maxRetries)
@@ -1,6 +1,5 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
- import { TASK_ID } from "./validation.js";
3
- const TASK_READ_ID = /^k-[0-9a-f]{7,12}$/;
2
+ import { isTaskId } from "./validation.js";
4
3
  function compareText(left, right) {
5
4
  return left < right ? -1 : left > right ? 1 : 0;
6
5
  }
@@ -60,6 +59,30 @@ function cycleComponents(tasks) {
60
59
  visit(id);
61
60
  return cycles;
62
61
  }
62
+ function parentCycleComponents(tasks) {
63
+ const cycles = [];
64
+ const visited = new Set();
65
+ for (const start of [...tasks.keys()].sort()) {
66
+ if (visited.has(start))
67
+ continue;
68
+ const path = [];
69
+ const positions = new Map();
70
+ let current = start;
71
+ while (current !== null && tasks.has(current) && !visited.has(current)) {
72
+ const prior = positions.get(current);
73
+ if (prior !== undefined) {
74
+ cycles.push(new Set(path.slice(prior)));
75
+ break;
76
+ }
77
+ positions.set(current, path.length);
78
+ path.push(current);
79
+ current = tasks.get(current).parent;
80
+ }
81
+ for (const id of path)
82
+ visited.add(id);
83
+ }
84
+ return cycles;
85
+ }
63
86
  /** Derive the complete storage-neutral board from domain records. */
64
87
  export function buildTaskBoardFromDocuments(documents) {
65
88
  const diseases = [];
@@ -91,6 +114,17 @@ export function buildTaskBoardFromDocuments(documents) {
91
114
  candidates.delete(id);
92
115
  }
93
116
  }
117
+ for (const component of parentCycleComponents(candidates)) {
118
+ const relatedIds = [...component].sort();
119
+ for (const id of relatedIds) {
120
+ diseases.push({
121
+ code: "parent_cycle",
122
+ message: `task ${id} participates in a parent cycle`,
123
+ id,
124
+ relatedIds,
125
+ });
126
+ }
127
+ }
94
128
  let changed = true;
95
129
  while (changed) {
96
130
  changed = false;
@@ -148,9 +182,11 @@ export function resolveTaskReadId(board, input) {
148
182
  return resolveTaskId(board, input);
149
183
  }
150
184
  export function resolveTaskId(board, input) {
151
- if (!TASK_READ_ID.test(input)) {
185
+ if (!isTaskId(input)) {
152
186
  throw new FlowError("INVALID_TARGET", `invalid task id '${input}'`);
153
187
  }
188
+ if (board.byId.has(input))
189
+ return input;
154
190
  const matches = board.tasks
155
191
  .map((task) => task.id)
156
192
  .filter((id) => id.startsWith(input))
@@ -163,7 +199,7 @@ export function resolveTaskId(board, input) {
163
199
  return matches[0];
164
200
  }
165
201
  export function requireCanonicalTaskId(input) {
166
- if (!TASK_ID.test(input)) {
202
+ if (!isTaskId(input)) {
167
203
  throw new FlowError("INVALID_TARGET", "task mutations require a canonical full task id");
168
204
  }
169
205
  return input;
@@ -1,4 +1,5 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
+ import { IdentifierSlugError, normalizeIdentifierSlug, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES, } from "../identifier-slug.js";
2
3
  import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
3
4
  import { dateTimestamp, invalid, nonblank, } from "./validation.js";
4
5
  export async function readTaskBoard(repository) {
@@ -20,9 +21,10 @@ function documentFromView(view) {
20
21
  body: view.body,
21
22
  };
22
23
  }
23
- function replaceDocument(document) {
24
+ function replaceDocument(document, subject) {
24
25
  return {
25
26
  replacements: [document],
27
+ subject,
26
28
  select(resulting) {
27
29
  const value = resulting.byId.get(document.id);
28
30
  if (!value)
@@ -43,6 +45,16 @@ function resolveMutationIds(board, ids) {
43
45
  export async function createTask(repository, input, deps) {
44
46
  nonblank("title", input.title);
45
47
  nonblank("creator", input.creator);
48
+ let baseId;
49
+ try {
50
+ baseId = normalizeIdentifierSlug(input.title, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES);
51
+ }
52
+ catch (error) {
53
+ if (error instanceof IdentifierSlugError) {
54
+ throw new FlowError("INVALID_TARGET", `task title ${error.message}`, { cause: error });
55
+ }
56
+ throw error;
57
+ }
46
58
  const pri = input.pri ?? 2;
47
59
  if (!Number.isInteger(pri) || pri < 0 || pri > 3)
48
60
  invalid("task priority must be an integer from 0 through 3");
@@ -55,19 +67,13 @@ export async function createTask(repository, input, deps) {
55
67
  const from = resolveMutationIds(board, input.from ?? []);
56
68
  if (new Set(from).size !== from.length)
57
69
  invalid("task from edges must be unique");
58
- let id;
59
- for (let attempt = 0; attempt < 32; attempt += 1) {
60
- const bytes = deps.randomBytes(6);
61
- if (bytes.byteLength !== 6)
62
- throw new FlowError("INTERNAL_STATE", "randomBytes(6) must return exactly 6 bytes");
63
- const candidate = `k-${Buffer.from(bytes).toString("hex")}`;
64
- if (!board.byId.has(candidate)) {
65
- id = candidate;
66
- break;
70
+ let id = baseId;
71
+ for (let ordinal = 2; board.byId.has(id); ordinal += 1) {
72
+ if (!Number.isSafeInteger(ordinal)) {
73
+ throw new FlowError("INTERNAL_STATE", `task id collision ordinal exceeded the safe integer range for ${baseId}`);
67
74
  }
75
+ id = `${baseId}-${ordinal}`;
68
76
  }
69
- if (!id)
70
- throw new FlowError("CONCURRENT_MODIFICATION", "could not allocate a unique task id after 32 attempts");
71
77
  const document = {
72
78
  id,
73
79
  title: input.title,
@@ -80,7 +86,7 @@ export async function createTask(repository, input, deps) {
80
86
  creator: input.creator,
81
87
  body: input.body ?? "",
82
88
  };
83
- return replaceDocument(document);
89
+ return replaceDocument(document, `Add task ${id}`);
84
90
  });
85
91
  }
86
92
  export async function updateTask(repository, fullIdValue, patch) {
@@ -112,10 +118,10 @@ export async function updateTask(repository, fullIdValue, patch) {
112
118
  if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
113
119
  throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
114
120
  }
115
- return replaceDocument(document);
121
+ return replaceDocument(document, `Update task ${id}`);
116
122
  });
117
123
  }
118
- async function transition(repository, fullIdValue, apply) {
124
+ async function transition(repository, fullIdValue, apply, subject) {
119
125
  return repository.mutate((board) => {
120
126
  requireHealthy(board);
121
127
  const id = fullIdValue === undefined
@@ -123,7 +129,7 @@ async function transition(repository, fullIdValue, apply) {
123
129
  : resolveTaskId(board, fullIdValue);
124
130
  const document = documentFromView(requireExisting(board, id));
125
131
  apply(document);
126
- return replaceDocument(document);
132
+ return replaceDocument(document, subject(id));
127
133
  });
128
134
  }
129
135
  function rejectBound(document, action = "state") {
@@ -141,7 +147,7 @@ export async function startTask(repository, fullId, now) {
141
147
  invalid(`task ${document.id} is ${document.state}, not open`);
142
148
  document.state = "in_progress";
143
149
  document.startedAt = dateTimestamp("startedAt", now);
144
- });
150
+ }, (id) => `Start task ${id}`);
145
151
  }
146
152
  export async function stopTask(repository, fullId) {
147
153
  return transition(repository, fullId, (document) => {
@@ -150,7 +156,7 @@ export async function stopTask(repository, fullId) {
150
156
  invalid(`task ${document.id} is ${document.state}, not in_progress`);
151
157
  document.state = "open";
152
158
  delete document.startedAt;
153
- });
159
+ }, (id) => `Stop task ${id}`);
154
160
  }
155
161
  export async function doneTask(repository, fullId) {
156
162
  return transition(repository, fullId, (document) => {
@@ -160,7 +166,7 @@ export async function doneTask(repository, fullId) {
160
166
  }
161
167
  document.state = "done";
162
168
  delete document.startedAt;
163
- });
169
+ }, (id) => `Done task ${id}`);
164
170
  }
165
171
  export async function dropTask(repository, fullId) {
166
172
  return transition(repository, fullId, (document) => {
@@ -170,7 +176,7 @@ export async function dropTask(repository, fullId) {
170
176
  }
171
177
  document.state = "drop";
172
178
  delete document.startedAt;
173
- });
179
+ }, (id) => `Drop task ${id}`);
174
180
  }
175
181
  export function selectSoleInProgress(board) {
176
182
  const candidates = board.tasks.filter((task) => task.state === "in_progress");
@@ -205,7 +211,10 @@ export async function rollbackOrphanTaskPointers(repository, contractIds) {
205
211
  });
206
212
  if (replacements.length === 0)
207
213
  throw new FlowError("CONCURRENT_MODIFICATION", "task pointers changed while doctor was reconciling them");
208
- return { replacements, select: () => undefined };
214
+ const subject = replacements.length === 1
215
+ ? `Clear orphan contract pointer from task ${replacements[0].id}`
216
+ : `Clear orphan contract pointers from ${replacements.length} tasks`;
217
+ return { replacements, subject, select: () => undefined };
209
218
  });
210
219
  return orphans;
211
220
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public surface for the task package.
3
+ * Only symbols required by current external consumers are re-exported.
4
+ * Package-internal modules must import each other directly, not through this file.
5
+ */
6
+ export { reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
7
+ export { resolveTaskCreator } from "./task-git-actor.js";
8
+ export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
9
+ export { openTaskStore } from "./task-git-store.js";
10
+ export { taskRepositoryFromStore } from "./task-store-repository.js";
11
+ export { isTaskId } from "./validation.js";
12
+ export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, } from "./task.js";