pi-goal-list-loop-audit 0.35.13 → 0.35.64

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +977 -0
  2. package/INSTALL.md +13 -3
  3. package/LIST-PHILOSOPHY.md +90 -0
  4. package/PLAN.md +316 -0
  5. package/README.md +201 -26
  6. package/docs/DESIGN-subagent-visibility.md +76 -0
  7. package/docs/DESIGN.md +59 -8
  8. package/docs/GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md +6 -1
  9. package/docs/INDEX.md +4 -0
  10. package/docs/VISION-ASSIST.md +2 -2
  11. package/extensions/auditor-extensions.ts +289 -0
  12. package/extensions/context-hygiene.ts +129 -0
  13. package/extensions/faulty-objective-recovery.ts +24 -0
  14. package/extensions/glla-state-root.ts +99 -0
  15. package/extensions/goal-agents-panel.ts +211 -0
  16. package/extensions/goal-commands.ts +226 -10
  17. package/extensions/goal-continuation.ts +119 -21
  18. package/extensions/goal-heartbeat.ts +560 -35
  19. package/extensions/goal-loop-auditor-process.ts +126 -4
  20. package/extensions/goal-loop-auditor.ts +3 -5
  21. package/extensions/goal-loop-backoff.ts +79 -0
  22. package/extensions/goal-loop-core.ts +197 -15
  23. package/extensions/goal-loop-dispatch.ts +3 -2
  24. package/extensions/goal-loop-display.ts +141 -8
  25. package/extensions/goal-loop-forever.ts +52 -3
  26. package/extensions/goal-loop-shield.ts +132 -26
  27. package/extensions/goal-loop-stats.ts +3 -2
  28. package/extensions/goal-loop.ts +101 -4
  29. package/extensions/goal-recovery.ts +8 -2
  30. package/extensions/goal-settings.ts +61 -14
  31. package/extensions/goal-state.ts +20 -0
  32. package/extensions/loops/goal-activation.ts +426 -45
  33. package/extensions/loops/goal-auditor-hooks.ts +39 -3
  34. package/extensions/loops/goal-auditor-surface.ts +32 -0
  35. package/extensions/loops/goal-list-queue.ts +72 -12
  36. package/extensions/loops/goal-orchestrator.ts +33 -1
  37. package/extensions/loops/goal-runtime-globals.ts +5 -0
  38. package/extensions/loops/goal-session.ts +68 -8
  39. package/extensions/loops/goal-settings-ui.ts +116 -5
  40. package/extensions/loops/goal-tools.ts +104 -29
  41. package/extensions/loops/goal-ui.ts +106 -1
  42. package/extensions/loops/goal.ts +7 -7
  43. package/extensions/main-model-recovery.ts +10 -0
  44. package/extensions/multi-model-picker.ts +45 -7
  45. package/extensions/payload-guard.ts +172 -0
  46. package/extensions/reviewer.ts +3 -1
  47. package/extensions/settings-menu.ts +39 -4
  48. package/package.json +9 -3
  49. package/prompts/goal-loop-plan-loop.md +31 -0
  50. package/prompts/goal-loop-plan.md +51 -0
  51. package/schemas/goal.schema.json +4 -1
  52. package/scripts/auditor-extension-fixture.mjs +19 -0
  53. package/scripts/goal-auditor-launch.mjs +19 -1
  54. package/scripts/goal-auditor-worker.mjs +15 -0
  55. package/scripts/verify-auditor-extensions-offline.mjs +72 -0
