@workflow-cannon/workspace-kit 0.13.0 → 0.15.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.
- package/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/task-engine/index.d.ts +4 -1
- package/dist/modules/task-engine/index.js +638 -7
- package/dist/modules/task-engine/store.d.ts +4 -1
- package/dist/modules/task-engine/store.js +16 -0
- package/dist/modules/task-engine/types.d.ts +13 -1
- package/dist/modules/task-engine/wishlist-store.d.ts +14 -0
- package/dist/modules/task-engine/wishlist-store.js +86 -0
- package/dist/modules/task-engine/wishlist-types.d.ts +33 -0
- package/dist/modules/task-engine/wishlist-types.js +5 -0
- package/dist/modules/task-engine/wishlist-validation.d.ts +16 -0
- package/dist/modules/task-engine/wishlist-validation.js +96 -0
- package/package.json +4 -2
package/dist/modules/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { workspaceConfigModule } from "./workspace-config/index.js";
|
|
|
7
7
|
export { planningModule } from "./planning/index.js";
|
|
8
8
|
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, getNextActions } from "./task-engine/index.js";
|
|
9
9
|
export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, TransitionEvidence, TransitionGuard, TransitionContext, GuardResult, TaskEngineErrorCode, TaskAdapter, TaskAdapterCapability, NextActionSuggestion, BlockingAnalysisEntry } from "./task-engine/index.js";
|
|
10
|
+
export type { WishlistItem, WishlistStatus, WishlistStoreDocument } from "./task-engine/index.js";
|
|
11
|
+
export { WishlistStore, validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./task-engine/index.js";
|
package/dist/modules/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmi
|
|
|
5
5
|
export { workspaceConfigModule } from "./workspace-config/index.js";
|
|
6
6
|
export { planningModule } from "./planning/index.js";
|
|
7
7
|
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, getNextActions } from "./task-engine/index.js";
|
|
8
|
+
export { WishlistStore, validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./task-engine/index.js";
|
|
@@ -1,8 +1,11 @@
|
|
|
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";
|
|
6
6
|
export { getNextActions } from "./suggestions.js";
|
|
7
7
|
export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
8
|
+
export { WishlistStore } from "./wishlist-store.js";
|
|
9
|
+
export type { WishlistItem, WishlistStatus, WishlistStoreDocument } from "./wishlist-types.js";
|
|
10
|
+
export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
8
11
|
export declare const taskEngineModule: WorkflowModule;
|
|
@@ -1,14 +1,19 @@
|
|
|
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";
|
|
4
5
|
import { TaskEngineError, getAllowedTransitionsFrom } from "./transitions.js";
|
|
5
6
|
import { getNextActions } from "./suggestions.js";
|
|
6
7
|
import { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
8
|
+
import { WishlistStore } from "./wishlist-store.js";
|
|
9
|
+
import { buildWishlistItemFromIntake, validateWishlistIntakePayload, validateWishlistUpdatePayload, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
7
10
|
export { TaskStore } from "./store.js";
|
|
8
11
|
export { TransitionService } from "./service.js";
|
|
9
12
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
10
13
|
export { getNextActions } from "./suggestions.js";
|
|
11
14
|
export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
15
|
+
export { WishlistStore } from "./wishlist-store.js";
|
|
16
|
+
export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
12
17
|
function taskStorePath(ctx) {
|
|
13
18
|
const tasks = ctx.effectiveConfig?.tasks;
|
|
14
19
|
if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
|
|
@@ -17,6 +22,107 @@ function taskStorePath(ctx) {
|
|
|
17
22
|
const p = tasks.storeRelativePath;
|
|
18
23
|
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
19
24
|
}
|
|
25
|
+
function wishlistStorePath(ctx) {
|
|
26
|
+
const tasks = ctx.effectiveConfig?.tasks;
|
|
27
|
+
if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const p = tasks.wishlistStoreRelativePath;
|
|
31
|
+
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
32
|
+
}
|
|
33
|
+
const TASK_ID_RE = /^T\d+$/;
|
|
34
|
+
const MUTABLE_TASK_FIELDS = new Set([
|
|
35
|
+
"title",
|
|
36
|
+
"type",
|
|
37
|
+
"priority",
|
|
38
|
+
"dependsOn",
|
|
39
|
+
"unblocks",
|
|
40
|
+
"phase",
|
|
41
|
+
"metadata",
|
|
42
|
+
"ownership",
|
|
43
|
+
"approach",
|
|
44
|
+
"technicalScope",
|
|
45
|
+
"acceptanceCriteria"
|
|
46
|
+
]);
|
|
47
|
+
function nowIso() {
|
|
48
|
+
return new Date().toISOString();
|
|
49
|
+
}
|
|
50
|
+
function parseConversionDecomposition(raw) {
|
|
51
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
52
|
+
return { ok: false, message: "convert-wishlist requires 'decomposition' object" };
|
|
53
|
+
}
|
|
54
|
+
const o = raw;
|
|
55
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
56
|
+
const boundaries = typeof o.boundaries === "string" ? o.boundaries.trim() : "";
|
|
57
|
+
const dependencyIntent = typeof o.dependencyIntent === "string" ? o.dependencyIntent.trim() : "";
|
|
58
|
+
if (!rationale || !boundaries || !dependencyIntent) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
message: "decomposition requires non-empty rationale, boundaries, and dependencyIntent"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: { rationale, boundaries, dependencyIntent } };
|
|
65
|
+
}
|
|
66
|
+
function buildTaskFromConversionPayload(row, timestamp) {
|
|
67
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
68
|
+
if (!TASK_ID_RE.test(id)) {
|
|
69
|
+
return { ok: false, message: "Each converted task requires 'id' matching T<number>" };
|
|
70
|
+
}
|
|
71
|
+
const title = typeof row.title === "string" ? row.title.trim() : "";
|
|
72
|
+
if (!title) {
|
|
73
|
+
return { ok: false, message: `Task '${id}' requires non-empty title` };
|
|
74
|
+
}
|
|
75
|
+
const phase = typeof row.phase === "string" ? row.phase.trim() : "";
|
|
76
|
+
if (!phase) {
|
|
77
|
+
return { ok: false, message: `Task '${id}' requires 'phase' for workable tasks` };
|
|
78
|
+
}
|
|
79
|
+
const type = typeof row.type === "string" && row.type.trim() ? row.type.trim() : "workspace-kit";
|
|
80
|
+
const priority = typeof row.priority === "string" && ["P1", "P2", "P3"].includes(row.priority)
|
|
81
|
+
? row.priority
|
|
82
|
+
: undefined;
|
|
83
|
+
const approach = typeof row.approach === "string" ? row.approach.trim() : "";
|
|
84
|
+
if (!approach) {
|
|
85
|
+
return { ok: false, message: `Task '${id}' requires 'approach'` };
|
|
86
|
+
}
|
|
87
|
+
const technicalScope = Array.isArray(row.technicalScope)
|
|
88
|
+
? row.technicalScope.filter((x) => typeof x === "string")
|
|
89
|
+
: [];
|
|
90
|
+
const acceptanceCriteria = Array.isArray(row.acceptanceCriteria)
|
|
91
|
+
? row.acceptanceCriteria.filter((x) => typeof x === "string")
|
|
92
|
+
: [];
|
|
93
|
+
if (technicalScope.length === 0) {
|
|
94
|
+
return { ok: false, message: `Task '${id}' requires non-empty technicalScope array` };
|
|
95
|
+
}
|
|
96
|
+
if (acceptanceCriteria.length === 0) {
|
|
97
|
+
return { ok: false, message: `Task '${id}' requires non-empty acceptanceCriteria array` };
|
|
98
|
+
}
|
|
99
|
+
const task = {
|
|
100
|
+
id,
|
|
101
|
+
title,
|
|
102
|
+
type,
|
|
103
|
+
status: "proposed",
|
|
104
|
+
createdAt: timestamp,
|
|
105
|
+
updatedAt: timestamp,
|
|
106
|
+
priority,
|
|
107
|
+
dependsOn: Array.isArray(row.dependsOn) ? row.dependsOn.filter((x) => typeof x === "string") : undefined,
|
|
108
|
+
unblocks: Array.isArray(row.unblocks) ? row.unblocks.filter((x) => typeof x === "string") : undefined,
|
|
109
|
+
phase,
|
|
110
|
+
approach,
|
|
111
|
+
technicalScope,
|
|
112
|
+
acceptanceCriteria
|
|
113
|
+
};
|
|
114
|
+
return { ok: true, task };
|
|
115
|
+
}
|
|
116
|
+
function mutationEvidence(mutationType, taskId, actor, details) {
|
|
117
|
+
return {
|
|
118
|
+
mutationId: `${mutationType}-${taskId}-${nowIso()}-${crypto.randomUUID().slice(0, 8)}`,
|
|
119
|
+
mutationType,
|
|
120
|
+
taskId,
|
|
121
|
+
timestamp: nowIso(),
|
|
122
|
+
actor,
|
|
123
|
+
details
|
|
124
|
+
};
|
|
125
|
+
}
|
|
20
126
|
export const taskEngineModule = {
|
|
21
127
|
registration: {
|
|
22
128
|
id: "task-engine",
|
|
@@ -43,6 +149,81 @@ export const taskEngineModule = {
|
|
|
43
149
|
file: "run-transition.md",
|
|
44
150
|
description: "Execute a validated task status transition."
|
|
45
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: "create-task",
|
|
154
|
+
file: "create-task.md",
|
|
155
|
+
description: "Create a new task through validated task-engine persistence."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "update-task",
|
|
159
|
+
file: "update-task.md",
|
|
160
|
+
description: "Update mutable task fields without lifecycle bypass."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "update-wishlist",
|
|
164
|
+
file: "update-wishlist.md",
|
|
165
|
+
description: "Update mutable fields on an open Wishlist item."
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "archive-task",
|
|
169
|
+
file: "archive-task.md",
|
|
170
|
+
description: "Archive a task without destructive deletion."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "add-dependency",
|
|
174
|
+
file: "add-dependency.md",
|
|
175
|
+
description: "Add a dependency edge between tasks with cycle checks."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "remove-dependency",
|
|
179
|
+
file: "remove-dependency.md",
|
|
180
|
+
description: "Remove a dependency edge between tasks."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "get-dependency-graph",
|
|
184
|
+
file: "get-dependency-graph.md",
|
|
185
|
+
description: "Get dependency graph data for one task or the full store."
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "get-task-history",
|
|
189
|
+
file: "get-task-history.md",
|
|
190
|
+
description: "Get transition and mutation history for a task."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "get-recent-task-activity",
|
|
194
|
+
file: "get-recent-task-activity.md",
|
|
195
|
+
description: "List recent transition and mutation activity across tasks."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "get-task-summary",
|
|
199
|
+
file: "get-task-summary.md",
|
|
200
|
+
description: "Get aggregate task-state summary for active tasks."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "get-blocked-summary",
|
|
204
|
+
file: "get-blocked-summary.md",
|
|
205
|
+
description: "Get blocked-task dependency summary for active tasks."
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "create-task-from-plan",
|
|
209
|
+
file: "create-task-from-plan.md",
|
|
210
|
+
description: "Promote planning output into a canonical task."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "convert-wishlist",
|
|
214
|
+
file: "convert-wishlist.md",
|
|
215
|
+
description: "Convert a Wishlist item into one or more phased tasks and close the wishlist item."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "create-wishlist",
|
|
219
|
+
file: "create-wishlist.md",
|
|
220
|
+
description: "Create a Wishlist ideation item with strict required fields (separate namespace from tasks)."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "get-wishlist",
|
|
224
|
+
file: "get-wishlist.md",
|
|
225
|
+
description: "Retrieve a single Wishlist item by ID."
|
|
226
|
+
},
|
|
46
227
|
{
|
|
47
228
|
name: "get-task",
|
|
48
229
|
file: "get-task.md",
|
|
@@ -53,6 +234,11 @@ export const taskEngineModule = {
|
|
|
53
234
|
file: "list-tasks.md",
|
|
54
235
|
description: "List tasks with optional status/phase filters."
|
|
55
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: "list-wishlist",
|
|
239
|
+
file: "list-wishlist.md",
|
|
240
|
+
description: "List Wishlist items (ideation-only; not part of task execution queues)."
|
|
241
|
+
},
|
|
56
242
|
{
|
|
57
243
|
name: "get-ready-queue",
|
|
58
244
|
file: "get-ready-queue.md",
|
|
@@ -129,6 +315,123 @@ export const taskEngineModule = {
|
|
|
129
315
|
};
|
|
130
316
|
}
|
|
131
317
|
}
|
|
318
|
+
if (command.name === "create-task" || command.name === "create-task-from-plan") {
|
|
319
|
+
const actor = typeof args.actor === "string"
|
|
320
|
+
? args.actor
|
|
321
|
+
: ctx.resolvedActor !== undefined
|
|
322
|
+
? ctx.resolvedActor
|
|
323
|
+
: undefined;
|
|
324
|
+
const id = typeof args.id === "string" && args.id.trim().length > 0 ? args.id.trim() : undefined;
|
|
325
|
+
const title = typeof args.title === "string" && args.title.trim().length > 0 ? args.title.trim() : undefined;
|
|
326
|
+
const type = typeof args.type === "string" && args.type.trim().length > 0 ? args.type.trim() : "workspace-kit";
|
|
327
|
+
const status = typeof args.status === "string" ? args.status : "proposed";
|
|
328
|
+
const priority = typeof args.priority === "string" && ["P1", "P2", "P3"].includes(args.priority)
|
|
329
|
+
? args.priority
|
|
330
|
+
: undefined;
|
|
331
|
+
if (!id || !title || !TASK_ID_RE.test(id) || !["proposed", "ready"].includes(status)) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
code: "invalid-task-schema",
|
|
335
|
+
message: "create-task requires id/title, id format T<number>, and status of proposed or ready"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (store.getTask(id)) {
|
|
339
|
+
return { ok: false, code: "duplicate-task-id", message: `Task '${id}' already exists` };
|
|
340
|
+
}
|
|
341
|
+
const timestamp = nowIso();
|
|
342
|
+
const task = {
|
|
343
|
+
id,
|
|
344
|
+
title,
|
|
345
|
+
type,
|
|
346
|
+
status: status,
|
|
347
|
+
createdAt: timestamp,
|
|
348
|
+
updatedAt: timestamp,
|
|
349
|
+
priority,
|
|
350
|
+
dependsOn: Array.isArray(args.dependsOn) ? args.dependsOn.filter((x) => typeof x === "string") : undefined,
|
|
351
|
+
unblocks: Array.isArray(args.unblocks) ? args.unblocks.filter((x) => typeof x === "string") : undefined,
|
|
352
|
+
phase: typeof args.phase === "string" ? args.phase : undefined,
|
|
353
|
+
metadata: typeof args.metadata === "object" && args.metadata !== null ? args.metadata : undefined,
|
|
354
|
+
ownership: typeof args.ownership === "string" ? args.ownership : undefined,
|
|
355
|
+
approach: typeof args.approach === "string" ? args.approach : undefined,
|
|
356
|
+
technicalScope: Array.isArray(args.technicalScope) ? args.technicalScope.filter((x) => typeof x === "string") : undefined,
|
|
357
|
+
acceptanceCriteria: Array.isArray(args.acceptanceCriteria) ? args.acceptanceCriteria.filter((x) => typeof x === "string") : undefined
|
|
358
|
+
};
|
|
359
|
+
store.addTask(task);
|
|
360
|
+
if (command.name === "create-task-from-plan") {
|
|
361
|
+
const planRef = typeof args.planRef === "string" && args.planRef.trim().length > 0 ? args.planRef.trim() : undefined;
|
|
362
|
+
if (!planRef) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
code: "invalid-task-schema",
|
|
366
|
+
message: "create-task-from-plan requires 'planRef'"
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
task.metadata = { ...(task.metadata ?? {}), planRef };
|
|
370
|
+
store.updateTask(task);
|
|
371
|
+
}
|
|
372
|
+
const evidenceType = command.name === "create-task-from-plan" ? "create-task-from-plan" : "create-task";
|
|
373
|
+
store.addMutationEvidence(mutationEvidence(evidenceType, id, actor, {
|
|
374
|
+
initialStatus: task.status,
|
|
375
|
+
source: command.name
|
|
376
|
+
}));
|
|
377
|
+
await store.save();
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
code: "task-created",
|
|
381
|
+
message: `Created task '${id}'`,
|
|
382
|
+
data: { task }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (command.name === "update-task") {
|
|
386
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
387
|
+
const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
|
|
388
|
+
const actor = typeof args.actor === "string"
|
|
389
|
+
? args.actor
|
|
390
|
+
: ctx.resolvedActor !== undefined
|
|
391
|
+
? ctx.resolvedActor
|
|
392
|
+
: undefined;
|
|
393
|
+
if (!taskId || !updates) {
|
|
394
|
+
return { ok: false, code: "invalid-task-schema", message: "update-task requires taskId and updates object" };
|
|
395
|
+
}
|
|
396
|
+
const task = store.getTask(taskId);
|
|
397
|
+
if (!task) {
|
|
398
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
399
|
+
}
|
|
400
|
+
const invalidKeys = Object.keys(updates).filter((key) => !MUTABLE_TASK_FIELDS.has(key));
|
|
401
|
+
if (invalidKeys.length > 0) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
code: "invalid-task-update",
|
|
405
|
+
message: `update-task cannot mutate immutable fields: ${invalidKeys.join(", ")}`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const updatedTask = { ...task, ...updates, updatedAt: nowIso() };
|
|
409
|
+
store.updateTask(updatedTask);
|
|
410
|
+
store.addMutationEvidence(mutationEvidence("update-task", taskId, actor, { updatedFields: Object.keys(updates) }));
|
|
411
|
+
await store.save();
|
|
412
|
+
return { ok: true, code: "task-updated", message: `Updated task '${taskId}'`, data: { task: updatedTask } };
|
|
413
|
+
}
|
|
414
|
+
if (command.name === "archive-task") {
|
|
415
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
416
|
+
const actor = typeof args.actor === "string"
|
|
417
|
+
? args.actor
|
|
418
|
+
: ctx.resolvedActor !== undefined
|
|
419
|
+
? ctx.resolvedActor
|
|
420
|
+
: undefined;
|
|
421
|
+
if (!taskId) {
|
|
422
|
+
return { ok: false, code: "invalid-task-schema", message: "archive-task requires taskId" };
|
|
423
|
+
}
|
|
424
|
+
const task = store.getTask(taskId);
|
|
425
|
+
if (!task) {
|
|
426
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
427
|
+
}
|
|
428
|
+
const archivedAt = nowIso();
|
|
429
|
+
const updatedTask = { ...task, archived: true, archivedAt, updatedAt: archivedAt };
|
|
430
|
+
store.updateTask(updatedTask);
|
|
431
|
+
store.addMutationEvidence(mutationEvidence("archive-task", taskId, actor));
|
|
432
|
+
await store.save();
|
|
433
|
+
return { ok: true, code: "task-archived", message: `Archived task '${taskId}'`, data: { task: updatedTask } };
|
|
434
|
+
}
|
|
132
435
|
if (command.name === "get-task") {
|
|
133
436
|
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
134
437
|
if (!taskId) {
|
|
@@ -165,8 +468,95 @@ export const taskEngineModule = {
|
|
|
165
468
|
data: { task, recentTransitions, allowedActions }
|
|
166
469
|
};
|
|
167
470
|
}
|
|
471
|
+
if (command.name === "add-dependency" || command.name === "remove-dependency") {
|
|
472
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
473
|
+
const dependencyTaskId = typeof args.dependencyTaskId === "string" ? args.dependencyTaskId : undefined;
|
|
474
|
+
const actor = typeof args.actor === "string"
|
|
475
|
+
? args.actor
|
|
476
|
+
: ctx.resolvedActor !== undefined
|
|
477
|
+
? ctx.resolvedActor
|
|
478
|
+
: undefined;
|
|
479
|
+
if (!taskId || !dependencyTaskId) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
code: "invalid-task-schema",
|
|
483
|
+
message: `${command.name} requires taskId and dependencyTaskId`
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (taskId === dependencyTaskId) {
|
|
487
|
+
return { ok: false, code: "dependency-cycle", message: "Task cannot depend on itself" };
|
|
488
|
+
}
|
|
489
|
+
const task = store.getTask(taskId);
|
|
490
|
+
const dep = store.getTask(dependencyTaskId);
|
|
491
|
+
if (!task || !dep) {
|
|
492
|
+
return { ok: false, code: "task-not-found", message: "taskId or dependencyTaskId not found" };
|
|
493
|
+
}
|
|
494
|
+
const deps = new Set(task.dependsOn ?? []);
|
|
495
|
+
if (command.name === "add-dependency") {
|
|
496
|
+
if (deps.has(dependencyTaskId)) {
|
|
497
|
+
return { ok: false, code: "duplicate-dependency", message: "Dependency already exists" };
|
|
498
|
+
}
|
|
499
|
+
deps.add(dependencyTaskId);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
deps.delete(dependencyTaskId);
|
|
503
|
+
}
|
|
504
|
+
const updatedTask = { ...task, dependsOn: [...deps], updatedAt: nowIso() };
|
|
505
|
+
store.updateTask(updatedTask);
|
|
506
|
+
const mutationType = command.name === "add-dependency" ? "add-dependency" : "remove-dependency";
|
|
507
|
+
store.addMutationEvidence(mutationEvidence(mutationType, taskId, actor, { dependencyTaskId }));
|
|
508
|
+
await store.save();
|
|
509
|
+
return {
|
|
510
|
+
ok: true,
|
|
511
|
+
code: command.name === "add-dependency" ? "dependency-added" : "dependency-removed",
|
|
512
|
+
message: `${command.name} applied for '${taskId}'`,
|
|
513
|
+
data: { task: updatedTask }
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (command.name === "get-dependency-graph") {
|
|
517
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
518
|
+
const tasks = store.getActiveTasks();
|
|
519
|
+
const nodes = tasks.map((task) => ({ id: task.id, status: task.status }));
|
|
520
|
+
const edges = tasks.flatMap((task) => (task.dependsOn ?? []).map((depId) => ({ from: task.id, to: depId })));
|
|
521
|
+
if (!taskId) {
|
|
522
|
+
return { ok: true, code: "dependency-graph", data: { nodes, edges } };
|
|
523
|
+
}
|
|
524
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
525
|
+
if (!task) {
|
|
526
|
+
return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
code: "dependency-graph",
|
|
531
|
+
data: {
|
|
532
|
+
taskId,
|
|
533
|
+
dependsOn: task.dependsOn ?? [],
|
|
534
|
+
directDependents: tasks.filter((candidate) => (candidate.dependsOn ?? []).includes(taskId)).map((x) => x.id),
|
|
535
|
+
nodes,
|
|
536
|
+
edges
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (command.name === "get-task-history" || command.name === "get-recent-task-activity") {
|
|
541
|
+
const limitRaw = args.limit;
|
|
542
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0
|
|
543
|
+
? Math.min(Math.floor(limitRaw), 500)
|
|
544
|
+
: 50;
|
|
545
|
+
const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
|
|
546
|
+
const transitions = store.getTransitionLog().map((entry) => ({ kind: "transition", ...entry }));
|
|
547
|
+
const mutations = store.getMutationLog().map((entry) => ({ kind: "mutation", ...entry }));
|
|
548
|
+
const merged = [...transitions, ...mutations]
|
|
549
|
+
.filter((entry) => (taskId ? entry.taskId === taskId : true))
|
|
550
|
+
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
|
|
551
|
+
.slice(0, limit);
|
|
552
|
+
return {
|
|
553
|
+
ok: true,
|
|
554
|
+
code: command.name === "get-task-history" ? "task-history" : "recent-task-activity",
|
|
555
|
+
data: { taskId: taskId ?? null, items: merged, count: merged.length }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
168
558
|
if (command.name === "dashboard-summary") {
|
|
169
|
-
const tasks = store.
|
|
559
|
+
const tasks = store.getActiveTasks();
|
|
170
560
|
const suggestion = getNextActions(tasks);
|
|
171
561
|
const workspaceStatus = await readWorkspaceStatusSnapshot(ctx.workspacePath);
|
|
172
562
|
const readyTop = suggestion.readyQueue.slice(0, 15).map((t) => ({
|
|
@@ -176,6 +566,15 @@ export const taskEngineModule = {
|
|
|
176
566
|
phase: t.phase ?? null
|
|
177
567
|
}));
|
|
178
568
|
const blockedTop = suggestion.blockingAnalysis.slice(0, 15);
|
|
569
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
570
|
+
try {
|
|
571
|
+
await wishlistStore.load();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
/* wishlist store optional */
|
|
575
|
+
}
|
|
576
|
+
const wishlistItems = wishlistStore.getAllItems();
|
|
577
|
+
const wishlistOpenCount = wishlistItems.filter((i) => i.status === "open").length;
|
|
179
578
|
const data = {
|
|
180
579
|
schemaVersion: 1,
|
|
181
580
|
taskStoreLastUpdated: store.getLastUpdated(),
|
|
@@ -183,6 +582,12 @@ export const taskEngineModule = {
|
|
|
183
582
|
stateSummary: suggestion.stateSummary,
|
|
184
583
|
readyQueueTop: readyTop,
|
|
185
584
|
readyQueueCount: suggestion.readyQueue.length,
|
|
585
|
+
executionPlanningScope: "tasks-only",
|
|
586
|
+
wishlist: {
|
|
587
|
+
schemaVersion: 1,
|
|
588
|
+
openCount: wishlistOpenCount,
|
|
589
|
+
totalCount: wishlistItems.length
|
|
590
|
+
},
|
|
186
591
|
blockedSummary: {
|
|
187
592
|
count: suggestion.blockingAnalysis.length,
|
|
188
593
|
top: blockedTop
|
|
@@ -208,7 +613,8 @@ export const taskEngineModule = {
|
|
|
208
613
|
if (command.name === "list-tasks") {
|
|
209
614
|
const statusFilter = typeof args.status === "string" ? args.status : undefined;
|
|
210
615
|
const phaseFilter = typeof args.phase === "string" ? args.phase : undefined;
|
|
211
|
-
|
|
616
|
+
const includeArchived = args.includeArchived === true;
|
|
617
|
+
let tasks = includeArchived ? store.getAllTasks() : store.getActiveTasks();
|
|
212
618
|
if (statusFilter) {
|
|
213
619
|
tasks = tasks.filter((t) => t.status === statusFilter);
|
|
214
620
|
}
|
|
@@ -219,11 +625,11 @@ export const taskEngineModule = {
|
|
|
219
625
|
ok: true,
|
|
220
626
|
code: "tasks-listed",
|
|
221
627
|
message: `Found ${tasks.length} tasks`,
|
|
222
|
-
data: { tasks, count: tasks.length }
|
|
628
|
+
data: { tasks, count: tasks.length, scope: "tasks-only" }
|
|
223
629
|
};
|
|
224
630
|
}
|
|
225
631
|
if (command.name === "get-ready-queue") {
|
|
226
|
-
const tasks = store.
|
|
632
|
+
const tasks = store.getActiveTasks();
|
|
227
633
|
const ready = tasks
|
|
228
634
|
.filter((t) => t.status === "ready")
|
|
229
635
|
.sort((a, b) => {
|
|
@@ -235,11 +641,11 @@ export const taskEngineModule = {
|
|
|
235
641
|
ok: true,
|
|
236
642
|
code: "ready-queue-retrieved",
|
|
237
643
|
message: `${ready.length} tasks in ready queue`,
|
|
238
|
-
data: { tasks: ready, count: ready.length }
|
|
644
|
+
data: { tasks: ready, count: ready.length, scope: "tasks-only" }
|
|
239
645
|
};
|
|
240
646
|
}
|
|
241
647
|
if (command.name === "get-next-actions") {
|
|
242
|
-
const tasks = store.
|
|
648
|
+
const tasks = store.getActiveTasks();
|
|
243
649
|
const suggestion = getNextActions(tasks);
|
|
244
650
|
return {
|
|
245
651
|
ok: true,
|
|
@@ -247,7 +653,232 @@ export const taskEngineModule = {
|
|
|
247
653
|
message: suggestion.suggestedNext
|
|
248
654
|
? `Suggested next: ${suggestion.suggestedNext.id} — ${suggestion.suggestedNext.title}`
|
|
249
655
|
: "No tasks in ready queue",
|
|
250
|
-
data: suggestion
|
|
656
|
+
data: { ...suggestion, scope: "tasks-only" }
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (command.name === "get-task-summary") {
|
|
660
|
+
const tasks = store.getActiveTasks();
|
|
661
|
+
const suggestion = getNextActions(tasks);
|
|
662
|
+
return {
|
|
663
|
+
ok: true,
|
|
664
|
+
code: "task-summary",
|
|
665
|
+
data: {
|
|
666
|
+
scope: "tasks-only",
|
|
667
|
+
stateSummary: suggestion.stateSummary,
|
|
668
|
+
readyQueueCount: suggestion.readyQueue.length,
|
|
669
|
+
suggestedNext: suggestion.suggestedNext
|
|
670
|
+
? {
|
|
671
|
+
id: suggestion.suggestedNext.id,
|
|
672
|
+
title: suggestion.suggestedNext.title,
|
|
673
|
+
priority: suggestion.suggestedNext.priority ?? null
|
|
674
|
+
}
|
|
675
|
+
: null
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (command.name === "get-blocked-summary") {
|
|
680
|
+
const tasks = store.getActiveTasks();
|
|
681
|
+
const suggestion = getNextActions(tasks);
|
|
682
|
+
return {
|
|
683
|
+
ok: true,
|
|
684
|
+
code: "blocked-summary",
|
|
685
|
+
data: {
|
|
686
|
+
blockedCount: suggestion.blockingAnalysis.length,
|
|
687
|
+
blockedItems: suggestion.blockingAnalysis,
|
|
688
|
+
scope: "tasks-only"
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (command.name === "create-wishlist") {
|
|
693
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
694
|
+
await wishlistStore.load();
|
|
695
|
+
const raw = args;
|
|
696
|
+
const v = validateWishlistIntakePayload(raw);
|
|
697
|
+
if (!v.ok) {
|
|
698
|
+
return { ok: false, code: "invalid-task-schema", message: v.errors.join(" ") };
|
|
699
|
+
}
|
|
700
|
+
const ts = nowIso();
|
|
701
|
+
const item = buildWishlistItemFromIntake(raw, ts);
|
|
702
|
+
try {
|
|
703
|
+
wishlistStore.addItem(item);
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
if (err instanceof TaskEngineError) {
|
|
707
|
+
return { ok: false, code: err.code, message: err.message };
|
|
708
|
+
}
|
|
709
|
+
throw err;
|
|
710
|
+
}
|
|
711
|
+
await wishlistStore.save();
|
|
712
|
+
return {
|
|
713
|
+
ok: true,
|
|
714
|
+
code: "wishlist-created",
|
|
715
|
+
message: `Created wishlist '${item.id}'`,
|
|
716
|
+
data: { item }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (command.name === "list-wishlist") {
|
|
720
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
721
|
+
await wishlistStore.load();
|
|
722
|
+
const statusFilter = typeof args.status === "string" ? args.status : undefined;
|
|
723
|
+
let items = wishlistStore.getAllItems();
|
|
724
|
+
if (statusFilter && ["open", "converted", "cancelled"].includes(statusFilter)) {
|
|
725
|
+
items = items.filter((i) => i.status === statusFilter);
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
ok: true,
|
|
729
|
+
code: "wishlist-listed",
|
|
730
|
+
message: `Found ${items.length} wishlist items`,
|
|
731
|
+
data: { items, count: items.length, scope: "wishlist-only" }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (command.name === "get-wishlist") {
|
|
735
|
+
const wishlistId = typeof args.wishlistId === "string" && args.wishlistId.trim().length > 0
|
|
736
|
+
? args.wishlistId.trim()
|
|
737
|
+
: typeof args.id === "string" && args.id.trim().length > 0
|
|
738
|
+
? args.id.trim()
|
|
739
|
+
: "";
|
|
740
|
+
if (!wishlistId) {
|
|
741
|
+
return { ok: false, code: "invalid-task-schema", message: "get-wishlist requires 'wishlistId' or 'id'" };
|
|
742
|
+
}
|
|
743
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
744
|
+
await wishlistStore.load();
|
|
745
|
+
const item = wishlistStore.getItem(wishlistId);
|
|
746
|
+
if (!item) {
|
|
747
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
ok: true,
|
|
751
|
+
code: "wishlist-retrieved",
|
|
752
|
+
data: { item }
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (command.name === "update-wishlist") {
|
|
756
|
+
const wishlistId = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
|
|
757
|
+
const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
|
|
758
|
+
if (!wishlistId || !updates) {
|
|
759
|
+
return { ok: false, code: "invalid-task-schema", message: "update-wishlist requires wishlistId and updates" };
|
|
760
|
+
}
|
|
761
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
762
|
+
await wishlistStore.load();
|
|
763
|
+
const existing = wishlistStore.getItem(wishlistId);
|
|
764
|
+
if (!existing) {
|
|
765
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
766
|
+
}
|
|
767
|
+
if (existing.status !== "open") {
|
|
768
|
+
return { ok: false, code: "invalid-transition", message: "Only open wishlist items can be updated" };
|
|
769
|
+
}
|
|
770
|
+
const uv = validateWishlistUpdatePayload(updates);
|
|
771
|
+
if (!uv.ok) {
|
|
772
|
+
return { ok: false, code: "invalid-task-schema", message: uv.errors.join(" ") };
|
|
773
|
+
}
|
|
774
|
+
const merged = { ...existing, updatedAt: nowIso() };
|
|
775
|
+
const mutable = [
|
|
776
|
+
"title",
|
|
777
|
+
"problemStatement",
|
|
778
|
+
"expectedOutcome",
|
|
779
|
+
"impact",
|
|
780
|
+
"constraints",
|
|
781
|
+
"successSignals",
|
|
782
|
+
"requestor",
|
|
783
|
+
"evidenceRef"
|
|
784
|
+
];
|
|
785
|
+
for (const key of mutable) {
|
|
786
|
+
if (key in updates && typeof updates[key] === "string") {
|
|
787
|
+
merged[key] = updates[key].trim();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
wishlistStore.updateItem(merged);
|
|
791
|
+
await wishlistStore.save();
|
|
792
|
+
return {
|
|
793
|
+
ok: true,
|
|
794
|
+
code: "wishlist-updated",
|
|
795
|
+
message: `Updated wishlist '${wishlistId}'`,
|
|
796
|
+
data: { item: merged }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (command.name === "convert-wishlist") {
|
|
800
|
+
const wishlistId = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
|
|
801
|
+
if (!wishlistId || !WISHLIST_ID_RE.test(wishlistId)) {
|
|
802
|
+
return {
|
|
803
|
+
ok: false,
|
|
804
|
+
code: "invalid-task-schema",
|
|
805
|
+
message: "convert-wishlist requires wishlistId matching W<number>"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const dec = parseConversionDecomposition(args.decomposition);
|
|
809
|
+
if (!dec.ok) {
|
|
810
|
+
return { ok: false, code: "invalid-task-schema", message: dec.message };
|
|
811
|
+
}
|
|
812
|
+
const tasksRaw = args.tasks;
|
|
813
|
+
if (!Array.isArray(tasksRaw) || tasksRaw.length === 0) {
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
code: "invalid-task-schema",
|
|
817
|
+
message: "convert-wishlist requires non-empty tasks array"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
821
|
+
await wishlistStore.load();
|
|
822
|
+
const wlItem = wishlistStore.getItem(wishlistId);
|
|
823
|
+
if (!wlItem) {
|
|
824
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
825
|
+
}
|
|
826
|
+
if (wlItem.status !== "open") {
|
|
827
|
+
return {
|
|
828
|
+
ok: false,
|
|
829
|
+
code: "invalid-transition",
|
|
830
|
+
message: "Only open wishlist items can be converted"
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const actor = typeof args.actor === "string"
|
|
834
|
+
? args.actor
|
|
835
|
+
: ctx.resolvedActor !== undefined
|
|
836
|
+
? ctx.resolvedActor
|
|
837
|
+
: undefined;
|
|
838
|
+
const timestamp = nowIso();
|
|
839
|
+
const built = [];
|
|
840
|
+
for (const row of tasksRaw) {
|
|
841
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
842
|
+
return { ok: false, code: "invalid-task-schema", message: "Each task must be an object" };
|
|
843
|
+
}
|
|
844
|
+
const bt = buildTaskFromConversionPayload(row, timestamp);
|
|
845
|
+
if (!bt.ok) {
|
|
846
|
+
return { ok: false, code: "invalid-task-schema", message: bt.message };
|
|
847
|
+
}
|
|
848
|
+
if (store.getTask(bt.task.id)) {
|
|
849
|
+
return {
|
|
850
|
+
ok: false,
|
|
851
|
+
code: "duplicate-task-id",
|
|
852
|
+
message: `Task '${bt.task.id}' already exists`
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
built.push(bt.task);
|
|
856
|
+
}
|
|
857
|
+
for (const t of built) {
|
|
858
|
+
store.addTask(t);
|
|
859
|
+
store.addMutationEvidence(mutationEvidence("create-task", t.id, actor, {
|
|
860
|
+
initialStatus: t.status,
|
|
861
|
+
source: "convert-wishlist",
|
|
862
|
+
wishlistId
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
const convertedIds = built.map((t) => t.id);
|
|
866
|
+
const updatedWishlist = {
|
|
867
|
+
...wlItem,
|
|
868
|
+
status: "converted",
|
|
869
|
+
updatedAt: timestamp,
|
|
870
|
+
convertedAt: timestamp,
|
|
871
|
+
convertedToTaskIds: convertedIds,
|
|
872
|
+
conversionDecomposition: dec.value
|
|
873
|
+
};
|
|
874
|
+
wishlistStore.updateItem(updatedWishlist);
|
|
875
|
+
await store.save();
|
|
876
|
+
await wishlistStore.save();
|
|
877
|
+
return {
|
|
878
|
+
ok: true,
|
|
879
|
+
code: "wishlist-converted",
|
|
880
|
+
message: `Converted wishlist '${wishlistId}' to tasks: ${convertedIds.join(", ")}`,
|
|
881
|
+
data: { wishlist: updatedWishlist, createdTasks: built }
|
|
251
882
|
};
|
|
252
883
|
}
|
|
253
884
|
return {
|
|
@@ -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[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WishlistItem } from "./wishlist-types.js";
|
|
2
|
+
export declare class WishlistStore {
|
|
3
|
+
private document;
|
|
4
|
+
private readonly filePath;
|
|
5
|
+
constructor(workspacePath: string, storeRelativePath?: string);
|
|
6
|
+
load(): Promise<void>;
|
|
7
|
+
save(): Promise<void>;
|
|
8
|
+
getAllItems(): WishlistItem[];
|
|
9
|
+
getItem(id: string): WishlistItem | undefined;
|
|
10
|
+
addItem(item: WishlistItem): void;
|
|
11
|
+
updateItem(item: WishlistItem): void;
|
|
12
|
+
getFilePath(): string;
|
|
13
|
+
getLastUpdated(): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { TaskEngineError } from "./transitions.js";
|
|
5
|
+
const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
|
|
6
|
+
function emptyWishlistDoc() {
|
|
7
|
+
return {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
items: [],
|
|
10
|
+
lastUpdated: new Date().toISOString()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export class WishlistStore {
|
|
14
|
+
document;
|
|
15
|
+
filePath;
|
|
16
|
+
constructor(workspacePath, storeRelativePath) {
|
|
17
|
+
this.filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
|
|
18
|
+
this.document = emptyWishlistDoc();
|
|
19
|
+
}
|
|
20
|
+
async load() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(this.filePath, "utf8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (parsed.schemaVersion !== 1) {
|
|
25
|
+
throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema version: ${parsed.schemaVersion}`);
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(parsed.items)) {
|
|
28
|
+
throw new TaskEngineError("storage-read-error", "Wishlist store 'items' must be an array");
|
|
29
|
+
}
|
|
30
|
+
this.document = parsed;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code === "ENOENT") {
|
|
34
|
+
this.document = emptyWishlistDoc();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (err instanceof TaskEngineError)
|
|
38
|
+
throw err;
|
|
39
|
+
throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async save() {
|
|
43
|
+
this.document.lastUpdated = new Date().toISOString();
|
|
44
|
+
const dir = path.dirname(this.filePath);
|
|
45
|
+
const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
|
|
46
|
+
try {
|
|
47
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
|
+
await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
|
|
49
|
+
await fs.rename(tmpPath, this.filePath);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(tmpPath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* cleanup best-effort */
|
|
57
|
+
}
|
|
58
|
+
throw new TaskEngineError("storage-write-error", `Failed to write wishlist store: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getAllItems() {
|
|
62
|
+
return [...this.document.items];
|
|
63
|
+
}
|
|
64
|
+
getItem(id) {
|
|
65
|
+
return this.document.items.find((i) => i.id === id);
|
|
66
|
+
}
|
|
67
|
+
addItem(item) {
|
|
68
|
+
if (this.document.items.some((i) => i.id === item.id)) {
|
|
69
|
+
throw new TaskEngineError("duplicate-task-id", `Wishlist item '${item.id}' already exists`);
|
|
70
|
+
}
|
|
71
|
+
this.document.items.push({ ...item });
|
|
72
|
+
}
|
|
73
|
+
updateItem(item) {
|
|
74
|
+
const idx = this.document.items.findIndex((i) => i.id === item.id);
|
|
75
|
+
if (idx === -1) {
|
|
76
|
+
throw new TaskEngineError("task-not-found", `Wishlist item '${item.id}' not found`);
|
|
77
|
+
}
|
|
78
|
+
this.document.items[idx] = { ...item };
|
|
79
|
+
}
|
|
80
|
+
getFilePath() {
|
|
81
|
+
return this.filePath;
|
|
82
|
+
}
|
|
83
|
+
getLastUpdated() {
|
|
84
|
+
return this.document.lastUpdated;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wishlist items live in a separate namespace from Task Engine tasks (`T###`).
|
|
3
|
+
* They are ideation-only until converted into canonical tasks via `convert-wishlist`.
|
|
4
|
+
*/
|
|
5
|
+
export type WishlistStatus = "open" | "converted" | "cancelled";
|
|
6
|
+
/** Recorded when a wishlist item is converted into one or more tasks. */
|
|
7
|
+
export type WishlistConversionDecomposition = {
|
|
8
|
+
rationale: string;
|
|
9
|
+
boundaries: string;
|
|
10
|
+
dependencyIntent: string;
|
|
11
|
+
};
|
|
12
|
+
export type WishlistItem = {
|
|
13
|
+
id: string;
|
|
14
|
+
status: WishlistStatus;
|
|
15
|
+
title: string;
|
|
16
|
+
problemStatement: string;
|
|
17
|
+
expectedOutcome: string;
|
|
18
|
+
impact: string;
|
|
19
|
+
constraints: string;
|
|
20
|
+
successSignals: string;
|
|
21
|
+
requestor: string;
|
|
22
|
+
evidenceRef: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
convertedAt?: string;
|
|
26
|
+
convertedToTaskIds?: string[];
|
|
27
|
+
conversionDecomposition?: WishlistConversionDecomposition;
|
|
28
|
+
};
|
|
29
|
+
export type WishlistStoreDocument = {
|
|
30
|
+
schemaVersion: 1;
|
|
31
|
+
items: WishlistItem[];
|
|
32
|
+
lastUpdated: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WishlistItem } from "./wishlist-types.js";
|
|
2
|
+
/** Wishlist identifiers use a dedicated namespace: `W` + digits (e.g. `W1`, `W42`). */
|
|
3
|
+
export declare const WISHLIST_ID_RE: RegExp;
|
|
4
|
+
export type WishlistValidationResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
errors: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Validates intake fields for creating or replacing content on an open wishlist item.
|
|
12
|
+
* Wishlist items never carry a Task Engine `phase`; reject if present.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateWishlistIntakePayload(args: Record<string, unknown>): WishlistValidationResult;
|
|
15
|
+
export declare function buildWishlistItemFromIntake(args: Record<string, unknown>, timestamp: string): Omit<WishlistItem, "convertedAt" | "convertedToTaskIds" | "conversionDecomposition">;
|
|
16
|
+
export declare function validateWishlistUpdatePayload(updates: Record<string, unknown>): WishlistValidationResult;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Wishlist identifiers use a dedicated namespace: `W` + digits (e.g. `W1`, `W42`). */
|
|
2
|
+
export const WISHLIST_ID_RE = /^W\d+$/;
|
|
3
|
+
const REQUIRED_STRING_FIELDS = [
|
|
4
|
+
"title",
|
|
5
|
+
"problemStatement",
|
|
6
|
+
"expectedOutcome",
|
|
7
|
+
"impact",
|
|
8
|
+
"constraints",
|
|
9
|
+
"successSignals",
|
|
10
|
+
"requestor",
|
|
11
|
+
"evidenceRef"
|
|
12
|
+
];
|
|
13
|
+
function nonEmptyString(v, label) {
|
|
14
|
+
if (typeof v !== "string" || v.trim().length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return v.trim();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validates intake fields for creating or replacing content on an open wishlist item.
|
|
21
|
+
* Wishlist items never carry a Task Engine `phase`; reject if present.
|
|
22
|
+
*/
|
|
23
|
+
export function validateWishlistIntakePayload(args) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if ("phase" in args && args.phase !== undefined) {
|
|
26
|
+
errors.push("Wishlist items must not include 'phase'; only canonical tasks are phased.");
|
|
27
|
+
}
|
|
28
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
29
|
+
if (!id) {
|
|
30
|
+
errors.push("Wishlist 'id' is required.");
|
|
31
|
+
}
|
|
32
|
+
else if (!WISHLIST_ID_RE.test(id)) {
|
|
33
|
+
errors.push(`Wishlist 'id' must match ${WISHLIST_ID_RE.source} (e.g. W1).`);
|
|
34
|
+
}
|
|
35
|
+
for (const key of REQUIRED_STRING_FIELDS) {
|
|
36
|
+
const s = nonEmptyString(args[key], key);
|
|
37
|
+
if (s === null) {
|
|
38
|
+
errors.push(`Wishlist '${key}' is required and must be a non-empty string.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (errors.length > 0) {
|
|
42
|
+
return { ok: false, errors };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
export function buildWishlistItemFromIntake(args, timestamp) {
|
|
47
|
+
const id = args.id.trim();
|
|
48
|
+
const item = {
|
|
49
|
+
id,
|
|
50
|
+
status: "open",
|
|
51
|
+
title: args.title.trim(),
|
|
52
|
+
problemStatement: args.problemStatement.trim(),
|
|
53
|
+
expectedOutcome: args.expectedOutcome.trim(),
|
|
54
|
+
impact: args.impact.trim(),
|
|
55
|
+
constraints: args.constraints.trim(),
|
|
56
|
+
successSignals: args.successSignals.trim(),
|
|
57
|
+
requestor: args.requestor.trim(),
|
|
58
|
+
evidenceRef: args.evidenceRef.trim(),
|
|
59
|
+
createdAt: timestamp,
|
|
60
|
+
updatedAt: timestamp
|
|
61
|
+
};
|
|
62
|
+
return item;
|
|
63
|
+
}
|
|
64
|
+
export function validateWishlistUpdatePayload(updates) {
|
|
65
|
+
if ("phase" in updates && updates.phase !== undefined) {
|
|
66
|
+
return { ok: false, errors: ["Wishlist updates cannot set 'phase'."] };
|
|
67
|
+
}
|
|
68
|
+
const errors = [];
|
|
69
|
+
const allowed = new Set([
|
|
70
|
+
"title",
|
|
71
|
+
"problemStatement",
|
|
72
|
+
"expectedOutcome",
|
|
73
|
+
"impact",
|
|
74
|
+
"constraints",
|
|
75
|
+
"successSignals",
|
|
76
|
+
"requestor",
|
|
77
|
+
"evidenceRef"
|
|
78
|
+
]);
|
|
79
|
+
for (const key of Object.keys(updates)) {
|
|
80
|
+
if (!allowed.has(key)) {
|
|
81
|
+
errors.push(`Cannot update unknown or immutable wishlist field '${key}'.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const key of REQUIRED_STRING_FIELDS) {
|
|
85
|
+
if (key in updates) {
|
|
86
|
+
const s = nonEmptyString(updates[key], key);
|
|
87
|
+
if (s === null) {
|
|
88
|
+
errors.push(`Wishlist '${key}' must be a non-empty string when provided.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
return { ok: false, errors };
|
|
94
|
+
}
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|