pi-better-harness 0.31.0 → 0.31.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.
@@ -59,7 +59,7 @@ After every successful context compaction (Pi's `session_compact` event) while a
59
59
 
60
60
  ### Scheduler-backed workflow gates
61
61
 
62
- A coordinator that ships the resolve-issues scheduler (`scripts/next-actions.mjs` in its skill directory, or in a sibling `resolve-issues` skill) keeps its Goal from ending while the run still has work. The extension runs `node next-actions.mjs --run <run dir> --slots 1 --peek --json` (read-only, 20-second timeout), where the run directory is the folder of the task plan bound through `update_plan`. Every gate fails open: if the scheduler is missing, errors, times out, prints bad output, or no plan is bound yet, the call proceeds and its result carries a warning, so nothing deadlocks.
62
+ A coordinator that ships the resolve-issues scheduler (`scripts/next-actions.mjs` in its skill directory, or in a sibling `resolve-issues` skill) keeps its Goal from ending while the run still has work. The extension first runs `node next-actions.mjs --run <run dir> --slots 1 --peek --json` (read-only, 20-second timeout), where the run directory is the folder of the task plan bound through `update_plan`. For completion and handback only, if the sole due action is `sync`, it reruns with `--sync` to obtain a fresh issue tree. This live check has a four-minute timeout to accommodate the tree fetcher's network attempts and follow-ups. Because `--peek` suppresses scheduler bookkeeping but not issue-tree cache writes, the live check uses a temporary copy of the scheduler's plan, tree, bookkeeping, review, and result inputs; it does not write the bound run. Heartbeat probes never request live synchronization. A failed or incomplete tree fetch that produces `sync` still refuses completion or handback; cached evidence is not treated as live. Every gate retains its fail-open behavior for execution failures: if the scheduler is missing, errors, times out, prints bad output, or no plan is bound yet, the call proceeds and its result carries a warning, so nothing deadlocks.
63
63
 
64
64
  - **Completion.** `update_goal` complete is refused, with each due action (action, unit, first 200 characters of detail), unless the scheduler reports `finish` and nothing else is due (an empty due list means workers are still running, so it is refused too). The Goal stays active.
65
65
  - **Pause.** `release_workflow` is refused unless every unfinished unit is quiet (nothing due for it) or blocked `human-decision` with `humanItem` 2-5. A unit blocked `retry-ci` or any other reason the run retries itself, or a `human-decision` item 1, is named in the refusal and the Goal stays active.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
- import { readFileSync, statSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
3
4
  import { dirname, join } from "node:path";
4
5
 
5
6
  import type { WorkflowOwner } from "./workflow.js";
@@ -8,6 +9,8 @@ import type { WorkflowOwner } from "./workflow.js";
8
9
  const WORKFLOW_PLAN_ENTRY = "pi-better-workflow-plan";
9
10
  const WORKFLOW_ENTRY = "pi-better-workflow";
10
11
  export const SCHEDULER_TIMEOUT_MS = 20_000;
12
+ // Live tree queries allow three 60-second attempts plus follow-ups; cached probes stay cheap.
13
+ const SCHEDULER_SYNC_TIMEOUT_MS = 240_000;
11
14
  const DETAIL_LIMIT = 200;
12
15
 
13
16
  /** Scheduler locations relative to the owner's skill directory, in precedence order. */
@@ -100,9 +103,13 @@ function parseSchedulerOutput(stdout: string): SchedulerRun {
100
103
  * are due and 2 on a usage or plan error, so only the latter, a timeout, or bad output is a failure.
101
104
  */
102
105
  export function runScheduler(script: string, runDir: string, timeoutMs = SCHEDULER_TIMEOUT_MS): Promise<SchedulerRun> {
106
+ return executeScheduler(script, runDir, timeoutMs);
107
+ }
108
+
109
+ function executeScheduler(script: string, runDir: string, timeoutMs: number, live = false, cwd = runDir): Promise<SchedulerRun> {
103
110
  return new Promise((resolve) => {
104
- execFile("node", [script, "--run", runDir, "--slots", "1", "--peek", "--json"],
105
- { cwd: runDir, timeout: timeoutMs, encoding: "utf8", windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
111
+ execFile("node", [script, "--run", runDir, "--slots", "1", "--peek", "--json", ...(live ? ["--sync"] : [])],
112
+ { cwd, timeout: timeoutMs, encoding: "utf8", windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
106
113
  (error, stdout, stderr) => {
107
114
  if (error) {
108
115
  // An exit status of 1 means "actions due"; any other failure (exit 2, timeout, spawn error) is real.
@@ -117,6 +124,27 @@ export function runScheduler(script: string, runDir: string, timeoutMs = SCHEDUL
117
124
  });
118
125
  }
119
126
 
127
+ async function settlementScheduler(script: string, runDir: string, timeoutMs?: number): Promise<SchedulerRun> {
128
+ const cached = await runScheduler(script, runDir, timeoutMs);
129
+ if (!cached.ok || cached.due.length !== 1 || cached.due[0]!.action !== "sync") return cached;
130
+
131
+ // --peek suppresses seen bookkeeping, not syncRun's issue-tree write. Give the live
132
+ // probe private inputs; keep cwd so relative workspace paths retain their meaning.
133
+ let snapshot: string | undefined;
134
+ try {
135
+ snapshot = mkdtempSync(join(tmpdir(), "pi-goal-scheduler-"));
136
+ for (const name of ["task-plan.json", "issue-tree.json", "next-actions-seen.json", "reviews", "results"]) {
137
+ const source = join(runDir, name);
138
+ if (existsSync(source)) cpSync(source, join(snapshot, name), { recursive: true, dereference: true });
139
+ }
140
+ return await executeScheduler(script, snapshot, timeoutMs ?? SCHEDULER_SYNC_TIMEOUT_MS, true, runDir);
141
+ } catch (error) {
142
+ return { ok: false, reason: `could not prepare the live scheduler check (${String(error).slice(0, DETAIL_LIMIT)})` };
143
+ } finally {
144
+ if (snapshot) rmSync(snapshot, { recursive: true, force: true });
145
+ }
146
+ }
147
+
120
148
  interface PlannedRun {
121
149
  runDir: string;
122
150
  script: string;
@@ -147,7 +175,7 @@ export async function completionGate(
147
175
  ): Promise<GateVerdict> {
148
176
  const located = locateRun(entries, owner);
149
177
  if ("warning" in located) return unchecked(located.warning);
150
- const run = await runScheduler(located.script, located.runDir, options.timeoutMs);
178
+ const run = await settlementScheduler(located.script, located.runDir, options.timeoutMs);
151
179
  if (!run.ok) return unchecked(`could not check the ${owner.name} run (${run.reason})`);
152
180
  const open = run.due.filter((action) => action.action !== "finish");
153
181
  if (open.length === 0 && run.due.length > 0) return { allow: true };
@@ -190,7 +218,7 @@ export async function pauseGate(
190
218
  if ("warning" in located) return unchecked(located.warning);
191
219
  const units = readUnits(located.runDir);
192
220
  if (!units) return unchecked(`could not read the ${owner.name} task plan`);
193
- const run = await runScheduler(located.script, located.runDir, options.timeoutMs);
221
+ const run = await settlementScheduler(located.script, located.runDir, options.timeoutMs);
194
222
  if (!run.ok) return unchecked(`could not check the ${owner.name} run (${run.reason})`);
195
223
 
196
224
  const refusals: string[] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.31.0",
3
+ "version": "0.31.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.1",
57
- "pi-better-goal": "0.10.0",
57
+ "pi-better-goal": "0.10.1",
58
58
  "pi-better-plan": "0.9.1",
59
59
  "pi-better-sandbox": "0.12.1",
60
60
  "pi-better-ssh": "0.1.3",