@narumitw/pi-goal 0.1.17 → 0.1.19
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/README.md +27 -25
- package/package.json +1 -1
- package/src/goal.ts +131 -54
package/README.md
CHANGED
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-goal) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@narumitw/pi-goal` is a native [Pi coding agent](https://pi.dev) extension that adds
|
|
5
|
+
`@narumitw/pi-goal` is a native [Pi coding agent](https://pi.dev) extension that adds session-scoped `/goal` commands and a `goal_complete` tool for autonomous, verifiable task completion.
|
|
6
6
|
|
|
7
|
-
Goal mode keeps sending guarded
|
|
7
|
+
Goal mode uses Codex-like persistence instructions and keeps sending guarded continuation messages until the agent calls `goal_complete`, the user pauses or clears the goal, or an optional token budget is reached.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
- Adds `/goal <goal_to_complete>` to start goal mode.
|
|
11
|
+
- Adds `/goal <goal_to_complete>` to start goal mode, with confirmation before replacing an existing goal.
|
|
12
12
|
- Bare `/goal` shows the current goal summary.
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- Keeps advanced goal management inside `/goal` subcommands: `pause`, `resume`, `clear`, and `edit`.
|
|
14
|
+
- Exposes only one top-level command: `/goal`.
|
|
15
15
|
- Supports optional token budgets such as `/goal --tokens 100k <goal>`.
|
|
16
16
|
- Tracks `active`, `paused`, `budget_limited`, and `complete` states.
|
|
17
|
-
-
|
|
17
|
+
- Stores goal state in the current Pi session, following Codex's thread-owned goal model instead of using a global per-directory goal.
|
|
18
18
|
- Registers a `goal_complete` tool for explicit completion.
|
|
19
|
-
- Automatically prompts the agent to continue if an active turn ends early.
|
|
19
|
+
- Automatically prompts the agent to continue if an active turn ends early, directly triggering the next turn when Pi is idle.
|
|
20
20
|
- Guards auto-follow-ups so replaced, paused, cleared, completed, or budget-limited goals are not continued.
|
|
21
21
|
- Encourages verification before the goal is marked complete.
|
|
22
22
|
|
|
@@ -44,41 +44,43 @@ pi -e ./extensions/pi-goal
|
|
|
44
44
|
/goal
|
|
45
45
|
/goal implement snake game
|
|
46
46
|
/goal --tokens 100k fix the failing test and verify it
|
|
47
|
+
/goal edit ship the smaller fix first
|
|
47
48
|
/goal pause
|
|
48
49
|
/goal resume
|
|
49
50
|
/goal clear
|
|
50
|
-
/goal edit ship the smaller fix first
|
|
51
|
-
/goal-status
|
|
52
|
-
/goal-stop
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
- `/goal` shows the current goal, status, iteration count, elapsed time, and
|
|
56
|
-
- `/goal <goal_to_complete>` starts goal mode
|
|
57
|
-
- `/goal --tokens 100k <goal_to_complete>` starts goal mode with a token budget. `k` and `m` suffixes are accepted, for example `100k` or `1.5m`.
|
|
53
|
+
- `/goal` shows the current goal, status, iteration count, elapsed time, token usage, and available `/goal` subcommands.
|
|
54
|
+
- `/goal <goal_to_complete>` starts goal mode. If another unfinished goal exists, Pi asks for confirmation before replacing it with a new active goal and resetting its usage counters.
|
|
55
|
+
- `/goal --tokens 100k <goal_to_complete>` starts or replaces goal mode with a token budget. `k` and `m` suffixes are accepted, for example `100k` or `1.5m`.
|
|
56
|
+
- `/goal edit <goal_to_complete>` updates the existing goal objective without resetting usage counters. Active goals stay active, paused goals stay paused, and budget-limited goals remain budget-limited if their budget is still exhausted.
|
|
58
57
|
- `/goal pause` stops prompt injection and auto-continuation without forgetting the goal.
|
|
59
|
-
- `/goal resume` resumes a paused or budget-limited goal.
|
|
60
|
-
- `/goal clear` cancels the current goal and
|
|
61
|
-
- `/goal edit <goal_to_complete>` replaces the current goal with a new active goal.
|
|
62
|
-
- `/goal-status` is a compatibility alias for `/goal`.
|
|
63
|
-
- `/goal-stop` is a compatibility alias for `/goal clear`.
|
|
58
|
+
- `/goal resume` resumes a paused or budget-limited goal when the token budget allows it.
|
|
59
|
+
- `/goal clear` cancels the current goal and also removes any legacy persisted state for the current working directory.
|
|
64
60
|
|
|
65
61
|
Goal objectives are limited to 4,000 characters. Put longer instructions in a file and reference the file path from `/goal`.
|
|
66
62
|
|
|
63
|
+
## 🔁 Session and reload behavior
|
|
64
|
+
|
|
65
|
+
Goal state is stored as Pi session state, similar to Codex's thread-owned goals. `/reload` and reopening the same Pi session can restore that session's unfinished goal. Starting a new Pi session in the same working directory does not inherit the old goal.
|
|
66
|
+
|
|
67
|
+
Older versions wrote unfinished goals to `~/.pi/agent/pi-goal-state.json` keyed by working directory. This version no longer reads that global file, and `/goal clear` removes any legacy entry for the current working directory.
|
|
68
|
+
|
|
67
69
|
## 📊 Statusline states
|
|
68
70
|
|
|
69
71
|
`pi-goal` writes compact status strings for statusline extensions:
|
|
70
72
|
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
73
|
+
- `🎯 active 3m` — an active goal without a token budget.
|
|
74
|
+
- `🎯 active 18k/100k` — an active goal with token usage and budget.
|
|
75
|
+
- `🎯 paused` — auto-continuation is paused.
|
|
76
|
+
- `🎯 budget 100k/100k` — the token budget was reached; auto-continuation stops.
|
|
77
|
+
- `🎯 complete` — shown briefly after `goal_complete` succeeds.
|
|
76
78
|
|
|
77
79
|
## ✅ How completion works
|
|
78
80
|
|
|
79
|
-
The extension registers a `goal_complete` tool. While a goal is active, the system prompt
|
|
81
|
+
The extension registers a `goal_complete` tool. While a goal is active, the system prompt uses Codex-like persistence rules: keep going until the goal is resolved end-to-end, do not stop at analysis, a plan, partial fixes, or suggested next steps, use available tools for implementation and verification, and call `goal_complete` only when the goal is fully done.
|
|
80
82
|
|
|
81
|
-
If an agent turn ends before `goal_complete` is called, the extension records elapsed time and token usage, checks the budget, verifies that the same goal id is still active, then sends a
|
|
83
|
+
If an agent turn ends before `goal_complete` is called, the extension records elapsed time and token usage, checks the budget, verifies that the same goal id is still active, then sends a continuation prompt for the same goal. When Pi is already idle, this directly triggers the next turn; otherwise it is queued as a follow-up until the agent finishes current work.
|
|
82
84
|
|
|
83
85
|
## 🧠 Use cases
|
|
84
86
|
|
package/package.json
CHANGED
package/src/goal.ts
CHANGED
|
@@ -25,6 +25,10 @@ interface GoalCompleteDetails {
|
|
|
25
25
|
summary: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
interface GoalStateEntryData {
|
|
29
|
+
goal?: ActiveGoal | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
interface CommandResult {
|
|
29
33
|
kind: "show" | "start" | "pause" | "resume" | "clear" | "edit";
|
|
30
34
|
objective?: string;
|
|
@@ -34,6 +38,7 @@ interface CommandResult {
|
|
|
34
38
|
interface StatusContext {
|
|
35
39
|
cwd: string;
|
|
36
40
|
ui: {
|
|
41
|
+
confirm: (title: string, message: string) => Promise<boolean>;
|
|
37
42
|
notify: (message: string, level?: "info" | "warning" | "error") => void;
|
|
38
43
|
setStatus: (key: string, value: string | undefined) => void;
|
|
39
44
|
};
|
|
@@ -42,6 +47,7 @@ interface StatusContext {
|
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
const STATUS_KEY = "goal";
|
|
50
|
+
const GOAL_STATE_ENTRY_TYPE = "goal-state";
|
|
45
51
|
const MAX_OBJECTIVE_LENGTH = 4_000;
|
|
46
52
|
const STATE_FILE = join(
|
|
47
53
|
process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? ".", ".pi", "agent"),
|
|
@@ -50,6 +56,7 @@ const STATE_FILE = join(
|
|
|
50
56
|
|
|
51
57
|
let activeGoal: ActiveGoal | undefined;
|
|
52
58
|
let completionStatusTimer: NodeJS.Timeout | undefined;
|
|
59
|
+
let extensionApi: ExtensionAPI | undefined;
|
|
53
60
|
|
|
54
61
|
const goalCompleteTool = defineTool({
|
|
55
62
|
name: "goal_complete",
|
|
@@ -71,7 +78,7 @@ const goalCompleteTool = defineTool({
|
|
|
71
78
|
if (completedGoal) {
|
|
72
79
|
activeGoal = transitionGoal(completedGoal, "complete");
|
|
73
80
|
updateGoalUsage(activeGoal, ctx);
|
|
74
|
-
persistGoal(
|
|
81
|
+
persistGoal(activeGoal);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
const goal = completedGoal?.text ?? "unknown goal";
|
|
@@ -91,6 +98,7 @@ const goalCompleteTool = defineTool({
|
|
|
91
98
|
});
|
|
92
99
|
|
|
93
100
|
export default function goal(pi: ExtensionAPI) {
|
|
101
|
+
extensionApi = pi;
|
|
94
102
|
pi.registerTool(goalCompleteTool);
|
|
95
103
|
|
|
96
104
|
pi.registerCommand("goal", {
|
|
@@ -116,33 +124,23 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
116
124
|
clearGoal(ctx);
|
|
117
125
|
return;
|
|
118
126
|
case "edit":
|
|
119
|
-
editGoal(result.objective ?? "", result.tokenBudget, ctx);
|
|
127
|
+
editGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
120
128
|
return;
|
|
121
129
|
case "start":
|
|
122
|
-
startGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
130
|
+
await startGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
123
131
|
return;
|
|
124
132
|
}
|
|
125
133
|
},
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
pi.registerCommand("goal-stop", {
|
|
129
|
-
description: "Compatibility alias for /goal clear",
|
|
130
|
-
handler: async (_args, ctx) => clearGoal(ctx),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
pi.registerCommand("goal-status", {
|
|
134
|
-
description: "Compatibility alias for bare /goal",
|
|
135
|
-
handler: async (_args, ctx) => showGoal(ctx),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
136
|
pi.on("session_start", (_event, ctx) => {
|
|
139
|
-
activeGoal =
|
|
137
|
+
activeGoal = loadGoalFromSession(ctx);
|
|
140
138
|
if (activeGoal) updateStatus(ctx, activeGoal);
|
|
141
139
|
else ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
142
140
|
});
|
|
143
141
|
|
|
144
142
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
145
|
-
if (activeGoal) persistGoal(
|
|
143
|
+
if (activeGoal) persistGoal(activeGoal);
|
|
146
144
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
147
145
|
if (completionStatusTimer) clearTimeout(completionStatusTimer);
|
|
148
146
|
});
|
|
@@ -164,40 +162,49 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
164
162
|
|
|
165
163
|
if (activeGoal.tokenBudget !== undefined && activeGoal.tokensUsed >= activeGoal.tokenBudget) {
|
|
166
164
|
activeGoal = transitionGoal(activeGoal, "budget_limited");
|
|
167
|
-
persistGoal(
|
|
165
|
+
persistGoal(activeGoal);
|
|
168
166
|
updateStatus(ctx, activeGoal);
|
|
169
167
|
ctx.ui.notify(`Goal token budget reached: ${formatBudget(activeGoal)}`, "warning");
|
|
170
168
|
return;
|
|
171
169
|
}
|
|
172
170
|
|
|
173
|
-
persistGoal(
|
|
171
|
+
persistGoal(activeGoal);
|
|
174
172
|
updateStatus(ctx, activeGoal);
|
|
175
173
|
|
|
176
174
|
const currentGoal = activeGoal;
|
|
177
175
|
if (!currentGoal || currentGoal.id !== goalId || currentGoal.status !== "active") return;
|
|
178
|
-
pi
|
|
176
|
+
sendContinuationPrompt(pi, ctx, currentGoal);
|
|
179
177
|
});
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
function startGoal(
|
|
180
|
+
async function startGoal(
|
|
181
|
+
objective: string,
|
|
182
|
+
tokenBudget: number | undefined,
|
|
183
|
+
pi: ExtensionAPI,
|
|
184
|
+
ctx: StatusContext,
|
|
185
|
+
) {
|
|
183
186
|
const validationError = validateObjective(objective);
|
|
184
187
|
if (validationError) {
|
|
185
188
|
ctx.ui.notify(validationError, "warning");
|
|
186
189
|
return;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
192
|
+
const existingGoal = activeGoal?.status !== "complete" ? activeGoal : undefined;
|
|
193
|
+
if (existingGoal) {
|
|
194
|
+
const shouldReplace = await ctx.ui.confirm(
|
|
195
|
+
"Replace goal?",
|
|
196
|
+
`Current goal: ${existingGoal.text}\n\nNew goal: ${objective}`,
|
|
193
197
|
);
|
|
194
|
-
|
|
198
|
+
if (!shouldReplace) {
|
|
199
|
+
ctx.ui.notify(`Goal kept: ${existingGoal.text}`, "info");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
activeGoal = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
|
|
198
|
-
persistGoal(
|
|
205
|
+
persistGoal(activeGoal);
|
|
199
206
|
updateStatus(ctx, activeGoal);
|
|
200
|
-
ctx.ui.notify(`Goal started: ${objective}`, "info");
|
|
207
|
+
ctx.ui.notify(existingGoal ? `Goal replaced: ${objective}` : `Goal started: ${objective}`, "info");
|
|
201
208
|
sendGoalPrompt(pi, ctx, activeGoal);
|
|
202
209
|
}
|
|
203
210
|
|
|
@@ -211,7 +218,7 @@ function pauseGoal(ctx: StatusContext) {
|
|
|
211
218
|
return;
|
|
212
219
|
}
|
|
213
220
|
activeGoal = transitionGoal(activeGoal, "paused");
|
|
214
|
-
persistGoal(
|
|
221
|
+
persistGoal(activeGoal);
|
|
215
222
|
updateStatus(ctx, activeGoal);
|
|
216
223
|
ctx.ui.notify(`Goal paused: ${activeGoal.text}`, "info");
|
|
217
224
|
}
|
|
@@ -226,7 +233,7 @@ function resumeGoal(ctx: StatusContext) {
|
|
|
226
233
|
return;
|
|
227
234
|
}
|
|
228
235
|
activeGoal = transitionGoal(activeGoal, "active");
|
|
229
|
-
persistGoal(
|
|
236
|
+
persistGoal(activeGoal);
|
|
230
237
|
updateStatus(ctx, activeGoal);
|
|
231
238
|
ctx.ui.notify(`Goal resumed: ${activeGoal.text}`, "info");
|
|
232
239
|
}
|
|
@@ -244,27 +251,44 @@ function clearGoal(ctx: StatusContext) {
|
|
|
244
251
|
ctx.ui.notify(`Goal cleared: ${stoppedGoal}`, "warning");
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
function editGoal(
|
|
254
|
+
function editGoal(
|
|
255
|
+
objective: string,
|
|
256
|
+
tokenBudget: number | undefined,
|
|
257
|
+
pi: ExtensionAPI,
|
|
258
|
+
ctx: StatusContext,
|
|
259
|
+
) {
|
|
248
260
|
const validationError = validateObjective(objective);
|
|
249
261
|
if (validationError) {
|
|
250
262
|
ctx.ui.notify(validationError, "warning");
|
|
251
263
|
return;
|
|
252
264
|
}
|
|
265
|
+
if (!activeGoal) {
|
|
266
|
+
ctx.ui.notify("No active goal. Use /goal <objective> to start one.", "warning");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
253
269
|
|
|
254
|
-
activeGoal
|
|
255
|
-
|
|
270
|
+
updateGoalUsage(activeGoal, ctx);
|
|
271
|
+
activeGoal = normalizeGoalForBudget({
|
|
272
|
+
...activeGoal,
|
|
273
|
+
text: objective,
|
|
274
|
+
status: editedGoalStatus(activeGoal.status),
|
|
275
|
+
tokenBudget: tokenBudget ?? activeGoal.tokenBudget,
|
|
276
|
+
updatedAt: Date.now(),
|
|
277
|
+
});
|
|
278
|
+
persistGoal(activeGoal);
|
|
256
279
|
updateStatus(ctx, activeGoal);
|
|
257
280
|
ctx.ui.notify(`Goal updated: ${objective}`, "info");
|
|
281
|
+
if (activeGoal.status === "active") sendObjectiveUpdatedPrompt(pi, ctx, activeGoal);
|
|
258
282
|
}
|
|
259
283
|
|
|
260
284
|
function showGoal(ctx: StatusContext) {
|
|
261
285
|
if (!activeGoal) {
|
|
262
|
-
ctx.ui.notify("
|
|
286
|
+
ctx.ui.notify("Usage: /goal <objective>\nNo goal is currently set.", "info");
|
|
263
287
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
264
288
|
return;
|
|
265
289
|
}
|
|
266
290
|
updateGoalUsage(activeGoal, ctx);
|
|
267
|
-
persistGoal(
|
|
291
|
+
persistGoal(activeGoal);
|
|
268
292
|
updateStatus(ctx, activeGoal);
|
|
269
293
|
ctx.ui.notify(goalSummary(activeGoal), "info");
|
|
270
294
|
}
|
|
@@ -286,7 +310,22 @@ function createGoal(text: string, tokenBudget: number | undefined, baselineToken
|
|
|
286
310
|
}
|
|
287
311
|
|
|
288
312
|
function transitionGoal(goal: ActiveGoal, status: GoalStatus): ActiveGoal {
|
|
289
|
-
return { ...goal, status, updatedAt: Date.now() };
|
|
313
|
+
return normalizeGoalForBudget({ ...goal, status, updatedAt: Date.now() });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function editedGoalStatus(status: GoalStatus): GoalStatus {
|
|
317
|
+
return status === "paused" ? "paused" : "active";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeGoalForBudget(goal: ActiveGoal): ActiveGoal {
|
|
321
|
+
if (
|
|
322
|
+
goal.status === "active" &&
|
|
323
|
+
goal.tokenBudget !== undefined &&
|
|
324
|
+
goal.tokensUsed >= goal.tokenBudget
|
|
325
|
+
) {
|
|
326
|
+
return { ...goal, status: "budget_limited" };
|
|
327
|
+
}
|
|
328
|
+
return goal;
|
|
290
329
|
}
|
|
291
330
|
|
|
292
331
|
function incrementGoal(goal: ActiveGoal): ActiveGoal {
|
|
@@ -377,7 +416,18 @@ function validateObjective(objective: string): string | undefined {
|
|
|
377
416
|
}
|
|
378
417
|
|
|
379
418
|
function sendGoalPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
380
|
-
|
|
419
|
+
sendPrompt(pi, ctx, buildGoalPrompt(goal));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function sendObjectiveUpdatedPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
423
|
+
sendPrompt(pi, ctx, buildObjectiveUpdatedPrompt(goal));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function sendContinuationPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
427
|
+
sendPrompt(pi, ctx, buildContinuePrompt(goal));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function sendPrompt(pi: ExtensionAPI, ctx: StatusContext, prompt: string) {
|
|
381
431
|
if (ctx.isIdle?.()) pi.sendUserMessage(prompt);
|
|
382
432
|
else pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
383
433
|
}
|
|
@@ -388,11 +438,11 @@ function updateStatus(ctx: StatusContext, goal: ActiveGoal) {
|
|
|
388
438
|
|
|
389
439
|
function formatStatus(goal: ActiveGoal | undefined) {
|
|
390
440
|
if (!goal) return undefined;
|
|
391
|
-
if (goal.status === "complete") return "
|
|
392
|
-
if (goal.status === "paused") return "
|
|
393
|
-
if (goal.status === "budget_limited") return
|
|
394
|
-
if (goal.tokenBudget !== undefined) return
|
|
395
|
-
return
|
|
441
|
+
if (goal.status === "complete") return "🎯 complete";
|
|
442
|
+
if (goal.status === "paused") return "🎯 paused";
|
|
443
|
+
if (goal.status === "budget_limited") return `🎯 budget ${formatBudget(goal)}`;
|
|
444
|
+
if (goal.tokenBudget !== undefined) return `🎯 active ${formatBudget(goal)}`;
|
|
445
|
+
return `🎯 active ${formatDuration(goal.timeUsedSeconds)}`;
|
|
396
446
|
}
|
|
397
447
|
|
|
398
448
|
function formatBudget(goal: ActiveGoal) {
|
|
@@ -406,9 +456,16 @@ function goalSummary(goal: ActiveGoal) {
|
|
|
406
456
|
`Iteration: ${goal.iteration}`,
|
|
407
457
|
`Elapsed: ${formatDuration(goal.timeUsedSeconds)}`,
|
|
408
458
|
`Tokens: ${goal.tokenBudget === undefined ? formatTokenCount(goal.tokensUsed) : formatBudget(goal)}`,
|
|
459
|
+
`Commands: ${goalCommandHint(goal.status)}`,
|
|
409
460
|
].join("\n");
|
|
410
461
|
}
|
|
411
462
|
|
|
463
|
+
function goalCommandHint(status: GoalStatus) {
|
|
464
|
+
if (status === "active") return "/goal edit <objective>, /goal pause, /goal clear";
|
|
465
|
+
if (status === "paused") return "/goal edit <objective>, /goal resume, /goal clear";
|
|
466
|
+
return "/goal edit <objective>, /goal clear";
|
|
467
|
+
}
|
|
468
|
+
|
|
412
469
|
function formatDuration(seconds: number) {
|
|
413
470
|
if (seconds < 60) return `${seconds}s`;
|
|
414
471
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -425,16 +482,25 @@ function formatTokenCount(value: number) {
|
|
|
425
482
|
|
|
426
483
|
function buildGoalPrompt(goal: ActiveGoal) {
|
|
427
484
|
const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatTokenCount(goal.tokenBudget)}.`;
|
|
428
|
-
return `Goal mode is active. Complete this goal fully:\n\n${goal.text}${budgetLine}\n\
|
|
485
|
+
return `Goal mode is active. Complete this goal fully:\n\n${goal.text}${budgetLine}\n\n${goalPersistenceRules("this goal")}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildObjectiveUpdatedPrompt(goal: ActiveGoal) {
|
|
489
|
+
const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatBudget(goal)} used.`;
|
|
490
|
+
return `The active /goal objective was updated. Continue working toward this goal:\n\n${goal.text}${budgetLine}\n\n${goalPersistenceRules("the updated goal")}`;
|
|
429
491
|
}
|
|
430
492
|
|
|
431
493
|
function buildGoalSystemPrompt(goal: ActiveGoal) {
|
|
432
494
|
const budgetLine = goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
|
|
433
|
-
return `Active /goal: ${goal.text}\n\nGoal-mode rules:\n-
|
|
495
|
+
return `Active /goal: ${goal.text}\n\nGoal-mode rules:\n- Keep going until the active goal is completely resolved end-to-end.\n- Do not stop at analysis, a plan, TODO list, partial fixes, or suggested next steps.\n- Autonomously perform implementation and verification with the available tools when they are needed to complete the goal.\n- Persevere through recoverable tool failures by trying reasonable alternatives instead of yielding early.\n- If the goal is not complete at the end of a turn, expect an automatic continuation and keep working from where you left off.\n- Only call the goal_complete tool after the goal is fully complete and verified.${budgetLine}`;
|
|
434
496
|
}
|
|
435
497
|
|
|
436
498
|
function buildContinuePrompt(goal: ActiveGoal) {
|
|
437
|
-
return `Continue the active /goal until it is complete:\n\n${goal.text}\n\nThis is automatic continuation #${goal.iteration}.
|
|
499
|
+
return `Continue the active /goal until it is complete:\n\n${goal.text}\n\nThis is automatic continuation #${goal.iteration}. ${goalPersistenceRules("this goal")}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function goalPersistenceRules(goalLabel: string) {
|
|
503
|
+
return `Keep going until ${goalLabel} is completely resolved end-to-end. Do not stop at analysis, a plan, TODO list, partial fixes, or suggested next steps. Autonomously perform implementation and verification with the available tools when they are needed. If a tool call fails, try reasonable alternatives instead of yielding early. Only call the goal_complete tool after ${goalLabel} is fully complete and verified.`;
|
|
438
504
|
}
|
|
439
505
|
|
|
440
506
|
function currentTokenTotal(ctx: StatusContext): number {
|
|
@@ -452,18 +518,28 @@ function currentTokenTotal(ctx: StatusContext): number {
|
|
|
452
518
|
return total;
|
|
453
519
|
}
|
|
454
520
|
|
|
455
|
-
function persistGoal(
|
|
456
|
-
|
|
521
|
+
function persistGoal(goal: ActiveGoal) {
|
|
522
|
+
extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal });
|
|
457
523
|
}
|
|
458
524
|
|
|
459
525
|
function clearPersistedGoal(cwd: string) {
|
|
460
|
-
|
|
526
|
+
extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal: null });
|
|
527
|
+
clearLegacyPersistedGoal(cwd);
|
|
461
528
|
}
|
|
462
529
|
|
|
463
|
-
function
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
530
|
+
function loadGoalFromSession(ctx: StatusContext): ActiveGoal | undefined {
|
|
531
|
+
const sessionManager = ctx.sessionManager as
|
|
532
|
+
| {
|
|
533
|
+
getBranch?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
|
|
534
|
+
getEntries?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
|
|
535
|
+
}
|
|
536
|
+
| undefined;
|
|
537
|
+
const entries = sessionManager?.getBranch?.() ?? sessionManager?.getEntries?.() ?? [];
|
|
538
|
+
const entry = entries
|
|
539
|
+
.filter((entry) => entry.type === "custom" && entry.customType === GOAL_STATE_ENTRY_TYPE)
|
|
540
|
+
.pop();
|
|
541
|
+
const data = entry?.data as GoalStateEntryData | undefined;
|
|
542
|
+
return isGoal(data?.goal) && data.goal.status !== "complete" ? data.goal : undefined;
|
|
467
543
|
}
|
|
468
544
|
|
|
469
545
|
function clearActiveGoal(ctx: StatusContext) {
|
|
@@ -474,7 +550,7 @@ function clearActiveGoal(ctx: StatusContext) {
|
|
|
474
550
|
|
|
475
551
|
function showCompletionStatus(ctx: StatusContext) {
|
|
476
552
|
if (completionStatusTimer) clearTimeout(completionStatusTimer);
|
|
477
|
-
ctx.ui.setStatus(STATUS_KEY, "
|
|
553
|
+
ctx.ui.setStatus(STATUS_KEY, "🎯 complete");
|
|
478
554
|
completionStatusTimer = setTimeout(() => ctx.ui.setStatus(STATUS_KEY, undefined), 8_000);
|
|
479
555
|
}
|
|
480
556
|
|
|
@@ -490,14 +566,15 @@ function readState(): Record<string, unknown> {
|
|
|
490
566
|
}
|
|
491
567
|
}
|
|
492
568
|
|
|
493
|
-
function
|
|
569
|
+
function clearLegacyPersistedGoal(cwd: string) {
|
|
570
|
+
if (!existsSync(STATE_FILE)) return;
|
|
494
571
|
const goals = readState();
|
|
495
|
-
|
|
496
|
-
else delete goals[cwd];
|
|
572
|
+
delete goals[cwd];
|
|
497
573
|
mkdirSync(dirname(STATE_FILE), { recursive: true });
|
|
498
574
|
writeFileSync(STATE_FILE, `${JSON.stringify(goals, null, 2)}\n`);
|
|
499
575
|
}
|
|
500
576
|
|
|
577
|
+
|
|
501
578
|
function isGoal(value: unknown): value is ActiveGoal {
|
|
502
579
|
if (!value || typeof value !== "object") return false;
|
|
503
580
|
const goal = value as Partial<ActiveGoal>;
|