@astrosheep/keiyaku 2.9.3 → 2.9.5

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 (282) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +14 -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 +26 -222
  6. package/build/agents/harness/index.js +3 -1
  7. package/build/agents/harness/outcome.js +68 -5
  8. package/build/agents/harness/projection.js +179 -124
  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 +82 -53
  21. package/build/agents/providers/codex-app-server/events.js +90 -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/events.js +4 -3
  36. package/build/agents/providers/pi/registration.js +145 -0
  37. package/build/agents/providers/pi/session-schema.js +6 -0
  38. package/build/agents/providers/pi/terms.js +1 -0
  39. package/build/agents/providers/registration.js +7 -0
  40. package/build/agents/providers/registry.js +76 -0
  41. package/build/agents/selector.js +2 -29
  42. package/build/agents/types.js +10 -0
  43. package/build/artifact-directories.js +22 -0
  44. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  45. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  46. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  47. package/build/cli/commands/akuma/view/handler.js +5 -4
  48. package/build/cli/commands/akuma/view/meta.js +1 -1
  49. package/build/cli/commands/akuma.js +6 -0
  50. package/build/cli/commands/contract/amend/handler.js +8 -2
  51. package/build/cli/commands/contract/amend/meta.js +4 -4
  52. package/build/cli/commands/contract/arc/handler.js +8 -2
  53. package/build/cli/commands/contract/arc/meta.js +4 -4
  54. package/build/cli/commands/contract/audit/handler.js +16 -0
  55. package/build/cli/commands/contract/audit/meta.js +16 -0
  56. package/build/cli/commands/contract/bind/handler.js +4 -3
  57. package/build/cli/commands/contract/bind/meta.js +44 -8
  58. package/build/cli/commands/contract/catalog.js +2 -0
  59. package/build/cli/commands/contract/forfeit/handler.js +4 -4
  60. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  61. package/build/cli/commands/contract/log/handler.js +3 -3
  62. package/build/cli/commands/contract/log/meta.js +1 -1
  63. package/build/cli/commands/contract/petition/handler.js +5 -8
  64. package/build/cli/commands/contract/petition/meta.js +6 -6
  65. package/build/cli/commands/contract/renew/handler.js +20 -6
  66. package/build/cli/commands/contract/renew/meta.js +6 -5
  67. package/build/cli/commands/metadata.js +59 -0
  68. package/build/cli/commands/projection/call/handler.js +6 -5
  69. package/build/cli/commands/projection/call/meta.js +4 -1
  70. package/build/cli/commands/projection/kill/handler.js +16 -32
  71. package/build/cli/commands/projection/kill/meta.js +1 -1
  72. package/build/cli/commands/projection/revive/meta.js +4 -1
  73. package/build/cli/commands/projection/shared.js +28 -0
  74. package/build/cli/commands/projection/status/handler.js +1 -1
  75. package/build/cli/commands/projection/status/meta.js +1 -1
  76. package/build/cli/commands/projection/tell/handler.js +14 -30
  77. package/build/cli/commands/projection/tell/meta.js +4 -1
  78. package/build/cli/commands/projection/wait/handler.js +45 -22
  79. package/build/cli/commands/projection/wait/meta.js +8 -3
  80. package/build/cli/commands/registry.js +2 -54
  81. package/build/cli/commands/shared.js +10 -7
  82. package/build/cli/commands/skills/install/handler.js +2 -5
  83. package/build/cli/commands/skills/install/meta.js +1 -1
  84. package/build/cli/commands/skills/skills/handler.js +1 -1
  85. package/build/cli/commands/skills/skills/meta.js +1 -1
  86. package/build/cli/commands/system/completion/meta.js +1 -1
  87. package/build/cli/commands/system/dump-env/meta.js +1 -1
  88. package/build/cli/commands/system/guide/meta.js +1 -1
  89. package/build/cli/commands/task/add/handler.js +7 -15
  90. package/build/cli/commands/task/add/meta.js +4 -1
  91. package/build/cli/commands/task/catalog.js +2 -2
  92. package/build/cli/commands/task/doctor/meta.js +1 -1
  93. package/build/cli/commands/task/done/handler.js +4 -8
  94. package/build/cli/commands/task/done/meta.js +1 -1
  95. package/build/cli/commands/task/drop/handler.js +3 -6
  96. package/build/cli/commands/task/drop/meta.js +1 -1
  97. package/build/cli/commands/task/ls/handler.js +12 -8
  98. package/build/cli/commands/task/ls/meta.js +3 -3
  99. package/build/cli/commands/task/shared.js +14 -4
  100. package/build/cli/commands/task/{view → show}/handler.js +7 -8
  101. package/build/cli/commands/task/show/meta.js +8 -0
  102. package/build/cli/commands/task/start/handler.js +3 -6
  103. package/build/cli/commands/task/start/meta.js +1 -1
  104. package/build/cli/commands/task/stop/handler.js +4 -8
  105. package/build/cli/commands/task/stop/meta.js +1 -1
  106. package/build/cli/commands/task/task/handler.js +1 -1
  107. package/build/cli/commands/task/task/meta.js +1 -1
  108. package/build/cli/commands/task/update/handler.js +6 -11
  109. package/build/cli/commands/task/update/meta.js +4 -1
  110. package/build/cli/completion.js +1 -1
  111. package/build/cli/flags.js +37 -6
  112. package/build/cli/help.js +7 -14
  113. package/build/cli/index.js +112 -154
  114. package/build/cli/missing-subcommand.js +7 -0
  115. package/build/cli/output-stream-error-policy.js +19 -5
  116. package/build/cli/parse-flags.js +33 -7
  117. package/build/cli/parse-metadata.js +3 -9
  118. package/build/cli/parse-selectors.js +2 -1
  119. package/build/cli/parse.js +35 -5
  120. package/build/cli/projection-address.js +12 -12
  121. package/build/cli/render/address.js +41 -37
  122. package/build/cli/render/arc.js +24 -12
  123. package/build/cli/render/audit.js +121 -0
  124. package/build/cli/render/call.js +11 -5
  125. package/build/cli/render/errors.js +36 -1
  126. package/build/cli/render/format.js +4 -0
  127. package/build/cli/render/kanshi.js +6 -12
  128. package/build/cli/render/kill.js +9 -0
  129. package/build/cli/render/misc.js +58 -13
  130. package/build/cli/render/petition.js +20 -15
  131. package/build/cli/render/projection-activity.js +15 -6
  132. package/build/cli/render/shared.js +60 -21
  133. package/build/cli/render/skills.js +10 -0
  134. package/build/cli/render/status.js +56 -23
  135. package/build/cli/render/success-response.js +78 -0
  136. package/build/cli/render/task.js +66 -57
  137. package/build/cli/render/tell.js +5 -0
  138. package/build/cli/render/terminal-failure.js +63 -0
  139. package/build/cli/render/types.js +1 -0
  140. package/build/cli/render/wait.js +68 -38
  141. package/build/cli/types.js +1 -1
  142. package/build/config/akuma-loader.js +0 -5
  143. package/build/config/env-keys.js +3 -1
  144. package/build/config/env.js +3 -0
  145. package/build/config/provider-policy-ownership.js +3 -32
  146. package/build/config/settings/disease.js +8 -1
  147. package/build/config/settings/loader.js +11 -3
  148. package/build/config/settings/provider-instance-fields.js +51 -0
  149. package/build/config/settings/schema.js +6 -176
  150. package/build/core/addressing.js +66 -16
  151. package/build/core/amend.js +61 -18
  152. package/build/core/arc.js +33 -73
  153. package/build/core/audit.js +650 -0
  154. package/build/core/bind.js +82 -35
  155. package/build/core/call/call.js +15 -9
  156. package/build/core/call/context.js +6 -51
  157. package/build/core/call/execution.js +22 -22
  158. package/build/core/call/prompt.js +14 -1
  159. package/build/core/command-markdown.js +45 -0
  160. package/build/core/context.js +13 -7
  161. package/build/core/contract-view.js +22 -6
  162. package/build/core/draft.js +139 -19
  163. package/build/core/entry.js +3 -4
  164. package/build/core/forfeit.js +5 -2
  165. package/build/core/hints.js +1 -75
  166. package/build/core/identifier-slug.js +66 -0
  167. package/build/core/ids.js +25 -11
  168. package/build/core/ledger.js +20 -68
  169. package/build/core/markdown/lex.js +2 -1
  170. package/build/core/markdown/parser.js +0 -68
  171. package/build/core/markdown/render.js +68 -0
  172. package/build/core/markdown/sections.js +2 -1
  173. package/build/core/outcome-base.js +4 -0
  174. package/build/core/projection/generation/database.js +41 -52
  175. package/build/core/projection/generation/model.js +63 -2
  176. package/build/core/{projection-generation-continuation.js → projection/generation/projection-generation-continuation.js} +3 -3
  177. package/build/core/{projection-generation-execution.js → projection/generation/projection-generation-execution.js} +36 -53
  178. package/build/core/{projection-generation-identity.js → projection/generation/projection-generation-identity.js} +2 -2
  179. package/build/core/{projection-generation-launcher.js → projection/generation/projection-generation-launcher.js} +9 -7
  180. package/build/core/{projection-generation-process.js → projection/generation/projection-generation-process.js} +2 -1
  181. package/build/core/{projection-generation-runner.js → projection/generation/projection-generation-runner.js} +19 -3
  182. package/build/core/projection/generation/store.js +3 -3
  183. package/build/core/projection/generation/transitions.js +19 -3
  184. package/build/core/projection/index.js +27 -0
  185. package/build/core/{projection-activity.js → projection/projection-activity.js} +12 -0
  186. package/build/core/{projection-alias.js → projection/projection-alias.js} +13 -13
  187. package/build/core/projection/projection-coordinate.js +61 -0
  188. package/build/core/{projection-core.js → projection/projection-core.js} +9 -9
  189. package/build/core/{projection-execution-observer.js → projection/projection-execution-observer.js} +37 -78
  190. package/build/core/{projection-identity.js → projection/projection-identity.js} +1 -1
  191. package/build/core/{projection-kill.js → projection/projection-kill.js} +1 -1
  192. package/build/core/{projection-life-observer.js → projection/projection-life-observer.js} +16 -2
  193. package/build/core/{projection-mint.js → projection/projection-mint.js} +10 -10
  194. package/build/core/{projection-status.js → projection/projection-status.js} +163 -45
  195. package/build/core/projection/projection-terminal-failure.js +106 -0
  196. package/build/core/projection/projection-wait.js +275 -0
  197. package/build/core/projection/projection-wake.js +227 -0
  198. package/build/core/provider-loader-ports.js +4 -0
  199. package/build/core/ref-log.js +65 -40
  200. package/build/core/registry.js +63 -10
  201. package/build/core/renew-session.js +128 -0
  202. package/build/core/renew.js +448 -102
  203. package/build/core/scope.js +272 -68
  204. package/build/core/seal.js +165 -40
  205. package/build/core/{claim-delivery.js → settlement/claim-delivery.js} +8 -8
  206. package/build/core/{claim.js → settlement/claim.js} +32 -8
  207. package/build/core/settlement/index.js +19 -0
  208. package/build/core/{petition-claim-gates.js → settlement/petition-claim-gates.js} +24 -49
  209. package/build/core/{petition-forfeit.js → settlement/petition-forfeit.js} +2 -2
  210. package/build/core/{petition-head-guard.js → settlement/petition-head-guard.js} +2 -2
  211. package/build/core/settlement/petition-io.js +1 -0
  212. package/build/core/{petition-preview.js → settlement/petition-preview.js} +2 -2
  213. package/build/core/{petition.js → settlement/petition.js} +21 -12
  214. package/build/core/settlement/queue-read-model.js +143 -0
  215. package/build/core/settlement/queue-seat-allocation.js +156 -0
  216. package/build/core/settlement/queue.js +6 -0
  217. package/build/core/settlement/settlement.js +288 -0
  218. package/build/core/{verdict.js → settlement/verdict.js} +9 -37
  219. package/build/core/settlement/verification.js +59 -0
  220. package/build/core/status/board.js +62 -41
  221. package/build/core/status/ledger-batch.js +158 -0
  222. package/build/core/status/reconciliation.js +10 -4
  223. package/build/core/stored-agent-event.js +6 -2
  224. package/build/core/task/board.js +41 -4
  225. package/build/core/task/commands.js +45 -28
  226. package/build/core/task/document.js +5 -0
  227. package/build/core/task/index.js +12 -0
  228. package/build/core/task/model.js +8 -1
  229. package/build/core/task/query.js +172 -0
  230. package/build/core/task/settlement-git.js +265 -74
  231. package/build/core/task/settlement-policy.js +67 -29
  232. package/build/core/task/source-board.js +1 -0
  233. package/build/core/task/task-contract.js +444 -0
  234. package/build/core/{task-doctor.js → task/task-doctor.js} +1 -1
  235. package/build/core/{task-git-actor.js → task/task-git-actor.js} +5 -10
  236. package/build/core/{task-git-runtime.js → task/task-git-runtime.js} +3 -3
  237. package/build/core/{task-git-store.js → task/task-git-store.js} +90 -19
  238. package/build/core/task/task-repository.js +1 -0
  239. package/build/core/{task-store-repository.js → task/task-store-repository.js} +6 -4
  240. package/build/core/task/task-store.js +1 -0
  241. package/build/core/{task-worktree.js → task/task-worktree.js} +2 -1
  242. package/build/core/task/task.js +7 -0
  243. package/build/core/task/tree.js +37 -0
  244. package/build/core/task/validation.js +5 -2
  245. package/build/core/transcripts.js +15 -3
  246. package/build/core/worktree-path.js +43 -28
  247. package/build/flow-error.js +83 -39
  248. package/build/fs/path-coordinate.js +16 -0
  249. package/build/generated/version.js +3 -2
  250. package/build/git/branches.js +2 -1
  251. package/build/git/core.js +106 -2
  252. package/build/git/diff/parsers.js +23 -13
  253. package/build/git/diff/structured.js +127 -0
  254. package/build/git/path-coordinate.js +17 -0
  255. package/build/git/refs.js +57 -0
  256. package/build/git/streaming-batch.js +301 -0
  257. package/build/git/worktree.js +65 -0
  258. package/build/index.js +9 -9
  259. package/build/keiyaku.js +2 -36
  260. package/build/telemetry/logger.js +4 -2
  261. package/package.json +4 -10
  262. package/skills/keiyaku/SKILL.md +5 -4
  263. package/skills/keiyaku-akuma/SKILL.md +25 -11
  264. package/skills/keiyaku-task/SKILL.md +17 -8
  265. package/skills/keiyaku-workflow/SKILL.md +58 -9
  266. package/build/agents/providers/codex-sdk.js +0 -329
  267. package/build/cli/commands/task/view/meta.js +0 -8
  268. package/build/core/command-io.js +0 -174
  269. package/build/core/path-coordinate.js +0 -27
  270. package/build/core/petition-claim.js +0 -117
  271. package/build/core/petition-run.js +0 -83
  272. package/build/core/projection-coordinate.js +0 -63
  273. package/build/core/projection-wait.js +0 -165
  274. package/build/core/projection-wake.js +0 -136
  275. package/build/core/queue.js +0 -343
  276. package/build/core/task-contract.js +0 -237
  277. package/build/core/task.js +0 -4
  278. /package/build/{core/task-repository.js → agents/launch-snapshot/types.js} +0 -0
  279. /package/build/{core/task-store.js → agents/providers/claude-agent-sdk/terms.js} +0 -0
  280. /package/build/core/{projection-generation-runtime.js → projection/generation/projection-generation-runtime.js} +0 -0
  281. /package/build/core/{projection-life-protocol.js → projection/projection-life-protocol.js} +0 -0
  282. /package/build/core/{projection-runner-lock.js → projection/projection-runner-lock.js} +0 -0
