pi-goal-x 0.18.10 → 0.20.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 +3 -1
- package/extensions/goal-auditor.ts +26 -2
- package/extensions/goal-pool.ts +9 -2
- package/extensions/goal-settings.ts +6 -0
- package/extensions/goal.ts +22 -111
- package/extensions/widgets/goal-widget.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Pressing `Esc` or aborting an active run pauses the goal so it does not remain f
|
|
|
104
104
|
- **Branch-local focus**: because focus is reconstructed from the current session branch, `/tree` navigation can restore a different focus for a different branch.
|
|
105
105
|
- **One continuation chain**: auto-continue only schedules work for the focused goal in the current session.
|
|
106
106
|
|
|
107
|
-
Creating a goal with `/goals`, `/sisyphus`, `/goals-set`, or `/sisyphus-set` no longer clears other open goals. It creates a new active goal file and focuses it. Use `/goal-list` to inspect open goals and `/goal-focus` to switch the session focus. If the latest focus entry explicitly clears focus, or points at a missing/stale goal, a remaining single open goal is not auto-focused
|
|
107
|
+
Creating a goal with `/goals`, `/sisyphus`, `/goals-set`, or `/sisyphus-set` no longer clears other open goals. It creates a new active goal file and focuses it. Use `/goal-list` to inspect open goals and `/goal-focus` to switch the session focus. If the latest focus entry explicitly clears focus, or points at a missing/stale goal, a remaining single open goal is not auto-focused. By default (`autoSelectSingleGoal: false`) sessions start unfocused so goals stay session-scoped — useful when multiple sessions share the same `.pi/goals/` directory. Set `autoSelectSingleGoal: true` to restore the old behavior where a single open goal is auto-focused when no focus entry exists at all. If multiple open goals exist and the session has no valid focus, `/goal-resume`, `/goal-clear`, `/goal-abort`, `/goal-pause`, and `/goal-tweak` ask the user to choose a goal instead of acting on all of them.
|
|
108
108
|
|
|
109
109
|
## Agent tools
|
|
110
110
|
|
|
@@ -216,6 +216,7 @@ Configured interactively via `/goal-settings`, or edited directly:
|
|
|
216
216
|
"disableTasks": false,
|
|
217
217
|
"disableContracts": false,
|
|
218
218
|
"subtaskDepth": 1,
|
|
219
|
+
"autoSelectSingleGoal": false,
|
|
219
220
|
"provider": "fireworks",
|
|
220
221
|
"model": "accounts/fireworks/models/deepseek-v4-flash",
|
|
221
222
|
"thinkingLevel": "high",
|
|
@@ -228,6 +229,7 @@ Configured interactively via `/goal-settings`, or edited directly:
|
|
|
228
229
|
| `disableTasks` | `false` | Suppress task list features entirely when `true` |
|
|
229
230
|
| `disableContracts` | `false` | Suppress verification contract enforcement when `true` |
|
|
230
231
|
| `subtaskDepth` | `1` | Maximum nesting depth for subtasks |
|
|
232
|
+
| `autoSelectSingleGoal` | `false` | When `true`, auto-focus the single open goal when a session has no focus entry (default keeps goals session-scoped) |
|
|
231
233
|
| `provider` | system default | Provider name for the auditor agent |
|
|
232
234
|
| `model` | system default | Model name for the auditor agent |
|
|
233
235
|
| `thinkingLevel` | system default | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
|
|
@@ -242,6 +242,30 @@ function resolveAuditorModel(ctx: ExtensionContext, config: GoalSettings): { mod
|
|
|
242
242
|
return { model: undefined, error: `Configured auditor model is ambiguous or unavailable: ${config.model}` };
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Options that reuse the parent session's auth + registered providers for the nested auditor.
|
|
247
|
+
*
|
|
248
|
+
* Pi 0.81+ `createAgentSession` accepts `modelRuntime` (and ignores `modelRegistry`).
|
|
249
|
+
* ExtensionContext still exposes `modelRegistry`, which wraps the live ModelRuntime.
|
|
250
|
+
* Sharing that runtime keeps extension providers such as `pi-cursor-sdk`'s `cursor`
|
|
251
|
+
* provider (and its auth) available. Without this, the auditor builds a fresh runtime
|
|
252
|
+
* with an empty resource loader, so Cursor models fail with "No API key found for cursor"
|
|
253
|
+
* even when `~/.pi/agent/auth.json` has a Cursor key.
|
|
254
|
+
*
|
|
255
|
+
* Older SDKs still accept `modelRegistry`; pass both for compatibility.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveAuditorSessionModelOptions(ctx: ExtensionContext): {
|
|
258
|
+
modelRegistry: ExtensionContext["modelRegistry"];
|
|
259
|
+
modelRuntime?: unknown;
|
|
260
|
+
} {
|
|
261
|
+
const registry = ctx.modelRegistry as ExtensionContext["modelRegistry"] & { runtime?: unknown };
|
|
262
|
+
const runtime = registry?.runtime;
|
|
263
|
+
if (runtime) {
|
|
264
|
+
return { modelRegistry: ctx.modelRegistry, modelRuntime: runtime };
|
|
265
|
+
}
|
|
266
|
+
return { modelRegistry: ctx.modelRegistry };
|
|
267
|
+
}
|
|
268
|
+
|
|
245
269
|
function modelLabel(model: Model<any> | undefined): string | undefined {
|
|
246
270
|
return model ? `${model.provider}/${model.id}` : undefined;
|
|
247
271
|
}
|
|
@@ -318,13 +342,13 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
318
342
|
cwd: args.ctx.cwd,
|
|
319
343
|
model,
|
|
320
344
|
thinkingLevel,
|
|
321
|
-
|
|
345
|
+
...resolveAuditorSessionModelOptions(args.ctx),
|
|
322
346
|
resourceLoader: makeAuditorResourceLoader(),
|
|
323
347
|
sessionManager: SessionManager.inMemory(args.ctx.cwd),
|
|
324
348
|
settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),
|
|
325
349
|
tools: ["read", "grep", "find", "ls", "bash", REPORT_AUDITOR_PROGRESS_TOOL_NAME],
|
|
326
350
|
customTools: [reportProgressTool],
|
|
327
|
-
});
|
|
351
|
+
} as Parameters<typeof createAgentSession>[0]);
|
|
328
352
|
const unsubscribe = session.subscribe((event) => {
|
|
329
353
|
if (event.type === "tool_execution_start") {
|
|
330
354
|
progress.currentTool = event.toolName;
|
package/extensions/goal-pool.ts
CHANGED
|
@@ -38,6 +38,7 @@ export function resolveSessionFocus(args: {
|
|
|
38
38
|
pool: Map<string, GoalRecord>;
|
|
39
39
|
focusEntry?: GoalFocusEntry | null;
|
|
40
40
|
legacyGoal?: GoalRecord | null;
|
|
41
|
+
autoSelectSingleGoal?: boolean;
|
|
41
42
|
}): string | null {
|
|
42
43
|
const focusedGoalId = args.focusEntry?.focusedGoalId ?? null;
|
|
43
44
|
const focused = focusedGoalId ? focusedGoalFromPool(args.pool, focusedGoalId) : null;
|
|
@@ -52,8 +53,14 @@ export function resolveSessionFocus(args: {
|
|
|
52
53
|
args.pool.set(args.legacyGoal.id, cloneGoal(args.legacyGoal));
|
|
53
54
|
return args.legacyGoal.id;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// Auto-select single open goal only when explicitly enabled.
|
|
57
|
+
// Default (false): sessions start unfocused to prevent goals from
|
|
58
|
+
// bleeding across sessions that share the same .pi/goals/ directory.
|
|
59
|
+
if (args.autoSelectSingleGoal) {
|
|
60
|
+
const open = openGoalsFromPool(args.pool);
|
|
61
|
+
return open.length === 1 ? open[0]?.id ?? null : null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export function goalSelectorLabel(goal: GoalRecord, focusedGoalId: string | null): string {
|
|
@@ -26,6 +26,7 @@ export interface GoalSettings {
|
|
|
26
26
|
model?: string;
|
|
27
27
|
thinkingLevel?: ThinkingLevel;
|
|
28
28
|
disabled?: boolean;
|
|
29
|
+
autoSelectSingleGoal?: boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export const PI_GOAL_SETTINGS_FILE_ENV = "PI_GOAL_SETTINGS_FILE";
|
|
@@ -41,6 +42,7 @@ const ALLOWED_SETTINGS_KEYS = new Set([
|
|
|
41
42
|
"thinkingLevel",
|
|
42
43
|
"thinking_level",
|
|
43
44
|
"disabled",
|
|
45
|
+
"autoSelectSingleGoal",
|
|
44
46
|
]);
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -105,6 +107,7 @@ export function parseGoalSettings(raw: unknown): GoalSettings {
|
|
|
105
107
|
if (model !== undefined) settings.model = model;
|
|
106
108
|
if (thinkingLevel !== undefined) settings.thinkingLevel = thinkingLevel;
|
|
107
109
|
if (record.disabled === true || record.disabled === "true") settings.disabled = true;
|
|
110
|
+
if (record.autoSelectSingleGoal === true || record.autoSelectSingleGoal === "true") settings.autoSelectSingleGoal = true;
|
|
108
111
|
return settings;
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -136,6 +139,7 @@ export function loadGoalSettings(cwd: string, env: NodeJS.ProcessEnv = process.e
|
|
|
136
139
|
model: fileConfig.model,
|
|
137
140
|
thinkingLevel: fileConfig.thinkingLevel,
|
|
138
141
|
disabled: fileConfig.disabled,
|
|
142
|
+
autoSelectSingleGoal: fileConfig.autoSelectSingleGoal ?? false,
|
|
139
143
|
};
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -166,6 +170,7 @@ export function saveGoalSettingsFileConfig(cwd: string, settings: GoalSettings):
|
|
|
166
170
|
if (disableTasks === true) clean.disableTasks = true;
|
|
167
171
|
if (disableContracts === true) clean.disableContracts = true;
|
|
168
172
|
if (subtaskDepth !== undefined) clean.subtaskDepth = subtaskDepth;
|
|
173
|
+
if (settings.autoSelectSingleGoal === true) clean.autoSelectSingleGoal = true;
|
|
169
174
|
const configPath = goalSettingsPath(cwd);
|
|
170
175
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
171
176
|
const persisted: Record<string, unknown> = {};
|
|
@@ -176,6 +181,7 @@ export function saveGoalSettingsFileConfig(cwd: string, settings: GoalSettings):
|
|
|
176
181
|
if (clean.disableTasks) persisted.disableTasks = true;
|
|
177
182
|
if (clean.disableContracts) persisted.disableContracts = true;
|
|
178
183
|
if (clean.subtaskDepth !== undefined) persisted.subtaskDepth = clean.subtaskDepth;
|
|
184
|
+
if (settings.autoSelectSingleGoal === true) persisted.autoSelectSingleGoal = true;
|
|
179
185
|
fs.writeFileSync(configPath, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
|
|
180
186
|
return clean;
|
|
181
187
|
}
|
package/extensions/goal.ts
CHANGED
|
@@ -146,7 +146,6 @@ const GOAL_EVENT_ENTRY = "pi-goal-event";
|
|
|
146
146
|
const GOAL_AUDIT_ENTRY = "pi-goal-audit-event";
|
|
147
147
|
const COMPLETE_STATUS = "complete";
|
|
148
148
|
const CONTINUATION_IDLE_RETRY_MS = 50;
|
|
149
|
-
const STATUS_REFRESH_MS = 1000;
|
|
150
149
|
/**
|
|
151
150
|
* Tools that count as "real work" toward the active goal. If a non-tool-use
|
|
152
151
|
* turn ends without any of these having been called, we DO NOT queue the next
|
|
@@ -410,8 +409,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
410
409
|
let continuationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
411
410
|
let runningGoalId: string | null = null;
|
|
412
411
|
let terminalInputUnsubscribe: (() => void) | null = null;
|
|
413
|
-
let statusRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
414
|
-
let statusRefreshCtx: ExtensionContext | null = null;
|
|
415
412
|
let auditProgress: AuditorWidgetProgress | null = null;
|
|
416
413
|
let auditAnimationTimer: ReturnType<typeof setInterval> | null = null;
|
|
417
414
|
let auditAbortController: AbortController | null = null;
|
|
@@ -429,15 +426,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
429
426
|
// after their successful execute. Once set, pi.on("tool_call") blocks all
|
|
430
427
|
// subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
|
|
431
428
|
// schema fix for "agent keeps writing files after pause_goal".
|
|
432
|
-
// turnSeq: lightweight generation counter, incremented at each turn start.
|
|
433
|
-
// Used to scope turnStoppedFor so stale markers from prior turns or sessions
|
|
434
|
-
// cannot poison a resumed active goal.
|
|
435
|
-
// checkpointGoalId: remembers the incoming goal id from queued continuations.
|
|
436
|
-
// When set but the goal is no longer active/autoContinue, work tools are blocked.
|
|
437
429
|
let goalWorkToolCalledThisTurn = false;
|
|
438
|
-
let
|
|
439
|
-
let turnStoppedFor: { goalId: string; turnSeq: number } | null = null;
|
|
440
|
-
let checkpointGoalId: string | null = null;
|
|
430
|
+
let turnStoppedFor: string | null = null;
|
|
441
431
|
|
|
442
432
|
// #5 post-compaction resync: when a compaction just happened, the next agent
|
|
443
433
|
// turn gets an extra reminder block. Set in session_compact, consumed
|
|
@@ -534,37 +524,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
534
524
|
}
|
|
535
525
|
}
|
|
536
526
|
|
|
537
|
-
function stopStatusRefresh(): void {
|
|
538
|
-
if (statusRefreshTimer) {
|
|
539
|
-
clearInterval(statusRefreshTimer);
|
|
540
|
-
statusRefreshTimer = null;
|
|
541
|
-
}
|
|
542
|
-
statusRefreshCtx = null;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function syncStatusRefresh(ctx: ExtensionContext): void {
|
|
546
|
-
if (!ctx.hasUI || state.goal?.status !== "active") {
|
|
547
|
-
stopStatusRefresh();
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
statusRefreshCtx = ctx;
|
|
551
|
-
if (statusRefreshTimer) return;
|
|
552
|
-
statusRefreshTimer = setInterval(() => {
|
|
553
|
-
if (!statusRefreshCtx || state.goal?.status !== "active") {
|
|
554
|
-
stopStatusRefresh();
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const displayGoal = goalForDisplay();
|
|
558
|
-
if (displayGoal) {
|
|
559
|
-
const otherCount = otherOpenGoalCount(goalsById, focusedGoalId);
|
|
560
|
-
statusRefreshCtx.ui.setStatus("goal", `${footerStatus(displayGoal)}${otherCount > 0 ? ` (+${otherCount} open)` : ""}`);
|
|
561
|
-
}
|
|
562
|
-
// Live-tick the above-editor widget so duration/tokens update.
|
|
563
|
-
goalWidgetComponent?.update();
|
|
564
|
-
}, STATUS_REFRESH_MS);
|
|
565
|
-
statusRefreshTimer.unref?.();
|
|
566
|
-
}
|
|
567
|
-
|
|
568
527
|
function clearContinuationTimer(): void {
|
|
569
528
|
if (continuationTimer) {
|
|
570
529
|
clearTimeout(continuationTimer);
|
|
@@ -583,28 +542,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
583
542
|
accounting.lastAccountedAt = null;
|
|
584
543
|
}
|
|
585
544
|
|
|
586
|
-
function advanceTurnSeq(): void {
|
|
587
|
-
turnSeq += 1;
|
|
588
|
-
if (turnStoppedFor?.turnSeq !== turnSeq) turnStoppedFor = null;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function currentTurnStoppedGoalId(): string | null {
|
|
592
|
-
if (!turnStoppedFor) return null;
|
|
593
|
-
if (turnStoppedFor.turnSeq !== turnSeq) {
|
|
594
|
-
turnStoppedFor = null;
|
|
595
|
-
return null;
|
|
596
|
-
}
|
|
597
|
-
return turnStoppedFor.goalId;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function isActionableContinuationGoal(goalId: string | null | undefined): goalId is string {
|
|
601
|
-
return !!goalId && state.goal?.id === goalId && state.goal.status === "active" && state.goal.autoContinue;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function isStaleCheckpointBlockedToolCall(toolName: string): boolean {
|
|
605
|
-
return !POST_STOP_ALLOWED_TOOL_SET.has(toolName);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
545
|
function clearStoppedRuntimeState(): void {
|
|
609
546
|
clearContinuationState();
|
|
610
547
|
clearActiveAccounting();
|
|
@@ -784,7 +721,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
784
721
|
if (next) state.goal = next.status === "complete" ? archiveGoalFile(ctx, next) : writeActiveGoalFile(ctx, next);
|
|
785
722
|
}
|
|
786
723
|
}
|
|
787
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
788
724
|
syncGoalTools();
|
|
789
725
|
if (ctx) updateUI(ctx);
|
|
790
726
|
}
|
|
@@ -793,7 +729,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
793
729
|
if (!state.goal || state.goal.status === "complete") return;
|
|
794
730
|
if (syncGoalPromptFromDisk(ctx)) {
|
|
795
731
|
state.goal = { ...state.goal, updatedAt: nowIso() };
|
|
796
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
797
732
|
}
|
|
798
733
|
syncGoalTools();
|
|
799
734
|
updateUI(ctx);
|
|
@@ -803,7 +738,10 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
803
738
|
* Live above-editor widget for the active goal. Inspired by rpiv-todo's
|
|
804
739
|
* TodoOverlay: register the widget once with a factory, read live state
|
|
805
740
|
* via the closure at render time, and call `tui.requestRender()` on every
|
|
806
|
-
* state change so the overlay refreshes without re-registration.
|
|
741
|
+
* state change so the overlay refreshes without re-registration. Normal pi
|
|
742
|
+
* renders still read current values through the closure; do not request
|
|
743
|
+
* periodic renders just to tick elapsed time because terminal redraws pull
|
|
744
|
+
* users out of scrollback while they review long goals and earlier context.
|
|
807
745
|
*
|
|
808
746
|
* Layout (sisyphus, running):
|
|
809
747
|
* ◆ Sisyphus [▰▰▰▱▱] 3/5
|
|
@@ -834,7 +772,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
834
772
|
const totalOpen = openGoals().length;
|
|
835
773
|
if (!state.goal && totalOpen === 0) {
|
|
836
774
|
clearGoalWidget(ctx);
|
|
837
|
-
stopStatusRefresh();
|
|
838
775
|
return;
|
|
839
776
|
}
|
|
840
777
|
if (!state.goal) {
|
|
@@ -860,7 +797,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
860
797
|
} else {
|
|
861
798
|
goalWidgetComponent?.update();
|
|
862
799
|
}
|
|
863
|
-
stopStatusRefresh();
|
|
864
800
|
return;
|
|
865
801
|
}
|
|
866
802
|
|
|
@@ -889,12 +825,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
889
825
|
} else {
|
|
890
826
|
goalWidgetComponent?.update();
|
|
891
827
|
}
|
|
892
|
-
|
|
893
|
-
if (state.goal.status === "complete") {
|
|
894
|
-
stopStatusRefresh();
|
|
895
|
-
} else {
|
|
896
|
-
syncStatusRefresh(ctx);
|
|
897
|
-
}
|
|
898
828
|
}
|
|
899
829
|
|
|
900
830
|
function loadState(ctx: ExtensionContext): void {
|
|
@@ -919,7 +849,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
919
849
|
if (legacyGoal && legacyGoal.status !== "complete") {
|
|
920
850
|
legacyGoal = sanitizeGoalPaths(ctx, mergeGoalPromptFromDisk(ctx, legacyGoal));
|
|
921
851
|
}
|
|
922
|
-
|
|
852
|
+
const settings = loadGoalSettings(ctx.cwd);
|
|
853
|
+
focusedGoalId = resolveSessionFocus({ pool: goalsById, focusEntry, legacyGoal, autoSelectSingleGoal: settings.autoSelectSingleGoal });
|
|
923
854
|
if (!focusEntry && focusedGoalId) {
|
|
924
855
|
try {
|
|
925
856
|
appendFocusEntry(focusedGoalId, legacyGoal?.id === focusedGoalId ? "migrated" : "selected");
|
|
@@ -1314,7 +1245,7 @@ Verification contract:
|
|
|
1314
1245
|
continuationTimer = null;
|
|
1315
1246
|
continuationScheduledFor = null;
|
|
1316
1247
|
syncGoalTools();
|
|
1317
|
-
if (!
|
|
1248
|
+
if (!state.goal || state.goal.id !== goalId || state.goal.status !== "active" || !state.goal.autoContinue) {
|
|
1318
1249
|
if (continuationQueuedFor === goalId) continuationQueuedFor = null;
|
|
1319
1250
|
return;
|
|
1320
1251
|
}
|
|
@@ -1651,6 +1582,7 @@ Verification contract:
|
|
|
1651
1582
|
if (key === "disabled") return config.disabled === true ? "true" : "false";
|
|
1652
1583
|
if (key === "disableTasks") return config.disableTasks === true ? "true" : "false";
|
|
1653
1584
|
if (key === "disableContracts") return config.disableContracts === true ? "true" : "false";
|
|
1585
|
+
if (key === "autoSelectSingleGoal") return config.autoSelectSingleGoal === true ? "true" : "false";
|
|
1654
1586
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
1655
1587
|
return config[key] ?? "(default)";
|
|
1656
1588
|
}
|
|
@@ -1672,7 +1604,7 @@ Verification contract:
|
|
|
1672
1604
|
ctx.ui.notify(`Settings file: ${goalSettingsPath(ctx.cwd)}`, "info");
|
|
1673
1605
|
return;
|
|
1674
1606
|
}
|
|
1675
|
-
const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth"] as const;
|
|
1607
|
+
const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth", "autoSelectSingleGoal"] as const;
|
|
1676
1608
|
while (true) {
|
|
1677
1609
|
const config = loadGoalSettingsFileConfig(ctx.cwd);
|
|
1678
1610
|
const options = settingsLines(config).map((line) => ` ${line}`);
|
|
@@ -1694,6 +1626,12 @@ Verification contract:
|
|
|
1694
1626
|
ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
|
|
1695
1627
|
continue;
|
|
1696
1628
|
}
|
|
1629
|
+
if (key === "autoSelectSingleGoal") {
|
|
1630
|
+
const next = { ...config, autoSelectSingleGoal: config.autoSelectSingleGoal !== true };
|
|
1631
|
+
saveGoalSettingsFileConfig(ctx.cwd, next);
|
|
1632
|
+
ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1697
1635
|
if (key === "subtaskDepth") {
|
|
1698
1636
|
const input = await ctx.ui.input("Set subtaskDepth", String(config.subtaskDepth ?? 1));
|
|
1699
1637
|
if (input === undefined) continue;
|
|
@@ -2275,10 +2213,9 @@ ${objective}` : objective,
|
|
|
2275
2213
|
// minimal state update manually:
|
|
2276
2214
|
// 1) write the new record to disk authoritatively
|
|
2277
2215
|
// 2) update in-memory `goal` to the canonical post-write record
|
|
2278
|
-
// 3)
|
|
2216
|
+
// 3) re-sync tools
|
|
2279
2217
|
// 4) clear the tweak drafting gate so propose_goal_tweak can't be re-used
|
|
2280
2218
|
state.goal = writeActiveGoalFile(ctx, next);
|
|
2281
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2282
2219
|
tweakDraftingFor = null;
|
|
2283
2220
|
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
2284
2221
|
resetGetGoalNudgeState(state.goal.id);
|
|
@@ -2442,7 +2379,6 @@ ${objective}` : objective,
|
|
|
2442
2379
|
updatedAt: nowIso(),
|
|
2443
2380
|
};
|
|
2444
2381
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2445
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2446
2382
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2447
2383
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2448
2384
|
syncGoalTools();
|
|
@@ -2509,7 +2445,6 @@ ${objective}` : objective,
|
|
|
2509
2445
|
updatedAt: nowIso(),
|
|
2510
2446
|
};
|
|
2511
2447
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2512
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2513
2448
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2514
2449
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2515
2450
|
syncGoalTools();
|
|
@@ -2639,7 +2574,6 @@ ${objective}` : objective,
|
|
|
2639
2574
|
updatedAt: nowIso(),
|
|
2640
2575
|
};
|
|
2641
2576
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2642
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2643
2577
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2644
2578
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2645
2579
|
syncGoalTools();
|
|
@@ -2740,7 +2674,6 @@ ${objective}` : objective,
|
|
|
2740
2674
|
updatedAt: nowIso(),
|
|
2741
2675
|
};
|
|
2742
2676
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2743
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2744
2677
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2745
2678
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2746
2679
|
syncGoalTools();
|
|
@@ -3334,8 +3267,8 @@ promptGuidelines: [
|
|
|
3334
3267
|
|
|
3335
3268
|
pi.on("turn_start", async (_event, ctx) => {
|
|
3336
3269
|
// Per-turn flag resets (#4 + C9 fix).
|
|
3337
|
-
advanceTurnSeq();
|
|
3338
3270
|
goalWorkToolCalledThisTurn = false;
|
|
3271
|
+
turnStoppedFor = null;
|
|
3339
3272
|
syncGoalTools();
|
|
3340
3273
|
beginAccounting();
|
|
3341
3274
|
updateUI(ctx);
|
|
@@ -3343,29 +3276,17 @@ promptGuidelines: [
|
|
|
3343
3276
|
|
|
3344
3277
|
// #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
|
|
3345
3278
|
pi.on("tool_call", async (event, ctx) => {
|
|
3346
|
-
const stoppedGoalId = currentTurnStoppedGoalId();
|
|
3347
3279
|
// Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
|
|
3348
3280
|
// complete_goal / propose_goal_tweak fires in this turn, block all subsequent tool calls except
|
|
3349
3281
|
// read-only inspection. Forces the agent to yield the turn instead of "fixing"
|
|
3350
3282
|
// the situation by creating extra files etc.
|
|
3351
|
-
if (
|
|
3283
|
+
if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
|
|
3352
3284
|
return {
|
|
3353
3285
|
block: true,
|
|
3354
|
-
reason: `The goal was already stopped earlier in this turn (goalId=${
|
|
3286
|
+
reason: `The goal was already stopped earlier in this turn (goalId=${turnStoppedFor}). ` +
|
|
3355
3287
|
`Do not call more tools; end the turn with a brief summary and yield to the user.`,
|
|
3356
3288
|
};
|
|
3357
3289
|
}
|
|
3358
|
-
// Stale checkpoint guard: if the turn was triggered by a queued continuation
|
|
3359
|
-
// for a goal that is no longer active/autoContinue, block work tools.
|
|
3360
|
-
if (checkpointGoalId !== null && !isActionableContinuationGoal(checkpointGoalId) && isStaleCheckpointBlockedToolCall(event.toolName)) {
|
|
3361
|
-
// Block the tool call with a stale-checkpoint message.
|
|
3362
|
-
return {
|
|
3363
|
-
block: true,
|
|
3364
|
-
reason: `Cannot call ${event.toolName}: the goal checkpoint that triggered this turn is no longer active. ` +
|
|
3365
|
-
`Goal ${checkpointGoalId} has been paused, cleared, or replaced. ` +
|
|
3366
|
-
`End the turn with a brief summary and yield to the user.`,
|
|
3367
|
-
};
|
|
3368
|
-
}
|
|
3369
3290
|
// Phase 5 soft gate relaxation: active-goal question block and repeated get_goal
|
|
3370
3291
|
// block are removed. The agent is trusted to prefer work tools; prompts nudge
|
|
3371
3292
|
// toward concrete work without hard-stopping the turn.
|
|
@@ -3446,8 +3367,8 @@ promptGuidelines: [
|
|
|
3446
3367
|
});
|
|
3447
3368
|
|
|
3448
3369
|
pi.on("session_start", async (event, ctx) => {
|
|
3449
|
-
loadState(ctx);
|
|
3450
3370
|
syncGoalTools();
|
|
3371
|
+
loadState(ctx);
|
|
3451
3372
|
syncTerminalInputPause(ctx);
|
|
3452
3373
|
if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
|
|
3453
3374
|
await focusGoalCommand(ctx);
|
|
@@ -3488,13 +3409,11 @@ promptGuidelines: [
|
|
|
3488
3409
|
});
|
|
3489
3410
|
|
|
3490
3411
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
3491
|
-
advanceTurnSeq();
|
|
3492
3412
|
syncGoalTools();
|
|
3493
3413
|
const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
|
|
3494
3414
|
const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
|
|
3495
3415
|
|
|
3496
3416
|
if (confirmationIntent !== null) {
|
|
3497
|
-
checkpointGoalId = null;
|
|
3498
3417
|
clearContinuationState();
|
|
3499
3418
|
clearActiveAccounting();
|
|
3500
3419
|
runningGoalId = null;
|
|
@@ -3502,7 +3421,6 @@ promptGuidelines: [
|
|
|
3502
3421
|
}
|
|
3503
3422
|
|
|
3504
3423
|
if (tweakDraftingFor !== null) {
|
|
3505
|
-
checkpointGoalId = null;
|
|
3506
3424
|
clearContinuationState();
|
|
3507
3425
|
clearActiveAccounting();
|
|
3508
3426
|
runningGoalId = null;
|
|
@@ -3513,12 +3431,8 @@ promptGuidelines: [
|
|
|
3513
3431
|
// matches the active goal, abort the whole turn instead of letting the
|
|
3514
3432
|
// model act on a stale instruction.
|
|
3515
3433
|
if (incomingGoalId !== null) {
|
|
3516
|
-
// Reconcile from disk to pick up any external state changes before
|
|
3517
|
-
// evaluating whether the checkpoint is actionable.
|
|
3518
|
-
reconcileFocusedGoalFromDisk(ctx);
|
|
3519
|
-
checkpointGoalId = incomingGoalId;
|
|
3520
3434
|
clearContinuationState();
|
|
3521
|
-
if (!
|
|
3435
|
+
if (!state.goal || state.goal.id !== incomingGoalId || (state.goal.status !== "active") || !state.goal.autoContinue) {
|
|
3522
3436
|
try {
|
|
3523
3437
|
ctx.abort?.();
|
|
3524
3438
|
} catch {}
|
|
@@ -3527,12 +3441,10 @@ promptGuidelines: [
|
|
|
3527
3441
|
systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, state.goal)}`,
|
|
3528
3442
|
};
|
|
3529
3443
|
}
|
|
3530
|
-
checkpointGoalId = null;
|
|
3531
3444
|
} else {
|
|
3532
3445
|
// A user-driven turn — clear any queued continuation so we don't
|
|
3533
3446
|
// double-fire after the user's own message returns. Also reset the
|
|
3534
3447
|
// autoContinue nudge state so the user always gets a fresh chain.
|
|
3535
|
-
checkpointGoalId = null;
|
|
3536
3448
|
clearContinuationState();
|
|
3537
3449
|
resetGetGoalNudgeState(state.goal?.id);
|
|
3538
3450
|
}
|
|
@@ -3634,7 +3546,6 @@ promptGuidelines: [
|
|
|
3634
3546
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
3635
3547
|
accountProgress(ctx);
|
|
3636
3548
|
clearContinuationTimer();
|
|
3637
|
-
stopStatusRefresh();
|
|
3638
3549
|
terminalInputUnsubscribe?.();
|
|
3639
3550
|
terminalInputUnsubscribe = null;
|
|
3640
3551
|
if (state.goal) persist(ctx);
|
|
@@ -179,7 +179,7 @@ export function renderAuditorWidgetLines(progress: AuditorWidgetProgress, theme:
|
|
|
179
179
|
? truncateText(progress.currentToolArgs, Math.max(10, safeWidth - 24))
|
|
180
180
|
: "";
|
|
181
181
|
const toolDuration = progress.currentToolStartedAt
|
|
182
|
-
? ` ${theme.fg("dim", formatDuration(Date.now() - progress.currentToolStartedAt))}`
|
|
182
|
+
? ` ${theme.fg("dim", formatDuration(Math.floor((Date.now() - progress.currentToolStartedAt) / 1000)))}`
|
|
183
183
|
: "";
|
|
184
184
|
lines.push(branchLine(
|
|
185
185
|
theme,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|