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
|
|
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,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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|