pi-better-goal 0.10.1 → 0.10.2

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
@@ -21,6 +21,12 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
21
21
  - A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (60s through 600s by default), followed by a sticky hold recoverable with `/goal resume` or `alt+g`. Conversation, changed callback results, and background drains do not reopen an exhausted hold. Delays saturate at Node's timer limit, and observed background drains reset non-held progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
22
22
  - An observable-progress stall state for active goals.
23
23
 
24
+ ## Optional Harness-Owned Outcome & Todo
25
+
26
+ When `pi-better-harness` is installed, its optional **Outcome & Todo** runtime owns the session's structured outcome and checklist. Goal keeps its own objective, clocks, pause and permission holds, workflow gates, background wake behavior, and continuation policy; it also sends lifecycle requests to the Harness runtime so a bound outcome starts, pauses, resumes, and completes with the Goal. When the runtime is present, Goal stops adding its own active-objective text to avoid duplicating the Harness-owned outcome, whether Outcome & Todo is enabled or off.
27
+
28
+ `/goal` does not install, enable, or configure Outcome & Todo. Install Harness and enable **Outcome & Todo** in `/harness-settings` if you want that shared outcome and checklist. Without Harness, this package keeps its standalone prompts and behavior unchanged. The stored Goal objective remains the raw original trigger (for example, a bound command invocation) so `/goal resume` can re-enter the work; it is not the authoritative business outcome when Harness owns one.
29
+
24
30
  ## Settings Storage
25
31
 
26
32
  Goal preferences are saved only in Pi's global `~/.pi/agent/settings.json` under
package/package.json CHANGED
@@ -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",
package/src/index.ts CHANGED
@@ -42,6 +42,7 @@ import { collectSubagentActivity } from "./subagents.js";
42
42
  import { currentWorkflowOwner, skillCommandName, workflowEntry, workflowOwnerFromSkill, WORKFLOW_ENTRY_TYPE } from "./workflow.js";
43
43
  import { completionGate, pauseGate, schedulerWaitUntil } from "./workflow-gates.js";
44
44
  import { workflowRehydrateMessage, WORKFLOW_REHYDRATE_MESSAGE_TYPE } from "./rehydrate.js";
