pi-goal-list-loop-audit 0.20.1 → 0.21.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.
- package/extensions/goal-loop-core.ts +13 -0
- package/extensions/goal-loop-display.ts +18 -16
- package/extensions/loops/goal.ts +82 -20
- package/package.json +1 -1
|
@@ -578,3 +578,16 @@ export function buildTaskSummary(tasks: Task[]): string {
|
|
|
578
578
|
export function cloneGoal(goal: Goal): Goal {
|
|
579
579
|
return JSON.parse(JSON.stringify(goal));
|
|
580
580
|
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Session-restore gate (v0.21.0): a session that carries conversation
|
|
584
|
+
* history ("resume" | "reload" | "fork") IS the goal's own context —
|
|
585
|
+
* auto-resuming work there is natural. A fresh session ("startup" | "new",
|
|
586
|
+
* or an older pi that reports no reason) has no context — restored state
|
|
587
|
+
* HOLDS until an explicit /goal resume (or /glla autoresume=on, the rig
|
|
588
|
+
* setting for unattended restarts). One mechanical predicate; no heuristics.
|
|
589
|
+
*/
|
|
590
|
+
export function shouldAutoResumeOnSessionStart(reason: string | undefined, autoResume: boolean | undefined): boolean {
|
|
591
|
+
if (autoResume === true) return true;
|
|
592
|
+
return reason === "resume" || reason === "reload" || reason === "fork";
|
|
593
|
+
}
|
|
@@ -127,34 +127,36 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
|
|
|
127
127
|
return goalLines(g, state, audit, now, theme);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Branch lines
|
|
131
|
-
//
|
|
130
|
+
// Branch lines sit flush-left (pi-tasks convention): pi's widget renderer
|
|
131
|
+
// adds its own one-space gutter, so any indent here doubles up.
|
|
132
132
|
function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme): string[] {
|
|
133
|
+
// Head glyph is ● (not ◆): U+25C6 renders as a color-emoji diamond in some
|
|
134
|
+
// terminal fonts and ignores ANSI color; ● takes the paint everywhere.
|
|
133
135
|
const icon =
|
|
134
136
|
g.status === "paused"
|
|
135
137
|
? paint(theme, pauseIsError(g) ? "error" : "warning", "⏸")
|
|
136
138
|
: g.status === "auditing"
|
|
137
139
|
? paint(theme, "accent", "⟡")
|
|
138
|
-
: paint(theme, "success", "
|
|
140
|
+
: paint(theme, "success", "●");
|
|
139
141
|
const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), 64)}`;
|
|
140
142
|
const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
|
|
141
143
|
const tokens = paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(g.usage?.tokensLimit ?? 10_000_000)} tok`);
|
|
142
|
-
const lines = [head,
|
|
144
|
+
const lines = [head, `├─ ${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))} · ${tokens}`];
|
|
143
145
|
if (g.status === "auditing") {
|
|
144
|
-
lines.push(
|
|
145
|
-
if (audit?.elapsedMs) lines.push(
|
|
146
|
-
else lines.push(
|
|
146
|
+
lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
|
|
147
|
+
if (audit?.elapsedMs) lines.push(`└─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
|
|
148
|
+
else lines.push(`└─ ${paint(theme, "dim", "isolated session, read-only tools")}`);
|
|
147
149
|
return lines;
|
|
148
150
|
}
|
|
149
151
|
if (g.status === "paused" && g.pauseReason) {
|
|
150
|
-
lines.push(
|
|
151
|
-
if (g.pauseSuggestedAction) lines.push(
|
|
152
|
+
lines.push(`├─ ${paint(theme, pauseIsError(g) ? "error" : "warning", truncate(g.pauseReason, 60))}`);
|
|
153
|
+
if (g.pauseSuggestedAction) lines.push(`└─ ${paint(theme, "dim", truncate(g.pauseSuggestedAction, 60))}`);
|
|
152
154
|
return lines;
|
|
153
155
|
}
|
|
154
156
|
const next = nextPending(g);
|
|
155
|
-
if (next) lines.push(
|
|
157
|
+
if (next) lines.push(`├─ next: ${truncate(next, 56)}`);
|
|
156
158
|
const queue = state.list?.length ?? 0;
|
|
157
|
-
lines.push(
|
|
159
|
+
lines.push(`└─ ${paint(theme, "dim", `${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`)}`);
|
|
158
160
|
return lines;
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -164,12 +166,12 @@ function loopLines(l: LoopState, now: number, theme?: DisplayTheme): string[] {
|
|
|
164
166
|
const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
|
|
165
167
|
const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
|
|
166
168
|
const lines = [
|
|
167
|
-
`${paint(theme, "accent", "
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
`${paint(theme, "accent", "●")} ${truncate(l.target, 64)}`,
|
|
170
|
+
`├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
|
|
171
|
+
`├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
|
|
172
|
+
`└─ ${paint(theme, "dim", truncate(l.measureCmd, 56))}`,
|
|
171
173
|
];
|
|
172
|
-
if (l.branchName) lines.push(
|
|
174
|
+
if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, 50))}`);
|
|
173
175
|
return lines;
|
|
174
176
|
}
|
|
175
177
|
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
piGlaDir,
|
|
56
56
|
readState,
|
|
57
57
|
renderGoalMarkdown,
|
|
58
|
+
shouldAutoResumeOnSessionStart,
|
|
58
59
|
statusLabel,
|
|
59
60
|
writeGoalMd,
|
|
60
61
|
} from "../goal-loop-core.js";
|
|
@@ -87,6 +88,8 @@ import {
|
|
|
87
88
|
|
|
88
89
|
const GOAL_EVENT_ENTRY = "goal-event";
|
|
89
90
|
const STATE_ENTRY = "goal-state";
|
|
91
|
+
/** stopReason marker for a loop held (not stopped) by the fresh-session restore gate. */
|
|
92
|
+
const HELD_ON_RESTORE = "held: restored in a fresh session";
|
|
90
93
|
|
|
91
94
|
// =================================================================
|
|
92
95
|
// Module-level state (one per session)
|
|
@@ -1142,12 +1145,24 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1142
1145
|
const rest = args.trim().slice(sub.length).trim();
|
|
1143
1146
|
|
|
1144
1147
|
if (!sub) {
|
|
1145
|
-
// /loop with no args →
|
|
1146
|
-
//
|
|
1148
|
+
// /loop with no args → resume a held loop if one is waiting; otherwise
|
|
1149
|
+
// draft the loop config (metric design is the whole game for a
|
|
1150
|
+
// long-running loop; never start one blind).
|
|
1147
1151
|
if (isLoopActive()) {
|
|
1148
1152
|
ctx.ui.notify("A loop is already active — /loop status to inspect, /loop stop to end it.", "info");
|
|
1149
1153
|
return;
|
|
1150
1154
|
}
|
|
1155
|
+
const stored = state.loop;
|
|
1156
|
+
if (stored && !stored.active && stored.stopReason === HELD_ON_RESTORE) {
|
|
1157
|
+
state.loop = { ...stored, active: true, stopReason: undefined };
|
|
1158
|
+
persistState(ctx);
|
|
1159
|
+
scheduleLoopTick(ctx);
|
|
1160
|
+
ctx.ui.notify(
|
|
1161
|
+
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations} · best ${stored.bestValue ?? "n/a"} — ${stored.target.slice(0, 60)}`,
|
|
1162
|
+
"info",
|
|
1163
|
+
);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1151
1166
|
await startDrafting(ctx, "loop");
|
|
1152
1167
|
return;
|
|
1153
1168
|
}
|
|
@@ -1843,6 +1858,9 @@ interface Settings {
|
|
|
1843
1858
|
notifyCmd?: string;
|
|
1844
1859
|
/** Per-goal token budget; crossing it pauses the goal. Default 1,000,000. */
|
|
1845
1860
|
tokenLimit?: number;
|
|
1861
|
+
/** on → restored goals/loops/lists auto-resume even in fresh sessions
|
|
1862
|
+
* (unattended rigs). Default off: restore holds until /goal resume. */
|
|
1863
|
+
autoResume?: boolean;
|
|
1846
1864
|
}
|
|
1847
1865
|
|
|
1848
1866
|
const DEFAULT_SETTINGS: Settings = {
|
|
@@ -1887,7 +1905,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
|
|
|
1887
1905
|
const glob = readSettingsFile(globalSettingsPath());
|
|
1888
1906
|
const effective = loadSettings(cwd);
|
|
1889
1907
|
const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
|
|
1890
|
-
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit"];
|
|
1908
|
+
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "autoResume"];
|
|
1891
1909
|
for (const k of keys) {
|
|
1892
1910
|
if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
|
|
1893
1911
|
else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
|
|
@@ -2046,6 +2064,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2046
2064
|
fmt("auditorThinkingLevel", "thinking"),
|
|
2047
2065
|
fmt("notifyCmd", "notify"),
|
|
2048
2066
|
fmt("tokenLimit", "tokenLimit"),
|
|
2067
|
+
fmt("autoResume", "autoResume"),
|
|
2049
2068
|
`\nglobal: ${globalSettingsPath()}`,
|
|
2050
2069
|
`project: ${projectSettingsPath(ctx.cwd)}`,
|
|
2051
2070
|
`Set with: /glla key=value (global) · /glla project key=value (project override)`,
|
|
@@ -2084,6 +2103,16 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2084
2103
|
} else {
|
|
2085
2104
|
ctx.ui.notify(`tokenlimit must be a positive integer, got: ${value}`, "warning");
|
|
2086
2105
|
}
|
|
2106
|
+
} else if (key === "autoresume") {
|
|
2107
|
+
if (["on", "true", "1", "yes"].includes(value)) {
|
|
2108
|
+
patch.autoResume = true;
|
|
2109
|
+
changed = true;
|
|
2110
|
+
} else if (["off", "false", "0", "no", "unset"].includes(value)) {
|
|
2111
|
+
patch.autoResume = undefined;
|
|
2112
|
+
changed = true;
|
|
2113
|
+
} else {
|
|
2114
|
+
ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
|
|
2115
|
+
}
|
|
2087
2116
|
} else if (key === "thinking" || key === "auditorthinkinglevel") {
|
|
2088
2117
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
2089
2118
|
patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
|
|
@@ -2094,13 +2123,13 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2094
2123
|
}
|
|
2095
2124
|
}
|
|
2096
2125
|
if (!changed) {
|
|
2097
|
-
ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit), optionally prefixed with 'project'.", "info");
|
|
2126
|
+
ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume), optionally prefixed with 'project'.", "info");
|
|
2098
2127
|
return;
|
|
2099
2128
|
}
|
|
2100
2129
|
saveSettings(scope, ctx.cwd, patch);
|
|
2101
2130
|
const effective = loadSettings(ctx.cwd);
|
|
2102
2131
|
ctx.ui.notify(
|
|
2103
|
-
`Saved to ${scope} config. Effective now: model=${effective.auditorModel ?? "(session model)"} thinking=${effective.auditorThinkingLevel ?? "(session)"} notify=${effective.notifyCmd ?? "(off)"} tokenLimit=${effective.tokenLimit ?? 0}${(effective.tokenLimit ?? 0) > 0 ? "" : " (off)"}\n` +
|
|
2132
|
+
`Saved to ${scope} config. Effective now: model=${effective.auditorModel ?? "(session model)"} thinking=${effective.auditorThinkingLevel ?? "(session)"} notify=${effective.notifyCmd ?? "(off)"} tokenLimit=${effective.tokenLimit ?? 0}${(effective.tokenLimit ?? 0) > 0 ? "" : " (off)"} autoResume=${effective.autoResume === true ? "on" : "off"}\n` +
|
|
2104
2133
|
`Note: the auditor runs without extensions — it must be a built-in provider, not an extension-registered one.`,
|
|
2105
2134
|
"info",
|
|
2106
2135
|
);
|
|
@@ -2228,7 +2257,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2228
2257
|
}
|
|
2229
2258
|
});
|
|
2230
2259
|
|
|
2231
|
-
pi.on("session_start", async (
|
|
2260
|
+
pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
|
|
2232
2261
|
rememberCtx(ctx);
|
|
2233
2262
|
state = readState(ctx.cwd);
|
|
2234
2263
|
if (!registeredCtx) {
|
|
@@ -2237,27 +2266,60 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2237
2266
|
}
|
|
2238
2267
|
warnOnCommandCollision(ctx);
|
|
2239
2268
|
warnIfAuditorProviderRisky(ctx);
|
|
2240
|
-
//
|
|
2269
|
+
// Restore gate (v0.21.0): a fresh session ("startup"/"new", or a pi too
|
|
2270
|
+
// old to report a reason) has no conversation context for the restored
|
|
2271
|
+
// work — HOLD instead of auto-firing, so opening pi in a folder never
|
|
2272
|
+
// starts working before you can even load your session. Sessions with
|
|
2273
|
+
// history ("resume"/"reload"/"fork") auto-resume; /glla autoresume=on
|
|
2274
|
+
// opts a project into auto-resume everywhere (unattended rigs).
|
|
2275
|
+
const autoResume = shouldAutoResumeOnSessionStart(event?.reason, loadSettings(ctx.cwd).autoResume);
|
|
2241
2276
|
if (isLoopActive()) {
|
|
2242
2277
|
const l = state.loop!;
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2278
|
+
if (autoResume) {
|
|
2279
|
+
ctx.ui.notify(
|
|
2280
|
+
`Resuming loop (iteration ${l.iteration}/${l.maxIterations}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
|
|
2281
|
+
"info",
|
|
2282
|
+
);
|
|
2283
|
+
scheduleLoopTick(ctx);
|
|
2284
|
+
} else {
|
|
2285
|
+
state.loop = { ...l, active: false, stopReason: HELD_ON_RESTORE };
|
|
2286
|
+
persistState(ctx);
|
|
2287
|
+
ctx.ui.notify(
|
|
2288
|
+
`Loop held on restore (fresh session, no work started): ${l.target.slice(0, 60)} — /loop to resume, /glla autoresume=on to auto-resume in this project.`,
|
|
2289
|
+
"info",
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2293
|
+
if (autoResume) {
|
|
2294
|
+
ctx.ui.notify(
|
|
2295
|
+
`Resuming goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2296
|
+
"info",
|
|
2297
|
+
);
|
|
2298
|
+
scheduleContinuation(ctx, true);
|
|
2299
|
+
} else {
|
|
2300
|
+
updateGoal({
|
|
2301
|
+
status: "paused",
|
|
2302
|
+
pauseReason: "restored in a fresh session — no work started",
|
|
2303
|
+
pauseSuggestedAction: "/goal resume to continue · /glla autoresume=on to auto-resume in this project",
|
|
2304
|
+
}, ctx);
|
|
2305
|
+
ctx.ui.notify(
|
|
2306
|
+
`Goal held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)} — /goal resume to continue.`,
|
|
2307
|
+
"info",
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2247
2310
|
} else if (state.goal && state.goal.status === "active") {
|
|
2311
|
+
// Active but autoContinue off: nothing auto-fires — just surface it.
|
|
2248
2312
|
ctx.ui.notify(
|
|
2249
|
-
`
|
|
2313
|
+
`Restored goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2250
2314
|
"info",
|
|
2251
2315
|
);
|
|
2252
|
-
}
|
|
2253
|
-
if (isLoopActive()) {
|
|
2254
|
-
// Session restarted mid-loop: resume measuring from persisted state.
|
|
2255
|
-
scheduleLoopTick(ctx);
|
|
2256
|
-
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2257
|
-
scheduleContinuation(ctx, true);
|
|
2258
2316
|
} else if ((!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") && listQueue().length > 0) {
|
|
2259
|
-
|
|
2260
|
-
|
|
2317
|
+
if (autoResume) {
|
|
2318
|
+
// Session restarted with a non-empty queue but no active goal.
|
|
2319
|
+
activateNextListItem(ctx);
|
|
2320
|
+
} else {
|
|
2321
|
+
ctx.ui.notify(`List has ${listQueue().length} item(s) waiting — /list next to activate the head.`, "info");
|
|
2322
|
+
}
|
|
2261
2323
|
}
|
|
2262
2324
|
});
|
|
2263
2325
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
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",
|