pi-goal-x 0.27.5 → 0.29.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
@@ -400,12 +400,21 @@ the timestamped `.backup-*` file created next to the session.
400
400
 
401
401
  ## Configuration
402
402
 
403
- Settings are stored in:
403
+ Settings resolve per setting in this order (highest wins):
404
404
 
405
405
  ```text
406
- .pi/pi-goal-x-settings.json
406
+ environment > project layer > global layer > defaults
407
407
  ```
408
408
 
409
+ Files:
410
+
411
+ ```text
412
+ global: ~/.pi/pi-goal-x-settings.json (or $PI_CODING_AGENT_DIR, or $PI_GOAL_GLOBAL_SETTINGS_FILE)
413
+ project: <cwd>/.pi/pi-goal-x-settings.json (or $PI_GOAL_SETTINGS_FILE)
414
+ ```
415
+
416
+ Define shared configuration once in the global file and override per project. Explicit `false`/`0` values in a lower layer override inherited values; nested `keybindings` inherit per key. `/goal-settings` shows each row's effective value and source, can switch the edited scope, and can remove a local override to return to inheritance.
417
+
409
418
  Use `/goal-settings` to configure task lists, verification contracts, subtask depth, automatic goal selection, and completion auditing. Goal objectives have no hard length limit by default; set `objectiveMaxChars` (or `PI_GOAL_OBJECTIVE_MAX_CHARS`, `0` = no limit) to cap objective length across `create_goal`, `propose_goal_draft`, and `/goal-tweak`.
410
419
 
411
420
  Configure the task shortcuts in the same file when the terminal captures the defaults:
@@ -424,6 +433,23 @@ Configure the task shortcuts in the same file when the terminal captures the def
424
433
 
425
434
  The default task bindings are `ctrl+shift+t`, `ctrl+shift+up`, and `ctrl+shift+down`. Use pi key names such as `ctrl+shift+up`.
426
435
 
436
+ ### Blocker Oracle (opt-in)
437
+
438
+ When an active goal reports blocked, a stronger read-only model can be consulted once per distinct blocker before the goal is allowed to stop. Off by default; configure under `/goal-settings → Blocker Oracle`:
439
+
440
+ ```json
441
+ {
442
+ "oracle": {
443
+ "enabled": true,
444
+ "provider": "anthropic",
445
+ "model": "<a stronger model>",
446
+ "maxFailedAttemptsPerBlocker": 2
447
+ }
448
+ }
449
+ ```
450
+
451
+ Both `provider` and `model` must be set explicitly — the executor model is never used as a silent fallback. The Oracle session can only read files (`read`, `grep`, `find`, `ls`) and returns structured advice. Actionable advice keeps the goal running until you attempt it; advice that needs human input lets the goal block immediately.
452
+
427
453
  ## License
428
454
 
429
455
  MIT
