@workflow-cannon/workspace-kit 0.18.0 → 0.24.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.
Files changed (140) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/doctor-planning-issues.js +3 -22
  3. package/dist/cli/run-command.js +22 -38
  4. package/dist/cli.js +95 -4
  5. package/dist/contracts/command-manifest.d.ts +17 -0
  6. package/dist/contracts/command-manifest.js +1 -0
  7. package/dist/contracts/index.d.ts +1 -1
  8. package/dist/contracts/module-contract.d.ts +12 -11
  9. package/dist/core/agent-instruction-surface.d.ts +33 -0
  10. package/dist/core/agent-instruction-surface.js +46 -0
  11. package/dist/core/config-cli.js +13 -17
  12. package/dist/core/config-metadata.js +61 -2
  13. package/dist/core/index.d.ts +4 -1
  14. package/dist/core/index.js +3 -0
  15. package/dist/core/module-command-router.js +19 -1
  16. package/dist/core/module-registry-resolve.d.ts +27 -0
  17. package/dist/core/module-registry-resolve.js +91 -0
  18. package/dist/core/module-registry.d.ts +14 -0
  19. package/dist/core/module-registry.js +57 -0
  20. package/dist/core/planning/build-plan-session-file.d.ts +29 -0
  21. package/dist/core/planning/build-plan-session-file.js +58 -0
  22. package/dist/core/planning/index.d.ts +17 -0
  23. package/dist/core/planning/index.js +15 -0
  24. package/dist/core/policy.js +18 -8
  25. package/dist/core/state/unified-state-db.d.ts +21 -0
  26. package/dist/core/state/unified-state-db.js +80 -0
  27. package/dist/core/workspace-kit-config.js +8 -0
  28. package/dist/modules/agent-behavior/builtins.d.ts +3 -0
  29. package/dist/modules/agent-behavior/builtins.js +71 -0
  30. package/dist/modules/agent-behavior/explain.d.ts +6 -0
  31. package/dist/modules/agent-behavior/explain.js +46 -0
  32. package/dist/modules/agent-behavior/index.d.ts +4 -0
  33. package/dist/modules/agent-behavior/index.js +461 -0
  34. package/dist/modules/agent-behavior/interview-session-file.d.ts +9 -0
  35. package/dist/modules/agent-behavior/interview-session-file.js +43 -0
  36. package/dist/modules/agent-behavior/interview.d.ts +13 -0
  37. package/dist/modules/agent-behavior/interview.js +88 -0
  38. package/dist/modules/agent-behavior/persistence.d.ts +6 -0
  39. package/dist/modules/agent-behavior/persistence.js +89 -0
  40. package/dist/modules/agent-behavior/store.d.ts +34 -0
  41. package/dist/modules/agent-behavior/store.js +119 -0
  42. package/dist/modules/agent-behavior/types.d.ts +28 -0
  43. package/dist/modules/agent-behavior/types.js +1 -0
  44. package/dist/modules/agent-behavior/validate.d.ts +11 -0
  45. package/dist/modules/agent-behavior/validate.js +123 -0
  46. package/dist/modules/approvals/index.js +54 -51
  47. package/dist/modules/approvals/policy-sensitive-commands.d.ts +4 -0
  48. package/dist/modules/approvals/policy-sensitive-commands.js +4 -0
  49. package/dist/modules/approvals/review-runtime.js +1 -2
  50. package/dist/modules/documentation/index.js +47 -45
  51. package/dist/modules/documentation/normalizer.d.ts +3 -0
  52. package/dist/modules/documentation/normalizer.js +171 -0
  53. package/dist/modules/documentation/parser.d.ts +7 -0
  54. package/dist/modules/documentation/parser.js +39 -0
  55. package/dist/modules/documentation/policy-sensitive-commands.d.ts +5 -0
  56. package/dist/modules/documentation/policy-sensitive-commands.js +8 -0
  57. package/dist/modules/documentation/renderer.d.ts +23 -0
  58. package/dist/modules/documentation/renderer.js +105 -0
  59. package/dist/modules/documentation/runtime-batch.d.ts +10 -0
  60. package/dist/modules/documentation/runtime-batch.js +67 -0
  61. package/dist/modules/documentation/runtime-config.d.ts +11 -0
  62. package/dist/modules/documentation/runtime-config.js +54 -0
  63. package/dist/modules/documentation/runtime-render-support.d.ts +8 -0
  64. package/dist/modules/documentation/runtime-render-support.js +36 -0
  65. package/dist/modules/documentation/runtime.js +22 -510
  66. package/dist/modules/documentation/types.d.ts +182 -0
  67. package/dist/modules/documentation/validator.d.ts +8 -0
  68. package/dist/modules/documentation/validator.js +234 -0
  69. package/dist/modules/documentation/view-models.d.ts +3 -0
  70. package/dist/modules/documentation/view-models.js +124 -0
  71. package/dist/modules/improvement/generate-recommendations-runtime.js +3 -3
  72. package/dist/modules/improvement/improvement-state.d.ts +2 -2
  73. package/dist/modules/improvement/improvement-state.js +52 -23
  74. package/dist/modules/improvement/index.js +140 -138
  75. package/dist/modules/improvement/ingest.d.ts +1 -1
  76. package/dist/modules/improvement/policy-sensitive-commands.d.ts +4 -0
  77. package/dist/modules/improvement/policy-sensitive-commands.js +7 -0
  78. package/dist/modules/index.d.ts +6 -0
  79. package/dist/modules/index.js +17 -0
  80. package/dist/modules/planning/index.js +384 -50
  81. package/dist/modules/planning/question-engine.d.ts +2 -0
  82. package/dist/modules/planning/question-engine.js +8 -1
  83. package/dist/modules/task-engine/doctor-planning-persistence.js +21 -13
  84. package/dist/modules/task-engine/index.d.ts +1 -2
  85. package/dist/modules/task-engine/index.js +1 -1143
  86. package/dist/modules/task-engine/migrate-task-persistence-runtime.js +31 -4
  87. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.d.ts +2 -0
  88. package/dist/modules/task-engine/migrate-wishlist-intake-runtime.js +146 -0
  89. package/dist/modules/task-engine/planning-open.d.ts +2 -9
  90. package/dist/modules/task-engine/planning-open.js +4 -15
  91. package/dist/modules/task-engine/policy-sensitive-commands.d.ts +5 -0
  92. package/dist/modules/task-engine/policy-sensitive-commands.js +5 -0
  93. package/dist/modules/task-engine/sqlite-dual-planning.d.ts +11 -2
  94. package/dist/modules/task-engine/sqlite-dual-planning.js +134 -28
  95. package/dist/modules/task-engine/strict-task-validation.js +3 -0
  96. package/dist/modules/task-engine/suggestions.js +2 -1
  97. package/dist/modules/task-engine/task-engine-internal.d.ts +2 -0
  98. package/dist/modules/task-engine/task-engine-internal.js +1304 -0
  99. package/dist/modules/task-engine/task-type-validation.js +40 -0
  100. package/dist/modules/task-engine/wishlist-intake.d.ts +22 -0
  101. package/dist/modules/task-engine/wishlist-intake.js +180 -0
  102. package/dist/modules/task-engine/wishlist-validation.d.ts +4 -0
  103. package/dist/modules/task-engine/wishlist-validation.js +19 -0
  104. package/dist/modules/workspace-config/index.js +9 -11
  105. package/package.json +2 -2
  106. package/schemas/agent-behavior-profile.schema.json +52 -0
  107. package/schemas/task-engine-run-contracts.schema.json +80 -5
  108. package/src/modules/documentation/README.md +16 -25
  109. package/src/modules/documentation/RULES.md +9 -9
  110. package/src/modules/documentation/index.ts +54 -49
  111. package/src/modules/documentation/instructions/document-project.md +6 -6
  112. package/src/modules/documentation/instructions/generate-document.md +4 -4
  113. package/src/modules/documentation/normalizer.ts +187 -0
  114. package/src/modules/documentation/parser.ts +41 -0
  115. package/src/modules/documentation/policy-sensitive-commands.ts +8 -0
  116. package/src/modules/documentation/renderer.ts +121 -0
  117. package/src/modules/documentation/runtime-batch.ts +74 -0
  118. package/src/modules/documentation/runtime-config.ts +68 -0
  119. package/src/modules/documentation/runtime-render-support.ts +39 -0
  120. package/src/modules/documentation/runtime.ts +28 -600
  121. package/src/modules/documentation/schemas/documentation-schema.md +37 -54
  122. package/src/modules/documentation/types.ts +228 -0
  123. package/src/modules/documentation/validator.ts +247 -0
  124. package/src/modules/documentation/view-models.ts +132 -0
  125. package/src/modules/documentation/views/agents.view.yaml +18 -0
  126. package/src/modules/documentation/views/architecture.view.yaml +18 -0
  127. package/src/modules/documentation/views/principles.view.yaml +18 -0
  128. package/src/modules/documentation/views/readme.view.yaml +18 -0
  129. package/src/modules/documentation/views/releasing.view.yaml +18 -0
  130. package/src/modules/documentation/views/roadmap.view.yaml +18 -0
  131. package/src/modules/documentation/views/runbooks-consumer-cadence.view.yaml +18 -0
  132. package/src/modules/documentation/views/runbooks-parity-validation-flow.view.yaml +18 -0
  133. package/src/modules/documentation/views/runbooks-release-channels.view.yaml +18 -0
  134. package/src/modules/documentation/views/security.view.yaml +18 -0
  135. package/src/modules/documentation/views/support.view.yaml +18 -0
  136. package/src/modules/documentation/views/terms.view.yaml +18 -0
  137. package/src/modules/documentation/views/workbooks-phase2-config-policy-workbook.view.yaml +18 -0
  138. package/src/modules/documentation/views/workbooks-task-engine-workbook.view.yaml +18 -0
  139. package/src/modules/documentation/views/workbooks-transcript-automation-baseline.view.yaml +18 -0
  140. package/src/modules/documentation/state.md +0 -8
