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,3 +1,19 @@
1
+ /**
2
+ * Error message design guidelines
3
+ *
4
+ * All error messages should follow a consistent three-part pattern:
5
+ * 1. PROBLEM — state what went wrong clearly and concisely
6
+ * 2. RULE — explain the constraint or invariant that was violated
7
+ * 3. RECOVERY HINT — suggest the next valid action the caller can take
8
+ *
9
+ * Example:
10
+ * "Quest [01] has incomplete steps. A parent quest can only be marked
11
+ * done after all its steps are complete. Try toggling the steps first."
12
+ *
13
+ * Keep messages actionable. Agents should never be left guessing why something
14
+ * failed or what to do next.
15
+ */
16
+
1
17
  export function formatQuestList(
2
18
  quests: { id: string; description: string; done: boolean; parentId?: string }[],
3
19
  ): string {
@@ -37,13 +53,86 @@ export function formatDeleteResult(q: { id: string; description: string }): stri
37
53
  }
38
54
 
39
55
  export function formatNotFound(id: string): string {
40
- return `Quest [${id}] not found`;
56
+ return `Quest [${id}] not found. IDs are random hex strings shown in brackets. Use the list action to see valid IDs.`;
57
+ }
58
+
59
+ export function formatBlockedBySteps(id: string): string {
60
+ return `Quest [${id}] has incomplete steps. A parent quest can only be marked done or deleted after all its steps are complete. Toggle the steps first, or delete them if they are no longer needed.`;
61
+ }
62
+
63
+ export function formatStepCannotHaveSteps(id: string): string {
64
+ return `Step [${id}] cannot have nested steps. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
65
+ }
66
+
67
+ export function formatEmptyDescriptionsError(): string {
68
+ return `Every description in a batch must be non-empty. Remove empty strings or split the batch into separate add calls.`;
69
+ }
70
+
71
+ export function formatMissingDescriptionsError(action = "add"): string {
72
+ return `At least one description is required for the ${action} action. Provide a description string to create a quest.`;
73
+ }
74
+
75
+ export function formatIdRequiredError(action: string): string {
76
+ return `An id is required for the ${action} action. Use the list action to see valid quest IDs. IDs are the hex strings shown in square brackets.`;
77
+ }
78
+
79
+ export function formatDescriptionRequiredError(): string {
80
+ return `A description is required for the update action. Provide a non-empty string to update the quest.`;
81
+ }
82
+
83
+ export function formatTargetIdRequiredError(): string {
84
+ return `A targetId is required for the reorder action. Provide the hex ID of the quest to insert before.`;
85
+ }
86
+
87
+ export function formatReorderNotFoundError(): string {
88
+ return `Quest not found or is a step. Reorder only works on top-level quests. Use the list action to verify the ID and ensure it is a top-level quest.`;
89
+ }
90
+
91
+ export function formatParentNotFoundError(id: string): string {
92
+ return `Parent quest [${id}] not found. Use the list action to see valid parent IDs.`;
93
+ }
94
+
95
+ export function formatParentDoneError(id: string): string {
96
+ return `Cannot add a step to completed parent quest [${id}]. Steps can only be added to open parents. Reopen the parent first if you need to add more work.`;
97
+ }
98
+
99
+ export function formatUnknownActionError(action: string): string {
100
+ return `Unknown action: ${action}. Use the list action or check the tool schema for supported actions.`;
101
+ }
102
+
103
+ export function formatNothingToRevertError(): string {
104
+ return `Nothing to revert. The history is empty because no mutating actions have been performed yet.`;
105
+ }
106
+
107
+ export function formatReorderedQuestNotFoundError(): string {
108
+ return `Reordered quest not found. It may have been deleted or cleared since the reorder was recorded.`;
109
+ }
110
+
111
+ export function formatReparentResult(
112
+ q: { id: string; description: string },
113
+ parentId?: string,
114
+ ): string {
115
+ return parentId
116
+ ? `Moved quest [${q.id}] under [${parentId}]: ${q.description}`
117
+ : `Promoted quest [${q.id}] to top-level: ${q.description}`;
118
+ }
119
+
120
+ export function formatReparentTargetNotFoundError(id: string): string {
121
+ return `Target quest [${id}] not found. Use the list action to see valid parent IDs.`;
122
+ }
123
+
124
+ export function formatReparentTargetIsStepError(id: string): string {
125
+ return `Step [${id}] cannot be a parent. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
126
+ }
127
+
128
+ export function formatReparentTargetDoneError(id: string): string {
129
+ return `Cannot reparent under completed parent [${id}]. Steps can only be added to open parents.`;
41
130
  }
