pi-goal-x 0.19.0 → 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-draft.ts +51 -55
- package/extensions/goal-pool.ts +9 -2
- package/extensions/goal-settings.ts +6 -0
- package/extensions/goal.ts +37 -181
- package/extensions/prompts/goal-prompts.ts +0 -11
- 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-draft.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { GoalTask } from "./goal-record.ts";
|
|
2
|
-
|
|
3
1
|
export type GoalDraftingFocus = "goal" | "sisyphus";
|
|
4
2
|
|
|
5
3
|
export interface GoalConfirmationIntentLike {
|
|
@@ -24,45 +22,6 @@ export type ToolGateDecision =
|
|
|
24
22
|
| { block: false }
|
|
25
23
|
| { block: true; reason: string };
|
|
26
24
|
|
|
27
|
-
// ── Shared formatting helpers ──────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
function formatModeLabel(sisyphus: boolean): string {
|
|
30
|
-
return sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function formatPrefixedLines(content: string): string[] {
|
|
34
|
-
const lines: string[] = [];
|
|
35
|
-
for (const rawLine of content.split("\n")) {
|
|
36
|
-
const trimmed = rawLine.trim();
|
|
37
|
-
if (!trimmed) continue;
|
|
38
|
-
if (trimmed.startsWith("│")) {
|
|
39
|
-
lines.push(rawLine);
|
|
40
|
-
} else {
|
|
41
|
-
lines.push(`│ ${rawLine}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return lines;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function formatSection(title: string, content: string): string[] {
|
|
48
|
-
const body = formatPrefixedLines(content);
|
|
49
|
-
return ["", `─── ${title} ───`, "", ...body];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function renderConfirmationTasks(tasks: GoalTask[], indent: number): string[] {
|
|
53
|
-
const prefix = " ".repeat(indent);
|
|
54
|
-
const lines: string[] = [];
|
|
55
|
-
for (const t of tasks) {
|
|
56
|
-
const lw = t.lightweightSubtasks ? " (lightweight)" : "";
|
|
57
|
-
const contract = t.verificationContract ? ` contract: ${t.verificationContract}` : "";
|
|
58
|
-
lines.push(`${prefix}[ ] ${t.id}: ${t.title}${lw}${contract}`);
|
|
59
|
-
if (t.subtasks && t.subtasks.length > 0) {
|
|
60
|
-
lines.push(...renderConfirmationTasks(t.subtasks, indent + 1));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return lines;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
25
|
export function promptSafeObjective(objective: string): string {
|
|
67
26
|
return objective.replace(/<\/?untrusted_objective>/gi, (tag) => tag.replace(/</g, "<").replace(/>/g, ">"));
|
|
68
27
|
}
|
|
@@ -125,13 +84,30 @@ export function buildDraftConfirmationText(args: {
|
|
|
125
84
|
autoContinue: boolean;
|
|
126
85
|
}): string {
|
|
127
86
|
const lines: string[] = [];
|
|
87
|
+
const modeLabel = args.focus === "sisyphus" ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
128
88
|
lines.push("● Goal draft ready for confirmation.");
|
|
129
89
|
lines.push("");
|
|
130
90
|
lines.push("─── Draft Details ───");
|
|
131
|
-
lines.push(`│ Mode: ${
|
|
91
|
+
lines.push(`│ Mode: ${modeLabel}`);
|
|
132
92
|
lines.push(`│ Auto-continue: ${args.autoContinue ? "yes" : "no"}`);
|
|
133
|
-
lines.push(
|
|
134
|
-
lines.push(
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push("─── Original Topic ───");
|
|
95
|
+
lines.push("");
|
|
96
|
+
for (const topicLine of args.originalTopic.trim().split("\n")) {
|
|
97
|
+
if (topicLine.trim()) lines.push(`│ ${topicLine}`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("─── Proposed Goal ───");
|
|
101
|
+
lines.push("");
|
|
102
|
+
for (const objLine of args.objective.split("\n")) {
|
|
103
|
+
const trimmed = objLine.trim();
|
|
104
|
+
if (!trimmed) continue;
|
|
105
|
+
if (trimmed.startsWith("│")) {
|
|
106
|
+
lines.push(objLine);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(`│ ${objLine}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
135
111
|
return lines.join("\n");
|
|
136
112
|
}
|
|
137
113
|
|
|
@@ -140,22 +116,42 @@ export function buildTweakConfirmationText(args: {
|
|
|
140
116
|
newObjective: string;
|
|
141
117
|
changeSummary: string;
|
|
142
118
|
sisyphus: boolean;
|
|
143
|
-
tasks?: GoalTask[];
|
|
144
119
|
}): string {
|
|
145
120
|
const lines: string[] = [];
|
|
121
|
+
const modeLabel = args.sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
146
122
|
lines.push("● Goal tweak ready for confirmation.");
|
|
147
123
|
lines.push("");
|
|
148
124
|
lines.push("─── Draft Details ───");
|
|
149
|
-
lines.push(`│ Mode: ${
|
|
150
|
-
lines.push(
|
|
151
|
-
lines.push(
|
|
152
|
-
lines.push(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
125
|
+
lines.push(`│ Mode: ${modeLabel}`);
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("─── Change ───");
|
|
128
|
+
lines.push("");
|
|
129
|
+
for (const changeLine of args.changeSummary.split("\n")) {
|
|
130
|
+
if (changeLine.trim()) lines.push(`│ ${changeLine}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("─── Current Objective ───");
|
|
134
|
+
lines.push("");
|
|
135
|
+
for (const curLine of args.currentObjective.split("\n")) {
|
|
136
|
+
const trimmed = curLine.trim();
|
|
137
|
+
if (!trimmed) continue;
|
|
138
|
+
if (trimmed.startsWith("│")) {
|
|
139
|
+
lines.push(curLine);
|
|
140
|
+
} else {
|
|
141
|
+
lines.push(`│ ${curLine}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("─── Proposed New Objective ───");
|
|
146
|
+
lines.push("");
|
|
147
|
+
for (const newLine of args.newObjective.split("\n")) {
|
|
148
|
+
const trimmed = newLine.trim();
|
|
149
|
+
if (!trimmed) continue;
|
|
150
|
+
if (trimmed.startsWith("│")) {
|
|
151
|
+
lines.push(newLine);
|
|
152
|
+
} else {
|
|
153
|
+
lines.push(`│ ${newLine}`);
|
|
154
|
+
}
|
|
159
155
|
}
|
|
160
156
|
return lines.join("\n");
|
|
161
157
|
}
|
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
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
buildTweakConfirmationText,
|
|
14
14
|
extractVerificationContract,
|
|
15
15
|
goalDraftingPrompt,
|
|
16
|
-
renderConfirmationTasks,
|
|
17
16
|
validateGoalDraftProposal,
|
|
18
17
|
type GoalDraftingFocus,
|
|
19
18
|
} from "./goal-draft.ts";
|
|
@@ -147,7 +146,6 @@ const GOAL_EVENT_ENTRY = "pi-goal-event";
|
|
|
147
146
|
const GOAL_AUDIT_ENTRY = "pi-goal-audit-event";
|
|
148
147
|
const COMPLETE_STATUS = "complete";
|
|
149
148
|
const CONTINUATION_IDLE_RETRY_MS = 50;
|
|
150
|
-
const STATUS_REFRESH_MS = 1000;
|
|
151
149
|
/**
|
|
152
150
|
* Tools that count as "real work" toward the active goal. If a non-tool-use
|
|
153
151
|
* turn ends without any of these having been called, we DO NOT queue the next
|
|
@@ -411,8 +409,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
411
409
|
let continuationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
412
410
|
let runningGoalId: string | null = null;
|
|
413
411
|
let terminalInputUnsubscribe: (() => void) | null = null;
|
|
414
|
-
let statusRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
415
|
-
let statusRefreshCtx: ExtensionContext | null = null;
|
|
416
412
|
let auditProgress: AuditorWidgetProgress | null = null;
|
|
417
413
|
let auditAnimationTimer: ReturnType<typeof setInterval> | null = null;
|
|
418
414
|
let auditAbortController: AbortController | null = null;
|
|
@@ -430,15 +426,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
430
426
|
// after their successful execute. Once set, pi.on("tool_call") blocks all
|
|
431
427
|
// subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
|
|
432
428
|
// schema fix for "agent keeps writing files after pause_goal".
|
|
433
|
-
// turnSeq: lightweight generation counter, incremented at each turn start.
|
|
434
|
-
// Used to scope turnStoppedFor so stale markers from prior turns or sessions
|
|
435
|
-
// cannot poison a resumed active goal.
|
|
436
|
-
// checkpointGoalId: remembers the incoming goal id from queued continuations.
|
|
437
|
-
// When set but the goal is no longer active/autoContinue, work tools are blocked.
|
|
438
429
|
let goalWorkToolCalledThisTurn = false;
|
|
439
|
-
let
|
|
440
|
-
let turnStoppedFor: { goalId: string; turnSeq: number } | null = null;
|
|
441
|
-
let checkpointGoalId: string | null = null;
|
|
430
|
+
let turnStoppedFor: string | null = null;
|
|
442
431
|
|
|
443
432
|
// #5 post-compaction resync: when a compaction just happened, the next agent
|
|
444
433
|
// turn gets an extra reminder block. Set in session_compact, consumed
|
|
@@ -535,37 +524,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
535
524
|
}
|
|
536
525
|
}
|
|
537
526
|
|
|
538
|
-
function stopStatusRefresh(): void {
|
|
539
|
-
if (statusRefreshTimer) {
|
|
540
|
-
clearInterval(statusRefreshTimer);
|
|
541
|
-
statusRefreshTimer = null;
|
|
542
|
-
}
|
|
543
|
-
statusRefreshCtx = null;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function syncStatusRefresh(ctx: ExtensionContext): void {
|
|
547
|
-
if (!ctx.hasUI || state.goal?.status !== "active") {
|
|
548
|
-
stopStatusRefresh();
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
statusRefreshCtx = ctx;
|
|
552
|
-
if (statusRefreshTimer) return;
|
|
553
|
-
statusRefreshTimer = setInterval(() => {
|
|
554
|
-
if (!statusRefreshCtx || state.goal?.status !== "active") {
|
|
555
|
-
stopStatusRefresh();
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const displayGoal = goalForDisplay();
|
|
559
|
-
if (displayGoal) {
|
|
560
|
-
const otherCount = otherOpenGoalCount(goalsById, focusedGoalId);
|
|
561
|
-
statusRefreshCtx.ui.setStatus("goal", `${footerStatus(displayGoal)}${otherCount > 0 ? ` (+${otherCount} open)` : ""}`);
|
|
562
|
-
}
|
|
563
|
-
// Live-tick the above-editor widget so duration/tokens update.
|
|
564
|
-
goalWidgetComponent?.update();
|
|
565
|
-
}, STATUS_REFRESH_MS);
|
|
566
|
-
statusRefreshTimer.unref?.();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
527
|
function clearContinuationTimer(): void {
|
|
570
528
|
if (continuationTimer) {
|
|
571
529
|
clearTimeout(continuationTimer);
|
|
@@ -584,28 +542,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
584
542
|
accounting.lastAccountedAt = null;
|
|
585
543
|
}
|
|
586
544
|
|
|
587
|
-
function advanceTurnSeq(): void {
|
|
588
|
-
turnSeq += 1;
|
|
589
|
-
if (turnStoppedFor?.turnSeq !== turnSeq) turnStoppedFor = null;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function currentTurnStoppedGoalId(): string | null {
|
|
593
|
-
if (!turnStoppedFor) return null;
|
|
594
|
-
if (turnStoppedFor.turnSeq !== turnSeq) {
|
|
595
|
-
turnStoppedFor = null;
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
return turnStoppedFor.goalId;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function isActionableContinuationGoal(goalId: string | null | undefined): goalId is string {
|
|
602
|
-
return !!goalId && state.goal?.id === goalId && state.goal.status === "active" && state.goal.autoContinue;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function isStaleCheckpointBlockedToolCall(toolName: string): boolean {
|
|
606
|
-
return !POST_STOP_ALLOWED_TOOL_SET.has(toolName);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
545
|
function clearStoppedRuntimeState(): void {
|
|
610
546
|
clearContinuationState();
|
|
611
547
|
clearActiveAccounting();
|
|
@@ -785,7 +721,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
785
721
|
if (next) state.goal = next.status === "complete" ? archiveGoalFile(ctx, next) : writeActiveGoalFile(ctx, next);
|
|
786
722
|
}
|
|
787
723
|
}
|
|
788
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
789
724
|
syncGoalTools();
|
|
790
725
|
if (ctx) updateUI(ctx);
|
|
791
726
|
}
|
|
@@ -794,7 +729,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
794
729
|
if (!state.goal || state.goal.status === "complete") return;
|
|
795
730
|
if (syncGoalPromptFromDisk(ctx)) {
|
|
796
731
|
state.goal = { ...state.goal, updatedAt: nowIso() };
|
|
797
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
798
732
|
}
|
|
799
733
|
syncGoalTools();
|
|
800
734
|
updateUI(ctx);
|
|
@@ -804,7 +738,10 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
804
738
|
* Live above-editor widget for the active goal. Inspired by rpiv-todo's
|
|
805
739
|
* TodoOverlay: register the widget once with a factory, read live state
|
|
806
740
|
* via the closure at render time, and call `tui.requestRender()` on every
|
|
807
|
-
* 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.
|
|
808
745
|
*
|
|
809
746
|
* Layout (sisyphus, running):
|
|
810
747
|
* ◆ Sisyphus [▰▰▰▱▱] 3/5
|
|
@@ -835,7 +772,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
835
772
|
const totalOpen = openGoals().length;
|
|
836
773
|
if (!state.goal && totalOpen === 0) {
|
|
837
774
|
clearGoalWidget(ctx);
|
|
838
|
-
stopStatusRefresh();
|
|
839
775
|
return;
|
|
840
776
|
}
|
|
841
777
|
if (!state.goal) {
|
|
@@ -861,7 +797,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
861
797
|
} else {
|
|
862
798
|
goalWidgetComponent?.update();
|
|
863
799
|
}
|
|
864
|
-
stopStatusRefresh();
|
|
865
800
|
return;
|
|
866
801
|
}
|
|
867
802
|
|
|
@@ -890,12 +825,6 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
890
825
|
} else {
|
|
891
826
|
goalWidgetComponent?.update();
|
|
892
827
|
}
|
|
893
|
-
|
|
894
|
-
if (state.goal.status === "complete") {
|
|
895
|
-
stopStatusRefresh();
|
|
896
|
-
} else {
|
|
897
|
-
syncStatusRefresh(ctx);
|
|
898
|
-
}
|
|
899
828
|
}
|
|
900
829
|
|
|
901
830
|
function loadState(ctx: ExtensionContext): void {
|
|
@@ -920,7 +849,8 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
920
849
|
if (legacyGoal && legacyGoal.status !== "complete") {
|
|
921
850
|
legacyGoal = sanitizeGoalPaths(ctx, mergeGoalPromptFromDisk(ctx, legacyGoal));
|
|
922
851
|
}
|
|
923
|
-
|
|
852
|
+
const settings = loadGoalSettings(ctx.cwd);
|
|
853
|
+
focusedGoalId = resolveSessionFocus({ pool: goalsById, focusEntry, legacyGoal, autoSelectSingleGoal: settings.autoSelectSingleGoal });
|
|
924
854
|
if (!focusEntry && focusedGoalId) {
|
|
925
855
|
try {
|
|
926
856
|
appendFocusEntry(focusedGoalId, legacyGoal?.id === focusedGoalId ? "migrated" : "selected");
|
|
@@ -1315,7 +1245,7 @@ Verification contract:
|
|
|
1315
1245
|
continuationTimer = null;
|
|
1316
1246
|
continuationScheduledFor = null;
|
|
1317
1247
|
syncGoalTools();
|
|
1318
|
-
if (!
|
|
1248
|
+
if (!state.goal || state.goal.id !== goalId || state.goal.status !== "active" || !state.goal.autoContinue) {
|
|
1319
1249
|
if (continuationQueuedFor === goalId) continuationQueuedFor = null;
|
|
1320
1250
|
return;
|
|
1321
1251
|
}
|
|
@@ -1652,6 +1582,7 @@ Verification contract:
|
|
|
1652
1582
|
if (key === "disabled") return config.disabled === true ? "true" : "false";
|
|
1653
1583
|
if (key === "disableTasks") return config.disableTasks === true ? "true" : "false";
|
|
1654
1584
|
if (key === "disableContracts") return config.disableContracts === true ? "true" : "false";
|
|
1585
|
+
if (key === "autoSelectSingleGoal") return config.autoSelectSingleGoal === true ? "true" : "false";
|
|
1655
1586
|
if (key === "subtaskDepth") return config.subtaskDepth !== undefined ? String(config.subtaskDepth) : "1";
|
|
1656
1587
|
return config[key] ?? "(default)";
|
|
1657
1588
|
}
|
|
@@ -1673,7 +1604,7 @@ Verification contract:
|
|
|
1673
1604
|
ctx.ui.notify(`Settings file: ${goalSettingsPath(ctx.cwd)}`, "info");
|
|
1674
1605
|
return;
|
|
1675
1606
|
}
|
|
1676
|
-
const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth"] as const;
|
|
1607
|
+
const editorKeys = ["disabled", "provider", "model", "thinking_level", "subtaskDepth", "autoSelectSingleGoal"] as const;
|
|
1677
1608
|
while (true) {
|
|
1678
1609
|
const config = loadGoalSettingsFileConfig(ctx.cwd);
|
|
1679
1610
|
const options = settingsLines(config).map((line) => ` ${line}`);
|
|
@@ -1695,6 +1626,12 @@ Verification contract:
|
|
|
1695
1626
|
ctx.ui.notify(`Settings saved:\n${settingsLines(loadGoalSettingsFileConfig(ctx.cwd)).join("\n")}`, "info");
|
|
1696
1627
|
continue;
|
|
1697
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
|
+
}
|
|
1698
1635
|
if (key === "subtaskDepth") {
|
|
1699
1636
|
const input = await ctx.ui.input("Set subtaskDepth", String(config.subtaskDepth ?? 1));
|
|
1700
1637
|
if (input === undefined) continue;
|
|
@@ -2027,6 +1964,20 @@ Verification contract:
|
|
|
2027
1964
|
const autoContinueFlag = params.autoContinue ?? true;
|
|
2028
1965
|
const sisyphusFlag = validation.expectedSisyphus;
|
|
2029
1966
|
// Build confirmation text: goal + optional task list
|
|
1967
|
+
function renderConfirmationTasks(tasks: GoalTask[], indent: number): string[] {
|
|
1968
|
+
const prefix = " ".repeat(indent);
|
|
1969
|
+
const lines: string[] = [];
|
|
1970
|
+
for (const t of tasks) {
|
|
1971
|
+
const lw = t.lightweightSubtasks ? " (lightweight)" : "";
|
|
1972
|
+
const contract = t.verificationContract ? ` contract: ${t.verificationContract}` : "";
|
|
1973
|
+
lines.push(`${prefix}[ ] ${t.id}: ${t.title}${lw}${contract}`);
|
|
1974
|
+
if (t.subtasks && t.subtasks.length > 0) {
|
|
1975
|
+
lines.push(...renderConfirmationTasks(t.subtasks, indent + 1));
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return lines;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
2030
1981
|
let taskSummarySection = "";
|
|
2031
1982
|
let tasksToCreate: GoalTask[] | undefined;
|
|
2032
1983
|
if (params.tasks && params.tasks.length > 0) {
|
|
@@ -2173,25 +2124,10 @@ ${objective}` : objective,
|
|
|
2173
2124
|
"The user will see a full plain-text report plus a [Confirm] / [Continue Chatting] choice. Confirm applies the tweak; Continue Chatting returns control to you to ask follow-up questions.",
|
|
2174
2125
|
"If the tool returns 'continue chatting', ask the user what they want changed. Do NOT re-propose the same content immediately; iterate based on their feedback first.",
|
|
2175
2126
|
"Do NOT use write/edit/bash to modify the active goal file directly. propose_goal_tweak is the only sanctioned channel.",
|
|
2176
|
-
"tasks (optional): an array of task objects to REPLACE the current goal's task list. When omitted,",
|
|
2177
|
-
" the existing task list is inherited as-is. When provided, the full task list is replaced with",
|
|
2178
|
-
" the new tasks — add/remove/change tasks by passing the complete updated list.",
|
|
2179
|
-
" Each task has: {id, title, verificationContract?, lightweightSubtasks?, subtasks?}.",
|
|
2180
|
-
" Subtasks use the same shape recursively. The task list is displayed in the confirmation",
|
|
2181
|
-
" dialog alongside the objective diff, matching propose_goal_draft's rendering format.",
|
|
2182
|
-
"Start from the current goal's objective text and task list (the inheritance defaults), then",
|
|
2183
|
-
" edit/rewrite them directly rather than composing a goal from scratch.",
|
|
2184
2127
|
],
|
|
2185
2128
|
parameters: Type.Object({
|
|
2186
2129
|
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
2187
|
-
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog
|
|
2188
|
-
tasks: Type.Optional(Type.Array(Type.Object({
|
|
2189
|
-
id: Type.String({ description: "Short stable slug e.g. 'task-1'" }),
|
|
2190
|
-
title: Type.String({ description: "Human-readable task title" }),
|
|
2191
|
-
verificationContract: Type.Optional(Type.String({ description: "Optional verification contract for this task." })),
|
|
2192
|
-
lightweightSubtasks: Type.Optional(Type.Boolean({ description: "If true, subtasks are lightweight (no completion enforcement). Default false." })),
|
|
2193
|
-
subtasks: Type.Optional(Type.Any({ description: "Optional recursive array of sub-tasks (same shape as parent)." })),
|
|
2194
|
-
}), { description: "Optional task list to replace the current goal's tasks. When omitted the existing task list is inherited. Each task supports recursive subtasks." })),
|
|
2130
|
+
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog and tweak log)." }),
|
|
2195
2131
|
}),
|
|
2196
2132
|
executionMode: "sequential",
|
|
2197
2133
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -2213,41 +2149,6 @@ ${objective}` : objective,
|
|
|
2213
2149
|
const changeSummary = params.changeSummary.trim();
|
|
2214
2150
|
if (!changeSummary) throw new Error("propose_goal_tweak requires a non-empty changeSummary.");
|
|
2215
2151
|
|
|
2216
|
-
// Resolve tasks: inherit from current goal if not provided, or normalize/validate the passed tasks.
|
|
2217
|
-
let tweakTasks: GoalTask[] | undefined;
|
|
2218
|
-
if (params.tasks && Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
2219
|
-
tweakTasks = params.tasks.map((t: Record<string, unknown>) => {
|
|
2220
|
-
const task: GoalTask = {
|
|
2221
|
-
id: t.id as string,
|
|
2222
|
-
title: t.title as string,
|
|
2223
|
-
status: "pending",
|
|
2224
|
-
verificationContract: t.verificationContract as string | undefined,
|
|
2225
|
-
lightweightSubtasks: t.lightweightSubtasks === true ? true : undefined,
|
|
2226
|
-
};
|
|
2227
|
-
const rawSubtasks = t.subtasks;
|
|
2228
|
-
if (Array.isArray(rawSubtasks) && rawSubtasks.length > 0) {
|
|
2229
|
-
task.subtasks = rawSubtasks
|
|
2230
|
-
.map((s: Record<string, unknown>) => normalizeTaskItem(s))
|
|
2231
|
-
.filter((s: GoalTask | undefined): s is GoalTask => !!s);
|
|
2232
|
-
}
|
|
2233
|
-
return task;
|
|
2234
|
-
});
|
|
2235
|
-
// Validate task list
|
|
2236
|
-
if (tweakTasks.length > 50) {
|
|
2237
|
-
return { content: [{ type: "text", text: "Task list cannot exceed 50 tasks." }], details: goalDetails(state.goal) };
|
|
2238
|
-
}
|
|
2239
|
-
const settings = loadGoalSettings(ctx.cwd);
|
|
2240
|
-
const depthViolation = findSubtaskDepthViolation(tweakTasks, settings.subtaskDepth ?? 1);
|
|
2241
|
-
if (depthViolation) {
|
|
2242
|
-
return { content: [{ type: "text", text: depthViolation }], details: goalDetails(state.goal) };
|
|
2243
|
-
}
|
|
2244
|
-
} else {
|
|
2245
|
-
// Inherit current task list when no tasks parameter provided
|
|
2246
|
-
if (state.goal.taskList && state.goal.taskList.tasks.length > 0) {
|
|
2247
|
-
tweakTasks = state.goal.taskList.tasks;
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
2152
|
// Auto-start the tweak drafting flow if not already active.
|
|
2252
2153
|
if (tweakDraftingFor !== state.goal.id) {
|
|
2253
2154
|
await startGoalTweakDrafting(changeSummary, ctx);
|
|
@@ -2267,7 +2168,6 @@ ${objective}` : objective,
|
|
|
2267
2168
|
newObjective,
|
|
2268
2169
|
changeSummary,
|
|
2269
2170
|
sisyphus: !!state.goal.sisyphus,
|
|
2270
|
-
tasks: tweakTasks,
|
|
2271
2171
|
});
|
|
2272
2172
|
|
|
2273
2173
|
const headless = shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM });
|
|
@@ -2304,9 +2204,6 @@ ${objective}` : objective,
|
|
|
2304
2204
|
// Clear any prior agent pause reason — the user has redefined the work.
|
|
2305
2205
|
pauseReason: undefined,
|
|
2306
2206
|
pauseSuggestedAction: undefined,
|
|
2307
|
-
taskList: tweakTasks && tweakTasks.length > 0
|
|
2308
|
-
? { tasks: tweakTasks, blockCompletion: false, proposedAt: nowIso() }
|
|
2309
|
-
: undefined,
|
|
2310
2207
|
};
|
|
2311
2208
|
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
2312
2209
|
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
@@ -2316,10 +2213,9 @@ ${objective}` : objective,
|
|
|
2316
2213
|
// minimal state update manually:
|
|
2317
2214
|
// 1) write the new record to disk authoritatively
|
|
2318
2215
|
// 2) update in-memory `goal` to the canonical post-write record
|
|
2319
|
-
// 3)
|
|
2216
|
+
// 3) re-sync tools
|
|
2320
2217
|
// 4) clear the tweak drafting gate so propose_goal_tweak can't be re-used
|
|
2321
2218
|
state.goal = writeActiveGoalFile(ctx, next);
|
|
2322
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2323
2219
|
tweakDraftingFor = null;
|
|
2324
2220
|
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
2325
2221
|
resetGetGoalNudgeState(state.goal.id);
|
|
@@ -2339,20 +2235,6 @@ ${objective}` : objective,
|
|
|
2339
2235
|
} catch {
|
|
2340
2236
|
// Ledger append failure should not crash tweak
|
|
2341
2237
|
}
|
|
2342
|
-
// Append ledger event for task list change if tasks were provided
|
|
2343
|
-
if (params.tasks && Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
2344
|
-
try {
|
|
2345
|
-
appendGoalEvent(ctx, {
|
|
2346
|
-
type: "task_list_set",
|
|
2347
|
-
goalId: state.goal.id,
|
|
2348
|
-
taskCount: params.tasks.length,
|
|
2349
|
-
blockCompletion: false,
|
|
2350
|
-
at: state.goal.updatedAt,
|
|
2351
|
-
});
|
|
2352
|
-
} catch {
|
|
2353
|
-
// Ledger append failure should not crash tweak
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
2238
|
return {
|
|
2357
2239
|
content: [{
|
|
2358
2240
|
type: "text",
|
|
@@ -2497,7 +2379,6 @@ ${objective}` : objective,
|
|
|
2497
2379
|
updatedAt: nowIso(),
|
|
2498
2380
|
};
|
|
2499
2381
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2500
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2501
2382
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2502
2383
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2503
2384
|
syncGoalTools();
|
|
@@ -2564,7 +2445,6 @@ ${objective}` : objective,
|
|
|
2564
2445
|
updatedAt: nowIso(),
|
|
2565
2446
|
};
|
|
2566
2447
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2567
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2568
2448
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2569
2449
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2570
2450
|
syncGoalTools();
|
|
@@ -2694,7 +2574,6 @@ ${objective}` : objective,
|
|
|
2694
2574
|
updatedAt: nowIso(),
|
|
2695
2575
|
};
|
|
2696
2576
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2697
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2698
2577
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2699
2578
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2700
2579
|
syncGoalTools();
|
|
@@ -2795,7 +2674,6 @@ ${objective}` : objective,
|
|
|
2795
2674
|
updatedAt: nowIso(),
|
|
2796
2675
|
};
|
|
2797
2676
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2798
|
-
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2799
2677
|
turnStoppedFor = state.goal?.id ?? null;
|
|
2800
2678
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2801
2679
|
syncGoalTools();
|
|
@@ -3389,8 +3267,8 @@ promptGuidelines: [
|
|
|
3389
3267
|
|
|
3390
3268
|
pi.on("turn_start", async (_event, ctx) => {
|
|
3391
3269
|
// Per-turn flag resets (#4 + C9 fix).
|
|
3392
|
-
advanceTurnSeq();
|
|
3393
3270
|
goalWorkToolCalledThisTurn = false;
|
|
3271
|
+
turnStoppedFor = null;
|
|
3394
3272
|
syncGoalTools();
|
|
3395
3273
|
beginAccounting();
|
|
3396
3274
|
updateUI(ctx);
|
|
@@ -3398,29 +3276,17 @@ promptGuidelines: [
|
|
|
3398
3276
|
|
|
3399
3277
|
// #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
|
|
3400
3278
|
pi.on("tool_call", async (event, ctx) => {
|
|
3401
|
-
const stoppedGoalId = currentTurnStoppedGoalId();
|
|
3402
3279
|
// Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
|
|
3403
3280
|
// complete_goal / propose_goal_tweak fires in this turn, block all subsequent tool calls except
|
|
3404
3281
|
// read-only inspection. Forces the agent to yield the turn instead of "fixing"
|
|
3405
3282
|
// the situation by creating extra files etc.
|
|
3406
|
-
if (
|
|
3283
|
+
if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
|
|
3407
3284
|
return {
|
|
3408
3285
|
block: true,
|
|
3409
|
-
reason: `The goal was already stopped earlier in this turn (goalId=${
|
|
3286
|
+
reason: `The goal was already stopped earlier in this turn (goalId=${turnStoppedFor}). ` +
|
|
3410
3287
|
`Do not call more tools; end the turn with a brief summary and yield to the user.`,
|
|
3411
3288
|
};
|
|
3412
3289
|
}
|
|
3413
|
-
// Stale checkpoint guard: if the turn was triggered by a queued continuation
|
|
3414
|
-
// for a goal that is no longer active/autoContinue, block work tools.
|
|
3415
|
-
if (checkpointGoalId !== null && !isActionableContinuationGoal(checkpointGoalId) && isStaleCheckpointBlockedToolCall(event.toolName)) {
|
|
3416
|
-
// Block the tool call with a stale-checkpoint message.
|
|
3417
|
-
return {
|
|
3418
|
-
block: true,
|
|
3419
|
-
reason: `Cannot call ${event.toolName}: the goal checkpoint that triggered this turn is no longer active. ` +
|
|
3420
|
-
`Goal ${checkpointGoalId} has been paused, cleared, or replaced. ` +
|
|
3421
|
-
`End the turn with a brief summary and yield to the user.`,
|
|
3422
|
-
};
|
|
3423
|
-
}
|
|
3424
3290
|
// Phase 5 soft gate relaxation: active-goal question block and repeated get_goal
|
|
3425
3291
|
// block are removed. The agent is trusted to prefer work tools; prompts nudge
|
|
3426
3292
|
// toward concrete work without hard-stopping the turn.
|
|
@@ -3501,8 +3367,8 @@ promptGuidelines: [
|
|
|
3501
3367
|
});
|
|
3502
3368
|
|
|
3503
3369
|
pi.on("session_start", async (event, ctx) => {
|
|
3504
|
-
loadState(ctx);
|
|
3505
3370
|
syncGoalTools();
|
|
3371
|
+
loadState(ctx);
|
|
3506
3372
|
syncTerminalInputPause(ctx);
|
|
3507
3373
|
if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
|
|
3508
3374
|
await focusGoalCommand(ctx);
|
|
@@ -3543,13 +3409,11 @@ promptGuidelines: [
|
|
|
3543
3409
|
});
|
|
3544
3410
|
|
|
3545
3411
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
3546
|
-
advanceTurnSeq();
|
|
3547
3412
|
syncGoalTools();
|
|
3548
3413
|
const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
|
|
3549
3414
|
const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
|
|
3550
3415
|
|
|
3551
3416
|
if (confirmationIntent !== null) {
|
|
3552
|
-
checkpointGoalId = null;
|
|
3553
3417
|
clearContinuationState();
|
|
3554
3418
|
clearActiveAccounting();
|
|
3555
3419
|
runningGoalId = null;
|
|
@@ -3557,7 +3421,6 @@ promptGuidelines: [
|
|
|
3557
3421
|
}
|
|
3558
3422
|
|
|
3559
3423
|
if (tweakDraftingFor !== null) {
|
|
3560
|
-
checkpointGoalId = null;
|
|
3561
3424
|
clearContinuationState();
|
|
3562
3425
|
clearActiveAccounting();
|
|
3563
3426
|
runningGoalId = null;
|
|
@@ -3568,12 +3431,8 @@ promptGuidelines: [
|
|
|
3568
3431
|
// matches the active goal, abort the whole turn instead of letting the
|
|
3569
3432
|
// model act on a stale instruction.
|
|
3570
3433
|
if (incomingGoalId !== null) {
|
|
3571
|
-
// Reconcile from disk to pick up any external state changes before
|
|
3572
|
-
// evaluating whether the checkpoint is actionable.
|
|
3573
|
-
reconcileFocusedGoalFromDisk(ctx);
|
|
3574
|
-
checkpointGoalId = incomingGoalId;
|
|
3575
3434
|
clearContinuationState();
|
|
3576
|
-
if (!
|
|
3435
|
+
if (!state.goal || state.goal.id !== incomingGoalId || (state.goal.status !== "active") || !state.goal.autoContinue) {
|
|
3577
3436
|
try {
|
|
3578
3437
|
ctx.abort?.();
|
|
3579
3438
|
} catch {}
|
|
@@ -3582,12 +3441,10 @@ promptGuidelines: [
|
|
|
3582
3441
|
systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, state.goal)}`,
|
|
3583
3442
|
};
|
|
3584
3443
|
}
|
|
3585
|
-
checkpointGoalId = null;
|
|
3586
3444
|
} else {
|
|
3587
3445
|
// A user-driven turn — clear any queued continuation so we don't
|
|
3588
3446
|
// double-fire after the user's own message returns. Also reset the
|
|
3589
3447
|
// autoContinue nudge state so the user always gets a fresh chain.
|
|
3590
|
-
checkpointGoalId = null;
|
|
3591
3448
|
clearContinuationState();
|
|
3592
3449
|
resetGetGoalNudgeState(state.goal?.id);
|
|
3593
3450
|
}
|
|
@@ -3689,7 +3546,6 @@ promptGuidelines: [
|
|
|
3689
3546
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
3690
3547
|
accountProgress(ctx);
|
|
3691
3548
|
clearContinuationTimer();
|
|
3692
|
-
stopStatusRefresh();
|
|
3693
3549
|
terminalInputUnsubscribe?.();
|
|
3694
3550
|
terminalInputUnsubscribe = null;
|
|
3695
3551
|
if (state.goal) persist(ctx);
|
|
@@ -238,21 +238,14 @@ export function goalTweakDraftingPrompt(current: GoalRecord, hint: string): stri
|
|
|
238
238
|
"<current_objective>",
|
|
239
239
|
promptSafeObjective(current.objective),
|
|
240
240
|
"</current_objective>",
|
|
241
|
-
...(current.taskList && current.taskList.tasks.length > 0
|
|
242
|
-
? ["", taskListBlock(current), ""]
|
|
243
|
-
: []),
|
|
244
241
|
`Sisyphus mode: ${sisyphusOn ? "on (prompt/criteria style)" : "off"}`,
|
|
245
242
|
"",
|
|
246
243
|
"User's tweak hint (may be empty):",
|
|
247
|
-
"User's tweak hint (may be empty):",
|
|
248
244
|
"<tweak_hint>",
|
|
249
245
|
safeHint,
|
|
250
246
|
"</tweak_hint>",
|
|
251
247
|
"",
|
|
252
248
|
"Drafting protocol:",
|
|
253
|
-
"- Start from the EXISTING goal — you are editing the current goal, not writing from scratch.",
|
|
254
|
-
" The current objective (above) and task list (if any) are your starting point. Edit/rewrite",
|
|
255
|
-
" them directly, preserving what works and changing what needs to change.",
|
|
256
249
|
"- Apply common sense: if the hint is fully self-explanatory, acknowledge in one sentence and apply the tweak immediately. Do not invent unnecessary questions.",
|
|
257
250
|
"- Otherwise ask focused questions (1-3 rounds) to clarify exactly what to change. Prefer numbered options or yes/no.",
|
|
258
251
|
"- Do NOT call create_goal (a goal already exists).",
|
|
@@ -271,10 +264,6 @@ export function goalTweakDraftingPrompt(current: GoalRecord, hint: string): stri
|
|
|
271
264
|
? " === Sisyphus Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked / Sisyphus reminder)."
|
|
272
265
|
: " === Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked)."),
|
|
273
266
|
" - changeSummary: one sentence describing what changed.",
|
|
274
|
-
" - tasks (optional): an array of task objects to REPLACE the current goal's task list. If omitted,",
|
|
275
|
-
" the existing task list is inherited as-is. If you need to add/remove/change tasks, pass the",
|
|
276
|
-
" full updated task list here. Each task has {id, title, verificationContract?, lightweightSubtasks?, subtasks?}.",
|
|
277
|
-
" Subtasks use the same shape recursively.",
|
|
278
267
|
"2. propose_goal_tweak opens the user's Confirm / Continue Chatting dialog.",
|
|
279
268
|
" - Confirm applies the tweak. Stop; the next continuation will arrive automatically if the goal is active.",
|
|
280
269
|
" - Continue Chatting means the drafting stays active — ask the user what they want changed, then revise and call propose_goal_tweak again.",
|
|
@@ -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",
|