pi-better-harness 0.28.1 → 0.28.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.
@@ -44,7 +44,7 @@ metadata:
44
44
  workflow-role: coordinator
45
45
  ```
46
46
 
47
- Invoke the skill with Pi's `/skill:name` command, or supervise it with `/goal /skill:name task`. The goal extension resolves the command against Pi's registry, persists its source, and expands the skill on kickoff and continuation (including after session resume). A bound command that disappears or changes source pauses the goal. `/goal /template task` also re-expands prompt templates on continuation; `/goal /extension-command task` dispatches the extension command once, then continues with ordinary goal supervision to avoid repeating side effects. Plain-language goals work as before. This command binding requires Pi 0.84.4 or later. Legacy slash-shaped goals without a binding pause on resume rather than running without the skill. `/workflow` shows a coordinator skill owner; call `release_workflow` after the workflow's completion audit, or use `/workflow clear` to release ownership manually. `release_workflow` also pauses an active Goal bound to that coordinator, preserving its objective and incomplete status. Reinvoke the skill explicitly before `/goal resume`; automatic continuation cannot reacquire ownership. Unrelated ordinary Goals are unchanged. Completing an active goal also releases ownership.
47
+ Invoke the skill with Pi's `/skill:name` command, or supervise it with `/goal /skill:name task`. The goal extension resolves the command against Pi's registry, persists its source, and expands the skill on kickoff and continuation (including after session resume). A bound command that disappears or changes source pauses the goal. `/goal /template task` also re-expands prompt templates on continuation; `/goal /extension-command task` dispatches the extension command once, then continues with ordinary goal supervision to avoid repeating side effects. Plain-language goals work as before. This command binding requires Pi 0.84.4 or later. Legacy slash-shaped goals without a binding pause on resume rather than running without the skill. `/workflow` shows a coordinator skill owner. When the workflow is done, complete the goal (`update_goal`), which also releases ownership. When it hands back unfinished work, call `release_workflow`, or use `/workflow clear` to release ownership manually. `release_workflow` also pauses an active Goal bound to that coordinator as a handback, preserving its objective and incomplete status. Resuming that Goal (`/goal resume`, `alt+g`, or, with conversational resume on, the user's go-ahead such as "continue") rebinds the coordinator it was created with, provided the skill is still registered at its original source; automatic continuation and synthetic input never reacquire ownership. Unrelated ordinary Goals are unchanged.
48
48
 
49
49
  A skill that is only an alias of a coordinator declares the target instead of a role:
50
50
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,7 +66,7 @@ function isGoalStatus(value: unknown): value is GoalStatus {
66
66
  }
67
67
 
68
68
  function isPauseReason(value: unknown): value is GoalPauseReason {
69
- return value === "interrupt" || value === "permission-blocker";
69
+ return value === "interrupt" || value === "permission-blocker" || value === "handback";
70
70
  }
71
71
 
72
72
  function isGoalLike(value: unknown): value is GoalLike {
@@ -87,7 +87,8 @@ const GOAL_RESUME_RULE =
87
87
 
88
88
  /** True when the agent may resume this goal with `goal_resume`. */
89
89
  export function agentResumable(goal: GoalSnapshot | null, conversationalResume = true): boolean {
90
- return conversationalResume && goal?.status === "paused" && goal.pauseReason === "interrupt";
90
+ return conversationalResume && goal?.status === "paused" &&
91
+ (goal.pauseReason === "interrupt" || goal.pauseReason === "handback");
91
92
  }
92
93
 
93
94
  /** Footer status for a paused goal, telling the user how to resume it. */
@@ -98,7 +99,9 @@ export function pausedGoalStatus(goal: GoalSnapshot, conversationalResume = true
98
99
 
99
100
  function pausedGoalPrompt(goal: GoalSnapshot, conversationalResume: boolean): string {
100
101
  return [
101
- `Pi Better Goal is paused because the user pressed escape. Goal: ${goal.objective}`,
102
+ goal.pauseReason === "handback"
103
+ ? `Pi Better Goal is paused because its workflow handed back unfinished work for the user's answer. Goal: ${goal.objective}`
104
+ : `Pi Better Goal is paused because the user pressed escape. Goal: ${goal.objective}`,
102
105
  "Treat the user's messages as ordinary conversation: answer them, but do not continue the goal's work until it is resumed.",