@@ -0,0 +1,211 @@
1
+ // pi-goal-list-loop-audit — v0.35.29
2
+ // goal-agents-panel.ts — issue #15 subagent visibility (design:
3
+ // docs/DESIGN-subagent-visibility.md, scope agreed 2026-08-22).
4
+ //
5
+ // Pure rendering + data assembly for:
6
+ // 1. /glla agents — tracked-subagent snapshot table
7
+ // 2. /glla agents --tail <id> — read-only child transcript tail
8
+ // 3. the widget agents segment (line built here, appended by the widget)
9
+ //
10
+ // Everything here is deterministic given its inputs; fs access happens only
11
+ // in tailChildTranscript through an injected reader so tests stay hermetic.
12
+
13
+ import * as path from "node:path";
14
+ import { sanitizeDisplayText } from "./goal-loop-core.js";
15
+
16
+ /** v0.35.45 (audit finding): the candidate scan reads a bounded TAIL of each
17
+ * transcript, not the whole file — up to 25 files were previously read in
18
+ * full, synchronously, on the main thread. 256 KiB is far more than any
19
+ * needle window needs (summary prefix / agentType / recordId recur
20
+ * throughout recent entries). */
21
+ export const TRANSCRIPT_SCAN_MAX_BYTES = 256 * 1024;
22
+
23
+ export interface AgentsPanelRow {
24
+ recordId: string;
25
+ agentType?: string;
26
+ summary?: string;
27
+ status: "running" | "hung" | "ended";
28
+ spawnedAt: number;
29
+ lastProgressAt: number;
30
+ toolUses: number;
31
+ outputTokens: number;
32
+ silentMs: number;
33
+ evidence: "record-frozen" | "event-only" | "live";
34
+ action?: "abort-requested" | "unavailable" | "failed";
35
+ endedOk?: boolean;
36
+ endedAt?: number;
37
+ }
38
+
39
+ /** Human label: agentType + short summary. */
40
+ function rowLabel(row: AgentsPanelRow): string {
41
+ const summary = (row.summary ?? "").replace(/\s+/g, " ").trim();
42
+ return [row.agentType ?? "subagent", summary ? truncate(summary, 28) : ""].filter(Boolean).join(" · ");
43
+ }
44
+
45
+ export function truncate(text: string, max: number): string {
46
+ return text.length <= max ? text : text.slice(0, Math.max(1, max - 1)) + "…";
47
+ }
48
+
49
+ function fmtDuration(ms: number): string {
50
+ const totalSec = Math.max(0, Math.round(ms / 1000));
51
+ const min = Math.floor(totalSec / 60);
52
+ const sec = totalSec % 60;
53
+ if (min >= 60) return `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}m`;
54
+ if (min > 0) return `${min}m${String(sec).padStart(2, "0")}s`;
55
+ return `${sec}s`;
56
+ }
57
+
58
+ const PANEL_ROW_CAP = 20;
59
+
60
+ /** Render the /glla agents table. Running/hung first (hung topmost), then
61
+ * ended; capped at PANEL_ROW_CAP rows with an explicit truncation notice. */
62
+ export function renderAgentsPanel(rows: AgentsPanelRow[], now: number, managerAvailable: boolean): string[] {
63
+ if (rows.length === 0) {
64
+ return ["No subagents tracked yet — spawn one via the Agent tool and it appears here.", "(evidence: glla's event probes" + (managerAvailable ? " + pi-subagents manager records" : "") + ")"];
65
+ }
66
+ const rank = (r: AgentsPanelRow): number => (r.status === "hung" ? 0 : r.status === "running" ? 1 : 2);
67
+ const ordered = [...rows].sort((a, b) => rank(a) - rank(b) || a.silentMs - b.silentMs);
68
+ const shown = ordered.slice(0, PANEL_ROW_CAP);
69
+ const lines: string[] = [];
70
+ for (const row of shown) {
71
+ const glyph = row.status === "ended" ? "✓" : row.status === "hung" ? "⚠" : "●";
72
+ const stateWord = row.status === "ended"
73
+ ? `ENDED ${row.endedOk === false ? "✗" : "ok"} ${fmtDuration((row.endedAt ?? now) - row.spawnedAt)}`
74
+ : row.action === "abort-requested"
75
+ ? `ABORTING ${fmtDuration(now - row.spawnedAt)}`
76
+ : `${row.status === "hung" ? "HUNG?" : "RUNNING"} ${fmtDuration(now - row.spawnedAt)}`;
77
+ lines.push(`${glyph} ${rowLabel(row)} ${stateWord}`);
78
+ lines.push(` tools ${row.toolUses} · out ${row.outputTokens >= 1000 ? `${(row.outputTokens / 1000).toFixed(1)}k` : row.outputTokens} · silent ${fmtDuration(row.silentMs)}${row.evidence !== "live" ? ` (${row.evidence})` : ""}`);
79
+ if (row.action === "abort-requested") {
80
+ lines.push(" └ child-specific abort requested; partial output remains available while it settles");
81
+ } else if (row.action === "unavailable") {
82
+ lines.push(" └ no safe child-abort capability; inspect the child and interrupt explicitly if needed");
83
+ } else if (row.action === "failed") {
84
+ lines.push(" └ child-abort request failed; inspect the child and interrupt explicitly if needed");
85
+ } else if (row.status === "hung") {
86
+ lines.push(" └ check the Agents panel: a child whose counters stopped moving is hung, not thinking");
87
+ }
88
+ }
89
+ if (ordered.length > shown.length) {
90
+ lines.push(`… ${ordered.length - shown.length} more (oldest ended trimmed — cap ${PANEL_ROW_CAP})`);
91
+ }
92
+ return lines;
93
+ }
94
+
95
+ /** The ambient widget segment: hidden at zero tracked children by the caller. */
96
+ export function renderAgentsWidgetLine(rows: AgentsPanelRow[]): string | undefined {
97
+ const active = rows.filter((r) => r.status !== "ended");
98
+ if (active.length === 0) return undefined;
99
+ const busiest = [...active].sort((a, b) => b.silentMs - a.silentMs)[0]!;
100
+ const hung = busiest.action === "abort-requested"
101
+ ? " ⚠ aborting"
102
+ : busiest.status === "hung"
103
+ ? " ⚠"
104
+ : "";
105
+ return `● ${active.length} agent${active.length === 1 ? "" : "s"} · ${(busiest.agentType ?? "subagent")} silent ${fmtDuration(busiest.silentMs)}${hung}`;
106
+ }
107
+
108
+ export interface TranscriptTailResult {
109
+ ok: boolean;
110
+ lines: string[];
111
+ /** What was searched / read — surfaced to the user on failure. */
112
+ detail: string;
113
+ }
114
+
115
+ /** Read-only tail of a child's session transcript. Candidate selection: all
116
+ * .jsonl files in sessionsDir whose content mentions the child's summary or
117
+ * agent type; most recently modified wins. NEVER resumes or attaches.
118
+ * readFile is injected for tests; production passes fs.readFileSync-wrapped. */
119
+ export function tailChildTranscript(
120
+ sessionsDir: string,
121
+ row: { recordId: string; agentType?: string; summary?: string },
122
+ opts: {
123
+ lines?: number;
124
+ readFile?: (file: string, maxBytes?: number) => Buffer;
125
+ listDir?: (dir: string) => string[];
126
+ statMtime?: (file: string) => number;
127
+ now?: number;
128
+ } = {},
129
+ ): TranscriptTailResult {
130
+ const wantLines = Math.max(1, Math.min(200, opts.lines ?? 20));
131
+ const readFile = opts.readFile ?? (() => { throw new Error("no reader"); });
132
+ // v0.35.45: readers may honor maxBytes (production does — partial tail
133
+ // read); injected test readers ignore it and return the whole buffer.
134
+ const scanRead = (file: string): Buffer => readFile(file, TRANSCRIPT_SCAN_MAX_BYTES);
135
+ const listDir = opts.listDir ?? (() => []);
136
+ const statMtime = opts.statMtime ?? (() => 0);
137
+ let entries: string[];
138
+ try {
139
+ entries = listDir(sessionsDir);
140
+ } catch (error) {
141
+ return { ok: false, lines: [], detail: `cannot list ${sessionsDir}: ${error instanceof Error ? error.message.slice(0, 120) : String(error)}` };
142
+ }
143
+ const needles = [row.summary?.slice(0, 48), row.agentType, row.recordId]
144
+ .filter((n): n is string => typeof n === "string" && n.trim().length >= 6)
145
+ .map((n) => n.toLowerCase());
146
+ const candidates = entries
147
+ .filter((f) => f.endsWith(".jsonl"))
148
+ .map((f) => path.join(sessionsDir, f))
149
+ .map((f) => ({ f, mtime: statMtime(f) }))
150
+ .sort((a, b) => b.mtime - a.mtime);
151
+ let matched: string | undefined;
152
+ for (const candidate of candidates.slice(0, 25)) {
153
+ try {
154
+ const content = scanRead(candidate.f).toString("utf8").toLowerCase();
155
+ if (needles.some((needle) => content.includes(needle))) { matched = candidate.f; break; }
156
+ } catch { /* unreadable file — skip */ }
157
+ }
158
+ if (!matched) {
159
+ return {
160
+ ok: false,
161
+ lines: [],
162
+ detail: `no session file in ${sessionsDir} matches this child (searched ${candidates.length} transcripts for: ${needles.map((n) => `"${truncate(n, 24)}"`).join(", ") || "any needle"}) — the child may not persist a session, or it lives under another working directory`,
163
+ };
164
+ }
165
+ try {
166
+ const raw = readFile(matched).toString("utf8").split("\n").filter(Boolean);
167
+ const formatted = raw.map(formatTranscriptEntry).filter(Boolean) as string[];
168
+ return { ok: true, lines: formatted.slice(-wantLines), detail: `${matched} (last ${Math.min(wantLines, formatted.length)} of ${formatted.length})` };
169
+ } catch (error) {
170
+ return { ok: false, lines: [], detail: `cannot read ${matched}: ${error instanceof Error ? error.message.slice(0, 120) : String(error)}` };
171
+ }
172
+ }
173
+
174
+ /** Tolerant pi-session JSONL → `[role] text` line. Unparseable lines are
175
+ * truncated raw so forensic value survives shape drift. v0.35.45: output is
176
+ * ANSI/control-char sanitized — a hostile child transcript must not be able
177
+ * to emit terminal escape sequences through ctx.ui.notify. */
178
+ export function formatTranscriptEntry(line: string): string | undefined {
179
+ const trimmed = line.trim();
180
+ if (!trimmed) return undefined;
181
+ try {
182
+ const entry = JSON.parse(trimmed) as {
183
+ type?: string;
184
+ role?: string;
185
+ message?: { role?: string; content?: unknown };
186
+ content?: unknown;
187
+ };
188
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return `[raw] ${truncate(sanitizeDisplayText(trimmed), 120)}`;
189
+ const role = entry.message?.role ?? entry.role ?? entry.type ?? "?";
190
+ const content = entry.message?.content ?? entry.content;
191
+ const text = extractText(content);
192
+ if (!text) return undefined;
193
+ return `[${role}] ${truncate(sanitizeDisplayText(text).replace(/\s+/g, " ").trim(), 160)}`;
194
+ } catch {
195
+ return `[raw] ${truncate(sanitizeDisplayText(trimmed), 120)}`;
196
+ }
197
+ }
198
+
199
+ function extractText(content: unknown): string {
200
+ if (typeof content === "string") return content;
201
+ if (Array.isArray(content)) {
202
+ return content
203
+ .map((block) => typeof block === "string"
204
+ ? block
205
+ : typeof block === "object" && block !== null && typeof (block as { text?: unknown }).text === "string"
206
+ ? (block as { text: string }).text
207
+ : "")
208
+ .join(" ");
209
+ }
210
+ return "";
211
+ }
@@ -8,6 +8,14 @@
8
8
  import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
