@nestos/kanban 0.0.0-stage → 0.5.2

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 (124) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +165 -2
  3. package/dist/activity-BbC4sgKL.d.cts +258 -0
  4. package/dist/activity-Czlan5xW.d.ts +258 -0
  5. package/dist/activity-module-DaO1Rry0.d.cts +22 -0
  6. package/dist/activity-module-T22wliZu.d.ts +22 -0
  7. package/dist/activity-ports-Dqxa86zT.d.cts +12 -0
  8. package/dist/activity-ports-cxfrelaL.d.ts +12 -0
  9. package/dist/activity.cjs +16 -0
  10. package/dist/activity.cjs.map +1 -0
  11. package/dist/activity.d.cts +12 -0
  12. package/dist/activity.d.ts +12 -0
  13. package/dist/activity.js +16 -0
  14. package/dist/activity.js.map +1 -0
  15. package/dist/chunk-3JYLJPE5.js +95 -0
  16. package/dist/chunk-3JYLJPE5.js.map +1 -0
  17. package/dist/chunk-3LEIHB7Y.js +221 -0
  18. package/dist/chunk-3LEIHB7Y.js.map +1 -0
  19. package/dist/chunk-4XF6LQSU.cjs +9 -0
  20. package/dist/chunk-4XF6LQSU.cjs.map +1 -0
  21. package/dist/chunk-5Q7WB63A.cjs +282 -0
  22. package/dist/chunk-5Q7WB63A.cjs.map +1 -0
  23. package/dist/chunk-7KDOTBRX.cjs +58 -0
  24. package/dist/chunk-7KDOTBRX.cjs.map +1 -0
  25. package/dist/chunk-AHHEGTUA.js +148 -0
  26. package/dist/chunk-AHHEGTUA.js.map +1 -0
  27. package/dist/chunk-AOHQCX7K.cjs +95 -0
  28. package/dist/chunk-AOHQCX7K.cjs.map +1 -0
  29. package/dist/chunk-DINLR6E6.cjs +443 -0
  30. package/dist/chunk-DINLR6E6.cjs.map +1 -0
  31. package/dist/chunk-EYTVC2H5.cjs +1612 -0
  32. package/dist/chunk-EYTVC2H5.cjs.map +1 -0
  33. package/dist/chunk-FEE2YBZ7.js +1972 -0
  34. package/dist/chunk-FEE2YBZ7.js.map +1 -0
  35. package/dist/chunk-FNO5K3A7.js +282 -0
  36. package/dist/chunk-FNO5K3A7.js.map +1 -0
  37. package/dist/chunk-IHHDXZPZ.js +382 -0
  38. package/dist/chunk-IHHDXZPZ.js.map +1 -0
  39. package/dist/chunk-J3EAIVA3.js +280 -0
  40. package/dist/chunk-J3EAIVA3.js.map +1 -0
  41. package/dist/chunk-KH3WF3K4.js +443 -0
  42. package/dist/chunk-KH3WF3K4.js.map +1 -0
  43. package/dist/chunk-M35X5PHO.js +196 -0
  44. package/dist/chunk-M35X5PHO.js.map +1 -0
  45. package/dist/chunk-QRGZD2JU.cjs +1972 -0
  46. package/dist/chunk-QRGZD2JU.cjs.map +1 -0
  47. package/dist/chunk-RBOER6EW.cjs +221 -0
  48. package/dist/chunk-RBOER6EW.cjs.map +1 -0
  49. package/dist/chunk-RQP77ZTY.js +9 -0
  50. package/dist/chunk-RQP77ZTY.js.map +1 -0
  51. package/dist/chunk-RSH6QSEC.cjs +196 -0
  52. package/dist/chunk-RSH6QSEC.cjs.map +1 -0
  53. package/dist/chunk-VMRDXOLX.cjs +148 -0
  54. package/dist/chunk-VMRDXOLX.cjs.map +1 -0
  55. package/dist/chunk-X4UHZX6F.js +58 -0
  56. package/dist/chunk-X4UHZX6F.js.map +1 -0
  57. package/dist/chunk-XBSGUVCY.cjs +382 -0
  58. package/dist/chunk-XBSGUVCY.cjs.map +1 -0
  59. package/dist/chunk-XKDZHAUE.js +1612 -0
  60. package/dist/chunk-XKDZHAUE.js.map +1 -0
  61. package/dist/chunk-ZUZGAO6G.cjs +280 -0
  62. package/dist/chunk-ZUZGAO6G.cjs.map +1 -0
  63. package/dist/content-C5aJZFlw.d.cts +35 -0
  64. package/dist/content-hLH5BNud.d.ts +35 -0
  65. package/dist/contracts/index.cjs +31 -0
  66. package/dist/contracts/index.cjs.map +1 -0
  67. package/dist/contracts/index.d.cts +5 -0
  68. package/dist/contracts/index.d.ts +5 -0
  69. package/dist/contracts/index.js +31 -0
  70. package/dist/contracts/index.js.map +1 -0
  71. package/dist/discovery-9E-LfwIw.d.cts +61 -0
  72. package/dist/discovery-D60yrjej.d.ts +61 -0
  73. package/dist/discovery.cjs +377 -0
  74. package/dist/discovery.cjs.map +1 -0
  75. package/dist/discovery.d.cts +22 -0
  76. package/dist/discovery.d.ts +22 -0
  77. package/dist/discovery.js +377 -0
  78. package/dist/discovery.js.map +1 -0
  79. package/dist/execution-bridge-fpCnH6z5.d.cts +52 -0
  80. package/dist/execution-bridge-rk0uVy_n.d.ts +52 -0
  81. package/dist/execution.cjs +18 -0
  82. package/dist/execution.cjs.map +1 -0
  83. package/dist/execution.d.cts +42 -0
  84. package/dist/execution.d.ts +42 -0
  85. package/dist/execution.js +18 -0
  86. package/dist/execution.js.map +1 -0
  87. package/dist/index.cjs +74 -0
  88. package/dist/index.cjs.map +1 -0
  89. package/dist/index.d.cts +54 -0
  90. package/dist/index.d.ts +54 -0
  91. package/dist/index.js +74 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/module-CYfE7YhM.d.ts +11 -0
  94. package/dist/module-Ca9L3qCn.d.cts +11 -0
  95. package/dist/planning-CHrOqQ6b.d.cts +879 -0
  96. package/dist/planning-CHrOqQ6b.d.ts +879 -0
  97. package/dist/ports-B0McVg7B.d.ts +143 -0
  98. package/dist/ports-BeGO1VRI.d.cts +49 -0
  99. package/dist/ports-GQy0x-RI.d.cts +143 -0
  100. package/dist/ports-ykGiNUSx.d.ts +49 -0
  101. package/dist/runtime/index.cjs +205 -0
  102. package/dist/runtime/index.cjs.map +1 -0
  103. package/dist/runtime/index.d.cts +75 -0
  104. package/dist/runtime/index.d.ts +75 -0
  105. package/dist/runtime/index.js +205 -0
  106. package/dist/runtime/index.js.map +1 -0
  107. package/dist/runtime/integration.cjs +423 -0
  108. package/dist/runtime/integration.cjs.map +1 -0
  109. package/dist/runtime/integration.d.cts +658 -0
  110. package/dist/runtime/integration.d.ts +658 -0
  111. package/dist/runtime/integration.js +423 -0
  112. package/dist/runtime/integration.js.map +1 -0
  113. package/dist/storage.cjs +21 -0
  114. package/dist/storage.cjs.map +1 -0
  115. package/dist/storage.d.cts +71 -0
  116. package/dist/storage.d.ts +71 -0
  117. package/dist/storage.js +21 -0
  118. package/dist/storage.js.map +1 -0
  119. package/dist/store-DPwHzGiz.d.cts +28 -0
  120. package/dist/store-DgdrPCV9.d.ts +28 -0
  121. package/dist/task-projection-CSorlMws.d.ts +11 -0
  122. package/dist/task-projection-DKKB_ueb.d.cts +11 -0
  123. package/examples/standalone.ts +22 -0
  124. package/package.json +143 -4
