@kky42/pi-goal 1.0.0 → 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 +10 -0
- package/index.ts +1 -0
- package/package.json +5 -3
- package/src/commands.ts +31 -9
- package/src/continuation-scheduler.ts +18 -9
- package/src/format.ts +1 -1
- package/src/goal-runtime-controller.ts +1 -1
- package/src/goal-runtime-event-handler-types.ts +1 -1
- package/src/goal-runtime-input-context-handlers.ts +0 -1
- package/src/prompts.ts +4 -0
- package/src/recovery-machine.ts +1 -0
- package/src/recovery-runtime.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
11
|
+
## 1.0.1 - 2026-05-30
|
|
12
|
+
|
|
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`.
|
|
14
|
+
|
|
5
15
|
## 1.0.0 - 2026-05-30
|
|
6
16
|
|
|
7
17
|
- Renames the public package to `@kky42/pi-goal` for npm and pi package installs.
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, __testHooks } from "./src/index.js";
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kky42/pi-goal",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Codex-style goal tracking and continuation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "kky42",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"main": "
|
|
8
|
+
"main": "./index.ts",
|
|
9
|
+
"exports": "./index.ts",
|
|
9
10
|
"files": [
|
|
11
|
+
"index.ts",
|
|
10
12
|
"src",
|
|
11
13
|
"README.md",
|
|
12
14
|
"CHANGELOG.md",
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
"homepage": "https://github.com/kky42/pi-goal#readme",
|
|
31
33
|
"pi": {
|
|
32
34
|
"extensions": [
|
|
33
|
-
"./
|
|
35
|
+
"./index.ts"
|
|
34
36
|
]
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|
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):
|
|
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
|
|
25
|
-
|
|
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
|
|
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):
|
|
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())}
|
|
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):
|
|
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) =>
|
|
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
|
}
|
package/src/recovery-machine.ts
CHANGED
package/src/recovery-runtime.ts
CHANGED
|
@@ -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) =>
|
|
22
|
+
requestContinuation: (ctx: ExtensionContext) => boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function createGoalRecoveryRuntime(deps: RecoveryRuntimeDeps) {
|