pi-goal-x 0.20.0 → 0.20.1
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/extensions/goal-draft.ts +55 -51
- package/extensions/goal.ts +142 -34
- package/extensions/prompts/goal-prompts.ts +11 -0
- package/package.json +1 -1
package/extensions/goal-draft.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { GoalTask } from "./goal-record.ts";
|
|
2
|
+
|
|
1
3
|
export type GoalDraftingFocus = "goal" | "sisyphus";
|
|
2
4
|
|
|
3
5
|
export interface GoalConfirmationIntentLike {
|
|
@@ -22,6 +24,45 @@ export type ToolGateDecision =
|
|
|
22
24
|
| { block: false }
|
|
23
25
|
| { block: true; reason: string };
|
|
24
26
|
|
|
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
|
+
|
|
25
66
|
export function promptSafeObjective(objective: string): string {
|
|
26
67
|
return objective.replace(/<\/?untrusted_objective>/gi, (tag) => tag.replace(/</g, "<").replace(/>/g, ">"));
|
|
27
68
|
}
|
|
@@ -84,30 +125,13 @@ export function buildDraftConfirmationText(args: {
|
|
|
84
125
|
autoContinue: boolean;
|
|
85
126
|
}): string {
|
|
86
127
|
const lines: string[] = [];
|
|
87
|
-
const modeLabel = args.focus === "sisyphus" ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
88
128
|
lines.push("● Goal draft ready for confirmation.");
|
|
89
129
|
lines.push("");
|
|
90
130
|
lines.push("─── Draft Details ───");
|
|
91
|
-
lines.push(`│ Mode: ${
|
|
131
|
+
lines.push(`│ Mode: ${formatModeLabel(args.focus === "sisyphus")}`);
|
|
92
132
|
lines.push(`│ Auto-continue: ${args.autoContinue ? "yes" : "no"}`);
|
|
93
|
-
lines.push("");
|
|
94
|
-
lines.push("
|
|
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
|
-
}
|
|
133
|
+
lines.push(...formatSection("Original Topic", args.originalTopic.trim()));
|
|
134
|
+
lines.push(...formatSection("Proposed Goal", args.objective));
|
|
111
135
|
return lines.join("\n");
|
|
112
136
|
}
|
|
113
137
|
|
|
@@ -116,42 +140,22 @@ export function buildTweakConfirmationText(args: {
|
|
|
116
140
|
newObjective: string;
|
|
117
141
|
changeSummary: string;
|
|
118
142
|
sisyphus: boolean;
|
|
143
|
+
tasks?: GoalTask[];
|
|
119
144
|
}): string {
|
|
120
145
|
const lines: string[] = [];
|
|
121
|
-
const modeLabel = args.sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal";
|
|
122
146
|
lines.push("● Goal tweak ready for confirmation.");
|
|
123
147
|
lines.push("");
|
|
124
148
|
lines.push("─── Draft Details ───");
|
|
125
|
-
lines.push(`│ Mode: ${
|
|
126
|
-
lines.push("");
|
|
127
|
-
lines.push("
|
|
128
|
-
lines.push("");
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}
|
|
149
|
+
lines.push(`│ Mode: ${formatModeLabel(args.sisyphus)}`);
|
|
150
|
+
lines.push(...formatSection("Change", args.changeSummary));
|
|
151
|
+
lines.push(...formatSection("Current Objective", args.currentObjective));
|
|
152
|
+
lines.push(...formatSection("Proposed New Objective", args.newObjective));
|
|
153
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
154
|
+
const taskLines = renderConfirmationTasks(args.tasks, 0);
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(`┌─ TASKS ─────────────────────────────────────┐`);
|
|
157
|
+
for (const tl of taskLines) lines.push(tl);
|
|
158
|
+
lines.push(`└──────────────────────────────────────────────┘`);
|
|
155
159
|
}
|
|
156
160
|
return lines.join("\n");
|
|
157
161
|
}
|
package/extensions/goal.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildTweakConfirmationText,
|
|
14
14
|
extractVerificationContract,
|
|
15
15
|
goalDraftingPrompt,
|
|
16
|
+
renderConfirmationTasks,
|
|
16
17
|
validateGoalDraftProposal,
|
|
17
18
|
type GoalDraftingFocus,
|
|
18
19
|
} from "./goal-draft.ts";
|
|
@@ -426,8 +427,15 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
426
427
|
// after their successful execute. Once set, pi.on("tool_call") blocks all
|
|
427
428
|
// subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
|
|
428
429
|
// schema fix for "agent keeps writing files after pause_goal".
|
|
430
|
+
// turnSeq: lightweight generation counter, incremented at each turn start.
|
|
431
|
+
// Used to scope turnStoppedFor so stale markers from prior turns or sessions
|
|
432
|
+
// cannot poison a resumed active goal.
|
|
433
|
+
// checkpointGoalId: remembers the incoming goal id from queued continuations.
|
|
434
|
+
// When set but the goal is no longer active/autoContinue, work tools are blocked.
|
|
429
435
|
let goalWorkToolCalledThisTurn = false;
|
|
430
|
-
let
|
|
436
|
+
let turnSeq = 0;
|
|
437
|
+
let turnStoppedFor: { goalId: string; turnSeq: number } | null = null;
|
|
438
|
+
let checkpointGoalId: string | null = null;
|
|
431
439
|
|
|
432
440
|
// #5 post-compaction resync: when a compaction just happened, the next agent
|
|
433
441
|
// turn gets an extra reminder block. Set in session_compact, consumed
|
|
@@ -542,6 +550,28 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
542
550
|
accounting.lastAccountedAt = null;
|
|
543
551
|
}
|
|
544
552
|
|
|
553
|
+
function advanceTurnSeq(): void {
|
|
554
|
+
turnSeq += 1;
|
|
555
|
+
if (turnStoppedFor?.turnSeq !== turnSeq) turnStoppedFor = null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function currentTurnStoppedGoalId(): string | null {
|
|
559
|
+
if (!turnStoppedFor) return null;
|
|
560
|
+
if (turnStoppedFor.turnSeq !== turnSeq) {
|
|
561
|
+
turnStoppedFor = null;
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return turnStoppedFor.goalId;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isActionableContinuationGoal(goalId: string | null | undefined): goalId is string {
|
|
568
|
+
return !!goalId && state.goal?.id === goalId && state.goal.status === "active" && state.goal.autoContinue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isStaleCheckpointBlockedToolCall(toolName: string): boolean {
|
|
572
|
+
return !POST_STOP_ALLOWED_TOOL_SET.has(toolName);
|
|
573
|
+
}
|
|
574
|
+
|
|
545
575
|
function clearStoppedRuntimeState(): void {
|
|
546
576
|
clearContinuationState();
|
|
547
577
|
clearActiveAccounting();
|
|
@@ -1245,7 +1275,7 @@ Verification contract:
|
|
|
1245
1275
|
continuationTimer = null;
|
|
1246
1276
|
continuationScheduledFor = null;
|
|
1247
1277
|
syncGoalTools();
|
|
1248
|
-
if (!
|
|
1278
|
+
if (!isActionableContinuationGoal(goalId)) {
|
|
1249
1279
|
if (continuationQueuedFor === goalId) continuationQueuedFor = null;
|
|
1250
1280
|
return;
|
|
1251
1281
|
}
|
|
@@ -1266,16 +1296,18 @@ Verification contract:
|
|
|
1266
1296
|
}
|
|
1267
1297
|
continuationQueuedFor = goalId;
|
|
1268
1298
|
const settings = loadGoalSettings(ctx.cwd);
|
|
1299
|
+
const goal = state.goal;
|
|
1300
|
+
if (!goal) return;
|
|
1269
1301
|
pi.sendMessage<GoalEventDetails>(
|
|
1270
1302
|
{
|
|
1271
1303
|
customType: GOAL_EVENT_ENTRY,
|
|
1272
|
-
content: continuationPrompt(
|
|
1304
|
+
content: continuationPrompt(goal, settings),
|
|
1273
1305
|
display: false,
|
|
1274
1306
|
details: {
|
|
1275
1307
|
kind: "checkpoint",
|
|
1276
|
-
goalId:
|
|
1277
|
-
status:
|
|
1278
|
-
objective:
|
|
1308
|
+
goalId: goal.id,
|
|
1309
|
+
status: goal.status,
|
|
1310
|
+
objective: goal.objective,
|
|
1279
1311
|
timestamp: Date.now(),
|
|
1280
1312
|
},
|
|
1281
1313
|
},
|
|
@@ -1964,20 +1996,6 @@ Verification contract:
|
|
|
1964
1996
|
const autoContinueFlag = params.autoContinue ?? true;
|
|
1965
1997
|
const sisyphusFlag = validation.expectedSisyphus;
|
|
1966
1998
|
// 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
|
-
|
|
1981
1999
|
let taskSummarySection = "";
|
|
1982
2000
|
let tasksToCreate: GoalTask[] | undefined;
|
|
1983
2001
|
if (params.tasks && params.tasks.length > 0) {
|
|
@@ -2124,10 +2142,25 @@ ${objective}` : objective,
|
|
|
2124
2142
|
"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.",
|
|
2125
2143
|
"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.",
|
|
2126
2144
|
"Do NOT use write/edit/bash to modify the active goal file directly. propose_goal_tweak is the only sanctioned channel.",
|
|
2145
|
+
"tasks (optional): an array of task objects to REPLACE the current goal's task list. When omitted,",
|
|
2146
|
+
" the existing task list is inherited as-is. When provided, the full task list is replaced with",
|
|
2147
|
+
" the new tasks — add/remove/change tasks by passing the complete updated list.",
|
|
2148
|
+
" Each task has: {id, title, verificationContract?, lightweightSubtasks?, subtasks?}.",
|
|
2149
|
+
" Subtasks use the same shape recursively. The task list is displayed in the confirmation",
|
|
2150
|
+
" dialog alongside the objective diff, matching propose_goal_draft's rendering format.",
|
|
2151
|
+
"Start from the current goal's objective text and task list (the inheritance defaults), then",
|
|
2152
|
+
" edit/rewrite them directly rather than composing a goal from scratch.",
|
|
2127
2153
|
],
|
|
2128
2154
|
parameters: Type.Object({
|
|
2129
2155
|
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
2130
|
-
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog and tweak log)." }),
|
|
2156
|
+
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog, activity log, and tweak log)." }),
|
|
2157
|
+
tasks: Type.Optional(Type.Array(Type.Object({
|
|
2158
|
+
id: Type.String({ description: "Short stable slug e.g. 'task-1'" }),
|
|
2159
|
+
title: Type.String({ description: "Human-readable task title" }),
|
|
2160
|
+
verificationContract: Type.Optional(Type.String({ description: "Optional verification contract for this task." })),
|
|
2161
|
+
lightweightSubtasks: Type.Optional(Type.Boolean({ description: "If true, subtasks are lightweight (no completion enforcement). Default false." })),
|
|
2162
|
+
subtasks: Type.Optional(Type.Any({ description: "Optional recursive array of sub-tasks (same shape as parent)." })),
|
|
2163
|
+
}), { description: "Optional task list to replace the current goal's tasks. When omitted the existing task list is inherited. Each task supports recursive subtasks." })),
|
|
2131
2164
|
}),
|
|
2132
2165
|
executionMode: "sequential",
|
|
2133
2166
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -2149,6 +2182,41 @@ ${objective}` : objective,
|
|
|
2149
2182
|
const changeSummary = params.changeSummary.trim();
|
|
2150
2183
|
if (!changeSummary) throw new Error("propose_goal_tweak requires a non-empty changeSummary.");
|
|
2151
2184
|
|
|
2185
|
+
// Resolve tasks: inherit from current goal if not provided, or normalize/validate the passed tasks.
|
|
2186
|
+
let tweakTasks: GoalTask[] | undefined;
|
|
2187
|
+
if (params.tasks && Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
2188
|
+
tweakTasks = params.tasks.map((t: Record<string, unknown>) => {
|
|
2189
|
+
const task: GoalTask = {
|
|
2190
|
+
id: t.id as string,
|
|
2191
|
+
title: t.title as string,
|
|
2192
|
+
status: "pending",
|
|
2193
|
+
verificationContract: t.verificationContract as string | undefined,
|
|
2194
|
+
lightweightSubtasks: t.lightweightSubtasks === true ? true : undefined,
|
|
2195
|
+
};
|
|
2196
|
+
const rawSubtasks = t.subtasks;
|
|
2197
|
+
if (Array.isArray(rawSubtasks) && rawSubtasks.length > 0) {
|
|
2198
|
+
task.subtasks = rawSubtasks
|
|
2199
|
+
.map((s: Record<string, unknown>) => normalizeTaskItem(s))
|
|
2200
|
+
.filter((s: GoalTask | undefined): s is GoalTask => !!s);
|
|
2201
|
+
}
|
|
2202
|
+
return task;
|
|
2203
|
+
});
|
|
2204
|
+
// Validate task list
|
|
2205
|
+
if (tweakTasks.length > 50) {
|
|
2206
|
+
return { content: [{ type: "text", text: "Task list cannot exceed 50 tasks." }], details: goalDetails(state.goal) };
|
|
2207
|
+
}
|
|
2208
|
+
const settings = loadGoalSettings(ctx.cwd);
|
|
2209
|
+
const depthViolation = findSubtaskDepthViolation(tweakTasks, settings.subtaskDepth ?? 1);
|
|
2210
|
+
if (depthViolation) {
|
|
2211
|
+
return { content: [{ type: "text", text: depthViolation }], details: goalDetails(state.goal) };
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
// Inherit current task list when no tasks parameter provided
|
|
2215
|
+
if (state.goal.taskList && state.goal.taskList.tasks.length > 0) {
|
|
2216
|
+
tweakTasks = state.goal.taskList.tasks;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2152
2220
|
// Auto-start the tweak drafting flow if not already active.
|
|
2153
2221
|
if (tweakDraftingFor !== state.goal.id) {
|
|
2154
2222
|
await startGoalTweakDrafting(changeSummary, ctx);
|
|
@@ -2168,6 +2236,7 @@ ${objective}` : objective,
|
|
|
2168
2236
|
newObjective,
|
|
2169
2237
|
changeSummary,
|
|
2170
2238
|
sisyphus: !!state.goal.sisyphus,
|
|
2239
|
+
tasks: tweakTasks,
|
|
2171
2240
|
});
|
|
2172
2241
|
|
|
2173
2242
|
const headless = shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM });
|
|
@@ -2204,6 +2273,9 @@ ${objective}` : objective,
|
|
|
2204
2273
|
// Clear any prior agent pause reason — the user has redefined the work.
|
|
2205
2274
|
pauseReason: undefined,
|
|
2206
2275
|
pauseSuggestedAction: undefined,
|
|
2276
|
+
taskList: tweakTasks && tweakTasks.length > 0
|
|
2277
|
+
? { tasks: tweakTasks, blockCompletion: false, proposedAt: nowIso() }
|
|
2278
|
+
: undefined,
|
|
2207
2279
|
};
|
|
2208
2280
|
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
2209
2281
|
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
@@ -2220,7 +2292,7 @@ ${objective}` : objective,
|
|
|
2220
2292
|
// Reset autoContinue counter — plan changed, agent gets a fresh chain.
|
|
2221
2293
|
resetGetGoalNudgeState(state.goal.id);
|
|
2222
2294
|
// Mark turn-stopped so subsequent in-turn tool calls are blocked.
|
|
2223
|
-
turnStoppedFor = state.goal.id;
|
|
2295
|
+
turnStoppedFor = { goalId: state.goal.id, turnSeq };
|
|
2224
2296
|
syncGoalTools();
|
|
2225
2297
|
updateUI(ctx);
|
|
2226
2298
|
ctx.ui.notify(`Goal tweaked: ${truncateText(changeSummary, 160)}`, "info");
|
|
@@ -2235,6 +2307,20 @@ ${objective}` : objective,
|
|
|
2235
2307
|
} catch {
|
|
2236
2308
|
// Ledger append failure should not crash tweak
|
|
2237
2309
|
}
|
|
2310
|
+
// Append ledger event for task list change if tasks were provided
|
|
2311
|
+
if (params.tasks && Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
2312
|
+
try {
|
|
2313
|
+
appendGoalEvent(ctx, {
|
|
2314
|
+
type: "task_list_set",
|
|
2315
|
+
goalId: state.goal.id,
|
|
2316
|
+
taskCount: params.tasks.length,
|
|
2317
|
+
blockCompletion: false,
|
|
2318
|
+
at: state.goal.updatedAt,
|
|
2319
|
+
});
|
|
2320
|
+
} catch {
|
|
2321
|
+
// Ledger append failure should not crash tweak
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2238
2324
|
return {
|
|
2239
2325
|
content: [{
|
|
2240
2326
|
type: "text",
|
|
@@ -2379,7 +2465,7 @@ ${objective}` : objective,
|
|
|
2379
2465
|
updatedAt: nowIso(),
|
|
2380
2466
|
};
|
|
2381
2467
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2382
|
-
turnStoppedFor = state.goal
|
|
2468
|
+
turnStoppedFor = state.goal ? { goalId: state.goal.id, turnSeq } : null;
|
|
2383
2469
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2384
2470
|
syncGoalTools();
|
|
2385
2471
|
updateUI(ctx);
|
|
@@ -2445,7 +2531,7 @@ ${objective}` : objective,
|
|
|
2445
2531
|
updatedAt: nowIso(),
|
|
2446
2532
|
};
|
|
2447
2533
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2448
|
-
turnStoppedFor = state.goal
|
|
2534
|
+
turnStoppedFor = state.goal ? { goalId: state.goal.id, turnSeq } : null;
|
|
2449
2535
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2450
2536
|
syncGoalTools();
|
|
2451
2537
|
updateUI(ctx);
|
|
@@ -2574,7 +2660,7 @@ ${objective}` : objective,
|
|
|
2574
2660
|
updatedAt: nowIso(),
|
|
2575
2661
|
};
|
|
2576
2662
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2577
|
-
turnStoppedFor = state.goal
|
|
2663
|
+
turnStoppedFor = state.goal ? { goalId: state.goal.id, turnSeq } : null;
|
|
2578
2664
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2579
2665
|
syncGoalTools();
|
|
2580
2666
|
updateUI(ctx);
|
|
@@ -2592,7 +2678,7 @@ ${objective}` : objective,
|
|
|
2592
2678
|
} else {
|
|
2593
2679
|
// ── Continue working → pause the goal ──────────────
|
|
2594
2680
|
pauseActiveGoal(ctx);
|
|
2595
|
-
turnStoppedFor = state.goal
|
|
2681
|
+
turnStoppedFor = state.goal ? { goalId: state.goal.id, turnSeq } : null;
|
|
2596
2682
|
return {
|
|
2597
2683
|
content: [{ type: "text", text: "Goal paused — user chose to continue working after skipping audit." }],
|
|
2598
2684
|
details: state.goal ? goalDetails(state.goal) : undefined,
|
|
@@ -2674,7 +2760,7 @@ ${objective}` : objective,
|
|
|
2674
2760
|
updatedAt: nowIso(),
|
|
2675
2761
|
};
|
|
2676
2762
|
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2677
|
-
turnStoppedFor = state.goal
|
|
2763
|
+
turnStoppedFor = state.goal ? { goalId: state.goal.id, turnSeq } : null;
|
|
2678
2764
|
resetGetGoalNudgeState(state.goal?.id);
|
|
2679
2765
|
syncGoalTools();
|
|
2680
2766
|
updateUI(ctx);
|
|
@@ -2741,7 +2827,7 @@ ${objective}` : objective,
|
|
|
2741
2827
|
resetGetGoalNudgeState(next.id);
|
|
2742
2828
|
// C9 fix: mark turn-stopped so subsequent in-turn tool calls are blocked.
|
|
2743
2829
|
// This is the schema-level closure of "agent kept writing files after pause_goal".
|
|
2744
|
-
turnStoppedFor = state.goal.id;
|
|
2830
|
+
turnStoppedFor = { goalId: state.goal.id, turnSeq };
|
|
2745
2831
|
|
|
2746
2832
|
const suggestionLine = suggested ? `\nSuggested: ${truncateText(suggested, 160)}` : "";
|
|
2747
2833
|
ctx.ui.notify(
|
|
@@ -2801,7 +2887,7 @@ ${objective}` : objective,
|
|
|
2801
2887
|
const archived = archiveCurrentGoal(ctx, "agent");
|
|
2802
2888
|
resetGetGoalNudgeState(abortedGoalId);
|
|
2803
2889
|
setGoal(null, ctx, true, "aborted");
|
|
2804
|
-
turnStoppedFor = abortedGoalId;
|
|
2890
|
+
turnStoppedFor = { goalId: abortedGoalId, turnSeq };
|
|
2805
2891
|
|
|
2806
2892
|
const archiveLine = archived?.archivedPath ? `\nArchive: ${archived.archivedPath}` : "";
|
|
2807
2893
|
ctx.ui.notify(
|
|
@@ -2990,7 +3076,7 @@ ${objective}` : objective,
|
|
|
2990
3076
|
}
|
|
2991
3077
|
state.goal = { ...state.goal, taskList, updatedAt: now };
|
|
2992
3078
|
setGoal(state.goal, ctx);
|
|
2993
|
-
turnStoppedFor = state.goal.id;
|
|
3079
|
+
turnStoppedFor = { goalId: state.goal.id, turnSeq };
|
|
2994
3080
|
resetGetGoalNudgeState(state.goal.id);
|
|
2995
3081
|
syncGoalTools();
|
|
2996
3082
|
updateUI(ctx);
|
|
@@ -3267,8 +3353,8 @@ promptGuidelines: [
|
|
|
3267
3353
|
|
|
3268
3354
|
pi.on("turn_start", async (_event, ctx) => {
|
|
3269
3355
|
// Per-turn flag resets (#4 + C9 fix).
|
|
3356
|
+
advanceTurnSeq();
|
|
3270
3357
|
goalWorkToolCalledThisTurn = false;
|
|
3271
|
-
turnStoppedFor = null;
|
|
3272
3358
|
syncGoalTools();
|
|
3273
3359
|
beginAccounting();
|
|
3274
3360
|
updateUI(ctx);
|
|
@@ -3276,17 +3362,29 @@ promptGuidelines: [
|
|
|
3276
3362
|
|
|
3277
3363
|
// #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
|
|
3278
3364
|
pi.on("tool_call", async (event, ctx) => {
|
|
3365
|
+
const stoppedGoalId = currentTurnStoppedGoalId();
|
|
3279
3366
|
// Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
|
|
3280
3367
|
// complete_goal / propose_goal_tweak fires in this turn, block all subsequent tool calls except
|
|
3281
3368
|
// read-only inspection. Forces the agent to yield the turn instead of "fixing"
|
|
3282
3369
|
// the situation by creating extra files etc.
|
|
3283
|
-
if (
|
|
3370
|
+
if (stoppedGoalId !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
|
|
3284
3371
|
return {
|
|
3285
3372
|
block: true,
|
|
3286
|
-
reason: `The goal was already stopped earlier in this turn (goalId=${
|
|
3373
|
+
reason: `The goal was already stopped earlier in this turn (goalId=${stoppedGoalId}). ` +
|
|
3287
3374
|
`Do not call more tools; end the turn with a brief summary and yield to the user.`,
|
|
3288
3375
|
};
|
|
3289
3376
|
}
|
|
3377
|
+
// Stale checkpoint guard: if the turn was triggered by a queued continuation
|
|
3378
|
+
// for a goal that is no longer active/autoContinue, block work tools.
|
|
3379
|
+
if (checkpointGoalId !== null && !isActionableContinuationGoal(checkpointGoalId) && isStaleCheckpointBlockedToolCall(event.toolName)) {
|
|
3380
|
+
// Block the tool call with a stale-checkpoint message.
|
|
3381
|
+
return {
|
|
3382
|
+
block: true,
|
|
3383
|
+
reason: `Cannot call ${event.toolName}: the goal checkpoint that triggered this turn is no longer active. ` +
|
|
3384
|
+
`Goal ${checkpointGoalId} has been paused, cleared, or replaced. ` +
|
|
3385
|
+
`End the turn with a brief summary and yield to the user.`,
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3290
3388
|
// Phase 5 soft gate relaxation: active-goal question block and repeated get_goal
|
|
3291
3389
|
// block are removed. The agent is trusted to prefer work tools; prompts nudge
|
|
3292
3390
|
// toward concrete work without hard-stopping the turn.
|
|
@@ -3369,6 +3467,7 @@ promptGuidelines: [
|
|
|
3369
3467
|
pi.on("session_start", async (event, ctx) => {
|
|
3370
3468
|
syncGoalTools();
|
|
3371
3469
|
loadState(ctx);
|
|
3470
|
+
syncGoalTools();
|
|
3372
3471
|
syncTerminalInputPause(ctx);
|
|
3373
3472
|
if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
|
|
3374
3473
|
await focusGoalCommand(ctx);
|
|
@@ -3409,11 +3508,13 @@ promptGuidelines: [
|
|
|
3409
3508
|
});
|
|
3410
3509
|
|
|
3411
3510
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
3511
|
+
advanceTurnSeq();
|
|
3412
3512
|
syncGoalTools();
|
|
3413
3513
|
const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
|
|
3414
3514
|
const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
|
|
3415
3515
|
|
|
3416
3516
|
if (confirmationIntent !== null) {
|
|
3517
|
+
checkpointGoalId = null;
|
|
3417
3518
|
clearContinuationState();
|
|
3418
3519
|
clearActiveAccounting();
|
|
3419
3520
|
runningGoalId = null;
|
|
@@ -3421,6 +3522,7 @@ promptGuidelines: [
|
|
|
3421
3522
|
}
|
|
3422
3523
|
|
|
3423
3524
|
if (tweakDraftingFor !== null) {
|
|
3525
|
+
checkpointGoalId = null;
|
|
3424
3526
|
clearContinuationState();
|
|
3425
3527
|
clearActiveAccounting();
|
|
3426
3528
|
runningGoalId = null;
|
|
@@ -3431,8 +3533,12 @@ promptGuidelines: [
|
|
|
3431
3533
|
// matches the active goal, abort the whole turn instead of letting the
|
|
3432
3534
|
// model act on a stale instruction.
|
|
3433
3535
|
if (incomingGoalId !== null) {
|
|
3536
|
+
// Reconcile from disk to pick up any external state changes before
|
|
3537
|
+
// evaluating whether the checkpoint is actionable.
|
|
3538
|
+
reconcileFocusedGoalFromDisk(ctx);
|
|
3539
|
+
checkpointGoalId = incomingGoalId;
|
|
3434
3540
|
clearContinuationState();
|
|
3435
|
-
if (!
|
|
3541
|
+
if (!isActionableContinuationGoal(incomingGoalId)) {
|
|
3436
3542
|
try {
|
|
3437
3543
|
ctx.abort?.();
|
|
3438
3544
|
} catch {}
|
|
@@ -3441,10 +3547,12 @@ promptGuidelines: [
|
|
|
3441
3547
|
systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, state.goal)}`,
|
|
3442
3548
|
};
|
|
3443
3549
|
}
|
|
3550
|
+
checkpointGoalId = null;
|
|
3444
3551
|
} else {
|
|
3445
3552
|
// A user-driven turn — clear any queued continuation so we don't
|
|
3446
3553
|
// double-fire after the user's own message returns. Also reset the
|
|
3447
3554
|
// autoContinue nudge state so the user always gets a fresh chain.
|
|
3555
|
+
checkpointGoalId = null;
|
|
3448
3556
|
clearContinuationState();
|
|
3449
3557
|
resetGetGoalNudgeState(state.goal?.id);
|
|
3450
3558
|
}
|
|
@@ -238,14 +238,21 @@ 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
|
+
: []),
|
|
241
244
|
`Sisyphus mode: ${sisyphusOn ? "on (prompt/criteria style)" : "off"}`,
|
|
242
245
|
"",
|
|
243
246
|
"User's tweak hint (may be empty):",
|
|
247
|
+
"User's tweak hint (may be empty):",
|
|
244
248
|
"<tweak_hint>",
|
|
245
249
|
safeHint,
|
|
246
250
|
"</tweak_hint>",
|
|
247
251
|
"",
|
|
248
252
|
"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.",
|
|
249
256
|
"- Apply common sense: if the hint is fully self-explanatory, acknowledge in one sentence and apply the tweak immediately. Do not invent unnecessary questions.",
|
|
250
257
|
"- Otherwise ask focused questions (1-3 rounds) to clarify exactly what to change. Prefer numbered options or yes/no.",
|
|
251
258
|
"- Do NOT call create_goal (a goal already exists).",
|
|
@@ -264,6 +271,10 @@ export function goalTweakDraftingPrompt(current: GoalRecord, hint: string): stri
|
|
|
264
271
|
? " === Sisyphus Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked / Sisyphus reminder)."
|
|
265
272
|
: " === Goal === block (Objective / Success criteria / Boundaries / Constraints / If blocked)."),
|
|
266
273
|
" - 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.",
|
|
267
278
|
"2. propose_goal_tweak opens the user's Confirm / Continue Chatting dialog.",
|
|
268
279
|
" - Confirm applies the tweak. Stop; the next continuation will arrive automatically if the goal is active.",
|
|
269
280
|
" - Continue Chatting means the drafting stays active — ask the user what they want changed, then revise and call propose_goal_tweak again.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
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",
|