@astrosheep/keiyaku 2.9.9 → 2.9.10

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 (230) hide show
  1. package/README.md +50 -182
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +2 -1
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +6 -1
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +3 -2
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +9 -16
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +21 -358
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +114 -119
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -2,13 +2,17 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { FlowError } from "../../flow-error.js";
4
4
  import { runGitProcess } from "../../git/core.js";
5
+ import { openGitStreamingBatch } from "../../git/streaming-batch.js";
5
6
  import { parseTaskDocument } from "./task.js";
6
- async function gitResult(cwd, args, input, env) {
7
- return await runGitProcess(cwd, args, { input, env });
7
+ import { parseTaskCoordinate } from "./coordinate.js";
8
+ async function gitResult(cwd, args, input, env, signal) {
9
+ return await runGitProcess(cwd, args, { input, env, signal });
8
10
  }
9
- async function git(cwd, args, input, env) {
10
- const result = await gitResult(cwd, args, input, env);
11
+ async function git(cwd, args, input, env, signal) {
12
+ const result = await gitResult(cwd, args, input, env, signal);
11
13
  if (result.error) {
14
+ if (result.error.name === "AbortError")
15
+ throw result.error;
12
16
  throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
13
17
  }
14
18
  if (result.status !== 0) {
@@ -23,15 +27,17 @@ function boardPath(root, source) {
23
27
  }
24
28
  return relative;
25
29
  }
26
- export async function inspectCurrentBranch(cwd) {
27
- const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
30
+ export async function inspectCurrentBranch(cwd, signal) {
31
+ const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"], undefined, undefined, signal);
28
32
  if (branch.error) {
33
+ if (branch.error.name === "AbortError")
34
+ throw branch.error;
29
35
  throw new FlowError("INTERNAL_STATE", `cannot inspect HEAD: ${branch.error.message}`, { cause: branch.error });
30
36
  }
31
37
  if (branch.status !== 0 || branch.stdout.trim() === "") {
32
38
  throw new FlowError("DETACHED_HEAD", "cannot publish task board from detached HEAD");
33
39
  }
34
- return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"])).trim() };
40
+ return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"], undefined, undefined, signal)).trim() };
35
41
  }
36
42
  export async function createTaskBoardCommit(input) {
37
43
  const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
@@ -61,55 +67,64 @@ export async function createTaskMutationCommit(input) {
61
67
  const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
62
68
  const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
63
69
  try {
64
- await git(input.cwd, ["read-tree", input.parent], undefined, env);
70
+ await git(input.cwd, ["read-tree", input.parent], undefined, env, input.signal);
65
71
  for (const write of input.writes) {
66
- const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
72
+ const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, input.signal)).trim();
67
73
  await git(input.cwd, [
68
74
  "update-index",
69
75
  "--add",
70
76
  "--cacheinfo",
71
77
  `100644,${oid},${taskMutationPath(write)}`,
72
- ], undefined, env);
78
+ ], undefined, env, input.signal);
73
79
  }
74
- const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
75
- const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent])).trim();
80
+ const tree = (await git(input.cwd, ["write-tree"], undefined, env, input.signal)).trim();
81
+ const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent], undefined, undefined, input.signal)).trim();
76
82
  if (tree === parentTree)
77
83
  return input.parent;
78
- return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
84
+ return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject], undefined, undefined, input.signal)).trim();
79
85
  }
80
86
  finally {
81
87
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
82
88
  }
83
89
  }
84
90
  /** Stage only the mutated task paths into the real index; leave unrelated entries alone. */
85
- export async function reconcileTaskMutationIndex(cwd, writes) {
91
+ export async function reconcileTaskMutationIndex(cwd, writes, signal) {
86
92
  for (const write of writes) {
87
- const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
93
+ const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, signal)).trim();
88
94
  await git(cwd, [
89
95
  "update-index",
90
96
  "--add",
91
97
  "--cacheinfo",
92
98
  `100644,${oid},${taskMutationPath(write)}`,
93
- ]);
99
+ ], undefined, undefined, signal);
94
100
  }
95
101
  }
96
102
  /**
97
- * Atomically publish a pathspec-scoped task micro-commit under the hub branch.
103
+ * Atomically publish a pathspec-scoped task micro-commit on the current checkout branch.
98
104
  * Unrelated staged and unstaged non-task bytes are never read into the commit.
99
105
  */
