pi-goal-list-loop-audit 0.23.7 → 0.23.8

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
@@ -44,7 +44,7 @@ Four top-level commands, that's all:
44
44
  /list fix the login bug, add dark mode, write docs # dump it — the agent shapes it into items, one Confirm
45
45
  /list plan.md # file detected → bulk import, one Confirm (sisyphus/Ralph style)
46
46
  /list <paste a checklist> # multi-line paste → same batch flow
47
- /list "fix the flaky test. Done when: npm test green" # explicit contract → queues directly, no interview
47
+ /list "fix the flaky test. Done when: npm test green" # explicit contract → added directly, no interview
48
48
  /list # show the list (add/import are optional no-op aliases — detection routes everything)
49
49
 
50
50
  (Or just say it: "queue these 10 things…" — the agent manages the list too.)
@@ -53,10 +53,10 @@ Four top-level commands, that's all:
53
53
  `/list next <n>` or the agent's `list_activate` tool picks any item — with
54
54
  subagents, what gets worked next is a choice, not a position. Numbering always
55
55
  matches `/list show`.
56
- /list # show active + queue
56
+ /list # show active + waiting items
57
57
  /list next # skip current, activate next
58
- /list remove <n> # drop item n from the queue
59
- /list clear # empty the queue
58
+ /list remove <n> # drop item n from the list
59
+ /list clear # empty the list
60
60
  /loop # draft the loop (agent grills; measure is test-run before you confirm)
61
61
  /loop start "keep polishing the UI" # infinite metricless loop (v0.23.6): no plateau, no cap — ends at time=/tokens= or /loop stop
62
62
  /loop start "reduce TODOs" measure="grep -c TODO src.txt | head -1" direction=min
@@ -91,7 +91,7 @@ sisyphus-style plan file (checklists, bullets, numbered, plain lines) imports
91
91
  as-is — headings become nothing, items become goals. And the drafter itself
92
92
  batches: asking for "these 50 tasks" in a `/list` drafting session produces
93
93
  ONE confirmed batch, not 50 dialogs.
94
- Note: every queue item is audited individually, so at hundreds of items the
94
+ Note: every list item is audited individually, so at hundreds of items the
95
95
  audit cost per item is the thing to think about.
96
96
 
97
97
  **Drafting is the default for long-running things.** `/goal` and
@@ -144,7 +144,7 @@ redirect you to `/goal`.
144
144
  | Loop | Command | Status |
145
145
  |---|---|---|
146
146
  | 1. Single ordered goal | `/goal "<objective>"` | **shipped v0.1.0** |
147
- | 2. Queue of goals | `/list [show\|next\|remove\|clear]` | **shipped v0.2.0** |
147
+ | 2. List of goals (a pool, not a FIFO) | `/list [show\|next\|remove\|clear]` | **shipped v0.2.0** |
148
148
  | 3. Metric-driven process loop | `/loop start\|status\|stop` | **shipped v0.3.0** |
149
149
 
150
150
  Each loop is a different policy class on the same status machine.
@@ -186,6 +186,8 @@ No external watchdog plugin needed.
186
186
  /glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
187
187
  /glla tokenlimit=0 # explicitly no cap (the default)
188
188
  /glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
189
+ /glla autoresume=on # held goals/loops auto-resume in fresh sessions (unattended rigs)
190
+ /glla autoaccept=on # drafts ACTIVATE without the Confirm dialog (unattended rigs)
189
191
  /glla project tokenlimit=500 # rare per-project override
