pi-goal-x 0.26.2 → 0.27.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
@@ -177,6 +177,18 @@ Show the focused goal:
177
177
  /goal-status
178
178
  ```
179
179
 
180
+ Run read-only storage/runtime health checks:
181
+
182
+ ```text
183
+ /goal-status health
184
+ ```
185
+
186
+ Re-read goal storage caches (pool, ledger, settings) from disk and report what changed — picks up external edits to `.pi` files without file watchers:
187
+
188
+ ```text
189
+ /goal-refresh
190
+ ```
191
+
180
192
  Select an open goal for the current session:
181
193
 
182
194
  ```text
@@ -341,6 +353,9 @@ Each session can focus on one goal while the project keeps other goals open.
341
353
  /goal-list List open goals
342
354
  /goal-status Show the focused goal (unified dashboard)
343
355
  /goal-status verbose Show the focused goal with full diagnostic detail
356
+ /goal-status health Check goal storage/runtime health (read-only)
357
+ /goal-refresh Re-read storage caches and report external changes
358
+ /goal-recovery Read-only recovery report; `/goal-recovery repair` fixes stale locks + snapshot after confirmation
344
359
  /goal-focus Select an open goal
345
360
  /goal-unfocus Remove the session’s focus
346
361
  /goal-tweak <change> Revise the focused goal
@@ -99,7 +99,7 @@ The curated fourteen-command palette:
99
99
  | `/goal-direct <objective>` | Direct regular-goal creation without drafting |
100
100
  | `/sisyphus-direct <objective>` | Direct Sisyphus creation without drafting |
101
101
  | `/goal-list` | List all open goals and the current focus |
102
- | `/goal-status` | Read-only focused-goal summary plus other-open-goal count |
102
+ | `/goal-status` | Read-only focused-goal summary plus other-open-goal count; append `verbose` for diagnostics or `health` for storage/runtime checks |
103
103
  | `/goal-focus` | Choose this session's focused goal |
104
104
  | `/goal-unfocus` | Detach the session without modifying the shared goal |
105
105
  | `/goal-settings` | Fully operable settings editor for all eight persisted fields |
@@ -182,6 +182,7 @@ user confirms.
182
182
  - `/sisyphus [seed]` starts a Sisyphus guided draft.
183
183
  - `/goal-direct <objective>` and `/sisyphus-direct <objective>` create directly without drafting.
184
184
  - `/goal-list` prints all open goals with id, status, mode, usage, objective title, path, and a focus marker.
185
+ - `/goal-status health` performs a read-only coherence check for focus, lifecycle, goal-file presence, malformed ledger entries, task progress, and token-budget pressure; it never acts as a completion verdict.
185
186
  - `/goal-focus` uses `ctx.ui.select` when multiple goals are open and updates only session focus.
186
187
  - `/goal-unfocus` writes a null session focus entry, clears continuation/runtime state, aborts in-flight work and audits for that session, and leaves the shared active goal file and project-global focus ledger unchanged. Focus revision tokens prevent pending completion and task-list results from mutating a goal after detachment.
187
188
  - `/goal-resume` resumes the focused paused goal; when unfocused with multiple open goals, it asks the user to choose. Choosing an already active goal only focuses it.
@@ -217,6 +217,7 @@ function makeAuditorResourceLoader(): ResourceLoader {
217
217
  };
218
218
  }
219
219
 
