@kky42/pi-goal 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.0.2 - 2026-05-30
6
+
7
+ - Prevents `/goal` argument autocomplete from injecting `pause`, `resume`, or `clear` into free-form goal objectives while preserving explicit subcommand completion.
8
+ - Sends overflow-recovery goal continuations as user-started follow-ups when pi's host overflow cap needs a user turn, while keeping normal continuations hidden.
9
+ - Makes headless `/goal <objective>` replace existing non-complete goals deterministically and drain scheduled goal continuations until the goal leaves `active`, instead of exiting without model output or after only the first turn.
10
+
5
11
  ## 1.0.1 - 2026-05-30
6
12
 
7
13
  - Exposes the extension through a root `index.ts` package entry so pi displays the installed package as `@kky42/pi-goal` instead of `@kky42/pi-goal:src`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kky42/pi-goal",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Codex-style goal tracking and continuation for pi.",
5
5
  "type": "module",
6
6
  "author": "kky42",
package/src/commands.ts CHANGED
@@ -8,7 +8,7 @@ export interface CommandHost {
8
8
  getGoal(): ThreadGoal | null;
9
9
  setGoal(goal: ThreadGoal, source: GoalEntrySource, ctx: GoalCommandContext): void;
10
10
  clearGoal(source: GoalEntrySource, ctx: GoalCommandContext): void;
11
- requestContinuation(ctx: GoalCommandContext): void;
11
+ requestContinuation(ctx: GoalCommandContext): boolean;
12
12
  }
13
13
 
14
14
  const COMMANDS = ["pause", "resume", "clear"] as const;
@@ -19,14 +19,38 @@ export type GoalCommandPi = Pick<ExtensionAPI, "registerCommand">;
19
19
  export interface GoalCommandContext {
20
20
  hasUI: boolean;
21
21
  ui: Pick<ExtensionCommandContext["ui"], "confirm" | "notify" | "setStatus">;
22
+ waitForIdle?: ExtensionCommandContext["waitForIdle"];
22
23
  }
23
24
 
24
- function completions(prefix: string) {
25
- return COMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
25
+ async function waitForHeadlessContinuationDrain(
26
+ host: CommandHost,
27
+ ctx: GoalCommandContext,
28
+ ): Promise<void> {
29
+ if (ctx.hasUI || !ctx.waitForIdle) {
30
+ return;
31
+ }
32
+ while (host.getGoal()?.status === "active") {
33
+ await ctx.waitForIdle();
34
+ if (host.getGoal()?.status !== "active") {
35
+ return;
36
+ }
37
+ if (!host.requestContinuation(ctx)) {
38
+ return;
39
+ }
40
+ }
41
+ }
42
+
43
+ function completions(argumentPrefix: string) {
44
+ const prefix = argumentPrefix.trim();
45
+ if (prefix.length === 0 || /\s/.test(argumentPrefix)) {
46
+ return null;
47
+ }
48
+ const items = COMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
26
49
  value: command,
27
50
  label: command,
28
51
  description: `goal ${command}`,
29
52
  }));
53
+ return items.length > 0 ? items : null;
30
54
  }
31
55
 
