pi-goal-list-loop-audit 0.29.4 → 0.29.6
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-settings.ts +15 -0
- package/extensions/loops/goal.ts +66 -35
- package/package.json +1 -1
|
@@ -152,6 +152,21 @@ export function loadSettings(cwd: string): Settings {
|
|
|
152
152
|
) as unknown as Settings;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* v0.29.5: autoResume is GLOBAL-only (user directive 2026-07-30: "we are
|
|
157
|
+
* not supporting project level setting for it now, just global"). Launch-
|
|
158
|
+
* time restore reads this, never the project file — a stale autoResume
|
|
159
|
+
* key in a project's settings.json is ignored (junk-runner field case: a
|
|
160
|
+
* project-local opt-in from the unattended-audit era kept auto-firing the
|
|
161
|
+
* list at every bare `pi` launch after the global default flipped off).
|
|
162
|
+
*/
|
|
163
|
+
export function loadGlobalSettings(): Settings {
|
|
164
|
+
return mergeSettings(
|
|
165
|
+
DEFAULT_SETTINGS as unknown as Record<string, unknown>,
|
|
166
|
+
readSettingsFile(globalSettingsPath()) as Record<string, unknown>,
|
|
167
|
+
) as unknown as Settings;
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
/** Every provenance-tracked key (the /glla headless display + UI). */
|
|
156
171
|
export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
157
172
|
"auditorModel",
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
DEFAULT_SETTINGS,
|
|
107
107
|
SETTINGS_KEYS,
|
|
108
108
|
globalSettingsPath,
|
|
109
|
+
loadGlobalSettings,
|
|
109
110
|
loadSettings,
|
|
110
111
|
projectSettingsPath,
|
|
111
112
|
saveSettings,
|
|
@@ -706,6 +707,10 @@ function heartbeatTick(): void {
|
|
|
706
707
|
ctx.ui.notify(msg, "warning");
|
|
707
708
|
notifyExternal(ctx, msg);
|
|
708
709
|
}
|
|
710
|
+
// v0.29.5: user-abort stand-down — the chain stays DOWN until the
|
|
711
|
+
// user resumes. Without this guard the 60s heartbeat re-fired the
|
|
712
|
+
// continuation and defeated the 0.29.4 stand-down within a minute.
|
|
713
|
+
if (abortedStandDown) return;
|
|
709
714
|
if (!fire) return;
|
|
710
715
|
// v0.26.6: the 0.25.0 "recent ship (<5m)" suppression was REMOVED. It fed
|
|
711
716
|
// lastShippedAtMs, which read the state-file MTIME — and the heartbeat's
|
|
@@ -747,6 +752,10 @@ let consecutiveErrorIterations = 0;
|
|
|
747
752
|
// v0.28.5 (E8): user aborts are NOT provider errors — separate counter,
|
|
748
753
|
// separate brake message, and no auto-resume (aborting is user intent).
|
|
749
754
|
let consecutiveAbortIterations = 0;
|
|
755
|
+
// v0.29.5: set when a user abort stands the chain down (0.29.4) — the
|
|
756
|
+
// heartbeat refire + post-compaction refire must NOT resurrect it; only
|
|
757
|
+
// an explicit schedule (resume/activate/next turn) clears it.
|
|
758
|
+
let abortedStandDown = false;
|
|
750
759
|
let consecutiveNoToolIterations = 0;
|
|
751
760
|
|
|
752
761
|
// =================================================================
|
|
@@ -779,6 +788,7 @@ function freshCtx(): ExtensionContext | null {
|
|
|
779
788
|
}
|
|
780
789
|
|
|
781
790
|
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
791
|
+
abortedStandDown = false; // v0.29.5: any explicit schedule ends the stand-down
|
|
782
792
|
if (!isActionableGoal()) return;
|
|
783
793
|
rememberCtx(ctx);
|
|
784
794
|
const goalId = state.goal!.id;
|
|
@@ -993,6 +1003,43 @@ function updateGoal(patch: Partial<Goal>, ctx: ExtensionContext): void {
|
|
|
993
1003
|
persistState(ctx);
|
|
994
1004
|
}
|
|
995
1005
|
|
|
1006
|
+
// v0.29.6: stacked-state auto-arbitration (user directive: "auto archive /
|
|
1007
|
+
// wipe extra goals/loops/lists … make sure that we only have one"). Dirty
|
|
1008
|
+
// pre-guard states can persist a live loop AND a live goal; the 0.28.21
|
|
1009
|
+
// decision picker asked the user to arbitrate artifacts they didn't
|
|
1010
|
+
// remember at every pi start. Now deterministic: MOST RECENT ACTIVITY
|
|
1011
|
+
// keeps the slot; the loser is ARCHIVED (recoverable), never wiped. The
|
|
1012
|
+
// queued list is a backlog, not a live artifact — untouched.
|
|
1013
|
+
function autoArbitrateStackedState(ctx: ExtensionContext): void {
|
|
1014
|
+
const loop = state.loop?.active ? state.loop : undefined;
|
|
1015
|
+
const goal = state.goal && state.goal.status !== "complete" && state.goal.status !== "aborted" ? state.goal : undefined;
|
|
1016
|
+
if (!loop || !goal) return; // at most one live artifact — the invariant holds
|
|
1017
|
+
const lastMeasure = loop.history.length > 0 ? loop.history[loop.history.length - 1] : undefined;
|
|
1018
|
+
const loopMs = Date.parse(lastMeasure?.at ?? loop.startedAt ?? "") || 0;
|
|
1019
|
+
const goalMs = Date.parse(goal.updatedAt ?? goal.createdAt ?? "") || 0;
|
|
1020
|
+
const keepGoal = goalMs > loopMs; // tie → the loop keeps the slot (0.28.21 default)
|
|
1021
|
+
appendLedger(ctx.cwd, "stacked_state_auto_arbitrated", {
|
|
1022
|
+
kept: keepGoal ? "goal" : "loop",
|
|
1023
|
+
goalId: goal.id,
|
|
1024
|
+
goalMs,
|
|
1025
|
+
loopMs,
|
|
1026
|
+
loopIteration: loop.iteration,
|
|
1027
|
+
loopTarget: loop.target.slice(0, 120),
|
|
1028
|
+
});
|
|
1029
|
+
if (keepGoal) {
|
|
1030
|
+
// Same shape as /loop stop: the loop record stays in state (inactive)
|
|
1031
|
+
// with an honest reason — /loop status still shows it.
|
|
1032
|
+
state = { ...state, loop: { ...loop, active: false, stopReason: "auto-arbitrated on session load: the goal was more recent (one active thing)" } };
|
|
1033
|
+
persistState(ctx);
|
|
1034
|
+
} else {
|
|
1035
|
+
archiveCurrentGoal(ctx, "aborted", "auto-arbitrated on session load: the loop was more recent (one active thing)");
|
|
1036
|
+
}
|
|
1037
|
+
ctx.ui.notify(
|
|
1038
|
+
`Stacked state auto-arbitrated (one active thing): kept the ${keepGoal ? "goal" : "loop"} — more recent activity — and archived the ${keepGoal ? `loop (iter ${loop.iteration}, best ${loop.bestValue ?? "n/a"})` : `goal (${goal.id})`}. Recoverable: /loop status · .pi-glla/archive/ · /glla wipe for a clean slate.`,
|
|
1039
|
+
"info",
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
996
1043
|
function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?: string): void {
|
|
997
1044
|
if (!state.goal) return;
|
|
998
1045
|
const goal = state.goal;
|
|
@@ -1223,7 +1270,7 @@ function fireReviewer(
|
|
|
1223
1270
|
manual: opts.manual,
|
|
1224
1271
|
ledgerEntries,
|
|
1225
1272
|
sources,
|
|
1226
|
-
enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer", { autoActivate:
|
|
1273
|
+
enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer", { autoActivate: loadGlobalSettings().autoResume === true }),
|
|
1227
1274
|
proposeGoal: (objective, reason) => {
|
|
1228
1275
|
try {
|
|
1229
1276
|
extensionApi?.sendUserMessage(
|
|
@@ -5060,7 +5107,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5060
5107
|
const c = freshCtx();
|
|
5061
5108
|
if (!c) return;
|
|
5062
5109
|
try {
|
|
5063
|
-
if (c.isIdle() && !c.hasPendingMessages() && continuationTimer === null && loopTimer === null && isSupervising()) {
|
|
5110
|
+
if (c.isIdle() && !c.hasPendingMessages() && continuationTimer === null && loopTimer === null && isSupervising() && !abortedStandDown) {
|
|
5064
5111
|
appendLedger(c.cwd, "compaction_refire", {});
|
|
5065
5112
|
if (isLoopActive()) scheduleLoopTick(c);
|
|
5066
5113
|
else scheduleContinuation(c, true);
|
|
@@ -5182,15 +5229,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5182
5229
|
} catch (err) {
|
|
5183
5230
|
ctx.ui.notify(`glla subagent override sync failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
5184
5231
|
}
|
|
5232
|
+
// v0.29.6: stacked-state auto-arbitration FIRST — one live artifact
|
|
5233
|
+
// survives before the restore gate decides hold-vs-resume for it.
|
|
5234
|
+
autoArbitrateStackedState(ctx);
|
|
5185
5235
|
// Restore gate (v0.26.9 tri-state): a human LOADING a session
|
|
5186
5236
|
// ("startup"/"new"/"resume", or no reason) HOLDS — the popup shows what
|
|
5187
5237
|
// is waiting and nothing starts until they resume explicitly. In-session
|
|
5188
|
-
// machinery ("reload"/"fork") auto-resumes. /glla autoresume=on opts
|
|
5189
|
-
//
|
|
5190
|
-
//
|
|
5191
|
-
//
|
|
5192
|
-
//
|
|
5193
|
-
|
|
5238
|
+
// machinery ("reload"/"fork") auto-resumes. /glla autoresume=on opts
|
|
5239
|
+
// into auto-resume everywhere (unattended rigs); autoresume=off never
|
|
5240
|
+
// auto-resumes. v0.29.5: the setting is GLOBAL-only — project-level
|
|
5241
|
+
// autoResume keys are ignored. Once running, the chain auto-continues
|
|
5242
|
+
// forever unless a super-stuck brake (stall escalation / stale-api /
|
|
5243
|
+
// latch) stops it loudly.
|
|
5244
|
+
const autoResumeSetting = resolveEffectiveAggressiveSettings(loadGlobalSettings()).autoResume;
|
|
5194
5245
|
const autoResume = shouldAutoResumeOnSessionStart(event?.reason, autoResumeSetting);
|
|
5195
5246
|
// v0.25.0 (contract item 6): aggressiveMode announces every auto-event.
|
|
5196
5247
|
if (
|
|
@@ -5212,7 +5263,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5212
5263
|
state.loop = { ...l, active: false, stopReason: HELD_ON_RESTORE };
|
|
5213
5264
|
persistState(ctx);
|
|
5214
5265
|
ctx.ui.notify(
|
|
5215
|
-
`Loop held on restore: ${l.target.slice(0, 60)} — /loop resume to continue, /glla autoresume=on to auto-resume on session load
|
|
5266
|
+
`Loop held on restore: ${l.target.slice(0, 60)} — /loop resume to continue, /glla autoresume=on to auto-resume on session load (global setting).`,
|
|
5216
5267
|
"info",
|
|
5217
5268
|
);
|
|
5218
5269
|
}
|
|
@@ -5239,7 +5290,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5239
5290
|
// v0.22.7: name WHAT is held — a list head resumes through /list.
|
|
5240
5291
|
const isListItem = state.goal.policy === "list";
|
|
5241
5292
|
const resumeCmd = isListItem ? "/list resume" : "/goal resume";
|
|
5242
|
-
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume
|
|
5293
|
+
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume on load (global setting)`;
|
|
5243
5294
|
updateGoal({
|
|
5244
5295
|
status: "paused",
|
|
5245
5296
|
pauseKind: "blocked",
|
|
@@ -5265,31 +5316,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5265
5316
|
ctx.ui.notify(`List has ${listQueue().length} item(s) waiting — /list next to activate the head.`, "info");
|
|
5266
5317
|
}
|
|
5267
5318
|
}
|
|
5268
|
-
// v0.28.21
|
|
5269
|
-
//
|
|
5270
|
-
//
|
|
5271
|
-
//
|
|
5272
|
-
// at most one thing owns the active slot, and nothing auto-starts.
|
|
5273
|
-
if (state.loop && state.goal && state.goal.status === "active") {
|
|
5274
|
-
updateGoal({
|
|
5275
|
-
status: "paused",
|
|
5276
|
-
pauseKind: "decision",
|
|
5277
|
-
// v0.29.3: third option — the wipe escape. Old projects stack a
|
|
5278
|
-
// goal AND a loop AND a list from pre-guard versions; arbitrating
|
|
5279
|
-
// between two artifacts the user doesn't even remember is the odd
|
|
5280
|
-
// part — "i feel like wipe does [make sense]". Wipe keeps its own
|
|
5281
|
-
// Confirm (destructive), so picking it is safe to offer.
|
|
5282
|
-
pauseOptions: ["Stop the loop, then resume the goal (/loop stop)", "Cancel the goal (/goal cancel) — the loop keeps running", "Wipe everything — clean slate for stale leftovers (/glla wipe)"],
|
|
5283
|
-
pauseRecommended: 1,
|
|
5284
|
-
pauseReason: "held on session load — the loop owns the active slot (one active thing at a time)",
|
|
5285
|
-
pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
|
|
5286
|
-
}, ctx);
|
|
5287
|
-
ctx.ui.notify(
|
|
5288
|
-
`Goal held — a loop also exists; one active thing at a time. /loop to resume the loop, or /loop stop then /goal resume.`,
|
|
5289
|
-
"info",
|
|
5290
|
-
);
|
|
5291
|
-
maybeDecisionPopup(ctx);
|
|
5292
|
-
}
|
|
5319
|
+
// v0.29.6: the 0.28.21 loop-vs-goal decision picker is SUPERSEDED by
|
|
5320
|
+
// auto-arbitration above — stacked states resolve deterministically
|
|
5321
|
+
// (most recent activity keeps the slot; the loser is archived) before
|
|
5322
|
+
// the restore gate, so a live loop and a live goal cannot coexist here.
|
|
5293
5323
|
// Always paint on session load (v0.22.1): the branches above only reach
|
|
5294
5324
|
// refreshUI via persistState, so a goal that was ALREADY paused (or any
|
|
5295
5325
|
// state that doesn't mutate on load) rendered nothing — "can't tell if
|
|
@@ -5529,6 +5559,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5529
5559
|
// was answered by another turn under the user's hands ("it auto
|
|
5530
5560
|
// triggered and I kept spamming esc on it" — pully, 2026-07-30).
|
|
5531
5561
|
ctx.ui.notify(`${goalNoun()} standing down — turn aborted by user (not counted toward stalls). /goal resume to continue, /goal cancel to stop.`, "info");
|
|
5562
|
+
abortedStandDown = true; // v0.29.5: heartbeat/compaction refires must not resurrect the chain
|
|
5532
5563
|
appendLedger(ctx.cwd, "abort_stand_down", { consecutiveAborts: consecutiveAbortIterations });
|
|
5533
5564
|
return;
|
|
5534
5565
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.6",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|