100
106
  export async function publishTaskMutationCommit(input, deps = {}) {
101
107
  let lastError;
102
108
  for (let attempt = 0; attempt < 4; attempt += 1) {
103
- const branch = await inspectCurrentBranch(input.cwd);
109
+ input.signal?.throwIfAborted();
110
+ const branch = await inspectCurrentBranch(input.cwd, input.signal);
104
111
  const commit = await createTaskMutationCommit({
105
112
  cwd: input.cwd,
106
113
  gitDir: input.gitDir,
107
114
  parent: branch.head,
108
115
  writes: input.writes,
109
116
  subject: input.subject,
117
+ signal: input.signal,
110
118
  });
111
119
  if (commit === branch.head) {
112
- await reconcileTaskMutationIndex(input.cwd, input.writes);
120
+ input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "current" });
121
+ try {
122
+ await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
123
+ input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
124
+ }
125
+ catch (error) {
126
+ input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
127
+ }
113
128
  return commit;
114
129
  }
115
130
  try {
@@ -118,12 +133,16 @@ export async function publishTaskMutationCommit(input, deps = {}) {
118
133
  ref: branch.ref,
119
134
  next: commit,
120
135
  expected: branch.head,
136
+ signal: input.signal,
121
137
  }, deps);
138
+ input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "published" });
122
139
  try {
123
- await reconcileTaskMutationIndex(input.cwd, input.writes);
140
+ await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
141
+ input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
124
142
  }
125
- catch {
143
+ catch (error) {
126
144
  // Branch fact remains authoritative; index may be repaired by a later mutation.
145
+ input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
127
146
  }
128
147
  return commit;
129
148
  }
@@ -131,6 +150,7 @@ export async function publishTaskMutationCommit(input, deps = {}) {
131
150
  lastError = error;
132
151
  }
133
152
  }
153
+ input.observeEffect?.({ effect: "task-reconciliation", status: "failed", error: lastError });
134
154
  throw lastError;
135
155
  }
