pi-quests 0.4.0 → 0.5.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,18 +1,36 @@
1
1
  import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { DEFAULT_CONFIG, type ResolvedConfig } from "../config.js";
3
3
  import { logger } from "../logger.js";
4
+ import { getQuestSkillDocument } from "../prompts.js";
4
5
  import {
5
6
  formatAddResult,
6
7
  formatBatchAddResult,
7
- formatBlockedBySubQuests,
8
+ formatBlockedBySteps,
8
9
  formatDeleteResult,
10
+ formatDescriptionRequiredError,
11
+ formatEmptyDescriptionsError,
12
+ formatIdRequiredError,
13
+ formatMissingDescriptionsError,
9
14
  formatNotFound,
15
+ formatNothingToRevertError,
16
+ formatParentDoneError,
17
+ formatParentNotFoundError,
10
18
  formatQuestList,
11
- formatSubQuestCannotHaveSubQuests,
19
+ formatReorderedQuestNotFoundError,
20
+ formatReorderNotFoundError,
21
+ formatReparentDemoteHasStepsError,
22
+ formatReparentResult,
23
+ formatReparentSelfParentError,
24
+ formatReparentTargetDoneError,
25
+ formatReparentTargetIsStepError,
26
+ formatReparentTargetNotFoundError,
27
+ formatStepCannotHaveSteps,
28
+ formatTargetIdRequiredError,
12
29
  formatToggleResult,
30
+ formatUnknownActionError,
13
31
  formatUpdateResult,
14
32
  } from "./formatters.js";
15
- import { QUEST_ACTIONS, type Quest, type SubQuest } from "./types.js";
33
+ import { QUEST_ACTIONS, type Quest, type Step } from "./types.js";
16
34
 
17
35
  export type HistoryEntry =
18
36
  | { type: typeof QUEST_ACTIONS.add; id: string; parentId?: string }
@@ -20,35 +38,46 @@ export type HistoryEntry =
20
38
  | { type: typeof QUEST_ACTIONS.update; id: string; previousDescription: string }
21
39
  | {
22
40
  type: typeof QUEST_ACTIONS.delete;
23
- quest: Quest | SubQuest;
41
+ quest: Quest | Step;
24
42
  index: number;
25
- isSubQuest?: boolean;
26
- cascadeDeletedSubs?: SubQuest[];
43
+ isStep?: boolean;
44
+ cascadeDeletedSteps?: Step[];
27
45
  }
28
46
  | {
29
47
  type: typeof QUEST_ACTIONS.clear;
30
48
  previousQuests: Quest[];
31
- previousSubQuests?: SubQuest[];
49
+ previousSteps?: Step[];
32
50
  all: false;
33
51
  }
34
- | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; subQuests?: SubQuest[]; all: true }
52
+ | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; steps?: Step[]; all: true }
35
53
  | {
36
54
  type: typeof QUEST_ACTIONS.reorder;
37
55
  quest: Quest;
38
56
  oldIndex: number;
39
57
  previousIds: string[];
40
58
  targetId: string;
59
+ }
60
+ | {
61
+ type: typeof QUEST_ACTIONS.reparent;
62
+ id: string;
63
+ previousParentId?: string;
64
+ previousQuestIndex?: number;
41
65
  };
42
66
 
43
67
  export type QuestAction =