@@ -0,0 +1,1304 @@
1
+ import crypto from "node:crypto";
2
+ import { maybeSpawnTranscriptHookAfterCompletion } from "../../core/transcript-completion-hook.js";
3
+ import { TransitionService } from "./service.js";
4
+ import { TaskEngineError, getAllowedTransitionsFrom } from "./transitions.js";
5
+ import { getNextActions } from "./suggestions.js";
6
+ import { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
7
+ import { readBuildPlanSession, toDashboardPlanningSession } from "../../core/planning/build-plan-session-file.js";
8
+ import { openPlanningStores } from "./planning-open.js";
9
+ import { runMigrateWishlistIntake } from "./migrate-wishlist-intake-runtime.js";
10
+ import { runMigrateTaskPersistence } from "./migrate-task-persistence-runtime.js";
11
+ import { planningSqliteDatabaseRelativePath, planningStrictValidationEnabled } from "./planning-config.js";
12
+ import { validateTaskSetForStrictMode } from "./strict-task-validation.js";
13
+ import { validateKnownTaskTypeRequirements } from "./task-type-validation.js";
14
+ import { UnifiedStateDb } from "../../core/state/unified-state-db.js";
15
+ import { buildWishlistItemFromIntake, validateWishlistContentFields, validateWishlistIntakePayload, validateWishlistUpdatePayload, WISHLIST_ID_RE } from "./wishlist-validation.js";
16
+ import { allocateNextTaskNumericId, findWishlistIntakeTaskByLegacyOrTaskId, isWishlistIntakeTask, LEGACY_WISHLIST_ID_METADATA_KEY, listWishlistIntakeTasksAsItems, taskEntityFromNewIntake, taskEntityFromWishlistItem, wishlistIntakeTaskToItem, WISHLIST_INTAKE_TASK_TYPE } from "./wishlist-intake.js";
17
+ const TASK_ID_RE = /^T\d+$/;
18
+ const SAFE_METADATA_PATH_RE = /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$/;
19
+ const MUTABLE_TASK_FIELDS = new Set([
20
+ "title",
21
+ "type",
22
+ "priority",
23
+ "dependsOn",
24
+ "unblocks",
25
+ "phase",
26
+ "metadata",
27
+ "ownership",
28
+ "approach",
29
+ "technicalScope",
30
+ "acceptanceCriteria"
31
+ ]);
32
+ function isRecordLike(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ function readMetadataPath(metadata, path) {
36
+ if (!metadata || !SAFE_METADATA_PATH_RE.test(path)) {
37
+ return undefined;
38
+ }
39
+ const parts = path.split(".");
40
+ let current = metadata;
41
+ for (const part of parts) {
42
+ if (!isRecordLike(current)) {
43
+ return undefined;
44
+ }
45
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
46
+ return undefined;
47
+ }
48
+ current = current[part];
49
+ }
50
+ return current;
51
+ }
52
+ function stableStringify(value) {
53
+ if (Array.isArray(value)) {
54
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
55
+ }
56
+ if (value && typeof value === "object") {
57
+ const record = value;
58
+ const keys = Object.keys(record).sort();
59
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
60
+ }
61
+ return JSON.stringify(value);
62
+ }
63
+ function digestPayload(value) {
64
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
65
+ }
66
+ function readIdempotencyValue(args) {
67
+ const raw = args.clientMutationId;
68
+ if (typeof raw !== "string") {
69
+ return undefined;
70
+ }
71
+ const trimmed = raw.trim();
72
+ return trimmed.length > 0 ? trimmed : undefined;
73
+ }
74
+ function findIdempotentMutation(store, mutationType, taskId, clientMutationId) {
75
+ const log = store.getMutationLog();
76
+ for (let idx = log.length - 1; idx >= 0; idx -= 1) {
77
+ const entry = log[idx];
78
+ if (entry.mutationType !== mutationType || entry.taskId !== taskId) {
79
+ continue;
80
+ }
81
+ if (!entry.details || entry.details.clientMutationId !== clientMutationId) {
82
+ continue;
83
+ }
84
+ return {
85
+ payloadDigest: typeof entry.details.payloadDigest === "string" ? entry.details.payloadDigest : undefined
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+ function strictValidationError(store, effectiveConfig) {
91
+ if (!planningStrictValidationEnabled({ effectiveConfig })) {
92
+ return null;
93
+ }
94
+ return validateTaskSetForStrictMode(store.getAllTasks());
95
+ }
96
+ function nowIso() {
97
+ return new Date().toISOString();
98
+ }
99
+ function parseConversionDecomposition(raw) {
100
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
101
+ return { ok: false, message: "convert-wishlist requires 'decomposition' object" };
102
+ }
103
+ const o = raw;
104
+ const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
105
+ const boundaries = typeof o.boundaries === "string" ? o.boundaries.trim() : "";
106
+ const dependencyIntent = typeof o.dependencyIntent === "string" ? o.dependencyIntent.trim() : "";
107
+ if (!rationale || !boundaries || !dependencyIntent) {
108
+ return {
109
+ ok: false,
110
+ message: "decomposition requires non-empty rationale, boundaries, and dependencyIntent"
111
+ };
112
+ }
113
+ return { ok: true, value: { rationale, boundaries, dependencyIntent } };
114
+ }
115
+ function buildTaskFromConversionPayload(row, timestamp) {
116
+ const id = typeof row.id === "string" ? row.id.trim() : "";
117
+ if (!TASK_ID_RE.test(id)) {
118
+ return { ok: false, message: "Each converted task requires 'id' matching T<number>" };
119
+ }
120
+ const title = typeof row.title === "string" ? row.title.trim() : "";
121
+ if (!title) {
122
+ return { ok: false, message: `Task '${id}' requires non-empty title` };
123
+ }
124
+ const phase = typeof row.phase === "string" ? row.phase.trim() : "";
125
+ if (!phase) {
126
+ return { ok: false, message: `Task '${id}' requires 'phase' for workable tasks` };
127
+ }
128
+ const type = typeof row.type === "string" && row.type.trim() ? row.type.trim() : "workspace-kit";
129
+ const priority = typeof row.priority === "string" && ["P1", "P2", "P3"].includes(row.priority)
130
+ ? row.priority
131
+ : undefined;
132
+ const approach = typeof row.approach === "string" ? row.approach.trim() : "";
133
+ if (!approach) {
134
+ return { ok: false, message: `Task '${id}' requires 'approach'` };
135
+ }
136
+ const technicalScope = Array.isArray(row.technicalScope)
137
+ ? row.technicalScope.filter((x) => typeof x === "string")
138
+ : [];
139
+ const acceptanceCriteria = Array.isArray(row.acceptanceCriteria)
140
+ ? row.acceptanceCriteria.filter((x) => typeof x === "string")
141
+ : [];
142
+ if (technicalScope.length === 0) {
143
+ return { ok: false, message: `Task '${id}' requires non-empty technicalScope array` };
144
+ }
145
+ if (acceptanceCriteria.length === 0) {
146
+ return { ok: false, message: `Task '${id}' requires non-empty acceptanceCriteria array` };
147
+ }
148
+ const task = {
149
+ id,
150
+ title,
151
+ type,
152
+ status: "proposed",
153
+ createdAt: timestamp,
154
+ updatedAt: timestamp,
155
+ priority,
156
+ dependsOn: Array.isArray(row.dependsOn) ? row.dependsOn.filter((x) => typeof x === "string") : undefined,
157
+ unblocks: Array.isArray(row.unblocks) ? row.unblocks.filter((x) => typeof x === "string") : undefined,
158
+ phase,
159
+ approach,
160
+ technicalScope,
161
+ acceptanceCriteria
162
+ };
163
+ return { ok: true, task };
164
+ }
165
+ function mutationEvidence(mutationType, taskId, actor, details) {
166
+ return {
167
+ mutationId: `${mutationType}-${taskId}-${nowIso()}-${crypto.randomUUID().slice(0, 8)}`,
168
+ mutationType,
169
+ taskId,
170
+ timestamp: nowIso(),
171
+ actor,
172
+ details
173
+ };
174
+ }
175
+ export const taskEngineModule = {
176
+ registration: {
177
+ id: "task-engine",
178
+ version: "0.6.0",
179
+ contractVersion: "1",
180
+ stateSchema: 1,
181
+ capabilities: ["task-engine"],
182
+ dependsOn: [],
183
+ optionalPeers: [],
184
+ enabledByDefault: true,
185
+ config: {
186
+ path: "src/modules/task-engine/config.md",
187
+ format: "md",
188
+ description: "Task Engine configuration contract."
189
+ },
190
+ instructions: {
191
+ directory: "src/modules/task-engine/instructions",
192
+ entries: [
193
+ {
194
+ name: "run-transition",
195
+ file: "run-transition.md",
196
+ description: "Execute a validated task status transition."
197
+ },
198
+ {
199
+ name: "create-task",
200
+ file: "create-task.md",
201
+ description: "Create a new task through validated task-engine persistence."
202
+ },
203
+ {
204
+ name: "update-task",
205
+ file: "update-task.md",
206
+ description: "Update mutable task fields without lifecycle bypass."
207
+ },
208
+ {
209
+ name: "update-wishlist",
210
+ file: "update-wishlist.md",
211
+ description: "Update mutable fields on an open Wishlist item."
212
+ },
213
+ {
214
+ name: "archive-task",
215
+ file: "archive-task.md",
216
+ description: "Archive a task without destructive deletion."
217
+ },
218
+ {
219
+ name: "add-dependency",
220
+ file: "add-dependency.md",
221
+ description: "Add a dependency edge between tasks with cycle checks."
222
+ },
223
+ {
224
+ name: "remove-dependency",
225
+ file: "remove-dependency.md",
226
+ description: "Remove a dependency edge between tasks."
227
+ },
228
+ {
229
+ name: "get-dependency-graph",
230
+ file: "get-dependency-graph.md",
231
+ description: "Get dependency graph data for one task or the full store."
232
+ },
233
+ {
234
+ name: "get-task-history",
235
+ file: "get-task-history.md",
236
+ description: "Get transition and mutation history for a task."
237
+ },
238
+ {
239
+ name: "get-recent-task-activity",
240
+ file: "get-recent-task-activity.md",
241
+ description: "List recent transition and mutation activity across tasks."
242
+ },
243
+ {
244
+ name: "get-task-summary",
245
+ file: "get-task-summary.md",
246
+ description: "Get aggregate task-state summary for active tasks."
247
+ },
248
+ {
249
+ name: "get-blocked-summary",
250
+ file: "get-blocked-summary.md",
251
+ description: "Get blocked-task dependency summary for active tasks."
252
+ },
253
+ {
254
+ name: "create-task-from-plan",
255
+ file: "create-task-from-plan.md",
256
+ description: "Promote planning output into a canonical task."
257
+ },
258
+ {
259
+ name: "convert-wishlist",
260
+ file: "convert-wishlist.md",
261
+ description: "Convert a Wishlist item into one or more phased tasks and close the wishlist item."
262
+ },
263
+ {
264
+ name: "create-wishlist",
265
+ file: "create-wishlist.md",
266
+ description: "Create a Wishlist ideation item with strict required fields (separate namespace from tasks)."
267
+ },
268
+ {
269
+ name: "get-wishlist",
270
+ file: "get-wishlist.md",
271
+ description: "Retrieve a single Wishlist item by ID."
272
+ },
273
+ {
274
+ name: "get-task",
275
+ file: "get-task.md",
276
+ description: "Retrieve a single task by ID."
277
+ },
278
+ {
279
+ name: "list-tasks",
280
+ file: "list-tasks.md",
281
+ description: "List tasks with optional status/phase filters."
282
+ },
283
+ {
284
+ name: "list-wishlist",
285
+ file: "list-wishlist.md",
286
+ description: "List Wishlist items (ideation-only; not part of task execution queues)."
287
+ },
288
+ {
289
+ name: "get-ready-queue",
290
+ file: "get-ready-queue.md",
291
+ description: "Get ready tasks sorted by priority."
292
+ },
293
+ {
294
+ name: "get-next-actions",
295
+ file: "get-next-actions.md",
296
+ description: "Get prioritized next-action suggestions with blocking analysis."
297
+ },
298
+ {
299
+ name: "migrate-task-persistence",
300
+ file: "migrate-task-persistence.md",
301
+ description: "Copy task + wishlist state between JSON files and a single SQLite database (offline migration)."
302
+ },
303
+ {
304
+ name: "migrate-wishlist-intake",
305
+ file: "migrate-wishlist-intake.md",
306
+ description: "One-time migration: legacy wishlist rows become wishlist_intake tasks; SQLite planning drops the wishlist JSON column."
307
+ },
308
+ {
309
+ name: "list-module-states",
310
+ file: "list-module-states.md",
311
+ description: "List unified SQLite module-state rows for diagnostics and migration verification."
312
+ },
313
+ {
314
+ name: "get-module-state",
315
+ file: "get-module-state.md",
316
+ description: "Read one module-state row from unified SQLite storage."
317
+ },
318
+ {
319
+ name: "dashboard-summary",
320
+ file: "dashboard-summary.md",
321
+ description: "Stable JSON cockpit summary for UI clients (tasks + maintainer status snapshot)."
322
+ },
323
+ {
324
+ name: "explain-task-engine-model",
325
+ file: "explain-task-engine-model.md",
326
+ description: "Explain model variants, planning boundaries, lifecycle transitions, and required fields."
327
+ }
328
+ ]
329
+ }
330
+ },
331
+ async onCommand(command, ctx) {
332
+ const args = command.args ?? {};
333
+ if (command.name === "migrate-task-persistence") {
334
+ return runMigrateTaskPersistence(ctx, args);
335
+ }
336
+ if (command.name === "migrate-wishlist-intake") {
337
+ return runMigrateWishlistIntake(ctx, args);
338
+ }
339
+ if (command.name === "list-module-states" || command.name === "get-module-state") {
340
+ const unified = new UnifiedStateDb(ctx.workspacePath, planningSqliteDatabaseRelativePath(ctx));
341
+ if (command.name === "list-module-states") {
342
+ return {
343
+ ok: true,
344
+ code: "module-states-listed",
345
+ message: "Listed module state rows",
346
+ data: { rows: unified.listModuleStates() }
347
+ };
348
+ }
349
+ const moduleId = typeof args.moduleId === "string" ? args.moduleId.trim() : "";
350
+ if (!moduleId) {
351
+ return { ok: false, code: "invalid-task-schema", message: "get-module-state requires moduleId" };
352
+ }
353
+ const row = unified.getModuleState(moduleId);
354
+ return row
355
+ ? {
356
+ ok: true,
357
+ code: "module-state-read",
358
+ message: `Read module state for ${moduleId}`,
359
+ data: { row }
360
+ }
361
+ : {
362
+ ok: false,
363
+ code: "task-not-found",
364
+ message: `No module state found for '${moduleId}'`
365
+ };
366
+ }
367
+ let planning;
368
+ try {
369
+ planning = await openPlanningStores(ctx);
370
+ }
371
+ catch (err) {
372
+ if (err instanceof TaskEngineError) {
373
+ return { ok: false, code: err.code, message: err.message };
374
+ }
375
+ return {
376
+ ok: false,
377
+ code: "storage-read-error",
378
+ message: `Failed to open task planning stores: ${err.message}`
379
+ };
380
+ }
381
+ const store = planning.taskStore;
382
+ if (command.name === "run-transition") {
383
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
384
+ const action = typeof args.action === "string" ? args.action : undefined;
385
+ const actor = typeof args.actor === "string"
386
+ ? args.actor
387
+ : ctx.resolvedActor !== undefined
388
+ ? ctx.resolvedActor
389
+ : undefined;
390
+ if (!taskId || !action) {
391
+ return {
392
+ ok: false,
393
+ code: "invalid-task-schema",
394
+ message: "run-transition requires 'taskId' and 'action' arguments"
395
+ };
396
+ }
397
+ try {
398
+ const service = new TransitionService(store);
399
+ const result = await service.runTransition({ taskId, action, actor });
400
+ if (result.evidence.toState === "completed") {
401
+ maybeSpawnTranscriptHookAfterCompletion(ctx.workspacePath, (ctx.effectiveConfig ?? {}));
402
+ }
403
+ return {
404
+ ok: true,
405
+ code: "transition-applied",
406
+ message: `${taskId}: ${result.evidence.fromState} → ${result.evidence.toState} (${action})`,
407
+ data: {
408
+ evidence: result.evidence,
409
+ autoUnblocked: result.autoUnblocked
410
+ }
411
+ };
412
+ }
413
+ catch (err) {
414
+ if (err instanceof TaskEngineError) {
415
+ return { ok: false, code: err.code, message: err.message };
416
+ }
417
+ return {
418
+ ok: false,
419
+ code: "invalid-transition",
420
+ message: err.message
421
+ };
422
+ }
423
+ }
424
+ if (command.name === "create-task" || command.name === "create-task-from-plan") {
425
+ const actor = typeof args.actor === "string"
426
+ ? args.actor
427
+ : ctx.resolvedActor !== undefined
428
+ ? ctx.resolvedActor
429
+ : undefined;
430
+ const id = typeof args.id === "string" && args.id.trim().length > 0 ? args.id.trim() : undefined;
431
+ const title = typeof args.title === "string" && args.title.trim().length > 0 ? args.title.trim() : undefined;
432
+ const type = typeof args.type === "string" && args.type.trim().length > 0 ? args.type.trim() : "workspace-kit";
433
+ const status = typeof args.status === "string" ? args.status : "proposed";
434
+ const priority = typeof args.priority === "string" && ["P1", "P2", "P3"].includes(args.priority)
435
+ ? args.priority
436
+ : undefined;
437
+ const clientMutationId = readIdempotencyValue(args);
438
+ if (!id || !title || !TASK_ID_RE.test(id) || !["proposed", "ready"].includes(status)) {
439
+ return {
440
+ ok: false,
441
+ code: "invalid-task-schema",
442
+ message: "create-task requires id/title, id format T<number>, and status of proposed or ready"
443
+ };
444
+ }
445
+ const evidenceType = command.name === "create-task-from-plan" ? "create-task-from-plan" : "create-task";
446
+ const timestamp = nowIso();
447
+ const task = {
448
+ id,
449
+ title,
450
+ type,
451
+ status: status,
452
+ createdAt: timestamp,
453
+ updatedAt: timestamp,
454
+ priority,
455
+ dependsOn: Array.isArray(args.dependsOn) ? args.dependsOn.filter((x) => typeof x === "string") : undefined,
456
+ unblocks: Array.isArray(args.unblocks) ? args.unblocks.filter((x) => typeof x === "string") : undefined,
457
+ phase: typeof args.phase === "string" ? args.phase : undefined,
458
+ metadata: typeof args.metadata === "object" && args.metadata !== null ? args.metadata : undefined,
459
+ ownership: typeof args.ownership === "string" ? args.ownership : undefined,
460
+ approach: typeof args.approach === "string" ? args.approach : undefined,
461
+ technicalScope: Array.isArray(args.technicalScope) ? args.technicalScope.filter((x) => typeof x === "string") : undefined,
462
+ acceptanceCriteria: Array.isArray(args.acceptanceCriteria) ? args.acceptanceCriteria.filter((x) => typeof x === "string") : undefined
463
+ };
464
+ if (command.name === "create-task-from-plan") {
465
+ const planRef = typeof args.planRef === "string" && args.planRef.trim().length > 0 ? args.planRef.trim() : undefined;
466
+ if (!planRef) {
467
+ return {
468
+ ok: false,
469
+ code: "invalid-task-schema",
470
+ message: "create-task-from-plan requires 'planRef'"
471
+ };
472
+ }
473
+ task.metadata = { ...(task.metadata ?? {}), planRef };
474
+ }
475
+ const createPayloadForDigest = {
476
+ id: task.id,
477
+ title: task.title,
478
+ type: task.type,
479
+ status: task.status,
480
+ priority: task.priority,
481
+ dependsOn: task.dependsOn ?? [],
482
+ unblocks: task.unblocks ?? [],
483
+ phase: task.phase ?? null,
484
+ metadata: task.metadata ?? null,
485
+ ownership: task.ownership ?? null,
486
+ approach: task.approach ?? null,
487
+ technicalScope: task.technicalScope ?? [],
488
+ acceptanceCriteria: task.acceptanceCriteria ?? []
489
+ };
490
+ const payloadDigest = digestPayload(createPayloadForDigest);
491
+ if (clientMutationId) {
492
+ const prior = findIdempotentMutation(store, evidenceType, id, clientMutationId);
493
+ if (prior) {
494
+ if (prior.payloadDigest !== payloadDigest) {
495
+ return {
496
+ ok: false,
497
+ code: "idempotency-key-conflict",
498
+ message: `clientMutationId '${clientMutationId}' was already used for a different ${evidenceType} payload on ${id}`
499
+ };
500
+ }
501
+ return {
502
+ ok: true,
503
+ code: "task-create-idempotent-replay",
504
+ message: `Idempotent create replay for task '${id}'`,
505
+ data: { task: store.getTask(id), replayed: true }
506
+ };
507
+ }
508
+ }
509
+ if (store.getTask(id)) {
510
+ return { ok: false, code: "duplicate-task-id", message: `Task '${id}' already exists` };
511
+ }
512
+ const knownTypeValidationError = validateKnownTaskTypeRequirements(task);
513
+ if (knownTypeValidationError) {
514
+ return {
515
+ ok: false,
516
+ code: knownTypeValidationError.code,
517
+ message: knownTypeValidationError.message
518
+ };
519
+ }
520
+ store.addTask(task);
521
+ store.addMutationEvidence(mutationEvidence(evidenceType, id, actor, {
522
+ initialStatus: task.status,
523
+ source: command.name,
524
+ clientMutationId,
525
+ payloadDigest
526
+ }));
527
+ const strictIssue = strictValidationError(store, ctx.effectiveConfig);
528
+ if (strictIssue) {
529
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
530
+ }
531
+ await store.save();
532
+ return {
533
+ ok: true,
534
+ code: "task-created",
535
+ message: `Created task '${id}'`,
536
+ data: { task }
537
+ };
538
+ }
539
+ if (command.name === "update-task") {
540
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
541
+ const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
542
+ const actor = typeof args.actor === "string"
543
+ ? args.actor
544
+ : ctx.resolvedActor !== undefined
545
+ ? ctx.resolvedActor
546
+ : undefined;
547
+ if (!taskId || !updates) {
548
+ return { ok: false, code: "invalid-task-schema", message: "update-task requires taskId and updates object" };
549
+ }
550
+ const clientMutationId = readIdempotencyValue(args);
551
+ const task = store.getTask(taskId);
552
+ if (!task) {
553
+ return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
554
+ }
555
+ const invalidKeys = Object.keys(updates).filter((key) => !MUTABLE_TASK_FIELDS.has(key));
556
+ if (invalidKeys.length > 0) {
557
+ return {
558
+ ok: false,
559
+ code: "invalid-task-update",
560
+ message: `update-task cannot mutate immutable fields: ${invalidKeys.join(", ")}`
561
+ };
562
+ }
563
+ const updatedTask = { ...task, ...updates, updatedAt: nowIso() };
564
+ const payloadDigest = digestPayload({ taskId, updates });
565
+ if (clientMutationId) {
566
+ const prior = findIdempotentMutation(store, "update-task", taskId, clientMutationId);
567
+ if (prior) {
568
+ if (prior.payloadDigest !== payloadDigest) {
569
+ return {
570
+ ok: false,
571
+ code: "idempotency-key-conflict",
572
+ message: `clientMutationId '${clientMutationId}' was already used for a different update-task payload on ${taskId}`
573
+ };
574
+ }
575
+ return {
576
+ ok: true,
577
+ code: "task-update-idempotent-replay",
578
+ message: `Idempotent update replay for task '${taskId}'`,
579
+ data: { task, replayed: true }
580
+ };
581
+ }
582
+ }
583
+ const knownTypeValidationError = validateKnownTaskTypeRequirements(updatedTask);
584
+ if (knownTypeValidationError) {
585
+ return {
586
+ ok: false,
587
+ code: knownTypeValidationError.code,
588
+ message: knownTypeValidationError.message
589
+ };
590
+ }
591
+ store.updateTask(updatedTask);
592
+ store.addMutationEvidence(mutationEvidence("update-task", taskId, actor, {
593
+ updatedFields: Object.keys(updates),
594
+ clientMutationId,
595
+ payloadDigest
596
+ }));
597
+ const strictIssue = strictValidationError(store, ctx.effectiveConfig);
598
+ if (strictIssue) {
599
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
600
+ }
601
+ await store.save();
602
+ return { ok: true, code: "task-updated", message: `Updated task '${taskId}'`, data: { task: updatedTask } };
603
+ }
604
+ if (command.name === "archive-task") {
605
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
606
+ const actor = typeof args.actor === "string"
607
+ ? args.actor
608
+ : ctx.resolvedActor !== undefined
609
+ ? ctx.resolvedActor
610
+ : undefined;
611
+ if (!taskId) {
612
+ return { ok: false, code: "invalid-task-schema", message: "archive-task requires taskId" };
613
+ }
614
+ const task = store.getTask(taskId);
615
+ if (!task) {
616
+ return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
617
+ }
618
+ const archivedAt = nowIso();
619
+ const updatedTask = { ...task, archived: true, archivedAt, updatedAt: archivedAt };
620
+ store.updateTask(updatedTask);
621
+ store.addMutationEvidence(mutationEvidence("archive-task", taskId, actor));
622
+ const strictIssue = strictValidationError(store, ctx.effectiveConfig);
623
+ if (strictIssue) {
624
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
625
+ }
626
+ await store.save();
627
+ return { ok: true, code: "task-archived", message: `Archived task '${taskId}'`, data: { task: updatedTask } };
628
+ }
629
+ if (command.name === "get-task") {
630
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
631
+ if (!taskId) {
632
+ return {
633
+ ok: false,
634
+ code: "invalid-task-schema",
635
+ message: "get-task requires 'taskId' argument"
636
+ };
637
+ }
638
+ const task = store.getTask(taskId);
639
+ if (!task) {
640
+ return {
641
+ ok: false,
642
+ code: "task-not-found",
643
+ message: `Task '${taskId}' not found`
644
+ };
645
+ }
646
+ const historyLimitRaw = args.historyLimit;
647
+ const historyLimit = typeof historyLimitRaw === "number" && Number.isFinite(historyLimitRaw) && historyLimitRaw > 0
648
+ ? Math.min(Math.floor(historyLimitRaw), 200)
649
+ : 50;
650
+ const log = store.getTransitionLog();
651
+ const recentTransitions = log
652
+ .filter((e) => e.taskId === taskId)
653
+ .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
654
+ .slice(0, historyLimit);
655
+ const allowedActions = getAllowedTransitionsFrom(task.status).map(({ to, action }) => ({
656
+ action,
657
+ targetStatus: to
658
+ }));
659
+ return {
660
+ ok: true,
661
+ code: "task-retrieved",
662
+ data: { task, recentTransitions, allowedActions }
663
+ };
664
+ }
665
+ if (command.name === "add-dependency" || command.name === "remove-dependency") {
666
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
667
+ const dependencyTaskId = typeof args.dependencyTaskId === "string" ? args.dependencyTaskId : undefined;
668
+ const actor = typeof args.actor === "string"
669
+ ? args.actor
670
+ : ctx.resolvedActor !== undefined
671
+ ? ctx.resolvedActor
672
+ : undefined;
673
+ if (!taskId || !dependencyTaskId) {
674
+ return {
675
+ ok: false,
676
+ code: "invalid-task-schema",
677
+ message: `${command.name} requires taskId and dependencyTaskId`
678
+ };
679
+ }
680
+ if (taskId === dependencyTaskId) {
681
+ return { ok: false, code: "dependency-cycle", message: "Task cannot depend on itself" };
682
+ }
683
+ const task = store.getTask(taskId);
684
+ const dep = store.getTask(dependencyTaskId);
685
+ if (!task || !dep) {
686
+ return { ok: false, code: "task-not-found", message: "taskId or dependencyTaskId not found" };
687
+ }
688
+ const deps = new Set(task.dependsOn ?? []);
689
+ if (command.name === "add-dependency") {
690
+ if (deps.has(dependencyTaskId)) {
691
+ return { ok: false, code: "duplicate-dependency", message: "Dependency already exists" };
692
+ }
693
+ deps.add(dependencyTaskId);
694
+ }
695
+ else {
696
+ deps.delete(dependencyTaskId);
697
+ }
698
+ const updatedTask = { ...task, dependsOn: [...deps], updatedAt: nowIso() };
699
+ store.updateTask(updatedTask);
700
+ const mutationType = command.name === "add-dependency" ? "add-dependency" : "remove-dependency";
701
+ store.addMutationEvidence(mutationEvidence(mutationType, taskId, actor, { dependencyTaskId }));
702
+ const strictIssue = strictValidationError(store, ctx.effectiveConfig);
703
+ if (strictIssue) {
704
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
705
+ }
706
+ await store.save();
707
+ return {
708
+ ok: true,
709
+ code: command.name === "add-dependency" ? "dependency-added" : "dependency-removed",
710
+ message: `${command.name} applied for '${taskId}'`,
711
+ data: { task: updatedTask }
712
+ };
713
+ }
714
+ if (command.name === "get-dependency-graph") {
715
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
716
+ const tasks = store.getActiveTasks();
717
+ const nodes = tasks.map((task) => ({ id: task.id, status: task.status }));
718
+ const edges = tasks.flatMap((task) => (task.dependsOn ?? []).map((depId) => ({ from: task.id, to: depId })));
719
+ if (!taskId) {
720
+ return { ok: true, code: "dependency-graph", data: { nodes, edges } };
721
+ }
722
+ const task = tasks.find((candidate) => candidate.id === taskId);
723
+ if (!task) {
724
+ return { ok: false, code: "task-not-found", message: `Task '${taskId}' not found` };
725
+ }
726
+ return {
727
+ ok: true,
728
+ code: "dependency-graph",
729
+ data: {
730
+ taskId,
731
+ dependsOn: task.dependsOn ?? [],
732
+ directDependents: tasks.filter((candidate) => (candidate.dependsOn ?? []).includes(taskId)).map((x) => x.id),
733
+ nodes,
734
+ edges
735
+ }
736
+ };
737
+ }
738
+ if (command.name === "get-task-history" || command.name === "get-recent-task-activity") {
739
+ const limitRaw = args.limit;
740
+ const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0
741
+ ? Math.min(Math.floor(limitRaw), 500)
742
+ : 50;
743
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
744
+ const transitions = store.getTransitionLog().map((entry) => ({ kind: "transition", ...entry }));
745
+ const mutations = store.getMutationLog().map((entry) => ({ kind: "mutation", ...entry }));
746
+ const merged = [...transitions, ...mutations]
747
+ .filter((entry) => (taskId ? entry.taskId === taskId : true))
748
+ .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
749
+ .slice(0, limit);
750
+ return {
751
+ ok: true,
752
+ code: command.name === "get-task-history" ? "task-history" : "recent-task-activity",
753
+ data: { taskId: taskId ?? null, items: merged, count: merged.length }
754
+ };
755
+ }
756
+ if (command.name === "dashboard-summary") {
757
+ const tasks = store.getActiveTasks();
758
+ const suggestion = getNextActions(tasks);
759
+ const workspaceStatus = await readWorkspaceStatusSnapshot(ctx.workspacePath);
760
+ const readyTop = suggestion.readyQueue.slice(0, 15).map((t) => ({
761
+ id: t.id,
762
+ title: t.title,
763
+ priority: t.priority ?? null,
764
+ phase: t.phase ?? null
765
+ }));
766
+ const blockedTop = suggestion.blockingAnalysis.slice(0, 15);
767
+ const wishlistItems = listWishlistIntakeTasksAsItems(store.getAllTasks());
768
+ const wishlistOpenItems = wishlistItems.filter((i) => i.status === "open");
769
+ const wishlistOpenCount = wishlistOpenItems.length;
770
+ const wishlistOpenTop = wishlistOpenItems.slice(0, 15).map((i) => ({
771
+ id: i.id,
772
+ title: i.title
773
+ }));
774
+ const planningSession = toDashboardPlanningSession(await readBuildPlanSession(ctx.workspacePath));
775
+ const data = {
776
+ schemaVersion: 1,
777
+ taskStoreLastUpdated: store.getLastUpdated(),
778
+ workspaceStatus,
779
+ planningSession,
780
+ stateSummary: suggestion.stateSummary,
781
+ readyQueueTop: readyTop,
782
+ readyQueueCount: suggestion.readyQueue.length,
783
+ executionPlanningScope: "tasks-only",
784
+ wishlist: {
785
+ schemaVersion: 1,
786
+ openCount: wishlistOpenCount,
787
+ totalCount: wishlistItems.length,
788
+ openTop: wishlistOpenTop
789
+ },
790
+ blockedSummary: {
791
+ count: suggestion.blockingAnalysis.length,
792
+ top: blockedTop
793
+ },
794
+ suggestedNext: suggestion.suggestedNext
795
+ ? {
796
+ id: suggestion.suggestedNext.id,
797
+ title: suggestion.suggestedNext.title,
798
+ status: suggestion.suggestedNext.status,
799
+ priority: suggestion.suggestedNext.priority ?? null,
800
+ phase: suggestion.suggestedNext.phase ?? null
801
+ }
802
+ : null,
803
+ blockingAnalysis: suggestion.blockingAnalysis
804
+ };
805
+ return {
806
+ ok: true,
807
+ code: "dashboard-summary",
808
+ message: "Dashboard summary built from task store and maintainer status snapshot",
809
+ data
810
+ };
811
+ }
812
+ if (command.name === "list-tasks") {
813
+ const statusFilter = typeof args.status === "string" ? args.status : undefined;
814
+ const phaseFilter = typeof args.phase === "string" ? args.phase : undefined;
815
+ const typeFilter = typeof args.type === "string" && args.type.trim().length > 0 ? args.type.trim() : undefined;
816
+ const categoryFilter = typeof args.category === "string" && args.category.trim().length > 0 ? args.category.trim() : undefined;
817
+ const tagsFilterRaw = args.tags;
818
+ const tagsFilter = typeof tagsFilterRaw === "string"
819
+ ? [tagsFilterRaw]
820
+ : Array.isArray(tagsFilterRaw)
821
+ ? tagsFilterRaw.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
822
+ : [];
823
+ const metadataFilters = isRecordLike(args.metadataFilters)
824
+ ? Object.entries(args.metadataFilters).filter(([path]) => SAFE_METADATA_PATH_RE.test(path))
825
+ : [];
826
+ const includeArchived = args.includeArchived === true;
827
+ let tasks = includeArchived ? store.getAllTasks() : store.getActiveTasks();
828
+ if (statusFilter) {
829
+ tasks = tasks.filter((t) => t.status === statusFilter);
830
+ }
831
+ if (phaseFilter) {
832
+ tasks = tasks.filter((t) => t.phase === phaseFilter);
833
+ }
834
+ if (typeFilter) {
835
+ tasks = tasks.filter((t) => t.type === typeFilter);
836
+ }
837
+ if (categoryFilter) {
838
+ tasks = tasks.filter((t) => readMetadataPath(t.metadata, "category") === categoryFilter);
839
+ }
840
+ if (tagsFilter.length > 0) {
841
+ tasks = tasks.filter((t) => {
842
+ const tags = readMetadataPath(t.metadata, "tags");
843
+ if (!Array.isArray(tags)) {
844
+ return false;
845
+ }
846
+ const normalized = tags.filter((entry) => typeof entry === "string");
847
+ return tagsFilter.every((tag) => normalized.includes(tag));
848
+ });
849
+ }
850
+ if (metadataFilters.length > 0) {
851
+ tasks = tasks.filter((t) => metadataFilters.every(([path, expected]) => readMetadataPath(t.metadata, path) === expected));
852
+ }
853
+ return {
854
+ ok: true,
855
+ code: "tasks-listed",
856
+ message: `Found ${tasks.length} tasks`,
857
+ data: { tasks, count: tasks.length, scope: "tasks-only" }
858
+ };
859
+ }
860
+ if (command.name === "get-ready-queue") {
861
+ const tasks = store.getActiveTasks();
862
+ const ready = tasks
863
+ .filter((t) => t.status === "ready" && !isWishlistIntakeTask(t))
864
+ .sort((a, b) => {
865
+ const pa = a.priority ?? "P9";
866
+ const pb = b.priority ?? "P9";
867
+ return pa.localeCompare(pb);
868
+ });
869
+ return {
870
+ ok: true,
871
+ code: "ready-queue-retrieved",
872
+ message: `${ready.length} tasks in ready queue`,
873
+ data: { tasks: ready, count: ready.length, scope: "tasks-only" }
874
+ };
875
+ }
876
+ if (command.name === "get-next-actions") {
877
+ const tasks = store.getActiveTasks();
878
+ const suggestion = getNextActions(tasks);
879
+ return {
880
+ ok: true,
881
+ code: "next-actions-retrieved",
882
+ message: suggestion.suggestedNext
883
+ ? `Suggested next: ${suggestion.suggestedNext.id} — ${suggestion.suggestedNext.title}`
884
+ : "No tasks in ready queue",
885
+ data: { ...suggestion, scope: "tasks-only" }
886
+ };
887
+ }
888
+ if (command.name === "explain-task-engine-model") {
889
+ const allStatuses = ["proposed", "ready", "in_progress", "blocked", "completed", "cancelled"];
890
+ const lifecycle = allStatuses.map((status) => ({
891
+ status,
892
+ allowedActions: getAllowedTransitionsFrom(status).map((entry) => ({
893
+ action: entry.action,
894
+ targetStatus: entry.to
895
+ }))
896
+ }));
897
+ return {
898
+ ok: true,
899
+ code: "task-engine-model-explained",
900
+ message: "Task Engine model variants, planning boundary, and lifecycle transitions.",
901
+ data: {
902
+ modelVersion: 1,
903
+ variants: [
904
+ {
905
+ variant: "execution-task",
906
+ idPattern: "^T[0-9]+$",
907
+ appearsInExecutionPlanning: true,
908
+ requiredFields: ["id", "title", "type", "status", "createdAt", "updatedAt"],
909
+ optionalFields: [
910
+ "priority",
911
+ "dependsOn",
912
+ "unblocks",
913
+ "phase",
914
+ "metadata",
915
+ "ownership",
916
+ "approach",
917
+ "technicalScope",
918
+ "acceptanceCriteria"
919
+ ]
920
+ },
921
+ {
922
+ variant: "wishlist-intake-task",
923
+ idPattern: "^T[0-9]+$",
924
+ taskType: WISHLIST_INTAKE_TASK_TYPE,
925
+ appearsInExecutionPlanning: false,
926
+ requiredFields: [
927
+ "id",
928
+ "title",
929
+ "type",
930
+ "status",
931
+ "createdAt",
932
+ "updatedAt",
933
+ "metadata.problemStatement",
934
+ "metadata.expectedOutcome",
935
+ "metadata.impact",
936
+ "metadata.constraints",
937
+ "metadata.successSignals",
938
+ "metadata.requestor",
939
+ "metadata.evidenceRef"
940
+ ],
941
+ optionalFields: [
942
+ "metadata.legacyWishlistId",
943
+ "metadata",
944
+ "priority",
945
+ "dependsOn",
946
+ "unblocks"
947
+ ],
948
+ notes: "Ideation backlog uses type wishlist_intake (T ids); optional metadata.legacyWishlistId preserves W### provenance after migration. Excluded from ready-queue suggestions."
949
+ }
950
+ ],
951
+ planningBoundary: {
952
+ executionQueues: "tasks-only",
953
+ wishlistScope: "task-backed-wishlist-intake"
954
+ },
955
+ executionTaskLifecycle: lifecycle
956
+ }
957
+ };
958
+ }
959
+ if (command.name === "get-task-summary") {
960
+ const tasks = store.getActiveTasks();
961
+ const suggestion = getNextActions(tasks);
962
+ return {
963
+ ok: true,
964
+ code: "task-summary",
965
+ data: {
966
+ scope: "tasks-only",
967
+ stateSummary: suggestion.stateSummary,
968
+ readyQueueCount: suggestion.readyQueue.length,
969
+ suggestedNext: suggestion.suggestedNext
970
+ ? {
971
+ id: suggestion.suggestedNext.id,
972
+ title: suggestion.suggestedNext.title,
973
+ priority: suggestion.suggestedNext.priority ?? null
974
+ }
975
+ : null
976
+ }
977
+ };
978
+ }
979
+ if (command.name === "get-blocked-summary") {
980
+ const tasks = store.getActiveTasks();
981
+ const suggestion = getNextActions(tasks);
982
+ return {
983
+ ok: true,
984
+ code: "blocked-summary",
985
+ data: {
986
+ blockedCount: suggestion.blockingAnalysis.length,
987
+ blockedItems: suggestion.blockingAnalysis,
988
+ scope: "tasks-only"
989
+ }
990
+ };
991
+ }
992
+ if (command.name === "create-wishlist") {
993
+ const raw = args;
994
+ const ts = nowIso();
995
+ const hasLegacyId = typeof raw.id === "string" && raw.id.trim().length > 0;
996
+ let task;
997
+ if (hasLegacyId) {
998
+ const v = validateWishlistIntakePayload(raw);
999
+ if (!v.ok) {
1000
+ return { ok: false, code: "invalid-task-schema", message: v.errors.join(" ") };
1001
+ }
1002
+ const wid = raw.id.trim();
1003
+ const dup = store
1004
+ .getAllTasks()
1005
+ .some((t) => isWishlistIntakeTask(t) && t.metadata?.[LEGACY_WISHLIST_ID_METADATA_KEY] === wid);
1006
+ if (dup) {
1007
+ return {
1008
+ ok: false,
1009
+ code: "duplicate-task-id",
1010
+ message: `Wishlist legacy id '${wid}' is already represented as a task`
1011
+ };
1012
+ }
1013
+ const item = buildWishlistItemFromIntake(raw, ts);
1014
+ const newTid = allocateNextTaskNumericId(store.getAllTasks());
1015
+ task = taskEntityFromWishlistItem(item, newTid, ts);
1016
+ }
1017
+ else {
1018
+ const v = validateWishlistContentFields(raw);
1019
+ if (!v.ok) {
1020
+ return { ok: false, code: "invalid-task-schema", message: v.errors.join(" ") };
1021
+ }
1022
+ const newTid = allocateNextTaskNumericId(store.getAllTasks());
1023
+ task = taskEntityFromNewIntake(raw, newTid, ts);
1024
+ }
1025
+ const typeErr = validateKnownTaskTypeRequirements(task);
1026
+ if (typeErr) {
1027
+ return { ok: false, code: typeErr.code, message: typeErr.message };
1028
+ }
1029
+ if (planningStrictValidationEnabled({ effectiveConfig: ctx.effectiveConfig })) {
1030
+ const strictIssue = validateTaskSetForStrictMode([...store.getAllTasks(), task]);
1031
+ if (strictIssue) {
1032
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
1033
+ }
1034
+ }
1035
+ try {
1036
+ if (planning.kind === "sqlite") {
1037
+ planning.sqliteDual.withTransaction(() => {
1038
+ store.addTask(task);
1039
+ });
1040
+ }
1041
+ else {
1042
+ store.addTask(task);
1043
+ await store.save();
1044
+ }
1045
+ }
1046
+ catch (err) {
1047
+ if (err instanceof TaskEngineError) {
1048
+ return { ok: false, code: err.code, message: err.message };
1049
+ }
1050
+ throw err;
1051
+ }
1052
+ const itemOut = wishlistIntakeTaskToItem(task);
1053
+ return {
1054
+ ok: true,
1055
+ code: "wishlist-created",
1056
+ message: `Created wishlist intake task '${task.id}'`,
1057
+ data: {
1058
+ wishlist: itemOut,
1059
+ item: itemOut,
1060
+ taskId: task.id,
1061
+ task
1062
+ }
1063
+ };
1064
+ }
1065
+ if (command.name === "list-wishlist") {
1066
+ const statusFilter = typeof args.status === "string" ? args.status : undefined;
1067
+ let items = listWishlistIntakeTasksAsItems(store.getAllTasks());
1068
+ if (statusFilter && ["open", "converted", "cancelled"].includes(statusFilter)) {
1069
+ items = items.filter((i) => i.status === statusFilter);
1070
+ }
1071
+ return {
1072
+ ok: true,
1073
+ code: "wishlist-listed",
1074
+ message: `Found ${items.length} wishlist items`,
1075
+ data: { items, count: items.length, scope: "wishlist-only" }
1076
+ };
1077
+ }
1078
+ if (command.name === "get-wishlist") {
1079
+ const wishlistId = typeof args.wishlistId === "string" && args.wishlistId.trim().length > 0
1080
+ ? args.wishlistId.trim()
1081
+ : typeof args.id === "string" && args.id.trim().length > 0
1082
+ ? args.id.trim()
1083
+ : "";
1084
+ if (!wishlistId) {
1085
+ return { ok: false, code: "invalid-task-schema", message: "get-wishlist requires 'wishlistId' or 'id'" };
1086
+ }
1087
+ const t = findWishlistIntakeTaskByLegacyOrTaskId(store.getAllTasks(), wishlistId);
1088
+ if (!t) {
1089
+ return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
1090
+ }
1091
+ const item = wishlistIntakeTaskToItem(t);
1092
+ return {
1093
+ ok: true,
1094
+ code: "wishlist-retrieved",
1095
+ data: { item, taskId: t.id }
1096
+ };
1097
+ }
1098
+ if (command.name === "update-wishlist") {
1099
+ const wishlistId = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
1100
+ const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
1101
+ if (!wishlistId || !updates) {
1102
+ return { ok: false, code: "invalid-task-schema", message: "update-wishlist requires wishlistId and updates" };
1103
+ }
1104
+ const existingTask = findWishlistIntakeTaskByLegacyOrTaskId(store.getAllTasks(), wishlistId);
1105
+ if (!existingTask) {
1106
+ return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
1107
+ }
1108
+ if (existingTask.status !== "proposed") {
1109
+ return { ok: false, code: "invalid-transition", message: "Only open wishlist items can be updated" };
1110
+ }
1111
+ const uv = validateWishlistUpdatePayload(updates);
1112
+ if (!uv.ok) {
1113
+ return { ok: false, code: "invalid-task-schema", message: uv.errors.join(" ") };
1114
+ }
1115
+ const meta = { ...(existingTask.metadata ?? {}) };
1116
+ const mutable = [
1117
+ "title",
1118
+ "problemStatement",
1119
+ "expectedOutcome",
1120
+ "impact",
1121
+ "constraints",
1122
+ "successSignals",
1123
+ "requestor",
1124
+ "evidenceRef"
1125
+ ];
1126
+ let title = existingTask.title;
1127
+ for (const key of mutable) {
1128
+ if (key in updates && typeof updates[key] === "string") {
1129
+ if (key === "title") {
1130
+ title = updates[key].trim();
1131
+ }
1132
+ else {
1133
+ meta[key] = updates[key].trim();
1134
+ }
1135
+ }
1136
+ }
1137
+ const merged = {
1138
+ ...existingTask,
1139
+ title,
1140
+ metadata: meta,
1141
+ updatedAt: nowIso()
1142
+ };
1143
+ const typeErr = validateKnownTaskTypeRequirements(merged);
1144
+ if (typeErr) {
1145
+ return { ok: false, code: typeErr.code, message: typeErr.message };
1146
+ }
1147
+ if (planningStrictValidationEnabled({ effectiveConfig: ctx.effectiveConfig })) {
1148
+ const others = store.getAllTasks().filter((x) => x.id !== merged.id);
1149
+ const strictIssue = validateTaskSetForStrictMode([...others, merged]);
1150
+ if (strictIssue) {
1151
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
1152
+ }
1153
+ }
1154
+ if (planning.kind === "sqlite") {
1155
+ planning.sqliteDual.withTransaction(() => {
1156
+ store.updateTask(merged);
1157
+ });
1158
+ }
1159
+ else {
1160
+ store.updateTask(merged);
1161
+ await store.save();
1162
+ }
1163
+ const itemOut = wishlistIntakeTaskToItem(merged);
1164
+ return {
1165
+ ok: true,
1166
+ code: "wishlist-updated",
1167
+ message: `Updated wishlist '${wishlistId}'`,
1168
+ data: { item: itemOut, taskId: merged.id }
1169
+ };
1170
+ }
1171
+ if (command.name === "convert-wishlist") {
1172
+ const wishlistTaskId = typeof args.wishlistTaskId === "string" && args.wishlistTaskId.trim().length > 0
1173
+ ? args.wishlistTaskId.trim()
1174
+ : "";
1175
+ const wishlistIdLegacy = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
1176
+ const lookupKey = wishlistTaskId || wishlistIdLegacy;
1177
+ if (!lookupKey) {
1178
+ return {
1179
+ ok: false,
1180
+ code: "invalid-task-schema",
1181
+ message: "convert-wishlist requires wishlistTaskId (T<number>) or wishlistId (W<number>)"
1182
+ };
1183
+ }
1184
+ if (wishlistTaskId && !TASK_ID_RE.test(wishlistTaskId)) {
1185
+ return {
1186
+ ok: false,
1187
+ code: "invalid-task-schema",
1188
+ message: "wishlistTaskId must match T<number>"
1189
+ };
1190
+ }
1191
+ if (wishlistIdLegacy && !wishlistTaskId && !WISHLIST_ID_RE.test(wishlistIdLegacy)) {
1192
+ return {
1193
+ ok: false,
1194
+ code: "invalid-task-schema",
1195
+ message: "wishlistId must match W<number> when wishlistTaskId is omitted"
1196
+ };
1197
+ }
1198
+ const dec = parseConversionDecomposition(args.decomposition);
1199
+ if (!dec.ok) {
1200
+ return { ok: false, code: "invalid-task-schema", message: dec.message };
1201
+ }
1202
+ const tasksRaw = args.tasks;
1203
+ if (!Array.isArray(tasksRaw) || tasksRaw.length === 0) {
1204
+ return {
1205
+ ok: false,
1206
+ code: "invalid-task-schema",
1207
+ message: "convert-wishlist requires non-empty tasks array"
1208
+ };
1209
+ }
1210
+ const source = findWishlistIntakeTaskByLegacyOrTaskId(store.getAllTasks(), lookupKey);
1211
+ if (!source) {
1212
+ return { ok: false, code: "task-not-found", message: `Wishlist intake '${lookupKey}' not found` };
1213
+ }
1214
+ if (source.status !== "proposed") {
1215
+ return {
1216
+ ok: false,
1217
+ code: "invalid-transition",
1218
+ message: "Only open wishlist intake tasks can be converted"
1219
+ };
1220
+ }
1221
+ const actor = typeof args.actor === "string"
1222
+ ? args.actor
1223
+ : ctx.resolvedActor !== undefined
1224
+ ? ctx.resolvedActor
1225
+ : undefined;
1226
+ const timestamp = nowIso();
1227
+ const built = [];
1228
+ for (const row of tasksRaw) {
1229
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
1230
+ return { ok: false, code: "invalid-task-schema", message: "Each task must be an object" };
1231
+ }
1232
+ const bt = buildTaskFromConversionPayload(row, timestamp);
1233
+ if (!bt.ok) {
1234
+ return { ok: false, code: "invalid-task-schema", message: bt.message };
1235
+ }
1236
+ if (store.getTask(bt.task.id)) {
1237
+ return {
1238
+ ok: false,
1239
+ code: "duplicate-task-id",
1240
+ message: `Task '${bt.task.id}' already exists`
1241
+ };
1242
+ }
1243
+ built.push(bt.task);
1244
+ }
1245
+ const convertedIds = built.map((t) => t.id);
1246
+ const updatedSource = {
1247
+ ...source,
1248
+ status: "completed",
1249
+ updatedAt: timestamp,
1250
+ metadata: {
1251
+ ...(source.metadata ?? {}),
1252
+ wishlistConvertedToTaskIds: convertedIds,
1253
+ wishlistConversionDecomposition: dec.value,
1254
+ wishlistConvertedAt: timestamp
1255
+ }
1256
+ };
1257
+ const applyConvertMutations = () => {
1258
+ for (const t of built) {
1259
+ store.addTask(t);
1260
+ store.addMutationEvidence(mutationEvidence("create-task", t.id, actor, {
1261
+ initialStatus: t.status,
1262
+ source: "convert-wishlist",
1263
+ wishlistTaskId: source.id,
1264
+ wishlistLegacyId: source.metadata?.[LEGACY_WISHLIST_ID_METADATA_KEY] ?? null
1265
+ }));
1266
+ }
1267
+ store.updateTask(updatedSource);
1268
+ store.addMutationEvidence(mutationEvidence("update-task", source.id, actor, {
1269
+ source: "convert-wishlist",
1270
+ convertedToTaskIds: convertedIds
1271
+ }));
1272
+ };
1273
+ if (planningStrictValidationEnabled({ effectiveConfig: ctx.effectiveConfig })) {
1274
+ const strictIssue = validateTaskSetForStrictMode([
1275
+ ...store.getAllTasks().filter((x) => x.id !== source.id),
1276
+ ...built,
1277
+ updatedSource
1278
+ ]);
1279
+ if (strictIssue) {
1280
+ return { ok: false, code: "strict-task-validation-failed", message: strictIssue };
1281
+ }
1282
+ }
1283
+ if (planning.kind === "sqlite") {
1284
+ planning.sqliteDual.withTransaction(applyConvertMutations);
1285
+ }
1286
+ else {
1287
+ applyConvertMutations();
1288
+ await store.save();
1289
+ }
1290
+ const wishlistShape = wishlistIntakeTaskToItem(updatedSource);
1291
+ return {
1292
+ ok: true,
1293
+ code: "wishlist-converted",
1294
+ message: `Converted wishlist intake '${source.id}' to tasks: ${convertedIds.join(", ")}`,
1295
+ data: { wishlist: wishlistShape, createdTasks: built, sourceTaskId: source.id }
1296
+ };
1297
+ }
1298
+ return {
1299
+ ok: false,
1300
+ code: "unsupported-command",
1301
+ message: `Task Engine does not support command '${command.name}'`
1302
+ };
1303
+ }
1304
+ };