pi-quests 0.6.0 → 0.6.1

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.1] - 2026-04-17
6
+
7
+ - fix: make quest prompt injection static and cache-friendly with `nudges.enable` and `display.showStatus` toggles
8
+
5
9
  ## [0.6.0] - 2026-04-17
6
10
 
7
11
  - feat: add footer quest progress indicator with configurable icon
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-quests
2
2
 
3
- [![version 0.6.0](https://img.shields.io/badge/version-0.6.0-blue)](CHANGELOG.md)
3
+ [![version 0.6.1](https://img.shields.io/badge/version-0.6.1-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.6.0",
3
+ "version": "0.6.1",
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": [
package/src/config.ts CHANGED
@@ -5,8 +5,9 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
5
5
 
6
6
  export interface ResolvedConfig {
7
7
  ids: { length: number };
8
- display: { pageSize: number; progressBarMaxWidth: number; icon: string };
8
+ display: { pageSize: number; progressBarMaxWidth: number; icon: string; showStatus: boolean };
9
9
  nudges: {
10
+ enable: boolean;
10
11
  toolCallThreshold: number;
11
12
  hintIntervalMinutes: number;
12
13
  timeBasedToolCallThreshold: number;
@@ -35,8 +36,9 @@ export const DEFAULT_FAKE_DONE_PATTERN = String.raw`\s[-\u2013\u2014]\s*(DONE|CO
35
36
 
36
37
  export const DEFAULT_CONFIG: ResolvedConfig = {
37
38
  ids: { length: 2 },
38
- display: { pageSize: 10, progressBarMaxWidth: 24, icon: "󰣏" },
39
+ display: { pageSize: 10, progressBarMaxWidth: 24, icon: "󰣏", showStatus: true },
39
40
  nudges: {
41
+ enable: true,
40
42
  toolCallThreshold: 8,
41
43
  hintIntervalMinutes: 4,
42
44
  timeBasedToolCallThreshold: 8,
@@ -115,8 +117,10 @@ export function getConfig(ctx: Pick<ExtensionContext, "cwd">): ResolvedConfig {
115
117
  progressBarMaxWidth:
116
118
  user.display?.progressBarMaxWidth ?? DEFAULT_CONFIG.display.progressBarMaxWidth,
117
119
  icon: user.display?.icon ?? DEFAULT_CONFIG.display.icon,
120
+ showStatus: user.display?.showStatus ?? DEFAULT_CONFIG.display.showStatus,
118
121
  },
119
122
  nudges: {
123
+ enable: user.nudges?.enable ?? DEFAULT_CONFIG.nudges.enable,
120
124
  toolCallThreshold: user.nudges?.toolCallThreshold ?? DEFAULT_CONFIG.nudges.toolCallThreshold,
121
125
  hintIntervalMinutes:
122
126
  user.nudges?.hintIntervalMinutes ?? DEFAULT_CONFIG.nudges.hintIntervalMinutes,
package/src/index.ts CHANGED
@@ -6,7 +6,6 @@ import { DEFAULT_CONFIG, getConfig, type ResolvedConfig } from "./config.js";
6
6
  import { logger } from "./logger.js";
7
7
  import { QUEST_PROMPT_GATE } from "./prompts.js";
8
8
  import { QuestLog } from "./quest/dataplane.js";
9
- import { formatQuestList } from "./quest/formatters.js";
10
9
  import { QuestUsageTracker } from "./quest/tracker.js";
11
10
  import { questChangelogRenderer } from "./renderers/changelog.js";
12
11
  import { QuestStatusWidget } from "./renderers/status.js";
@@ -22,11 +21,20 @@ import { registerQuestTool } from "./tools/handler.js";
22
21
  * - Snapshot relevant session state at each quest milestone.
23
22
  * - Provide rollback support to restore a previous snapshot.
24
23
  */
24
+ const NO_QUESTS_REMINDER =
25
+ "No active quests. Use the quest tool to track your work. Use action: 'skill' for usage documentation.";
26
+
27
+ const FAKE_DONE_REMINDER =
28
+ "QUEST REMINDER: A quest has a completion marker appended to its description but is not toggled done. Use the list action to find it, then toggle it done. NEVER append completion markers via update.";
29
+
25
30
  export default function (pi: ExtensionAPI): void {
26
31
  let questLog = new QuestLog();
27
32
  let tracker = new QuestUsageTracker(DEFAULT_CONFIG);
28
33
  let config: ResolvedConfig = DEFAULT_CONFIG;
29
- let statusWidget = new QuestStatusWidget(DEFAULT_CONFIG.display.icon);
34
+ let statusWidget = new QuestStatusWidget(
35
+ DEFAULT_CONFIG.display.icon,
36
+ DEFAULT_CONFIG.display.showStatus,
37
+ );
30
38
 
31
39
  const shortcutKey = getConfig({ cwd: process.cwd() }).shortcuts?.openQuests ?? "ctrl+shift+l";
32
40
  logger.debug("quests:shortcut", "register", { key: shortcutKey });
@@ -49,8 +57,9 @@ export default function (pi: ExtensionAPI): void {
49
57
  config = getConfig(ctx);
50
58
  questLog = new QuestLog(config);
51
59
  tracker = new QuestUsageTracker(config);
52
- statusWidget = new QuestStatusWidget(config.display.icon);
60
+ statusWidget = new QuestStatusWidget(config.display.icon, config.display.showStatus);
53
61
  questLog.reconstructFromSession(ctx);
62
+ if (questLog.getAll().length > 0) tracker.markQuestToolUsed();
54
63
  statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
55
64
 
56
65
  registerQuestTool(pi, questLog, config);
@@ -66,6 +75,7 @@ export default function (pi: ExtensionAPI): void {
66
75
 
67
76
  pi.on("session_tree", async (_event, ctx) => {
68
77
  questLog.reconstructFromSession(ctx);
78
+ if (questLog.getAll().length > 0) tracker.markQuestToolUsed();
69
79
  statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
70
80
  });
71
81
 
@@ -79,6 +89,7 @@ export default function (pi: ExtensionAPI): void {
79
89
  });
80
90
 
81
91
  pi.on("context", async (event) => {
92
+ if (!config.nudges.enable) return undefined;
82
93
  const latestPrompt = event.messages
83
94
  .filter((m) => m.role === "user")
84
95
  .map((m) => (typeof m.content === "string" ? m.content : ""))
@@ -91,44 +102,28 @@ export default function (pi: ExtensionAPI): void {
91
102
  const hasTopLevelQuestWithoutSubs = activeTopLevel.some(
92
103
  (q) => !allQuests.some((step) => (step as { parentId?: string }).parentId === q.id),
93
104
  );
94
- const nudge = tracker.getNudge(
95
- activeQuestCount,
96
- allQuests,
97
- latestPrompt,
98
- hasTopLevelQuestWithoutSubs,
99
- );
105
+ const nudge = tracker.getNudge(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
100
106
 
101
107
  const fakeDoneRegex = new RegExp(config.validation.fakeDonePattern, "i");
102
108
  const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
103
109
  if (!nudge && !fakeDone) return undefined;
104
110
 
105
- let content = nudge ?? "";
106
- if (fakeDone) {
107
- 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.`;
108
- }
111
+ const parts: string[] = [];
112
+ if (nudge) parts.push(nudge);
113
+ if (fakeDone) parts.push(FAKE_DONE_REMINDER);
109
114
 
110
115
  const reminder: UserMessage = {
111
116
  role: "user",
112
- content: content.trim(),
117
+ content: parts.join("\n"),
113
118
  timestamp: Date.now(),
114
119
  };
115
120
  return { messages: [...event.messages, reminder] };
116
121
  });
117
122
 
118
123
  pi.on("before_agent_start", async (event) => {
119
- const quests = questLog.getAll();
120
- let reminder = "";
121
- if (quests.length > 0) {
122
- const remaining = quests.filter((q) => !q.done).length;
123
- const list = formatQuestList(quests);
124
- reminder = `Active quests (${remaining}/${quests.length}):\n${list}\n\nKeep quest progress updated as you work.`;
125
- } else {
126
- reminder =
127
- "No active quests. Use the quest tool to track your work. Use action: 'skill' for usage documentation.";
128
- }
129
-
124
+ if (tracker.hasUsedQuestTool) return undefined;
130
125
  return {
131
- systemPrompt: `# Quest Management\n${QUEST_PROMPT_GATE}${event.systemPrompt}\n\n## Quest Management\n${reminder}`,
126
+ systemPrompt: `# Quest Management\n${QUEST_PROMPT_GATE}${event.systemPrompt}\n\n## Quest Management\n${NO_QUESTS_REMINDER}`,
132
127
  };
133
128
  });
134
129
 
@@ -1,20 +1,8 @@
1
1
  import type { ResolvedConfig } from "../config.js";
2
2
  import { logger } from "../logger.js";
3
- import type { Quest } from "./types.js";
4
3
 
5
4
  const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
6
5
 
7
- function formatActiveQuests(allQuests: Quest[], limit = 3): string {
8
- const active = allQuests.filter((q) => !q.done);
9
- if (active.length === 0) return "";
10
-
11
- const shown = active.slice(0, limit);
12
- const lines = shown.map((q) => ` [${q.id}]: ${q.description}`);
13
- if (active.length > limit) lines.push(` ... and ${active.length - limit} more`);
14
-
15
- return `\nActive quests:\n${lines.join("\n")}`;
16
- }
17
-
18
6
  type NudgeCandidate = { index: number; message: string };
19
7
 
20
8
  export class QuestUsageTracker {
@@ -43,9 +31,17 @@ export class QuestUsageTracker {
43
31
  this.nudgedThisTurn = false;
44
32
  }
45
33
 
34
+ get hasUsedQuestTool(): boolean {
35
+ return this.hasEverUsedQuestTool;
36
+ }
37
+
38
+ markQuestToolUsed(): void {
39
+ this.hasEverUsedQuestTool = true;
40
+ this.lastQuestToolTime = Date.now();
41
+ }
42
+
46
43
  getNudge(
47
44
  activeQuestCount: number,
48
- allQuests: Quest[],
49
45
  latestPrompt?: string,
50
46
  hasTopLevelQuestWithoutSubs?: boolean,
51
47
  ): string | undefined {
@@ -67,7 +63,6 @@ export class QuestUsageTracker {
67
63
 
68
64
  const eligible = this.getEligibleNudges(
69
65
  activeQuestCount,
70
- allQuests,
71
66
  latestPrompt,
72
67
  hasTopLevelQuestWithoutSubs,
73
68
  );
@@ -101,7 +96,6 @@ export class QuestUsageTracker {
101
96
 
102
97
  private getEligibleNudges(
103
98
  activeQuestCount: number,
104
- allQuests: Quest[],
105
99
  latestPrompt?: string,
106
100
  hasTopLevelQuestWithoutSubs?: boolean,
107
101
  ): NudgeCandidate[] {
@@ -111,7 +105,7 @@ export class QuestUsageTracker {
111
105
  if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
112
106
  candidates.push({
113
107
  index: 0,
114
- 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}`,
108
+ message: `QUEST REMINDER: You have made multiple 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}`,
115
109
  });
116
110
  }
117
111
 
@@ -119,7 +113,7 @@ export class QuestUsageTracker {
119
113
  if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
120
114
  candidates.push({
121
115
  index: 1,
122
- message: `QUEST REMINDER: Your latest prompt is a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
116
+ message: `QUEST REMINDER: Your latest prompt looks like a complex task, but there are 0 active quests. USE the quest tool to break it into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
123
117
  });
124
118
  }
125
119
 
@@ -130,10 +124,9 @@ export class QuestUsageTracker {
130
124
  this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
131
125
  Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
132
126
  ) {
133
- const questContext = formatActiveQuests(allQuests);
134
127
  candidates.push({
135
128
  index: 2,
136
- 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.${questContext} ${ACKNOWLEDGEMENT}`,
129
+ message: `QUEST REMINDER: It has been a while since your last quest tool use and many tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`,
137
130
  });
138
131
  }
139
132
 
@@ -144,7 +137,7 @@ export class QuestUsageTracker {
144
137
  ) {
145
138
  candidates.push({
146
139
  index: 3,
147
- 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}`,
140
+ message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`,
148
141
  });
149
142
  }
150
143
 
@@ -157,7 +150,7 @@ export class QuestUsageTracker {
157
150
  ) {
158
151
  candidates.push({
159
152
  index: 4,
160
- message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without steps. Consider the quest complexity and use the \`split\` action to break them into smaller steps and track progress. ${ACKNOWLEDGEMENT}`,
153
+ message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and have active top-level quests without steps. Consider whether decomposing them with the \`split\` action would help track progress. ${ACKNOWLEDGEMENT}`,
161
154
  });
162
155
  }
163
156
 
@@ -168,10 +161,9 @@ export class QuestUsageTracker {
168
161
  this.lastQuestToolTime > 0 &&
169
162
  Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
170
163
  ) {
171
- const questContext = formatActiveQuests(allQuests);
172
164
  candidates.push({
173
165
  index: 5,
174
- message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests. UPDATE your quest progress to reflect current status.${questContext} ${ACKNOWLEDGEMENT}`,
166
+ message: `QUEST REMINDER: You have made many consecutive tool calls without using the quest tool despite having active quests. UPDATE your quest progress to reflect current status. ${ACKNOWLEDGEMENT}`,
175
167
  });
176
168
  }
177
169
 
@@ -5,13 +5,18 @@ export class QuestStatusWidget {
5
5
  private readonly key = "pi-quests";
6
6
  private readonly barWidth = 5;
7
7
 
8
- constructor(private readonly icon: string) {}
8
+ constructor(
9
+ private readonly icon: string,
10
+ private readonly enabled: boolean = true,
11
+ ) {}
9
12
 
10
13
  update(
11
14
  questLog: QuestLog,
12
15
  ui: { setStatus(key: string, text: string | undefined): void },
13
16
  theme: Theme,
14
17
  ): void {
18
+ if (!this.enabled) return;
19
+
15
20
  const all = questLog.getAll();
16
21
  const total = all.length;
17
22
  const done = all.filter((q) => q.done).length;