pi-better-harness 0.28.0 → 0.28.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.
@@ -18,7 +18,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
18
18
  - Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
19
19
  - A compact goal widget that does not replace Pi's footer.
20
20
  - Background activity tracking for subagents and other registered providers.
21
- - A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (60s through 600s by default), followed by a hold recoverable with `/goal resume`. Delays saturate at Node's timer limit, and observed background drains reset progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
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
24
  ## Settings Storage
@@ -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 it manually). 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; 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.
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.2",
3
+ "version": "0.8.3",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -257,6 +257,7 @@ function formatGoal(
257
257
  `Observable progress: ${stall?.state ?? "unknown"}`,
258
258
  continuationStatus,
259
259
  settings,
260
+ ...(continuation?.blocked && goal.status === "active" ? ["Resume: /goal resume or alt+g"] : []),
260
261
  ...(goal.status === "paused"
261
262
  ? [agentResumable(goal, preferences.conversationalResume)
262
263
  ? 'Resume: say "go" (the agent calls goal_resume), /goal resume, or alt+g'
@@ -432,10 +433,11 @@ export default function (pi: ExtensionAPI): void {
432
433
  if (!isPokeable(goal)) {
433
434
  return;
434
435
  }
435
- setGoal(goalWithStatus(goal, "paused", undefined, "interrupt"), ctx, "runtime");
436
+ const held = currentContinuationState(ctx, goal.goalId)?.blocked === true;
437
+ setGoal(goalWithStatus(goal, "paused", undefined, held ? undefined : "interrupt"), ctx, "runtime");
436
438
  notifyGoal(ctx, currentPermissionHold(ctx, goal.goalId).blockers.length > 0
437
439
  ? "Goal remains permission-held. Use /goal resume or alt+g for one same-scope retry."
438
- : preferences.conversationalResume
440
+ : preferences.conversationalResume && !held
439
441
  ? 'Goal paused. Say "go" to resume it, or use /goal resume.'
440
442
  : "Goal paused. Use /goal resume or alt+g to resume it.");
441
443
  };
@@ -446,13 +448,20 @@ export default function (pi: ExtensionAPI): void {
446
448
  escapeAbortSuppression.execution === executionGeneration;
447
449
 
448
450
  /** Why a paused goal cannot become active again, or null when it can. */
449
- const resumeBlocker = (goal: GoalSnapshot): string | null => {
451
+ const resumeBlocker = (goal: GoalSnapshot, ctx: ExtensionContext): string | null => {
450
452
  if (!goal.command && skillCommandName(goal.objective)) {
451
453
  return "Invoke the skill directly; this legacy slash-command goal cannot resume as plain text.";
452
454
  }
453
455
  if (goal.command && !commandAvailable(pi, goal.command)) {
454
456
  return `Cannot resume: /${goal.command.name} is no longer registered at its original source.`;
455
457
  }
458
+ 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.`;
463
+ }
464
+ }
456
465
  return null;
457
466
  };
458
467
 
@@ -469,7 +478,10 @@ export default function (pi: ExtensionAPI): void {
469
478
  if (!current || (current.status !== "paused" && !held)) {
470
479
  return { ok: false, message: "Only paused or no-progress-held goals can be resumed." };
471
480
  }
472
- const blocker = resumeBlocker(current);
481
+ if (source === "tool" && currentContinuationState(ctx, current.goalId)?.blocked) {
482
+ return { ok: false, message: "Only /goal resume or alt+g can reopen a no-progress hold." };
483
+ }
484
+ const blocker = resumeBlocker(current, ctx);
473
485
  if (blocker) {
474
486
  return { ok: false, message: blocker };
475
487
  }
@@ -544,7 +556,7 @@ export default function (pi: ExtensionAPI): void {
544
556
  return;
545
557
  }
546
558
  if (currentPermissionHold(ctx, goal.goalId).blockers.length > 0) return;
547
- if (kind === "continuation" && currentContinuationState(ctx, goal.goalId)?.blocked) {
559
+ if (currentContinuationState(ctx, goal.goalId)?.blocked) {
548
560
  return;
549
561
  }
550
562
  const signature = `${goal.goalId}:${kind}`;
@@ -575,6 +587,7 @@ export default function (pi: ExtensionAPI): void {
575
587
  const current = currentGoalSnapshot(ctx);
576
588
  return isPokeable(current) && current.goalId === goalId &&
577
589
  currentPermissionHold(ctx, goalId).blockers.length === 0 &&
590
+ !currentContinuationState(ctx, goalId)?.blocked &&
578
591
  continuationQueuedFor !== goalId && !isForegroundBusy(ctx);
579
592
  };
580
593
  const goal = currentGoalSnapshot(ctx);
@@ -637,7 +650,8 @@ export default function (pi: ExtensionAPI): void {
637
650
  const goal = currentGoalSnapshot(ctx);
638
651
  const wakePlan = planBackgroundDrainWake(backgroundDrainTracker, goal, snapshot);
639
652
  backgroundDrainTracker = wakePlan.nextTracker;
640
- if (isPokeable(goal) && wakePlan.wakeSignature && currentPermissionHold(ctx, goal.goalId).blockers.length === 0) {
653
+ if (isPokeable(goal) && wakePlan.wakeSignature && currentPermissionHold(ctx, goal.goalId).blockers.length === 0 &&
654
+ !currentContinuationState(ctx, goal.goalId)?.blocked) {
641
655
  // Persist external progress now: a foreground/callback turn may cancel its delayed wake.
642
656
  clearIdleContinuation();
643
657
  resetContinuationState(goal);
@@ -1101,13 +1115,25 @@ export default function (pi: ExtensionAPI): void {
1101
1115
  pi.registerTool({
1102
1116
  name: "release_workflow",
1103
1117
  label: "Release Workflow",
1104
- description: "Release the active skill-owned workflow after its final handoff and completion audit.",
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.",
1105
1119
  parameters: Type.Object({}),
1106
1120
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1107
1121
  const owner = getWorkflow(ctx);
1108
1122
  if (!owner) return { content: [{ type: "text", text: "No workflow owns this session." }], details: { released: false } };
1123
+ const goal = getGoal(ctx);
1124
+ let resumeCommand: string | undefined;
1125
+ if (goal?.status === "active" && goal.command?.source === "skill") {
1126
+ const boundOwner = workflowOwnerFromSkill(goal.command.name.slice("skill:".length), goal.command.path, registeredSkillPath);
1127
+ if (boundOwner?.name === owner.name && boundOwner.path === owner.path) {
1128
+ setGoal(goalWithStatus(goal, "paused"), ctx, "tool");
1129
+ resumeCommand = goal.command.name;
1130
+ }
1131
+ }
1109
1132
  recordWorkflow(null);
1110
- return { content: [{ type: "text", text: `Released ${owner.name} workflow ownership.` }], details: { released: true, owner: owner.name } };
1133
+ return {
1134
+ content: [{ type: "text", text: `Released ${owner.name} workflow ownership.${resumeCommand ? ` Its Goal is paused, not complete. Reinvoke /${resumeCommand} before /goal resume.` : ""}` }],
1135
+ details: { released: true, owner: owner.name, goalPaused: resumeCommand !== undefined },
1136
+ };
1111
1137
  },
1112
1138
  });
1113
1139
 
@@ -1211,7 +1237,8 @@ export default function (pi: ExtensionAPI): void {
1211
1237
  // A paused goal stays paused while the user talks: the message is ordinary
1212
1238
  // conversation. Only /goal resume, the hotkey, or goal_resume resume it.
1213
1239
  const goal = getGoal(ctx);
1214
- if (goal?.status === "active" && currentPermissionHold(ctx, goal.goalId).blockers.length === 0) {
1240
+ if (goal?.status === "active" && currentPermissionHold(ctx, goal.goalId).blockers.length === 0 &&
1241
+ !currentContinuationState(ctx, goal.goalId)?.blocked) {
1215
1242
  executionGeneration += 1;
1216
1243
  clearIdleContinuation();
1217
1244
  lastAgentEvidence = null;
@@ -1243,6 +1270,9 @@ export default function (pi: ExtensionAPI): void {
1243
1270
  const questionInstruction = snapshot.backgroundRunning
1244
1271
  ? " A blocking user question (ask_user_question) holds this whole turn until the user answers, and background completions wait behind it. Harvest finished background results before asking, and ask only when the answer is needed to proceed."
1245
1272
  : "";
1273
+ const heldInstruction = goal?.status === "active" && currentContinuationState(ctx, goal.goalId)?.blocked
1274
+ ? "Automatic Goal continuation is held for no progress. Answer the current user message or inspect delivered background results without restarting autonomous Goal work. Only /goal resume or alt+g reopens this hold."
1275
+ : "";
1246
1276
 
1247
1277
  if (owner) {
1248
1278
  if (!workflowAvailable(owner)) {
@@ -1266,7 +1296,8 @@ export default function (pi: ExtensionAPI): void {
1266
1296
  : "") +
1267
1297
  `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}` +
1268
1298
  (isPokeable(goal) ? `\n\nActive objective: ${goal.objective}. Complete it only after the workflow completion audit.` : "") +
1269
- (questionInstruction ? `\n\n${questionInstruction.trim()}` : ""),
1299
+ (questionInstruction ? `\n\n${questionInstruction.trim()}` : "") +
1300
+ (heldInstruction ? `\n\n${heldInstruction}` : ""),
1270
1301
  };
1271
1302
  }
1272
1303
 
@@ -1277,7 +1308,7 @@ export default function (pi: ExtensionAPI): void {
1277
1308
  return {
1278
1309
  systemPrompt:
1279
1310
  `${event.systemPrompt}\n\n` +
1280
- `Pi Better Goal active objective: ${goal.objective}. Keep working through clear low-risk next steps, and mark complete only after an evidence-backed completion audit.${backgroundInstruction}${questionInstruction}` +
1311
+ `Pi Better Goal active objective: ${goal.objective}. ${heldInstruction || "Keep working through clear low-risk next steps, and mark complete only after an evidence-backed completion audit."}${backgroundInstruction}${questionInstruction}` +
1281
1312
  (permissionPrompt ? `\n\n${permissionPrompt}` : ""),
1282
1313
  };
1283
1314
  });
@@ -1408,6 +1439,7 @@ export default function (pi: ExtensionAPI): void {
1408
1439
  return;
1409
1440
  }
1410
1441
  const previous = currentContinuationState(ctx, goal.goalId) ?? createContinuationState(goal.goalId);
1442
+ if (previous.blocked) return;
1411
1443
  const repeated = previous.lastEvidenceSignature === evidence.signature;
1412
1444
  const noProgressRetries = repeated ? previous.noProgressRetries + 1 : 0;
1413
1445
  const blocked = repeated && noProgressRetries >= MAX_NO_PROGRESS_RETRIES;
@@ -1428,7 +1460,7 @@ export default function (pi: ExtensionAPI): void {
1428
1460
  if (ctx.hasUI) {
1429
1461
  ctx.ui.setStatus(EXTENSION_NAME, "waiting: no progress");
1430
1462
  ctx.ui.notify(
1431
- `Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). New input or background activity will resume it.`,
1463
+ `Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). Use /goal resume or alt+g to resume it.`,
1432
1464
  "warning",
1433
1465
  );
1434
1466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
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.2",
57
+ "pi-better-goal": "0.8.3",
58
58
  "pi-better-plan": "0.8.0",
59
59
  "pi-better-sandbox": "0.12.0",
60
60
  "pi-better-ssh": "0.1.2",