pi-goal-x 0.26.1 → 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 +9 -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-questionnaire.ts +145 -34
- 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
|
@@ -129,6 +129,8 @@ Start a guided regular goal:
|
|
|
129
129
|
|
|
130
130
|
The agent can ask questions and propose a complete objective and task plan. Confirm the proposal to create the goal and begin work.
|
|
131
131
|
|
|
132
|
+
The questionnaire dialog never truncates the question; when it is taller than the terminal it stays within the height and scrolls — `PgUp`/`PgDn` page and `Ctrl+↑/↓` line-scroll without moving the selection, `↑/↓` selection auto-follows into view, and a `▲`/`… +N more` edge indicator shows what is clipped.
|
|
133
|
+
|
|
132
134
|
Start a guided Sisyphus goal:
|
|
133
135
|
|
|
134
136
|
```text
|
|
@@ -175,6 +177,12 @@ Show the focused goal:
|
|
|
175
177
|
/goal-status
|
|
176
178
|
```
|
|
177
179
|
|
|
180
|
+
Run read-only storage/runtime health checks:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
/goal-status health
|
|
184
|
+
```
|
|
185
|
+
|
|
178
186
|
Select an open goal for the current session:
|
|
179
187
|
|
|
180
188
|
```text
|
|
@@ -339,6 +347,7 @@ Each session can focus on one goal while the project keeps other goals open.
|
|
|
339
347
|
/goal-list List open goals
|
|
340
348
|
/goal-status Show the focused goal (unified dashboard)
|
|
341
349
|
/goal-status verbose Show the focused goal with full diagnostic detail
|
|
350
|
+
/goal-status health Check goal storage/runtime health (read-only)
|
|
342
351
|
/goal-focus Select an open goal
|
|
343
352
|
/goal-unfocus Remove the session’s focus
|
|
344
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
|
-
|
|
@@ -108,54 +108,108 @@ export function findProposalPresentationSegments(lines: string[], tailStart: num
|
|
|
108
108
|
* question. The protected head (top border + tabs + question line) is always
|
|
109
109
|
* kept; the remaining budget is spent on the tail — options/footer/bottom
|
|
110
110
|
* border — so long context blocks (proposal confirmations) are sliced from
|
|
111
|
-
* their head exactly as the pre-fix tail-slice did (383ae52 surface).
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* recommended/first option stays visible; the footer hint and bottom border
|
|
115
|
-
* are always the last rendered lines. Never returns more than maxDialogLines.
|
|
111
|
+
* their head exactly as the pre-fix tail-slice did (383ae52 surface). The
|
|
112
|
+
* footer hint and bottom border are always the last rendered lines. Never
|
|
113
|
+
* returns more than maxDialogLines.
|
|
116
114
|
*
|
|
117
115
|
* Proposal confirmations (proposal segments given) keep the head, the tasks
|
|
118
116
|
* section (header + every task line), and the options/footer/bottom border in
|
|
119
117
|
* frame; only the objective-box middle is sacrificed in-frame — the full
|
|
120
118
|
* objective is always present in the scrollable transcript presentation
|
|
121
119
|
* (propose_goal_draft renderCall), so nothing of the goal is ever omitted.
|
|
120
|
+
*
|
|
121
|
+
* §options-scroll: when a `scroll` state is given (select-mode question tabs
|
|
122
|
+
* and the submit summary), the dialog becomes a `less`-style viewport over the
|
|
123
|
+
* FULL content — nothing is truncated, nothing is sliced away — and the
|
|
124
|
+
* options are always reachable by scrolling (PageUp/PageDown, Ctrl+↑/↓, and
|
|
125
|
+
* ↑/↓ selection auto-follow). See fitDialogViewport.
|
|
122
126
|
*/
|
|
127
|
+
export interface DialogScrollState {
|
|
128
|
+
/** Current viewport offset over the full content lines. */
|
|
129
|
+
scrollTop: number;
|
|
130
|
+
/** When true, the fitter nudges scrollTop so the selection range is visible. */
|
|
131
|
+
needsFollow: boolean;
|
|
132
|
+
/** Line ranges [start, end] of each selectable option in the full content. */
|
|
133
|
+
optionRanges: Array<[number, number]>;
|
|
134
|
+
/** Index of the current selection in optionRanges. */
|
|
135
|
+
followIndex: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
export function fitDialogLines(
|
|
124
139
|
lines: string[],
|
|
125
140
|
maxDialogLines: number,
|
|
126
141
|
protectedHead: number,
|
|
127
|
-
optionsImmediatelyAfterHead = false,
|
|
128
142
|
proposal: ProposalPresentationSegments | null = null,
|
|
143
|
+
scroll: DialogScrollState | null = null,
|
|
144
|
+
dimStyle: (s: string) => string = (s) => s,
|
|
129
145
|
): string[] {
|
|
130
146
|
if (maxDialogLines <= 0 || lines.length <= maxDialogLines) return lines;
|
|
131
147
|
const keepHead = Math.min(protectedHead, maxDialogLines);
|
|
148
|
+
if (proposal) {
|
|
149
|
+
return fitProposalPresentation(lines, maxDialogLines, keepHead, proposal);
|
|
150
|
+
}
|
|
151
|
+
if (scroll) {
|
|
152
|
+
return fitDialogViewport(lines, maxDialogLines, scroll, dimStyle);
|
|
153
|
+
}
|
|
154
|
+
// Context-heavy / input / submit dialogs without a scroll state: keep the
|
|
155
|
+
// head and the tail (options/footer/bottom border) exactly as the pre-fix
|
|
156
|
+
// tail-slice did.
|
|
132
157
|
const budget = maxDialogLines - keepHead;
|
|
133
158
|
if (budget <= 0) return lines.slice(0, keepHead);
|
|
134
159
|
const rest = lines.slice(protectedHead);
|
|
135
160
|
if (rest.length <= budget) return [...lines.slice(0, keepHead), ...rest];
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
return [...lines.slice(0, keepHead), ...rest.slice(rest.length - budget)];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* §options-scroll viewport fit: never truncate or drop content — window the
|
|
166
|
+
* full dialog lines with a scrollTop offset so every line (question, context,
|
|
167
|
+
* options, footer, border) stays reachable. The last viewport row is reserved
|
|
168
|
+
* as the bottom edge: the bottom border when the end is reached, otherwise a
|
|
169
|
+
* themed `… +N more · PgUp/PgDn scroll` indicator (mirrors the dashboard's
|
|
170
|
+
* `… +N more task` rows). A themed `▲ N more` indicator replaces the first
|
|
171
|
+
* content row when scrolled down. Selection auto-follow (needsFollow) nudges
|
|
172
|
+
* the window so the selected option's line range is inside it. Output length
|
|
173
|
+
* is exactly maxDialogLines when clipped, never more (churn-guard invariant).
|
|
174
|
+
*/
|
|
175
|
+
function fitDialogViewport(
|
|
176
|
+
lines: string[],
|
|
177
|
+
maxDialogLines: number,
|
|
178
|
+
scroll: DialogScrollState,
|
|
179
|
+
dimStyle: (s: string) => string,
|
|
180
|
+
): string[] {
|
|
181
|
+
const contentWindow = Math.max(0, maxDialogLines - 1);
|
|
182
|
+
const contentEnd = Math.max(0, lines.length - 1);
|
|
183
|
+
// Selection auto-follow: nudge scrollTop so the selected option's range is
|
|
184
|
+
// inside the content window (minimal scroll; clamp below).
|
|
185
|
+
if (scroll.needsFollow && scroll.optionRanges[scroll.followIndex]) {
|
|
186
|
+
const [os, oe] = scroll.optionRanges[scroll.followIndex];
|
|
187
|
+
if (os < scroll.scrollTop || oe >= scroll.scrollTop + contentWindow) {
|
|
188
|
+
scroll.scrollTop = oe >= scroll.scrollTop + contentWindow
|
|
189
|
+
? Math.min(os, Math.max(0, oe - contentWindow + 1))
|
|
190
|
+
: os;
|
|
191
|
+
}
|
|
192
|
+
scroll.needsFollow = false;
|
|
138
193
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const lead = firstContent > 0 ? rest.slice(0, firstContent) : [];
|
|
147
|
-
const contentStart = firstContent > 0 ? firstContent : 0;
|
|
148
|
-
const tailBudget = Math.min(2, budget); // footer hint + bottom border
|
|
149
|
-
const tail = rest.slice(-tailBudget);
|
|
150
|
-
const room = budget - tailBudget;
|
|
151
|
-
const keepContent = Math.max(0, Math.min(rest.length - contentStart, room));
|
|
152
|
-
const keepLead = Math.max(0, Math.min(lead.length, room - keepContent));
|
|
153
|
-
const content = rest.slice(contentStart, contentStart + keepContent);
|
|
154
|
-
return [...headLines, ...lead.slice(0, keepLead), ...content, ...tail];
|
|
194
|
+
scroll.scrollTop = Math.max(0, Math.min(scroll.scrollTop, Math.max(0, contentEnd - contentWindow)));
|
|
195
|
+
const s = scroll.scrollTop;
|
|
196
|
+
const hiddenAbove = s;
|
|
197
|
+
const hiddenBelow = contentEnd - (s + contentWindow);
|
|
198
|
+
const viewport = lines.slice(s, s + contentWindow);
|
|
199
|
+
if (hiddenAbove > 0 && viewport.length > 0) {
|
|
200
|
+
viewport[0] = dimStyle(`▲ ${hiddenAbove} more`);
|
|
155
201
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
if (hiddenBelow > 0) {
|
|
203
|
+
// The reserved bottom edge row advertises the scroll affordance — it
|
|
204
|
+
// replaces the border while content is clipped below (mirrors the
|
|
205
|
+
// dashboard's `… +N more task` rows) and gives way to the border at the
|
|
206
|
+
// end. The footer hint sits directly above the border, so it is only
|
|
207
|
+
// visible when the end is reached (never while clipped — geometry).
|
|
208
|
+
viewport.push(dimStyle(`… +${hiddenBelow} more · PgUp/PgDn scroll`));
|
|
209
|
+
} else {
|
|
210
|
+
viewport.push(lines[lines.length - 1]);
|
|
211
|
+
}
|
|
212
|
+
return viewport;
|
|
159
213
|
}
|
|
160
214
|
|
|
161
215
|
/**
|
|
@@ -291,6 +345,10 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
291
345
|
let inputQuestionId: string | null = null;
|
|
292
346
|
let cachedLines: string[] | undefined;
|
|
293
347
|
let optionsStartIndex = -1;
|
|
348
|
+
// §options-scroll viewport state (select-mode + submit tabs only).
|
|
349
|
+
let scrollTop = 0;
|
|
350
|
+
let needsFollow = false;
|
|
351
|
+
let optionRanges: Array<[number, number]> = [];
|
|
294
352
|
let auditorEnabled = auditorToggleInit?.defaultEnabled ?? true;
|
|
295
353
|
const answers = new Map<string, GoalQuestionnaireAnswer>();
|
|
296
354
|
const drafts = new Map<string, string>();
|
|
@@ -340,6 +398,9 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
340
398
|
function enterQuestion(q: GoalQuestionnaireQuestion) {
|
|
341
399
|
const existing = answers.get(q.id);
|
|
342
400
|
const draft = drafts.get(q.id);
|
|
401
|
+
// §options-scroll: start each question tab from the top.
|
|
402
|
+
scrollTop = 0;
|
|
403
|
+
needsFollow = false;
|
|
343
404
|
if (q.options.length === 0) {
|
|
344
405
|
inputMode = true;
|
|
345
406
|
inputQuestionId = q.id;
|
|
@@ -354,7 +415,14 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
354
415
|
}
|
|
355
416
|
}
|
|
356
417
|
|
|
357
|
-
function
|
|
418
|
+
function enterSubmitTab() {
|
|
419
|
+
// §options-scroll: the submit summary also opens from the top.
|
|
420
|
+
optionIndex = 0;
|
|
421
|
+
scrollTop = 0;
|
|
422
|
+
needsFollow = false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function advanceAfterAnswer() {
|
|
358
426
|
if (!isMulti) {
|
|
359
427
|
submit(false);
|
|
360
428
|
return;
|
|
@@ -363,7 +431,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
363
431
|
else currentTab = questions.length;
|
|
364
432
|
const nextQ = currentQuestion();
|
|
365
433
|
if (nextQ) enterQuestion(nextQ);
|
|
366
|
-
else
|
|
434
|
+
else enterSubmitTab();
|
|
367
435
|
refresh();
|
|
368
436
|
}
|
|
369
437
|
|
|
@@ -416,7 +484,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
416
484
|
currentTab = matchesKey(data, Key.tab) ? (currentTab + 1) % totalTabs : (currentTab - 1 + totalTabs) % totalTabs;
|
|
417
485
|
const nextQ = currentQuestion();
|
|
418
486
|
if (nextQ) enterQuestion(nextQ);
|
|
419
|
-
else
|
|
487
|
+
else enterSubmitTab();
|
|
420
488
|
refresh();
|
|
421
489
|
return;
|
|
422
490
|
}
|
|
@@ -433,7 +501,7 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
433
501
|
currentTab = (currentTab + 1) % totalTabs;
|
|
434
502
|
const nextQ = currentQuestion();
|
|
435
503
|
if (nextQ) enterQuestion(nextQ);
|
|
436
|
-
else
|
|
504
|
+
else enterSubmitTab();
|
|
437
505
|
refresh();
|
|
438
506
|
return;
|
|
439
507
|
}
|
|
@@ -441,12 +509,41 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
441
509
|
currentTab = (currentTab - 1 + totalTabs) % totalTabs;
|
|
442
510
|
const nextQ = currentQuestion();
|
|
443
511
|
if (nextQ) enterQuestion(nextQ);
|
|
444
|
-
else
|
|
512
|
+
else enterSubmitTab();
|
|
445
513
|
refresh();
|
|
446
514
|
return;
|
|
447
515
|
}
|
|
448
516
|
}
|
|
449
517
|
|
|
518
|
+
// §options-scroll viewport keys (select-mode question tabs AND the
|
|
519
|
+
// submit summary): page and line scroll without moving the selection.
|
|
520
|
+
// Handled before the submit-tab early return so the summary scrolls
|
|
521
|
+
// too. ↑/↓ still select and auto-follow into view on the next render.
|
|
522
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
523
|
+
scrollTop -= Math.max(1, (maxDialogLines ?? 10) - 1);
|
|
524
|
+
needsFollow = false;
|
|
525
|
+
refresh();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
529
|
+
scrollTop += Math.max(1, (maxDialogLines ?? 10) - 1);
|
|
530
|
+
needsFollow = false;
|
|
531
|
+
refresh();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (matchesKey(data, Key.ctrl("up"))) {
|
|
535
|
+
scrollTop -= 1;
|
|
536
|
+
needsFollow = false;
|
|
537
|
+
refresh();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (matchesKey(data, Key.ctrl("down"))) {
|
|
541
|
+
scrollTop += 1;
|
|
542
|
+
needsFollow = false;
|
|
543
|
+
refresh();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
450
547
|
if (currentTab === questions.length) {
|
|
451
548
|
if (matchesKey(data, Key.enter) && allAnswered()) submit(false);
|
|
452
549
|
else if (matchesKey(data, Key.escape)) submit(true);
|
|
@@ -455,11 +552,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
455
552
|
|
|
456
553
|
if (matchesKey(data, Key.up)) {
|
|
457
554
|
optionIndex = Math.max(0, optionIndex - 1);
|
|
555
|
+
needsFollow = true; // §options-scroll: keep the selection visible
|
|
458
556
|
refresh();
|
|
459
557
|
return;
|
|
460
558
|
}
|
|
461
559
|
if (matchesKey(data, Key.down)) {
|
|
462
560
|
optionIndex = Math.min(opts.length - 1, optionIndex + 1);
|
|
561
|
+
needsFollow = true;
|
|
463
562
|
refresh();
|
|
464
563
|
return;
|
|
465
564
|
}
|
|
@@ -633,11 +732,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
633
732
|
function renderOptions() {
|
|
634
733
|
optionsStartIndex = lines.length;
|
|
635
734
|
for (let i = 0; i < opts.length; i++) {
|
|
735
|
+
const start = lines.length;
|
|
636
736
|
const opt = opts[i];
|
|
637
737
|
const selected = i === optionIndex;
|
|
638
738
|
const prefix = selected ? theme.fg("accent", "> ") : " ";
|
|
639
739
|
const recTag = !opt.isCustom && q?.recommended === i ? theme.fg("success", " ★") : "";
|
|
640
740
|
addWrapped(prefix + theme.fg(selected ? "accent" : "text", `${i + 1}. ${opt.label}`) + recTag);
|
|
741
|
+
optionRanges[i] = [start, lines.length - 1];
|
|
641
742
|
}
|
|
642
743
|
}
|
|
643
744
|
|
|
@@ -705,11 +806,21 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
705
806
|
// objective-box middle is sacrificed there because the full objective
|
|
706
807
|
// is always in the scrollable transcript presentation (renderCall).
|
|
707
808
|
if (maxDialogLines !== undefined && lines.length > maxDialogLines) {
|
|
708
|
-
const optionsImmediatelyAfterHead = !inputMode && currentTab !== questions.length && !!q && !q.context && opts.length > 0;
|
|
709
809
|
const proposalSegments = !inputMode && currentTab !== questions.length && !!q && q.context
|
|
710
810
|
? findProposalPresentationSegments(lines, optionsStartIndex)
|
|
711
811
|
: null;
|
|
712
|
-
|
|
812
|
+
// §options-scroll: select-mode question tabs and the submit summary
|
|
813
|
+
// are a viewport over the full content (never truncated); input
|
|
814
|
+
// mode keeps the legacy tail-keep so the editor stays the priority.
|
|
815
|
+
let scrollState: DialogScrollState | null = null;
|
|
816
|
+
if (!inputMode) {
|
|
817
|
+
scrollState = { scrollTop, needsFollow, optionRanges, followIndex: optionIndex };
|
|
818
|
+
}
|
|
819
|
+
lines = fitDialogLines(lines, maxDialogLines, protectedCount, proposalSegments, scrollState, (s) => theme.fg("dim", s));
|
|
820
|
+
if (scrollState) {
|
|
821
|
+
scrollTop = scrollState.scrollTop;
|
|
822
|
+
needsFollow = scrollState.needsFollow;
|
|
823
|
+
}
|
|
713
824
|
}
|
|
714
825
|
cachedLines = lines;
|
|
715
826
|
return lines;
|
|
@@ -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",
|