pi-goal-list-loop-audit 0.18.0 → 0.19.0

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,11 +41,11 @@ 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
45
- /list add # draft a contract (or a whole batch via items[])
46
- /list add "<objective>" # queue one directly no interview (the /goal start of lists)
47
- /list add plan.md # file detectedbulk import, one Confirm
48
- /list add <paste a checklist> # multi-line paste same batch flow
44
+ /list fix the login bug, add dark mode, write docs # dump it — the agent shapes it into items, one Confirm
45
+ /list plan.md # file detected bulk import, one Confirm (sisyphus/Ralph style)
46
+ /list <paste a checklist> # multi-line paste same batch flow
47
+ /list "fix the flaky test. Done when: npm test green" # explicit contractqueues directly, no interview
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.)
51
51
 
@@ -82,9 +82,9 @@ ONE confirmed batch, not 50 dialogs.
82
82
  Note: every queue item is audited individually, so at hundreds of items the
83
83
  audit cost per item is the thing to think about.
84
84
 
85
- **Drafting is the default for long-running things.** `/goal`, `/list add`, and
86
- `/loop` with no arguments all start a grilling turn that ends in a Confirm
87
- dialog. For `/loop` specifically, the orchestrator **test-runs the proposed
85
+ **Drafting is the default for long-running things.** `/goal` and
86
+ `/loop` with no arguments and any vague `/list` dump all start a
87
+ grilling turn that ends in a Confirm dialog. For `/loop` specifically, the orchestrator **test-runs the proposed
88
88
  measure command once** and shows the real number in the dialog — you validate
89
89
  the metric before a single iteration burns tokens.
90
90
 
@@ -132,8 +132,8 @@ redirect you to `/goal`.
132
132
  | Loop | Command | Status |
133
133
  |---|---|---|
134
134
  | 1. Single ordered goal | `/goal "<objective>"` | **shipped v0.1.0** |
135
- | 2. Queue of goals | `/list add\|show\|next\|remove\|clear` | **shipped v0.2.0** |
136
- | 3. Forever-polish loop | `/loop start\|status\|stop` | **shipped v0.3.0** |
135
+ | 2. Queue of goals | `/list [show\|next\|remove\|clear]` | **shipped v0.2.0** |
136
+ | 3. Metric-driven process loop | `/loop start\|status\|stop` | **shipped v0.3.0** |
137
137
 
138
138
  Each loop is a different policy class on the same status machine.
139
139
 
