@nestos/kanban 0.5.1 → 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 (147) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +73 -29
  3. package/dist/{activity-C6xDM-cg.d.cts → activity-BbC4sgKL.d.cts} +28 -2
  4. package/dist/{activity-Dw8DOw0L.d.ts → activity-Czlan5xW.d.ts} +28 -2
  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 +10 -282
  10. package/dist/activity.cjs.map +1 -1
  11. package/dist/activity.d.cts +8 -30
  12. package/dist/activity.d.ts +8 -30
  13. package/dist/activity.js +9 -281
  14. package/dist/activity.js.map +1 -1
  15. package/dist/{chunk-O4OGAAV4.js → chunk-3JYLJPE5.js} +29 -5
  16. package/dist/chunk-3JYLJPE5.js.map +1 -0
  17. package/dist/{chunk-ATUFFNLV.js → chunk-3LEIHB7Y.js} +5 -5
  18. package/dist/chunk-4XF6LQSU.cjs.map +1 -1
  19. package/dist/chunk-5Q7WB63A.cjs +282 -0
  20. package/dist/chunk-5Q7WB63A.cjs.map +1 -0
  21. package/dist/{chunk-PSKQAFDF.cjs → chunk-7KDOTBRX.cjs} +1 -1
  22. package/dist/chunk-7KDOTBRX.cjs.map +1 -0
  23. package/dist/{chunk-5DLZWW5Y.js → chunk-AHHEGTUA.js} +3 -3
  24. package/dist/chunk-AOHQCX7K.cjs +95 -0
  25. package/dist/chunk-AOHQCX7K.cjs.map +1 -0
  26. package/dist/{chunk-IUI5CI2S.cjs → chunk-DINLR6E6.cjs} +96 -92
  27. package/dist/chunk-DINLR6E6.cjs.map +1 -0
  28. package/dist/{chunk-L44TM4EE.cjs → chunk-EYTVC2H5.cjs} +568 -454
  29. package/dist/chunk-EYTVC2H5.cjs.map +1 -0
  30. package/dist/chunk-FEE2YBZ7.js +1972 -0
  31. package/dist/chunk-FEE2YBZ7.js.map +1 -0
  32. package/dist/chunk-FNO5K3A7.js +282 -0
  33. package/dist/chunk-FNO5K3A7.js.map +1 -0
  34. package/dist/chunk-IHHDXZPZ.js +382 -0
  35. package/dist/chunk-IHHDXZPZ.js.map +1 -0
  36. package/dist/chunk-J3EAIVA3.js +280 -0
  37. package/dist/chunk-J3EAIVA3.js.map +1 -0
  38. package/dist/{chunk-DFTOTBGF.js → chunk-KH3WF3K4.js} +20 -16
  39. package/dist/chunk-KH3WF3K4.js.map +1 -0
  40. package/dist/{chunk-TB2RZGDV.js → chunk-M35X5PHO.js} +14 -1
  41. package/dist/chunk-M35X5PHO.js.map +1 -0
  42. package/dist/chunk-QRGZD2JU.cjs +1972 -0
  43. package/dist/chunk-QRGZD2JU.cjs.map +1 -0
  44. package/dist/{chunk-4KF7KUAI.cjs → chunk-RBOER6EW.cjs} +6 -6
  45. package/dist/chunk-RBOER6EW.cjs.map +1 -0
  46. package/dist/{chunk-6MN3H3YF.cjs → chunk-RSH6QSEC.cjs} +16 -3
  47. package/dist/chunk-RSH6QSEC.cjs.map +1 -0
  48. package/dist/{chunk-T7P5EIC4.cjs → chunk-VMRDXOLX.cjs} +33 -33
  49. package/dist/chunk-VMRDXOLX.cjs.map +1 -0
  50. package/dist/{chunk-5XKOCKY2.js → chunk-X4UHZX6F.js} +1 -1
  51. package/dist/chunk-XBSGUVCY.cjs +382 -0
  52. package/dist/chunk-XBSGUVCY.cjs.map +1 -0
  53. package/dist/{chunk-OHQTKGQN.js → chunk-XKDZHAUE.js} +242 -128
  54. package/dist/chunk-XKDZHAUE.js.map +1 -0
  55. package/dist/chunk-ZUZGAO6G.cjs +280 -0
  56. package/dist/chunk-ZUZGAO6G.cjs.map +1 -0
  57. package/dist/{content-CAIp1njn.d.cts → content-C5aJZFlw.d.cts} +1 -1
  58. package/dist/{content-CGMJdyH_.d.ts → content-hLH5BNud.d.ts} +1 -1
  59. package/dist/contracts/index.cjs +13 -3
  60. package/dist/contracts/index.cjs.map +1 -1
  61. package/dist/contracts/index.d.cts +5 -4
  62. package/dist/contracts/index.d.ts +5 -4
  63. package/dist/contracts/index.js +14 -4
  64. package/dist/{discovery-DxpirOz3.d.cts → discovery-9E-LfwIw.d.cts} +1 -1
  65. package/dist/{discovery-gSqWpuRm.d.ts → discovery-D60yrjej.d.ts} +1 -1
  66. package/dist/discovery.cjs +63 -65
  67. package/dist/discovery.cjs.map +1 -1
  68. package/dist/discovery.d.cts +4 -4
  69. package/dist/discovery.d.ts +4 -4
  70. package/dist/discovery.js +5 -7
  71. package/dist/discovery.js.map +1 -1
  72. package/dist/execution-bridge-fpCnH6z5.d.cts +52 -0
  73. package/dist/execution-bridge-rk0uVy_n.d.ts +52 -0
  74. package/dist/execution.cjs +6 -356
  75. package/dist/execution.cjs.map +1 -1
  76. package/dist/execution.d.cts +4 -51
  77. package/dist/execution.d.ts +4 -51
  78. package/dist/execution.js +9 -359
  79. package/dist/execution.js.map +1 -1
  80. package/dist/index.cjs +10 -12
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/index.d.cts +9 -16
  83. package/dist/index.d.ts +9 -16
  84. package/dist/index.js +10 -12
  85. package/dist/index.js.map +1 -1
  86. package/dist/{module-CWNK2BMj.d.ts → module-CYfE7YhM.d.ts} +2 -2
  87. package/dist/{module-BbvwdCDF.d.cts → module-Ca9L3qCn.d.cts} +2 -2
  88. package/dist/{planning-CcFZYdIL.d.cts → planning-CHrOqQ6b.d.cts} +45 -10
  89. package/dist/{planning-CcFZYdIL.d.ts → planning-CHrOqQ6b.d.ts} +45 -10
  90. package/dist/{ports-Bn6ro2FB.d.ts → ports-B0McVg7B.d.ts} +4 -3
  91. package/dist/ports-BeGO1VRI.d.cts +49 -0
  92. package/dist/{ports-CaL6geqR.d.cts → ports-GQy0x-RI.d.cts} +4 -3
  93. package/dist/ports-ykGiNUSx.d.ts +49 -0
  94. package/dist/runtime/index.cjs +205 -0
  95. package/dist/runtime/index.cjs.map +1 -0
  96. package/dist/runtime/index.d.cts +75 -0
  97. package/dist/runtime/index.d.ts +75 -0
  98. package/dist/runtime/index.js +205 -0
  99. package/dist/runtime/index.js.map +1 -0
  100. package/dist/runtime/integration.cjs +423 -0
  101. package/dist/runtime/integration.cjs.map +1 -0
  102. package/dist/runtime/integration.d.cts +658 -0
  103. package/dist/runtime/integration.d.ts +658 -0
  104. package/dist/runtime/integration.js +423 -0
  105. package/dist/runtime/integration.js.map +1 -0
  106. package/dist/storage.cjs +9 -277
  107. package/dist/storage.cjs.map +1 -1
  108. package/dist/storage.d.cts +6 -6
  109. package/dist/storage.d.ts +6 -6
  110. package/dist/storage.js +11 -279
  111. package/dist/storage.js.map +1 -1
  112. package/dist/{store-q63eSMoT.d.cts → store-DPwHzGiz.d.cts} +2 -2
  113. package/dist/{store-Ct0gOAAg.d.ts → store-DgdrPCV9.d.ts} +2 -2
  114. package/dist/task-projection-CSorlMws.d.ts +11 -0
  115. package/dist/task-projection-DKKB_ueb.d.cts +11 -0
  116. package/examples/standalone.ts +22 -0
  117. package/package.json +117 -87
  118. package/dist/chunk-2YFPV5GF.cjs +0 -16
  119. package/dist/chunk-2YFPV5GF.cjs.map +0 -1
  120. package/dist/chunk-4KF7KUAI.cjs.map +0 -1
  121. package/dist/chunk-6MN3H3YF.cjs.map +0 -1
  122. package/dist/chunk-DFTOTBGF.js.map +0 -1
  123. package/dist/chunk-G6FCDLDF.js +0 -16
  124. package/dist/chunk-G6FCDLDF.js.map +0 -1
  125. package/dist/chunk-IUI5CI2S.cjs.map +0 -1
  126. package/dist/chunk-KYNYPKKH.cjs +0 -71
  127. package/dist/chunk-KYNYPKKH.cjs.map +0 -1
  128. package/dist/chunk-L44TM4EE.cjs.map +0 -1
  129. package/dist/chunk-LSE6JG5I.cjs +0 -20
  130. package/dist/chunk-LSE6JG5I.cjs.map +0 -1
  131. package/dist/chunk-O4OGAAV4.js.map +0 -1
  132. package/dist/chunk-OHQTKGQN.js.map +0 -1
  133. package/dist/chunk-PSKQAFDF.cjs.map +0 -1
  134. package/dist/chunk-QTMTBEBA.js +0 -20
  135. package/dist/chunk-QTMTBEBA.js.map +0 -1
  136. package/dist/chunk-T7P5EIC4.cjs.map +0 -1
  137. package/dist/chunk-TB2RZGDV.js.map +0 -1
  138. package/dist/ui/index.cjs +0 -1961
  139. package/dist/ui/index.cjs.map +0 -1
  140. package/dist/ui/index.d.cts +0 -209
  141. package/dist/ui/index.d.ts +0 -209
  142. package/dist/ui/index.js +0 -1961
  143. package/dist/ui/index.js.map +0 -1
  144. package/dist/ui/kanban.css +0 -194
  145. /package/dist/{chunk-ATUFFNLV.js.map → chunk-3LEIHB7Y.js.map} +0 -0
  146. /package/dist/{chunk-5DLZWW5Y.js.map → chunk-AHHEGTUA.js.map} +0 -0
  147. /package/dist/{chunk-5XKOCKY2.js.map → chunk-X4UHZX6F.js.map} +0 -0