190
192
  ```
191
193
 
@@ -195,6 +197,29 @@ auditor can't auth it — you're told once (info level) with the fix:
195
197
  `/glla model=provider/id`, set once, rarely touched again. The plugin never
196
198
  picks a model itself. Thinking follows the session too (floor `high`).
197
199
 
200
+ `autoaccept=on` skips BOTH the Confirm dialog and the drafting interview
201
+ floor — every `propose_*` draft (goal, list batch, loop, task list)
202
+ activates the moment the agent proposes it, with a notification and a
203
+ `draft_autoaccepted` ledger entry (auto-accept is never silent). The seed
204
+ carries the intent. Pair with `autoresume=on` for fully unattended rigs.
205
+
206
+ ## Subagents (`@tintinweb/pi-subagents`)
207
+
208
+ Subagent sessions bind extensions too, so glla loads there — by design the
209
+ **main session owns the goal/loop/list; subagents are workers** (v0.23.8):
210
+
211
+ - Read-only agents (Explore, Plan) get no glla tools (pi-subagents gates
212
+ them); general-purpose agents see them but state-mutating calls
213
+ (`complete_goal`, `propose_*`, `list_add`, `pause_goal`, …) are refused
214
+ with "report back to the main agent".
215
+ - A subagent session never clobbers the loop's session handle, never runs
216
+ the restore gate, and never drives continuation — so the heartbeat,
217
+ wedge alert, and auto-resume machinery always act on the main session.
218
+ (pi hands a fresh ctx wrapper per event; `ctx.sessionManager` identity
219
+ is the discriminator.)
220
+ - Subagent tool activity counts as activity for the wedge clock — a long
221
+ subagent run is work, not a hang.
222
+
198
223
  ## Token guard
199
224
 
200
225
  Every goal tracks real token usage; crossing the budget pauses the goal.
@@ -670,3 +670,21 @@ export function extractVerificationContract(raw: string): { objective: string; v
670
670
  }
671
671
  return { objective, verificationContract };
672
672
  }
673
+
674
+ /**
675
+ * v0.23.8: subagent-session ownership. pi-subagents binds extensions in
676
+ * subagent sessions too, so glla's session_start/handlers fire there with
677
+ * the same module state. The MAIN session owns the goal/loop; subagent
678
+ * sessions are workers — they must never clobber the loop's ctx handle
679
+ * (a headless subagent ctx would silently kill the heartbeat/wedge
680
+ * machinery), never receive continuation injection, and never mutate goal
681
+ * state. pi hands a FRESH ctx wrapper per event (verified in
682
+ * dist/core/extensions/runner.js — createContext() per emit), so object
683
+ * identity is useless; ctx.sessionManager is the stable per-session
684
+ * discriminator (each subagent gets its own SessionManager).
685
+ */
686
+ export type OwnerClaim = "claim" | "refresh" | "foreign";
687
+ export function classifySessionCtx(ownerSession: unknown, ownerLive: boolean, sessionManager: unknown): OwnerClaim {
688
+ if (!ownerSession || !ownerLive) return "claim";
689
+ return sessionManager === ownerSession ? "refresh" : "foreign";
690
+ }
@@ -56,6 +56,7 @@ import {
56
56
  normalizeDraftContract,
57
57
  draftContractItemCount,
58
58
  extractVerificationContract,
59
+ classifySessionCtx,
59
60
  readState,
60
61
  renderGoalMarkdown,
61
62
  shouldAutoResumeOnSessionStart,
@@ -105,11 +106,37 @@ let extensionApi: ExtensionAPI | null = null;
105
106
  // pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
106
107
  // so timers must never capture a ctx — they read lastCtx at fire time.
107
108
  let lastCtx: ExtensionContext | null = null;
109
+ // v0.23.8: the session that OWNS the loop (its sessionManager). Subagent
110
+ // sessions (pi-subagents binds extensions there too) fire our handlers
111
+ // with their own ctx — they must never take over lastCtx (a headless
112
+ // subagent ctx would silently kill the heartbeat/wedge machinery).
113
+ let ownerSession: unknown = null;
108
114
 
109
115
  function rememberCtx(ctx: ExtensionContext): void {
116
+ let ownerLive = false;
117
+ if (ownerSession && lastCtx) {
118
+ try { lastCtx.isIdle(); ownerLive = true; } catch { /* owner went stale (session replaced) */ }
119
+ }
120
+ const claim = classifySessionCtx(ownerSession, ownerLive, ctx.sessionManager);
121
+ if (claim === "foreign") return;
122
+ ownerSession = ctx.sessionManager;
110
123
  lastCtx = ctx;
111
124
  }
112
125
 
126
+ /** True when ctx belongs to a subagent/foreign session, not the loop owner. */
127
+ function isForeignCtx(ctx: ExtensionContext): boolean {
128
+ return ownerSession !== null && ctx.sessionManager !== ownerSession;
129
+ }
130
+
131
+ const FOREIGN_SESSION_TOOL_MESSAGE =
132
+ "This tool changes goal/loop/list state, which only the MAIN session owns — you are running in a subagent session. Report back to the main agent; it owns the goal and can call this tool.";
133
+
134
+ /** Refusal message when a state-mutating tool is called from a subagent session, else null. */
135
+ function foreignToolGuard(execCtx: unknown): string | null {
136
+ const c = execCtx as ExtensionContext | undefined;
137
+ return c && isForeignCtx(c) ? FOREIGN_SESSION_TOOL_MESSAGE : null;
138
+ }
139
+
113
140
  let state: State = { goal: null };
114
141
 
115
142
  // Drafting mode: a no-arg loop command starts a clarification turn; the agent
@@ -361,7 +388,7 @@ function persistState(ctx: ExtensionContext): void {
361
388
  }
362
389
 
363
390
  function setGoal(goal: Goal, ctx: ExtensionContext): void {
364
- state = { goal, list: state.list ?? [] }; // preserve the queue!
391
+ state = { goal, list: state.list ?? [] }; // preserve the list!
365
392
  const file = writeGoalMd(ctx.cwd, goal);
366
393
  state.goal!.activePath = path.relative(ctx.cwd, file) || file;
367
394
  persistState(ctx);
@@ -453,7 +480,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
453
480
  if (target === "list") {
454
481
  tmpl = tmpl.replace(
455
482
  "[GOAL DRAFTING]",
456
- "[GOAL DRAFTING — the confirmed goal goes into the /list LIST, it does not activate immediately. If the user wants MANY things queued (a plan, a checklist, 'these 50 tasks'), propose them ALL AT ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals.]",
483
+ "[GOAL DRAFTING — the confirmed goal goes into the /list LIST, it does not activate immediately. If the user wants MANY things added at once (a plan, a checklist, 'these 50 tasks'), propose them ALL AT ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals.]",
457
484
  );
458
485
  }
459
486
  } catch {
@@ -567,7 +594,7 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
567
594
  // v0.22.7: name WHAT was paused — a list item resumes through /list.
568
595
  if (state.goal.policy === "list") {
569
596
  const queued = listQueue().length;
570
- ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued} queued in the list)` : ""}. /list resume to continue.`, "info");
597
+ ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued} waiting in the list)` : ""}. /list resume to continue.`, "info");
571
598
  return;
572
599
  }
573
600
  ctx.ui.notify(`Goal ${state.goal.id} paused. /goal resume to continue.`, "info");
@@ -590,8 +617,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
590
617
  const isListItem = state.goal.policy === "list";
591
618
  ctx.ui.notify(
592
619
  isListItem
593
- ? `Resumed list item [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""}`
594
- : `Resumed goal [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list — resuming the list's head)` : ""}`,
620
+ ? `Resumed list item [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} waiting in the list)` : ""}`
621
+ : `Resumed goal [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} waiting in the list — resuming the list's head)` : ""}`,
595
622
  "info",
