pi-goal-x 0.26.0 → 0.26.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.
@@ -211,7 +211,10 @@ export function proposalText(draft: ActiveGoalDraft, objective: string, autoCont
211
211
  } else if (taskList && taskList.tasks.length > 0) {
212
212
  tasksText = "\n\nTasks proposed for confirmation:\n" + renderConfirmationTasks(taskList.tasks, 0).join("\n");
213
213
  } else {
214
- const derived = deriveTasksFromObjective(objective);
214
+ // §single-task-set: the derived preview must derive from the SAME
215
+ // objective text the apply path persists (extracted — the Verification
216
+ // contract line removed), so shown == persisted.
217
+ const derived = deriveTasksFromObjective(extractVerificationContract(objective).objective);
215
218
  if (derived && derived.length > 0) {
216
219
  tasksText = "\n\nTasks derived from the objective (confirm or ask the agent to adjust):\n" + renderConfirmationTasks(derived, 0).join("\n");
217
220
  }
@@ -465,12 +468,26 @@ export function registerDraftingTools(core: GoalCore): void {
465
468
  const objective = String(args?.objective ?? "");
466
469
  let text = theme.fg("toolTitle", "propose_goal_draft ") + theme.fg("muted", objective);
467
470
  const flatTasks = Array.isArray(args?.tasks) ? (args.tasks as FlatTaskInput[]) : [];
468
- const derived = flatTasks.length === 0 ? (deriveTasksFromObjective(objective) ?? []) : [];
469
- const taskBlock = flatTasks.length > 0
470
- ? { header: "Tasks proposed for confirmation:", lines: flatTasks.map((t) => `[ ] ${t.id}: ${t.title}`) }
471
- : derived.length > 0
472
- ? { header: "Tasks derived from the objective (confirm or ask the agent to adjust):", lines: renderConfirmationTasks(derived, 0) }
473
- : null;
471
+ // §single-task-set: a proposal shows exactly ONE task set — the set
472
+ // that will actually be persisted. Explicit tasks are that set; a
473
+ // tweak without explicit tasks retains the current list (mirror
474
+ // proposalText — never a derived-from-objective phantom); a new
475
+ // draft without explicit tasks shows the F2-derived preview derived
476
+ // from the SAME objective text the apply path persists (extracted).
477
+ let taskBlock: { header: string; lines: string[] } | null = null;
478
+ if (flatTasks.length > 0) {
479
+ taskBlock = { header: "Tasks proposed for confirmation:", lines: flatTasks.map((t) => `[ ] ${t.id}: ${t.title}`) };
480
+ } else if (activeDraft(core)?.mode === "tweak") {
481
+ const retained = core.state.goal?.taskList?.tasks;
482
+ if (retained && retained.length > 0) {
483
+ taskBlock = { header: "Current task list (retained unchanged):", lines: renderConfirmationTasks(retained, 0) };
484
+ }
485
+ } else {
486
+ const derived = deriveTasksFromObjective(extractVerificationContract(objective).objective) ?? [];
487
+ if (derived.length > 0) {
488
+ taskBlock = { header: "Tasks derived from the objective (confirm or ask the agent to adjust):", lines: renderConfirmationTasks(derived, 0) };
489
+ }
490
+ }
474
491
  if (taskBlock) {
475
492
  text += "\n\n" + theme.fg("toolTitle", theme.bold(taskBlock.header)) + "\n" + theme.fg("toolOutput", taskBlock.lines.join("\n"));
476
493
  }
@@ -54,10 +54,27 @@ export interface ProposalPresentationSegments {
54
54
  tailStart: number;
55
55
  }
56
56
 
57
+ /** Strip ANSI SGR sequences so structural scans see the plain text. */
58
+ function stripAnsiCodes(value: string): string {
59
+ return value.replace(/\x1b\[[0-9;]*m/g, "");
60
+ }
61
+
57
62
  /**
58
63
  * Locate the proposal tasks segment and the options tail in a rendered dialog.
59
64
  * Returns null when the dialog is not a proposal-style confirmation (no
60
65
  * "Tasks proposed for confirmation:" header / "┌─ TASKS ─" box).
66
+ *
67
+ * The scan is ANSI-aware: every rendered line carries `theme.fg(...)` escape
68
+ * sequences in the real TUI (the mock theme does not), so plain-text regexes
69
+ * must be run against the stripped line or the tasks section collapses to its
70
+ * header and every styled task line is sliced out of the bounded frame. The
71
+ * box-drawn bottom border (`└───┘`) after the last task line is kept so the
72
+ * `┌─ TASKS ─┐` box renders complete.
73
+ *
74
+ * The interactive auditor toggle line ("press 'a' to toggle") renders between
75
+ * the context and the options; `tailStart` is pulled back to it so the bounded
76
+ * fit always keeps the auditor status visible (its status text is the only
77
+ * visible toggle feedback in the dialog).
61
78
  */
62
79
  export function findProposalPresentationSegments(lines: string[], tailStart: number): ProposalPresentationSegments | null {
63
80
  if (tailStart < 0) return null;
@@ -65,10 +82,25 @@ export function findProposalPresentationSegments(lines: string[], tailStart: num
65
82
  if (tasksStart < 0 || tasksStart >= tailStart) return null;
66
83
  let tasksEnd = tasksStart;
67
84
  for (let i = tasksStart + 1; i < tailStart; i++) {
68
- if (/^\s*\[[ x~]\]/.test(lines[i])) tasksEnd = i;
69
- else break;
85
+ const plain = stripAnsiCodes(lines[i]);
86
+ if (/^\s*\[[ x~]\]/.test(plain)) {
87
+ tasksEnd = i;
88
+ continue;
89
+ }
90
+ // Box-drawn TASKS section bottom border after the last task line.
91
+ if (tasksEnd > tasksStart && /^[└┐┤┘─]/.test(plain.trim())) tasksEnd = i;
92
+ break;
93
+ }
94
+ // Protect the auditor toggle line: it renders between context and options,
95
+ // so pull the tail start back to it when present.
96
+ let effectiveTail = tailStart;
97
+ for (let i = tailStart - 1; i > tasksEnd; i--) {
98
+ if (lines[i].includes("press 'a' to toggle")) {
99
+ effectiveTail = i;
100
+ break;
101
+ }
70
102
  }
71
- return { tasksStart, tasksEnd, tailStart };
103
+ return { tasksStart, tasksEnd, tailStart: effectiveTail };
72
104
  }
73
105
 
74
106
  /**
@@ -127,12 +159,13 @@ export function fitDialogLines(
127
159
  }
128
160
 
129
161
  /**
130
- * Proposal confirmation fit: keep the protected head, the tasks section, and
131
- * the options/footer/bottom border; the objective-box middle is sacrificed
132
- * in-frame (it stays fully readable in the scrollback presentation). Interior
133
- * blank spacing lines are dropped first when room is short; task lines are
134
- * only dropped after that, from the end, when the bound is exhausted (those
135
- * lines remain in the scrollback presentation). Never exceeds maxDialogLines.
162
+ * Proposal confirmation fit: keep the protected head, the tasks section, the
163
+ * interactive auditor toggle line (when present), and the options/footer/
164
+ * bottom border; the objective-box middle is sacrificed in-frame (it stays
165
+ * fully readable in the scrollback presentation). Interior blank spacing lines
166
+ * are dropped first when room is short; task lines are only dropped after
167
+ * that, from the end, when the bound is exhausted (those lines remain in the
168
+ * scrollback presentation). Never exceeds maxDialogLines.
136
169
  */
137
170
  function fitProposalPresentation(
138
171
  lines: string[],
@@ -150,15 +183,18 @@ function fitProposalPresentation(
150
183
  // the options and the footer hint) before touching any content line.
151
184
  const stripped = candidate.filter((l, i) => i < keepHead || l.trim() !== "");
152
185
  if (stripped.length <= maxDialogLines) return stripped;
153
- // Bound still exhausted: keep the head, then spend the room on the tail
154
- // first (options/footer/bottom border — the actionable decision surface —
155
- // kept from its end so the border and footer never drop), then on the
156
- // tasks from the start. The dropped task lines remain fully readable in
186
+ // Bound still exhausted: keep the head, then the auditor toggle line (the
187
+ // actionable status control — its feedback must stay visible), then spend
188
+ // the room on the tail (options/footer/bottom border — the decision
189
+ // surface — kept from its end so the border and footer never drop), then on
190
+ // the tasks from the start. The dropped task lines remain fully readable in
157
191
  // the scrollback presentation. Never exceeds maxDialogLines.
158
192
  const tailNoBlanks = tail.filter((l) => l.trim() !== "");
159
- const keepTail = Math.min(tailNoBlanks.length, Math.max(0, maxDialogLines - keepHead));
160
- const tailKept = tailNoBlanks.slice(tailNoBlanks.length - keepTail);
161
- const keepTasks = Math.min(tasks.length, Math.max(0, maxDialogLines - keepHead - keepTail));
193
+ const hasAuditor = tail.length > 0 && tail[0].includes("press 'a' to toggle");
194
+ const restTail = hasAuditor ? tailNoBlanks.slice(1) : tailNoBlanks;
195
+ const keepRest = Math.min(restTail.length, Math.max(0, maxDialogLines - keepHead - (hasAuditor ? 1 : 0)));
196
+ const tailKept = [...(hasAuditor ? [tail[0]] : []), ...restTail.slice(restTail.length - keepRest)];
197
+ const keepTasks = Math.min(tasks.length, Math.max(0, maxDialogLines - keepHead - tailKept.length));
162
198
  return [...head, ...tasks.slice(0, keepTasks), ...tailKept];
163
199
  }
164
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "Goal mode extension for pi: durable long-running objectives, five model tools, structured tasks, independent completion audit, Sisyphus mode, auto-continue, and a status overlay.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",