@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.
- package/LICENSE +674 -0
- package/README.md +73 -29
- package/dist/{activity-C6xDM-cg.d.cts → activity-BbC4sgKL.d.cts} +28 -2
- package/dist/{activity-Dw8DOw0L.d.ts → activity-Czlan5xW.d.ts} +28 -2
- package/dist/activity-module-DaO1Rry0.d.cts +22 -0
- package/dist/activity-module-T22wliZu.d.ts +22 -0
- package/dist/activity-ports-Dqxa86zT.d.cts +12 -0
- package/dist/activity-ports-cxfrelaL.d.ts +12 -0
- package/dist/activity.cjs +10 -282
- package/dist/activity.cjs.map +1 -1
- package/dist/activity.d.cts +8 -30
- package/dist/activity.d.ts +8 -30
- package/dist/activity.js +9 -281
- package/dist/activity.js.map +1 -1
- package/dist/{chunk-O4OGAAV4.js → chunk-3JYLJPE5.js} +29 -5
- package/dist/chunk-3JYLJPE5.js.map +1 -0
- package/dist/{chunk-ATUFFNLV.js → chunk-3LEIHB7Y.js} +5 -5
- package/dist/chunk-4XF6LQSU.cjs.map +1 -1
- package/dist/chunk-5Q7WB63A.cjs +282 -0
- package/dist/chunk-5Q7WB63A.cjs.map +1 -0
- package/dist/{chunk-PSKQAFDF.cjs → chunk-7KDOTBRX.cjs} +1 -1
- package/dist/chunk-7KDOTBRX.cjs.map +1 -0
- package/dist/{chunk-5DLZWW5Y.js → chunk-AHHEGTUA.js} +3 -3
- package/dist/chunk-AOHQCX7K.cjs +95 -0
- package/dist/chunk-AOHQCX7K.cjs.map +1 -0
- package/dist/{chunk-IUI5CI2S.cjs → chunk-DINLR6E6.cjs} +96 -92
- package/dist/chunk-DINLR6E6.cjs.map +1 -0
- package/dist/{chunk-L44TM4EE.cjs → chunk-EYTVC2H5.cjs} +568 -454
- package/dist/chunk-EYTVC2H5.cjs.map +1 -0
- package/dist/chunk-FEE2YBZ7.js +1972 -0
- package/dist/chunk-FEE2YBZ7.js.map +1 -0
- package/dist/chunk-FNO5K3A7.js +282 -0
- package/dist/chunk-FNO5K3A7.js.map +1 -0
- package/dist/chunk-IHHDXZPZ.js +382 -0
- package/dist/chunk-IHHDXZPZ.js.map +1 -0
- package/dist/chunk-J3EAIVA3.js +280 -0
- package/dist/chunk-J3EAIVA3.js.map +1 -0
- package/dist/{chunk-DFTOTBGF.js → chunk-KH3WF3K4.js} +20 -16
- package/dist/chunk-KH3WF3K4.js.map +1 -0
- package/dist/{chunk-TB2RZGDV.js → chunk-M35X5PHO.js} +14 -1
- package/dist/chunk-M35X5PHO.js.map +1 -0
- package/dist/chunk-QRGZD2JU.cjs +1972 -0
- package/dist/chunk-QRGZD2JU.cjs.map +1 -0
- package/dist/{chunk-4KF7KUAI.cjs → chunk-RBOER6EW.cjs} +6 -6
- package/dist/chunk-RBOER6EW.cjs.map +1 -0
- package/dist/{chunk-6MN3H3YF.cjs → chunk-RSH6QSEC.cjs} +16 -3
- package/dist/chunk-RSH6QSEC.cjs.map +1 -0
- package/dist/{chunk-T7P5EIC4.cjs → chunk-VMRDXOLX.cjs} +33 -33
- package/dist/chunk-VMRDXOLX.cjs.map +1 -0
- package/dist/{chunk-5XKOCKY2.js → chunk-X4UHZX6F.js} +1 -1
- package/dist/chunk-XBSGUVCY.cjs +382 -0
- package/dist/chunk-XBSGUVCY.cjs.map +1 -0
- package/dist/{chunk-OHQTKGQN.js → chunk-XKDZHAUE.js} +242 -128
- package/dist/chunk-XKDZHAUE.js.map +1 -0
- package/dist/chunk-ZUZGAO6G.cjs +280 -0
- package/dist/chunk-ZUZGAO6G.cjs.map +1 -0
- package/dist/{content-CAIp1njn.d.cts → content-C5aJZFlw.d.cts} +1 -1
- package/dist/{content-CGMJdyH_.d.ts → content-hLH5BNud.d.ts} +1 -1
- package/dist/contracts/index.cjs +13 -3
- package/dist/contracts/index.cjs.map +1 -1
- package/dist/contracts/index.d.cts +5 -4
- package/dist/contracts/index.d.ts +5 -4
- package/dist/contracts/index.js +14 -4
- package/dist/{discovery-DxpirOz3.d.cts → discovery-9E-LfwIw.d.cts} +1 -1
- package/dist/{discovery-gSqWpuRm.d.ts → discovery-D60yrjej.d.ts} +1 -1
- package/dist/discovery.cjs +63 -65
- package/dist/discovery.cjs.map +1 -1
- package/dist/discovery.d.cts +4 -4
- package/dist/discovery.d.ts +4 -4
- package/dist/discovery.js +5 -7
- package/dist/discovery.js.map +1 -1
- package/dist/execution-bridge-fpCnH6z5.d.cts +52 -0
- package/dist/execution-bridge-rk0uVy_n.d.ts +52 -0
- package/dist/execution.cjs +6 -356
- package/dist/execution.cjs.map +1 -1
- package/dist/execution.d.cts +4 -51
- package/dist/execution.d.ts +4 -51
- package/dist/execution.js +9 -359
- package/dist/execution.js.map +1 -1
- package/dist/index.cjs +10 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -16
- package/dist/index.d.ts +9 -16
- package/dist/index.js +10 -12
- package/dist/index.js.map +1 -1
- package/dist/{module-CWNK2BMj.d.ts → module-CYfE7YhM.d.ts} +2 -2
- package/dist/{module-BbvwdCDF.d.cts → module-Ca9L3qCn.d.cts} +2 -2
- package/dist/{planning-CcFZYdIL.d.cts → planning-CHrOqQ6b.d.cts} +45 -10
- package/dist/{planning-CcFZYdIL.d.ts → planning-CHrOqQ6b.d.ts} +45 -10
- package/dist/{ports-Bn6ro2FB.d.ts → ports-B0McVg7B.d.ts} +4 -3
- package/dist/ports-BeGO1VRI.d.cts +49 -0
- package/dist/{ports-CaL6geqR.d.cts → ports-GQy0x-RI.d.cts} +4 -3
- package/dist/ports-ykGiNUSx.d.ts +49 -0
- package/dist/runtime/index.cjs +205 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.cts +75 -0
- package/dist/runtime/index.d.ts +75 -0
- package/dist/runtime/index.js +205 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/integration.cjs +423 -0
- package/dist/runtime/integration.cjs.map +1 -0
- package/dist/runtime/integration.d.cts +658 -0
- package/dist/runtime/integration.d.ts +658 -0
- package/dist/runtime/integration.js +423 -0
- package/dist/runtime/integration.js.map +1 -0
- package/dist/storage.cjs +9 -277
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.d.cts +6 -6
- package/dist/storage.d.ts +6 -6
- package/dist/storage.js +11 -279
- package/dist/storage.js.map +1 -1
- package/dist/{store-q63eSMoT.d.cts → store-DPwHzGiz.d.cts} +2 -2
- package/dist/{store-Ct0gOAAg.d.ts → store-DgdrPCV9.d.ts} +2 -2
- package/dist/task-projection-CSorlMws.d.ts +11 -0
- package/dist/task-projection-DKKB_ueb.d.cts +11 -0
- package/examples/standalone.ts +22 -0
- package/package.json +117 -87
- package/dist/chunk-2YFPV5GF.cjs +0 -16
- package/dist/chunk-2YFPV5GF.cjs.map +0 -1
- package/dist/chunk-4KF7KUAI.cjs.map +0 -1
- package/dist/chunk-6MN3H3YF.cjs.map +0 -1
- package/dist/chunk-DFTOTBGF.js.map +0 -1
- package/dist/chunk-G6FCDLDF.js +0 -16
- package/dist/chunk-G6FCDLDF.js.map +0 -1
- package/dist/chunk-IUI5CI2S.cjs.map +0 -1
- package/dist/chunk-KYNYPKKH.cjs +0 -71
- package/dist/chunk-KYNYPKKH.cjs.map +0 -1
- package/dist/chunk-L44TM4EE.cjs.map +0 -1
- package/dist/chunk-LSE6JG5I.cjs +0 -20
- package/dist/chunk-LSE6JG5I.cjs.map +0 -1
- package/dist/chunk-O4OGAAV4.js.map +0 -1
- package/dist/chunk-OHQTKGQN.js.map +0 -1
- package/dist/chunk-PSKQAFDF.cjs.map +0 -1
- package/dist/chunk-QTMTBEBA.js +0 -20
- package/dist/chunk-QTMTBEBA.js.map +0 -1
- package/dist/chunk-T7P5EIC4.cjs.map +0 -1
- package/dist/chunk-TB2RZGDV.js.map +0 -1
- package/dist/ui/index.cjs +0 -1961
- package/dist/ui/index.cjs.map +0 -1
- package/dist/ui/index.d.cts +0 -209
- package/dist/ui/index.d.ts +0 -209
- package/dist/ui/index.js +0 -1961
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/kanban.css +0 -194
- /package/dist/{chunk-ATUFFNLV.js.map → chunk-3LEIHB7Y.js.map} +0 -0
- /package/dist/{chunk-5DLZWW5Y.js.map → chunk-AHHEGTUA.js.map} +0 -0
- /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-
|
|
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-
|
|
26
|
+
} from "./chunk-X4UHZX6F.js";
|
|
36
27
|
import {
|
|
28
|
+
KanbanError,
|
|
37
29
|
randomUUID,
|
|
38
30
|
sha256HexSync
|
|
39
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-M35X5PHO.js";
|
|
40
32
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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")
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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"
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
requireValue(
|
|
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
|
|
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 === "
|
|
1282
|
-
else if (
|
|
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 === "
|
|
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-
|
|
1612
|
+
//# sourceMappingURL=chunk-XKDZHAUE.js.map
|