@workflow-cannon/workspace-kit 0.13.0 → 0.14.0
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkflowModule } from "../../contracts/module-contract.js";
|
|
2
|
-
export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, TransitionEvidence, TransitionGuard, TransitionContext, GuardResult, TaskEngineError as TaskEngineErrorType, TaskEngineErrorCode, TaskAdapter, TaskAdapterCapability, NextActionSuggestion, BlockingAnalysisEntry } from "./types.js";
|
|
2
|
+
export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, TransitionEvidence, TransitionGuard, TransitionContext, GuardResult, TaskEngineError as TaskEngineErrorType, TaskEngineErrorCode, TaskAdapter, TaskAdapterCapability, NextActionSuggestion, BlockingAnalysisEntry, TaskMutationEvidence, TaskMutationType } from "./types.js";
|
|
3
3
|
export { TaskStore } from "./store.js";
|
|
4
4
|
export { TransitionService } from "./service.js";
|
|
5
5
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
|
|
2
3
|
import { TaskStore } from "./store.js";
|
|
3
4
|
import { TransitionService } from "./service.js";
|
|
@@ -17,6 +18,33 @@ function taskStorePath(ctx) {
|
|
|
17
18
|
const p = tasks.storeRelativePath;
|
|
18
19
|
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
19
20
|
}
|
|
21
|
+
const TASK_ID_RE = /^T\d+$/;
|
|
22
|
+
const MUTABLE_TASK_FIELDS = new Set([
|
|
23
|
+
"title",
|
|
24
|
+
"type",
|
|
25
|
+
"priority",
|
|
26
|
+
"dependsOn",
|
|
27
|
+
"unblocks",
|
|
28
|
+
"phase",
|
|
29
|
+
"metadata",
|
|
30
|
+
"ownership",
|
|
31
|
+
"approach",
|
|
32
|
+
"technicalScope",
|
|
33
|
+
"acceptanceCriteria"
|
|
34
|
+
]);
|
|
35
|
+
function nowIso() {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
function mutationEvidence(mutationType, taskId, actor, details) {
|
|
39
|
+
return {
|
|
40
|
+
mutationId: `${mutationType}-${taskId}-${nowIso()}-${crypto.randomUUID().slice(0, 8)}`,
|
|
41
|
+
mutationType,
|
|
42
|
+
taskId,
|
|
43
|
+
timestamp: nowIso(),
|
|
44
|
+
actor,
|
|
45
|
+
details
|
|
46
|
+
};
|
|
47
|
+
}
|
|
20
48
|
export const taskEngineModule = {
|
|
21
49
|
registration: {
|
|
22
50
|
id: "task-engine",
|
|
@@ -43,6 +71,61 @@ export const taskEngineModule = {
|
|
|
43
71
|
file: "run-transition.md",
|
|
44
72
|
description: "Execute a validated task status transition."
|
|
45
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "create-task",
|
|
76
|
+
file: "create-task.md",
|
|
77
|
+
description: "Create a new task through validated task-engine persistence."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "update-task",
|
|
81
|
+
file: "update-task.md",
|
|
82
|
+
description: "Update mutable task fields without lifecycle bypass."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "archive-task",
|
|
86
|
+
file: "archive-task.md",
|
|
87
|
+
description: "Archive a task without destructive deletion."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "add-dependency",
|
|
91
|
+
file: "add-dependency.md",
|
|
92
|
+
description: "Add a dependency edge between tasks with cycle checks."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "remove-dependency",
|
|
96
|
+
file: "remove-dependency.md",
|
|
97
|
+
description: "Remove a dependency edge between tasks."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "get-dependency-graph",
|
|
101
|
+
file: "get-dependency-graph.md",
|
|
102
|
+
description: "Get dependency graph data for one task or the full store."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "get-task-history",
|
|
106
|
+
file: "get-task-history.md",
|
|
107
|
+
description: "Get transition and mutation history for a task."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "get-recent-task-activity",
|
|
111
|
+
file: "get-recent-task-activity.md",
|
|
112
|
+
description: "List recent transition and mutation activity across tasks."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "get-task-summary",
|
|
116
|
+
file: "get-task-summary.md",
|
|
117
|
+
description: "Get aggregate task-state summary for active tasks."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "get-blocked-summary",
|
|
121
|
+
file: "get-blocked-summary.md",
|
|
122
|
+
description: "Get blocked-task dependency summary for active tasks."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "create-task-from-plan",
|
|
126
|
+
file: "create-task-from-plan.md",
|
|
127
|
+
description: "Promote planning output into a canonical task."
|
|
128
|
+
},
|
|
46
129
|
{
|
|
47
130
|
name: "get-task",
|
|
48
131
|
file: "get-task.md",
|
|
@@ -129,6 +212,123 @@ export const taskEngineModule = {
|
|
|
129
212
|
};
|
|
130
213
|
}
|
|
131
214
|
}
|
|
215
|
+
if (command.name === "create-task" || command.name === "create-task-from-plan") {
|
|
216
|
+
const actor = typeof args.actor === "string"
|
|
217
|
+
? args.actor
|
|
218
|
+
: ctx.resolvedActor !== undefined
|
|
219
|
+
? ctx.resolvedActor
|
|
220
|
+
: undefined;
|
|
221
|
+
const id = typeof args.id === "string" && args.id.trim().length > 0 ? args.id.trim() : undefined;
|
|
222
|
+
const title = typeof args.title === "string" && args.title.trim().length > 0 ? args.title.trim() : undefined;
|
|
223
|
+
const type = typeof args.type === "string" && args.type.trim().length > 0 ? args.type.trim() : "workspace-kit";
|
|
224
|
+
const status = typeof args.status === "string" ? args.status : "proposed";
|
|
225
|
+
const priority = typeof args.priority === "string" && ["P1", "P2", "P3"].includes(args.priority)
|
|
226
|
+
? args.priority
|
|
227
|
+
: undefined;
|
|
228
|
+
if (!id || !title || !TASK_ID_RE.test(id) || !["proposed", "ready"].includes(status)) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
code: "invalid-task-schema",
|
|
232
|
+
message: "create-task requires id/title, id format T<number>, and status of proposed or ready"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (store.getTask(id)) {
|
|
236
|
+
return { ok: false, code: "duplicate-task-id", message: `Task '${id}' already exists` };
|
|
237
|
+
}
|
|
238
|
+
const timestamp = nowIso();
|
|
239
|
+
const task = {
|
|
240
|
+
id,
|
|
241
|
+
title,
|
|
242
|
+
type,
|
|
243
|
+
status: status,
|
|
244
|
+
createdAt: timestamp,
|
|
245
|
+
updatedAt: timestamp,
|
|
246
|
+
priority,
|
|
247
|
+
dependsOn: Array.isArray(args.dependsOn) ? args.dependsOn.filter((x) => typeof x === "string") : undefined,
|
|
248
|
+
unblocks: Array.isArray(args.unblocks) ? args.unblocks.filter((x) => typeof x === "string") : undefined,
|
|
249
|
+
phase: typeof args.phase === "string" ? args.phase : undefined,
|
|
250
|
+
metadata: typeof args.metadata === "object" && args.metadata !== null ? args.metadata : undefined,
|
|
251
|
+
ownership: typeof args.ownership === "string" ? args.ownership : undefined,
|
|
252
|
+
approach: typeof args.approach === "string" ? args.approach : undefined,
|
|
253
|
+
technicalScope: Array.isArray(args.technicalScope) ? args.technicalScope.filter((x) => typeof x === "string") : undefined,
|
|
254
|
+
acceptanceCriteria: Array.isArray(args.acceptanceCriteria) ? args.acceptanceCriteria.filter((x) => typeof x === "string") : undefined
|
|
255
|
+
};
|
|
256
|
+
store.addTask(task);
|
|
257
|
+
if (command.name === "create-task-from-plan") {
|
|
258
|
+
const planRef = typeof args.planRef === "string" && args.planRef.trim().length > 0 ? args.planRef.trim() : undefined;
|
|
259
|
+
if (!planRef) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
code: "invalid-task-schema",
|
|
263
|
+
message: "create-task-from-plan requires 'planRef'"
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
task.metadata = { ...(task.metadata ?? {}), planRef };
|
|
267
|
+
store.updateTask(task);
|
|
268
|
+
}
|
|
269
|
+
const evidenceType = command.name === "create-task-from-plan" ? "create-task-from-plan" : "create-task";
|
|
270
|
+
store.addMutationEvidence(mutationEvidence(evidenceType, id, actor, {
|
|
271
|
+
initialStatus: task.status,
|
|
272
|
+
source: command.name
|
|
273
|
+
}));
|
|
274
|
+
await store.save();
|
|
275
|
+
return {
|
|
276
|
+
ok: true,
|
|
277
|
+
code: "task-created",
|
|
278
|
+
message: `Created task '${id}'`,
|
|
279
|
+
data: { task }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (command.name === "update-task") {
|
|
283
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
284
|
+
const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
|
|
285
|
+
const actor = typeof args.actor === "string"
|
|
286
|
+
? args.actor
|
|
287
|
+
: ctx.resolvedActor !== undefined
|
|
288
|
+
? ctx.resolvedActor
|
|
289
|
+
: undefined;
|
|
290
|
+
if (!taskId || !updates) {
|
|
291
|
+
return { ok: false, code: "invalid-task-schema", message: "update-task requires taskId and updates object" };
|
|
292
|
+
}
|
|
293
|
+
const task = store.getTask(taskId);
|
|
294
|
+
if (!task) {
|
|
295
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
296
|
+
}
|
|
297
|
+
const invalidKeys = Object.keys(updates).filter((key) => !MUTABLE_TASK_FIELDS.has(key));
|
|
298
|
+
if (invalidKeys.length > 0) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
code: "invalid-task-update",
|
|
302
|
+
message: `update-task cannot mutate immutable fields: ${invalidKeys.join(", ")}`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const updatedTask = { ...task, ...updates, updatedAt: nowIso() };
|
|
306
|
+
store.updateTask(updatedTask);
|
|
307
|
+
store.addMutationEvidence(mutationEvidence("update-task", taskId, actor, { updatedFields: Object.keys(updates) }));
|
|
308
|
+
await store.save();
|
|
309
|
+
return { ok: true, code: "task-updated", message: `Updated task '${taskId}'`, data: { task: updatedTask } };
|
|
310
|
+
}
|
|
311
|
+
if (command.name === "archive-task") {
|
|
312
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
313
|
+
const actor = typeof args.actor === "string"
|
|
314
|
+
? args.actor
|
|
315
|
+
: ctx.resolvedActor !== undefined
|
|
316
|
+
? ctx.resolvedActor
|
|
317
|
+
: undefined;
|
|
318
|
+
if (!taskId) {
|
|
319
|
+
return { ok: false, code: "invalid-task-schema", message: "archive-task requires taskId" };
|
|
320
|
+
}
|
|
321
|
+
const task = store.getTask(taskId);
|
|
322
|
+
if (!task) {
|
|
323
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
324
|
+
}
|
|
325
|
+
const archivedAt = nowIso();
|
|
326
|
+
const updatedTask = { ...task, archived: true, archivedAt, updatedAt: archivedAt };
|
|
327
|
+
store.updateTask(updatedTask);
|
|
328
|
+
store.addMutationEvidence(mutationEvidence("archive-task", taskId, actor));
|
|
329
|
+
await store.save();
|
|
330
|
+
return { ok: true, code: "task-archived", message: `Archived task '${taskId}'`, data: { task: updatedTask } };
|
|
331
|
+
}
|
|
132
332
|
if (command.name === "get-task") {
|
|
133
333
|
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
134
334
|
if (!taskId) {
|
|
@@ -165,8 +365,95 @@ export const taskEngineModule = {
|
|
|
165
365
|
data: { task, recentTransitions, allowedActions }
|
|
166
366
|
};
|
|
167
367
|
}
|
|
368
|
+
if (command.name === "add-dependency" || command.name === "remove-dependency") {
|
|
369
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
370
|
+
const dependencyTaskId = typeof args.dependencyTaskId === "string" ? args.dependencyTaskId : undefined;
|
|
371
|
+
const actor = typeof args.actor === "string"
|
|
372
|
+
? args.actor
|
|
373
|
+
: ctx.resolvedActor !== undefined
|
|
374
|
+
? ctx.resolvedActor
|
|
375
|
+
: undefined;
|
|
376
|
+
if (!taskId || !dependencyTaskId) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
code: "invalid-task-schema",
|
|
380
|
+
message: `${command.name} requires taskId and dependencyTaskId`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (taskId === dependencyTaskId) {
|
|
384
|
+
return { ok: false, code: "dependency-cycle", message: "Task cannot depend on itself" };
|
|
385
|
+
}
|
|
386
|
+
const task = store.getTask(taskId);
|
|
387
|
+
const dep = store.getTask(dependencyTaskId);
|
|
388
|
+
if (!task || !dep) {
|
|
389
|
+
return { ok: false, code: "task-not-found", message: "taskId or dependencyTaskId not found" };
|
|
390
|
+
}
|
|
391
|
+
const deps = new Set(task.dependsOn ?? []);
|
|
392
|
+
if (command.name === "add-dependency") {
|
|
393
|
+
if (deps.has(dependencyTaskId)) {
|
|
394
|
+
return { ok: false, code: "duplicate-dependency", message: "Dependency already exists" };
|
|
395
|
+
}
|
|
396
|
+
deps.add(dependencyTaskId);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
deps.delete(dependencyTaskId);
|
|
400
|
+
}
|
|
401
|
+
const updatedTask = { ...task, dependsOn: [...deps], updatedAt: nowIso() };
|
|
402
|
+
store.updateTask(updatedTask);
|
|
403
|
+
const mutationType = command.name === "add-dependency" ? "add-dependency" : "remove-dependency";
|
|
404
|
+
store.addMutationEvidence(mutationEvidence(mutationType, taskId, actor, { dependencyTaskId }));
|
|
405
|
+
await store.save();
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
code: command.name === "add-dependency" ? "dependency-added" : "dependency-removed",
|
|
409
|
+
message: `${command.name} applied for '${taskId}'`,
|
|
410
|
+
data: { task: updatedTask }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (command.name === "get-dependency-graph") {
|
|
414
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
415
|
+
const tasks = store.getActiveTasks();
|
|
416
|
+
const nodes = tasks.map((task) => ({ id: task.id, status: task.status }));
|
|
417
|
+
const edges = tasks.flatMap((task) => (task.dependsOn ?? []).map((depId) => ({ from: task.id, to: depId })));
|
|
418
|
+
if (!taskId) {
|
|
419
|
+
return { ok: true, code: "dependency-graph", data: { nodes, edges } };
|
|
420
|
+
}
|
|
421
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
422
|
+
if (!task) {
|
|
423
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
ok: true,
|
|
427
|
+
code: "dependency-graph",
|
|
428
|
+
data: {
|
|
429
|
+
taskId,
|
|
430
|
+
dependsOn: task.dependsOn ?? [],
|
|
431
|
+
directDependents: tasks.filter((candidate) => (candidate.dependsOn ?? []).includes(taskId)).map((x) => x.id),
|
|
432
|
+
nodes,
|
|
433
|
+
edges
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (command.name === "get-task-history" || command.name === "get-recent-task-activity") {
|
|
438
|
+
const limitRaw = args.limit;
|
|
439
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0
|
|
440
|
+
? Math.min(Math.floor(limitRaw), 500)
|
|
441
|
+
: 50;
|
|
442
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
443
|
+
const transitions = store.getTransitionLog().map((entry) => ({ kind: "transition", ...entry }));
|
|
444
|
+
const mutations = store.getMutationLog().map((entry) => ({ kind: "mutation", ...entry }));
|
|
445
|
+
const merged = [...transitions, ...mutations]
|
|
446
|
+
.filter((entry) => (taskId ? entry.taskId === taskId : true))
|
|
447
|
+
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
|
|
448
|
+
.slice(0, limit);
|
|
449
|
+
return {
|
|
450
|
+
ok: true,
|
|
451
|
+
code: command.name === "get-task-history" ? "task-history" : "recent-task-activity",
|
|
452
|
+
data: { taskId: taskId ?? null, items: merged, count: merged.length }
|
|
453
|
+
};
|
|
454
|
+
}
|
|
168
455
|
if (command.name === "dashboard-summary") {
|
|
169
|
-
const tasks = store.
|
|
456
|
+
const tasks = store.getActiveTasks();
|
|
170
457
|
const suggestion = getNextActions(tasks);
|
|
171
458
|
const workspaceStatus = await readWorkspaceStatusSnapshot(ctx.workspacePath);
|
|
172
459
|
const readyTop = suggestion.readyQueue.slice(0, 15).map((t) => ({
|
|
@@ -208,7 +495,8 @@ export const taskEngineModule = {
|
|
|
208
495
|
if (command.name === "list-tasks") {
|
|
209
496
|
const statusFilter = typeof args.status === "string" ? args.status : undefined;
|
|
210
497
|
const phaseFilter = typeof args.phase === "string" ? args.phase : undefined;
|
|
211
|
-
|
|
498
|
+
const includeArchived = args.includeArchived === true;
|
|
499
|
+
let tasks = includeArchived ? store.getAllTasks() : store.getActiveTasks();
|
|
212
500
|
if (statusFilter) {
|
|
213
501
|
tasks = tasks.filter((t) => t.status === statusFilter);
|
|
214
502
|
}
|
|
@@ -223,7 +511,7 @@ export const taskEngineModule = {
|
|
|
223
511
|
};
|
|
224
512
|
}
|
|
225
513
|
if (command.name === "get-ready-queue") {
|
|
226
|
-
const tasks = store.
|
|
514
|
+
const tasks = store.getActiveTasks();
|
|
227
515
|
const ready = tasks
|
|
228
516
|
.filter((t) => t.status === "ready")
|
|
229
517
|
.sort((a, b) => {
|
|
@@ -239,7 +527,7 @@ export const taskEngineModule = {
|
|
|
239
527
|
};
|
|
240
528
|
}
|
|
241
529
|
if (command.name === "get-next-actions") {
|
|
242
|
-
const tasks = store.
|
|
530
|
+
const tasks = store.getActiveTasks();
|
|
243
531
|
const suggestion = getNextActions(tasks);
|
|
244
532
|
return {
|
|
245
533
|
ok: true,
|
|
@@ -250,6 +538,37 @@ export const taskEngineModule = {
|
|
|
250
538
|
data: suggestion
|
|
251
539
|
};
|
|
252
540
|
}
|
|
541
|
+
if (command.name === "get-task-summary") {
|
|
542
|
+
const tasks = store.getActiveTasks();
|
|
543
|
+
const suggestion = getNextActions(tasks);
|
|
544
|
+
return {
|
|
545
|
+
ok: true,
|
|
546
|
+
code: "task-summary",
|
|
547
|
+
data: {
|
|
548
|
+
stateSummary: suggestion.stateSummary,
|
|
549
|
+
readyQueueCount: suggestion.readyQueue.length,
|
|
550
|
+
suggestedNext: suggestion.suggestedNext
|
|
551
|
+
? {
|
|
552
|
+
id: suggestion.suggestedNext.id,
|
|
553
|
+
title: suggestion.suggestedNext.title,
|
|
554
|
+
priority: suggestion.suggestedNext.priority ?? null
|
|
555
|
+
}
|
|
556
|
+
: null
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (command.name === "get-blocked-summary") {
|
|
561
|
+
const tasks = store.getActiveTasks();
|
|
562
|
+
const suggestion = getNextActions(tasks);
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
code: "blocked-summary",
|
|
566
|
+
data: {
|
|
567
|
+
blockedCount: suggestion.blockingAnalysis.length,
|
|
568
|
+
blockedItems: suggestion.blockingAnalysis
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
253
572
|
return {
|
|
254
573
|
ok: false,
|
|
255
574
|
code: "unsupported-command",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskEntity, TransitionEvidence } from "./types.js";
|
|
1
|
+
import type { TaskEntity, TaskMutationEvidence, TransitionEvidence } from "./types.js";
|
|
2
2
|
export declare class TaskStore {
|
|
3
3
|
private document;
|
|
4
4
|
private readonly filePath;
|
|
@@ -6,11 +6,14 @@ export declare class TaskStore {
|
|
|
6
6
|
load(): Promise<void>;
|
|
7
7
|
save(): Promise<void>;
|
|
8
8
|
getAllTasks(): TaskEntity[];
|
|
9
|
+
getActiveTasks(): TaskEntity[];
|
|
9
10
|
getTask(id: string): TaskEntity | undefined;
|
|
10
11
|
addTask(task: TaskEntity): void;
|
|
11
12
|
updateTask(task: TaskEntity): void;
|
|
12
13
|
addEvidence(evidence: TransitionEvidence): void;
|
|
14
|
+
addMutationEvidence(evidence: TaskMutationEvidence): void;
|
|
13
15
|
getTransitionLog(): TransitionEvidence[];
|
|
16
|
+
getMutationLog(): TaskMutationEvidence[];
|
|
14
17
|
replaceAllTasks(tasks: TaskEntity[]): void;
|
|
15
18
|
getFilePath(): string;
|
|
16
19
|
getLastUpdated(): string;
|
|
@@ -8,6 +8,7 @@ function emptyStore() {
|
|
|
8
8
|
schemaVersion: 1,
|
|
9
9
|
tasks: [],
|
|
10
10
|
transitionLog: [],
|
|
11
|
+
mutationLog: [],
|
|
11
12
|
lastUpdated: new Date().toISOString()
|
|
12
13
|
};
|
|
13
14
|
}
|
|
@@ -26,6 +27,9 @@ export class TaskStore {
|
|
|
26
27
|
throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
|
|
27
28
|
}
|
|
28
29
|
this.document = parsed;
|
|
30
|
+
if (!Array.isArray(this.document.mutationLog)) {
|
|
31
|
+
this.document.mutationLog = [];
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
catch (err) {
|
|
31
35
|
if (err.code === "ENOENT") {
|
|
@@ -57,6 +61,9 @@ export class TaskStore {
|
|
|
57
61
|
getAllTasks() {
|
|
58
62
|
return [...this.document.tasks];
|
|
59
63
|
}
|
|
64
|
+
getActiveTasks() {
|
|
65
|
+
return this.document.tasks.filter((task) => !task.archived).map((task) => ({ ...task }));
|
|
66
|
+
}
|
|
60
67
|
getTask(id) {
|
|
61
68
|
return this.document.tasks.find((t) => t.id === id);
|
|
62
69
|
}
|
|
@@ -76,9 +83,18 @@ export class TaskStore {
|
|
|
76
83
|
addEvidence(evidence) {
|
|
77
84
|
this.document.transitionLog.push(evidence);
|
|
78
85
|
}
|
|
86
|
+
addMutationEvidence(evidence) {
|
|
87
|
+
if (!Array.isArray(this.document.mutationLog)) {
|
|
88
|
+
this.document.mutationLog = [];
|
|
89
|
+
}
|
|
90
|
+
this.document.mutationLog.push(evidence);
|
|
91
|
+
}
|
|
79
92
|
getTransitionLog() {
|
|
80
93
|
return [...this.document.transitionLog];
|
|
81
94
|
}
|
|
95
|
+
getMutationLog() {
|
|
96
|
+
return [...(this.document.mutationLog ?? [])];
|
|
97
|
+
}
|
|
82
98
|
replaceAllTasks(tasks) {
|
|
83
99
|
this.document.tasks = tasks.map((t) => ({ ...t }));
|
|
84
100
|
}
|
|
@@ -7,6 +7,8 @@ export type TaskEntity = {
|
|
|
7
7
|
title: string;
|
|
8
8
|
createdAt: string;
|
|
9
9
|
updatedAt: string;
|
|
10
|
+
archived?: boolean;
|
|
11
|
+
archivedAt?: string;
|
|
10
12
|
priority?: TaskPriority;
|
|
11
13
|
dependsOn?: string[];
|
|
12
14
|
unblocks?: string[];
|
|
@@ -47,13 +49,23 @@ export type TaskStoreDocument = {
|
|
|
47
49
|
schemaVersion: 1;
|
|
48
50
|
tasks: TaskEntity[];
|
|
49
51
|
transitionLog: TransitionEvidence[];
|
|
52
|
+
mutationLog?: TaskMutationEvidence[];
|
|
50
53
|
lastUpdated: string;
|
|
51
54
|
};
|
|
55
|
+
export type TaskMutationType = "create-task" | "update-task" | "archive-task" | "add-dependency" | "remove-dependency" | "create-task-from-plan";
|
|
56
|
+
export type TaskMutationEvidence = {
|
|
57
|
+
mutationId: string;
|
|
58
|
+
mutationType: TaskMutationType;
|
|
59
|
+
taskId: string;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
actor?: string;
|
|
62
|
+
details?: Record<string, unknown>;
|
|
63
|
+
};
|
|
52
64
|
export type TaskEngineError = {
|
|
53
65
|
code: TaskEngineErrorCode;
|
|
54
66
|
message: string;
|
|
55
67
|
};
|
|
56
|
-
export type TaskEngineErrorCode = "invalid-transition" | "guard-rejected" | "dependency-unsatisfied" | "task-not-found" | "duplicate-task-id" | "invalid-task-schema" | "storage-read-error" | "storage-write-error" | "invalid-adapter" | "import-parse-error";
|
|
68
|
+
export type TaskEngineErrorCode = "invalid-transition" | "guard-rejected" | "dependency-unsatisfied" | "task-not-found" | "duplicate-task-id" | "invalid-task-schema" | "invalid-task-update" | "invalid-task-id-format" | "task-archived" | "dependency-cycle" | "duplicate-dependency" | "storage-read-error" | "storage-write-error" | "invalid-adapter" | "import-parse-error";
|
|
57
69
|
export type TaskAdapter = {
|
|
58
70
|
name: string;
|
|
59
71
|
supports: () => TaskAdapterCapability[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"pre-release-transcript-hook": "pnpm run build && node scripts/pre-release-transcript-hook.mjs",
|
|
37
37
|
"transcript:sync": "node scripts/run-transcript-cli.mjs sync-transcripts",
|
|
38
38
|
"transcript:ingest": "node scripts/run-transcript-cli.mjs ingest-transcripts",
|
|
39
|
-
"ext:compile": "cd extensions/cursor-workflow-cannon && npm run compile"
|
|
39
|
+
"ext:compile": "cd extensions/cursor-workflow-cannon && npm run compile",
|
|
40
|
+
"ui:prepare": "pnpm run build && pnpm run ext:compile",
|
|
41
|
+
"ui:watch": "cd extensions/cursor-workflow-cannon && npm run watch"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@types/node": "^25.5.0",
|