@phi-code-admin/phi-code 0.82.3 → 0.84.0

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 (49) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/docs/usage.md +1 -1
  3. package/extensions/phi/btw/LICENSE +21 -0
  4. package/extensions/phi/btw/btw-ui.ts +238 -0
  5. package/extensions/phi/btw/btw.ts +363 -0
  6. package/extensions/phi/btw/index.ts +17 -0
  7. package/extensions/phi/btw/prompts/btw-system.txt +9 -0
  8. package/extensions/phi/chrome/LICENSE +21 -0
  9. package/extensions/phi/chrome/browser-extension/manifest.json +26 -0
  10. package/extensions/phi/chrome/browser-extension/service_worker.js +2388 -0
  11. package/extensions/phi/chrome/browser-extension/snapshot_injected.js +677 -0
  12. package/extensions/phi/chrome/index.ts +1799 -0
  13. package/extensions/phi/goal/LICENSE +21 -0
  14. package/extensions/phi/goal/index.ts +784 -0
  15. package/extensions/phi/mcp/LICENSE +21 -0
  16. package/extensions/phi/mcp/callback-server.ts +263 -0
  17. package/extensions/phi/mcp/config.ts +195 -0
  18. package/extensions/phi/mcp/errors.ts +35 -0
  19. package/extensions/phi/mcp/index.ts +376 -0
  20. package/extensions/phi/mcp/oauth-provider.ts +367 -0
  21. package/extensions/phi/mcp/server-manager.ts +464 -0
  22. package/extensions/phi/mcp/tool-bridge.ts +494 -0
  23. package/extensions/phi/todo/LICENSE +21 -0
  24. package/extensions/phi/todo/config.ts +14 -0
  25. package/extensions/phi/todo/index.ts +113 -0
  26. package/extensions/phi/todo/locales/de.json +17 -0
  27. package/extensions/phi/todo/locales/en.json +15 -0
  28. package/extensions/phi/todo/locales/es.json +17 -0
  29. package/extensions/phi/todo/locales/fr.json +17 -0
  30. package/extensions/phi/todo/locales/pt-BR.json +17 -0
  31. package/extensions/phi/todo/locales/pt.json +17 -0
  32. package/extensions/phi/todo/locales/ru.json +17 -0
  33. package/extensions/phi/todo/locales/uk.json +17 -0
  34. package/extensions/phi/todo/rpiv-config/config.ts +223 -0
  35. package/extensions/phi/todo/rpiv-config/index.ts +12 -0
  36. package/extensions/phi/todo/state/i18n-bridge.ts +65 -0
  37. package/extensions/phi/todo/state/invariants.ts +20 -0
  38. package/extensions/phi/todo/state/replay.ts +38 -0
  39. package/extensions/phi/todo/state/selectors.ts +107 -0
  40. package/extensions/phi/todo/state/state-reducer.ts +187 -0
  41. package/extensions/phi/todo/state/state.ts +18 -0
  42. package/extensions/phi/todo/state/store.ts +54 -0
  43. package/extensions/phi/todo/state/task-graph.ts +57 -0
  44. package/extensions/phi/todo/todo-overlay.ts +194 -0
  45. package/extensions/phi/todo/todo.ts +146 -0
  46. package/extensions/phi/todo/tool/response-envelope.ts +94 -0
  47. package/extensions/phi/todo/tool/types.ts +128 -0
  48. package/extensions/phi/todo/view/format.ts +162 -0
  49. package/package.json +4 -2