596
623
  );
597
624
  scheduleContinuation(ctx, true);
@@ -654,8 +681,8 @@ async function cmdTweak(args: string, ctx: ExtensionContext): Promise<void> {
654
681
  try {
655
682
  confirmed = await ctx.ui.confirm(
656
683
  "Tweak goal?",
657
- `CURRENT:\n${current.objective.slice(0, 400)}\n\nNEW:\n${newObjective.slice(0, 400)}` +
658
- (newContract ? `\n\nNew contract:\n${newContract.slice(0, 200)}` : "\n\n(New text carries no contract; old contract is dropped.)"),
684
+ `CURRENT:\n${current.objective}\n\nNEW:\n${newObjective}` +
685
+ (newContract ? `\n\nNew contract:\n${newContract}` : "\n\n(New text carries no contract; old contract is dropped.)"),
659
686
  );
660
687
  } catch {
661
688
  confirmed = false;
@@ -700,13 +727,15 @@ async function bulkAddItems(ctx: ExtensionContext, parsed: string[], sourceName:
700
727
  ctx.ui.notify("No items found (headings/blank lines don't count).", "warning");
701
728
  return;
702
729
  }
703
- const preview = parsed.slice(0, 5).map((t, i) => ` ${i + 1}. ${t.slice(0, 70)}`).join("\n");
730
+ // v0.23.7: show ALL items in full a Confirm the user can't fully
731
+ // read is not a gate (same rule as the draft dialog, v0.23.5).
732
+ const preview = parsed.map((t, i) => ` ${i + 1}. ${t}`).join("\n");
704
733
  let confirmed = true;
705
734
  if (ctx.hasUI) {
706
735
  try {
707
736
  confirmed = await ctx.ui.confirm(
708
- "Import into queue?",
709
- `${parsed.length} items from ${sourceName}:\n${preview}${parsed.length > 5 ? `\n … and ${parsed.length - 5} more` : ""}`,
737
+ "Import into list?",
738
+ `${parsed.length} items from ${sourceName}:\n${preview}`,
710
739
  );
711
740
  } catch {
712
741
  confirmed = false;
@@ -718,7 +747,7 @@ async function bulkAddItems(ctx: ExtensionContext, parsed: string[], sourceName:
718
747
  }
719
748
  const n = enqueueItems(ctx, parsed, sourceName);
720
749
  if (state.goal && state.goal.status === "active") {
721
- ctx.ui.notify(`Imported ${n} items (${listQueue().length} queued).`, "info");
750
+ ctx.ui.notify(`Imported ${n} items (${listQueue().length} waiting in the list).`, "info");
722
751
  }
723
752
  }
724
753
 
@@ -744,7 +773,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
744
773
  // is the same motion as /goal resume — named for the surface the user is
745
774
  // looking at (v0.22.7: "we would just unpause, and that is next").
746
775
  if (!state.goal || state.goal.status !== "paused") {
747
- ctx.ui.notify("No paused list item to resume. /list show to see the queue.", "info");
776
+ ctx.ui.notify("No paused list item to resume. /list show to see the list.", "info");
748
777
  return;
749
778
  }
750
779
  if (state.goal.policy !== "list") {
@@ -885,7 +914,7 @@ function addSingleItem(ctx: ExtensionContext, raw: string): void {
885
914
  if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
886
915
  activateNextListItem(ctx);
887
916
  } else {
888
- ctx.ui.notify(`Queued (${listQueue().length} waiting): ${objective.slice(0, 80)}`, "info");
917
+ ctx.ui.notify(`Added to the list (${listQueue().length} waiting): ${objective.slice(0, 80)}`, "info");
889
918
  }
890
919
  }
891
920
 
@@ -1316,7 +1345,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1316
1345
  completionSummary: Type.Optional(Type.String({ description: "1-paragraph completion claim" })),
1317
1346
  verificationSummary: Type.Optional(Type.String({ description: "Per-item evidence for the verification contract" })),
1318
1347
  }),
1319
- async execute(_id, params, signal) {
1348
+ async execute(_id, params, signal, _onUpdate, execCtx) {
1349
+ const foreign0 = foreignToolGuard(execCtx);
1350
+ if (foreign0) return { content: [{ type: "text", text: foreign0 }], details: {} };
1320
1351
  if (!state.goal || state.goal.status !== "active") {
1321
1352
  return { content: [{ type: "text", text: "No active goal." }], details: {} };
1322
1353
  }
@@ -1475,7 +1506,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1475
1506
  reason: Type.String({ description: "Why the work is paused" }),
1476
1507
  suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
1477
1508
  }),
1478
- async execute(_id, params) {
1509
+ async execute(_id, params, _signal, _onUpdate, execCtx) {
1510
+ const foreign1 = foreignToolGuard(execCtx);
1511
+ if (foreign1) return { content: [{ type: "text", text: foreign1 }], details: {} };
1479
1512
  const p = params as { reason: string; suggestedAction?: string };
1480
1513
  if (!state.goal) return { content: [{ type: "text", text: "No active goal." }], details: {} };
1481
1514
  updateGoal({
@@ -1554,6 +1587,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1554
1587
  items: Type.Optional(Type.Array(Type.String(), { description: "LIST drafting only: many objectives at once (e.g. 'queue these 50 things'). Each becomes a list item; per-item 'Done when:' clauses are honored." })),
1555
1588
  }),
1556
1589
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1590
+ const foreign2 = foreignToolGuard(execCtx);
1591
+ if (foreign2) return { content: [{ type: "text", text: foreign2 }], details: {} };
1557
1592
  const p = params as { objective: string; verificationContract?: string; items?: string[] };
1558
1593
  if (draftingTarget !== "goal" && draftingTarget !== "list") {
1559
1594
  return {
@@ -1562,11 +1597,15 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1562
1597
  };
1563
1598
  }
1564
1599
  // v0.14.0: the interview floor — no Confirm until the user replied.
1565
- // v0.14.0: the interview floor no Confirm until the user replied.
1566
- if (draftingUserReplies === 0) draftingBlockedProposals++;
1567
- const block = draftProposalBlock(draftingUserReplies, draftingBlockedProposals);
1568
- if (block) {
1569
- return { content: [{ type: "text", text: block }], details: {} };
1600
+ // v0.23.8: /glla autoaccept=on skips the floor AND the Confirm
1601
+ // the seed carries the intent (unattended rigs). Default off.
1602
+ const autoAccept = loadSettings(ctx.cwd).autoAcceptDrafts === true;
1603
+ if (!autoAccept) {
1604
+ if (draftingUserReplies === 0) draftingBlockedProposals++;
1605
+ const block = draftProposalBlock(draftingUserReplies, draftingBlockedProposals);
1606
+ if (block) {
1607
+ return { content: [{ type: "text", text: block }], details: {} };
1608
+ }
1570
1609
  }
1571
1610
  // Multi-item drafts are LIST-only: a goal is single by definition.
1572
1611
  if (p.items && p.items.length > 0 && draftingTarget !== "list") {
@@ -1583,13 +1622,19 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1583
1622
  const preview = p.items.map((t, i) => ` ${i + 1}. ${t}`).join("\n");
1584
1623
  const batchActivates = !state.goal || state.goal.status === "complete" || state.goal.status === "aborted";
1585
1624
  let batchConfirmed = false;
1586
- try {
1587
- batchConfirmed = await liveCtx.ui.confirm(
1588
- "Confirm queue batch",
1589
- `${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
1590
- );
1591
- } catch {
1592
- batchConfirmed = false;
1625
+ if (autoAccept) {
1626
+ batchConfirmed = true;
1627
+ liveCtx.ui.notify(`List batch auto-accepted (/glla autoaccept=on): ${p.items.length} items${batchActivates ? " item 1 ACTIVATES now" : ""}.`, "info");
1628
+ appendLedger(liveCtx.cwd, "draft_autoaccepted", { kind: "batch", count: p.items.length });
1629
+ } else {
1630
+ try {
1631
+ batchConfirmed = await liveCtx.ui.confirm(
1632
+ "Confirm list batch",
1633
+ `${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
1634
+ );
1635
+ } catch {
1636
+ batchConfirmed = false;
1637
+ }
1593
1638
  }
1594
1639
  if (!batchConfirmed) {
1595
1640
  return {
@@ -1603,7 +1648,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1603
1648
  if (wasIdle) {
1604
1649
  return { content: [{ type: "text", text: `${n} items confirmed; first activated (list was empty). Begin work now.` }], details: {} };
1605
1650
  }
1606
- return { content: [{ type: "text", text: `${n} items confirmed and queued (${listQueue().length} waiting).` }], details: {} };
1651
+ return { content: [{ type: "text", text: `${n} items confirmed and added to the list (${listQueue().length} waiting).` }], details: {} };
1607
1652
  }
1608
1653
  const normContract = p.verificationContract?.trim() ? normalizeDraftContract(p.verificationContract) : "";
1609
1654
  const checkCount = normContract ? draftContractItemCount(normContract) : 0;
@@ -1617,14 +1662,20 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1617
1662
  const willActivate = isListDraft && (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted");
1618
1663
  const activationNote = isListDraft
1619
1664
  ? willActivate
1620
- ? "\n\n(List is empty — confirming ACTIVATES this immediately as the active goal. Reject if you only wanted to queue it.)"
1621
- : "\n\n(Goes into the list, queued behind the active goal.)"
1665
+ ? "\n\n(List is empty — confirming ACTIVATES this immediately as the active goal. Reject if you only wanted to add it, not start it.)"
1666
+ : "\n\n(Goes into the list, waiting behind the active goal.)"
1622
1667
  : "";
1623
1668
  let confirmed = false;
1624
- try {
1625
- confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
1626
- } catch {
1627
- confirmed = false;
1669
+ if (autoAccept) {
1670
+ confirmed = true;
1671
+ liveCtx.ui.notify(`Draft auto-accepted (/glla autoaccept=on)${willActivate ? " — ACTIVATING now" : ""}: ${p.objective.trim().slice(0, 90)}`, "info");
1672
+ appendLedger(liveCtx.cwd, "draft_autoaccepted", { kind: isListDraft ? "list" : "goal", objective: p.objective.trim().slice(0, 200) });
1673
+ } else {
1674
+ try {
1675
+ confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
1676
+ } catch {
1677
+ confirmed = false;
1678
+ }
1628
1679
  }
1629
1680
  if (!confirmed) {
1630
1681
  return {
@@ -1646,7 +1697,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1646
1697
  activateNextListItem(liveCtx);
1647
1698
  return { content: [{ type: "text", text: "Confirmed and activated (list was empty). Begin work now." }], details: {} };
1648
1699
  }
1649
- return { content: [{ type: "text", text: `Confirmed and queued (${listQueue().length} waiting). It activates when the current goal completes.` }], details: {} };
1700
+ return { content: [{ type: "text", text: `Confirmed and added to the list (${listQueue().length} waiting). It activates when the current goal completes.` }], details: {} };
1650
1701
  }
1651
1702
  const goal = createGoal(full, liveCtx);
1652
1703
  setGoal(goal, liveCtx);
@@ -1675,6 +1726,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1675
1726
  branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
1676
1727
  }),
1677
1728
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1729
+ const foreign3 = foreignToolGuard(execCtx);
1730
+ if (foreign3) return { content: [{ type: "text", text: foreign3 }], details: {} };
1678
1731
  const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; branch?: boolean };
1679
1732
  if (draftingTarget !== "loop") {
1680
1733
  return {
@@ -1722,17 +1775,27 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1722
1775
  }
1723
1776
  const window = p.window && p.window > 0 ? Math.floor(p.window) : 5;
1724
1777
  // v0.23.0: explicit max=0 = truly unbounded (no iteration cap).
1725
- const max = p.max !== undefined && Number.isFinite(p.max) && p.max >= 0 ? Math.floor(p.max) : 50;
1778
+ // v0.23.8: metricless + no explicit max = UNBOUNDED here too the
1779
+ // drafter path was still defaulting to 50 after v0.23.6 flipped the
1780
+ // CLI default.
1781
+ const max = p.max !== undefined && Number.isFinite(p.max) && p.max >= 0 ? Math.floor(p.max) : metricless ? 0 : 50;
1782
+ const autoAccept = loadSettings(ctx.cwd).autoAcceptDrafts === true;
1726
1783
  let confirmed = false;
1727
- try {
1728
- confirmed = await liveCtx.ui.confirm(
1784
+ if (autoAccept) {
1785
+ confirmed = true;
1786
+ liveCtx.ui.notify(`Loop draft auto-accepted (/glla autoaccept=on): ${p.target.trim().slice(0, 90)}`, "info");
1787
+ appendLedger(liveCtx.cwd, "draft_autoaccepted", { kind: "loop", target: p.target.trim().slice(0, 200), metricless });
1788
+ } else {
1789
+ try {
1790
+ confirmed = await liveCtx.ui.confirm(
1729
1791
  "Confirm loop",
1730
1792
  metricless
1731
1793
  ? `Target: ${p.target.trim()}\n\nMeasure: NONE — metricless spec loop. There is NO plateau stop: the loop ends only at ${max > 0 ? `${max} iterations` : "NO iteration cap"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""} · /loop stop.${p.branch ? "\nbranch mode: scratch branch, every iteration committed (clean tree required)" : ""}\n\nEvery iteration must make ONE real, inspectable change — cosmetic churn is the known failure mode (doorknob-polishing). Start it?`
1732
1794
  : `Target: ${p.target.trim()}\n\nMeasure: ${p.measureCmd}\nTest-run output: ${rawOutput.slice(0, 200)}\nParsed number: ${parsed} (${p.direction === "min" ? "lower is better" : "higher is better"})\n\nPlateau stop: ${window} non-improving iterations · Cap: ${max > 0 ? `${max} iterations` : "none (unbounded)"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""}${p.branch ? "\nbranch mode: scratch branch (clean tree required)" : ""}\n\nThe loop never completes — it runs until one of these bounds, plateau, or /loop stop. Start it?`,
1733
- );
1734
- } catch {
1735
- confirmed = false;
1795
+ );
1796
+ } catch {
1797
+ confirmed = false;
1798
+ }
1736
1799
  }
1737
1800
  if (!confirmed) {
1738
1801
  return {
@@ -1771,6 +1834,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1771
1834
  rationale: Type.String({ description: "Why the current spec no longer captures 'better' — shown to the user in the Confirm dialog" }),
1772
1835
  }),
1773
1836
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1837
+ const foreign4 = foreignToolGuard(execCtx);
1838
+ if (foreign4) return { content: [{ type: "text", text: foreign4 }], details: {} };
1774
1839
  const p = params as { target?: string; measureCmd?: string; rationale: string };
1775
1840
  const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
1776
1841
  const loop = state.loop;
@@ -1835,12 +1900,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1835
1900
 
1836
1901
  pi.registerTool(defineTool({
1837
1902
  name: "list_add",
1838
- label: "Add to queue",
1839
- description: "Add one or many objectives to the /list list (loop 2). Use when the user asks to queue work — 'add these to my list', 'queue these 10 things', 'put this on the backlog'. Each item becomes an audited goal; per-item 'Done when:' clauses are honored. The first queued item activates automatically when nothing is running. The list is UNBOUNDED — hundreds of small items are fine; propose them all.",
1903
+ label: "Add to list",
1904
+ description: "Add one or many objectives to the /list list (loop 2). Use when the user asks to add work — 'add these to my list', 'queue these 10 things', 'put this on the backlog'. The list is a POOL, not a FIFO: order is the default, not the law — any item can be activated next. Each item becomes an audited goal; per-item 'Done when:' clauses are honored. The first item activates automatically when nothing is running. The list is UNBOUNDED — hundreds of small items are fine; propose them all.",
1840
1905
  parameters: Type.Object({
1841
- items: Type.Array(Type.String(), { description: "Objectives to enqueue — no count limit; large plans belong in ONE call." }),
1906
+ items: Type.Array(Type.String(), { description: "Objectives to add — no count limit; large plans belong in ONE call." }),
1842
1907
  }),
1843
1908
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1909
+ const foreign5 = foreignToolGuard(execCtx);
1910
+ if (foreign5) return { content: [{ type: "text", text: foreign5 }], details: {} };
1844
1911
  const p = params as { items: string[] };
1845
1912
  if (listMutationBlocked(draftingTarget)) {
1846
1913
  return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
@@ -1856,7 +1923,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1856
1923
  content: [{
1857
1924
  type: "text",
1858
1925
  text: wasIdle
1859
- ? `${n} item(s) queued; the first is now active. Work it normally and call complete_goal when done — the next item activates automatically.`
1926
+ ? `${n} item(s) added; the first is now active. Work it normally and call complete_goal when done — the next item activates automatically.`
1860
1927
  : `${n} item(s) queued (${listQueue().length} waiting behind the active goal).`,
1861
1928
  }],
1862
1929
  details: {},
@@ -1872,6 +1939,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1872
1939
  n: Type.Number({ description: "1-based position in the queue (1 = head)" }),
1873
1940
  }),
1874
1941
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1942
+ const foreign6 = foreignToolGuard(execCtx);
1943
+ if (foreign6) return { content: [{ type: "text", text: foreign6 }], details: {} };
1875
1944
  const p = params as { n: number };
1876
1945
  if (listMutationBlocked(draftingTarget)) {
1877
1946
  return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
@@ -1945,11 +2014,18 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1945
2014
  const subs = (t.subtasks ?? []).map((s, j) => ` ${i + 1}.${j + 1} ${s}`).join("\n");
1946
2015
  return `${i + 1}. ${t.title}` + (subs ? `\n${subs}` : "");
1947
2016
  }).join("\n");
2017
+ const autoAcceptTasks = loadSettings(ctx.cwd).autoAcceptDrafts === true;
1948
2018
  let confirmed = false;
1949
- try {
1950
- confirmed = await liveCtx.ui.confirm("Confirm task list", preview);
1951
- } catch {
1952
- confirmed = false;
2019
+ if (autoAcceptTasks) {
2020
+ confirmed = true;
2021
+ liveCtx.ui.notify(`Task list auto-accepted (/glla autoaccept=on): ${p.tasks.length} tasks.`, "info");
2022
+ appendLedger(liveCtx.cwd, "draft_autoaccepted", { kind: "tasks", count: p.tasks.length });
2023
+ } else {
2024
+ try {
2025
+ confirmed = await liveCtx.ui.confirm("Confirm task list", preview);
2026
+ } catch {
2027
+ confirmed = false;
2028
+ }
1953
2029
  }
1954
2030
  if (!confirmed) {
1955
2031
  return { content: [{ type: "text", text: "Task list rejected by the user. Adjust and propose again." }], details: {} };
@@ -1984,6 +2060,10 @@ interface Settings {
1984
2060
  /** on → restored goals/loops/lists auto-resume even in fresh sessions
1985
2061
  * (unattended rigs). Default off: restore holds until /goal resume. */
1986
2062
  autoResume?: boolean;
2063
+ /** on → propose_* drafts activate WITHOUT the Confirm dialog and the
2064
+ * interview floor is skipped — the seed carries the intent (unattended
2065
+ * rigs). Default off: nothing activates before the user confirms. */
2066
+ autoAcceptDrafts?: boolean;
1987
2067
  }
1988
2068
 
1989
2069
  const DEFAULT_SETTINGS: Settings = {
@@ -2028,7 +2108,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
2028
2108
  const glob = readSettingsFile(globalSettingsPath());
2029
2109
  const effective = loadSettings(cwd);
2030
2110
  const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
2031
- const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume"];
2111
+ const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume", "autoAcceptDrafts"];
2032
2112
  for (const k of keys) {
2033
2113
  if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
2034
2114
  else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
@@ -2198,6 +2278,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2198
2278
  fmt("notifyCmd", "notify"),
2199
2279
  fmt("tokenLimit", "tokenLimit"),
2200
2280
  fmt("autoResume", "autoResume"),
2281
+ fmt("autoAcceptDrafts", "autoAccept"),
2201
2282
  `\nglobal: ${globalSettingsPath()}`,
2202
2283
  `project: ${projectSettingsPath(ctx.cwd)}`,
2203
2284
  `Set with: /glla key=value (global) · /glla project key=value (project override)`,
@@ -2259,6 +2340,17 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2259
2340
  } else {
2260
2341
  ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
2261
2342
  }
2343
+ } else if (key === "autoaccept") {
2344
+ if (["on", "true", "1", "yes"].includes(value)) {
2345
+ patch.autoAcceptDrafts = true;
2346
+ changed = true;
2347
+ ctx.ui.notify("autoaccept=on: drafts will ACTIVATE without the Confirm dialog (the interview floor is skipped too — the seed is the intent). /glla autoaccept=off restores the gate.", "warning");
2348
+ } else if (["off", "false", "0", "no", "unset"].includes(value)) {
2349
+ patch.autoAcceptDrafts = undefined;
2350
+ changed = true;
2351
+ } else {
2352
+ ctx.ui.notify(`autoaccept must be on or off, got: ${value}`, "warning");
2353
+ }
2262
2354
  } else if (key === "thinking" || key === "auditorthinkinglevel") {
2263
2355
  if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
2264
2356
  patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
@@ -2390,6 +2482,7 @@ export default function (pi: ExtensionAPI): void {
2390
2482
  ["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
2391
2483
  ["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
2392
2484
  ["autoresume=", "on: auto-resume held goals/loops in fresh sessions"],
2485
+ ["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
2393
2486
  ["project", "write a project override: /glla project key=value"],
2394
2487
  ]),
2395
2488
  handler: settingsHandler,
@@ -2397,7 +2490,7 @@ export default function (pi: ExtensionAPI): void {
2397
2490
  pi.registerCommand("list", {
2398
2491
  description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear",
2399
2492
  getArgumentCompletions: completions([
2400
- ["show", "display the queued items"],
2493
+ ["show", "display the waiting items"],
2401
2494
  ["resume", "resume the paused list item (the list's head)"],
2402
2495
  ["next", "activate the next item (or /list next <n> for position n)"],
2403
2496
  ["remove", "remove an item: /list remove <n>"],
@@ -2443,6 +2536,10 @@ export default function (pi: ExtensionAPI): void {
2443
2536
 
2444
2537
  pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
2445
2538
  rememberCtx(ctx);
2539
+ // v0.23.8: subagent sessions (pi-subagents binds extensions there too)
2540
+ // are workers — never run the restore gate or reschedule the loop from
2541
+ // a foreign session.
2542
+ if (isForeignCtx(ctx)) return;
2446
2543
  state = readState(ctx.cwd);
2447
2544
  if (!registeredCtx) {
2448
2545
  registerAgentTools(pi, ctx);
@@ -2485,14 +2582,14 @@ export default function (pi: ExtensionAPI): void {
2485
2582
  // v0.22.7: name WHAT is held — a list head resumes through /list.
2486
2583
  const isListItem = state.goal.policy === "list";
2487
2584
  const resumeCmd = isListItem ? "/list resume" : "/goal resume";
2488
- const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} queued in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
2585
+ const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
2489
2586
  updateGoal({
2490
2587
  status: "paused",
2491
2588
  pauseReason: "restored in a fresh session — no work started",
2492
2589
  pauseSuggestedAction: resumeHint,
2493
2590
  }, ctx);
2494
2591
  ctx.ui.notify(
2495
- `${isListItem ? "List item" : "Goal"} held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""} — ${resumeCmd} to continue.`,
2592
+ `${isListItem ? "List item" : "Goal"} held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} waiting in the list)` : ""} — ${resumeCmd} to continue.`,
2496
2593
  "info",
2497
2594
  );
2498
2595
  }
@@ -2520,6 +2617,9 @@ export default function (pi: ExtensionAPI): void {
2520
2617
 
2521
2618
  pi.on("agent_end", async (event: any, ctx: ExtensionContext) => {
2522
2619
  rememberCtx(ctx);
2620
+ // v0.23.8: a subagent finishing must not drive the main session's
2621
+ // continuation loop.
2622
+ if (isForeignCtx(ctx)) return;
2523
2623
  noteActivity();
2524
2624
  if (!registeredCtx) {
2525
2625
  registerAgentTools(pi, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.23.7",
3
+ "version": "0.23.8",
4
4
  "description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",