pi-goal-x 0.26.2 → 0.26.3

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,12 @@ 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
+
180
186
  Select an open goal for the current session:
181
187
 
182
188
  ```text
@@ -341,6 +347,7 @@ Each session can focus on one goal while the project keeps other goals open.
341
347
  /goal-list List open goals
342
348
  /goal-status Show the focused goal (unified dashboard)
343
349
  /goal-status verbose Show the focused goal with full diagnostic detail
350
+ /goal-status health Check goal storage/runtime health (read-only)
344
351
  /goal-focus Select an open goal
345
352
  /goal-unfocus Remove the session’s focus
346
353
  /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.
@@ -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";
@@ -161,12 +163,19 @@ export function registerGoalCommands(core: GoalCore): void {
161
163
  const view = core.goalForDisplay() ?? core.state.goal;
162
164
  const otherCount = otherOpenGoalCount(core.goalsById, core.focusedGoalId);
163
165
  const verbose = /^\s*verbose\b/i.test(rawArgs);
166
+ const health = /^\s*health\b/i.test(rawArgs);
167
+ const ledger = readGoalLedger(ctx);
164
168
  const text = buildGoalStatusText({
165
169
  goal: view,
166
170
  focused: view !== null && core.focusedGoalId === view.id,
167
171
  otherOpenGoals: otherCount,
168
- ledgerEvents: readGoalLedger(ctx).events,
172
+ ledgerEvents: ledger.events,
173
+ ledgerMalformed: ledger.malformed,
169
174
  verbose,
175
+ health,
176
+ activeFilePresent: health && view?.activePath
177
+ ? existsSync(path.resolve(ctx.cwd, view.activePath))
178
+ : undefined,
170
179
  // §13.2: effective settings with provenance appear only in verbose mode;
171
180
  // the standard mode stays free of settings noise (§13.1).
172
181
  settingsReport: verbose ? effectiveSettingsReport(ctx.cwd) : [],
@@ -481,8 +490,8 @@ export function registerGoalCommands(core: GoalCore): void {
481
490
  ctx.ui.notify("Provide the replacement objective: /goal-tweak <new objective>", "info");
482
491
  return;
483
492
  }
484
- if (trimmed.length > (loadGoalSettings(ctx.cwd).objectiveMaxChars ?? 0)) {
485
- const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
493
+ const max = loadGoalSettings(ctx.cwd).objectiveMaxChars;
494
+ if (trimmed.length > (max ?? 0)) {
486
495
  if (max !== undefined && max > 0) {
487
496
  ctx.ui.notify(`Replacement objective exceeds ${max} characters (${trimmed.length}).`, "warning");
488
497
  return;
@@ -533,7 +542,7 @@ export function registerGoalCommands(core: GoalCore): void {
533
542
  },
534
543
  });
535
544
  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).",
545
+ description: "Show the unified goal dashboard (read-only). Append \"verbose\" for full detail or \"health\" for storage/runtime checks.",
537
546
  handler: async (rawArgs, ctx) => {
538
547
  await showGoalStatus(rawArgs ?? "", ctx);
539
548
  },
@@ -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,9 +410,9 @@ 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);
413
+ // Use deterministic compaction summary instead of generic reminder
414
+ try {
415
+ const ledger = getPromptLedger();
411
416
  const compaction = buildCompactionSummary({ goalsById: core.goalsById, focusedGoalId: core.focusedGoalId, ledgerEvents: ledger.events });
412
417
  prompt = `${prompt}\n\n[POST-COMPACTION RESYNC goalId=${activeGoal.id}]\n${compaction}`;
413
418
  } catch {
@@ -473,4 +478,3 @@ export function registerGoalEvents(core: GoalCore): void {
473
478
  if (core.state.goal) core.persist(ctx);
474
479
  });
475
480
  }
476
-
@@ -1,5 +1,5 @@
1
1
  import { cloneGoal, nowIso, type GoalFocusReason, type GoalRecord, type GoalTask, type GoalUsage } from "./goal-record.ts";
2
- import { appendGoalEvent, type GoalLedgerEvent , appendGoalEvents } from "./goal-ledger.ts";
2
+ import { appendGoalEvent, appendGoalEvents, type GoalLedgerEvent } from "./goal-ledger.ts";
3
3
  import { findTaskInTree, updateTaskInTree } from "./goal-policy.ts";
4
4
  import {
5
5
  GOALS_DIR,
@@ -175,7 +175,12 @@ export class GoalService {
175
175
 
176
176
  /** Open (or reopen) the per-turn transaction for the focused goal. */
177
177
  beginTurn(ctx: GoalServiceContext, goalId: string | null): void {
178
- if (this.turn.active) this.flushTurn(ctx);
178
+ if (this.turn.active) {
179
+ this.flushTurn(ctx);
180
+ // Lock contention must not discard the previous transaction. Keep it
181
+ // open so the next turn boundary retries the same buffered write.
182
+ if (this.turn.active) return;
183
+ }
179
184
  this.turn = { active: true, goalId, goal: null, archive: false, ledger: [] };
180
185
  }
181
186
 
@@ -186,16 +191,20 @@ export class GoalService {
186
191
  */
187
192
  flushTurn(ctx: GoalServiceContext): GoalRecord | null {
188
193
  if (!this.turn.active) return null;
189
- this.turn.active = false;
190
194
  const goal = this.turn.goal;
191
- if (!goal) return null;
195
+ if (!goal) {
196
+ this.turn.active = false;
197
+ return null;
198
+ }
192
199
  let lock: GoalLock;
193
200
  try {
194
201
  lock = acquireGoalLock(ctx, goal.id);
195
202
  } catch {
196
- // Another writer holds the lock; skip this flush (same policy as persist).
203
+ // Another writer holds the lock; preserve the buffer so a later turn
204
+ // boundary can retry it instead of silently losing the mutation.
197
205
  return null;
198
206
  }
207
+ this.turn.active = false;
199
208
  try {
200
209
  const freshDisk = this.readFreshDiskGoal(ctx, goal);
201
210
  const base = freshDisk ?? goal;
@@ -203,25 +212,7 @@ export class GoalService {
203
212
  const written = this.turn.archive || next.status === "complete"
204
213
  ? archiveGoalFile(ctx, next)
205
214
  : writeActiveGoalFile(ctx, next);
206
- if (this.turn.ledger.length > 0) {
207
- const batch = appendGoalEvents(ctx, this.turn.ledger);
208
- if (!batch.ok) {
209
- // Fall back to per-event appends so each failure still surfaces
210
- // through the diagnostic hook with its original granularity.
211
- for (const event of this.turn.ledger) {
212
- const append = appendGoalEvent(ctx, event);
213
- if (!append.ok) {
214
- this.ref.onDiagnostic({
215
- severity: "warning",
216
- source: "ledger",
217
- goalId: "goalId" in event ? event.goalId : undefined,
218
- eventType: event.type,
219
- message: `Ledger append failed for ${event.type}${("goalId" in event) ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
220
- });
221
- }
222
- }
223
- }
224
- }
215
+ this.appendLedgerEventsBestEffort(ctx, this.turn.ledger);
225
216
  this.trackBaseline(written.id, written.usage);
