@narumitw/pi-goal 0.1.17 → 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.
Files changed (3) hide show
  1. package/README.md +23 -21
  2. package/package.json +1 -1
  3. package/src/goal.ts +117 -49
package/README.md CHANGED
@@ -2,19 +2,19 @@
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
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
- - 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
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 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
 
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.18",
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,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(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;
@@ -179,25 +177,34 @@ export default function goal(pi: ExtensionAPI) {
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 {
@@ -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 "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)}`;
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(cwd: string, goal: ActiveGoal) {
456
- writeState(cwd, goal);
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
- writeState(cwd, undefined);
517
+ extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal: null });
518
+ clearLegacyPersistedGoal(cwd);
461
519
  }
462
520
 
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;
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, "goal: complete");
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 writeState(cwd: string, goal: ActiveGoal | undefined) {
560
+ function clearLegacyPersistedGoal(cwd: string) {
561
+ if (!existsSync(STATE_FILE)) return;
494
562
  const goals = readState();
495
- if (goal) goals[cwd] = goal;
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>;