pi-quests 0.3.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,37 +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,
7
+ formatBlockedBySubQuests,
5
8
  formatDeleteResult,
6
9
  formatNotFound,
7
10
  formatQuestList,
11
+ formatSubQuestCannotHaveSubQuests,
8
12
  formatToggleResult,
9
13
  formatUpdateResult,
10
14
  } from "./formatters.js";
11
- import { QUEST_ACTIONS, type Quest } from "./types.js";
15
+ import { QUEST_ACTIONS, type Quest, type SubQuest } from "./types.js";
12
16
 
13
17
  export type HistoryEntry =
14
- | { type: typeof QUEST_ACTIONS.add; id: number }
15
- | { type: typeof QUEST_ACTIONS.toggle; id: number }
16
- | { type: typeof QUEST_ACTIONS.update; id: number; previousDescription: string }
17
- | { type: typeof QUEST_ACTIONS.delete; quest: Quest; index: 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
+ }
18
28
  | {
19
29
  type: typeof QUEST_ACTIONS.clear;
20
30
  previousQuests: Quest[];
21
- previousNextId: number;
31
+ previousSubQuests?: SubQuest[];
22
32
  all: false;
23
33
  }
24
- | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; nextId: number; all: true }
25
- | { type: typeof QUEST_ACTIONS.reorder; quest: Quest; oldIndex: number; previousIds: number[] };
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
+ };
26
42
 
27
43
  export type QuestAction =
28
- | { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
44
+ | { type: typeof QUEST_ACTIONS.add; descriptions?: string[]; parentId?: string }
29
45
  | { type: typeof QUEST_ACTIONS.list }
30
- | { type: typeof QUEST_ACTIONS.toggle; id?: number }
31
- | { type: typeof QUEST_ACTIONS.update; id?: number; description?: string }
32
- | { type: typeof QUEST_ACTIONS.delete; id?: number }
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 }
33
49
  | { type: typeof QUEST_ACTIONS.clear; all?: boolean }
34
- | { type: typeof QUEST_ACTIONS.reorder; id?: number; targetIndex?: number }
50
+ | { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
35
51
  | { type: typeof QUEST_ACTIONS.revert };
36
52
 
37
53
  export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
@@ -44,8 +60,35 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
44
60
  */
45
61
  export class QuestLog {
46
62
  private quests: Quest[] = [];
47
- private nextId = 1;
63
+ private subQuests: SubQuest[] = [];
64
+ private usedIds: Set<string> = new Set();
48
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
+ }
49
92
  private undoHandlers: {
50
93
  [K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
51
94
  success: boolean;
@@ -54,51 +97,68 @@ export class QuestLog {
54
97
  } = {
55
98
  [QUEST_ACTIONS.add]: (entry) => {
56
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);
57
102
 
58
- const maxId = this.quests.reduce((max, q) => Math.max(max, q.id), 0);
59
- this.nextId = Math.max(maxId + 1, entry.id);
60
-
61
- logger.debug("quests:state", "revert-add", { id: entry.id, total: this.quests.length });
62
- 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}]` };
63
108
  },
64
109
  [QUEST_ACTIONS.toggle]: (entry) => {
65
- const quest = this.quests.find((q) => q.id === entry.id);
110
+ const quest = this.findById(entry.id);
66
111
 
67
112
  if (quest) {
68
113
  quest.done = !quest.done;
69
114
  logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
70
- return { success: true, message: `Reverted toggle for quest #${entry.id}` };
115
+ return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
71
116
  }
72
117
 
73
118
  logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
74
- return { success: false, message: `Quest #${entry.id} not found` };
119
+ return { success: false, message: `Quest [${entry.id}] not found` };
75
120
  },
