pi-goal-list-loop-audit 0.23.6 → 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 +31 -6
- package/extensions/goal-loop-core.ts +62 -1
- package/extensions/loops/goal.ts +159 -86
- package/package.json +1 -1
- package/prompts/goal-loop-forever-draft.md +6 -4
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 →
|
|
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 +
|
|
56
|
+
/list # show active + waiting items
|
|
57
57
|
/list next # skip current, activate next
|
|
58
|
-
/list remove <n> # drop item n from the
|
|
59
|
-
/list clear # empty the
|
|
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
|
|
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.
|
|
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.
|
|
@@ -300,7 +300,10 @@ export interface ListItem {
|
|
|
300
300
|
export function goalArgsNeedDrafting(args: string): boolean {
|
|
301
301
|
const t = args.trim();
|
|
302
302
|
if (!t) return false; // no-args is already the drafting path
|
|
303
|
-
|
|
303
|
+
// v0.23.7: any "done when" phrase counts — requiring the colon to
|
|
304
|
+
// immediately follow made "Done when ALL of the following are true:"
|
|
305
|
+
// route to the interview even though the user wrote a contract.
|
|
306
|
+
return !/\bdone\s+when\b/i.test(t);
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
/**
|
|
@@ -627,3 +630,61 @@ export function normalizeDraftContract(raw: string): string {
|
|
|
627
630
|
export function draftContractItemCount(normalized: string): number {
|
|
628
631
|
return normalized.split("\n").filter((l) => /^\d+\.\s/.test(l)).length;
|
|
629
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Split raw objective text into { objective, verificationContract } at the
|
|
636
|
+
* first "Done when…:"-family marker (line-start preferred, inline fallback
|
|
637
|
+
* for one-liners). v0.23.7: the marker family accepts ANY text between the
|
|
638
|
+
* keyword and the colon ("Done when ALL of the following are true:") —
|
|
639
|
+
* the shield's contractItems already drops such introducer lines
|
|
640
|
+
* (v0.23.4), and goalArgsNeedDrafting recognizes the same phrase, so the
|
|
641
|
+
* three "done when" parsers can no longer drift apart. Lives in the pure
|
|
642
|
+
* module so tests exercise THIS function, not a copy (the pre-0.23.7 test
|
|
643
|
+
* re-implemented it and silently went stale).
|
|
644
|
+
*/
|
|
645
|
+
export function extractVerificationContract(raw: string): { objective: string; verificationContract: string } {
|
|
646
|
+
// Line-based first: a marker at line start begins the contract block.
|
|
647
|
+
const lines = raw.split("\n");
|
|
648
|
+
let mode: "obj" | "verify" = "obj";
|
|
649
|
+
const objParts: string[] = [];
|
|
650
|
+
const verifyParts: string[] = [];
|
|
651
|
+
for (const line of lines) {
|
|
652
|
+
if (line.match(/^\s*(?:done when|verified when|verify|verification|done)\b[^:]*:/i)) {
|
|
653
|
+
mode = "verify";
|
|
654
|
+
}
|
|
655
|
+
if (mode === "obj") objParts.push(line);
|
|
656
|
+
else verifyParts.push(line);
|
|
657
|
+
}
|
|
658
|
+
let objective = objParts.join("\n").trim();
|
|
659
|
+
let verificationContract = verifyParts.join("\n").trim();
|
|
660
|
+
|
|
661
|
+
// Inline fallback: users write one-liners like
|
|
662
|
+
// "Create x.txt. Done when: grep -q ok x.txt"
|
|
663
|
+
// where the marker is mid-line. Split at the first inline marker.
|
|
664
|
+
if (!verificationContract) {
|
|
665
|
+
const m = raw.match(/^(.*?)(?:\.|;)??\s+(?:done when|verified when|verify|verification)\b[^:]*:\s*(.+)$/is);
|
|
666
|
+
if (m) {
|
|
667
|
+
objective = (m[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
668
|
+
verificationContract = (m[2] ?? "").trim();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return { objective, verificationContract };
|
|
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
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
piGlaDir,
|
|
56
56
|
normalizeDraftContract,
|
|
57
57
|
draftContractItemCount,
|
|
58
|
+
extractVerificationContract,
|
|
59
|
+
classifySessionCtx,
|
|
58
60
|
readState,
|
|
59
61
|
renderGoalMarkdown,
|
|
60
62
|
shouldAutoResumeOnSessionStart,
|
|
@@ -104,11 +106,37 @@ let extensionApi: ExtensionAPI | null = null;
|
|
|
104
106
|
// pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
|
|
105
107
|
// so timers must never capture a ctx — they read lastCtx at fire time.
|
|
106
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;
|
|
107
114
|
|
|
108
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;
|
|
109
123
|
lastCtx = ctx;
|
|
110
124
|
}
|
|
111
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
|
+
|
|
112
140
|
let state: State = { goal: null };
|
|
113
141
|
|
|
114
142
|
// Drafting mode: a no-arg loop command starts a clarification turn; the agent
|
|
@@ -354,43 +382,13 @@ function createGoal(objective: string, ctx: ExtensionContext, policy: "goal" | "
|
|
|
354
382
|
return goal;
|
|
355
383
|
}
|
|
356
384
|
|
|
357
|
-
function extractVerificationContract(raw: string): { objective: string; verificationContract: string } {
|
|
358
|
-
// Line-based first: a marker at line start begins the contract block.
|
|
359
|
-
const lines = raw.split("\n");
|
|
360
|
-
let mode: "obj" | "verify" = "obj";
|
|
361
|
-
const objParts: string[] = [];
|
|
362
|
-
const verifyParts: string[] = [];
|
|
363
|
-
for (const line of lines) {
|
|
364
|
-
const lower = line.toLowerCase();
|
|
365
|
-
if (lower.match(/^\s*(?:done when|verify|verified when|verification|done):/)) {
|
|
366
|
-
mode = "verify";
|
|
367
|
-
}
|
|
368
|
-
if (mode === "obj") objParts.push(line);
|
|
369
|
-
else verifyParts.push(line);
|
|
370
|
-
}
|
|
371
|
-
let objective = objParts.join("\n").trim();
|
|
372
|
-
let verificationContract = verifyParts.join("\n").trim();
|
|
373
|
-
|
|
374
|
-
// Inline fallback: users write one-liners like
|
|
375
|
-
// "Create x.txt. Done when: grep -q ok x.txt"
|
|
376
|
-
// where the marker is mid-line. Split at the first inline marker.
|
|
377
|
-
if (!verificationContract) {
|
|
378
|
-
const m = raw.match(/^(.*?)(?:\.|;)??\s+(done when|verified when|verify|verification)\s*:\s*(.+)$/is);
|
|
379
|
-
if (m) {
|
|
380
|
-
objective = (m[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
381
|
-
verificationContract = (m[3] ?? "").trim();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return { objective, verificationContract };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
385
|
function persistState(ctx: ExtensionContext): void {
|
|
388
386
|
appendLedger(ctx.cwd, "state", { goal: state.goal, list: state.list ?? [], loop: state.loop ?? null });
|
|
389
387
|
refreshUI(ctx); // every state transition flows through here → the TUI is always current
|
|
390
388
|
}
|
|
391
389
|
|
|
392
390
|
function setGoal(goal: Goal, ctx: ExtensionContext): void {
|
|
393
|
-
state = { goal, list: state.list ?? [] }; // preserve the
|
|
391
|
+
state = { goal, list: state.list ?? [] }; // preserve the list!
|
|
394
392
|
const file = writeGoalMd(ctx.cwd, goal);
|
|
395
393
|
state.goal!.activePath = path.relative(ctx.cwd, file) || file;
|
|
396
394
|
persistState(ctx);
|
|
@@ -482,7 +480,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
482
480
|
if (target === "list") {
|
|
483
481
|
tmpl = tmpl.replace(
|
|
484
482
|
"[GOAL DRAFTING]",
|
|
485
|
-
"[GOAL DRAFTING — the confirmed goal goes into the /list LIST, it does not activate immediately. If the user wants MANY things
|
|
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.]",
|
|
486
484
|
);
|
|
487
485
|
}
|
|
488
486
|
} catch {
|
|
@@ -596,7 +594,7 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
596
594
|
// v0.22.7: name WHAT was paused — a list item resumes through /list.
|
|
597
595
|
if (state.goal.policy === "list") {
|
|
598
596
|
const queued = listQueue().length;
|
|
599
|
-
ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued}
|
|
597
|
+
ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued} waiting in the list)` : ""}. /list resume to continue.`, "info");
|
|
600
598
|
return;
|
|
601
599
|
}
|
|
602
600
|
ctx.ui.notify(`Goal ${state.goal.id} paused. /goal resume to continue.`, "info");
|
|
@@ -619,8 +617,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
619
617
|
const isListItem = state.goal.policy === "list";
|
|
620
618
|
ctx.ui.notify(
|
|
621
619
|
isListItem
|
|
622
|
-
? `Resumed list item [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued}
|
|
623
|
-
: `Resumed goal [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued}
|
|
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)` : ""}`,
|
|
624
622
|
"info",
|
|
625
623
|
);
|
|
626
624
|
scheduleContinuation(ctx, true);
|
|
@@ -683,8 +681,8 @@ async function cmdTweak(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
683
681
|
try {
|
|
684
682
|
confirmed = await ctx.ui.confirm(
|
|
685
683
|
"Tweak goal?",
|
|
686
|
-
`CURRENT:\n${current.objective
|
|
687
|
-
(newContract ? `\n\nNew contract:\n${newContract
|
|
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.)"),
|
|
688
686
|
);
|
|
689
687
|
} catch {
|
|
690
688
|
confirmed = false;
|
|
@@ -729,13 +727,15 @@ async function bulkAddItems(ctx: ExtensionContext, parsed: string[], sourceName:
|
|
|
729
727
|
ctx.ui.notify("No items found (headings/blank lines don't count).", "warning");
|
|
730
728
|
return;
|
|
731
729
|
}
|
|
732
|
-
|
|
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");
|
|
733
733
|
let confirmed = true;
|
|
734
734
|
if (ctx.hasUI) {
|
|
735
735
|
try {
|
|
736
736
|
confirmed = await ctx.ui.confirm(
|
|
737
|
-
"Import into
|
|
738
|
-
`${parsed.length} items from ${sourceName}:\n${preview}
|
|
737
|
+
"Import into list?",
|
|
738
|
+
`${parsed.length} items from ${sourceName}:\n${preview}`,
|
|
739
739
|
);
|
|
740
740
|
} catch {
|
|
741
741
|
confirmed = false;
|
|
@@ -747,7 +747,7 @@ async function bulkAddItems(ctx: ExtensionContext, parsed: string[], sourceName:
|
|
|
747
747
|
}
|
|
748
748
|
const n = enqueueItems(ctx, parsed, sourceName);
|
|
749
749
|
if (state.goal && state.goal.status === "active") {
|
|
750
|
-
ctx.ui.notify(`Imported ${n} items (${listQueue().length}
|
|
750
|
+
ctx.ui.notify(`Imported ${n} items (${listQueue().length} waiting in the list).`, "info");
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
753
|
|
|
@@ -773,7 +773,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
773
773
|
// is the same motion as /goal resume — named for the surface the user is
|
|
774
774
|
// looking at (v0.22.7: "we would just unpause, and that is next").
|
|
775
775
|
if (!state.goal || state.goal.status !== "paused") {
|
|
776
|
-
ctx.ui.notify("No paused list item to resume. /list show to see the
|
|
776
|
+
ctx.ui.notify("No paused list item to resume. /list show to see the list.", "info");
|
|
777
777
|
return;
|
|
778
778
|
}
|
|
779
779
|
if (state.goal.policy !== "list") {
|
|
@@ -914,7 +914,7 @@ function addSingleItem(ctx: ExtensionContext, raw: string): void {
|
|
|
914
914
|
if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
|
|
915
915
|
activateNextListItem(ctx);
|
|
916
916
|
} else {
|
|
917
|
-
ctx.ui.notify(`
|
|
917
|
+
ctx.ui.notify(`Added to the list (${listQueue().length} waiting): ${objective.slice(0, 80)}`, "info");
|
|
918
918
|
}
|
|
919
919
|
}
|
|
920
920
|
|
|
@@ -1345,7 +1345,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1345
1345
|
completionSummary: Type.Optional(Type.String({ description: "1-paragraph completion claim" })),
|
|
1346
1346
|
verificationSummary: Type.Optional(Type.String({ description: "Per-item evidence for the verification contract" })),
|
|
1347
1347
|
}),
|
|
1348
|
-
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: {} };
|
|
1349
1351
|
if (!state.goal || state.goal.status !== "active") {
|
|
1350
1352
|
return { content: [{ type: "text", text: "No active goal." }], details: {} };
|
|
1351
1353
|
}
|
|
@@ -1504,7 +1506,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1504
1506
|
reason: Type.String({ description: "Why the work is paused" }),
|
|
1505
1507
|
suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
|
|
1506
1508
|
}),
|
|
1507
|
-
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: {} };
|
|
1508
1512
|
const p = params as { reason: string; suggestedAction?: string };
|
|
1509
1513
|
if (!state.goal) return { content: [{ type: "text", text: "No active goal." }], details: {} };
|
|
1510
1514
|
updateGoal({
|
|
@@ -1583,6 +1587,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1583
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." })),
|
|
1584
1588
|
}),
|
|
1585
1589
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1590
|
+
const foreign2 = foreignToolGuard(execCtx);
|
|
1591
|
+
if (foreign2) return { content: [{ type: "text", text: foreign2 }], details: {} };
|
|
1586
1592
|
const p = params as { objective: string; verificationContract?: string; items?: string[] };
|
|
1587
1593
|
if (draftingTarget !== "goal" && draftingTarget !== "list") {
|
|
1588
1594
|
return {
|
|
@@ -1591,11 +1597,15 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1591
1597
|
};
|
|
1592
1598
|
}
|
|
1593
1599
|
// v0.14.0: the interview floor — no Confirm until the user replied.
|
|
1594
|
-
// v0.
|
|
1595
|
-
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
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
|
+
}
|
|
1599
1609
|
}
|
|
1600
1610
|
// Multi-item drafts are LIST-only: a goal is single by definition.
|
|
1601
1611
|
if (p.items && p.items.length > 0 && draftingTarget !== "list") {
|
|
@@ -1607,16 +1617,24 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1607
1617
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
1608
1618
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
1609
1619
|
if (p.items && p.items.length > 0) {
|
|
1610
|
-
|
|
1620
|
+
// v0.23.7: show ALL items in full — the user approves the whole
|
|
1621
|
+
// batch; hidden items would be approved blind.
|
|
1622
|
+
const preview = p.items.map((t, i) => ` ${i + 1}. ${t}`).join("\n");
|
|
1611
1623
|
const batchActivates = !state.goal || state.goal.status === "complete" || state.goal.status === "aborted";
|
|
1612
1624
|
let batchConfirmed = false;
|
|
1613
|
-
|
|
1614
|
-
batchConfirmed =
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
+
}
|
|
1620
1638
|
}
|
|
1621
1639
|
if (!batchConfirmed) {
|
|
1622
1640
|
return {
|
|
@@ -1630,7 +1648,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1630
1648
|
if (wasIdle) {
|
|
1631
1649
|
return { content: [{ type: "text", text: `${n} items confirmed; first activated (list was empty). Begin work now.` }], details: {} };
|
|
1632
1650
|
}
|
|
1633
|
-
return { content: [{ type: "text", text: `${n} items confirmed and
|
|
1651
|
+
return { content: [{ type: "text", text: `${n} items confirmed and added to the list (${listQueue().length} waiting).` }], details: {} };
|
|
1634
1652
|
}
|
|
1635
1653
|
const normContract = p.verificationContract?.trim() ? normalizeDraftContract(p.verificationContract) : "";
|
|
1636
1654
|
const checkCount = normContract ? draftContractItemCount(normContract) : 0;
|
|
@@ -1644,14 +1662,20 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1644
1662
|
const willActivate = isListDraft && (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted");
|
|
1645
1663
|
const activationNote = isListDraft
|
|
1646
1664
|
? willActivate
|
|
1647
|
-
? "\n\n(List is empty — confirming ACTIVATES this immediately as the active goal. Reject if you only wanted to
|
|
1648
|
-
: "\n\n(Goes into the list,
|
|
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.)"
|
|
1649
1667
|
: "";
|
|
1650
1668
|
let confirmed = false;
|
|
1651
|
-
|
|
1652
|
-
confirmed =
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
+
}
|
|
1655
1679
|
}
|
|
1656
1680
|
if (!confirmed) {
|
|
1657
1681
|
return {
|
|
@@ -1673,7 +1697,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1673
1697
|
activateNextListItem(liveCtx);
|
|
1674
1698
|
return { content: [{ type: "text", text: "Confirmed and activated (list was empty). Begin work now." }], details: {} };
|
|
1675
1699
|
}
|
|
1676
|
-
return { content: [{ type: "text", text: `Confirmed and
|
|
1700
|
+
return { content: [{ type: "text", text: `Confirmed and added to the list (${listQueue().length} waiting). It activates when the current goal completes.` }], details: {} };
|
|
1677
1701
|
}
|
|
1678
1702
|
const goal = createGoal(full, liveCtx);
|
|
1679
1703
|
setGoal(goal, liveCtx);
|
|
@@ -1702,6 +1726,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1702
1726
|
branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
|
|
1703
1727
|
}),
|
|
1704
1728
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1729
|
+
const foreign3 = foreignToolGuard(execCtx);
|
|
1730
|
+
if (foreign3) return { content: [{ type: "text", text: foreign3 }], details: {} };
|
|
1705
1731
|
const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; branch?: boolean };
|
|
1706
1732
|
if (draftingTarget !== "loop") {
|
|
1707
1733
|
return {
|
|
@@ -1749,17 +1775,27 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1749
1775
|
}
|
|
1750
1776
|
const window = p.window && p.window > 0 ? Math.floor(p.window) : 5;
|
|
1751
1777
|
// v0.23.0: explicit max=0 = truly unbounded (no iteration cap).
|
|
1752
|
-
|
|
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;
|
|
1753
1783
|
let confirmed = false;
|
|
1754
|
-
|
|
1755
|
-
confirmed =
|
|
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(
|
|
1756
1791
|
"Confirm loop",
|
|
1757
1792
|
metricless
|
|
1758
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?`
|
|
1759
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?`,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1795
|
+
);
|
|
1796
|
+
} catch {
|
|
1797
|
+
confirmed = false;
|
|
1798
|
+
}
|
|
1763
1799
|
}
|
|
1764
1800
|
if (!confirmed) {
|
|
1765
1801
|
return {
|
|
@@ -1798,6 +1834,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1798
1834
|
rationale: Type.String({ description: "Why the current spec no longer captures 'better' — shown to the user in the Confirm dialog" }),
|
|
1799
1835
|
}),
|
|
1800
1836
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1837
|
+
const foreign4 = foreignToolGuard(execCtx);
|
|
1838
|
+
if (foreign4) return { content: [{ type: "text", text: foreign4 }], details: {} };
|
|
1801
1839
|
const p = params as { target?: string; measureCmd?: string; rationale: string };
|
|
1802
1840
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
1803
1841
|
const loop = state.loop;
|
|
@@ -1862,12 +1900,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1862
1900
|
|
|
1863
1901
|
pi.registerTool(defineTool({
|
|
1864
1902
|
name: "list_add",
|
|
1865
|
-
label: "Add to
|
|
1866
|
-
description: "Add one or many objectives to the /list list (loop 2). Use when the user asks to
|
|
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.",
|
|
1867
1905
|
parameters: Type.Object({
|
|
1868
|
-
items: Type.Array(Type.String(), { description: "Objectives to
|
|
1906
|
+
items: Type.Array(Type.String(), { description: "Objectives to add — no count limit; large plans belong in ONE call." }),
|
|
1869
1907
|
}),
|
|
1870
1908
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1909
|
+
const foreign5 = foreignToolGuard(execCtx);
|
|
1910
|
+
if (foreign5) return { content: [{ type: "text", text: foreign5 }], details: {} };
|
|
1871
1911
|
const p = params as { items: string[] };
|
|
1872
1912
|
if (listMutationBlocked(draftingTarget)) {
|
|
1873
1913
|
return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
|
|
@@ -1883,7 +1923,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1883
1923
|
content: [{
|
|
1884
1924
|
type: "text",
|
|
1885
1925
|
text: wasIdle
|
|
1886
|
-
? `${n} item(s)
|
|
1926
|
+
? `${n} item(s) added; the first is now active. Work it normally and call complete_goal when done — the next item activates automatically.`
|
|
1887
1927
|
: `${n} item(s) queued (${listQueue().length} waiting behind the active goal).`,
|
|
1888
1928
|
}],
|
|
1889
1929
|
details: {},
|
|
@@ -1899,6 +1939,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1899
1939
|
n: Type.Number({ description: "1-based position in the queue (1 = head)" }),
|
|
1900
1940
|
}),
|
|
1901
1941
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1942
|
+
const foreign6 = foreignToolGuard(execCtx);
|
|
1943
|
+
if (foreign6) return { content: [{ type: "text", text: foreign6 }], details: {} };
|
|
1902
1944
|
const p = params as { n: number };
|
|
1903
1945
|
if (listMutationBlocked(draftingTarget)) {
|
|
1904
1946
|
return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
|
|
@@ -1972,11 +2014,18 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1972
2014
|
const subs = (t.subtasks ?? []).map((s, j) => ` ${i + 1}.${j + 1} ${s}`).join("\n");
|
|
1973
2015
|
return `${i + 1}. ${t.title}` + (subs ? `\n${subs}` : "");
|
|
1974
2016
|
}).join("\n");
|
|
2017
|
+
const autoAcceptTasks = loadSettings(ctx.cwd).autoAcceptDrafts === true;
|
|
1975
2018
|
let confirmed = false;
|
|
1976
|
-
|
|
1977
|
-
confirmed =
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
+
}
|
|
1980
2029
|
}
|
|
1981
2030
|
if (!confirmed) {
|
|
1982
2031
|
return { content: [{ type: "text", text: "Task list rejected by the user. Adjust and propose again." }], details: {} };
|
|
@@ -2011,6 +2060,10 @@ interface Settings {
|
|
|
2011
2060
|
/** on → restored goals/loops/lists auto-resume even in fresh sessions
|
|
2012
2061
|
* (unattended rigs). Default off: restore holds until /goal resume. */
|
|
2013
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;
|
|
2014
2067
|
}
|
|
2015
2068
|
|
|
2016
2069
|
const DEFAULT_SETTINGS: Settings = {
|
|
@@ -2055,7 +2108,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
|
|
|
2055
2108
|
const glob = readSettingsFile(globalSettingsPath());
|
|
2056
2109
|
const effective = loadSettings(cwd);
|
|
2057
2110
|
const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
|
|
2058
|
-
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume"];
|
|
2111
|
+
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume", "autoAcceptDrafts"];
|
|
2059
2112
|
for (const k of keys) {
|
|
2060
2113
|
if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
|
|
2061
2114
|
else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
|
|
@@ -2181,7 +2234,7 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2181
2234
|
else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
|
|
2182
2235
|
}
|
|
2183
2236
|
} else if (choice.startsWith("Wedge alert")) {
|
|
2184
|
-
const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default
|
|
2237
|
+
const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default 30");
|
|
2185
2238
|
if (v !== undefined) {
|
|
2186
2239
|
const n = Number.parseInt(v.trim(), 10);
|
|
2187
2240
|
if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: n });
|
|
@@ -2225,6 +2278,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2225
2278
|
fmt("notifyCmd", "notify"),
|
|
2226
2279
|
fmt("tokenLimit", "tokenLimit"),
|
|
2227
2280
|
fmt("autoResume", "autoResume"),
|
|
2281
|
+
fmt("autoAcceptDrafts", "autoAccept"),
|
|
2228
2282
|
`\nglobal: ${globalSettingsPath()}`,
|
|
2229
2283
|
`project: ${projectSettingsPath(ctx.cwd)}`,
|
|
2230
2284
|
`Set with: /glla key=value (global) · /glla project key=value (project override)`,
|
|
@@ -2270,7 +2324,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2270
2324
|
} else {
|
|
2271
2325
|
const n = Number.parseInt(value, 10);
|
|
2272
2326
|
if (Number.isFinite(n) && n >= 0) {
|
|
2273
|
-
patch.wedgeAlertMinutes = n; // 0 = off; unset = default
|
|
2327
|
+
patch.wedgeAlertMinutes = n; // 0 = off; unset = default 30
|
|
2274
2328
|
changed = true;
|
|
2275
2329
|
} else {
|
|
2276
2330
|
ctx.ui.notify(`wedgealert must be a non-negative integer (minutes, 0 = off), got: ${value}`, "warning");
|
|
@@ -2286,6 +2340,17 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2286
2340
|
} else {
|
|
2287
2341
|
ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
|
|
2288
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
|
+
}
|
|
2289
2354
|
} else if (key === "thinking" || key === "auditorthinkinglevel") {
|
|
2290
2355
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
2291
2356
|
patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
|
|
@@ -2417,6 +2482,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2417
2482
|
["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
|
|
2418
2483
|
["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
|
|
2419
2484
|
["autoresume=", "on: auto-resume held goals/loops in fresh sessions"],
|
|
2485
|
+
["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
|
|
2420
2486
|
["project", "write a project override: /glla project key=value"],
|
|
2421
2487
|
]),
|
|
2422
2488
|
handler: settingsHandler,
|
|
@@ -2424,7 +2490,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2424
2490
|
pi.registerCommand("list", {
|
|
2425
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",
|
|
2426
2492
|
getArgumentCompletions: completions([
|
|
2427
|
-
["show", "display the
|
|
2493
|
+
["show", "display the waiting items"],
|
|
2428
2494
|
["resume", "resume the paused list item (the list's head)"],
|
|
2429
2495
|
["next", "activate the next item (or /list next <n> for position n)"],
|
|
2430
2496
|
["remove", "remove an item: /list remove <n>"],
|
|
@@ -2470,6 +2536,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2470
2536
|
|
|
2471
2537
|
pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
|
|
2472
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;
|
|
2473
2543
|
state = readState(ctx.cwd);
|
|
2474
2544
|
if (!registeredCtx) {
|
|
2475
2545
|
registerAgentTools(pi, ctx);
|
|
@@ -2512,14 +2582,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2512
2582
|
// v0.22.7: name WHAT is held — a list head resumes through /list.
|
|
2513
2583
|
const isListItem = state.goal.policy === "list";
|
|
2514
2584
|
const resumeCmd = isListItem ? "/list resume" : "/goal resume";
|
|
2515
|
-
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued}
|
|
2585
|
+
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
|
|
2516
2586
|
updateGoal({
|
|
2517
2587
|
status: "paused",
|
|
2518
2588
|
pauseReason: "restored in a fresh session — no work started",
|
|
2519
2589
|
pauseSuggestedAction: resumeHint,
|
|
2520
2590
|
}, ctx);
|
|
2521
2591
|
ctx.ui.notify(
|
|
2522
|
-
`${isListItem ? "List item" : "Goal"} held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued}
|
|
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.`,
|
|
2523
2593
|
"info",
|
|
2524
2594
|
);
|
|
2525
2595
|
}
|
|
@@ -2547,6 +2617,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2547
2617
|
|
|
2548
2618
|
pi.on("agent_end", async (event: any, ctx: ExtensionContext) => {
|
|
2549
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;
|
|
2550
2623
|
noteActivity();
|
|
2551
2624
|
if (!registeredCtx) {
|
|
2552
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.
|
|
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",
|
|
@@ -38,10 +38,12 @@ stops it.
|
|
|
38
38
|
3. Clarify the **direction**: is lower better (min) or higher better (max)?
|
|
39
39
|
(Skip this for a metricless loop — there is no direction without a metric.)
|
|
40
40
|
4. Optional tuning: `window` (plateau stop after N non-improving iterations,
|
|
41
|
-
default 5 — meaningless for metricless), `max` (iteration cap
|
|
42
|
-
`max=0`
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
default 5 — meaningless for metricless), `max` (iteration cap: default 50
|
|
42
|
+
for METRIC loops; default UNBOUNDED (`max=0`) for metricless loops,
|
|
43
|
+
v0.23.6 — a bare infinite loop is the point of the metricless form).
|
|
44
|
+
Suggest smaller values for expensive measures. For metricless loops,
|
|
45
|
+
ALWAYS discuss bounds — an unbounded metricless loop is a deliberate
|
|
46
|
+
furnace.
|
|
45
47
|
5. When concrete, call `propose_loop_draft` with `target`, `measureCmd` (or
|
|
46
48
|
omit/`"none"` for metricless), `direction` (measured only), and optional
|
|
47
49
|
`window`/`max`/`time`/`tokens`.
|