pi-better-harness 0.33.1 → 0.34.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 CHANGED
@@ -13,15 +13,26 @@ Use `pi-better-harness` when you want the full working set for Pi. It manages:
13
13
  - `pi-better-background-tasks` for durable shell tasks and watchers.
14
14
  - `pi-better-ssh` for short remote commands over reusable SSH connections.
15
15
  - `pi-better-goal` for explicit triggers, pause/resume, and background-aware continuation.
16
- - Harness-owned Outcome & Todo for session context and checklist progress. It starts off; enable **Outcome & Todo** in `/harness-settings`.
16
+ - Harness-owned Outcome & Todo for session context and checklist progress. Saved state displays by default; its hooks start off.
17
17
 
18
18
  `pi-better-read-aloud` is intentionally not included yet.
19
19
 
20
20
  ## Outcome & Todo
21
21
 
22
- In `/harness-settings`, **Outcome & Todo** starts **Off**. Turn it on to expose
23
- `update_todo`, `get_todo`, and the native Todo widget. `/todo` opens the full view;
24
- `/todo expand` and `/todo collapse` control the compact checklist.
22
+ In `/harness-settings`, **Outcome & Todo display** (`outcome-todo-display`)
23
+ defaults to **On**, and **Outcome & Todo hooks** (`outcome-todo`) defaults to
24
+ **Off**. Saved outcomes and checklists display even when hooks are off; missing
25
+ state creates no placeholder, state, or settings. Turning display off does not
26
+ disable hooks. Saved v1 `enabled` remains the hooks authority; the new optional
27
+ display preference defaults to true.
28
+
29
+ The compact widget has one Todo heading, with a caret after Todo and the completion
30
+ count on the left. A matching Goal's status and existing active-time clock appear
31
+ on the right. The outcome is one muted, truncated subtitle. The collapsed view
32
+ shows up to five relevant rows; the expanded view shows all rows. Click to toggle.
33
+ `/todo` shows the full objective, acceptance criteria, boundaries, and plan;
34
+ `/todo expand` and `/todo collapse` also control the compact checklist. Todo
35
+ commands remain available with hooks off for explicit user control.
25
36
 
26
37
  An accepted outcome can be saved without a checklist. Completing or clearing its
27
38
  Todo does not complete or remove the outcome. Write labels as short, plain actions,
@@ -32,15 +43,20 @@ existing saved labels remain unchanged until deliberately updated.
32
43
 
33
44
  Use `newTask: true` with a new
34
45
  outcome for unrelated work; a correction to the current task keeps its binding.
35
- Harness includes a short reminder before each model call while the outcome is
36
- active. Disabling the feature hides its tools and display and stops reminders,
37
- without deleting saved state or starting a turn. `/goal` and skills never enable it.
46
+ Hooks enable `update_todo`, `get_todo`, capture, matching Goal lifecycle bridge
47
+ writes, reminders, and automated cleanup. While hooks are enabled and the outcome is active, Harness
48
+ includes a short reminder before each model call. Turning hooks off stops those
49
+ hooks without deleting saved state or starting a turn. `/goal` and skills never
50
+ enable them.
38
51
 
39
52
  Existing Plan session histories restore as Todo. Workflow task-plan files and
40
53
  revision checks stay unchanged; skills continue using their own records when the