76
121
  [QUEST_ACTIONS.update]: (entry) => {
77
- const quest = this.quests.find((q) => q.id === entry.id);
122
+ const quest = this.findById(entry.id);
78
123
  if (quest) {
79
124
  quest.description = entry.previousDescription;
80
125
  logger.debug("quests:state", "revert-update", { id: entry.id });
81
- return { success: true, message: `Reverted update for quest #${entry.id}` };
126
+ return { success: true, message: `Reverted update for quest [${entry.id}]` };
82
127
  }
83
128
 
84
129
  logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
85
- return { success: false, message: `Quest #${entry.id} not found` };
130
+ return { success: false, message: `Quest [${entry.id}] not found` };
86
131
  },
87
132
  [QUEST_ACTIONS.delete]: (entry) => {
88
- 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);
89
145
 
90
146
  logger.debug("quests:state", "revert-delete", {
91
147
  id: entry.quest.id,
92
148
  index: entry.index,
93
- total: this.quests.length,
149
+ total: this.quests.length + this.subQuests.length,
94
150
  });
95
- return { success: true, message: `Reverted delete for quest #${entry.quest.id}` };
151
+ return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
96
152
  },
97
153
  [QUEST_ACTIONS.clear]: (entry) => {
98
154
  if ("previousQuests" in entry) {
99
- const restoredCount = entry.previousQuests.length - this.quests.length;
155
+ const restoredCount =
156
+ entry.previousQuests.length +
157
+ (entry.previousSubQuests?.length ?? 0) -
158
+ (this.quests.length + this.subQuests.length);
100
159
  this.quests = [...entry.previousQuests];
101
- this.nextId = entry.previousNextId;
160
+ this.subQuests = [...(entry.previousSubQuests ?? [])];
161
+ this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
102
162
 
103
163
  return {
104
164
  success: true,
@@ -107,7 +167,8 @@ export class QuestLog {
107
167
  }
108
168
 
109
169
  this.quests = [...entry.quests];
110
- this.nextId = entry.nextId;
170
+ this.subQuests = [...(entry.subQuests ?? [])];
171
+ this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
111
172
 
112
173
  return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
113
174
  },
@@ -121,24 +182,53 @@ export class QuestLog {
121
182
  this.quests[i].id = entry.previousIds[i];
122
183
  }
123
184
 
124
- this.nextId = Math.max(...entry.previousIds, 0) + 1;
125
- return { success: true, message: `Reverted reorder for quest #${entry.quest.id}` };
185
+ return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
126
186
  },
127
187
  };
128
188
 
189
+ /*
190
+ * Returns all quests including sub-quests, inserted in order
191
+ * sub-quests are returned immediately after their parent quest
192
+ */
129
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[] {
130
206
  return [...this.quests];
131
207
  }
132
208
 
133
- getNextId(): number {
134
- 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);
135
218
  }
136
219
 
137
- 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 {
138
229
  const quest: Quest = {
139
- id: this.nextId++,
230
+ id: this.generateId(),
140
231
  description,
141
- additionalContext,
142
232
  done: false,
143
233
  createdAt: Date.now(),
144
234
  };
@@ -154,26 +244,63 @@ export class QuestLog {
154
244
  return quest;
155
245
  }
156
246
 
157
- toggle(id: number): Quest | undefined {
158
- 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);
159
278
  if (!quest) {
160
279
  logger.debug("quests:state", "toggle-not-found", { id });
161
280
  return undefined;
162
281
  }
163
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
+
164
291
  quest.done = !quest.done;
165
292
  this.history.push({ type: QUEST_ACTIONS.toggle, id });
166
293
 
167
294
  logger.debug("quests:state", QUEST_ACTIONS.toggle, {
168
295
  id,
169
296
  done: quest.done,
170
- total: this.quests.length,
297
+ total: this.quests.length + this.subQuests.length,
171
298
  });
172
299
  return quest;
173
300
  }
174
301
 
