pi-goal-x 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -27,7 +27,7 @@ All core features of [@capyup/pi-goal](https://github.com/capyup/pi-goal) are pr
27
27
 
28
28
  ### Completion auditor
29
29
 
30
- - **Live progress widget** — when the auditor runs, the TUI shows a spinner, the current tool being executed, and recent output lines. No more wondering if anything is happening.
30
+ - **Live progress widget** — when the auditor runs, the TUI shows a spinner, a progress bar (`[████░░░░] 40%`), step labels (`Inspecting files...`, `Verifying success criteria...`), the current tool being executed, and recent output lines. No more wondering if anything is happening.
31
31
  - **Escape to skip** — press Escape during an audit to abort it and complete the goal immediately. The skip is recorded in the ledger as `audit_skipped` with reason `user_aborted` and auditor model metadata.
32
32
  - **Disable the auditor entirely** — set `disabled: true` in `.pi/goal-auditor.json` (or toggle it via `/goal-settings` → `disabled`). The agent can still bypass with user confirmation by passing `confirmBypassAuditor: true` to `update_goal`.
33
33
  - **Skipped audits are recorded** — every skip (whether disabled or Escape-aborted) is logged to the ledger with the reason, provider, model, and thinking level for full traceability.
@@ -1,10 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import type { Static } from "@earendil-works/pi-ai";
4
+ import { Type } from "@earendil-works/pi-ai";
3
5
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
4
6
  import type { Model } from "@earendil-works/pi-ai";
5
7
  import {
6
8
  createAgentSession,
7
9
  createExtensionRuntime,
10
+ defineTool,
8
11
  SessionManager,
9
12
  SettingsManager,
10
13
  type ExtensionContext,
@@ -29,9 +32,13 @@ export interface AuditorProgress {
29
32
  /** Recent text output lines from the auditor's assistant messages */
30
33
  recentOutput: string[];
31
34
  /** Phase of the audit */
32
- phase: "running" | "tool_executing" | "producing_report" | "done";
35
+ phase: "running" | "tool_executing" | "producing_report" | "thinking" | "done";
33
36
  /** Elapsed ms since audit started */
34
37
  elapsedMs: number;
38
+ /** Current step label shown to the user (e.g. "Inspecting files...") */
39
+ label?: string;
40
+ /** Completion percentage from 0 to 100 */
41
+ percentage?: number;
35
42
  }
36
43
 
37
44
  export type AuditorProgressCallback = (progress: AuditorProgress) => void;
@@ -156,9 +163,28 @@ export function buildGoalAuditorPrompt(args: {
156
163
  "2. Inspect artifacts or command output that can prove or disprove those criteria.",
157
164
  "3. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
158
165
  "4. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
166
+ "",
167
+ "Progress reporting:",
168
+ "You have the report_auditor_progress tool available to report your progress to the user.",
169
+ "Please use it at natural phase boundaries:",
170
+ " - When starting: report_auditor_progress(label='Starting audit...', percentage=0)",
171
+ " - When beginning file inspection: report_auditor_progress(label='Inspecting files...', percentage=25)",
172
+ " - When verifying success criteria: report_auditor_progress(label='Verifying success criteria...', percentage=50)",
173
+ " - When evaluating evidence: report_auditor_progress(label='Evaluating evidence...', percentage=75)",
174
+ " - When producing final report: report_auditor_progress(label='Producing report...', percentage=90)",
175
+ "This is purely for user visibility and does not affect the audit outcome.",
159
176
  ].join("\n");
160
177
  }
161
178
 
179
+ /** Tool name for auditor progress reporting */
180
+ export const REPORT_AUDITOR_PROGRESS_TOOL_NAME = "report_auditor_progress";
181
+
182
+ /** Parameters for the report_auditor_progress tool */
183
+ export const reportAuditorProgressParams = Type.Object({
184
+ label: Type.String({ description: "Current step label describing what the auditor is doing (e.g. 'Inspecting files...', 'Verifying success criteria...', 'Producing report...')" }),
185
+ percentage: Type.Number({ description: "Completion percentage from 0 to 100", minimum: 0, maximum: 100 }),
186
+ });
187
+
162
188
  function makeAuditorResourceLoader(): ResourceLoader {
163
189
  return {
164
190
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
@@ -170,9 +196,14 @@ function makeAuditorResourceLoader(): ResourceLoader {
170
196
  "You are a read-only completion auditor running in an isolated pi agent session.",
171
197
  "Inspect the repository and decide whether the claimed goal completion is genuinely satisfied.",
172
198
  "Never modify files. Never approve unless the actual user objective is complete.",
199
+ "",
200
+ "You have the report_auditor_progress tool available. Use it to report your audit progress",
201
+ "to the user at natural phase boundaries (starting, inspecting files, verifying criteria,",
202
+ "producing report). This helps the user understand what the auditor is doing and how far",
203
+ "along it is.",
173
204
  ].join("\n"),
174
205
  getAppendSystemPrompt: () => [],
175
- extendResources: () => {},
206
+ extendResources: () => {},
176
207
  reload: async () => {},
177
208
  };
178
209
  }
@@ -228,16 +259,6 @@ export async function runGoalCompletionAuditor(args: {
228
259
  }
229
260
  try {
230
261
  const createSession = args.createSession ?? createAgentSession;
231
- const { session } = await createSession({
232
- cwd: args.ctx.cwd,
233
- model,
234
- thinkingLevel,
235
- modelRegistry: args.ctx.modelRegistry,
236
- resourceLoader: makeAuditorResourceLoader(),
237
- sessionManager: SessionManager.inMemory(args.ctx.cwd),
238
- settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
239
- tools: ["read", "grep", "find", "ls", "bash"],
240
- });
241
262
  const startedAt = Date.now();
242
263
  const progress: AuditorProgress = {
243
264
  recentOutput: [],
@@ -248,6 +269,49 @@ export async function runGoalCompletionAuditor(args: {
248
269
  progress.elapsedMs = Date.now() - startedAt;
249
270
  args.onProgress?.({ ...progress });
250
271
  }
272
+
273
+ // Build the report_auditor_progress tool, capturing the progress state
274
+ const reportProgressTool = defineTool({
275
+ name: REPORT_AUDITOR_PROGRESS_TOOL_NAME,
276
+ label: "Report Auditor Progress",
277
+ description: "Report current progress of the audit to the user. Call this at natural phase boundaries (starting, inspecting files, verifying criteria, producing report) to keep the user informed.",
278
+ promptSnippet: "Report current audit progress (step label and completion percentage) to the user.",
279
+ promptGuidelines: [
280
+ "Use report_auditor_progress at natural phase boundaries during the audit:",
281
+ " - When starting the audit: label='Starting audit...' percentage=0",
282
+ " - When beginning file inspection: label='Inspecting files...' percentage=25",
283
+ " - When verifying success criteria: label='Verifying success criteria...' percentage=50",
284
+ " - When evaluating evidence: label='Evaluating evidence...' percentage=75",
285
+ " - When producing final report: label='Producing report...' percentage=90",
286
+ "This is purely for user visibility — it does not affect the audit outcome.",
287
+ "Do not call this tool more than once every few seconds to avoid flooding.",
288
+ ],
289
+ parameters: reportAuditorProgressParams,
290
+ executionMode: "sequential",
291
+ async execute(_toolCallId, params) {
292
+ const { label, percentage } = params as Static<typeof reportAuditorProgressParams>;
293
+ progress.label = label;
294
+ progress.percentage = percentage;
295
+ progress.phase = "running";
296
+ emitProgress();
297
+ return {
298
+ content: [{ type: "text", text: `Progress reported: ${label} (${percentage}%)` }],
299
+ details: {},
300
+ };
301
+ },
302
+ });
303
+
304
+ const { session } = await createSession({
305
+ cwd: args.ctx.cwd,
306
+ model,
307
+ thinkingLevel,
308
+ modelRegistry: args.ctx.modelRegistry,
309
+ resourceLoader: makeAuditorResourceLoader(),
310
+ sessionManager: SessionManager.inMemory(args.ctx.cwd),
311
+ settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
312
+ tools: ["read", "grep", "find", "ls", "bash", REPORT_AUDITOR_PROGRESS_TOOL_NAME],
313
+ customTools: [reportProgressTool],
314
+ });
251
315
  const unsubscribe = session.subscribe((event) => {
252
316
  if (event.type === "tool_execution_start") {
253
317
  progress.currentTool = event.toolName;
@@ -268,6 +332,20 @@ export async function runGoalCompletionAuditor(args: {
268
332
  return;
269
333
  }
270
334
  if (event.type === "message_update") {
335
+ // Check for thinking events from the assistant stream
336
+ const streamEvent = (event as any).assistantMessageEvent;
337
+ if (streamEvent?.type === "thinking_start") {
338
+ progress.phase = "thinking";
339
+ if (!progress.label) progress.label = "Analyzing goal...";
340
+ emitProgress();
341
+ return;
342
+ }
343
+ if (streamEvent?.type === "thinking_end") {
344
+ progress.phase = "running";
345
+ emitProgress();
346
+ return;
347
+ }
348
+ // For text content, show producing_report phase
271
349
  progress.phase = "producing_report";
272
350
  const message = event.message as any;
273
351
  if (message?.role === "assistant") {
@@ -300,6 +378,8 @@ export async function runGoalCompletionAuditor(args: {
300
378
  args.signal?.addEventListener("abort", abortSession, { once: true });
301
379
 
302
380
  // Emit initial progress
381
+ progress.label = "Starting audit...";
382
+ progress.percentage = 0;
303
383
  emitProgress();
304
384
  try {
305
385
  if (args.signal?.aborted) return { approved: false, disapproved: true, output: "", model: modelLabel(model), thinkingLevel, error: "Auditor aborted." };
@@ -307,6 +387,8 @@ export async function runGoalCompletionAuditor(args: {
307
387
  } finally {
308
388
  args.signal?.removeEventListener("abort", abortSession);
309
389
  progress.phase = "done";
390
+ progress.label = "Audit complete.";
391
+ progress.percentage = 100;
310
392
  emitProgress();
311
393
  unsubscribe();
312
394
  }
@@ -24,8 +24,12 @@ export interface AuditorWidgetProgress {
24
24
  currentToolArgs?: string;
25
25
  currentToolStartedAt?: number;
26
26
  recentOutput: string[];
27
- phase: "running" | "tool_executing" | "producing_report" | "done";
27
+ phase: "running" | "tool_executing" | "producing_report" | "thinking" | "done";
28
28
  elapsedMs: number;
29
+ /** Current step label shown to the user */
30
+ label?: string;
31
+ /** Completion percentage from 0 to 100 */
32
+ percentage?: number;
29
33
  }
30
34
 
31
35
  export interface GoalWidgetOptions {
@@ -52,6 +56,13 @@ function branchLine(theme: Theme, width: number, isLast: boolean, content: strin
52
56
  return fit(`${theme.fg("dim", prefix)} ${content}`, width);
53
57
  }
54
58
 
59
+ function progressBar(pct: number, barWidth: number, theme: Theme): string {
60
+ const safeBar = Math.max(3, barWidth);
61
+ const filled = Math.min(safeBar, Math.max(0, Math.round((pct / 100) * safeBar)));
62
+ const empty = safeBar - filled;
63
+ return `[${theme.fg("accent", "█".repeat(filled))}${theme.fg("dim", "░".repeat(empty))}]`;
64
+ }
65
+
55
66
  function displayIcon(goal: GoalWidgetRecord): { icon: string; color: GoalWidgetColor; label: string } {
56
67
  if (goal.status === "complete") return { icon: "✓", color: "success", label: "complete" };
57
68
  if (goal.status === "paused") {
@@ -81,8 +92,17 @@ function spinnerFrame(): string {
81
92
  export function renderAuditorWidgetLines(progress: AuditorWidgetProgress, theme: Theme, width: number): string[] {
82
93
  const safeWidth = Math.max(1, width);
83
94
  const isActive = progress.phase !== "done";
84
- const icon = isActive ? theme.fg("accent", spinnerFrame()) : theme.fg("success", "✓");
85
- const label = isActive ? "auditing" : "audit complete";
95
+ const isThinking = progress.phase === "thinking";
96
+ const icon = isActive
97
+ ? isThinking
98
+ ? theme.fg("muted", "⟡")
99
+ : theme.fg("accent", spinnerFrame())
100
+ : theme.fg("success", "✓");
101
+ const label = isActive
102
+ ? isThinking
103
+ ? "thinking..."
104
+ : "auditing"
105
+ : "audit complete";
86
106
  // formatDuration expects seconds, progress.elapsedMs is in milliseconds
87
107
  const duration = formatDuration(Math.floor(progress.elapsedMs / 1000));
88
108
  const lines: string[] = [
@@ -94,7 +114,30 @@ export function renderAuditorWidgetLines(progress: AuditorWidgetProgress, theme:
94
114
  ),
95
115
  ];
96
116
 
97
- if (isActive && progress.currentTool) {
117
+ // Show step label when available
118
+ if (progress.label) {
119
+ lines.push(branchLine(
120
+ theme,
121
+ safeWidth,
122
+ false,
123
+ `${theme.fg("text", truncateText(progress.label, Math.max(8, safeWidth - 6)))}`,
124
+ ));
125
+ }
126
+
127
+ // Show progress bar when percentage is available
128
+ if (typeof progress.percentage === "number") {
129
+ const barWidth = Math.max(6, Math.min(safeWidth - 10, 30));
130
+ const bar = progressBar(progress.percentage, barWidth, theme);
131
+ const pct = `${theme.fg("muted", `${Math.round(progress.percentage)}%`)}`;
132
+ lines.push(branchLine(
133
+ theme,
134
+ safeWidth,
135
+ isActive && !progress.currentTool && progress.recentOutput.length === 0 && !isThinking,
136
+ `${bar} ${pct}`,
137
+ ));
138
+ }
139
+
140
+ if (isActive && !isThinking && progress.currentTool) {
98
141
  const argText = progress.currentToolArgs
99
142
  ? truncateText(progress.currentToolArgs, Math.max(10, safeWidth - 24))
100
143
  : "";
@@ -129,7 +172,7 @@ export function renderAuditorWidgetLines(progress: AuditorWidgetProgress, theme:
129
172
  }
130
173
 
131
174
  // Show skip hint when audit is actively running
132
- if (isActive) {
175
+ if (isActive && !isThinking) {
133
176
  lines.push(branchLine(
134
177
  theme,
135
178
  safeWidth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",