pi-goal-list-loop-audit 0.17.1 → 0.18.1
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 +2 -1
- package/extensions/goal-loop-core.ts +44 -0
- package/extensions/loops/goal.ts +57 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,8 +41,9 @@ Four top-level commands, that's all:
|
|
|
41
41
|
/goal tweak "<new objective>" # edit in place (Confirm dialog)
|
|
42
42
|
/goal archive # archived goals, newest first
|
|
43
43
|
/glla # open the settings UI (or /glla key=value)
|
|
44
|
+
/list fix the login bug, add dark mode, write docs # dump it — the agent decomposes into items, one Confirm
|
|
44
45
|
/list add # draft a contract (or a whole batch via items[])
|
|
45
|
-
/list add "<objective>" # queue one directly
|
|
46
|
+
/list add "<objective>" # queue one directly — no interview (the /goal start of lists)
|
|
46
47
|
/list add plan.md # file detected → bulk import, one Confirm
|
|
47
48
|
/list add <paste a checklist> # multi-line paste → same batch flow
|
|
48
49
|
|
|
@@ -201,6 +201,50 @@ export function parseListImport(content: string): string[] {
|
|
|
201
201
|
return items;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* During a LIST drafting session the agent must not queue items one by one
|
|
206
|
+
* with list_add/list_activate — that bypasses the user's Confirm gate
|
|
207
|
+
* (observed in the wild: the agent decomposed a dump and ACTIVATED the first
|
|
208
|
+
* item with zero confirmation). The batch path is propose_goal_draft's
|
|
209
|
+
* items[]: one Confirm for the whole list. User commands (/list add) are
|
|
210
|
+
* unaffected — only the agent tools are gated.
|
|
211
|
+
*/
|
|
212
|
+
export function listMutationBlocked(draftingTarget: string | null): boolean {
|
|
213
|
+
return draftingTarget === "list";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const LIST_DRAFTING_BLOCK_MESSAGE =
|
|
217
|
+
"LIST DRAFTING IN PROGRESS — do not queue items one by one. Decompose the request into an items[] array and call propose_goal_draft ONCE: the user confirms the whole batch in a single dialog. list_add / list_activate work again after the drafting session ends.";
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Route natural-language text handed to `/list` with no subcommand verb
|
|
221
|
+
* (v0.18.0). The user typed a dump — "fix x, do y, write docs" — not a
|
|
222
|
+
* command. Flexible by detection, never a usage error:
|
|
223
|
+
* file path → bulk import (sisyphus/Ralph plan file)
|
|
224
|
+
* multi-line paste → batch add (structure is already explicit)
|
|
225
|
+
* has "Done when:" → one direct item (explicit contract)
|
|
226
|
+
* anything else → conversational decomposition (drafting session;
|
|
227
|
+
* the agent shapes it into items[], one Confirm)
|
|
228
|
+
* The explicit verb `/list add` stays the direct escape hatch (symmetric
|
|
229
|
+
* with `/goal start`): it skips the draft branch.
|
|
230
|
+
*/
|
|
231
|
+
export type ListTextRoute =
|
|
232
|
+
| { kind: "file"; path: string }
|
|
233
|
+
| { kind: "batch"; items: string[] }
|
|
234
|
+
| { kind: "direct"; text: string }
|
|
235
|
+
| { kind: "draft"; seed: string };
|
|
236
|
+
|
|
237
|
+
export function routeListText(cwd: string, raw: string): ListTextRoute {
|
|
238
|
+
const importFile = resolveImportFile(cwd, raw);
|
|
239
|
+
if (importFile) return { kind: "file", path: importFile };
|
|
240
|
+
if (raw.includes("\n")) {
|
|
241
|
+
const pasted = parseListImport(raw);
|
|
242
|
+
if (pasted.length > 1) return { kind: "batch", items: pasted };
|
|
243
|
+
}
|
|
244
|
+
if (!goalArgsNeedDrafting(raw)) return { kind: "direct", text: raw };
|
|
245
|
+
return { kind: "draft", seed: raw };
|
|
246
|
+
}
|
|
247
|
+
|
|
204
248
|
/**
|
|
205
249
|
* Detect whether a `/list add` argument is a readable file (v0.8.2). File
|
|
206
250
|
* detection, not a separate verb: `/list add plan.md` bulk-imports when the
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
parseListImport,
|
|
36
36
|
resolveImportFile,
|
|
37
37
|
routeGoalArgs,
|
|
38
|
+
routeListText,
|
|
39
|
+
listMutationBlocked,
|
|
40
|
+
LIST_DRAFTING_BLOCK_MESSAGE,
|
|
38
41
|
sumNewAssistantTokens,
|
|
39
42
|
takeAt,
|
|
40
43
|
goalArgsNeedDrafting,
|
|
@@ -434,7 +437,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
434
437
|
const [file, label, tool] = prompts[target]!;
|
|
435
438
|
ctx.ui.notify(
|
|
436
439
|
seed
|
|
437
|
-
? `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). Skip the interview entirely: /goal start <objective
|
|
440
|
+
? `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). Skip the interview entirely: ${target === "list" ? "/list add <objective>" : "/goal start <objective>"}.`
|
|
438
441
|
: `${label} started. The agent will grill until the contract is concrete, then ${tool} opens a Confirm dialog. No work begins before confirmation.`,
|
|
439
442
|
"info",
|
|
440
443
|
);
|
|
@@ -724,7 +727,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
724
727
|
lines.push("Active: (none)");
|
|
725
728
|
}
|
|
726
729
|
if (queue.length === 0) {
|
|
727
|
-
lines.push("List: empty. /list add <
|
|
730
|
+
lines.push("List: empty. /list <describe your tasks> | /list add <one item, no interview> | /list add <file>");
|
|
728
731
|
} else {
|
|
729
732
|
lines.push(`Queue (${queue.length}):`);
|
|
730
733
|
const PAGE = 15;
|
|
@@ -759,8 +762,9 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
759
762
|
await startDrafting(ctx, "list");
|
|
760
763
|
return;
|
|
761
764
|
}
|
|
762
|
-
//
|
|
763
|
-
// multi-line
|
|
765
|
+
// Explicit verb = explicit intent (the /goal start of lists): file and
|
|
766
|
+
// multi-line structure are still honored, but vague text adds AS ONE
|
|
767
|
+
// ITEM — no interview. The conversational path is bare /list <text>.
|
|
764
768
|
const importFile = resolveImportFile(ctx.cwd, raw);
|
|
765
769
|
if (importFile) {
|
|
766
770
|
await bulkAddFromFile(ctx, importFile);
|
|
@@ -773,17 +777,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
773
777
|
return;
|
|
774
778
|
}
|
|
775
779
|
}
|
|
776
|
-
|
|
777
|
-
const item = { id: newGoalId(), objective, verificationContract: verificationContract || undefined, addedAt: nowIso() };
|
|
778
|
-
state = { ...state, list: [...listQueue(), item] };
|
|
779
|
-
persistState(ctx);
|
|
780
|
-
appendLedger(ctx.cwd, "list_added", { id: item.id, objective: item.objective });
|
|
781
|
-
// Nothing active → activate immediately.
|
|
782
|
-
if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
|
|
783
|
-
activateNextListItem(ctx);
|
|
784
|
-
} else {
|
|
785
|
-
ctx.ui.notify(`Queued (${listQueue().length} waiting): ${objective.slice(0, 80)}`, "info");
|
|
786
|
-
}
|
|
780
|
+
addSingleItem(ctx, raw);
|
|
787
781
|
return;
|
|
788
782
|
}
|
|
789
783
|
|
|
@@ -828,7 +822,44 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
828
822
|
return;
|
|
829
823
|
}
|
|
830
824
|
|
|
831
|
-
|
|
825
|
+
// v0.18.0: an unknown first word isn't an error — it's a natural-language
|
|
826
|
+
// dump. "/list fix the login bug, add dark mode, write docs" should MAKE
|
|
827
|
+
// a list, not print usage. Detection chain: file → batch → contract →
|
|
828
|
+
// conversational decomposition (drafting). The explicit verb for adding
|
|
829
|
+
// one item verbatim is /list add.
|
|
830
|
+
let raw = args.trim();
|
|
831
|
+
if (raw.length >= 2 && ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")))) {
|
|
832
|
+
raw = raw.slice(1, -1).trim();
|
|
833
|
+
}
|
|
834
|
+
const route = routeListText(ctx.cwd, raw);
|
|
835
|
+
if (route.kind === "file") {
|
|
836
|
+
await bulkAddFromFile(ctx, route.path);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (route.kind === "batch") {
|
|
840
|
+
await bulkAddItems(ctx, route.items, "pasted text");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (route.kind === "direct") {
|
|
844
|
+
addSingleItem(ctx, route.text);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
await startDrafting(ctx, "list", route.seed);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Append one objective to the list; activate immediately when idle. */
|
|
851
|
+
function addSingleItem(ctx: ExtensionContext, raw: string): void {
|
|
852
|
+
const { objective, verificationContract } = extractVerificationContract(raw);
|
|
853
|
+
const item = { id: newGoalId(), objective, verificationContract: verificationContract || undefined, addedAt: nowIso() };
|
|
854
|
+
state = { ...state, list: [...listQueue(), item] };
|
|
855
|
+
persistState(ctx);
|
|
856
|
+
appendLedger(ctx.cwd, "list_added", { id: item.id, objective: item.objective });
|
|
857
|
+
// Nothing active → activate immediately.
|
|
858
|
+
if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
|
|
859
|
+
activateNextListItem(ctx);
|
|
860
|
+
} else {
|
|
861
|
+
ctx.ui.notify(`Queued (${listQueue().length} waiting): ${objective.slice(0, 80)}`, "info");
|
|
862
|
+
}
|
|
832
863
|
}
|
|
833
864
|
|
|
834
865
|
/**
|
|
@@ -1686,6 +1717,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1686
1717
|
}),
|
|
1687
1718
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1688
1719
|
const p = params as { items: string[] };
|
|
1720
|
+
if (listMutationBlocked(draftingTarget)) {
|
|
1721
|
+
return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
|
|
1722
|
+
}
|
|
1689
1723
|
if (!Array.isArray(p.items) || p.items.length === 0) {
|
|
1690
1724
|
return { content: [{ type: "text", text: "No items given." }], details: {} };
|
|
1691
1725
|
}
|
|
@@ -1717,6 +1751,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1717
1751
|
}),
|
|
1718
1752
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1719
1753
|
const p = params as { n: number };
|
|
1754
|
+
if (listMutationBlocked(draftingTarget)) {
|
|
1755
|
+
return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
|
|
1756
|
+
}
|
|
1720
1757
|
const n = Math.floor(p.n);
|
|
1721
1758
|
if (!Number.isInteger(n) || n < 1) {
|
|
1722
1759
|
return { content: [{ type: "text", text: "n must be a positive integer (1-based position)." }], details: {} };
|
|
@@ -1823,7 +1860,7 @@ interface Settings {
|
|
|
1823
1860
|
const DEFAULT_SETTINGS: Settings = {
|
|
1824
1861
|
// Unset = follow the pi session thinking level (user selects thinking in
|
|
1825
1862
|
// pi, auditor follows), floor "high" — the auditor is the verification
|
|
1826
|
-
// gate, depth is worth more there than speed. /
|
|
1863
|
+
// gate, depth is worth more there than speed. /glla thinking= overrides.
|
|
1827
1864
|
auditorThinkingLevel: undefined,
|
|
1828
1865
|
};
|
|
1829
1866
|
|
|
@@ -1856,7 +1893,7 @@ function loadSettings(cwd: string): Settings {
|
|
|
1856
1893
|
) as unknown as Settings;
|
|
1857
1894
|
}
|
|
1858
1895
|
|
|
1859
|
-
/** Where each effective setting comes from (for the /
|
|
1896
|
+
/** Where each effective setting comes from (for the /glla display). */
|
|
1860
1897
|
function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknown; source: "project" | "global" | "default" }> {
|
|
1861
1898
|
const proj = readSettingsFile(projectSettingsPath(cwd));
|
|
1862
1899
|
const glob = readSettingsFile(globalSettingsPath());
|
|
@@ -1905,7 +1942,7 @@ function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high
|
|
|
1905
1942
|
* model in pi; the auditor uses it.** The plugin never picks a model itself.
|
|
1906
1943
|
*
|
|
1907
1944
|
* Chain:
|
|
1908
|
-
* 1. Explicit `/
|
|
1945
|
+
* 1. Explicit `/glla model=provider/id` override (rare).
|
|
1909
1946
|
* 2. The pi session model (ctx.model) — whatever the user selected.
|
|
1910
1947
|
*
|
|
1911
1948
|
* If the session model's provider is extension-registered, the auditor's
|
|
@@ -2169,7 +2206,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2169
2206
|
handler: settingsHandler,
|
|
2170
2207
|
});
|
|
2171
2208
|
pi.registerCommand("list", {
|
|
2172
|
-
description: "Loop 2: the list of audited goals — order is the default, not the law. /list add <
|
|
2209
|
+
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe your tasks> (agent decomposes into items, one Confirm) | /list add <item or file> (direct, no interview) | /list show | /list next [n] | /list remove <n> | /list clear",
|
|
2173
2210
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2174
2211
|
});
|
|
2175
2212
|
pi.registerCommand("loop", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
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",
|