@@ -0,0 +1,172 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { isCommissionId } from "../ids.js";
3
+ import { taskUpdatedAt } from "./model.js";
4
+ import { isTaskId } from "./validation.js";
5
+ const FILTER_FIELDS = new Set([
6
+ "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
7
+ ]);
8
+ const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
9
+ const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
10
+ const SORT_KEYS = new Set(["priority", "modified", "created"]);
11
+ const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
12
+ const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
13
+ export function deriveTaskDisplay(task) {
14
+ if (task.state === "done")
15
+ return "done";
16
+ if (task.state === "drop")
17
+ return "dropped";
18
+ if (task.contractId !== undefined)
19
+ return "bound";
20
+ if (task.state === "in_progress")
21
+ return "in_progress";
22
+ return task.blocked ? "blocked" : "ready";
23
+ }
24
+ function invalidFilter(detail) {
25
+ throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`);
26
+ }
27
+ export function parseTaskFilter(expression) {
28
+ const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|=|<|>)(.*)$/.exec(expression);
29
+ if (!match)
30
+ invalidFilter(`'${expression}' must use FIELD OP VALUE`);
31
+ const field = match[1];
32
+ const operator = match[2];
33
+ if (!FILTER_FIELDS.has(field))
34
+ invalidFilter(`unknown field '${field}'`);
35
+ if (!ORDERED_FIELDS.has(field) && operator !== "=" && operator !== "!=") {
36
+ invalidFilter(`field '${field}' does not support operator '${operator}'`);
37
+ }
38
+ const values = match[3].split(",");
39
+ if (values.some((value) => value.length === 0))
40
+ invalidFilter(`field '${field}' has an empty value`);
41
+ validateFilterValues(field, values);
42
+ return { field, operator, values };
43
+ }
44
+ function parseTimestamp(value, nowMs) {
45
+ const relative = RELATIVE_TIME.exec(value);
46
+ if (relative) {
47
+ const amount = Number(relative[1]);
48
+ const offset = amount * UNIT_MS[relative[2]];
49
+ if (!Number.isSafeInteger(offset))
50
+ invalidFilter(`timestamp '${value}' is outside the supported range`);
51
+ return nowMs - offset;
52
+ }
53
+ const timestamp = Date.parse(value);
54
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
55
+ invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`);
56
+ }
57
+ return timestamp;
58
+ }
59
+ function validateFilterValues(field, values) {
60
+ if (field === "state") {
61
+ for (const value of values)
62
+ if (!DISPLAY_VALUES.has(value))
63
+ invalidFilter(`unknown state '${value}'`);
64
+ return;
65
+ }
66
+ if (field === "priority") {
67
+ for (const value of values)
68
+ if (!/^[0-3]$/.test(value))
69
+ invalidFilter(`priority '${value}' must be 0 through 3`);
70
+ return;
71
+ }
72
+ if (field === "created" || field === "updated") {
73
+ for (const value of values)
74
+ parseTimestamp(value, 0);
75
+ return;
76
+ }
77
+ if (field === "parent" || field === "needs" || field === "blocks") {
78
+ for (const value of values) {
79
+ if (field === "parent" && value === "none")
80
+ continue;
81
+ if (!isTaskId(value))
82
+ invalidFilter(`value '${value}' must be a canonical full task id`);
83
+ }
84
+ return;
85
+ }
86
+ if (field === "contract") {
87
+ for (const value of values) {
88
+ if (value !== "none" && !isCommissionId(value))
89
+ invalidFilter(`value '${value}' must be a full commission id or none`);
90
+ }
91
+ }
92
+ }
93
+ export function parseTaskSort(value) {
94
+ if (value === undefined)
95
+ return undefined;
96
+ const keys = value.split(",");
97
+ if (keys.some((key) => !SORT_KEYS.has(key))) {
98
+ const invalid = keys.find((key) => !SORT_KEYS.has(key));
99
+ throw new FlowError("INVALID_TARGET", `invalid task sort key '${String(invalid)}'; expected priority, modified, or created`);
100
+ }
101
+ if (new Set(keys).size !== keys.length) {
102
+ throw new FlowError("INVALID_TARGET", "task sort keys must not repeat");
103
+ }
104
+ return keys;
105
+ }
106
+ function compareText(left, right) {
107
+ return left < right ? -1 : left > right ? 1 : 0;
108
+ }
109
+ function compareValue(left, right, operator) {
110
+ switch (operator) {
111
+ case "=": return left === right;
112
+ case "!=": return left !== right;
113
+ case "<": return left < right;
114
+ case "<=": return left <= right;
115
+ case ">": return left > right;
116
+ case ">=": return left >= right;
117
+ }
118
+ }
119
+ function matchesFilter(task, filter, nowMs) {
120
+ return filter.values.some((raw) => {
121
+ if (filter.field === "state")
122
+ return compareValue(deriveTaskDisplay(task), raw, filter.operator);
123
+ if (filter.field === "priority")
124
+ return compareValue(task.pri, Number(raw), filter.operator);
125
+ if (filter.field === "created")
126
+ return compareValue(Date.parse(task.createdAt), parseTimestamp(raw, nowMs), filter.operator);
127
+ if (filter.field === "updated")
128
+ return compareValue(Date.parse(taskUpdatedAt(task)), parseTimestamp(raw, nowMs), filter.operator);
129
+ const expected = raw === "none" ? null : raw;
130
+ if (filter.field === "parent")
131
+ return compareValue(task.parent ?? "\0", expected ?? "\0", filter.operator);
132
+ if (filter.field === "contract")
133
+ return compareValue(task.contractId ?? "\0", expected ?? "\0", filter.operator);
134
+ const relations = filter.field === "needs" ? task.needs : task.blocks;
135
+ return filter.operator === "=" ? relations.includes(raw) : !relations.includes(raw);
136
+ });
137
+ }
138
+ function selected(task, selection) {
139
+ const terminal = task.state === "done" || task.state === "drop";
140
+ return selection === "all" || (selection === "done" ? terminal : !terminal);
141
+ }
142
+ function displayOrder(task) {
143
+ const display = deriveTaskDisplay(task);
144
+ return display === "in_progress" ? 0 : display === "ready" ? 1 : display === "blocked" ? 2 : display === "bound" ? 3 : display === "done" ? 4 : 5;
145
+ }
146
+ function defaultCompare(left, right) {
147
+ return displayOrder(left) - displayOrder(right)
148
+ || left.pri - right.pri
149
+ || compareText(left.createdAt, right.createdAt)
150
+ || compareText(left.id, right.id);
151
+ }
152
+ function explicitCompare(keys, left, right) {
153
+ for (const key of keys) {
154
+ const difference = key === "priority"
155
+ ? left.pri - right.pri
156
+ : key === "modified"
157
+ ? compareText(taskUpdatedAt(right), taskUpdatedAt(left))
158
+ : compareText(right.createdAt, left.createdAt);
159
+ if (difference !== 0)
160
+ return difference;
161
+ }
162
+ return compareText(left.id, right.id);
163
+ }
164
+ export function selectTaskList(tasks, options = {}) {
165
+ const filters = (options.filters ?? []).map(parseTaskFilter);
166
+ const sort = parseTaskSort(options.sort);
167
+ const nowMs = options.nowMs ?? Date.now();
168
+ return tasks
169
+ .filter((task) => selected(task, options.selection ?? "default"))
170
+ .filter((task) => filters.every((filter) => matchesFilter(task, filter, nowMs)))
171
+ .sort(sort === undefined ? defaultCompare : (left, right) => explicitCompare(sort, left, right));
172
+ }
@@ -1,19 +1,13 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
  import { FlowError } from "../../flow-error.js";
