@zeliper/zscode-mcp-server 1.0.1 → 1.0.3

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 (59) hide show
  1. package/dist/errors/index.d.ts +28 -0
  2. package/dist/errors/index.d.ts.map +1 -1
  3. package/dist/errors/index.js +50 -0
  4. package/dist/errors/index.js.map +1 -1
  5. package/dist/state/manager.d.ts +74 -2
  6. package/dist/state/manager.d.ts.map +1 -1
  7. package/dist/state/manager.js +551 -138
  8. package/dist/state/manager.js.map +1 -1
  9. package/dist/state/manager.test.d.ts +2 -0
  10. package/dist/state/manager.test.d.ts.map +1 -0
  11. package/dist/state/manager.test.js +310 -0
  12. package/dist/state/manager.test.js.map +1 -0
  13. package/dist/state/schema.d.ts +443 -21
  14. package/dist/state/schema.d.ts.map +1 -1
  15. package/dist/state/schema.js +99 -1
  16. package/dist/state/schema.js.map +1 -1
  17. package/dist/state/schema.test.d.ts +2 -0
  18. package/dist/state/schema.test.d.ts.map +1 -0
  19. package/dist/state/schema.test.js +300 -0
  20. package/dist/state/schema.test.js.map +1 -0
  21. package/dist/state/types.d.ts +47 -1
  22. package/dist/state/types.d.ts.map +1 -1
  23. package/dist/state/types.js +3 -1
  24. package/dist/state/types.js.map +1 -1
  25. package/dist/tools/archive.d.ts.map +1 -1
  26. package/dist/tools/archive.js +57 -0
  27. package/dist/tools/archive.js.map +1 -1
  28. package/dist/tools/context.d.ts.map +1 -1
  29. package/dist/tools/context.js +14 -0
  30. package/dist/tools/context.js.map +1 -1
  31. package/dist/tools/files.d.ts +7 -0
  32. package/dist/tools/files.d.ts.map +1 -0
  33. package/dist/tools/files.js +131 -0
  34. package/dist/tools/files.js.map +1 -0
  35. package/dist/tools/index.d.ts.map +1 -1
  36. package/dist/tools/index.js +9 -0
  37. package/dist/tools/index.js.map +1 -1
  38. package/dist/tools/memory.d.ts +7 -0
  39. package/dist/tools/memory.d.ts.map +1 -0
  40. package/dist/tools/memory.js +243 -0
  41. package/dist/tools/memory.js.map +1 -0
  42. package/dist/tools/modify.d.ts.map +1 -1
  43. package/dist/tools/modify.js +43 -7
  44. package/dist/tools/modify.js.map +1 -1
  45. package/dist/tools/plan.d.ts.map +1 -1
  46. package/dist/tools/plan.js +77 -4
  47. package/dist/tools/plan.js.map +1 -1
  48. package/dist/tools/staging.d.ts.map +1 -1
  49. package/dist/tools/staging.js +104 -6
  50. package/dist/tools/staging.js.map +1 -1
  51. package/dist/tools/summary.d.ts +7 -0
  52. package/dist/tools/summary.d.ts.map +1 -0
  53. package/dist/tools/summary.js +127 -0
  54. package/dist/tools/summary.js.map +1 -0
  55. package/dist/utils/paths.d.ts +34 -0
  56. package/dist/utils/paths.d.ts.map +1 -1
  57. package/dist/utils/paths.js +94 -2
  58. package/dist/utils/paths.js.map +1 -1
  59. package/package.json +10 -3
@@ -1,23 +1,29 @@
1
- import { readFile, writeFile, access, rm, cp } from "fs/promises";
1
+ import { readFile, access, rm, cp } from "fs/promises";
2
2
  import { join, dirname } from "path";
3
3
  import { constants } from "fs";
4
4
  import { StateSchema } from "./schema.js";
5
- import { normalizePath, toPosixPath, ensureDir } from "../utils/paths.js";
5
+ import { STATE_VERSION, } from "./types.js";
6
+ import { normalizePath, toPosixPath, ensureDir, isPathSafe, isValidId, atomicWriteFile, generateSecureId, } from "../utils/paths.js";
7
+ import { ProjectNotInitializedError, PlanNotFoundError, PlanInvalidStateError, StagingNotFoundError, StagingOrderError, StagingPlanMismatchError, StagingInvalidStateError, TaskNotFoundError, TaskInvalidStateError, TaskStateTransitionError, CircularDependencyError, MemoryNotFoundError, PathTraversalError, InvalidIdError, } from "../errors/index.js";
8
+ // ============ Constants ============
9
+ const MAX_HISTORY_ENTRIES = 1000;
10
+ // Valid task state transitions
11
+ const VALID_TASK_TRANSITIONS = {
12
+ pending: ["in_progress", "blocked", "cancelled"],
13
+ in_progress: ["done", "blocked", "cancelled"],
14
+ blocked: ["in_progress", "cancelled"],
15
+ done: [], // Terminal state - no transitions allowed
16
+ cancelled: [], // Terminal state - no transitions allowed
17
+ };
6
18
  // ============ ID Generator ============
7
- function generateRandomId(length) {
8
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
9
- let result = "";
10
- for (let i = 0; i < length; i++) {
11
- result += chars.charAt(Math.floor(Math.random() * chars.length));
12
- }
13
- return result;
14
- }
19
+ // Uses cryptographically secure random generation
15
20
  const idGenerator = {
16
- generatePlanId: () => `plan-${generateRandomId(8)}`,
17
- generateStagingId: () => `staging-${generateRandomId(4)}`,
18
- generateTaskId: () => `task-${generateRandomId(8)}`,
19
- generateHistoryId: () => `hist-${Date.now()}-${generateRandomId(4)}`,
20
- generateDecisionId: () => `dec-${Date.now()}-${generateRandomId(4)}`,
21
+ generatePlanId: () => `plan-${generateSecureId(8)}`,
22
+ generateStagingId: () => `staging-${generateSecureId(4)}`,
23
+ generateTaskId: () => `task-${generateSecureId(8)}`,
24
+ generateHistoryId: () => `hist-${Date.now()}-${generateSecureId(4)}`,
25
+ generateDecisionId: () => `dec-${Date.now()}-${generateSecureId(4)}`,
26
+ generateMemoryId: () => `mem-${generateSecureId(8)}`,
21
27
  };
