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 +31 -6
- package/extensions/goal-loop-core.ts +18 -0
- package/extensions/loops/goal.ts +153 -53
- package/package.json +1 -1
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.
|
|
@@ -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
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
|
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
|
|
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}
|
|
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}
|
|
594
|
-
: `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)` : ""}`,
|
|
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
|
|
658
|
-
(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.)"),
|
|
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
|
-
|
|
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
|
|
709
|
-
`${parsed.length} items from ${sourceName}:\n${preview}
|
|
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}
|
|
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
|
|
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(`
|
|
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.
|
|
1566
|
-
|
|
1567
|
-
const
|
|
1568
|
-
if (
|
|
1569
|
-
|
|
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
|
-
|
|
1587
|
-
batchConfirmed =
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
|
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
|
|
1621
|
-
: "\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.)"
|
|
1622
1667
|
: "";
|
|
1623
1668
|
let confirmed = false;
|
|
1624
|
-
|
|
1625
|
-
confirmed =
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1728
|
-
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(
|
|
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
|
-
|
|
1735
|
-
|
|
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
|
|
1839
|
-
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.",
|
|
1840
1905
|
parameters: Type.Object({
|
|
1841
|
-
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." }),
|
|
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)
|
|
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
|
-
|
|
1950
|
-
confirmed =
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
|
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}
|
|
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}
|
|
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.
|
|
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",
|