5
- import { parseTaskDocument } from "../task.js";
6
- function gitResult(cwd, args, input, env) {
7
- return spawnSync("git", [...args], {
8
- cwd,
9
- input,
10
- encoding: "utf8",
11
- env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
12
- stdio: ["pipe", "pipe", "pipe"],
13
- });
4
+ import { runGitProcess } from "../../git/core.js";
5
+ import { parseTaskDocument } from "./task.js";
6
+ async function gitResult(cwd, args, input, env) {
7
+ return await runGitProcess(cwd, args, { input, env });
14
8
  }
15
- function git(cwd, args, input, env) {
16
- const result = gitResult(cwd, args, input, env);
9
+ async function git(cwd, args, input, env) {
10
+ const result = await gitResult(cwd, args, input, env);
17
11
  if (result.error) {
18
12
  throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
19
13
  }
@@ -29,36 +23,118 @@ function boardPath(root, source) {
29
23
  }
30
24
  return relative;
31
25
  }
32
- export function inspectCurrentBranch(cwd) {
33
- const branch = gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
26
+ export async function inspectCurrentBranch(cwd) {
27
+ const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
34
28
  if (branch.error) {
35
29
  throw new FlowError("INTERNAL_STATE", `cannot inspect HEAD: ${branch.error.message}`, { cause: branch.error });
36
30
  }
37
31
  if (branch.status !== 0 || branch.stdout.trim() === "") {
38
32
  throw new FlowError("DETACHED_HEAD", "cannot publish task board from detached HEAD");
39
33
  }
40
- return { ref: branch.stdout.trim(), head: git(cwd, ["rev-parse", "HEAD"]).trim() };
34
+ return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"])).trim() };
41
35
  }
42
- export function createTaskBoardCommit(input) {
36
+ export async function createTaskBoardCommit(input) {
43
37
  const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
44
38
  const indexPath = path.join(temporaryDirectory, "index");
45
39
  const env = { GIT_INDEX_FILE: indexPath };
46
40
  try {
47
- git(input.cwd, ["read-tree", input.parent], undefined, env);
48
- git(input.cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
41
+ await git(input.cwd, ["read-tree", input.parent], undefined, env);
49
42
  for (const source of input.sources) {
50
- const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
51
- git(input.cwd, ["update-index", "--add", "--cacheinfo", `100644,${oid},${boardPath(input.root, source)}`], undefined, env);
43
+ const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
44
+ await git(input.cwd, ["update-index", "--add", "--cacheinfo", `100644,${oid},${boardPath(input.root, source)}`], undefined, env);
45
+ }
46
+ const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
47
+ return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
48
+ }
49
+ finally {
50
+ fs.rmSync(temporaryDirectory, { recursive: true, force: true });
51
+ }
52
+ }
53
+ function taskMutationPath(write) {
54
+ return `.keiyaku/tasks/${write.id}.md`;
55
+ }
56
+ /** Build a commit that updates only the listed task paths on top of parent. */
57
+ export async function createTaskMutationCommit(input) {
58
+ if (input.writes.length === 0) {
59
+ throw new FlowError("INTERNAL_STATE", "task mutation commit requires at least one write");
60
+ }
61
+ const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
62
+ const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
63
+ try {
64
+ await git(input.cwd, ["read-tree", input.parent], undefined, env);
65
+ for (const write of input.writes) {
66
+ const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
67
+ await git(input.cwd, [
68
+ "update-index",
69
+ "--add",
70
+ "--cacheinfo",
71
+ `100644,${oid},${taskMutationPath(write)}`,
72
+ ], undefined, env);
52
73
  }
53
- const tree = git(input.cwd, ["write-tree"], undefined, env).trim();
54
- return git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject]).trim();
74
+ const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
75
+ const parentTree = (await git(input.cwd, ["rev-parse", `${input.parent}^{tree}`])).trim();
76
+ if (tree === parentTree)
77
+ return input.parent;
78
+ return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
55
79
  }
56
80
  finally {
57
81
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
58
82
  }
59
83
  }
60
- function taskTreeEntries(cwd, commit) {
61
- return git(cwd, ["ls-tree", "-r", "-z", commit, "--", ".keiyaku/tasks"])
84
+ /** Stage only the mutated task paths into the real index; leave unrelated entries alone. */
85
+ export async function reconcileTaskMutationIndex(cwd, writes) {
86
+ for (const write of writes) {
87
+ const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
88
+ await git(cwd, [
89
+ "update-index",
90
+ "--add",
91
+ "--cacheinfo",
92
+ `100644,${oid},${taskMutationPath(write)}`,
93
+ ]);
94
+ }
95
+ }
96
+ /**
97
+ * Atomically publish a pathspec-scoped task micro-commit under the hub branch.
98
+ * Unrelated staged and unstaged non-task bytes are never read into the commit.
99
+ */
100
+ export async function publishTaskMutationCommit(input, deps = {}) {
101
+ let lastError;
102
+ for (let attempt = 0; attempt < 4; attempt += 1) {
103
+ const branch = await inspectCurrentBranch(input.cwd);
104
+ const commit = await createTaskMutationCommit({
105
+ cwd: input.cwd,
106
+ gitDir: input.gitDir,
107
+ parent: branch.head,
108
+ writes: input.writes,
109
+ subject: input.subject,
110
+ });
111
+ if (commit === branch.head) {
112
+ await reconcileTaskMutationIndex(input.cwd, input.writes);
113
+ return commit;
114
+ }
115
+ try {
116
+ await updateBranch({
117
+ cwd: input.cwd,
118
+ ref: branch.ref,
119
+ next: commit,
120
+ expected: branch.head,
121
+ }, deps);
122
+ try {
123
+ await reconcileTaskMutationIndex(input.cwd, input.writes);
124
+ }
125
+ catch {
126
+ // Branch fact remains authoritative; index may be repaired by a later mutation.
127
+ }
128
+ return commit;
129
+ }
130
+ catch (error) {
131
+ lastError = error;
132
+ }
133
+ }
134
+ throw lastError;
135
+ }
136
+ async function taskTreeEntries(cwd, commit) {
137
+ return (await git(cwd, ["ls-tree", "-r", "-z", commit, "--", ".keiyaku/tasks"]))
62
138
  .split("\0")
63
139
  .filter(Boolean)
64
140
  .map((record) => {
@@ -70,47 +146,79 @@ function taskTreeEntries(cwd, commit) {
70
146
  return { mode, oid, path: record.slice(tab + 1) };
71
147
  });
72
148
  }
73
- export function reconcileTaskIndex(cwd, commit) {
74
- const existing = git(cwd, ["ls-files", "-z", "--", ".keiyaku/tasks"])
75
- .split("\0")
76
- .filter(Boolean);
77
- for (const file of existing)
78
- git(cwd, ["update-index", "--force-remove", "--", file]);
79
- for (const entry of taskTreeEntries(cwd, commit)) {
80
- git(cwd, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${entry.path}`]);
149
+ /** Reconcile only settlement-owned task paths; never rewrite an unrelated index entry. */
150
+ export async function reconcileTaskPathsetIndex(cwd, commit, paths) {
151
+ const entries = new Map((await taskTreeEntries(cwd, commit)).map((entry) => [entry.path, entry]));
152
+ for (const taskPath of paths) {
153
+ const entry = entries.get(taskPath);
154
+ if (entry) {
155
+ await git(cwd, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${entry.path}`]);
156
+ }
157
+ else {
158
+ await git(cwd, ["update-index", "--force-remove", "--", taskPath]);
159
+ }
81
160
  }
82
161
  }
83
- export function refHead(cwd, ref) {
84
- return git(cwd, ["rev-parse", ref]).trim();
162
+ /** Task paths tracked by one authoritative commit, independent of the worktree index. */
163
+ export async function trackedTaskPaths(cwd, commit) {
164
+ return (await taskTreeEntries(cwd, commit)).map((entry) => entry.path).sort();
85
165
  }
86
- export function readableRefHead(cwd, ref) {
87
- const result = gitResult(cwd, ["rev-parse", ref]);
88
- if (result.status !== 0)
89
- return undefined;
90
- return result.stdout.trim();
166
+ export async function refHead(cwd, ref) {
167
+ return (await git(cwd, ["rev-parse", ref])).trim();
91
168
  }
92
- export function taskContractAtCommit(cwd, commit, id) {
169
+ export async function taskContractAtCommit(cwd, commit, id) {
93
170
  const sourcePath = `.keiyaku/tasks/${id}.md`;
94
- const result = gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
171
+ const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
95
172
  if (result.status !== 0)
96
173
  return undefined;
97
174
  return parseTaskDocument({ path: sourcePath, text: result.stdout }).contractId;
98
175
  }
176
+ /** Bound task ids and their complete parent ancestry from the immutable bind base. */
177
+ export async function taskSettlementOwnershipAtCommit(cwd, commit, contractId) {
178
+ const documents = new Map();
179
+ for (const entry of await taskTreeEntries(cwd, commit)) {
180
+ const text = await git(cwd, ["cat-file", "blob", entry.oid]);
181
+ const document = parseTaskDocument({ path: entry.path, text });
182
+ documents.set(document.id, document);
183
+ }
184
+ const taskIds = [...documents.values()]
185
+ .filter((document) => document.contractId === contractId)
186
+ .map((document) => document.id)
187
+ .sort();
188
+ const ownedIds = new Set();
189
+ for (const taskId of taskIds) {
190
+ let current = taskId;
191
+ while (current !== null) {
192
+ if (ownedIds.has(current))
193
+ break;
194
+ ownedIds.add(current);
195
+ const document = documents.get(current);
196
+ if (!document) {
197
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} lost task ${current} from the owned ancestry`);
198
+ }
199
+ current = document.parent;
200
+ }
201
+ }
202
+ return {
203
+ taskIds,
204
+ paths: [...ownedIds].map((id) => `.keiyaku/tasks/${id}.md`).sort(),
205
+ };
206
+ }
99
207
  export async function updateBranch(input, deps) {
100
208
  if (deps.updateBranch)
101
209
  await deps.updateBranch(input);
102
210
  else
103
- git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
211
+ await git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
104
212
  }
105
213
  export async function publishForwardTaskBoardCommit(input, deps) {
106
214
  let lastError;
107
215
  for (let attempt = 0; attempt < 4; attempt += 1) {
108
- const parent = refHead(input.cwd, input.ref);
109
- const commit = createTaskBoardCommit({ ...input, parent });
216
+ const parent = await refHead(input.cwd, input.ref);
217
+ const commit = await createTaskBoardCommit({ ...input, parent });
110
218
  try {
111
219
  await updateBranch({ cwd: input.cwd, ref: input.ref, next: commit, expected: parent }, deps);
112
220
  try {
113
- reconcileTaskIndex(input.cwd, commit);
221
+ await reconcileTaskPathsetIndex(input.cwd, commit, input.paths);
114
222
  }
115
223
  catch { /* the forward branch fact remains authoritative */ }
116
224
  return commit;
@@ -121,32 +229,32 @@ export async function publishForwardTaskBoardCommit(input, deps) {
121
229
  }
122
230
  throw lastError;
123
231
  }
124
- function treeWithoutTasks(cwd, commit, gitDir) {
232
+ async function treeWithoutTasks(cwd, commit, gitDir) {
125
233
  const temporaryDirectory = fs.mkdtempSync(path.join(gitDir, "keiyaku-landing-index-"));
126
234
  const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
127
235
  try {
128
- git(cwd, ["read-tree", commit], undefined, env);
129
- git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
130
- return git(cwd, ["write-tree"], undefined, env).trim();
236
+ await git(cwd, ["read-tree", commit], undefined, env);
237
+ await git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
238
+ return (await git(cwd, ["write-tree"], undefined, env)).trim();
131
239
  }
132
240
  finally {
133
241
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
134
242
  }
135
243
  }
136
- function composeLandingTree(input) {
137
- const baseTree = treeWithoutTasks(input.cwd, input.base, input.gitDir);
138
- const targetTree = treeWithoutTasks(input.cwd, input.target, input.gitDir);
139
- const deliveryTree = treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
244
+ export async function composeLandingTree(input) {
245
+ const baseTree = await treeWithoutTasks(input.cwd, input.base, input.gitDir);
246
+ const targetTree = await treeWithoutTasks(input.cwd, input.target, input.gitDir);
247
+ const deliveryTree = await treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
140
248
  let mergedTree;
141
249
  try {
142
- mergedTree = git(input.cwd, [
250
+ mergedTree = (await git(input.cwd, [
143
251
  "merge-tree",
144
252
  "--write-tree",
145
253
  "--merge-base",
146
254
  baseTree,
147
255
  targetTree,
148
256
  deliveryTree,
149
- ]).trim();
257
+ ])).trim();
150
258
  }
151
259
  catch (error) {
152
260
  throw new FlowError("CLOSE_MERGE_CONFLICT", "claim merge conflict outside task board", { cause: error });
@@ -154,43 +262,126 @@ function composeLandingTree(input) {
154
262
  const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-landing-index-"));
155
263
  const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
156
264
  try {
157
- git(input.cwd, ["read-tree", mergedTree], undefined, env);
158
- git(input.cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
265
+ await git(input.cwd, ["read-tree", mergedTree], undefined, env);
266
+ // merge-tree deliberately excluded task paths. Restore the target board as
267
+ // the inherited baseline, then overlay only settlement-authorized writes.
268
+ for (const entry of await taskTreeEntries(input.cwd, input.target)) {
269
+ await git(input.cwd, [
270
+ "update-index",
271
+ "--add",
272
+ "--cacheinfo",
273
+ `${entry.mode},${entry.oid},${entry.path}`,
274
+ ], undefined, env);
275
+ }
159
276
  for (const source of input.sources) {
160
- const oid = git(input.cwd, ["hash-object", "-w", "--stdin"], source.text).trim();
161
- git(input.cwd, [
277
+ const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
278
+ await git(input.cwd, [
162
279
  "update-index",
163
280
  "--add",
164
281
  "--cacheinfo",
165
282
  `100644,${oid},${boardPath(input.root, source)}`,
166
283
  ], undefined, env);
167
284
  }
168
- return git(input.cwd, ["write-tree"], undefined, env).trim();
285
+ return (await git(input.cwd, ["write-tree"], undefined, env)).trim();
169
286
  }
170
287
  finally {
171
288
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
172
289
  }
173
290
  }
174
- export function createLandingCommit(input) {
175
- const tree = composeLandingTree(input);
176
- return git(input.cwd, [
291
+ function landingCommitMessage(contractId, targetBranch) {
292
+ return `keiyaku: claim ${contractId}\n\nMerge contract ${contractId} into ${targetBranch}.\n`;
293
+ }
294
+ async function isAncestor(cwd, ancestor, descendant) {
295
+ return (await gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant])).status === 0;
296
+ }
297
+ export async function taskSourcesAtCommit(cwd, root, commit, paths) {
298
+ const sources = [];
299
+ for (const sourcePath of paths) {
300
+ const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
301
+ if (result.status !== 0)
302
+ return undefined;
303
+ sources.push({ path: path.join(root, ...sourcePath.split("/")), text: result.stdout });
304
+ }
305
+ return sources;
306
+ }
307
+ /**
308
+ * Find the durable landing receipt on the target's first-parent history.
309
+ * Equivalent trees and lookalike messages are insufficient: the commit must
310
+ * have our parent shape, settle every bind-base-owned task, and exactly match
311
+ * a recomposed landing tree over its own first parent.
312
+ */
313
+ export async function findAcceptedLandingCommit(input) {
314
+ const commits = (await git(input.cwd, ["rev-list", "--first-parent", input.target, `^${input.base}`]))
315
+ .trim()
316
+ .split("\n")
317
+ .filter(Boolean);
318
+ for (const commit of commits) {
319
+ const message = await git(input.cwd, ["show", "-s", "--format=%B", commit]);
320
+ if (message.trimEnd() !== landingCommitMessage(input.contractId, input.targetBranch).trimEnd())
321
+ continue;
322
+ const record = (await git(input.cwd, ["rev-list", "--parents", "-n", "1", commit])).trim().split(/\s+/);
323
+ const parents = record.slice(1);
324
+ const parentShapeAccepted = parents.length === 2
325
+ ? parents[1] === input.delivery
326
+ : parents.length === 1 && await isAncestor(input.cwd, input.delivery, parents[0]);
327
+ if (!parentShapeAccepted)
328
+ continue;
329
+ const sources = await taskSourcesAtCommit(input.cwd, input.root, commit, input.ownedPaths);
330
+ if (!sources)
331
+ continue;
332
+ let settled = false;
333
+ try {
334
+ const sourceByPath = new Map(sources.map((source) => [boardPath(input.root, source), source]));
335
+ settled = input.settledPaths.every((settledPath) => {
336
+ const source = sourceByPath.get(settledPath);
337
+ if (!source)
338
+ return false;
339
+ const document = parseTaskDocument(source);
340
+ return document.state === "done" && document.contractId === undefined;
341
+ });
342
+ }
343
+ catch {
344
+ // A malformed lookalike is not this operation's durable receipt.
345
+ }
346
+ if (!settled)
347
+ continue;
348
+ const expectedTree = await composeLandingTree({
349
+ cwd: input.cwd,
350
+ gitDir: input.gitDir,
351
+ root: input.root,
352
+ base: input.base,
353
+ target: parents[0],
354
+ delivery: input.delivery,
355
+ sources,
356
+ });
357
+ const actualTree = (await git(input.cwd, ["rev-parse", `${commit}^{tree}`])).trim();
358
+ if (actualTree === expectedTree)
359
+ return commit;
360
+ }
361
+ return undefined;
362
+ }
363
+ export async function containsCommit(cwd, ancestor, descendant) {
364
+ return await isAncestor(cwd, ancestor, descendant);
365
+ }
366
+ /** Create the landing commit over a previously composed candidate tree. */
367
+ export async function createLandingCommit(input) {
368
+ // Once delivery is already reachable from target, a single-parent landing
369
+ // records this claim without fabricating a redundant merge parent.
370
+ const parents = input.target === input.delivery || await isAncestor(input.cwd, input.delivery, input.target)
371
+ ? ["-p", input.target]
372
+ : ["-p", input.target, "-p", input.delivery];
373
+ return (await git(input.cwd, [
177
374
  "commit-tree",
178
- tree,
179
- "-p",
180
- input.target,
181
- "-p",
182
- input.delivery,
375
+ input.tree,
376
+ ...parents,
183
377
  "-m",
184
378
  `keiyaku: claim ${input.contractId}`,
185
379
  "-m",
186
380
  `Merge contract ${input.contractId} into ${input.targetBranch}.`,
187
- ]).trim();
188
- }
189
- export function containsCommit(cwd, ancestor, descendant) {
190
- return gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
381
+ ])).trim();
191
382
  }
192
- export function updateTargetRef(cwd, ref, next, expected) {
193
- git(cwd, ["update-ref", ref, next, expected]);
383
+ export async function updateTargetRef(cwd, ref, next, expected) {
384
+ await git(cwd, ["update-ref", ref, next, expected]);
194
385
  }
195
386
  export function sameTaskSourceBytes(root, left, right) {
196
387
  const snapshot = (sources) => new Map(sources.map((source) => [boardPath(root, source), source.text]));