@allpepper/task-orchestrator 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allpepper/task-orchestrator",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for hierarchical task orchestration with SQLite persistence",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/repos/base.ts CHANGED
@@ -149,3 +149,19 @@ export function countTasksByProject(projectId: string): TaskCounts {
149
149
  }
150
150
  return { total, byStatus };
151
151
  }
152
+
153
+ export function countFeaturesByProject(projectId: string): number {
154
+ const row = queryOne<{ count: number }>(
155
+ 'SELECT COUNT(*) as count FROM features WHERE project_id = ?',
156
+ [projectId]
157
+ );
158
+ return row?.count ?? 0;
159
+ }
160
+
161
+ export function countTasksByFeatureId(featureId: string): number {
162
+ const row = queryOne<{ count: number }>(
163
+ 'SELECT COUNT(*) as count FROM tasks WHERE feature_id = ?',
164
+ [featureId]
165
+ );
166
+ return row?.count ?? 0;
167
+ }
@@ -343,16 +343,51 @@ export function updateFeature(
343
343
  /**
344
344
  * Delete a feature
345
345
  */
346
- export function deleteFeature(id: string): Result<boolean> {
346
+ export function deleteFeature(id: string, options?: { cascade?: boolean }): Result<boolean> {
347
347
  try {
348
+ const cascade = options?.cascade ?? false;
349
+
350
+ // Check if feature exists
351
+ const exists = queryOne<{ id: string }>('SELECT id FROM features WHERE id = ?', [id]);
352
+
353
+ if (!exists) {
354
+ throw new NotFoundError('Feature', id);
355
+ }
356
+
357
+ // Count children
358
+ const taskCounts = countTasksByFeature(id);
359
+ const taskCount = taskCounts.total;
360
+
361
+ // If children exist and no cascade, return error with counts
362
+ if (taskCount > 0 && !cascade) {
363
+ return err(
364
+ `Cannot delete feature: contains ${taskCount} task${taskCount > 1 ? 's' : ''}. Use cascade: true to delete all.`,
365
+ 'HAS_CHILDREN'
366
+ );
367
+ }
368
+
348
369
  const result = transaction(() => {
349
- // Check if feature exists
350
- const exists = queryOne<{ id: string }>('SELECT id FROM features WHERE id = ?', [id]);
370
+ if (cascade) {
371
+ // Get all task IDs for this feature
372
+ const taskIds = queryAll<{ id: string }>(
373
+ 'SELECT id FROM tasks WHERE feature_id = ?',
374
+ [id]
375
+ );
351
376
 
352
- if (!exists) {
353
- throw new NotFoundError('Feature', id);
377
+ // Delete each task's dependencies, sections, and tags
378
+ for (const task of taskIds) {
379
+ execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [task.id, task.id]);
380
+ execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, task.id]);
381
+ deleteTags(task.id, EntityType.TASK);
382
+ }
383
+
384
+ // Delete all tasks for this feature
385
+ execute('DELETE FROM tasks WHERE feature_id = ?', [id]);
354
386
  }
355
387
 
388
+ // Delete feature sections
389
+ execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.FEATURE, id]);
390
+
356
391
  // Delete associated tags
357
392
  deleteTags(id, EntityType.FEATURE);
358
393
 
@@ -1,6 +1,6 @@
1
- import { queryOne, queryAll, execute, generateId, now, buildSearchVector, loadTags, saveTags, deleteTags, ok, err, buildPaginationClause, countTasksByProject, type TaskCounts } from './base';
1
+ import { queryOne, queryAll, execute, generateId, now, buildSearchVector, loadTags, saveTags, deleteTags, ok, err, buildPaginationClause, countTasksByProject, countFeaturesByProject, type TaskCounts } from './base';
2
2
  import type { Project, Result } from '../domain/types';
3
- import { ProjectStatus, NotFoundError, ConflictError, ValidationError } from '../domain/types';
3
+ import { ProjectStatus, NotFoundError, ConflictError, ValidationError, EntityType } from '../domain/types';
4
4
  import { transaction } from '../db/client';
5
5
  import { isValidTransition, getAllowedTransitions, isTerminalStatus } from '../services/status-validator';
6
6
 
@@ -221,20 +221,75 @@ export function updateProject(
221
221
  }
222
222
  }
223
223
 
224
- export function deleteProject(id: string): Result<boolean> {
224
+ export function deleteProject(id: string, options?: { cascade?: boolean }): Result<boolean> {
225
225
  try {
226
- const result = transaction(() => {
227
- const existing = queryOne<ProjectRow>(
228
- 'SELECT id FROM projects WHERE id = ?',
229
- [id]
226
+ const cascade = options?.cascade ?? false;
227
+
228
+ // Check if project exists
229
+ const existing = queryOne<ProjectRow>(
230
+ 'SELECT id FROM projects WHERE id = ?',
231
+ [id]
232
+ );
233
+
234
+ if (!existing) {
235
+ throw new NotFoundError('Project', id);
236
+ }
237
+
238
+ // Count children
239
+ const featureCount = countFeaturesByProject(id);
240
+ const taskCounts = countTasksByProject(id);
241
+ const taskCount = taskCounts.total;
242
+
243
+ // If children exist and no cascade, return error with counts
244
+ if ((featureCount > 0 || taskCount > 0) && !cascade) {
245
+ const parts: string[] = [];
246
+ if (featureCount > 0) parts.push(`${featureCount} feature${featureCount > 1 ? 's' : ''}`);
247
+ if (taskCount > 0) parts.push(`${taskCount} task${taskCount > 1 ? 's' : ''}`);
248
+ return err(
249
+ `Cannot delete project: contains ${parts.join(' and ')}. Use cascade: true to delete all.`,
250
+ 'HAS_CHILDREN'
230
251
  );
252
+ }
231
253
 
232
- if (!existing) {
233
- throw new NotFoundError('Project', id);
254
+ const result = transaction(() => {
255
+ if (cascade) {
256
+ // Get all task IDs for this project
257
+ const taskIds = queryAll<{ id: string }>(
258
+ 'SELECT id FROM tasks WHERE project_id = ?',
259
+ [id]
260
+ );
261
+
262
+ // Delete each task's dependencies, sections, and tags
263
+ for (const task of taskIds) {
264
+ execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [task.id, task.id]);
265
+ execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, task.id]);
266
+ deleteTags(task.id, EntityType.TASK);
267
+ }
268
+
269
+ // Delete all tasks for this project
270
+ execute('DELETE FROM tasks WHERE project_id = ?', [id]);
271
+
272
+ // Get all feature IDs for this project
273
+ const featureIds = queryAll<{ id: string }>(
274
+ 'SELECT id FROM features WHERE project_id = ?',
275
+ [id]
276
+ );
277
+
278
+ // Delete each feature's sections and tags
279
+ for (const feature of featureIds) {
280
+ execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.FEATURE, feature.id]);
281
+ deleteTags(feature.id, EntityType.FEATURE);
282
+ }
283
+
284
+ // Delete all features for this project
285
+ execute('DELETE FROM features WHERE project_id = ?', [id]);
234
286
  }
235
287
 
236
- // Delete tags first
237
- deleteTags(id, 'PROJECT');
288
+ // Delete project sections
289
+ execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.PROJECT, id]);
290
+
291
+ // Delete project tags
292
+ deleteTags(id, EntityType.PROJECT);
238
293
 
239
294
  // Delete project
240
295
  execute('DELETE FROM projects WHERE id = ?', [id]);
@@ -52,6 +52,7 @@ export function registerManageContainerTool(server: McpServer): void {
52
52
  complexity: z.number().int().optional(),
53
53
  tags: z.string().optional(),
54
54
  version: z.number().int().optional(),
55
+ cascade: z.boolean().optional().describe('For delete operation: if true, deletes all child entities (features, tasks) recursively. Required when deleting a project or feature that has children.'),
55
56
  },
56
57
  async (params) => {
57
58
  try {
@@ -235,11 +236,13 @@ export function registerManageContainerTool(server: McpServer): void {
235
236
  };
236
237
  }
237
238
 
239
+ const cascadeOption = params.cascade ? { cascade: true } : undefined;
240
+
238
241
  let result;
239
242
  if (containerType === 'project') {
240
- result = deleteProject(id);
243
+ result = deleteProject(id, cascadeOption);
241
244
  } else if (containerType === 'feature') {
242
- result = deleteFeature(id);
245
+ result = deleteFeature(id, cascadeOption);
243
246
  } else {
244
247
  result = deleteTask(id);
245
248
  }