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 +6 -0
- package/package.json +1 -1
- package/src/index.ts +40 -5
- package/src/task-context.ts +50 -0
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
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
|
|
110
|
-
: `Pi Better Goal is paused because the user pressed escape
|
|
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)
|
|
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
|
+
}
|