pi-quests 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,53 @@
1
1
  import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { DEFAULT_CONFIG, type ResolvedConfig } from "../config.js";
2
3
  import { logger } from "../logger.js";
3
4
  import {
5
+ formatAddResult,
4
6
  formatBatchAddResult,
5
- formatClearResult,
7
+ formatBlockedBySubQuests,
6
8
  formatDeleteResult,
7
9
  formatNotFound,
8
10
  formatQuestList,
11
+ formatSubQuestCannotHaveSubQuests,
9
12
  formatToggleResult,
10
13
  formatUpdateResult,
11
14
  } from "./formatters.js";
12
- import { QUEST_ACTIONS, type Quest } from "./types.js";
15
+ import { QUEST_ACTIONS, type Quest, type SubQuest } from "./types.js";
13
16
 
14
17
  export type HistoryEntry =
15
- | { type: typeof QUEST_ACTIONS.add; id: number }
16
- | { type: typeof QUEST_ACTIONS.toggle; id: number }
17
- | { type: typeof QUEST_ACTIONS.update; id: number; previousDescription: string }
18
- | { type: typeof QUEST_ACTIONS.delete; quest: Quest; index: number }
19
- | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; nextId: number };
18
+ | { type: typeof QUEST_ACTIONS.add; id: string; parentId?: string }
19
+ | { type: typeof QUEST_ACTIONS.toggle; id: string }
20
+ | { type: typeof QUEST_ACTIONS.update; id: string; previousDescription: string }
21
+ | {
22
+ type: typeof QUEST_ACTIONS.delete;
23
+ quest: Quest | SubQuest;
24
+ index: number;
25
+ isSubQuest?: boolean;
26
+ cascadeDeletedSubs?: SubQuest[];
27
+ }
28
+ | {
29
+ type: typeof QUEST_ACTIONS.clear;
30
+ previousQuests: Quest[];
31
+ previousSubQuests?: SubQuest[];
32
+ all: false;
33
+ }
34
+ | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; subQuests?: SubQuest[]; all: true }
35
+ | {
36
+ type: typeof QUEST_ACTIONS.reorder;
37
+ quest: Quest;
38
+ oldIndex: number;
39
+ previousIds: string[];
40
+ targetId: string;
41
+ };
20
42
 
21
43
  export type QuestAction =