226
217
  this.ref.setFocused(written);
227
218
  return written;
@@ -264,6 +255,28 @@ export class GoalService {
264
255
  };
265
256
  }
266
257
 
258
+ /**
259
+ * Append ledger events without making them part of the authoritative state
260
+ * transaction. A batch is the common path; individual retries preserve the
261
+ * existing best-effort behavior and keep failures observable per event.
262
+ */
263
+ private appendLedgerEventsBestEffort(ctx: GoalServiceContext, events: GoalLedgerEvent[]): void {
264
+ if (events.length === 0) return;
265
+ if (events.length > 1 && appendGoalEvents(ctx, events).ok) return;
266
+ for (const event of events) {
267
+ const append = appendGoalEvent(ctx, event);
268
+ if (!append.ok) {
269
+ this.ref.onDiagnostic({
270
+ severity: "warning",
271
+ source: "ledger",
272
+ goalId: "goalId" in event ? event.goalId : undefined,
273
+ eventType: event.type,
274
+ message: `Ledger append failed for ${event.type}${"goalId" in event ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
275
+ });
276
+ }
277
+ }
278
+ }
279
+
267
280
  /** Safe focused record reconciliation from disk. */
268
281
  reconcileFocused(ctx: GoalServiceContext, opts: { preserveMemoryUsage?: boolean } = {}): boolean {
269
282
  const current = this.ref.getFocused();
@@ -426,20 +439,7 @@ export class GoalService {
426
439
  } catch {
427
440
  events = [];
428
441
  }
429
- for (const event of events) {
430
- const append = appendGoalEvent(ctx, event);
431
- if (!append.ok) {
432
- // Ledger append failure after the authoritative write keeps the
433
- // successful state transition; surface an observable diagnostic.
434
- this.ref.onDiagnostic({
435
- severity: "warning",
436
- source: "ledger",
437
- goalId: "goalId" in event ? event.goalId : undefined,
438
- eventType: event.type,
439
- message: `Ledger append failed for ${event.type}${("goalId" in event) ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
440
- });
441
- }
442
- }
442
+ this.appendLedgerEventsBestEffort(ctx, events);
443
443
  }
444
444
 
445
445
  // 6. in-memory pool/focus commit.
@@ -594,18 +594,7 @@ export class GoalService {
594
594
  const written = writeActiveGoalFile(ctx, mutated);
595
595
  if (spec.ledger) {
596
596
  try {
597
- for (const event of spec.ledger(written, updatedTask)) {
598
- const append = appendGoalEvent(ctx, event);
599
- if (!append.ok) {
600
- this.ref.onDiagnostic({
601
- severity: "warning",
602
- source: "ledger",
603
- goalId: "goalId" in event ? event.goalId : undefined,
604
- eventType: event.type,
605
- message: `Ledger append failed for ${event.type}${("goalId" in event) ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
606
- });
607
- }
608
- }
597
+ this.appendLedgerEventsBestEffort(ctx, spec.ledger(written, updatedTask));
609
598
  } catch (err) {
610
599
  // Unexpected ledger-spec error after the authoritative write keeps
611
600
  // the successful state transition.
@@ -689,25 +678,7 @@ export class GoalService {
689
678
  const previousGoalId = this.ref.getFocused()?.id ?? null;
690
679
  const written = writeActiveGoalFile(ctx, sanitizeGoalPaths(ctx, spec.goal));
691
680
  if (spec.ledger && spec.ledger.length > 0) {
692
- // Batch append (P1-8): one write+append for the whole event block.
693
- const batch = appendGoalEvents(ctx, spec.ledger);
694
- if (!batch.ok) {
695
- // Fall back to per-event appends so each failure still surfaces
696
- // through the diagnostic hook with its original granularity.
697
- for (const event of spec.ledger) {
698
- const append = appendGoalEvent(ctx, event);
699
- if (!append.ok) {
700
- // Best-effort ledger; creation still succeeds.
701
- this.ref.onDiagnostic({
702
- severity: "warning",
703
- source: "ledger",
704
- goalId: "goalId" in event ? event.goalId : undefined,
705
- eventType: event.type,
706
- message: `Ledger append failed for ${event.type}${"goalId" in event ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
707
- });
708
- }
709
- }
710
- }
681
+ this.appendLedgerEventsBestEffort(ctx, spec.ledger);
711
682
  }
712
683
  this.ref.setFocused(written);
713
684
  this.trackBaseline(written.id, written.usage);
@@ -723,25 +694,7 @@ export class GoalService {
723
694
  this.turn.ledger.push(...events);
724
695
  return;
725
696
  }
726
- if (events.length > 1) {
727
- // Batch append (P1-8): one write+append for the whole event block.
728
- const batch = appendGoalEvents(ctx, events);
729
- if (batch.ok) return;
730
- }
731
- for (const event of events) {
732
- const append = appendGoalEvent(ctx, event);
733
- if (!append.ok) {
734
- // Ledger append failure must not crash the surrounding flow, but it
735
- // stays observable through the diagnostic hook.
736
- this.ref.onDiagnostic({
737
- severity: "warning",
738
- source: "ledger",
739
- goalId: "goalId" in event ? event.goalId : undefined,
740
- eventType: event.type,
741
- message: `Ledger append failed for ${event.type}${"goalId" in event ? ` (goal ${event.goalId})` : ""}: ${String(append.error)}`,
742
- });
743
- }
744
- }
697
+ this.appendLedgerEventsBestEffort(ctx, events);
745
698
  }
746
699
 
747
700
  /** Diagnostic write for the debug widget toggle (separate debug dir; not a goal mutation). */
@@ -508,7 +508,6 @@ export function createGoalCore(
508
508
  // goal budget_limited exactly once (status no longer active, so accounting
509
509
  // stops and the transition cannot re-fire), emit the ledger event, arm the
510
510
  // one-time wrap-up steering, and cancel pending continuations.
511
- const budgetGoal2 = state.goal;
512
511
  if (budgetGoal && budgetGoal.status === "active" && typeof budgetGoal.tokenBudget === "number" && budgetReached(budgetGoal)) {
513
512
  const transition = goalService.apply(ctx, {
514
513
  reconcile: false,
@@ -34,7 +34,11 @@ export interface GoalStatusTextOptions {
34
34
  focused: boolean;
35
35
  otherOpenGoals: number;
36
36
  ledgerEvents?: GoalLedgerEvent[];
37
+ ledgerMalformed?: number;
37
38
  verbose?: boolean;
39
+ health?: boolean;
40
+ /** Supplied by the command layer only for the explicit health check. */
41
+ activeFilePresent?: boolean;
38
42
  /** Effective settings lines with provenance (verbose only). */
39
43
  settingsReport?: string[];
40
44
  width?: number;
@@ -55,6 +59,7 @@ export interface GoalStatusTextOptions {
55
59
  */
56
60
  export function buildGoalStatusText(options: GoalStatusTextOptions): string {
57
61
  const width = options.width ?? GOAL_STATUS_WIDTH;
62
+ if (options.health) return buildHealthStatus(options, width);
58
63
  if (!options.goal) {
59
64
  return options.otherOpenGoals > 0
60
65
  ? renderUnfocusedDashboard(options.otherOpenGoals, PLAIN_THEME, width).join("\n")
@@ -87,6 +92,101 @@ export function buildGoalStatusText(options: GoalStatusTextOptions): string {
87
92
  return parts.join("\n\n");
88
93
  }
89
94
 
95
+ interface HealthCheck {
96
+ label: string;
97
+ value: string;
98
+ severity: "ok" | "warn" | "error";
99
+ }
100
+
101
+ /**
102
+ * Render a concise, read-only integrity report. This deliberately checks
103
+ * storage/runtime coherence only; it does not infer that the work itself is
104
+ * complete from task counts or contracts.
105
+ */
106
+ function buildHealthStatus(options: GoalStatusTextOptions, width: number): string {
107
+ const goal = options.goal;
108
+ if (!goal) {
109
+ const focus = options.otherOpenGoals > 0
110
+ ? `WARN — no goal is focused; ${options.otherOpenGoals} open goal${options.otherOpenGoals === 1 ? "" : "s"} remain.`
111
+ : "OK — no goal is focused and there are no open goals.";
112
+ return ["Goal health: " + (options.otherOpenGoals > 0 ? "WARN" : "OK"), `Focus: ${focus}`, "Run /goal-status for the normal dashboard."].join("\n");
113
+ }
114
+
115
+ const checks: HealthCheck[] = [];
116
+ checks.push({
117
+ label: "Focus",
118
+ value: options.focused ? "focused" : "not focused",
119
+ severity: options.focused ? "ok" : "warn",
120
+ });
121
+ checks.push({
122
+ label: "Lifecycle",
123
+ value: `${statusLabelForHealth(goal)}${goal.autoContinue ? " · auto-continue on" : " · auto-continue off"}`,
124
+ severity: goal.status === "active" || goal.status === "paused" || goal.status === "blocked" || goal.status === "budget_limited" || goal.status === "complete" ? "ok" : "error",
125
+ });
126
+
127
+ if (goal.status !== "complete") {
128
+ const fileSeverity = options.activeFilePresent === false ? "error" : options.activeFilePresent === true ? "ok" : "warn";
129
+ checks.push({
130
+ label: "Goal file",
131
+ value: goal.activePath ? (options.activeFilePresent === false ? `missing · ${goal.activePath}` : goal.activePath) : "missing active path",
132
+ severity: fileSeverity,
133
+ });
134
+ }
135
+
136
+ const malformed = options.ledgerMalformed ?? 0;
137
+ checks.push({
138
+ label: "Ledger",
139
+ value: malformed > 0 ? `${malformed} malformed entr${malformed === 1 ? "y" : "ies"}` : "valid",
140
+ severity: malformed > 0 ? "warn" : "ok",
141
+ });
142
+
143
+ if (goal.taskList) {
144
+ const tasks = flattenHealthTasks(goal.taskList.tasks);
145
+ const completed = tasks.filter((task) => task.status === "complete" || task.status === "skipped").length;
146
+ const pending = tasks.length - completed;
147
+ const contractedPending = tasks.filter((task) => task.status === "pending" && Boolean(task.verificationContract?.trim())).length;
148
+ checks.push({
149
+ label: "Tasks",
150
+ value: `${completed}/${tasks.length} terminal · ${pending} pending${contractedPending > 0 ? ` · ${contractedPending} contracted` : ""}`,
151
+ severity: goal.taskList.blockCompletion && pending > 0 ? "warn" : "ok",
152
+ });
153
+ }
154
+
155
+ if (typeof goal.tokenBudget === "number" && goal.tokenBudget > 0) {
156
+ const percentage = Math.round((goal.usage.tokensUsed / goal.tokenBudget) * 100);
157
+ checks.push({
158
+ label: "Budget",
159
+ value: `${formatCompactTokensForHealth(goal.usage.tokensUsed)} / ${formatCompactTokensForHealth(goal.tokenBudget)} (${Math.max(0, percentage)}%)`,
160
+ severity: goal.status === "budget_limited" || percentage >= 100 ? "warn" : percentage >= 90 ? "warn" : "ok",
161
+ });
162
+ }
163
+
164
+ const overall = checks.some((check) => check.severity === "error") ? "ERROR" : checks.some((check) => check.severity === "warn") ? "WARN" : "OK";
165
+ const lines = [`Goal health: ${overall}`, `Goal: ${truncateText(goal.objective, Math.max(20, width - 12))}`];
166
+ for (const check of checks) lines.push(`${check.severity === "error" ? "ERROR" : check.severity === "warn" ? "WARN" : "OK"} ${check.label}: ${check.value}`);
167
+ lines.push("", "This is a storage/runtime health check, not a completion verdict.");
168
+ return lines.join("\n");
169
+ }
170
+
171
+ function flattenHealthTasks(tasks: GoalTask[]): GoalTask[] {
172
+ const result: GoalTask[] = [];
173
+ for (const task of tasks) {
174
+ result.push(task);
175
+ if (task.subtasks) result.push(...flattenHealthTasks(task.subtasks));
176
+ }
177
+ return result;
178
+ }
179
+
180
+ function statusLabelForHealth(goal: GoalRecord): string {
181
+ return goal.status === "budget_limited" ? "budget limited" : goal.status;
182
+ }
183
+
184
+ function formatCompactTokensForHealth(value: number): string {
185
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
186
+ if (value >= 1_000) return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
187
+ return String(Math.max(0, Math.floor(value)));
188
+ }
189
+
90
190
  function lastAuditBlock(
91
191
  audit: { verdict: "approved" | "disapproved" | "error"; report: string; at: string },
92
192
  width: number,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.26.2",
3
+ "version": "0.26.3",
4
4
  "description": "Goal mode extension for pi: durable long-running objectives, five model tools, structured tasks, independent completion audit, Sisyphus mode, auto-continue, and a status overlay.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",