220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pi SDK types Model<any> idiomatically (pi-coding-agent sdk.d.ts)
220
221
  export function resolveAuditorModel(ctx: ExtensionContext, config: GoalSettings): { model: Model<any> | undefined; error?: string } {
221
222
  if (!config.model && !config.provider) return { model: ctx.model };
222
223
  if (config.provider && config.model) {
@@ -268,6 +269,7 @@ export function resolveAuditorSessionModelOptions(ctx: ExtensionContext): {
268
269
  return { modelRegistry: ctx.modelRegistry };
269
270
  }
270
271
 
272
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pi SDK types Model<any> idiomatically (pi-coding-agent sdk.d.ts)
271
273
  function modelLabel(model: Model<any> | undefined): string | undefined {
272
274
  return model ? `${model.provider}/${model.id}` : undefined;
273
275
  }
@@ -379,7 +381,7 @@ export async function runGoalCompletionAuditor(args: {
379
381
  }
380
382
  if (event.type === "message_update") {
381
383
  // Check for thinking events from the assistant stream
382
- const streamEvent = (event as any).assistantMessageEvent;
384
+ const streamEvent = (event as { assistantMessageEvent?: { type?: string } }).assistantMessageEvent;
383
385
  if (streamEvent?.type === "thinking_start") {
384
386
  progress.phase = "thinking";
385
387
  if (!progress.label) progress.label = "Analyzing goal...";
@@ -393,7 +395,7 @@ export async function runGoalCompletionAuditor(args: {
393
395
  }
394
396
  // For text content, show producing_report phase
395
397
  progress.phase = "producing_report";
396
- const message = event.message as any;
398
+ const message = event.message as { role?: string; content?: Array<{ type?: string; text?: string }> };
397
399
  if (message?.role === "assistant") {
398
400
  for (const part of message.content ?? []) {
399
401
  if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
@@ -407,7 +409,7 @@ export async function runGoalCompletionAuditor(args: {
407
409
  return;
408
410
  }
409
411
  if (event.type !== "message_end") return;
410
- const message = event.message as any;
412
+ const message = event.message as { role?: string; content?: Array<{ type?: string; text?: string }> };
411
413
  if (message.role !== "assistant") return;
412
414
  for (const part of message.content ?? []) {
413
415
  if (part.type === "text" && typeof part.text === "string") outputParts.push(part.text);
@@ -1,3 +1,5 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
4
  import { extractVerificationContract, sisyphusObjectiveSufficient } from "./goal-contract.ts";
3
5
  import { detailedSummary, oneLineSummary } from "./goal-format.ts";
@@ -15,12 +17,45 @@ import {
15
17
  otherOpenGoalCount,
16
18
  } from "./goal-pool.ts";
17
19
  import { clearGoalCommandMessage, validateResumeGoal } from "./goal-policy.ts";
18
- import { readGoalLedger } from "./goal-ledger.ts";
20
+ import { invalidateGoalLedgerCache, readGoalLedger } from "./goal-ledger.ts";
19
21
  import { buildGoalStatusText } from "./goal-status.ts";
20
- import { effectiveSettingsReport, envOverrideFor } from "./goal-settings.ts";
21
- import { mergeGoalPromptFromDisk } from "./storage/goal-files.ts";
22
+ import { effectiveSettingsReport, envOverrideFor, invalidateGoalSettingsCache } from "./goal-settings.ts";
23
+ import { invalidateGoalPoolCache, mergeGoalPromptFromDisk, readActiveGoalPool } from "./storage/goal-files.ts";
22
24
  import { nowIso, type GoalMode, type GoalRecord } from "./goal-record.ts";
23
25
  import { clearGoalDrafting, hasActiveDraft, startGoalDrafting } from "./goal-drafting.ts";
26
+ import { formatRecoveryReport, runRecoveryReport, runRecoveryRepair } from "./goal-recovery.ts";
27
+
28
+ export interface GoalRefreshState {
29
+ poolIds: Iterable<string>;
30
+ ledgerEvents: number;
31
+ ledgerMalformed: number;
32
+ /** Stable fingerprint of the effective settings (JSON of the parsed file). */
33
+ settings: string;
34
+ }
35
+
36
+ /**
37
+ * Pure diff of a goal-refresh cycle (before invalidation vs after re-read).
38
+ * Unit-testable without the command harness; the command renders the changes.
39
+ */
40
+ export function diffGoalRefreshState(before: GoalRefreshState, after: GoalRefreshState): string[] {
41
+ const changes: string[] = [];
42
+ const beforeIds = new Set(before.poolIds);
43
+ const afterIds = new Set(after.poolIds);
44
+ const added = [...afterIds].filter((id) => !beforeIds.has(id)).sort();
45
+ const removed = [...beforeIds].filter((id) => !afterIds.has(id)).sort();
46
+ if (added.length > 0) changes.push(`pool: ${added.length} goal(s) added — ${added.join(", ")}`);
47
+ if (removed.length > 0) changes.push(`pool: ${removed.length} goal(s) removed — ${removed.join(", ")}`);
48
+ if (after.ledgerEvents !== before.ledgerEvents) {
49
+ changes.push(`ledger: ${before.ledgerEvents} -> ${after.ledgerEvents} events`);
50
+ }
51
+ if (after.ledgerMalformed !== before.ledgerMalformed) {
52
+ changes.push(`ledger: malformed entries ${before.ledgerMalformed} -> ${after.ledgerMalformed}`);
53
+ }
54
+ if (before.settings !== after.settings) {
55
+ changes.push("settings: effective settings changed (external edit)");
56
+ }
57
+ return changes;
58
+ }
24
59
  import {
25
60
  AUDITOR_THINKING_LEVELS,
26
61
  buildAuditorModelChoices,
@@ -155,18 +190,87 @@ export function registerGoalCommands(core: GoalCore): void {
155
190
  core.replaceGoal({ objective, autoContinue: true, sisyphus: mode === "sisyphus" }, ctx, true, verificationContract);
156
191
  }
157
192
 
193
+ async function runGoalRecovery(rawArgs: string, ctx: ExtensionContext): Promise<void> {
194
+ const report = runRecoveryReport({ cwd: ctx.cwd });
195
+ if (/^repair$/i.test(rawArgs)) {
196
+ const result = await runRecoveryRepair({ cwd: ctx.cwd }, report, async () => {
197
+ const confirmed = await ctx.ui.confirm(`Remove ${report.staleLocks.length} stale lock(s) and refresh the pool snapshot?`, `Files are backed up to .pi/goals/.recovery-backup first.`);
198
+ return confirmed === true;
199
+ });
200
+ if (result.confirmed) {
201
+ ctx.ui.notify(result.applied.length > 0
202
+ ? `goal-recovery repair: ${result.applied.length} operation(s) applied.\n${result.applied.map((a) => ` - ${a}`).join("\n")}\nBackup: ${result.backupDir}`
203
+ : "goal-recovery repair: nothing to repair.", "info");
204
+ } else {
205
+ ctx.ui.notify("goal-recovery repair: cancelled — nothing changed.", "info");
206
+ }
207
+ return;
208
+ }
209
+ ctx.ui.notify(formatRecoveryReport(report), report.healthy ? "info" : "warning");
210
+ }
211
+
212
+ async function runGoalRefresh(ctx: ExtensionContext): Promise<void> {
213
+ // Pre-state snapshots are cache-served (0 fs ops); the re-reads after
214
+ // invalidation go cold. This is the explicit user-owned path for picking
215
+ // up external edits — no watchers, no per-turn polling.
216
+ const beforePool = readActiveGoalPool(ctx);
217
+ const beforeLedger = readGoalLedger(ctx);
218
+ // Settings: the before fingerprint is cache-served; the after one is a
219
+ // cold re-read after invalidation — so an external settings edit shows
220
+ // up as a fingerprint difference.
221
+ const beforeSettings = settingsFingerprint(ctx);
222
+
223
+ invalidateGoalPoolCache();
224
+ invalidateGoalLedgerCache();
225
+ invalidateGoalSettingsCache();
226
+
227
+ const afterPool = readActiveGoalPool(ctx);
228
+ const afterLedger = readGoalLedger(ctx);
229
+ const afterSettings = settingsFingerprint(ctx);
230
+
231
+ const changes = diffGoalRefreshState({
232
+ poolIds: beforePool.keys(),
233
+ ledgerEvents: beforeLedger.events.length,
234
+ ledgerMalformed: beforeLedger.malformed,
235
+ settings: beforeSettings,
236
+ }, {
237
+ poolIds: afterPool.keys(),
238
+ ledgerEvents: afterLedger.events.length,
239
+ ledgerMalformed: afterLedger.malformed,
240
+ settings: afterSettings,
241
+ });
242
+
243
+ const text = changes.length > 0
244
+ ? `goal-refresh: re-read caches from disk — ${changes.length} change(s):\n${changes.map((c) => ` - ${c}`).join("\n")}`
245
+ : "goal-refresh: no changes detected — caches were already current.";
246
+ ctx.ui.notify(text, "info");
247
+ core.updateUI(ctx);
248
+ }
249
+
250
+ /** Stable fingerprint of the effective settings (cache-served before invalidation). */
251
+ function settingsFingerprint(ctx: ExtensionContext): string {
252
+ return JSON.stringify(loadGoalSettingsFileConfig(ctx.cwd));
253
+ }
254
+
158
255
  async function showGoalStatus(rawArgs: string, ctx: ExtensionContext): Promise<void> {
159
256
  core.reconcileFocusedGoalFromDisk(ctx);
160
257
  if (core.state.goal) core.syncGoalPromptFromDisk(ctx);
161
258
  const view = core.goalForDisplay() ?? core.state.goal;
162
259
  const otherCount = otherOpenGoalCount(core.goalsById, core.focusedGoalId);
163
260
  const verbose = /^\s*verbose\b/i.test(rawArgs);
261
+ const health = /^\s*health\b/i.test(rawArgs);
262
+ const ledger = readGoalLedger(ctx);
164
263
  const text = buildGoalStatusText({
165
264
  goal: view,
166
265
  focused: view !== null && core.focusedGoalId === view.id,
167
266
  otherOpenGoals: otherCount,
168
- ledgerEvents: readGoalLedger(ctx).events,
267
+ ledgerEvents: ledger.events,
268
+ ledgerMalformed: ledger.malformed,
169
269
  verbose,
270
+ health,
271
+ activeFilePresent: health && view?.activePath
272
+ ? existsSync(path.resolve(ctx.cwd, view.activePath))
273
+ : undefined,
170
274
  // §13.2: effective settings with provenance appear only in verbose mode;
171
275
  // the standard mode stays free of settings noise (§13.1).
172
276
  settingsReport: verbose ? effectiveSettingsReport(ctx.cwd) : [],
@@ -481,8 +585,8 @@ export function registerGoalCommands(core: GoalCore): void {
481
585
  ctx.ui.notify("Provide the replacement objective: /goal-tweak <new objective>", "info");
482
586
  return;
483
587
  }
484
- if (trimmed.length > (loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0)) {
485
- const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
588
+ const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
589
+ if (trimmed.length > (max ?? 0)) {
486
590
  if (max !== undefined && max > 0) {
487
591
  ctx.ui.notify(`Replacement objective exceeds ${max} characters (${trimmed.length}).`, "warning");
488
592
  return;
@@ -533,11 +637,23 @@ export function registerGoalCommands(core: GoalCore): void {
533
637
  },
534
638
  });
535
639
  pi.registerCommand("goal-status", {
536
- description: "Show the unified goal dashboard for the focused goal (read-only). Append \"verbose\" for full diagnostic detail (goal id, revision, objective, task tree with evidence and contracts, ledger history, budget, pause/blocker, paths, audit report, effective settings).",
640
+ description: "Show the unified goal dashboard (read-only). Append \"verbose\" for full detail or \"health\" for storage/runtime checks.",
537
641
  handler: async (rawArgs, ctx) => {
538
642
  await showGoalStatus(rawArgs ?? "", ctx);
539
643
  },
540
644
  });
645
+ pi.registerCommand("goal-refresh", {
646
+ description: "Re-read goal storage caches (pool, ledger, settings) from disk and report what changed. Picks up external edits to .pi files — no file watchers needed.",
647
+ handler: async (_rawArgs, ctx) => {
648
+ await runGoalRefresh(ctx);
649
+ },
650
+ });
651
+ pi.registerCommand("goal-recovery", {
652
+ description: "Read-only storage/recovery report (malformed goal files, malformed ledger lines, stale locks, orphaned snapshot data). Append \"repair\" to remove stale locks and refresh the pool snapshot after confirmation (with backup).",
653
+ handler: async (rawArgs, ctx) => {
654
+ await runGoalRecovery((rawArgs ?? "").trim(), ctx);
655
+ },
656
+ });
541
657
  pi.registerCommand("goal-focus", {
542
658
  description: "Choose which open goal this session focuses on.",
543
659
  handler: async (_rawArgs, ctx) => {
@@ -9,10 +9,15 @@ import {
9
9
  latestEventsForGoal,
10
10
  reconstructGoalLedger,
11
11
  type GoalLedgerEvent,
12
+ type LedgerStateReadResult,
12
13
  } from "./goal-ledger.ts";
13
14
  import { type GoalRecord } from "./goal-record.ts";
14
15
 
15
- export function buildGoalCompactSummary(goal: GoalRecord, events: GoalLedgerEvent[]): string {
16
+ export function buildGoalCompactSummary(
17
+ goal: GoalRecord,
18
+ events: GoalLedgerEvent[],
19
+ auditorOverride?: { verdict: "approved" | "disapproved" | "error"; report: string; at: string },
20
+ ): string {
16
21
  const lines: string[] = [];
17
22
  lines.push(`Goal ${goal.id} — ${statusLabel(goal)}`);
18
23
  lines.push(` Objective: ${truncateText(goal.objective, 200)}`);
@@ -70,7 +75,7 @@ export function buildGoalCompactSummary(goal: GoalRecord, events: GoalLedgerEven
70
75
  }
71
76
  }
72
77
 
73
- const auditor = latestAuditorResultForGoal(events, goal.id);
78
+ const auditor = auditorOverride ?? latestAuditorResultForGoal(events, goal.id);
74
79
  if (auditor && auditor.verdict === "disapproved") {
75
80
  lines.push(` Auditor rejection (latest): ${truncateText(auditor.report, 120)}`);
76
81
  }
@@ -88,20 +93,29 @@ export function buildGoalCompactSummary(goal: GoalRecord, events: GoalLedgerEven
88
93
  export function buildCompactionSummary(args: {
89
94
  goalsById: Map<string, GoalRecord>;
90
95
  focusedGoalId: string | null;
91
- ledgerEvents: GoalLedgerEvent[];
96
+ ledgerEvents?: GoalLedgerEvent[];
97
+ /** Checkpoint-fed view: uses reconstructed state + per-goal recent tails instead of a full event list. */
98
+ ledgerState?: LedgerStateReadResult;
92
99
  capOpenGoals?: number;
93
100
  capEventsPerGoal?: number;
94
101
  }): string {
95
- const { goalsById, focusedGoalId, ledgerEvents, capOpenGoals = 20, capEventsPerGoal = 5 } = args;
102
+ const { goalsById, focusedGoalId, ledgerEvents = [], ledgerState, capOpenGoals = 20, capEventsPerGoal = 5 } = args;
96
103
 
97
104
  const lines: string[] = [];
98
105
  const openGoals = Array.from(goalsById.values()).filter((g) => g.status !== "complete");
99
- const reconstructed = reconstructGoalLedger(ledgerEvents);
106
+ const reconstructed = ledgerState?.state ?? reconstructGoalLedger(ledgerEvents);
107
+ const recentEventsFor = (goalId: string, cap: number): GoalLedgerEvent[] => {
108
+ if (ledgerState) return (ledgerState.recentEventsByGoal.get(goalId) ?? []).slice(0, cap);
109
+ return latestEventsForGoal(ledgerEvents, goalId, cap);
110
+ };
100
111
 
101
112
  if (focusedGoalId && goalsById.has(focusedGoalId)) {
102
113
  const focused = goalsById.get(focusedGoalId)!;
114
+ const auditor = ledgerState
115
+ ? reconstructed.goals.get(focusedGoalId)?.latestAuditorResult ?? reconstructed.terminalGoals.get(focusedGoalId)?.latestAuditorResult
116
+ : undefined;
103
117
  lines.push(`[FOCUSED GOAL]`);
104
- lines.push(buildGoalCompactSummary(focused, latestEventsForGoal(ledgerEvents, focusedGoalId, capEventsPerGoal)));
118
+ lines.push(buildGoalCompactSummary(focused, recentEventsFor(focusedGoalId, capEventsPerGoal), auditor));
105
119
  lines.push("");
106
120
  }
107
121
 
@@ -27,7 +27,7 @@ export function extractVerificationContract(objective: string): { objective: str
27
27
  const trimmed = line.trim();
28
28
  const m = VERIFICATION_CONTRACT_RE.exec(trimmed);
29
29
  if (m) {
30
- contract = m[1].trim();
30
+ contract = m[1]!.trim();
31
31
  // Skip this line — don't add it to the cleaned objective
32
32
  } else {
33
33
  filtered.push(line);
@@ -71,7 +71,7 @@ export function statusLabel(goal: Pick<GoalDisplayRecordLike, "sisyphus" | "stat
71
71
  export function footerStatus(goal: GoalDisplayRecordLike): string {
72
72
  const usageBits: string[] = [];
73
73
  if (goal.usage.activeSeconds > 0) usageBits.push(formatDuration(goal.usage.activeSeconds));
74
- if (goal.usage.tokensUsed > 0) usageBits.push(formatTokenValue(goal.usage.tokensUsed).split(" ")[0]);
74
+ if (goal.usage.tokensUsed > 0) usageBits.push(formatTokenValue(goal.usage.tokensUsed).split(" ")[0]!);
75
75
  const usage = usageBits.length > 0 ? ` [${usageBits.join(" ")}]` : "";
76
76
  const prefix = goal.sisyphus ? "goal✊" : "goal";
77
77
  return `${prefix}: ${statusLabel(goal)}${usage} - ${truncateText(goal.objective, 60)}`;
@@ -276,7 +276,7 @@ export function registerDraftingTools(core: GoalCore): void {
276
276
  }, { additionalProperties: false }),
277
277
  async execute(_id, params, _signal, _update, ctx) {
278
278
  if (!activeDraft(core)) return { content: [{ type: "text", text: "No guided goal draft is active." }], details: goalDetails(core.state.goal) };
279
- const questions: GoalQuestionnaireQuestion[] = params.questions.map((q: any) => ({ ...q, allowCustom: q.allow_custom }));
279
+ const questions: GoalQuestionnaireQuestion[] = params.questions.map((q: GoalQuestionnaireQuestion & { allow_custom?: boolean }) => ({ ...q, allowCustom: q.allow_custom }));
280
280
  core.enterGoalModal();
281
281
  try {
282
282
  const result = await runGoalQuestionnaire(ctx, questions);
@@ -12,7 +12,7 @@ import {
12
12
  isToolUseAssistantMessage,
13
13
  } from "./goal-format.ts";
14
14
  import { buildCompactionSummary } from "./goal-compaction.ts";
15
- import { latestAuditorResultForGoal, readGoalLedger, invalidateGoalLedgerCache } from "./goal-ledger.ts";
15
+ import { latestAuditorResultForGoal, loadLedgerState, readGoalLedger, invalidateGoalLedgerCache } from "./goal-ledger.ts";
16
16
  import { shouldArmPostCompactReminder, shouldInjectPostCompactReminder } from "./goal-policy.ts";
17
17
  import { formatTokenValue } from "./goal-core.ts";
18
18
  import { loadGoalSettings, invalidateGoalSettingsCache } from "./goal-settings.ts";
@@ -300,6 +300,11 @@ export function registerGoalEvents(core: GoalCore): void {
300
300
  core.advanceTurnSeq();
301
301
  const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
302
302
  const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
303
+ // Several prompt enrichments may need the same ledger snapshot. Keep one
304
+ // local read for this hook instead of repeatedly traversing the cached
305
+ // ledger when rejection and post-compaction steering overlap.
306
+ let promptLedger: ReturnType<typeof readGoalLedger> | undefined;
307
+ const getPromptLedger = () => promptLedger ??= readGoalLedger(ctx);
303
308
 
304
309
  // If this turn was triggered by a hidden goal checkpoint that no longer
305
310
  // matches the active goal, abort the whole turn instead of letting the
@@ -353,10 +358,10 @@ export function registerGoalEvents(core: GoalCore): void {
353
358
  pauseExtras.push(`Pause reason: ${current.pauseReason ?? "(unknown)"}`);
354
359
  if (current.pauseSuggestedAction) pauseExtras.push(`Suggested action: ${current.pauseSuggestedAction}`);
355
360
  }
356
- // Inject durable auditor feedback if available
357
- let auditorExtra = "";
358
- try {
359
- const ledger = readGoalLedger(ctx);
361
+ // Inject durable auditor feedback if available
362
+ let auditorExtra = "";
363
+ try {
364
+ const ledger = getPromptLedger();
360
365
  const auditorResult = latestAuditorResultForGoal(ledger.events, current.id);
361
366
  if (auditorResult && auditorResult.verdict === "disapproved") {
362
367
  auditorExtra = `\n\n[AUDITOR REJECTION] An independent auditor previously rejected a completion request for this goal. Reason: ${auditorResult.report.slice(0, 300)}\nAddress the auditor's objections before requesting completion again.`;
@@ -395,7 +400,7 @@ export function registerGoalEvents(core: GoalCore): void {
395
400
  if (stalledNote) prompt += stalledNote;
396
401
  // Inject durable auditor feedback if the latest result was a rejection
397
402
  try {
398
- const ledger = readGoalLedger(ctx);
403
+ const ledger = getPromptLedger();
399
404
  const auditorResult = latestAuditorResultForGoal(ledger.events, activeGoal.id);
400
405
  if (auditorResult && auditorResult.verdict === "disapproved" && ledger.events.some((e) => e.type === "completion_requested" && e.goalId === activeGoal.id)) {
401
406
  prompt = `${prompt}\n\n[AUDITOR REJECTION goalId=${activeGoal.id}]\nAn independent auditor previously rejected a completion request for this goal. Reason: ${auditorResult.report.slice(0, 300)}\nAddress the auditor's objections before requesting completion again.`;
@@ -405,10 +410,10 @@ export function registerGoalEvents(core: GoalCore): void {
405
410
  }
406
411
  if (core.runtime.isPostCompactReminderPending() && shouldInjectPostCompactReminder({ pending: true, goal: activeGoal })) {
407
412
  core.runtime.clearPostCompactReminder();
408
- // Use deterministic compaction summary instead of generic reminder
409
- try {
410
- const ledger = readGoalLedger(ctx);
411
- const compaction = buildCompactionSummary({ goalsById: core.goalsById, focusedGoalId: core.focusedGoalId, ledgerEvents: ledger.events });
413
+ // Use deterministic compaction summary instead of generic reminder
414
+ try {
415
+ const ledger = getPromptLedger();
416
+ const compaction = buildCompactionSummary({ goalsById: core.goalsById, focusedGoalId: core.focusedGoalId, ledgerState: loadLedgerState(ctx) });
412
417
  prompt = `${prompt}\n\n[POST-COMPACTION RESYNC goalId=${activeGoal.id}]\n${compaction}`;
413
418
  } catch {
414
419
  prompt = `${prompt}\n\n[POST-COMPACTION RESYNC goalId=${core.state.goal.id}]\nThe conversation was just compacted. Re-read the objective and continue from the actual artifacts/state; do not rely on memory of the prior chat.`;
@@ -473,4 +478,3 @@ export function registerGoalEvents(core: GoalCore): void {
473
478
  if (core.state.goal) core.persist(ctx);
474
479
  });
475
480
  }
476
-
@@ -181,7 +181,7 @@ export function extractGoalIdFromInjectedMessage(text: string): string | null {
181
181
  // Borrowed from pi-codex-goal. More robust than bare bracket text because
182
182
  // the angle brackets + attributes are nearly impossible for users to type
183
183
  // by accident, and the structure is grep-able / parse-able by external tooling.
184
- const xmlMatch = text.match(/^<pi_goal_continuation\s+goal_id=\"([^\"]+)\"/);
184
+ const xmlMatch = text.match(/^<pi_goal_continuation\s+goal_id="([^"]+)"/);
185
185
  if (xmlMatch) return xmlMatch[1] ?? null;
186
186
  const match = text.match(/^\[(?:GOAL CHECKPOINT|GOAL CONTINUATION|GOAL STALE) goalId=([^\]\s]+)\]/);
187
187
  return match?.[1] ?? null;