pi-goal-x 0.18.9 → 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.
@@ -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, "&lt;").replace(/>/g, "&gt;"));
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: ${modeLabel}`);
131
+ lines.push(`│ Mode: ${formatModeLabel(args.focus === "sisyphus")}`);
92
132
  lines.push(`│ Auto-continue: ${args.autoContinue ? "yes" : "no"}`);
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
- }
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: ${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
- }
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
  }
@@ -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",
@@ -3296,8 +3351,6 @@ promptGuidelines: [
3296
3351
  },
3297
3352
  }));
3298
3353
 
3299
- syncGoalTools();
3300
-
3301
3354
  pi.on("context", async (event): Promise<{ messages: typeof event.messages } | undefined> => {
3302
3355
  let changed = false;
3303
3356
  const latestGoalEventIndex = new Map<string, number>();
@@ -3449,6 +3502,7 @@ promptGuidelines: [
3449
3502
 
3450
3503
  pi.on("session_start", async (event, ctx) => {
3451
3504
  loadState(ctx);
3505
+ syncGoalTools();
3452
3506
  syncTerminalInputPause(ctx);
3453
3507
  if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
3454
3508
  await focusGoalCommand(ctx);
@@ -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.18.9",
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",