22
- | { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
44
+ | { type: typeof QUEST_ACTIONS.add; descriptions?: string[]; parentId?: string }
23
45
  | { type: typeof QUEST_ACTIONS.list }
24
- | { type: typeof QUEST_ACTIONS.toggle; id?: number }
25
- | { type: typeof QUEST_ACTIONS.update; id?: number; description?: string }
26
- | { type: typeof QUEST_ACTIONS.delete; id?: number }
27
- | { type: typeof QUEST_ACTIONS.clear }
46
+ | { type: typeof QUEST_ACTIONS.toggle; id?: string }
47
+ | { type: typeof QUEST_ACTIONS.update; id?: string; description?: string }
48
+ | { type: typeof QUEST_ACTIONS.delete; id?: string }
49
+ | { type: typeof QUEST_ACTIONS.clear; all?: boolean }
50
+ | { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
28
51
  | { type: typeof QUEST_ACTIONS.revert };
29
52
 
30
53
  export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
@@ -37,8 +60,35 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
37
60
  */
38
61
  export class QuestLog {
39
62
  private quests: Quest[] = [];
40
- private nextId = 1;
63
+ private subQuests: SubQuest[] = [];
64
+ private usedIds: Set<string> = new Set();
41
65
  private history: HistoryEntry[] = [];
66
+
67
+ private readonly ID_LENGTH: number;
68
+ private readonly MAX_IDS: number;
69
+
70
+ constructor(config: ResolvedConfig = DEFAULT_CONFIG) {
71
+ this.ID_LENGTH = config.ids.length;
72
+ this.MAX_IDS = 16 ** this.ID_LENGTH;
73
+ }
74
+
75
+ private generateId(): string {
76
+ if (this.usedIds.size >= this.MAX_IDS) {
77
+ logger.debug("quests:state", "generate-id-exhausted", { usedIds: this.usedIds.size });
78
+ throw new Error("No available quest IDs. Clear done or all quests to free up IDs.");
79
+ }
80
+
81
+ let id: string;
82
+ do {
83
+ id = Math.floor(Math.random() * this.MAX_IDS)
84
+ .toString(16)
85
+ .padStart(this.ID_LENGTH, "0")
86
+ .toLowerCase();
87
+ } while (this.usedIds.has(id));
88
+
89
+ this.usedIds.add(id);
90
+ return id;
91
+ }
42
92
  private undoHandlers: {
43
93
  [K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
44
94
  success: boolean;
@@ -47,68 +97,138 @@ export class QuestLog {
47
97
  } = {
48
98
  [QUEST_ACTIONS.add]: (entry) => {
49
99
  this.quests = this.quests.filter((q) => q.id !== entry.id);
100
+ this.subQuests = this.subQuests.filter((q) => q.id !== entry.id);
101
+ this.usedIds.delete(entry.id);
50
102
 
51
- const maxId = this.quests.reduce((max, q) => Math.max(max, q.id), 0);
52
- this.nextId = Math.max(maxId + 1, entry.id);
53
-
54
- logger.debug("quests:state", "revert-add", { id: entry.id, total: this.quests.length });
55
- return { success: true, message: `Reverted add quest #${entry.id}` };
103
+ logger.debug("quests:state", "revert-add", {
104
+ id: entry.id,
105
+ total: this.quests.length + this.subQuests.length,
106
+ });
107
+ return { success: true, message: `Reverted add quest [${entry.id}]` };
56
108
  },
57
109
  [QUEST_ACTIONS.toggle]: (entry) => {
58
- const quest = this.quests.find((q) => q.id === entry.id);
110
+ const quest = this.findById(entry.id);
59
111
 
60
112
  if (quest) {
61
113
  quest.done = !quest.done;
62
114
  logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
63
- return { success: true, message: `Reverted toggle for quest #${entry.id}` };
115
+ return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
64
116
  }
65
117
 
66
118
  logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
67
- return { success: false, message: `Quest #${entry.id} not found` };
119
+ return { success: false, message: `Quest [${entry.id}] not found` };
68
120
  },
69
121
  [QUEST_ACTIONS.update]: (entry) => {
70
- const quest = this.quests.find((q) => q.id === entry.id);
122
+ const quest = this.findById(entry.id);
71
123
  if (quest) {
72
124
  quest.description = entry.previousDescription;
73
125
  logger.debug("quests:state", "revert-update", { id: entry.id });
74
- return { success: true, message: `Reverted update for quest #${entry.id}` };
126
+ return { success: true, message: `Reverted update for quest [${entry.id}]` };
75
127
  }
76
128
 
77
129
  logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
78
- return { success: false, message: `Quest #${entry.id} not found` };
130
+ return { success: false, message: `Quest [${entry.id}] not found` };
79
131
  },
80
132
  [QUEST_ACTIONS.delete]: (entry) => {
81
- this.quests.splice(entry.index, 0, entry.quest);
133
+ if (entry.isSubQuest) {
134
+ this.subQuests.splice(entry.index, 0, entry.quest as unknown as SubQuest);
135
+ } else {
136
+ this.quests.splice(entry.index, 0, entry.quest);
137
+ if (entry.cascadeDeletedSubs) {
138
+ for (const sub of entry.cascadeDeletedSubs) {
139
+ this.subQuests.push(sub);
140
+ this.usedIds.add(sub.id);
141
+ }
142
+ }
143
+ }
144
+ this.usedIds.add(entry.quest.id);
82
145
 
83
146
  logger.debug("quests:state", "revert-delete", {
84
147
  id: entry.quest.id,
85
148
  index: entry.index,
86
- total: this.quests.length,
149
+ total: this.quests.length + this.subQuests.length,
87
150
  });
88
- return { success: true, message: `Reverted delete for quest #${entry.quest.id}` };
151
+ return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
89
152
  },
90
153
  [QUEST_ACTIONS.clear]: (entry) => {
154
+ if ("previousQuests" in entry) {
155
+ const restoredCount =
156
+ entry.previousQuests.length +
157
+ (entry.previousSubQuests?.length ?? 0) -
158
+ (this.quests.length + this.subQuests.length);
159
+ this.quests = [...entry.previousQuests];
160
+ this.subQuests = [...(entry.previousSubQuests ?? [])];
161
+ this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
162
+
163
+ return {
164
+ success: true,
165
+ message: `Reverted clear (${restoredCount} quests restored)`,
166
+ };
167
+ }
168
+
91
169
  this.quests = [...entry.quests];
92
- this.nextId = entry.nextId;
170
+ this.subQuests = [...(entry.subQuests ?? [])];
171
+ this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
93
172
 
94
- logger.debug("quests:state", "revert-clear", { count: entry.quests.length });
95
173
  return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
96
174
  },
175
+ [QUEST_ACTIONS.reorder]: (entry) => {
176
+ const currentIndex = this.quests.indexOf(entry.quest);
177
+ if (currentIndex === -1) return { success: false, message: "Reordered quest not found" };
178
+
179
+ this.quests.splice(currentIndex, 1);
180
+ this.quests.splice(entry.oldIndex, 0, entry.quest);
181
+ for (let i = 0; i < this.quests.length; i++) {
182
+ this.quests[i].id = entry.previousIds[i];
183
+ }
184
+
185
+ return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
186
+ },
97
187
  };
98
188
 
189
+ /*
190
+ * Returns all quests including sub-quests, inserted in order
191
+ * sub-quests are returned immediately after their parent quest
192
+ */
99
193
  getAll(): Quest[] {
194
+ const result: Quest[] = [];
195
+ for (const q of this.quests) {
196
+ result.push(q);
197
+ result.push(...this.subQuests.filter((sq) => sq.parentId === q.id));
198
+ }
199
+ return result;
200
+ }
201
+
202
+ /*
203
+ * Returns only top-level quests. Sub-quests should be accessed via `getSubQuests()`.
204
+ */
205
+ getQuests(): Quest[] {
100
206
  return [...this.quests];
101
207
  }
102
208
 
103
- getNextId(): number {
104
- return this.nextId;
209
+ /*
210
+ * Returns sub-quests for a given parent quest ID. Sub-quests are not included in `getQuests()`.
211
+ */
212
+ getSubQuests(parentId: string): SubQuest[] {
213
+ return this.subQuests.filter((sq) => sq.parentId === parentId);
214
+ }
215
+
216
+ getParentQuest(subQuest: SubQuest): Quest | undefined {
217
+ return this.quests.find((q) => q.id === subQuest.parentId);
105
218
  }
106
219
 
107
- add(description: string, additionalContext?: string): Quest {
220
+ private findById(id: string): Quest | SubQuest | undefined {
221
+ return this.quests.find((q) => q.id === id) ?? this.subQuests.find((sq) => sq.id === id);
222
+ }
223
+
224
+ getUsedIds(): string[] {
225
+ return Array.from(this.usedIds);
226
+ }
227
+
228
+ add(description: string): Quest {
108
229
  const quest: Quest = {
109
- id: this.nextId++,
230
+ id: this.generateId(),
110
231
  description,
111
- additionalContext,
112
232
  done: false,
113
233
  createdAt: Date.now(),
114
234
  };
@@ -124,26 +244,63 @@ export class QuestLog {
124
244
  return quest;
125
245
  }
126
246
 
127
- toggle(id: number): Quest | undefined {
128
- const quest = this.quests.find((q) => q.id === id);
247
+ addSubQuest(description: string, parentId: string): SubQuest {
248
+ if (this.subQuests.some((sq) => sq.id === parentId)) {
249
+ throw new Error(formatSubQuestCannotHaveSubQuests(parentId));
250
+ }
251
+ const parent = this.quests.find((q) => q.id === parentId);
252
+ if (!parent) {
253
+ throw new Error(`Parent quest [${parentId}] not found`);
254
+ }
255
+ if (parent.done) {
256
+ throw new Error(`Cannot add sub-quest to completed parent quest [${parentId}]`);
257
+ }
258
+ const subQuest: SubQuest = {
259
+ id: this.generateId(),
260
+ description,
261
+ done: false,
262
+ createdAt: Date.now(),
263
+ parentId,
264
+ };
265
+ this.subQuests.push(subQuest);
266
+ this.history.push({ type: QUEST_ACTIONS.add, id: subQuest.id, parentId });
267
+ logger.debug("quests:state", "add-subquest", {
268
+ id: subQuest.id,
269
+ parentId,
270
+ description,
271
+ totalSubQuests: this.subQuests.length,
272
+ });
273
+ return subQuest;
274
+ }
275
+
276
+ toggle(id: string): Quest | SubQuest | null | undefined {
277
+ const quest = this.findById(id);
129
278
  if (!quest) {
130
279
  logger.debug("quests:state", "toggle-not-found", { id });
131
280
  return undefined;
132
281
  }
133
282
 
283
+ if (!quest.done && !("parentId" in quest)) {
284
+ const subs = this.getSubQuests(id);
285
+ if (subs.some((q) => !q.done)) {
286
+ logger.debug("quests:state", "toggle-blocked-subquests", { id });
287
+ return null;
288
+ }
289
+ }
290
+
134
291
  quest.done = !quest.done;
135
292
  this.history.push({ type: QUEST_ACTIONS.toggle, id });
136
293
 
137
294
  logger.debug("quests:state", QUEST_ACTIONS.toggle, {
138
295
  id,
139
296
  done: quest.done,
140
- total: this.quests.length,
297
+ total: this.quests.length + this.subQuests.length,
141
298
  });
142
299
  return quest;
143
300
  }
144
301
 
145
- update(id: number, description: string): Quest | undefined {
146
- const quest = this.quests.find((q) => q.id === id);
302
+ update(id: string, description: string): Quest | SubQuest | undefined {
303
+ const quest = this.findById(id);
147
304
  if (!quest) {
148
305
  logger.debug("quests:state", "update-not-found", { id });
149
306
  return undefined;
@@ -155,33 +312,127 @@ export class QuestLog {
155
312
  logger.debug("quests:state", QUEST_ACTIONS.update, {
156
313
  id,
157
314
  description,
158
- total: this.quests.length,
315
+ total: this.quests.length + this.subQuests.length,
159
316
  });
160
317
  return quest;
161
318
  }
162
319
 
163
- delete(id: number): Quest | undefined {
320
+ delete(id: string): Quest | SubQuest | null | undefined {
164
321
  const index = this.quests.findIndex((q) => q.id === id);
165
- if (index === -1) {
166
- logger.debug("quests:state", "delete-not-found", { id });
322
+ if (index !== -1) {
323
+ const subs = this.getSubQuests(id);
324
+ if (subs.some((q) => !q.done)) {
325
+ logger.debug("quests:state", "delete-blocked-subquests", { id });
326
+ return null;
327
+ }
328
+ const [quest] = this.quests.splice(index, 1);
329
+ this.usedIds.delete(quest.id);
330
+ const cascadeDeletedSubs = this.subQuests.filter((sq) => sq.parentId === id);
331
+ this.history.push({ type: QUEST_ACTIONS.delete, quest, index, cascadeDeletedSubs });
332
+ this.subQuests = this.subQuests.filter((sq) => {
333
+ if (sq.parentId === id) {
334
+ this.usedIds.delete(sq.id);
335
+ return false;
336
+ }
337
+ return true;
338
+ });
339
+ logger.debug("quests:state", QUEST_ACTIONS.delete, {
340
+ id,
341
+ index,
342
+ total: this.quests.length + this.subQuests.length,
343
+ });
344
+ return quest;
345
+ }
346
+ const subIndex = this.subQuests.findIndex((sq) => sq.id === id);
347
+ if (subIndex !== -1) {
348
+ const [quest] = this.subQuests.splice(subIndex, 1);
349
+ this.usedIds.delete(quest.id);
350
+ this.history.push({ type: QUEST_ACTIONS.delete, quest, index: subIndex, isSubQuest: true });
351
+ logger.debug("quests:state", QUEST_ACTIONS.delete, {
352
+ id,
353
+ index: subIndex,
354
+ total: this.quests.length + this.subQuests.length,
355
+ });
356
+ return quest;
357
+ }
358
+ logger.debug("quests:state", "delete-not-found", { id });
359
+ return undefined;
360
+ }
361
+
362
+ clear(all = false): number {
363
+ if (!all) {
364
+ const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
365
+ const done = this.quests.filter((q) => q.done);
366
+ const removedSubs = this.subQuests.filter((sq) => sq.done || doneParentIds.has(sq.parentId));
367
+ const keptSubs = this.subQuests.filter((sq) => !sq.done && !doneParentIds.has(sq.parentId));
368
+ const previousQuests = [...this.quests];
369
+ const previousSubQuests = [...this.subQuests];
370
+ for (const q of done) this.usedIds.delete(q.id);
371
+ for (const q of removedSubs) this.usedIds.delete(q.id);
372
+ this.quests = this.quests.filter((q) => !q.done);
373
+ this.subQuests = keptSubs;
374
+ this.history.push({
375
+ type: QUEST_ACTIONS.clear,
376
+ previousQuests,
377
+ previousSubQuests,
378
+ all: false,
379
+ });
380
+ const count = done.length + removedSubs.length;
381
+ logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
382
+ return count;
383
+ }
384
+ const count = this.quests.length + this.subQuests.length;
385
+ this.history.push({
386
+ type: QUEST_ACTIONS.clear,
387
+ quests: [...this.quests],
388
+ subQuests: [...this.subQuests],
389
+ all: true,
390
+ });
391
+ this.quests = [];
392
+ this.subQuests = [];
393
+ this.usedIds.clear();
394
+ logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
395
+ return count;
396
+ }
397
+
398
+ reorder(id: string, targetId: string): Quest | undefined {
399
+ if (this.subQuests.some((sq) => sq.id === id || sq.id === targetId)) {
400
+ logger.debug("quests:state", "reorder-subquest-rejected", { id, targetId });
401
+ return undefined;
402
+ }
403
+ const idx = this.quests.findIndex((q) => q.id === id);
404
+ if (idx === -1) {
405
+ logger.debug("quests:state", "reorder-not-found", { id });
167
406
  return undefined;
168
407
  }
169
408
 
170
- const [quest] = this.quests.splice(index, 1);
171
- this.history.push({ type: QUEST_ACTIONS.delete, quest, index });
409
+ const targetIdx = this.quests.findIndex((q) => q.id === targetId);
410
+ if (targetIdx === -1) {
411
+ logger.debug("quests:state", "reorder-target-not-found", { id, targetId });
412
+ return undefined;
413
+ }
172
414
 
173
- logger.debug("quests:state", QUEST_ACTIONS.delete, { id, index, total: this.quests.length });
174
- return quest;
175
- }
415
+ if (idx === targetIdx) {
416
+ return this.quests[idx];
417
+ }
176
418
 
177
- clear(): number {
178
- const count = this.quests.length;
179
- this.history.push({ type: QUEST_ACTIONS.clear, quests: [...this.quests], nextId: this.nextId });
180
- this.quests = [];
181
- this.nextId = 1;
419
+ const previousIds = this.quests.map((q) => q.id);
420
+ const [quest] = this.quests.splice(idx, 1);
182
421
 
183
- logger.debug("quests:state", QUEST_ACTIONS.clear, { count });
184
- return count;
422
+ let insertIndex = targetIdx;
423
+ if (idx < targetIdx) {
424
+ insertIndex = targetIdx - 1;
425
+ }
426
+
427
+ this.quests.splice(insertIndex, 0, quest);
428
+ this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds, targetId });
429
+ logger.debug("quests:state", QUEST_ACTIONS.reorder, {
430
+ id: quest.id,
431
+ targetId,
432
+ total: this.quests.length + this.subQuests.length,
433
+ });
434
+
435
+ return quest;
185
436
  }
186
437
 
187
438
  revert(): QuestOperationResult {
@@ -216,13 +467,53 @@ export class QuestLog {
216
467
  message: "Error: all descriptions in a batch must be non-empty",
217
468
  };
218
469
  }
219
- const added: { id: number; description: string }[] = [];
220
- for (const desc of action.descriptions) {
221
- const q = this.add(desc);
222
- added.push({ id: q.id, description: q.description });
470
+ if (action.parentId) {
471
+ if (this.subQuests.some((sq) => sq.id === action.parentId)) {
472
+ return {
473
+ success: false,
474
+ message: formatSubQuestCannotHaveSubQuests(action.parentId),
475
+ };
476
+ }
477
+ const parent = this.quests.find((q) => q.id === action.parentId);
478
+ if (!parent) {
479
+ return { success: false, message: formatNotFound(action.parentId) };
480
+ }
481
+ if (parent.done) {
482
+ return {
483
+ success: false,
484
+ message: `Error: cannot add sub-quest to completed parent quest [${action.parentId}]`,
485
+ };
486
+ }
487
+ }
488
+ if (action.descriptions.length === 1) {
489
+ try {
490
+ const [firstDesc] = action.descriptions;
491
+ const q = action.parentId
492
+ ? this.addSubQuest(firstDesc, action.parentId)
493
+ : this.add(firstDesc);
494
+
495
+ return { success: true, message: formatAddResult(q) };
496
+ } catch (err) {
497
+ return {
498
+ success: false,
499
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
500
+ };
501
+ }
502
+ }
503
+ try {
504
+ const added: { id: string; description: string }[] = [];
505
+ for (const desc of action.descriptions) {
506
+ const q = action.parentId ? this.addSubQuest(desc, action.parentId) : this.add(desc);
507
+ added.push({ id: q.id, description: q.description });
508
+ }
509
+
510
+ return { success: true, message: formatBatchAddResult(added) };
511
+ } catch (err) {
512
+ return {
513
+ success: false,
514
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
515
+ };
223
516
  }
224
-
225
- return { success: true, message: formatBatchAddResult(added) };
226
517
  }
227
518
  return {
228
519
  success: false,
@@ -239,9 +530,12 @@ export class QuestLog {
239
530
  }
240
531
 
241
532
  const q = this.toggle(action.id);
242
- if (!q) {
533
+ if (q === undefined) {
243
534
  return { success: false, message: formatNotFound(action.id) };
244
535
  }
536
+ if (q === null) {
537
+ return { success: false, message: formatBlockedBySubQuests(action.id) };
538
+ }
245
539
 
246
540
  return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
247
541
  }
@@ -250,7 +544,7 @@ export class QuestLog {
250
544
  return { success: false, message: "Error: id is required for update action" };
251
545
  }
252
546
 
253
- if (!action.description) {
547
+ if (!action.description || action.description.trim().length === 0) {
254
548
  return { success: false, message: "Error: description is required for update action" };
255
549
  }
256
550
 
@@ -267,15 +561,34 @@ export class QuestLog {
267
561
  }
268
562
 
269
563
  const q = this.delete(action.id);
270
- if (!q) {
564
+ if (q === undefined) {
271
565
  return { success: false, message: formatNotFound(action.id) };
272
566
  }
567
+ if (q === null) {
568
+ return { success: false, message: formatBlockedBySubQuests(action.id) };
569
+ }
273
570
 
274
571
  return { success: true, message: formatDeleteResult(q), quest: q };
275
572
  }
276
573
  case QUEST_ACTIONS.clear: {
277
- const count = this.clear();
278
- return { success: true, message: formatClearResult(count) };
574
+ const count = this.clear(action.all);
575
+ const message = action.all
576
+ ? `Cleared ${count} quests`
577
+ : `Cleared ${count} completed quests`;
578
+
579
+ return { success: true, message };
580
+ }
581
+ case QUEST_ACTIONS.reorder: {
582
+ if (action.id === undefined)
583
+ return { success: false, message: "Error: id is required for reorder action" };
584
+
585
+ if (action.targetId === undefined)
586
+ return { success: false, message: "Error: targetId is required for reorder action" };
587
+
588
+ const q = this.reorder(action.id, action.targetId);
589
+ if (!q) return { success: false, message: "Quest not found or is a sub-quest" };
590
+
591
+ return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
279
592
  }
280
593
  case QUEST_ACTIONS.revert: {
281
594
  return this.revert();
@@ -305,15 +618,23 @@ export class QuestLog {
305
618
 
306
619
  if (lastState) {
307
620
  const quests = lastState.quests;
308
- const nextId = lastState.nextId;
309
621
  const questCount = Array.isArray(quests) ? quests.length : 0;
310
622
 
311
- this.quests = Array.isArray(quests) ? [...(quests as Quest[])] : [];
312
- this.nextId = typeof nextId === "number" ? nextId : 1;
313
- logger.debug("quests:state", "reconstruct", { toolResults, questCount, nextId: this.nextId });
623
+ if (Array.isArray(quests)) {
624
+ this.quests = (quests as Quest[]).filter((q) => !("parentId" in q && q.parentId));
625
+ this.subQuests = (quests as Quest[]).filter(
626
+ (q) => "parentId" in q && q.parentId,
627
+ ) as unknown as SubQuest[];
628
+ } else {
629
+ this.quests = [];
630
+ this.subQuests = [];
631
+ }
632
+ this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
633
+ logger.debug("quests:state", "reconstruct", { toolResults, questCount });
314
634
  } else {
315
635
  this.quests = [];
316
- this.nextId = 1;
636
+ this.subQuests = [];
637
+ this.usedIds.clear();
317
638
  logger.debug("quests:state", "reconstruct-empty", { toolResults });
318
639
  }
319
640
 
@@ -330,7 +651,7 @@ export function makeToolResult(
330
651
  content: [{ type: "text", text }],
331
652
  details: {
332
653
  quests: questLog.getAll(),
333
- nextId: questLog.getNextId(),
654
+ usedIds: questLog.getUsedIds(),
334
655
  displayQuests,
335
656
  },
336
657
  };