22
28
  // ============ StateManager Class ============
23
29
  export class StateManager {
@@ -64,8 +70,47 @@ export class StateManager {
64
70
  if (!this.state) {
65
71
  throw new Error("No state to save");
66
72
  }
67
- await ensureDir(dirname(this.stateFilePath));
68
- await writeFile(this.stateFilePath, JSON.stringify(this.state, null, 2), "utf-8");
73
+ // Use atomic write to prevent data corruption
74
+ await atomicWriteFile(this.stateFilePath, JSON.stringify(this.state, null, 2));
75
+ }
76
+ // ============ Validation Helpers ============
77
+ ensureInitialized() {
78
+ if (!this.state) {
79
+ throw new ProjectNotInitializedError();
80
+ }
81
+ return this.state;
82
+ }
83
+ requirePlan(planId) {
84
+ const state = this.ensureInitialized();
85
+ const plan = state.plans[planId];
86
+ if (!plan) {
87
+ throw new PlanNotFoundError(planId);
88
+ }
89
+ return plan;
90
+ }
91
+ requireStaging(stagingId) {
92
+ const state = this.ensureInitialized();
93
+ const staging = state.stagings[stagingId];
94
+ if (!staging) {
95
+ throw new StagingNotFoundError(stagingId);
96
+ }
97
+ return staging;
98
+ }
99
+ requireTask(taskId) {
100
+ const state = this.ensureInitialized();
101
+ const task = state.tasks[taskId];
102
+ if (!task) {
103
+ throw new TaskNotFoundError(taskId);
104
+ }
105
+ return task;
106
+ }
107
+ requireMemory(memoryId) {
108
+ const state = this.ensureInitialized();
109
+ const memory = state.context.memories.find(m => m.id === memoryId);
110
+ if (!memory) {
111
+ throw new MemoryNotFoundError(memoryId);
112
+ }
113
+ return memory;
69
114
  }
70
115
  // ============ Getters ============
71
116
  getState() {
@@ -120,7 +165,7 @@ export class StateManager {
120
165
  updatedAt: now,
121
166
  };
122
167
  this.state = {
123
- version: "2.0.0",
168
+ version: STATE_VERSION,
124
169
  project,
125
170
  plans: {},
126
171
  stagings: {},
@@ -130,6 +175,7 @@ export class StateManager {
130
175
  lastUpdated: now,
131
176
  activeFiles: [],
132
177
  decisions: [],
178
+ memories: [],
133
179
  },
134
180
  };
135
181
  await this.addHistory("project_initialized", { projectName: name });
@@ -138,9 +184,7 @@ export class StateManager {
138
184
  }
139
185
  // ============ Plan Operations ============
140
186
  async createPlan(title, description, stagingConfigs) {
141
- if (!this.state) {
142
- throw new Error("Project not initialized");
143
- }
187
+ const state = this.ensureInitialized();
144
188
  const now = new Date().toISOString();
145
189
  const planId = idGenerator.generatePlanId();
146
190
  const artifactsRoot = toPosixPath(join(".claude", "plans", planId, "artifacts"));
@@ -166,14 +210,17 @@ export class StateManager {
166
210
  priority: taskConfig.priority,
167
211
  status: "pending",
168
212
  execution_mode: taskConfig.execution_mode,
213
+ model: taskConfig.model, // Model override for this task
169
214
  depends_on: [], // Will be resolved after all tasks are created
215
+ cross_staging_refs: [], // Will be populated if cross-staging refs are provided
216
+ memory_tags: [], // Will be populated if memory tags are provided
170
217
  order: taskIndex,
171
218
  createdAt: now,
172
219
  updatedAt: now,
173
220
  };
174
221
  tasks.push(task);
175
222
  taskIds.push(taskId);
176
- this.state.tasks[taskId] = task;
223
+ state.tasks[taskId] = task;
177
224
  }
178
225
  // Resolve task dependencies within the same staging
179
226
  for (let i = 0; i < tasks.length; i++) {
@@ -193,11 +240,16 @@ export class StateManager {
193
240
  order: stagingIndex,
194
241
  execution_type: config.execution_type,
195
242
  status: "pending",
243
+ default_model: config.default_model, // Default model for tasks in this staging
244
+ session_budget: config.session_budget, // Session budget category
245
+ recommended_sessions: config.recommended_sessions, // Recommended session count
196
246
  tasks: taskIds,
247
+ depends_on_stagings: [], // Will be populated if staging dependencies are provided
248
+ auto_include_artifacts: true, // Default to auto-include
197
249
  artifacts_path: artifactsPath,
198
250
  createdAt: now,
199
251
  };
200
- this.state.stagings[stagingId] = staging;
252
+ state.stagings[stagingId] = staging;
201
253
  stagingIds.push(stagingId);
202
254
  }
203
255
  // Create plan
@@ -211,17 +263,13 @@ export class StateManager {
211
263
  createdAt: now,
212
264
  updatedAt: now,
213
265
  };
214
- this.state.plans[planId] = plan;
266
+ state.plans[planId] = plan;
215
267
  await this.addHistory("plan_created", { planId, title, stagingCount: stagingIds.length });
216
268
  await this.save();
217
269
  return plan;
218
270
  }
219
271
  async updatePlanStatus(planId, status) {
220
- if (!this.state)
221
- throw new Error("Project not initialized");
222
- const plan = this.getPlan(planId);
223
- if (!plan)
224
- throw new Error(`Plan not found: ${planId}`);
272
+ const plan = this.requirePlan(planId);
225
273
  plan.status = status;
226
274
  plan.updatedAt = new Date().toISOString();
227
275
  if (status === "completed") {
@@ -234,22 +282,17 @@ export class StateManager {
234
282
  }
235
283
  // ============ Staging Operations ============
236
284
  async startStaging(planId, stagingId) {
237
- if (!this.state)
238
- throw new Error("Project not initialized");
239
- const plan = this.getPlan(planId);
240
- if (!plan)
241
- throw new Error(`Plan not found: ${planId}`);
242
- const staging = this.getStaging(stagingId);
243
- if (!staging)
244
- throw new Error(`Staging not found: ${stagingId}`);
285
+ const state = this.ensureInitialized();
286
+ const plan = this.requirePlan(planId);
287
+ const staging = this.requireStaging(stagingId);
245
288
  if (staging.planId !== planId) {
246
- throw new Error(`Staging ${stagingId} does not belong to plan ${planId}`);
289
+ throw new StagingPlanMismatchError(stagingId, planId, staging.planId);
247
290
  }
248
291
  // Check if previous staging is completed
249
292
  if (staging.order > 0) {
250
293
  const prevStaging = this.getStagingByOrder(planId, staging.order - 1);
251
294
  if (prevStaging && prevStaging.status !== "completed") {
252
- throw new Error(`Cannot start staging: previous staging ${prevStaging.id} is not completed`);
295
+ throw new StagingOrderError(stagingId, prevStaging.id);
253
296
  }
254
297
  }
255
298
  const now = new Date().toISOString();
@@ -258,9 +301,9 @@ export class StateManager {
258
301
  plan.currentStagingId = stagingId;
259
302
  plan.status = "active";
260
303
  plan.updatedAt = now;
261
- this.state.context.currentPlanId = planId;
262
- this.state.context.currentStagingId = stagingId;
263
- this.state.context.lastUpdated = now;
304
+ state.context.currentPlanId = planId;
305
+ state.context.currentStagingId = stagingId;
306
+ state.context.lastUpdated = now;
264
307
  // Create artifacts directory
265
308
  const artifactsDir = normalizePath(join(this.projectRoot, staging.artifacts_path));
266
309
  await ensureDir(artifactsDir);
@@ -268,12 +311,9 @@ export class StateManager {
268
311
  await this.save();
269
312
  return staging;
270
313
  }
271
- async completestaging(stagingId) {
272
- if (!this.state)
273
- throw new Error("Project not initialized");
274
- const staging = this.getStaging(stagingId);
275
- if (!staging)
276
- throw new Error(`Staging not found: ${stagingId}`);
314
+ async completeStaging(stagingId) {
315
+ const state = this.ensureInitialized();
316
+ const staging = this.requireStaging(stagingId);
277
317
  const now = new Date().toISOString();
278
318
  staging.status = "completed";
279
319
  staging.completedAt = now;
@@ -288,8 +328,8 @@ export class StateManager {
288
328
  if (allCompleted) {
289
329
  plan.status = "completed";
290
330
  plan.completedAt = now;
291
- this.state.context.currentPlanId = undefined;
292
- this.state.context.currentStagingId = undefined;
331
+ state.context.currentPlanId = undefined;
332
+ state.context.currentStagingId = undefined;
293
333
  }
294
334
  }
295
335
  await this.addHistory("staging_completed", { stagingId, stagingName: staging.name });
@@ -297,11 +337,12 @@ export class StateManager {
297
337
  }
298
338
  // ============ Task Operations ============
299
339
  async updateTaskStatus(taskId, status, notes) {
300
- if (!this.state)
301
- throw new Error("Project not initialized");
302
- const task = this.getTask(taskId);
303
- if (!task)
304
- throw new Error(`Task not found: ${taskId}`);
340
+ const task = this.requireTask(taskId);
341
+ // Validate state transition
342
+ const allowedTransitions = VALID_TASK_TRANSITIONS[task.status];
343
+ if (!allowedTransitions.includes(status)) {
344
+ throw new TaskStateTransitionError(taskId, task.status, status, allowedTransitions);
345
+ }
305
346
  const now = new Date().toISOString();
306
347
  task.status = status;
307
348
  task.updatedAt = now;
@@ -323,7 +364,7 @@ export class StateManager {
323
364
  return t?.status === "done";
324
365
  });
325
366
  if (allTasksDone) {
326
- await this.completestaging(staging.id);
367
+ await this.completeStaging(staging.id);
327
368
  }
328
369
  }
329
370
  }
@@ -333,19 +374,24 @@ export class StateManager {
333
374
  await this.save();
334
375
  }
335
376
  async saveTaskOutput(taskId, output) {
336
- if (!this.state)
337
- throw new Error("Project not initialized");
338
- const task = this.getTask(taskId);
339
- if (!task)
340
- throw new Error(`Task not found: ${taskId}`);
377
+ // Validate taskId to prevent path traversal
378
+ if (!isValidId(taskId)) {
379
+ throw new InvalidIdError(taskId, "task");
380
+ }
381
+ const task = this.requireTask(taskId);
341
382
  task.output = output;
342
383
  task.updatedAt = new Date().toISOString();
343
384
  // Save output to artifacts file
344
385
  const staging = this.getStaging(task.stagingId);
345
386
  if (staging) {
387
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
346
388
  const outputPath = normalizePath(join(this.projectRoot, staging.artifacts_path, `${taskId}-output.json`));
347
- await ensureDir(dirname(outputPath));
348
- await writeFile(outputPath, JSON.stringify(output, null, 2), "utf-8");
389
+ // Verify path is within the .claude directory (path traversal protection)
390
+ if (!isPathSafe(claudeDir, outputPath)) {
391
+ throw new PathTraversalError(outputPath);
392
+ }
393
+ // Use atomic write to prevent data corruption
394
+ await atomicWriteFile(outputPath, JSON.stringify(output, null, 2));
349
395
  }
350
396
  await this.save();
351
397
  }
@@ -369,17 +415,23 @@ export class StateManager {
369
415
  }
370
416
  // ============ Archive Operations ============
371
417
  async archivePlan(planId, reason) {
372
- if (!this.state)
373
- throw new Error("Project not initialized");
374
- const plan = this.getPlan(planId);
375
- if (!plan)
376
- throw new Error(`Plan not found: ${planId}`);
418
+ const state = this.ensureInitialized();
419
+ // Validate planId to prevent path traversal
420
+ if (!isValidId(planId)) {
421
+ throw new InvalidIdError(planId, "plan");
422
+ }
423
+ const plan = this.requirePlan(planId);
377
424
  if (plan.status !== "completed" && plan.status !== "cancelled") {
378
- throw new Error(`Cannot archive plan in ${plan.status} status. Only completed or cancelled plans can be archived.`);
425
+ throw new PlanInvalidStateError(planId, plan.status, ["completed", "cancelled"]);
379
426
  }
380
427
  const now = new Date().toISOString();
428
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
381
429
  const sourcePath = normalizePath(join(this.projectRoot, ".claude", "plans", planId));
382
430
  const archivePath = normalizePath(join(this.projectRoot, ".claude", "archive", planId));
431
+ // Verify paths are within the .claude directory (path traversal protection)
432
+ if (!isPathSafe(claudeDir, sourcePath) || !isPathSafe(claudeDir, archivePath)) {
433
+ throw new PathTraversalError(planId);
434
+ }
383
435
  // Move plan directory to archive
384
436
  try {
385
437
  await ensureDir(dirname(archivePath));
@@ -394,23 +446,56 @@ export class StateManager {
394
446
  plan.archivedAt = now;
395
447
  plan.updatedAt = now;
396
448
  // Clear current context if this was the active plan
397
- if (this.state.context.currentPlanId === planId) {
398
- this.state.context.currentPlanId = undefined;
399
- this.state.context.currentStagingId = undefined;
449
+ if (state.context.currentPlanId === planId) {
450
+ state.context.currentPlanId = undefined;
451
+ state.context.currentStagingId = undefined;
400
452
  }
401
453
  await this.addHistory("plan_archived", { planId, title: plan.title, reason });
402
454
  await this.save();
403
455
  return toPosixPath(join(".claude", "archive", planId));
404
456
  }
457
+ async unarchivePlan(planId) {
458
+ this.ensureInitialized();
459
+ // Validate planId to prevent path traversal
460
+ if (!isValidId(planId)) {
461
+ throw new InvalidIdError(planId, "plan");
462
+ }
463
+ const plan = this.requirePlan(planId);
464
+ if (plan.status !== "archived") {
465
+ throw new PlanInvalidStateError(planId, plan.status, ["archived"]);
466
+ }
467
+ const now = new Date().toISOString();
468
+ const claudeDir = normalizePath(join(this.projectRoot, ".claude"));
469
+ const archivePath = normalizePath(join(this.projectRoot, ".claude", "archive", planId));
470
+ const restorePath = normalizePath(join(this.projectRoot, ".claude", "plans", planId));
471
+ // Verify paths are within the .claude directory (path traversal protection)
472
+ if (!isPathSafe(claudeDir, archivePath) || !isPathSafe(claudeDir, restorePath)) {
473
+ throw new PathTraversalError(planId);
474
+ }
475
+ // Move plan directory back from archive
476
+ try {
477
+ await ensureDir(dirname(restorePath));
478
+ await cp(archivePath, restorePath, { recursive: true });
479
+ await rm(archivePath, { recursive: true, force: true });
480
+ }
481
+ catch (error) {
482
+ // Directory might not exist if no artifacts were created
483
+ console.error(`Unarchive directory operation failed: ${error}`);
484
+ }
485
+ // Restore plan to completed status (since it was completed before archiving)
486
+ plan.status = "completed";
487
+ plan.archivedAt = undefined;
488
+ plan.updatedAt = now;
489
+ await this.addHistory("plan_unarchived", { planId, title: plan.title });
490
+ await this.save();
491
+ return { plan, restoredPath: toPosixPath(join(".claude", "plans", planId)) };
492
+ }
405
493
  // ============ Cancel Operations ============
406
494
  async cancelPlan(planId, reason) {
407
- if (!this.state)
408
- throw new Error("Project not initialized");
409
- const plan = this.getPlan(planId);
410
- if (!plan)
411
- throw new Error(`Plan not found: ${planId}`);
495
+ const state = this.ensureInitialized();
496
+ const plan = this.requirePlan(planId);
412
497
  if (plan.status === "archived" || plan.status === "cancelled") {
413
- throw new Error(`Plan is already ${plan.status}`);
498
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active", "completed"]);
414
499
  }
415
500
  const now = new Date().toISOString();
416
501
  let affectedStagings = 0;
@@ -435,9 +520,9 @@ export class StateManager {
435
520
  plan.status = "cancelled";
436
521
  plan.updatedAt = now;
437
522
  // Clear current context if this was the active plan
438
- if (this.state.context.currentPlanId === planId) {
439
- this.state.context.currentPlanId = undefined;
440
- this.state.context.currentStagingId = undefined;
523
+ if (state.context.currentPlanId === planId) {
524
+ state.context.currentPlanId = undefined;
525
+ state.context.currentStagingId = undefined;
441
526
  }
442
527
  await this.addHistory("plan_cancelled", { planId, title: plan.title, reason, affectedStagings, affectedTasks });
443
528
  await this.save();
@@ -455,10 +540,14 @@ export class StateManager {
455
540
  };
456
541
  this.state.history.push(entry);
457
542
  this.state.context.lastUpdated = entry.timestamp;
543
+ // Enforce history size limit - remove oldest entries if exceeded
544
+ if (this.state.history.length > MAX_HISTORY_ENTRIES) {
545
+ const excess = this.state.history.length - MAX_HISTORY_ENTRIES;
546
+ this.state.history.splice(0, excess);
547
+ }
458
548
  }
459
549
  async addDecision(title, decision, rationale, relatedPlanId, relatedStagingId) {
460
- if (!this.state)
461
- throw new Error("Project not initialized");
550
+ const state = this.ensureInitialized();
462
551
  const now = new Date().toISOString();
463
552
  const decisionEntry = {
464
553
  id: idGenerator.generateDecisionId(),
@@ -469,11 +558,291 @@ export class StateManager {
469
558
  relatedStagingId,
470
559
  timestamp: now,
471
560
  };
472
- this.state.context.decisions.push(decisionEntry);
561
+ state.context.decisions.push(decisionEntry);
473
562
  await this.addHistory("decision_added", { decisionId: decisionEntry.id, title });
474
563
  await this.save();
475
564
  return decisionEntry;
476
565
  }
566
+ // ============ Memory Operations ============
567
+ async addMemory(category, title, content, tags, priority) {
568
+ const state = this.ensureInitialized();
569
+ const now = new Date().toISOString();
570
+ const memory = {
571
+ id: idGenerator.generateMemoryId(),
572
+ category,
573
+ title,
574
+ content,
575
+ tags: tags ?? [],
576
+ priority: priority ?? 50,
577
+ enabled: true,
578
+ createdAt: now,
579
+ updatedAt: now,
580
+ };
581
+ state.context.memories.push(memory);
582
+ await this.addHistory("memory_added", { memoryId: memory.id, title, category });
583
+ await this.save();
584
+ return memory;
585
+ }
586
+ listMemories(category, tags, enabledOnly = true) {
587
+ if (!this.state)
588
+ return [];
589
+ let memories = this.state.context.memories;
590
+ if (enabledOnly) {
591
+ memories = memories.filter(m => m.enabled);
592
+ }
593
+ if (category) {
594
+ memories = memories.filter(m => m.category === category);
595
+ }
596
+ if (tags && tags.length > 0) {
597
+ memories = memories.filter(m => tags.some(tag => m.tags.includes(tag)));
598
+ }
599
+ // Sort by priority (descending)
600
+ return memories.sort((a, b) => b.priority - a.priority);
601
+ }
602
+ getMemoriesForContext(context) {
603
+ if (!this.state)
604
+ return [];
605
+ const enabledMemories = this.state.context.memories.filter(m => m.enabled);
606
+ let result;
607
+ if (context === "all") {
608
+ result = enabledMemories;
609
+ }
610
+ else if (context === "general") {
611
+ result = enabledMemories.filter(m => m.category === "general");
612
+ }
613
+ else {
614
+ // Return general + context-specific memories
615
+ result = enabledMemories.filter(m => m.category === "general" || m.category === context);
616
+ }
617
+ // Sort by priority (descending)
618
+ return result.sort((a, b) => b.priority - a.priority);
619
+ }
620
+ /**
621
+ * Get memories for a specific event (staging-start, task-start, etc.)
622
+ * Returns general + event-specific memories, optionally filtered by additional tags
623
+ */
624
+ getMemoriesForEvent(event, additionalTags) {
625
+ if (!this.state)
626
+ return [];
627
+ const enabledMemories = this.state.context.memories.filter(m => m.enabled);
628
+ // Get general + event-specific memories
629
+ let result = enabledMemories.filter(m => m.category === "general" || m.category === event);
630
+ // If additional tags are provided, also include memories matching those tags
631
+ if (additionalTags && additionalTags.length > 0) {
632
+ const tagMatched = enabledMemories.filter(m => additionalTags.some(tag => m.tags.includes(tag)));
633
+ // Add tag-matched memories that aren't already in result
634
+ const resultIds = new Set(result.map(m => m.id));
635
+ for (const m of tagMatched) {
636
+ if (!resultIds.has(m.id)) {
637
+ result.push(m);
638
+ }
639
+ }
640
+ }
641
+ // Sort by priority (descending)
642
+ return result.sort((a, b) => b.priority - a.priority);
643
+ }
644
+ /**
645
+ * Get artifacts from stagings that this staging depends on
646
+ */
647
+ getRelatedStagingArtifacts(stagingId) {
648
+ const staging = this.getStaging(stagingId);
649
+ if (!staging)
650
+ return [];
651
+ const results = [];
652
+ for (const ref of staging.depends_on_stagings) {
653
+ const depStaging = this.getStaging(ref.stagingId);
654
+ if (!depStaging)
655
+ continue;
656
+ const tasks = this.getTasksByStaging(ref.stagingId);
657
+ const taskOutputs = {};
658
+ // If taskIds is specified, only include those tasks; otherwise include all
659
+ const targetTaskIds = ref.taskIds ?? tasks.map(t => t.id);
660
+ for (const task of tasks) {
661
+ if (targetTaskIds.includes(task.id) && task.output) {
662
+ taskOutputs[task.id] = task.output;
663
+ }
664
+ }
665
+ results.push({
666
+ stagingId: depStaging.id,
667
+ stagingName: depStaging.name,
668
+ taskOutputs,
669
+ });
670
+ }
671
+ return results;
672
+ }
673
+ /**
674
+ * Get task outputs for cross-staging references
675
+ */
676
+ getCrossReferencedTaskOutputs(taskId) {
677
+ const task = this.getTask(taskId);
678
+ if (!task)
679
+ return [];
680
+ const results = [];
681
+ for (const ref of task.cross_staging_refs) {
682
+ const refTask = this.getTask(ref.taskId);
683
+ if (!refTask)
684
+ continue;
685
+ results.push({
686
+ taskId: refTask.id,
687
+ taskTitle: refTask.title,
688
+ stagingId: ref.stagingId,
689
+ output: refTask.output ?? null,
690
+ });
691
+ }
692
+ return results;
693
+ }
694
+ async updateMemory(memoryId, updates) {
695
+ const memory = this.requireMemory(memoryId);
696
+ const now = new Date().toISOString();
697
+ if (updates.title !== undefined)
698
+ memory.title = updates.title;
699
+ if (updates.content !== undefined)
700
+ memory.content = updates.content;
701
+ if (updates.category !== undefined)
702
+ memory.category = updates.category;
703
+ if (updates.tags !== undefined)
704
+ memory.tags = updates.tags;
705
+ if (updates.priority !== undefined)
706
+ memory.priority = updates.priority;
707
+ if (updates.enabled !== undefined)
708
+ memory.enabled = updates.enabled;
709
+ memory.updatedAt = now;
710
+ await this.addHistory("memory_updated", { memoryId, updates });
711
+ await this.save();
712
+ return memory;
713
+ }
714
+ async removeMemory(memoryId) {
715
+ const state = this.ensureInitialized();
716
+ const memory = this.requireMemory(memoryId);
717
+ const index = state.context.memories.findIndex(m => m.id === memoryId);
718
+ state.context.memories.splice(index, 1);
719
+ await this.addHistory("memory_removed", { memoryId, title: memory.title });
720
+ await this.save();
721
+ }
722
+ getMemory(memoryId) {
723
+ return this.state?.context.memories.find(m => m.id === memoryId);
724
+ }
725
+ getCategories() {
726
+ if (!this.state)
727
+ return [];
728
+ const categories = new Set();
729
+ for (const memory of this.state.context.memories) {
730
+ categories.add(memory.category);
731
+ }
732
+ return Array.from(categories).sort();
733
+ }
734
+ // ============ Project Summary Operations ============
735
+ /**
736
+ * Get the existing project summary memory (if any)
737
+ */
738
+ getProjectSummary() {
739
+ if (!this.state)
740
+ return undefined;
741
+ return this.state.context.memories.find(m => m.category === "project-summary");
742
+ }
743
+ /**
744
+ * Generate a project summary from current state
745
+ */
746
+ generateProjectSummaryContent() {
747
+ if (!this.state)
748
+ return "";
749
+ const project = this.state.project;
750
+ const plans = Object.values(this.state.plans);
751
+ const memories = this.state.context.memories.filter(m => m.category !== "project-summary");
752
+ // Calculate stats
753
+ const activePlans = plans.filter(p => p.status === "active");
754
+ const completedPlans = plans.filter(p => p.status === "completed");
755
+ const totalTasks = Object.keys(this.state.tasks).length;
756
+ const completedTasks = Object.values(this.state.tasks).filter(t => t.status === "done").length;
757
+ // Build summary content
758
+ const lines = [];
759
+ // Project info
760
+ lines.push(`# ${project.name}`);
761
+ if (project.description) {
762
+ lines.push(`\n${project.description}`);
763
+ }
764
+ // Goals
765
+ if (project.goals.length > 0) {
766
+ lines.push(`\n## Goals`);
767
+ project.goals.forEach(g => lines.push(`- ${g}`));
768
+ }
769
+ // Constraints
770
+ if (project.constraints.length > 0) {
771
+ lines.push(`\n## Constraints`);
772
+ project.constraints.forEach(c => lines.push(`- ${c}`));
773
+ }
774
+ // Current status
775
+ lines.push(`\n## Status`);
776
+ lines.push(`- Active Plans: ${activePlans.length}`);
777
+ lines.push(`- Completed Plans: ${completedPlans.length}`);
778
+ lines.push(`- Tasks: ${completedTasks}/${totalTasks} completed`);
779
+ // Active plan details
780
+ if (activePlans.length > 0) {
781
+ lines.push(`\n## Active Work`);
782
+ for (const plan of activePlans) {
783
+ const stagings = this.getStagingsByPlan(plan.id);
784
+ const currentStaging = stagings.find(s => s.status === "in_progress");
785
+ lines.push(`- **${plan.title}**`);
786
+ if (currentStaging) {
787
+ const tasks = this.getTasksByStaging(currentStaging.id);
788
+ const inProgress = tasks.filter(t => t.status === "in_progress");
789
+ lines.push(` - Current: ${currentStaging.name}`);
790
+ if (inProgress.length > 0) {
791
+ lines.push(` - Tasks: ${inProgress.map(t => t.title).join(", ")}`);
792
+ }
793
+ }
794
+ }
795
+ }
796
+ // Key memories (non project-summary)
797
+ const keyMemories = memories.filter(m => m.enabled && m.priority >= 70);
798
+ if (keyMemories.length > 0) {
799
+ lines.push(`\n## Key Rules`);
800
+ for (const m of keyMemories.slice(0, 5)) {
801
+ lines.push(`- **${m.title}** (${m.category})`);
802
+ }
803
+ }
804
+ // Recent decisions
805
+ const recentDecisions = this.state.context.decisions.slice(-3);
806
+ if (recentDecisions.length > 0) {
807
+ lines.push(`\n## Recent Decisions`);
808
+ for (const d of recentDecisions) {
809
+ lines.push(`- ${d.title}: ${d.decision}`);
810
+ }
811
+ }
812
+ return lines.join("\n");
813
+ }
814
+ /**
815
+ * Create or update the project summary memory
816
+ */
817
+ async saveProjectSummary(content) {
818
+ const state = this.ensureInitialized();
819
+ const summaryContent = content ?? this.generateProjectSummaryContent();
820
+ const existingSummary = this.getProjectSummary();
821
+ if (existingSummary) {
822
+ // Update existing summary
823
+ return this.updateMemory(existingSummary.id, {
824
+ content: summaryContent,
825
+ title: `${state.project.name} - Project Summary`,
826
+ });
827
+ }
828
+ else {
829
+ // Create new summary
830
+ return this.addMemory("project-summary", `${state.project.name} - Project Summary`, summaryContent, ["auto-generated", "summary"], 100 // Highest priority to appear first
831
+ );
832
+ }
833
+ }
834
+ /**
835
+ * Get memories that should always be applied (general + project-summary)
836
+ */
837
+ getAlwaysAppliedMemories() {
838
+ if (!this.state)
839
+ return [];
840
+ const enabledMemories = this.state.context.memories.filter(m => m.enabled);
841
+ // Return general + project-summary memories
842
+ const result = enabledMemories.filter(m => m.category === "general" || m.category === "project-summary");
843
+ // Sort by priority (descending)
844
+ return result.sort((a, b) => b.priority - a.priority);
845
+ }
477
846
  // ============ Session Operations ============
478
847
  async startSession() {
479
848
  await this.addHistory("session_started", {});
@@ -490,13 +859,9 @@ export class StateManager {
490
859
  }
491
860
  // ============ Modify Operations ============
492
861
  async updatePlan(planId, updates) {
493
- if (!this.state)
494
- throw new Error("Project not initialized");
495
- const plan = this.getPlan(planId);
496
- if (!plan)
497
- throw new Error(`Plan not found: ${planId}`);
862
+ const plan = this.requirePlan(planId);
498
863
  if (plan.status === "archived" || plan.status === "cancelled") {
499
- throw new Error(`Cannot modify ${plan.status} plan`);
864
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active", "completed"]);
500
865
  }
501
866
  const now = new Date().toISOString();
502
867
  if (updates.title !== undefined)
@@ -509,13 +874,9 @@ export class StateManager {
509
874
  return plan;
510
875
  }
511
876
  async updateStaging(stagingId, updates) {
512
- if (!this.state)
513
- throw new Error("Project not initialized");
514
- const staging = this.getStaging(stagingId);
515
- if (!staging)
516
- throw new Error(`Staging not found: ${stagingId}`);
877
+ const staging = this.requireStaging(stagingId);
517
878
  if (staging.status === "completed" || staging.status === "cancelled") {
518
- throw new Error(`Cannot modify ${staging.status} staging`);
879
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "in_progress"]);
519
880
  }
520
881
  if (updates.name !== undefined)
521
882
  staging.name = updates.name;
@@ -523,18 +884,21 @@ export class StateManager {
523
884
  staging.description = updates.description;
524
885
  if (updates.execution_type !== undefined)
525
886
  staging.execution_type = updates.execution_type;
887
+ if (updates.default_model !== undefined)
888
+ staging.default_model = updates.default_model;
889
+ if (updates.session_budget !== undefined)
890
+ staging.session_budget = updates.session_budget;
891
+ if (updates.recommended_sessions !== undefined)
892
+ staging.recommended_sessions = updates.recommended_sessions;
526
893
  await this.addHistory("staging_updated", { stagingId, updates });
527
894
  await this.save();
528
895
  return staging;
529
896
  }
530
897
  async addStaging(planId, config) {
531
- if (!this.state)
532
- throw new Error("Project not initialized");
533
- const plan = this.getPlan(planId);
534
- if (!plan)
535
- throw new Error(`Plan not found: ${planId}`);
898
+ const state = this.ensureInitialized();
899
+ const plan = this.requirePlan(planId);
536
900
  if (plan.status === "archived" || plan.status === "cancelled" || plan.status === "completed") {
537
- throw new Error(`Cannot add staging to ${plan.status} plan`);
901
+ throw new PlanInvalidStateError(planId, plan.status, ["draft", "active"]);
538
902
  }
539
903
  const now = new Date().toISOString();
540
904
  const stagingId = idGenerator.generateStagingId();
@@ -555,11 +919,16 @@ export class StateManager {
555
919
  order: insertAt,
556
920
  execution_type: config.execution_type,
557
921
  status: "pending",
922
+ default_model: config.default_model,
923
+ session_budget: config.session_budget,
924
+ recommended_sessions: config.recommended_sessions,
558
925
  tasks: [],
926
+ depends_on_stagings: [],
927
+ auto_include_artifacts: true,
559
928
  artifacts_path: artifactsPath,
560
929
  createdAt: now,
561
930
  };
562
- this.state.stagings[stagingId] = staging;
931
+ state.stagings[stagingId] = staging;
563
932
  // Insert into plan's staging list
564
933
  plan.stagings.splice(insertAt, 0, stagingId);
565
934
  plan.updatedAt = now;
@@ -568,20 +937,15 @@ export class StateManager {
568
937
  return staging;
569
938
  }
570
939
  async removeStaging(stagingId) {
571
- if (!this.state)
572
- throw new Error("Project not initialized");
573
- const staging = this.getStaging(stagingId);
574
- if (!staging)
575
- throw new Error(`Staging not found: ${stagingId}`);
940
+ const state = this.ensureInitialized();
941
+ const staging = this.requireStaging(stagingId);
576
942
  if (staging.status === "in_progress") {
577
- throw new Error("Cannot remove in_progress staging");
943
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "completed", "cancelled"]);
578
944
  }
579
- const plan = this.getPlan(staging.planId);
580
- if (!plan)
581
- throw new Error(`Plan not found: ${staging.planId}`);
945
+ const plan = this.requirePlan(staging.planId);
582
946
  // Remove all tasks in this staging
583
947
  for (const taskId of staging.tasks) {
584
- delete this.state.tasks[taskId];
948
+ delete state.tasks[taskId];
585
949
  }
586
950
  // Remove staging from plan
587
951
  plan.stagings = plan.stagings.filter(id => id !== stagingId);
@@ -593,22 +957,52 @@ export class StateManager {
593
957
  s.order = i;
594
958
  });
595
959
  // Remove staging
596
- delete this.state.stagings[stagingId];
960
+ delete state.stagings[stagingId];
597
961
  await this.addHistory("staging_removed", { stagingId, stagingName: staging.name });
598
962
  await this.save();
599
963
  }
964
+ // ============ Circular Dependency Detection ============
965
+ /**
966
+ * Check for circular dependencies in task dependencies
967
+ * @param taskId - The task being checked
968
+ * @param depends_on - Dependencies to validate
969
+ * @returns dependency chain if circular, null otherwise
970
+ */
971
+ detectCircularDependency(taskId, depends_on, visited = new Set()) {
972
+ if (visited.has(taskId)) {
973
+ return Array.from(visited);
974
+ }
975
+ visited.add(taskId);
976
+ for (const depId of depends_on) {
977
+ const depTask = this.getTask(depId);
978
+ if (!depTask)
979
+ continue;
980
+ if (depTask.depends_on.includes(taskId)) {
981
+ return [...Array.from(visited), depId, taskId];
982
+ }
983
+ const chain = this.detectCircularDependency(depId, depTask.depends_on, new Set(visited));
984
+ if (chain) {
985
+ return chain;
986
+ }
987
+ }
988
+ return null;
989
+ }
600
990
  async addTask(stagingId, config) {
601
- if (!this.state)
602
- throw new Error("Project not initialized");
603
- const staging = this.getStaging(stagingId);
604
- if (!staging)
605
- throw new Error(`Staging not found: ${stagingId}`);
991
+ const state = this.ensureInitialized();
992
+ const staging = this.requireStaging(stagingId);
606
993
  if (staging.status === "completed" || staging.status === "cancelled") {
607
- throw new Error(`Cannot add task to ${staging.status} staging`);
994
+ throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "in_progress"]);
608
995
  }
609
996
  const now = new Date().toISOString();
610
997
  const taskId = idGenerator.generateTaskId();
611
998
  const existingTasks = this.getTasksByStaging(stagingId);
999
+ // Check for circular dependencies
1000
+ if (config.depends_on && config.depends_on.length > 0) {
1001
+ const circularChain = this.detectCircularDependency(taskId, config.depends_on);
1002
+ if (circularChain) {
1003
+ throw new CircularDependencyError(taskId, circularChain);
1004
+ }
1005
+ }
612
1006
  const task = {
613
1007
  id: taskId,
614
1008
  planId: staging.planId,
@@ -618,33 +1012,31 @@ export class StateManager {
618
1012
  priority: config.priority,
619
1013
  status: "pending",
620
1014
  execution_mode: config.execution_mode,
1015
+ model: config.model,
621
1016
  depends_on: config.depends_on ?? [],
1017
+ cross_staging_refs: [],
1018
+ memory_tags: [],
622
1019
  order: existingTasks.length,
623
1020
  createdAt: now,
624
1021
  updatedAt: now,
625
1022
  };
626
- this.state.tasks[taskId] = task;
1023
+ state.tasks[taskId] = task;
627
1024
  staging.tasks.push(taskId);
628
1025
  await this.addHistory("task_added", { stagingId, taskId, title: config.title });
629
1026
  await this.save();
630
1027
  return task;
631
1028
  }
632
1029
  async removeTask(taskId) {
633
- if (!this.state)
634
- throw new Error("Project not initialized");
635
- const task = this.getTask(taskId);
636
- if (!task)
637
- throw new Error(`Task not found: ${taskId}`);
1030
+ const state = this.ensureInitialized();
1031
+ const task = this.requireTask(taskId);
638
1032
  if (task.status === "in_progress") {
639
- throw new Error("Cannot remove in_progress task");
1033
+ throw new TaskInvalidStateError(taskId, task.status, ["pending", "done", "blocked", "cancelled"]);
640
1034
  }
641
- const staging = this.getStaging(task.stagingId);
642
- if (!staging)
643
- throw new Error(`Staging not found: ${task.stagingId}`);
1035
+ const staging = this.requireStaging(task.stagingId);
644
1036
  // Remove task from staging
645
1037
  staging.tasks = staging.tasks.filter(id => id !== taskId);
646
1038
  // Remove this task from other tasks' dependencies
647
- for (const t of Object.values(this.state.tasks)) {
1039
+ for (const t of Object.values(state.tasks)) {
648
1040
  t.depends_on = t.depends_on.filter(id => id !== taskId);
649
1041
  }
650
1042
  // Reorder remaining tasks
@@ -654,18 +1046,14 @@ export class StateManager {
654
1046
  t.order = i;
655
1047
  });
656
1048
  // Remove task
657
- delete this.state.tasks[taskId];
1049
+ delete state.tasks[taskId];
658
1050
  await this.addHistory("task_removed", { taskId, taskTitle: task.title });
659
1051
  await this.save();
660
1052
  }
661
1053
  async updateTaskDetails(taskId, updates) {
662
- if (!this.state)
663
- throw new Error("Project not initialized");
664
- const task = this.getTask(taskId);
665
- if (!task)
666
- throw new Error(`Task not found: ${taskId}`);
1054
+ const task = this.requireTask(taskId);
667
1055
  if (task.status === "done" || task.status === "cancelled") {
668
- throw new Error(`Cannot modify ${task.status} task`);
1056
+ throw new TaskInvalidStateError(taskId, task.status, ["pending", "in_progress", "blocked"]);
669
1057
  }
670
1058
  const now = new Date().toISOString();
671
1059
  if (updates.title !== undefined)
@@ -676,6 +1064,31 @@ export class StateManager {
676
1064
  task.priority = updates.priority;
677
1065
  if (updates.execution_mode !== undefined)
678
1066
  task.execution_mode = updates.execution_mode;
1067
+ if (updates.model !== undefined)
1068
+ task.model = updates.model;
1069
+ // Handle depends_on updates with circular dependency check
1070
+ if (updates.depends_on !== undefined) {
1071
+ // Validate all dependency IDs exist and are in the same staging
1072
+ for (const depId of updates.depends_on) {
1073
+ const depTask = this.getTask(depId);
1074
+ if (!depTask) {
1075
+ throw new TaskNotFoundError(depId);
1076
+ }
1077
+ if (depTask.stagingId !== task.stagingId) {
1078
+ throw new TaskInvalidStateError(depId, "different staging", ["same staging as dependent task"]);
1079
+ }
1080
+ // Cannot depend on itself
1081
+ if (depId === taskId) {
1082
+ throw new CircularDependencyError(taskId, [taskId]);
1083
+ }
1084
+ }
1085
+ // Check for circular dependencies
1086
+ const circularChain = this.detectCircularDependency(taskId, updates.depends_on);
1087
+ if (circularChain) {
1088
+ throw new CircularDependencyError(taskId, circularChain);
1089
+ }
1090
+ task.depends_on = updates.depends_on;
1091
+ }
679
1092
  task.updatedAt = now;
680
1093
  await this.addHistory("task_updated", { taskId, updates });
681
1094
  await this.save();