pi-goal-x 0.18.10 → 0.19.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/extensions/goal-draft.ts +55 -51
- package/extensions/goal.ts +70 -15
- 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";
|
|
@@ -2026,20 +2027,6 @@ Verification contract:
|
|
|
2026
2027
|
const autoContinueFlag = params.autoContinue ?? true;
|
|
2027
2028
|
const sisyphusFlag = validation.expectedSisyphus;
|
|
2028
2029
|
// Build confirmation text: goal + optional task list
|
|
2029
|
-
function renderConfirmationTasks(tasks: GoalTask[], indent: number): string[] {
|
|
2030
|
-
const prefix = " ".repeat(indent);
|
|
2031
|
-
const lines: string[] = [];
|
|
2032
|
-
for (const t of tasks) {
|
|
2033
|
-
const lw = t.lightweightSubtasks ? " (lightweight)" : "";
|
|
2034
|
-
const contract = t.verificationContract ? ` contract: ${t.verificationContract}` : "";
|
|
2035
|
-
lines.push(`${prefix}[ ] ${t.id}: ${t.title}${lw}${contract}`);
|
|
2036
|
-
if (t.subtasks && t.subtasks.length > 0) {
|
|
2037
|
-
lines.push(...renderConfirmationTasks(t.subtasks, indent + 1));
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
return lines;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
2030
|
let taskSummarySection = "";
|
|
2044
2031
|
let tasksToCreate: GoalTask[] | undefined;
|
|
2045
2032
|
if (params.tasks && params.tasks.length > 0) {
|
|
@@ -2186,10 +2173,25 @@ ${objective}` : objective,
|
|
|
2186
2173
|
"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.",
|
|
2187
2174
|
"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.",
|
|
2188
2175
|
"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.",
|
|
2189
2184
|
],
|
|
2190
2185
|
parameters: Type.Object({
|
|
2191
2186
|
newObjective: Type.String({ description: "The complete revised objective text. For Sisyphus goals, preserve the Sisyphus style unless the user explicitly changes it." }),
|
|
2192
|
-
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog and tweak log)." }),
|
|
2187
|
+
changeSummary: Type.String({ description: "One-sentence description of what was changed (used in confirmation dialog, activity log, and tweak log)." }),
|
|
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." })),
|
|
2193
2195
|
}),
|
|
2194
2196
|
executionMode: "sequential",
|
|
2195
2197
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -2211,6 +2213,41 @@ ${objective}` : objective,
|
|
|
2211
2213
|
const changeSummary = params.changeSummary.trim();
|
|
2212
2214
|
if (!changeSummary) throw new Error("propose_goal_tweak requires a non-empty changeSummary.");
|
|
2213
2215
|
|
|
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
|
+
|
|
2214
2251
|
// Auto-start the tweak drafting flow if not already active.
|
|
2215
2252
|
if (tweakDraftingFor !== state.goal.id) {
|
|
2216
2253
|
await startGoalTweakDrafting(changeSummary, ctx);
|
|
@@ -2230,6 +2267,7 @@ ${objective}` : objective,
|
|
|
2230
2267
|
newObjective,
|
|
2231
2268
|
changeSummary,
|
|
2232
2269
|
sisyphus: !!state.goal.sisyphus,
|
|
2270
|
+
tasks: tweakTasks,
|
|
2233
2271
|
});
|
|
2234
2272
|
|
|
2235
2273
|
const headless = shouldAutoConfirmProposal({ hasUI: ctx.hasUI, autoConfirmEnv: process.env.PI_GOAL_AUTO_CONFIRM });
|
|
@@ -2266,6 +2304,9 @@ ${objective}` : objective,
|
|
|
2266
2304
|
// Clear any prior agent pause reason — the user has redefined the work.
|
|
2267
2305
|
pauseReason: undefined,
|
|
2268
2306
|
pauseSuggestedAction: undefined,
|
|
2307
|
+
taskList: tweakTasks && tweakTasks.length > 0
|
|
2308
|
+
? { tasks: tweakTasks, blockCompletion: false, proposedAt: nowIso() }
|
|
2309
|
+
: undefined,
|
|
2269
2310
|
};
|
|
2270
2311
|
// IMPORTANT: bypass setGoal() / persist() here. persist() calls
|
|
2271
2312
|
// syncGoalPromptFromDisk() which would RE-READ the stale objective
|
|
@@ -2298,6 +2339,20 @@ ${objective}` : objective,
|
|
|
2298
2339
|
} catch {
|
|
2299
2340
|
// Ledger append failure should not crash tweak
|
|
2300
2341
|
}
|
|
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
|
+
}
|
|
2301
2356
|
return {
|
|
2302
2357
|
content: [{
|
|
2303
2358
|
type: "text",
|
|
@@ -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.
|
|
3
|
+
"version": "0.19.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",
|