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 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
@@ -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 <objective> | /list add <file>");
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
- // Flexible by detection, not by verb: an existing file path bulk-imports,
763
- // multi-line pasted text parses as a batch, anything else is one objective.
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
- const { objective, verificationContract } = extractVerificationContract(raw);
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
- ctx.ui.notify("Usage: /list [show] | /list add <objective or file> | /list next | /list remove <n> | /list clear", "info");
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. /gla thinking= overrides.
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 /gla display). */
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 `/gla model=provider/id` override (rare).
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 <obj or file or paste> | /list show | /list next [n] | /list remove <n> | /list clear",
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.17.1",
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",