pi-goal-x 0.27.4 → 0.28.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 +43 -2
- package/extensions/goal-commands.ts +188 -102
- package/extensions/goal-events.ts +57 -36
- package/extensions/goal-format.ts +22 -1
- package/extensions/goal-record.ts +35 -1
- package/extensions/goal-runtime.ts +30 -16
- package/extensions/goal-session-health.ts +132 -0
- package/extensions/goal-settings.ts +828 -204
- package/extensions/goal-state.ts +7 -0
- package/extensions/goal-status.ts +8 -0
- package/extensions/prompts/goal-prompts.ts +52 -34
- package/package.json +7 -1
- package/scripts/recover-session-checkpoints.mjs +289 -0
package/README.md
CHANGED
|
@@ -343,6 +343,38 @@ Completed and cleared goals are stored in:
|
|
|
343
343
|
|
|
344
344
|
Each session can focus on one goal while the project keeps other goals open.
|
|
345
345
|
|
|
346
|
+
## Session checkpoint recovery
|
|
347
|
+
|
|
348
|
+
Auto-continue checkpoints are tiny structured markers (≤160 chars) that
|
|
349
|
+
trigger the next turn; the full goal state is injected once per turn by the
|
|
350
|
+
extension and is never persisted into checkpoints. Sessions created by older
|
|
351
|
+
versions may still contain large "legacy" full-prompt checkpoints.
|
|
352
|
+
|
|
353
|
+
Check `/goal-recovery` or `/goal-status health` for a read-only report:
|
|
354
|
+
|
|
355
|
+
```text
|
|
356
|
+
Session checkpoints:
|
|
357
|
+
total: 851
|
|
358
|
+
legacy full checkpoints: 851
|
|
359
|
+
checkpoint content: 5.4 MB
|
|
360
|
+
projected content after recovery: 92 KB
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
To repair an affected session file, close Pi first (the tool refuses to run
|
|
364
|
+
while a live session could be open), then:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Report only — writes nothing:
|
|
368
|
+
pi-goal-x-recover --session <session.jsonl>
|
|
369
|
+
|
|
370
|
+
# Repair (creates a timestamped backup, rewrites only checkpoint entries):
|
|
371
|
+
pi-goal-x-recover --session <session.jsonl> --apply --confirm-pi-closed
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Recovery preserves every entry id and parent link, keeps all non-goal lines
|
|
375
|
+
byte-identical (including malformed lines), and is idempotent. Rollback is
|
|
376
|
+
the timestamped `.backup-*` file created next to the session.
|
|
377
|
+
|
|
346
378
|
## Commands
|
|
347
379
|
|
|
348
380
|
```text
|
|
@@ -368,12 +400,21 @@ Each session can focus on one goal while the project keeps other goals open.
|
|
|
368
400
|
|
|
369
401
|
## Configuration
|
|
370
402
|
|
|
371
|
-
Settings
|
|
403
|
+
Settings resolve per setting in this order (highest wins):
|
|
404
|
+
|
|
405
|
+
```text
|
|
406
|
+
environment > project layer > global layer > defaults
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Files:
|
|
372
410
|
|
|
373
411
|
```text
|
|
374
|
-
|
|
412
|
+
global: ~/.pi/pi-goal-x-settings.json (or $PI_CODING_AGENT_DIR, or $PI_GOAL_GLOBAL_SETTINGS_FILE)
|
|
413
|
+
project: <cwd>/.pi/pi-goal-x-settings.json (or $PI_GOAL_SETTINGS_FILE)
|
|
375
414
|
```
|
|
376
415
|
|
|
416
|
+
Define shared configuration once in the global file and override per project. Explicit `false`/`0` values in a lower layer override inherited values; nested `keybindings` inherit per key. `/goal-settings` shows each row's effective value and source, can switch the edited scope, and can remove a local override to return to inheritance.
|
|
417
|
+
|
|
377
418
|
Use `/goal-settings` to configure task lists, verification contracts, subtask depth, automatic goal selection, and completion auditing. Goal objectives have no hard length limit by default; set `objectiveMaxChars` (or `PI_GOAL_OBJECTIVE_MAX_CHARS`, `0` = no limit) to cap objective length across `create_goal`, `propose_goal_draft`, and `/goal-tweak`.
|
|
378
419
|
|
|
379
420
|
Configure the task shortcuts in the same file when the terminal captures the defaults:
|
|
@@ -5,10 +5,15 @@ import { extractVerificationContract, sisyphusObjectiveSufficient } from "./goal
|
|
|
5
5
|
import { detailedSummary, oneLineSummary } from "./goal-format.ts";
|
|
6
6
|
import {
|
|
7
7
|
goalSettingsPath,
|
|
8
|
+
goalGlobalSettingsPath,
|
|
8
9
|
loadGoalSettings,
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
loadSettingsSnapshot,
|
|
11
|
+
mutateSettingsLayer,
|
|
12
|
+
readSettingsLayer,
|
|
13
|
+
envOverrideFor,
|
|
11
14
|
type GoalSettings,
|
|
15
|
+
type SettingsMutation,
|
|
16
|
+
type SettingsScope,
|
|
12
17
|
} from "./goal-settings.ts";
|
|
13
18
|
import {
|
|
14
19
|
buildGoalListText,
|
|
@@ -19,11 +24,12 @@ import {
|
|
|
19
24
|
import { clearGoalCommandMessage, validateResumeGoal } from "./goal-policy.ts";
|
|
20
25
|
import { invalidateGoalLedgerCache, readGoalLedger } from "./goal-ledger.ts";
|
|
21
26
|
import { buildGoalStatusText } from "./goal-status.ts";
|
|
22
|
-
import { effectiveSettingsReport,
|
|
27
|
+
import { effectiveSettingsReport, invalidateGoalSettingsCache, loadGoalSettingsFileConfig } from "./goal-settings.ts";
|
|
23
28
|
import { invalidateGoalPoolCache, mergeGoalPromptFromDisk, readActiveGoalPool } from "./storage/goal-files.ts";
|
|
24
29
|
import { nowIso, type GoalMode, type GoalRecord } from "./goal-record.ts";
|
|
25
30
|
import { clearGoalDrafting, hasActiveDraft, startGoalDrafting } from "./goal-drafting.ts";
|
|
26
31
|
import { formatRecoveryReport, runRecoveryReport, runRecoveryRepair } from "./goal-recovery.ts";
|
|
32
|
+
import { formatCheckpointHealthReport, readSessionCheckpointHealth } from "./goal-session-health.ts";
|
|
27
33
|
|
|
28
34
|
export interface GoalRefreshState {
|
|
29
35
|
poolIds: Iterable<string>;
|
|
@@ -206,7 +212,18 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
206
212
|
}
|
|
207
213
|
return;
|
|
208
214
|
}
|
|
209
|
-
|
|
215
|
+
// Issue #30: surface persisted-checkpoint health for this session file
|
|
216
|
+
// (read-only). Legacy full checkpoints are repairable offline with the
|
|
217
|
+
// shipped pi-goal-x-recover CLI; this command never writes.
|
|
218
|
+
const sessionFile = (ctx.sessionManager as { getSessionFile?: () => string | undefined } | undefined)?.getSessionFile?.();
|
|
219
|
+
const checkpointHealth = sessionFile ? readSessionCheckpointHealth(sessionFile) : null;
|
|
220
|
+
const checkpointSection = checkpointHealth && checkpointHealth.total > 0
|
|
221
|
+
? `\n\n${formatCheckpointHealthReport(checkpointHealth, sessionFile)}`
|
|
222
|
+
: "";
|
|
223
|
+
ctx.ui.notify(
|
|
224
|
+
formatRecoveryReport(report) + checkpointSection,
|
|
225
|
+
report.healthy && (checkpointHealth?.legacyFull ?? 0) === 0 ? "info" : "warning",
|
|
226
|
+
);
|
|
210
227
|
}
|
|
211
228
|
|
|
212
229
|
async function runGoalRefresh(ctx: ExtensionContext): Promise<void> {
|
|
@@ -271,6 +288,13 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
271
288
|
activeFilePresent: health && view?.activePath
|
|
272
289
|
? existsSync(path.resolve(ctx.cwd, view.activePath))
|
|
273
290
|
: undefined,
|
|
291
|
+
// Issue #30: checkpoint growth report (health view only, read-only).
|
|
292
|
+
checkpointHealth: health
|
|
293
|
+
? readSessionCheckpointHealth(
|
|
294
|
+
(ctx.sessionManager as { getSessionFile?: () => string | undefined } | undefined)?.getSessionFile?.() ?? "",
|
|
295
|
+
)
|
|
296
|
+
: null,
|
|
297
|
+
checkpointSessionFile: (ctx.sessionManager as { getSessionFile?: () => string | undefined } | undefined)?.getSessionFile?.(),
|
|
274
298
|
// §13.2: effective settings with provenance appear only in verbose mode;
|
|
275
299
|
// the standard mode stays free of settings noise (§13.1).
|
|
276
300
|
settingsReport: verbose ? effectiveSettingsReport(ctx.cwd) : [],
|
|
@@ -364,6 +388,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
364
388
|
|
|
365
389
|
const SETTING_ROWS: readonly SettingRow[] = [
|
|
366
390
|
{ key: "autoSelectSingleGoal", label: "autoSelectSingleGoal", section: "Goal behavior", kind: "boolean" },
|
|
391
|
+
{ key: "hideUnfocusedBanner", label: "hideUnfocusedBanner", section: "Goal behavior", kind: "boolean" },
|
|
367
392
|
{ key: "disableContracts", label: "disableContracts", section: "Goal behavior", kind: "boolean" },
|
|
368
393
|
{ key: "stallTimeoutMinutes", label: "stall timeout (minutes)", section: "Goal behavior", kind: "positiveInteger" },
|
|
369
394
|
{ key: "objectiveMaxChars", label: "max objective length (0 = none)", section: "Goal behavior", kind: "positiveInteger" },
|
|
@@ -376,7 +401,7 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
376
401
|
];
|
|
377
402
|
|
|
378
403
|
function settingsValue(config: GoalSettings, key: keyof GoalSettings): string {
|
|
379
|
-
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources") {
|
|
404
|
+
if (key === "disabled" || key === "disableTasks" || key === "disableContracts" || key === "autoSelectSingleGoal" || key === "auditorProjectResources" || key === "hideUnfocusedBanner") {
|
|
380
405
|
return config[key] === true ? "true" : "false";
|
|
381
406
|
}
|
|
382
407
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
@@ -393,29 +418,52 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
393
418
|
|
|
394
419
|
async function handleSettingsMenu(ctx: ExtensionContext): Promise<void> {
|
|
395
420
|
if (!ctx.hasUI) {
|
|
396
|
-
|
|
421
|
+
// Headless: read-only report of both layer paths.
|
|
422
|
+
ctx.ui.notify(
|
|
423
|
+
`Settings — project file: ${goalSettingsPath(ctx.cwd)}\nGlobal file: ${goalGlobalSettingsPath()}`,
|
|
424
|
+
"info",
|
|
425
|
+
);
|
|
397
426
|
return;
|
|
398
427
|
}
|
|
428
|
+
|
|
399
429
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* off within one menu session reinstalls on every real change instead of
|
|
405
|
-
* comparing against the value captured when the menu opened.
|
|
430
|
+
* After any mutation: reinstall the fixed three/five tool profile when
|
|
431
|
+
* the effective disableTasks value changed since the last install, and
|
|
432
|
+
* refresh the UI. This is the central settings side-effect hook — no
|
|
433
|
+
* ad hoc per-row refresh code.
|
|
406
434
|
*/
|
|
407
|
-
const
|
|
408
|
-
saveGoalSettingsFileConfig(ctx.cwd, next);
|
|
435
|
+
const applyEffectiveSettings = (): void => {
|
|
409
436
|
const tasksEnabledNow = !loadGoalSettings(ctx.cwd).disableTasks;
|
|
410
437
|
if (tasksEnabledNow !== core.tasksEnabled) {
|
|
411
438
|
core.installGoalToolProfile(tasksEnabledNow);
|
|
412
439
|
}
|
|
440
|
+
// PR #29: settings changes must be visible immediately — the banner
|
|
441
|
+
// hide/restore happens through this coalesced UI refresh.
|
|
442
|
+
core.updateUI(ctx);
|
|
413
443
|
};
|
|
444
|
+
|
|
445
|
+
const applyMutation = (scope: SettingsScope, mutation: SettingsMutation): boolean => {
|
|
446
|
+
try {
|
|
447
|
+
mutateSettingsLayer({ scope, cwd: ctx.cwd, mutation });
|
|
448
|
+
applyEffectiveSettings();
|
|
449
|
+
return true;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
ctx.ui.notify(`Settings change failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const scopePath = (scope: SettingsScope): string =>
|
|
457
|
+
scope === "global" ? goalGlobalSettingsPath() : goalSettingsPath(ctx.cwd);
|
|
458
|
+
|
|
414
459
|
core.enterGoalModal();
|
|
415
460
|
try {
|
|
461
|
+
let scope: SettingsScope = "project";
|
|
416
462
|
while (true) {
|
|
417
|
-
const
|
|
463
|
+
const snapshot = loadSettingsSnapshot(ctx.cwd);
|
|
464
|
+
const localLayer = snapshot[scope].layer as Record<string, unknown>;
|
|
418
465
|
const options: string[] = [];
|
|
466
|
+
options.push(`─── Editing: ${scope} (${scopePath(scope)}) ───`);
|
|
419
467
|
let lastSection: string | null = null;
|
|
420
468
|
for (const row of SETTING_ROWS) {
|
|
421
469
|
if (row.section !== lastSection) {
|
|
@@ -423,109 +471,147 @@ export function registerGoalCommands(core: GoalCore): void {
|
|
|
423
471
|
lastSection = row.section;
|
|
424
472
|
}
|
|
425
473
|
const envVar = envOverrideFor(row.key);
|
|
426
|
-
|
|
474
|
+
if (envVar) {
|
|
475
|
+
options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} (environment; read-only)`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const provenance = snapshot.provenance.get(String(row.key));
|
|
479
|
+
let source: string;
|
|
480
|
+
if (provenance?.source === "global") source = scope === "project" ? "(inherited from global)" : "(global override)";
|
|
481
|
+
else if (provenance?.source === "project") source = scope === "project" ? "(project override)" : "(project; edit via project scope)";
|
|
482
|
+
else source = "(default)";
|
|
483
|
+
const hasLocalOverride = localLayer[row.key] !== undefined;
|
|
484
|
+
if (hasLocalOverride && provenance?.source === scope) source = `(${scope} override)`;
|
|
485
|
+
options.push(` ${row.label}: ${settingsValue(snapshot.value, row.key)} ${source}`);
|
|
427
486
|
}
|
|
428
487
|
options.push("Done");
|
|
429
488
|
const selected = await ctx.ui.select("Goal settings", options);
|
|
430
489
|
if (!selected || selected === "Done") break;
|
|
431
|
-
if (selected.startsWith("───")) continue; //
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
490
|
+
if (selected.startsWith("───")) continue; // headers are not rows
|
|
491
|
+
const trimmed = selected.trim();
|
|
492
|
+
if (trimmed.startsWith("Scope:")) {
|
|
493
|
+
scope = scope === "project" ? "global" : "project";
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const colon = trimmed.indexOf(":");
|
|
435
497
|
if (colon === -1) continue;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(key)} env var.`, "warning");
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (row.kind === "boolean") {
|
|
445
|
-
const next = { ...config, [key]: config[key] !== true };
|
|
446
|
-
saveSettings(next);
|
|
447
|
-
ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
if (row.kind === "positiveInteger") {
|
|
451
|
-
const input = await ctx.ui.input(`Set ${row.label}`, settingsValue(config, key));
|
|
452
|
-
if (input === undefined) continue;
|
|
453
|
-
// Row-driven lower bound: subtaskDepth is a nesting depth (min 1);
|
|
454
|
-
// stallTimeoutMinutes and objectiveMaxChars default to 0 meaning
|
|
455
|
-
// "off / no limit".
|
|
456
|
-
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
457
|
-
// Full-string decimal validation: no partial parseInt. Rejects
|
|
458
|
-
// 1.5, 1x, negatives, infinity, and unsafe integers alike.
|
|
459
|
-
const trimmed = input.trim();
|
|
460
|
-
if (!/^[0-9]+$/.test(trimmed) || !Number.isSafeInteger(Number(trimmed)) || Number(trimmed) < min) {
|
|
461
|
-
ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
|
|
498
|
+
const label = trimmed.slice(0, colon).trim();
|
|
499
|
+
const row = SETTING_ROWS.find((r) => r.label === label);
|
|
500
|
+
if (!row) continue;
|
|
501
|
+
if (envOverrideFor(row.key)) {
|
|
502
|
+
ctx.ui.notify(`${row.label} is read-only: overridden by the ${envOverrideFor(row.key)} env var.`, "warning");
|
|
462
503
|
continue;
|
|
463
504
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
505
|
+
|
|
506
|
+
const inheritLabel = scope === "project" ? "Use inherited value" : "Use default (delete global override)";
|
|
507
|
+
const hasLocalOverride = localLayer[row.key] !== undefined;
|
|
508
|
+
|
|
509
|
+
if (row.kind === "boolean") {
|
|
510
|
+
const effective = snapshot.provenance.get(String(row.key))?.value === true;
|
|
511
|
+
const actions = [
|
|
512
|
+
`Set ${scope} override to true`,
|
|
513
|
+
`Set ${scope} override to false`,
|
|
514
|
+
];
|
|
515
|
+
if (hasLocalOverride) actions.push(inheritLabel);
|
|
516
|
+
actions.push("Cancel");
|
|
517
|
+
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
518
|
+
if (!action || action === "Cancel") continue;
|
|
519
|
+
if (action === inheritLabel) {
|
|
520
|
+
applyMutation(scope, { op: "unset", path: [row.key] });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const value = action.endsWith("true");
|
|
524
|
+
if (value !== effective || !hasLocalOverride) {
|
|
525
|
+
applyMutation(scope, { op: "set", path: [row.key], value });
|
|
526
|
+
}
|
|
482
527
|
continue;
|
|
483
528
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
delete next.model;
|
|
507
|
-
} else if (choice.kind === "manual") {
|
|
508
|
-
const input = await ctx.ui.input("Set auditor provider/model", configured ?? "provider/model");
|
|
509
|
-
if (input === undefined) continue;
|
|
510
|
-
const parsed = parseManualAuditorModel(input);
|
|
511
|
-
if ("error" in parsed) {
|
|
512
|
-
ctx.ui.notify(parsed.error, "warning");
|
|
529
|
+
|
|
530
|
+
if (row.kind === "positiveInteger") {
|
|
531
|
+
const min = (row.key === "stallTimeoutMinutes" || row.key === "objectiveMaxChars") ? 0 : 1;
|
|
532
|
+
const actions = [`Set ${scope} override...`];
|
|
533
|
+
if (hasLocalOverride) actions.push(inheritLabel);
|
|
534
|
+
actions.push("Cancel");
|
|
535
|
+
const action = await ctx.ui.select(`${row.label} (${scope})`, actions);
|
|
536
|
+
if (!action || action === "Cancel") continue;
|
|
537
|
+
if (action === inheritLabel) {
|
|
538
|
+
applyMutation(scope, { op: "unset", path: [row.key] });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const input = await ctx.ui.input(`Set ${row.label}`, String(snapshot.provenance.get(String(row.key))?.value ?? min));
|
|
542
|
+
if (input === undefined) continue;
|
|
543
|
+
// Full-string decimal validation: rejects 1.5, 1x, negatives,
|
|
544
|
+
// infinity, and unsafe integers alike.
|
|
545
|
+
const value = input.trim();
|
|
546
|
+
if (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min) {
|
|
547
|
+
ctx.ui.notify(`${row.label} must be an integer >= ${min} (e.g. ${min}, ${min + 1}, ${min + 2})`, "warning");
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
applyMutation(scope, { op: "set", path: [row.key], value: Number(value) });
|
|
513
551
|
continue;
|
|
514
552
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
553
|
+
|
|
554
|
+
if (row.kind === "thinking") {
|
|
555
|
+
const currentValue = settingsValue(snapshot.value, row.key);
|
|
556
|
+
const levels = thinkingLevelChoices(currentValue === "(default)" ? undefined : currentValue);
|
|
557
|
+
const picked = await ctx.ui.select(`Set ${row.label} (${scope})`, levels);
|
|
558
|
+
if (!picked) continue;
|
|
559
|
+
const choice = picked.trim().replace(/^\u2713\s+/, "");
|
|
560
|
+
if (choice === "(default)") {
|
|
561
|
+
if (hasLocalOverride) applyMutation(scope, { op: "unset", path: ["thinkingLevel"] });
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (!(AUDITOR_THINKING_LEVELS as readonly string[]).includes(choice)) {
|
|
565
|
+
ctx.ui.notify(`thinking_level must be one of: ${AUDITOR_THINKING_LEVELS.join(", ")} (or "(default)")`, "warning");
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
applyMutation(scope, { op: "set", path: ["thinkingLevel"], value: choice });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// modelSelector rows (provider, model): searchable auditor model
|
|
573
|
+
// picker with explicit inherit/default handling. Either row applies
|
|
574
|
+
// the same provider+model pair.
|
|
575
|
+
const configured = configuredAuditorModelKey(snapshot.value as GoalSettings);
|
|
576
|
+
const session = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
577
|
+
const choices = buildAuditorModelChoices(ctx.modelRegistry.getAvailable(), configured, session);
|
|
578
|
+
const filter = await ctx.ui.input("Filter auditor models (provider/id/name; blank = all)", "");
|
|
579
|
+
if (filter === undefined) continue;
|
|
580
|
+
const filtered = filterAuditorModelChoices(choices, filter);
|
|
581
|
+
const picked = await ctx.ui.select("Select auditor model", filtered.map((choice: AuditorChoice) => choice.label));
|
|
582
|
+
if (!picked) continue;
|
|
583
|
+
const choice = filtered.find((candidate: AuditorChoice) => candidate.label === picked);
|
|
584
|
+
if (!choice) continue;
|
|
585
|
+
if (choice.kind === "default") {
|
|
586
|
+
for (const key of ["provider", "model"] as const) {
|
|
587
|
+
if (localLayer[key] !== undefined) applyMutation(scope, { op: "unset", path: [key] });
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
let providerValue: string;
|
|
592
|
+
let modelValue: string;
|
|
593
|
+
if (choice.kind === "manual") {
|
|
594
|
+
const input = await ctx.ui.input("Set auditor provider/model", configured ?? "provider/model");
|
|
595
|
+
if (input === undefined) continue;
|
|
596
|
+
const parsed = parseManualAuditorModel(input);
|
|
597
|
+
if ("error" in parsed) {
|
|
598
|
+
ctx.ui.notify(parsed.error, "warning");
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
providerValue = parsed.provider;
|
|
602
|
+
modelValue = parsed.model;
|
|
603
|
+
} else {
|
|
604
|
+
providerValue = choice.provider;
|
|
605
|
+
modelValue = choice.model;
|
|
606
|
+
}
|
|
607
|
+
if (applyMutation(scope, { op: "set", path: ["provider"], value: providerValue })) {
|
|
608
|
+
applyMutation(scope, { op: "set", path: ["model"], value: modelValue });
|
|
609
|
+
}
|
|
523
610
|
}
|
|
524
611
|
} finally {
|
|
525
612
|
core.exitGoalModal();
|
|
526
613
|
}
|
|
527
614
|
}
|
|
528
|
-
|
|
529
615
|
async function handleGoalClear(ctx: ExtensionContext): Promise<void> {
|
|
530
616
|
core.reconcileFocusedGoalFromDisk(ctx);
|
|
531
617
|
if (!core.state.goal && core.openGoals().length > 0) {
|
|
@@ -17,9 +17,10 @@ import { shouldArmPostCompactReminder, shouldInjectPostCompactReminder } from ".
|
|
|
17
17
|
import { formatTokenValue } from "./goal-core.ts";
|
|
18
18
|
import { loadGoalSettings, invalidateGoalSettingsCache } from "./goal-settings.ts";
|
|
19
19
|
import { budgetLine, budgetRemaining } from "./goal-accounting.ts";
|
|
20
|
-
import { asRecord, nowIso, type AssistantMessageLike } from "./goal-record.ts";
|
|
20
|
+
import { asRecord, nowIso, type AssistantMessageLike, type GoalRecord } from "./goal-record.ts";
|
|
21
21
|
import { goalSelectorLabel } from "./goal-pool.ts";
|
|
22
|
-
import { invalidateGoalPoolCache } from "./storage/goal-files.ts";
|
|
22
|
+
import { invalidateGoalPoolCache } from "./storage/goal-files.ts";
|
|
23
|
+
import { checkpointTriggerPrompt } from "./prompts/goal-prompts.ts";import {
|
|
23
24
|
goalPrompt,
|
|
24
25
|
staleContinuationPrompt,
|
|
25
26
|
unfocusedOpenGoalsPrompt,
|
|
@@ -30,6 +31,56 @@ import { syncTerminalInputPause } from "./goal-widget.ts";
|
|
|
30
31
|
import type { GoalCore } from "./goal-state.ts";
|
|
31
32
|
import type { GoalMutationOutcome } from "./goal-service.ts";
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Issue #30: provider-context checkpoint compaction (pure helper).
|
|
36
|
+
*
|
|
37
|
+
* Every historical checkpoint message is redundant: its authoritative state is
|
|
38
|
+
* reconstructed from goal storage and injected once per turn by
|
|
39
|
+
* before_agent_start. Normal provider requests therefore retain at most ONE
|
|
40
|
+
* checkpoint marker — the latest — rewritten to the tiny bounded v2 trigger
|
|
41
|
+
* content. Keeping one user-role turn-start marker avoids provider edge cases
|
|
42
|
+
* where removing it would leave the request ending on an assistant or tool
|
|
43
|
+
* result. Audit events, user messages, assistant messages, and tool results
|
|
44
|
+
* pass through untouched.
|
|
45
|
+
*/
|
|
46
|
+
export function compactGoalCheckpointContext(
|
|
47
|
+
messages: readonly unknown[],
|
|
48
|
+
currentGoal: GoalRecord | null,
|
|
49
|
+
): unknown[] | null {
|
|
50
|
+
let lastCheckpointIndex = -1;
|
|
51
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
52
|
+
if (goalEventMessageId(messages[i] as { customType?: string; details?: unknown; content?: unknown }) !== null) {
|
|
53
|
+
lastCheckpointIndex = i;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (lastCheckpointIndex < 0) return null;
|
|
57
|
+
|
|
58
|
+
const output: unknown[] = [];
|
|
59
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
60
|
+
const message = messages[i] as { customType?: string; details?: unknown; content?: unknown };
|
|
61
|
+
const checkpointGoalId = goalEventMessageId(message);
|
|
62
|
+
if (checkpointGoalId === null) {
|
|
63
|
+
output.push(messages[i]);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Every historical checkpoint is dropped entirely.
|
|
67
|
+
if (i !== lastCheckpointIndex) continue;
|
|
68
|
+
output.push({
|
|
69
|
+
...(message as Record<string, unknown>),
|
|
70
|
+
content: checkpointTriggerPrompt(checkpointGoalId),
|
|
71
|
+
display: false,
|
|
72
|
+
details: {
|
|
73
|
+
version: 2,
|
|
74
|
+
kind: currentGoal?.id === checkpointGoalId && currentGoal?.status === "active" ? "checkpoint" : "stale",
|
|
75
|
+
goalId: checkpointGoalId,
|
|
76
|
+
currentGoalId: currentGoal?.id ?? null,
|
|
77
|
+
currentStatus: currentGoal?.status ?? null,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
/**
|
|
34
85
|
* The goal extension's lifecycle event handlers (context, turn_start,
|
|
35
86
|
* tool_call, tool_execution_end, turn_end, message_end, session_start,
|
|
@@ -41,40 +92,10 @@ export function registerGoalEvents(core: GoalCore): void {
|
|
|
41
92
|
const { pi } = core;
|
|
42
93
|
let continuationAfterSettleFor: string | null = null;
|
|
43
94
|
|
|
44
|
-
pi.on("context", async (event)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const queuedGoalId = goalEventMessageId(message as { customType?: string; details?: unknown; content?: unknown });
|
|
49
|
-
if (queuedGoalId) latestGoalEventIndex.set(queuedGoalId, index);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const messages = event.messages.map((message, index) => {
|
|
53
|
-
const candidate = message as { customType?: string; details?: unknown; content?: unknown };
|
|
54
|
-
const queuedGoalId = goalEventMessageId(candidate);
|
|
55
|
-
if (!queuedGoalId) return message;
|
|
56
|
-
if (
|
|
57
|
-
core.state.goal?.id === queuedGoalId
|
|
58
|
-
&& (core.state.goal.status === "active")
|
|
59
|
-
&& core.state.goal.autoContinue
|
|
60
|
-
&& latestGoalEventIndex.get(queuedGoalId) === index
|
|
61
|
-
) return message;
|
|
62
|
-
changed = true;
|
|
63
|
-
const details = asRecord(candidate.details) ?? {};
|
|
64
|
-
return {
|
|
65
|
-
...message,
|
|
66
|
-
content: staleContinuationPrompt(queuedGoalId, core.state.goal),
|
|
67
|
-
display: false,
|
|
68
|
-
details: {
|
|
69
|
-
...details,
|
|
70
|
-
kind: "stale",
|
|
71
|
-
goalId: queuedGoalId,
|
|
72
|
-
currentGoalId: core.state.goal?.id ?? null,
|
|
73
|
-
currentStatus: core.state.goal?.status ?? null,
|
|
74
|
-
},
|
|
75
|
-
} as typeof message;
|
|
76
|
-
});
|
|
77
|
-
return changed ? { messages } : undefined;
|
|
95
|
+
pi.on("context", async (event) => {
|
|
96
|
+
const messages = compactGoalCheckpointContext(event.messages, core.state.goal);
|
|
97
|
+
// Reference equality means no goal-event messages existed at all.
|
|
98
|
+
return messages === null ? undefined : { messages: messages as typeof event.messages };
|
|
78
99
|
});
|
|
79
100
|
|
|
80
101
|
pi.on("turn_start", async (_event, ctx) => {
|
|
@@ -176,17 +176,38 @@ export function renderGoalAuditEvent(message: { content?: unknown; details?: Goa
|
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Issue #30 v2 markers are self-closing and carry an explicit version:
|
|
181
|
+
* `<pi_goal_continuation goal_id="..." kind="checkpoint" v="2"/>`.
|
|
182
|
+
*/
|
|
183
|
+
function matchV2SelfClosingContinuation(text: string): string | null {
|
|
184
|
+
const match = text.match(/^<pi_goal_continuation\s+goal_id="([^"]+)"[^>]*v="2"\s*\/?>/);
|
|
185
|
+
return match?.[1] ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function matchLegacyContinuation(text: string): string | null {
|
|
180
189
|
// Phase 5 C1: structured outer marker `<pi_goal_continuation goal_id="..." kind="...">`.
|
|
181
190
|
// Borrowed from pi-codex-goal. More robust than bare bracket text because
|
|
182
191
|
// the angle brackets + attributes are nearly impossible for users to type
|
|
183
192
|
// by accident, and the structure is grep-able / parse-able by external tooling.
|
|
184
193
|
const xmlMatch = text.match(/^<pi_goal_continuation\s+goal_id="([^"]+)"/);
|
|
185
194
|
if (xmlMatch) return xmlMatch[1] ?? null;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function matchLegacyBracketCheckpoint(text: string): string | null {
|
|
186
199
|
const match = text.match(/^\[(?:GOAL CHECKPOINT|GOAL CONTINUATION|GOAL STALE) goalId=([^\]\s]+)\]/);
|
|
187
200
|
return match?.[1] ?? null;
|
|
188
201
|
}
|
|
189
202
|
|
|
203
|
+
export function extractGoalIdFromInjectedMessage(text: string): string | null {
|
|
204
|
+
return (
|
|
205
|
+
matchV2SelfClosingContinuation(text)
|
|
206
|
+
?? matchLegacyContinuation(text)
|
|
207
|
+
?? matchLegacyBracketCheckpoint(text)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
190
211
|
export function goalEventMessageId(message: { customType?: string; details?: unknown; content?: unknown }): string | null {
|
|
191
212
|
if (message.customType !== GOAL_EVENT_ENTRY) return null;
|
|
192
213
|
const details = asRecord(message.details);
|
|
@@ -81,7 +81,24 @@ export interface GoalFocusEntry {
|
|
|
81
81
|
reason: GoalFocusReason;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Issue #30 v2 checkpoint details: a tiny structured trigger record. Never
|
|
86
|
+
* contains objective text, task text, verification contracts, budget strings,
|
|
87
|
+
* or policy text — the authoritative state is injected once per turn by
|
|
88
|
+
* before_agent_start from goal storage.
|
|
89
|
+
*/
|
|
90
|
+
export interface GoalCheckpointDetailsV2 {
|
|
91
|
+
version: 2;
|
|
92
|
+
kind: "checkpoint";
|
|
93
|
+
goalId: string;
|
|
94
|
+
status: "active";
|
|
95
|
+
revision: number;
|
|
96
|
+
checkpointSeq: number;
|
|
97
|
+
timestamp: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Pre-v2 event details (full objective persisted). Read-only legacy shape. */
|
|
101
|
+
export interface LegacyGoalEventDetails {
|
|
85
102
|
kind: GoalEventKind;
|
|
86
103
|
goalId: string;
|
|
87
104
|
status?: GoalStatus;
|
|
@@ -93,6 +110,23 @@ export interface GoalEventDetails {
|
|
|
93
110
|
focus?: GoalMode;
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
/** Historical "stale" rewrite of a checkpoint that no longer matches the goal. */
|
|
114
|
+
export interface GoalStaleDetails {
|
|
115
|
+
kind: "stale";
|
|
116
|
+
goalId: string;
|
|
117
|
+
currentGoalId?: string | null;
|
|
118
|
+
currentStatus?: GoalStatus | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Everything that can appear in a persisted pi-goal-event entry's details. */
|
|
122
|
+
export type GoalEventEntryDetails = GoalCheckpointDetailsV2 | LegacyGoalEventDetails | GoalStaleDetails;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Legacy normalized renderer shape. Kept as the stable contract for
|
|
126
|
+
* renderGoalEvent / registerMessageRenderer; v2 entries normalize into it.
|
|
127
|
+
*/
|
|
128
|
+
export type GoalEventDetails = LegacyGoalEventDetails;
|
|
129
|
+
|
|
96
130
|
export interface GoalCreationConfig {
|
|
97
131
|
objective: string;
|
|
98
132
|
autoContinue: boolean;
|