pi-goal-x 0.28.0 → 0.30.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
@@ -433,6 +433,23 @@ Configure the task shortcuts in the same file when the terminal captures the def
433
433
 
434
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`.
435
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
+
436
453
  ## License
437
454
 
438
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());
@@ -380,10 +380,12 @@ export function registerGoalCommands(core: GoalCore): void {
380
380
  * eight persisted fields are present and operable.
381
381
  */
382
382
  type SettingRow = {
383
- key: keyof GoalSettings;
383
+ key: keyof GoalSettings | string;
384
384
  label: string;
385
- section: "Goal behavior" | "Task tracking" | "Completion auditor";
385
+ section: "Goal behavior" | "Task tracking" | "Completion auditor" | "Blocker Oracle";
386
386
  kind: "boolean" | "modelSelector" | "thinking" | "positiveInteger";
387
+ /** Settings path for the mutation (defaults to [key]). */
388
+ path?: string[];
387
389
  };
388
390
 
389
391
  const SETTING_ROWS: readonly SettingRow[] = [
@@ -398,9 +400,16 @@ export function registerGoalCommands(core: GoalCore): void {
398
400
  { key: "provider", label: "provider", section: "Completion auditor", kind: "modelSelector" },
399
401
  { key: "model", label: "model", section: "Completion auditor", kind: "modelSelector" },
400
402
  { key: "thinkingLevel", label: "thinking_level", section: "Completion auditor", kind: "thinking" },
403
+ // Issue #26: opt-in blocker Oracle. Disabled by default; provider/model
404
+ // must BOTH be set explicitly — the executor model is never used silently.
405
+ { key: "oracleEnabled", label: "oracle enabled", section: "Blocker Oracle", kind: "boolean", path: ["oracle", "enabled"] },
406
+ { key: "oracleProviderModel", label: "oracle provider/model", section: "Blocker Oracle", kind: "modelSelector", path: ["oracle"] },
407
+ { key: "oracleThinkingLevel", label: "oracle thinking_level", section: "Blocker Oracle", kind: "thinking", path: ["oracle", "thinkingLevel"] },
408
+ { key: "oracleProjectResources", label: "oracle project resources", section: "Blocker Oracle", kind: "boolean", path: ["oracle", "projectResources"] },
409
+ { key: "oracleMaxFailedAttemptsPerBlocker", label: "max failed attempts per blocker", section: "Blocker Oracle", kind: "positiveInteger", path: ["oracle", "maxFailedAttemptsPerBlocker"] },
401
410
  ];
402
411
 
403
- function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
412
+ function settingsValue(config: GoalSettings, key: keyof GoalSettings | string): string {
404
413
  if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
405
414
  return config[key] === true ? "true" : "false";
406
415
  }
@@ -408,7 +417,7 @@ export function registerGoalCommands(core: GoalCore): void {
408
417
  if (key === "stallTimeoutMinutes") return config.stallTimeoutMinutes !== undefined ? String(config.stallTimeoutMinutes) : "0";
409
418
  if (key === "objectiveMaxChars") return config.objectiveMaxChars !== undefined ? String(config.objectiveMaxChars) : "0";
410
419
  if (key === "keybindings") return config.keybindings ? `${config.keybindings.dashboard.toggleExpand}, ${config.keybindings.dashboard.scrollUp}, ${config.keybindings.dashboard.scrollDown}` : "(default)";
411
- const value = config[key];
420
+ const value = (config as Record<string, unknown>)[key];
412
421
  return typeof value === "string" ? value : "(default)";
413
422
  }
414
423
 
@@ -465,24 +474,38 @@ export function registerGoalCommands(core: GoalCore): void {
465
474
  const options: string[] = [];
466
475
  options.push(`─── Editing: ${scope} (${scopePath(scope)}) ───`);
467
476
  let lastSection: string | null = null;
477
+ const pathOf = (row: SettingRow): string[] => row.path ?? [String(row.key)];
478
+ const valueAtPath = (obj: unknown, path: string[]): unknown => {
479
+ let cursor: unknown = obj;
480
+ for (const segment of path) {
481
+ if (!cursor || typeof cursor !== "object") return undefined;
482
+ cursor = (cursor as Record<string, unknown>)[segment];
483
+ }
484
+ return cursor;
485
+ };
468
486
  for (const row of SETTING_ROWS) {
469
487
  if (row.section !== lastSection) {
470
488
  options.push(`─── ${row.section} ───`);
471
489
  lastSection = row.section;
472
490
  }
473
- const envVar = envOverrideFor(row.key);
491
+ const envVar = typeof row.key === "string" ? envOverrideFor(row.key as keyof GoalSettings) : null;
474
492
  if (envVar) {
475
493
  options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} (environment; read-only)`);