136
156
  async function taskTreeEntries(cwd, commit) {
@@ -167,23 +187,139 @@ export async function refHead(cwd, ref) {
167
187
  return (await git(cwd, ["rev-parse", ref])).trim();
168
188
  }
169
189
  export async function taskContractAtCommit(cwd, commit, id) {
170
- const sourcePath = `.keiyaku/tasks/${id}.md`;
190
+ const coordinate = parseTaskCoordinate(id).coordinate;
191
+ const sourcePath = `.keiyaku/tasks/${coordinate}.md`;
171
192
  const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
172
193
  if (result.status !== 0)
173
194
  return undefined;
174
195
  return parseTaskDocument({ path: sourcePath, text: result.stdout }).contractId;
175
196
  }
197
+ async function taskDocumentAtCommit(cwd, commit, taskId) {
198
+ let coordinate;
199
+ try {
200
+ coordinate = parseTaskCoordinate(taskId);
201
+ }
202
+ catch {
203
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} names noncanonical task id ${taskId}`);
204
+ }
205
+ const sourcePath = `.keiyaku/tasks/${coordinate.coordinate}.md`;
206
+ const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
207
+ if (result.status !== 0 || result.error) {
208
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} is missing task ${taskId}`, { cause: result.error });
209
+ }
210
+ const document = parseTaskDocument({ path: sourcePath, text: result.stdout });
211
+ if (document.id !== coordinate.localId) {
212
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} task path ${taskId} contains ${document.id}`);
213
+ }
214
+ return document;
215
+ }
216
+ async function readTaskAdmissionEvidence(input, readDocument = taskDocumentAtCommit) {
217
+ if (input.selectedTaskIds.length === 0 || new Set(input.selectedTaskIds).size !== input.selectedTaskIds.length) {
218
+ throw new FlowError("INTERNAL_STATE", `task-selected bind ${input.bindObjectId} must name unique selected tasks`);
219
+ }
220
+ const cache = new Map();
221
+ const read = async (taskId) => {
222
+ const cached = cache.get(taskId);
223
+ if (cached)
224
+ return cached;
225
+ const document = await readDocument(input.cwd, input.base, taskId);
226
+ cache.set(taskId, document);
227
+ return document;
228
+ };
229
+ const selected = [];
230
+ for (const taskId of input.selectedTaskIds) {
231
+ const document = await read(taskId);
232
+ if (document.contractId !== input.contract || document.state !== "open") {
233
+ throw new FlowError("INTERNAL_STATE", `bind base ${input.base} task ${taskId} does not carry open linkage to ${input.contract}`);
234
+ }
235
+ const needs = [];
236
+ for (const need of document.needs) {
237
+ const prerequisite = await read(need);
238
+ if (prerequisite.state !== "done" && prerequisite.state !== "drop") {
239
+ throw new FlowError("INTERNAL_STATE", `bind base ${input.base} task ${taskId} has unsatisfied need ${need}`);
240
+ }
241
+ needs.push({ taskId: need, state: prerequisite.state });
242
+ }
243
+ selected.push({ taskId, needs });
244
+ }
245
+ return {
246
+ bindObjectId: input.bindObjectId,
247
+ base: input.base,
248
+ selected,
249
+ };
250
+ }
251
+ /** Immutable current bind-time task linkage and needs evidence; never reads the current board. */
252
+ export async function readTaskAdmissionEvidenceAtCommit(input) {
253
+ return await readTaskAdmissionEvidence(input);
254
+ }
255
+ /** Read every bind-time task document in one lifecycle observation through one Git process. */
256
+ export function openTaskAdmissionEvidenceReader(cwd) {
257
+ const batch = openGitStreamingBatch(cwd);
258
+ const documents = new Map();
259
+ let transportFailed = false;
260
+ const readDocument = async (_cwd, commit, taskId) => {
261
+ let coordinate;
262
+ try {
263
+ coordinate = parseTaskCoordinate(taskId);
264
+ }
265
+ catch {
266
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} names noncanonical task id ${taskId}`);
267
+ }
268
+ const sourcePath = `.keiyaku/tasks/${coordinate.coordinate}.md`;
269
+ const expression = `${commit}:${sourcePath}`;
270
+ const cached = documents.get(expression);
271
+ if (cached)
272
+ return cached;
273
+ let result;
274
+ try {
275
+ result = await batch.request(expression);
276
+ }
277
+ catch (error) {
278
+ transportFailed = true;
279
+ throw new FlowError("INTERNAL_STATE", `cannot read bind base ${commit} task ${taskId}`, { cause: error });
280
+ }
281
+ if (result.kind !== "object" || result.record.type !== "blob") {
282
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} is missing task ${taskId}`);
283
+ }
284
+ const document = parseTaskDocument({ path: sourcePath, text: result.record.content.toString("utf8") });
285
+ if (document.id !== coordinate.localId) {
286
+ throw new FlowError("INTERNAL_STATE", `bind base ${commit} task path ${taskId} contains ${document.id}`);
287
+ }
288
+ documents.set(expression, document);
289
+ return document;
290
+ };
291
+ return {
292
+ read: async (input) => await readTaskAdmissionEvidence(input, readDocument),
293
+ close: async () => {
294
+ try {
295
+ const fact = await batch.close();
296
+ if (!transportFailed && (fact.timedOut || fact.code !== 0 || fact.signal !== null)) {
297
+ const detail = fact.stderr.toString("utf8").trim() || `exit ${String(fact.code)}`;
298
+ throw new FlowError("INTERNAL_STATE", `git cat-file --batch failed while reading task evidence: ${detail}`);
299
+ }
300
+ }
301
+ catch (error) {
302
+ if (transportFailed)
303
+ return;
304
+ if (error instanceof FlowError)
305
+ throw error;
306
+ throw new FlowError("INTERNAL_STATE", "cannot close task evidence batch reader", { cause: error });
307
+ }
308
+ },
309
+ };
310
+ }
176
311
  /** Bound task ids and their complete parent ancestry from the immutable bind base. */
177
312
  export async function taskSettlementOwnershipAtCommit(cwd, commit, contractId) {
178
313
  const documents = new Map();
179
314
  for (const entry of await taskTreeEntries(cwd, commit)) {
180
315
  const text = await git(cwd, ["cat-file", "blob", entry.oid]);
181
316
  const document = parseTaskDocument({ path: entry.path, text });
182
- documents.set(document.id, document);
317
+ const coordinate = parseTaskCoordinate(entry.path.slice(".keiyaku/tasks/".length, -3)).coordinate;
318
+ documents.set(coordinate, document);
183
319
  }
184
- const taskIds = [...documents.values()]
185
- .filter((document) => document.contractId === contractId)
186
- .map((document) => document.id)
320
+ const taskIds = [...documents]
321
+ .filter(([, document]) => document.contractId === contractId)
322
+ .map(([coordinate]) => coordinate)
187
323
  .sort();
188
324
  const ownedIds = new Set();
189
325
  for (const taskId of taskIds) {
@@ -208,7 +344,7 @@ export async function updateBranch(input, deps) {
208
344
  if (deps.updateBranch)
209
345
  await deps.updateBranch(input);
210
346
  else
211
- await git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
347
+ await git(input.cwd, ["update-ref", input.ref, input.next, input.expected], undefined, undefined, input.signal);
212
348
  }
213
349
  export async function publishForwardTaskBoardCommit(input, deps) {
214
350
  let lastError;
@@ -229,71 +365,6 @@ export async function publishForwardTaskBoardCommit(input, deps) {
229
365
  }
230
366
  throw lastError;
231
367
  }
232
- async function treeWithoutTasks(cwd, commit, gitDir) {
233
- const temporaryDirectory = fs.mkdtempSync(path.join(gitDir, "keiyaku-landing-index-"));
234
- const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
235
- try {
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();
239
- }
240
- finally {
241
- fs.rmSync(temporaryDirectory, { recursive: true, force: true });
242
- }
243
- }
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);
248
- let mergedTree;
249
- try {
250
- mergedTree = (await git(input.cwd, [
251
- "merge-tree",
252
- "--write-tree",
253
- "--merge-base",
254
- baseTree,
255
- targetTree,
256
- deliveryTree,
257
- ])).trim();
258
- }
259
- catch (error) {
260
- throw new FlowError("CLOSE_MERGE_CONFLICT", "claim merge conflict outside task board", { cause: error });
261
- }
262
- const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-landing-index-"));
263
- const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
264
- try {
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
- }
276
- for (const source of input.sources) {
277
- const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
278
- await git(input.cwd, [
279
- "update-index",
280
- "--add",
281
- "--cacheinfo",
282
- `100644,${oid},${boardPath(input.root, source)}`,
283
- ], undefined, env);
284
- }
285
- return (await git(input.cwd, ["write-tree"], undefined, env)).trim();
286
- }
287
- finally {
288
- fs.rmSync(temporaryDirectory, { recursive: true, force: true });
289
- }
290
- }
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
368
  export async function taskSourcesAtCommit(cwd, root, commit, paths) {
298
369
  const sources = [];
299
370
  for (const sourcePath of paths) {
@@ -304,95 +375,9 @@ export async function taskSourcesAtCommit(cwd, root, commit, paths) {
304
375
  }
305
376
  return sources;
306
377
  }
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, ["show", "-s", "--format=%T", commit])).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, [
374
- "commit-tree",
375
- input.tree,
376
- ...parents,
377
- "-m",
378
- `keiyaku: claim ${input.contractId}`,
379
- "-m",
380
- `Merge contract ${input.contractId} into ${input.targetBranch}.`,
381
- ])).trim();
382
- }
383
- export async function updateTargetRef(cwd, ref, next, expected) {
384
- await git(cwd, ["update-ref", ref, next, expected]);
385
- }
386
- export function sameTaskSourceBytes(root, left, right) {
387
- const snapshot = (sources) => new Map(sources.map((source) => [boardPath(root, source), source.text]));
388
- const leftSnapshot = snapshot(left);
389
- const rightSnapshot = snapshot(right);
390
- return leftSnapshot.size === rightSnapshot.size
391
- && [...leftSnapshot].every(([sourcePath, sourceText]) => rightSnapshot.get(sourcePath) === sourceText);
392
- }
393
378
  export function taskWritesFromSources(sources) {
394
379
  return sources.map((source) => ({
395
- id: path.basename(source.path, ".md"),
380
+ id: parseTaskCoordinate(source.path.replaceAll("\\", "/").split("/.keiyaku/tasks/").at(-1).slice(0, -3)).coordinate,
396
381
  text: source.text,
397
382
  }));
398
383
  }
@@ -10,12 +10,16 @@ export function requireHealthySettlementBoard(board) {
10
10
  function settlementDocument(task, contractId, updatedAt) {
11
11
  return {
12
12
  id: task.id,
13
+ namespace: task.namespace,
14
+ coordinate: task.coordinate,
13
15
  title: task.title,
14
16
  state: task.state,
15
17
  pri: task.pri,
16
18
  needs: [...task.needs],
17
19
  parent: task.parent,
18
20
  from: [...task.from],
21
+ supersedes: [...task.supersedes],
22
+ relatesTo: [...task.relatesTo],
19
23
  createdAt: task.createdAt,
20
24
  updatedAt: advancedTaskUpdatedAt(task, updatedAt),
21
25
  creator: task.creator,
@@ -35,22 +39,22 @@ export function reopenedTaskDocument(task, updatedAt) {
35
39
  }
36
40
  function validateReadyForBind(board, task) {
37
41
  if (task.state === "in_progress") {
38
- throw new FlowError("INVALID_TARGET", `task ${task.id} is in_progress; stop it before bind`);
42
+ throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is in_progress; stop it before bind`);
39
43
  }
