pi-goal-list-loop-audit 0.23.2 → 0.23.4
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.
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ No external watchdog plugin needed.
|
|
|
184
184
|
/glla notify='cmd "$1"' # push on complete/pause/stop → GLOBAL
|
|
185
185
|
/glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
|
|
186
186
|
/glla tokenlimit=0 # explicitly no cap (the default)
|
|
187
|
-
/glla wedgealert=30 # hung-command alert minutes (default:
|
|
187
|
+
/glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
|
|
188
188
|
/glla project tokenlimit=500 # rare per-project override
|
|
189
189
|
```
|
|
190
190
|
|
|
@@ -208,10 +208,15 @@ this cap — it has its own brakes
|
|
|
208
208
|
The turn-based watchdogs can't see one failure shape: the session is busy
|
|
209
209
|
but silent for a long stretch because ONE unbounded command (a test suite
|
|
210
210
|
that never exits, a dev server) is holding the whole goal hostage. The
|
|
211
|
-
heartbeat watches the wall clock: busy + no activity for
|
|
211
|
+
heartbeat watches the wall clock: busy + no activity for 30 minutes →
|
|
212
212
|
in-session warning + your configured notify push, once per interval while
|
|
213
213
|
it persists. Tune with `/glla wedgealert=<minutes>` (0 = off).
|
|
214
214
|
|
|
215
|
+
Every other wait is bounded too: continuation retries are milliseconds,
|
|
216
|
+
stuck backoff caps at 5 minutes then pauses, measure commands get a 10m
|
|
217
|
+
hard timeout, and the auditor aborts after 10m with zero session activity
|
|
218
|
+
(infrastructure error, never a verdict).
|
|
219
|
+
|
|
215
220
|
## Compatibility (what goes well, what conflicts)
|
|
216
221
|
|
|
217
222
|
**The Two-Driver Rule**: any plugin that drives agent turns on `agent_end`
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
|
|
26
26
|
import type { Goal } from "./goal-loop-core.js";
|
|
27
27
|
import { renderGoalMarkdown } from "./goal-loop-core.js";
|
|
28
|
+
import { AUDITOR_STALL_MS } from "./goal-loop-backoff.js";
|
|
28
29
|
|
|
29
30
|
// =================================================================
|
|
30
31
|
// Result type
|
|
@@ -239,7 +240,22 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
239
240
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
240
241
|
});
|
|
241
242
|
let streamError: string | undefined;
|
|
243
|
+
// v0.23.3 stall watchdog: any session event is activity. An auditor
|
|
244
|
+
// with NO events for AUDITOR_STALL_MS is wedged (dead stream, hung
|
|
245
|
+
// provider call) — abort it and return an ERROR, never a verdict,
|
|
246
|
+
// and never let the completion gate hang forever (the pi-goal-x
|
|
247
|
+
// failure shape: unbounded silent waits).
|
|
248
|
+
let lastEventAt = Date.now();
|
|
249
|
+
let stalled = false;
|
|
250
|
+
const stallTimer = setInterval(() => {
|
|
251
|
+
if (Date.now() - lastEventAt > AUDITOR_STALL_MS) {
|
|
252
|
+
stalled = true;
|
|
253
|
+
void session.abort();
|
|
254
|
+
}
|
|
255
|
+
}, 15_000);
|
|
256
|
+
stallTimer.unref?.();
|
|
242
257
|
const unsub = session.subscribe((event) => {
|
|
258
|
+
lastEventAt = Date.now();
|
|
243
259
|
// Capture provider/stream errors (401/403/429/credits) — these often
|
|
244
260
|
// arrive as events rather than thrown exceptions, and without this they
|
|
245
261
|
// surface as a silent empty report that looks like a disapproval.
|
|
@@ -306,9 +322,21 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
306
322
|
}
|
|
307
323
|
await session.prompt(buildGoalAuditorPrompt(args.goal, args.completionSummary, args.verificationSummary));
|
|
308
324
|
} finally {
|
|
325
|
+
clearInterval(stallTimer);
|
|
309
326
|
unsub();
|
|
310
327
|
}
|
|
311
328
|
|
|
329
|
+
if (stalled) {
|
|
330
|
+
return {
|
|
331
|
+
approved: false,
|
|
332
|
+
disapproved: false,
|
|
333
|
+
output: outputParts.join("\n\n"),
|
|
334
|
+
model: modelLabel(model),
|
|
335
|
+
thinkingLevel,
|
|
336
|
+
error: `Auditor stalled — no session activity for ${Math.round(AUDITOR_STALL_MS / 60_000)}m, aborted. This is an infrastructure failure, not a verdict; retry completion (check the auditor model with /glla model=).`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
const output = outputParts.join("\n\n");
|
|
313
341
|
|
|
314
342
|
// SILENT-FAILURE GUARD (v0.9.9, wild-caught): an auditor that produced
|
|
@@ -72,8 +72,23 @@ export const HEARTBEAT_MAX_NUDGES = 3;
|
|
|
72
72
|
* activity for this long is almost always a hung unbounded command
|
|
73
73
|
* (test suite / dev server) holding the whole goal hostage. The
|
|
74
74
|
* turn-based watchdogs are blind to it (it's ONE long turn); only the
|
|
75
|
-
* wall clock sees it. 0 in settings = off.
|
|
76
|
-
|
|
75
|
+
* wall clock sees it. 0 in settings = off.
|
|
76
|
+
* v0.23.3: 45 → 30. The alert is notification-only, so a false positive
|
|
77
|
+
* costs one notification while a false negative costs hours — that
|
|
78
|
+
* asymmetry argues tight. (pi-goal-x, the cautionary tale, had NO wall
|
|
79
|
+
* clock at all: a wedged session was silent forever.) */
|
|
80
|
+
export const WEDGE_ALERT_DEFAULT_MINUTES = 30;
|
|
81
|
+
/** v0.23.3: hard cap on one measure command. An unbounded measure is the
|
|
82
|
+
* same wedge shape as an unbounded test suite — it freezes the loop tick
|
|
83
|
+
* forever. Timeout → measure failure (null) → stall path → plateau stop,
|
|
84
|
+
* never a silent hang. Matches pi-loop-mode's --check-timeout default. */
|
|
85
|
+
export const MEASURE_TIMEOUT_MS = 10 * 60_000;
|
|
86
|
+
/** v0.23.3: auditor inactivity abort. The auditor legitimately runs the
|
|
87
|
+
* project's own verification (test suites!), so the bound is on
|
|
88
|
+
* INACTIVITY (no session events at all), not wall time. An auditor with
|
|
89
|
+
* no events for this long is wedged — abort and report an error, never
|
|
90
|
+
* disapprove, never hang the completion gate forever. */
|
|
91
|
+
export const AUDITOR_STALL_MS = 10 * 60_000;
|
|
77
92
|
|
|
78
93
|
export interface WedgeInput {
|
|
79
94
|
/** A goal is active (autoContinue) or a loop is running. */
|
|
@@ -23,7 +23,16 @@ export function contractItems(contract: string): string[] {
|
|
|
23
23
|
.filter((l) => l.length > 0)
|
|
24
24
|
// Boundary lines ("Out of scope: ...") constrain the auditor's judgment;
|
|
25
25
|
// they are not deliverables and have no evidence to quote (v0.22.6).
|
|
26
|
-
.filter((l) => !/^out of scope\b/i.test(l))
|
|
26
|
+
.filter((l) => !/^out of scope\b/i.test(l))
|
|
27
|
+
// Preamble lines are not checkable items (v0.23.4, darklord field bug:
|
|
28
|
+
// "Done when ALL of the following are true:" survived as an "item" —
|
|
29
|
+
// the prefix strip only fires when a colon directly follows "done
|
|
30
|
+
// when" — and the shield then blocked TWO genuine approvals forever,
|
|
31
|
+
// because no evidence can reference a preamble). Two mechanical
|
|
32
|
+
// predicates: a line still ending in a colon introduces a list, and a
|
|
33
|
+
// "(done when) (all of) the following ..." line IS the introducer.
|
|
34
|
+
.filter((l) => !l.endsWith(":"))
|
|
35
|
+
.filter((l) => !/^(?:done when\s+)?(?:all of\s+)?the following\b/i.test(l));
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export interface RegressionShieldResult {
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
HEARTBEAT_MAX_NUDGES,
|
|
78
78
|
HEARTBEAT_STALL_MS,
|
|
79
79
|
shouldHeartbeatRefire,
|
|
80
|
+
MEASURE_TIMEOUT_MS,
|
|
80
81
|
WEDGE_ALERT_DEFAULT_MINUTES,
|
|
81
82
|
shouldWedgeAlert,
|
|
82
83
|
} from "../goal-loop-backoff.js";
|
|
@@ -958,7 +959,7 @@ function isLoopActive(): boolean {
|
|
|
958
959
|
async function runMeasure(ctx: ExtensionContext, cmd: string): Promise<number | null> {
|
|
959
960
|
if (!extensionApi) return null;
|
|
960
961
|
try {
|
|
961
|
-
const result = await extensionApi.exec("bash", ["-c", cmd], { cwd: ctx.cwd });
|
|
962
|
+
const result = await extensionApi.exec("bash", ["-c", cmd], { cwd: ctx.cwd, timeout: MEASURE_TIMEOUT_MS });
|
|
962
963
|
const stdout = (result as any)?.stdout ?? "";
|
|
963
964
|
return parseMetric(String(stdout));
|
|
964
965
|
} catch {
|
|
@@ -2197,7 +2198,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2197
2198
|
// /glla thinking=high write to GLOBAL config
|
|
2198
2199
|
// /glla notify='cmd $1' write to GLOBAL config
|
|
2199
2200
|
// /glla tokenlimit=2000000 write to GLOBAL config
|
|
2200
|
-
// /glla wedgealert=30 hung-command alert minutes (0=off, unset=
|
|
2201
|
+
// /glla wedgealert=30 hung-command alert minutes (0=off, unset=30)
|
|
2201
2202
|
// /glla project model=... write to PROJECT override (rare)
|
|
2202
2203
|
// /glla model=unset remove key (from global; project model=unset for project)
|
|
2203
2204
|
const trimmed = args.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.4",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|