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 +7 -0
- package/docs/agent-flow-design.md +1 -1
- package/docs/architecture.md +1 -0
- package/extensions/goal-commands.ts +13 -4
- package/extensions/goal-events.ts +13 -9
- package/extensions/goal-service.ts +41 -88
- package/extensions/goal-state.ts +0 -1
- package/extensions/goal-status.ts +100 -0
- package/package.json +1 -1
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 |
|
package/docs/architecture.md
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
485
|
-
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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 =
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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). */
|
package/extensions/goal-state.ts
CHANGED
|
@@ -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.
|
|
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",
|