@astrosheep/keiyaku 2.9.9 → 2.9.11

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 +93 -174
  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
@@ -1,20 +1,26 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
- import { IdentifierSlugError, normalizeIdentifierSlug, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES, } from "../identifier-slug.js";
2
+ import { IdentifierSlugError, taskIdentifierSlug, } from "../identifier-slug.js";
3
+ import { markdownSourceSections } from "../scope.js";
3
4
  import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
4
5
  import { dateTimestamp, invalid, nonblank, } from "./validation.js";
5
6
  import { advancedTaskUpdatedAt } from "./model.js";
7
+ import { formatTaskCoordinate, parseTaskCoordinate } from "./coordinate.js";
6
8
  export async function readTaskBoard(repository) {
7
9
  return repository.read();
8
10
  }
9
11
  function documentFromView(view) {
10
12
  return {
11
13
  id: view.id,
14
+ namespace: view.namespace,
15
+ coordinate: view.coordinate,
12
16
  title: view.title,
13
17
  state: view.state,
14
18
  pri: view.pri,
15
19
  needs: [...view.needs],
16
20
  parent: view.parent,
17
21
  from: [...view.from],
22
+ supersedes: [...view.supersedes],
23
+ relatesTo: [...view.relatesTo],
18
24
  createdAt: view.createdAt,
19
25
  ...(view.updatedAt === undefined ? {} : { updatedAt: view.updatedAt }),
20
26
  creator: view.creator,
@@ -23,14 +29,18 @@ function documentFromView(view) {
23
29
  body: view.body,
24
30
  };
25
31
  }
32
+ function documentCoordinate(document) {
33
+ return document.coordinate ?? formatTaskCoordinate(document.namespace ?? null, document.id);
34
+ }
26
35
  function replaceDocument(document, subject) {
27
36
  return {
28
37
  replacements: [document],
29
38
  subject,
30
39
  select(resulting) {
31
- const value = resulting.byId.get(document.id);
40
+ const coordinate = documentCoordinate(document);
41
+ const value = resulting.byId.get(coordinate);
32
42
  if (!value)
33
- throw new FlowError("INTERNAL_STATE", `mutated task ${document.id} disappeared from the board`);
43
+ throw new FlowError("INTERNAL_STATE", `mutated task ${coordinate} disappeared from the board`);
34
44
  return value;
35
45
  },
36
46
  };
@@ -44,24 +54,45 @@ function requireExisting(board, id) {
44
54
  function resolveMutationIds(board, ids) {
45
55
  return ids.map((id) => resolveTaskId(board, id));
46
56
  }
57
+ function validateRelations(board, id, supersedes, relatesTo) {
58
+ if (supersedes.includes(id) || relatesTo.includes(id))
59
+ invalid(`task ${id} relations must not reference itself`);
60
+ const seen = new Set();
61
+ const reachesSource = (current) => {
62
+ if (current === id)
63
+ return true;
64
+ if (seen.has(current))
65
+ return false;
66
+ seen.add(current);
67
+ return (board.byId.get(current)?.supersedes ?? []).some(reachesSource);
68
+ };
69
+ for (const target of supersedes) {
70
+ seen.clear();
71
+ if (reachesSource(target))
72
+ invalid(`task ${id} would create a supersedes cycle`);
73
+ }
74
+ }
75
+ function updateRelations(board, document, field, additions, removals) {
76
+ const added = resolveMutationIds(board, additions ?? []);
77
+ if (new Set(added).size !== added.length)
78
+ invalid(`task ${field} edges must be unique`);
79
+ const removed = new Set(resolveMutationIds(board, removals ?? []));
80
+ const retained = (document[field] ?? []).filter((value) => !removed.has(value));
81
+ if (added.some((value) => retained.includes(value)))
82
+ invalid(`task ${field} edges must be unique`);
83
+ document[field] = [...retained, ...added];
84
+ }
47
85
  export async function createTask(repository, input, deps) {
48
86
  nonblank("title", input.title);
49
87
  nonblank("creator", input.creator);
50
- let baseId;
51
- try {
52
- baseId = normalizeIdentifierSlug(input.title, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES);
53
- }
54
- catch (error) {
55
- if (error instanceof IdentifierSlugError) {
56
- throw new FlowError("INVALID_TARGET", `task title ${error.message}`, { cause: error });
57
- }
58
- throw error;
59
- }
60
88
  const pri = input.pri ?? 2;
61
89
  if (!Number.isInteger(pri) || pri < 0 || pri > 3)
62
90
  invalid("task priority must be an integer from 0 through 3");
63
91
  return repository.mutate((board) => {
64
92
  requireHealthy(board);
93
+ const namespace = input.namespace ?? null;
94
+ if (namespace !== null)
95
+ parseTaskCoordinate(`${namespace}/task`, "task namespace");
65
96
  const needs = resolveMutationIds(board, input.needs ?? []);
66
97
  if (new Set(needs).size !== needs.length)
67
98
  invalid("task needs must be unique");
@@ -69,34 +100,64 @@ export async function createTask(repository, input, deps) {
69
100
  const from = resolveMutationIds(board, input.from ?? []);
70
101
  if (new Set(from).size !== from.length)
71
102
  invalid("task from edges must be unique");
72
- let id = baseId;
73
- for (let ordinal = 2; board.byId.has(id); ordinal += 1) {
103
+ const supersedes = resolveMutationIds(board, input.supersedes ?? []);
104
+ if (new Set(supersedes).size !== supersedes.length)
105
+ invalid("task supersedes edges must be unique");
106
+ const relatesTo = resolveMutationIds(board, input.relatesTo ?? []);
107
+ if (new Set(relatesTo).size !== relatesTo.length)
108
+ invalid("task relatesTo edges must be unique");
109
+ let ordinal = 1;
110
+ let id;
111
+ for (;;) {
74
112
  if (!Number.isSafeInteger(ordinal)) {
75
- throw new FlowError("INTERNAL_STATE", `task id collision ordinal exceeded the safe integer range for ${baseId}`);
113
+ throw new FlowError("INTERNAL_STATE", "task id collision ordinal exceeded the safe integer range");
114
+ }
115
+ try {
116
+ id = taskIdentifierSlug(input.title, ordinal === 1 ? "" : `-${ordinal}`);
76
117
  }
77
- id = `${baseId}-${ordinal}`;
118
+ catch (error) {
119
+ if (error instanceof IdentifierSlugError) {
120
+ throw new FlowError("INVALID_TARGET", `task title ${error.message}`, { cause: error });
121
+ }
122
+ throw error;
123
+ }
124
+ if (!board.byId.has(formatTaskCoordinate(namespace, id)))
125
+ break;
126
+ ordinal += 1;
78
127
  }
128
+ const coordinate = formatTaskCoordinate(namespace, id);
129
+ validateRelations(board, coordinate, supersedes, relatesTo);
79
130
  const now = deps.now();
80
131
  const document = {
81
132
  id,
133
+ namespace,
134
+ coordinate,
82
135
  title: input.title,
83
136
  state: "open",
84
137
  pri,
85
138
  needs,
86
139
  parent,
87
140
  from,
141
+ supersedes,
142
+ relatesTo,
88
143
  createdAt: dateTimestamp("createdAt", now),
89
144
  updatedAt: dateTimestamp("updatedAt", now),
90
145
  creator: input.creator,
91
146
  body: input.body ?? "",
92
147
  };
93
- return replaceDocument(document, `Add task ${id}`);
148
+ return replaceDocument(document, `Add task ${coordinate}`);
94
149
  });
95
150
  }
96
151
  export async function updateTask(repository, fullIdValue, patch, now = new Date()) {
152
+ return (await updateTaskWithReceipt(repository, fullIdValue, patch, now)).task;
153
+ }
154
+ export async function updateTaskWithReceipt(repository, fullIdValue, patch, now = new Date()) {
97
155
  const hasChange = Object.values(patch).some((value) => value !== undefined);
98
156
  if (!hasChange)
99
157
  throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
158
+ if (patch.body !== undefined && patch.appendBody !== undefined) {
159
+ throw new FlowError("EMPTY_PARAM", "task update cannot combine --body and --append");
160
+ }
100
161
  return repository.mutate((board) => {
101
162
  requireHealthy(board);
102
163
  const id = resolveTaskId(board, fullIdValue);
@@ -106,6 +167,8 @@ export async function updateTask(repository, fullIdValue, patch, now = new Date(
106
167
  document.title = nonblank("title", patch.title);
107
168
  if (patch.body !== undefined)
108
169
  document.body = patch.body;
170
+ if (patch.appendBody !== undefined)
171
+ document.body = appendBody(document.body, patch.appendBody);
109
172
  if (patch.pri !== undefined) {
110
173
  if (!Number.isInteger(patch.pri) || patch.pri < 0 || patch.pri > 3)
111
174
  invalid("task priority must be an integer from 0 through 3");
@@ -119,18 +182,82 @@ export async function updateTask(repository, fullIdValue, patch, now = new Date(
119
182
  if (patch.from !== undefined) {
120
183
  document.from = [...new Set([...document.from, ...resolveMutationIds(board, patch.from)])];
121
184
  }
185
+ updateRelations(board, document, "supersedes", patch.addSupersedes, patch.dropSupersedes);
186
+ updateRelations(board, document, "relatesTo", patch.addRelatesTo, patch.dropRelatesTo);
187
+ validateRelations(board, document.coordinate, document.supersedes, document.relatesTo);
122
188
  if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
123
189
  throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
124
190
  }
125
191
  document.updatedAt = advancedTaskUpdatedAt(current, now);
126
- return replaceDocument(document, `Update task ${id}`);
192
+ const bodyChange = patch.body === undefined ? undefined : describeBodyChange(current.body, document.body);
193
+ return {
194
+ replacements: [document],
195
+ subject: `Update task ${id}`,
196
+ select(resulting) {
197
+ const coordinate = documentCoordinate(document);
198
+ const task = resulting.byId.get(coordinate);
199
+ if (!task)
200
+ throw new FlowError("INTERNAL_STATE", `mutated task ${coordinate} disappeared from the board`);
201
+ return { task, ...(bodyChange === undefined ? {} : { bodyChange }) };
202
+ },
203
+ };
127
204
  });
128
205
  }
206
+ function appendBody(body, addition) {
207
+ if (!body || !addition || body.endsWith("\n\n") || addition.startsWith("\n"))
208
+ return body + addition;
209
+ return `${body}${body.endsWith("\n") ? "\n" : "\n\n"}${addition}`;
210
+ }
211
+ function markdownLevelTwoSections(body) {
212
+ return markdownSourceSections(body)
213
+ .filter((section) => section.level === 2)
214
+ .map((section) => section.title.trim());
215
+ }
216
+ function bodyLines(body) {
217
+ return body.match(/[^\n]*\n|[^\n]+$/g) ?? [];
218
+ }
219
+ function lineDiffstat(before, after) {
220
+ const oldLines = bodyLines(before);
221
+ const newLines = bodyLines(after);
222
+ const lcs = Array(newLines.length + 1).fill(0);
223
+ for (const oldLine of oldLines) {
224
+ let previous = 0;
225
+ for (let index = 1; index <= newLines.length; index += 1) {
226
+ const saved = lcs[index];
227
+ lcs[index] = oldLine === newLines[index - 1]
228
+ ? previous + 1
229
+ : Math.max(lcs[index], lcs[index - 1]);
230
+ previous = saved;
231
+ }
232
+ }
233
+ const unchanged = lcs[newLines.length];
234
+ return { additions: newLines.length - unchanged, deletions: oldLines.length - unchanged };
235
+ }
236
+ function describeBodyChange(before, after) {
237
+ const remaining = new Map();
238
+ for (const section of markdownLevelTwoSections(after)) {
239
+ remaining.set(section, (remaining.get(section) ?? 0) + 1);
240
+ }
241
+ const oldBytes = Buffer.byteLength(before, "utf8");
242
+ const newBytes = Buffer.byteLength(after, "utf8");
243
+ return {
244
+ oldBytes,
245
+ newBytes,
246
+ removedSections: markdownLevelTwoSections(before).filter((section) => {
247
+ const count = remaining.get(section) ?? 0;
248
+ if (count === 0)
249
+ return true;
250
+ remaining.set(section, count - 1);
251
+ return false;
252
+ }),
253
+ diffstat: lineDiffstat(before, after),
254
+ };
255
+ }
129
256
  async function transition(repository, fullIdValue, apply, subject, now) {
130
257
  return repository.mutate((board) => {
131
258
  requireHealthy(board);
132
259
  const id = fullIdValue === undefined
133
- ? selectSoleInProgress(board).id
260
+ ? selectSoleInProgress(board).coordinate
134
261
  : resolveTaskId(board, fullIdValue);
135
262
  const current = requireExisting(board, id);
136
263
  const document = documentFromView(current);
@@ -141,17 +268,18 @@ async function transition(repository, fullIdValue, apply, subject, now) {
141
268
  }
142
269
  function rejectBound(document, action = "state") {
143
270
  if (document.contractId !== undefined) {
271
+ const coordinate = documentCoordinate(document);
144
272
  if (action === "drop") {
145
- throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; forfeit the contract before dropping this task`);
273
+ throw new FlowError("UNSUPPORTED_POLICY", `task ${coordinate} is bound to contract ${document.contractId}; forfeit the contract before dropping this task`);
146
274
  }
147
- throw new FlowError("UNSUPPORTED_POLICY", `task ${document.id} is bound to contract ${document.contractId}; its state follows the contract`);
275
+ throw new FlowError("UNSUPPORTED_POLICY", `task ${coordinate} is bound to contract ${document.contractId}; its state follows the contract`);
148
276
  }
149
277
  }
150
278
  export async function startTask(repository, fullId, now) {
151
279
  return transition(repository, fullId, (document) => {
152
280
  rejectBound(document);
153
281
  if (document.state !== "open")
154
- invalid(`task ${document.id} is ${document.state}, not open`);
282
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not open`);
155
283
  document.state = "in_progress";
156
284
  document.startedAt = dateTimestamp("startedAt", now);
157
285
  }, (id) => `Start task ${id}`, now);
@@ -160,16 +288,34 @@ export async function stopTask(repository, fullId, now = new Date()) {
160
288
  return transition(repository, fullId, (document) => {
161
289
  rejectBound(document);
162
290
  if (document.state !== "in_progress")
163
- invalid(`task ${document.id} is ${document.state}, not in_progress`);
291
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not in_progress`);
164
292
  document.state = "open";
165
293
  delete document.startedAt;
166
294
  }, (id) => `Stop task ${id}`, now);
167
295
  }
168
- export async function doneTask(repository, fullId, now = new Date()) {
296
+ export async function holdTask(repository, fullId, now = new Date()) {
169
297
  return transition(repository, fullId, (document) => {
170
298
  rejectBound(document);
171
299
  if (document.state !== "open" && document.state !== "in_progress") {
172
- invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
300
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not open or in_progress`);
301
+ }
302
+ document.state = "on_hold";
303
+ delete document.startedAt;
304
+ }, (id) => `Hold task ${id}`, now);
305
+ }
306
+ export async function resumeTask(repository, fullId, now = new Date()) {
307
+ return transition(repository, fullId, (document) => {
308
+ rejectBound(document);
309
+ if (document.state !== "on_hold")
310
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not on_hold`);
311
+ document.state = "open";
312
+ }, (id) => `Resume task ${id}`, now);
313
+ }
314
+ export async function doneTask(repository, fullId, now = new Date()) {
315
+ return transition(repository, fullId, (document) => {
316
+ rejectBound(document);
317
+ if (document.state !== "open" && document.state !== "in_progress" && document.state !== "on_hold") {
318
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not open, in_progress, or on_hold`);
173
319
  }
174
320
  document.state = "done";
175
321
  delete document.startedAt;
@@ -178,8 +324,8 @@ export async function doneTask(repository, fullId, now = new Date()) {
178
324
  export async function dropTask(repository, fullId, now = new Date()) {
179
325
  return transition(repository, fullId, (document) => {
180
326
  rejectBound(document, "drop");
181
- if (document.state !== "open" && document.state !== "in_progress") {
182
- invalid(`task ${document.id} is ${document.state}, not open or in_progress`);
327
+ if (document.state !== "open" && document.state !== "in_progress" && document.state !== "on_hold") {
328
+ invalid(`task ${documentCoordinate(document)} is ${document.state}, not open, in_progress, or on_hold`);
183
329
  }
184
330
  document.state = "drop";
185
331
  delete document.startedAt;
@@ -189,11 +335,13 @@ export function selectSoleInProgress(board) {
189
335
  const candidates = board.tasks.filter((task) => task.state === "in_progress");
190
336
  if (candidates.length === 1)
191
337
  return candidates[0];
192
- const ids = candidates.map((task) => task.id).sort();
193
- const code = candidates.length === 0 ? "INVALID_TARGET" : "CONTRACT_AMBIGUOUS";
338
+ const ids = candidates.map((task) => task.coordinate).sort();
339
+ const code = candidates.length === 0 ? "INVALID_TARGET" : "TASK_AMBIGUOUS";
194
340
  throw Object.assign(new FlowError(code, candidates.length === 0
195
341
  ? "no task in progress"
196
- : "more than one task in progress; pass an ID"), { candidates: ids, candidateTasks: candidates });
342
+ : "more than one task in progress; pass a coordinate", candidates.length === 0 ? undefined : {
343
+ facts: { kind: "task_ambiguous", candidates: ids },
344
+ }), { candidates: ids, candidateTasks: candidates });
197
345
  }
198
346
  /** Reopen task pointers whose contract authority has disappeared. The caller
199
347
  * supplies the ledger fact; this operation itself remains storage-neutral. */
@@ -201,7 +349,7 @@ export async function rollbackOrphanTaskPointers(repository, contractIds, now =
201
349
  const before = await repository.read();
202
350
  const orphans = before.tasks
203
351
  .filter((task) => task.contractId !== undefined && !contractIds.has(task.contractId))
204
- .map((task) => ({ taskId: task.id, contractId: task.contractId }))
352
+ .map((task) => ({ taskId: task.coordinate, contractId: task.contractId }))
205
353
  .sort((a, b) => a.taskId.localeCompare(b.taskId));
206
354
  if (orphans.length === 0)
207
355
  return [];
@@ -220,7 +368,7 @@ export async function rollbackOrphanTaskPointers(repository, contractIds, now =
220
368
  if (replacements.length === 0)
221
369
  throw new FlowError("CONCURRENT_MODIFICATION", "task pointers changed while doctor was reconciling them");
222
370
  const subject = replacements.length === 1
223
- ? `Clear orphan contract pointer from task ${replacements[0].id}`
371
+ ? `Clear orphan contract pointer from task ${documentCoordinate(replacements[0])}`
224
372
  : `Clear orphan contract pointers from ${replacements.length} tasks`;
225
373
  return { replacements, subject, select: () => undefined };
226
374
  });
@@ -0,0 +1,56 @@
1
+ import { FlowError } from "../../flow-error.js";
2
+ import { isNormalizedIdentifierSlug } from "../identifier-slug.js";
3
+ import { taskIdentifierSlug } from "../identifier-slug.js";
4
+ export function formatTaskCoordinate(namespace, localId) {
5
+ return namespace === null ? localId : `${namespace}/${localId}`;
6
+ }
7
+ export function parseTaskCoordinate(value, field = "task coordinate") {
8
+ const parts = value.split("/");
9
+ if (parts.length < 1 || parts.length > 2) {
10
+ throw new FlowError("INVALID_TARGET", `${field} must be <local-id> or <namespace>/<local-id>`);
11
+ }
12
+ const namespace = parts.length === 1 ? null : parts[0];
13
+ const localId = parts.at(-1);
14
+ if (!isNormalizedIdentifierSlug(localId) || (namespace !== null && !isNormalizedIdentifierSlug(namespace))) {
15
+ throw new FlowError("INVALID_TARGET", `${field} must be <local-id> or <namespace>/<local-id>`);
16
+ }
17
+ return { namespace, localId, coordinate: formatTaskCoordinate(namespace, localId) };
18
+ }
19
+ export function isTaskCoordinate(value) {
20
+ try {
21
+ parseTaskCoordinate(value);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ export function taskCoordinateFromSourcePath(sourcePath) {
29
+ const normalized = sourcePath.replaceAll("\\", "/");
30
+ const marker = "/.keiyaku/tasks/";
31
+ const markerIndex = normalized.lastIndexOf(marker);
32
+ const relative = markerIndex < 0 ? normalized : normalized.slice(markerIndex + marker.length);
33
+ if (!relative.endsWith(".md")) {
34
+ throw new FlowError("INVALID_TARGET", `task source must be a Markdown file below .keiyaku/tasks: ${sourcePath}`);
35
+ }
36
+ return parseTaskCoordinate(relative.slice(0, -3), "task source coordinate");
37
+ }
38
+ export const TASK_NAMESPACE_SCOPE_PREFIX = ".keiyaku/tasks/";
39
+ export function taskNamespaceScope(namespace) {
40
+ parseTaskCoordinate(`${namespace}/task`, "task namespace");
41
+ return `${TASK_NAMESPACE_SCOPE_PREFIX}${namespace}/**`;
42
+ }
43
+ export function taskNamespaceFromScope(term) {
44
+ const match = /^\.keiyaku\/tasks\/([^/]+)\/\*\*$/.exec(term);
45
+ if (!match || !isNormalizedIdentifierSlug(match[1]))
46
+ return undefined;
47
+ return match[1];
48
+ }
49
+ export function allocateTaskNamespace(title, occupied) {
50
+ for (let ordinal = 1; Number.isSafeInteger(ordinal); ordinal += 1) {
51
+ const candidate = taskIdentifierSlug(title, ordinal === 1 ? "" : `-${ordinal}`);
52
+ if (!occupied.has(candidate))
53
+ return candidate;
54
+ }
55
+ throw new FlowError("INTERNAL_STATE", "task namespace collision ordinal exceeded the safe integer range");
56
+ }
@@ -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)