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