44
- | { type: typeof QUEST_ACTIONS.add; descriptions?: string[]; parentId?: string }
68
+ | { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
69
+ | { type: typeof QUEST_ACTIONS.split; id?: string; descriptions?: string[] }
70
+ | { type: typeof QUEST_ACTIONS.add_step; id?: string; descriptions?: string[] }
45
71
  | { type: typeof QUEST_ACTIONS.list }
46
72
  | { type: typeof QUEST_ACTIONS.toggle; id?: string }
47
73
  | { type: typeof QUEST_ACTIONS.update; id?: string; description?: string }
48
74
  | { type: typeof QUEST_ACTIONS.delete; id?: string }
49
75
  | { type: typeof QUEST_ACTIONS.clear; all?: boolean }
50
76
  | { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
51
- | { type: typeof QUEST_ACTIONS.revert };
77
+ | { type: typeof QUEST_ACTIONS.revert }
78
+ | { type: typeof QUEST_ACTIONS.reparent; id?: string; parentId?: string }
79
+ | { type: typeof QUEST_ACTIONS.rules }
80
+ | { type: typeof QUEST_ACTIONS.skill };
52
81
 
53
82
  export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
54
83
 
@@ -60,7 +89,7 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
60
89
  */
61
90
  export class QuestLog {
62
91
  private quests: Quest[] = [];
63
- private subQuests: SubQuest[] = [];
92
+ private steps: Step[] = [];
64
93
  private usedIds: Set<string> = new Set();
65
94
  private history: HistoryEntry[] = [];
66
95
 
@@ -97,12 +126,12 @@ export class QuestLog {
97
126
  } = {
98
127
  [QUEST_ACTIONS.add]: (entry) => {
99
128
  this.quests = this.quests.filter((q) => q.id !== entry.id);
100
- this.subQuests = this.subQuests.filter((q) => q.id !== entry.id);
129
+ this.steps = this.steps.filter((q) => q.id !== entry.id);
101
130
  this.usedIds.delete(entry.id);
102
131
 
103
132
  logger.debug("quests:state", "revert-add", {
104
133
  id: entry.id,
105
- total: this.quests.length + this.subQuests.length,
134
+ total: this.quests.length + this.steps.length,
106
135
  });
107
136
  return { success: true, message: `Reverted add quest [${entry.id}]` };
108
137
  },
@@ -116,7 +145,7 @@ export class QuestLog {
116
145
  }
117
146
 
118
147
  logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
119
- return { success: false, message: `Quest [${entry.id}] not found` };
148
+ return { success: false, message: formatNotFound(entry.id) };
120
149
  },
121
150
  [QUEST_ACTIONS.update]: (entry) => {
122
151
  const quest = this.findById(entry.id);
@@ -127,16 +156,16 @@ export class QuestLog {
127
156
  }
128
157
 
129
158
  logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
130
- return { success: false, message: `Quest [${entry.id}] not found` };
159
+ return { success: false, message: formatNotFound(entry.id) };
131
160
  },
132
161
  [QUEST_ACTIONS.delete]: (entry) => {
133
- if (entry.isSubQuest) {
134
- this.subQuests.splice(entry.index, 0, entry.quest as unknown as SubQuest);
162
+ if (entry.isStep) {
163
+ this.steps.splice(entry.index, 0, entry.quest as Step);
135
164
  } else {
136
165
  this.quests.splice(entry.index, 0, entry.quest);
137
- if (entry.cascadeDeletedSubs) {
138
- for (const sub of entry.cascadeDeletedSubs) {
139
- this.subQuests.push(sub);
166
+ if (entry.cascadeDeletedSteps) {
167
+ for (const sub of entry.cascadeDeletedSteps) {
168
+ this.steps.push(sub);
140
169
  this.usedIds.add(sub.id);
141
170
  }
142
171
  }
@@ -146,7 +175,7 @@ export class QuestLog {
146
175
  logger.debug("quests:state", "revert-delete", {
147
176
  id: entry.quest.id,
148
177
  index: entry.index,
149
- total: this.quests.length + this.subQuests.length,
178
+ total: this.quests.length + this.steps.length,
150
179
  });
151
180
  return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
152
181
  },
@@ -154,11 +183,11 @@ export class QuestLog {
154
183
  if ("previousQuests" in entry) {
155
184
  const restoredCount =
156
185
  entry.previousQuests.length +
157
- (entry.previousSubQuests?.length ?? 0) -
158
- (this.quests.length + this.subQuests.length);
186
+ (entry.previousSteps?.length ?? 0) -
187
+ (this.quests.length + this.steps.length);
159
188
  this.quests = [...entry.previousQuests];
160
- this.subQuests = [...(entry.previousSubQuests ?? [])];
161
- this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
189
+ this.steps = [...(entry.previousSteps ?? [])];
190
+ this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
162
191
 
163
192
  return {
164
193
  success: true,
@@ -167,14 +196,15 @@ export class QuestLog {
167
196
  }
168
197
 
169
198
  this.quests = [...entry.quests];
170
- this.subQuests = [...(entry.subQuests ?? [])];
171
- this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
199
+ this.steps = [...(entry.steps ?? [])];
200
+ this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
172
201
 
173
202
  return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
174
203
  },
175
204
  [QUEST_ACTIONS.reorder]: (entry) => {
176
205
  const currentIndex = this.quests.indexOf(entry.quest);
177
- if (currentIndex === -1) return { success: false, message: "Reordered quest not found" };
206
+ if (currentIndex === -1)
207
+ return { success: false, message: formatReorderedQuestNotFoundError() };
178
208
 
179
209
  this.quests.splice(currentIndex, 1);
180
210
  this.quests.splice(entry.oldIndex, 0, entry.quest);
@@ -184,41 +214,55 @@ export class QuestLog {
184
214
 
185
215
  return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
186
216
  },
217
+ [QUEST_ACTIONS.reparent]: (entry) => {
218
+ const quest = this.findById(entry.id);
219
+ if (!quest) return { success: false, message: formatNotFound(entry.id) };
220
+ if (entry.previousParentId !== undefined) {
221
+ this.quests = this.quests.filter((q) => q.id !== entry.id);
222
+ (quest as Step).parentId = entry.previousParentId;
223
+ if (!this.steps.some((s) => s.id === entry.id)) this.steps.push(quest as Step);
224
+ } else if (entry.previousQuestIndex !== undefined) {
225
+ this.steps = this.steps.filter((s) => s.id !== entry.id);
226
+ delete (quest as Partial<Step>).parentId;
227
+ this.quests.splice(entry.previousQuestIndex, 0, quest);
228
+ }
229
+ return { success: true, message: `Reverted reparent for quest [${entry.id}]` };
230
+ },
187
231
  };
188
232
 
189
233
  /*
190
- * Returns all quests including sub-quests, inserted in order
191
- * sub-quests are returned immediately after their parent quest
234
+ * Returns all quests including steps, inserted in order
235
+ * steps are returned immediately after their parent quest
192
236
  */
193
237
  getAll(): Quest[] {
194
238
  const result: Quest[] = [];
195
239
  for (const q of this.quests) {
196
240
  result.push(q);
197
- result.push(...this.subQuests.filter((sq) => sq.parentId === q.id));
241
+ result.push(...this.steps.filter((step) => step.parentId === q.id));
198
242
  }
199
243
  return result;
200
244
  }
201
245
 
202
246
  /*
203
- * Returns only top-level quests. Sub-quests should be accessed via `getSubQuests()`.
247
+ * Returns only top-level quests. Steps should be accessed via `getSteps()`.
204
248
  */
205
249
  getQuests(): Quest[] {
206
250
  return [...this.quests];
207
251
  }
208
252
 
209
253
  /*
210
- * Returns sub-quests for a given parent quest ID. Sub-quests are not included in `getQuests()`.
254
+ * Returns steps for a given parent quest ID. Steps are not included in `getQuests()`.
211
255
  */
212
- getSubQuests(parentId: string): SubQuest[] {
213
- return this.subQuests.filter((sq) => sq.parentId === parentId);
256
+ getSteps(parentId: string): Step[] {
257
+ return this.steps.filter((step) => step.parentId === parentId);
214
258
  }
215
259
 
216
- getParentQuest(subQuest: SubQuest): Quest | undefined {
217
- return this.quests.find((q) => q.id === subQuest.parentId);
260
+ getParent(step: Step): Quest | undefined {
261
+ return this.quests.find((q) => q.id === step.parentId);
218
262
  }
219
263
 
220
- private findById(id: string): Quest | SubQuest | undefined {
221
- return this.quests.find((q) => q.id === id) ?? this.subQuests.find((sq) => sq.id === id);
264
+ private findById(id: string): Quest | Step | undefined {
265
+ return this.quests.find((q) => q.id === id) ?? this.steps.find((step) => step.id === id);
222
266
  }
223
267
 
224
268
  getUsedIds(): string[] {
@@ -244,36 +288,50 @@ export class QuestLog {
244
288
  return quest;
245
289
  }
246
290
 
247
- addSubQuest(description: string, parentId: string): SubQuest {
248
- if (this.subQuests.some((sq) => sq.id === parentId)) {
249
- throw new Error(formatSubQuestCannotHaveSubQuests(parentId));
291
+ addStep(description: string, parentId: string): Step {
292
+ if (this.steps.some((step) => step.id === parentId)) {
293
+ throw new Error(formatStepCannotHaveSteps(parentId));
250
294
  }
251
295
  const parent = this.quests.find((q) => q.id === parentId);
252
296
  if (!parent) {
253
- throw new Error(`Parent quest [${parentId}] not found`);
297
+ throw new Error(formatParentNotFoundError(parentId));
254
298
  }
255
299
  if (parent.done) {
256
- throw new Error(`Cannot add sub-quest to completed parent quest [${parentId}]`);
300
+ throw new Error(formatParentDoneError(parentId));
257
301
  }
258
- const subQuest: SubQuest = {
302
+ const step: Step = {
259
303
  id: this.generateId(),
260
304
  description,
261
305
  done: false,
262
306
  createdAt: Date.now(),
263
307
  parentId,
264
308
  };
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,
309
+ this.steps.push(step);
310
+ this.history.push({ type: QUEST_ACTIONS.add, id: step.id, parentId });
311
+ logger.debug("quests:state", "add-step", {
312
+ id: step.id,
269
313
  parentId,
270
314
  description,
271
- totalSubQuests: this.subQuests.length,
315
+ totalSteps: this.steps.length,
272
316
  });
273
- return subQuest;
317
+ return step;
318
+ }
319
+
320
+ split(id: string, descriptions: string[]): Step[] {
321
+ if (this.steps.some((step) => step.id === id)) {
322
+ throw new Error(formatStepCannotHaveSteps(id));
323
+ }
324
+ const parent = this.quests.find((q) => q.id === id);
325
+ if (!parent) throw new Error(formatNotFound(id));
326
+ if (parent.done) throw new Error(formatParentDoneError(id));
327
+ const created: Step[] = [];
328
+ for (const desc of descriptions) {
329
+ created.push(this.addStep(desc, id));
330
+ }
331
+ return created;
274
332
  }
275
333
 
276
- toggle(id: string): Quest | SubQuest | null | undefined {
334
+ toggle(id: string): Quest | Step | null | undefined {
277
335
  const quest = this.findById(id);
278
336
  if (!quest) {
279
337
  logger.debug("quests:state", "toggle-not-found", { id });
@@ -281,9 +339,9 @@ export class QuestLog {
281
339
  }
282
340
 
283
341
  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 });
342
+ const steps = this.getSteps(id);
343
+ if (steps.some((q) => !q.done)) {
344
+ logger.debug("quests:state", "toggle-blocked-steps", { id });
287
345
  return null;
288
346
  }
289
347
  }
@@ -294,12 +352,12 @@ export class QuestLog {
294
352
  logger.debug("quests:state", QUEST_ACTIONS.toggle, {
295
353
  id,
296
354
  done: quest.done,
297
- total: this.quests.length + this.subQuests.length,
355
+ total: this.quests.length + this.steps.length,
298
356
  });
299
357
  return quest;
300
358
  }
301
359
 
302
- update(id: string, description: string): Quest | SubQuest | undefined {
360
+ update(id: string, description: string): Quest | Step | undefined {
303
361
  const quest = this.findById(id);
304
362
  if (!quest) {
305
363
  logger.debug("quests:state", "update-not-found", { id });
@@ -312,26 +370,26 @@ export class QuestLog {
312
370
  logger.debug("quests:state", QUEST_ACTIONS.update, {
313
371
  id,
314
372
  description,
315
- total: this.quests.length + this.subQuests.length,
373
+ total: this.quests.length + this.steps.length,
316
374
  });
317
375
  return quest;
318
376
  }
319
377
 
320
- delete(id: string): Quest | SubQuest | null | undefined {
378
+ delete(id: string): Quest | Step | null | undefined {
321
379
  const index = this.quests.findIndex((q) => q.id === id);
322
380
  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 });
381
+ const steps = this.getSteps(id);
382
+ if (steps.some((q) => !q.done)) {
383
+ logger.debug("quests:state", "delete-blocked-steps", { id });
326
384
  return null;
327
385
  }
328
386
  const [quest] = this.quests.splice(index, 1);
329
387
  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);
388
+ const cascadeDeletedSteps = this.steps.filter((step) => step.parentId === id);
389
+ this.history.push({ type: QUEST_ACTIONS.delete, quest, index, cascadeDeletedSteps });
390
+ this.steps = this.steps.filter((step) => {
391
+ if (step.parentId === id) {
392
+ this.usedIds.delete(step.id);
335
393
  return false;
336
394
  }
337
395
  return true;
@@ -339,19 +397,19 @@ export class QuestLog {
339
397
  logger.debug("quests:state", QUEST_ACTIONS.delete, {
340
398
  id,
341
399
  index,
342
- total: this.quests.length + this.subQuests.length,
400
+ total: this.quests.length + this.steps.length,
343
401
  });
344
402
  return quest;
345
403
  }
346
- const subIndex = this.subQuests.findIndex((sq) => sq.id === id);
347
- if (subIndex !== -1) {
348
- const [quest] = this.subQuests.splice(subIndex, 1);
404
+ const stepIndex = this.steps.findIndex((step) => step.id === id);
405
+ if (stepIndex !== -1) {
406
+ const [quest] = this.steps.splice(stepIndex, 1);
349
407
  this.usedIds.delete(quest.id);
350
- this.history.push({ type: QUEST_ACTIONS.delete, quest, index: subIndex, isSubQuest: true });
408
+ this.history.push({ type: QUEST_ACTIONS.delete, quest, index: stepIndex, isStep: true });
351
409
  logger.debug("quests:state", QUEST_ACTIONS.delete, {
352
410
  id,
353
- index: subIndex,
354
- total: this.quests.length + this.subQuests.length,
411
+ index: stepIndex,
412
+ total: this.quests.length + this.steps.length,
355
413
  });
356
414
  return quest;
357
415
  }
@@ -363,41 +421,45 @@ export class QuestLog {
363
421
  if (!all) {
364
422
  const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
365
423
  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));
424
+ const removedSteps = this.steps.filter(
425
+ (step) => step.done || doneParentIds.has(step.parentId),
426
+ );
427
+ const keptSteps = this.steps.filter(
428
+ (step) => !step.done && !doneParentIds.has(step.parentId),
429
+ );
368
430
  const previousQuests = [...this.quests];
369
- const previousSubQuests = [...this.subQuests];
431
+ const previousSteps = [...this.steps];
370
432
  for (const q of done) this.usedIds.delete(q.id);
371
- for (const q of removedSubs) this.usedIds.delete(q.id);
433
+ for (const q of removedSteps) this.usedIds.delete(q.id);
372
434
  this.quests = this.quests.filter((q) => !q.done);
373
- this.subQuests = keptSubs;
435
+ this.steps = keptSteps;
374
436
  this.history.push({
375
437
  type: QUEST_ACTIONS.clear,
376
438
  previousQuests,
377
- previousSubQuests,
439
+ previousSteps,
378
440
  all: false,
379
441
  });
380
- const count = done.length + removedSubs.length;
442
+ const count = done.length + removedSteps.length;
381
443
  logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
382
444
  return count;
383
445
  }
384
- const count = this.quests.length + this.subQuests.length;
446
+ const count = this.quests.length + this.steps.length;
385
447
  this.history.push({
386
448
  type: QUEST_ACTIONS.clear,
387
449
  quests: [...this.quests],
388
- subQuests: [...this.subQuests],
450
+ steps: [...this.steps],
389
451
  all: true,
390
452
  });
391
453
  this.quests = [];
392
- this.subQuests = [];
454
+ this.steps = [];
393
455
  this.usedIds.clear();
394
456
  logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
395
457
  return count;
396
458
  }
397
459
 
398
460
  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 });
461
+ if (this.steps.some((step) => step.id === id || step.id === targetId)) {
462
+ logger.debug("quests:state", "reorder-step-rejected", { id, targetId });
401
463
  return undefined;
402
464
  }
403
465
  const idx = this.quests.findIndex((q) => q.id === id);
@@ -429,7 +491,63 @@ export class QuestLog {
429
491
  logger.debug("quests:state", QUEST_ACTIONS.reorder, {
430
492
  id: quest.id,
431
493
  targetId,
432
- total: this.quests.length + this.subQuests.length,
494
+ total: this.quests.length + this.steps.length,
495
+ });
496
+
497
+ return quest;
498
+ }
499
+
500
+ reparent(id: string, parentId?: string): Quest | Step | undefined {
501
+ const quest = this.findById(id);
502
+ if (!quest) return undefined;
503
+
504
+ if (parentId) {
505
+ if (id === parentId) {
506
+ throw new Error(formatReparentSelfParentError(id));
507
+ }
508
+ const target = this.findById(parentId);
509
+ if (!target) {
510
+ throw new Error(formatReparentTargetNotFoundError(parentId));
511
+ }
512
+ if ("parentId" in target) {
513
+ throw new Error(formatReparentTargetIsStepError(parentId));
514
+ }
515
+ if (target.done) {
516
+ throw new Error(formatReparentTargetDoneError(parentId));
517
+ }
518
+ if (!("parentId" in quest)) {
519
+ const steps = this.getSteps(id);
520
+ if (steps.length > 0) {
521
+ throw new Error(formatReparentDemoteHasStepsError(id));
522
+ }
523
+ }
524
+ const previousQuestIndex = this.quests.findIndex((q) => q.id === id);
525
+ const previousParentId = "parentId" in quest ? quest.parentId : undefined;
526
+ this.quests = this.quests.filter((q) => q.id !== id);
527
+ this.steps = this.steps.filter((s) => s.id !== id);
528
+ (quest as Step).parentId = parentId;
529
+ this.steps.push(quest as Step);
530
+ this.history.push({
531
+ type: QUEST_ACTIONS.reparent,
532
+ id,
533
+ previousParentId,
534
+ previousQuestIndex: previousQuestIndex === -1 ? undefined : previousQuestIndex,
535
+ });
536
+ } else {
537
+ if (!("parentId" in quest)) {
538
+ return quest;
539
+ }
540
+ const previousParentId = quest.parentId;
541
+ this.steps = this.steps.filter((s) => s.id !== id);
542
+ delete (quest as Partial<Step>).parentId;
543
+ this.quests.push(quest);
544
+ this.history.push({ type: QUEST_ACTIONS.reparent, id, previousParentId });
545
+ }
546
+
547
+ logger.debug("quests:state", QUEST_ACTIONS.reparent, {
548
+ id,
549
+ parentId,
550
+ total: this.quests.length + this.steps.length,
433
551
  });
434
552
 
435
553
  return quest;
@@ -439,7 +557,7 @@ export class QuestLog {
439
557
  const entry = this.history.pop();
440
558
  if (!entry) {
441
559
  logger.debug("quests:state", "revert-empty");
442
- return { success: false, message: "Nothing to revert" };
560
+ return { success: false, message: formatNothingToRevertError() };
443
561
  }
444
562
 
445
563
  logger.debug("quests:state", "revert", { type: entry.type });
@@ -464,33 +582,13 @@ export class QuestLog {
464
582
  if (action.descriptions.some((d) => !d || d.trim().length === 0)) {
465
583
  return {
466
584
  success: false,
467
- message: "Error: all descriptions in a batch must be non-empty",
585
+ message: formatEmptyDescriptionsError(),
468
586
  };
469
587
  }
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
588
  if (action.descriptions.length === 1) {
489
589
  try {
490
590
  const [firstDesc] = action.descriptions;
491
- const q = action.parentId
492
- ? this.addSubQuest(firstDesc, action.parentId)
493
- : this.add(firstDesc);
591
+ const q = this.add(firstDesc);
494
592
 
495
593
  return { success: true, message: formatAddResult(q) };
496
594
  } catch (err) {
@@ -503,7 +601,7 @@ export class QuestLog {
503
601
  try {
504
602
  const added: { id: string; description: string }[] = [];
505
603
  for (const desc of action.descriptions) {
506
- const q = action.parentId ? this.addSubQuest(desc, action.parentId) : this.add(desc);
604
+ const q = this.add(desc);
507
605
  added.push({ id: q.id, description: q.description });
508
606
  }
509
607
 
@@ -517,7 +615,7 @@ export class QuestLog {
517
615
  }
518
616
  return {
519
617
  success: false,
520
- message: "Error: at least one description is required for add action",
618
+ message: formatMissingDescriptionsError(),
521
619
  };
522
620
  }
523
621
  case QUEST_ACTIONS.list: {
@@ -526,7 +624,7 @@ export class QuestLog {
526
624
  }
527
625
  case QUEST_ACTIONS.toggle: {
528
626
  if (action.id === undefined) {
529
- return { success: false, message: "Error: id is required for toggle action" };
627
+ return { success: false, message: formatIdRequiredError("toggle") };
530
628
  }
531
629
 
532
630
  const q = this.toggle(action.id);
@@ -534,18 +632,18 @@ export class QuestLog {
534
632
  return { success: false, message: formatNotFound(action.id) };
535
633
  }
536
634
  if (q === null) {
537
- return { success: false, message: formatBlockedBySubQuests(action.id) };
635
+ return { success: false, message: formatBlockedBySteps(action.id) };
538
636
  }
539
637
 
540
638
  return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
541
639
  }
542
640
  case QUEST_ACTIONS.update: {
543
641
  if (action.id === undefined) {
544
- return { success: false, message: "Error: id is required for update action" };
642
+ return { success: false, message: formatIdRequiredError("update") };
545
643
  }
546
644
 
547
645
  if (!action.description || action.description.trim().length === 0) {
548
- return { success: false, message: "Error: description is required for update action" };
646
+ return { success: false, message: formatDescriptionRequiredError() };
549
647
  }
550
648
 
551
649
  const q = this.update(action.id, action.description);
@@ -557,7 +655,7 @@ export class QuestLog {
557
655
  }
558
656
  case QUEST_ACTIONS.delete: {
559
657
  if (action.id === undefined) {
560
- return { success: false, message: "Error: id is required for delete action" };
658
+ return { success: false, message: formatIdRequiredError("delete") };
561
659
  }
562
660
 
563
661
  const q = this.delete(action.id);
@@ -565,7 +663,7 @@ export class QuestLog {
565
663
  return { success: false, message: formatNotFound(action.id) };
566
664
  }
567
665
  if (q === null) {
568
- return { success: false, message: formatBlockedBySubQuests(action.id) };
666
+ return { success: false, message: formatBlockedBySteps(action.id) };
569
667
  }
570
668
 
571
669
  return { success: true, message: formatDeleteResult(q), quest: q };
@@ -580,21 +678,66 @@ export class QuestLog {
580
678
  }
581
679
  case QUEST_ACTIONS.reorder: {
582
680
  if (action.id === undefined)
583
- return { success: false, message: "Error: id is required for reorder action" };
681
+ return { success: false, message: formatIdRequiredError("reorder") };
584
682
 
585
683
  if (action.targetId === undefined)
586
- return { success: false, message: "Error: targetId is required for reorder action" };
684
+ return { success: false, message: formatTargetIdRequiredError() };
587
685
 
588
686
  const q = this.reorder(action.id, action.targetId);
589
- if (!q) return { success: false, message: "Quest not found or is a sub-quest" };
687
+ if (!q) return { success: false, message: formatReorderNotFoundError() };
590
688
 
591
689
  return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
592
690
  }
691
+ case QUEST_ACTIONS.split:
692
+ case QUEST_ACTIONS.add_step: {
693
+ if (action.id === undefined) {
694
+ return { success: false, message: formatIdRequiredError("split") };
695
+ }
696
+ if (!action.descriptions || action.descriptions.length === 0) {
697
+ return { success: false, message: formatMissingDescriptionsError("split") };
698
+ }
699
+ if (action.descriptions.some((d) => !d || d.trim().length === 0)) {
700
+ return { success: false, message: formatEmptyDescriptionsError() };
701
+ }
702
+ try {
703
+ const steps = this.split(action.id, action.descriptions);
704
+ return {
705
+ success: true,
706
+ message: `Split quest [${action.id}] into ${steps.length} steps`,
707
+ };
708
+ } catch (err) {
709
+ return {
710
+ success: false,
711
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
712
+ };
713
+ }
714
+ }
715
+ case QUEST_ACTIONS.reparent: {
716
+ if (action.id === undefined)
717
+ return { success: false, message: formatIdRequiredError("reparent") };
718
+ try {
719
+ const q = this.reparent(action.id, action.parentId);
720
+ if (!q) return { success: false, message: formatNotFound(action.id) };
721
+ return { success: true, message: formatReparentResult(q, action.parentId), quest: q };
722
+ } catch (err) {
723
+ return {
724
+ success: false,
725
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
726
+ };
727
+ }
728
+ }
729
+ case QUEST_ACTIONS.rules:
730
+ case QUEST_ACTIONS.skill: {
731
+ return { success: true, message: getQuestSkillDocument() };
732
+ }
593
733
  case QUEST_ACTIONS.revert: {
594
734
  return this.revert();
595
735
  }
596
736
  default: {
597
- return { success: false, message: `Unknown action: ${(action as { type: string }).type}` };
737
+ return {
738
+ success: false,
739
+ message: formatUnknownActionError((action as { type: string }).type),
740
+ };
598
741
  }
599
742
  }
600
743
  }
@@ -622,18 +765,16 @@ export class QuestLog {
622
765
 
623
766
  if (Array.isArray(quests)) {
624
767
  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[];
768
+ this.steps = (quests as Quest[]).filter((q) => "parentId" in q && q.parentId) as Step[];
628
769
  } else {
629
770
  this.quests = [];
630
- this.subQuests = [];
771
+ this.steps = [];
631
772
  }
632
- this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
773
+ this.usedIds = new Set([...this.quests, ...this.steps].map((q) => q.id));
633
774
  logger.debug("quests:state", "reconstruct", { toolResults, questCount });
634
775
  } else {
635
776
  this.quests = [];
636
- this.subQuests = [];
777
+ this.steps = [];
637
778
  this.usedIds.clear();
638
779
  logger.debug("quests:state", "reconstruct-empty", { toolResults });
639
780
  }