@astrosheep/keiyaku 2.8.5 → 2.9.1

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/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 +42 -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 +37 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +56 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +25 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +36 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +23 -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-flags.js +197 -0
  106. package/build/cli/parse-metadata.js +129 -0
  107. package/build/cli/parse-selectors.js +65 -0
  108. package/build/cli/parse.js +46 -313
  109. package/build/cli/projection-address.js +5 -0
  110. package/build/cli/render/arc.js +1 -1
  111. package/build/cli/render/errors.js +4 -3
  112. package/build/cli/render/format.js +14 -0
  113. package/build/cli/render/kanshi.js +106 -0
  114. package/build/cli/render/petition.js +24 -1
  115. package/build/cli/render/projection-activity.js +5 -1
  116. package/build/cli/render/shared.js +2 -2
  117. package/build/cli/render/status.js +79 -13
  118. package/build/cli/render/task.js +113 -0
  119. package/build/cli/render/wait.js +23 -3
  120. package/build/cli/types.js +1 -1
  121. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +7 -4
  122. package/build/config/env.js +9 -4
  123. package/build/config/settings/disease.js +26 -0
  124. package/build/config/settings/knobs.js +8 -0
  125. package/build/config/settings/loader.js +244 -0
  126. package/build/config/settings/schema.js +276 -0
  127. package/build/core/addressing.js +1 -1
  128. package/build/core/amend.js +5 -8
  129. package/build/core/arc.js +7 -8
  130. package/build/core/bind-workspace.js +123 -0
  131. package/build/core/bind.js +75 -155
  132. package/build/core/call/call.js +229 -0
  133. package/build/core/call/context.js +251 -0
  134. package/build/core/call/execution.js +217 -0
  135. package/build/core/call/prompt.js +42 -0
  136. package/build/core/call/reference.js +24 -0
  137. package/build/core/call-persist.js +10 -3
  138. package/build/core/claim-delivery.js +247 -0
  139. package/build/core/claim.js +39 -277
  140. package/build/core/context.js +2 -1
  141. package/build/core/{render.js → contract-view.js} +12 -140
  142. package/build/core/draft.js +107 -9
  143. package/build/core/entry.js +9 -1
  144. package/build/core/execution-coordinate.js +4 -20
  145. package/build/core/execution-pact.js +16 -31
  146. package/build/core/forfeit.js +35 -22
  147. package/build/core/hints.js +2 -2
  148. package/build/core/log.js +2 -2
  149. package/build/core/path-coordinate.js +27 -0
  150. package/build/core/petition-claim-gates.js +8 -6
  151. package/build/core/petition-preview.js +97 -0
  152. package/build/core/petition-run.js +5 -7
  153. package/build/core/petition.js +39 -26
  154. package/build/core/projection/generation/database.js +306 -0
  155. package/build/core/projection/generation/identity.js +84 -0
  156. package/build/core/projection/generation/model.js +201 -0
  157. package/build/core/projection/generation/store.js +73 -0
  158. package/build/core/projection/generation/transitions.js +258 -0
  159. package/build/core/projection/heart.js +0 -307
  160. package/build/core/projection/tell/database.js +109 -0
  161. package/build/core/projection/tell/model.js +97 -0
  162. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  163. package/build/core/projection-activity.js +23 -2
  164. package/build/core/projection-coordinate.js +14 -2
  165. package/build/core/projection-core.js +2 -1
  166. package/build/core/projection-execution-observer.js +259 -0
  167. package/build/core/projection-generation-continuation.js +1 -1
  168. package/build/core/projection-generation-execution.js +90 -20
  169. package/build/core/projection-generation-launcher.js +1 -1
  170. package/build/core/projection-generation-runner.js +198 -4
  171. package/build/core/projection-identity.js +1 -1
  172. package/build/core/projection-kill.js +50 -17
  173. package/build/core/projection-life-observer.js +1 -1
  174. package/build/core/projection-life-protocol.js +10 -0
  175. package/build/core/projection-mint.js +1 -1
  176. package/build/core/projection-status.js +45 -32
  177. package/build/core/projection-wait.js +12 -2
  178. package/build/core/projection-wake.js +5 -2
  179. package/build/core/{queue_v2.js → queue.js} +2 -1
  180. package/build/core/registry.js +1 -1
  181. package/build/core/renew.js +9 -11
  182. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  183. package/build/core/response-artifact-catalog.js +7 -0
  184. package/build/core/seal.js +44 -16
  185. package/build/core/status/board.js +289 -0
  186. package/build/core/status/drift.js +99 -0
  187. package/build/core/status/lifecycle.js +84 -0
  188. package/build/core/status/reconciliation.js +131 -0
  189. package/build/core/stored-agent-event.js +28 -4
  190. package/build/core/target-ref.js +10 -0
  191. package/build/core/task/board.js +175 -0
  192. package/build/core/task/commands.js +211 -0
  193. package/build/core/task/document.js +189 -0
  194. package/build/core/task/model.js +1 -0
  195. package/build/core/task/settlement-git.js +207 -0
  196. package/build/core/task/settlement-policy.js +82 -0
  197. package/build/core/task/source-board.js +84 -0
  198. package/build/core/task/source-model.js +1 -0
  199. package/build/core/task/validation.js +44 -0
  200. package/build/core/task-contract.js +237 -0
  201. package/build/core/task-doctor.js +14 -0
  202. package/build/core/task-git-actor.js +38 -0
  203. package/build/core/task-git-runtime.js +37 -0
  204. package/build/core/task-git-store.js +385 -0
  205. package/build/core/task-repository.js +1 -0
  206. package/build/core/task-store-repository.js +86 -0
  207. package/build/core/task-store.js +1 -0
  208. package/build/core/task-worktree.js +51 -0
  209. package/build/core/task.js +4 -0
  210. package/build/core/transcripts.js +138 -56
  211. package/build/core/validation.js +17 -0
  212. package/build/core/verdict.js +2 -2
  213. package/build/core/worktree-path.js +33 -8
  214. package/build/flow-error.js +5 -2
  215. package/build/generated/version.js +1 -1
  216. package/build/git/branches.js +33 -9
  217. package/build/git/commits.js +3 -0
  218. package/build/git/core.js +9 -0
  219. package/build/git/diff/parsers.js +0 -94
  220. package/build/index.js +2 -0
  221. package/build/keiyaku-constants.js +2 -0
  222. package/build/keiyaku.js +4 -3
  223. package/package.json +3 -2
  224. package/skills/keiyaku/SKILL.md +30 -13
  225. package/skills/keiyaku-akuma/SKILL.md +30 -57
  226. package/skills/keiyaku-task/SKILL.md +51 -0
  227. package/skills/keiyaku-workflow/SKILL.md +71 -0
  228. package/build/agents/harness/pump.js +0 -158
  229. package/build/agents/providers/codex-app-server.js +0 -864
  230. package/build/agents/providers/opencode-sdk.js +0 -363
  231. package/build/agents/providers/pi.js +0 -557
  232. package/build/config/settings.js +0 -533
  233. package/build/core/call.js +0 -686
  234. package/build/core/projection/generation.js +0 -412
  235. package/build/core/projection/identity.js +0 -81
  236. package/build/core/projection/mint.js +0 -174
  237. package/build/core/projection-generation-store.js +0 -707
  238. package/build/core/status.js +0 -539