@@ -19,27 +19,23 @@ import {
19
19
  validateCandidates,
20
20
  validateEstimate,
21
21
  workType
22
- } from "./chunk-ATUFFNLV.js";
23
- import {
24
- KanbanError
25
- } from "./chunk-G6FCDLDF.js";
26
- import {
27
- TASK_START_DELAY_MS,
28
- taskDue,
29
- taskEditing,
30
- taskStartAt
31
- } from "./chunk-O4OGAAV4.js";
22
+ } from "./chunk-3LEIHB7Y.js";
32
23
  import {
33
24
  executableWorkItem,
34
25
  sameAutomationBinding
35
- } from "./chunk-5XKOCKY2.js";
26
+ } from "./chunk-X4UHZX6F.js";
36
27
  import {
28
+ KanbanError,
37
29
  randomUUID,
38
30
  sha256HexSync
39
- } from "./chunk-TB2RZGDV.js";
31
+ } from "./chunk-M35X5PHO.js";
40
32
  import {
41
- boardAllowsStart
42
- } from "./chunk-QTMTBEBA.js";
33
+ TASK_START_DELAY_MS,
34
+ boardAllowsStart,
35
+ taskDue,
36
+ taskEditing,
37
+ taskStartAt
38
+ } from "./chunk-3JYLJPE5.js";
43
39
 
44
40
  // src/planning.ts
