pi-orchestrator-config 4.2.1 → 4.3.2

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 (92) hide show
  1. package/README.md +52 -61
  2. package/agents/git-expert.md +5 -1
  3. package/agents/github-expert.md +4 -1
  4. package/extensions/acpx-provider/runtime-models.ts +18 -6
  5. package/extensions/cli-provider/agents/claude.ts +3 -2
  6. package/extensions/cli-provider/agents/cursor.ts +3 -5
  7. package/extensions/cli-provider/agents/gemini.ts +3 -2
  8. package/extensions/cli-provider/discover.ts +7 -7
  9. package/extensions/cli-provider/index.ts +5 -0
  10. package/extensions/cli-provider/runtime-models.ts +18 -6
  11. package/extensions/cli-provider/session-reaper.ts +5 -7
  12. package/extensions/cli-provider/sessions.ts +4 -0
  13. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  14. package/extensions/coms/coms-p2p.ts +1719 -423
  15. package/extensions/coms/coms-shared.ts +261 -39
  16. package/extensions/coms/coms-wrapper.ts +100 -62
  17. package/extensions/coms/index.ts +8 -4
  18. package/extensions/coms/mixed-turn.ts +26 -0
  19. package/extensions/coms/probe-socket.ts +42 -0
  20. package/extensions/coms/themeMap.ts +0 -1
  21. package/extensions/image-gen/image-gen.ts +19 -1
  22. package/extensions/orchestrator/async-agents.ts +509 -85
  23. package/extensions/orchestrator/cron.ts +14 -8
  24. package/extensions/orchestrator/dreaming.ts +31 -24
  25. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  26. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  27. package/extensions/orchestrator/enforcement.ts +79 -25
  28. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  29. package/extensions/orchestrator/git-helpers.ts +2 -2
  30. package/extensions/orchestrator/index.ts +23 -10
  31. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  32. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  33. package/extensions/orchestrator/memory-provenance.ts +29 -25
  34. package/extensions/orchestrator/memory-scoring.ts +23 -18
  35. package/extensions/orchestrator/memory-tools.ts +10 -3
  36. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  37. package/extensions/orchestrator/pi-config-review-state.ts +44 -33
  38. package/extensions/orchestrator/project-settings.ts +220 -21
  39. package/extensions/orchestrator/review-porcelain.ts +4 -0
  40. package/extensions/orchestrator/review-ui.ts +11 -5
  41. package/extensions/orchestrator/rule-placeholders.ts +384 -1
  42. package/extensions/orchestrator/rules.ts +80 -132
  43. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  44. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  45. package/extensions/orchestrator/settings-tui.ts +636 -0
  46. package/extensions/orchestrator/state-jsonl.ts +431 -0
  47. package/extensions/orchestrator/subagent-tool.ts +105 -50
  48. package/extensions/orchestrator/sync-limit.ts +23 -0
  49. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  50. package/extensions/orchestrator/utils.ts +8 -5
  51. package/extensions/pidash/pidash.ts +193 -156
  52. package/extensions/pidiff/pidiff.ts +64 -38
  53. package/extensions/pidiff/spawn-lock.ts +62 -0
  54. package/extensions/pitasks/index.ts +430 -0
  55. package/extensions/pitasks/reminders.ts +37 -0
  56. package/extensions/pitasks/task-store.ts +467 -0
  57. package/extensions/pitasks/task-tools.ts +375 -0
  58. package/extensions/pitasks/task-widget.ts +181 -0
  59. package/extensions/pitasks/types.ts +38 -0
  60. package/extensions/providers/acpx-driver.ts +12 -2
  61. package/extensions/providers/built-in-drivers.ts +6 -6
  62. package/extensions/providers/claude-driver.ts +1 -1
  63. package/extensions/providers/cursor-cli-driver.ts +1 -1
  64. package/extensions/providers/gemini-driver.ts +1 -1
  65. package/extensions/providers/index.ts +144 -26
  66. package/extensions/providers/initialized-guard.ts +27 -0
  67. package/extensions/providers/restore-default-model.ts +596 -0
  68. package/extensions/providers/session-shutdown.ts +39 -0
  69. package/extensions/shared/coms-active.ts +23 -0
  70. package/extensions/shared/create-runtime-provider.ts +2 -0
  71. package/extensions/shared/daemon-manager.ts +0 -8
  72. package/extensions/shared/file-logger.ts +132 -25
  73. package/extensions/shared/logger.ts +50 -0
  74. package/extensions/shared/models-dev.ts +421 -0
  75. package/extensions/shared/oneshot.ts +116 -0
  76. package/extensions/shared/resolve-binary.ts +191 -0
  77. package/extensions/shared/types.ts +3 -0
  78. package/package.json +7 -2
  79. package/prompts/coderabbit-rate-limit.md +2 -0
  80. package/rules/00-orchestrator-core.md +3 -0
  81. package/rules/10-agent-routing.md +1 -1
  82. package/rules/20-code-review-loop.md +38 -16
  83. package/rules/30-prompt-templates.md +4 -0
  84. package/rules/35-memory.md +3 -3
  85. package/rules/40-critical-rules.md +3 -0
  86. package/rules/55-coms-protocol.md +84 -18
  87. package/rules/60-task-tracking.md +2 -0
  88. package/settings-keys.json +481 -20
  89. package/templates/coms-feature-manager-prompt.md +4 -6
  90. package/extensions/coms/coms-net-server.ts +0 -1642
  91. package/extensions/coms/coms-net-wrapper.ts +0 -521
  92. package/extensions/coms/coms-net.ts +0 -1589