@@ -0,0 +1,84 @@
1
+ function transitionFromEntry(current, entry) {
2
+ if (current === "claimed" || current === "forfeited" || current === "corrupt") {
3
+ return "corrupt";
4
+ }
5
+ switch (entry.kind) {
6
+ case "bind":
7
+ return "corrupt";
8
+ case "bootstrap":
9
+ // Placement legality is owned exclusively by deriveContractState.
10
+ return "corrupt";
11
+ case "arc":
12
+ return current === "bound" || current === "active" ? "active" : "corrupt";
13
+ case "seal":
14
+ if (current === "active")
15
+ return "active";
16
+ // Derived default arc a0 may seal from bound (bind/bootstrap → seal0 → petition).
17
+ if (current === "bound" && entry.data.arc === 0)
18
+ return "active";
19
+ return "corrupt";
20
+ case "amend":
21
+ return current === "petitioned" ? "active" : current;
22
+ case "renew":
23
+ return current;
24
+ case "verdict":
25
+ if (current !== "petitioned")
26
+ return "corrupt";
27
+ return entry.data.result === "rejected" ? "active" : "petitioned";
28
+ case "petition":
29
+ return current === "bound" || current === "active" ? "petitioned" : "corrupt";
30
+ case "claim":
31
+ return current === "petitioned" ? "claimed" : "corrupt";
32
+ case "forfeit":
33
+ return "forfeited";
34
+ }
35
+ return "corrupt";
36
+ }
37
+ export function deriveContractState(entries) {
38
+ if (entries.length === 0 || entries[0]?.kind !== "bind") {
39
+ return { state: "corrupt", at: 0, reason: "ledger must start with bind" };
40
+ }
41
+ const bind = entries[0];
42
+ let state = "bound";
43
+ for (let index = 1; index < entries.length; index += 1) {
44
+ const entry = entries[index];
45
+ if (entry.kind === "bootstrap") {
46
+ if (index !== 1 || state !== "bound") {
47
+ return { state: "corrupt", at: index + 1, reason: "bootstrap must immediately follow bind" };
48
+ }
49
+ if (!bind.data.bootstrap) {
50
+ return { state: "corrupt", at: index + 1, reason: "bootstrap without bind plan" };
51
+ }
52
+ // Legal bootstrap is a durable fact and leaves the derived state bound.
53
+ continue;
54
+ }
55
+ const next = transitionFromEntry(state, entry);
56
+ if (next === "corrupt") {
57
+ return { state: "corrupt", at: index + 1, reason: `invalid ${entry.kind} after ${state}` };
58
+ }
59
+ state = next;
60
+ }
61
+ return { state };
62
+ }
63
+ export function deriveBootstrapDiagnostics(entries) {
64
+ const bind = entries.find((entry) => entry.kind === "bind");
65
+ if (!bind?.data.bootstrap)
66
+ return [];
67
+ if (entries.some((entry) => entry.kind === "bootstrap"))
68
+ return [];
69
+ return [{ kind: "plan-without-result" }];
70
+ }
71
+ export function isTerminalState(state) {
72
+ return state.state === "claimed" || state.state === "forfeited";
73
+ }
74
+ /** Current-petition verdict predicate used by board queue-head stall logic. */
75
+ export function hasCurrentPetitionVerdict(entries) {
76
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
77
+ const entry = entries[index];
78
+ if (entry.kind === "verdict")
79
+ return true;
80
+ if (entry.kind === "petition")
81
+ return false;
82
+ }
83
+ return false;
84
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createGit } from "../../git/core.js";
4
+ import { stableRepoRoot } from "../worktree-path.js";
5
+ function isClaimReceipt(value) {
6
+ if (!value || typeof value !== "object")
7
+ return false;
8
+ const receipt = value;
9
+ return receipt.kind === "receipt" && [receipt.oldHead, receipt.newHead, receipt.contract].every((field) => typeof field === "string" && field.length > 0);
10
+ }
11
+ function isClaimReceiptBaseline(value) {
12
+ if (!value || typeof value !== "object")
13
+ return false;
14
+ const baseline = value;
15
+ if (baseline.kind !== "baseline" || typeof baseline.head !== "string" || typeof baseline.by !== "string" || baseline.by.length === 0) {
16
+ return false;
17
+ }
18
+ if (typeof baseline.establishedAt !== "string" || !baseline.establishedAt.endsWith("Z"))
19
+ return false;
20
+ const at = Date.parse(baseline.establishedAt);
21
+ return Number.isFinite(at) && new Date(at).toISOString() === baseline.establishedAt;
22
+ }
23
+ async function readClaimReceipts(cwd) {
24
+ const receiptsPath = path.join(await stableRepoRoot(cwd), ".keiyaku", "claim-receipts.jsonl");
25
+ try {
26
+ const content = fs.readFileSync(receiptsPath, "utf8");
27
+ return content
28
+ .split("\n")
29
+ .map((line, index) => ({ line, index: index + 1 }))
30
+ .filter(({ line }) => line.trim().length > 0)
31
+ .map(({ line, index }) => {
32
+ try {
33
+ const parsed = JSON.parse(line);
34
+ return isClaimReceipt(parsed) || isClaimReceiptBaseline(parsed)
35
+ ? { line: index, record: parsed }
36
+ : { line: index, issue: { line: index, reason: "malformed" } };
37
+ }
38
+ catch {
39
+ return { line: index, issue: { line: index, reason: "malformed" } };
40
+ }
41
+ });
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ export async function computeClaimReconciliation(cwd, defaultBranch) {
48
+ if (!defaultBranch)
49
+ return null;
50
+ const receiptLines = await readClaimReceipts(cwd);
51
+ if (receiptLines.length === 0)
52
+ return null;
53
+ const git = createGit(cwd);
54
+ const firstParentHistory = (await git.raw(["rev-list", "--first-parent", defaultBranch.head])).trim().split("\n").filter(Boolean);
55
+ const firstParentSet = new Set(firstParentHistory);
56
+ let baselineIndex = -1;
57
+ for (let index = receiptLines.length - 1; index >= 0; index -= 1) {
58
+ const record = receiptLines[index].record;
59
+ if (record?.kind === "baseline" && firstParentSet.has(record.head)) {
60
+ baselineIndex = index;
61
+ break;
62
+ }
63
+ }
64
+ if (baselineIndex < 0)
65
+ return null;
66
+ const baseline = receiptLines[baselineIndex].record;
67
+ const issues = receiptLines.slice(baselineIndex + 1).flatMap((line) => {
68
+ if (line.issue)
69
+ return [line.issue];
70
+ return line.record?.kind === "baseline" ? [{ line: line.line, reason: "non-chain" }] : [];
71
+ });
72
+ // Only records after the latest valid baseline belong to this fence window.
73
+ let revList;
74
+ 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);
77
+ }
78
+ catch {
79
+ return {
80
+ defaultBranch: defaultBranch.branch,
81
+ head: defaultBranch.head,
82
+ unclaimedSegments: [],
83
+ receiptIssues: [...issues, ...receiptLines.slice(baselineIndex + 1).filter((line) => line.record?.kind === "receipt").map((line) => ({ line: line.line, reason: "non-chain" }))],
84
+ };
85
+ }
86
+ const firstParent = new Map();
87
+ let parent = baseline.head;
88
+ for (const commit of revList) {
89
+ firstParent.set(commit, parent);
90
+ parent = commit;
91
+ }
92
+ const indexByCommit = new Map(revList.map((commit, index) => [commit, index]));
93
+ const claimedHeads = new Set();
94
+ let previousReceiptIndex = -1;
95
+ for (const line of receiptLines.slice(baselineIndex + 1)) {
96
+ if (line.record?.kind !== "receipt")
97
+ continue;
98
+ const index = indexByCommit.get(line.record.newHead);
99
+ if (index === undefined || index <= previousReceiptIndex || firstParent.get(line.record.newHead) !== line.record.oldHead) {
100
+ issues.push({ line: line.line, reason: "non-chain" });
101
+ continue;
102
+ }
103
+ previousReceiptIndex = index;
104
+ claimedHeads.add(line.record.newHead);
105
+ }
106
+ // Walk default-branch first-parent edges and group only movement not covered
107
+ // by a valid receipt. These are facts, not a judgment about their source.
108
+ const segments = [];
109
+ let segmentStart = null;
110
+ let segmentCount = 0;
111
+ for (const commit of revList) {
112
+ if (claimedHeads.has(commit)) {
113
+ if (segmentStart !== null) {
114
+ segments.push({ oldHead: segmentStart, newHead: firstParent.get(commit), commitCount: segmentCount });
115
+ segmentStart = null;
116
+ segmentCount = 0;
117
+ }
118
+ }
119
+ else {
120
+ if (segmentStart === null) {
121
+ segmentStart = firstParent.get(commit);
122
+ }
123
+ segmentCount++;
124
+ }
125
+ }
126
+ // Trailing unclaimed segment up to HEAD
127
+ if (segmentStart !== null) {
128
+ segments.push({ oldHead: segmentStart, newHead: defaultBranch.head, commitCount: segmentCount });
129
+ }
130
+ return { defaultBranch: defaultBranch.branch, head: defaultBranch.head, unclaimedSegments: segments, receiptIssues: issues };
131
+ }
@@ -1,11 +1,28 @@
1
- import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "../agents/harness/index.js";
2
- import { EXEC_EVENT_BUFFER_HARD_CAP } from "../agents/harness/event-channel.js";
1
+ import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, EXEC_EVENT_BUFFER_HARD_CAP, } from "../agents/harness/index.js";
3
2
  import { isSubagentProvider } from "../keiyaku.js";