+ import { renderAgentsPanel, tailChildTranscript } from "./goal-agents-panel.js";
12
+
13
+ /** Child pi sessions live under the shared session store, munged by cwd
14
+ * (verified layout: ~/.pi/agent/sessions/--home-user-proj--/*.jsonl). */
15
+ function childSessionsDir(cwd: string): string {
16
+ const munged = "--" + cwd.replaceAll("/", "-") + "--";
17
+ return path.join(os.homedir(), ".pi", "agent", "sessions", munged);
18
+ }
11
19
  import type { ExtensionContext, ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
20
  import { state, replaceState } from "./goal-state.js";
13
21
  import {
@@ -16,9 +24,11 @@ import {
16
24
  formatListDepth, goalArgsNeedDrafting, ledgerPath, newGoalId, nowIso, parseListImport, parseListItemDeclaration,
17
25
  assignQueueOrder, compareQueueItems, readAuditLog, readQueueFromDisk, routeGoalArgs, routeListText, sanitizeDisplayText, sanitizeProviderAuditReport, statusLabel,
18
26
  writeQueueItemFile, type ModeCommand, type State, LIST_MUTATING_SUBCOMMANDS, SETTINGS_MUTATING_ACTIONS,
27
+ clearLoadHold, stateRootPending,
19
28
  } from "./goal-loop-core.js";
20
29
  import { clearDispatchRecord, dispatchRecordExists } from "./goal-loop-dispatch.js";
21
30
  import type { AuditDisplayProgress } from "./goal-loop-display.js";
31
+ import { fmtElapsed } from "./goal-loop-display.js";
22
32
  import { AUDIT_FINDINGS_REL, HELD_ON_RESTORE, LOOP_AUDIT_MARKER, listAuditCollectTarget, projectAuditTarget } from "./goal-loop-forever.js";
23
33
  import { ProjectRollup, discoverGllaProjects, filterPremature, formatRollupJson, formatRollupTable, parseLedgerEntries, rollupProject } from "./goal-loop-stats.js";
24
34
  import { OVERRIDABLE_AGENT_TYPES, resolveEffectiveSubagentModel } from "./goal-loop-subagents.js";
@@ -30,6 +40,7 @@ import { cmdLoop, clearLoopTimer, finishLoopGit, isLoopActive, scheduleLoopTick
30
40
  import { chooseObjectiveConflict, liveObjectives } from "./goal-objective-conflict.js";
31
41
  import { formatGllaVersion } from "./glla-version.js";
32
42
  import { cancelDetachedGoalCompletionAuditor } from "./goal-loop-auditor-process.js";
43
+ import { releaseAuditorSurface } from "./loops/goal-auditor-surface.js";
33
44
 
34
45
  export interface CommandFlags {
35
46
  get draftingTarget(): "goal" | "list" | "loop" | null; set draftingTarget(v: "goal" | "list" | "loop" | null);
@@ -55,7 +66,7 @@ export interface CommandDeps {
55
66
  setGoal: (goal: Goal, ctx: ExtensionContext, via?: string) => boolean;
56
67
  archiveCurrentGoal: (ctx: ExtensionContext, status: Status, stopReason?: string) => boolean;
57
68
  healGoalPolicy: (ctx: ExtensionContext) => boolean;
58
- startDrafting: (ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string) => Promise<boolean>;
69
+ startDrafting: (ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string, depth?: "normal" | "plan") => Promise<boolean>;
59
70
  warnIfStaleAtEntry: (ctx: ExtensionContext, what: string) => boolean;
60
71
  queuePendingListOperation: (ctx: ExtensionContext, args: string) => boolean;
61
72
  freshCtx: () => ExtensionContext | null;
@@ -87,9 +98,13 @@ export interface CommandDeps {
87
98
  goalNoun: () => string;
88
99
  displaySlice: (s: string, max: number) => string;
89
100
  shortObj: (s: string) => string;
101
+ /** v0.35.29 (issue #15): read-only tracked-subagent snapshot for /glla agents.
102
+ * Injected because goal-heartbeat must not be imported from here (cycle). */
103
+ agentsSnapshot: () => { agents: import("./goal-agents-panel.js").AgentsPanelRow[]; managerAvailable: boolean };
90
104
  }
91
105
 
92
106
  let deps: CommandDeps;
107
+ let agentsSnapshot: CommandDeps["agentsSnapshot"];
93
108
  let flags: CommandFlags;
94
109
  let listQueue: CommandDeps["listQueue"], notifyExternal: CommandDeps["notifyExternal"], persistState: CommandDeps["persistState"], updateGoal: CommandDeps["updateGoal"], setGoal: CommandDeps["setGoal"],
95
110
  archiveCurrentGoal: CommandDeps["archiveCurrentGoal"], healGoalPolicy: CommandDeps["healGoalPolicy"], startDrafting: CommandDeps["startDrafting"], warnIfStaleAtEntry: CommandDeps["warnIfStaleAtEntry"], queuePendingListOperation: CommandDeps["queuePendingListOperation"], freshCtx: CommandDeps["freshCtx"],
@@ -108,6 +123,7 @@ export function createGoalCommands(d: CommandDeps): void {
108
123
  releaseInitialSessionLoadBarrier = d.releaseInitialSessionLoadBarrier; resolveCarryover = d.resolveCarryover; safeSteerUser = d.safeSteerUser; scheduleContinuation = d.scheduleContinuation; scheduleSessionTimeout = d.scheduleSessionTimeout;
109
124
  createGoal = d.createGoal; fireReviewer = d.fireReviewer; openSettingsUI = d.openSettingsUI; manuallyResumeMainModelRecovery = d.manuallyResumeMainModelRecovery; activeGoalCommand = d.activeGoalCommand;
110
125
  activeGoalStatusCommand = d.activeGoalStatusCommand; activeGoalSurfaceCommand = d.activeGoalSurfaceCommand; goalNoun = d.goalNoun; displaySlice = d.displaySlice; shortObj = d.shortObj;
126
+ agentsSnapshot = d.agentsSnapshot;
111
127
  }
112
128
 
113
129
  /* Moved bodies (bands b1 + b3 from goal.ts). */
@@ -188,6 +204,18 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
188
204
  }
189
205
  return cmdSet(route.rest, ctx, true);
190
206
  }
207
+ // v0.35.33: /goal plan [seed] — the EXTENDED DRAFT. Same trust machinery
208
+ // as a regular draft (Confirm card gates activation), deeper process:
209
+ // research-first, multi-round interview, structured expanded objective.
210
+ // For greenfield/megaplan work where the standard 5–7-question interview
211
+ // is too shallow. Regular /goal stays the fast path.
212
+ if (route.name === "plan") {
213
+ // Same entry gate as cmdSet: a stale MAIN cannot deliver the seed; do
214
+ // not leave an orphaned drafting gate behind a doomed process.
215
+ if (warnIfStaleAtEntry(ctx, "/goal")) return;
216
+ await startDrafting(ctx, "goal", route.rest || undefined, "plan");
217
+ return;
218
+ }
191
219
  }
192
220
  return cmdSet(route.kind === "set" ? route.text : "", ctx);
193
221
  }
@@ -331,9 +359,28 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
331
359
 
332
360
  async function cmdResume(ctx: ExtensionContext): Promise<void> {
333
361
  releaseInitialSessionLoadBarrier();
362
+ // v0.35.23 (note.md Next #2): an explicit resume is exactly the decision
363
+ // the load hold waits for — release it before re-arming automation, or
364
+ // the scheduleContinuation below would be a frozen no-op.
365
+ if (clearLoadHold(state)) {
366
+ persistState(ctx);
367
+ appendLedger(ctx.cwd, "load_hold_released", { via: "goal-resume" });
368
+ ctx.ui.notify("Load hold released — automation is live again.", "info");
369
+ }
334
370
  const resumeCommand = activeGoalCommand("resume");
335
- if (manuallyResumeMainModelRecovery(ctx)) return;
336
- if (state.mainModelRecovery?.retryAt || state.mainModelRecovery?.pendingModelSwitch) {
371
+ // Main-model recovery returns before the normal paused-goal admission path.
372
+ // Probe the same stale/foreign boundary first so an admitted recovery resume
373
+ // can release the auditor surface, while a rejected one falls through to the
374
+ // existing stale-resume recovery marker without exposing old context.
375
+ const recoveryStaleEntry = state.mainModelRecovery
376
+ ? warnIfStaleAtEntry(ctx, resumeCommand)
377
+ : false;
378
+ if (!recoveryStaleEntry && manuallyResumeMainModelRecovery(ctx)) {
379
+ releaseAuditorSurface();
380
+ return;
381
+ }
382
+ if (!recoveryStaleEntry && (state.mainModelRecovery?.retryAt || state.mainModelRecovery?.pendingModelSwitch)) {
383
+ releaseAuditorSurface();
337
384
  clearMainModelRecoveryTimer();
338
385
  flags.continuationDispatchStoodDown = false;
339
386
  // v0.34.92: the chat-prompt re-arm was removed; recovery is timer-driven.
@@ -341,7 +388,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
341
388
  void probeMainModelRecovery(ctx);
342
389
  return;
343
390
  }
344
- if (state.mainModelRecovery?.primaryProbeAt || state.mainModelRecovery?.primaryProbeInFlight) {
391
+ if (!recoveryStaleEntry && (state.mainModelRecovery?.primaryProbeAt || state.mainModelRecovery?.primaryProbeInFlight)) {
392
+ releaseAuditorSurface();
345
393
  clearMainModelRecoveryTimer();
346
394
  flags.continuationDispatchStoodDown = false;
347
395
  ctx.ui.notify("Probing the preferred primary now — the current fallback remains available if the primary is still unhealthy.", "info");
@@ -357,6 +405,21 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
357
405
  ctx.ui.notify("A loop is active — one active thing at a time. /loop stop it first, then resume the goal.", "warning");
358
406
  return;
359
407
  }
408
+ // An ACTIVE-but-idle goal still needs the entry admission probe: a stale
409
+ // command must not release the auditor surface before its schedule is
410
+ // rejected by the stale/foreign session boundary.
411
+ const staleEntry = warnIfStaleAtEntry(ctx, resumeCommand);
412
+ if (staleEntry) return;
413
+ releaseAuditorSurface();
414
+ if (state.goal.repairTarget?.replanPromptedAt) {
415
+ const target = state.goal.repairTarget;
416
+ updateGoal({ repairTarget: { ...target, replanPromptedAt: undefined } }, ctx);
417
+ appendLedger(ctx.cwd, "faulty_objective_replan_turn_reset", {
418
+ goalId: state.goal.id,
419
+ targetId: target.id,
420
+ via: "manual-resume",
421
+ });
422
+ }
360
423
  appendLedger(ctx.cwd, "resume_rekick", { goalId: state.goal.id, policy: state.goal.policy, via: resumeCommand });
361
424
  if (state.goal.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx); // v0.34.7: same marker law here
362
425
  ctx.ui.notify(
@@ -381,6 +444,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
381
444
  }
382
445
  const staleEntry = warnIfStaleAtEntry(ctx, resumeCommand);
383
446
  if (staleEntry) return;
447
+ releaseAuditorSurface();
384
448
  markCompletionAuditRecoveryPending(ctx, "manual-resume");
385
449
  flags.completionAuditRecoveryArmed = true;
386
450
  ctx.ui.notify("Resuming the stored completion claim — starting a detached auditor (no agent turn needed).", "info");
@@ -437,8 +501,9 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
437
501
  // manual resume exactly as by an automatic one. (staleEntry still re-marks
438
502
  // below — a resume inside a stale session is a NEW interrupt.)
439
503
  const storedCompletion = state.goal.pendingCompletion;
440
- updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, interruptedAt: undefined, interruptedReason: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
504
+ updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, interruptedAt: undefined, interruptedReason: undefined, autoResumedAt: undefined, autoResumedEvent: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
441
505
  if (staleEntry) return;
506
+ releaseAuditorSurface();
442
507
  // A stored completion claim is a direct-audit resume, not an agent turn.
443
508
  // Keeping the claim while merely scheduling a continuation left manual
444
509
  // pause/resume with an ACTIVE goal that no timer would ever consume.
@@ -462,6 +527,10 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
462
527
  }
463
528
 
464
529
  async function cmdCancel(ctx: ExtensionContext): Promise<void> {
530
+ if (stateRootPending()) {
531
+ ctx.ui.notify("Cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
532
+ return;
533
+ }
465
534
  if (!state.goal) {
466
535
  // v0.28.14: users reach for /goal cancel to kill a LOOP (no goal
467
536
  // active) — point at the right verb instead of doing nothing silently.
@@ -1074,7 +1143,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1074
1143
  return;
1075
1144
  }
1076
1145
 
1077
- if (sub === "pause") {
1146
+ if (sub === "pause" && !rest) {
1078
1147
  if (!state.goal || state.goal.policy !== "list") {
1079
1148
  ctx.ui.notify(
1080
1149
  state.goal ? "The active work is a standalone goal — /goal pause to pause it." : "No active list item to pause. /list show to see the list.",
@@ -1086,7 +1155,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1086
1155
  return;
1087
1156
  }
1088
1157
 
1089
- if (sub === "resume") {
1158
+ if (sub === "resume" && !rest) {
1090
1159
  // Resume the list's head. The head activates AS the active goal, so this
1091
1160
  // is the same motion as /goal resume — named for the surface the user is
1092
1161
  // looking at (v0.22.7: "we would just unpause, and that is next").
@@ -1219,7 +1288,22 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1219
1288
  return;
1220
1289
  }
1221
1290
 
1222
- if (sub === "clear") {
1291
+ // v0.35.33: /list plan [seed] — the extended draft for LIST items: deep
1292
+ // research + multi-round interview, then the agent proposes items[] through
1293
+ // the SAME one-Confirm batch path (never per-item activation). Intended for
1294
+ // greenfield/megaplan decomposition where the standard interview is too
1295
+ // shallow. Plain /list add keeps its fast behavior unchanged.
1296
+ if (sub === "plan") {
1297
+ // staleEntry was already gated by LIST_MUTATING_SUBCOMMANDS above.
1298
+ await startDrafting(ctx, "list", rest || undefined, "plan");
1299
+ return;
1300
+ }
1301
+
1302
+ if (sub === "clear" && !rest) {
1303
+ if (stateRootPending()) {
1304
+ ctx.ui.notify("List clear deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
1305
+ return;
1306
+ }
1223
1307
  // v0.34.61: delete the sidecars of every removed item BEFORE clearing
1224
1308
  // state. The /list disk-recovery fallback scans .pi-glla/goals/*.queue.json
1225
1309
  // when memQueue is empty; without this, a /list clear followed by a
@@ -1237,7 +1321,11 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1237
1321
  // v0.24.1: ONE verb for "stop this whole list" — aborts the active item
1238
1322
  // when it's list-sourced AND drops the waiting items. Before this the user
1239
1323
  // had to know to combine /goal cancel + /list clear.
1240
- if (sub === "cancel") {
1324
+ if (sub === "cancel" && !rest) {
1325
+ if (stateRootPending()) {
1326
+ ctx.ui.notify("List cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
1327
+ return;
1328
+ }
1241
1329
  const waiting = listQueue().length;
1242
1330
  const activeIsListItem = state.goal?.policy === "list" && (state.goal.status === "active" || state.goal.status === "paused" || state.goal.status === "auditing");
1243
1331
  if (waiting === 0 && !activeIsListItem) {
@@ -1297,6 +1385,10 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1297
1385
  }
1298
1386
  if (!activateNextListItem(ctx, n, { explicit: true })) {
1299
1387
  ctx.ui.notify(listQueue().length === 0 ? "List is empty — nothing to activate." : `No item #${n} (list has ${listQueue().length}).`, "info");
1388
+ } else {
1389
+ // Explicit list activation is also continuation consent, even when the
1390
+ // activated item is a fresh queue head rather than the parked goal.
1391
+ releaseAuditorSurface();
1300
1392
  }
1301
1393
  return;
1302
1394
  }
@@ -1737,6 +1829,10 @@ function cmdSwitchlog(args: string, ctx: ExtensionContext): void {
1737
1829
  */
1738
1830
  async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise<void> {
1739
1831
  if (!entryChecked && warnIfStaleAtEntry(ctx, "/glla wipe")) return;
1832
+ if (stateRootPending()) {
1833
+ ctx.ui.notify("Wipe deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
1834
+ return;
1835
+ }
1740
1836
  const g = state.goal;
1741
1837
  const live = g && (g.status === "active" || g.status === "paused" || g.status === "auditing");
1742
1838
  const memoryQueue = listQueue();
@@ -1784,8 +1880,11 @@ async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise
1784
1880
  // Do not abort here: wipe still has to clear queue sidecars and loop
1785
1881
  // state. The old early abort made a second /glla wipe appear necessary.
1786
1882
  if (!archiveCurrentGoal(ctx, "aborted", "user wipe (/glla wipe)")) return;
1883
+ // v0.35.30: the archive's closeArchivedSlot just recorded lastOutcome;
1884
+ // a wipe means CLEAN SLATE — the ✓/▪ line must not survive it.
1885
+ replaceState({ ...state, lastOutcome: undefined });
1787
1886
  } else if (g) {
1788
- replaceState({ ...state, goal: null });
1887
+ replaceState({ ...state, goal: null, lastOutcome: undefined });
1789
1888
  }
1790
1889
  // Clear provider-recovery and continuation artifacts only after a live-goal
1791
1890
  // archive succeeds. An archive failure must leave all resumable work intact.
@@ -1837,12 +1936,56 @@ async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise
1837
1936
  * today) → the v0.28.23 decision-picker pattern. Verbs whose semantics
1838
1937
  * genuinely differ per type (tweak/finish/next/decide/refine) stay typed.
1839
1938
  */
1939
+ /**
1940
+ * v0.35.15: `/glla pause` — freeze the SUPERVISOR, not the work.
1941
+ *
1942
+ * Broad by design (user-confirmed 2026-08-21): every automatic side-effect
1943
+ * stops — heartbeat re-arms/stale probes/zombie cleanup, recovery probes,
1944
+ * auto-resume, auto-continuation dispatch, loop ticks, and the proactive
1945
+ * auditor quiet-phase notification. The active goal/list item/loop state is
1946
+ * untouched and any detached worker keeps running; only the automatic
1947
+ * machinery around it freezes. Manual user commands still work. Persisted
1948
+ * via `supervisorPausedAt`, so a session restart cannot silently re-arm
1949
+ * machinery the user explicitly stopped. Pausing while a recovery claim is
1950
+ * parked keeps the claim durable on disk — nothing is discarded.
1951
+ */
1952
+ async function cmdGllaPause(ctx: ExtensionContext): Promise<void> {
1953
+ if (warnIfStaleAtEntry(ctx, "/glla pause")) return;
1954
+ releaseInitialSessionLoadBarrier();
1955
+ const already = typeof state.supervisorPausedAt === "number";
1956
+ const pausedAtMs = Date.now();
1957
+ replaceState({ ...state, supervisorPausedAt: pausedAtMs });
1958
+ persistState(ctx);
1959
+ appendLedger(ctx.cwd, "supervisor_pause", already ? { repeat: true } : {});
1960
+ // An armed continuation/recovery timer may already be scheduled; every
1961
+ // dispatch point checks supervisorPaused() so stale timers become no-ops.
1962
+ ctx.ui.notify(already
1963
+ ? "Supervisor is already paused — all automatic machinery stays frozen (active work untouched). /glla resume to unfreeze."
1964
+ : "Supervisor PAUSED — heartbeat re-arms, recovery probes, auto-resume, continuation dispatch, loop ticks, and auditor quiet notifies are frozen. The active goal/list item/loop and detached workers keep running. /glla resume to unfreeze.",
1965
+ "warning");
1966
+ }
1967
+
1840
1968
  async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
1841
1969
  // v0.29.12: a zombie instance (handle dead after session replacement)
1842
1970
  // used to answer "Nothing to resume" — the resume path must name the
1843
1971
  // real recovery (/reload rebuilds extensions in place), not mislead.
1844
1972
  if (warnIfStaleAtEntry(ctx, "/glla resume")) return;
1973
+ // /glla resume is the broad resume surface and therefore carries the same
1974
+ // consent semantics as /goal resume or /list resume.
1975
+ releaseAuditorSurface();
1845
1976
  releaseInitialSessionLoadBarrier();
1977
+ // v0.35.15: clearing a supervisor pause is resume's first job — the flag
1978
+ // outlives sessions, so an explicit /glla resume must always unfreeze the
1979
+ // machinery even when there is nothing else to resume afterwards.
1980
+ let clearedSupervisorPause = false;
1981
+ if (typeof state.supervisorPausedAt === "number") {
1982
+ const frozenMs = Date.now() - state.supervisorPausedAt;
1983
+ delete (state as { supervisorPausedAt?: number }).supervisorPausedAt;
1984
+ persistState(ctx);
1985
+ clearedSupervisorPause = true;
1986
+ appendLedger(ctx.cwd, "supervisor_resume", { frozenMs });
1987
+ ctx.ui.notify(`Supervisor RESUMED after ${fmtElapsed(frozenMs)} — heartbeat re-arms, recovery probes, auto-resume, continuation dispatch, and auditor quiet notifies are live again.`, "info");
1988
+ }
1846
1989
  if (manuallyResumeMainModelRecovery(ctx)) return;
1847
1990
  if (state.mainModelRecovery?.retryAt || state.mainModelRecovery?.pendingModelSwitch) {
1848
1991
  clearMainModelRecoveryTimer();
@@ -1929,6 +2072,11 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
1929
2072
  scheduleLoopTick(ctx);
1930
2073
  return;
1931
2074
  }
2075
+ if (clearedSupervisorPause) {
2076
+ // The pause was the only thing being resumed — do not follow it with a
2077
+ // misleading "Nothing to resume".
2078
+ return;
2079
+ }
1932
2080
  ctx.ui.notify("Nothing to resume — no paused goal/list-item, no held loop. /goal, /list, or /loop to start something.", "info");
1933
2081
  }
1934
2082
 
@@ -1945,6 +2093,10 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
1945
2093
  * queue after aborting only its current item.
1946
2094
  */
1947
2095
  async function cmdGllaCancel(ctx: ExtensionContext): Promise<void> {
2096
+ if (stateRootPending()) {
2097
+ ctx.ui.notify("Cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
2098
+ return;
2099
+ }
1948
2100
  // A running loop is itself the active objective. Stop it before looking at
1949
2101
  // an unrelated waiting list; otherwise `/glla cancel` can silently drop the
1950
2102
  // queue while leaving the loop running.
@@ -2013,6 +2165,55 @@ function cmdGllaVersion(ctx: ExtensionContext): void {
2013
2165
  ctx.ui.notify(formatGllaVersion(), "info");
2014
2166
  }
2015
2167
 
2168
+ // v0.35.29 (issue #15): tracked-subagent visibility — snapshot panel plus a
2169
+ // read-only child transcript tail (design: docs/DESIGN-subagent-visibility.md).
2170
+ function cmdAgents(args: string, ctx: ExtensionContext): void {
2171
+ const { agents, managerAvailable } = agentsSnapshot();
2172
+ const tailMatch = args.match(/--tail\s+(\S+)/);
2173
+ if (tailMatch) {
2174
+ const id = tailMatch[1]!;
2175
+ const row = agents.find((a) => a.recordId === id || a.recordId.startsWith(id));
2176
+ if (!row) {
2177
+ ctx.ui.notify(`No tracked subagent matches "${id}". /glla agents lists the current ids.`, "warning");
2178
+ return;
2179
+ }
2180
+ const linesMatch = args.match(/--lines\s+(\d+)/);
2181
+ // v0.35.45 (audit finding): the candidate scan reads a bounded TAIL of
2182
+ // each transcript instead of up to 25 FULL files synchronously on the
2183
+ // main thread. The single matched file still gets a full read so the
2184
+ // "last N of M" detail stays honest.
2185
+ const readTailAware = (file: string, maxBytes?: number): Buffer => {
2186
+ if (maxBytes === undefined) return fs.readFileSync(file);
2187
+ try {
2188
+ const size = fs.statSync(file).size;
2189
+ if (size <= maxBytes) return fs.readFileSync(file);
2190
+ const fd = fs.openSync(file, "r");
2191
+ try {
2192
+ const buf = Buffer.alloc(maxBytes);
2193
+ const bytesRead = fs.readSync(fd, buf, 0, maxBytes, size - maxBytes);
2194
+ return buf.subarray(0, bytesRead);
2195
+ } finally {
2196
+ fs.closeSync(fd);
2197
+ }
2198
+ } catch {
2199
+ return fs.readFileSync(file);
2200
+ }
2201
+ };
2202
+ const result = tailChildTranscript(childSessionsDir(ctx.cwd), row, {
2203
+ lines: linesMatch ? Number(linesMatch[1]) : undefined,
2204
+ readFile: readTailAware,
2205
+ listDir: (dir) => fs.readdirSync(dir),
2206
+ statMtime: (file) => { try { return fs.statSync(file).mtimeMs; } catch { return 0; } },
2207
+ });
2208
+ const header = result.ok
2209
+ ? `transcript tail — ${row.agentType ?? row.recordId} (${result.detail})`
2210
+ : `no transcript tail for ${row.agentType ?? row.recordId}`;
2211
+ ctx.ui.notify([header, ...result.lines, ...(result.ok ? [] : [result.detail])].join("\n"), result.ok ? "info" : "warning");
2212
+ return;
2213
+ }
2214
+ ctx.ui.notify(`glla agents${managerAvailable ? "" : " (pi-subagents manager registry not present — event evidence only)"}\n${renderAgentsPanel(agents, Date.now(), managerAvailable).join("\n")}`, "info");
2215
+ }
2216
+
2016
2217
  // v0.29.8: /glla status — the unified "what's running" surface (user: "we
2017
2218
  // need to type goal status [to check], so that command at least is missing
2018
2219
  // for checking on whatever active process we have"). Read-only aggregate of
@@ -2020,6 +2221,11 @@ function cmdGllaVersion(ctx: ExtensionContext): void {
2020
2221
  // to the deep surfaces.
2021
2222
  function cmdGllaStatus(ctx: ExtensionContext): void {
2022
2223
  const lines: string[] = [];
2224
+ // v0.35.15: name a frozen supervisor FIRST — it changes how every other
2225
+ // line reads (nothing automatic will fire while this is set).
2226
+ if (typeof state.supervisorPausedAt === "number") {
2227
+ lines.push(`supervisor: ⏸ PAUSED since ${new Date(state.supervisorPausedAt).toISOString()} — re-arms/recovery/dispatch frozen — /glla resume`);
2228
+ }
2023
2229
  const g = state.goal;
2024
2230
  if (g) {
2025
2231
  const tok = (g.usage?.tokensUsed ?? 0) > 0 ? ` · ${g.usage!.tokensUsed} tok` : "";
@@ -2078,6 +2284,11 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2078
2284
  cmdAudits(trimmed.slice("audits".length).trim(), ctx);
2079
2285
  return;
2080
2286
  }
2287
+ // v0.35.29 (issue #15): tracked-subagent panel — read-only, stale-safe.
2288
+ if (/^agents(?:\s|$)/.test(trimmed)) {
2289
+ cmdAgents(trimmed.slice("agents".length).trim(), ctx);
2290
+ return;
2291
+ }
2081
2292
  if (/^status(?:\s|$)/.test(trimmed)) {
2082
2293
  cmdGllaStatus(ctx);
2083
2294
  return;
@@ -2127,6 +2338,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2127
2338
  await cmdGllaResume(ctx);
2128
2339
  return;
2129
2340
  }
2341
+ if (/^pause(?:\s|$)/.test(trimmed)) {
2342
+ await cmdGllaPause(ctx);
2343
+ return;
2344
+ }
2130
2345
  if (/^cancel(?:\s|$)/.test(trimmed)) {
2131
2346
  await cmdGllaCancel(ctx);
2132
2347
  return;
@@ -2198,6 +2413,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2198
2413
  fmt("aggressiveMode", "aggressiveMode"),
2199
2414
  fmt("stuckMaxInterventions", "stuckMaxInterventions"),
2200
2415
  fmt("stallEscalationRefires", "stallEscalation"),
2416
+ fmt("subagentHangEscalationMinutes", "subagentHangActionMinutes"),
2201
2417
  fmt("wedgeAlertMinutes", "wedgeAlert"),
2202
2418
  fmt("stallShortWords", "stallShortWords"),
2203
2419
  fmt("stallSimilarityThreshold", "stallSimilarityThreshold"),