@narumitw/pi-goal 0.1.21 → 0.1.23
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 +16 -7
- package/package.json +1 -1
- package/src/goal.ts +203 -24
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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 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.
|
|
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, an interrupt/error pauses the goal, or an optional token budget is reached.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
@@ -16,9 +16,10 @@ Goal mode uses Codex-like persistence instructions and keeps sending guarded con
|
|
|
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
|
-
- Automatically prompts the agent to continue if an active turn ends early, directly triggering the next turn when Pi is idle.
|
|
20
|
-
-
|
|
21
|
-
-
|
|
19
|
+
- Automatically prompts the agent to continue if an active turn ends early, directly triggering the next turn when Pi is idle and no pending messages are queued.
|
|
20
|
+
- Pauses instead of auto-continuing when Pi reports an aborted or errored assistant turn.
|
|
21
|
+
- Guards auto-follow-ups so duplicate, replaced, paused, cleared, completed, or budget-limited goals are not continued.
|
|
22
|
+
- Encourages requirement-by-requirement verification before the goal is marked complete.
|
|
22
23
|
|
|
23
24
|
## 📦 Install
|
|
24
25
|
|
|
@@ -55,7 +56,7 @@ pi -e ./extensions/pi-goal
|
|
|
55
56
|
- `/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
57
|
- `/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.
|
|
57
58
|
- `/goal pause` stops prompt injection and auto-continuation without forgetting the goal.
|
|
58
|
-
- `/goal resume` resumes a paused or budget-limited goal when the token budget allows it.
|
|
59
|
+
- `/goal resume` resumes a paused or budget-limited goal when the token budget allows it, then queues a resume prompt so work continues.
|
|
59
60
|
- `/goal clear` cancels the current goal and also removes any legacy persisted state for the current working directory.
|
|
60
61
|
|
|
61
62
|
Goal objectives are limited to 4,000 characters. Put longer instructions in a file and reference the file path from `/goal`.
|
|
@@ -78,9 +79,17 @@ Older versions wrote unfinished goals to `~/.pi/agent/pi-goal-state.json` keyed
|
|
|
78
79
|
|
|
79
80
|
## ✅ How completion works
|
|
80
81
|
|
|
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,
|
|
82
|
+
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, treat current files, command output, tests, and external state as authoritative, avoid redefining the goal into a smaller task, and call `goal_complete` only after requirement-by-requirement verification.
|
|
82
83
|
|
|
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,
|
|
84
|
+
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, and sends a continuation prompt only when no user or extension messages are already pending. 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. A continuation-pending guard prevents repeated end events from enqueueing duplicate continuations.
|
|
85
|
+
|
|
86
|
+
## 🛑 Interruption and queued-input behavior
|
|
87
|
+
|
|
88
|
+
When Pi reports the final assistant message with `stopReason: "aborted"` or `stopReason: "error"`, `pi-goal` records usage, changes the goal to `paused`, and does not auto-continue. Run `/goal resume` to continue after reviewing the interruption or error.
|
|
89
|
+
|
|
90
|
+
If user or extension messages are already queued at the end of a goal turn, those messages take priority and `pi-goal` skips that automatic continuation. Active goal instructions are still injected into the next agent turn, and normal continuation can resume after pending work finishes.
|
|
91
|
+
|
|
92
|
+
Queued automatic continuation prompts are tracked by goal id and iteration. If a queued continuation is invalidated by `/goal pause`, `/goal clear`, `/goal edit`, replacement, or completion before delivery, the extension handles that stale prompt instead of letting it restart old goal work.
|
|
84
93
|
|
|
85
94
|
## 🧠 Use cases
|
|
86
95
|
|
package/package.json
CHANGED
package/src/goal.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
|
|
8
8
|
type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
|
|
9
|
+
type AgentStopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
|
|
9
10
|
|
|
10
11
|
interface ActiveGoal {
|
|
11
12
|
id: string;
|
|
@@ -25,6 +26,19 @@ interface GoalCompleteDetails {
|
|
|
25
26
|
summary: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface ContinuationPending {
|
|
30
|
+
goalId: string;
|
|
31
|
+
iteration: number;
|
|
32
|
+
marker: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AssistantMessageLike {
|
|
37
|
+
role: "assistant";
|
|
38
|
+
stopReason?: AgentStopReason;
|
|
39
|
+
errorMessage?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
interface GoalStateEntryData {
|
|
29
43
|
goal?: ActiveGoal | null;
|
|
30
44
|
}
|
|
@@ -43,12 +57,15 @@ interface StatusContext {
|
|
|
43
57
|
setStatus: (key: string, value: string | undefined) => void;
|
|
44
58
|
};
|
|
45
59
|
isIdle?: () => boolean;
|
|
60
|
+
hasPendingMessages?: () => boolean;
|
|
46
61
|
sessionManager?: unknown;
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const STATUS_KEY = "goal";
|
|
50
65
|
const GOAL_STATE_ENTRY_TYPE = "goal-state";
|
|
51
66
|
const MAX_OBJECTIVE_LENGTH = 4_000;
|
|
67
|
+
const MAX_CANCELLED_CONTINUATION_PROMPTS = 20;
|
|
68
|
+
const CONTINUATION_MARKER_PREFIX = "pi-goal-continuation:";
|
|
52
69
|
const STATE_FILE = join(
|
|
53
70
|
process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? ".", ".pi", "agent"),
|
|
54
71
|
"pi-goal-state.json",
|
|
@@ -57,6 +74,8 @@ const STATE_FILE = join(
|
|
|
57
74
|
let activeGoal: ActiveGoal | undefined;
|
|
58
75
|
let completionStatusTimer: NodeJS.Timeout | undefined;
|
|
59
76
|
let extensionApi: ExtensionAPI | undefined;
|
|
77
|
+
let continuationPending: ContinuationPending | undefined;
|
|
78
|
+
const cancelledContinuationMarkers = new Set<string>();
|
|
60
79
|
|
|
61
80
|
const goalCompleteTool = defineTool({
|
|
62
81
|
name: "goal_complete",
|
|
@@ -66,6 +85,7 @@ const goalCompleteTool = defineTool({
|
|
|
66
85
|
promptSnippet: "Mark the active /goal as complete after fully finishing and verifying it",
|
|
67
86
|
promptGuidelines: [
|
|
68
87
|
"When a /goal is active, keep working until the goal is complete; do not stop with only a plan or partial progress.",
|
|
88
|
+
"Before calling goal_complete, audit the active goal requirement by requirement against the current files, command output, tests, or external state.",
|
|
69
89
|
"Call goal_complete only after the requested goal is fully implemented, verified, and no known required work remains.",
|
|
70
90
|
],
|
|
71
91
|
parameters: Type.Object({
|
|
@@ -118,13 +138,13 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
118
138
|
pauseGoal(ctx);
|
|
119
139
|
return;
|
|
120
140
|
case "resume":
|
|
121
|
-
resumeGoal(ctx);
|
|
141
|
+
await resumeGoal(pi, ctx);
|
|
122
142
|
return;
|
|
123
143
|
case "clear":
|
|
124
144
|
clearGoal(ctx);
|
|
125
145
|
return;
|
|
126
146
|
case "edit":
|
|
127
|
-
editGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
147
|
+
await editGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
128
148
|
return;
|
|
129
149
|
case "start":
|
|
130
150
|
await startGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
|
|
@@ -134,6 +154,7 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
134
154
|
});
|
|
135
155
|
|
|
136
156
|
pi.on("session_start", (_event, ctx) => {
|
|
157
|
+
clearContinuationTracking();
|
|
137
158
|
activeGoal = loadGoalFromSession(ctx);
|
|
138
159
|
if (activeGoal) updateStatus(ctx, activeGoal);
|
|
139
160
|
else ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -141,11 +162,18 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
141
162
|
|
|
142
163
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
143
164
|
if (activeGoal) persistGoal(activeGoal);
|
|
165
|
+
clearContinuationTracking();
|
|
144
166
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
145
167
|
if (completionStatusTimer) clearTimeout(completionStatusTimer);
|
|
146
168
|
});
|
|
147
169
|
|
|
170
|
+
pi.on("input", (event) => {
|
|
171
|
+
if (event.source !== "extension") return;
|
|
172
|
+
if (consumeCancelledContinuationPrompt(event.text)) return { action: "handled" as const };
|
|
173
|
+
});
|
|
174
|
+
|
|
148
175
|
pi.on("before_agent_start", (event) => {
|
|
176
|
+
markContinuationDelivered(event.prompt);
|
|
149
177
|
if (!activeGoal || activeGoal.status !== "active") return;
|
|
150
178
|
|
|
151
179
|
return {
|
|
@@ -153,14 +181,23 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
153
181
|
};
|
|
154
182
|
});
|
|
155
183
|
|
|
156
|
-
pi.on("agent_end", (
|
|
184
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
157
185
|
if (!activeGoal || activeGoal.status !== "active") return;
|
|
158
186
|
|
|
159
187
|
const goalId = activeGoal.id;
|
|
160
|
-
|
|
188
|
+
const hadPendingContinuation = continuationPending?.goalId === goalId;
|
|
189
|
+
const finalAssistant = findFinalAssistantMessage(event.messages);
|
|
190
|
+
|
|
191
|
+
if (!hadPendingContinuation) activeGoal = incrementGoal(activeGoal);
|
|
161
192
|
updateGoalUsage(activeGoal, ctx);
|
|
162
193
|
|
|
194
|
+
if (finalAssistant?.stopReason === "aborted" || finalAssistant?.stopReason === "error") {
|
|
195
|
+
pauseGoalAfterAgentEnd(ctx, activeGoal, finalAssistant);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
163
199
|
if (activeGoal.tokenBudget !== undefined && activeGoal.tokensUsed >= activeGoal.tokenBudget) {
|
|
200
|
+
cancelContinuationPending();
|
|
164
201
|
activeGoal = transitionGoal(activeGoal, "budget_limited");
|
|
165
202
|
persistGoal(activeGoal);
|
|
166
203
|
updateStatus(ctx, activeGoal);
|
|
@@ -171,9 +208,15 @@ export default function goal(pi: ExtensionAPI) {
|
|
|
171
208
|
persistGoal(activeGoal);
|
|
172
209
|
updateStatus(ctx, activeGoal);
|
|
173
210
|
|
|
211
|
+
if (hadPendingContinuation) {
|
|
212
|
+
if (hasPendingMessages(ctx)) return;
|
|
213
|
+
if (continuationPending?.goalId === goalId) continuationPending = undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
174
216
|
const currentGoal = activeGoal;
|
|
175
217
|
if (!currentGoal || currentGoal.id !== goalId || currentGoal.status !== "active") return;
|
|
176
|
-
|
|
218
|
+
if (hasPendingMessages(ctx)) return;
|
|
219
|
+
await sendContinuationPrompt(pi, ctx, currentGoal);
|
|
177
220
|
});
|
|
178
221
|
}
|
|
179
222
|
|
|
@@ -201,11 +244,12 @@ async function startGoal(
|
|
|
201
244
|
}
|
|
202
245
|
}
|
|
203
246
|
|
|
247
|
+
cancelContinuationPending();
|
|
204
248
|
activeGoal = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
|
|
205
249
|
persistGoal(activeGoal);
|
|
206
250
|
updateStatus(ctx, activeGoal);
|
|
207
251
|
ctx.ui.notify(existingGoal ? `Goal replaced: ${objective}` : `Goal started: ${objective}`, "info");
|
|
208
|
-
sendGoalPrompt(pi, ctx, activeGoal);
|
|
252
|
+
await sendGoalPrompt(pi, ctx, activeGoal);
|
|
209
253
|
}
|
|
210
254
|
|
|
211
255
|
function pauseGoal(ctx: StatusContext) {
|
|
@@ -217,13 +261,14 @@ function pauseGoal(ctx: StatusContext) {
|
|
|
217
261
|
ctx.ui.notify(`Goal is ${activeGoal.status}; only active goals can be paused.`, "warning");
|
|
218
262
|
return;
|
|
219
263
|
}
|
|
264
|
+
cancelContinuationPending();
|
|
220
265
|
activeGoal = transitionGoal(activeGoal, "paused");
|
|
221
266
|
persistGoal(activeGoal);
|
|
222
267
|
updateStatus(ctx, activeGoal);
|
|
223
268
|
ctx.ui.notify(`Goal paused: ${activeGoal.text}`, "info");
|
|
224
269
|
}
|
|
225
270
|
|
|
226
|
-
function resumeGoal(ctx: StatusContext) {
|
|
271
|
+
async function resumeGoal(pi: ExtensionAPI, ctx: StatusContext) {
|
|
227
272
|
if (!activeGoal) {
|
|
228
273
|
ctx.ui.notify("No active goal.", "info");
|
|
229
274
|
return;
|
|
@@ -235,12 +280,18 @@ function resumeGoal(ctx: StatusContext) {
|
|
|
235
280
|
activeGoal = transitionGoal(activeGoal, "active");
|
|
236
281
|
persistGoal(activeGoal);
|
|
237
282
|
updateStatus(ctx, activeGoal);
|
|
283
|
+
if (activeGoal.status !== "active") {
|
|
284
|
+
ctx.ui.notify(`Goal token budget is still reached: ${formatBudget(activeGoal)}`, "warning");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
238
287
|
ctx.ui.notify(`Goal resumed: ${activeGoal.text}`, "info");
|
|
288
|
+
await sendResumePrompt(pi, ctx, activeGoal);
|
|
239
289
|
}
|
|
240
290
|
|
|
241
291
|
function clearGoal(ctx: StatusContext) {
|
|
242
292
|
if (!activeGoal) {
|
|
243
293
|
ctx.ui.notify("No active goal.", "info");
|
|
294
|
+
cancelContinuationPending();
|
|
244
295
|
clearPersistedGoal(ctx.cwd);
|
|
245
296
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
246
297
|
return;
|
|
@@ -251,7 +302,7 @@ function clearGoal(ctx: StatusContext) {
|
|
|
251
302
|
ctx.ui.notify(`Goal cleared: ${stoppedGoal}`, "warning");
|
|
252
303
|
}
|
|
253
304
|
|
|
254
|
-
function editGoal(
|
|
305
|
+
async function editGoal(
|
|
255
306
|
objective: string,
|
|
256
307
|
tokenBudget: number | undefined,
|
|
257
308
|
pi: ExtensionAPI,
|
|
@@ -268,6 +319,7 @@ function editGoal(
|
|
|
268
319
|
}
|
|
269
320
|
|
|
270
321
|
updateGoalUsage(activeGoal, ctx);
|
|
322
|
+
cancelContinuationPending();
|
|
271
323
|
activeGoal = normalizeGoalForBudget({
|
|
272
324
|
...activeGoal,
|
|
273
325
|
text: objective,
|
|
@@ -278,7 +330,7 @@ function editGoal(
|
|
|
278
330
|
persistGoal(activeGoal);
|
|
279
331
|
updateStatus(ctx, activeGoal);
|
|
280
332
|
ctx.ui.notify(`Goal updated: ${objective}`, "info");
|
|
281
|
-
if (activeGoal.status === "active") sendObjectiveUpdatedPrompt(pi, ctx, activeGoal);
|
|
333
|
+
if (activeGoal.status === "active") await sendObjectiveUpdatedPrompt(pi, ctx, activeGoal);
|
|
282
334
|
}
|
|
283
335
|
|
|
284
336
|
function showGoal(ctx: StatusContext) {
|
|
@@ -332,6 +384,21 @@ function incrementGoal(goal: ActiveGoal): ActiveGoal {
|
|
|
332
384
|
return { ...goal, iteration: goal.iteration + 1, updatedAt: Date.now() };
|
|
333
385
|
}
|
|
334
386
|
|
|
387
|
+
function pauseGoalAfterAgentEnd(
|
|
388
|
+
ctx: StatusContext,
|
|
389
|
+
goal: ActiveGoal,
|
|
390
|
+
assistant: AssistantMessageLike,
|
|
391
|
+
) {
|
|
392
|
+
cancelContinuationPending();
|
|
393
|
+
activeGoal = transitionGoal(goal, "paused");
|
|
394
|
+
persistGoal(activeGoal);
|
|
395
|
+
updateStatus(ctx, activeGoal);
|
|
396
|
+
|
|
397
|
+
const reason = assistant.stopReason === "aborted" ? "interruption" : "agent error";
|
|
398
|
+
const details = assistant.errorMessage ? ` (${truncateNotification(assistant.errorMessage)})` : "";
|
|
399
|
+
ctx.ui.notify(`Goal paused after ${reason}${details}. Run /goal resume to continue.`, "warning");
|
|
400
|
+
}
|
|
401
|
+
|
|
335
402
|
function updateGoalUsage(goal: ActiveGoal, ctx: StatusContext) {
|
|
336
403
|
goal.tokensUsed = Math.max(0, currentTokenTotal(ctx) - goal.baselineTokens);
|
|
337
404
|
goal.timeUsedSeconds = Math.max(0, Math.floor((Date.now() - goal.startedAt) / 1000));
|
|
@@ -415,21 +482,41 @@ function validateObjective(objective: string): string | undefined {
|
|
|
415
482
|
return undefined;
|
|
416
483
|
}
|
|
417
484
|
|
|
418
|
-
function sendGoalPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
419
|
-
sendPrompt(pi, ctx, buildGoalPrompt(goal));
|
|
485
|
+
async function sendGoalPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
486
|
+
return sendPrompt(pi, ctx, buildGoalPrompt(goal));
|
|
420
487
|
}
|
|
421
488
|
|
|
422
|
-
function sendObjectiveUpdatedPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
423
|
-
sendPrompt(pi, ctx, buildObjectiveUpdatedPrompt(goal));
|
|
489
|
+
async function sendObjectiveUpdatedPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
490
|
+
return sendPrompt(pi, ctx, buildObjectiveUpdatedPrompt(goal));
|
|
424
491
|
}
|
|
425
492
|
|
|
426
|
-
function
|
|
427
|
-
sendPrompt(pi, ctx,
|
|
493
|
+
async function sendResumePrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
494
|
+
return sendPrompt(pi, ctx, buildResumePrompt(goal));
|
|
428
495
|
}
|
|
429
496
|
|
|
430
|
-
function
|
|
431
|
-
if (
|
|
432
|
-
|
|
497
|
+
async function sendContinuationPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
|
|
498
|
+
if (continuationPending?.goalId === goal.id) return false;
|
|
499
|
+
if (hasPendingMessages(ctx)) return false;
|
|
500
|
+
|
|
501
|
+
const marker = continuationMarker(goal);
|
|
502
|
+
const prompt = buildContinuePrompt(goal, marker);
|
|
503
|
+
continuationPending = { goalId: goal.id, iteration: goal.iteration, marker, prompt };
|
|
504
|
+
const sent = await sendPrompt(pi, ctx, prompt);
|
|
505
|
+
if (!sent && continuationPending?.marker === marker) continuationPending = undefined;
|
|
506
|
+
return sent;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function sendPrompt(pi: ExtensionAPI, ctx: StatusContext, prompt: string) {
|
|
510
|
+
try {
|
|
511
|
+
const sent = ctx.isIdle?.()
|
|
512
|
+
? (pi.sendUserMessage(prompt) as void | Promise<void>)
|
|
513
|
+
: (pi.sendUserMessage(prompt, { deliverAs: "followUp" }) as void | Promise<void>);
|
|
514
|
+
await sent;
|
|
515
|
+
return true;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
ctx.ui.notify(`Goal prompt failed: ${formatError(error)}`, "error");
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
433
520
|
}
|
|
434
521
|
|
|
435
522
|
function updateStatus(ctx: StatusContext, goal: ActiveGoal) {
|
|
@@ -482,25 +569,116 @@ function formatTokenCount(value: number) {
|
|
|
482
569
|
|
|
483
570
|
function buildGoalPrompt(goal: ActiveGoal) {
|
|
484
571
|
const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatTokenCount(goal.tokenBudget)}.`;
|
|
485
|
-
return `Goal mode is active. Complete this goal fully:\n\n${goal
|
|
572
|
+
return `Goal mode is active. Complete this goal fully:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("this goal")}`;
|
|
486
573
|
}
|
|
487
574
|
|
|
488
575
|
function buildObjectiveUpdatedPrompt(goal: ActiveGoal) {
|
|
489
576
|
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
|
|
577
|
+
return `The active /goal objective was updated. Continue working toward this goal:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("the updated goal")}`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function buildResumePrompt(goal: ActiveGoal) {
|
|
581
|
+
const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatBudget(goal)} used.`;
|
|
582
|
+
return `The user explicitly resumed the paused /goal. Continue working toward this goal:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("this goal")}`;
|
|
491
583
|
}
|
|
492
584
|
|
|
493
585
|
function buildGoalSystemPrompt(goal: ActiveGoal) {
|
|
494
586
|
const budgetLine = goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
|
|
495
|
-
return `Active /goal
|
|
587
|
+
return `Active /goal:\n${goalObjectiveBlock(goal)}\n\nGoal-mode rules:\n- Keep going until the active goal is completely resolved end-to-end.\n- Treat the current worktree, command output, tests, and external state as authoritative.\n- Do not redefine the goal into a smaller task; audit every requirement before completion.\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}`;
|
|
496
588
|
}
|
|
497
589
|
|
|
498
|
-
function buildContinuePrompt(goal: ActiveGoal) {
|
|
499
|
-
return `Continue the active /goal until it is complete:\n\n${goal
|
|
590
|
+
function buildContinuePrompt(goal: ActiveGoal, marker: string) {
|
|
591
|
+
return `Continue the active /goal until it is complete:\n\n${goalObjectiveBlock(goal)}\n\nThis is automatic continuation #${goal.iteration}. Current files, command output, tests, and external state are authoritative; re-check them as needed. ${goalPersistenceRules("this goal")}\n\n${continuationMarkerComment(marker)}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function goalObjectiveBlock(goal: ActiveGoal) {
|
|
595
|
+
return `<goal_objective>\n${escapeXmlText(goal.text)}\n</goal_objective>`;
|
|
500
596
|
}
|
|
501
597
|
|
|
502
598
|
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.`;
|
|
599
|
+
return `Keep going until ${goalLabel} is completely resolved end-to-end. Do not redefine ${goalLabel} into a smaller task. 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. Treat the current worktree, command output, tests, and external state as authoritative. If a tool call fails, try reasonable alternatives instead of yielding early. Before calling goal_complete, audit ${goalLabel} requirement by requirement against the verified current state. Only call the goal_complete tool after ${goalLabel} is fully complete and verified.`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function hasPendingMessages(ctx: StatusContext) {
|
|
603
|
+
return ctx.hasPendingMessages?.() ?? false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function clearContinuationTracking() {
|
|
607
|
+
continuationPending = undefined;
|
|
608
|
+
cancelledContinuationMarkers.clear();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function cancelContinuationPending() {
|
|
612
|
+
if (continuationPending) rememberCancelledContinuationMarker(continuationPending.marker);
|
|
613
|
+
continuationPending = undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function rememberCancelledContinuationMarker(marker: string) {
|
|
617
|
+
cancelledContinuationMarkers.add(marker);
|
|
618
|
+
if (cancelledContinuationMarkers.size <= MAX_CANCELLED_CONTINUATION_PROMPTS) return;
|
|
619
|
+
const oldest = cancelledContinuationMarkers.values().next().value;
|
|
620
|
+
if (oldest) cancelledContinuationMarkers.delete(oldest);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function consumeCancelledContinuationPrompt(prompt: string) {
|
|
624
|
+
const marker = extractContinuationMarker(prompt);
|
|
625
|
+
return marker ? cancelledContinuationMarkers.delete(marker) : false;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function markContinuationDelivered(prompt: string) {
|
|
629
|
+
const marker = extractContinuationMarker(prompt);
|
|
630
|
+
if (marker && continuationPending?.marker === marker) continuationPending = undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function continuationMarker(goal: ActiveGoal) {
|
|
634
|
+
return `${goal.id}:${goal.iteration}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function continuationMarkerComment(marker: string) {
|
|
638
|
+
return `<!-- ${CONTINUATION_MARKER_PREFIX}${marker} -->`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function escapeRegExpText(value: string) {
|
|
642
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const CONTINUATION_MARKER_PATTERN = new RegExp(
|
|
646
|
+
`<!--\\s*${escapeRegExpText(CONTINUATION_MARKER_PREFIX)}([^\\s>]+)\\s*-->`,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
function extractContinuationMarker(prompt: string) {
|
|
650
|
+
return CONTINUATION_MARKER_PATTERN.exec(prompt)?.[1];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function findFinalAssistantMessage(messages: unknown[]): AssistantMessageLike | undefined {
|
|
654
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
655
|
+
const message = messages[i];
|
|
656
|
+
if (!message || typeof message !== "object") continue;
|
|
657
|
+
const candidate = message as Record<string, unknown>;
|
|
658
|
+
if (candidate.role !== "assistant") continue;
|
|
659
|
+
return {
|
|
660
|
+
role: "assistant",
|
|
661
|
+
stopReason: isAgentStopReason(candidate.stopReason) ? candidate.stopReason : undefined,
|
|
662
|
+
errorMessage: typeof candidate.errorMessage === "string" ? candidate.errorMessage : undefined,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function isAgentStopReason(value: unknown): value is AgentStopReason {
|
|
669
|
+
return ["stop", "length", "toolUse", "error", "aborted"].includes(String(value));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function escapeXmlText(value: string) {
|
|
673
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function formatError(error: unknown) {
|
|
677
|
+
return truncateNotification(error instanceof Error ? error.message : String(error));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function truncateNotification(value: string) {
|
|
681
|
+
return value.length > 160 ? `${value.slice(0, 157)}...` : value;
|
|
504
682
|
}
|
|
505
683
|
|
|
506
684
|
function currentTokenTotal(ctx: StatusContext): number {
|
|
@@ -543,6 +721,7 @@ function loadGoalFromSession(ctx: StatusContext): ActiveGoal | undefined {
|
|
|
543
721
|
}
|
|
544
722
|
|
|
545
723
|
function clearActiveGoal(ctx: StatusContext) {
|
|
724
|
+
cancelContinuationPending();
|
|
546
725
|
activeGoal = undefined;
|
|
547
726
|
clearPersistedGoal(ctx.cwd);
|
|
548
727
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|