@@ -0,0 +1,784 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import process from "node:process";
4
+ import { randomUUID } from "node:crypto";
5
+ import { defineTool, type ExtensionAPI, getAgentDir } from "phi-code";
6
+ import { Type } from "typebox";
7
+
8
+ type GoalStatus = "active" | "paused" | "budget_limited" | "complete";
9
+ type AgentStopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
10
+
11
+ interface ActiveGoal {
12
+ id: string;
13
+ text: string;
14
+ status: GoalStatus;
15
+ startedAt: number;
16
+ updatedAt: number;
17
+ iteration: number;
18
+ tokenBudget?: number;
19
+ tokensUsed: number;
20
+ timeUsedSeconds: number;
21
+ baselineTokens: number;
22
+ }
23
+
24
+ interface GoalCompleteDetails {
25
+ goal: string;
26
+ summary: string;
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
+
42
+ interface GoalStateEntryData {
43
+ goal?: ActiveGoal | null;
44
+ }
45
+
46
+ interface CommandResult {
47
+ kind: "show" | "start" | "pause" | "resume" | "clear" | "edit";
48
+ objective?: string;
49
+ tokenBudget?: number;
50
+ }
51
+
52
+ interface StatusContext {
53
+ cwd: string;
54
+ ui: {
55
+ confirm: (title: string, message: string) => Promise<boolean>;
56
+ notify: (message: string, level?: "info" | "warning" | "error") => void;
57
+ setStatus: (key: string, value: string | undefined) => void;
58
+ };
59
+ isIdle?: () => boolean;
60
+ hasPendingMessages?: () => boolean;
61
+ sessionManager?: unknown;
62
+ }
63
+
64
+ const STATUS_KEY = "goal";
65
+ const GOAL_STATE_ENTRY_TYPE = "goal-state";
66
+ const MAX_OBJECTIVE_LENGTH = 4_000;
67
+ const MAX_CANCELLED_CONTINUATION_PROMPTS = 20;
68
+ const CONTINUATION_MARKER_PREFIX = "pi-goal-continuation:";
69
+ const STATE_FILE = join(getAgentDir(), "pi-goal-state.json");
70
+
71
+ let activeGoal: ActiveGoal | undefined;
72
+ let completionStatusTimer: NodeJS.Timeout | undefined;
73
+ let extensionApi: ExtensionAPI | undefined;
74
+ let continuationPending: ContinuationPending | undefined;
75
+ const cancelledContinuationMarkers = new Set<string>();
76
+
77
+ const goalCompleteTool = defineTool({
78
+ name: "goal_complete",
79
+ label: "Goal Complete",
80
+ description:
81
+ "Mark the active /goal as complete. Only call this after the requested goal is fully done and verified.",
82
+ promptSnippet: "Mark the active /goal as complete after fully finishing and verifying it",
83
+ promptGuidelines: [
84
+ "When a /goal is active, keep working until the goal is complete; do not stop with only a plan or partial progress.",
85
+ "Before calling goal_complete, audit the active goal requirement by requirement against the current files, command output, tests, or external state.",
86
+ "Call goal_complete only after the requested goal is fully implemented, verified, and no known required work remains.",
87
+ ],
88
+ parameters: Type.Object({
89
+ summary: Type.String({
90
+ description: "Concise summary of what was completed and how it was verified.",
91
+ }),
92
+ }),
93
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
94
+ const completedGoal = activeGoal;
95
+ if (completedGoal) {
96
+ activeGoal = transitionGoal(completedGoal, "complete");
97
+ updateGoalUsage(activeGoal, ctx);
98
+ persistGoal(activeGoal);
99
+ }
100
+
101
+ const goal = completedGoal?.text ?? "unknown goal";
102
+ const summary = params.summary.trim();
103
+
104
+ ctx.ui.setStatus(STATUS_KEY, completedGoal ? formatStatus(activeGoal) : undefined);
105
+ clearActiveGoal(ctx);
106
+ showCompletionStatus(ctx);
107
+ ctx.ui.notify(`Goal complete: ${goal}`, "info");
108
+
109
+ return {
110
+ content: [{ type: "text", text: `Goal complete: ${summary}` }],
111
+ details: { goal, summary } satisfies GoalCompleteDetails,
112
+ terminate: true,
113
+ };
114
+ },
115
+ });
116
+
117
+ export default function goal(pi: ExtensionAPI) {
118
+ extensionApi = pi;
119
+ pi.registerTool(goalCompleteTool);
120
+
121
+ pi.registerCommand("goal", {
122
+ description: "Run a goal to completion: /goal [--tokens 100k] <goal_to_complete>",
123
+ handler: async (args, ctx) => {
124
+ const result = parseCommand(args);
125
+ if (typeof result === "string") {
126
+ ctx.ui.notify(result, "warning");
127
+ return;
128
+ }
129
+
130
+ switch (result.kind) {
131
+ case "show":
132
+ showGoal(ctx);
133
+ return;
134
+ case "pause":
135
+ pauseGoal(ctx);
136
+ return;
137
+ case "resume":
138
+ await resumeGoal(pi, ctx);
139
+ return;
140
+ case "clear":
141
+ clearGoal(ctx);
142
+ return;
143
+ case "edit":
144
+ await editGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
145
+ return;
146
+ case "start":
147
+ await startGoal(result.objective ?? "", result.tokenBudget, pi, ctx);
148
+ return;
149
+ }
150
+ },
151
+ });
152
+
153
+ pi.on("session_start", (_event, ctx) => {
154
+ clearCompletionStatusTimer();
155
+ clearContinuationTracking();
156
+ activeGoal = loadGoalFromSession(ctx);
157
+ if (activeGoal) updateStatus(ctx, activeGoal);
158
+ else ctx.ui.setStatus(STATUS_KEY, undefined);
159
+ });
160
+
161
+ pi.on("session_shutdown", (_event, ctx) => {
162
+ if (activeGoal) persistGoal(activeGoal);
163
+ clearContinuationTracking();
164
+ ctx.ui.setStatus(STATUS_KEY, undefined);
165
+ clearCompletionStatusTimer();
166
+ });
167
+
168
+ pi.on("input", (event) => {
169
+ if (event.source !== "extension") return;
170
+ if (consumeCancelledContinuationPrompt(event.text)) return { action: "handled" as const };
171
+ });
172
+
173
+ pi.on("before_agent_start", (event) => {
174
+ markContinuationDelivered(event.prompt);
175
+ if (!activeGoal || activeGoal.status !== "active") return;
176
+
177
+ return {
178
+ systemPrompt: `${event.systemPrompt}\n\n${buildGoalSystemPrompt(activeGoal)}`,
179
+ };
180
+ });
181
+
182
+ pi.on("agent_end", async (event, ctx) => {
183
+ if (!activeGoal || activeGoal.status !== "active") return;
184
+
185
+ const goalId = activeGoal.id;
186
+ const hadPendingContinuation = continuationPending?.goalId === goalId;
187
+ const finalAssistant = findFinalAssistantMessage(event.messages);
188
+
189
+ if (!hadPendingContinuation) activeGoal = incrementGoal(activeGoal);
190
+ updateGoalUsage(activeGoal, ctx);
191
+
192
+ if (finalAssistant?.stopReason === "aborted" || finalAssistant?.stopReason === "error") {
193
+ pauseGoalAfterAgentEnd(ctx, activeGoal, finalAssistant);
194
+ return;
195
+ }
196
+
197
+ if (activeGoal.tokenBudget !== undefined && activeGoal.tokensUsed >= activeGoal.tokenBudget) {
198
+ cancelContinuationPending();
199
+ activeGoal = transitionGoal(activeGoal, "budget_limited");
200
+ persistGoal(activeGoal);
201
+ updateStatus(ctx, activeGoal);
202
+ ctx.ui.notify(`Goal token budget reached: ${formatBudget(activeGoal)}`, "warning");
203
+ return;
204
+ }
205
+
206
+ persistGoal(activeGoal);
207
+ updateStatus(ctx, activeGoal);
208
+
209
+ if (hadPendingContinuation) {
210
+ if (hasPendingMessages(ctx)) return;
211
+ if (continuationPending?.goalId === goalId) continuationPending = undefined;
212
+ }
213
+
214
+ const currentGoal = activeGoal;
215
+ if (!currentGoal || currentGoal.id !== goalId || currentGoal.status !== "active") return;
216
+ if (hasPendingMessages(ctx)) return;
217
+ await sendContinuationPrompt(pi, ctx, currentGoal);
218
+ });
219
+ }
220
+
221
+ async function startGoal(
222
+ objective: string,
223
+ tokenBudget: number | undefined,
224
+ pi: ExtensionAPI,
225
+ ctx: StatusContext,
226
+ ) {
227
+ const validationError = validateObjective(objective);
228
+ if (validationError) {
229
+ ctx.ui.notify(validationError, "warning");
230
+ return;
231
+ }
232
+
233
+ const existingGoal = activeGoal?.status !== "complete" ? activeGoal : undefined;
234
+ if (existingGoal) {
235
+ const shouldReplace = await ctx.ui.confirm(
236
+ "Replace goal?",
237
+ `Current goal: ${existingGoal.text}\n\nNew goal: ${objective}`,
238
+ );
239
+ if (!shouldReplace) {
240
+ ctx.ui.notify(`Goal kept: ${existingGoal.text}`, "info");
241
+ return;
242
+ }
243
+ }
244
+
245
+ cancelContinuationPending();
246
+ activeGoal = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
247
+ persistGoal(activeGoal);
248
+ updateStatus(ctx, activeGoal);
249
+ ctx.ui.notify(existingGoal ? `Goal replaced: ${objective}` : `Goal started: ${objective}`, "info");
250
+ await sendGoalPrompt(pi, ctx, activeGoal);
251
+ }
252
+
253
+ function pauseGoal(ctx: StatusContext) {
254
+ if (!activeGoal) {
255
+ ctx.ui.notify("No active goal.", "info");
256
+ return;
257
+ }
258
+ if (activeGoal.status !== "active") {
259
+ ctx.ui.notify(`Goal is ${activeGoal.status}; only active goals can be paused.`, "warning");
260
+ return;
261
+ }
262
+ cancelContinuationPending();
263
+ activeGoal = transitionGoal(activeGoal, "paused");
264
+ persistGoal(activeGoal);
265
+ updateStatus(ctx, activeGoal);
266
+ ctx.ui.notify(`Goal paused: ${activeGoal.text}`, "info");
267
+ }
268
+
269
+ async function resumeGoal(pi: ExtensionAPI, ctx: StatusContext) {
270
+ if (!activeGoal) {
271
+ ctx.ui.notify("No active goal.", "info");
272
+ return;
273
+ }
274
+ if (activeGoal.status !== "paused" && activeGoal.status !== "budget_limited") {
275
+ ctx.ui.notify(`Goal is ${activeGoal.status}; only paused or budget-limited goals can be resumed.`, "warning");
276
+ return;
277
+ }
278
+ activeGoal = transitionGoal(activeGoal, "active");
279
+ persistGoal(activeGoal);
280
+ updateStatus(ctx, activeGoal);
281
+ if (activeGoal.status !== "active") {
282
+ ctx.ui.notify(`Goal token budget is still reached: ${formatBudget(activeGoal)}`, "warning");
283
+ return;
284
+ }
285
+ ctx.ui.notify(`Goal resumed: ${activeGoal.text}`, "info");
286
+ await sendResumePrompt(pi, ctx, activeGoal);
287
+ }
288
+
289
+ function clearGoal(ctx: StatusContext) {
290
+ if (!activeGoal) {
291
+ ctx.ui.notify("No active goal.", "info");
292
+ cancelContinuationPending();
293
+ clearPersistedGoal(ctx.cwd);
294
+ ctx.ui.setStatus(STATUS_KEY, undefined);
295
+ return;
296
+ }
297
+
298
+ const stoppedGoal = activeGoal.text;
299
+ clearActiveGoal(ctx);
300
+ ctx.ui.notify(`Goal cleared: ${stoppedGoal}`, "warning");
301
+ }
302
+
303
+ async function editGoal(
304
+ objective: string,
305
+ tokenBudget: number | undefined,
306
+ pi: ExtensionAPI,
307
+ ctx: StatusContext,
308
+ ) {
309
+ const validationError = validateObjective(objective);
310
+ if (validationError) {
311
+ ctx.ui.notify(validationError, "warning");
312
+ return;
313
+ }
314
+ if (!activeGoal) {
315
+ ctx.ui.notify("No active goal. Use /goal <objective> to start one.", "warning");
316
+ return;
317
+ }
318
+
319
+ updateGoalUsage(activeGoal, ctx);
320
+ cancelContinuationPending();
321
+ activeGoal = normalizeGoalForBudget({
322
+ ...activeGoal,
323
+ text: objective,
324
+ status: editedGoalStatus(activeGoal.status),
325
+ tokenBudget: tokenBudget ?? activeGoal.tokenBudget,
326
+ updatedAt: Date.now(),
327
+ });
328
+ persistGoal(activeGoal);
329
+ updateStatus(ctx, activeGoal);
330
+ ctx.ui.notify(`Goal updated: ${objective}`, "info");
331
+ if (activeGoal.status === "active") await sendObjectiveUpdatedPrompt(pi, ctx, activeGoal);
332
+ }
333
+
334
+ function showGoal(ctx: StatusContext) {
335
+ if (!activeGoal) {
336
+ ctx.ui.notify("Usage: /goal <objective>\nNo goal is currently set.", "info");
337
+ ctx.ui.setStatus(STATUS_KEY, undefined);
338
+ return;
339
+ }
340
+ updateGoalUsage(activeGoal, ctx);
341
+ persistGoal(activeGoal);
342
+ updateStatus(ctx, activeGoal);
343
+ ctx.ui.notify(goalSummary(activeGoal), "info");
344
+ }
345
+
346
+ function createGoal(text: string, tokenBudget: number | undefined, baselineTokens: number): ActiveGoal {
347
+ const now = Date.now();
348
+ return {
349
+ id: randomUUID(),
350
+ text,
351
+ status: "active",
352
+ startedAt: now,
353
+ updatedAt: now,
354
+ iteration: 0,
355
+ tokenBudget,
356
+ tokensUsed: 0,
357
+ timeUsedSeconds: 0,
358
+ baselineTokens,
359
+ };
360
+ }
361
+
362
+ function transitionGoal(goal: ActiveGoal, status: GoalStatus): ActiveGoal {
363
+ return normalizeGoalForBudget({ ...goal, status, updatedAt: Date.now() });
364
+ }
365
+
366
+ function editedGoalStatus(status: GoalStatus): GoalStatus {
367
+ return status === "paused" ? "paused" : "active";
368
+ }
369
+
370
+ function normalizeGoalForBudget(goal: ActiveGoal): ActiveGoal {
371
+ if (
372
+ goal.status === "active" &&
373
+ goal.tokenBudget !== undefined &&
374
+ goal.tokensUsed >= goal.tokenBudget
375
+ ) {
376
+ return { ...goal, status: "budget_limited" };
377
+ }
378
+ return goal;
379
+ }
380
+
381
+ function incrementGoal(goal: ActiveGoal): ActiveGoal {
382
+ return { ...goal, iteration: goal.iteration + 1, updatedAt: Date.now() };
383
+ }
384
+
385
+ function pauseGoalAfterAgentEnd(
386
+ ctx: StatusContext,
387
+ goal: ActiveGoal,
388
+ assistant: AssistantMessageLike,
389
+ ) {
390
+ cancelContinuationPending();
391
+ activeGoal = transitionGoal(goal, "paused");
392
+ persistGoal(activeGoal);
393
+ updateStatus(ctx, activeGoal);
394
+
395
+ const reason = assistant.stopReason === "aborted" ? "interruption" : "agent error";
396
+ const details = assistant.errorMessage ? ` (${truncateNotification(assistant.errorMessage)})` : "";
397
+ ctx.ui.notify(`Goal paused after ${reason}${details}. Run /goal resume to continue.`, "warning");
398
+ }
399
+
400
+ function updateGoalUsage(goal: ActiveGoal, ctx: StatusContext) {
401
+ goal.tokensUsed = Math.max(0, currentTokenTotal(ctx) - goal.baselineTokens);
402
+ goal.timeUsedSeconds = Math.max(0, Math.floor((Date.now() - goal.startedAt) / 1000));
403
+ goal.updatedAt = Date.now();
404
+ }
405
+
406
+ export function parseCommand(args: string): CommandResult | string {
407
+ const tokens = tokenize(args.trim());
408
+ if (tokens.length === 0) return { kind: "show" };
409
+
410
+ const [first, ...rest] = tokens;
411
+ if (first === "pause") return rest.length === 0 ? { kind: "pause" } : "Usage: /goal pause";
412
+ if (first === "resume") return rest.length === 0 ? { kind: "resume" } : "Usage: /goal resume";
413
+ if (first === "clear" || first === "stop") return rest.length === 0 ? { kind: "clear" } : "Usage: /goal clear";
414
+ if (first === "status") return rest.length === 0 ? { kind: "show" } : "Usage: /goal status";
415
+ if (first === "edit") return parseObjective("edit", rest);
416
+ return parseObjective("start", tokens);
417
+ }
418
+
419
+ function parseObjective(kind: "start" | "edit", tokens: string[]): CommandResult | string {
420
+ let tokenBudget: number | undefined;
421
+ const objectiveTokens = [...tokens];
422
+
423
+ if (objectiveTokens[0] === "--tokens") {
424
+ const rawBudget = objectiveTokens[1];
425
+ if (!rawBudget) return "Usage: /goal --tokens 100k <goal_to_complete>";
426
+ const parsedBudget = parseTokenBudget(rawBudget);
427
+ if (parsedBudget === undefined) return `Invalid token budget: ${rawBudget}`;
428
+ tokenBudget = parsedBudget;
429
+ objectiveTokens.splice(0, 2);
430
+ }
431
+
432
+ if (objectiveTokens.length === 0) {
433
+ return kind === "edit" ? "Usage: /goal edit <goal_to_complete>" : "Usage: /goal <goal_to_complete>";
434
+ }
435
+
436
+ return { kind, objective: objectiveTokens.join(" "), tokenBudget };
437
+ }
438
+
439
+ function tokenize(input: string): string[] {
440
+ const tokens: string[] = [];
441
+ let current = "";
442
+ let quote: '"' | "'" | undefined;
443
+
444
+ for (const char of input) {
445
+ if (quote) {
446
+ if (char === quote) quote = undefined;
447
+ else current += char;
448
+ continue;
449
+ }
450
+ if (char === '"' || char === "'") {
451
+ quote = char;
452
+ continue;
453
+ }
454
+ if (/\s/.test(char)) {
455
+ if (current) tokens.push(current);
456
+ current = "";
457
+ continue;
458
+ }
459
+ current += char;
460
+ }
461
+ if (current) tokens.push(current);
462
+ return tokens;
463
+ }
464
+
465
+ export function parseTokenBudget(value: string): number | undefined {
466
+ const match = /^(\d+(?:\.\d+)?)([km])?$/iu.exec(value.trim());
467
+ if (!match) return undefined;
468
+ const amount = Number(match[1]);
469
+ if (!Number.isFinite(amount) || amount <= 0) return undefined;
470
+ const multiplier = match[2]?.toLowerCase() === "m" ? 1_000_000 : match[2]?.toLowerCase() === "k" ? 1_000 : 1;
471
+ return Math.floor(amount * multiplier);
472
+ }
473
+
474
+ export function validateObjective(objective: string): string | undefined {
475
+ const trimmed = objective.trim();
476
+ if (!trimmed) return "Usage: /goal <goal_to_complete>";
477
+ if (trimmed.length > MAX_OBJECTIVE_LENGTH) {
478
+ return `Goal objective is too long (${trimmed.length}/${MAX_OBJECTIVE_LENGTH} characters). Put long instructions in a file and reference it from /goal instead.`;
479
+ }
480
+ return undefined;
481
+ }
482
+
483
+ async function sendGoalPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
484
+ return sendPrompt(pi, ctx, buildGoalPrompt(goal));
485
+ }
486
+
487
+ async function sendObjectiveUpdatedPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
488
+ return sendPrompt(pi, ctx, buildObjectiveUpdatedPrompt(goal));
489
+ }
490
+
491
+ async function sendResumePrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
492
+ return sendPrompt(pi, ctx, buildResumePrompt(goal));
493
+ }
494
+
495
+ async function sendContinuationPrompt(pi: ExtensionAPI, ctx: StatusContext, goal: ActiveGoal) {
496
+ if (continuationPending?.goalId === goal.id) return false;
497
+ if (hasPendingMessages(ctx)) return false;
498
+
499
+ const marker = continuationMarker(goal);
500
+ const prompt = buildContinuePrompt(goal, marker);
501
+ continuationPending = { goalId: goal.id, iteration: goal.iteration, marker, prompt };
502
+ const sent = await sendPrompt(pi, ctx, prompt);
503
+ if (!sent && continuationPending?.marker === marker) continuationPending = undefined;
504
+ return sent;
505
+ }
506
+
507
+ async function sendPrompt(pi: ExtensionAPI, ctx: StatusContext, prompt: string) {
508
+ try {
509
+ const sent = ctx.isIdle?.()
510
+ ? (pi.sendUserMessage(prompt) as void | Promise<void>)
511
+ : (pi.sendUserMessage(prompt, { deliverAs: "followUp" }) as void | Promise<void>);
512
+ await sent;
513
+ return true;
514
+ } catch (error) {
515
+ ctx.ui.notify(`Goal prompt failed: ${formatError(error)}`, "error");
516
+ return false;
517
+ }
518
+ }
519
+
520
+ function updateStatus(ctx: StatusContext, goal: ActiveGoal) {
521
+ clearCompletionStatusTimer();
522
+ ctx.ui.setStatus(STATUS_KEY, formatStatus(goal));
523
+ }
524
+
525
+ export function formatStatus(goal: ActiveGoal | undefined) {
526
+ if (!goal) return undefined;
527
+ if (goal.status === "complete") return "🎯 complete";
528
+ if (goal.status === "paused") return "🎯 paused";
529
+ if (goal.status === "budget_limited") return `🎯 budget ${formatBudget(goal)}`;
530
+ if (goal.tokenBudget !== undefined) return `🎯 active ${formatBudget(goal)}`;
531
+ return `🎯 active ${formatDuration(goal.timeUsedSeconds)}`;
532
+ }
533
+
534
+ function formatBudget(goal: ActiveGoal) {
535
+ return `${formatTokenCount(goal.tokensUsed)}/${formatTokenCount(goal.tokenBudget ?? 0)}`;
536
+ }
537
+
538
+ function goalSummary(goal: ActiveGoal) {
539
+ return [
540
+ `Goal: ${goal.text}`,
541
+ `Status: ${goal.status}`,
542
+ `Iteration: ${goal.iteration}`,
543
+ `Elapsed: ${formatDuration(goal.timeUsedSeconds)}`,
544
+ `Tokens: ${goal.tokenBudget === undefined ? formatTokenCount(goal.tokensUsed) : formatBudget(goal)}`,
545
+ `Commands: ${goalCommandHint(goal.status)}`,
546
+ ].join("\n");
547
+ }
548
+
549
+ function goalCommandHint(status: GoalStatus) {
550
+ if (status === "active") return "/goal edit <objective>, /goal pause, /goal clear";
551
+ if (status === "paused") return "/goal edit <objective>, /goal resume, /goal clear";
552
+ return "/goal edit <objective>, /goal clear";
553
+ }
554
+
555
+ export function formatDuration(seconds: number) {
556
+ if (seconds < 60) return `${seconds}s`;
557
+ const minutes = Math.floor(seconds / 60);
558
+ if (minutes < 60) return `${minutes}m`;
559
+ const hours = Math.floor(minutes / 60);
560
+ return `${hours}h${minutes % 60}m`;
561
+ }
562
+
563
+ export function formatTokenCount(value: number) {
564
+ if (value < 1_000) return `${value}`;
565
+ if (value < 1_000_000) return `${Number.isInteger(value / 1_000) ? value / 1_000 : (value / 1_000).toFixed(1)}k`;
566
+ return `${Number.isInteger(value / 1_000_000) ? value / 1_000_000 : (value / 1_000_000).toFixed(1)}m`;
567
+ }
568
+
569
+ function buildGoalPrompt(goal: ActiveGoal) {
570
+ const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatTokenCount(goal.tokenBudget)}.`;
571
+ return `Goal mode is active. Complete this goal fully:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("this goal")}`;
572
+ }
573
+
574
+ function buildObjectiveUpdatedPrompt(goal: ActiveGoal) {
575
+ const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatBudget(goal)} used.`;
576
+ return `The active /goal objective was updated. Continue working toward this goal:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("the updated goal")}`;
577
+ }
578
+
579
+ function buildResumePrompt(goal: ActiveGoal) {
580
+ const budgetLine = goal.tokenBudget === undefined ? "" : `\nToken budget: ${formatBudget(goal)} used.`;
581
+ return `The user explicitly resumed the paused /goal. Continue working toward this goal:\n\n${goalObjectiveBlock(goal)}${budgetLine}\n\n${goalPersistenceRules("this goal")}`;
582
+ }
583
+
584
+ export function buildGoalSystemPrompt(goal: ActiveGoal) {
585
+ const budgetLine = goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
586
+ 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}`;
587
+ }
588
+
589
+ function buildContinuePrompt(goal: ActiveGoal, marker: string) {
590
+ 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)}`;
591
+ }
592
+
593
+ function goalObjectiveBlock(goal: ActiveGoal) {
594
+ return `<goal_objective>\n${escapeXmlText(goal.text)}\n</goal_objective>`;
595
+ }
596
+
597
+ function goalPersistenceRules(goalLabel: string) {
598
+ 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.`;
599
+ }
600
+
601
+ function hasPendingMessages(ctx: StatusContext) {
602
+ return ctx.hasPendingMessages?.() ?? false;
603
+ }
604
+
605
+ function clearContinuationTracking() {
606
+ continuationPending = undefined;
607
+ cancelledContinuationMarkers.clear();
608
+ }
609
+
610
+ function cancelContinuationPending() {
611
+ if (continuationPending) rememberCancelledContinuationMarker(continuationPending.marker);
612
+ continuationPending = undefined;
613
+ }
614
+
615
+ function rememberCancelledContinuationMarker(marker: string) {
616
+ cancelledContinuationMarkers.add(marker);
617
+ if (cancelledContinuationMarkers.size <= MAX_CANCELLED_CONTINUATION_PROMPTS) return;
618
+ const oldest = cancelledContinuationMarkers.values().next().value;
619
+ if (oldest) cancelledContinuationMarkers.delete(oldest);
620
+ }
621
+
622
+ function consumeCancelledContinuationPrompt(prompt: string) {
623
+ const marker = extractContinuationMarker(prompt);
624
+ return marker ? cancelledContinuationMarkers.delete(marker) : false;
625
+ }
626
+
627
+ function markContinuationDelivered(prompt: string) {
628
+ const marker = extractContinuationMarker(prompt);
629
+ if (marker && continuationPending?.marker === marker) continuationPending = undefined;
630
+ }
631
+
632
+ function continuationMarker(goal: ActiveGoal) {
633
+ return `${goal.id}:${goal.iteration}`;
634
+ }
635
+
636
+ function continuationMarkerComment(marker: string) {
637
+ return `<!-- ${CONTINUATION_MARKER_PREFIX}${marker} -->`;
638
+ }
639
+
640
+ function escapeRegExpText(value: string) {
641
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
642
+ }
643
+
644
+ const CONTINUATION_MARKER_PATTERN = new RegExp(
645
+ `<!--\\s*${escapeRegExpText(CONTINUATION_MARKER_PREFIX)}([^\\s>]+)\\s*-->`,
646
+ );
647
+
648
+ function extractContinuationMarker(prompt: string) {
649
+ return CONTINUATION_MARKER_PATTERN.exec(prompt)?.[1];
650
+ }
651
+
652
+ export function findFinalAssistantMessage(messages: unknown[]): AssistantMessageLike | undefined {
653
+ for (let i = messages.length - 1; i >= 0; i--) {
654
+ const message = messages[i];
655
+ if (!message || typeof message !== "object") continue;
656
+ const candidate = message as Record<string, unknown>;
657
+ if (candidate.role !== "assistant") continue;
658
+ return {
659
+ role: "assistant",
660
+ stopReason: isAgentStopReason(candidate.stopReason) ? candidate.stopReason : undefined,
661
+ errorMessage: typeof candidate.errorMessage === "string" ? candidate.errorMessage : undefined,
662
+ };
663
+ }
664
+ return undefined;
665
+ }
666
+
667
+ function isAgentStopReason(value: unknown): value is AgentStopReason {
668
+ return ["stop", "length", "toolUse", "error", "aborted"].includes(String(value));
669
+ }
670
+
671
+ function escapeXmlText(value: string) {
672
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
673
+ }
674
+
675
+ function formatError(error: unknown) {
676
+ return truncateNotification(error instanceof Error ? error.message : String(error));
677
+ }
678
+
679
+ function truncateNotification(value: string) {
680
+ return value.length > 160 ? `${value.slice(0, 157)}...` : value;
681
+ }
682
+
683
+ function currentTokenTotal(ctx: StatusContext): number {
684
+ const sessionManager = ctx.sessionManager as
685
+ | { getBranch?: () => Array<{ type?: string; message?: { role?: string; usage?: unknown } }> }
686
+ | undefined;
687
+ const branch = sessionManager?.getBranch?.() ?? [];
688
+ let total = 0;
689
+ for (const entry of branch) {
690
+ if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
691
+ const usage = entry.message.usage as { input?: number; output?: number } | undefined;
692
+ total += usage?.input ?? 0;
693
+ total += usage?.output ?? 0;
694
+ }
695
+ return total;
696
+ }
697
+
698
+ function persistGoal(goal: ActiveGoal) {
699
+ extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal });
700
+ }
701
+
702
+ function clearPersistedGoal(cwd: string) {
703
+ extensionApi?.appendEntry<GoalStateEntryData>(GOAL_STATE_ENTRY_TYPE, { goal: null });
704
+ clearLegacyPersistedGoal(cwd);
705
+ }
706
+
707
+ function loadGoalFromSession(ctx: StatusContext): ActiveGoal | undefined {
708
+ const sessionManager = ctx.sessionManager as
709
+ | {
710
+ getBranch?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
711
+ getEntries?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
712
+ }
713
+ | undefined;
714
+ const entries = sessionManager?.getBranch?.() ?? sessionManager?.getEntries?.() ?? [];
715
+ const entry = entries
716
+ .filter((entry) => entry.type === "custom" && entry.customType === GOAL_STATE_ENTRY_TYPE)
717
+ .pop();
718
+ const data = entry?.data as GoalStateEntryData | undefined;
719
+ return isGoal(data?.goal) && data.goal.status !== "complete" ? data.goal : undefined;
720
+ }
721
+
722
+ function clearActiveGoal(ctx: StatusContext) {
723
+ cancelContinuationPending();
724
+ activeGoal = undefined;
725
+ clearPersistedGoal(ctx.cwd);
726
+ ctx.ui.setStatus(STATUS_KEY, undefined);
727
+ }
728
+
729
+ function showCompletionStatus(ctx: StatusContext) {
730
+ clearCompletionStatusTimer();
731
+ ctx.ui.setStatus(STATUS_KEY, "🎯 complete");
732
+ completionStatusTimer = setTimeout(() => {
733
+ completionStatusTimer = undefined;
734
+ try {
735
+ ctx.ui.setStatus(STATUS_KEY, undefined);
736
+ } catch {
737
+ // The completion status is best-effort; the captured ctx may be stale after
738
+ // session replacement or reload before this timer fires.
739
+ }
740
+ }, 8_000);
741
+ }
742
+
743
+ function clearCompletionStatusTimer() {
744
+ if (!completionStatusTimer) return;
745
+ clearTimeout(completionStatusTimer);
746
+ completionStatusTimer = undefined;
747
+ }
748
+
749
+ function readState(): Record<string, unknown> {
750
+ if (!existsSync(STATE_FILE)) return {};
751
+ try {
752
+ const parsed = JSON.parse(readFileSync(STATE_FILE, "utf8")) as unknown;
753
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
754
+ ? (parsed as Record<string, unknown>)
755
+ : {};
756
+ } catch {
757
+ return {};
758
+ }
759
+ }
760
+
761
+ function clearLegacyPersistedGoal(cwd: string) {
762
+ if (!existsSync(STATE_FILE)) return;
763
+ const goals = readState();
764
+ delete goals[cwd];
765
+ mkdirSync(dirname(STATE_FILE), { recursive: true });
766
+ writeFileSync(STATE_FILE, `${JSON.stringify(goals, null, 2)}\n`);
767
+ }
768
+
769
+
770
+ function isGoal(value: unknown): value is ActiveGoal {
771
+ if (!value || typeof value !== "object") return false;
772
+ const goal = value as Partial<ActiveGoal>;
773
+ return (
774
+ typeof goal.id === "string" &&
775
+ typeof goal.text === "string" &&
776
+ ["active", "paused", "budget_limited", "complete"].includes(String(goal.status)) &&
777
+ typeof goal.startedAt === "number" &&
778
+ typeof goal.updatedAt === "number" &&
779
+ typeof goal.iteration === "number" &&
780
+ typeof goal.tokensUsed === "number" &&
781
+ typeof goal.timeUsedSeconds === "number" &&
782
+ typeof goal.baselineTokens === "number"
783
+ );
784
+ }