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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +17 -2
- package/src/commands/parse-args.ts +31 -16
- package/src/config.ts +10 -10
- package/src/index.ts +6 -5
- package/src/prompts/skill.md +60 -0
- package/src/prompts.ts +13 -2
- package/src/quest/dataplane.ts +269 -128
- package/src/quest/formatters.ts +94 -5
- package/src/quest/tracker.ts +73 -22
- package/src/quest/types.ts +11 -1
- package/src/renderers/commands.ts +7 -7
- package/src/renderers/quests.ts +11 -11
- package/src/renderers/tools.ts +10 -10
- package/src/tools/handler.ts +47 -11
- package/src/tools/params.ts +7 -7
package/src/quest/dataplane.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 |
|
|
41
|
+
quest: Quest | Step;
|
|
24
42
|
index: number;
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
isStep?: boolean;
|
|
44
|
+
cascadeDeletedSteps?: Step[];
|
|
27
45
|
}
|
|
28
46
|
| {
|
|
29
47
|
type: typeof QUEST_ACTIONS.clear;
|
|
30
48
|
previousQuests: Quest[];
|
|
31
|
-
|
|
49
|
+
previousSteps?: Step[];
|
|
32
50
|
all: false;
|
|
33
51
|
}
|
|
34
|
-
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[];
|
|
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[]
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
159
|
+
return { success: false, message: formatNotFound(entry.id) };
|
|
131
160
|
},
|
|
132
161
|
[QUEST_ACTIONS.delete]: (entry) => {
|
|
133
|
-
if (entry.
|
|
134
|
-
this.
|
|
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.
|
|
138
|
-
for (const sub of entry.
|
|
139
|
-
this.
|
|
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.
|
|
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.
|
|
158
|
-
(this.quests.length + this.
|
|
186
|
+
(entry.previousSteps?.length ?? 0) -
|
|
187
|
+
(this.quests.length + this.steps.length);
|
|
159
188
|
this.quests = [...entry.previousQuests];
|
|
160
|
-
this.
|
|
161
|
-
this.usedIds = new Set([...this.quests, ...this.
|
|
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.
|
|
171
|
-
this.usedIds = new Set([...this.quests, ...this.
|
|
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)
|
|
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
|
|
191
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
254
|
+
* Returns steps for a given parent quest ID. Steps are not included in `getQuests()`.
|
|
211
255
|
*/
|
|
212
|
-
|
|
213
|
-
return this.
|
|
256
|
+
getSteps(parentId: string): Step[] {
|
|
257
|
+
return this.steps.filter((step) => step.parentId === parentId);
|
|
214
258
|
}
|
|
215
259
|
|
|
216
|
-
|
|
217
|
-
return this.quests.find((q) => q.id ===
|
|
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 |
|
|
221
|
-
return this.quests.find((q) => q.id === id) ?? this.
|
|
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
|
-
|
|
248
|
-
if (this.
|
|
249
|
-
throw new Error(
|
|
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(
|
|
297
|
+
throw new Error(formatParentNotFoundError(parentId));
|
|
254
298
|
}
|
|
255
299
|
if (parent.done) {
|
|
256
|
-
throw new Error(
|
|
300
|
+
throw new Error(formatParentDoneError(parentId));
|
|
257
301
|
}
|
|
258
|
-
const
|
|
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.
|
|
266
|
-
this.history.push({ type: QUEST_ACTIONS.add, id:
|
|
267
|
-
logger.debug("quests:state", "add-
|
|
268
|
-
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
|
-
|
|
315
|
+
totalSteps: this.steps.length,
|
|
272
316
|
});
|
|
273
|
-
return
|
|
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 |
|
|
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
|
|
285
|
-
if (
|
|
286
|
-
logger.debug("quests:state", "toggle-blocked-
|
|
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.
|
|
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 |
|
|
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.
|
|
373
|
+
total: this.quests.length + this.steps.length,
|
|
316
374
|
});
|
|
317
375
|
return quest;
|
|
318
376
|
}
|
|
319
377
|
|
|
320
|
-
delete(id: string): Quest |
|
|
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
|
|
324
|
-
if (
|
|
325
|
-
logger.debug("quests:state", "delete-blocked-
|
|
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
|
|
331
|
-
this.history.push({ type: QUEST_ACTIONS.delete, quest, index,
|
|
332
|
-
this.
|
|
333
|
-
if (
|
|
334
|
-
this.usedIds.delete(
|
|
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.
|
|
400
|
+
total: this.quests.length + this.steps.length,
|
|
343
401
|
});
|
|
344
402
|
return quest;
|
|
345
403
|
}
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
348
|
-
const [quest] = this.
|
|
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:
|
|
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:
|
|
354
|
-
total: this.quests.length + this.
|
|
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
|
|
367
|
-
|
|
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
|
|
431
|
+
const previousSteps = [...this.steps];
|
|
370
432
|
for (const q of done) this.usedIds.delete(q.id);
|
|
371
|
-
for (const q of
|
|
433
|
+
for (const q of removedSteps) this.usedIds.delete(q.id);
|
|
372
434
|
this.quests = this.quests.filter((q) => !q.done);
|
|
373
|
-
this.
|
|
435
|
+
this.steps = keptSteps;
|
|
374
436
|
this.history.push({
|
|
375
437
|
type: QUEST_ACTIONS.clear,
|
|
376
438
|
previousQuests,
|
|
377
|
-
|
|
439
|
+
previousSteps,
|
|
378
440
|
all: false,
|
|
379
441
|
});
|
|
380
|
-
const count = done.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.
|
|
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
|
-
|
|
450
|
+
steps: [...this.steps],
|
|
389
451
|
all: true,
|
|
390
452
|
});
|
|
391
453
|
this.quests = [];
|
|
392
|
-
this.
|
|
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.
|
|
400
|
-
logger.debug("quests:state", "reorder-
|
|
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.
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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: "
|
|
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:
|
|
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: "
|
|
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:
|
|
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: "
|
|
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:
|
|
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: "
|
|
681
|
+
return { success: false, message: formatIdRequiredError("reorder") };
|
|
584
682
|
|
|
585
683
|
if (action.targetId === undefined)
|
|
586
|
-
return { success: false, message:
|
|
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:
|
|
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 {
|
|
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.
|
|
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.
|
|
771
|
+
this.steps = [];
|
|
631
772
|
}
|
|
632
|
-
this.usedIds = new Set([...this.quests, ...this.
|
|
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.
|
|
777
|
+
this.steps = [];
|
|
637
778
|
this.usedIds.clear();
|
|
638
779
|
logger.debug("quests:state", "reconstruct-empty", { toolResults });
|
|
639
780
|
}
|