pi-goal-list-loop-audit 0.28.19 → 0.28.21

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
@@ -203,7 +203,7 @@ No external watchdog plugin needed.
203
203
  /glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
204
204
  /glla tokenlimit=0 # explicitly no cap (the default)
205
205
  /glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
206
- /glla autoresume=on # auto-resume goals/loops on session LOAD too (default: hold on load, auto-resume on reload/fork; off: never)
206
+ /glla autoresume=on # auto-resume goals/loops on ANY session start (default: load HELD, never auto-start — explicit /goal resume, /list resume, or /loop; off: never)
207
207
  /glla auditcap=5 # pause the goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)
208
208
  /glla aggressivemode=on # keep-going defaults: autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs
209
209
  /glla quotaretryminutes=60 # minutes before auto-retrying a quota-exhausted auditor
@@ -728,21 +728,21 @@ export function cloneGoal(goal: Goal): Goal {
728
728
  * setting for unattended restarts). One mechanical predicate; no heuristics.
729
729
  */
730
730
  export function shouldAutoResumeOnSessionStart(reason: string | undefined, autoResume: boolean | undefined): boolean {
731
- // v0.26.9 tri-state:
732
- // true → auto-resume on EVERY session start (unattended rigs).
731
+ // v0.28.21: the DEFAULT flipped to hold-everything (user directive:
732
+ // "load it on session load but not auto start it"). Tri-state:
733
+ // true → auto-resume on EVERY session start (unattended rigs;
734
+ // /glla autoresume=on — this is the ONLY auto-resume path).
733
735
  // false → never auto-resume; always hold for an explicit resume.
734
- // undefined → DEFAULT: a human LOADING a session ("startup"/"new"/
735
- // "resume", or old pi reporting no reason) must not trigger
736
- // work — show the held popup, they resume explicitly.
737
- // In-session MACHINERY ("reload"/"fork") auto-resumes so an
738
- // extension reload or session fork never strands work.
736
+ // undefined → DEFAULT: never auto-resume either — whatever the reason
737
+ // ("startup"/"new"/"resume"/"reload"/"fork"/none), the
738
+ // item is LOADED (visible, state intact) but HELD until an
739
+ // explicit /goal resume, /list resume, or /loop.
739
740
  // Mid-session continuation (agent_end chains, heartbeat refires,
740
741
  // post-compaction, list/loop transitions) is not gated here at all — it
741
742
  // auto-continues forever unless a super-stuck brake (stall escalation,
742
743
  // stale-api terminal, pending-latch watchdog) stops it loudly.
743
- if (autoResume === true) return true;
744
- if (autoResume === false) return false;
745
- return reason === "reload" || reason === "fork";
744
+ void reason; // retained for the signature; no reason auto-resumes by default anymore
745
+ return autoResume === true;
746
746
  }
747
747
 
