pi-quests 0.3.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 +16 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +43 -11
- package/src/commands/parse-args.ts +51 -22
- package/src/config.ts +148 -0
- package/src/index.ts +55 -23
- package/src/prompts/skill.md +60 -0
- package/src/prompts.ts +16 -2
- package/src/quest/dataplane.ts +473 -104
- package/src/quest/formatters.ts +119 -16
- package/src/quest/tracker.ts +99 -39
- package/src/quest/types.ts +15 -2
- package/src/renderers/commands.ts +46 -24
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +80 -31
- package/src/tools/handler.ts +59 -16
- package/src/tools/params.ts +44 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
13
|
+
## [0.4.0] - 2026-04-13
|
|
14
|
+
|
|
15
|
+
- feat: add configurable shortcut to open quest list, default: `ctrl+shift+l`
|
|
16
|
+
- feat: add sub-quest support with lifecycle management
|
|
17
|
+
- feat: add user-configurable settings via pi settings files
|
|
18
|
+
- feat: use random 2-digit hex IDs and targetId-based reorder
|
|
19
|
+
- fix: add .pi/setting.json for development
|
|
20
|
+
|
|
5
21
|
## [0.3.0] - 2026-04-11
|
|
6
22
|
|
|
7
23
|
- feat: add reorder action to move quests by position (dataplane, tool, /quests command, renderer, revert support)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-quests
|
|
2
2
|
|
|
3
|
-
[](CHANGELOG.md)
|
|
4
4
|
[](LICENSE.md)
|
|
5
5
|
[](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
|
|
6
6
|
|
|
@@ -47,6 +47,24 @@ Available /quests subcommands:
|
|
|
47
47
|

|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
pi-quests reads configuration from pi's settings files. Global settings live at `~/.pi/agent/settings.json` and project overrides go in `.pi/settings.json`.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"pi-quests": {
|
|
57
|
+
"ids": { "length": 2 },
|
|
58
|
+
"display": {
|
|
59
|
+
"pageSize": 10,
|
|
60
|
+
"progressBarMaxWidth": 24
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [docs/configuration.md](docs/configuration.md) for the full options reference.
|
|
67
|
+
|
|
50
68
|
## Documentation
|
|
51
69
|
|
|
52
70
|
| Doc | Description |
|
|
@@ -54,6 +72,7 @@ Available /quests subcommands:
|
|
|
54
72
|
| [Pattern](docs/pattern.md) | "How do I...?" recipes for common workflows |
|
|
55
73
|
| [Quests](docs/quests.md) | What are quests? |
|
|
56
74
|
| [Reference](docs/reference.md) | Complete tool and command schemas, types |
|
|
75
|
+
| [Configuration](docs/configuration.md) | Settings, overrides, and examples |
|
|
57
76
|
| [Architecture](docs/architecture.md) | Module map, data flow diagrams, design decisions |
|
|
58
77
|
| [Changelog](CHANGELOG.md) | Version history |
|
|
59
78
|
|
package/package.json
CHANGED
package/src/commands/handler.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionCommandContext,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
} from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
8
|
import { logger } from "../logger.js";
|
|
4
9
|
import type { QuestAction, QuestLog } from "../quest/dataplane.js";
|
|
5
10
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
@@ -13,11 +18,13 @@ type MutatingCommand = Extract<
|
|
|
13
18
|
{
|
|
14
19
|
action:
|
|
15
20
|
| typeof QUEST_ACTIONS.add
|
|
21
|
+
| typeof QUEST_ACTIONS.split
|
|
16
22
|
| typeof QUEST_ACTIONS.toggle
|
|
17
23
|
| typeof QUEST_ACTIONS.update
|
|
18
24
|
| typeof QUEST_ACTIONS.delete
|
|
19
25
|
| typeof QUEST_ACTIONS.clear
|
|
20
26
|
| typeof QUEST_ACTIONS.reorder
|
|
27
|
+
| typeof QUEST_ACTIONS.reparent
|
|
21
28
|
| typeof QUEST_ACTIONS.revert;
|
|
22
29
|
}
|
|
23
30
|
>["action"];
|
|
@@ -25,7 +32,15 @@ type MutatingCommand = Extract<
|
|
|
25
32
|
const commandActionBuilders: {
|
|
26
33
|
[K in MutatingCommand]: (parsed: Extract<ParsedArgs, { action: K }>) => QuestAction;
|
|
27
34
|
} = {
|
|
28
|
-
[QUEST_ACTIONS.add]: (p) => ({
|
|
35
|
+
[QUEST_ACTIONS.add]: (p) => ({
|
|
36
|
+
type: QUEST_ACTIONS.add,
|
|
37
|
+
descriptions: p.descriptions,
|
|
38
|
+
}),
|
|
39
|
+
[QUEST_ACTIONS.split]: (p) => ({
|
|
40
|
+
type: QUEST_ACTIONS.split,
|
|
41
|
+
id: p.id,
|
|
42
|
+
descriptions: p.descriptions,
|
|
43
|
+
}),
|
|
29
44
|
[QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle, id: p.id }),
|
|
30
45
|
[QUEST_ACTIONS.update]: (p) => ({
|
|
31
46
|
type: QUEST_ACTIONS.update,
|
|
@@ -37,16 +52,32 @@ const commandActionBuilders: {
|
|
|
37
52
|
[QUEST_ACTIONS.reorder]: (p) => ({
|
|
38
53
|
type: QUEST_ACTIONS.reorder,
|
|
39
54
|
id: p.id,
|
|
40
|
-
|
|
55
|
+
targetId: p.targetId,
|
|
56
|
+
}),
|
|
57
|
+
[QUEST_ACTIONS.reparent]: (p) => ({
|
|
58
|
+
type: QUEST_ACTIONS.reparent,
|
|
59
|
+
id: p.id,
|
|
60
|
+
parentId: p.parentId,
|
|
41
61
|
}),
|
|
42
62
|
[QUEST_ACTIONS.revert]: () => ({ type: QUEST_ACTIONS.revert }),
|
|
43
63
|
};
|
|
44
64
|
|
|
45
|
-
export function
|
|
65
|
+
export function openQuestList(
|
|
66
|
+
_pi: ExtensionAPI,
|
|
67
|
+
questLog: QuestLog,
|
|
68
|
+
config: ResolvedConfig,
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
return ctx.ui.custom(
|
|
72
|
+
(_, theme, __, done) => new QuestListWidget(questLog, theme, () => done(undefined), config),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog, config: ResolvedConfig) {
|
|
46
77
|
return async function handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
47
78
|
logger.debug("quests:cmd", "handler", { args, hasUI: ctx.hasUI });
|
|
48
79
|
|
|
49
|
-
const parsed = parseQuestArgs(args);
|
|
80
|
+
const parsed = parseQuestArgs(args, config.ids.length);
|
|
50
81
|
if ("error" in parsed) {
|
|
51
82
|
logger.debug("quests:cmd", "handler-error", { error: parsed.error });
|
|
52
83
|
ctx.ui.notify(parsed.error, "error");
|
|
@@ -94,20 +125,19 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
94
125
|
}
|
|
95
126
|
|
|
96
127
|
logger.debug("quests:cmd", "list-open-widget", { count: questLog.getAll().length });
|
|
97
|
-
await ctx
|
|
98
|
-
(_, theme, __, done) =>
|
|
99
|
-
new QuestListWidget(questLog.getAll(), theme, () => done(undefined)),
|
|
100
|
-
);
|
|
128
|
+
await openQuestList(pi, questLog, config, ctx);
|
|
101
129
|
|
|
102
130
|
logger.debug("quests:cmd", "list-widget-closed");
|
|
103
131
|
return;
|
|
104
132
|
}
|
|
105
133
|
case QUEST_ACTIONS.add:
|
|
134
|
+
case QUEST_ACTIONS.split:
|
|
106
135
|
case QUEST_ACTIONS.toggle:
|
|
107
136
|
case QUEST_ACTIONS.update:
|
|
108
137
|
case QUEST_ACTIONS.delete:
|
|
109
138
|
case QUEST_ACTIONS.clear:
|
|
110
139
|
case QUEST_ACTIONS.reorder:
|
|
140
|
+
case QUEST_ACTIONS.reparent:
|
|
111
141
|
case QUEST_ACTIONS.revert: {
|
|
112
142
|
const builder = commandActionBuilders[parsed.action];
|
|
113
143
|
const action = builder(parsed as never);
|
|
@@ -121,12 +151,14 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
121
151
|
logger.debug("quests:cmd", "help");
|
|
122
152
|
|
|
123
153
|
const lines = ["Available /quests subcommands:"];
|
|
124
|
-
lines.push(" add <description>
|
|
154
|
+
lines.push(" add <description> - Add a new top-level quest");
|
|
155
|
+
lines.push(" add-step <id> <description> - Split a quest into a step");
|
|
125
156
|
lines.push(" list - List all quests");
|
|
126
157
|
lines.push(" toggle <id> - Toggle quest completion");
|
|
127
158
|
lines.push(" delete <id> - Delete a quest");
|
|
128
159
|
lines.push(" update <id> <desc> - Update a quest description");
|
|
129
|
-
lines.push("
|
|
160
|
+
lines.push(" reparent <id> [parentId] - Reparent a quest/step (omit parentId to promote)");
|
|
161
|
+
lines.push(" reorder <id> <targetId> - Reorder a quest before the target quest");
|
|
130
162
|
lines.push(" revert - Revert the last quest change");
|
|
131
163
|
lines.push(" clear [all] - Clear completed quests, or optionally all quests");
|
|
132
164
|
lines.push(" version - Show version");
|
|
@@ -3,13 +3,15 @@ import { QUEST_ACTIONS } from "../quest/types.js";
|
|
|
3
3
|
|
|
4
4
|
export type ParsedArgs =
|
|
5
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
|
-
| { action: typeof QUEST_ACTIONS.toggle; id:
|
|
8
|
-
| { action: typeof QUEST_ACTIONS.update; id:
|
|
9
|
-
| { action: typeof QUEST_ACTIONS.delete; id:
|
|
8
|
+
| { action: typeof QUEST_ACTIONS.toggle; id: string }
|
|
9
|
+
| { action: typeof QUEST_ACTIONS.update; id: string; description: string }
|
|
10
|
+
| { action: typeof QUEST_ACTIONS.delete; id: string }
|
|
10
11
|
| { action: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
11
|
-
| { action: typeof QUEST_ACTIONS.reorder; id:
|
|
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" }
|
|
@@ -18,7 +20,7 @@ export type ParsedArgs =
|
|
|
18
20
|
/**
|
|
19
21
|
* Parse user input from the /quests command into structured arguments.
|
|
20
22
|
*/
|
|
21
|
-
export function parseQuestArgs(args: string): ParsedArgs {
|
|
23
|
+
export function parseQuestArgs(args: string, idLength = 2): ParsedArgs {
|
|
22
24
|
logger.debug("quests:cmd", "parse-args", { args });
|
|
23
25
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
24
26
|
|
|
@@ -42,10 +44,12 @@ export function parseQuestArgs(args: string): ParsedArgs {
|
|
|
42
44
|
|
|
43
45
|
// Quest actions with arguments
|
|
44
46
|
if (command === QUEST_ACTIONS.add) return parseAddArgs(rest);
|
|
45
|
-
if (command ===
|
|
46
|
-
if (command === QUEST_ACTIONS.
|
|
47
|
-
if (command === QUEST_ACTIONS.
|
|
48
|
-
if (command === QUEST_ACTIONS.
|
|
47
|
+
if (command === "add-step") return parseAddStepArgs(rest, idLength);
|
|
48
|
+
if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest, idLength);
|
|
49
|
+
if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest, idLength);
|
|
50
|
+
if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest, idLength);
|
|
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
|
}
|
|
@@ -57,21 +61,31 @@ function parseAddArgs(tokens: string[]): ParsedArgs {
|
|
|
57
61
|
return { action: QUEST_ACTIONS.add, descriptions: [description] };
|
|
58
62
|
}
|
|
59
63
|
|
|
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] };
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
function parseIdAction(
|
|
61
74
|
action: typeof QUEST_ACTIONS.toggle | typeof QUEST_ACTIONS.delete,
|
|
62
75
|
tokens: string[],
|
|
76
|
+
idLength: number,
|
|
63
77
|
): ParsedArgs {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
78
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
79
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
80
|
+
if (!pattern.test(id)) return { error: `Usage: /quests ${action} <id>` };
|
|
67
81
|
|
|
68
82
|
return { action, id };
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
function parseUpdateArgs(tokens: string[]): ParsedArgs {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
85
|
+
function parseUpdateArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
86
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
87
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
88
|
+
if (!pattern.test(id)) return { error: "Usage: /quests update <id> <description>" };
|
|
75
89
|
|
|
76
90
|
const description = tokens.slice(1).join(" ").trim();
|
|
77
91
|
if (!description) return { error: "Usage: /quests update <id> <description>" };
|
|
@@ -85,10 +99,25 @@ function parseClearArgs(tokens: string[]): ParsedArgs {
|
|
|
85
99
|
return { action: QUEST_ACTIONS.clear, all };
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
function parseReorderArgs(tokens: string[]): ParsedArgs {
|
|
89
|
-
const id =
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
function parseReorderArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
103
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
104
|
+
const targetId = tokens[1] ? tokens[1].toLowerCase() : "";
|
|
105
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
106
|
+
|
|
107
|
+
if (!pattern.test(id) || !pattern.test(targetId))
|
|
108
|
+
return { error: "Usage: /quests reorder <id> <targetId>" };
|
|
109
|
+
|
|
110
|
+
return { action: QUEST_ACTIONS.reorder, id, targetId };
|
|
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 };
|
|
94
123
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface ResolvedConfig {
|
|
7
|
+
ids: { length: number };
|
|
8
|
+
display: { pageSize: number; progressBarMaxWidth: number };
|
|
9
|
+
nudges: {
|
|
10
|
+
toolCallThreshold: number;
|
|
11
|
+
hintIntervalMinutes: number;
|
|
12
|
+
timeBasedToolCallThreshold: number;
|
|
13
|
+
zeroActiveToolCallThreshold: number;
|
|
14
|
+
staleProgressToolCallThreshold: number;
|
|
15
|
+
stepSuggestionToolCallThreshold: number;
|
|
16
|
+
complexTaskKeywords: string[];
|
|
17
|
+
};
|
|
18
|
+
validation: { fakeDonePattern: string };
|
|
19
|
+
shortcuts?: { openQuests?: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_COMPLEX_TASK_KEYWORDS = [
|
|
23
|
+
"implement",
|
|
24
|
+
"refactor",
|
|
25
|
+
"investigate",
|
|
26
|
+
"review",
|
|
27
|
+
"analyze",
|
|
28
|
+
"audit",
|
|
29
|
+
"plan",
|
|
30
|
+
"design",
|
|
31
|
+
"create",
|
|
32
|
+
"build",
|
|
33
|
+
"write",
|
|
34
|
+
"fix",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_FAKE_DONE_PATTERN = String.raw`\s[-\u2013\u2014]\s*(DONE|COMPLETED|FINISHED)$|\s[([](DONE|COMPLETED|FINISHED)[)\]]$`;
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_CONFIG: ResolvedConfig = {
|
|
40
|
+
ids: { length: 2 },
|
|
41
|
+
display: { pageSize: 10, progressBarMaxWidth: 24 },
|
|
42
|
+
nudges: {
|
|
43
|
+
toolCallThreshold: 8,
|
|
44
|
+
hintIntervalMinutes: 4,
|
|
45
|
+
timeBasedToolCallThreshold: 5,
|
|
46
|
+
zeroActiveToolCallThreshold: 8,
|
|
47
|
+
staleProgressToolCallThreshold: 16,
|
|
48
|
+
stepSuggestionToolCallThreshold: 10,
|
|
49
|
+
complexTaskKeywords: [...DEFAULT_COMPLEX_TASK_KEYWORDS],
|
|
50
|
+
},
|
|
51
|
+
validation: { fakeDonePattern: DEFAULT_FAKE_DONE_PATTERN },
|
|
52
|
+
shortcuts: {},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function deepMerge(
|
|
56
|
+
target: Record<string, unknown>,
|
|
57
|
+
source: Record<string, unknown>,
|
|
58
|
+
): Record<string, unknown> {
|
|
59
|
+
const result: Record<string, unknown> = { ...target };
|
|
60
|
+
for (const key of Object.keys(source)) {
|
|
61
|
+
if (
|
|
62
|
+
source[key] &&
|
|
63
|
+
typeof source[key] === "object" &&
|
|
64
|
+
!Array.isArray(source[key]) &&
|
|
65
|
+
result[key] &&
|
|
66
|
+
typeof result[key] === "object" &&
|
|
67
|
+
!Array.isArray(result[key])
|
|
68
|
+
) {
|
|
69
|
+
result[key] = deepMerge(
|
|
70
|
+
result[key] as Record<string, unknown>,
|
|
71
|
+
source[key] as Record<string, unknown>,
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
result[key] = source[key];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadSettings(cwd: string): Record<string, unknown> {
|
|
81
|
+
const settings: Record<string, unknown> = {};
|
|
82
|
+
const globalPath = join(getAgentDir(), "settings.json");
|
|
83
|
+
if (existsSync(globalPath)) {
|
|
84
|
+
try {
|
|
85
|
+
Object.assign(settings, JSON.parse(readFileSync(globalPath, "utf-8")));
|
|
86
|
+
} catch {
|
|
87
|
+
/* ignore */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const projectPath = join(cwd, ".pi", "settings.json");
|
|
91
|
+
if (existsSync(projectPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const project = JSON.parse(readFileSync(projectPath, "utf-8")) as Record<string, unknown>;
|
|
94
|
+
if (project["pi-quests"] && typeof project["pi-quests"] === "object") {
|
|
95
|
+
const globalPiQuests =
|
|
96
|
+
settings["pi-quests"] && typeof settings["pi-quests"] === "object"
|
|
97
|
+
? (settings["pi-quests"] as Record<string, unknown>)
|
|
98
|
+
: {};
|
|
99
|
+
settings["pi-quests"] = deepMerge(
|
|
100
|
+
globalPiQuests,
|
|
101
|
+
project["pi-quests"] as Record<string, unknown>,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
/* ignore */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return settings;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getConfig(ctx: Pick<ExtensionContext, "cwd">): ResolvedConfig {
|
|
112
|
+
const settings = loadSettings(ctx.cwd);
|
|
113
|
+
const user = (settings["pi-quests"] ?? {}) as Partial<ResolvedConfig>;
|
|
114
|
+
return {
|
|
115
|
+
ids: { length: user.ids?.length ?? DEFAULT_CONFIG.ids.length },
|
|
116
|
+
display: {
|
|
117
|
+
pageSize: user.display?.pageSize ?? DEFAULT_CONFIG.display.pageSize,
|
|
118
|
+
progressBarMaxWidth:
|
|
119
|
+
user.display?.progressBarMaxWidth ?? DEFAULT_CONFIG.display.progressBarMaxWidth,
|
|
120
|
+
},
|
|
121
|
+
nudges: {
|
|
122
|
+
toolCallThreshold: user.nudges?.toolCallThreshold ?? DEFAULT_CONFIG.nudges.toolCallThreshold,
|
|
123
|
+
hintIntervalMinutes:
|
|
124
|
+
user.nudges?.hintIntervalMinutes ?? DEFAULT_CONFIG.nudges.hintIntervalMinutes,
|
|
125
|
+
timeBasedToolCallThreshold:
|
|
126
|
+
user.nudges?.timeBasedToolCallThreshold ?? DEFAULT_CONFIG.nudges.timeBasedToolCallThreshold,
|
|
127
|
+
zeroActiveToolCallThreshold:
|
|
128
|
+
user.nudges?.zeroActiveToolCallThreshold ??
|
|
129
|
+
DEFAULT_CONFIG.nudges.zeroActiveToolCallThreshold,
|
|
130
|
+
staleProgressToolCallThreshold:
|
|
131
|
+
user.nudges?.staleProgressToolCallThreshold ??
|
|
132
|
+
DEFAULT_CONFIG.nudges.staleProgressToolCallThreshold,
|
|
133
|
+
stepSuggestionToolCallThreshold:
|
|
134
|
+
user.nudges?.stepSuggestionToolCallThreshold ??
|
|
135
|
+
DEFAULT_CONFIG.nudges.stepSuggestionToolCallThreshold,
|
|
136
|
+
complexTaskKeywords: [
|
|
137
|
+
...(user.nudges?.complexTaskKeywords ?? DEFAULT_CONFIG.nudges.complexTaskKeywords),
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
validation: {
|
|
141
|
+
fakeDonePattern:
|
|
142
|
+
user.validation?.fakeDonePattern ?? DEFAULT_CONFIG.validation.fakeDonePattern,
|
|
143
|
+
},
|
|
144
|
+
shortcuts: {
|
|
145
|
+
openQuests: user.shortcuts?.openQuests,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { UserMessage } from "@mariozechner/pi-ai";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import {
|
|
3
|
+
import type { KeyId } from "@mariozechner/pi-tui";
|
|
4
|
+
import { createQuestsHandler, openQuestList } from "./commands/handler.js";
|
|
5
|
+
import { DEFAULT_CONFIG, getConfig, type ResolvedConfig } from "./config.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
4
7
|
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "./prompts.js";
|
|
5
8
|
import { QuestLog } from "./quest/dataplane.js";
|
|
9
|
+
import { formatQuestList } from "./quest/formatters.js";
|
|
6
10
|
import { QuestUsageTracker } from "./quest/tracker.js";
|
|
7
11
|
import { questChangelogRenderer } from "./renderers/changelog.js";
|
|
8
12
|
import { registerQuestTool } from "./tools/handler.js";
|
|
@@ -18,10 +22,42 @@ import { registerQuestTool } from "./tools/handler.js";
|
|
|
18
22
|
* - Provide rollback support to restore a previous snapshot.
|
|
19
23
|
*/
|
|
20
24
|
export default function (pi: ExtensionAPI): void {
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
let questLog = new QuestLog();
|
|
26
|
+
let tracker = new QuestUsageTracker(DEFAULT_CONFIG);
|
|
27
|
+
let config: ResolvedConfig = DEFAULT_CONFIG;
|
|
28
|
+
|
|
29
|
+
const shortcutKey = getConfig({ cwd: process.cwd() }).shortcuts?.openQuests ?? "ctrl+shift+l";
|
|
30
|
+
logger.debug("quests:shortcut", "register", { key: shortcutKey });
|
|
31
|
+
pi.registerShortcut(shortcutKey as KeyId, {
|
|
32
|
+
description: "Open quest list",
|
|
33
|
+
handler: async (ctx) => {
|
|
34
|
+
logger.debug("quests:shortcut", "handler", { hasUI: ctx.hasUI });
|
|
35
|
+
if (!ctx.hasUI) {
|
|
36
|
+
logger.debug("quests:shortcut", "no-ui");
|
|
37
|
+
ctx.ui.notify("Interactive mode required", "error");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
logger.debug("quests:shortcut", "open", { questCount: questLog.getAll().length });
|
|
41
|
+
await openQuestList(pi, questLog, config, ctx);
|
|
42
|
+
logger.debug("quests:shortcut", "closed");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
47
|
+
config = getConfig(ctx);
|
|
48
|
+
questLog = new QuestLog(config);
|
|
49
|
+
tracker = new QuestUsageTracker(config);
|
|
50
|
+
questLog.reconstructFromSession(ctx);
|
|
51
|
+
|
|
52
|
+
registerQuestTool(pi, questLog, config);
|
|
53
|
+
|
|
54
|
+
const questsHandler = createQuestsHandler(pi, questLog, config);
|
|
55
|
+
pi.registerCommand("quests", {
|
|
56
|
+
description: "Quest commands: /quests [help] to see usage",
|
|
57
|
+
handler: questsHandler,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
23
60
|
|
|
24
|
-
pi.on("session_start", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
25
61
|
pi.on("session_tree", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
26
62
|
|
|
27
63
|
pi.on("turn_start", async () => tracker.clearTurnNudge());
|
|
@@ -36,17 +72,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
36
72
|
.map((m) => (typeof m.content === "string" ? m.content : ""))
|
|
37
73
|
.join("\n");
|
|
38
74
|
|
|
39
|
-
const
|
|
40
|
-
const
|
|
75
|
+
const allQuests = questLog.getAll();
|
|
76
|
+
const activeQuests = allQuests.filter((q) => !q.done);
|
|
77
|
+
const activeQuestCount = activeQuests.length;
|
|
78
|
+
const activeTopLevel = activeQuests.filter((q) => !(q as { parentId?: string }).parentId);
|
|
79
|
+
const hasTopLevelQuestWithoutSubs = activeTopLevel.some(
|
|
80
|
+
(q) => !allQuests.some((step) => (step as { parentId?: string }).parentId === q.id),
|
|
81
|
+
);
|
|
82
|
+
const nudge = tracker.getNudge(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
|
|
41
83
|
|
|
42
|
-
const fakeDoneRegex =
|
|
43
|
-
/\s[-–—]\s*(DONE|COMPLETED|FINISHED)$|\s[([](DONE|COMPLETED|FINISHED)[)\]]$/i;
|
|
84
|
+
const fakeDoneRegex = new RegExp(config.validation.fakeDonePattern, "i");
|
|
44
85
|
const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
|
|
45
86
|
if (!nudge && !fakeDone) return undefined;
|
|
46
87
|
|
|
47
88
|
let content = nudge ?? "";
|
|
48
89
|
if (fakeDone) {
|
|
49
|
-
content += `\nQUEST REMINDER: Quest
|
|
90
|
+
content += `\nQUEST REMINDER: Quest [${fakeDone.id}] has a completion marker appended to its description but is not toggled done. Use the toggle action to mark it done. NEVER append completion markers to descriptions via the update action.`;
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
const reminder: UserMessage = {
|
|
@@ -59,14 +100,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
59
100
|
|
|
60
101
|
pi.on("before_agent_start", async (event) => {
|
|
61
102
|
const quests = questLog.getAll();
|
|
62
|
-
let reminder =
|
|
103
|
+
let reminder = "";
|
|
63
104
|
if (quests.length > 0) {
|
|
64
105
|
const remaining = quests.filter((q) => !q.done).length;
|
|
65
|
-
const list = quests
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
reminder += `\n\nActive quests (${remaining}/${quests.length}):\n${list}`;
|
|
106
|
+
const list = formatQuestList(quests);
|
|
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.`;
|
|
70
110
|
}
|
|
71
111
|
|
|
72
112
|
return {
|
|
@@ -74,14 +114,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
74
114
|
};
|
|
75
115
|
});
|
|
76
116
|
|
|
77
|
-
registerQuestTool(pi, questLog);
|
|
78
|
-
|
|
79
117
|
// Register custom message renderers
|
|
80
118
|
pi.registerMessageRenderer("quest-changelog", questChangelogRenderer);
|
|
81
|
-
|
|
82
|
-
// Register the top-level /quests command dispatcher.
|
|
83
|
-
pi.registerCommand("quests", {
|
|
84
|
-
description: "Quest commands: /quests [help] to see usage",
|
|
85
|
-
handler: createQuestsHandler(pi, questLog),
|
|
86
|
-
});
|
|
87
119
|
}
|
|
@@ -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,10 +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
|
-
"
|
|
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.",
|
|
10
21
|
"Before delegating to a minion, add a quest for the delegated task.",
|
|
11
|
-
"
|
|
22
|
+
"As work evolves, use the reorder action to reflect changes in priority",
|
|
23
|
+
"For reorder, provide the targetId (the hex ID of the quest to insert before).",
|
|
12
24
|
"If you are unsure what to do next, use the list action to check active quests.",
|
|
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.",
|
|
13
27
|
] as const;
|
|
14
28
|
|
|
15
29
|
export const QUEST_PROMPT_GATE =
|