476
494
  continue;
477
495
  }
478
- const provenance = snapshot.provenance.get(String(row.key));
496
+ const rowPath = pathOf(row);
497
+ const provenance = snapshot.provenance.get(rowPath.join("."));
479
498
  let source: string;
480
499
  if (provenance?.source === "global") source = scope === "project" ? "(inherited from global)" : "(global override)";
481
500
  else if (provenance?.source === "project") source = scope === "project" ? "(project override)" : "(project; edit via project scope)";
482
501
  else source = "(default)";
483
- const hasLocalOverride = localLayer[row.key] !== undefined;
502
+ const hasLocalOverride = valueAtPath(localLayer, rowPath) !== undefined;
484
503
  if (hasLocalOverride && provenance?.source === scope) source = `(${scope} override)`;
485
- options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} ${source}`);
504
+ // Nested rows render their leaf value directly.
505
+ const displayValue = row.path
506
+ ? String(valueAtPath(snapshot.value, rowPath) ?? "(default)")
507
+ : settingsValue(snapshot.value, row.key);
508
+ options.push(` ${row.label}: ${displayValue} ${source}`);
486
509
  }
487
510
  options.push("Done");
488
511
  const selected = await ctx.ui.select("Goal settings", options);
@@ -498,16 +521,29 @@ export function registerGoalCommands(core: GoalCore): void {
498
521
  const label = trimmed.slice(0, colon).trim();
499
522
  const row = SETTING_ROWS.find((r) => r.label === label);
500
523
  if (!row) continue;
501
- if (envOverrideFor(row.key)) {
502
- ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(row.key)} env var.`, "warning");
524
+ const envVarForRow2 = envOverrideFor(row.key as keyof GoalSettings);
525
+ if (envVarForRow2) {
526
+ ctx.ui.notify(`${row.label} is read-only: overridden by the ${envVarForRow2} env var.`, "warning");
503
527
  continue;
504
528
  }
529
+ // Per-row settings path (flat keys default to [key]; nested Blocker
530
+ // Oracle rows carry explicit paths).
531
+ const rowPath = row.path ?? [String(row.key)];
532
+ const pathKeyStr = rowPath.join(".");
533
+ const localValueAtPath = (() => {
534
+ let cursor: unknown = localLayer;
535
+ for (const segment of rowPath) {
536
+ if (!cursor || typeof cursor !== "object") return undefined;
537
+ cursor = (cursor as Record<string, unknown>)[segment];
538
+ }
539
+ return cursor;
540
+ })();
505
541
 
506
542
  const inheritLabel = scope === "project" ? "Use inherited value" : "Use default (delete global override)";
507
- const hasLocalOverride = localLayer[row.key] !== undefined;
543
+ const hasLocalOverride = localValueAtPath !== undefined;
508
544
 
509
545
  if (row.kind === "boolean") {
510
- const effective = snapshot.provenance.get(String(row.key))?.value === true;
546
+ const effective = snapshot.provenance.get(pathKeyStr)?.value === true;
511
547
  const actions = [
512
548
  `Set ${scope} override to true`,
513
549
  `Set ${scope} override to false`,
@@ -517,28 +553,28 @@ export function registerGoalCommands(core: GoalCore): void {
517
553
  const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
518
554
  if (!action || action === "Cancel") continue;
519
555
  if (action === inheritLabel) {
520
- applyMutation(scope, { op: "unset", path: [row.key] });
556
+ applyMutation(scope, { op: "unset", path: rowPath });
521
557
  continue;
522
558
  }
523
559
  const value = action.endsWith("true");
524
560
  if (value !== effective || !hasLocalOverride) {
525
- applyMutation(scope, { op: "set", path: [row.key], value });
561
+ applyMutation(scope, { op: "set", path: rowPath, value });
526
562
  }
527
563
  continue;
528
564
  }
529
565
 
530
566
  if (row.kind === "positiveInteger") {
531
- const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
567
+ const min = row.path ? 1 : ((row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1);
532
568
  const actions = [`Set ${scope} override...`];
533
569
  if (hasLocalOverride) actions.push(inheritLabel);
534
570
  actions.push("Cancel");
535
571
  const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
536
572
  if (!action || action === "Cancel") continue;
537
573
  if (action === inheritLabel) {
538
- applyMutation(scope, { op: "unset", path: [row.key] });
574
+ applyMutation(scope, { op: "unset", path: rowPath });
539
575
  continue;
540
576
  }
541
- const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(String(row.key))?.value ?? min));
577
+ const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(pathKeyStr)?.value ?? min));
542
578
  if (input === undefined) continue;