4
3
  const FAIL_CODES = new Set(["SUBAGENT_TIMEOUT", "SUBAGENT_NON_ZERO_EXIT", "SUBAGENT_SPAWN_ERROR", "SUBAGENT_EXEC_ERROR", "SUBAGENT_PROVIDER_UNAVAILABLE", "PROJECTION_STATE_ERROR"]);
5
4
  const KNOWN = new Set(["message", "unknown", "activity", "thought", "plan", "turn", "usage", "warning", "diagnostic", "truncation", "done", "fail"]);
6
5
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
7
6
  const finite = (value) => typeof value === "number" && Number.isFinite(value);
8
7
  const optionalString = (value, key) => value[key] === undefined || typeof value[key] === "string";
8
+ const nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
9
+ const safeIntegerOrNull = (value) => value === null || (typeof value === "number" && Number.isSafeInteger(value));
10
+ function isValidStoredWarning(warning) {
11
+ if (warning.reason === "retry") {
12
+ return nonNegativeSafeInteger(warning.attempt)
13
+ && nonNegativeSafeInteger(warning.maxRetries)
14
+ && nonNegativeSafeInteger(warning.retryDelayMs)
15
+ && safeIntegerOrNull(warning.errorStatus);
16
+ }
17
+ if (warning.reason === "refusal" || warning.reason === "overload") {
18
+ return optionalString(warning, "originalModel")
19
+ && optionalString(warning, "fallbackModel")
20
+ && optionalString(warning, "direction")
21
+ && (warning.requestId === undefined || warning.requestId === null || typeof warning.requestId === "string")
22
+ && optionalString(warning, "signerModel");
23
+ }
24
+ return false;
25
+ }
9
26
  export function classifyStoredAgentEvent(value) {
10
27
  if (!record(value) || typeof value.type !== "string")
11
28
  return "malformed";
@@ -20,7 +37,14 @@ export function classifyStoredAgentEvent(value) {
20
37
  || value.turn < 0)
21
38
  return "malformed";
22
39
  switch (value.type) {
23
- case "message": return typeof value.text === "string" ? "valid" : "malformed";
40
+ case "message":
41
+ return typeof value.id === "string"
42
+ && value.id.length > 0
43
+ && value.id.length <= AGENT_EVENT_TEXT_MAX_CHARS
44
+ && typeof value.text === "string"
45
+ && value.text.length <= AGENT_EVENT_TEXT_MAX_CHARS
46
+ ? "valid"
47
+ : "malformed";
24
48
  case "unknown": {
25
49
  if (typeof value.provider !== "string" || value.provider.length > AGENT_EVENT_TEXT_MAX_CHARS ||
26
50
  typeof value.nativeType !== "string" || value.nativeType.length > AGENT_EVENT_TEXT_MAX_CHARS ||
@@ -51,7 +75,7 @@ export function classifyStoredAgentEvent(value) {
51
75
  case "plan": return typeof value.id === "string" && Array.isArray(value.items) && value.items.every((item) => record(item) && typeof item.text === "string" && typeof item.done === "boolean" && (item.active === undefined || item.active === true)) ? "valid" : "malformed";
52
76
  case "turn": return value.phase === "started" || value.phase === "completed" ? "valid" : "malformed";
53
77
  case "usage": return finite(value.inputTokens) && finite(value.outputTokens) && (value.cachedInputTokens === undefined || finite(value.cachedInputTokens)) && (value.costUsd === undefined || finite(value.costUsd)) && optionalString(value, "model") ? "valid" : "malformed";
54
- case "warning": return record(value.warning) && (value.warning.reason === "refusal" || value.warning.reason === "overload") && optionalString(value.warning, "originalModel") && optionalString(value.warning, "fallbackModel") && optionalString(value.warning, "direction") && (value.warning.requestId === undefined || value.warning.requestId === null || typeof value.warning.requestId === "string") && optionalString(value.warning, "signerModel") ? "valid" : "malformed";
78
+ case "warning": return record(value.warning) && isValidStoredWarning(value.warning) ? "valid" : "malformed";
55
79
  case "diagnostic": return typeof value.text === "string" ? "valid" : "malformed";
56
80
  case "truncation": {
57
81
  return Number.isInteger(value.limit)
@@ -0,0 +1,10 @@
1
+ import { FlowError } from "../flow-error.js";
2
+ import { fullBranchRefSchema } from "./entry.js";
3
+ /** Prefix bare branch names and validate the full ref. Sole status/bind normalizer. */
4
+ export function normalizeTargetRef(raw) {
5
+ const ref = raw.startsWith("refs/heads/") ? raw : `refs/heads/${raw}`;
6
+ if (!fullBranchRefSchema.safeParse(ref).success) {
7
+ throw new FlowError("INVALID_TARGET", `invalid target ref '${raw}' — pass a branch name or refs/heads/<name>`);
8
+ }
9
+ return ref;
10
+ }
@@ -0,0 +1,175 @@
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}$/;
4
+ function compareText(left, right) {
5
+ return left < right ? -1 : left > right ? 1 : 0;
6
+ }
7
+ function diseaseSort(left, right) {
8
+ return compareText(left.code, right.code)
9
+ || compareText(left.id ?? "", right.id ?? "")
10
+ || compareText(left.message, right.message);
11
+ }
12
+ function taskSort(left, right) {
13
+ const leftTerminal = left.state === "done" || left.state === "drop" ? 1 : 0;
14
+ const rightTerminal = right.state === "done" || right.state === "drop" ? 1 : 0;
15
+ return leftTerminal - rightTerminal
16
+ || left.pri - right.pri
17
+ || compareText(left.createdAt, right.createdAt)
18
+ || compareText(left.id, right.id);
19
+ }
20
+ function cycleComponents(tasks) {
21
+ let nextIndex = 0;
22
+ const indexes = new Map();
23
+ const low = new Map();
24
+ const stack = [];
25
+ const onStack = new Set();
26
+ const cycles = [];
27
+ const visit = (id) => {
28
+ indexes.set(id, nextIndex);
29
+ low.set(id, nextIndex);
30
+ nextIndex += 1;
31
+ stack.push(id);
32
+ onStack.add(id);
33
+ for (const need of tasks.get(id).needs) {
34
+ if (!tasks.has(need))
35
+ continue;
36
+ if (!indexes.has(need)) {
37
+ visit(need);
38
+ low.set(id, Math.min(low.get(id), low.get(need)));
39
+ }
40
+ else if (onStack.has(need)) {
41
+ low.set(id, Math.min(low.get(id), indexes.get(need)));
42
+ }
43
+ }
44
+ if (low.get(id) !== indexes.get(id))
45
+ return;
46
+ const component = [];
47
+ while (stack.length > 0) {
48
+ const member = stack.pop();
49
+ onStack.delete(member);
50
+ component.push(member);
51
+ if (member === id)
52
+ break;
53
+ }
54
+ if (component.length > 1 || tasks.get(id).needs.includes(id)) {
55
+ cycles.push(new Set(component));
56
+ }
57
+ };
58
+ for (const id of [...tasks.keys()].sort())
59
+ if (!indexes.has(id))
60
+ visit(id);
61
+ return cycles;
62
+ }
63
+ /** Derive the complete storage-neutral board from domain records. */
64
+ export function buildTaskBoardFromDocuments(documents) {
65
+ const diseases = [];
66
+ const counts = new Map();
67
+ for (const task of documents)
68
+ counts.set(task.id, (counts.get(task.id) ?? 0) + 1);
69
+ const candidates = new Map();
70
+ for (const task of documents) {
71
+ if (counts.get(task.id) > 1) {
72
+ diseases.push({
73
+ code: "duplicate_id",
74
+ message: `duplicate authoritative task id ${task.id}`,
75
+ id: task.id,
76
+ });
77
+ }
78
+ else {
79
+ candidates.set(task.id, task);
80
+ }
81
+ }
82
+ for (const component of cycleComponents(candidates)) {
83
+ const relatedIds = [...component].sort();
84
+ for (const id of relatedIds) {
85
+ diseases.push({
86
+ code: "needs_cycle",
87
+ message: `task ${id} participates in a needs cycle`,
88
+ id,
89
+ relatedIds,
90
+ });
91
+ candidates.delete(id);
92
+ }
93
+ }
94
+ let changed = true;
95
+ while (changed) {
96
+ changed = false;
97
+ for (const [id, task] of [...candidates].sort(([a], [b]) => compareText(a, b))) {
98
+ const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from]
99
+ .filter((related) => !candidates.has(related));
100
+ if (missing.length === 0)
101
+ continue;
102
+ const relatedIds = [...new Set(missing)].sort();
103
+ diseases.push({
104
+ code: "missing_reference",
105
+ message: `task ${id} references missing task(s): ${relatedIds.join(", ")}`,
106
+ id,
107
+ relatedIds,
108
+ });
109
+ candidates.delete(id);
110
+ changed = true;
111
+ }
112
+ }
113
+ const blocks = new Map();
114
+ for (const task of candidates.values()) {
115
+ for (const need of task.needs)
116
+ blocks.set(need, [...(blocks.get(need) ?? []), task.id]);
117
+ }
118
+ const tasks = [...candidates.values()].sort(taskSort).map((task) => {
119
+ const blockingNeeds = task.needs.filter((id) => {
120
+ const state = candidates.get(id).state;
121
+ return state === "open" || state === "in_progress";
122
+ });
123
+ return {
124
+ id: task.id,
125
+ title: task.title,
126
+ state: task.state,
127
+ pri: task.pri,
128
+ needs: [...task.needs],
129
+ parent: task.parent,
130
+ from: [...task.from],
131
+ createdAt: task.createdAt,
132
+ creator: task.creator,
133
+ ...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
134
+ ...(task.contractId === undefined ? {} : { contractId: task.contractId }),
135
+ body: task.body,
136
+ blocks: [...(blocks.get(task.id) ?? [])].sort(),
137
+ blocked: task.state === "open" && blockingNeeds.length > 0,
138
+ releasedNeeds: task.needs.filter((id) => candidates.get(id).state === "drop").sort(),
139
+ };
140
+ });
141
+ return {
142
+ tasks,
143
+ diseases: diseases.sort(diseaseSort),
144
+ byId: new Map(tasks.map((task) => [task.id, task])),
145
+ };
146
+ }
147
+ export function resolveTaskReadId(board, input) {
148
+ return resolveTaskId(board, input);
149
+ }
150
+ export function resolveTaskId(board, input) {
151
+ if (!TASK_READ_ID.test(input)) {
152
+ throw new FlowError("INVALID_TARGET", `invalid task id '${input}'`);
153
+ }
154
+ const matches = board.tasks
155
+ .map((task) => task.id)
156
+ .filter((id) => id.startsWith(input))
157
+ .sort();
158
+ if (matches.length === 0)
159
+ throw new FlowError("INVALID_TARGET", `task '${input}' not found`);
160
+ if (matches.length > 1) {
161
+ throw Object.assign(new FlowError("CONTRACT_AMBIGUOUS", `task prefix '${input}' is ambiguous: ${matches.join(", ")}`), { candidates: matches });
162
+ }
163
+ return matches[0];
164
+ }
165
+ export function requireCanonicalTaskId(input) {
166
+ if (!TASK_ID.test(input)) {
167
+ throw new FlowError("INVALID_TARGET", "task mutations require a canonical full task id");
168
+ }
169
+ return input;
170
+ }
171
+ export function requireHealthy(board) {
172
+ if (board.diseases.length > 0) {
173
+ throw new FlowError("INTERNAL_STATE", `authoritative task board is diseased: ${board.diseases.map((item) => item.message).join("; ")}`);
174
+ }
175
+ }
@@ -0,0 +1,211 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
3
+ import { dateTimestamp, invalid, nonblank, } from "./validation.js";
4
+ export async function readTaskBoard(repository) {
5
+ return repository.read();
6
+ }
7
+ function documentFromView(view) {
8
+ return {
9
+ id: view.id,
10
+ title: view.title,
11
+ state: view.state,
12
+ pri: view.pri,
13
+ needs: [...view.needs],
14
+ parent: view.parent,
15
+ from: [...view.from],
16
+ createdAt: view.createdAt,
17
+ creator: view.creator,
18
+ ...(view.startedAt === undefined ? {} : { startedAt: view.startedAt }),
19
+ ...(view.contractId === undefined ? {} : { contractId: view.contractId }),
20
+ body: view.body,
21
+ };
22
+ }
23
+ function replaceDocument(document) {
24
+ return {
25
+ replacements: [document],
26
+ select(resulting) {
27
+ const value = resulting.byId.get(document.id);
28
+ if (!value)
29
+ throw new FlowError("INTERNAL_STATE", `mutated task ${document.id} disappeared from the board`);
30
+ return value;
31
+ },
32
+ };
33
+ }
34
+ function requireExisting(board, id) {
35
+ const task = board.byId.get(requireCanonicalTaskId(id));
36
+ if (!task)
37
+ throw new FlowError("INVALID_TARGET", `task '${id}' not found`);
38
+ return task;
39
+ }
40
+ function resolveMutationIds(board, ids) {
41
+ return ids.map((id) => resolveTaskId(board, id));
42
+ }
43
+ export async function createTask(repository, input, deps) {
44
+ nonblank("title", input.title);
45
+ nonblank("creator", input.creator);
46
+ const pri = input.pri ?? 2;
47
+ if (!Number.isInteger(pri) || pri < 0 || pri > 3)
48
+ invalid("task priority must be an integer from 0 through 3");
49
+ return repository.mutate((board) => {
50
+ requireHealthy(board);
51
+ const needs = resolveMutationIds(board, input.needs ?? []);
52
+ if (new Set(needs).size !== needs.length)
53
+ invalid("task needs must be unique");
54
+ const parent = input.parent == null ? null : resolveTaskId(board, input.parent);
55
+ const from = resolveMutationIds(board, input.from ?? []);
56
+ if (new Set(from).size !== from.length)
57
+ 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;
67
+ }
68
+ }
69
+ if (!id)
70
+ throw new FlowError("CONCURRENT_MODIFICATION", "could not allocate a unique task id after 32 attempts");
71
+ const document = {
72
+ id,
73
+ title: input.title,
74
+ state: "open",
75
+ pri,
76
+ needs,
77
+ parent,
78
+ from,
79
+ createdAt: dateTimestamp("createdAt", deps.now()),
80
+ creator: input.creator,
81
+ body: input.body ?? "",
82
+ };
83
+ return replaceDocument(document);
84
+ });
85
+ }
86
+ export async function updateTask(repository, fullIdValue, patch) {
87
+ const hasChange = Object.values(patch).some((value) => value !== undefined);
88
+ if (!hasChange)
89
+ throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
90
+ return repository.mutate((board) => {
91
+ requireHealthy(board);
92
+ const id = resolveTaskId(board, fullIdValue);
93
+ const current = requireExisting(board, id);
94
+ const document = documentFromView(current);
95
+ if (patch.title !== undefined)
96
+ document.title = nonblank("title", patch.title);
97
+ if (patch.body !== undefined)
98
+ document.body = patch.body;
99
+ if (patch.pri !== undefined) {
100
+ if (!Number.isInteger(patch.pri) || patch.pri < 0 || patch.pri > 3)
101
+ invalid("task priority must be an integer from 0 through 3");
102
+ document.pri = patch.pri;
103
+ }
104
+ const additions = resolveMutationIds(board, patch.addNeeds ?? []);
105
+ const removals = new Set(resolveMutationIds(board, patch.dropNeeds ?? []));
106
+ document.needs = [...new Set([...document.needs.filter((need) => !removals.has(need)), ...additions])];
107
+ if (patch.parent !== undefined)
108
+ document.parent = patch.parent === null ? null : resolveTaskId(board, patch.parent);
109
+ if (patch.from !== undefined) {
110
+ document.from = [...new Set([...document.from, ...resolveMutationIds(board, patch.from)])];
111
+ }
112
+ if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
113
+ throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
114
+ }
115
+ return replaceDocument(document);
116
+ });
117
+ }
118
+ async function transition(repository, fullIdValue, apply) {
119
+ return repository.mutate((board) => {
120
+ requireHealthy(board);
121
+ const id = fullIdValue === undefined
122
+ ? selectSoleInProgress(board).id
123
+ : resolveTaskId(board, fullIdValue);
124
+ const document = documentFromView(requireExisting(board, id));
125
+ apply(document);
126
+ return replaceDocument(document);
127
+ });
128
+ }
129
+ function rejectBound(document, action = "state") {
130
+ if (document.contractId !== undefined) {
131
+ if (action === "drop") {
132
+ throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; forfeit the contract before dropping this task`);
133
+ }
134
+ throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; its state follows the contract`);
135
+ }
136
+ }
137
+ export async function startTask(repository, fullId, now) {
138
+ return transition(repository, fullId, (document) => {
139
+ rejectBound(document);
140
+ if (document.state !== "open")
141
+ invalid(`task ${document.id} is ${document.state}, not open`);
142
+ document.state = "in_progress";
143
+ document.startedAt = dateTimestamp("startedAt", now);
144
+ });
145
+ }
146
+ export async function stopTask(repository, fullId) {
147
+ return transition(repository, fullId, (document) => {
148
+ rejectBound(document);
149
+ if (document.state !== "in_progress")
150
+ invalid(`task ${document.id} is ${document.state}, not in_progress`);
151
+ document.state = "open";
152
+ delete document.startedAt;
153
+ });
154
+ }
155
+ export async function doneTask(repository, fullId) {
156
+ return transition(repository, fullId, (document) => {
157
+ rejectBound(document);
158
+ if (document.state !== "open" && document.state !== "in_progress") {
159
+ invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
160
+ }
161
+ document.state = "done";
162
+ delete document.startedAt;
163
+ });
164
+ }
165
+ export async function dropTask(repository, fullId) {
166
+ return transition(repository, fullId, (document) => {
167
+ rejectBound(document, "drop");
168
+ if (document.state !== "open" && document.state !== "in_progress") {
169
+ invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
170
+ }
171
+ document.state = "drop";
172
+ delete document.startedAt;
173
+ });
174
+ }
175
+ export function selectSoleInProgress(board) {
176
+ const candidates = board.tasks.filter((task) => task.state === "in_progress");
177
+ if (candidates.length === 1)
178
+ return candidates[0];
179
+ const ids = candidates.map((task) => task.id).sort();
180
+ const code = candidates.length === 0 ? "INVALID_TARGET" : "CONTRACT_AMBIGUOUS";
181
+ throw Object.assign(new FlowError(code, candidates.length === 0
182
+ ? "no task in progress"
183
+ : "more than one task in progress; pass an ID"), { candidates: ids, candidateTasks: candidates });
184
+ }
185
+ /** Reopen task pointers whose contract authority has disappeared. The caller
186
+ * supplies the ledger fact; this operation itself remains storage-neutral. */
187
+ export async function rollbackOrphanTaskPointers(repository, contractIds) {
188
+ const before = await repository.read();
189
+ const orphans = before.tasks
190
+ .filter((task) => task.contractId !== undefined && !contractIds.has(task.contractId))
191
+ .map((task) => ({ taskId: task.id, contractId: task.contractId }))
192
+ .sort((a, b) => a.taskId.localeCompare(b.taskId));
193
+ if (orphans.length === 0)
194
+ return [];
195
+ await repository.mutate((board) => {
196
+ const replacements = orphans.flatMap(({ taskId: id }) => {
197
+ const current = board.byId.get(id);
198
+ if (!current || current.contractId === undefined || contractIds.has(current.contractId))
199
+ return [];
200
+ const document = documentFromView(current);
201
+ document.state = "open";
202
+ delete document.startedAt;
203
+ delete document.contractId;
204
+ return [document];
205
+ });
206
+ if (replacements.length === 0)
207
+ throw new FlowError("CONCURRENT_MODIFICATION", "task pointers changed while doctor was reconciling them");
208
+ return { replacements, select: () => undefined };
209
+ });
210
+ return orphans;
211
+ }