40
44
  if (task.state !== "open") {
41
- throw new FlowError("INVALID_TARGET", `task ${task.id} is ${task.state}, not ready for bind`);
45
+ throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is ${task.state}, not ready for bind`);
42
46
  }
43
47
  const unsatisfied = task.needs
44
48
  .filter((need) => {
45
49
  const dependency = board.byId.get(need);
46
- return dependency.state === "open" || dependency.state === "in_progress";
50
+ return dependency.state === "open" || dependency.state === "in_progress" || dependency.state === "on_hold";
47
51
  })
48
52
  .sort();
49
53
  if (unsatisfied.length > 0) {
50
- throw new FlowError("INVALID_TARGET", `task ${task.id} is blocked by: ${unsatisfied.join(", ")}`);
54
+ throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is blocked by: ${unsatisfied.join(", ")}`);
51
55
  }
52
56
  if (task.contractId !== undefined) {
53
- throw new FlowError("INVALID_TARGET", `task ${task.id} is already bound to contract ${task.contractId}`);
57
+ throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is already bound to contract ${task.contractId}`);
54
58
  }
55
59
  }
56
60
  export function selectReadyTasksForBind(board, taskIds) {
@@ -64,17 +68,17 @@ export function selectReadyTasksForBind(board, taskIds) {
64
68
  const tasks = [];
65
69
  for (const rootId of rootIds) {
66
70
  const root = board.byId.get(rootId);
67
- if (!admitted.has(root.id)) {
68
- admitted.add(root.id);
71
+ if (!admitted.has(root.coordinate)) {
72
+ admitted.add(root.coordinate);
69
73
  tasks.push(root);
70
74
  }
71
75
  for (const candidate of board.tasks) {
72
- if (candidate.parent === null || admitted.has(candidate.id))
76
+ if (candidate.parent === null || admitted.has(candidate.coordinate))
73
77
  continue;
74
78
  if (candidate.state === "done" || candidate.state === "drop")
75
79
  continue;
76
80
  if (isDescendantOf(board, candidate, rootId)) {
77
- admitted.add(candidate.id);
81
+ admitted.add(candidate.coordinate);
78
82
  tasks.push(candidate);
79
83
  }
80
84
  }
@@ -96,7 +100,7 @@ export function tasksForContract(board, contractId) {
96
100
  requireHealthySettlementBoard(board);
97
101
  return board.tasks
98
102
  .filter((task) => task.contractId === contractId)
99
- .sort((left, right) => left.id.localeCompare(right.id));
103
+ .sort((left, right) => left.coordinate.localeCompare(right.coordinate));
100
104
  }
101
105
  export function doneTaskReplacements(board, contractId, taskIds, updatedAt) {
102
106
  return taskIds.flatMap((taskId) => {
@@ -104,7 +108,7 @@ export function doneTaskReplacements(board, contractId, taskIds, updatedAt) {
104
108
  if (task.state === "done" && task.contractId === undefined)
105
109
  return [];
106
110
  if (task.contractId !== contractId) {
107
- throw new FlowError("INTERNAL_STATE", `task ${task.id} no longer points to contract ${contractId} while settling its immutable bind-base ownership`);
111
+ throw new FlowError("INTERNAL_STATE", `task ${task.coordinate} no longer points to contract ${contractId} while settling its immutable bind-base ownership`);
108
112
  }
109
113
  const settled = settlementDocument(task, undefined, updatedAt);
110
114
  settled.state = "done";
@@ -1,6 +1,7 @@
1
1
  import { buildTaskBoardFromDocuments } from "./board.js";
2
2
  import { parseTaskDocument } from "./document.js";
3
3
  import { invalid } from "./validation.js";
4
+ import { taskCoordinateFromSourcePath } from "./coordinate.js";
4
5
  function compareText(left, right) {
5
6
  return left < right ? -1 : left > right ? 1 : 0;
6
7
  }
@@ -10,18 +11,19 @@ function diseaseSort(left, right) {
10
11
  || compareText(left.id ?? "", right.id ?? "")
11
12
  || compareText(left.message, right.message);
12
13
  }
13
- function sourceBasename(sourcePath) {
14
- return sourcePath.split(/[\\/]/).at(-1) ?? "";
15
- }
16
14
  function taskDocument(parsed) {
17
15
  return {
18
16
  id: parsed.id,
17
+ namespace: parsed.namespace ?? null,
18
+ coordinate: parsed.coordinate ?? parsed.id,
19
19
  title: parsed.title,
20
20
  state: parsed.state,
21
21
  pri: parsed.pri,
22
22
  needs: [...parsed.needs],
23
23
  parent: parsed.parent,
24
24
  from: [...parsed.from],
25
+ supersedes: [...parsed.supersedes],
26
+ relatesTo: [...parsed.relatesTo],
25
27
  createdAt: parsed.createdAt,
26
28
  ...(parsed.updatedAt === undefined ? {} : { updatedAt: parsed.updatedAt }),
27
29
  creator: parsed.creator,
@@ -34,36 +36,52 @@ export function buildTaskSourceBoard(sources) {
34
36
  const sourceDiseases = [];
35
37
  const authoritative = [];
36
38
  for (const source of [...sources].sort((a, b) => compareText(a.path, b.path))) {
39
+ let sourceCoordinate;
37
40
  try {
41
+ const coordinate = taskCoordinateFromSourcePath(source.path);
42
+ sourceCoordinate = coordinate.coordinate;
38
43
  const parsed = parseTaskDocument(source);
39
- if (sourceBasename(source.path) !== `${parsed.id}.md`) {
44
+ if (coordinate.localId !== parsed.id) {
40
45
  invalid(`task source basename must be ${parsed.id}.md`);
41
46
  }
42
- authoritative.push(parsed);
47
+ for (const [field, relations] of [
48
+ ["needs", parsed.needs],
49
+ ["from", parsed.from],
50
+ ["supersedes", parsed.supersedes],
51
+ ["relatesTo", parsed.relatesTo],
52
+ ]) {
53
+ if (relations.includes(coordinate.coordinate))
54
+ invalid(`task field '${field}' must not reference itself`);
55
+ }
56
+ if (parsed.parent === coordinate.coordinate)
57
+ invalid("task field 'parent' must not reference itself");
58
+ authoritative.push({ ...parsed, namespace: coordinate.namespace, coordinate: coordinate.coordinate });
43
59
  }
44
60
  catch (error) {
45
61
  sourceDiseases.push({
46
62
  code: "invalid_document",
47
63
  path: source.path,
48
- message: error instanceof Error ? error.message : String(error),
64
+ message: `task ${sourceCoordinate ?? source.path} invalid document: ${error instanceof Error ? error.message : String(error)}`,
65
+ ...(sourceCoordinate === undefined ? {} : { id: sourceCoordinate }),
49
66
  });
50
67
  }
51
68
  }
52
69
  const counts = new Map();
53
70
  for (const task of authoritative)
54
- counts.set(task.id, (counts.get(task.id) ?? 0) + 1);
71
+ counts.set(task.coordinate, (counts.get(task.coordinate) ?? 0) + 1);
55
72
  const candidates = new Map();
56
73
  for (const task of authoritative) {
57
- if (counts.get(task.id) > 1) {
74
+ const coordinate = task.coordinate;
75
+ if (counts.get(coordinate) > 1) {
58
76
  sourceDiseases.push({
59
77
  code: "duplicate_id",
60
78
  path: task.source.path,
61
- message: `duplicate authoritative task id ${task.id}`,
62
- id: task.id,
79
+ message: `duplicate authoritative task coordinate ${coordinate}`,
80
+ id: coordinate,
63
81
  });
64
82
  }
65
83
  else {
66
- candidates.set(task.id, task);
84
+ candidates.set(coordinate, task);
67
85
  }
68
86
  }
69
87
  const domainBoard = buildTaskBoardFromDocuments([...candidates.values()].map(taskDocument));
@@ -73,12 +91,12 @@ export function buildTaskSourceBoard(sources) {
73
91
  }));
74
92
  const tasks = domainBoard.tasks.map((task) => ({
75
93
  ...task,
76
- path: candidates.get(task.id).source.path,
94
+ path: candidates.get(task.coordinate).source.path,
77
95
  }));
78
96
  return {
79
97
  tasks,
80
98
  diseases: [...sourceDiseases, ...graphDiseases].sort(diseaseSort),
81
- byId: new Map(tasks.map((task) => [task.id, task])),
99
+ byId: new Map(tasks.map((task) => [task.coordinate, task])),
82
100
  };
83
101
  }
84
102
  /** Compatibility name for source-backed board consumers. */