543
579
  // Full-string decimal validation: rejects 1.5, 1x, negatives,
544
580
  // infinity, and unsafe integers alike.
@@ -547,7 +583,12 @@ export function registerGoalCommands(core: GoalCore): void {
547
583
  ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
548
584
  continue;
549
585
  }
550
- applyMutation(scope, { op: "set", path: [row.key], value: Number(value) });
586
+ // Oracle attempt cap is bounded at 3 by the settings parser.
587
+ if (row.path?.[0] === "oracle" && Number(value) > 3) {
588
+ ctx.ui.notify(`${row.label} must be an integer between 1 and 3`, "warning");
589
+ continue;
590
+ }
591
+ applyMutation(scope, { op: "set", path: rowPath, value: Number(value) });
551
592
  continue;
552
593
  }
553
594
 
@@ -558,21 +599,26 @@ export function registerGoalCommands(core: GoalCore): void {
558
599
  if (!picked) continue;
559
600
  const choice = picked.trim().replace(/^\u2713\s+/, "");
560
601
  if (choice === "(default)") {
561
- if (hasLocalOverride) applyMutation(scope, { op: "unset", path: ["thinkingLevel"] });
602
+ if (hasLocalOverride) applyMutation(scope, { op: "unset", path: rowPath });
562
603
  continue;
563
604
  }
564
605
  if (!(AUDITOR_THINKING_LEVELS as readonly string[]).includes(choice)) {
565
606
  ctx.ui.notify(`thinking_level must be one of: ${AUDITOR_THINKING_LEVELS.join(", ")} (or "(default)")`, "warning");
566
607
  continue;
567
608
  }
568
- applyMutation(scope, { op: "set", path: ["thinkingLevel"], value: choice });
609
+ applyMutation(scope, { op: "set", path: rowPath, value: choice });
569
610
  continue;
570
611
  }
571
612
 
572
- // modelSelector rows (provider, model): searchable auditor model
573
- // picker with explicit inherit/default handling. Either row applies
574
- // the same provider+model pair.
575
- const configured = configuredAuditorModelKey(snapshot.value as GoalSettings);
613
+ // modelSelector rows (provider, model): searchable model picker with
614
+ // explicit inherit/default handling. Auditor rows apply provider+model
615
+ // at the flat paths; the Oracle row writes oracle.provider/oracle.model.
616
+ const oraclePair = row.path?.[0] === "oracle";
617
+ const pairPrefix = oraclePair ? ["oracle"] : [];
618
+ const configuredBase = oraclePair
619
+ ? [snapshot.value.oracle?.provider, snapshot.value.oracle?.model].filter(Boolean).join("/")
620
+ : configuredAuditorModelKey(snapshot.value as GoalSettings);
621
+ const configured = configuredBase || undefined;
576
622
  const session = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
577
623
  const choices = buildAuditorModelChoices(ctx.modelRegistry.getAvailable(), configured, session);
578
624
  const filter = await ctx.ui.input("Filter auditor models (provider/id/name; blank = all)", "");
@@ -584,7 +630,7 @@ export function registerGoalCommands(core: GoalCore): void {
584
630
  if (!choice) continue;
585
631
  if (choice.kind === "default") {
586
632
  for (const key of ["provider", "model"] as const) {
587
- if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [key] });
633
+ if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [...pairPrefix, key] });
588
634
  }
589
635
  continue;
