pi-goal-list-loop-audit 0.28.26 → 0.28.28

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
@@ -39,6 +39,7 @@ Five top-level commands — `/goal`, `/list`, `/loop`, `/glla`, `/review`:
39
39
  /goal resume # resume
40
40
  /goal cancel # abort
41
41
  /goal decide # re-open the decision picker (v0.28.23)
42
+ /goal audit # run the isolated auditor on the current goal now — no agent turn (v0.28.27)
42
43
  /goal tweak "<new objective>" # edit in place (Confirm dialog)
43
44
  /goal archive # archived goals, newest first
44
45
  /glla # settings UI table · /glla key=value · /glla stats · /glla audits [N|full] · /glla postaudit · /glla autoaccept=on
@@ -185,6 +185,10 @@ export interface Goal {
185
185
  * the retry resolves. Only consumed while paused with an "auditor quota:"
186
186
  * reason, so a stale value is unreachable by construction. */
187
187
  pendingCompletion?: { completionSummary?: string; verificationSummary?: string; at: string };
188
+ /** v0.28.28: provenance — who created this goal ("user", "list-cascade",
189
+ * "draft-confirmed", "draft-autoaccepted"). Ledgered on goal_created so
190
+ * "where did this come from" is answerable after the fact. */
191
+ createdVia?: string;
188
192
  /** v0.25.0 (contract item 22): auditor objections extracted as TODOs when
189
193
  * aggressiveMode keeps the goal active past the disapproval cap. Rendered
190
194
  * into every continuation prompt until the next audit clears them. */
@@ -212,9 +216,9 @@ export interface Goal {
212
216
  export type GoalRoute =
213
217
  | { kind: "draft" }
214
218
  | { kind: "set"; text: string }
215
- | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "decide" | "tweak" | "archive" | "start"; rest: string };
219
+ | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "decide" | "audit" | "tweak" | "archive" | "start"; rest: string };
216
220
 
217
- const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel", "decide"]);
221
+ const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel", "decide", "audit"]);
218
222
  const GOAL_ARG_SUBS = new Set(["tweak", "archive", "start"]);
219
223
 
