@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.
- package/dist/errors/index.d.ts +28 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +50 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/state/manager.d.ts +74 -2
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +551 -138
- package/dist/state/manager.js.map +1 -1
- package/dist/state/manager.test.d.ts +2 -0
- package/dist/state/manager.test.d.ts.map +1 -0
- package/dist/state/manager.test.js +310 -0
- package/dist/state/manager.test.js.map +1 -0
- package/dist/state/schema.d.ts +443 -21
- package/dist/state/schema.d.ts.map +1 -1
- package/dist/state/schema.js +99 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/state/schema.test.d.ts +2 -0
- package/dist/state/schema.test.d.ts.map +1 -0
- package/dist/state/schema.test.js +300 -0
- package/dist/state/schema.test.js.map +1 -0
- package/dist/state/types.d.ts +47 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/state/types.js +3 -1
- package/dist/state/types.js.map +1 -1
- package/dist/tools/archive.d.ts.map +1 -1
- package/dist/tools/archive.js +57 -0
- package/dist/tools/archive.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +14 -0
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +7 -0
- package/dist/tools/files.d.ts.map +1 -0
- package/dist/tools/files.js +131 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory.d.ts +7 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +243 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/modify.d.ts.map +1 -1
- package/dist/tools/modify.js +43 -7
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/plan.js +77 -4
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/staging.d.ts.map +1 -1
- package/dist/tools/staging.js +104 -6
- package/dist/tools/staging.js.map +1 -1
- package/dist/tools/summary.d.ts +7 -0
- package/dist/tools/summary.d.ts.map +1 -0
- package/dist/tools/summary.js +127 -0
- package/dist/tools/summary.js.map +1 -0
- package/dist/utils/paths.d.ts +34 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +94 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +10 -3
package/dist/state/manager.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import { readFile,
|
|
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 {
|
|
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
|
-
|
|
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-${
|
|
17
|
-
generateStagingId: () => `staging-${
|
|
18
|
-
generateTaskId: () => `task-${
|
|
19
|
-
generateHistoryId: () => `hist-${Date.now()}-${
|
|
20
|
-
generateDecisionId: () => `dec-${Date.now()}-${
|
|
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
|
-
|
|
68
|
-
await
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
if (!
|
|
304
|
-
throw new
|
|
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.
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
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 (
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
|
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 (
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
532
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
|
943
|
+
throw new StagingInvalidStateError(stagingId, staging.status, ["pending", "completed", "cancelled"]);
|
|
578
944
|
}
|
|
579
|
-
const plan = this.
|
|
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
|
|
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
|
|
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
|
-
|
|
602
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
|
1033
|
+
throw new TaskInvalidStateError(taskId, task.status, ["pending", "done", "blocked", "cancelled"]);
|
|
640
1034
|
}
|
|
641
|
-
const staging = this.
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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();
|