590
636
  }
@@ -604,8 +650,8 @@ export function registerGoalCommands(core: GoalCore): void {
604
650
  providerValue = choice.provider;
605
651
  modelValue = choice.model;
606
652
  }
607
- if (applyMutation(scope, { op: "set", path: ["provider"], value: providerValue })) {
608
- applyMutation(scope, { op: "set", path: ["model"], value: modelValue });
653
+ if (applyMutation(scope, { op: "set", path: [...pairPrefix, "provider"], value: providerValue })) {
654
+ applyMutation(scope, { op: "set", path: [...pairPrefix, "model"], value: modelValue });
609
655
  }
610
656
  }
611
657
  } finally {
@@ -11,7 +11,7 @@ import {
11
11
  type GoalLedgerEvent,
12
12
  type LedgerStateReadResult,
13
13
  } from "./goal-ledger.ts";
14
- import { type GoalRecord } from "./goal-record.ts";
14
+ import { type GoalRecord, type GoalTask } from "./goal-record.ts";
15
15
 
16
16
  export function buildGoalCompactSummary(
17
17
  goal: GoalRecord,
@@ -151,3 +151,51 @@ export function buildCompactionSummary(args: {
151
151
 
152
152
  return lines.join("\n");
153
153
  }
154
+
155
+ /**
156
+ * PR E §62: post-compaction DELTA for the focused goal. The active system
157
+ * goal block already contains the objective, full policy, task gate, and
158
+ * verification contract — this delta adds only what compaction may have
159
+ * lost: the execution focus, a bounded recent-events tail, the latest
160
+ * unresolved auditor finding, and the other-open-goals count.
161
+ */
162
+ export function buildPostCompactionGoalDelta(args: {
163
+ goal: GoalRecord;
164
+ ledgerEvents: GoalLedgerEvent[];
165
+ otherOpenCount: number;
166
+ }): string {
167
+ const { goal, ledgerEvents, otherOpenCount } = args;
168
+ const lines: string[] = [];
169
+ lines.push(`[POST-COMPACTION RESYNC goalId=${goal.id}]`);
170
+ // Execution focus (with contract) — the one thing memory must not lose.
171
+ if (goal.currentTaskId && goal.taskList) {
172
+ const find = (tasks: GoalTask[]): GoalTask | undefined => {
173
+ for (const t of tasks) {
174
+ if (t.id === goal.currentTaskId) return t;
175
+ const nested = t.subtasks ? find(t.subtasks) : undefined;
176
+ if (nested) return nested;
177
+ }
178
+ return undefined;
179
+ };
180
+ const current = find(goal.taskList.tasks);
181
+ if (current) {
182
+ lines.push(`Current task: ${current.id} — ${current.title}${current.verificationContract ? ` (contract: ${current.verificationContract})` : ""}`);
183
+ }
184
+ }
185
+ // Bounded recent-event tail.
186
+ const recent = latestEventsForGoal(ledgerEvents, goal.id, 5);
187
+ if (recent.length > 0) {
188
+ lines.push("Recent events:");
189
+ for (const event of recent) {
190
+ lines.push(` ${event.at.slice(11, 19)} ${event.type}`);
191
+ }
192
+ }
193
+ // Latest unresolved auditor finding.
194
+ const audit = latestAuditorResultForGoal(ledgerEvents, goal.id);
195
+ if (audit && audit.verdict === "disapproved") {
196
+ lines.push(`Latest unresolved auditor finding: ${audit.report.slice(0, 300)}`);
197
+ }
198
+ lines.push(`Other open goals: ${otherOpenCount}`);
199
+ lines.push("Continue from authoritative files and goal storage.");
200
+ return lines.join("\n");
201
+ }
@@ -195,7 +195,7 @@ if (settings.disabled === true) {
195
195
  ].filter((line): line is string => line !== undefined).join("\n"),
196
196
  display: true,
197
197
  details: { phase: "started", goalId: auditTarget.id, auditor: auditorLabel },
198
- }, { triggerTurn: true });
198
+ }, { triggerTurn: false });
199
199
  if (!core.isFocusedOperationCurrent(completionFocus)) {
200
200
  return core.focusedOperationCancelledResult("Goal completion", completionFocus);
201
201
  }