45
+ import { requestTaskContext } from "./task-context.js";
45
46
  import {
46
47
  EVENT_ACTIVITY,
47
48
  EVENT_READY,
@@ -103,11 +104,11 @@ export function pausedGoalStatus(goal: GoalSnapshot, conversationalResume = true
103
104
  return agentResumable(goal, conversationalResume) ? 'goal paused · say "go" or /goal resume' : "goal paused · /goal resume";
104
105
  }
105
106
 
106
- function pausedGoalPrompt(goal: GoalSnapshot, conversationalResume: boolean): string {
107
+ function pausedGoalPrompt(goal: GoalSnapshot, conversationalResume: boolean, includeObjective = true): string {
107
108
  return [
108
109
  goal.pauseReason === "handback"
109
- ? `Pi Better Goal is paused because its workflow handed back unfinished work for the user's answer. Goal: ${goal.objective}`
110
- : `Pi Better Goal is paused because the user pressed escape. Goal: ${goal.objective}`,
110
+ ? `Pi Better Goal is paused because its workflow handed back unfinished work for the user's answer.${includeObjective ? ` Goal: ${goal.objective}` : ""}`
111
+ : `Pi Better Goal is paused because the user pressed escape.${includeObjective ? ` Goal: ${goal.objective}` : ""}`,
111
112
  "Treat the user's messages as ordinary conversation: answer them, but do not continue the goal's work until it is resumed.",
112
113
  conversationalResume ? GOAL_RESUME_RULE : "Only the user can resume this goal, with /goal resume or alt+g. A conversational go-ahead does not resume it.",
113
114
  ].join("\n");
@@ -407,6 +408,17 @@ export default function (pi: ExtensionAPI): void {
407
408
  const previous = getGoal(ctx);
408
409
  const wasVisible = previous !== null && isGoalClockVisible(previous);
409
410
  pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
411
+ // Bridge lifecycle after the raw Goal entry is durable. Harness may own the
412
+ // structured outcome, but Goal still owns its trigger, clocks, pauses, and wakes.
413
+ if (!previous || previous.goalId !== goal.goalId) {
414
+ requestTaskContext(pi, "start", goal);
415
+ } else if (goal.status === "complete") {
416
+ requestTaskContext(pi, "complete", goal);
417
+ } else if (goal.status === "paused" || goal.status === "budgetLimited") {
418
+ requestTaskContext(pi, "pause", goal);
419
+ } else if (goal.status === "active" && previous.status !== "active") {
420
+ requestTaskContext(pi, "resume", goal);
421
+ }
410
422
  continuationQueuedFor = null;
411
423
  backgroundDrainTracker = null;
412
424
  clearIdleContinuation();
@@ -422,6 +434,8 @@ export default function (pi: ExtensionAPI): void {
422
434
  lastAgentEvidence = null;
423
435
  const current = getGoal(ctx);
424
436
  const wasVisible = current !== null && isGoalClockVisible(current);
437
+ // Clearing pauses a Harness-bound outcome; it never deletes or completes it.
438
+ if (current) requestTaskContext(pi, "pause", current);
425
439
  pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
426
440
  if (getWorkflow(ctx)) recordWorkflow(null);
427
441
  continuationQueuedFor = null;
@@ -868,6 +882,11 @@ export default function (pi: ExtensionAPI): void {
868
882
  }
869
883
  const nextRenderDelay = goalClockRefreshDelayMs(goal);
870
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
+ }
871
890
  const fg = typeof theme?.fg === "function"
872
891
  ? (color: string, value: string) => theme.fg(color as never, value)
873
892
  : undefined;
@@ -1318,6 +1337,9 @@ export default function (pi: ExtensionAPI): void {
1318
1337
  currentCtx = ctx;
1319
1338
  const goal = currentGoalSnapshot(ctx);
1320
1339
  const owner = getWorkflow(ctx);
1340
+ // A defined `enabled` means a Harness runtime is present, even when
1341
+ // Outcome & Todo is off. Then Harness owns the structured outcome text.
1342
+ const harnessPresent = requestTaskContext(pi, "inspect", goal).enabled !== undefined;
1321
1343
  const snapshot = await publishSnapshot(ctx);
1322
1344
  if (!snapshot) return;
1323
1345
  const permissions = goal ? currentPermissionHold(ctx, goal.goalId) : null;
@@ -1330,7 +1352,7 @@ export default function (pi: ExtensionAPI): void {
1330
1352
  return { systemPrompt: `${event.systemPrompt}\n\n${permissionPrompt}` };
1331
1353
  }
1332
1354
  const pausedInstruction = permissionPrompt || (goal?.status === "paused" && (goal.pauseReason === "interrupt" || goal.pauseReason === "handback")
1333
- ? pausedGoalPrompt(goal, preferences.conversationalResume) : "");
1355
+ ? pausedGoalPrompt(goal, preferences.conversationalResume, !harnessPresent) : "");
1334
1356
  if (!isPokeable(goal) && !owner) {
1335
1357
  return pausedInstruction ? { systemPrompt: `${event.systemPrompt}\n\n${pausedInstruction}` } : undefined;
1336
1358
  }
@@ -1363,7 +1385,11 @@ export default function (pi: ExtensionAPI): void {
1363
1385
  : "While the goal is paused, this overrides the workflow instructions below: do not advance the workflow until the goal is resumed.\n\n")
1364
1386
  : "") +
1365
1387
  `Active workflow: ${owner.name} (${owner.path}). Its task plan owns planning and the parent is a coordinator, not a product-code implementer. Follow the workflow instructions below, including on resumed turns:\n\n${instructions}` +
1366
- (isPokeable(goal) ? `\n\nActive objective: ${goal.objective}. Complete it only after the workflow completion audit.` : "") +
1388
+ (isPokeable(goal)
1389
+ ? harnessPresent
1390
+ ? "\n\nComplete the goal only after the workflow completion audit."
1391
+ : `\n\nActive objective: ${goal.objective}. Complete it only after the workflow completion audit.`
1392
+ : "") +
1367
1393
  (questionInstruction ? `\n\n${questionInstruction.trim()}` : "") +
1368
1394
  (heldInstruction ? `\n\n${heldInstruction}` : ""),
1369
1395
  };
@@ -1373,6 +1399,15 @@ export default function (pi: ExtensionAPI): void {
1373
1399
  const backgroundInstruction = snapshot.backgroundRunning
1374
1400
  ? ` The goal still has delegated background work running (${summarizeActiveBackground(snapshot)}). Foreground idleness alone is not goal completion; keep any structured plan current and do not mark verification, the plan, or the goal complete until every relevant delegated task reaches a terminal state and its result or failure has been inspected and integrated.`
1375
1401
  : "";
1402
+ if (harnessPresent) {
1403
+ // Harness owns the outcome text. Retain only the Goal-specific guidance
1404
+ // that the Outcome & Todo prompt cannot replace.
1405
+ const retained = [heldInstruction, backgroundInstruction.trim(), questionInstruction.trim(), permissionPrompt]
1406
+ .filter((part) => part.length > 0);
1407
+ return retained.length > 0
1408
+ ? { systemPrompt: `${event.systemPrompt}\n\n${retained.join("\n\n")}` }
1409
+ : undefined;
1410
+ }
1376
1411
  return {
1377
1412
  systemPrompt:
1378
1413
  `${event.systemPrompt}\n\n` +
@@ -0,0 +1,50 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { GoalSnapshot } from "./types.js";
3
+
4
+ /** Synchronous request bus shared with an installed `pi-better-harness` runtime. */
5
+ export const TASK_CONTEXT_EVENT = "pi-better-harness:task-context";
6
+
7
+ export type TaskContextAction = "inspect" | "start" | "pause" | "resume" | "complete";
8
+
9
+ /**
10
+ * Mutable request. A present Harness runtime sets `enabled` to a boolean even
11
+ * when Outcome & Todo is off. Without Harness the field stays undefined, which
12
+ * lets Goal retain its standalone behavior.
13
+ */
14
+ export interface TaskContextRequest {
15
+ action: TaskContextAction;
16
+ goalId?: string;
17
+ /** Current widget width on a visual ownership inspection. */
18
+ width?: number;
19
+ /** The raw original goal trigger; only start requests carry it. */
20
+ objective?: string;
21
+ /** Hooks authority: a present Harness runtime sets this even when Outcome & Todo is off. */
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;
30
+ }
31
+
32
+ /**
33
+ * Emit one task-context request and return it so the caller can inspect the
34
+ * Harness-owned `enabled` field after synchronous listeners run.
35
+ */
36
+ export function requestTaskContext(
37
+ pi: Pick<ExtensionAPI, "events">,
38
+ action: TaskContextAction,
39
+ goal?: GoalSnapshot | null,
40
+ width?: number,
41
+ ): TaskContextRequest {
42
+ const request: TaskContextRequest = { action };
43
+ if (width !== undefined) request.width = width;
44
+ if (goal) {
45
+ request.goalId = goal.goalId;
46
+ if (action === "start") request.objective = goal.objective;
47
+ }
48
+ pi.events.emit(TASK_CONTEXT_EVENT, request);
49
+ return request;
50
+ }