@@ -15,6 +15,7 @@ import {
15
15
  import type { GoalRecord, GoalTask, GoalTaskList } from "./goal-record.ts";
16
16
  import { countTaskSubtree } from "./goal-task-count.ts";
17
17
  import { loadGoalSettings, type GoalSettings, type ThinkingLevel } from "./goal-settings.ts";
18
+ import { statusLabel } from "./goal-core.ts";
18
19
 
19
20
  export interface AuditorProgress {
20
21
  /** Current tool being executed by the auditor, if any */
@@ -108,6 +109,47 @@ function escapePromptPayload(value: string): string {
108
109
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
109
110
  }
110
111
 
112
+ /** §60: human-readable labels for the auditor's read-only tool set. */
113
+ export function labelForReadOnlyTool(toolName: string): string {
114
+ switch (toolName) {
115
+ case "read": return "Inspecting files...";
116
+ case "grep": return "Searching content...";
117
+ case "find": return "Locating files...";
118
+ case "ls": return "Listing directory...";
119
+ case "bash": return "Running verification commands...";
120
+ default: return `Inspecting (${toolName})...`;
121
+ }
122
+ }
123
+
124
+ /** §60: rough phase estimate by tool kind (display only). */
125
+ export function estimateAuditProgress(toolName: string): number {
126
+ switch (toolName) {
127
+ case "read":
128
+ case "ls":
129
+ case "find":
130
+ return 25;
131
+ case "grep":
132
+ return 50;
133
+ case "bash":
134
+ return 75;
135
+ default:
136
+ return 25;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * §61: goal metadata WITHOUT the objective or task tree — those appear exactly
142
+ * once each in their own blocks. Replaces detailedSummary in the auditor prompt.
143
+ */
144
+ function minimalGoalMetadata(goal: GoalRecord): string {
145
+ return [
146
+ `Goal id: ${goal.id}`,
147
+ `Status: ${statusLabel(goal)}`,
148
+ `Mode: ${goal.sisyphus ? "sisyphus" : "regular"}`,
149
+ goal.tokenBudget ? `Budget: ${goal.tokenBudget} tokens (${goal.usage.tokensUsed} used)` : undefined,
150
+ ].filter(Boolean).join("\n");
151
+ }
152
+
111
153
  export function buildGoalAuditorPrompt(args: {
112
154
  goal: GoalRecord;
113
155
  detailedSummary: string;
@@ -140,11 +182,19 @@ export function buildGoalAuditorPrompt(args: {
140
182
  "",
141
183
  "The executor claim above is a claim, never evidence. It cannot make an otherwise incomplete goal complete; cross-check it against real artifacts where relevant.",
142
184
  "",
143
- "Current goal metadata:",
185
+ "Current goal metadata (objective and task tree appear in their own sections):",
144
186
  "<goal_details>",
145
- escapePromptPayload(args.detailedSummary),
146
- ...(!args.settings?.disableTasks && taskSummaryBlock(args.goal.taskList) ? ["", taskSummaryBlock(args.goal.taskList)] : []),
187
+ minimalGoalMetadata(args.goal),
147
188
  "</goal_details>",
189
+ ...(!args.settings?.disableTasks && args.goal.taskList ? [
190
+ "",
191
+ "Task tree:",
192
+ "<task_state>",
193
+ // Task titles are already escaped inside renderAuditorTaskTree; an
194
+ // extra pass would double-escape (&amp;lt;).
195
+ taskSummaryBlock(args.goal.taskList),
196
+ "</task_state>",
197
+ ] : []),
148
198
  ...(!args.settings?.disableContracts && args.goal.verificationContract?.trim() ? [
149
199
  "",
150
200
  "Goal verification contract (what the executor was required to verify):",
@@ -170,28 +220,9 @@ export function buildGoalAuditorPrompt(args: {
170
220
  "4. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
171
221
  "5. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
172
222
  ],
173
- "",
174
- "Progress reporting:",
175
- "You have the report_auditor_progress tool available to report your progress to the user.",
176
- "Please use it at natural phase boundaries:",
177
- " - When starting: report_auditor_progress(label='Starting audit...', percentage=0)",
178
- " - When beginning file inspection: report_auditor_progress(label='Inspecting files...', percentage=25)",
179
- " - When verifying success criteria: report_auditor_progress(label='Verifying success criteria...', percentage=50)",
180
- " - When evaluating evidence: report_auditor_progress(label='Evaluating evidence...', percentage=75)",
181
- " - When producing final report: report_auditor_progress(label='Producing report...', percentage=90)",
182
- "This is purely for user visibility and does not affect the audit outcome.",
183
223
  ].join("\n");
184
224
  }
185
225
 
186
- /** Tool name for auditor progress reporting */
187
- export const REPORT_AUDITOR_PROGRESS_TOOL_NAME = "report_auditor_progress";
188
-
189
- /** Parameters for the report_auditor_progress tool */
190
- export const reportAuditorProgressParams = Type.Object({
191
- label: Type.String({ description: "Current step label describing what the auditor is doing (e.g. 'Inspecting files...', 'Verifying success criteria...', 'Producing report...')" }),
192
- percentage: Type.Number({ description: "Completion percentage from 0 to 100", minimum: 0, maximum: 100 }),
193
- });
194
-
195
226
  function makeAuditorResourceLoader(): ResourceLoader {
196
227
  return {
197
228
  getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
@@ -203,11 +234,6 @@ function makeAuditorResourceLoader(): ResourceLoader {
203
234
  "You are a read-only completion auditor running in an isolated pi agent session.",
204
235
  "Inspect the repository and decide whether the claimed goal completion is genuinely satisfied.",
205
236
  "Never modify files. Never approve unless the actual user objective is complete.",
206
- "",
207
- "You have the report_auditor_progress tool available. Use it to report your audit progress",
208
- "to the user at natural phase boundaries (starting, inspecting files, verifying criteria,",
209
- "producing report). This helps the user understand what the auditor is doing and how far",
210
- "along it is.",
211
237
  ].join("\n"),
212
238
  getSystemPromptSource: () => undefined,
213
239
  getAppendSystemPrompt: () => [],
@@ -312,37 +338,6 @@ export async function runGoalCompletionAuditor(args: {
312
338
  args.onProgress?.({ ...progress });
313
339
  }
314
340
 
315
- // Build the report_auditor_progress tool, capturing the progress state
316
- const reportProgressTool = defineTool({
317
- name: REPORT_AUDITOR_PROGRESS_TOOL_NAME,
318
- label: "Report Auditor Progress",
319
- 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.",
320
- promptSnippet: "Report current audit progress (step label and completion percentage) to the user.",
321
- promptGuidelines: [
322
- "Use report_auditor_progress at natural phase boundaries during the audit:",
323
- " - When starting the audit: label='Starting audit...' percentage=0",
324
- " - When beginning file inspection: label='Inspecting files...' percentage=25",
325
- " - When verifying success criteria: label='Verifying success criteria...' percentage=50",
326
- " - When evaluating evidence: label='Evaluating evidence...' percentage=75",
327
- " - When producing final report: label='Producing report...' percentage=90",
328
- "This is purely for user visibility — it does not affect the audit outcome.",
329
- "Do not call this tool more than once every few seconds to avoid flooding.",
330
- ],
331
- parameters: reportAuditorProgressParams,
332
- executionMode: "sequential",
333
- async execute(_toolCallId, params) {
334
- const { label, percentage } = params as Static<typeof reportAuditorProgressParams>;
335
- progress.label = label;
336
- progress.percentage = percentage;
337
- progress.phase = "running";
338
- emitProgress();
339
- return {
340
- content: [{ type: "text", text: `Progress reported: ${label} (${percentage}%)` }],
341
- details: {},
342
- };
343
- },
344
- });
345
-
346
341
  const projectResources = args.settings?.auditorProjectResources === true;
347
342
  const { session } = await createSession({
348
343
  cwd: args.ctx.cwd,
@@ -357,10 +352,18 @@ export async function runGoalCompletionAuditor(args: {
357
352
  : { resourceLoader: makeAuditorResourceLoader() }),
358
353
  sessionManager: SessionManager.inMemory(args.ctx.cwd),
359
354
  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
360
- tools: ["read", "grep", "find", "ls", "bash", REPORT_AUDITOR_PROGRESS_TOOL_NAME],
361
- customTools: [reportProgressTool],
355
+ tools: ["read", "grep", "find", "ls", "bash"],
362
356
  } as Parameters<typeof createAgentSession>[0]);
363
357
  const unsubscribe = session.subscribe((event) => {
358
+ if (event.type === "agent_start") {
359
+ // PR E §60: progress is derived from session events — no model-facing
360
+ // report_auditor_progress tool exists anymore.
361
+ progress.label = "Starting audit...";
362
+ progress.percentage = 0;
363
+ progress.phase = "running";
364
+ emitProgress();
365
+ return;
366
+ }
364
367
  if (event.type === "tool_execution_start") {
365
368
  progress.currentTool = event.toolName;
366
369
  progress.currentToolArgs = typeof event.args === "object" && event.args !== null
@@ -368,6 +371,9 @@ export async function runGoalCompletionAuditor(args: {
368
371
  : String(event.args ?? "").slice(0, 120);
369
372
  progress.currentToolStartedAt = Date.now();
370
373
  progress.phase = "tool_executing";
374
+ // §60 derive the human label + estimate from the read-only tool.
375
+ progress.label = labelForReadOnlyTool(event.toolName);
376
+ progress.percentage = Math.max(progress.percentage ?? 0, estimateAuditProgress(event.toolName));
371
377
  emitProgress();
372
378
  return;
373
379
  }
@@ -414,6 +420,12 @@ export async function runGoalCompletionAuditor(args: {
414
420
  for (const part of message.content ?? []) {
415
421
  if (part.type === "text" && typeof part.text === "string") outputParts.push(part.text);
416
422
  }
423
+ // Final report production: derived label for the widget.
424
+ if (outputParts.some((t) => t.trim())) {
425
+ progress.label = "Producing report...";
426
+ progress.percentage = Math.max(progress.percentage ?? 0, 90);
427
+ progress.phase = "producing_report";
428
+ }
417
429
  // Show the accumulated output in progress
418
430
  const fullText = outputParts.join("\n\n");
419
431
  const lines = fullText.split("\n").filter((l: string) => l.trim());