pi-quests 0.2.0 → 0.4.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 +18 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +36 -11
- package/src/commands/parse-args.ts +52 -22
- package/src/config.ts +148 -0
- package/src/index.ts +66 -26
- package/src/prompts.ts +19 -0
- package/src/quest/dataplane.ts +393 -72
- package/src/quest/formatters.ts +31 -16
- package/src/quest/tracker.ts +34 -25
- package/src/quest/types.ts +7 -2
- package/src/renderers/commands.ts +46 -22
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +81 -27
- package/src/tools/handler.ts +25 -17
- package/src/tools/params.ts +44 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-04-13
|
|
6
|
+
|
|
7
|
+
- feat: add configurable shortcut to open quest list, default: `ctrl+shift+l`
|
|
8
|
+
- feat: add sub-quest support with lifecycle management
|
|
9
|
+
- feat: add user-configurable settings via pi settings files
|
|
10
|
+
- feat: use random 2-digit hex IDs and targetId-based reorder
|
|
11
|
+
- fix: add .pi/setting.json for development
|
|
12
|
+
|
|
13
|
+
## [0.3.0] - 2026-04-11
|
|
14
|
+
|
|
15
|
+
- feat: add reorder action to move quests by position (dataplane, tool, /quests command, renderer, revert support)
|
|
16
|
+
- feat: extend clear action with optional all flag to remove all quests regardless of done state
|
|
17
|
+
- feat: detect appended completion markers (e.g. "- DONE") and inject correction nudge via context hook
|
|
18
|
+
- feat: update list rendering to show 1-based positions instead of raw IDs
|
|
19
|
+
- fix: inline type imports across test files to comply with no-inline-imports rule
|
|
20
|
+
- chore: extract shared prompt strings into prompts.ts to eliminate duplication
|
|
21
|
+
- chore: fix release workflow output mapping in create-release job
|
|
22
|
+
|
|
5
23
|
## [0.2.0] - 2026-04-11
|
|
6
24
|
|
|
7
25
|
- feat: add dynamic quest usage nudges via context hook
|
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";
|
|
@@ -17,6 +22,7 @@ type MutatingCommand = Extract<
|
|
|
17
22
|
| typeof QUEST_ACTIONS.update
|
|
18
23
|
| typeof QUEST_ACTIONS.delete
|
|
19
24
|
| typeof QUEST_ACTIONS.clear
|
|
25
|
+
| typeof QUEST_ACTIONS.reorder
|
|
20
26
|
| typeof QUEST_ACTIONS.revert;
|
|
21
27
|
}
|
|
22
28
|
>["action"];
|
|
@@ -24,7 +30,11 @@ type MutatingCommand = Extract<
|
|
|
24
30
|
const commandActionBuilders: {
|
|
25
31
|
[K in MutatingCommand]: (parsed: Extract<ParsedArgs, { action: K }>) => QuestAction;
|
|
26
32
|
} = {
|
|
27
|
-
[QUEST_ACTIONS.add]: (p) => ({
|
|
33
|
+
[QUEST_ACTIONS.add]: (p) => ({
|
|
34
|
+
type: QUEST_ACTIONS.add,
|
|
35
|
+
descriptions: p.descriptions,
|
|
36
|
+
parentId: p.parentId,
|
|
37
|
+
}),
|
|
28
38
|
[QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle, id: p.id }),
|
|
29
39
|
[QUEST_ACTIONS.update]: (p) => ({
|
|
30
40
|
type: QUEST_ACTIONS.update,
|
|
@@ -32,15 +42,31 @@ const commandActionBuilders: {
|
|
|
32
42
|
description: p.description,
|
|
33
43
|
}),
|
|
34
44
|
[QUEST_ACTIONS.delete]: (p) => ({ type: QUEST_ACTIONS.delete, id: p.id }),
|
|
35
|
-
[QUEST_ACTIONS.clear]: () => ({ type: QUEST_ACTIONS.clear }),
|
|
45
|
+
[QUEST_ACTIONS.clear]: (p) => ({ type: QUEST_ACTIONS.clear, all: p.all }),
|
|
46
|
+
[QUEST_ACTIONS.reorder]: (p) => ({
|
|
47
|
+
type: QUEST_ACTIONS.reorder,
|
|
48
|
+
id: p.id,
|
|
49
|
+
targetId: p.targetId,
|
|
50
|
+
}),
|
|
36
51
|
[QUEST_ACTIONS.revert]: () => ({ type: QUEST_ACTIONS.revert }),
|
|
37
52
|
};
|
|
38
53
|
|
|
39
|
-
export function
|
|
54
|
+
export function openQuestList(
|
|
55
|
+
_pi: ExtensionAPI,
|
|
56
|
+
questLog: QuestLog,
|
|
57
|
+
config: ResolvedConfig,
|
|
58
|
+
ctx: ExtensionContext,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
return ctx.ui.custom(
|
|
61
|
+
(_, theme, __, done) => new QuestListWidget(questLog, theme, () => done(undefined), config),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog, config: ResolvedConfig) {
|
|
40
66
|
return async function handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
41
67
|
logger.debug("quests:cmd", "handler", { args, hasUI: ctx.hasUI });
|
|
42
68
|
|
|
43
|
-
const parsed = parseQuestArgs(args);
|
|
69
|
+
const parsed = parseQuestArgs(args, config.ids.length);
|
|
44
70
|
if ("error" in parsed) {
|
|
45
71
|
logger.debug("quests:cmd", "handler-error", { error: parsed.error });
|
|
46
72
|
ctx.ui.notify(parsed.error, "error");
|
|
@@ -88,10 +114,7 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
logger.debug("quests:cmd", "list-open-widget", { count: questLog.getAll().length });
|
|
91
|
-
await ctx
|
|
92
|
-
(_, theme, __, done) =>
|
|
93
|
-
new QuestListWidget(questLog.getAll(), theme, () => done(undefined)),
|
|
94
|
-
);
|
|
117
|
+
await openQuestList(pi, questLog, config, ctx);
|
|
95
118
|
|
|
96
119
|
logger.debug("quests:cmd", "list-widget-closed");
|
|
97
120
|
return;
|
|
@@ -101,6 +124,7 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
101
124
|
case QUEST_ACTIONS.update:
|
|
102
125
|
case QUEST_ACTIONS.delete:
|
|
103
126
|
case QUEST_ACTIONS.clear:
|
|
127
|
+
case QUEST_ACTIONS.reorder:
|
|
104
128
|
case QUEST_ACTIONS.revert: {
|
|
105
129
|
const builder = commandActionBuilders[parsed.action];
|
|
106
130
|
const action = builder(parsed as never);
|
|
@@ -114,13 +138,14 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
114
138
|
logger.debug("quests:cmd", "help");
|
|
115
139
|
|
|
116
140
|
const lines = ["Available /quests subcommands:"];
|
|
117
|
-
lines.push(" add <description> - Add a new quest");
|
|
141
|
+
lines.push(" add [--parent <id>] <description> - Add a new quest or sub-quest");
|
|
118
142
|
lines.push(" list - List all quests");
|
|
119
143
|
lines.push(" toggle <id> - Toggle quest completion");
|
|
120
144
|
lines.push(" delete <id> - Delete a quest");
|
|
121
145
|
lines.push(" update <id> <desc> - Update a quest description");
|
|
146
|
+
lines.push(" reorder <id> <targetId> - Reorder a quest before the target quest");
|
|
122
147
|
lines.push(" revert - Revert the last quest change");
|
|
123
|
-
lines.push(" clear
|
|
148
|
+
lines.push(" clear [all] - Clear completed quests, or optionally all quests");
|
|
124
149
|
lines.push(" version - Show version");
|
|
125
150
|
lines.push(" changelog - Show changelog");
|
|
126
151
|
lines.push(" h, help - Show this help message");
|
|
@@ -2,12 +2,13 @@ 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[] }
|
|
5
|
+
| { action: typeof QUEST_ACTIONS.add; descriptions: string[]; parentId?: string }
|
|
6
6
|
| { 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:
|
|
10
|
-
| { action: typeof QUEST_ACTIONS.clear }
|
|
7
|
+
| { action: typeof QUEST_ACTIONS.toggle; id: string }
|
|
8
|
+
| { action: typeof QUEST_ACTIONS.update; id: string; description: string }
|
|
9
|
+
| { action: typeof QUEST_ACTIONS.delete; id: string }
|
|
10
|
+
| { action: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
11
|
+
| { action: typeof QUEST_ACTIONS.reorder; id: string; targetId: string }
|
|
11
12
|
| { action: typeof QUEST_ACTIONS.revert }
|
|
12
13
|
| { action: "help" }
|
|
13
14
|
| { action: "version" }
|
|
@@ -17,7 +18,7 @@ export type ParsedArgs =
|
|
|
17
18
|
/**
|
|
18
19
|
* Parse user input from the /quests command into structured arguments.
|
|
19
20
|
*/
|
|
20
|
-
export function parseQuestArgs(args: string): ParsedArgs {
|
|
21
|
+
export function parseQuestArgs(args: string, idLength = 2): ParsedArgs {
|
|
21
22
|
logger.debug("quests:cmd", "parse-args", { args });
|
|
22
23
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
23
24
|
|
|
@@ -36,43 +37,72 @@ export function parseQuestArgs(args: string): ParsedArgs {
|
|
|
36
37
|
|
|
37
38
|
// Quest actions without arguments
|
|
38
39
|
if (command === QUEST_ACTIONS.list) return { action: QUEST_ACTIONS.list };
|
|
39
|
-
if (command === QUEST_ACTIONS.clear) return
|
|
40
|
+
if (command === QUEST_ACTIONS.clear) return parseClearArgs(rest);
|
|
40
41
|
if (command === QUEST_ACTIONS.revert) return { action: QUEST_ACTIONS.revert };
|
|
41
42
|
|
|
42
43
|
// Quest actions with arguments
|
|
43
|
-
if (command === QUEST_ACTIONS.add) return parseAddArgs(rest);
|
|
44
|
-
if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest);
|
|
45
|
-
if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest);
|
|
46
|
-
if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest);
|
|
44
|
+
if (command === QUEST_ACTIONS.add) return parseAddArgs(rest, idLength);
|
|
45
|
+
if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest, idLength);
|
|
46
|
+
if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest, idLength);
|
|
47
|
+
if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest, idLength);
|
|
48
|
+
if (command === QUEST_ACTIONS.reorder) return parseReorderArgs(rest, idLength);
|
|
47
49
|
|
|
48
50
|
return { error: `Unknown subcommand: ${command}. Use /quests help to see available commands.` };
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function parseAddArgs(tokens: string[]): ParsedArgs {
|
|
52
|
-
|
|
53
|
-
|
|
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>" };
|
|
54
66
|
|
|
55
|
-
return { action: QUEST_ACTIONS.add, descriptions: [description] };
|
|
67
|
+
return { action: QUEST_ACTIONS.add, descriptions: [description], parentId };
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
function parseIdAction(
|
|
59
71
|
action: typeof QUEST_ACTIONS.toggle | typeof QUEST_ACTIONS.delete,
|
|
60
72
|
tokens: string[],
|
|
73
|
+
idLength: number,
|
|
61
74
|
): ParsedArgs {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
75
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
76
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
77
|
+
if (!pattern.test(id)) return { error: `Usage: /quests ${action} <id>` };
|
|
65
78
|
|
|
66
79
|
return { action, id };
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
function parseUpdateArgs(tokens: string[]): ParsedArgs {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
82
|
+
function parseUpdateArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
83
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
84
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
85
|
+
if (!pattern.test(id)) return { error: "Usage: /quests update <id> <description>" };
|
|
73
86
|
|
|
74
87
|
const description = tokens.slice(1).join(" ").trim();
|
|
75
88
|
if (!description) return { error: "Usage: /quests update <id> <description>" };
|
|
76
89
|
|
|
77
90
|
return { action: QUEST_ACTIONS.update, id, description };
|
|
78
91
|
}
|
|
92
|
+
|
|
93
|
+
function parseClearArgs(tokens: string[]): ParsedArgs {
|
|
94
|
+
const all = tokens[0] === "all";
|
|
95
|
+
if (tokens.length > 0 && !all) return { error: "Usage: /quests clear [all]" };
|
|
96
|
+
return { action: QUEST_ACTIONS.clear, all };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseReorderArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
100
|
+
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
101
|
+
const targetId = tokens[1] ? tokens[1].toLowerCase() : "";
|
|
102
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
103
|
+
|
|
104
|
+
if (!pattern.test(id) || !pattern.test(targetId))
|
|
105
|
+
return { error: "Usage: /quests reorder <id> <targetId>" };
|
|
106
|
+
|
|
107
|
+
return { action: QUEST_ACTIONS.reorder, id, targetId };
|
|
108
|
+
}
|
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
|
+
subQuestSuggestionToolCallThreshold: 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: 3,
|
|
44
|
+
hintIntervalMinutes: 8,
|
|
45
|
+
timeBasedToolCallThreshold: 3,
|
|
46
|
+
zeroActiveToolCallThreshold: 5,
|
|
47
|
+
staleProgressToolCallThreshold: 10,
|
|
48
|
+
subQuestSuggestionToolCallThreshold: 6,
|
|
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
|
+
subQuestSuggestionToolCallThreshold:
|
|
134
|
+
user.nudges?.subQuestSuggestionToolCallThreshold ??
|
|
135
|
+
DEFAULT_CONFIG.nudges.subQuestSuggestionToolCallThreshold,
|
|
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,6 +1,12 @@
|
|
|
1
|
+
import type { UserMessage } from "@mariozechner/pi-ai";
|
|
1
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
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";
|
|
7
|
+
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "./prompts.js";
|
|
3
8
|
import { QuestLog } from "./quest/dataplane.js";
|
|
9
|
+
import { formatQuestList } from "./quest/formatters.js";
|
|
4
10
|
import { QuestUsageTracker } from "./quest/tracker.js";
|
|
5
11
|
import { questChangelogRenderer } from "./renderers/changelog.js";
|
|
6
12
|
import { registerQuestTool } from "./tools/handler.js";
|
|
@@ -16,10 +22,42 @@ import { registerQuestTool } from "./tools/handler.js";
|
|
|
16
22
|
* - Provide rollback support to restore a previous snapshot.
|
|
17
23
|
*/
|
|
18
24
|
export default function (pi: ExtensionAPI): void {
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
});
|
|
21
60
|
|
|
22
|
-
pi.on("session_start", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
23
61
|
pi.on("session_tree", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
24
62
|
|
|
25
63
|
pi.on("turn_start", async () => tracker.clearTurnNudge());
|
|
@@ -34,13 +72,27 @@ export default function (pi: ExtensionAPI): void {
|
|
|
34
72
|
.map((m) => (typeof m.content === "string" ? m.content : ""))
|
|
35
73
|
.join("\n");
|
|
36
74
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
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((sq) => (sq as { parentId?: string }).parentId === q.id),
|
|
81
|
+
);
|
|
82
|
+
const nudge = tracker.getNudge(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
|
|
83
|
+
|
|
84
|
+
const fakeDoneRegex = new RegExp(config.validation.fakeDonePattern, "i");
|
|
85
|
+
const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
|
|
86
|
+
if (!nudge && !fakeDone) return undefined;
|
|
40
87
|
|
|
41
|
-
|
|
88
|
+
let content = nudge ?? "";
|
|
89
|
+
if (fakeDone) {
|
|
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.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reminder: UserMessage = {
|
|
42
94
|
role: "user",
|
|
43
|
-
content:
|
|
95
|
+
content: content.trim(),
|
|
44
96
|
timestamp: Date.now(),
|
|
45
97
|
};
|
|
46
98
|
return { messages: [...event.messages, reminder] };
|
|
@@ -48,31 +100,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
48
100
|
|
|
49
101
|
pi.on("before_agent_start", async (event) => {
|
|
50
102
|
const quests = questLog.getAll();
|
|
51
|
-
|
|
52
|
-
"# Quest Management\nBefore reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests. ALWAYS break broad requests into concrete steps.\n\n";
|
|
53
|
-
|
|
54
|
-
let reminder =
|
|
55
|
-
"## Quest Management\nUse the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation. It is critical that you toggle quests to done as soon as you complete them. NEVER batch up multiple tasks before marking them completed.\n\nNEVER create a single vague quest for broad requests. Analyze the user's intent and break it into specific, independent, actionable quests that each represent a concrete step.";
|
|
103
|
+
let reminder = QUEST_PROMPT_REMINDER.join("\n");
|
|
56
104
|
if (quests.length > 0) {
|
|
57
105
|
const remaining = quests.filter((q) => !q.done).length;
|
|
58
|
-
const list = quests
|
|
59
|
-
.map((q) => `#${q.id} [${q.done ? "x" : " "}] ${q.description}`)
|
|
60
|
-
.join("\n");
|
|
106
|
+
const list = formatQuestList(quests);
|
|
61
107
|
|
|
62
108
|
reminder += `\n\nActive quests (${remaining}/${quests.length}):\n${list}`;
|
|
63
109
|
}
|
|
64
110
|
|
|
65
|
-
return {
|
|
111
|
+
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
|
+
};
|
|
66
114
|
});
|
|
67
115
|
|
|
68
|
-
registerQuestTool(pi, questLog);
|
|
69
|
-
|
|
70
116
|
// Register custom message renderers
|
|
71
117
|
pi.registerMessageRenderer("quest-changelog", questChangelogRenderer);
|
|
72
|
-
|
|
73
|
-
// Register the top-level /quests command dispatcher.
|
|
74
|
-
pi.registerCommand("quests", {
|
|
75
|
-
description: "Quest commands: /quests [help] to see usage",
|
|
76
|
-
handler: createQuestsHandler(pi, questLog),
|
|
77
|
-
});
|
|
78
118
|
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const QUEST_PROMPT_REMINDER = [
|
|
2
|
+
"Use the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
3
|
+
"Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
|
|
4
|
+
"Do not create a single vague quest for broad requests. Break them into concrete, independent steps.",
|
|
5
|
+
"When the user gives a plan or a list of tasks, add them as quests immediately.",
|
|
6
|
+
"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
|
+
"It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
|
|
8
|
+
"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.",
|
|
11
|
+
"Before delegating to a minion, add a quest for the delegated task.",
|
|
12
|
+
"As work evolves, use the reorder action to reflect changes in priority",
|
|
13
|
+
"For reorder, provide the targetId (the hex ID of the quest to insert before).",
|
|
14
|
+
"If you are unsure what to do next, use the list action to check active quests.",
|
|
15
|
+
"Always use the hex ID shown in brackets (e.g. 0a, ff, 44e1, f712a) for toggle, update, delete, and reorder actions.",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const QUEST_PROMPT_GATE =
|
|
19
|
+
"Before reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests. ALWAYS break broad requests into concrete steps.";
|