175
- update(id: number, description: string): Quest | undefined {
176
- const quest = this.quests.find((q) => q.id === id);
302
+ update(id: string, description: string): Quest | SubQuest | undefined {
303
+ const quest = this.findById(id);
177
304
  if (!quest) {
178
305
  logger.debug("quests:state", "update-not-found", { id });
179
306
  return undefined;
@@ -185,77 +312,124 @@ export class QuestLog {
185
312
  logger.debug("quests:state", QUEST_ACTIONS.update, {
186
313
  id,
187
314
  description,
188
- total: this.quests.length,
315
+ total: this.quests.length + this.subQuests.length,
189
316
  });
190
317
  return quest;
191
318
  }
192
319
 
193
- delete(id: number): Quest | undefined {
320
+ delete(id: string): Quest | SubQuest | null | undefined {
194
321
  const index = this.quests.findIndex((q) => q.id === id);
195
- if (index === -1) {
196
- logger.debug("quests:state", "delete-not-found", { id });
197
- return undefined;
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;
198
345
  }
199
-
200
- const [quest] = this.quests.splice(index, 1);
201
- this.history.push({ type: QUEST_ACTIONS.delete, quest, index });
202
-
203
- logger.debug("quests:state", QUEST_ACTIONS.delete, { id, index, total: this.quests.length });
204
- return quest;
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;
205
360
  }
206
361
 
207
362
  clear(all = false): number {
208
363
  if (!all) {
364
+ const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
209
365
  const done = this.quests.filter((q) => q.done);
210
- const previousQuests = this.quests.map((q) => ({ ...q }));
211
- const previousNextId = this.nextId;
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);
212
372
  this.quests = this.quests.filter((q) => !q.done);
213
- for (let i = 0; i < this.quests.length; i++) {
214
- this.quests[i].id = i + 1;
215
- }
216
- this.nextId = this.quests.length + 1;
373
+ this.subQuests = keptSubs;
217
374
  this.history.push({
218
375
  type: QUEST_ACTIONS.clear,
219
376
  previousQuests,
220
- previousNextId,
377
+ previousSubQuests,
221
378
  all: false,
222
379
  });
223
- logger.debug("quests:state", QUEST_ACTIONS.clear, { count: done.length, all });
224
- return done.length;
380
+ const count = done.length + removedSubs.length;
381
+ logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
382
+ return count;
225
383
  }
226
- const count = this.quests.length;
384
+ const count = this.quests.length + this.subQuests.length;
227
385
  this.history.push({
228
386
  type: QUEST_ACTIONS.clear,
229
387
  quests: [...this.quests],
230
- nextId: this.nextId,
388
+ subQuests: [...this.subQuests],
231
389
  all: true,
232
390
  });
233
391
  this.quests = [];
234
- this.nextId = 1;
392
+ this.subQuests = [];
393
+ this.usedIds.clear();
235
394
  logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
236
395
  return count;
237
396
  }
238
397
 
239
- reorder(id: number, targetIndex: number): Quest | undefined {
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
+ }
240
403
  const idx = this.quests.findIndex((q) => q.id === id);
241
404
  if (idx === -1) {
242
405
  logger.debug("quests:state", "reorder-not-found", { id });
243
406
  return undefined;
244
407
  }
245
408
 
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
+ }
414
+
415
+ if (idx === targetIdx) {
416
+ return this.quests[idx];
417
+ }
418
+
246
419
  const previousIds = this.quests.map((q) => q.id);
247
420
  const [quest] = this.quests.splice(idx, 1);
248
- this.quests.splice(targetIndex, 0, quest);
249
- for (let i = 0; i < this.quests.length; i++) {
250
- this.quests[i].id = i + 1;
421
+
422
+ let insertIndex = targetIdx;
423
+ if (idx < targetIdx) {
424
+ insertIndex = targetIdx - 1;
251
425
  }
252
426
 
253
- this.nextId = this.quests.length + 1;
254
- this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds });
427
+ this.quests.splice(insertIndex, 0, quest);
428
+ this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds, targetId });
255
429
  logger.debug("quests:state", QUEST_ACTIONS.reorder, {
256
430
  id: quest.id,
257
- targetIndex,
258
- total: this.quests.length,
431
+ targetId,
432
+ total: this.quests.length + this.subQuests.length,
259
433
  });