42
131
 
43
- export function formatBlockedBySubQuests(id: string): string {
44
- return `Quest [${id}] has incomplete sub-quests`;
132
+ export function formatReparentDemoteHasStepsError(id: string): string {
133
+ return `Quest [${id}] has steps and cannot be demoted to a step. Delete or reparent its steps first.`;
45
134
  }
46
135
 
47
- export function formatSubQuestCannotHaveSubQuests(id: string): string {
48
- return `Sub-quest [${id}] cannot have nested sub-quests`;
136
+ export function formatReparentSelfParentError(id: string): string {
137
+ return `Quest [${id}] cannot be its own parent. Choose a different parent ID.`;
49
138
  }
@@ -1,7 +1,8 @@
1
1
  import type { ResolvedConfig } from "../config.js";
2
2
 
3
- const ACKNOWLEDGEMENT =
4
- "ALWAYS acknowledge this reminder immediately and create, update, or align on quests before making further tool calls. DO NOT add this acknowledgement as another quest.";
3
+ const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
4
+
5
+ type NudgeCandidate = { index: number; message: string };
5
6
 
6
7
  export class QuestUsageTracker {
7
8
  private totalToolCalls = 0;
@@ -9,6 +10,8 @@ export class QuestUsageTracker {
9
10
  private hasEverUsedQuestTool = false;
10
11
  private lastQuestToolTime = 0;
11
12
  private nudgedThisTurn = false;
13
+ private lastNudgeTime = 0;
14
+ private lastNudgeIndex = -1;
12
15
 
13
16
  constructor(private readonly config: ResolvedConfig) {}
14
17
 
@@ -34,59 +37,107 @@ export class QuestUsageTracker {
34
37
  ): string | undefined {
35
38
  if (this.nudgedThisTurn) return undefined;
36
39
 
37
- // 1. Initialization nudge
38
- if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
40
+ const now = Date.now();
41
+ const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
42
+ if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
43
+ return undefined;
44
+ }
45
+
46
+ const eligible = this.getEligibleNudges(
47
+ activeQuestCount,
48
+ latestPrompt,
49
+ hasTopLevelQuestWithoutSubs,
50
+ );
51
+
52
+ // Rotate priority: start checking from the nudge after the last one that fired
53
+ const rotated = [
54
+ ...eligible.filter((n) => n.index > this.lastNudgeIndex),
55
+ ...eligible.filter((n) => n.index <= this.lastNudgeIndex),
56
+ ];
57
+
58
+ const winner = rotated[0];
59
+ if (winner) {
39
60
  this.nudgedThisTurn = true;
40
- return `QUEST REMINDER: You have made ${this.totalToolCalls} tool calls but have NEVER used the quest tool this session. USE the quest tool to initialize tracking and break your work into concrete steps. ${ACKNOWLEDGEMENT}`;
61
+ this.lastNudgeTime = now;
62
+ this.lastNudgeIndex = winner.index;
63
+ return winner.message;
64
+ }
65
+
66
+ return undefined;
67
+ }
68
+
69
+ private getEligibleNudges(
70
+ activeQuestCount: number,
71
+ latestPrompt?: string,
72
+ hasTopLevelQuestWithoutSubs?: boolean,
73
+ ): NudgeCandidate[] {
74
+ const candidates: NudgeCandidate[] = [];
75
+
76
+ // 0. Initialization nudge
77
+ if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
78
+ candidates.push({
79
+ index: 0,
80
+ message: `QUEST REMINDER: You have made ${this.totalToolCalls} tool calls but have NEVER used the quest tool this session. USE the quest tool to initialize tracking and break your work into concrete steps. ${ACKNOWLEDGEMENT}`,
81
+ });
41
82
  }
42
83
 
43
- // 2. Complex-task entrypoint nudge
84
+ // 1. Complex-task entrypoint nudge
44
85
  if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
45
- this.nudgedThisTurn = true;
46
- return `QUEST REMINDER: Your latest prompt appears to be a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`;
86
+ candidates.push({
87
+ index: 1,
88
+ message: `QUEST REMINDER: Your latest prompt appears to be a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
89
+ });
47
90
  }
48
91
 
49
- // 3. Time-based alignment nudge
92
+ // 2. Time-based alignment nudge
50
93
  if (
51
94
  this.hasEverUsedQuestTool &&
52
95
  this.lastQuestToolTime > 0 &&
53
96
  this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
54
97
  Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
55
98
  ) {
56
- this.nudgedThisTurn = true;
57
- return `QUEST REMINDER: It has been a while since your last quest tool use and ${this.consecutiveNonQuestToolCalls} tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`;
99
+ candidates.push({
100
+ index: 2,
101
+ message: `QUEST REMINDER: It has been a while since your last quest tool use and ${this.consecutiveNonQuestToolCalls} tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`,
102
+ });
58
103
  }
59
104
 
60
- // 4. Zero-active sustained-work nudge
105
+ // 3. Zero-active sustained-work nudge
61
106
  if (
62
107
  this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
63
108
  activeQuestCount === 0
64
109
  ) {
65
- this.nudgedThisTurn = true;
66
- return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`;
110
+ candidates.push({
111
+ index: 3,
112
+ message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`,
113
+ });
67
114
  }
68
115
 
69
- // 5. Sub-quest suggestion nudge
116
+ // 4. Sub-quest suggestion nudge
70
117
  if (
71
118
  this.hasEverUsedQuestTool &&
72
- this.consecutiveNonQuestToolCalls >= this.config.nudges.subQuestSuggestionToolCallThreshold &&
119
+ this.consecutiveNonQuestToolCalls >= this.config.nudges.stepSuggestionToolCallThreshold &&
73
120
  activeQuestCount > 0 &&
74
121
  hasTopLevelQuestWithoutSubs
75
122
  ) {
76
- this.nudgedThisTurn = true;
77
- return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without sub-quests. Use the \`add\` action with \`parentId\` to break down complex tasks into smaller, trackable steps. ${ACKNOWLEDGEMENT}`;
123
+ candidates.push({
124
+ index: 4,
125
+ message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without steps. Use the \`split\` action to break down complex tasks into smaller, trackable steps. ${ACKNOWLEDGEMENT}`,
126
+ });
78
127
  }
79
128
 
80
- // 6. Stale-progress sustained-work nudge
129
+ // 5. Stale-progress sustained-work nudge
81
130
  if (
82
131
  this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
83
132
  activeQuestCount > 0
84
133
  ) {
85
- this.nudgedThisTurn = true;
86
- return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests.\nUPDATE your quest progress to reflect current status.\nALWAYS use sub quests to break down a quest into smaller steps, and to group related tasks together. ${ACKNOWLEDGEMENT}`;
134
+ candidates.push({
135
+ index: 5,
136
+ message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests.\nUPDATE your quest progress to reflect current status.\nALWAYS use steps to break down a quest into smaller steps, and to group related tasks together. ${ACKNOWLEDGEMENT}`,
137
+ });
87
138
  }
88
139
 
89
- return undefined;
140
+ return candidates;
90
141
  }
91
142
 
92
143
  private isComplexPrompt(prompt: string): boolean {
@@ -11,6 +11,11 @@ export const QUEST_ACTIONS = {
11
11
  clear: "clear",
12
12
  reorder: "reorder",
13
13
  revert: "revert",
14
+ reparent: "reparent",
15
+ rules: "rules",
16
+ skill: "skill",
17
+ split: "split",
18
+ add_step: "add_step",
14
19
  } as const;
15
20
 
16
21
  export const QUEST_ACTION_VALUES = [
@@ -22,6 +27,11 @@ export const QUEST_ACTION_VALUES = [
22
27
  QUEST_ACTIONS.clear,
23
28
  QUEST_ACTIONS.reorder,
24
29
  QUEST_ACTIONS.revert,
30
+ QUEST_ACTIONS.reparent,
31
+ QUEST_ACTIONS.rules,
32
+ QUEST_ACTIONS.skill,
33
+ QUEST_ACTIONS.split,
34
+ QUEST_ACTIONS.add_step,
25
35
  ] as const;
26
36
 
27
37
  export type QuestActionType = (typeof QUEST_ACTION_VALUES)[number];
@@ -33,6 +43,6 @@ export interface Quest {
33
43
  createdAt: number;
34
44
  }
35
45
 
36
- export interface SubQuest extends Quest {
46
+ export interface Step extends Quest {
37
47
  parentId: string;
38
48
  }
@@ -3,7 +3,7 @@ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { QuestLog } from "../quest/dataplane.js";
6
- import { formatQuestRow, formatSubQuestSpacerLine } from "./quests.js";
6
+ import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
7
7
 
8
8
  export class QuestListWidget {
9
9
  private cachedWidth?: number;
@@ -50,17 +50,17 @@ export class QuestListWidget {
50
50
 
51
51
  for (let i = 0; i < parents.length; i++) {
52
52
  const parent = parents[i];
53
- const subs = this.questLog.getSubQuests(parent.id);
53
+ const steps = this.questLog.getSteps(parent.id);
54
54
 
55
55
  const row = formatQuestRow(th, parent, this.config.ids.length, i + 1);
56
56
  lines.push(width > 0 ? truncateToWidth(row, width) : row);
57
57
 
58
- if (subs.length > 0) {
59
- lines.push(formatSubQuestSpacerLine(th, this.config.ids.length));
58
+ if (steps.length > 0) {
59
+ lines.push(formatStepSpacerLine(th, this.config.ids.length));
60
60
 
61
- for (const sub of subs) {
62
- const subRow = formatQuestRow(th, sub, this.config.ids.length);
63
- lines.push(width > 0 ? truncateToWidth(subRow, width) : subRow);
61
+ for (const step of steps) {
62
+ const stepRow = formatQuestRow(th, step, this.config.ids.length);
63
+ lines.push(width > 0 ? truncateToWidth(stepRow, width) : stepRow);
64
64
  }
65
65
 
66
66
  lines.push("");
@@ -1,17 +1,17 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { visibleWidth } from "@mariozechner/pi-tui";
3
- import type { Quest, SubQuest } from "../quest/types.js";
3
+ import type { Quest, Step } from "../quest/types.js";
4
4
 
5
5
  export function formatQuestRow(
6
6
  theme: Theme,
7
- q: Quest | SubQuest,
7
+ q: Quest | Step,
8
8
  idLength: number,
9
9
  pos?: number,
10
10
  ): string {
11
- const isSub = "parentId" in q && q.parentId;
11
+ const isStep = "parentId" in q && q.parentId;
12
12
 
13
- // position test is only relevant for quests, not subquests
14
- const posText = isSub ? "" : pos !== undefined ? `#${pos}` : "";
13
+ // position test is only relevant for quests, not steps
14
+ const posText = isStep ? "" : pos !== undefined ? `#${pos}` : "";
15
15
  const posWidth = visibleWidth(posText);
16
16
  const idText = `[${q.id}]`;
17
17
 
@@ -19,13 +19,13 @@ export function formatQuestRow(
19
19
  const spacing = " ".repeat(visibleWidth(`${16 ** idLength}`) - posWidth);
20
20
  const idStr = `${theme.fg("muted", idText)} ${theme.fg(q.done ? "dim" : "accent", `${posText}`)}${spacing}`;
21
21
 
22
- // for subquests, use a different marker and indent
23
- const markerNotDone = isSub ? theme.fg("muted", " └── ○ ") : theme.fg("muted", " ○ ");
24
- const markerDone = isSub ? theme.fg("muted", " └── ✓ ") : theme.fg("success", " ✓ ");
22
+ // for steps, use a different marker and indent
23
+ const markerNotDone = isStep ? theme.fg("muted", " └── ○ ") : theme.fg("muted", " ○ ");
24
+ const markerDone = isStep ? theme.fg("muted", " └── ✓ ") : theme.fg("success", " ✓ ");
25
25
  const marker = q.done ? markerDone : markerNotDone;
26
26
 
27
- // for subquests, use dim text to contrast with the parent quests
28
- const descColor = q.done || isSub ? "dim" : "text";
27
+ // for steps, use dim text to contrast with the parent quests
28
+ const descColor = q.done || isStep ? "dim" : "text";
29
29
  const desc = q.done
30
30
  ? theme.fg(descColor, theme.strikethrough(q.description))
31
31
  : theme.fg(descColor, q.description);
@@ -34,7 +34,7 @@ export function formatQuestRow(
34
34
  return line;
35
35
  }
36
36
 
37
- export function formatSubQuestSpacerLine(theme: Theme, idLength: number): string {
37
+ export function formatStepSpacerLine(theme: Theme, idLength: number): string {
38
38
  const idTextLength = visibleWidth(`${16 ** idLength}`);
39
39
  // spacing for `[id](position string)`
40
40
  const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(idTextLength)}`;
@@ -4,7 +4,7 @@ import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { Quest } from "../quest/types.js";
6
6
  import { QUEST_ACTIONS } from "../quest/types.js";
7
- import { formatQuestRow, formatSubQuestSpacerLine } from "./quests.js";
7
+ import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
8
8
 
9
9
  type QuestArgs = {
10
10
  action: string;
@@ -76,7 +76,7 @@ export function renderQuestResult(config: ResolvedConfig) {
76
76
  const parents = quests.filter((q) => !(q as Quest & { parentId?: string }).parentId);
77
77
  const lines: string[] = [];
78
78
 
79
- function getSubQuests(parentId: string) {
79
+ function getSteps(parentId: string) {
80
80
  return quests.filter((q) => (q as Quest & { parentId?: string }).parentId === parentId);
81
81
  }
82
82
 
@@ -85,8 +85,8 @@ export function renderQuestResult(config: ResolvedConfig) {
85
85
  const p = parents[j];
86
86
  if (renderedIds.has(p.id)) return true;
87
87
 
88
- const subs = getSubQuests(p.id).filter((sq) => renderedIds.has(sq.id));
89
- if (subs.length > 0) return true;
88
+ const steps = getSteps(p.id).filter((step) => renderedIds.has(step.id));
89
+ if (steps.length > 0) return true;
90
90
  }
91
91
 
92
92
  return false;
@@ -95,20 +95,20 @@ export function renderQuestResult(config: ResolvedConfig) {
95
95
  for (let i = 0; i < parents.length; i++) {
96
96
  const parent = parents[i];
97
97
  const parentIncluded = renderedIds.has(parent.id);
98
- const includedSubs = getSubQuests(parent.id).filter((sq) => renderedIds.has(sq.id));
98
+ const includedSteps = getSteps(parent.id).filter((step) => renderedIds.has(step.id));
99
99
 
100
- if (!parentIncluded && includedSubs.length === 0) continue;
100
+ if (!parentIncluded && includedSteps.length === 0) continue;
101
101
 
102
102
  if (parentIncluded) {
103
103
  lines.push(formatQuestRow(theme, parent, config.ids.length, i + 1));
104
- if (includedSubs.length > 0) lines.push(formatSubQuestSpacerLine(theme, config.ids.length));
104
+ if (includedSteps.length > 0) lines.push(formatStepSpacerLine(theme, config.ids.length));
105
105
  }
106
106
 
107
- for (const sub of includedSubs) {
108
- lines.push(formatQuestRow(theme, sub, config.ids.length));
107
+ for (const step of includedSteps) {
108
+ lines.push(formatQuestRow(theme, step, config.ids.length));
109
109
  }
110
110
 
111
- if (includedSubs.length > 0 && willRenderLater(i)) {
111
+ if (includedSteps.length > 0 && willRenderLater(i)) {
112
112
  lines.push("");
113
113
  }
114
114
  }
@@ -5,6 +5,15 @@ import { logger } from "../logger.js";
5
5
  import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
6
6
  import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
7
7
  import { QUEST_ACTIONS } from "../quest/types.js";
8
+
9
+ const SPLIT_DISPLAY_ACTIONS = [
10
+ QUEST_ACTIONS.add,
11
+ QUEST_ACTIONS.list,
12
+ QUEST_ACTIONS.split,
13
+ QUEST_ACTIONS.add_step,
14
+ QUEST_ACTIONS.revert,
15
+ ] as const;
16
+
8
17
  import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
9
18
  import { createQuestParams, type QuestParamsType } from "./params.js";
10
19
 
@@ -21,7 +30,20 @@ const toolHandlers: {
21
30
  return runTool(questLog, toolCallId, {
22
31
  type: QUEST_ACTIONS.add,
23
32
  descriptions: params.descriptions,
24
- parentId: params.parentId,
33
+ });
34
+ },
35
+ [QUEST_ACTIONS.split](questLog, params, toolCallId) {
36
+ return runTool(questLog, toolCallId, {
37
+ type: QUEST_ACTIONS.split,
38
+ id: params.id,
39
+ descriptions: params.descriptions,
40
+ });
41
+ },
42
+ [QUEST_ACTIONS.add_step](questLog, params, toolCallId) {
43
+ return runTool(questLog, toolCallId, {
44
+ type: QUEST_ACTIONS.add_step,
45
+ id: params.id,
46
+ descriptions: params.descriptions,
25
47
  });
26
48
  },
27
49
  [QUEST_ACTIONS.list](questLog, _params, toolCallId) {
@@ -50,6 +72,19 @@ const toolHandlers: {
50
72
  targetId: params.targetId,
51
73
  });
52
74
  },
75
+ [QUEST_ACTIONS.reparent](questLog, params, toolCallId) {
76
+ return runTool(questLog, toolCallId, {
77
+ type: QUEST_ACTIONS.reparent,
78
+ id: params.id,
79
+ parentId: params.parentId,
80
+ });
81
+ },
82
+ [QUEST_ACTIONS.rules](questLog, _params, toolCallId) {
83
+ return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules });
84
+ },
85
+ [QUEST_ACTIONS.skill](questLog, _params, toolCallId) {
86
+ return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill });
87
+ },
53
88
  [QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
54
89
  return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
55
90
  },
@@ -63,14 +98,13 @@ function runTool(
63
98
  const result = questLog.execute(action);
64
99
  logger.debug("quests:tool", "execute-complete", { toolCallId, success: result.success });
65
100
 
66
- const displayQuests =
67
- action.type === QUEST_ACTIONS.add ||
68
- action.type === QUEST_ACTIONS.list ||
69
- action.type === QUEST_ACTIONS.revert
70
- ? questLog.getAll()
71
- : result.quest
72
- ? [result.quest]
73
- : undefined;
101
+ const displayQuests = SPLIT_DISPLAY_ACTIONS.includes(
102
+ action.type as (typeof SPLIT_DISPLAY_ACTIONS)[number],
103
+ )
104
+ ? questLog.getAll()
105
+ : result.quest
106
+ ? [result.quest]
107
+ : undefined;
74
108
 
75
109
  return makeToolResult(result.message, questLog, displayQuests);
76
110
  }
@@ -95,9 +129,11 @@ export function registerQuestTool(
95
129
  name: "quest",
96
130
  label: "Quest",
97
131
  description:
98
- "Manage the session quest log including top-level quests and sub-quests. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
132
+ "Manage the session quest log and retrieve the complete quest system documentation. " +
133
+ "Use this VERY frequently to track tasks, plans, and progress. " +
134
+ "When you need to understand quests, steps, rules, or best practices, use action: 'skill' or action: 'rules'.",
99
135
  promptSnippet:
100
- "Add (with optional parentId for sub-quests), list, toggle, update, delete, clear, or revert quest items",
136
+ "Manage quests and steps, or retrieve quest rules and best practices via skill/rules",
101
137
  promptGuidelines: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
102
138
  parameters: createQuestParams(config.ids.length),
103
139
  execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
@@ -18,13 +18,7 @@ export function createQuestParams(idLength: number) {
18
18
  description: "New description (required for update action)",
19
19
  }),
20
20
  ),
21
- parentId: Type.Optional(
22
- Type.String({
23
- pattern,
24
- description:
25
- "Parent quest hex ID. Use this to break a large task into smaller, trackable steps. When provided, the added quest becomes a sub-quest under the parent.",
26
- }),
27
- ),
21
+
28
22
  id: Type.Optional(
29
23
  Type.String({
30
24
  pattern,
@@ -38,6 +32,12 @@ export function createQuestParams(idLength: number) {
38
32
  "Target quest ID for reorder action. The quest will be moved to just before the target quest.",
39
33
  }),
40
34
  ),
35
+ parentId: Type.Optional(
36
+ Type.String({
37
+ pattern,
38
+ description: "Parent quest ID for reparent action. Omit to promote to top-level.",
39
+ }),
40
+ ),
41
41
  all: Type.Optional(
42
42
  Type.Boolean({
43
43
  description: "Clear all quests when true (defaults to clearing only completed quests)",