@astrosheep/keiyaku 2.9.8 → 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 +10 -7
  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 +242 -79
  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 +65 -29
  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 +13 -35
  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 +45 -69
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +3 -1
  90. package/build/config/env.js +24 -1
  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 +13 -6
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +29 -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 +35 -337
  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 +125 -127
  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
@@ -1,5 +1,5 @@
1
1
  import { isAlias, isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
2
- import { canonicalTimestamp, fullId, invalid, nonblank, TASK_STATES, } from "./validation.js";
2
+ import { canonicalTimestamp, fullId, isTaskId, invalid, nonblank, TASK_STATES, } from "./validation.js";
3
3
  const FIELD_ORDER = [
4
4
  "id",
5
5
  "title",
@@ -8,6 +8,8 @@ const FIELD_ORDER = [
8
8
  "needs",
9
9
  "parent",
10
10
  "from",
11
+ "supersedes",
12
+ "relatesTo",
11
13
  "createdAt",
12
14
  "updatedAt",
13
15
  "creator",
@@ -100,11 +102,13 @@ export function parseTaskDocument(source) {
100
102
  invalid(`task field '${key}' must be a scalar or string array`);
101
103
  }
102
104
  }
103
- const id = fullId("id", values.get("id"));
105
+ const id = nonblank("id", values.get("id"));
106
+ if (!isTaskId(id))
107
+ invalid("task field 'id' must be a canonical local task id");
104
108
  const title = nonblank("title", values.get("title"));
105
109
  const state = values.get("state");
106
110
  if (typeof state !== "string" || !TASK_STATES.has(state)) {
107
- invalid("task field 'state' must be open, in_progress, done, or drop");
111
+ invalid("task field 'state' must be open, in_progress, on_hold, done, or drop");
108
112
  }
109
113
  const pri = values.get("pri");
110
114
  if (typeof pri !== "number" || !Number.isInteger(pri) || pri < 0 || pri > 3) {
@@ -120,10 +124,7 @@ export function parseTaskDocument(source) {
120
124
  const value = values.get(field);
121
125
  if (value === null)
122
126
  return null;
123
- const related = fullId(field, value);
124
- if (related === id)
125
- invalid(`task field '${field}' must not reference itself`);
126
- return related;
127
+ return fullId(field, value);
127
128
  };
128
129
  const parent = relationship("parent");
129
130
  const fromValue = values.get("from");
@@ -132,8 +133,19 @@ export function parseTaskDocument(source) {
132
133
  const from = fromValue.map((related) => fullId("from", related));
133
134
  if (new Set(from).size !== from.length)
134
135
  invalid("task field 'from' must be unique");
135
- if (from.includes(id))
136
- invalid("task field 'from' must not reference itself");
136
+ const relationships = (field) => {
137
+ const value = values.get(field);
138
+ if (value === undefined)
139
+ return [];
140
+ if (!Array.isArray(value))
141
+ invalid(`task field '${field}' must be a string array`);
142
+ const related = value.map((item) => fullId(field, item));
143
+ if (new Set(related).size !== related.length)
144
+ invalid(`task field '${field}' must be unique`);
145
+ return related;
146
+ };
147
+ const supersedes = relationships("supersedes");
148
+ const relatesTo = relationships("relatesTo");
137
149
  const createdAt = canonicalTimestamp("createdAt", values.get("createdAt"));
138
150
  const updatedValue = values.get("updatedAt");
139
151
  const updatedAt = updatedValue === undefined ? undefined : canonicalTimestamp("updatedAt", updatedValue);
@@ -156,6 +168,8 @@ export function parseTaskDocument(source) {
156
168
  needs,
157
169
  parent,
158
170
  from,
171
+ supersedes,
172
+ relatesTo,
159
173
  createdAt,
160
174
  ...(updatedAt === undefined ? {} : { updatedAt }),
161
175
  creator,
@@ -177,6 +191,8 @@ function serializeUnchecked(document) {
177
191
  needs: [...document.needs],
178
192
  parent: document.parent,
179
193
  from: document.from,
194
+ ...(document.supersedes.length === 0 ? {} : { supersedes: document.supersedes }),
195
+ ...(document.relatesTo.length === 0 ? {} : { relatesTo: document.relatesTo }),
180
196
  createdAt: document.createdAt,
181
197
  ...(document.updatedAt === undefined ? {} : { updatedAt: document.updatedAt }),
182
198
  creator: document.creator,
@@ -3,10 +3,13 @@
3
3
  * Only symbols required by current external consumers are re-exported.
4
4
  * Package-internal modules must import each other directly, not through this file.
5
5
  */
6
- export { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
6
+ export { previewTaskBoardForBind, reconcileTaskForfeit, replayClaimTasks, } from "./task-contract.js";
7
+ export { replayPreparedTaskBind, replayPreparedTaskBindLocked, replayCommittedTaskBind, replayCommittedTaskBindForfeit, replayPreparedTaskBindForfeit, withPreparedTaskBind, } from "./task-bind-preparation.js";
8
+ export { readTaskBoardGeneration } from "./task-git-store.js";
7
9
  export { resolveTaskCreator } from "./task-git-actor.js";
8
- export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
9
- export { openTaskStore, readTaskHistory } from "./task-git-store.js";
10
+ export { openTaskCommandRepository, openTaskMutationContext, readTaskDoctor, taskCommandContractId, taskCommandNamespace, } from "./task-git-runtime.js";
11
+ export { openTaskStore, readTaskHistory, readTaskNamespaces } from "./task-git-store.js";
10
12
  export { taskRepositoryFromStore } from "./task-store-repository.js";
11
13
  export { isTaskId } from "./validation.js";
12
- export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
14
+ export { isTaskCoordinate, parseTaskCoordinate, taskCoordinateFromSourcePath } from "./coordinate.js";
15
+ export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, holdTask, resumeTask, updateTask, updateTaskWithReceipt, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
@@ -1,18 +1,19 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
2
  import { isCommissionId } from "../ids.js";
3
3
  import { taskUpdatedAt } from "./model.js";
4
- import { isTaskId } from "./validation.js";
4
+ import { isTaskCoordinate } from "./coordinate.js";
5
+ import { parseStructuredQuery, STRUCTURED_QUERY_COMPLETION, STRUCTURED_QUERY_GRAMMAR, } from "../structured-query.js";
5
6
  export const TASK_FILTER_FIELDS = [
6
7
  "title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
7
8
  ];
8
9
  export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
9
- export const TASK_FILTER_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
10
- export const TASK_FILTER_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
10
+ export const TASK_FILTER_GRAMMAR = STRUCTURED_QUERY_GRAMMAR;
11
+ export const TASK_FILTER_COMPLETION = STRUCTURED_QUERY_COMPLETION;
11
12
  const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
12
13
  const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
13
14
  const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
14
15
  const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
15
- const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
16
+ const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "on_hold", "done", "dropped", "bound"]);
16
17
  const SORT_KEYS = new Set(["priority", "modified", "created"]);
17
18
  const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
18
19
  const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
@@ -21,6 +22,8 @@ export function deriveTaskDisplay(task) {
21
22
  return "done";
22
23
  if (task.state === "drop")
23
24
  return "dropped";
25
+ if (task.state === "on_hold")
26
+ return "on_hold";
24
27
  if (task.contractId !== undefined)
25
28
  return "bound";
26
29
  if (task.state === "in_progress")
@@ -43,14 +46,7 @@ function invalidFilter(detail, recoveredTerms = []) {
43
46
  },
44
47
  });
45
48
  }
46
- function parseTaskFilterTerm(term, expression, recoveredTerms) {
47
- const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
48
- if (!match)
49
- invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
50
- const field = match[1];
51
- const operator = match[2];
52
- if (!FILTER_FIELDS.has(field))
53
- invalidFilter(`unknown field '${field}'`, recoveredTerms);
49
+ function validateTaskFilterTerm(field, operator, values, recoveredTerms) {
54
50
  if (field === "title" && !TITLE_OPERATORS.has(operator)) {
55
51
  invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
56
52
  }
@@ -59,21 +55,15 @@ function parseTaskFilterTerm(term, expression, recoveredTerms) {
59
55
  : operator !== "=" && operator !== "!=")) {
60
56
  invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
61
57
  }
62
- const values = match[3].split(",");
63
- if (values.some((value) => value.length === 0))
64
- invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
65
58
  validateFilterValues(field, values, recoveredTerms);
66
- return { field, operator, values };
67
59
  }
68
60
  export function parseTaskFilter(expression) {
69
- const terms = expression.split("|");
70
- const parsed = [];
71
- for (const term of terms) {
72
- if (term.length === 0)
73
- invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, parsed);
74
- parsed.push(parseTaskFilterTerm(term, expression, parsed));
75
- }
76
- return parsed;
61
+ return parseStructuredQuery({
62
+ expression,
63
+ fields: FILTER_FIELDS,
64
+ invalid: invalidFilter,
65
+ validate: validateTaskFilterTerm,
66
+ });
77
67
  }
78
68
  function parseTimestamp(value, nowMs, recoveredTerms = []) {
79
69
  const relative = RELATIVE_TIME.exec(value);
@@ -114,8 +104,8 @@ function validateFilterValues(field, values, recoveredTerms) {
114
104
  for (const value of values) {
115
105
  if (field === "parent" && value === "none")
116
106
  continue;
117
- if (!isTaskId(value))
118
- invalidFilter(`value '${value}' must be a canonical full task id`, recoveredTerms);
107
+ if (!isTaskCoordinate(value))
108
+ invalidFilter(`value '${value}' must be a canonical full task coordinate`, recoveredTerms);
119
109
  }
120
110
  return;
121
111
  }
@@ -183,7 +173,7 @@ function selected(task, selection) {
183
173
  }
184
174
  function displayOrder(task) {
185
175
  const display = deriveTaskDisplay(task);
186
- return display === "in_progress" ? 0 : display === "ready" ? 1 : display === "blocked" ? 2 : display === "bound" ? 3 : display === "done" ? 4 : 5;
176
+ return display === "in_progress" ? 0 : display === "on_hold" ? 1 : display === "ready" ? 2 : display === "blocked" ? 3 : display === "bound" ? 4 : display === "done" ? 5 : 6;
187
177
  }
188
178
  function defaultCompare(left, right) {
189
179
  return displayOrder(left) - displayOrder(right)
@@ -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, ["rev-parse", `${input.parent}^{tree}`])).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, ["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, [
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
  }