220
224
  export function routeGoalArgs(raw: string): GoalRoute {
@@ -568,6 +568,12 @@ function heartbeatTick(): void {
568
568
  // machinery below stays quiet for 3 minutes while the replaced session
569
569
  // settles (latch watchdog, wedge alert, refire counting all resume after).
570
570
  if (Date.now() < compactionGraceUntil) return;
571
+ // v0.28.27: a stale (session-replaced) handle can never land a send —
572
+ // the terminal warning already fired once. ALL stall machinery stays
573
+ // quiet from here on: refiring into a dead process is misleading, and
574
+ // worse, the stall escalation would PAUSE the goal — silently cancelling
575
+ // the interruptedAt → auto-resume-on-restart promise the footer shows.
576
+ if (extensionApiStale) return;
571
577
  // v0.26.5: pending-latch watchdog — a queued continuation whose turn
572
578
  // trigger was dropped (field-observed post-compaction: continuation
573
579
  // ACCEPTED at compact+0s, then 22 minutes of silence). The stuck latch
@@ -889,18 +895,19 @@ function notifyPersistenceState(ctx: ExtensionContext): void {
889
895
  }
890
896
  }
891
897
 
892
- function setGoal(goal: Goal, ctx: ExtensionContext): void {
898
+ function setGoal(goal: Goal, ctx: ExtensionContext, via = "user"): void {
893
899
  // v0.28.14: never silently orphan a live goal — a paused/active goal
894
900
  // being replaced is archived honestly first (the old behavior left it in
895
901
  // goals/ but untracked: "older goals lying around leading to confusion").
896
902
  if (state.goal && state.goal.id !== goal.id && (state.goal.status === "active" || state.goal.status === "paused")) {
897
903
  archiveCurrentGoal(ctx, "aborted", `replaced by goal ${goal.id}`);
898
904
  }
905
+ goal.createdVia = via; // v0.28.28: provenance — answerable from the ledger + /glla log
899
906
  state = { ...state, goal }; // preserve list AND loop (v0.28.14: the bare reconstruction used to nuke a held/active loop whenever a goal was set)
900
907
  const file = writeGoalMd(ctx.cwd, goal);
901
908
  state.goal!.activePath = path.relative(ctx.cwd, file) || file;
902
909
  persistState(ctx);
903
- appendLedger(ctx.cwd, "goal_created", { goalId: goal.id, objective: goal.objective, policy: goal.policy });
910
+ appendLedger(ctx.cwd, "goal_created", { goalId: goal.id, objective: goal.objective, policy: goal.policy, via });
904
911
  }
905
912
 
906
913
  function updateGoal(patch: Partial<Goal>, ctx: ExtensionContext): void {
@@ -963,15 +970,17 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
963
970
  * infra) → hand back to the agent: resume active + continuation, verdict
964
971
  * durable in auditHistory.
965
972
  */
966
- async function retryStoredCompletionAudit(ctx: ExtensionContext): Promise<void> {
973
+ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-retry" | "manual" = "quota-retry"): Promise<void> {
967
974
  const goal = state.goal;
968
975
  if (!goal?.pendingCompletion) return;
969
976
  if (completionAuditInFlight) return;
970
977
  const liveCtx = freshCtx() ?? ctx;
971
978
  const claim = goal.pendingCompletion;
972
979
  updateGoal({ status: "auditing" }, liveCtx);
973
- appendLedger(liveCtx.cwd, "goal_resumed", { via: "quota-retry-direct-audit" });
974
- liveCtx.ui.notify("Auditor quota window elapsed — retrying the audit with your stored completion claim (no agent turn needed).", "info");
980
+ appendLedger(liveCtx.cwd, "goal_resumed", { via: origin === "manual" ? "manual-audit" : "quota-retry-direct-audit" });
981
+ liveCtx.ui.notify(origin === "manual"
982
+ ? "Manual /goal audit — running the isolated auditor now (no agent turn needed)."
983
+ : "Auditor quota window elapsed — retrying the audit with your stored completion claim (no agent turn needed).", "info");
975
984
  const settings = loadSettings(liveCtx.cwd);
976
985
  const { model: auditorModel, error: modelError, via } = resolveAuditorModel(liveCtx, settings.auditorModel);
977
986
  if (modelError) liveCtx.ui.notify(`Auditor model issue: ${modelError}`, "warning");
@@ -1027,9 +1036,9 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext): Promise<void>
1027
1036
  if (result.approved) {
1028
1037
  updateGoal({ auditHistory: history, pendingCompletion: undefined }, liveCtx);
1029
1038
  const objective = state.goal.objective;
1030
- archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (quota-retry)`);
1031
- liveCtx.ui.notify(`Goal complete — auditor ${result.model} approved on the quota retry.`, "info");
1032
- notifyExternal(liveCtx, `Goal complete (auditor approved on quota-retry): ${objective.slice(0, 120)}`);
1039
+ archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`);
1040
+ liveCtx.ui.notify(`Goal complete — auditor ${result.model} approved${origin === "manual" ? " on /goal audit" : " on the quota retry"}.`, "info");
1041
+ notifyExternal(liveCtx, `Goal complete (auditor approved, ${origin}): ${objective.slice(0, 120)}`);
1033
1042
  return;
1034
1043
  }
1035
1044
 
@@ -1051,7 +1060,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext): Promise<void>
1051
1060
  liveCtx.ui.notify(`Auditor still quota-limited — next auto-retry in ${retryMin}m (your completion claim is stored; no action needed).`, "warning");
1052
1061
  scheduleQuotaRetry(liveCtx, quota.retryAfterSec, result.error, () => {
1053
1062
  if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("auditor quota:") && state.goal.pendingCompletion) {
1054
- void retryStoredCompletionAudit(liveCtx);
1063
+ void retryStoredCompletionAudit(liveCtx, origin);
1055
1064
  }
1056
1065
  });
1057
1066
  return;
@@ -1072,10 +1081,10 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext): Promise<void>
1072
1081
  }, liveCtx);
1073
1082
  liveCtx.ui.notify(
1074
1083
  result.disapproved
1075
- ? `Auditor (quota-retry) DISAPPROVED — resuming; the report is in /goal status.`
1084
+ ? `Auditor (${origin}) DISAPPROVED — resuming; the report is in /goal status.`
1076
1085
  : result.impossible
1077
- ? `Auditor (quota-retry): goal IMPOSSIBLE — ${(result.impossibleReason ?? "").slice(0, 100)}. Resuming; consider /goal tweak.`
1078
- : `Auditor (quota-retry) hit an infrastructure error — resuming; re-call complete_goal when ready.`,
1086
+ ? `Auditor (${origin}): goal IMPOSSIBLE — ${(result.impossibleReason ?? "").slice(0, 100)}. Resuming; consider /goal tweak.`
1087
+ : `Auditor (${origin}) hit an infrastructure error — resuming; re-call complete_goal when ready.`,
1079
1088
  "warning",
1080
1089
  );