748
748
  /**
@@ -1194,9 +1194,9 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
1194
1194
  consecutiveNoToolIterations = 0;
1195
1195
  if (staleEntry) {
1196
1196
  // v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
1197
- // fresh session auto-resumes, and tell the truth instead of "starting now".
1197
+ // fresh session LOADS it (held by default since v0.28.21), and tell the truth instead of "starting now".
1198
1198
  updateGoal({ interruptedAt: nowIso(), interruptedReason: "created in a stale session" }, ctx);
1199
- ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi and it auto-resumes. (id: ${goal.id})`, "warning");
1199
+ ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi, then /goal resume (v0.28.21: session loads no longer auto-start by default). (id: ${goal.id})`, "warning");
1200
1200
  return;
1201
1201
  }
1202
1202
  ctx.ui.notify(`Goal started: ${shortObj(goal.objective)} — the auditor will verify on completion. (id: ${goal.id})`, "info");
@@ -1239,6 +1239,13 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
1239
1239
 
1240
1240
  async function cmdResume(ctx: ExtensionContext): Promise<void> {
1241
1241
  if (!state.goal || state.goal.status !== "paused") return;
1242
+ // v0.28.21: one-active-thing — the LAST unguarded activation path. A
1243
+ // paused goal/list-item must not resume over a live loop (covers
1244
+ // /goal resume AND /list resume, which routes here).
1245
+ if (isLoopActive()) {
1246
+ ctx.ui.notify("A loop is active — one active thing at a time. /loop stop it first, then resume the goal.", "warning");
1247
+ return;
1248
+ }
1242
1249
  // v0.28.1 (S1/S3): resuming in a stale session used to flip status to
1243
1250
  // active, claim "Resumed goal", then re-pause on the stale send failure
1244
1251
  // (or zombie — S1). Now: persist the resume (the next fresh session
@@ -4460,12 +4467,13 @@ export default function (pi: ExtensionAPI): void {
4460
4467
  );
4461
4468
  }
4462
4469
  } else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
4463
- // v0.28.3 (S2 completed): an infra interrupt outranks the DEFAULT
4464
- // hold — the goal never chose to stop; pi killed its handle. With
4465
- // autoresume unset, an interrupted goal auto-resumes even on a human
4466
- // session load; explicit /glla autoresume=off still holds.
4467
4470
  const wasInterrupted = !!state.goal.interruptedAt;
4468
- if (autoResume || (wasInterrupted && autoResumeSetting !== false)) {
4471
+ // v0.28.21: the 0.28.3 interrupted-goal exemption is SUPERSEDED —
4472
+ // the default is now hold-everything on session load (user directive:
4473
+ // "load it but not auto start it"). Interrupted goals hold like
4474
+ // everything else; autoresume=on (unattended rigs) still auto-resumes
4475
+ // them, and the marker is cleared only on that promised auto-resume.
4476
+ if (autoResume) {
4469
4477
  // v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
4470
4478
  // the auto-resume the marker promised.
4471
4479
  if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
@@ -4506,6 +4514,22 @@ export default function (pi: ExtensionAPI): void {
4506
4514
  ctx.ui.notify(`List has ${listQueue().length} item(s) waiting — /list next to activate the head.`, "info");
4507
4515
  }
4508
4516
  }
4517
+ // v0.28.21: enforce one-active-thing at the restore boundary for DIRTY
4518
+ // legacy states — pre-guard versions could persist an active goal AND
4519
+ // an active/held loop; the chain above handles the loop first, and the
4520
+ // goal would otherwise stay active and fire on agent_end. Pause it:
4521
+ // at most one thing owns the active slot, and nothing auto-starts.
4522
+ if (state.loop && state.goal && state.goal.status === "active") {
4523
+ updateGoal({
4524
+ status: "paused",
4525
+ pauseReason: "held on session load — the loop owns the active slot (one active thing at a time)",
4526
+ pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
4527
+ }, ctx);
4528
+ ctx.ui.notify(
4529
+ `Goal [${state.goal.id}] held — a loop also exists; one active thing at a time. /loop to resume the loop, or /loop stop then /goal resume.`,
4530
+ "info",
4531
+ );
4532
+ }
4509
4533
  // Always paint on session load (v0.22.1): the branches above only reach
4510
4534
  // refreshUI via persistState, so a goal that was ALREADY paused (or any
4511
4535
  // state that doesn't mutate on load) rendered nothing — "can't tell if
@@ -70,7 +70,7 @@ export interface SettingsRow {
70
70
  label: string;
71
71
  /** VALUE column — current effective value, e.g. `true` / `(off)` / `60`. */
72
72
  valueText: string;
73
- /** SOURCE column — provenance tag, one of `[project]` / `[global]` / `[default]`. */
73
+ /** SOURCE column — provenance tag: `project` / `global` / `default` / `runtime` (v0.28.20: bare — brackets were chrome). */
74
74
  sourceText: string;
75
75
  /** DESCRIPTION column — one-line explanation; truncated with ellipsis when narrow. */
76
76
  description: string;
@@ -116,7 +116,7 @@ export function buildSettingsRows(
116
116
  const p = provFor(k);
117
117
  return p.value === undefined ? fallback : String(p.value);
118
118
  };
