@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.
Files changed (3) hide show
  1. package/README.md +27 -25
  2. package/package.json +1 -1
  3. package/src/goal.ts +131 -54
package/README.md CHANGED
@@ -2,21 +2,21 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-goal)](https://www.npmjs.com/package/@narumitw/pi-goal) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- `@narumitw/pi-goal` is a native [Pi coding agent](https://pi.dev) extension that adds durable `/goal` commands and a `goal_complete` tool for autonomous, verifiable task completion.
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 automatic follow-up messages until the agent calls `goal_complete`, the user pauses or clears the goal, or an optional token budget is reached.
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
- - Adds `/goal pause`, `/goal resume`, `/goal clear`, and `/goal edit <goal_to_complete>`.
14
- - Keeps `/goal-status` and `/goal-stop` as compatibility aliases.
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
- - Persists in-progress goal state per working directory under the Pi agent config directory.
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 token usage.
56
- - `/goal <goal_to_complete>` starts goal mode when no other goal is active.
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 clears persisted state.
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
- - `goal: active 3m` — an active goal without a token budget.
72
- - `goal: active 18k/100k` — an active goal with token usage and budget.
73
- - `goal: paused` — auto-continuation is paused.
74
- - `goal: budget 100k/100k` — the token budget was reached; auto-continuation stops.
75
- - `goal: complete` — shown briefly after `goal_complete` succeeds.
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 tells the agent to keep working, verify the result, and call `goal_complete` only when the goal is fully done.
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 follow-up prompt to continue the same goal.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-goal",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Pi extension that keeps working on a /goal until the agent marks it complete.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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(ctx.cwd, activeGoal);
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 = loadGoal(ctx.cwd);
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(ctx.cwd, activeGoal);
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(ctx.cwd, activeGoal);
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(ctx.cwd, activeGoal);
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.sendUserMessage(buildContinuePrompt(currentGoal), { deliverAs: "followUp" });
176
+ sendContinuationPrompt(pi, ctx, currentGoal);
179
177
  });
180
178
  }
181
179
 
182
- function startGoal(objective: string, tokenBudget: number | undefined, pi: ExtensionAPI, ctx: StatusContext) {
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
- if (activeGoal && activeGoal.status !== "complete") {
190
- ctx.ui.notify(
191
- `A goal is already ${activeGoal.status}. Use /goal edit <objective> to replace it, /goal clear to stop it, or /goal to inspect it.`,
192
- "warning",
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
- return;
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(ctx.cwd, activeGoal);
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(ctx.cwd, activeGoal);
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(ctx.cwd, activeGoal);
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(objective: string, tokenBudget: number | undefined, ctx: StatusContext) {
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 = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
255
- persistGoal(ctx.cwd, activeGoal);
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("No active goal.", "info");
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(ctx.cwd, activeGoal);
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
- const prompt = buildGoalPrompt(goal);
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 "goal: complete";
392
- if (goal.status === "paused") return "goal: paused";
393
- if (goal.status === "budget_limited") return `goal: budget ${formatBudget(goal)}`;
394
- if (goal.tokenBudget !== undefined) return `goal: active ${formatBudget(goal)}`;
395
- return `goal: active ${formatDuration(goal.timeUsedSeconds)}`;
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\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.`;
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- 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}`;
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}. If the goal is not complete yet, keep working and verify progress. If it is fully complete and verified, call the goal_complete tool.`;
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(cwd: string, goal: ActiveGoal) {
456
- writeState(cwd, goal);
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
- writeState(cwd, undefined);
526
+ extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal: null });
527
+ clearLegacyPersistedGoal(cwd);
461
528
  }
462
529
 
463
- function loadGoal(cwd: string): ActiveGoal | undefined {
464
- const goals = readState();
465
- const stored = goals[cwd];
466
- return isGoal(stored) && stored.status !== "complete" ? stored : undefined;
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, "goal: complete");
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 writeState(cwd: string, goal: ActiveGoal | undefined) {
569
+ function clearLegacyPersistedGoal(cwd: string) {
570
+ if (!existsSync(STATE_FILE)) return;
494
571
  const goals = readState();
495
- if (goal) goals[cwd] = goal;
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>;