41
54
  optional interface is off. See the [usage guide](docs/usage.md#outcome--todo).
42
55
 
43
- <p><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/todo-examples/middle.png" alt="Compact Harness Todo" width="49%" /><img src="https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/todo-examples/expanded.png" alt="Expanded Harness Todo" width="49%" /></p>
56
+ Subagent and background-task rows can be clicked to open their existing navigator
57
+ details. Enter and keyboard controls remain available, and editor input stays
58
+ passive. No permanent status bar is added; existing critical permission and
59
+ paused notices remain unchanged.
44
60
 
45
61
  ## Screenshots
46
62
 
@@ -71,7 +87,7 @@ pi install npm:pi-better-harness
71
87
  After a normal bundle install or `pi update`, Harness masks any separately loaded
72
88
  `pi-better-plan` package before Pi registers its tools, `/plan` command, widgets,
73
89
  or prompt hooks. This applies in either load order, including when Outcome & Todo
74
- is off. The old registration and cached files may remain, but are inactive while
90
+ hooks are off. The old registration and cached files may remain, but are inactive while
75
91
  Harness is loaded. Settings and session history are not rewritten. The explicit
76
92
  installer above removes the old registration in its selected scope instead.
77
93
 
@@ -0,0 +1,35 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { PlanRenderTheme } from "./plan-presentation.js";
3
+
4
+ export interface CompactTaskHeader {
5
+ subtitle?: string;
6
+ disclosure?: string;
7
+ runtime?: { status: string; clock?: string };
8
+ }
9
+
10
+ export function canRenderTaskRuntime(width: number, runtime: CompactTaskHeader["runtime"]): boolean {
11
+ if (!runtime || !Number.isFinite(width)) return false;
12
+ return visibleWidth(runtime.status + (runtime.clock ? ` ${runtime.clock}` : "")) + 6 <= Math.floor(width);
13
+ }
14
+
15
+ export function renderCompactTaskHeader(summary: string | undefined, width: number, theme: PlanRenderTheme, header: CompactTaskHeader): string[] {
16
+ const size = Math.max(1, Math.floor(width));
17
+ const left = theme.fg("warning", "todo") + (header.disclosure ? theme.fg("accent", ` ${header.disclosure}`) : "") +
18
+ (summary ? theme.fg("dim", ` ${summary}`) : "");
19
+ const runtime = header.runtime;
20
+ const tone = runtime?.status === "complete" ? "success" : runtime?.status === "active" ? "accent" : "warning";
21
+ const right = runtime ? theme.fg(tone, runtime.status) + (runtime.clock ? theme.fg("dim", ` ${runtime.clock}`) : "") : "";
22
+ const rightWidth = visibleWidth(right);
23
+ const leftWidth = Math.max(4, size - rightWidth - 2);
24
+ const title = truncateToWidth(left, leftWidth);
25
+ const heading = right && canRenderTaskRuntime(size, runtime)
26
+ ? title + " ".repeat(Math.max(2, size - visibleWidth(title) - rightWidth)) + right
27
+ : truncateToWidth(left, size);
28
+ const lines = [truncateToWidth(heading, size)];
29
+ if (header.subtitle) {
30
+ const indent = Math.min(2, size - 1);
31
+ const text = header.subtitle.replace(/[\r\n\t]+/g, " ").replace(/ {2,}/g, " ").trim();
32
+ lines.push(" ".repeat(indent) + theme.fg("muted", truncateToWidth(text, size - indent)), "");
33
+ }
34
+ return lines;
35
+ }
@@ -4,7 +4,11 @@ import { Text, matchesKey, type AutocompleteItem } from "@earendil-works/pi-tui"
4
4
  import { Type } from "typebox";
5
5
  import { readHarnessSetting, updateHarnessSetting } from "../shared-harness-settings.js";
6
6
  import { reconstructGoalSnapshot } from "pi-better-goal/src/goal-state.ts";
7
+ import { formatClockDuration, goalTiming } from "pi-better-goal/src/goal-clock.ts";
7
8
  import { OUTCOME_ENTRY, prepareOutcome, restoreOutcome, formatOutcome, type Outcome, type OutcomeInput } from "./outcome-state.js";
9
+ import { changeOutcomeTodoPreference, parseOutcomeTodoPreferences } from "./preferences.js";
10
+ import { canRenderTaskRuntime, renderCompactTaskHeader, type CompactTaskHeader } from "./compact-task-header.js";
11
+ import type { GoalSnapshot } from "pi-better-goal/src/types.ts";
8
12
 
9
13
  import {
10
14
  completedPlanClearDelay,
@@ -87,6 +91,7 @@ const UpdatePlanSchema = Type.Object({
87
91
  type WorkflowPlanInput = Omit<RushPlanUpdate, "event"> & { path?: string; event?: string };
88
92
 
89
93
  const LEGACY_PLAN_NAV_STATUS_KEY = "pi-better-plan-nav";
94
+ const goalHeaderRuntime = (goal: GoalSnapshot) => ({ status: goal.status, clock: formatClockDuration(goalTiming(goal).activeSeconds) });
90
95
  const PLAN_ACTIONS: readonly AutocompleteItem[] = [
91
96
  { value: "clear", label: "clear", description: "Remove the current Todo without changing its outcome" },
92
97
  { value: "hide", label: "hide", description: "Hide the Todo widget" },
@@ -120,12 +125,9 @@ function workflowPlanOwner(ctx: ExtensionContext): string | null {
120
125
 
121
126
  export default function taskContextExtension(pi: ExtensionAPI): void {
122
127
  let enabled = false;
128
+ let display = true;
129
+ let widgetContext: ExtensionContext | undefined;
123
130
  let outcome: Outcome | null = null;
124
- const readEnabled = (value: unknown): boolean => {
125
- if (value === undefined) return false;
126
- if (!value || typeof value !== "object" || Array.isArray(value) || (value as any).version !== 1 || typeof (value as any).enabled !== "boolean") throw new Error("Invalid outcomeTodo setting.");
127
- return (value as { enabled: boolean }).enabled;
128
- };
129
131
  const syncTools = (): void => {
130
132
  if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
131
133
  const active = pi.getActiveTools().filter(name => name !== "update_todo" && name !== "get_todo");
@@ -153,7 +155,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
153
155
  };
154
156
  const scheduleCancelledRowsClear = (): void => {
155
157
  cancelCancelledRowsTimer();
156
- if (!rushPlan) return;
158
+ if (!enabled || !rushPlan) return;
157
159
  const delay = cancelledRowsClearDelay(rushPlan);
158
160
  if (delay === null) return;
159
161
  const timer = setTimeout(() => {
@@ -166,12 +168,13 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
166
168
  timer.unref?.();
167
169
  };
168
170
  const observeCancellations = (plan: RushPlan, binding: WorkflowPlanBinding, ctx: ExtensionContext): void => {
171
+ if (!enabled) return;
169
172
  observeCancelledRows(plan, binding, ctx.sessionManager.getBranch(), (type, data) => pi.appendEntry(type, data));
170
173
  };
171
174
  const unsubscribeWorkflow = pi.events.on("pi-better-workflow:changed", (owner) => {
172
175
  displayedWorkflowOwner = owner && typeof owner === "object" && "name" in owner && typeof owner.name === "string"
173
176
  ? owner.name : null;
174
- if (!enabled) return;
177
+ if (!enabled && !display) return;
175
178
  if (displayedWorkflowOwner) {
176
179
  cancelCancelledRowsTimer();
177
180
  rushBinding = null;
@@ -283,7 +286,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
283
286
  rushBinding = binding && (!displayedWorkflowOwner || binding.owner === displayedWorkflowOwner) ? binding : null;
284
287
  rushPlan = null;
285
288
  rushError = null;
286
- if (enabled && rushBinding) loadRushPlan(ctx);
289
+ if ((enabled || display) && rushBinding) loadRushPlan(ctx);
287
290
  displayMode = state.displayMode;
288
291
  expanded = state.expanded;
289
292
  if (ctx.hasUI) ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
@@ -313,24 +316,32 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
313
316
 
314
317
  const installWidget = (ctx: ExtensionContext): void => {
315
318
  if (!ctx.hasUI) return;
319
+ widgetContext = ctx;
316
320
  ctx.ui.setWidget(
317
321
  EXTENSION_NAME,
318
322
  (tui, theme) => {
319
323
  const localRefresh = (force = false): void => tui.requestRender(force);
320
324
  refreshWidget = localRefresh;
321
325
  const widget = createPlanWidget(
322
- (width: number): string[] => {
323
- if (!enabled || displayMode === "hidden" || displayedWorkflowOwner !== workflowPlanOwner(ctx)) return [];
324
- const heading = outcome ? new Text(`Outcome (${outcome.status}): ${outcome.objective}`, 0, 0).render(width).slice(0, 2) : [];
326
+ (width: number, disclosure?: string): string[] => {
327
+ if (!display || displayMode === "hidden" || displayedWorkflowOwner !== workflowPlanOwner(ctx)) return [];
328
+ const goal = reconstructGoalSnapshot(ctx.sessionManager.getBranch());
329
+ const boundGoal = goal && outcome?.goalId === goal.goalId ? goal : null;
330
+ const header: CompactTaskHeader = {
331
+ subtitle: outcome?.objective, disclosure,
332
+ runtime: boundGoal ? goalHeaderRuntime(boundGoal)
333
+ : outcome ? { status: outcome.status } : undefined,
334
+ };
335
+ const heading = outcome ? renderCompactTaskHeader(undefined, width, theme as never, header) : [];
325
336
  if (displayedWorkflowOwner || rushBinding) {
326
337
  if (!rushBinding || (displayedWorkflowOwner && rushBinding.owner !== displayedWorkflowOwner) ||
327
338
  !workflowBinding(ctx.sessionManager.getBranch())) return heading;
328
339
  const fg = typeof theme?.fg === "function"
329
340
  ? (color: string, value: string) => theme.fg(color as never, value)
330
341
  : undefined;
331
- return [...heading, ...(rushPlan ? renderRushPlan(rushPlan, width, false, fg, rushBinding.owner, !displayedWorkflowOwner, expanded) : [])];
342
+ return rushPlan ? renderRushPlan(rushPlan, width, false, fg, rushBinding.owner, !displayedWorkflowOwner, expanded, header) : heading;
332
343
  }
333
- return [...heading, ...(currentPlan ? renderCompactPlan(currentPlan, width, theme as never, expanded) : [])];
344
+ return currentPlan ? renderCompactPlan(currentPlan, width, theme as never, expanded, header) : heading;
334
345
  },
335
346
  () => (rushBinding && rushPlan ? visibleRushUnits(rushPlan).length : currentPlan?.steps.length ?? 0) > COMPACT_PLAN_ROWS,
336
347
  () => expanded,
@@ -368,7 +379,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
368
379
  ? (color: string, value: string) => theme.fg(color as never, value)
369
380
  : undefined;
370
381
  const component = createRushPlanComponent(() => rushPlan ?? { ...plan, issues: [] }, () => done(), fg, planOwner, !owner);
371
- return { render: (width) => [...(outcome ? new Text(formatOutcome(outcome, true), 0, 0).render(width).slice(0, 6) : []), ...component.render(width)], handleInput(data) {
382
+ return { render: (width) => [...(outcome ? new Text(formatOutcome(outcome), 0, 0).render(width) : []), ...component.render(width)], handleInput(data) {
372
383
  component.handleInput?.(data);
373
384
  tui.requestRender();
374
385
  }, invalidate: () => component.invalidate() };
@@ -396,7 +407,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
396
407
  await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
397
408
  const component = createFullPlanComponent(snapshot, theme as never, () => done());
398
409
  return {
399
- render: (width) => [...(outcome ? new Text(formatOutcome(outcome, true), 0, 0).render(width).slice(0, 6) : []), ...component.render(width)],
410
+ render: (width) => [...(outcome ? new Text(formatOutcome(outcome), 0, 0).render(width) : []), ...component.render(width)],
400
411
  handleInput(data) {
401
412
  component.handleInput?.(data);
402
413
  tui.requestRender();
@@ -426,7 +437,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
426
437
  ],
427
438
  parameters: UpdatePlanSchema,
428
439
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
429
- if (!enabled) throw new Error("Outcome & Todo is off. Enable it in /harness-settings.");
440
+ if (!enabled) throw new Error("Outcome & Todo hooks are off. Enable them in /harness-settings.");
430
441
  const owner = workflowPlanOwner(ctx);
431
442
  const input = params as { explanation?: string; newTask?: boolean; todo?: PlanStepInput[]; outcome?: OutcomeInput; workflow?: WorkflowPlanInput };
432
443
  if (!input.outcome && input.todo === undefined && !input.workflow) throw new Error("Send outcome, todo, or workflow.");
@@ -541,7 +552,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
541
552
  promptSnippet: "Inspect the current outcome and Todo",
542
553
  parameters: getTodoParameters,
543
554
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
544
- if (!enabled) throw new Error("Outcome & Todo is off. Enable it in /harness-settings.");
555
+ if (!enabled) throw new Error("Outcome & Todo hooks are off. Enable them in /harness-settings.");
545
556
  const owner = workflowPlanOwner(ctx);
546
557
  const binding = workflowBinding(ctx.sessionManager.getBranch());
547
558
  if (owner || binding) {
@@ -573,7 +584,6 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
573
584
  description: "Inspect, show, hide, or clear the current Todo and inspect its outcome",
574
585
  getArgumentCompletions: planArgumentCompletions,
575
586
  handler: async (args, ctx) => {
576
- if (!enabled) { ctx.ui.notify("Enable Outcome & Todo in /harness-settings.", "info"); return; }
577
587
  const input = args.trim().toLowerCase();
578
588
  if (!input) return showFullPlan(ctx);
579
589
  if (input === "clear") {
@@ -608,25 +618,35 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
608
618
  },
609
619
  });
610
620
 
611
- const setting = {
612
- id: "outcome-todo", label: "Outcome & Todo", values: ["Off", "On"],
613
- get: () => enabled ? "On" : "Off",
621
+ const setting = (key: "enabled" | "display") => ({
622
+ id: key === "enabled" ? "outcome-todo" : "outcome-todo-display",
623
+ label: key === "enabled" ? "Outcome & Todo hooks" : "Outcome & Todo display", values: ["Off", "On"],
624
+ get: () => (key === "enabled" ? enabled : display) ? "On" : "Off",
614
625
  async change(value: string, ctx: ExtensionContext) {
615
626
  if (value !== "On" && value !== "Off") throw new Error("Invalid Outcome & Todo mode.");
616
- updateHarnessSetting("outcomeTodo", current => { readEnabled(current); return { version: 1, enabled: value === "On" }; });
617
- enabled = value === "On";
627
+ const preferences = updateHarnessSetting("outcomeTodo", current => changeOutcomeTodoPreference(current, key, value === "On"));
628
+ enabled = preferences.enabled;
629
+ display = preferences.display;
618
630
  syncTools();
619
- if (enabled) { installWidget(ctx); restore(ctx); }
620
- else { cancelCompletedPlanClear(); cancelCancelledRowsTimer(); if (ctx.hasUI) ctx.ui.setWidget(EXTENSION_NAME, undefined); refreshWidget = undefined; }
631
+ if (!enabled) { cancelCompletedPlanClear(); cancelCancelledRowsTimer(); }
632
+ installWidget(ctx);
633
+ restore(ctx);
621
634
  },
622
- };
623
- const registerSettings = () => pi.events.emit("harness-settings:register", setting);
635
+ });
636
+ const controls = [setting("display"), setting("enabled")];
637
+ const registerSettings = () => controls.forEach(control => pi.events.emit("harness-settings:register", control));
624
638
  const unsubscribeSettings = pi.events.on("harness-settings:request", registerSettings);
625
639
  registerSettings();
626
640
  const unsubscribeTask = pi.events.on("pi-better-harness:task-context", (event: unknown) => {
627
641
  if (!event || typeof event !== "object") return;
628
- const request = event as { action: string; goalId?: string; objective?: string; enabled?: boolean };
642
+ const request = event as { action: string; goalId?: string; width?: number; objective?: string; enabled?: boolean; display?: boolean };
629
643
  request.enabled = enabled;
644
+ if (request.action === "inspect") {
645
+ const goal = widgetContext ? reconstructGoalSnapshot(widgetContext.sessionManager.getBranch()) : null;
646
+ request.display = !!(typeof request.goalId === "string" && widgetContext && display && displayMode !== "hidden" &&
647
+ outcome && request.goalId === outcome.goalId && goal?.goalId === request.goalId && typeof request.width === "number" &&
648
+ canRenderTaskRuntime(request.width, goalHeaderRuntime(goal)) && displayedWorkflowOwner === workflowPlanOwner(widgetContext));
649
+ }
630
650
  if (!enabled || request.action === "inspect") return;
631
651
  if (request.action === "start" && request.goalId && request.objective?.trim()) {
632
652
  if (outcome?.goalId === request.goalId) return;
@@ -642,9 +662,13 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
642
662
  const onSessionStart = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
643
663
  // Retire a separately installed pi-better-plan before installing our Todo widget.
644
664
  await legacyPlanMask?.cleanup(ctx);
645
- try { enabled = readEnabled(readHarnessSetting("outcomeTodo")); } catch (error) { enabled = false; if (ctx.hasUI) ctx.ui.notify(String(error), "warning"); }
665
+ try {
666
+ const preferences = parseOutcomeTodoPreferences(readHarnessSetting("outcomeTodo"));
667
+ enabled = preferences.enabled;
668
+ display = preferences.display;
669
+ } catch (error) { enabled = false; display = true; if (ctx.hasUI) ctx.ui.notify(String(error), "warning"); }
646
670
  syncTools();
647
- if (enabled) installWidget(ctx);
671
+ installWidget(ctx);
648
672
  restore(ctx);
649
673
  registerSettings();
650
674
  };
@@ -669,6 +693,7 @@ export default function taskContextExtension(pi: ExtensionAPI): void {
669
693
  if (typeof unsubscribeSettings === "function") unsubscribeSettings();
670
694
  if (typeof unsubscribeTask === "function") unsubscribeTask();
671
695
  refreshWidget = undefined;
696
+ widgetContext = undefined;
672
697
  try {
673
698
  ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
674
699
  ctx.ui.setWidget(EXTENSION_NAME, undefined);
@@ -1,5 +1,6 @@
1
1
  import type { Component } from "@earendil-works/pi-tui";
2
2
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
+ import { renderCompactTaskHeader, type CompactTaskHeader } from "./compact-task-header.js";
3
4
 
4
5
  export interface PlanRenderTheme {
5
6
  fg(color: string, value: string): string;
@@ -64,6 +65,7 @@ export function planText(value: string): string {
64
65
  export function renderPlanPresentation(
65
66
  plan: PlanPresentation, width: number, theme: PlanRenderTheme,
66
67
  full = false, selectedIndex = -1, expanded = false,
68
+ header?: CompactTaskHeader,
67
69
  ): string[] {
68
70
  const size = Math.max(1, Math.floor(width));
69
71
  const count = (status: DisplayStatus) => plan.rows.filter((row) => row.status === status).length;
@@ -77,7 +79,8 @@ export function renderPlanPresentation(
77
79
  if (count(status)) summary.push(`${count(status)} ${label}`);
78
80
  }
79
81
  if (count("pending") && summary.length === 1) summary.push(`${count("pending")} pending`);
80
- const lines = [theme.fg("warning", "todo") + theme.fg("dim", ` ${summary.join(" · ")}`)];
82
+ const lines = !full && header ? renderCompactTaskHeader(summary[0], size, theme, header)
83
+ : [theme.fg("warning", "todo") + theme.fg("dim", ` ${summary.join(" · ")}`)];
81
84
  for (const metadata of plan.metadata ?? []) {
82
85
  const content = metadata.map((part) => theme.fg(part.color, planText(part.text))).join("");
83
86
  for (const line of wrapTextWithAnsi(content, Math.max(1, size - 2))) lines.push(` ${line}`);
@@ -4,6 +4,7 @@ import {
4
4
  type PlanPresentation, type PlanRenderTheme,
5
5
  } from "./plan-presentation.js";
6
6
  import type { PlanSnapshot } from "./types.js";
7
+ import type { CompactTaskHeader } from "./compact-task-header.js";
7
8
 
8
9
  export type { PlanRenderTheme } from "./plan-presentation.js";
9
10
 
@@ -20,8 +21,8 @@ function presentPlan(plan: PlanSnapshot, showIds = false): PlanPresentation {
20
21
  };
21
22
  }
22
23
 
23
- export function renderCompactPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme, expanded = false): string[] {
24
- return renderPlanPresentation(presentPlan(plan), width, theme, false, -1, expanded);
24
+ export function renderCompactPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme, expanded = false, header?: CompactTaskHeader): string[] {
25
+ return renderPlanPresentation(presentPlan(plan), width, theme, false, -1, expanded, header);
25
26
  }
26
27
 
27
28
  export function renderFullPlan(plan: PlanSnapshot, width: number, theme: PlanRenderTheme, selectedIndex = -1): string[] {
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "@earendil-works/pi-tui";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
+
3
3
 
4
4
  // Structural typing keeps keyboard-only Pi versions compatible with the fullscreen mouse API.
5
5
  interface PlanMouseEvent {
@@ -14,7 +14,7 @@ interface PlanMouseEvent {
14
14
  }
15
15
 
16
16
  export function createPlanWidget(
17
- render: (width: number) => string[],
17
+ render: (width: number, disclosure?: string) => string[],
18
18
  isFoldable: () => boolean,
19
19
  isExpanded: () => boolean,
20
20
  toggle: () => void,
@@ -22,10 +22,7 @@ export function createPlanWidget(
22
22
  let lines: string[] = [];
23
23
  return {
24
24
  render(width) {
25
- lines = render(width);
26
- if (lines.length && isFoldable()) {
27
- lines = [truncateToWidth(`${isExpanded() ? "▾" : "▸"} ${lines[0]}`, width), ...lines.slice(1)];
28
- }
25
+ lines = render(width, isFoldable() ? isExpanded() ? "▾" : "▸" : undefined);
29
26
  return lines.length ? ["", ...lines] : [];
30
27
  },
31
28
  handleMouse(event) {
@@ -0,0 +1,24 @@
1
+ export interface OutcomeTodoPreferences {
2
+ version: 1;
3
+ enabled: boolean;
4
+ display: boolean;
5
+ }
6
+
7
+ export function parseOutcomeTodoPreferences(value: unknown): OutcomeTodoPreferences {
8
+ if (value === undefined) return { version: 1, enabled: false, display: true };
9
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid outcomeTodo setting.");
10
+ const record = value as Record<string, unknown>;
11
+ if (record.version !== 1) throw new Error("Invalid outcomeTodo setting.");
12
+ for (const key of ["enabled", "display"] as const) {
13
+ if (record[key] !== undefined && typeof record[key] !== "boolean") throw new Error(`Invalid outcomeTodo setting: ${key} must be boolean.`);
14
+ }
15
+ return { version: 1, enabled: record.enabled === undefined ? false : record.enabled as boolean,
16
+ display: record.display === undefined ? true : record.display as boolean };
17
+ }
18
+
19
+ export function changeOutcomeTodoPreference(current: unknown, key: "enabled" | "display", value: boolean): OutcomeTodoPreferences {
20
+ const preferences = parseOutcomeTodoPreferences(current);
21
+ if (key !== "enabled" && key !== "display") throw new Error("Invalid outcomeTodo preference.");
22
+ if (typeof value !== "boolean") throw new Error("Invalid outcomeTodo preference value.");
23
+ return { ...preferences, [key]: value };
24
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import type { Component } from "@earendil-works/pi-tui";
4
+ import type { CompactTaskHeader } from "./compact-task-header.js";
4
5
  import {
5
6
  createPlanPresentationComponent, plainPlanTheme, planText, renderPlanPresentation, statusStyle,
6
7
  type DisplayStatus, type PlanPresentation,
@@ -191,9 +192,10 @@ export function renderRushPlan(
191
192
  owner = "rush-issues",
192
193
  readOnly = false,
193
194
  expanded = false,
195
+ header?: CompactTaskHeader,
194
196
  ): string[] {
195
197
  const theme = fg ? { fg } : plainPlanTheme;
196
- return renderPlanPresentation(presentRushPlan(plan, owner, readOnly), width, theme, full, -1, expanded);
198
+ return renderPlanPresentation(presentRushPlan(plan, owner, readOnly), width, theme, full, -1, expanded, header);
197
199
  }
198
200
 
199
201
  export function createRushPlanComponent(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -328,28 +328,92 @@ function themeFgFromTheme(theme: unknown): (color: string, value: string) => str
328
328
  return (color, value) => maybeTheme?.fg ? maybeTheme.fg(color, value) : value;
329
329
  }
330
330
 
331
- function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): Component & { dispose?(): void } {
331
+ // Structural typing keeps keyboard-only Pi hosts (no mouse API) loading this widget unchanged.
332
+ type WidgetMouseEvent = {
333
+ type: string;
334
+ button: string;
335
+ /** Coordinates local to this component, zero-based. */
336
+ x: number;
337
+ y: number;
338
+ width: number;
339
+ shift?: boolean;
340
+ alt?: boolean;
341
+ ctrl?: boolean;
342
+ };
343
+
344
+ type WidgetMouseResult = { handled: true; render: boolean };
345
+
346
+ type MainListWidget = Component & {
347
+ dispose?(): void;
348
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined;
349
+ };
350
+
351
+ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): MainListWidget {
332
352
  const requestRender = () => tui.requestRender?.();
333
353
  state().mainListRequestRender = requestRender;
354
+ // navigatorId per rendered line, so a pointer row resolves by structure, never by row text.
355
+ let lineOwners: Array<string | undefined> = [];
356
+ let lineWidth = MAIN_LIST_FALLBACK_WIDTH;
357
+ // Pi synthesizes a click only for the component that handled the initial press.
358
+ let pressedRow: { navigatorId: string; y: number } | undefined;
359
+ const rowAt = (event: WidgetMouseEvent): string | undefined => {
360
+ const width = Number.isFinite(event.width) && event.width > 0 ? Math.floor(event.width) : lineWidth;
361
+ if (!Number.isFinite(event.x) || event.x < 0 || event.x >= width) return undefined;
362
+ if (!Number.isInteger(event.y) || event.y < 0 || event.y >= lineOwners.length) return undefined;
363
+ return lineOwners[event.y];
364
+ };
334
365
  return {
335
366
  render(width: number): string[] {
336
367
  const rows = listRows();
337
368
  syncMainListSelection(rows);
338
369
  if (!rows.length) {
339
370
  state().lastMainListLines = undefined;
371
+ lineOwners = [];
372
+ pressedRow = undefined;
340
373
  return [];
341
374
  }
342
- const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
375
+ const layout = buildMainListLayout(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
343
376
  selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
344
377
  focused: state().mainListFocused === true,
345
- })];
378
+ });
379
+ lineWidth = renderWidth(width);
380
+ // The leading gap line belongs to the widget too, so its slot stays ownerless.
381
+ lineOwners = [undefined, ...layout.owners];
382
+ const lines = ["", ...layout.lines];
346
383
  state().lastMainListLines = lines;
347
384
  return lines;
348
385
  },
349
- invalidate() { state().lastMainListLines = undefined; },
386
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
387
+ // Only an unmodified left press is ours; every other gesture reaches the host untouched.
388
+ if (event.button !== "left" || event.shift === true || event.alt === true || event.ctrl === true) {
389
+ pressedRow = undefined;
390
+ return undefined;
391
+ }
392
+ if (event.type === "press") {
393
+ pressedRow = undefined;
394
+ const navigatorId = rowAt(event);
395
+ if (!navigatorId || !listRows().some((row) => row.navigatorId === navigatorId)) return undefined;
396
+ pressedRow = { navigatorId, y: event.y };
397
+ return { handled: true, render: false };
398
+ }
399
+ if (event.type !== "click") return undefined;
400
+ const pressed = pressedRow;
401
+ pressedRow = undefined;
402
+ // A click after another component's press, or after the layout moved, is not the row we claimed.
403
+ if (!pressed || pressed.y !== event.y || rowAt(event) !== pressed.navigatorId) return undefined;
404
+ if (!openNavigatorRow(pressed.navigatorId)) return undefined;
405
+ return { handled: true, render: true };
406
+ },
407
+ invalidate() {
408
+ state().lastMainListLines = undefined;
409
+ lineOwners = [];
410
+ pressedRow = undefined;
411
+ },
350
412
  dispose() {
351
413
  const s = state();
352
414
  if (s.mainListRequestRender === requestRender) s.mainListRequestRender = undefined;
415
+ lineOwners = [];
416
+ pressedRow = undefined;
353
417
  },
354
418
  };
355
419
  }
@@ -454,7 +518,26 @@ function buildMainListLines(
454
518
  fg: (color: string, value: string) => string,
455
519
  options: { selectedId?: string; focused?: boolean } = {},
456
520
  ): string[] {
521
+ return buildMainListLayout(rows, width, truncate, fg, options).lines;
522
+ }
523
+
524
+ /**
525
+ * The list's lines plus the navigatorId each line shows, so pointer input maps a rendered row to its
526
+ * stable row identity. Headers, separators and the shortcut line keep an undefined owner.
527
+ */
528
+ function buildMainListLayout(
529
+ rows: InternalRow[],
530
+ width: number,
531
+ truncate: (s: string, width: number) => string,
532
+ fg: (color: string, value: string) => string,
533
+ options: { selectedId?: string; focused?: boolean } = {},
534
+ ): { lines: string[]; owners: Array<string | undefined> } {
457
535
  const lines: string[] = [];
536
+ const owners: Array<string | undefined> = [];
537
+ const push = (line: string, owner?: string) => {
538
+ lines.push(line);
539
+ owners.push(owner);
540
+ };
458
541
  const grouped = new Map<string, InternalRow[]>();
459
542
  for (const row of rows) {
460
543
  const existing = grouped.get(row.providerLabel) ?? [];
@@ -466,16 +549,16 @@ function buildMainListLines(
466
549
  for (let i = 0; i < orderedLabels.length; i += 1) {
467
550
  const label = orderedLabels[i]!;
468
551
  const group = grouped.get(label)!;
469
- if (i > 0) lines.push("");
470
- lines.push(providerGroupLabel(label, fg));
552
+ if (i > 0) push("");
553
+ push(providerGroupLabel(label, fg));
471
554
  for (const row of group) {
472
555
  const selected = options.focused && row.navigatorId === options.selectedId;
473
- lines.push(formatMainListRow(row, selected === true, fg, width));
556
+ push(formatMainListRow(row, selected === true, fg, width), row.navigatorId);
474
557
  }
475
558
  }
476
- lines.push("");
477
- lines.push(shortcutsLine(options.focused === true, fg));
478
- return lines.map((line) => safeTruncate(line, width, truncate));
559
+ push("");
560
+ push(shortcutsLine(options.focused === true, fg));
561
+ return { lines: lines.map((line) => safeTruncate(line, width, truncate)), owners };
479
562
  }
480
563
 
481
564
  function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
@@ -766,6 +849,20 @@ function handleMainListCloseKey(): void {
766
849
  refreshMainListWidget();
767
850
  }
768
851
 
852
+ /**
853
+ * Open the shared detail overlay on the row a pointer chose. Setting the selection first keeps the
854
+ * keyboard path identical: Enter, x and the overlay's own keys then act on the clicked row.
855
+ */
856
+ function openNavigatorRow(navigatorId: string): boolean {
857
+ const rows = listRows();
858
+ if (!rows.some((row) => row.navigatorId === navigatorId)) return false;
859
+ clearMainListCloseArm();
860
+ state().mainListSelectedId = navigatorId;
861
+ refreshMainListWidget();
862
+ openNavigator();
863
+ return true;
864
+ }
865
+
769
866
  function openNavigator(): void {
770
867
  const s = state();
771
868
  const ctx = s.uiCtx;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -882,6 +882,11 @@ export default function (pi: ExtensionAPI): void {
882
882
  }
883
883
  const nextRenderDelay = goalClockRefreshDelayMs(goal);
884
884
  if (nextRenderDelay !== null) scheduler.schedule(nextRenderDelay);
885
+ // Schedule before handing visual ownership to Harness so its header
886
+ // keeps ticking. An absent display response preserves standalone Goal.
887
+ if (requestTaskContext(pi, "inspect", goal, width).display === true) {
888
+ return [];
889
+ }
885
890
  const fg = typeof theme?.fg === "function"
886
891
  ? (color: string, value: string) => theme.fg(color as never, value)
887
892
  : undefined;
@@ -14,9 +14,19 @@ export type TaskContextAction = "inspect" | "start" | "pause" | "resume" | "comp
14
14
  export interface TaskContextRequest {
15
15
  action: TaskContextAction;
16
16
  goalId?: string;
17
+ /** Current widget width on a visual ownership inspection. */
18
+ width?: number;
17
19
  /** The raw original goal trigger; only start requests carry it. */
18
20
  objective?: string;
21
+ /** Hooks authority: a present Harness runtime sets this even when Outcome & Todo is off. */
19
22
  enabled?: boolean;
23
+ /**
24
+ * Visual ownership. A Harness runtime sets this on `inspect` responses when
25
+ * its combined Todo header is rendering the requested `goalId`. It is
26
+ * independent of `enabled` and stays undefined without Harness, so Goal keeps
27
+ * its standalone clock widget whenever that header does not own the goal.
28
+ */
29
+ display?: boolean;
20
30
  }
21
31
 
22
32
  /**
@@ -27,8 +37,10 @@ export function requestTaskContext(
27
37
  pi: Pick<ExtensionAPI, "events">,
28
38
  action: TaskContextAction,
29
39
  goal?: GoalSnapshot | null,
40
+ width?: number,
30
41
  ): TaskContextRequest {
31
42
  const request: TaskContextRequest = { action };
43
+ if (width !== undefined) request.width = width;
32
44
  if (goal) {
33
45
  request.goalId = goal.goalId;
34
46
  if (action === "start") request.objective = goal.objective;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -328,28 +328,92 @@ function themeFgFromTheme(theme: unknown): (color: string, value: string) => str
328
328
  return (color, value) => maybeTheme?.fg ? maybeTheme.fg(color, value) : value;
329
329
  }
330
330
 
331
- function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): Component & { dispose?(): void } {
331
+ // Structural typing keeps keyboard-only Pi hosts (no mouse API) loading this widget unchanged.
332
+ type WidgetMouseEvent = {
333
+ type: string;
334
+ button: string;
335
+ /** Coordinates local to this component, zero-based. */
336
+ x: number;
337
+ y: number;
338
+ width: number;
339
+ shift?: boolean;
340
+ alt?: boolean;
341
+ ctrl?: boolean;
342
+ };
343
+
344
+ type WidgetMouseResult = { handled: true; render: boolean };
345
+
346
+ type MainListWidget = Component & {
347
+ dispose?(): void;
348
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined;
349
+ };
350
+
351
+ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): MainListWidget {
332
352
  const requestRender = () => tui.requestRender?.();
333
353
  state().mainListRequestRender = requestRender;
354
+ // navigatorId per rendered line, so a pointer row resolves by structure, never by row text.
355
+ let lineOwners: Array<string | undefined> = [];
356
+ let lineWidth = MAIN_LIST_FALLBACK_WIDTH;
357
+ // Pi synthesizes a click only for the component that handled the initial press.
358
+ let pressedRow: { navigatorId: string; y: number } | undefined;
359
+ const rowAt = (event: WidgetMouseEvent): string | undefined => {
360
+ const width = Number.isFinite(event.width) && event.width > 0 ? Math.floor(event.width) : lineWidth;
361
+ if (!Number.isFinite(event.x) || event.x < 0 || event.x >= width) return undefined;
362
+ if (!Number.isInteger(event.y) || event.y < 0 || event.y >= lineOwners.length) return undefined;
363
+ return lineOwners[event.y];
364
+ };
334
365
  return {
335
366
  render(width: number): string[] {
336
367
  const rows = listRows();
337
368
  syncMainListSelection(rows);
338
369
  if (!rows.length) {
339
370
  state().lastMainListLines = undefined;
371
+ lineOwners = [];
372
+ pressedRow = undefined;
340
373
  return [];
341
374
  }
342
- const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
375
+ const layout = buildMainListLayout(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
343
376
  selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
344
377
  focused: state().mainListFocused === true,
345
- })];
378
+ });
379
+ lineWidth = renderWidth(width);
380
+ // The leading gap line belongs to the widget too, so its slot stays ownerless.
381
+ lineOwners = [undefined, ...layout.owners];
382
+ const lines = ["", ...layout.lines];
346
383
  state().lastMainListLines = lines;
347
384
  return lines;
348
385
  },
349
- invalidate() { state().lastMainListLines = undefined; },
386
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
387
+ // Only an unmodified left press is ours; every other gesture reaches the host untouched.
388
+ if (event.button !== "left" || event.shift === true || event.alt === true || event.ctrl === true) {
389
+ pressedRow = undefined;
390
+ return undefined;
391
+ }
392
+ if (event.type === "press") {
393
+ pressedRow = undefined;
394
+ const navigatorId = rowAt(event);
395
+ if (!navigatorId || !listRows().some((row) => row.navigatorId === navigatorId)) return undefined;
396
+ pressedRow = { navigatorId, y: event.y };
397
+ return { handled: true, render: false };
398
+ }
399
+ if (event.type !== "click") return undefined;
400
+ const pressed = pressedRow;
401
+ pressedRow = undefined;
402
+ // A click after another component's press, or after the layout moved, is not the row we claimed.
403
+ if (!pressed || pressed.y !== event.y || rowAt(event) !== pressed.navigatorId) return undefined;
404
+ if (!openNavigatorRow(pressed.navigatorId)) return undefined;
405
+ return { handled: true, render: true };
406
+ },
407
+ invalidate() {
408
+ state().lastMainListLines = undefined;
409
+ lineOwners = [];
410
+ pressedRow = undefined;
411
+ },
350
412
  dispose() {
351
413
  const s = state();
352
414
  if (s.mainListRequestRender === requestRender) s.mainListRequestRender = undefined;
415
+ lineOwners = [];
416
+ pressedRow = undefined;
353
417
  },
354
418
  };
355
419
  }
@@ -454,7 +518,26 @@ function buildMainListLines(
454
518
  fg: (color: string, value: string) => string,
455
519
  options: { selectedId?: string; focused?: boolean } = {},
456
520
  ): string[] {
521
+ return buildMainListLayout(rows, width, truncate, fg, options).lines;
522
+ }
523
+
524
+ /**
525
+ * The list's lines plus the navigatorId each line shows, so pointer input maps a rendered row to its
526
+ * stable row identity. Headers, separators and the shortcut line keep an undefined owner.
527
+ */
528
+ function buildMainListLayout(
529
+ rows: InternalRow[],
530
+ width: number,
531
+ truncate: (s: string, width: number) => string,
532
+ fg: (color: string, value: string) => string,
533
+ options: { selectedId?: string; focused?: boolean } = {},
534
+ ): { lines: string[]; owners: Array<string | undefined> } {
457
535
  const lines: string[] = [];
536
+ const owners: Array<string | undefined> = [];
537
+ const push = (line: string, owner?: string) => {
538
+ lines.push(line);
539
+ owners.push(owner);
540
+ };
458
541
  const grouped = new Map<string, InternalRow[]>();
459
542
  for (const row of rows) {
460
543
  const existing = grouped.get(row.providerLabel) ?? [];
@@ -466,16 +549,16 @@ function buildMainListLines(
466
549
  for (let i = 0; i < orderedLabels.length; i += 1) {
467
550
  const label = orderedLabels[i]!;
468
551
  const group = grouped.get(label)!;
469
- if (i > 0) lines.push("");
470
- lines.push(providerGroupLabel(label, fg));
552
+ if (i > 0) push("");
553
+ push(providerGroupLabel(label, fg));
471
554
  for (const row of group) {
472
555
  const selected = options.focused && row.navigatorId === options.selectedId;
473
- lines.push(formatMainListRow(row, selected === true, fg, width));
556
+ push(formatMainListRow(row, selected === true, fg, width), row.navigatorId);
474
557
  }
475
558
  }
476
- lines.push("");
477
- lines.push(shortcutsLine(options.focused === true, fg));
478
- return lines.map((line) => safeTruncate(line, width, truncate));
559
+ push("");
560
+ push(shortcutsLine(options.focused === true, fg));
561
+ return { lines: lines.map((line) => safeTruncate(line, width, truncate)), owners };
479
562
  }
480
563
 
481
564
  function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
@@ -766,6 +849,20 @@ function handleMainListCloseKey(): void {
766
849
  refreshMainListWidget();
767
850
  }
768
851
 
852
+ /**
853
+ * Open the shared detail overlay on the row a pointer chose. Setting the selection first keeps the
854
+ * keyboard path identical: Enter, x and the overlay's own keys then act on the clicked row.
855
+ */
856
+ function openNavigatorRow(navigatorId: string): boolean {
857
+ const rows = listRows();
858
+ if (!rows.some((row) => row.navigatorId === navigatorId)) return false;
859
+ clearMainListCloseArm();
860
+ state().mainListSelectedId = navigatorId;
861
+ refreshMainListWidget();
862
+ openNavigator();
863
+ return true;
864
+ }
865
+
769
866
  function openNavigator(): void {
770
867
  const s = state();
771
868
  const ctx = s.uiCtx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and Outcome & Todo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,11 +53,11 @@
53
53
  "test": "node --import tsx --import ../../scripts/isolate-registry.mjs --test test/*.test.mjs test/task-context/*.test.ts"
54
54
  },
55
55
  "dependencies": {
56
- "pi-better-background-tasks": "0.11.0",
57
- "pi-better-goal": "0.10.1",
56
+ "pi-better-background-tasks": "0.12.0",
57
+ "pi-better-goal": "0.10.2",
58
58
  "pi-better-sandbox": "0.14.0",
59
59
  "pi-better-ssh": "0.1.3",
60
- "pi-better-subagents": "0.21.1",
60
+ "pi-better-subagents": "0.22.0",
61
61
  "proper-lockfile": "^4.1.2",
62
62
  "smol-toml": "1.9.0",
63
63
  "yaml": "^2.9.1"