@workflow-cannon/workspace-kit 0.13.0 → 0.14.0

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