@narumitw/pi-goal 0.1.16 → 0.1.18
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 +23 -21
- package/package.json +1 -1
- package/src/goal.ts +117 -49
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
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
7
|
Goal mode keeps sending guarded automatic follow-up 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
19
|
- Automatically prompts the agent to continue if an active turn ends early.
|
|
20
20
|
- Guards auto-follow-ups so replaced, paused, cleared, completed, or budget-limited goals are not continued.
|
|
@@ -44,35 +44,37 @@ 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
|
|
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,13 +162,13 @@ 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;
|
|
@@ -179,25 +177,34 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
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 {
|
|
@@ -382,17 +421,23 @@ function sendGoalPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal)
|
|
|
382
421
|
else pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
383
422
|
}
|
|
384
423
|
|
|
424
|
+
function sendObjectiveUpdatedPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
425
|
+
const prompt = buildObjectiveUpdatedPrompt(goal);
|
|
426
|
+
if (ctx.isIdle?.()) pi.sendUserMessage(prompt);
|
|
427
|
+
else pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
428
|
+
}
|
|
429
|
+
|
|
385
430
|
function updateStatus(ctx: StatusContext, goal: ActiveGoal) {
|
|
386
431
|
ctx.ui.setStatus(STATUS_KEY, formatStatus(goal));
|
|
387
432
|
}
|
|
388
433
|
|
|
389
434
|
function formatStatus(goal: ActiveGoal | undefined) {
|
|
390
435
|
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
|
|
436
|
+
if (goal.status === "complete") return "🎯 complete";
|
|
437
|
+
if (goal.status === "paused") return "🎯 paused";
|
|
438
|
+
if (goal.status === "budget_limited") return `🎯 budget ${formatBudget(goal)}`;
|
|
439
|
+
if (goal.tokenBudget !== undefined) return `🎯 active ${formatBudget(goal)}`;
|
|
440
|
+
return `🎯 active ${formatDuration(goal.timeUsedSeconds)}`;
|
|
396
441
|
}
|
|
397
442
|
|
|
398
443
|
function formatBudget(goal: ActiveGoal) {
|
|
@@ -406,9 +451,16 @@ function goalSummary(goal: ActiveGoal) {
|
|
|
406
451
|
`Iteration: ${goal.iteration}`,
|
|
407
452
|
`Elapsed: ${formatDuration(goal.timeUsedSeconds)}`,
|
|
408
453
|
`Tokens: ${goal.tokenBudget === undefined ? formatTokenCount(goal.tokensUsed) : formatBudget(goal)}`,
|
|
454
|
+
`Commands: ${goalCommandHint(goal.status)}`,
|
|
409
455
|
].join("\n");
|
|
410
456
|
}
|
|
411
457
|
|
|
458
|
+
function goalCommandHint(status: GoalStatus) {
|
|
459
|
+
if (status === "active") return "/goal edit <objective>, /goal pause, /goal clear";
|
|
460
|
+
if (status === "paused") return "/goal edit <objective>, /goal resume, /goal clear";
|
|
461
|
+
return "/goal edit <objective>, /goal clear";
|
|
462
|
+
}
|
|
463
|
+
|
|
412
464
|
function formatDuration(seconds: number) {
|
|
413
465
|
if (seconds < 60) return `${seconds}s`;
|
|
414
466
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -428,6 +480,11 @@ function buildGoalPrompt(goal: ActiveGoal) {
|
|
|
428
480
|
return `Goal mode is active. Complete this goal fully:\n\n${goal.text}${budgetLine}\n\nKeep working until the goal is done. Do not stop after planning or partial progress. When the goal is fully complete and verified, call the goal_complete tool with a concise completion summary.`;
|
|
429
481
|
}
|
|
430
482
|
|
|
483
|
+
function buildObjectiveUpdatedPrompt(goal: ActiveGoal) {
|
|
484
|
+
const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatBudget(goal)} used.`;
|
|
485
|
+
return `The active /goal objective was updated. Continue working toward this goal:\n\n${goal.text}${budgetLine}\n\nKeep working until the updated goal is fully complete and verified, then call the goal_complete tool.`;
|
|
486
|
+
}
|
|
487
|
+
|
|
431
488
|
function buildGoalSystemPrompt(goal: ActiveGoal) {
|
|
432
489
|
const budgetLine = goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
|
|
433
490
|
return `Active /goal: ${goal.text}\n\nGoal-mode rules:\n- Continue making concrete progress until the active goal is fully complete.\n- Do not end your response with only a plan, TODO list, or partial progress.\n- Verify the result when possible using appropriate checks.\n- If the goal is not complete at the end of a turn, expect an automatic follow-up and continue from where you left off.\n- Only call the goal_complete tool after the goal is fully complete and verified.${budgetLine}`;
|
|
@@ -452,18 +509,28 @@ function currentTokenTotal(ctx: StatusContext): number {
|
|
|
452
509
|
return total;
|
|
453
510
|
}
|
|
454
511
|
|
|
455
|
-
function persistGoal(
|
|
456
|
-
|
|
512
|
+
function persistGoal(goal: ActiveGoal) {
|
|
513
|
+
extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal });
|
|
457
514
|
}
|
|
458
515
|
|
|
459
516
|
function clearPersistedGoal(cwd: string) {
|
|
460
|
-
|
|
517
|
+
extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal: null });
|
|
518
|
+
clearLegacyPersistedGoal(cwd);
|
|
461
519
|
}
|
|
462
520
|
|
|
463
|
-
function
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
521
|
+
function loadGoalFromSession(ctx: StatusContext): ActiveGoal | undefined {
|
|
522
|
+
const sessionManager = ctx.sessionManager as
|
|
523
|
+
| {
|
|
524
|
+
getBranch?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
|
|
525
|
+
getEntries?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
|
|
526
|
+
}
|
|
527
|
+
| undefined;
|
|
528
|
+
const entries = sessionManager?.getBranch?.() ?? sessionManager?.getEntries?.() ?? [];
|
|
529
|
+
const entry = entries
|
|
530
|
+
.filter((entry) => entry.type === "custom" && entry.customType === GOAL_STATE_ENTRY_TYPE)
|
|
531
|
+
.pop();
|
|
532
|
+
const data = entry?.data as GoalStateEntryData | undefined;
|
|
533
|
+
return isGoal(data?.goal) && data.goal.status !== "complete" ? data.goal : undefined;
|
|
467
534
|
}
|
|
468
535
|
|
|
469
536
|
function clearActiveGoal(ctx: StatusContext) {
|
|
@@ -474,7 +541,7 @@ function clearActiveGoal(ctx: StatusContext) {
|
|
|
474
541
|
|
|
475
542
|
function showCompletionStatus(ctx: StatusContext) {
|
|
476
543
|
if (completionStatusTimer) clearTimeout(completionStatusTimer);
|
|
477
|
-
ctx.ui.setStatus(STATUS_KEY, "
|
|
544
|
+
ctx.ui.setStatus(STATUS_KEY, "🎯 complete");
|
|
478
545
|
completionStatusTimer = setTimeout(() => ctx.ui.setStatus(STATUS_KEY, undefined), 8_000);
|
|
479
546
|
}
|
|
480
547
|
|
|
@@ -490,14 +557,15 @@ function readState(): Record<string, unknown> {
|
|
|
490
557
|
}
|
|
491
558
|
}
|
|
492
559
|
|
|
493
|
-
function
|
|
560
|
+
function clearLegacyPersistedGoal(cwd: string) {
|
|
561
|
+
if (!existsSync(STATE_FILE)) return;
|
|
494
562
|
const goals = readState();
|
|
495
|
-
|
|
496
|
-
else delete goals[cwd];
|
|
563
|
+
delete goals[cwd];
|
|
497
564
|
mkdirSync(dirname(STATE_FILE), { recursive: true });
|
|
498
565
|
writeFileSync(STATE_FILE, `${JSON.stringify(goals, null, 2)}\n`);
|
|
499
566
|
}
|
|
500
567
|
|
|
568
|
+
|
|
501
569
|
function isGoal(value: unknown): value is ActiveGoal {
|
|
502
570
|
if (!value || typeof value !== "object") return false;
|
|
503
571
|
const goal = value as Partial<ActiveGoal>;
|