@@ -0,0 +1,1972 @@
1
+ import {
2
+ createKanbanActivityModule
3
+ } from "./chunk-FNO5K3A7.js";
4
+ import {
5
+ SeqKanbanStore
6
+ } from "./chunk-AHHEGTUA.js";
7
+ import {
8
+ createKanbanExecutionBridge
9
+ } from "./chunk-IHHDXZPZ.js";
10
+ import {
11
+ requireValue
12
+ } from "./chunk-3LEIHB7Y.js";
13
+ import {
14
+ DEFAULT_KANBAN_WORK_TYPES,
15
+ executableWorkItem
16
+ } from "./chunk-X4UHZX6F.js";
17
+ import {
18
+ KanbanError,
19
+ randomUUID,
20
+ sha256HexSync
21
+ } from "./chunk-M35X5PHO.js";
22
+ import {
23
+ boardAllowsStart,
24
+ taskDue
25
+ } from "./chunk-3JYLJPE5.js";
26
+
27
+ // src/runtime/diagnostics.ts
28
+ function safeCode(error, fallback) {
29
+ const code = error?.code;
30
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(code) ? code : fallback;
31
+ }
32
+
33
+ // src/runtime/automation-journal.ts
34
+ import { normalizeVirtualPath } from "@nestos/vfs-core";
35
+
36
+ // src/runtime/automation-board-index.ts
37
+ var boardIndexPath = "/kanban/runtime/automation-board-index.seq";
38
+ var families = ["review-operation:", "record-runs:", "fix:", "task-reply:"];
39
+ function boardIndexPrefix(boardId, family) {
40
+ return JSON.stringify([boardId, family]).slice(0, -1) + ",";
41
+ }
42
+ function boardIndexKeys(key, value) {
43
+ const family = families.find((prefix2) => key.startsWith(prefix2));
44
+ if (!family) return [];
45
+ const boards = /* @__PURE__ */ new Set();
46
+ for (const row of Array.isArray(value) ? value : [value]) {
47
+ if (!row || typeof row !== "object") continue;
48
+ for (const id of [row.boardId, row.sourceBoardId]) if (typeof id === "string" && id) boards.add(id);
49
+ }
50
+ return [...boards].sort().map((board) => JSON.stringify([board, family, key]));
51
+ }
52
+ async function updateBoardIndex(tx, key, previous, value, indexPath = boardIndexPath) {
53
+ const next = new Set(boardIndexKeys(key, value)), oldKeys = new Set(previous);
54
+ for (const old of previous) if (!next.has(old)) await tx.deleteEntry(indexPath, old);
55
+ for (const index of next) if (!oldKeys.has(index)) await tx.setEntry(indexPath, index, key);
56
+ }
57
+ async function migrateBoardIndex(tx, source, indexPath = boardIndexPath, diagnostic = () => {
58
+ }) {
59
+ if (await tx.getEntry(indexPath, "version") === "1") return;
60
+ const started = Date.now(), operationId = randomUUID();
61
+ let count = 0;
62
+ try {
63
+ for (const keyPrefix of families) await tx.walkEntries(source, async (entry) => {
64
+ await updateBoardIndex(tx, entry.key, [], JSON.parse(entry.value), indexPath);
65
+ count++;
66
+ return true;
67
+ }, { keyPrefix });
68
+ await tx.setEntry(indexPath, "version", "1");
69
+ diagnostic({ operation: "kanban.board.index", operationId, stage: "migrate", result: "success", count, durationMs: Date.now() - started });
70
+ } catch (error) {
71
+ diagnostic({ operation: "kanban.board.index", operationId, stage: "migrate", result: "failure", code: safeCode(error, "BOARD_INDEX_MIGRATION_FAILED"), durationMs: Date.now() - started });
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ // src/runtime/automation-journal.ts
77
+ var KanbanAutomationJournal = class {
78
+ constructor(fs, options = {}) {
79
+ this.fs = fs;
80
+ this.options = options;
81
+ this.directory = normalizeVirtualPath(options.directory ?? "/kanban/runtime");
82
+ this.path = this.directory + "/automation.seq";
83
+ this.indexPath = this.directory + "/automation-board-index.seq";
84
+ this.artifactScheme = options.artifactScheme ?? "kanban-artifact:";
85
+ }
86
+ writes = /* @__PURE__ */ new Set();
87
+ directory;
88
+ indexPath;
89
+ path;
90
+ artifactScheme;
91
+ diagnostic(event) {
92
+ this.options.diagnostic?.(event);
93
+ }
94
+ async open() {
95
+ requireValue(this.fs.meta.seq?.transaction, "TRANSACTION_REQUIRED");
96
+ for (const name of ["automation.seq", "automation-board-index.seq"]) {
97
+ try {
98
+ await this.fs.driver.createFile({ parentPath: this.directory, name, type: "seqfile", recursive: true });
99
+ } catch (error) {
100
+ if (error.code !== "EEXIST") throw error;
101
+ }
102
+ }
103
+ if (await this.fs.meta.seq.getEntry(this.indexPath, "version") !== "1")
104
+ await this.fs.meta.seq.transaction((tx) => migrateBoardIndex(tx, this.path, this.indexPath, (event) => this.diagnostic(event)));
105
+ }
106
+ async get(key) {
107
+ const raw = await this.fs.meta.seq.getEntry(this.path, key);
108
+ return raw ? JSON.parse(raw) : void 0;
109
+ }
110
+ async getMany(keys) {
111
+ if (!keys.length) return [];
112
+ const rows = await this.fs.meta.seq.getEntries(this.path, keys);
113
+ return keys.map((key) => rows[key] === void 0 ? void 0 : JSON.parse(rows[key]));
114
+ }
115
+ async findForBoard(family, boardId, matches) {
116
+ const keys = [];
117
+ await this.fs.meta.seq.walkEntries(this.indexPath, (entry) => {
118
+ keys.push(entry.value);
119
+ requireValue(keys.length <= 1e3, "SESSION_RELATION_LIMIT");
120
+ return true;
121
+ }, { keyPrefix: boardIndexPrefix(boardId, family) });
122
+ const values = await this.getMany(keys);
123
+ return values.flatMap((value, index) => value !== void 0 && matches(value) ? [{ key: keys[index], value }] : []);
124
+ }
125
+ async find(keyPrefix, matches) {
126
+ const rows = [];
127
+ await this.fs.meta.seq.walkEntries(this.path, (entry) => {
128
+ const value = JSON.parse(entry.value);
129
+ if (matches(value, entry.key)) rows.push({ key: entry.key, value });
130
+ requireValue(rows.length <= 1e3, "SESSION_RELATION_LIMIT");
131
+ return true;
132
+ }, { keyPrefix });
133
+ return rows;
134
+ }
135
+ update(key, change) {
136
+ const pending2 = this.commit(key, change).finally(() => {
137
+ this.writes.delete(pending2);
138
+ });
139
+ this.writes.add(pending2);
140
+ return pending2;
141
+ }
142
+ async drain() {
143
+ await Promise.allSettled([...this.writes]);
144
+ }
145
+ claim(key) {
146
+ const pending2 = this.fs.meta.seq.transaction(async (tx) => {
147
+ const raw = await tx.getEntry(this.path, key);
148
+ requireValue(raw, "AUTOMATION_JOURNAL_MISSING");
149
+ const record = JSON.parse(raw);
150
+ if (record.phase !== "prepared") return false;
151
+ await tx.setEntry(this.path, key, JSON.stringify({ ...record, phase: "submitting" }));
152
+ return true;
153
+ }).finally(() => {
154
+ this.writes.delete(pending2);
155
+ });
156
+ this.writes.add(pending2);
157
+ return pending2;
158
+ }
159
+ async commit(key, change) {
160
+ return this.fs.meta.seq.transaction(async (tx) => {
161
+ const raw = await tx.getEntry(this.path, key), previous = raw ? JSON.parse(raw) : void 0;
162
+ const previousIndex = boardIndexKeys(key, previous), value = change(previous);
163
+ const encoded = JSON.stringify(value);
164
+ if (encoded === raw) return structuredClone(value);
165
+ await updateBoardIndex(tx, key, previousIndex, value, this.indexPath);
166
+ await tx.setEntry(this.path, key, encoded);
167
+ return structuredClone(value);
168
+ });
169
+ }
170
+ async artifact(dispatchId, value) {
171
+ const text = JSON.stringify(value ?? null), bytes = new TextEncoder().encode(text).byteLength;
172
+ requireValue(bytes <= 1024 * 1024, "AUTOMATION_OUTPUT_TOO_LARGE");
173
+ const sha256 = sha256HexSync(text);
174
+ await this.update("artifact:" + sha256, (previous) => {
175
+ requireValue(previous === void 0 || previous === text, "ARTIFACT_CONFLICT");
176
+ return text;
177
+ });
178
+ const outputs = [{ name: "result", ref: this.artifactScheme + sha256, sha256, bytes }];
179
+ await this.update("artifact-owner:" + dispatchId, (previous) => {
180
+ requireValue(!previous || JSON.stringify(previous) === JSON.stringify(outputs), "ARTIFACT_CONFLICT");
181
+ return outputs;
182
+ });
183
+ return outputs;
184
+ }
185
+ async verify(outputs, dispatchId) {
186
+ if (dispatchId && JSON.stringify(await this.get("artifact-owner:" + dispatchId)) !== JSON.stringify(outputs)) return false;
187
+ for (const output of outputs) {
188
+ if (output.ref !== this.artifactScheme + output.sha256) return false;
189
+ const text = await this.get("artifact:" + output.sha256);
190
+ if (text === void 0 || sha256HexSync(text) !== output.sha256 || new TextEncoder().encode(text).byteLength !== output.bytes) return false;
191
+ }
192
+ return true;
193
+ }
194
+ };
195
+
196
+ // src/runtime/ports.ts
197
+ var defaultReferences = {
198
+ resource: "kanban-resource",
199
+ decision: "kanban-decision",
200
+ execution: "kanban-execution",
201
+ workspace: "kanban-workspace"
202
+ };
203
+
204
+ // src/runtime/work-log-snapshots.ts
205
+ async function workLogSnapshots(boardId, source, items, seen, projection) {
206
+ const { activityRecord, ancestorIds, lifecycleReferences } = projection;
207
+ const workflow = await source.workflow(boardId), records = bindingSnapshots(boardId, items, seen, projection);
208
+ for (const dispatch of Object.values(workflow.dispatches)) {
209
+ const item = items.find((item2) => item2.id === dispatch.itemId);
210
+ if (dispatch.outcome !== "accepted" || item?.status !== "completed") continue;
211
+ records.push(activityRecord(
212
+ { id: boardId },
213
+ item,
214
+ `${dispatch.id}:task.completed:`,
215
+ item.closedAt ?? Date.now(),
216
+ { stage: "task.completed", result: "success", dispatchId: dispatch.id, fields: { evidence: "accepted-dispatch" } },
217
+ lifecycleReferences(item.externalRefs),
218
+ void 0,
219
+ ancestorIds(item, items)
220
+ ));
221
+ }
222
+ return records;
223
+ }
224
+ function bindingSnapshots(board, items, seen, projection) {
225
+ const { activityRecord, ancestorIds, lifecycleReferences } = projection;
226
+ const at = Date.now();
227
+ return items.flatMap((item) => {
228
+ const refs = lifecycleReferences(item.externalRefs).filter((ref) => !seen.has(JSON.stringify([item.id, ref.system, ref.id])));
229
+ if (!refs.length) return [];
230
+ return [activityRecord(
231
+ { id: board },
232
+ item,
233
+ "snapshot:" + item.id,
234
+ at,
235
+ { stage: "association.observed", result: "success", fields: { evidence: "existing-reference" } },
236
+ refs,
237
+ void 0,
238
+ ancestorIds(item, items)
239
+ )];
240
+ });
241
+ }
242
+
243
+ // src/runtime/work-log-store.ts
244
+ import { FSError } from "@nestos/vfs-core";
245
+ var snapshotVersion = "2";
246
+ var prefix = (board) => encodeURIComponent(board) + "/";
247
+ var rowKey = (board, id) => "row/" + prefix(board) + encodeURIComponent(id);
248
+ var KanbanWorkLogStore = class {
249
+ constructor(fs, directory = "/kanban/runtime", executionSystem = "kanban-execution") {
250
+ this.fs = fs;
251
+ this.directory = directory;
252
+ this.executionSystem = executionSystem;
253
+ this.path = directory + "/work-log.seq";
254
+ }
255
+ path;
256
+ async open() {
257
+ try {
258
+ await this.fs.driver.createFile({ parentPath: this.directory, name: "work-log.seq", type: "seqfile", recursive: true });
259
+ } catch (error) {
260
+ if (!(error instanceof FSError) || error.code !== "EEXIST") throw error;
261
+ }
262
+ }
263
+ async record(board, id) {
264
+ const raw = await this.fs.meta.seq.getEntry(this.path, rowKey(board, id));
265
+ return raw ? JSON.parse(raw) : void 0;
266
+ }
267
+ async append(board, record) {
268
+ await this.fs.meta.seq.transaction((tx) => this.appendTx(tx, board, record));
269
+ }
270
+ async checkpoint(board) {
271
+ return Number(await this.fs.meta.seq.getEntry(this.path, "checkpoint/" + prefix(board)) ?? 0);
272
+ }
273
+ async commit(board, after, through, rows) {
274
+ await this.fs.meta.seq.transaction(async (tx) => {
275
+ if (Number(await tx.getEntry(this.path, "checkpoint/" + prefix(board)) ?? 0) !== after) return;
276
+ for (const row of rows) await this.appendTx(tx, board, row);
277
+ await tx.setEntry(this.path, "checkpoint/" + prefix(board), String(through));
278
+ });
279
+ }
280
+ async needsSnapshot(board) {
281
+ return await this.fs.meta.seq.getEntry(this.path, "snapshot/" + prefix(board)) !== snapshotVersion;
282
+ }
283
+ async snapshot(board, records) {
284
+ await this.fs.meta.seq.transaction(async (tx) => {
285
+ const key = "snapshot/" + prefix(board);
286
+ if (await tx.getEntry(this.path, key) === snapshotVersion) return;
287
+ for (const record of records) await this.appendTx(tx, board, record);
288
+ await tx.setEntry(this.path, key, snapshotVersion);
289
+ });
290
+ }
291
+ async page(board, itemId, offset, limit) {
292
+ return this.fs.meta.seq.transaction(async (tx) => {
293
+ const rows = [];
294
+ const keyPrefix = itemId ? "item/" + prefix(board) + encodeURIComponent(itemId) + "/" : "summary/" + prefix(board);
295
+ const page = await tx.walkEntries(this.path, (entry) => {
296
+ rows.push(JSON.parse(entry.value));
297
+ return true;
298
+ }, { keyPrefix, offset, limit });
299
+ return { items: rows, total: page.total, revision: Number(await tx.getEntry(this.path, "revision/" + prefix(board)) ?? 0) };
300
+ });
301
+ }
302
+ async appendTx(tx, board, row) {
303
+ if (await tx.getEntry(this.path, rowKey(board, row.id))) return;
304
+ const { content: _content, rationale: _rationale, sources: _sources, ...metadata } = row;
305
+ const summary = { ...metadata, sessionIds: row.externalRefs?.filter((ref) => ref.system === this.executionSystem).map((ref) => ref.id) ?? [] };
306
+ const order2 = String(Number.MAX_SAFE_INTEGER - row.createdAt).padStart(16, "0") + "/" + row.id;
307
+ await tx.setEntry(this.path, rowKey(board, row.id), JSON.stringify(row));
308
+ await tx.setEntry(this.path, "summary/" + prefix(board) + order2, JSON.stringify(summary));
309
+ for (const id of row.itemIds) await tx.setEntry(this.path, "item/" + prefix(board) + encodeURIComponent(id) + "/" + order2, JSON.stringify(summary));
310
+ await tx.increment(this.path, "revision/" + prefix(board));
311
+ }
312
+ };
313
+
314
+ // src/runtime/work-log-events.ts
315
+ function createWorkLogProjection(options = {}) {
316
+ const references = options.references ?? defaultReferences;
317
+ function eventActivity(event) {
318
+ const base = { actor: { kind: event.actor.kind ?? "agent", id: event.actor.id ?? event.actor.agentId ?? "unknown" }, result: "success", ...event.data.dispatchId ? { dispatchId: event.data.dispatchId } : {} };
319
+ if (event.kind === "report-analysis") return { ...base, stage: "analysis." + (event.data.reason?.slice("analysis-".length) ?? "unknown"), result: event.data.code ? "failure" : "success", code: event.data.code };
320
+ if (event.kind === "control-receipt") return { ...base, stage: "execution." + (event.data.reason ?? "control-unknown"), result: event.data.code ? "failure" : "success", code: event.data.code };
321
+ if (event.kind === "create-item") return { ...base, stage: "task.created" };
322
+ if (event.kind === "prepare-dispatch") return { ...base, stage: "execution.prepared" };
323
+ if (event.kind === "continue-execution") return { ...base, stage: "execution.continued" };
324
+ if (event.kind === "submit") return { ...base, stage: "execution.review" };
325
+ if (event.kind === "close-plan" || event.kind === "review" && event.data.reason === "accepted" || event.kind === "accept-execution" && event.data.reason === "accepted") return { ...base, stage: "task.completed" };
326
+ if (event.kind === "cancel-item" || event.kind === "release-item" && event.data.reason === "cancelled") return { ...base, stage: "task.cancelled", result: "cancelled" };
327
+ if (event.kind === "observe-execution") {
328
+ const result = event.data.reason?.slice("execution-".length);
329
+ if (result && ["running", "succeeded", "failed", "cancelled", "unknown"].includes(result)) return {
330
+ ...base,
331
+ stage: result === "succeeded" && event.data.status === "completed" ? "task.completed" : "execution." + result,
332
+ result: result === "failed" || result === "unknown" ? "failure" : result === "cancelled" ? "cancelled" : "success",
333
+ code: event.data.code
334
+ };
335
+ }
336
+ if (event.kind === "link-reference" && event.data.reference) {
337
+ const system = event.data.reference.system;
338
+ const stage = system === references.execution ? "session.bound" : system === references.nativeExecution ? "session.native" : system === references.workspace ? "worktree.prepared" : void 0;
339
+ if (stage) return { ...base, stage };
340
+ }
341
+ if (event.kind === "message") return { ...base, stage: "task.message" };
342
+ if (event.kind === "block-item") return { ...base, stage: "task.blocked", result: "failure", code: event.data.reason };
343
+ return void 0;
344
+ }
345
+ function activityRecord(board, item, id, at, activity, refs = [], content, ancestors = []) {
346
+ const label = options.label?.(activity.stage) ?? activity.stage;
347
+ return {
348
+ id: "activity-" + sha256HexSync(JSON.stringify([board.id, id])),
349
+ kind: "log",
350
+ title: [item?.title, label].filter(Boolean).join(" \xB7 "),
351
+ content: content ?? [label, activity.code, ...Object.entries(activity.fields ?? {}).map(([key, value]) => `${key}: ${value}`)].filter(Boolean).join("\n"),
352
+ activity,
353
+ externalRefs: refs,
354
+ itemIds: [.../* @__PURE__ */ new Set([...item ? [item.id] : [], ...ancestors])],
355
+ sources: [],
356
+ author: activity.actor ?? { kind: "service", id: "kanban-activity" },
357
+ origin: "flow",
358
+ status: "confirmed",
359
+ revision: 1,
360
+ createdAt: at,
361
+ updatedAt: at
362
+ };
363
+ }
364
+ function ancestorIds(item, items) {
365
+ const result = [], seen = new Set(item ? [item.id] : []);
366
+ let parent = item?.planning.parentId;
367
+ while (parent && !seen.has(parent)) {
368
+ seen.add(parent);
369
+ result.push(parent);
370
+ parent = items.find((row) => row.id === parent)?.planning.parentId;
371
+ }
372
+ return result;
373
+ }
374
+ function lifecycleReferences(refs = []) {
375
+ return [references.execution, references.nativeExecution, references.workspace].flatMap((system) => {
376
+ const ref = refs.filter((ref2) => ref2.system === system).at(-1);
377
+ return ref ? [ref] : [];
378
+ });
379
+ }
380
+ return { eventActivity, activityRecord, ancestorIds, lifecycleReferences };
381
+ }
382
+
383
+ // src/runtime/work-log.ts
384
+ var order = (a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id);
385
+ var KanbanWorkLog = class {
386
+ constructor(fs, options = {}) {
387
+ this.options = options;
388
+ this.projection = createWorkLogProjection(options);
389
+ this.log = (event) => options.diagnostic?.(event);
390
+ this.store = new KanbanWorkLogStore(fs, options.directory, (options.references ?? defaultReferences).execution);
391
+ }
392
+ store;
393
+ projection;
394
+ log;
395
+ open() {
396
+ return this.store.open();
397
+ }
398
+ async synchronize(boardId, source) {
399
+ const started = Date.now(), operationId = randomUUID();
400
+ try {
401
+ let after = await this.store.checkpoint(boardId);
402
+ const [page, snapshot] = await Promise.all([source.events(boardId, after, 500), this.store.needsSnapshot(boardId)]);
403
+ if (!page.length && !snapshot) return;
404
+ const board = { id: boardId }, items = await allItems(source, boardId);
405
+ let events = page;
406
+ const seenReferences = /* @__PURE__ */ new Set();
407
+ while (events.length) {
408
+ const records = events.flatMap((event) => {
409
+ if (event.data.reference && event.itemId) seenReferences.add(JSON.stringify([event.itemId, event.data.reference.system, event.data.reference.id]));
410
+ const activity = this.projection.eventActivity(event);
411
+ if (!activity || !event.itemId) return [];
412
+ const item = items.find((item2) => item2.id === event.itemId), reference = event.data.reference;
413
+ const snapshot2 = item && event.data.itemTitle ? { ...item, title: event.data.itemTitle } : item;
414
+ const key = factKey(activity, reference ? [reference] : [], event.itemId, "event:" + event.sequence, this.options.references ?? defaultReferences);
415
+ return [this.projection.activityRecord(board, snapshot2, key, event.at, activity, reference ? [reference] : this.projection.lifecycleReferences(item?.externalRefs), event.data.message, this.projection.ancestorIds(item, items))];
416
+ });
417
+ const through = events.at(-1).sequence;
418
+ await this.store.commit(boardId, after, through, records);
419
+ after = through;
420
+ if (events.length < 500) break;
421
+ events = await source.events(boardId, after, 500);
422
+ }
423
+ if (snapshot) await this.store.snapshot(boardId, await workLogSnapshots(boardId, source, items, seenReferences, this.projection));
424
+ this.log({ operationId, boardId, stage: "work-log.project", result: "success", durationMs: Date.now() - started });
425
+ } catch (error) {
426
+ const code = error?.code;
427
+ this.log({ operationId, boardId, stage: "work-log.project", result: "failure", code: code && /^[A-Z_]{1,80}$/.test(code) ? code : "WORK_LOG_PROJECTION_FAILED", durationMs: Date.now() - started });
428
+ throw error;
429
+ }
430
+ }
431
+ async append(source, boardId, itemId, fact) {
432
+ requireValue(/^[a-z][a-z0-9.-]{0,79}$/.test(fact.stage) && ["success", "failure", "cancelled"].includes(fact.result), "INVALID_WORK_LOG");
433
+ const board = { id: boardId }, items = await allItems(source, boardId), item = items.find((item2) => item2.id === itemId);
434
+ requireValue(item, "ITEM_NOT_FOUND");
435
+ const { references = [], summary, ...activity } = fact;
436
+ const id = factKey(activity, references, itemId, fact.dispatchId ?? itemId, this.options.references ?? defaultReferences);
437
+ const record = this.projection.activityRecord(board, item, id, Date.now(), activity, references, summary, this.projection.ancestorIds(item, items));
438
+ await this.store.append(boardId, record);
439
+ await this.options.onRecord?.({ boardId, itemId, record });
440
+ }
441
+ async record(source, boardId, id) {
442
+ if (!id.startsWith("activity-")) return source.record(boardId, id);
443
+ await source.records(boardId, { limit: 1 });
444
+ await this.synchronize(boardId, source);
445
+ return await this.store.record(boardId, id) ?? source.record(boardId, id);
446
+ }
447
+ async records(source, boardId, query = {}) {
448
+ if (query.kind === "decision" || query.status && query.status !== "confirmed") return source.records(boardId, query);
449
+ await this.synchronize(boardId, source);
450
+ const { cursor, limit = 50, ...filter } = query, fingerprint = sha256HexSync(JSON.stringify({ ...filter, limit }));
451
+ requireValue(Number.isSafeInteger(limit) && limit > 0 && limit <= 200, "INVALID_LIMIT");
452
+ const saved = parseCursor(cursor, boardId, fingerprint);
453
+ const manual = await source.records(boardId, { ...filter, limit: 200 });
454
+ const automatic = await this.store.page(boardId, query.itemId, saved?.automatic ?? 0, limit);
455
+ if (saved && (saved.revision !== manual.revision || saved.logRevision !== automatic.revision)) throw new KanbanError("CURSOR_STALE");
456
+ const remaining = manual.items.slice(saved?.manual ?? 0), autoIds = new Set(automatic.items.map((item) => item.id));
457
+ const items = [...remaining, ...automatic.items].sort(order).slice(0, limit);
458
+ const usedAuto = items.filter((item) => autoIds.has(item.id)).length, usedManual = items.length - usedAuto;
459
+ const next = {
460
+ version: 1,
461
+ board: boardId,
462
+ query: fingerprint,
463
+ revision: manual.revision,
464
+ logRevision: automatic.revision,
465
+ manual: (saved?.manual ?? 0) + usedManual,
466
+ automatic: (saved?.automatic ?? 0) + usedAuto
467
+ };
468
+ return {
469
+ revision: manual.revision,
470
+ total: manual.total + automatic.total,
471
+ items,
472
+ ...next.manual < manual.total || next.automatic < automatic.total ? { nextCursor: JSON.stringify(next) } : {}
473
+ };
474
+ }
475
+ };
476
+ async function allItems(source, board) {
477
+ const items = [];
478
+ let cursor;
479
+ do {
480
+ const page = await source.query(board, { view: "all", limit: 200, ...cursor ? { cursor } : {} });
481
+ items.push(...page.items);
482
+ cursor = page.nextCursor;
483
+ } while (cursor);
484
+ return items;
485
+ }
486
+ function parseCursor(cursor, board, query) {
487
+ if (!cursor) return;
488
+ let value;
489
+ try {
490
+ value = JSON.parse(cursor);
491
+ } catch {
492
+ throw new KanbanError("INVALID_CURSOR");
493
+ }
494
+ requireValue(value?.version === 1 && value.board === board && value.query === query, "CURSOR_SCOPE_MISMATCH");
495
+ requireValue([value.manual, value.automatic, value.revision, value.logRevision].every((value2) => Number.isSafeInteger(value2) && value2 >= 0), "INVALID_CURSOR");
496
+ return value;
497
+ }
498
+ function factKey(activity, refs, itemId, fallback, systems) {
499
+ const tree = refs.find((ref2) => ref2.system === systems.workspace);
500
+ if (tree && activity.stage === "worktree.prepared") return `${tree.id}:worktree.prepared`;
501
+ if (activity.dispatchId) return `${activity.dispatchId}:${activity.stage}:${activity.code ?? ""}${["execution.running", "execution.unknown"].includes(activity.stage) && fallback.startsWith("event:") ? ":" + fallback : ""}`;
502
+ const ref = refs.find((ref2) => [systems.nativeExecution, systems.execution].includes(ref2.system));
503
+ return ref ? JSON.stringify([itemId, activity.stage, ref.system, ref.id]) : fallback;
504
+ }
505
+
506
+ // src/runtime/journal-command.ts
507
+ async function journalCommand(journal, service, boardId, key, command) {
508
+ if ((await journal.get(key))?.done) return;
509
+ const board = await service.read(boardId);
510
+ const entry = await journal.update(key, (old) => old ?? { request: { operationId: randomUUID(), expectedRevision: board.revision, command } });
511
+ if (entry.done) return;
512
+ try {
513
+ await service.execute(boardId, entry.request);
514
+ } catch (error) {
515
+ if (error instanceof KanbanError && error.code === "REVISION_CONFLICT") {
516
+ const latest = await service.read(boardId);
517
+ await journal.update(key, (old) => old?.request.operationId !== entry.request.operationId || old.done ? old : {
518
+ request: { ...entry.request, operationId: randomUUID(), expectedRevision: latest.revision }
519
+ });
520
+ }
521
+ throw error;
522
+ }
523
+ await journal.update(key, (old) => ({ ...old, done: true }));
524
+ }
525
+
526
+ // src/runtime/record-controls.ts
527
+ var recordControlKey = (board, record) => "record-control:" + JSON.stringify([board, record]);
528
+ async function advanceRecordControl(service, journal, run, control) {
529
+ const ids = /* @__PURE__ */ new Set([run.rootItemId, ...run.itemIds ?? []]);
530
+ let board;
531
+ try {
532
+ board = await service.read(run.boardId);
533
+ } catch (error) {
534
+ if (control.action === "cancel" && error.code === "BOARD_NOT_FOUND") return "cancelled";
535
+ throw error;
536
+ }
537
+ let items = board.items.filter((item) => ids.has(item.id));
538
+ for (let index = 0; index < board.items.length; index++) {
539
+ const children = board.items.filter((item) => ids.has(item.planning?.parentId));
540
+ if (children.every((item) => ids.has(item.id))) break;
541
+ for (const child of children) ids.add(child.id);
542
+ }
543
+ items = board.items.filter((item) => ids.has(item.id));
544
+ for (const item of items.filter((item2) => !["completed", "cancelled"].includes(item2.status))) await requestItemControl(service, journal, run, control, item);
545
+ const latest = await service.read(run.boardId), active = latest.items.filter((item) => ids.has(item.id) && !["completed", "cancelled"].includes(item.status));
546
+ if (control.action === "cancel") return active.length ? "cancelling" : "cancelled";
547
+ const managed = active.filter((item) => control.controls[item.id]);
548
+ if (managed.some((item) => item.control?.state === "unknown")) return "unknown";
549
+ if (managed.some((item) => item.control?.state === "failed")) return "blocked";
550
+ const target = control.action === "pause" ? "paused" : "resumed";
551
+ return managed.every((item) => item.control?.state === target) ? control.action === "pause" ? "paused" : "running" : control.action === "pause" ? "pausing" : "resuming";
552
+ }
553
+ async function requestItemControl(service, journal, run, control, item) {
554
+ const action = control.action, controlId = sha256HexSync(JSON.stringify([run.sourceBoardId, run.recordId, control.operationId, run.boardId, item.id, action]));
555
+ if (item.control?.id !== controlId) {
556
+ if (action === "resume" && (!control.controls[item.id] || item.control?.id !== control.controls[item.id])) return;
557
+ if (action === "pause" && item.control?.action === "pause") return;
558
+ await journalCommand(
559
+ journal,
560
+ service,
561
+ run.boardId,
562
+ "record-control-item:" + controlId,
563
+ { type: "request-control", itemId: item.id, controlId, action }
564
+ );
565
+ }
566
+ const saved = await journal.update(recordControlKey(run.sourceBoardId, run.recordId), (old) => ({ ...old, controls: { ...old.controls, [item.id]: controlId } }));
567
+ control.controls = saved.controls;
568
+ }
569
+ async function requestRecordControl(service, journal, boardId, request) {
570
+ requireValue(/^[\w-]{1,128}$/.test(request.operationId) && ["pause", "resume", "cancel"].includes(request.action), "INVALID_RECORD_CONTROL");
571
+ const board = await service.read(boardId), actor = await service.currentPrincipal(), record = await service.record(boardId, request.recordId);
572
+ requireValue(actor.kind === "human" && board.members?.some((m) => m.kind === actor.kind && m.id === actor.id && m.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
573
+ requireValue(record.kind === "decision" && record.deletedAt === void 0, "DECISION_REQUIRED");
574
+ await journal.update(recordControlKey(boardId, record.id), (old) => {
575
+ if (old?.operationId === request.operationId) {
576
+ requireValue(old.action === request.action, "IDEMPOTENCY_CONFLICT");
577
+ return old;
578
+ }
579
+ requireValue(!old || old.action !== "cancel", "RECORD_CANCELLING");
580
+ requireValue(request.action !== "resume" || old?.action === "pause", "TASK_NOT_PAUSED");
581
+ return { boardId, recordId: record.id, operationId: request.operationId, action: request.action, controls: old?.controls ?? {} };
582
+ });
583
+ if (request.action === "cancel") await journalCommand(journal, service, boardId, "record-cancelling:" + JSON.stringify([boardId, record.id, request.operationId]), { type: "request-record-cancellation", recordId: record.id });
584
+ }
585
+
586
+ // src/runtime/record-execution.ts
587
+ function createRecordExecution(options) {
588
+ let stopped = false;
589
+ const pending2 = /* @__PURE__ */ new Map();
590
+ const resume = (run) => {
591
+ requireValue(!stopped, "SERVICE_STOPPING");
592
+ let job = pending2.get(run.id);
593
+ if (!job) {
594
+ job = advance(options, run).finally(() => {
595
+ pending2.delete(run.id);
596
+ });
597
+ pending2.set(run.id, job);
598
+ }
599
+ return job;
600
+ };
601
+ return {
602
+ itemResources: async (boardId, parentId) => {
603
+ const board = await options.service.read(boardId), parent = parentId ? (await options.service.item(boardId, parentId)).item : void 0;
604
+ const refs = parent?.externalRefs?.filter((ref) => ref.system === options.resources.system) ?? [];
605
+ const resources = await options.resources.list();
606
+ return { resources, selected: refs.length === 1 ? refs[0] : void 0, readonly: refs.length === 1, required: refs.length === 1 || options.requiresItemResource?.(board) === true };
607
+ },
608
+ recordResources: async () => await options.resources.list(),
609
+ runRecord: async (boardId, request) => {
610
+ requireValue(!stopped, "SERVICE_STOPPING");
611
+ const started = Date.now(), operationId = sha256HexSync(JSON.stringify([boardId, request.operationId]));
612
+ let run;
613
+ try {
614
+ run = await prepare(options, boardId, request);
615
+ options.log({ operationId, boardId, stage: "record.prepare", result: "success", durationMs: Date.now() - started });
616
+ } catch (error) {
617
+ options.log({ operationId, boardId, stage: "record.prepare", result: "failure", code: safeCode(error, "RECORD_PREPARATION_FAILED"), durationMs: Date.now() - started });
618
+ throw error;
619
+ }
620
+ return resume(run);
621
+ },
622
+ controlRecord: async (boardId, request) => {
623
+ await requestRecordControl(options.service, options.journal, boardId, request);
624
+ for (const run of await options.journal.get("record-runs:" + boardId) ?? []) if (run.recordId === request.recordId) {
625
+ await pending2.get(run.id)?.catch(() => {
626
+ });
627
+ await resume(run);
628
+ }
629
+ const runs = (await options.journal.get("record-runs:" + boardId) ?? []).filter((run) => run.recordId === request.recordId);
630
+ if (request.action === "cancel" && runs.every((run) => ["completed", "failed", "cancelled"].includes(run.state))) await finishDecisionCancellation(options, boardId, request.recordId, request.operationId);
631
+ },
632
+ recordExecutions: async (boardId, recordId) => {
633
+ await options.service.record(boardId, recordId);
634
+ const runs = await options.journal.get("record-runs:" + boardId) ?? [];
635
+ return runs.filter((run) => run.recordId === recordId).map(view);
636
+ },
637
+ async scan() {
638
+ if (stopped) return;
639
+ const errors = [];
640
+ for (const board of await options.service.catalog()) {
641
+ if (stopped) return;
642
+ for (const run of await options.journal.get("record-runs:" + board.id) ?? []) {
643
+ if (stopped) return;
644
+ if (!["completed", "failed", "cancelled"].includes(run.state)) {
645
+ try {
646
+ await resume(run);
647
+ } catch (error) {
648
+ errors.push(error);
649
+ }
650
+ }
651
+ }
652
+ }
653
+ for (const row of await options.journal.find("record-control:", (value) => value.action === "cancel" && !value.settled)) {
654
+ if (!row.value.boardId || !row.value.recordId) continue;
655
+ try {
656
+ await finishDecisionCancellation(options, row.value.boardId, row.value.recordId, row.value.operationId);
657
+ } catch (error) {
658
+ errors.push(error);
659
+ }
660
+ }
661
+ if (errors.length) throw new AggregateError(errors, "RECORD_EXECUTION_SCAN_FAILED");
662
+ },
663
+ async quiesce() {
664
+ stopped = true;
665
+ await Promise.allSettled([...pending2.values()]);
666
+ }
667
+ };
668
+ }
669
+ async function validateResourceReferences(resources, record) {
670
+ const refs = record.externalRefs?.filter((ref) => ref.system === resources.system) ?? [];
671
+ requireValue(refs.length <= 16 && new Set(refs.map((ref) => ref.id)).size === refs.length, "INVALID_RESOURCE_SELECTION");
672
+ await resources.validate(refs);
673
+ }
674
+ async function prepare(options, boardId, request) {
675
+ requireValue(typeof request.operationId === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(request.operationId), "INVALID_OPERATION_ID");
676
+ requireValue(!(await options.service.read(boardId)).lifecycle, "BOARD_STOPPING");
677
+ const record = await options.service.record(boardId, request.recordId);
678
+ const control = await options.journal.get(recordControlKey(boardId, record.id));
679
+ requireValue(record.deletedAt === void 0 && (!control || control.action === "resume" && control.settled), "RECORD_CONTROL_ACTIVE");
680
+ const frozen = await options.journal.get("record-operation:" + request.operationId);
681
+ if (frozen) {
682
+ requireValue(frozen.sourceBoardId === boardId && frozen.recordId === request.recordId && (frozen.requestRevision ?? frozen.recordRevision) === request.expectedRevision && (!frozen.placement || frozen.placement === (request.placement ?? "current-board")), "IDEMPOTENCY_CONFLICT");
683
+ const current = (await options.journal.get("record-runs:" + boardId))?.find((run) => run.id === frozen.id);
684
+ requireValue(current, "RECORD_EXECUTION_CHECKPOINT_MISSING");
685
+ return freezeOperation(options, request, current);
686
+ }
687
+ requireValue(record.kind === "decision" && record.status === "confirmed", "DECISION_NOT_CONFIRMED");
688
+ requireValue(record.revision === request.expectedRevision, "REVISION_CONFLICT");
689
+ const board = await options.service.read(boardId);
690
+ requireValue(board.automation, "AUTOMATION_BINDING_REQUIRED");
691
+ await validateResourceReferences(options.resources, record);
692
+ requireValue(record.externalRefs?.some((ref) => ref.system === options.resources.system), "REMOTE_PROJECT_REQUIRED");
693
+ const id = sha256HexSync(JSON.stringify([boardId, record.id, record.revision]));
694
+ const placement = request.placement ?? "current-board";
695
+ requireValue(["current-board", "new-board"].includes(placement), "INVALID_EXECUTION_PLACEMENT");
696
+ const saved = await options.journal.update("record-runs:" + boardId, (previous) => {
697
+ requireValue(!previous?.some((row) => row.recordId === record.id && row.state === "completed"), "DECISION_EXECUTION_COMPLETED");
698
+ const same = previous?.find((row) => row.id === id || row.recordId === record.id && decisionSignature(row.record) === decisionSignature(record));
699
+ if (same) {
700
+ requireValue(!same.placement || same.placement === placement, "DECISION_EXECUTION_PLACEMENT_CONFLICT");
701
+ return previous;
702
+ }
703
+ requireValue(!previous?.some((row) => row.recordId === record.id && !["completed", "failed"].includes(row.state)), "DECISION_EXECUTION_UNSETTLED");
704
+ requireValue((previous?.length ?? 0) < 100, "RECORD_EXECUTION_LIMIT");
705
+ return [...previous ?? [], newRun(board, record, request, id, options.resources.system)];
706
+ });
707
+ await options.journal.update("record-context:" + id, (old) => old ?? { sourceBoardId: boardId, recordId: record.id, recordRevision: record.revision, runId: id });
708
+ const original = saved.find((row) => row.id === id || row.recordId === record.id && decisionSignature(row.record) === decisionSignature(record));
709
+ return freezeOperation(options, request, original);
710
+ }
711
+ async function freezeOperation(options, request, run) {
712
+ await options.journal.update("record-operation:" + request.operationId, (previous) => {
713
+ requireValue(!previous || previous.id === run.id && (previous.requestRevision ?? previous.recordRevision) === request.expectedRevision && (!previous.placement || previous.placement === (request.placement ?? "current-board")), "IDEMPOTENCY_CONFLICT");
714
+ return {
715
+ schema: 2,
716
+ id: run.id,
717
+ sourceBoardId: run.sourceBoardId,
718
+ recordId: run.recordId,
719
+ recordRevision: run.recordRevision,
720
+ requestRevision: request.expectedRevision,
721
+ ...run.placement ? { placement: run.placement } : {}
722
+ };
723
+ });
724
+ return run;
725
+ }
726
+ async function advance(options, run) {
727
+ const started = Date.now();
728
+ try {
729
+ const control = await options.journal.get(recordControlKey(run.sourceBoardId, run.recordId));
730
+ if (control && !control.settled) {
731
+ const state2 = await advanceRecordControl(options.service, options.journal, run, control);
732
+ const updated2 = await save(options, { ...run, state: state2, updatedAt: Date.now() });
733
+ if (state2 !== run.state) options.log({ operationId: control.operationId, boardId: run.sourceBoardId, stage: "record.control." + control.action, result: state2 === "unknown" || state2 === "blocked" ? "failure" : "success", code: updated2.code, durationMs: Date.now() - started });
734
+ if (control.action === "resume" && state2 === "running") {
735
+ const runs = await options.journal.get("record-runs:" + run.sourceBoardId);
736
+ if (runs?.filter((row) => row.recordId === run.recordId).every((row) => ["running", "completed", "failed", "cancelled"].includes(row.state))) await options.journal.update(recordControlKey(run.sourceBoardId, run.recordId), (old) => ({ ...old, settled: true }));
737
+ }
738
+ if (state2 === "cancelled") {
739
+ const runs = await options.journal.get("record-runs:" + run.sourceBoardId);
740
+ if (runs?.filter((row) => row.recordId === run.recordId).every((row) => ["completed", "failed", "cancelled"].includes(row.state))) await finishDecisionCancellation(options, run.sourceBoardId, run.recordId, control.operationId);
741
+ }
742
+ return view(updated2);
743
+ }
744
+ await validateResourceReferences(options.resources, run.record);
745
+ await materialize(options, run);
746
+ const board = await options.service.read(run.boardId);
747
+ const items = run.itemIds ? await Promise.all(run.itemIds.map(async (id) => board.items.find((item) => item.id === id) ?? (await options.service.item(run.boardId, id)).item)) : board.items;
748
+ const state = runState(items);
749
+ if (state === "completed" && run.rootItemId && !run.itemIds?.includes(run.rootItemId) && board.items.find((item) => item.id === run.rootItemId)?.status !== "completed") {
750
+ await once(options, run, "close-root", { type: "close-plan", itemId: run.rootItemId, resultRef: "kanban-run:" + run.id });
751
+ }
752
+ const code = state === "failed" ? "DECISION_TASK_CANCELLED" : void 0;
753
+ if (run.state === state && run.code === code) return view(run);
754
+ const updated = await save(options, { ...run, state, code, updatedAt: Date.now() });
755
+ options.log({ operationId: run.id, boardId: run.sourceBoardId, stage: "record.run", result: state === "failed" ? "failure" : "success", code: updated.code, durationMs: Date.now() - started });
756
+ return view(updated);
757
+ } catch (error) {
758
+ const code = safeCode(error, "RECORD_EXECUTION_FAILED");
759
+ if (code === "SERVICE_STOPPING") throw error;
760
+ await save(options, { ...run, state: "blocked", code, updatedAt: Date.now() });
761
+ options.log({ operationId: run.id, boardId: run.sourceBoardId, stage: "record.run", result: "failure", code, durationMs: Date.now() - started });
762
+ throw error;
763
+ }
764
+ }
765
+ function decisionSignature(record) {
766
+ return sha256HexSync(JSON.stringify([record.title, record.content, record.rationale, record.externalRefs]));
767
+ }
768
+ function newRun(board, record, request, id, resourceSystem) {
769
+ const placement = request.placement ?? "current-board";
770
+ requireValue(["current-board", "new-board"].includes(placement), "INVALID_EXECUTION_PLACEMENT");
771
+ requireValue(placement === "new-board" || board.automation?.enabled, "AUTOMATION_PAUSED");
772
+ const refs = record.externalRefs.filter((ref) => ref.system === resourceSystem);
773
+ const types = board.workTypes ?? DEFAULT_KANBAN_WORK_TYPES;
774
+ const task = types.find((type) => type.id === (board.defaultWorkType ?? "task") && type.executable) ?? types.find((type) => type.executable);
775
+ const parent = types.filter((type) => !type.executable && type.rank < (task?.rank ?? 0)).sort((a, b) => b.rank - a.rank)[0];
776
+ requireValue(task && (refs.length === 1 || parent), "DECISION_WORK_TYPES_REQUIRED");
777
+ const itemIds = refs.map((_, index) => "work-" + id + "-" + index);
778
+ return {
779
+ id,
780
+ sourceBoardId: board.id,
781
+ recordId: record.id,
782
+ recordRevision: record.revision,
783
+ boardId: placement === "new-board" ? "run-" + id : board.id,
784
+ placement,
785
+ taskKind: task.id,
786
+ parentKind: parent?.id,
787
+ rootItemId: refs.length === 1 ? itemIds[0] : "feature-" + id,
788
+ itemIds,
789
+ state: "preparing",
790
+ createdAt: Date.now(),
791
+ updatedAt: Date.now(),
792
+ record,
793
+ automation: structuredClone(board.automation)
794
+ };
795
+ }
796
+ function runState(items) {
797
+ if (items.some((item) => item.status === "cancelled")) return "failed";
798
+ if (items.length && items.every((item) => item.status === "completed")) return "completed";
799
+ if (items.some((item) => item.blocked)) return "blocked";
800
+ return items.length && items.every((item) => ["completed", "review"].includes(item.status)) ? "review" : "running";
801
+ }
802
+ async function materialize(options, run) {
803
+ if (run.boardId !== run.sourceBoardId) {
804
+ const actor = await options.service.currentPrincipal(), source = await options.service.read(run.sourceBoardId);
805
+ await options.service.create({
806
+ id: run.boardId,
807
+ title: run.record.title,
808
+ scope: { kind: "collection", namespace: "decision-run", id: run.id },
809
+ members: [{ ...actor, responsibility: "Review decision execution", roles: ["coordinator", "reviewer", "worker"] }],
810
+ ...run.placement && source.workTypes ? { workTypes: source.workTypes, defaultWorkType: source.defaultWorkType } : {},
811
+ policy: { requireEstimate: false, review: "independent" }
812
+ });
813
+ }
814
+ const refs = run.record.externalRefs.filter((ref) => ref.system === options.resources.system);
815
+ const group = !!run.rootItemId && !run.itemIds?.includes(run.rootItemId);
816
+ if (group) await once(options, run, "root", {
817
+ type: "create-item",
818
+ item: {
819
+ id: run.rootItemId,
820
+ title: run.record.title,
821
+ responsibility: run.record.content,
822
+ acceptance: run.record.rationale ?? run.record.content
823
+ },
824
+ planning: { kind: run.parentKind, priority: 2, dependencies: [], references: [] }
825
+ });
826
+ for (const [index, reference] of refs.entries()) {
827
+ await once(options, run, "item-" + index, {
828
+ type: "create-item",
829
+ item: {
830
+ id: run.itemIds?.[index] ?? "work-" + index,
831
+ title: run.record.title,
832
+ responsibility: run.record.content,
833
+ acceptance: run.record.rationale ?? run.record.content,
834
+ externalRefs: [reference, { system: options.resources.decisionSystem ?? "kanban-decision", id: run.id }]
835
+ },
836
+ ...run.taskKind ? { planning: { kind: run.taskKind, priority: 2, dependencies: [], references: [], ...group ? { parentId: run.rootItemId } : {} } } : {}
837
+ });
838
+ }
839
+ if (run.boardId !== run.sourceBoardId) await once(options, run, "automation", { type: "configure-automation", settings: { ...run.automation, enabled: true, autoAccept: false } });
840
+ if (run.rootItemId && run.boardId === run.sourceBoardId) await once(options, run, "link-root", { type: "link-record", recordId: run.recordId, itemId: run.rootItemId });
841
+ }
842
+ async function once(options, run, stage, command) {
843
+ const key = "record-stage:" + run.id + ":" + stage;
844
+ await journalCommand(options.journal, options.service, run.boardId, key, command);
845
+ }
846
+ async function save(options, run) {
847
+ const rows = await options.journal.update("record-runs:" + run.sourceBoardId, (previous) => {
848
+ const current = previous ?? [];
849
+ requireValue(current.some((row) => row.id === run.id), "RECORD_EXECUTION_CHECKPOINT_MISSING");
850
+ return current.map((row) => row.id !== run.id || row.state === run.state && row.code === run.code ? row : run);
851
+ });
852
+ return rows.find((row) => row.id === run.id);
853
+ }
854
+ function view(run) {
855
+ const { id, recordId, recordRevision, boardId, rootItemId, itemIds, state, createdAt, updatedAt, code } = run;
856
+ return { id, recordId, recordRevision, boardId, ...rootItemId ? { rootItemId, itemIds } : {}, state, createdAt, updatedAt, ...code ? { code } : {} };
857
+ }
858
+ async function cancelDecision(options, boardId, recordId, operationId) {
859
+ const record = await options.service.record(boardId, recordId);
860
+ if (record.status !== "cancelled") await journalCommand(options.journal, options.service, boardId, "record-cancel:" + JSON.stringify([boardId, recordId, operationId]), { type: "cancel-record", recordId });
861
+ await options.journal.update(recordControlKey(boardId, recordId), (old) => ({ ...old, settled: true }));
862
+ }
863
+ async function finishDecisionCancellation(options, boardId, recordId, operationId) {
864
+ const runs = (await options.journal.get("record-runs:" + boardId) ?? []).filter((run) => run.recordId === recordId);
865
+ if (runs.some((run) => !["completed", "failed", "cancelled"].includes(run.state))) return;
866
+ const record = await options.service.record(boardId, recordId);
867
+ const control = await options.journal.get(recordControlKey(boardId, recordId));
868
+ requireValue(control, "RECORD_CONTROL_ACTIVE");
869
+ const state = await advanceRecordControl(options.service, options.journal, { id: operationId, sourceBoardId: boardId, boardId, recordId, recordRevision: record.revision, itemIds: record.itemIds, state: "cancelling", createdAt: 0, updatedAt: 0 }, control);
870
+ if (state === "cancelled") await cancelDecision(options, boardId, recordId, operationId);
871
+ }
872
+
873
+ // src/runtime/analysis-aggregation.ts
874
+ async function completeCompositePlans(service, journal, boardId, serviceFor) {
875
+ const workflow = await service.workflow(boardId), boardSnapshot = await service.read(boardId);
876
+ const roots = new Set(boardSnapshot.items.filter((item) => item.planning?.execution === "children").map((item) => item.id));
877
+ for (const id of roots) {
878
+ const board = await service.read(boardId), parent = board.items.find((item) => item.id === id);
879
+ if (!board.automation || !parent || parent.control?.action === "cancel" || parent.owner || parent.blocked || parent.planning?.execution !== "children" || !["backlog", "open"].includes(parent.status)) continue;
880
+ if (!await childrenCompleted(service, boardId, id)) continue;
881
+ await journalCommand(
882
+ journal,
883
+ serviceFor(board.automation.coordinator),
884
+ boardId,
885
+ "aggregate-complete:" + id,
886
+ { type: "close-plan", itemId: id, resultRef: "kanban:plans:" + JSON.stringify(Object.values(workflow.plans).filter((plan) => plan.rootItemId === id).map((plan) => plan.id)) }
887
+ );
888
+ }
889
+ }
890
+ async function childrenCompleted(service, boardId, parentId) {
891
+ let cursor, count = 0;
892
+ do {
893
+ const page = await service.query(boardId, { view: "all", parentId, limit: 200, cursor });
894
+ if (page.items.some((item) => item.status !== "completed")) return false;
895
+ count += page.items.length;
896
+ cursor = page.nextCursor;
897
+ } while (cursor);
898
+ return count > 0;
899
+ }
900
+
901
+ // src/runtime/analysis-decomposition.ts
902
+ function validateSubtasks(value) {
903
+ if (value === void 0) return;
904
+ requireValue(Array.isArray(value) && value.length >= 2 && value.length <= 16, "AUTOMATION_SUBTASKS_INVALID");
905
+ const tasks = value, keys = new Set(tasks.map((task) => task?.key));
906
+ requireValue(keys.size === tasks.length, "AUTOMATION_SUBTASKS_INVALID");
907
+ for (const task of tasks) {
908
+ requireValue(task && typeof task.key === "string" && /^[a-zA-Z0-9_-]{1,64}$/.test(task.key) && [task.title, task.responsibility, task.acceptance].every((text) => typeof text === "string" && text.trim().length > 0 && text.length <= 8e3), "AUTOMATION_SUBTASKS_INVALID");
909
+ requireValue(Array.isArray(task.dependsOn) && task.dependsOn.length <= 16 && new Set(task.dependsOn).size === task.dependsOn.length && task.dependsOn.every((key) => keys.has(key) && key !== task.key), "AUTOMATION_SUBTASK_DEPENDENCY_INVALID");
910
+ if (task.estimate) requireValue(["low", "medium", "high"].includes(task.estimate.confidence) && [task.estimate.complexity, task.estimate.workUnits, task.estimate.durationMs].every((number) => Number.isFinite(number) && number > 0), "AUTOMATION_ESTIMATE_INVALID");
911
+ }
912
+ const pending2 = [...tasks], done2 = /* @__PURE__ */ new Set();
913
+ while (pending2.length) {
914
+ const index = pending2.findIndex((task) => task.dependsOn.every((key) => done2.has(key)));
915
+ requireValue(index >= 0, "DEPENDENCY_CYCLE");
916
+ done2.add(pending2.splice(index, 1)[0].key);
917
+ }
918
+ return tasks;
919
+ }
920
+ async function adoptDecomposition(journal, board, parent, input, service, inherited = []) {
921
+ const types = board.workTypes ?? DEFAULT_KANBAN_WORK_TYPES, parentRank = types.find((type2) => type2.id === (parent.planning?.kind ?? board.defaultWorkType ?? "task"))?.rank;
922
+ const type = types.filter((type2) => type2.executable && parentRank !== void 0 && type2.rank > parentRank).sort((a, b) => a.rank - b.rank)[0];
923
+ requireValue(type, "AUTOMATION_SUBTASK_TYPE_UNAVAILABLE");
924
+ const ids = new Map(input.subtasks.map((task) => [task.key, "sub-" + sha256HexSync(JSON.stringify([input.id, task.key])).slice(0, 40)]));
925
+ const definitions = input.subtasks.map((task) => ({
926
+ id: ids.get(task.key),
927
+ workType: type.id,
928
+ title: task.title,
929
+ responsibility: task.responsibility,
930
+ acceptance: task.acceptance,
931
+ assignee: board.automation.worker,
932
+ externalRefs: inherited,
933
+ dependsOn: [.../* @__PURE__ */ new Set([...input.dependencies, ...task.dependsOn.map((key) => ids.get(key))])],
934
+ outputs: ["result"]
935
+ }));
936
+ for (const [index, definition] of definitions.entries()) {
937
+ const estimate = input.subtasks[index].estimate;
938
+ requireValue(board.policy.requireEstimate === false || estimate, "ESTIMATE_REQUIRED");
939
+ if (estimate) await journal.update("decomposition-estimate:" + JSON.stringify([board.id, definition.id]), (old) => old ?? estimate);
940
+ await journal.update("decomposition-status:" + JSON.stringify([board.id, definition.id]), (old) => old ?? { itemId: definition.id, state: "ready", reason: input.reason, dependencies: definition.dependsOn });
941
+ }
942
+ await service.execute(board.id, { operationId: "analysis-plan:" + input.id, expectedRevision: board.revision, command: { type: "publish-plan", plan: {
943
+ schema: "kanban.plan/1",
944
+ id: input.id,
945
+ revision: 1,
946
+ sealed: true,
947
+ rootItemId: parent.id,
948
+ items: definitions
949
+ } } });
950
+ }
951
+
952
+ // src/runtime/analysis.ts
953
+ var keyFor = (board, item) => "analysis-current:" + JSON.stringify([board, item]);
954
+ async function analysisStatus(journal, board, item) {
955
+ const key = await journal.get(keyFor(board, item));
956
+ return key ? (await journal.get(key))?.status ?? null : await journal.get("decomposition-status:" + JSON.stringify([board, item])) ?? null;
957
+ }
958
+ function analysisSignature(board) {
959
+ return sha256HexSync(JSON.stringify([board.automation, board.items.map((item) => [item.id, item.title, item.responsibility, item.acceptance, item.planning])]));
960
+ }
961
+ async function analyzeItem(options, board, item, target, service) {
962
+ const started = Date.now();
963
+ if (!options.active()) return;
964
+ const loaded = await loadJob(options, board, item, target);
965
+ if (!loaded || !options.active()) return;
966
+ const { key } = loaded;
967
+ let { job } = loaded;
968
+ if (job.phase === "prepared") job = await submitAnalysis(options, key, job);
969
+ if (!options.active()) return;
970
+ if (job.phase === "submitting") job = await observeAnalysis(options, key, job);
971
+ job = await finalizeAnalysis(options, board.id, item, key, job);
972
+ if (job.result?.ready && job.status.state === "ready" && !board.executionControl?.paused && board.automation?.enabled && options.active()) {
973
+ try {
974
+ await adoptAnalysis(options.journal, board, item, job, service, options.inheritReferences?.(item.externalRefs ?? []) ?? []);
975
+ } catch (error) {
976
+ await saveFailure(options, key, job, safeCode(error, "AUTOMATION_PLAN_REJECTED"));
977
+ }
978
+ }
979
+ if (!options.active()) return;
980
+ const status = (await options.journal.get(key))?.status;
981
+ if (status) {
982
+ const currentBoard = await service.read(board.id), currentItem = currentBoard.items.find((i) => i.id === item.id);
983
+ if (currentItem.control?.action === "cancel" || ["completed", "cancelled"].includes(currentItem.status)) return;
984
+ if (JSON.stringify(currentItem.analysis) !== JSON.stringify(status)) await service.execute(board.id, { operationId: "analysis-status:" + job.id + ":" + sha256HexSync(JSON.stringify(status)), expectedRevision: currentBoard.revision, command: { type: "report-analysis", itemId: item.id, operationId: job.id, status } });
985
+ }
986
+ options.log({ operationId: job.id, stage: "automation.analysis", result: status?.state === "failed" || status?.state === "unknown" ? "failure" : "success", code: status?.code, durationMs: Date.now() - started });
987
+ }
988
+ async function finalizeAnalysis(options, boardId, item, key, job) {
989
+ if (!options.active() || job.phase !== "settled" || !options.onSettled) return job;
990
+ try {
991
+ await options.onSettled({
992
+ boardId,
993
+ id: job.id,
994
+ item,
995
+ request: job.input,
996
+ outcome: job.terminalOutcome ?? (job.status.state === "failed" ? "failed" : "succeeded"),
997
+ code: job.status.code
998
+ });
999
+ return job;
1000
+ } catch (error) {
1001
+ return options.active() ? saveFailure(options, key, job, safeCode(error, "AUTOMATION_ANALYSIS_FINALIZATION_FAILED")) : job;
1002
+ }
1003
+ }
1004
+ async function loadJob(options, board, item, target) {
1005
+ requireValue(board.items.length <= 200, "AUTOMATION_CONTEXT_LIMIT");
1006
+ const signature = analysisSignature(board), currentKey = await options.journal.get(keyFor(board.id, item.id));
1007
+ const currentJob = currentKey ? await options.journal.get(currentKey) : void 0;
1008
+ if (!board.automation?.enabled && currentJob?.phase !== "submitting") return;
1009
+ const key = currentJob?.phase === "submitting" ? currentKey : "analysis:" + sha256HexSync(JSON.stringify([board.id, item.id, item.launch?.requestedAt, item.launch?.analysisEpoch, signature]));
1010
+ let job = await options.journal.get(key);
1011
+ if (!job) {
1012
+ requireValue(target, "AUTOMATION_TARGET_CHANGED");
1013
+ const id = await options.journal.update("analysis-identity:" + key, (old) => old ?? randomUUID());
1014
+ const input = await options.port.prepare({ id, board, item, target });
1015
+ const candidate = { id, signature, itemRevision: item.revision, phase: "prepared", status: { itemId: item.id, state: "analyzing" }, input };
1016
+ job = await options.journal.update(key, (previous) => previous ?? candidate);
1017
+ }
1018
+ if (!options.active()) return;
1019
+ if (currentKey !== key) await options.journal.update("analysis-history:" + JSON.stringify([board.id, item.id]), (old) => ({ ...old, keys: [.../* @__PURE__ */ new Set([...old?.keys ?? [], ...currentKey ? [currentKey] : [], key])] }));
1020
+ await options.journal.update(keyFor(board.id, item.id), () => key);
1021
+ if (!options.active()) return;
1022
+ return { key, job };
1023
+ }
1024
+ async function submitAnalysis(options, key, job) {
1025
+ try {
1026
+ await options.port.prepareExecution(job.id, job.input);
1027
+ } catch (error) {
1028
+ return options.active() ? saveFailure(options, key, job, safeCode(error, "AUTOMATION_ANALYSIS_PREPARE_FAILED")) : job;
1029
+ }
1030
+ if (!options.active()) return job;
1031
+ const claimed = await options.journal.claim(key), latest = await options.journal.get(key);
1032
+ if (!claimed || !options.active()) return latest;
1033
+ try {
1034
+ await options.port.submit(job.input);
1035
+ } catch {
1036
+ return options.active() ? options.journal.update(key, (current) => current.phase === "submitting" ? { ...current, status: { ...job.status, state: "unknown", code: "AUTOMATION_ADMISSION_UNKNOWN" } } : current) : latest;
1037
+ }
1038
+ return latest;
1039
+ }
1040
+ async function observeAnalysis(options, key, job) {
1041
+ const observation = await options.port.inspect(job.id, job.input);
1042
+ if (!options.active() || observation.state === "running") return job;
1043
+ if (observation.state === "unknown") return options.journal.update(key, (current) => current.phase === "submitting" ? { ...current, status: { ...job.status, state: "unknown", code: observation.code ?? "AUTOMATION_ADMISSION_UNKNOWN" } } : current);
1044
+ if (observation.state === "failed" || observation.state === "cancelled") return saveFailure(options, key, job, observation.code ?? "AUTOMATION_ANALYSIS_FAILED", observation.state);
1045
+ try {
1046
+ requireValue(observation.output !== void 0, "AUTOMATION_ANALYSIS_INVALID");
1047
+ const analysis = parseAnalysis(observation.output);
1048
+ return options.journal.update(key, (current) => ({ ...current, phase: "settled", result: analysis, status: { itemId: job.status.itemId, state: analysis.ready ? "ready" : "clarification", reason: analysis.reason, dependencies: analysis.dependencies } }));
1049
+ } catch (error) {
1050
+ return saveFailure(options, key, job, safeCode(error, "AUTOMATION_ANALYSIS_INVALID"));
1051
+ }
1052
+ }
1053
+ async function saveFailure(options, key, job, code, terminalOutcome = "failed") {
1054
+ return options.journal.update(key, (current) => ({ ...current, phase: "settled", terminalOutcome, status: { itemId: job.status.itemId, state: "failed", code } }));
1055
+ }
1056
+ function parseAnalysis(output) {
1057
+ const result = output;
1058
+ requireValue(result && typeof result.ready === "boolean" && typeof result.reason === "string" && result.reason.length > 0 && result.reason.length <= 8e3, "AUTOMATION_ANALYSIS_INVALID");
1059
+ requireValue(Array.isArray(result.dependencies) && result.dependencies.length <= 100 && result.dependencies.every((id) => typeof id === "string" && id.length > 0 && id.length <= 256), "AUTOMATION_DEPENDENCIES_INVALID");
1060
+ if (result.estimate) requireValue(["low", "medium", "high"].includes(result.estimate.confidence) && [result.estimate.complexity, result.estimate.workUnits, result.estimate.durationMs].every((value) => Number.isFinite(value) && value > 0), "AUTOMATION_ESTIMATE_INVALID");
1061
+ const subtasks = validateSubtasks(result.subtasks);
1062
+ return { ready: result.ready, reason: result.reason, dependencies: [...new Set(result.dependencies)], ...subtasks ? { subtasks } : {}, ...result.estimate ? { estimate: result.estimate } : {} };
1063
+ }
1064
+ async function adoptAnalysis(journal, snapshot, item, job, service, inherited) {
1065
+ if ((await service.workflow(snapshot.id)).plans[job.id]) return;
1066
+ const board = await service.read(snapshot.id), current = board.items.find((candidate) => candidate.id === item.id);
1067
+ requireValue(board.automation?.enabled && current && analysisSignature(board) === job.signature, "AUTOMATION_ANALYSIS_STALE");
1068
+ requireValue(board.policy.requireEstimate === false || job.result?.estimate, "ESTIMATE_REQUIRED");
1069
+ const dependencies = [.../* @__PURE__ */ new Set([...item.planning?.dependencies ?? [], ...job.result.dependencies])];
1070
+ if (job.result?.subtasks?.length) return adoptDecomposition(journal, board, item, { id: job.id, reason: job.result.reason, dependencies, subtasks: job.result.subtasks }, service, inherited);
1071
+ await service.execute(board.id, { operationId: "analysis-plan:" + job.id, expectedRevision: board.revision, command: { type: "publish-plan", plan: {
1072
+ schema: "kanban.plan/1",
1073
+ id: job.id,
1074
+ revision: 1,
1075
+ sealed: true,
1076
+ items: [{
1077
+ id: item.id,
1078
+ title: item.title,
1079
+ responsibility: item.responsibility,
1080
+ acceptance: item.acceptance,
1081
+ expectedRevision: current.revision,
1082
+ assignee: board.automation.worker,
1083
+ dependsOn: dependencies,
1084
+ outputs: ["result"]
1085
+ }]
1086
+ } } });
1087
+ }
1088
+
1089
+ // src/runtime/automation.ts
1090
+ function createKanbanAutomation(options) {
1091
+ let stopped = false, pending2;
1092
+ const bridges = /* @__PURE__ */ new Map();
1093
+ const scan = () => stopped ? Promise.resolve() : pending2 ??= scanBoards(options, bridges, () => !stopped).finally(() => {
1094
+ pending2 = void 0;
1095
+ });
1096
+ return {
1097
+ scan,
1098
+ async quiesce() {
1099
+ stopped = true;
1100
+ await Promise.allSettled([...bridges.values()].map((bridge) => bridge.quiesce()));
1101
+ let timer;
1102
+ try {
1103
+ await Promise.race([pending2?.catch(() => {
1104
+ }), new Promise((resolve) => {
1105
+ timer = setTimeout(resolve, 5e3);
1106
+ })]);
1107
+ } finally {
1108
+ if (timer) clearTimeout(timer);
1109
+ await options.journal.drain();
1110
+ }
1111
+ },
1112
+ automationCatalog: () => {
1113
+ requireValue(!stopped, "SERVICE_STOPPING");
1114
+ return options.catalog();
1115
+ },
1116
+ analysisStatus: async (board, item) => (await options.service.item(board, item)).item.analysis ?? analysisStatus(options.journal, board, item),
1117
+ verifyOutputs: (outputs) => options.journal.verify(outputs)
1118
+ };
1119
+ }
1120
+ async function scanBoards(options, bridges, active) {
1121
+ const errors = [];
1122
+ for (const entry of await options.service.catalog()) {
1123
+ if (!active()) return;
1124
+ const started = Date.now();
1125
+ try {
1126
+ const board = await options.service.read(entry.id);
1127
+ if (!active()) return;
1128
+ if (!board.automation) {
1129
+ await options.controls?.(board, options.service);
1130
+ continue;
1131
+ }
1132
+ const service = options.serviceFor(board.automation.coordinator);
1133
+ let bridge = bridges.get(board.id);
1134
+ if (!bridge) {
1135
+ bridge = await createBridge(options, board.id);
1136
+ bridges.set(board.id, bridge);
1137
+ }
1138
+ if (!active()) {
1139
+ await bridge.quiesce();
1140
+ return;
1141
+ }
1142
+ await options.controls?.(board, service);
1143
+ if (!active()) return;
1144
+ await bridge.scan();
1145
+ if (!active()) return;
1146
+ await options.drain?.(await service.read(board.id), service);
1147
+ await planNext(options, await service.read(board.id), active);
1148
+ if (active()) await bridge.scan();
1149
+ if (active()) await completeCompositePlans(options.service, options.journal, board.id, options.serviceFor);
1150
+ options.log({ operationId: board.id, boardId: board.id, stage: "automation.scan", result: "success", durationMs: Date.now() - started });
1151
+ } catch (error) {
1152
+ errors.push(error);
1153
+ options.log({ operationId: entry.id, boardId: entry.id, stage: "automation.scan", result: "failure", code: safeCode(error, "AUTOMATION_SCAN_FAILED"), durationMs: Date.now() - started });
1154
+ }
1155
+ }
1156
+ if (errors.length) throw new AggregateError(errors, "KANBAN_AUTOMATION_FAILED");
1157
+ }
1158
+ async function planNext(options, board, active) {
1159
+ if (!options.analyze) return;
1160
+ const workflow = await options.service.workflow(board.id), planned = new Set(Object.values(workflow.plans).flatMap((plan) => plan.itemIds));
1161
+ const candidates = board.items.filter((item) => !planned.has(item.id) && ["backlog", "open"].includes(item.status) && !item.owner && !item.blocked && boardAllowsStart(board, item) && !["failed", "clarification"].includes(item.analysis?.state ?? "") && executableWorkItem(board, item) && (!item.control || item.control.action === "resume" && item.control.state === "resumed") && (item.launch?.admittedAt !== void 0 || taskDue(item, Date.now())));
1162
+ for (const item of candidates) {
1163
+ if (!active()) return;
1164
+ if (!await dependenciesComplete(options.service, board, item)) continue;
1165
+ const service = options.serviceFor(board.automation.coordinator);
1166
+ if (item.launch?.admittedAt === void 0) {
1167
+ if (!await admit(options, board, item, service)) return;
1168
+ board = await service.read(board.id);
1169
+ Object.assign(item, board.items.find((row) => row.id === item.id));
1170
+ }
1171
+ try {
1172
+ await options.analyze(board, item, service, active);
1173
+ } catch (error) {
1174
+ const latest = await service.read(board.id), code = safeCode(error, "AUTOMATION_ANALYSIS_PREPARATION_FAILED");
1175
+ await service.execute(board.id, { operationId: "analysis-prepare-failed:" + item.id + ":" + item.revision, expectedRevision: latest.revision, command: { type: "report-analysis", itemId: item.id, operationId: item.id, status: { itemId: item.id, state: "failed", code } } });
1176
+ options.log({ operationId: item.id, boardId: board.id, stage: "automation.analysis.prepare", result: "failure", code, durationMs: 0 });
1177
+ }
1178
+ const status = await analysisStatus(options.journal, board.id, item.id);
1179
+ if (status && ["analyzing", "unknown", "ready"].includes(status.state)) return;
1180
+ }
1181
+ }
1182
+ async function admit(_options, board, item, service) {
1183
+ const latest = await service.read(board.id);
1184
+ try {
1185
+ await service.execute(board.id, { operationId: "admit:" + item.id + ":" + item.revision, expectedRevision: latest.revision, command: { type: "admit-item", itemId: item.id, trigger: item.launch?.requestedAt !== void 0 ? "manual" : "automatic" } });
1186
+ return true;
1187
+ } catch (error) {
1188
+ if (["REVISION_CONFLICT", "TASK_DEFINITION_FROZEN", "EDIT_LEASE_ACTIVE"].includes(safeCode(error, ""))) return false;
1189
+ throw error;
1190
+ }
1191
+ }
1192
+ async function createBridge(options, boardId) {
1193
+ const board = await options.service.read(boardId), service = options.serviceFor(board.automation.coordinator);
1194
+ const executor = await options.executor({ boardId, service, dependencies: (item) => dependencyResults(options, boardId, item) });
1195
+ return createKanbanExecutionBridge({
1196
+ fs: options.fs,
1197
+ directory: options.directory + "/" + encodeURIComponent(boardId),
1198
+ boardId,
1199
+ service,
1200
+ worker: async (actor) => options.serviceFor(actor),
1201
+ executor,
1202
+ executionReference: options.executionReference,
1203
+ autoDispatch: async () => (await options.service.read(boardId)).automation?.enabled === true,
1204
+ estimate: (item) => estimateFor(options.journal, boardId, item),
1205
+ authorize: async (stage) => {
1206
+ const latest = await options.service.read(boardId);
1207
+ if (stage !== "inspect") {
1208
+ requireValue(latest.automation?.enabled, "AUTOMATION_PAUSED");
1209
+ requireValue(!latest.executionControl?.paused, "BOARD_EXECUTION_PAUSED");
1210
+ }
1211
+ },
1212
+ log: (event) => options.log({ ...event, boardId })
1213
+ });
1214
+ }
1215
+ async function estimateFor(journal, boardId, item) {
1216
+ const decomposed = await journal.get("decomposition-estimate:" + JSON.stringify([boardId, item.id]));
1217
+ if (decomposed) return decomposed;
1218
+ const key = await journal.get("analysis-current:" + JSON.stringify([boardId, item.id]));
1219
+ return key ? (await journal.get(key))?.result?.estimate : void 0;
1220
+ }
1221
+ async function dependencyResults(options, boardId, item) {
1222
+ const workflow = await options.service.workflow(boardId), results = [];
1223
+ for (const itemId of item.planning?.dependencies ?? []) {
1224
+ const dispatch = Object.values(workflow.dispatches).reverse().find((dispatch2) => dispatch2.itemId === itemId && dispatch2.outcome === "accepted");
1225
+ const outputs = [];
1226
+ for (const output of dispatch?.outputs ?? []) {
1227
+ requireValue(await options.journal.verify(dispatch?.outputs ?? [], dispatch?.id), "DEPENDENCY_OUTPUT_INVALID");
1228
+ outputs.push({ reference: output, value: JSON.parse(await options.journal.get("artifact:" + output.sha256)) });
1229
+ }
1230
+ results.push({ itemId, outputs });
1231
+ }
1232
+ requireValue(new TextEncoder().encode(JSON.stringify(results)).byteLength <= 128 * 1024, "DEPENDENCY_CONTEXT_TOO_LARGE");
1233
+ return results;
1234
+ }
1235
+ async function dependenciesComplete(service, board, item) {
1236
+ for (const id of item.planning?.dependencies ?? []) {
1237
+ const dependency = board.items.find((i) => i.id === id) ?? (await service.item(board.id, id)).item;
1238
+ if (dependency.status !== "completed") return false;
1239
+ }
1240
+ return true;
1241
+ }
1242
+
1243
+ // src/runtime/analysis-history.ts
1244
+ async function analysisHistory(journal, boardId, itemId) {
1245
+ const key = "analysis-history:" + JSON.stringify([boardId, itemId]);
1246
+ let saved = await journal.get(key);
1247
+ if (!saved?.indexed) {
1248
+ const rows = await journal.find("analysis:", (job, entryKey) => job.status?.itemId === itemId && entryKey === "analysis:" + sha256HexSync(JSON.stringify([boardId, itemId, job.itemRevision, job.signature])));
1249
+ saved = await journal.update(key, (old) => ({ keys: [.../* @__PURE__ */ new Set([...old?.keys ?? [], ...rows.map((row) => row.key)])], indexed: true }));
1250
+ }
1251
+ const jobs = await journal.getMany(saved.keys);
1252
+ return jobs.filter((job) => !!job);
1253
+ }
1254
+
1255
+ // src/runtime/logging.ts
1256
+ function runtimeLogger(journal) {
1257
+ const emit = (_message, event) => journal.diagnostic(event);
1258
+ return { info: emit, warn: emit };
1259
+ }
1260
+
1261
+ // src/runtime/board-deletion-inspection.ts
1262
+ var settled = /* @__PURE__ */ new Set(["idle", "completed", "failed", "cancelled"]);
1263
+ async function boardDeletionBlockers({ service, journal }, boardId, items) {
1264
+ const board = await service.read(boardId), workflow = await service.workflow(boardId), rows = [];
1265
+ for (const item of board.items) if (!["completed", "cancelled"].includes(item.status) && (item.owner || item.transfer || item.status === "claimed")) rows.push({ code: "BOARD_EXECUTION_ACTIVE", itemId: item.id });
1266
+ for (const item of board.items) if (item.control?.action === "cancel" && item.control.state !== "cancelled" && !["completed", "cancelled"].includes(item.status)) rows.push({ code: "BOARD_CONTROL_UNSETTLED", itemId: item.id });
1267
+ for (const dispatch of Object.values(workflow.dispatches)) if (["prepared", "running", "unknown", "succeeded"].includes(dispatch.outcome)) rows.push({ code: "BOARD_EXECUTION_UNSETTLED", itemId: dispatch.itemId });
1268
+ const reviews = await journal.findForBoard("review-operation:", boardId, (job) => job.boardId === boardId && job.phase !== "settled");
1269
+ rows.push(...reviews.map(({ value }) => ({ code: "BOARD_MAINTENANCE_ACTIVE", itemId: value.itemId })));
1270
+ const runs = await journal.findForBoard("record-runs:", boardId, (runs2) => runs2.some((run) => (run.sourceBoardId === boardId || run.boardId === boardId) && !["completed", "failed", "cancelled"].includes(run.state)));
1271
+ if (runs.length) rows.push({ code: "BOARD_DECISION_UNSETTLED" });
1272
+ rows.push(...await pendingCollaboration(journal, boardId));
1273
+ rows.push(...(await inspectItems(items ?? await deletionItems(service, boardId), (item) => itemBlockers(service, journal, boardId, item.id))).flat());
1274
+ return [...new Map(rows.map((row) => [JSON.stringify([row.code, row.itemId, row.sessionId]), row])).values()];
1275
+ }
1276
+ async function deletionPreview(options, boardId) {
1277
+ const { service } = options;
1278
+ const board = await service.read(boardId), actor = await service.currentPrincipal();
1279
+ requireValue(board.deletedAt === void 0, "BOARD_DELETED");
1280
+ requireValue(actor.kind === "human" && board.members?.some((row) => row.kind === actor.kind && row.id === actor.id && row.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
1281
+ const items = await deletionItems(service, boardId), itemTitles = Object.fromEntries(items.map((item) => [item.id, item.title]));
1282
+ const operationId = randomUUID();
1283
+ const blockers = await inspectionStage(operationId, "local-and-sessions", () => boardDeletionBlockers(options, boardId, items), options.journal);
1284
+ const resources = (await inspectionStage(operationId, "work-resources", () => inspectItems(items, async (item) => (await service.workResources?.(boardId, item.id) ?? []).map((resource) => ({ itemId: item.id, resource }))), options.journal)).flat();
1285
+ for (const { itemId, resource } of resources) if (resource.state === "in-use" && resource.ownership !== "other" && resource.ownership !== "unknown" && !blockers.some((row) => row.code === "BOARD_EXECUTION_ACTIVE" && row.itemId === itemId)) blockers.push({ code: "BOARD_EXECUTION_ACTIVE", itemId });
1286
+ const result = { ...board.lifecycle?.intent === "delete" ? { pendingDeletion: { operationId: board.lifecycle.operationId, mode: board.lifecycle.mode, phase: board.lifecycle.phase, code: board.lifecycle.code } } : {}, boardId, title: board.title, revision: board.revision, itemTitles, blockers, pendingItems: board.items.filter((item) => !["completed", "cancelled"].includes(item.status)).length, resources };
1287
+ requireValue((await service.read(boardId)).revision === board.revision, "REVISION_CONFLICT");
1288
+ return { ...result, version: sha256HexSync(JSON.stringify(result)) };
1289
+ }
1290
+ async function deletionItems(service, board) {
1291
+ const items = [];
1292
+ let cursor;
1293
+ do {
1294
+ const page = await service.query(board, { view: "all", includeDeleted: true, limit: 200, ...cursor ? { cursor } : {} });
1295
+ items.push(...page.items.map(({ id, title }) => ({ id, title })));
1296
+ cursor = page.nextCursor;
1297
+ requireValue(items.length <= 1e4, "BOARD_DELETE_RESOURCE_LIMIT");
1298
+ } while (cursor);
1299
+ return items;
1300
+ }
1301
+ async function itemBlockers(service, journal, board, item) {
1302
+ const rows = [];
1303
+ if ((await analysisHistory(journal, board, item)).some((job) => job.phase !== "settled")) rows.push({ code: "BOARD_ANALYSIS_UNSETTLED", itemId: item });
1304
+ if (!service.executionSession) return rows;
1305
+ const detail = await service.executionSession(board, item, { includeRelated: true });
1306
+ if (detail.sessionsCode) rows.push({ code: detail.sessionsCode, itemId: item });
1307
+ for (const session of detail.sessions ?? []) {
1308
+ if (session.purposes?.every((purpose) => purpose === "discussion")) continue;
1309
+ const executions = session.executions.filter((run) => run.purpose !== "discussion");
1310
+ const pending2 = executions.length && executions.every((run) => run.state) ? executions.some((run) => !settled.has(run.state)) : !settled.has(session.state);
1311
+ if (session.stale || pending2) rows.push({ code: "BOARD_SESSION_UNSETTLED", itemId: item, sessionId: session.sessionId });
1312
+ }
1313
+ return rows;
1314
+ }
1315
+ async function pendingCollaboration(journal, boardId) {
1316
+ const [fixes, replies] = await Promise.all([
1317
+ journal.findForBoard("fix:", boardId, (fix) => !fix.accepted && fix.state !== "cancelled"),
1318
+ journal.findForBoard("task-reply:", boardId, () => true)
1319
+ ]);
1320
+ const delivered = await journal.getMany(replies.map((row) => "task-reply-delivered:" + row.key.slice("task-reply:".length)));
1321
+ const cancelled2 = await journal.getMany(replies.map((row) => "task-reply-cancelled:" + row.key.slice("task-reply:".length)));
1322
+ return [
1323
+ ...fixes.map(({ value }) => ({ code: "BOARD_FIX_UNSETTLED", itemId: value.itemId })),
1324
+ ...replies.filter((_, index) => (!delivered[index] || replies[index].value.phase === "cancelling") && !cancelled2[index]).map(({ value }) => ({ code: "BOARD_REPLY_UNSETTLED", itemId: value.request.itemId }))
1325
+ ];
1326
+ }
1327
+ async function inspectItems(items, run) {
1328
+ const results = [];
1329
+ let next = 0, stopped = false;
1330
+ const workers = Array.from({ length: Math.min(4, items.length) }, async () => {
1331
+ while (!stopped && next < items.length) {
1332
+ const index = next++;
1333
+ try {
1334
+ results[index] = await run(items[index]);
1335
+ } catch (error) {
1336
+ stopped = true;
1337
+ throw error;
1338
+ }
1339
+ }
1340
+ });
1341
+ const settled2 = await Promise.allSettled(workers), failed = settled2.find((row) => row.status === "rejected");
1342
+ if (failed?.status === "rejected") throw failed.reason;
1343
+ return results;
1344
+ }
1345
+ async function inspectionStage(operationId, stage, run, journal) {
1346
+ const log = runtimeLogger(journal);
1347
+ const started = Date.now();
1348
+ try {
1349
+ const result = await run();
1350
+ log.info("board.deletion.inspect", { operation: "kanban.board.delete", operationId, stage, result: "success", durationMs: Date.now() - started });
1351
+ return result;
1352
+ } catch (error) {
1353
+ log.warn("board.deletion.inspect", { operation: "kanban.board.delete", operationId, stage, result: "failure", code: safeCode(error, "BOARD_INSPECTION_FAILED"), durationMs: Date.now() - started });
1354
+ throw error;
1355
+ }
1356
+ }
1357
+
1358
+ // src/runtime/board-stop.ts
1359
+ var keyFor2 = (board) => "board-stop:" + board;
1360
+ function createBoardStop(options, remove) {
1361
+ const log = runtimeLogger(options.journal);
1362
+ return {
1363
+ cancelBoard: (board, request) => requestStop(options, board, { ...request, mode: "cancel" }, "cancel"),
1364
+ requestBoardDeletion: (board, request) => requestStop(options, board, request, "delete"),
1365
+ preparePending: async () => {
1366
+ for (const row of await pending(options)) {
1367
+ const started = Date.now();
1368
+ try {
1369
+ await admit2(options, row.value);
1370
+ } catch (error) {
1371
+ log.warn("Board stop admission pending", { operation: "kanban.board.stop", operationId: row.value.request.operationId, boardId: row.value.boardId, stage: "admit", result: "failure", code: safeCode(error, "BOARD_STOP_ADMISSION_UNKNOWN"), durationMs: Date.now() - started });
1372
+ }
1373
+ }
1374
+ },
1375
+ resumePending: async () => {
1376
+ for (const row of await pending(options)) {
1377
+ const started = Date.now(), saved = row.value;
1378
+ try {
1379
+ if ((await options.service.read(saved.boardId)).deletedAt !== void 0) {
1380
+ await done(options, saved);
1381
+ continue;
1382
+ }
1383
+ if ((await authorize(options, saved)).lifecycle?.operationId !== saved.request.operationId) continue;
1384
+ const preview = await deletionPreview(options, saved.boardId);
1385
+ if (preview.blockers.length) {
1386
+ await progress(options, saved, "blocked", preview.blockers[0].code);
1387
+ continue;
1388
+ }
1389
+ await progress(options, saved, "ready");
1390
+ if (saved.intent === "cancel") {
1391
+ await done(options, saved);
1392
+ continue;
1393
+ }
1394
+ const current = await deletionPreview(options, saved.boardId);
1395
+ await remove(saved.boardId, { operationId: sha256HexSync(JSON.stringify([saved.request.operationId, current.version])), expectedRevision: current.revision, version: current.version, cleanup: saved.request.cleanup === true });
1396
+ await done(options, saved);
1397
+ log.info("Deferred board deletion completed", { operation: "kanban.board.stop", operationId: saved.request.operationId, boardId: saved.boardId, stage: "delete", result: "success", durationMs: Date.now() - started });
1398
+ } catch (error) {
1399
+ const code = safeCode(error, "BOARD_DELETION_UNKNOWN");
1400
+ await progress(options, saved, "blocked", code).catch(() => {
1401
+ });
1402
+ log.warn("Deferred board deletion pending", { operation: "kanban.board.stop", operationId: saved.request.operationId, boardId: saved.boardId, stage: "settle", result: "failure", code, durationMs: Date.now() - started });
1403
+ }
1404
+ }
1405
+ }
1406
+ };
1407
+ }
1408
+ function pending({ journal }) {
1409
+ return journal.find("board-stop:", (value) => value.phase !== "done");
1410
+ }
1411
+ async function authorize(options, saved) {
1412
+ const board = await options.service.read(saved.boardId), actor = await options.service.currentPrincipal();
1413
+ requireValue(actor.kind === "human" && actor.id === saved.principalId && board.members?.some((member) => member.kind === actor.kind && member.id === actor.id && member.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
1414
+ return board;
1415
+ }
1416
+ async function requestStop(options, boardId, request, intent) {
1417
+ requireValue(/^[\w-]{1,128}$/.test(request.operationId) && Number.isSafeInteger(request.expectedRevision), "INVALID_OPERATION_ID");
1418
+ if (intent === "delete") requireValue(typeof request.cleanup === "boolean" && ["drain", "cancel"].includes(request.mode ?? "") && /^[a-f0-9]{64}$/.test(request.version ?? ""), "INVALID_BOARD_CONTROL");
1419
+ const actor = await options.service.currentPrincipal(), board = await authorize(options, { boardId, principalId: actor.id });
1420
+ const old = await options.journal.get(keyFor2(boardId));
1421
+ if (old?.request.operationId === request.operationId) {
1422
+ requireValue(old.intent === intent && JSON.stringify(old.request) === JSON.stringify(request), "IDEMPOTENCY_CONFLICT");
1423
+ if (old.phase !== "done") await admit2(options, old);
1424
+ return;
1425
+ }
1426
+ requireValue(board.deletedAt === void 0, "BOARD_DELETED");
1427
+ requireValue(!old || old.intent === "cancel" && intent === "delete", "BOARD_STOP_PENDING");
1428
+ requireValue(board.revision === request.expectedRevision, "REVISION_CONFLICT");
1429
+ if (intent === "delete") {
1430
+ const preview = await deletionPreview(options, boardId);
1431
+ requireValue(preview.version === request.version, "BOARD_DELETE_PREVIEW_CHANGED");
1432
+ }
1433
+ const saved = await options.journal.update(keyFor2(boardId), (current) => {
1434
+ requireValue(!current || current.request.operationId === old?.request.operationId, "BOARD_STOP_PENDING");
1435
+ return { boardId, principalId: actor.id, intent, request: structuredClone(request), phase: "prepared" };
1436
+ });
1437
+ await admit2(options, saved);
1438
+ }
1439
+ async function admit2(options, saved) {
1440
+ if (saved.phase !== "prepared") return;
1441
+ const board = await authorize(options, saved);
1442
+ if (board.deletedAt !== void 0) {
1443
+ await done(options, saved);
1444
+ return;
1445
+ }
1446
+ await journalCommand(
1447
+ options.journal,
1448
+ options.service,
1449
+ saved.boardId,
1450
+ "board-stop-admit:" + JSON.stringify([saved.boardId, saved.request.operationId]),
1451
+ { type: "request-board-stop", controlId: saved.request.operationId, intent: saved.intent, mode: saved.request.mode ?? "cancel" }
1452
+ );
1453
+ await options.journal.update(keyFor2(saved.boardId), (old) => old?.request.operationId === saved.request.operationId ? { ...old, phase: "admitted" } : old);
1454
+ }
1455
+ async function progress(options, saved, phase, code) {
1456
+ const board = await authorize(options, saved);
1457
+ if (board.deletedAt !== void 0 || board.lifecycle?.operationId !== saved.request.operationId || board.lifecycle?.phase === phase && board.lifecycle.code === code) return;
1458
+ await journalCommand(
1459
+ options.journal,
1460
+ options.service,
1461
+ saved.boardId,
1462
+ "board-stop-progress:" + JSON.stringify([saved.boardId, saved.request.operationId, board.revision, phase, code ?? ""]),
1463
+ { type: "observe-board-stop", controlId: saved.request.operationId, phase, code }
1464
+ );
1465
+ }
1466
+ async function done(options, saved) {
1467
+ await options.journal.update(keyFor2(saved.boardId), (old) => old?.request.operationId === saved.request.operationId ? { ...old, phase: "done" } : old);
1468
+ }
1469
+
1470
+ // src/runtime/board-deletion.ts
1471
+ function createBoardDeletion(options) {
1472
+ const stop = createBoardStop(options, (board, request) => removeBoard(options, board, request));
1473
+ return {
1474
+ ...stop,
1475
+ previewDeletion: (id) => traced(id, randomUUID(), "preview", () => deletionPreview(options, id), options.journal),
1476
+ assertSettled: async (id) => {
1477
+ const rows = await boardDeletionBlockers(options, id);
1478
+ requireValue(!rows.length, rows[0]?.code ?? "BOARD_EXECUTION_UNSETTLED");
1479
+ },
1480
+ deleteBoard: (id, request) => traced(id, request.operationId, "delete", () => removeBoard(options, id, request), options.journal)
1481
+ };
1482
+ }
1483
+ async function removeBoard(options, id, request) {
1484
+ const { service, journal } = options;
1485
+ requireValue(/^[\w-]{1,128}$/.test(request.operationId) && typeof request.cleanup === "boolean" && /^[a-f0-9]{64}$/.test(request.version), "INVALID_OPERATION_ID");
1486
+ const key = "board-deletion:" + id, receipt = await journal.get(key);
1487
+ if (receipt?.request.operationId === request.operationId) {
1488
+ requireValue(JSON.stringify(receipt.request) === JSON.stringify(request), "IDEMPOTENCY_CONFLICT");
1489
+ if ((await service.read(id)).deletedAt !== void 0) return;
1490
+ }
1491
+ const current = await deletionPreview(options, id);
1492
+ requireValue(current.revision === request.expectedRevision && current.version === request.version, "BOARD_DELETE_PREVIEW_CHANGED");
1493
+ requireValue(!current.blockers.length, current.blockers[0]?.code ?? "BOARD_EXECUTION_UNSETTLED");
1494
+ await journal.update(key, () => ({ request, preview: current, phase: "prepared" }));
1495
+ if (request.cleanup) await cleanupResources(options, id, request, current);
1496
+ const board = await service.read(id);
1497
+ requireValue(board.revision === request.expectedRevision, "BOARD_DELETE_PREVIEW_CHANGED");
1498
+ await service.execute(id, { operationId: request.operationId, expectedRevision: board.revision, command: { type: "delete-board" } });
1499
+ await journal.update(key, (old) => ({ ...old, phase: "deleted" }));
1500
+ }
1501
+ async function cleanupResources({ service, journal }, id, request, preview) {
1502
+ const groups = /* @__PURE__ */ new Map();
1503
+ for (const row of preview.resources) {
1504
+ const key = JSON.stringify([row.resource.kind, row.resource.id, row.resource.location]);
1505
+ const group = groups.get(key) ?? [];
1506
+ group.push(row);
1507
+ groups.set(key, group);
1508
+ }
1509
+ for (const group of groups.values()) if (group.every((row) => row.resource.canCleanup && row.resource.ownership !== "other" && row.resource.ownership !== "unknown")) {
1510
+ const { itemId, resource } = group[0];
1511
+ requireValue(service.cleanupWorkResource, "WORK_RESOURCE_CLEANUP_UNAVAILABLE");
1512
+ await traced(id, request.operationId, "cleanup", () => service.cleanupWorkResource(id, {
1513
+ itemId,
1514
+ resourceId: resource.id,
1515
+ version: resource.version,
1516
+ operationId: sha256HexSync(JSON.stringify([request.operationId, resource.id]))
1517
+ }), journal);
1518
+ }
1519
+ }
1520
+ async function traced(boardId, operationId, stage, run, journal) {
1521
+ const log = runtimeLogger(journal);
1522
+ const started = Date.now();
1523
+ operationId = /^[\w-]{1,128}$/.test(operationId) ? operationId : randomUUID();
1524
+ try {
1525
+ const result = await run();
1526
+ log.info("board.deletion", { operation: "kanban.board.delete", boardId, operationId, stage, result: "success", durationMs: Date.now() - started });
1527
+ return result;
1528
+ } catch (error) {
1529
+ log.warn("board.deletion", { operation: "kanban.board.delete", boardId, operationId, stage, result: "failure", code: safeCode(error, "BOARD_DELETE_FAILED"), durationMs: Date.now() - started });
1530
+ throw error;
1531
+ }
1532
+ }
1533
+ function withBoardDeletion(service, deletion) {
1534
+ let barrier = Promise.resolve(), stopping = false;
1535
+ const pending2 = /* @__PURE__ */ new Set();
1536
+ const items = /* @__PURE__ */ new Map();
1537
+ const itemMutation = (board, item, run, checkDeleted = true) => {
1538
+ const key = JSON.stringify([board, item]);
1539
+ const next = (items.get(key) ?? Promise.resolve()).catch(() => {
1540
+ }).then(async () => {
1541
+ if (checkDeleted) requireValue((await service.item(board, item)).item.deletedAt === void 0, "ITEM_DELETED");
1542
+ return run();
1543
+ });
1544
+ items.set(key, next);
1545
+ void next.finally(() => {
1546
+ if (items.get(key) === next) items.delete(key);
1547
+ }).catch(() => {
1548
+ });
1549
+ return next;
1550
+ };
1551
+ let quiescing, disposing;
1552
+ const quiesce = () => {
1553
+ stopping = true;
1554
+ if (!quiescing) {
1555
+ const acknowledgements = Promise.allSettled([service.quiesce?.() ?? Promise.resolve(), ...pending2]);
1556
+ quiescing = (async () => {
1557
+ await barrier;
1558
+ const [ack] = await acknowledgements;
1559
+ if (ack.status === "rejected") throw ack.reason;
1560
+ })();
1561
+ }
1562
+ return quiescing;
1563
+ };
1564
+ const close = (method) => method === "quiesce" ? quiesce() : disposing ??= quiesce().then(() => service.dispose?.());
1565
+ const admit3 = (run) => {
1566
+ if (stopping) return Promise.reject(new KanbanError("SERVICE_STOPPING"));
1567
+ const next = barrier.then(run);
1568
+ pending2.add(next);
1569
+ void next.finally(() => pending2.delete(next)).catch(() => {
1570
+ });
1571
+ return next;
1572
+ };
1573
+ const exclusive = (run) => {
1574
+ if (stopping) return Promise.reject(new KanbanError("SERVICE_STOPPING"));
1575
+ const admitted = [...pending2], next = barrier.then(async () => {
1576
+ await Promise.allSettled(admitted);
1577
+ return run();
1578
+ });
1579
+ barrier = next.catch(() => {
1580
+ });
1581
+ return next;
1582
+ };
1583
+ const active = async (id, run) => {
1584
+ requireValue((await service.read(id)).deletedAt === void 0, "BOARD_DELETED");
1585
+ return run();
1586
+ };
1587
+ return {
1588
+ ...service,
1589
+ ...service.quiesce ? { quiesce: () => close("quiesce") } : {},
1590
+ ...service.dispose ? { dispose: () => close("dispose") } : {},
1591
+ previewDeletion: deletion.previewDeletion,
1592
+ deleteBoard: (id, request) => exclusive(() => deletion.deleteBoard(id, request)),
1593
+ execute: (id, request) => admit3(() => active(id, () => {
1594
+ if (request.command.type === "delete-board") throw new KanbanError("BOARD_DELETE_CONFIRMATION_REQUIRED");
1595
+ return "itemId" in request.command && request.command.itemId ? itemMutation(id, request.command.itemId, () => service.execute(id, request), false) : service.execute(id, request);
1596
+ })),
1597
+ scan: async () => {
1598
+ await admit3(async () => {
1599
+ await deletion.preparePending();
1600
+ await service.scan();
1601
+ });
1602
+ if (!stopping) await exclusive(() => deletion.resumePending());
1603
+ },
1604
+ cancelBoard: (id, request) => admit3(() => active(id, () => deletion.cancelBoard(id, request))),
1605
+ requestBoardDeletion: (id, request) => admit3(() => active(id, () => deletion.requestBoardDeletion(id, request))),
1606
+ ...service.cancelFix ? { cancelFix: (id, request) => admit3(() => active(id, () => service.cancelFix(id, request))) } : {},
1607
+ ...service.controlRecord ? { controlRecord: (id, request) => admit3(() => active(id, () => service.controlRecord(id, request))) } : {},
1608
+ ...service.fixTask ? { fixTask: (id, request) => admit3(() => active(id, () => itemMutation(id, request.itemId, () => service.fixTask(id, request)))) } : {},
1609
+ ...service.replyTask ? { replyTask: (id, request) => admit3(() => active(id, () => itemMutation(id, request.itemId, () => service.replyTask(id, request)))) } : {},
1610
+ ...service.runRecord ? { runRecord: (id, request) => admit3(() => active(id, () => service.runRecord(id, request))) } : {},
1611
+ ...service.runItemOperation ? { runItemOperation: (id, request) => admit3(() => active(id, () => itemMutation(id, request.itemId, () => service.runItemOperation(id, request)))) } : {},
1612
+ ...service.bindItemOperation ? { bindItemOperation: (id, request) => admit3(() => active(id, () => itemMutation(id, request.itemId, () => service.bindItemOperation(id, request)))) } : {},
1613
+ ...service.cleanupWorkResource ? { cleanupWorkResource: (id, request) => admit3(() => service.cleanupWorkResource(id, request)) } : {}
1614
+ };
1615
+ }
1616
+
1617
+ // src/runtime/control.ts
1618
+ function controlExecution(options, _journal, executionId, operationId, action, attemptId = executionId, previousKey, direct = false) {
1619
+ return options.control.control({ executionId, operationId, action, attemptId, previousKey, ...direct ? { fallbackSubmission: true } : {} });
1620
+ }
1621
+
1622
+ // src/runtime/task-maintenance-cancellation.ts
1623
+ async function cancelTaskMaintenance(options, journal, board, item, controlId) {
1624
+ const jobs = await journal.findForBoard("review-operation:", board, (job) => job.itemId === item && job.phase !== "settled");
1625
+ for (const row of jobs) {
1626
+ const saved = await journal.update(row.key, (old) => old.phase === "prepared" ? { ...old, phase: "settled", state: "failed", code: "USER_CANCELLED" } : old);
1627
+ if (saved.phase === "settled") continue;
1628
+ if (saved.action === "accept") return "accepted";
1629
+ requireValue(saved.request ?? saved.input, "TASK_CONTROL_ROUTE_MISSING");
1630
+ const id = sha256HexSync(JSON.stringify([controlId, saved.id]));
1631
+ const state = await options.control.control({ executionId: saved.sessionId ?? saved.id, operationId: id, action: "cancel", attemptId: saved.id, kind: saved.request ? "submission" : "call", prepared: saved.request ?? saved.input });
1632
+ if (state !== "cancelled") return state === "unknown" ? "unknown" : "accepted";
1633
+ await journal.update(row.key, (old) => old.phase === "settled" ? old : { ...old, phase: "settled", state: "failed", code: "USER_CANCELLED" });
1634
+ }
1635
+ return cancelPostloads(options, journal, board, item, controlId);
1636
+ }
1637
+ async function cancelPostloads(options, journal, board, item, controlId) {
1638
+ const rows = await journal.find(options.finalizerPrefix ?? "execution-finalizer:", (job) => job.boardId === board && job.dispatch.itemId === item && !job.done);
1639
+ for (const row of rows) {
1640
+ const job = await journal.update(row.key, (old) => old.phase === "prepared" ? { ...old, phase: "settled", done: true, observation: { outcome: "cancelled", code: "USER_CANCELLED" } } : old);
1641
+ if (job.done) continue;
1642
+ const state = await controlExecution(options, journal, job.dispatch.id, sha256HexSync(JSON.stringify([controlId, job.dispatch.id])), "cancel");
1643
+ if (state !== "cancelled") return state === "unknown" ? "unknown" : "accepted";
1644
+ await journal.update(row.key, (old) => ({ ...old, phase: "settled", done: true, observation: { outcome: "cancelled", code: "USER_CANCELLED" } }));
1645
+ }
1646
+ return "cancelled";
1647
+ }
1648
+
1649
+ // src/runtime/task-reply-cancellation.ts
1650
+ async function cancelTaskReplies(options, journal, board, item, controlId) {
1651
+ const replies = await journal.findForBoard("task-reply:", board, (reply) => reply.request.itemId === item);
1652
+ for (const row of replies) {
1653
+ const id = row.value.request.operationId;
1654
+ if (await journal.get("task-reply-cancelled:" + id)) continue;
1655
+ const saved = await journal.update(row.key, (old) => ({ ...old, phase: old.phase === "prepared" ? "cancelled" : old.phase === "cancelled" ? "cancelled" : "cancelling" }));
1656
+ if (saved.phase !== "cancelled") {
1657
+ requireValue(options.control, "TASK_CONTROL_UNSUPPORTED");
1658
+ const state = await options.control.control({ executionId: saved.sessionId, attemptId: id, operationId: sha256HexSync(JSON.stringify([controlId, id])), action: "cancel", kind: "submission" });
1659
+ if (state !== "cancelled") return state === "unknown" ? "unknown" : "accepted";
1660
+ }
1661
+ await journal.update("task-reply-cancelled:" + id, () => true);
1662
+ }
1663
+ return "cancelled";
1664
+ }
1665
+
1666
+ // src/runtime/board-stop-work.ts
1667
+ async function scanBoardStopWork(options) {
1668
+ for (const board of await options.service.list()) {
1669
+ if (board.lifecycle?.mode !== "cancel" || board.lifecycle.phase === "ready") continue;
1670
+ const control = board.lifecycle.operationId;
1671
+ if (options.fixes) for (const row of await options.journal.findForBoard("fix:", board.id, (fix) => !fix.accepted && fix.state !== "cancelled"))
1672
+ await attempt(options.journal, board.id, control, "fix.cancel", () => options.fixes.cancelFix(board.id, { fixId: row.value.id, operationId: sha256HexSync(JSON.stringify([control, row.value.id])) }));
1673
+ if (options.records) for (const run of await options.journal.get("record-runs:" + board.id) ?? []) {
1674
+ const key = "record-control:" + JSON.stringify([board.id, run.recordId]);
1675
+ if ((await options.journal.get(key))?.action !== "cancel") await attempt(options.journal, board.id, control, "record.cancel", () => options.records.controlRecord(board.id, { recordId: run.recordId, operationId: sha256HexSync(JSON.stringify([control, run.recordId])), action: "cancel" }));
1676
+ }
1677
+ for (const item of board.items.filter((item2) => ["completed", "cancelled"].includes(item2.status))) {
1678
+ const id = sha256HexSync(JSON.stringify([control, item.id]));
1679
+ await attempt(options.journal, board.id, control, "reply.cancel", () => cancelTaskReplies(options.ports, options.journal, board.id, item.id, id));
1680
+ await attempt(options.journal, board.id, control, "maintenance.cancel", () => cancelTaskMaintenance(options.ports, options.journal, board.id, item.id, id));
1681
+ }
1682
+ }
1683
+ }
1684
+ async function attempt(journal, boardId, operationId, stage, run) {
1685
+ const started = Date.now();
1686
+ try {
1687
+ await run();
1688
+ } catch (error) {
1689
+ runtimeLogger(journal).warn("Board work settlement pending", { operation: "kanban.board.stop", operationId, boardId, stage, result: "failure", code: safeCode(error, "BOARD_WORK_STOP_UNKNOWN"), durationMs: Date.now() - started });
1690
+ }
1691
+ }
1692
+
1693
+ // src/runtime/task-analysis-controls.ts
1694
+ async function controlTaskAnalyses(options, journal, board, item) {
1695
+ const analyses = await analysisHistory(journal, board, item.id);
1696
+ requireValue(analyses.length || !["analyzing", "unknown"].includes(item.analysis?.state ?? ""), "TASK_CONTROL_ROUTE_MISSING");
1697
+ const keys = (await journal.get("analysis-history:" + JSON.stringify([board, item.id])))?.keys ?? [];
1698
+ const byId = new Map((await journal.getMany(keys)).flatMap((job, index) => job ? [[job.id, keys[index]]] : []));
1699
+ for (const job of analyses) {
1700
+ if (job.phase === "settled") continue;
1701
+ const key = byId.get(job.id);
1702
+ requireValue(key, "TASK_CONTROL_ROUTE_MISSING");
1703
+ const state = await controlAnalysis(options, journal, key, job, item.control);
1704
+ if (state) return state;
1705
+ }
1706
+ }
1707
+ async function controlAnalysis(options, journal, key, job, control) {
1708
+ if (control.action === "cancel") {
1709
+ const current = await journal.update(key, (old) => old.phase === "prepared" ? cancelled(old) : old);
1710
+ if (current.phase === "settled") return;
1711
+ } else if (job.phase === "prepared") return;
1712
+ const state = await controlExecution(options, journal, job.id, control.id + "-analysis-" + job.id, control.action);
1713
+ const target = control.action === "pause" ? "paused" : control.action === "resume" ? "resumed" : "cancelled";
1714
+ if (state !== target) return state;
1715
+ if (control.action === "cancel") await journal.update(key, (old) => cancelled(old));
1716
+ }
1717
+ function cancelled(job) {
1718
+ return { ...job, phase: "settled", terminalOutcome: "cancelled", status: { ...job.status, state: "failed", code: "USER_CANCELLED" } };
1719
+ }
1720
+
1721
+ // src/runtime/task-controls.ts
1722
+ async function scanTaskControls(options, journal, board, service) {
1723
+ const log = runtimeLogger(journal);
1724
+ for (const item of board.items) {
1725
+ const control = item.control;
1726
+ if (!control || ["failed", "paused", "resumed", "cancelled"].includes(control.state)) continue;
1727
+ const started = Date.now();
1728
+ let result;
1729
+ try {
1730
+ const state = await observe(options, journal, board, item, service);
1731
+ const latest = state === "cancelled" ? await service.read(board.id) : board;
1732
+ const childrenPending = latest.items.some((child) => child.planning?.parentId === item.id && !["completed", "cancelled"].includes(child.status));
1733
+ result = { state: state === "cancelled" && childrenPending ? "accepted" : state };
1734
+ } catch (error) {
1735
+ result = controlFailure(error);
1736
+ }
1737
+ if (result.state === control.state && result.code === control.code) continue;
1738
+ const fields = { operation: "kanban.task.control", operationId: control.id, boardId: board.id, itemId: item.id, stage: control.action, result: ["failed", "unknown"].includes(result.state) ? "failure" : "success", code: result.code, state: result.state, durationMs: Date.now() - started };
1739
+ if (fields.result === "failure") log.warn("Task control observed", fields);
1740
+ else log.info("Task control observed", fields);
1741
+ await journalCommand(
1742
+ journal,
1743
+ service,
1744
+ board.id,
1745
+ "task-control-projection:" + JSON.stringify([board.id, item.id, control.id]) + ":" + result.state + ":" + (result.code ?? ""),
1746
+ { type: "control-receipt", itemId: item.id, controlId: control.id, ...result }
1747
+ );
1748
+ }
1749
+ }
1750
+ async function observe(options, journal, board, item, service) {
1751
+ const control = item.control;
1752
+ if (control.action === "cancel") {
1753
+ const state = await cancelTaskReplies(options, journal, board.id, item.id, control.id);
1754
+ if (state !== "cancelled") return state;
1755
+ const maintenance = await cancelTaskMaintenance(options, journal, board.id, item.id, control.id);
1756
+ if (maintenance !== "cancelled") return maintenance;
1757
+ }
1758
+ const analysis = await controlTaskAnalyses(options, journal, board.id, item);
1759
+ if (analysis) return analysis;
1760
+ const workflow = await service.workflow(board.id);
1761
+ const dispatch = Object.values(workflow.dispatches).filter((d) => d.itemId === item.id).sort((a, b) => b.attempt - a.attempt)[0];
1762
+ if (dispatch) {
1763
+ if (control.action === "cancel" && ["failed", "cancelled", "rejected"].includes(dispatch.outcome)) return "cancelled";
1764
+ return controlExecution(options, journal, dispatch.continuation ? dispatch.executionSessionId ?? dispatch.id : dispatch.id, control.id, control.action, dispatch.id, "task-control:" + board.id + ":" + control.id, !!dispatch.continuation);
1765
+ }
1766
+ requireValue((!item.owner || item.owner.kind === "human") && !item.externalRefs?.some((ref) => ref.system === (options.references ?? defaultReferences).execution), "TASK_CONTROL_ROUTE_MISSING");
1767
+ return control.action === "cancel" ? "cancelled" : control.action === "pause" ? "paused" : "resumed";
1768
+ }
1769
+ function controlFailure(error) {
1770
+ const raw = error?.code, code = typeof raw === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(raw) ? raw : "TASK_CONTROL_UNKNOWN";
1771
+ return { state: ["TASK_CONTROL_UNSUPPORTED", "CALL_READ_ONLY", "CONTROL_EPOCH_CONFLICT"].includes(code) ? "failed" : "unknown", code };
1772
+ }
1773
+
1774
+ // src/runtime/runtime-lifecycle.ts
1775
+ async function verifyRuntimeLifecycle(service, journal, board, command) {
1776
+ if (command.type === "delete-item") {
1777
+ const blocker = (await boardDeletionBlockers({ service, journal }, board.id)).find((row) => row.itemId === command.itemId);
1778
+ if (blocker) throw new KanbanError(blocker.code);
1779
+ }
1780
+ if (command.type === "cancel-record" || command.type === "delete-record") {
1781
+ const runs = await journal.get("record-runs:" + board.id);
1782
+ if (runs?.some((run) => run.recordId === command.recordId && !["completed", "failed", "cancelled"].includes(run.state))) throw new KanbanError("RECORD_EXECUTION_UNSETTLED");
1783
+ }
1784
+ }
1785
+ function runtimeLifecycle(service, journal, extensions, beforeScan) {
1786
+ let stopping = false, pending2, closing, disposing;
1787
+ const scan = () => stopping ? Promise.resolve() : pending2 ??= (async () => {
1788
+ await service.scan();
1789
+ await beforeScan();
1790
+ await all(extensions, "scan", journal);
1791
+ })().finally(() => {
1792
+ pending2 = void 0;
1793
+ });
1794
+ const quiesce = () => closing ??= (async () => {
1795
+ stopping = true;
1796
+ const results = await Promise.allSettled([all(extensions, "quiesce", journal), pending2]);
1797
+ await service.quiesce();
1798
+ await journal.drain();
1799
+ failures(results, "KANBAN_QUIESCE_FAILED");
1800
+ })();
1801
+ return { scan, quiesce, dispose: () => disposing ??= (async () => {
1802
+ await quiesce();
1803
+ await all(extensions, "dispose", journal);
1804
+ await service.dispose();
1805
+ })() };
1806
+ }
1807
+ async function all(extensions, method, journal) {
1808
+ const results = await Promise.allSettled(extensions.map(async (extension, index) => {
1809
+ const started = Date.now();
1810
+ try {
1811
+ await extension[method]?.();
1812
+ } catch (error) {
1813
+ journal.diagnostic({ operationId: "runtime:" + method + ":" + index, stage: "runtime." + method, result: "failure", code: safeCode(error, "KANBAN_" + method.toUpperCase() + "_FAILED"), durationMs: Date.now() - started });
1814
+ throw error;
1815
+ }
1816
+ }));
1817
+ failures(results, "KANBAN_" + method.toUpperCase() + "_FAILED");
1818
+ }
1819
+ function failures(results, code) {
1820
+ const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1821
+ if (errors.length) throw new AggregateError(errors, code);
1822
+ }
1823
+
1824
+ // src/runtime/runtime.ts
1825
+ async function assembleKanbanRuntime(options) {
1826
+ const directory = options.directory ?? "/kanban", fs = options.fs;
1827
+ const store = options.store ?? await SeqKanbanStore.open(fs, directory);
1828
+ const journal = options.journal instanceof KanbanAutomationJournal ? options.journal : new KanbanAutomationJournal(fs, { directory: directory + "/runtime", diagnostic: options.log, ...options.journal });
1829
+ const workLog = new KanbanWorkLog(fs, { directory: directory + "/runtime", diagnostic: options.log, ...options.workLog });
1830
+ await journal.open();
1831
+ await workLog.open();
1832
+ let verifyDeletion = async () => {
1833
+ };
1834
+ const createService = options.createService ?? createKanbanActivityModule;
1835
+ const serviceFor = (actor) => createService({
1836
+ ...options,
1837
+ store,
1838
+ actor,
1839
+ referenceSystem: options.referenceSystem ?? defaultReferences.execution,
1840
+ verifyLifecycle: async (board, command) => {
1841
+ await verifyRuntimeLifecycle(service, journal, board, command);
1842
+ await options.verifyLifecycle?.(board, command);
1843
+ },
1844
+ verifyDeletion: async (board) => {
1845
+ await verifyDeletion(board.id);
1846
+ await options.verifyDeletion?.(board);
1847
+ },
1848
+ onCommitted: async (id, source, now) => {
1849
+ await workLog.synchronize(id, source);
1850
+ await options.onCommitted?.(id, source, now);
1851
+ }
1852
+ });
1853
+ const service = serviceFor(options.actor), context = { fs, store, journal, workLog, service, serviceFor };
1854
+ const extensions = options.extensions?.(context) ?? [];
1855
+ return assembleRuntime(options, context, extensions, (verify) => {
1856
+ verifyDeletion = verify;
1857
+ });
1858
+ }
1859
+ function assembleRuntime(options, context, extensions, setDeletion) {
1860
+ const { service, journal, workLog } = context, directory = options.directory ?? "/kanban";
1861
+ const records = options.resources ? createRecordExecution({ service, journal, resources: options.resources, requiresItemResource: options.requiresItemResource, log: options.log ?? (() => {
1862
+ }) }) : void 0;
1863
+ const automation = options.automation ? createKanbanAutomation({
1864
+ ...options.automation,
1865
+ ...context,
1866
+ directory: directory + "/automation",
1867
+ log: options.log ?? (() => {
1868
+ })
1869
+ }) : void 0;
1870
+ const capabilities = Object.assign({}, ...extensions.map((extension) => extension.capabilities));
1871
+ const client = {
1872
+ ...service,
1873
+ ...capabilities,
1874
+ ...records ? { ...records, recordExecutions: async (board, id) => (await workLog.record(service, board, id)).kind === "decision" ? records.recordExecutions(board, id) : [] } : {},
1875
+ ...automation ? { automationCatalog: automation.automationCatalog, analysisStatus: automation.analysisStatus } : {},
1876
+ records: (board, query) => workLog.records(service, board, query),
1877
+ record: (board, id) => workLog.record(service, board, id)
1878
+ };
1879
+ const deletion = createBoardDeletion({ service: client, journal });
1880
+ setDeletion(deletion.assertSettled);
1881
+ const participants = [...extensions, ...records ? [records] : [], ...automation ? [automation] : []];
1882
+ const lifecycle = runtimeLifecycle(service, journal, participants, async () => {
1883
+ await scanBoardStopWork({ service, journal, ports: options.control ?? { control: { control: async () => "unknown" } }, fixes: capabilities.cancelFix ? { cancelFix: capabilities.cancelFix } : void 0, records });
1884
+ if (!automation && options.control) for (const board of await service.list()) await scanTaskControls(options.control, journal, board, service);
1885
+ });
1886
+ return { ...context, service: withBoardDeletion({ ...client, ...lifecycle }, deletion) };
1887
+ }
1888
+
1889
+ // src/runtime/board-drain.ts
1890
+ async function drainBoardAnalyses(ports, journal, board, service) {
1891
+ if (!board.lifecycle) return;
1892
+ for (const item of board.items) {
1893
+ const jobs = await analysisHistory(journal, board.id, item.id);
1894
+ const keys = (await journal.get("analysis-history:" + JSON.stringify([board.id, item.id])))?.keys ?? [];
1895
+ for (const job of jobs) {
1896
+ if (job.phase === "settled") continue;
1897
+ const key = keys[(await journal.getMany(keys)).findIndex((row) => row?.id === job.id)];
1898
+ if (!key) continue;
1899
+ const view2 = job.phase === "submitting" ? await ports.control.inspect?.(job.id, job.id) : void 0;
1900
+ if (job.phase === "submitting" && (!view2 || !view2.settled)) continue;
1901
+ const saved = await journal.update(key, (old) => old.phase !== job.phase ? old : { ...old, phase: "settled", terminalOutcome: view2?.outcome ?? (job.phase === "prepared" ? "cancelled" : void 0), status: { ...old.status, state: "failed", code: "BOARD_STOPPED_AFTER_ANALYSIS" } });
1902
+ if (saved.phase === "settled" && !["completed", "cancelled"].includes(item.status) && item.control?.action !== "cancel") await journalCommand(
1903
+ journal,
1904
+ service,
1905
+ board.id,
1906
+ "board-drained-analysis:" + JSON.stringify([board.id, job.id]),
1907
+ { type: "report-analysis", itemId: item.id, operationId: job.id, status: saved.status }
1908
+ );
1909
+ }
1910
+ }
1911
+ }
1912
+
1913
+ // src/runtime/work-context.ts
1914
+ async function workContext(journal, board, item, references = defaultReferences) {
1915
+ const runId = item.externalRefs?.find((ref) => ref.system === references.decision)?.id;
1916
+ let decision = runId ? await journal.get("record-context:" + runId) : void 0;
1917
+ if (runId && !decision) {
1918
+ const runs = await journal.get("record-runs:" + board.id);
1919
+ const run = runs?.find((run2) => run2.id === runId);
1920
+ if (run) decision = { sourceBoardId: run.sourceBoardId, recordId: run.recordId, recordRevision: run.recordRevision, runId };
1921
+ }
1922
+ const snapshot = decision ? (await journal.get("record-runs:" + decision.sourceBoardId))?.find((run) => run.id === decision.runId)?.record : void 0;
1923
+ return {
1924
+ board: { id: board.id, title: board.title, revision: board.revision },
1925
+ decision: decision ? { ...decision, ...snapshot ? { snapshot } : {} } : null,
1926
+ task: {
1927
+ id: item.id,
1928
+ revision: item.revision,
1929
+ parentId: item.planning?.parentId ?? null,
1930
+ title: item.title,
1931
+ responsibility: item.responsibility,
1932
+ acceptance: item.acceptance,
1933
+ dependencies: item.planning?.dependencies ?? []
1934
+ },
1935
+ resources: item.externalRefs?.filter((ref) => ref.system === references.resource) ?? []
1936
+ };
1937
+ }
1938
+
1939
+ export {
1940
+ safeCode,
1941
+ KanbanAutomationJournal,
1942
+ defaultReferences,
1943
+ KanbanWorkLog,
1944
+ journalCommand,
1945
+ recordControlKey,
1946
+ advanceRecordControl,
1947
+ requestRecordControl,
1948
+ createRecordExecution,
1949
+ validateResourceReferences,
1950
+ completeCompositePlans,
1951
+ validateSubtasks,
1952
+ adoptDecomposition,
1953
+ analysisStatus,
1954
+ analysisSignature,
1955
+ analyzeItem,
1956
+ parseAnalysis,
1957
+ createKanbanAutomation,
1958
+ dependencyResults,
1959
+ analysisHistory,
1960
+ boardDeletionBlockers,
1961
+ deletionPreview,
1962
+ createBoardDeletion,
1963
+ withBoardDeletion,
1964
+ cancelTaskMaintenance,
1965
+ cancelTaskReplies,
1966
+ scanBoardStopWork,
1967
+ scanTaskControls,
1968
+ assembleKanbanRuntime,
1969
+ drainBoardAnalyses,
1970
+ workContext
1971
+ };
1972
+ //# sourceMappingURL=chunk-FEE2YBZ7.js.map