45
41
  function plan(item) {
@@ -47,6 +43,7 @@ function plan(item) {
47
43
  }
48
44
  function summary(item) {
49
45
  return {
46
+ deletedAt: item.deletedAt,
50
47
  launch: item.launch,
51
48
  analysis: item.analysis,
52
49
  control: item.control,
@@ -90,7 +87,7 @@ function validatePlan(state, item, value) {
90
87
  const all = catalog(state), parent = all.find((i) => i.id === value.parentId);
91
88
  if (value.parentId) {
92
89
  identity(value.parentId);
93
- requireValue(parent && !parent.archivedAt && !terminal(parent) && !["claimed", "review"].includes(parent.status), "PARENT_UNAVAILABLE");
90
+ requireValue(parent && !parent.archivedAt && !parent.deletedAt && !terminal(parent) && parent.control?.action !== "cancel" && !["claimed", "review"].includes(parent.status), "PARENT_UNAVAILABLE");
94
91
  requireValue(parent.id !== item.id && workType(state.board, parent.planning.kind).rank < type.rank, "INVALID_PARENT_KIND");
95
92
  }
96
93
  requireValue(value.execution === "children" || !all.some((child) => child.planning.parentId === item.id), "CHILDREN_REQUIRE_AGGREGATION");
@@ -106,8 +103,11 @@ function validatePlan(state, item, value) {
106
103
  }
107
104
  }
108
105
  function assertReady(state, item) {
106
+ requireValue(!state.board.lifecycle, "BOARD_STOPPING");
107
+ requireValue(!state.board.executionControl?.paused, "BOARD_EXECUTION_PAUSED");
109
108
  requireValue(executable(state.board, item), "PLAN_NOT_EXECUTABLE");
110
109
  requireValue(!item.blocked, "ITEM_BLOCKED");
110
+ requireValue(!item.control || item.control.action === "resume" && item.control.state === "resumed", "TASK_CONTROL_PENDING");
111
111
  const all = catalog(state);
112
112
  requireValue(plan(item).dependencies.every((id) => all.find((i) => i.id === id)?.status === "completed"), "DEPENDENCIES_UNFINISHED");
113
113
  }
@@ -209,7 +209,7 @@ function reviewAmendment(board, command, actor, now) {
209
209
 
210
210
  // src/project-records.ts
211
211
  function isRecordCommand(command) {
212
- return ["append-record", "review-record", "link-record"].includes(command.type);
212
+ return ["append-record", "review-record", "link-record", "request-record-cancellation", "cancel-record", "delete-record"].includes(command.type);
213
213
  }
214
214
  function applyRecord(state, command, actor, now, forceDraft = false) {
215
215
  if (command.type === "append-record") {
@@ -218,7 +218,23 @@ function applyRecord(state, command, actor, now, forceDraft = false) {
218
218
  }
219
219
  const record = state.records?.find((record2) => record2.id === command.recordId);
220
220
  requireValue(record, "RECORD_NOT_FOUND");
221
- if (command.type === "link-record") {
221
+ requireValue(record.deletedAt === void 0, "RECORD_DELETED");
222
+ if (command.type === "request-record-cancellation" || command.type === "cancel-record" || command.type === "delete-record") {
223
+ requireReviewer(state.board, actor);
224
+ requireValue(principal(actor).kind === "human", "FORBIDDEN");
225
+ requireValue(record.kind === "decision", "DECISION_REQUIRED");
226
+ if (command.type !== "request-record-cancellation") requireValue(record.itemIds.every((id) => {
227
+ const item = state.board.items.find((i) => i.id === id) ?? state.archiveIndex?.find((i) => i.id === id);
228
+ return item && ["completed", "cancelled"].includes(item.status);
229
+ }), "RECORD_EXECUTION_UNSETTLED");
230
+ if (command.type === "request-record-cancellation") record.status = record.status === "cancelled" ? "cancelled" : "cancelling";
231
+ else if (command.type === "cancel-record") record.status = "cancelled";
232
+ else {
233
+ requireValue(["cancelled", "rejected", "superseded"].includes(record.status), "RECORD_CANCEL_REQUIRED");
234
+ record.deletedAt = now;
235
+ }
236
+ } else if (command.type === "link-record") {
237
+ requireValue(!["cancelling", "cancelled"].includes(record.status), "RECORD_CANCELLING");
222
238
  validateItems(state, [command.itemId]);
223
239
  if (!record.itemIds.includes(command.itemId)) {
224
240
  requireValue(record.itemIds.length < 100, "RECORD_LINK_LIMIT");
@@ -299,7 +315,7 @@ function queryRecords(state, query = {}, summaries = (state.records ?? []).map(r
299
315
  requireValue(Number.isSafeInteger(limit) && limit > 0 && limit <= 200, "INVALID_LIMIT");
300
316
  if (query.kind) requireValue(["log", "decision"].includes(query.kind), "INVALID_RECORD_KIND");
301
317
  if (query.itemId !== void 0) identity(query.itemId);
302
- if (query.status) requireValue(["draft", "confirmed", "rejected", "superseded"].includes(query.status), "INVALID_RECORD_STATUS");
318
+ if (query.status) requireValue(["draft", "confirmed", "rejected", "superseded", "cancelling", "cancelled"].includes(query.status), "INVALID_RECORD_STATUS");
303
319
  const digest = sha256HexSync(JSON.stringify(filter));
304
320
  let offset = 0;
305
321
  if (cursor) {
@@ -314,7 +330,7 @@ function queryRecords(state, query = {}, summaries = (state.records ?? []).map(r
314
330
  offset = decoded.offset;
315
331
  requireValue(Number.isSafeInteger(offset) && offset >= 0, "INVALID_CURSOR");
316
332
  }
317
- const matched = summaries.filter((record) => (!query.kind || query.kind === record.kind) && (!query.itemId || record.itemIds.includes(query.itemId)) && (!query.status || query.status === record.status)).sort((a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id));
333
+ const matched = summaries.filter((record) => (query.includeDeleted || record.deletedAt === void 0) && (!query.kind || query.kind === record.kind) && (!query.itemId || record.itemIds.includes(query.itemId)) && (!query.status || query.status === record.status)).sort((a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id));
318
334
  const items = matched.slice(offset, offset + limit);
319
335
  return { revision: state.board.revision, total: matched.length, items, ...offset + limit < matched.length ? { nextCursor: encodeURIComponent(JSON.stringify({ revision: state.board.revision, filter: digest, offset: offset + limit })) } : {} };
320
336
  }
@@ -514,6 +530,93 @@ function allocateBoardNumber(raw, high) {
514
530
  return high + 1;
515
531
  }
516
532
 
533
+ // src/workflow-plans.ts
534
+ function requireCoordinator(state, actor) {
535
+ requireValue(state.board.members?.some((member) => principalKey(member) === principalKey(principal(actor)) && member.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
536
+ }
537
+ function publishPlan(state, plan2, actor, apply) {
538
+ requireCoordinator(state, actor);
539
+ requireValue(plan2 && typeof plan2 === "object", "INVALID_WORK_PLAN");
540
+ identity(plan2.id);
541
+ requireValue(plan2.schema === "kanban.plan/1" && typeof plan2.sealed === "boolean" && Array.isArray(plan2.items) && plan2.items.length > 0 && plan2.items.length <= 100, "INVALID_WORK_PLAN");
542
+ const workflow = state.workflow, previous = workflow.plans[plan2.id];
543
+ requireValue(Number.isSafeInteger(plan2.revision) && plan2.revision === (previous?.revision ?? 0) + 1, "PLAN_REVISION_CONFLICT");
544
+ requireValue(!Object.values(workflow.dispatches).some((dispatch) => dispatch.planId === plan2.id && ["prepared", "running", "unknown", "succeeded"].includes(dispatch.outcome)), "PLAN_EXECUTION_UNSETTLED");
545
+ if (plan2.decisionId) {
546
+ const decision = state.records?.find((record) => record.id === plan2.decisionId && record.kind === "decision" && record.status === "confirmed");
547
+ requireValue(decision, "DECISION_NOT_CONFIRMED");
548
+ if (decision.coordination) requireValue(principalKey(decision.coordination.manager) === principalKey(principal(actor)), "DECISION_MANAGER_REQUIRED");
549
+ }
550
+ if (plan2.rootItemId) {
551
+ const root = state.board.items.find((item) => item.id === plan2.rootItemId);
552
+ requireValue(root && !root.owner && ["backlog", "open"].includes(root.status), "PLAN_ROOT_UNAVAILABLE");
553
+ if (root.planning?.execution !== "children") apply({ type: "plan-item", itemId: root.id, planning: { ...plan(root), execution: "children" } });
554
+ }
555
+ const ordered = orderWork(plan2.items, state), definitions = {};
556
+ for (const item of ordered) {
557
+ const digest = sha256HexSync(JSON.stringify(item));
558
+ requireValue(!Object.values(workflow.plans).some((other) => other.id !== plan2.id && other.itemIds.includes(item.id)), "ITEM_IN_OTHER_PLAN");
559
+ const exists = state.board.items.some((old) => old.id === item.id) || state.archiveIndex?.some((old) => old.id === item.id);
560
+ if (exists && previous?.definitions[item.id]?.digest !== digest) {
561
+ requireValue(!previous?.definitions[item.id], "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
562
+ adoptItem(state, item, apply);
563
+ }
564
+ if (!exists) apply({
565
+ type: "create-item",
566
+ item: { id: item.id, title: item.title, responsibility: item.responsibility, acceptance: item.acceptance, eligiblePrincipals: [item.assignee], externalRefs: item.externalRefs },
567
+ planning: { kind: item.workType ?? state.board.defaultWorkType ?? "task", priority: 2, dependencies: item.dependsOn, references: [], ...plan2.rootItemId ? { parentId: plan2.rootItemId } : {} }
568
+ });
569
+ definitions[item.id] = { digest, inputs: item.inputs, outputs: item.outputs ?? [] };
570
+ }
571
+ workflow.plans[plan2.id] = { schema: plan2.schema, id: plan2.id, revision: plan2.revision, decisionId: plan2.decisionId, rootItemId: plan2.rootItemId, sealed: plan2.sealed, itemIds: ordered.map((item) => item.id), definitions };
572
+ }
573
+ function adoptItem(state, definition, apply) {
574
+ const item = state.board.items.find((item2) => item2.id === definition.id);
575
+ requireValue(item && definition.expectedRevision === item.revision && ["backlog", "open"].includes(item.status) && !item.owner && !item.blocked && (!item.control || item.control.action === "resume" && item.control.state === "resumed"), "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
576
+ requireValue(!definition.workType || definition.workType === item.planning?.kind, "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
577
+ requireValue(!definition.externalRefs || definition.externalRefs.every((ref) => item.externalRefs?.some((existing) => JSON.stringify(existing) === JSON.stringify(ref))), "PLAN_RESOURCE_MISMATCH");
578
+ requireValue(item.title === definition.title && item.responsibility === definition.responsibility && item.acceptance === definition.acceptance, "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
579
+ requireValue(plan(item).dependencies.every((id) => definition.dependsOn.includes(id)), "EXISTING_DEPENDENCY_REQUIRED");
580
+ apply({ type: "plan-item", itemId: item.id, planning: { ...plan(item), dependencies: definition.dependsOn } });
581
+ item.eligiblePrincipals = [definition.assignee];
582
+ delete item.eligibleAgentIds;
583
+ validateCandidates(state.board, item, true);
584
+ }
585
+ function orderWork(items, state) {
586
+ for (const item of items) validateDefinition(item);
587
+ const ids = new Set(items.map((item) => item.id));
588
+ requireValue(ids.size === items.length, "DUPLICATE_PLAN_ITEM");
589
+ for (const item of items) {
590
+ identity(item.id);
591
+ requireValue(Array.isArray(item.dependsOn) && item.dependsOn.length <= 100 && new Set(item.dependsOn).size === item.dependsOn.length, "INVALID_DEPENDENCIES");
592
+ requireValue(Object.keys(item.inputs ?? {}).length <= 100 && Array.isArray(item.outputs ?? []) && (item.outputs ?? []).length <= 100 && new Set(item.outputs ?? []).size === (item.outputs ?? []).length, "PLAN_BINDING_LIMIT");
593
+ for (const name of [...Object.keys(item.inputs ?? {}), ...item.outputs ?? []]) requireValue(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name), "INVALID_OUTPUT_NAME");
594
+ for (const binding of Object.values(item.inputs ?? {})) {
595
+ requireValue(binding && item.dependsOn.includes(binding.fromItemId) && typeof binding.output === "string", "INPUT_DEPENDENCY_REQUIRED");
596
+ const producer = items.find((other) => other.id === binding.fromItemId), existing = Object.values(state.workflow.dispatches).some((dispatch) => dispatch.itemId === binding.fromItemId && dispatch.outcome === "accepted" && dispatch.outputs?.some((output) => output.name === binding.output));
597
+ requireValue(producer ? producer.outputs?.includes(binding.output) : existing, "OUTPUT_NOT_DECLARED");
598
+ }
599
+ for (const dependency of item.dependsOn) requireValue(ids.has(dependency) || [...state.board.items, ...state.archiveIndex ?? []].some((old) => old.id === dependency), "DEPENDENCY_NOT_FOUND");
600
+ }
601
+ const ordered = [], remaining = [...items], done = /* @__PURE__ */ new Set();
602
+ while (remaining.length) {
603
+ const next = remaining.findIndex((item2) => item2.dependsOn.every((id) => !ids.has(id) || done.has(id)));
604
+ requireValue(next >= 0, "DEPENDENCY_CYCLE");
605
+ const [item] = remaining.splice(next, 1);
606
+ ordered.push(item);
607
+ done.add(item.id);
608
+ }
609
+ return ordered;
610
+ }
611
+ function validateDefinition(item) {
612
+ requireValue(item && typeof item === "object" && !Array.isArray(item), "INVALID_WORK_DEFINITION");
613
+ const assignee = item.assignee;
614
+ requireValue(assignee && ["agent", "human", "service"].includes(assignee.kind), "INVALID_PRINCIPAL");
615
+ identity(assignee.id);
616
+ if (item.inputs !== void 0) requireValue(item.inputs && typeof item.inputs === "object" && !Array.isArray(item.inputs), "INVALID_INPUT_BINDINGS");
617
+ if (item.outputs !== void 0) requireValue(Array.isArray(item.outputs) && item.outputs.every((name) => typeof name === "string"), "INVALID_OUTPUT_NAMES");
618
+ }
619
+
517
620
  // src/archives.ts
518
621
  function archiveItems(state, ids, reason, now) {
519
622
  text(reason);
@@ -592,6 +695,7 @@ function operate(item, command, now, actor) {
592
695
  item.revision++;
593
696
  }
594
697
  function updatePlan(state, item, command, now) {
698
+ if (command.type === "close-plan") requireValue(item.control?.action !== "cancel", "TASK_CANCELLING");
595
699
  if (command.type === "cancel-item") requireValue(item.launch?.admittedAt === void 0, "TASK_CONTROL_REQUIRED");
596
700
  if (command.type === "publish-proposal") {
597
701
  requireValue(item.status === "proposed", "NOT_PROPOSED");
@@ -640,99 +744,13 @@ function attachReference(item, reference) {
640
744
  item.externalRefs.push(reference);
641
745
  }
642
746
 
643
- // src/workflow-plans.ts
644
- function requireCoordinator(state, actor) {
645
- requireValue(state.board.members?.some((member) => principalKey(member) === principalKey(principal(actor)) && member.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
646
- }
647
- function publishPlan(state, plan2, actor, apply) {
648
- requireCoordinator(state, actor);
649
- requireValue(plan2 && typeof plan2 === "object", "INVALID_WORK_PLAN");
650
- identity(plan2.id);
651
- requireValue(plan2.schema === "kanban.plan/1" && typeof plan2.sealed === "boolean" && Array.isArray(plan2.items) && plan2.items.length > 0 && plan2.items.length <= 100, "INVALID_WORK_PLAN");
652
- const workflow = state.workflow, previous = workflow.plans[plan2.id];
653
- requireValue(Number.isSafeInteger(plan2.revision) && plan2.revision === (previous?.revision ?? 0) + 1, "PLAN_REVISION_CONFLICT");
654
- requireValue(!Object.values(workflow.dispatches).some((dispatch) => dispatch.planId === plan2.id && ["prepared", "running", "unknown", "succeeded"].includes(dispatch.outcome)), "PLAN_EXECUTION_UNSETTLED");
655
- if (plan2.decisionId) {
656
- const decision = state.records?.find((record) => record.id === plan2.decisionId && record.kind === "decision" && record.status === "confirmed");
657
- requireValue(decision, "DECISION_NOT_CONFIRMED");
658
- if (decision.coordination) requireValue(principalKey(decision.coordination.manager) === principalKey(principal(actor)), "DECISION_MANAGER_REQUIRED");
659
- }
660
- if (plan2.rootItemId) {
661
- const root = state.board.items.find((item) => item.id === plan2.rootItemId);
662
- requireValue(root && !root.owner && ["backlog", "open"].includes(root.status), "PLAN_ROOT_UNAVAILABLE");
663
- if (root.planning?.execution !== "children") apply({ type: "plan-item", itemId: root.id, planning: { ...plan(root), execution: "children" } });
664
- }
665
- const ordered = orderWork(plan2.items, state), definitions = {};
666
- for (const item of ordered) {
667
- const digest = sha256HexSync(JSON.stringify(item));
668
- requireValue(!Object.values(workflow.plans).some((other) => other.id !== plan2.id && other.itemIds.includes(item.id)), "ITEM_IN_OTHER_PLAN");
669
- const exists = state.board.items.some((old) => old.id === item.id) || state.archiveIndex?.some((old) => old.id === item.id);
670
- if (exists && previous?.definitions[item.id]?.digest !== digest) {
671
- requireValue(!previous?.definitions[item.id], "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
672
- adoptItem(state, item, apply);
673
- }
674
- if (!exists) apply({
675
- type: "create-item",
676
- item: { id: item.id, title: item.title, responsibility: item.responsibility, acceptance: item.acceptance, eligiblePrincipals: [item.assignee], externalRefs: item.externalRefs },
677
- planning: { kind: item.workType ?? state.board.defaultWorkType ?? "task", priority: 2, dependencies: item.dependsOn, references: [], ...plan2.rootItemId ? { parentId: plan2.rootItemId } : {} }
678
- });
679
- definitions[item.id] = { digest, inputs: item.inputs, outputs: item.outputs ?? [] };
680
- }
681
- workflow.plans[plan2.id] = { schema: plan2.schema, id: plan2.id, revision: plan2.revision, decisionId: plan2.decisionId, rootItemId: plan2.rootItemId, sealed: plan2.sealed, itemIds: ordered.map((item) => item.id), definitions };
682
- }
683
- function adoptItem(state, definition, apply) {
684
- const item = state.board.items.find((item2) => item2.id === definition.id);
685
- requireValue(item && definition.expectedRevision === item.revision && ["backlog", "open"].includes(item.status) && !item.owner && !item.blocked, "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
686
- requireValue(!definition.workType || definition.workType === item.planning?.kind, "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
687
- requireValue(!definition.externalRefs || definition.externalRefs.every((ref) => item.externalRefs?.some((existing) => JSON.stringify(existing) === JSON.stringify(ref))), "PLAN_RESOURCE_MISMATCH");
688
- requireValue(item.title === definition.title && item.responsibility === definition.responsibility && item.acceptance === definition.acceptance, "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
689
- requireValue(plan(item).dependencies.every((id) => definition.dependsOn.includes(id)), "EXISTING_DEPENDENCY_REQUIRED");
690
- apply({ type: "plan-item", itemId: item.id, planning: { ...plan(item), dependencies: definition.dependsOn } });
691
- item.eligiblePrincipals = [definition.assignee];
692
- delete item.eligibleAgentIds;
693
- validateCandidates(state.board, item, true);
694
- }
695
- function orderWork(items, state) {
696
- for (const item of items) validateDefinition(item);
697
- const ids = new Set(items.map((item) => item.id));
698
- requireValue(ids.size === items.length, "DUPLICATE_PLAN_ITEM");
699
- for (const item of items) {
700
- identity(item.id);
701
- requireValue(Array.isArray(item.dependsOn) && item.dependsOn.length <= 100 && new Set(item.dependsOn).size === item.dependsOn.length, "INVALID_DEPENDENCIES");
702
- requireValue(Object.keys(item.inputs ?? {}).length <= 100 && Array.isArray(item.outputs ?? []) && (item.outputs ?? []).length <= 100 && new Set(item.outputs ?? []).size === (item.outputs ?? []).length, "PLAN_BINDING_LIMIT");
703
- for (const name of [...Object.keys(item.inputs ?? {}), ...item.outputs ?? []]) requireValue(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name), "INVALID_OUTPUT_NAME");
704
- for (const binding of Object.values(item.inputs ?? {})) {
705
- requireValue(binding && item.dependsOn.includes(binding.fromItemId) && typeof binding.output === "string", "INPUT_DEPENDENCY_REQUIRED");
706
- const producer = items.find((other) => other.id === binding.fromItemId), existing = Object.values(state.workflow.dispatches).some((dispatch) => dispatch.itemId === binding.fromItemId && dispatch.outcome === "accepted" && dispatch.outputs?.some((output) => output.name === binding.output));
707
- requireValue(producer ? producer.outputs?.includes(binding.output) : existing, "OUTPUT_NOT_DECLARED");
708
- }
709
- for (const dependency of item.dependsOn) requireValue(ids.has(dependency) || [...state.board.items, ...state.archiveIndex ?? []].some((old) => old.id === dependency), "DEPENDENCY_NOT_FOUND");
710
- }
711
- const ordered = [], remaining = [...items], done = /* @__PURE__ */ new Set();
712
- while (remaining.length) {
713
- const next = remaining.findIndex((item2) => item2.dependsOn.every((id) => !ids.has(id) || done.has(id)));
714
- requireValue(next >= 0, "DEPENDENCY_CYCLE");
715
- const [item] = remaining.splice(next, 1);
716
- ordered.push(item);
717
- done.add(item.id);
718
- }
719
- return ordered;
720
- }
721
- function validateDefinition(item) {
722
- requireValue(item && typeof item === "object" && !Array.isArray(item), "INVALID_WORK_DEFINITION");
723
- const assignee = item.assignee;
724
- requireValue(assignee && ["agent", "human", "service"].includes(assignee.kind), "INVALID_PRINCIPAL");
725
- identity(assignee.id);
726
- if (item.inputs !== void 0) requireValue(item.inputs && typeof item.inputs === "object" && !Array.isArray(item.inputs), "INVALID_INPUT_BINDINGS");
727
- if (item.outputs !== void 0) requireValue(Array.isArray(item.outputs) && item.outputs.every((name) => typeof name === "string"), "INVALID_OUTPUT_NAMES");
728
- }
729
-
730
747
  // src/task-commands.ts
731
748
  var types = ["set-launch-mode", "retry-analysis", "begin-edit", "end-edit", "save-edit", "request-run", "admit-item", "report-analysis", "request-control", "control-receipt", "report-issue", "resolve-issue"];
732
749
  function taskCommand(command) {
733
750
  return types.includes(command.type);
734
751
  }
735
752
  function assertDefinitionEditable(item, now, editId, actor) {
753
+ requireValue(item.control?.action !== "cancel", "TASK_CANCELLING");
736
754
  requireValue(["proposed", "backlog", "open"].includes(item.status) && !item.owner && item.launch?.admittedAt === void 0, "TASK_DEFINITION_FROZEN");
737
755
  if (taskEditing(item, now)) requireValue(item.launch.editing.id === editId && !!actor && principalKey(item.launch.editing.principal) === principalKey(principal(actor)), "EDIT_LEASE_ACTIVE");
738
756
  }
@@ -756,6 +774,7 @@ function applyTaskCommand(state, command, actor, now) {
756
774
  requireCoordinator(state, actor);
757
775
  assertDefinitionEditable(item, now);
758
776
  requireValue(boardAllowsStart(state.board, item), "BOARD_EXECUTION_PAUSED");
777
+ requireValue(!item.control || item.control.action === "resume" && item.control.state === "resumed", "TASK_CONTROL_PENDING");
759
778
  requireValue(command.trigger === "manual" ? item.launch.requestedAt !== void 0 : taskDue(item, now), "START_NOT_DUE");
760
779
  requireValue(!item.blocked && item.control?.action !== "pause", "ITEM_BLOCKED");
761
780
  requireValue(item.planning?.dependencies.every((id) => catalog(state).find((i) => i.id === id)?.status === "completed") ?? true, "DEPENDENCIES_UNFINISHED");
@@ -764,15 +783,13 @@ function applyTaskCommand(state, command, actor, now) {
764
783
  requireCoordinator(state, actor);
765
784
  identity(command.operationId);
766
785
  requireValue(command.status.itemId === item.id, "ANALYSIS_SCOPE_MISMATCH");
786
+ requireValue(item.control?.action !== "cancel" && !["completed", "cancelled"].includes(item.status), "TASK_CANCELLING");
767
787
  item.analysis = structuredClone(command.status);
768
- } else if (command.type === "request-control") requestControl(state, item, command);
769
- else if (command.type === "control-receipt") {
770
- requireCoordinator(state, actor);
771
- requireValue(item.control?.id === command.controlId, "STALE_CONTROL_RECEIPT");
772
- requireValue(["accepted", "paused", "resumed", "failed", "unknown"].includes(command.state), "INVALID_CONTROL_STATE");
773
- item.control.state = command.state;
774
- item.control.code = command.code;
775
- } else issues(state, item, command, actor);
788
+ } else if (command.type === "request-control") {
789
+ requestControl(state, item, command);
790
+ if (command.action === "cancel") cancelChildren(state, item, command.controlId);
791
+ } else if (command.type === "control-receipt") applyControlReceipt(state, item, command, actor, now);
792
+ else issues(state, item, command, actor);
776
793
  item.updatedAt = now;
777
794
  item.revision++;
778
795
  }
@@ -808,17 +825,18 @@ function edit(state, item, command, actor, now) {
808
825
  delete item.launch.editing;
809
826
  }
810
827
  function requestControl(state, item, command) {
811
- requireValue(!["completed", "cancelled", "review"].includes(item.status), "ITEM_COMPLETED");
828
+ requireValue(!["completed", "cancelled"].includes(item.status), "ITEM_COMPLETED");
812
829
  identity(command.controlId);
813
- requireValue(["pause", "resume"].includes(command.action), "INVALID_CONTROL_ACTION");
830
+ requireValue(["pause", "resume", "cancel"].includes(command.action), "INVALID_CONTROL_ACTION");
814
831
  if (item.control?.id === command.controlId) {
815
832
  requireValue(item.control.action === command.action, "IDEMPOTENCY_CONFLICT");
816
833
  return;
817
834
  }
818
- requireValue(!item.control || !["requested", "unknown"].includes(item.control.state), "CONTROL_UNSETTLED");
819
- const dispatch = Object.values(state.workflow?.dispatches ?? {}).find((d) => d.itemId === item.id && ["prepared", "running", "unknown"].includes(d.outcome));
820
- requireValue(dispatch || item.launch?.admittedAt === void 0, "TASK_CONTROL_UNSUPPORTED");
821
- requireValue(command.action === "pause" || item.control?.action === "pause", "TASK_NOT_PAUSED");
835
+ if (command.action === "cancel" && item.control?.action === "cancel" && item.control.state !== "failed") return;
836
+ requireValue(command.action === "cancel" || !item.control || !["requested", "unknown"].includes(item.control.state), "CONTROL_UNSETTLED");
837
+ const dispatch = Object.values(state.workflow?.dispatches ?? {}).filter((d) => d.itemId === item.id).sort((a, b) => b.attempt - a.attempt)[0];
838
+ requireValue(!item.owner || dispatch || command.action === "cancel", "TASK_CONTROL_UNSUPPORTED");
839
+ requireValue(command.action !== "resume" || item.control?.action === "pause", "TASK_NOT_PAUSED");
822
840
  item.control = { id: command.controlId, dispatchId: dispatch?.id, action: command.action, state: "requested" };
823
841
  }
824
842
  function issues(state, item, command, actor) {
@@ -856,6 +874,84 @@ function setMode(item, mode) {
856
874
  item.launch.mode = mode;
857
875
  delete item.launch.requestedAt;
858
876
  }
877
+ function cancelChildren(state, parent, controlId) {
878
+ const ids = /* @__PURE__ */ new Set([parent.id]);
879
+ for (let pass = 0; pass < state.board.items.length; pass++) {
880
+ const children = state.board.items.filter((item) => item.planning?.parentId && ids.has(item.planning.parentId) && !ids.has(item.id));
881
+ if (!children.length) return;
882
+ for (const child of children) {
883
+ ids.add(child.id);
884
+ if (["completed", "cancelled"].includes(child.status) || child.control?.action === "cancel") continue;
885
+ requestControl(state, child, { type: "request-control", itemId: child.id, controlId: sha256HexSync(JSON.stringify([controlId, child.id])), action: "cancel" });
886
+ child.updatedAt = parent.updatedAt;
887
+ child.revision++;
888
+ }
889
+ }
890
+ }
891
+ function applyControlReceipt(state, item, command, actor, now) {
892
+ requireCoordinator(state, actor);
893
+ requireValue(item.control?.id === command.controlId, "STALE_CONTROL_RECEIPT");
894
+ requireValue(["accepted", "paused", "resumed", "cancelled", "failed", "unknown"].includes(command.state), "INVALID_CONTROL_STATE");
895
+ requireValue(command.code === void 0 || /^[A-Z][A-Z0-9_]{0,79}$/.test(command.code), "INVALID_ERROR_CODE");
896
+ if (command.state === "cancelled") {
897
+ requireValue(item.control.action === "cancel", "INVALID_CONTROL_STATE");
898
+ for (const dispatch of Object.values(state.workflow?.dispatches ?? {}).filter((d) => d.itemId === item.id && ["prepared", "running", "unknown", "succeeded"].includes(d.outcome))) {
899
+ requireValue(dispatch.id === item.control.dispatchId && item.owner?.token === dispatch.fence, "STALE_EXECUTION_RESULT");
900
+ dispatch.outcome = "cancelled";
901
+ dispatch.code = "USER_CANCELLED";
902
+ }
903
+ requireValue(!catalog(state).some((i) => i.planning.parentId === item.id && !["completed", "cancelled"].includes(i.status)), "CHILDREN_UNFINISHED");
904
+ item.status = "cancelled";
905
+ item.closedAt = now;
906
+ item.closureReason = "USER_CANCELLED";
907
+ delete item.owner;
908
+ delete item.transfer;
909
+ delete item.blocked;
910
+ delete item.launch.editing;
911
+ }
912
+ requireValue(item.control.state !== "cancelled" || command.state === "cancelled", "STALE_CONTROL_RECEIPT");
913
+ item.control.state = command.state;
914
+ item.control.code = command.code;
915
+ }
916
+
917
+ // src/board-stop.ts
918
+ function stopBoard(state, command, actor, now) {
919
+ requireCoordinator(state, actor);
920
+ requireValue(actor.kind === "human", "FORBIDDEN");
921
+ identity(command.controlId);
922
+ if (command.type === "observe-board-stop") {
923
+ requireValue(state.board.lifecycle?.operationId === command.controlId, "STALE_CONTROL_RECEIPT");
924
+ requireValue(["waiting", "ready", "blocked"].includes(command.phase), "INVALID_CONTROL_STATE");
925
+ requireValue(command.code === void 0 || /^[A-Z][A-Z0-9_]{0,79}$/.test(command.code), "INVALID_ERROR_CODE");
926
+ state.board.lifecycle.phase = command.phase;
927
+ state.board.lifecycle.code = command.code;
928
+ return;
929
+ }
930
+ requireValue(["cancel", "delete"].includes(command.intent) && ["drain", "cancel"].includes(command.mode), "INVALID_BOARD_CONTROL");
931
+ const old = state.board.lifecycle;
932
+ if (old) {
933
+ requireValue(old.intent === "cancel" && command.intent === "delete" || old.operationId === command.controlId, "BOARD_STOP_PENDING");
934
+ if (old.operationId === command.controlId) return;
935
+ }
936
+ state.board.lifecycle = { operationId: command.controlId, intent: command.intent, mode: command.mode, phase: "waiting" };
937
+ state.board.executionControl = { ...state.board.executionControl, mode: "manual", paused: true, controls: {} };
938
+ if (state.board.automation) state.board.automation.enabled = false;
939
+ if (command.mode !== "cancel") return;
940
+ for (const item of state.board.items) if (!["completed", "cancelled"].includes(item.status)) applyTaskCommand(
941
+ state,
942
+ { type: "request-control", itemId: item.id, controlId: sha256HexSync(JSON.stringify([command.controlId, item.id])), action: "cancel" },
943
+ actor,
944
+ now
945
+ );
946
+ }
947
+ function protectStoppedBoard(state, command) {
948
+ if (!state.board.lifecycle) return;
949
+ if (command.type === "append-record" && command.record.kind === "decision") requireValue(false, "BOARD_STOPPING");
950
+ const fresh = ["create-item", "admit-item", "prepare-dispatch", "continue-execution", "publish-plan", "request-run", "apply-discovery", "begin-edit", "save-edit"];
951
+ requireValue(!fresh.includes(command.type), "BOARD_STOPPING");
952
+ if (command.type === "set-board-control") requireValue(command.paused !== false && command.mode !== "automatic", "BOARD_STOPPING");
953
+ if (command.type === "configure-automation") requireValue(!command.settings.enabled, "BOARD_STOPPING");
954
+ }
859
955
 
860
956
  // src/board-control.ts
861
957
  function setBoardControl(state, command, actor, now, token) {
@@ -871,7 +967,7 @@ function setBoardControl(state, command, actor, now, token) {
871
967
  if (policy2.paused) policy2.controls = {};
872
968
  state.board.items.forEach((item, index) => {
873
969
  if (["completed", "cancelled", "review"].includes(item.status)) return;
874
- const active = Object.values(state.workflow?.dispatches ?? {}).some((d) => d.itemId === item.id && ["prepared", "running", "unknown"].includes(d.outcome));
970
+ const active = item.analysis?.state === "analyzing" || item.launch?.admittedAt !== void 0 || Object.values(state.workflow?.dispatches ?? {}).some((d) => d.itemId === item.id && ["prepared", "running", "unknown"].includes(d.outcome));
875
971
  const managed = policy2.controls[item.id] === item.control?.id;
876
972
  if (policy2.paused ? !active || item.control?.action === "pause" : !managed || item.control?.action !== "pause") return;
877
973
  if (item.control && ["requested", "unknown"].includes(item.control.state)) return;
@@ -1064,7 +1160,7 @@ function protectWorkflowItem(state, command) {
1064
1160
  requireValue(!["edit-item", "plan-item", "review-amendment"].includes(command.type), "PLAN_ITEM_CHANGED_REQUIRES_NEW_ID");
1065
1161
  if (plan2.sealed) requireValue(!["claim", "submit", "review", "close-plan"].includes(command.type), "WORKFLOW_COMMAND_REQUIRED");
1066
1162
  const live = Object.values(state.workflow.dispatches).some((dispatch) => dispatch.itemId === command.itemId && ["prepared", "running", "unknown", "succeeded"].includes(dispatch.outcome));
1067
- if (live) requireValue(!["request-transfer", "accept-transfer", "release-item", "cancel-item"].includes(command.type), "EXECUTION_UNSETTLED");
1163
+ if (live) requireValue(!["delete-item", "request-transfer", "accept-transfer", "release-item", "cancel-item"].includes(command.type), "EXECUTION_UNSETTLED");
1068
1164
  }
1069
1165
  function applyWorkflow(state, command, actor, now, apply) {
1070
1166
  state.workflow ??= { plans: {}, dispatches: {}, ready: [], completedPlans: [] };
@@ -1082,6 +1178,8 @@ function applyWorkflow(state, command, actor, now, apply) {
1082
1178
  function prepare(state, command, actor, now, apply) {
1083
1179
  identity(command.dispatchId);
1084
1180
  const workflow = state.workflow;
1181
+ const controlled = state.board.items.find((item2) => item2.id === command.itemId);
1182
+ requireValue(!controlled?.control || controlled.control.action === "resume" && controlled.control.state === "resumed", "TASK_CONTROL_PENDING");
1085
1183
  requireValue(!workflow.dispatches[command.dispatchId], "DISPATCH_EXISTS");
1086
1184
  refreshReady(state);
1087
1185
  requireValue(workflow.ready.includes(command.itemId), "WORK_NOT_READY");
@@ -1147,6 +1245,7 @@ function observe(state, command, actor, now) {
1147
1245
  function accept(state, command, actor, apply) {
1148
1246
  requireCoordinator(state, actor);
1149
1247
  const dispatch = activeDispatch(state, command.dispatchId);
1248
+ requireValue(state.board.items.find((item) => item.id === dispatch.itemId)?.control?.action !== "cancel", "TASK_CANCELLING");
1150
1249
  requireValue(dispatch.outcome === "succeeded", "EXECUTION_NOT_SUCCEEDED");
1151
1250
  apply({ type: "review", itemId: dispatch.itemId, accept: command.accept }, actor);
1152
1251
  dispatch.outcome = command.accept ? "accepted" : "rejected";
@@ -1171,7 +1270,7 @@ function refreshReady(state) {
1171
1270
  if (!state.workflow) return;
1172
1271
  const all = catalog(state), included = new Set(Object.values(state.workflow.plans).filter((plan2) => plan2.sealed).flatMap((plan2) => plan2.itemIds));
1173
1272
  state.workflow.completedPlans = Object.values(state.workflow.plans).filter((plan2) => plan2.sealed && plan2.itemIds.every((id) => all.find((item) => item.id === id)?.status === "completed")).map((plan2) => plan2.id);
1174
- state.workflow.ready = state.board.items.filter((item) => included.has(item.id) && executableWorkItem(state.board, item) && ["backlog", "open"].includes(item.status) && !item.owner && !item.blocked && item.control?.action !== "pause" && (item.planning?.dependencies ?? []).every((id) => all.find((other) => other.id === id)?.status === "completed")).map((item) => item.id);
1273
+ state.workflow.ready = state.board.items.filter((item) => included.has(item.id) && executableWorkItem(state.board, item) && ["backlog", "open"].includes(item.status) && !item.owner && !item.blocked && (!item.control || item.control.action === "resume" && item.control.state === "resumed") && (item.planning?.dependencies ?? []).every((id) => all.find((other) => other.id === id)?.status === "completed")).map((item) => item.id);
1175
1274
  }
1176
1275
 
1177
1276
  // src/transitions.ts
@@ -1213,6 +1312,7 @@ function claim(state, item, command, actor, now, token) {
1213
1312
  if (actor.sessionId) participant(item.participants, actor.sessionId, actor.agentId, "owner", true, now);
1214
1313
  }
1215
1314
  function review(state, item, accept2, actor, now) {
1315
+ requireValue(item.control?.action !== "cancel", "TASK_CANCELLING");
1216
1316
  requireValue(item.status === "review", "NOT_IN_REVIEW");
1217
1317
  const key = principalKey(principal(actor));
1218
1318
  const registered = actor.sessionId && item.participants.some((p) => p.sessionId === actor.sessionId && p.roles.includes("reviewer")) || state.board.members?.some((m) => principalKey(m) === key && m.roles?.includes("reviewer"));
@@ -1269,6 +1369,8 @@ function transferTarget(value) {
1269
1369
  function reduce(state, command, actor, operationId, now, token, workflowInternal = false) {
1270
1370
  requireValue(state.board.deletedAt === void 0, "BOARD_DELETED");
1271
1371
  numberEntities(state);
1372
+ protectStoppedBoard(state, command);
1373
+ if ("itemId" in command && command.itemId) requireValue(state.board.items.find((i) => i.id === command.itemId)?.deletedAt === void 0, "ITEM_DELETED");
1272
1374
  if (!workflowInternal) protectWorkflowItem(state, command);
1273
1375
  const board = state.board;
1274
1376
  if (command.type === "delete-board") return deleteBoard(state, actor, operationId, now);
@@ -1278,8 +1380,18 @@ function reduce(state, command, actor, operationId, now, token, workflowInternal
1278
1380
  return state;
1279
1381
  }
1280
1382
  let recordEvent = command.type !== "renew";
1281
- if (command.type === "set-board-control") setBoardControl(state, command, actor, now, token);
1282
- else if (taskCommand(command)) applyTaskCommand(state, command, actor, now);
1383
+ if (command.type === "request-board-stop" || command.type === "observe-board-stop") stopBoard(state, command, actor, now);
1384
+ else if (command.type === "set-board-control") setBoardControl(state, command, actor, now, token);
1385
+ else if (command.type === "delete-item") {
1386
+ const item = itemFor(board, command.itemId);
1387
+ requireValue(principal(actor).kind === "human", "FORBIDDEN");
1388
+ requireValue(terminal(item), "ITEM_DELETE_REQUIRES_CLOSED");
1389
+ delete item.owner;
1390
+ delete item.transfer;
1391
+ item.deletedAt = now;
1392
+ item.updatedAt = now;
1393
+ item.revision++;
1394
+ } else if (taskCommand(command)) applyTaskCommand(state, command, actor, now);
1283
1395
  else if (isWorkflowCommand(command)) recordEvent = applyWorkflow(state, command, actor, now, (inner, owner = actor) => {
1284
1396
  reduce(state, inner, owner, operationId, now, token, true);
1285
1397
  });
@@ -1419,7 +1531,8 @@ function escalationReasons(board, item, now) {
1419
1531
  return result;
1420
1532
  }
1421
1533
  function eventData(command, state) {
1422
- if (command.type === "set-board-control") return { code: "BOARD_" + (state.board.executionControl?.paused ? "PAUSE_REQUESTED" : state.board.executionControl?.mode?.toUpperCase()) };
1534
+ if (command.type === "request-board-stop" || command.type === "observe-board-stop") return { code: state.board.lifecycle?.code, reason: state.board.lifecycle?.intent, itemIds: state.board.items.filter((item) => item.control?.action === "cancel").map((item) => item.id) };
1535
+ else if (command.type === "set-board-control") return { code: "BOARD_" + (state.board.executionControl?.paused ? "PAUSE_REQUESTED" : state.board.executionControl?.mode?.toUpperCase()) };
1423
1536
  if (command.type === "apply-discovery") return discoveryEvent(state, command.batch);
1424
1537
  if (command.type === "report-analysis") return { code: command.status.code, reason: "analysis-" + command.status.state };
1425
1538
  if (command.type === "control-receipt") return { code: command.code, reason: "control-" + command.state };
@@ -1453,8 +1566,9 @@ function discoveryEvent(state, batch) {
1453
1566
  function deleteBoard(state, actor, operationId, now) {
1454
1567
  const owner = principal(actor), board = state.board;
1455
1568
  requireValue(owner.kind === "human" && board.members?.some((member) => member.kind === owner.kind && member.id === owner.id && member.roles?.includes("coordinator")), "COORDINATOR_REQUIRED");
1456
- requireValue(!board.items.some((item) => item.owner || item.transfer || item.status === "claimed"), "BOARD_EXECUTION_ACTIVE");
1569
+ requireValue(!board.items.some((item) => !terminal(item) && (item.owner || item.transfer || item.status === "claimed")), "BOARD_EXECUTION_ACTIVE");
1457
1570
  requireValue(!Object.values(state.workflow?.dispatches ?? {}).some((dispatch) => ["prepared", "running", "unknown", "succeeded"].includes(dispatch.outcome)), "BOARD_EXECUTION_UNSETTLED");
1571
+ requireValue(!board.items.some((item) => item.control?.action === "cancel" && item.control.state !== "cancelled" && !terminal(item)), "BOARD_CONTROL_UNSETTLED");
1458
1572
  board.deletedAt = now;
1459
1573
  board.revision++;
1460
1574
  if (board.automation) board.automation.enabled = false;
@@ -1495,4 +1609,4 @@ export {
1495
1609
  related,
1496
1610
  escalationReasons
1497
1611
  };
1498
- //# sourceMappingURL=chunk-OHQTKGQN.js.map
1612
+ //# sourceMappingURL=chunk-XKDZHAUE.js.map