119
- const src = (k: keyof Settings): string => `[${provFor(k).source}]`;
119
+ const src = (k: keyof Settings): string => provFor(k).source;
120
120
 
121
121
  const rows: SettingsRow[] = [];
122
122
 
@@ -135,7 +135,7 @@ export function buildSettingsRows(
135
135
  id: "autoAcceptDrafts",
136
136
  section: "keep-going",
137
137
  label: "Auto-accept drafts",
138
- valueText: show("autoAcceptDrafts", "(off)"),
138
+ valueText: show("autoAcceptDrafts", "off"),
139
139
  sourceText: src("autoAcceptDrafts"),
140
140
  description: "on: goal/loop drafts activate without the Confirm dialog (unattended rigs)",
141
141
  },
@@ -143,7 +143,7 @@ export function buildSettingsRows(
143
143
  id: "aggressiveMode",
144
144
  section: "keep-going",
145
145
  label: "Aggressive mode",
146
- valueText: show("aggressiveMode", "(off)"),
146
+ valueText: show("aggressiveMode", "off"),
147
147
  sourceText: src("aggressiveMode"),
148
148
  description:
149
149
  "flips DEFAULTS toward keep-going (autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs); explicit per-key settings still win",
@@ -156,7 +156,7 @@ export function buildSettingsRows(
156
156
  id: "auditorModel",
157
157
  section: "auditor",
158
158
  label: "Auditor model",
159
- valueText: show("auditorModel", "(pi session model)"),
159
+ valueText: show("auditorModel", "pi session model"),
160
160
  sourceText: src("auditorModel"),
161
161
  description: "provider/model override for the isolated auditor",
162
162
  },
@@ -164,7 +164,7 @@ export function buildSettingsRows(
164
164
  id: "auditorThinkingLevel",
165
165
  section: "auditor",
166
166
  label: "Auditor thinking",
167
- valueText: show("auditorThinkingLevel", "(session, floor high)"),
167
+ valueText: show("auditorThinkingLevel", "session, floor high"),
168
168
  sourceText: src("auditorThinkingLevel"),
169
169
  description: "thinking level for the auditor session",
170
170
  },
@@ -172,7 +172,7 @@ export function buildSettingsRows(
172
172
  id: "auditCap",
173
173
  section: "auditor",
174
174
  label: "Audit cap",
175
- valueText: show("auditCap", `(${defaults.auditCap})`),
175
+ valueText: show("auditCap", `${defaults.auditCap}`),
176
176
  sourceText: src("auditCap"),
177
177
  description: "pause the goal after N consecutive disapprovals (0 = unlimited)",
178
178
  },
@@ -182,7 +182,7 @@ export function buildSettingsRows(
182
182
  label: "Audit feedback chars",
183
183
  valueText: show(
184
184
  "auditFeedbackChars",
185
- DEFAULT_AUDIT_FEEDBACK_CHARS === 0 ? "(full report)" : `(${DEFAULT_AUDIT_FEEDBACK_CHARS})`,
185
+ DEFAULT_AUDIT_FEEDBACK_CHARS === 0 ? "full report" : `${DEFAULT_AUDIT_FEEDBACK_CHARS}`,
186
186
  ),
187
187
  sourceText: src("auditFeedbackChars"),
188
188
  description: "cap the executor-visible disapproval report (0 = full report)",
@@ -191,7 +191,7 @@ export function buildSettingsRows(
191
191
  id: "quotaRetryMinutes",
192
192
  section: "auditor",
193
193
  label: "Quota retry minutes",
194
- valueText: show("quotaRetryMinutes", `(${DEFAULT_QUOTA_RETRY_MINUTES})`),
194
+ valueText: show("quotaRetryMinutes", `${DEFAULT_QUOTA_RETRY_MINUTES}`),
195
195
  sourceText: src("quotaRetryMinutes"),
196
196
  description: "auto-retry a quota-exhausted auditor after N minutes",
197
197
  },
@@ -203,7 +203,7 @@ export function buildSettingsRows(
203
203
  id: "wedgeAlertMinutes",
204
204
  section: "stall-brakes",
205
205
  label: "Wedge alert minutes",
206
- valueText: show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES})`),
206
+ valueText: show("wedgeAlertMinutes", `${WEDGE_ALERT_DEFAULT_MINUTES}`),
207
207
  sourceText: src("wedgeAlertMinutes"),
208
208
  description: "hung-command alert while the session is busy (0 = off)",
209
209
  },
@@ -211,7 +211,7 @@ export function buildSettingsRows(
211
211
  id: "stuckMaxInterventions",
212
212
  section: "stall-brakes",
213
213
  label: "Stuck max interventions",
214
- valueText: show("stuckMaxInterventions", `(${defaults.stuckMaxInterventions})`),
214
+ valueText: show("stuckMaxInterventions", `${defaults.stuckMaxInterventions}`),
215
215
  sourceText: src("stuckMaxInterventions"),
216
216
  description: "consecutive stuck interventions before a loop stops",
217
217
  },
@@ -219,7 +219,7 @@ export function buildSettingsRows(
219
219
  id: "stallEscalationRefires",
220
220
  section: "stall-brakes",
221
221
  label: "Stall escalation refires",
222
- valueText: show("stallEscalationRefires", `(${DEFAULT_STALL_ESCALATION_REFIRES})`),
222
+ valueText: show("stallEscalationRefires", `${DEFAULT_STALL_ESCALATION_REFIRES}`),
223
223
  sourceText: src("stallEscalationRefires"),
224
224
  description:
225
225
  "heartbeat refires with no turn before the goal pauses / loop stops (0 = never)",
@@ -228,7 +228,7 @@ export function buildSettingsRows(
228
228
  id: "stallShortWords",
229
229
  section: "stall-brakes",
230
230
  label: "Stall short words",
231
- valueText: show("stallShortWords", `(${DEFAULT_STALL_SHORT_WORDS})`),
231
+ valueText: show("stallShortWords", `${DEFAULT_STALL_SHORT_WORDS}`),
232
232
  sourceText: src("stallShortWords"),
233
233
  description: "turns with no tools AND fewer words than this count as a nudge",
234
234
  },
@@ -236,7 +236,7 @@ export function buildSettingsRows(
236
236
  id: "stallSimilarityThreshold",
237
237
  section: "stall-brakes",
238
238
  label: "Stall similarity threshold",
239
- valueText: show("stallSimilarityThreshold", `(${DEFAULT_STALL_SIM_THRESHOLD})`),
239
+ valueText: show("stallSimilarityThreshold", `${DEFAULT_STALL_SIM_THRESHOLD}`),
240
240
  sourceText: src("stallSimilarityThreshold"),
241
241
  description:
242
242
  "no-tool turns whose text is > this similar to the prior turn count as a nudge (0–1)",
@@ -249,7 +249,7 @@ export function buildSettingsRows(
249
249
  id: "subagentModelStrategy",
250
250
  section: "subagents",
251
251
  label: "Subagent model strategy",
252
- valueText: show("subagentModelStrategy", "(inherit-parent)"),
252
+ valueText: show("subagentModelStrategy", "inherit-parent"),
253
253
  sourceText: src("subagentModelStrategy"),
254
254
  description:
255
255
  "inherit-parent shares your session model + quota pool; agent-default uses the upstream pi-subagents default agents",
@@ -258,45 +258,52 @@ export function buildSettingsRows(
258
258
  id: "subagentModelOverrides.Explore",
259
259
  section: "subagents",
260
260
  label: "Subagent Explore pin",
261
- valueText: settings.subagentModelOverrides?.Explore ?? "(follows strategy)",
261
+ valueText: settings.subagentModelOverrides?.Explore ?? "follows strategy",
262
262
  sourceText:
263
263
  settings.subagentModelOverrides?.Explore !== undefined
264
264
  ? src("subagentModelOverrides")
265
- : "[default]",
265
+ : "default",
266
266
  description: "provider/model pin; always wins over strategy",
267
267
  },
268
268
  {
269
269
  id: "subagentModelOverrides.Plan",
270
270
  section: "subagents",
271
271
  label: "Subagent Plan pin",
272
- valueText: settings.subagentModelOverrides?.Plan ?? "(follows strategy)",
272
+ valueText: settings.subagentModelOverrides?.Plan ?? "follows strategy",
273
273
  sourceText:
274
274
  settings.subagentModelOverrides?.Plan !== undefined
275
275
  ? src("subagentModelOverrides")
276
- : "[default]",
276
+ : "default",
277
277
  description: "provider/model pin; always wins over strategy",
278
278
  },
279
279
  {
280
280
  id: "subagentModelOverrides.general-purpose",
281
281
  section: "subagents",
282
282
  label: "Subagent general-purpose pin",
283
- valueText: settings.subagentModelOverrides?.["general-purpose"] ?? "(follows strategy)",
283
+ valueText: settings.subagentModelOverrides?.["general-purpose"] ?? "follows strategy",
284
284
  sourceText:
285
285
  settings.subagentModelOverrides?.["general-purpose"] !== undefined
286
286
  ? src("subagentModelOverrides")
287
- : "[default]",
287
+ : "default",
288
288
  description: "provider/model pin; always wins over strategy",
289
289
  },
290
290
  {
291
291
  id: "subagentResolved",
292
292
  section: "subagents",
293
293
  label: "Effective resolution",
294
- valueText: [
295
- resolveEffectiveSubagentModel("Explore", settings, subagent.sessionModel),
296
- resolveEffectiveSubagentModel("Plan", settings, subagent.sessionModel),
297
- resolveEffectiveSubagentModel("general-purpose", settings, subagent.sessionModel),
298
- ].join(" · "),
299
- sourceText: "[runtime]",
294
+ // v0.28.20: compact — strip the parenthesized qualifier (the
295
+ // DESCRIPTION column carries semantics) and dedupe identical
296
+ // resolutions; the old 3-part parenthesized composite never fit.
297
+ valueText: (() => {
298
+ const strip = (r: string) => r.replace(/ \([^)]*\)$/, "").replace(/^\((.*)\)$/, "$1");
299
+ const parts = [
300
+ resolveEffectiveSubagentModel("Explore", settings, subagent.sessionModel),
301
+ resolveEffectiveSubagentModel("Plan", settings, subagent.sessionModel),
302
+ resolveEffectiveSubagentModel("general-purpose", settings, subagent.sessionModel),
303
+ ].map(strip);
304
+ return parts.every((p) => p === parts[0]) ? parts[0]! : parts.join(" · ");
305
+ })(),
306
+ sourceText: "runtime",
300
307
  description: "effective Explore / Plan / general-purpose model given current settings",
301
308
  },
302
309
  );
@@ -307,7 +314,7 @@ export function buildSettingsRows(
307
314
  id: "notifyCmd",
308
315
  section: "other",
309
316
  label: "Notify command",
310
- valueText: show("notifyCmd", "(off)"),
317
+ valueText: show("notifyCmd", "off"),
311
318
  sourceText: src("notifyCmd"),
312
319
  description: "desktop push command; the event message is passed as $1",
313
320
  },
@@ -315,16 +322,16 @@ export function buildSettingsRows(
315
322
  id: "tokenLimit",
316
323
  section: "other",
317
324
  label: "Token limit per goal",
318
- valueText: show("tokenLimit", "(off)"),
325
+ valueText: show("tokenLimit", "off"),
319
326
  sourceText: src("tokenLimit"),
320
327
  description: "per-goal token budget; pause when exceeded (0 = off)",
321
328
  },
322
329
  {
323
330
  id: "postaudit",
324
331
  section: "other",
325
- label: "Postaudit config…",
332
+ label: "Postaudit",
326
333
  valueText: "open sub-menu",
327
- sourceText: "[—]",
334
+ sourceText: "—",
328
335
  description:
329
336
  "post-completion follow-up enqueuer: mode, triggers, cascade, caps (postaudit / reviewer)",
330
337
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.19",
3
+ "version": "0.28.21",
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",