1081
1090
  appendLedger(liveCtx.cwd, "quota_retry_audit_verdict", {
@@ -1134,7 +1143,7 @@ function fireReviewer(
1134
1143
  manual: opts.manual,
1135
1144
  ledgerEntries,
1136
1145
  sources,
1137
- enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer"),
1146
+ enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer", { autoActivate: loadSettings(ctx.cwd).autoResume === true }),
1138
1147
  proposeGoal: (objective, reason) => {
1139
1148
  try {
1140
1149
  extensionApi?.sendUserMessage(
@@ -1204,7 +1213,7 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
1204
1213
  state = { ...state, list: rest };
1205
1214
  const goal = createGoal(next.objective, ctx, "list");
1206
1215
  if (next.verificationContract) goal.verificationContract = next.verificationContract;
1207
- setGoal(goal, ctx);
1216
+ setGoal(goal, ctx, "list-cascade");
1208
1217
  iterationCounter = 0;
1209
1218
  consecutiveErrorIterations = 0;
1210
1219
  consecutiveAbortIterations = 0;
@@ -1309,6 +1318,30 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
1309
1318
  if (!shown) ctx.ui.notify("No pending decision — the goal isn't paused on a choice (or no UI).", "info");
1310
1319
  return;
1311
1320
  }
1321
+ // v0.28.27: /goal audit — run the isolated auditor on the current goal
1322
+ // RIGHT NOW, without engaging the agent. The user's "the work looks
1323
+ // done — just verify it" handle (and the manual counterpart of the
1324
+ // v0.28.26 stored-claim quota retry). Seeds a synthesized claim so a
1325
+ // quota block falls into the same pendingCompletion retry machinery.
1326
+ if (route.name === "audit") {
1327
+ if (!state.goal) {
1328
+ ctx.ui.notify("No active goal — /goal audit needs a goal to verify.", "warning");
1329
+ return;
1330
+ }
1331
+ if (completionAuditInFlight) {
1332
+ ctx.ui.notify("An audit is already running…", "info");
1333
+ return;
1334
+ }
1335
+ updateGoal({
1336
+ pendingCompletion: {
1337
+ completionSummary: "Manual audit requested by the user via /goal audit (no agent completion claim). Verify the objective against the repo directly.",
1338
+ at: nowIso(),
1339
+ },
1340
+ }, ctx);
1341
+ appendLedger(ctx.cwd, "manual_audit_requested", { goalId: state.goal.id });
1342
+ void retryStoredCompletionAudit(ctx, "manual");
1343
+ return;
1344
+ }
1312
1345
  if (route.name === "tweak") return cmdTweak(route.rest, ctx);
1313
1346
  if (route.name === "archive") return cmdGoals(ctx);
1314
1347
  // v0.16.0: /goal start <objective> — explicit skip-draft. Activates
@@ -1599,7 +1632,7 @@ async function cmdTweak(args: string, ctx: ExtensionContext): Promise<void> {
1599
1632
  * contract extraction) → appended to the queue → persisted → first item
1600
1633
  * activated when nothing is running. Returns the count enqueued.
1601
1634
  */
1602
- function enqueueItems(ctx: ExtensionContext, texts: string[], source: string): number {
1635
+ function enqueueItems(ctx: ExtensionContext, texts: string[], source: string, opts?: { autoActivate?: boolean }): number {
1603
1636
  const items = texts.map((text) => {
1604
1637
  const extracted = extractVerificationContract(text);
1605
1638
  return { id: newGoalId(), objective: extracted.objective, verificationContract: extracted.verificationContract || undefined, addedAt: nowIso() };
@@ -1608,7 +1641,16 @@ function enqueueItems(ctx: ExtensionContext, texts: string[], source: string): n
1608
1641
  persistState(ctx);
1609
1642
  appendLedger(ctx.cwd, "list_imported", { source, count: items.length });
1610
1643
  if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
1611
- activateNextListItem(ctx);
1644
+ // v0.28.28: unsolicited sources (the reviewer) do NOT auto-start the
1645
+ // head unless autoResume is on — "I cancelled a goal and the next one
1646
+ // started itself" was the field complaint. User-driven imports keep
1647
+ // the immediate-start behavior (opts default true).
1648
+ if (opts?.autoActivate === false) {
1649
+ ctx.ui.notify(`Queued ${items.length} item(s) from ${source} — /list next when ready (auto-start is opt-in: /glla autoresume=on).`, "info");
1650
+ appendLedger(ctx.cwd, "list_autoactivation_held", { source, count: items.length });
1651
+ } else {
1652
+ activateNextListItem(ctx);
1653
+ }
1612
1654
  }
1613
1655
  return items.length;
1614
1656
  }
@@ -3135,13 +3177,34 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3135
3177
  persistState(liveCtx);
3136
3178
  appendLedger(liveCtx.cwd, "list_added", { id: item.id, objective: item.objective, drafted: true });
3137
3179
  if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
3180
+ // v0.28.28: an AUTO-ACCEPTED draft does not auto-start unless
3181
+ // autoResume is on — accepting a draft is not consent to start.
3182
+ if (autoAccept && loadSettings(liveCtx.cwd).autoResume !== true) {
3183
+ liveCtx.ui.notify(`Auto-accepted and QUEUED (autoResume off — not auto-started): ${extracted.objective.slice(0, 80)} — /list next when ready.`, "info");
3184
+ appendLedger(liveCtx.cwd, "list_autoactivation_held", { source: "draft-autoaccepted", count: 1 });
3185
+ return { content: [{ type: "text", text: "Draft accepted and added to the list, but NOT started (the user's autoResume setting is off — auto-accepted drafts queue, they don't auto-start). Do NOT begin work. Tell the user: /list next starts it." }], details: {} };
3186
+ }
3138
3187
  activateNextListItem(liveCtx);
3139
3188
  return { content: [{ type: "text", text: "Confirmed and activated (list was empty). Begin work now." }], details: {} };
3140
3189
  }
3141
3190
  return { content: [{ type: "text", text: `Confirmed and added to the list (${listQueue().length} waiting). It activates when the current goal completes.` }], details: {} };
3142
3191
  }
3143
3192
  const goal = createGoal(full, liveCtx);
3144
- setGoal(goal, liveCtx);
3193
+ setGoal(goal, liveCtx, autoAccept ? "draft-autoaccepted" : "draft-confirmed");
3194
+ // v0.28.28: auto-accepted goal drafts are created HELD when autoResume
3195
+ // is off — auto-accept delegates the Confirm click, not the decision
3196
+ // to start. Explicit user-confirmed drafts still start immediately.
3197
+ if (autoAccept && loadSettings(liveCtx.cwd).autoResume !== true) {
3198
+ updateGoal({
3199
+ status: "paused",
3200
+ pauseKind: "blocked",
3201
+ pauseReason: "auto-accepted draft — held for the user's go-ahead (autoResume off)",
3202
+ pauseSuggestedAction: "/goal resume to start · /goal cancel to drop · /glla autoresume=on starts auto-accepted drafts automatically",
3203
+ }, liveCtx);
3204
+ appendLedger(liveCtx.cwd, "draft_held", { goalId: goal.id, reason: "autoaccept-autoresume-off" });
3205
+ liveCtx.ui.notify(`Draft auto-accepted and HELD (autoResume off): ${goal.objective.slice(0, 80)} — /goal resume to start, /goal cancel to drop.`, "info");
3206
+ return { content: [{ type: "text", text: "Goal accepted but HELD (the user's autoResume setting is off — auto-accepted drafts do not auto-start). Do NOT begin work. Tell the user: /goal resume starts it, /goal cancel drops it." }], details: {} };
3207
+ }
3145
3208
  iterationCounter = 0;
3146
3209
  consecutiveErrorIterations = 0;
3147
3210
  consecutiveAbortIterations = 0;
@@ -4053,6 +4116,42 @@ function cmdStats(args: string, ctx: ExtensionContext): void {
4053
4116
  * log (.pi-glla/audits.jsonl). Default: last 10 verdicts, one line each.
4054
4117
  * "full" prints the latest report in full.
4055
4118
  */
4119
+ /**
4120
+ * v0.28.28: /glla log [N] — human-readable tail of the event ledger (the
4121
+ * forensic trail: who created/resumed/paused goals, from where). Skips the
4122
+ * high-frequency noise entries (state snapshots, re-arm internals) unless
4123
+ * "all" is passed. N defaults to 15.
4124
+ */
4125
+ const LOG_NOISE = new Set(["state", "send_rearm_start", "heartbeat_suppressed_tick"]);
4126
+ function cmdLog(args: string, ctx: ExtensionContext): void {
4127
+ const all = /\ball\b/.test(args);
4128
+ const nMatch = args.match(/\b(\d+)\b/);
4129
+ const n = Math.min(Math.max(parseInt(nMatch?.[1] ?? "15", 10) || 15, 1), 100);
4130
+ let entries: Array<{ type: string; at?: string; value?: any }> = [];
4131
+ try {
4132
+ entries = parseLedgerEntries(fs.readFileSync(ledgerPath(ctx.cwd), "utf-8"));
4133
+ } catch {
4134
+ ctx.ui.notify("No ledger yet — .pi-glla/active.jsonl doesn't exist.", "info");
4135
+ return;
4136
+ }
4137
+ const visible = all ? entries : entries.filter((e) => !LOG_NOISE.has(e.type));
4138
+ const tail = visible.slice(-n);
4139
+ if (tail.length === 0) {
4140
+ ctx.ui.notify("Ledger is empty (no non-noise events yet).", "info");
4141
+ return;
4142
+ }
4143
+ const lines = tail.map((e) => {
4144
+ const t = (e.at ?? "").slice(11, 19);
4145
+ const v = e.value ?? {};
4146
+ const detail = Object.entries(v)
4147
+ .filter(([k]) => k !== "goalId" && k !== "report")
4148
+ .map(([k, val]) => `${k}=${typeof val === "string" ? val.slice(0, 60) : JSON.stringify(val)?.slice(0, 60)}`)
4149
+ .join(" ");
4150
+ return `${t} ${e.type}${detail ? ` ${detail}` : ""}`;
4151
+ });
4152
+ ctx.ui.notify(`Ledger tail (last ${tail.length}${all ? "" : " non-noise"} events — /glla log <N> for more, /glla log all to include noise):\n${lines.join("\n")}`, "info");
4153
+ }
4154
+
4056
4155
  function cmdAudits(args: string, ctx: ExtensionContext): void {
4057
4156
  const full = /\bfull\b/.test(args);
4058
4157
  const all = /\b(?:all|global|log)\b/.test(args);
@@ -4104,6 +4203,12 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
4104
4203
  cmdAudits(trimmed.slice("audits".length).trim(), ctx);
4105
4204
  return;
4106
4205
  }
4206
+ // v0.28.28: /glla log [N] — the raw event trail, human-readable. "Log it
4207
+ // so we can look back and see where we are doing things wrong."
4208
+ if (/^log\b/.test(trimmed)) {
4209
+ cmdLog(trimmed.slice("log".length).trim(), ctx);
4210
+ return;
4211
+ }
4107
4212
  if (/^reviewer\b/.test(trimmed)) {
4108
4213
  await cmdReviewerSettings(ctx);
4109
4214
  return;
@@ -4494,6 +4599,7 @@ export default function (pi: ExtensionAPI): void {
4494
4599
  ["autoresume=", "default: hold when a session is loaded, auto-resume on reload/fork; on: always auto-resume; off: never"],
4495
4600
  ["decisionpopup=", "on|off: decision pauses pop the select() picker (default on; the widget card always lists the options, /goal decide reopens the picker)"],
4496
4601
  ["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)"],
4602
+ ["log", "event-trail tail: /glla log [N] — who created/resumed/paused what, from where (v0.28.28)"],
4497
4603
  ["auditfeedbackchars=", "cap on executor-visible disapproval report chars (0 = full report, the default)"],
4498
4604
  ["aggressivemode=", "on: keep-going defaults — autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs"],
4499
4605
  ["quotaretryminutes=", "N: minutes before auto-retrying a quota-exhausted auditor (default 60)"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.26",
3
+ "version": "0.28.28",
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",
@@ -54,6 +54,7 @@
54
54
  "interruptedReason": { "type": "string" },
55
55
  "auditInfraStreak": { "type": "number" },
56
56
  "pendingCompletion": { "type": "object" },
57
+ "createdVia": { "type": "string" },
57
58
  "activePath": { "type": "string" },
58
59
  "archivedPath": { "type": "string" },
59
60
  "usage": {