260
434
 
261
435
  return quest;
@@ -293,13 +467,53 @@ export class QuestLog {
293
467
  message: "Error: all descriptions in a batch must be non-empty",
294
468
  };
295
469
  }
296
- const added: { id: number; description: string }[] = [];
297
- for (const desc of action.descriptions) {
298
- const q = this.add(desc);
299
- 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
+ };
300
516
  }
301
-
302
- return { success: true, message: formatBatchAddResult(added) };
303
517
  }
304
518
  return {
305
519
  success: false,
@@ -316,9 +530,12 @@ export class QuestLog {
316
530
  }
317
531
 
318
532
  const q = this.toggle(action.id);
319
- if (!q) {
533
+ if (q === undefined) {
320
534
  return { success: false, message: formatNotFound(action.id) };
321
535
  }
536
+ if (q === null) {
537
+ return { success: false, message: formatBlockedBySubQuests(action.id) };
538
+ }
322
539
 
323
540
  return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
324
541
  }
@@ -327,7 +544,7 @@ export class QuestLog {
327
544
  return { success: false, message: "Error: id is required for update action" };
328
545
  }
329
546
 
330
- if (!action.description) {
547
+ if (!action.description || action.description.trim().length === 0) {
331
548
  return { success: false, message: "Error: description is required for update action" };
332
549
  }
333
550
 
@@ -344,9 +561,12 @@ export class QuestLog {
344
561
  }
345
562
 
346
563
  const q = this.delete(action.id);
347
- if (!q) {
564
+ if (q === undefined) {
348
565
  return { success: false, message: formatNotFound(action.id) };
349
566
  }
567
+ if (q === null) {
568
+ return { success: false, message: formatBlockedBySubQuests(action.id) };
569
+ }
350
570
 
351
571
  return { success: true, message: formatDeleteResult(q), quest: q };
352
572
  }
@@ -362,13 +582,13 @@ export class QuestLog {
362
582
  if (action.id === undefined)
363
583
  return { success: false, message: "Error: id is required for reorder action" };
364
584
 
365
- if (action.targetIndex === undefined)
366
- return { success: false, message: "Error: targetIndex is required for reorder action" };
585
+ if (action.targetId === undefined)
586
+ return { success: false, message: "Error: targetId is required for reorder action" };
367
587
 
368
- const q = this.reorder(action.id, action.targetIndex);
369
- if (!q) return { success: false, message: formatNotFound(action.id) };
588
+ const q = this.reorder(action.id, action.targetId);
589
+ if (!q) return { success: false, message: "Quest not found or is a sub-quest" };
370
590
 
371
- return { success: true, message: `Reordered quest #${q.id}: ${q.description}`, quest: q };
591
+ return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
372
592
  }
373
593
  case QUEST_ACTIONS.revert: {
374
594
  return this.revert();
@@ -398,15 +618,23 @@ export class QuestLog {
398
618
 
399
619
  if (lastState) {
400
620
  const quests = lastState.quests;
401
- const nextId = lastState.nextId;
402
621
  const questCount = Array.isArray(quests) ? quests.length : 0;
403
622
 
404
- this.quests = Array.isArray(quests) ? [...(quests as Quest[])] : [];
405
- this.nextId = typeof nextId === "number" ? nextId : 1;
406
- 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 });
407
634
  } else {
408
635
  this.quests = [];
409
- this.nextId = 1;
636
+ this.subQuests = [];
637
+ this.usedIds.clear();
410
638
  logger.debug("quests:state", "reconstruct-empty", { toolResults });
411
639
  }
412
640
 
@@ -423,7 +651,7 @@ export function makeToolResult(
423
651
  content: [{ type: "text", text }],
424
652
  details: {
425
653
  quests: questLog.getAll(),
426
- nextId: questLog.getNextId(),
654
+ usedIds: questLog.getUsedIds(),
427
655
  displayQuests,
428
656
  },
429
657
  };