32
56
  export async function handleGoalCommand(
@@ -64,16 +88,13 @@ export async function handleGoalCommand(
64
88
  ctx.ui.notify(result.message);
65
89
  if (trimmed === "resume" && result.goal.status === "active") {
66
90
  host.requestContinuation(ctx);
91
+ await waitForHeadlessContinuationDrain(host, ctx);
67
92
  }
68
93
  return;
69
94
  }
70
95
 
71
96
  const current = host.getGoal();
72
- if (current && current.status !== "complete") {
73
- if (!ctx.hasUI) {
74
- ctx.ui.notify("Clear the existing goal before replacing it.", "error");
75
- return;
76
- }
97
+ if (current && current.status !== "complete" && ctx.hasUI) {
77
98
  const shouldReplace = await ctx.ui.confirm(
78
99
  "Replace goal?",
79
100
  `Current goal:\n${current.objective}\n\nNew goal:\n${trimmed}`,
@@ -92,13 +113,14 @@ export async function handleGoalCommand(
92
113
  host.setGoal(result.goal, "command", ctx);
93
114
  ctx.ui.notify([GOLDEN_SET_BANNER, formatGoalSummary(result.goal)].join("\n"));
94
115
  host.requestContinuation(ctx);
116
+ await waitForHeadlessContinuationDrain(host, ctx);
95
117
  }
96
118
 
97
119
  export function registerGoalCommand(pi: GoalCommandPi, host: CommandHost): void {
98
120
  pi.registerCommand("goal", {
99
121
  description: "Show or manage the current Codex-style goal.",
100
122
  getArgumentCompletions(argumentPrefix) {
101
- return completions(argumentPrefix.trim());
123
+ return completions(argumentPrefix);
102
124
  },
103
125
  async handler(args: string, ctx: ExtensionCommandContext) {
104
126
  await handleGoalCommand(pi, host, args, ctx);
@@ -1,7 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { continuationGoalIdFromPrompt, continuationPrompt } from "./prompts.js";
3
+ import { continuationGoalIdFromPrompt, continuationPrompt, markedContinuationPrompt } from "./prompts.js";
4
4
  import {
5
+ goalStartTurnStrategy,
5
6
  recoveryPhaseBlocksContinuation,
6
7
  type GoalRecoveryMachineState,
7
8
  } from "./recovery-machine.js";
@@ -11,7 +12,7 @@ import type { StaleQueuedWorkGuard } from "./stale-queued-work-guard.js";
11
12
  import { CUSTOM_ENTRY_TYPE, type ThreadGoal } from "./types.js";
12
13
 
13
14
  interface ContinuationSchedulerDeps {
14
- pi: Pick<ExtensionAPI, "sendMessage">;
15
+ pi: Pick<ExtensionAPI, "sendMessage" | "sendUserMessage">;
15
16
  getGoal: () => ThreadGoal | null;
16
17
  getRecoveryState: () => GoalRecoveryMachineState;
17
18
  staleQueuedWorkGuard: StaleQueuedWorkGuard;
@@ -96,6 +97,10 @@ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
96
97
 
97
98
  const sendContinuation = (goalToContinue: ThreadGoal): void => {
98
99
  continuationQueuedFor = goalToContinue.goalId;
100
+ if (goalStartTurnStrategy(deps.getRecoveryState().phase) === "userFollowUp") {
101
+ deps.pi.sendUserMessage(markedContinuationPrompt(goalToContinue), { deliverAs: "followUp" });
102
+ return;
103
+ }
99
104
  deps.pi.sendMessage(
100
105
  {
101
106
  customType: CUSTOM_ENTRY_TYPE,
@@ -107,17 +112,20 @@ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
107
112
  );
108
113
  };
109
114
 
110
- const requestContinuation = (ctx: ExtensionContext): void => {
115
+ const requestContinuation = (ctx: ExtensionContext): boolean => {
111
116
  const goal = deps.getGoal();
112
117
  if (
113
118
  deps.staleQueuedWorkGuard.isBlockingContinuation() ||
114
119
  !goal ||
115
120
  goal.status !== "active" ||
116
- continuationQueuedFor === goal.goalId ||
117
121
  hasPendingRecoveryAttention() ||
118
122
  recoveryPhaseBlocksContinuation(deps.getRecoveryState().phase)
119
123
  ) {
120
- return;
124
+ return false;
125
+ }
126
+
127
+ if (continuationQueuedFor === goal.goalId) {
128
+ return true;
121
129
  }
122
130
 
123
131
  const goalId = goal.goalId;
@@ -125,12 +133,12 @@ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
125
133
  if (continuationScheduledFor === goalId) {
126
134
  clearContinuationTimer();
127
135
  }
128
- return;
136
+ return false;
129
137
  }
130
138
 
131
139
  if (!ctx.isIdle()) {
132
140
  if (continuationScheduledFor === goalId) {
133
- return;
141
+ return true;
134
142
  }
135
143
  continuationScheduledFor = goalId;
136
144
  continuationTimer = setTimeout(() => {
@@ -139,7 +147,7 @@ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
139
147
  requestContinuation(ctx);
140
148
  }, CONTINUATION_RETRY_MS);
141
149
  continuationTimer.unref?.();
142
- return;
150
+ return true;
143
151
  }
144
152
 
145
153
  clearContinuationTimer();
@@ -155,9 +163,10 @@ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
155
163
  hasPendingRecoveryAttention() ||
156
164
  recoveryPhaseBlocksContinuation(deps.getRecoveryState().phase)
157
165
  ) {
158
- return;
166
+ return false;
159
167
  }
160
168
  sendContinuation(currentGoal);
169
+ return true;
161
170
  };
162
171
 
163
172
  return {
package/src/format.ts CHANGED
@@ -45,7 +45,7 @@ function twoDigit(value: number): string {
45
45
  export function formatLocalTimestamp(unixSeconds: number): string {
46
46
  const date = new Date(Math.max(0, Math.trunc(unixSeconds)) * 1000);
47
47
  const day = `${date.getFullYear()}-${twoDigit(date.getMonth() + 1)}-${twoDigit(date.getDate())}`;
48
- const time = `${twoDigit(date.getHours())}-${twoDigit(date.getMinutes())}-${twoDigit(date.getSeconds())}`;
48
+ const time = `${twoDigit(date.getHours())}:${twoDigit(date.getMinutes())}:${twoDigit(date.getSeconds())}`;
49
49
  return `${day} ${time}`;
50
50
  }
51
51
 
@@ -27,7 +27,7 @@ export interface GoalRuntimeController extends GoalRuntimeEventHandlers {
27
27
  setGoal(goal: ThreadGoal, source: GoalEntrySource, ctx: ExtensionContext): void;
28
28
  clearGoal(source: GoalEntrySource, ctx: ExtensionContext): void;
29
29
  updateGoal(status: "complete" | "blocked", source: GoalEntrySource, ctx: ExtensionContext): GoalResult;
30
- requestContinuation(ctx: ExtensionContext): void;
30
+ requestContinuation(ctx: ExtensionContext): boolean;
31
31
  }
32
32
 
33
33
  export function createGoalRuntimeController(pi: ExtensionAPI): GoalRuntimeController {
@@ -54,7 +54,7 @@ export interface GoalRuntimeContinuationPort {
54
54
  clearPassthroughContinuationInput: () => void;
55
55
  continuationGoalIdFromRuntimePrompt: (prompt: string) => string | null;
56
56
  notePassthroughContinuationInput: (input: string) => void;
57
- requestContinuation: (ctx: ExtensionContext) => void;
57
+ requestContinuation: (ctx: ExtensionContext) => boolean;
58
58
  }
59
59
 
60
60
  export interface GoalAccountingPort {
@@ -130,7 +130,6 @@ export function createInputContextEventHandlers(
130
130
 
131
131
  continuation.clearContinuationStateFor(queuedGoalId);
132
132
  if (stateController.isCurrentActiveGoalId(queuedGoalId)) {
133
- stateController.persistHostOverflowUserReset(false);
134
133
  runtimeState.staleQueuedWorkGuard.noteRunnableWorkStarted();
135
134
  if (isCommandResumeQueuedGoalMessage(event.message)) {
136
135
  resetErrorRecovery();
package/src/prompts.ts CHANGED
@@ -36,6 +36,10 @@ export function continuationGoalIdFromPrompt(prompt: string): string | null {
36
36
  return prompt.slice(CONTINUATION_MARKER_PREFIX.length, end);
37
37
  }
38
38
 
39
+ export function markedContinuationPrompt(goal: ThreadGoal): string {
40
+ return [`${CONTINUATION_MARKER_PREFIX}${goal.goalId}" />`, "", continuationPrompt(goal)].join("\n");
41
+ }
42
+
39
43
  function formatOptionalTokenBudget(goal: ThreadGoal): string {
40
44
  return goal.tokenBudget === null ? "none" : formatTokenValue(goal.tokenBudget);
41
45
  }
@@ -71,6 +71,7 @@ export function onRecoverySuccessfulTurn(
71
71
  return false;
72
72
  }
73
73
  resetRecoveryCounters(state);
74
+ clearActiveHostOverflowRecovery(state);
74
75
  return true;
75
76
  }
76
77
 
@@ -19,7 +19,7 @@ interface RecoveryRuntimeDeps {
19
19
  clearContinuationState: () => void;
20
20
  pauseGoalForRecovery: (ctx: ExtensionContext, recoveryReason: string) => void;
21
21
  refreshUi: (ctx: ExtensionContext) => void;
22
- requestContinuation: (ctx: ExtensionContext) => void;
22
+ requestContinuation: (ctx: ExtensionContext) => boolean;
23
23
  }
24
24
 
25
25
  export function createGoalRecoveryRuntime(deps: RecoveryRuntimeDeps) {