@@ -0,0 +1,467 @@
1
+ /**
2
+ * task-store.ts — File-backed task store with CRUD, dependency management, and file locking.
3
+ * Copied from @tintinweb/pi-tasks (MIT license).
4
+ *
5
+ * Session-scoped (default): in-memory Map — no disk I/O.
6
+ * Shared (PI_TASK_LIST_ID set): ~/.pi/tasks/<listId>.json with file locking.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, watch, type FSWatcher } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, isAbsolute, join } from "node:path";
11
+ import type { Task, TaskStatus, TaskStoreData } from "./types.js";
12
+
13
+ function sortById(a: Task, b: Task): number {
14
+ return Number(a.id) - Number(b.id);
15
+ }
16
+ function sortByStatus(a: Task, b: Task): number {
17
+ const rank = (s: string) => s === "completed" ? 0 : s === "in_progress" ? 1 : 2;
18
+ return rank(a.status) - rank(b.status) || Number(a.id) - Number(b.id);
19
+ }
20
+ function sortByRecent(a: Task, b: Task): number {
21
+ return b.updatedAt - a.updatedAt || Number(b.id) - Number(a.id);
22
+ }
23
+ function sortByOldest(a: Task, b: Task): number {
24
+ return a.updatedAt - b.updatedAt || Number(a.id) - Number(b.id);
25
+ }
26
+
27
+ const SORT_FNS: Record<string, (a: Task, b: Task) => number> = { id: sortById, status: sortByStatus, recent: sortByRecent, oldest: sortByOldest };
28
+ const TASKS_DIR = join(homedir(), ".pi", "tasks");
29
+ const LOCK_RETRY_MS = 50;
30
+ const LOCK_MAX_RETRIES = 100;
31
+
32
+ function acquireLock(lockPath: string): void {
33
+ mkdirSync(dirname(lockPath), { recursive: true });
34
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
35
+ try {
36
+ writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
37
+ return;
38
+ } catch (e: any) {
39
+ if (e.code === "EEXIST") {
40
+ try {
41
+ const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
42
+ if (pid && !isProcessRunning(pid)) { unlinkSync(lockPath); continue; }
43
+ } catch { /* ignore */ }
44
+ const start = Date.now();
45
+ while (Date.now() - start < LOCK_RETRY_MS) { /* busy wait */ }
46
+ continue;
47
+ }
48
+ throw e;
49
+ }
50
+ }
51
+ throw new Error(`Failed to acquire lock: ${lockPath}`);
52
+ }
53
+
54
+ function releaseLock(lockPath: string): void {
55
+ try { unlinkSync(lockPath); } catch { /* ignore */ }
56
+ }
57
+
58
+ function isProcessRunning(pid: number): boolean {
59
+ try { process.kill(pid, 0); return true; } catch { return false; }
60
+ }
61
+
62
+ function normalizeTask(t: any): Task {
63
+ const now = Date.now();
64
+ return {
65
+ ...t,
66
+ createdBy: t.createdBy && typeof t.createdBy === "object" ? t.createdBy :
67
+ (t.metadata?.coms_origin ? { type: "coms" as const, origin: t.metadata.coms_origin.sender_name || "", session: t.metadata.coms_origin.sender_session || "", project: "" } :
68
+ { type: "local" as const, origin: "system", session: "", project: "" }),
69
+ metadata: (() => {
70
+ const m = t.metadata && typeof t.metadata === "object" && !Array.isArray(t.metadata) ? { ...t.metadata } : {};
71
+ delete m.coms_origin;
72
+ return m;
73
+ })(),
74
+ blocks: Array.isArray(t.blocks) ? t.blocks : [],
75
+ blockedBy: Array.isArray(t.blockedBy) ? t.blockedBy : [],
76
+ createdAt: typeof t.createdAt === "number" ? t.createdAt : now,
77
+ updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : now,
78
+ statusHistory: t.statusHistory && typeof t.statusHistory === "object" ? {
79
+ pending_at: typeof t.statusHistory.pending_at === "number" ? new Date(t.statusHistory.pending_at).toISOString() : (t.statusHistory.pending_at || new Date(typeof t.createdAt === "number" ? t.createdAt : now).toISOString()),
80
+ in_progress_at: typeof t.statusHistory.in_progress_at === "number" ? new Date(t.statusHistory.in_progress_at).toISOString() : (t.statusHistory.in_progress_at || null),
81
+ completed_at: typeof t.statusHistory.completed_at === "number" ? new Date(t.statusHistory.completed_at).toISOString() : (t.statusHistory.completed_at || null),
82
+ deleted_at: typeof t.statusHistory.deleted_at === "number" ? new Date(t.statusHistory.deleted_at).toISOString() : (t.statusHistory.deleted_at || null),
83
+ } : {
84
+ pending_at: new Date(typeof t.createdAt === "number" ? t.createdAt : now).toISOString(),
85
+ in_progress_at: null,
86
+ completed_at: null,
87
+ deleted_at: null,
88
+ },
89
+ };
90
+ }
91
+
92
+ export class TaskStore {
93
+ filePath: string | undefined;
94
+ lockPath: string | undefined;
95
+ private nextId = 1;
96
+ private tasks = new Map<string, Task>();
97
+ private _watcher: FSWatcher | null = null;
98
+ private _selfWrite = false;
99
+ private _onChange: (() => void) | null = null;
100
+
101
+ constructor(listIdOrPath?: string) {
102
+ if (!listIdOrPath) return;
103
+ const isAbsPath = isAbsolute(listIdOrPath);
104
+ const filePath = isAbsPath ? listIdOrPath : join(TASKS_DIR, `${listIdOrPath}.json`);
105
+ this.filePath = filePath;
106
+ this.lockPath = filePath + ".lock";
107
+ this.load();
108
+ // Watch for external file changes (e.g. remote task creation via coms)
109
+ this._startWatcher();
110
+ }
111
+
112
+ private _startWatcher(): void {
113
+ if (!this.filePath) return;
114
+ // Subagent children must not keep watchers — they prevent the child
115
+ // process from exiting, which blocks the parent's proc.on("close").
116
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
117
+ try {
118
+ if (existsSync(this.filePath)) {
119
+ this._watcher = watch(this.filePath, (eventType) => {
120
+ if (eventType === "change" || eventType === "rename") {
121
+ if (this._selfWrite) { this._selfWrite = false; return; }
122
+ try { this.load(); if (this._onChange) this._onChange(); } catch {}
123
+ }
124
+ });
125
+ } else {
126
+ const dir = dirname(this.filePath);
127
+ const filename = this.filePath.split("/").pop() || this.filePath.split("\\").pop() || "";
128
+ try {
129
+ mkdirSync(dir, { recursive: true });
130
+ this._watcher = watch(dir, (eventType, changedFile) => {
131
+ if (changedFile !== filename) return;
132
+ if (this._selfWrite) { this._selfWrite = false; return; }
133
+ try { this.load(); if (this._onChange) this._onChange(); } catch {}
134
+ });
135
+ } catch {}
136
+ }
137
+ } catch {}
138
+ }
139
+
140
+ setOnChange(cb: () => void): void {
141
+ this._onChange = cb;
142
+ }
143
+
144
+ close(): void {
145
+ if (this._watcher) { this._watcher.close(); this._watcher = null; }
146
+ }
147
+
148
+ private load(): void {
149
+ if (!this.filePath) return;
150
+ if (!existsSync(this.filePath)) return;
151
+ try {
152
+ const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
153
+ this.nextId = data.nextId;
154
+ this.tasks.clear();
155
+ for (const t of data.tasks) {
156
+ this.tasks.set(t.id, normalizeTask(t));
157
+ }
158
+ } catch { /* corrupt file — start fresh */ }
159
+ }
160
+
161
+ private save(): void {
162
+ if (!this.filePath) return;
163
+ this._selfWrite = true;
164
+ const data: TaskStoreData = { nextId: this.nextId, tasks: Array.from(this.tasks.values()) };
165
+ mkdirSync(dirname(this.filePath), { recursive: true });
166
+ const tmpPath = this.filePath + ".tmp";
167
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2));
168
+ renameSync(tmpPath, this.filePath);
169
+ }
170
+
171
+ private withLock<T>(fn: () => T): T {
172
+ if (!this.lockPath) return fn();
173
+ acquireLock(this.lockPath);
174
+ try {
175
+ this.load();
176
+ const result = fn();
177
+ this.save();
178
+ return result;
179
+ } finally {
180
+ releaseLock(this.lockPath);
181
+ }
182
+ }
183
+
184
+ create(subject: string, description: string, createdBy: Task["createdBy"], activeForm?: string, metadata?: Record<string, any>): Task {
185
+ return this.withLock(() => {
186
+ const now = Date.now();
187
+ const task: Task = {
188
+ id: String(this.nextId++),
189
+ subject,
190
+ description,
191
+ status: "pending",
192
+ activeForm,
193
+ owner: undefined,
194
+ createdBy,
195
+ metadata: metadata ?? {},
196
+ blocks: [],
197
+ blockedBy: [],
198
+ createdAt: now,
199
+ updatedAt: now,
200
+ statusHistory: {
201
+ pending_at: new Date(now).toISOString(),
202
+ in_progress_at: null,
203
+ completed_at: null,
204
+ deleted_at: null,
205
+ },
206
+ };
207
+ this.tasks.set(task.id, task);
208
+ return task;
209
+ });
210
+ }
211
+
212
+ createTasks(tasks: Array<{ subject: string; description: string; createdBy: Task["createdBy"]; blockedBy?: string[]; activeForm?: string; metadata?: Record<string, any> }>): Task[] {
213
+ return this.withLock(() => {
214
+ const now = Date.now();
215
+ const created: Task[] = [];
216
+ for (const t of tasks) {
217
+ const task: Task = {
218
+ id: String(this.nextId++),
219
+ subject: t.subject,
220
+ description: t.description,
221
+ status: "pending",
222
+ activeForm: t.activeForm,
223
+ owner: undefined,
224
+ createdBy: t.createdBy,
225
+ metadata: t.metadata ?? {},
226
+ blocks: [],
227
+ blockedBy: t.blockedBy ?? [],
228
+ createdAt: now,
229
+ updatedAt: now,
230
+ statusHistory: {
231
+ pending_at: new Date(now).toISOString(),
232
+ in_progress_at: null,
233
+ completed_at: null,
234
+ deleted_at: null,
235
+ },
236
+ };
237
+ this.tasks.set(task.id, task);
238
+ created.push(task);
239
+ }
240
+ return created;
241
+ });
242
+ }
243
+
244
+ get(id: string): Task | undefined {
245
+ if (this.filePath) this.load();
246
+ return this.tasks.get(id);
247
+ }
248
+
249
+ list(sortOrder: "id" | "status" | "recent" | "oldest" = "id"): Task[] {
250
+ if (this.filePath) this.load();
251
+ return Array.from(this.tasks.values()).sort(SORT_FNS[sortOrder]);
252
+ }
253
+
254
+ update(id: string, fields: {
255
+ status?: TaskStatus | "deleted";
256
+ subject?: string;
257
+ description?: string;
258
+ activeForm?: string;
259
+ owner?: string;
260
+ metadata?: Record<string, any>;
261
+ addBlocks?: string[];
262
+ addBlockedBy?: string[];
263
+ }): { task: Task | undefined; changedFields: string[]; warnings: string[] } {
264
+ return this.withLock(() => {
265
+ const task = this.tasks.get(id);
266
+ if (!task) return { task: undefined, changedFields: [], warnings: [] };
267
+ const changedFields: string[] = [];
268
+ const warnings: string[] = [];
269
+
270
+ if (fields.status === "deleted") {
271
+ task.statusHistory.deleted_at = new Date().toISOString();
272
+ this.tasks.delete(id);
273
+ for (const t of this.tasks.values()) {
274
+ t.blocks = t.blocks.filter(bid => bid !== id);
275
+ t.blockedBy = t.blockedBy.filter(bid => bid !== id);
276
+ }
277
+ return { task: undefined, changedFields: ["deleted"], warnings: [] };
278
+ }
279
+
280
+ if (fields.status !== undefined) {
281
+ task.status = fields.status as TaskStatus;
282
+ changedFields.push("status");
283
+ const now = Date.now();
284
+ const isoNow = new Date(now).toISOString();
285
+ if (fields.status === "in_progress") task.statusHistory.in_progress_at = isoNow;
286
+ if (fields.status === "completed") task.statusHistory.completed_at = isoNow;
287
+ if (fields.status === "pending") task.statusHistory.pending_at = isoNow;
288
+ }
289
+ if (fields.subject !== undefined) { task.subject = fields.subject; changedFields.push("subject"); }
290
+ if (fields.description !== undefined) { task.description = fields.description; changedFields.push("description"); }
291
+ if (fields.activeForm !== undefined) { task.activeForm = fields.activeForm; changedFields.push("activeForm"); }
292
+ if (fields.owner !== undefined) { task.owner = fields.owner; changedFields.push("owner"); }
293
+
294
+ if (fields.metadata !== undefined) {
295
+ for (const [key, value] of Object.entries(fields.metadata)) {
296
+ if (value === null) delete task.metadata[key];
297
+ else task.metadata[key] = value;
298
+ }
299
+ changedFields.push("metadata");
300
+ }
301
+
302
+ if (fields.addBlocks?.length) {
303
+ for (const targetId of fields.addBlocks) {
304
+ if (!task.blocks.includes(targetId)) task.blocks.push(targetId);
305
+ const target = this.tasks.get(targetId);
306
+ if (target && !target.blockedBy.includes(id)) { target.blockedBy.push(id); target.updatedAt = Date.now(); }
307
+ if (targetId === id) warnings.push(`#${id} blocks itself`);
308
+ else if (!target) warnings.push(`#${targetId} does not exist`);
309
+ else if (target.blocks.includes(id)) warnings.push(`cycle: #${id} and #${targetId} block each other`);
310
+ }
311
+ changedFields.push("blocks");
312
+ }
313
+
314
+ if (fields.addBlockedBy?.length) {
315
+ for (const targetId of fields.addBlockedBy) {
316
+ if (!task.blockedBy.includes(targetId)) task.blockedBy.push(targetId);
317
+ const target = this.tasks.get(targetId);
318
+ if (target && !target.blocks.includes(id)) { target.blocks.push(id); target.updatedAt = Date.now(); }
319
+ if (targetId === id) warnings.push(`#${id} blocks itself`);
320
+ else if (!target) warnings.push(`#${targetId} does not exist`);
321
+ else if (task.blocks.includes(targetId)) warnings.push(`cycle: #${id} and #${targetId} block each other`);
322
+ }
323
+ changedFields.push("blockedBy");
324
+ }
325
+
326
+ task.updatedAt = Date.now();
327
+ return { task, changedFields, warnings };
328
+ });
329
+ }
330
+
331
+ updateTasks(updates: Array<{ id: string; fields: Record<string, any> }>): Array<{ id: string; success: boolean; changedFields?: string[] }> {
332
+ return this.withLock(() => {
333
+ const results: Array<{ id: string; success: boolean; changedFields?: string[] }> = [];
334
+ for (const { id, fields } of updates) {
335
+ const task = this.tasks.get(id);
336
+ if (!task) { results.push({ id, success: false }); continue; }
337
+ const changedFields: string[] = [];
338
+
339
+ if (fields.status === "deleted") {
340
+ task.statusHistory.deleted_at = new Date().toISOString();
341
+ this.tasks.delete(id);
342
+ for (const t of this.tasks.values()) {
343
+ t.blocks = t.blocks.filter(bid => bid !== id);
344
+ t.blockedBy = t.blockedBy.filter(bid => bid !== id);
345
+ }
346
+ results.push({ id, success: true, changedFields: ["deleted"] });
347
+ continue;
348
+ }
349
+
350
+ if (fields.status !== undefined) {
351
+ task.status = fields.status as TaskStatus;
352
+ changedFields.push("status");
353
+ const isoNow = new Date().toISOString();
354
+ if (fields.status === "in_progress") task.statusHistory.in_progress_at = isoNow;
355
+ if (fields.status === "completed") task.statusHistory.completed_at = isoNow;
356
+ if (fields.status === "pending") task.statusHistory.pending_at = isoNow;
357
+ }
358
+ if (fields.subject !== undefined) { task.subject = fields.subject; changedFields.push("subject"); }
359
+ if (fields.description !== undefined) { task.description = fields.description; changedFields.push("description"); }
360
+ if (fields.activeForm !== undefined) { task.activeForm = fields.activeForm; changedFields.push("activeForm"); }
361
+ if (fields.owner !== undefined) { task.owner = fields.owner; changedFields.push("owner"); }
362
+
363
+ if (fields.metadata !== undefined) {
364
+ for (const [key, value] of Object.entries(fields.metadata)) {
365
+ if (value === null) delete task.metadata[key];
366
+ else task.metadata[key] = value;
367
+ }
368
+ changedFields.push("metadata");
369
+ }
370
+
371
+ if (fields.addBlocks?.length) {
372
+ for (const targetId of fields.addBlocks) {
373
+ if (!task.blocks.includes(targetId)) task.blocks.push(targetId);
374
+ const target = this.tasks.get(targetId);
375
+ if (target && !target.blockedBy.includes(id)) { target.blockedBy.push(id); target.updatedAt = Date.now(); }
376
+ }
377
+ changedFields.push("blocks");
378
+ }
379
+
380
+ if (fields.addBlockedBy?.length) {
381
+ for (const targetId of fields.addBlockedBy) {
382
+ if (!task.blockedBy.includes(targetId)) task.blockedBy.push(targetId);
383
+ const target = this.tasks.get(targetId);
384
+ if (target && !target.blocks.includes(id)) { target.blocks.push(id); target.updatedAt = Date.now(); }
385
+ }
386
+ changedFields.push("blockedBy");
387
+ }
388
+
389
+ task.updatedAt = Date.now();
390
+ results.push({ id, success: true, changedFields });
391
+ }
392
+ return results;
393
+ });
394
+ }
395
+
396
+ delete(id: string): boolean {
397
+ return this.withLock(() => {
398
+ if (!this.tasks.has(id)) return false;
399
+ this.tasks.delete(id);
400
+ for (const t of this.tasks.values()) {
401
+ t.blocks = t.blocks.filter(bid => bid !== id);
402
+ t.blockedBy = t.blockedBy.filter(bid => bid !== id);
403
+ }
404
+ return true;
405
+ });
406
+ }
407
+
408
+ deleteTasks(ids: string[]): number {
409
+ return this.withLock(() => {
410
+ let count = 0;
411
+ for (const id of ids) {
412
+ if (this.tasks.has(id)) {
413
+ this.tasks.delete(id);
414
+ count++;
415
+ }
416
+ }
417
+ if (count > 0) {
418
+ for (const t of this.tasks.values()) {
419
+ t.blocks = t.blocks.filter(bid => !ids.includes(bid));
420
+ t.blockedBy = t.blockedBy.filter(bid => !ids.includes(bid));
421
+ }
422
+ }
423
+ return count;
424
+ });
425
+ }
426
+
427
+ clearAll(): number {
428
+ return this.withLock(() => { const count = this.tasks.size; this.tasks.clear(); return count; });
429
+ }
430
+
431
+ snapshot(): TaskStoreData {
432
+ if (this.filePath) this.load();
433
+ return { nextId: this.nextId, tasks: Array.from(this.tasks.values()) };
434
+ }
435
+
436
+ seed(data: TaskStoreData): void {
437
+ if (this.tasks.size > 0) return;
438
+ this.withLock(() => {
439
+ this.nextId = data.nextId;
440
+ this.tasks.clear();
441
+ for (const t of data.tasks) this.tasks.set(t.id, t);
442
+ });
443
+ }
444
+
445
+ deleteFileIfEmpty(): boolean {
446
+ if (!this.filePath || this.tasks.size > 0) return false;
447
+ try { unlinkSync(this.filePath); } catch { /* ignore */ }
448
+ return true;
449
+ }
450
+
451
+ clearCompleted(): number {
452
+ return this.withLock(() => {
453
+ let count = 0;
454
+ for (const [id, task] of this.tasks) {
455
+ if (task.status === "completed") { this.tasks.delete(id); count++; }
456
+ }
457
+ if (count > 0) {
458
+ const validIds = new Set(this.tasks.keys());
459
+ for (const t of this.tasks.values()) {
460
+ t.blocks = t.blocks.filter(bid => validIds.has(bid));
461
+ t.blockedBy = t.blockedBy.filter(bid => validIds.has(bid));
462
+ }
463
+ }
464
+ return count;
465
+ });
466
+ }
467
+ }