103
106
  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.",
104
107
  ].join("\n");
@@ -456,15 +459,21 @@ export default function (pi: ExtensionAPI): void {
456
459
  return `Cannot resume: /${goal.command.name} is no longer registered at its original source.`;
457
460
  }
458
461
  if (goal.command?.source === "skill") {
459
- const owner = workflowOwnerFromSkill(goal.command.name.slice("skill:".length), goal.command.path, registeredSkillPath);
460
- const activeOwner = getWorkflow(ctx);
461
- if (owner && (activeOwner?.name !== owner.name || activeOwner.path !== owner.path)) {
462
- return `Reinvoke /${goal.command.name} before resuming its workflow goal.`;
462
+ try {
463
+ goalWorkflowOwner(goal);
464
+ } catch (error) {
465
+ return `Cannot resume: ${error instanceof Error ? error.message : String(error)} Reinvoke /${goal.command.name}.`;
463
466
  }
464
467
  }
465
468
  return null;
466
469
  };
467
470
 
471
+ /** The workflow a skill-bound goal owns, re-resolved from its registered skill; null for other goals. */
472
+ const goalWorkflowOwner = (goal: GoalSnapshot): ReturnType<typeof currentWorkflowOwner> =>
473
+ goal.command?.source === "skill"
474
+ ? workflowOwnerFromSkill(goal.command.name.slice("skill:".length), goal.command.path, registeredSkillPath)
475
+ : null;
476
+
468
477
  /**
469
478
  * The one resume path shared by `/goal resume`, the hotkey, and `goal_resume`:
470
479
  * reactivate a paused goal or reopen an active no-progress hold.
@@ -498,6 +507,11 @@ export default function (pi: ExtensionAPI): void {
498
507
  }
499
508
  pi.appendEntry(EXTENSION_NAME, permissionRecord(current.goalId, "permission-release"));
500
509
  }
510
+ // Resuming a skill-bound goal is explicit re-entry into its workflow: rebind the owner the
511
+ // goal was created with (released by a handback) before the continuation runs.
512
+ const owner = goalWorkflowOwner(current);
513
+ const activeOwner = getWorkflow(ctx);
514
+ if (owner && (activeOwner?.name !== owner.name || activeOwner.path !== owner.path)) recordWorkflow(owner);
501
515
  setGoal(goal, ctx, source);
502
516
  queueGoalContinuation(goal, ctx);
503
517
  return { ok: true, goal };
@@ -1049,8 +1063,8 @@ export default function (pi: ExtensionAPI): void {
1049
1063
  name: GOAL_RESUME_TOOL,
1050
1064
  label: "Resume Goal",
1051
1065
  description:
1052
- "Resume the goal the user paused with escape, exactly like /goal resume. " + GOAL_RESUME_RULE,
1053
- promptSnippet: "Resume the escape-paused goal, only on the user's clear go-ahead.",
1066
+ "Resume the goal the user paused with escape, or one its workflow handed back, exactly like /goal resume. " + GOAL_RESUME_RULE,
1067
+ promptSnippet: "Resume the escape-paused or handed-back goal, only on the user's clear go-ahead.",
1054
1068
  parameters: Type.Object({
1055
1069
  reason: Type.Optional(Type.String({ description: "Short quote or summary of the user's go-ahead." })),
1056
1070
  }),
@@ -1115,7 +1129,7 @@ export default function (pi: ExtensionAPI): void {
1115
1129
  pi.registerTool({
1116
1130
  name: "release_workflow",
1117
1131
  label: "Release Workflow",
1118
- description: "Release the active skill-owned workflow after its final handoff and completion audit. Its bound active Goal pauses; explicitly reinvoke the skill before resuming that Goal.",
1132
+ description: "Release the active skill-owned workflow when it hands back unfinished work (a blocked unit awaiting the human). Its bound active Goal pauses; /goal resume rebinds the workflow. When the workflow is done, call update_goal complete instead, which also releases it.",
1119
1133
  parameters: Type.Object({}),
1120
1134
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1121
1135
  const owner = getWorkflow(ctx);
@@ -1125,13 +1139,13 @@ export default function (pi: ExtensionAPI): void {
1125
1139
  if (goal?.status === "active" && goal.command?.source === "skill") {
1126
1140
  const boundOwner = workflowOwnerFromSkill(goal.command.name.slice("skill:".length), goal.command.path, registeredSkillPath);
1127
1141
  if (boundOwner?.name === owner.name && boundOwner.path === owner.path) {
1128
- setGoal(goalWithStatus(goal, "paused"), ctx, "tool");
1142
+ setGoal(goalWithStatus(goal, "paused", undefined, "handback"), ctx, "tool");
1129
1143
  resumeCommand = goal.command.name;
1130
1144
  }
1131
1145
  }
1132
1146
  recordWorkflow(null);
1133
1147
  return {
1134
- content: [{ type: "text", text: `Released ${owner.name} workflow ownership.${resumeCommand ? ` Its Goal is paused, not complete. Reinvoke /${resumeCommand} before /goal resume.` : ""}` }],
1148
+ content: [{ type: "text", text: `Released ${owner.name} workflow ownership.${resumeCommand ? ` Its Goal is paused, not complete; the user's go-ahead or /goal resume rebinds /${resumeCommand}.` : ""}` }],
1135
1149
  details: { released: true, owner: owner.name, goalPaused: resumeCommand !== undefined },
1136
1150
  };
1137
1151
  },
@@ -1261,7 +1275,7 @@ export default function (pi: ExtensionAPI): void {
1261
1275
  if (permissionPrompt && goal?.status === "paused") {
1262
1276
  return { systemPrompt: `${event.systemPrompt}\n\n${permissionPrompt}` };
1263
1277
  }
1264
- const pausedInstruction = permissionPrompt || (goal?.status === "paused" && goal.pauseReason === "interrupt"
1278
+ const pausedInstruction = permissionPrompt || (goal?.status === "paused" && (goal.pauseReason === "interrupt" || goal.pauseReason === "handback")
1265
1279
  ? pausedGoalPrompt(goal, preferences.conversationalResume) : "");
1266
1280
  if (!isPokeable(goal) && !owner) {
1267
1281
  return pausedInstruction ? { systemPrompt: `${event.systemPrompt}\n\n${pausedInstruction}` } : undefined;
@@ -75,7 +75,8 @@ export type GoalStatus = "active" | "paused" | "budgetLimited" | "complete";
75
75
  * requires those human controls, and releases just one same-scope retry while
76
76
  * keeping its separate blocker/release history.
77
77
  */
78
- export type GoalPauseReason = "interrupt" | "permission-blocker";
78
+ /** Why a goal paused: escape, a permission hold, or a workflow handing back unfinished work for the user's answer. */
79
+ export type GoalPauseReason = "interrupt" | "permission-blocker" | "handback";
79
80
 
80
81
  export interface GoalUsage {
81
82
  tokensUsed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "pi-better-background-tasks": "0.8.0",
57
- "pi-better-goal": "0.8.3",
57
+ "pi-better-goal": "0.8.4",
58
58
  "pi-better-plan": "0.8.0",
59
59
  "pi-better-sandbox": "0.12.0",
60
60
  "pi-better-ssh": "0.1.2",