@@ -201,6 +201,21 @@ 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
+
204
219
  /**
205
220
  * Route natural-language text handed to `/list` with no subcommand verb
206
221
  * (v0.18.0). The user typed a dump — "fix x, do y, write docs" — not a
@@ -33,9 +33,11 @@ import {
33
33
  DEFAULT_TOKEN_LIMIT,
34
34
  mergeSettings,
35
35
  parseListImport,
36
- resolveImportFile,
36
+
37
37
  routeGoalArgs,
38
38
  routeListText,
39
+ listMutationBlocked,
40
+ LIST_DRAFTING_BLOCK_MESSAGE,
39
41
  sumNewAssistantTokens,
40
42
  takeAt,
41
43
  goalArgsNeedDrafting,
@@ -435,7 +437,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
435
437
  const [file, label, tool] = prompts[target]!;
436
438
  ctx.ui.notify(
437
439
  seed
438
- ? `${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>"}.`
440
+ ? `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). ${target === "list" ? "Add directly instead: include a \"Done when:\" clause." : "Skip the interview entirely: /goal start <objective>."}`
439
441
  : `${label} started. The agent will grill until the contract is concrete, then ${tool} opens a Confirm dialog. No work begins before confirmation.`,
440
442
  "info",
441
443
  );
@@ -725,7 +727,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
725
727
  lines.push("Active: (none)");
726
728
  }
727
729
  if (queue.length === 0) {
728
- lines.push("List: empty. /list <describe your tasks> | /list add <one item, no interview> | /list add <file>");
730
+ lines.push("List: empty. /list <describe your tasks, or a plan file> the agent shapes dumps into items, files import directly.");
729
731
  } else {
730
732
  lines.push(`Queue (${queue.length}):`);
731
733
  const PAGE = 15;
@@ -739,43 +741,33 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
739
741
  }
740
742
 
741
743
 
742
- if (sub === "import") {
743
- // Backwards-compat alias (0.8.1 shipped it; 0.8.2 folded it into add).
744
- const abs = resolveImportFile(ctx.cwd, rest.replace(/^["']|["']$/g, ""));
745
- if (!abs) {
746
- ctx.ui.notify("Usage: /list add <path> file detection is automatic now; 'import' is just an alias", "info");
744
+ // v0.19.0: `add` and `import` are pure no-op aliases — the verb changes
745
+ // nothing, detection routes everything. `/list plan.md` and
746
+ // `/list add plan.md` both import; `/list fix x, do y` and
747
+ // `/list add fix x, do y` both draft. Rationale: a list item activates
748
+ // RAW when it reaches the head, so the drafting interview is the only
749
+ // quality gate an item ever gets — a verb whose only job was skipping
750
+ // that gate was a leak, not an escape hatch. The direct path is an
751
+ // explicit "Done when:" clause (user already did the contract work).
752
+ if (sub === "add" || sub === "import") {
753
+ if (!rest) {
754
+ await startDrafting(ctx, "list");
747
755
  return;
748
756
  }
749
- await bulkAddFromFile(ctx, abs);
750
- return;
751
- }
752
-
753
- if (sub === "add") {
754
- let raw = rest;
755
- if (raw.length >= 2 && ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")))) {
756
- raw = raw.slice(1, -1).trim();
757
- }
758
- if (!raw) {
759
- // /list add with no args → draft a confirmed contract INTO THE QUEUE.
760
- await startDrafting(ctx, "list");
757
+ const aliased = routeListText(ctx.cwd, rest.replace(/^["']|["']$/g, ""));
758
+ if (aliased.kind === "file") {
759
+ await bulkAddFromFile(ctx, aliased.path);
761
760
  return;
762
761
  }
763
- // Explicit verb = explicit intent (the /goal start of lists): file and
764
- // multi-line structure are still honored, but vague text adds AS ONE
765
- // ITEM — no interview. The conversational path is bare /list <text>.
766
- const importFile = resolveImportFile(ctx.cwd, raw);
767
- if (importFile) {
768
- await bulkAddFromFile(ctx, importFile);
762
+ if (aliased.kind === "batch") {
763
+ await bulkAddItems(ctx, aliased.items, "pasted text");
769
764
  return;
770
765
  }
771
- if (raw.includes("\n")) {
772
- const pasted = parseListImport(raw);
773
- if (pasted.length > 1) {
774
- await bulkAddItems(ctx, pasted, "pasted text");
775
- return;
776
- }
766
+ if (aliased.kind === "direct") {
767
+ addSingleItem(ctx, aliased.text);
768
+ return;
777
769
  }
778
- addSingleItem(ctx, raw);
770
+ await startDrafting(ctx, "list", aliased.seed);
779
771
  return;
780
772
  }
781
773
 
@@ -1715,6 +1707,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1715
1707
  }),
1716
1708
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1717
1709
  const p = params as { items: string[] };
1710
+ if (listMutationBlocked(draftingTarget)) {
1711
+ return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
1712
+ }
1718
1713
  if (!Array.isArray(p.items) || p.items.length === 0) {
1719
1714
  return { content: [{ type: "text", text: "No items given." }], details: {} };
1720
1715
  }
@@ -1746,6 +1741,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1746
1741
  }),
1747
1742
  async execute(_id, params, _signal, _onUpdate, execCtx) {
1748
1743
  const p = params as { n: number };
1744
+ if (listMutationBlocked(draftingTarget)) {
1745
+ return { content: [{ type: "text", text: LIST_DRAFTING_BLOCK_MESSAGE }], details: {} };
1746
+ }
1749
1747
  const n = Math.floor(p.n);
1750
1748
  if (!Number.isInteger(n) || n < 1) {
1751
1749
  return { content: [{ type: "text", text: "n must be a positive integer (1-based position)." }], details: {} };
@@ -2198,7 +2196,7 @@ export default function (pi: ExtensionAPI): void {
2198
2196
  handler: settingsHandler,
2199
2197
  });
2200
2198
  pi.registerCommand("list", {
2201
- 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",
2199
+ 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 next [n] | /list remove <n> | /list clear",
2202
2200
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
2203
2201
  });
2204
2202
  pi.registerCommand("loop", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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",