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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.0] - 2026-04-14
6
+
7
+ - feat: add reparent action to promote, demote, or move quests and steps with validation and revert support
8
+ - feat: add rules and skill actions that return the built-in quest-management skill document
9
+ - feat: add split action to break a quest into steps with add_step alias and command support
10
+ - feat: rename sub-quest terminology to step across types, commands, docs, and prompts
11
+ - feat: improve error messages with actionable recovery hints and refine nudge behavior
12
+
5
13
  ## [0.4.0] - 2026-04-13
6
14
 
7
15
  - feat: add configurable shortcut to open quest list, default: `ctrl+shift+l`
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-quests
2
2
 
3
- [![version 0.4.0](https://img.shields.io/badge/version-0.4.0-blue)](CHANGELOG.md)
3
+ [![version 0.5.0](https://img.shields.io/badge/version-0.5.0-blue)](CHANGELOG.md)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
5
5
  [![pi extension](https://img.shields.io/badge/pi-extension-purple)](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quests",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A quest-log for your pi. Keep your agent on track, one quest at a time.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -18,11 +18,13 @@ type MutatingCommand = Extract<
18
18
  {
19
19
  action:
20
20
  | typeof QUEST_ACTIONS.add
21
+ | typeof QUEST_ACTIONS.split
21
22
  | typeof QUEST_ACTIONS.toggle
22
23
  | typeof QUEST_ACTIONS.update
23
24
  | typeof QUEST_ACTIONS.delete
24
25
  | typeof QUEST_ACTIONS.clear
25
26
  | typeof QUEST_ACTIONS.reorder
27
+ | typeof QUEST_ACTIONS.reparent
26
28
  | typeof QUEST_ACTIONS.revert;
27
29
  }
28
30
  >["action"];
@@ -33,7 +35,11 @@ const commandActionBuilders: {
33
35
  [QUEST_ACTIONS.add]: (p) => ({
34
36
  type: QUEST_ACTIONS.add,
35
37
  descriptions: p.descriptions,
36
- parentId: p.parentId,
38
+ }),
39
+ [QUEST_ACTIONS.split]: (p) => ({
40
+ type: QUEST_ACTIONS.split,
41
+ id: p.id,
42
+ descriptions: p.descriptions,
37
43
  }),
38
44
  [QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle, id: p.id }),
39
45
  [QUEST_ACTIONS.update]: (p) => ({
@@ -48,6 +54,11 @@ const commandActionBuilders: {
48
54
  id: p.id,
49
55
  targetId: p.targetId,
50
56
  }),
57
+ [QUEST_ACTIONS.reparent]: (p) => ({
58
+ type: QUEST_ACTIONS.reparent,
59
+ id: p.id,
60
+ parentId: p.parentId,
61
+ }),
51
62
  [QUEST_ACTIONS.revert]: () => ({ type: QUEST_ACTIONS.revert }),
52
63
  };
53
64
 
@@ -120,11 +131,13 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog, config
120
131
  return;
121
132
  }
122
133
  case QUEST_ACTIONS.add:
134
+ case QUEST_ACTIONS.split:
123
135
  case QUEST_ACTIONS.toggle:
124
136
  case QUEST_ACTIONS.update:
125
137
  case QUEST_ACTIONS.delete:
126
138
  case QUEST_ACTIONS.clear:
127
139
  case QUEST_ACTIONS.reorder:
140
+ case QUEST_ACTIONS.reparent:
128
141
  case QUEST_ACTIONS.revert: {
129
142
  const builder = commandActionBuilders[parsed.action];
130
143
  const action = builder(parsed as never);
@@ -138,11 +151,13 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog, config
138
151
  logger.debug("quests:cmd", "help");
139
152
 
140
153
  const lines = ["Available /quests subcommands:"];
141
- lines.push(" add [--parent <id>] <description> - Add a new quest or sub-quest");
154
+ lines.push(" add <description> - Add a new top-level quest");
155
+ lines.push(" add-step <id> <description> - Split a quest into a step");
142
156
  lines.push(" list - List all quests");
143
157
  lines.push(" toggle <id> - Toggle quest completion");
144
158
  lines.push(" delete <id> - Delete a quest");
145
159
  lines.push(" update <id> <desc> - Update a quest description");
160
+ lines.push(" reparent <id> [parentId] - Reparent a quest/step (omit parentId to promote)");
146
161
  lines.push(" reorder <id> <targetId> - Reorder a quest before the target quest");
147
162
  lines.push(" revert - Revert the last quest change");
148
163
  lines.push(" clear [all] - Clear completed quests, or optionally all quests");
@@ -2,7 +2,8 @@ import { logger } from "../logger.js";
2
2
  import { QUEST_ACTIONS } from "../quest/types.js";
3
3
 
4
4
  export type ParsedArgs =
5
- | { action: typeof QUEST_ACTIONS.add; descriptions: string[]; parentId?: string }
5
+ | { action: typeof QUEST_ACTIONS.add; descriptions: string[] }
6
+ | { action: typeof QUEST_ACTIONS.split; id: string; descriptions: string[] }
6
7
  | { action: typeof QUEST_ACTIONS.list }
7
8
  | { action: typeof QUEST_ACTIONS.toggle; id: string }
8
9
  | { action: typeof QUEST_ACTIONS.update; id: string; description: string }
@@ -10,6 +11,7 @@ export type ParsedArgs =
10
11
  | { action: typeof QUEST_ACTIONS.clear; all?: boolean }
11
12
  | { action: typeof QUEST_ACTIONS.reorder; id: string; targetId: string }
12
13
  | { action: typeof QUEST_ACTIONS.revert }
14
+ | { action: typeof QUEST_ACTIONS.reparent; id: string; parentId?: string }
13
15
  | { action: "help" }
14
16
  | { action: "version" }
15
17
  | { action: "changelog" }
@@ -41,30 +43,31 @@ export function parseQuestArgs(args: string, idLength = 2): ParsedArgs {
41
43
  if (command === QUEST_ACTIONS.revert) return { action: QUEST_ACTIONS.revert };
42
44
 
43
45
  // Quest actions with arguments
44
- if (command === QUEST_ACTIONS.add) return parseAddArgs(rest, idLength);
46
+ if (command === QUEST_ACTIONS.add) return parseAddArgs(rest);
47
+ if (command === "add-step") return parseAddStepArgs(rest, idLength);
45
48
  if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest, idLength);
46
49
  if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest, idLength);
47
50
  if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest, idLength);
48
51
  if (command === QUEST_ACTIONS.reorder) return parseReorderArgs(rest, idLength);
52
+ if (command === QUEST_ACTIONS.reparent) return parseReparentArgs(rest, idLength);
49
53
 
50
54
  return { error: `Unknown subcommand: ${command}. Use /quests help to see available commands.` };
51
55
  }
52
56
 
53
- function parseAddArgs(tokens: string[], idLength: number): ParsedArgs {
54
- let parentId: string | undefined;
55
- let descTokens = tokens;
56
- const pIdx = tokens.indexOf("--parent");
57
- if (pIdx !== -1) {
58
- const pid = tokens[pIdx + 1]?.toLowerCase() ?? "";
59
- const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
60
- if (!pattern.test(pid)) return { error: "Usage: /quests add [--parent <id>] <description>" };
61
- parentId = pid;
62
- descTokens = tokens.slice(0, pIdx).concat(tokens.slice(pIdx + 2));
63
- }
64
- const description = descTokens.join(" ").trim();
65
- if (!description) return { error: "Usage: /quests add [--parent <id>] <description>" };
57
+ function parseAddArgs(tokens: string[]): ParsedArgs {
58
+ const description = tokens.join(" ").trim();
59
+ if (!description) return { error: "Usage: /quests add <description>" };
60
+
61
+ return { action: QUEST_ACTIONS.add, descriptions: [description] };
62
+ }
66
63
 
67
- return { action: QUEST_ACTIONS.add, descriptions: [description], parentId };
64
+ function parseAddStepArgs(tokens: string[], idLength: number): ParsedArgs {
65
+ const id = tokens[0] ? tokens[0].toLowerCase() : "";
66
+ const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
67
+ if (!pattern.test(id)) return { error: "Usage: /quests add-step <id> <description>" };
68
+ const description = tokens.slice(1).join(" ").trim();
69
+ if (!description) return { error: "Usage: /quests add-step <id> <description>" };
70
+ return { action: QUEST_ACTIONS.split, id, descriptions: [description] };
68
71
  }
69
72
 
70
73
  function parseIdAction(
@@ -106,3 +109,15 @@ function parseReorderArgs(tokens: string[], idLength: number): ParsedArgs {
106
109
 
107
110
  return { action: QUEST_ACTIONS.reorder, id, targetId };
108
111
  }
112
+
113
+ function parseReparentArgs(tokens: string[], idLength: number): ParsedArgs {
114
+ const id = tokens[0] ? tokens[0].toLowerCase() : "";
115
+ const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
116
+ if (!pattern.test(id)) return { error: "Usage: /quests reparent <id> [parentId]" };
117
+
118
+ const parentId = tokens[1] ? tokens[1].toLowerCase() : undefined;
119
+ if (parentId && !pattern.test(parentId))
120
+ return { error: "Usage: /quests reparent <id> [parentId]" };
121
+
122
+ return { action: QUEST_ACTIONS.reparent, id, parentId };
123
+ }
package/src/config.ts CHANGED
@@ -12,7 +12,7 @@ export interface ResolvedConfig {
12
12
  timeBasedToolCallThreshold: number;
13
13
  zeroActiveToolCallThreshold: number;
14
14
  staleProgressToolCallThreshold: number;
15
- subQuestSuggestionToolCallThreshold: number;
15
+ stepSuggestionToolCallThreshold: number;
16
16
  complexTaskKeywords: string[];
17
17
  };
18
18
  validation: { fakeDonePattern: string };
@@ -40,12 +40,12 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
40
40
  ids: { length: 2 },
41
41
  display: { pageSize: 10, progressBarMaxWidth: 24 },
42
42
  nudges: {
43
- toolCallThreshold: 3,
44
- hintIntervalMinutes: 8,
45
- timeBasedToolCallThreshold: 3,
46
- zeroActiveToolCallThreshold: 5,
47
- staleProgressToolCallThreshold: 10,
48
- subQuestSuggestionToolCallThreshold: 6,
43
+ toolCallThreshold: 8,
44
+ hintIntervalMinutes: 4,
45
+ timeBasedToolCallThreshold: 5,
46
+ zeroActiveToolCallThreshold: 8,
47
+ staleProgressToolCallThreshold: 16,
48
+ stepSuggestionToolCallThreshold: 10,
49
49
  complexTaskKeywords: [...DEFAULT_COMPLEX_TASK_KEYWORDS],
50
50
  },
51
51
  validation: { fakeDonePattern: DEFAULT_FAKE_DONE_PATTERN },
@@ -130,9 +130,9 @@ export function getConfig(ctx: Pick<ExtensionContext, "cwd">): ResolvedConfig {
130
130
  staleProgressToolCallThreshold:
131
131
  user.nudges?.staleProgressToolCallThreshold ??
132
132
  DEFAULT_CONFIG.nudges.staleProgressToolCallThreshold,
133
- subQuestSuggestionToolCallThreshold:
134
- user.nudges?.subQuestSuggestionToolCallThreshold ??
135
- DEFAULT_CONFIG.nudges.subQuestSuggestionToolCallThreshold,
133
+ stepSuggestionToolCallThreshold:
134
+ user.nudges?.stepSuggestionToolCallThreshold ??
135
+ DEFAULT_CONFIG.nudges.stepSuggestionToolCallThreshold,
136
136
  complexTaskKeywords: [
137
137
  ...(user.nudges?.complexTaskKeywords ?? DEFAULT_CONFIG.nudges.complexTaskKeywords),
138
138
  ],
package/src/index.ts CHANGED
@@ -77,7 +77,7 @@ export default function (pi: ExtensionAPI): void {
77
77
  const activeQuestCount = activeQuests.length;
78
78
  const activeTopLevel = activeQuests.filter((q) => !(q as { parentId?: string }).parentId);
79
79
  const hasTopLevelQuestWithoutSubs = activeTopLevel.some(
80
- (q) => !allQuests.some((sq) => (sq as { parentId?: string }).parentId === q.id),
80
+ (q) => !allQuests.some((step) => (step as { parentId?: string }).parentId === q.id),
81
81
  );
82
82
  const nudge = tracker.getNudge(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
83
83
 
@@ -100,16 +100,17 @@ export default function (pi: ExtensionAPI): void {
100
100
 
101
101
  pi.on("before_agent_start", async (event) => {
102
102
  const quests = questLog.getAll();
103
- let reminder = QUEST_PROMPT_REMINDER.join("\n");
103
+ let reminder = "";
104
104
  if (quests.length > 0) {
105
105
  const remaining = quests.filter((q) => !q.done).length;
106
106
  const list = formatQuestList(quests);
107
-
108
- reminder += `\n\nActive quests (${remaining}/${quests.length}):\n${list}`;
107
+ reminder = `Active quests (${remaining}/${quests.length}):\n${list}\n\nKeep quest progress updated as you work.`;
108
+ } else {
109
+ reminder = `${QUEST_PROMPT_REMINDER.join("\n")}\n\nBefore adding any independent quests, clear any previously completed quests from the log to keep it focused on current work.`;
109
110
  }
110
111
 
111
112
  return {
112
- systemPrompt: `# Quest Management\n${QUEST_PROMPT_GATE}${event.systemPrompt}\n\n## Quest Management\n${reminder}. Before adding any independent quests, clear any previously completed quests from the log to keep it focused on current work.`,
113
+ systemPrompt: `# Quest Management\n${QUEST_PROMPT_GATE}${event.systemPrompt}\n\n## Quest Management\n${reminder}`,
113
114
  };
114
115
  });
115
116
 
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: quest-management
3
+ description: Quest management best practices for the pi-quests extension. Use when the user asks about tracking tasks, managing quests, using the quest tool, or when you need guidance on how to structure session work into quests and steps.
4
+ ---
5
+
6
+ # Quest Management
7
+
8
+ ## When to use quests
9
+
10
+ Use the quest tool at the start of any non-trivial task. If the request involves multiple steps, files, tool calls, or minion delegation, track it with quests.
11
+
12
+ ## Capabilities
13
+
14
+ | Action | Purpose |
15
+ |--------|---------|
16
+ | `add` | Create top-level quests |
17
+ | `split` / `add_step` | Break a quest into steps under a parent |
18
+ | `reparent` | Promote, demote, or move a quest/step via optional `parentId` |
19
+ | `toggle` | Mark a quest or step done/undone |
20
+ | `update` | Change a quest description |
21
+ | `delete` | Remove a quest or step |
22
+ | `clear` | Remove completed quests (or all with `all: true`) |
23
+ | `reorder` | Change the priority order of top-level quests |
24
+ | `revert` | Undo the most recent mutating action |
25
+ | `list` | View all quests and steps |
26
+
27
+ ## Common patterns
28
+
29
+ ### Multi-step task workflow
30
+ 1. Add many top-level quests for the overall goal
31
+ 2. Split them into steps for each distinct deliverable
32
+ 3. Execute steps sequentially, toggling each done as you finish
33
+ 4. Toggle the parent quest done only after all steps are complete
34
+
35
+ ### Delegation workflow
36
+ 1. Add a quest for the delegated task
37
+ 2. Spawn the minion and assign the work
38
+ 3. Toggle the quest done when the minion returns successfully
39
+
40
+ ### Reparenting
41
+ - Promote a step: `reparent <step-id>` (omit `parentId`)
42
+ - Demote a quest: `reparent <quest-id> <parent-id>`
43
+ - Move a step: `reparent <step-id> <new-parent-id>`
44
+
45
+ ## Rules
46
+
47
+ - Steps cannot have nested steps. Only top-level quests can be parents.
48
+ - A parent with incomplete steps cannot be toggled done or deleted.
49
+ - Deleting a done parent cascade-deletes its done steps.
50
+ - Revert only undoes the most recent mutating action.
51
+ - Quest IDs are random hex strings shown in square brackets (e.g., `[01]`, `[a3f1]`).
52
+ - Always use the hex ID for actions, never the positional number.
53
+
54
+ ## Gotchas
55
+
56
+ - Use toggle for done states. NEVER use `update` to append "DONE", "- DONE", or any completion marker to a quest description.
57
+ - IDs are hex, not positional. The list shows `#1 [01] ...`. Use `01` in tool calls, not `1`.
58
+ - Steps cannot be reordered independently. Use `reorder` on top-level quests only.
59
+ - Clear completed quests before adding unrelated work. This keeps the log focused and reduces context noise.
60
+ - Parent blocked?: If you cannot toggle a parent done, check that all its steps are toggled done first.
package/src/prompts.ts CHANGED
@@ -1,3 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export function getQuestSkillDocument(): string {
8
+ return readFileSync(join(__dirname, "prompts", "skill.md"), "utf-8");
9
+ }
10
+
1
11
  export const QUEST_PROMPT_REMINDER = [
2
12
  "Use the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation.",
3
13
  "Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
@@ -6,13 +16,14 @@ export const QUEST_PROMPT_REMINDER = [
6
16
  "When reading a skill file, implementation plan, or protocol document that contains numbered steps or checklists, add those steps as quests immediately so they are tracked, and reorder them as needed in the quest log.",
7
17
  "It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
8
18
  "ALWAYS use the toggle action to mark a quest done. NEVER use the update action to append 'DONE', '- DONE', or any completion marker to a quest description.",
9
- "ALWAYS use sub-quests to break down a complex quest into smaller steps. To create a sub-quest, use the `add` action and set `parentId` to the parent quest's hex ID. Use sub-quests for multi-step tasks, minion delegations, or when a quest has more than one distinct deliverable.",
10
- "A parent quest cannot be toggled done until all of its sub-quests are completed. Sub-quests cannot be reordered independently.",
19
+ "ALWAYS use steps to break down a complex quest into smaller steps. To create a step, use the `split` action with the parent quest's hex ID. Use steps for multi-step tasks, minion delegations, or when a quest has more than one distinct deliverable.",
20
+ "A parent quest cannot be toggled done until all of its steps are completed. Steps cannot be reordered independently.",
11
21
  "Before delegating to a minion, add a quest for the delegated task.",
12
22
  "As work evolves, use the reorder action to reflect changes in priority",
13
23
  "For reorder, provide the targetId (the hex ID of the quest to insert before).",
14
24
  "If you are unsure what to do next, use the list action to check active quests.",
15
25
  "Always use the hex ID shown in brackets (e.g. 0a, ff, 44e1, f712a) for toggle, update, delete, and reorder actions.",
26
+ "When you need to understand the quest system, rules, or best practices, use the quest tool with action: 'skill' or action: 'rules' rather than reading documentation files.",
16
27
  ] as const;
17
28
 
18
29
  export const QUEST_PROMPT_GATE =