pi-goal-list-loop-audit 0.21.1 → 0.22.0
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 +6 -5
- package/docs/DESIGN.md +8 -8
- package/extensions/goal-loop-auditor.ts +5 -7
- package/extensions/goal-loop-core.ts +2 -2
- package/extensions/goal-loop-display.ts +8 -2
- package/extensions/loops/goal.ts +15 -21
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +3 -3
- package/schemas/goal.schema.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ grilling turn that ends in a Confirm dialog. For `/loop` specifically, the orche
|
|
|
88
88
|
measure command once** and shows the real number in the dialog — you validate
|
|
89
89
|
the metric before a single iteration burns tokens.
|
|
90
90
|
|
|
91
|
-
With `branch=1`, all work lands on a scratch branch (`pi-
|
|
91
|
+
With `branch=1`, all work lands on a scratch branch (`pi-glla-loop/<ts>-<slug>`):
|
|
92
92
|
improvements are committed, regressions are hard-reset (scratch branch only),
|
|
93
93
|
and on stop you return to your original branch with merge instructions.
|
|
94
94
|
Requires a clean working tree.
|
|
@@ -185,9 +185,10 @@ picks a model itself. Thinking follows the session too (floor `high`).
|
|
|
185
185
|
## Token guard
|
|
186
186
|
|
|
187
187
|
Every goal tracks real token usage; crossing the budget pauses the goal.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
Off by default (opt-in) — set a budget with `/glla tokenlimit=<n>`. A high
|
|
189
|
+
value like 10000000 is a runaway threshold, not a big-goal threshold
|
|
190
|
+
(real research/feature goals legitimately burn 2-4M). Loop 3 doesn't need
|
|
191
|
+
this cap — it has its own brakes
|
|
191
192
|
(max iterations + plateau).
|
|
192
193
|
|
|
193
194
|
## Compatibility (what goes well, what conflicts)
|
|
@@ -234,7 +235,7 @@ prompts/
|
|
|
234
235
|
goal-loop-forever-draft.md # /loop drafting prompt
|
|
235
236
|
scripts/
|
|
236
237
|
smoke.sh # live integration harness (tmux + real models)
|
|
237
|
-
tests/ #
|
|
238
|
+
tests/ # 168 unit tests, no live pi required
|
|
238
239
|
docs/DESIGN.md # architectural decisions
|
|
239
240
|
PLAN.md # milestones, decisions, gates
|
|
240
241
|
```
|
package/docs/DESIGN.md
CHANGED
|
@@ -51,8 +51,8 @@ below; later releases append addenda rather than rewrite history.
|
|
|
51
51
|
auditor's evidence → disapproval, never a false approval.
|
|
52
52
|
- **Token guard**: real accumulation from assistant-message `usage.totalTokens`
|
|
53
53
|
(deduped across replayed `agent_end` history). Crossing `tokenlimit`
|
|
54
|
-
(default
|
|
55
|
-
- **Loop 3 `branch=1`**: scratch branch `pi-
|
|
54
|
+
(opt-in; off by default since v0.12.0) pauses the goal with a clear reason.
|
|
55
|
+
- **Loop 3 `branch=1`**: scratch branch `pi-glla-loop/<ts>-<slug>`; commit per
|
|
56
56
|
improvement, `git reset --hard` per regression — scratch branch only.
|
|
57
57
|
Refuses non-git dirs and dirty trees; returns to the original branch on
|
|
58
58
|
stop with merge instructions.
|
|
@@ -107,7 +107,7 @@ This is a deliberate trade-off. If the user wants drafting in v0.1.0, say so and
|
|
|
107
107
|
|
|
108
108
|
Some alternatives considered:
|
|
109
109
|
- Three packages: `pi-goal-list-loop-audit`, `pi-goal-list-loop-audit-list`, `pi-goal-list-loop-audit-loop`.
|
|
110
|
-
- One package with three subcommands: `/goal`, `/
|
|
110
|
+
- One package with three subcommands: `/goal`, `/list`, `/loop`.
|
|
111
111
|
|
|
112
112
|
We choose **one package with subcommands**. Reasoning:
|
|
113
113
|
- Single install (`pi install npm:pi-goal-list-loop-audit`).
|
|
@@ -173,8 +173,8 @@ active → auditing (complete_goal called)
|
|
|
173
173
|
auditing → complete (auditor <approved/>)
|
|
174
174
|
auditing → active (auditor <disapproved/>; reset iteration counter)
|
|
175
175
|
active → paused (pause_goal called, or stuck > 5 min, or empty turn)
|
|
176
|
-
paused → active (user /
|
|
177
|
-
active → aborted (user /
|
|
176
|
+
paused → active (user /goal resume)
|
|
177
|
+
active → aborted (user /goal cancel)
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
### Decision 9: JSONL state (deterministic compaction)
|
|
@@ -189,9 +189,9 @@ This protects against model-generated summaries losing fidelity.
|
|
|
189
189
|
|---|---|
|
|
190
190
|
| `Esc` during auditor | Pause; user picks "complete without audit" or "continue" |
|
|
191
191
|
| `Esc` during agent turn | Pause |
|
|
192
|
-
| User `/
|
|
193
|
-
| User `/
|
|
194
|
-
|
|
|
192
|
+
| User `/goal pause` | Pause |
|
|
193
|
+
| User `/goal cancel` | Abort (wipes active goal) |
|
|
194
|
+
| Stall watchdog (3 consecutive no-tool turns) | Pause + notify |
|
|
195
195
|
| Empty turn (no tool calls) | Pause (no momentum) |
|
|
196
196
|
|
|
197
197
|
## Open follow-ups (post-v0.1.0)
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
* Isolated completion auditor. Runs in a fresh pi agent session with no
|
|
6
6
|
* extensions, no skills, no prompts, no themes, no editor. Only read tools.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* must-verify item in the verification contract. The orchestrator will
|
|
14
|
-
* reject <approved/> without that evidence.
|
|
8
|
+
* Two enforced floors: the auditor must call at least one read tool before
|
|
9
|
+
* <approved/>, and regression_shield (goal-loop-shield.ts) requires the
|
|
10
|
+
* report to include raw output (cat / grep / bash) for every must-verify
|
|
11
|
+
* item in the verification contract — the orchestrator rejects
|
|
12
|
+
* evidence-free approvals.
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
15
|
import type { Model } from "@earendil-works/pi-ai";
|
|
@@ -181,7 +181,7 @@ export function routeGoalArgs(raw: string): GoalRoute {
|
|
|
181
181
|
/**
|
|
182
182
|
* Parse a bulk list-import file (v0.8.1): markdown checklists (`- [ ]`,
|
|
183
183
|
* `- [x]`), bullets (`-`, `*`, `•`), numbered items (`1.`, `2)`), and plain
|
|
184
|
-
* lines all become
|
|
184
|
+
* lines all become list items. Headings (`# …`), blank lines, and HTML
|
|
185
185
|
* comments are skipped. A sisyphus-style plan file should import clean.
|
|
186
186
|
*/
|
|
187
187
|
export function parseListImport(content: string): string[] {
|
|
@@ -357,7 +357,7 @@ export function takeAt<T>(items: T[], n: number): [T, T[]] | null {
|
|
|
357
357
|
|
|
358
358
|
export interface State {
|
|
359
359
|
goal: Goal | null;
|
|
360
|
-
/** Loop 2:
|
|
360
|
+
/** Loop 2: list of pending goal items. Activated one at a time. */
|
|
361
361
|
list?: ListItem[];
|
|
362
362
|
/** Loop 3: metric-driven forever loop. */
|
|
363
363
|
loop?: import("./goal-loop-forever.js").LoopState;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Pure display builders for the live TUI (status line + above-editor widget).
|
|
6
6
|
* No pi imports — unit tests exercise these directly. The orchestrator calls
|
|
7
|
+
* No RUNTIME imports at all: tests run under `node --experimental-strip-types`,
|
|
8
|
+
* which does not rewrite `.js` → `.ts` specifiers — a value import from
|
|
9
|
+
* ./goal-loop-core.js breaks the suite (type-only imports are erased, safe).
|
|
7
10
|
* ctx.ui.setStatus/setWidget with whatever these return.
|
|
8
11
|
*/
|
|
9
12
|
|
|
@@ -140,8 +143,11 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
140
143
|
: paint(theme, "success", "●");
|
|
141
144
|
const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), 64)}`;
|
|
142
145
|
const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
// Token segment only when a budget is set (v0.22.0): the guard is opt-in,
|
|
147
|
+
// and "0/0 tok" carried no information when off.
|
|
148
|
+
const tokenLimit = g.usage?.tokensLimit ?? 0;
|
|
149
|
+
const tokens = tokenLimit > 0 ? ` · ${paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(tokenLimit)} tok`)}` : "";
|
|
150
|
+
const lines = [head, `├─ ${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
|
|
145
151
|
if (g.status === "auditing") {
|
|
146
152
|
lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
|
|
147
153
|
if (audit?.elapsedMs) lines.push(`└─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -71,15 +71,11 @@ import {
|
|
|
71
71
|
} from "../goal-loop-forever.js";
|
|
72
72
|
import {
|
|
73
73
|
accountTurnForNudges,
|
|
74
|
-
BACKOFF_HARD_CAP_MS,
|
|
75
74
|
BACKOFF_IDLE_RETRY_MS,
|
|
76
|
-
backoffMs,
|
|
77
75
|
HEARTBEAT_INTERVAL_MS,
|
|
78
76
|
HEARTBEAT_MAX_NUDGES,
|
|
79
77
|
HEARTBEAT_STALL_MS,
|
|
80
|
-
humanMs,
|
|
81
78
|
shouldHeartbeatRefire,
|
|
82
|
-
shouldPauseAfterBackoff,
|
|
83
79
|
} from "../goal-loop-backoff.js";
|
|
84
80
|
|
|
85
81
|
// =================================================================
|
|
@@ -87,7 +83,6 @@ import {
|
|
|
87
83
|
// =================================================================
|
|
88
84
|
|
|
89
85
|
const GOAL_EVENT_ENTRY = "goal-event";
|
|
90
|
-
const STATE_ENTRY = "goal-state";
|
|
91
86
|
/** stopReason marker for a loop held (not stopped) by the fresh-session restore gate. */
|
|
92
87
|
const HELD_ON_RESTORE = "held: restored in a fresh session";
|
|
93
88
|
|
|
@@ -201,7 +196,6 @@ let continuationTimer: NodeJS.Timeout | null = null;
|
|
|
201
196
|
let continuationScheduledFor: string | null = null;
|
|
202
197
|
let iterationCounter = 0;
|
|
203
198
|
let toolCallsThisTurn = 0;
|
|
204
|
-
let consecutiveStuckIterations = 0;
|
|
205
199
|
let consecutiveErrorIterations = 0;
|
|
206
200
|
let consecutiveNoToolIterations = 0;
|
|
207
201
|
|
|
@@ -420,7 +414,6 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
|
|
|
420
414
|
if (next.verificationContract) goal.verificationContract = next.verificationContract;
|
|
421
415
|
setGoal(goal, ctx);
|
|
422
416
|
iterationCounter = 0;
|
|
423
|
-
consecutiveStuckIterations = 0;
|
|
424
417
|
consecutiveErrorIterations = 0;
|
|
425
418
|
ctx.ui.notify(`List item #${n} activated (${rest.length} remaining): ${goal.objective.slice(0, 80)}`, "info");
|
|
426
419
|
scheduleContinuation(ctx, true);
|
|
@@ -534,7 +527,6 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
534
527
|
setGoal(goal, ctx);
|
|
535
528
|
// Reset counters
|
|
536
529
|
iterationCounter = 0;
|
|
537
|
-
consecutiveStuckIterations = 0;
|
|
538
530
|
consecutiveErrorIterations = 0;
|
|
539
531
|
consecutiveNoToolIterations = 0;
|
|
540
532
|
ctx.ui.notify(`Goal ${goal.id} created — starting now. Auditor will verify on completion.`, "info");
|
|
@@ -1233,7 +1225,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1233
1225
|
return;
|
|
1234
1226
|
}
|
|
1235
1227
|
|
|
1236
|
-
ctx.ui.notify("Usage: /loop [status] | /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [
|
|
1228
|
+
ctx.ui.notify("Usage: /loop [status] | /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] | /loop stop", "info");
|
|
1237
1229
|
}
|
|
1238
1230
|
|
|
1239
1231
|
// =================================================================
|
|
@@ -1545,7 +1537,6 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1545
1537
|
const goal = createGoal(full, liveCtx);
|
|
1546
1538
|
setGoal(goal, liveCtx);
|
|
1547
1539
|
iterationCounter = 0;
|
|
1548
|
-
consecutiveStuckIterations = 0;
|
|
1549
1540
|
consecutiveErrorIterations = 0;
|
|
1550
1541
|
scheduleContinuation(liveCtx, true);
|
|
1551
1542
|
return {
|
|
@@ -1774,7 +1765,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1774
1765
|
|
|
1775
1766
|
pi.registerTool(defineTool({
|
|
1776
1767
|
name: "list_status",
|
|
1777
|
-
label: "
|
|
1768
|
+
label: "List status",
|
|
1778
1769
|
description: "Show the active goal and the /list list (loop 2) as text: what's running, what's waiting.",
|
|
1779
1770
|
parameters: Type.Object({}),
|
|
1780
1771
|
async execute() {
|
|
@@ -1856,7 +1847,8 @@ interface Settings {
|
|
|
1856
1847
|
auditorThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
1857
1848
|
/** Shell command run on goal complete / goal pause / loop stop; message passed as $1. */
|
|
1858
1849
|
notifyCmd?: string;
|
|
1859
|
-
/** Per-goal token budget; crossing it pauses the goal.
|
|
1850
|
+
/** Per-goal token budget; crossing it pauses the goal. Off by default
|
|
1851
|
+
* (opt-in guard, v0.12.0): unset/0 = no budget. */
|
|
1860
1852
|
tokenLimit?: number;
|
|
1861
1853
|
/** on → restored goals/loops/lists auto-resume even in fresh sessions
|
|
1862
1854
|
* (unattended rigs). Default off: restore holds until /goal resume. */
|
|
@@ -2147,10 +2139,13 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2147
2139
|
const OUR_COMMANDS = ["goal", "glla", "list", "loop"];
|
|
2148
2140
|
let collisionWarned = false;
|
|
2149
2141
|
|
|
2150
|
-
// Providers
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
//
|
|
2142
|
+
// Providers known to pi core. The auditor inherits the already-resolved
|
|
2143
|
+
// Model object from this session (in-process createAgentSession), so a
|
|
2144
|
+
// provider defined in ~/.pi/agent/models.json with auth.json credentials
|
|
2145
|
+
// works even though it is not "built-in". Unknown providers get a soft
|
|
2146
|
+
// one-time conditional notice: if audits error with auth failures, an
|
|
2147
|
+
// explicit /glla model= override is the fix. (v0.22.0: reworded from the
|
|
2148
|
+
// stale "extension-registered → auditor fails auth" premise.)
|
|
2154
2149
|
const KNOWN_BUILTIN_PROVIDERS = new Set([
|
|
2155
2150
|
"anthropic", "google", "google-vertex", "google-gemini-cli", "openai", "openai-codex",
|
|
2156
2151
|
"openrouter", "opencode", "azure-openai-responses", "groq", "cerebras", "xai", "zai",
|
|
@@ -2167,7 +2162,7 @@ function warnIfAuditorProviderRisky(ctx: ExtensionContext): void {
|
|
|
2167
2162
|
const provider = (ctx.model as any)?.provider as string | undefined;
|
|
2168
2163
|
if (!provider || KNOWN_BUILTIN_PROVIDERS.has(provider)) return;
|
|
2169
2164
|
ctx.ui.notify(
|
|
2170
|
-
`pi-goal-list-loop-audit: session provider "${provider}" is
|
|
2165
|
+
`pi-goal-list-loop-audit: session provider "${provider}" is not a known built-in. The auditor inherits the resolved model in-process, so this usually works — but if audits error with auth/provider failures, set an explicit override once: /glla model=provider/id`,
|
|
2171
2166
|
"info",
|
|
2172
2167
|
);
|
|
2173
2168
|
} catch {
|
|
@@ -2410,10 +2405,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2410
2405
|
consecutiveErrorIterations = 0;
|
|
2411
2406
|
}
|
|
2412
2407
|
|
|
2413
|
-
//
|
|
2414
|
-
//
|
|
2415
|
-
//
|
|
2416
|
-
// the next agent_end iteration of THIS branch will trigger the cap.
|
|
2408
|
+
// No wall-clock cap by design: a goal ends via completion, explicit
|
|
2409
|
+
// pause/cancel, the stall watchdog, the 5-consecutive-errors pause, or
|
|
2410
|
+
// the token guard — never via an elapsed-time cutoff.
|
|
2417
2411
|
|
|
2418
2412
|
scheduleContinuation(ctx, false);
|
|
2419
2413
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
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",
|
|
@@ -73,11 +73,11 @@ pause_goal({reason: "...", suggestedAction: "..."})
|
|
|
73
73
|
|
|
74
74
|
## HARD RULES
|
|
75
75
|
|
|
76
|
-
- **Do not modify the objective autonomously.** The objective is the user's; if it has drifted from what makes sense, call `pause_goal` and propose a `/
|
|
76
|
+
- **Do not modify the objective autonomously.** The objective is the user's; if it has drifted from what makes sense, call `pause_goal` and propose a `/goal tweak` instead.
|
|
77
77
|
- **Do not pretend completion.** If verification evidence is missing, call `pause_goal` instead of `complete_goal`.
|
|
78
78
|
- **Do not polish doorknobs.** If you are out of work and the goal is satisfied, call `complete_goal` instead of inventing a side-improvement.
|
|
79
79
|
- **Do not give up early.** If a task is hard, run it down properly. The auditor will catch doorknobs; the agent's job is to do the real work.
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## STALLS
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
The orchestrator's backstop is the stall watchdog: three consecutive turns with no tool calls pause the goal. If you feel yourself spinning — repeating the same approach, no new evidence — stop early instead: call `pause_goal` with what is blocking and a concrete suggested action, rather than burning the remaining watchdog turns.
|
package/schemas/goal.schema.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/DraconDev/pi-goal-list-loop-audit/main/schemas/goal.schema.json",
|
|
4
4
|
"title": "pi-goal-list-loop-audit goal state",
|
|
5
|
-
"description": "Active goal state for pi-goal-list-loop-audit
|
|
5
|
+
"description": "Active goal state for pi-goal-list-loop-audit (loops 1-3). Persisted as markdown at .pi-glla/goals/<id>.md and mirrored into .pi-glla/active.jsonl state events.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["id", "objective", "status", "policy", "autoContinue", "usage", "createdAt", "updatedAt"],
|
|
8
8
|
"properties": {
|