pi-better-harness 0.29.0 → 0.29.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.
@@ -55,6 +55,8 @@ metadata:
55
55
 
56
56
  Invoking the alias (`/skill:resolve-issues`, or `/goal /skill:resolve-issues task`) records the target coordinator, with the target's registered path, as the workflow owner, so its task plan, continuation instructions, and plan tools behave exactly as if the target had been invoked. The target must be a registered `workflow-role: coordinator` skill; an unregistered target, a non-coordinator target, or a chain of aliases is refused with a notice.
57
57
 
58
+ After every successful context compaction (Pi's `session_compact` event) while a coordinator owns the session, the goal extension re-injects the skill so the model sees it before its next step; the compaction summary keeps run state but not the skill's rules, and the summary itself is left unchanged. If the skill directory ships `scripts/rehydrate.mjs` (checked first) or `rehydrate.mjs`, it runs as `node <script> --cwd <session cwd>` (no shell, 10-second timeout, stderr ignored) and its trimmed stdout becomes the message; empty output injects nothing, so the script decides whether a run is in progress. Without a script, or when it fails or times out, the message is: "Your context was just compacted while you coordinate the `<name>` workflow. Before your next step, read `<SKILL.md path>` in full, with no line limit." The message is a `workflow-rehydrate` custom message: inside a running agent loop (or before an overflow retry) it steers the next model call; otherwise it is appended without starting a turn.
59
+
58
60
  When `pi-better-plan` is installed, it defers its prompt, checklist, and `update_plan` tool to a skill-owned task plan. Skills without this metadata retain normal goal and plan behavior. The skill itself owns its planning format and worker policy; the harness does not enumerate skills or impose a shared workflow schema. The former `pi-better-plan-workflow: coordinator` metadata remains supported for installed skills.
59
61
 
60
62
  ## Install
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,6 +40,7 @@ import {
40
40
  import { createRenderScheduler } from "./shared-render-scheduler.js";
41
41
  import { collectSubagentActivity } from "./subagents.js";
42
42
  import { currentWorkflowOwner, skillCommandName, workflowEntry, workflowOwnerFromSkill, WORKFLOW_ENTRY_TYPE } from "./workflow.js";
43
+ import { workflowRehydrateMessage, WORKFLOW_REHYDRATE_MESSAGE_TYPE } from "./rehydrate.js";
43
44
  import {
44
45
  EVENT_ACTIVITY,
45
46
  EVENT_READY,
@@ -312,6 +313,8 @@ export default function (pi: ExtensionAPI): void {
312
313
  let snapshotSequence = 0;
313
314
  let lastAppliedSnapshotSequence = 0;
314
315
  let foregroundRunning = false;
316
+ /** True between agent_start and agent_end: the agent loop will call the model again without a new prompt. */
317
+ let agentLoopActive = false;
315
318
  let pollTimer: ReturnType<typeof setInterval> | undefined;
316
319
  let collecting = false;
317
320
  let collectionPending = false;
@@ -1186,6 +1189,7 @@ export default function (pi: ExtensionAPI): void {
1186
1189
  collectionPending = false;
1187
1190
  currentCtx = ctx;
1188
1191
  foregroundRunning = !ctx.isIdle();
1192
+ agentLoopActive = false;
1189
1193
  try {
1190
1194
  preferences = readGoalPreferences();
1191
1195
  } catch (error) {
@@ -1334,6 +1338,7 @@ export default function (pi: ExtensionAPI): void {
1334
1338
  agentGeneration = executionGeneration;
1335
1339
  currentCtx = ctx;
1336
1340
  foregroundRunning = true;
1341
+ agentLoopActive = true;
1337
1342
  clearIdleContinuation();
1338
1343
  continuationQueuedFor = null;
1339
1344
  lastAgentEvidence = null;
@@ -1355,6 +1360,7 @@ export default function (pi: ExtensionAPI): void {
1355
1360
  });
1356
1361
 
1357
1362
  pi.on("agent_end", async (event, ctx) => {
1363
+ agentLoopActive = false;
1358
1364
  lastAgentEvidence = agentGeneration === executionGeneration ? continuationEvidence(event.messages) : null;
1359
1365
  // Only an observed editor Escape with pause disabled exempts this abort.
1360
1366
  // Unknown interrupt sources retain the safety fallback on every host.
@@ -1483,6 +1489,29 @@ export default function (pi: ExtensionAPI): void {
1483
1489
  scheduleIdleContinuation(goal, ctx, "continuation", snapshot);
1484
1490
  });
1485
1491
 
1492
+ // A compaction summary keeps run state but drops the coordinator skill's rules. Re-inject them
1493
+ // (or the skill's own rehydrate script output) so the model sees them before its next step.
1494
+ pi.on("session_compact", async (event, ctx) => {
1495
+ const owner = getWorkflow(ctx);
1496
+ if (!owner) return;
1497
+ const content = await workflowRehydrateMessage(owner, ctx.cwd);
1498
+ if (!content) return;
1499
+ // Inside a running agent loop (mid-run compaction or an overflow retry) steer it into the next
1500
+ // model call. Otherwise append it without triggering a turn: the next turn, whatever starts it,
1501
+ // sees it. Steering after the loop ended would start an unprompted continuation.
1502
+ const options = agentLoopActive || event.willRetry ? { deliverAs: "steer" as const } : { triggerTurn: false };
1503
+ try {
1504
+ pi.sendMessage({
1505
+ customType: WORKFLOW_REHYDRATE_MESSAGE_TYPE,
1506
+ content,
1507
+ display: true,
1508
+ details: { workflow: owner.name, path: owner.path, reason: event.reason },
1509
+ }, options);
1510
+ } catch {
1511
+ // Best effort: never fail or block the session over a rehydrate notice.
1512
+ }
1513
+ });
1514
+
1486
1515
  pi.on("session_shutdown", async (_event, ctx) => {
1487
1516
  if (typeof unsubscribeSettingsRequest === "function") unsubscribeSettingsRequest();
1488
1517
 
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ import { statSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ import type { WorkflowOwner } from "./workflow.js";
6
+
7
+ export const WORKFLOW_REHYDRATE_MESSAGE_TYPE = "workflow-rehydrate";
8
+ export const REHYDRATE_TIMEOUT_MS = 10_000;
9
+
10
+ /** Script locations a coordinator skill may ship, in precedence order, relative to its SKILL.md directory. */
11
+ const REHYDRATE_SCRIPTS = [join("scripts", "rehydrate.mjs"), "rehydrate.mjs"];
12
+
13
+ export function rehydrateFallbackMessage(owner: WorkflowOwner): string {
14
+ return `Your context was just compacted while you coordinate the ${owner.name} workflow. ` +
15
+ `Before your next step, read ${owner.path} in full, with no line limit.`;
16
+ }
17
+
18
+ /** The first rehydrate script the owner's skill ships, or undefined when it ships none. */
19
+ export function findRehydrateScript(owner: WorkflowOwner): string | undefined {
20
+ const skillDir = dirname(owner.path);
21
+ for (const relative of REHYDRATE_SCRIPTS) {
22
+ const candidate = join(skillDir, relative);
23
+ try {
24
+ if (statSync(candidate).isFile()) return candidate;
25
+ } catch {
26
+ // Missing or unreadable: try the next location.
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function runScript(script: string, cwd: string, timeoutMs: number): Promise<string> {
33
+ return new Promise((resolve, reject) => {
34
+ execFile("node", [script, "--cwd", cwd], { cwd, timeout: timeoutMs, encoding: "utf8", windowsHide: true },
35
+ (error, stdout) => (error ? reject(error) : resolve(stdout)));
36
+ });
37
+ }
38
+
39
+ /**
40
+ * The message to inject after a compaction while `owner` coordinates the session, or null for none.
41
+ * A skill's rehydrate script decides the text (empty output means no run is in progress); without
42
+ * a script, or when it fails or times out, the model is told to re-read SKILL.md. Never throws.
43
+ */
44
+ export async function workflowRehydrateMessage(
45
+ owner: WorkflowOwner,
46
+ cwd: string,
47
+ options: { timeoutMs?: number } = {},
48
+ ): Promise<string | null> {
49
+ try {
50
+ const script = findRehydrateScript(owner);
51
+ if (!script) return rehydrateFallbackMessage(owner);
52
+ const stdout = (await runScript(script, cwd, options.timeoutMs ?? REHYDRATE_TIMEOUT_MS)).trim();
53
+ return stdout.length > 0 ? stdout : null;
54
+ } catch {
55
+ return rehydrateFallbackMessage(owner);
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.29.0",
3
+ "version": "0.29.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.9.0",
57
- "pi-better-goal": "0.8.4",
57
+ "pi-better-goal": "0.9.0",
58
58
  "pi-better-plan": "0.9.0",
59
59
  "pi-better-sandbox": "0.12.0",
60
60
  "pi-better-ssh": "0.1.2",