pi-goal-list-loop-audit 0.15.0 → 0.16.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
@@ -33,6 +33,7 @@ Four top-level commands, that's all:
33
33
  /goal # drafting: agent grills, you Confirm
34
34
  /goal "audit the repo" # no contract clause → agent grills you first (propose is gated on it)
35
35
  /goal "Step 1. Step 2. Done when: tests pass." # has contract → starts now
36
+ /goal start "fix the flaky login test" # explicit skip-draft: starts now, no interview (auditor infers the contract)
36
37
  /goal status # show state
37
38
  /goal pause # pause
38
39
  /goal resume # resume
@@ -69,7 +70,9 @@ your objectives can start with any verb.)
69
70
 
70
71
  Drafting rules: **no-args drafts, args-without-a-`Done when:`-clause get
71
72
  grilled by the agent (proposing is mechanically blocked until you have
72
- replied at least once), args-with-a-clause start instantly, a file path is
73
+ replied at least once typed chat or an answered `ask_user_question` dialog
74
+ both count), args-with-a-clause start instantly, `/goal start` skips the
75
+ interview by explicit command, a file path is
73
76
  bulk direct.** A
74
77
  sisyphus-style plan file (checklists, bullets, numbered, plain lines) imports
75
78
  as-is — headings become nothing, items become goals. And the drafter itself
@@ -158,10 +158,10 @@ export interface Goal {
158
158
  export type GoalRoute =
159
159
  | { kind: "draft" }
160
160
  | { kind: "set"; text: string }
161
- | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "tweak" | "archive"; rest: string };
161
+ | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "tweak" | "archive" | "start"; rest: string };
162
162
 
163
163
  const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel"]);
164
- const GOAL_ARG_SUBS = new Set(["tweak", "archive"]);
164
+ const GOAL_ARG_SUBS = new Set(["tweak", "archive", "start"]);
165
165
 
166
166
  export function routeGoalArgs(raw: string): GoalRoute {
167
167
  const trimmed = raw.trim();
@@ -173,7 +173,7 @@ export function routeGoalArgs(raw: string): GoalRoute {
173
173
  return { kind: "sub", name: first as "status" | "pause" | "resume" | "cancel", rest: "" };
174
174
  }
175
175
  if (GOAL_ARG_SUBS.has(first)) {
176
- return { kind: "sub", name: first as "tweak" | "archive", rest };
176
+ return { kind: "sub", name: first as "tweak" | "archive" | "start", rest };
177
177
  }
178
178
  return { kind: "set", text: trimmed };
179
179
  }
@@ -275,9 +275,29 @@ export function buildSeedGrillMessage(tmpl: string, seed: string, tool: string):
275
275
  * mechanism guarantees an interview HAPPENED; question quality is the
276
276
  * model's job (shaped by buildSeedGrillMessage).
277
277
  */
278
- export function draftProposalBlock(userReplies: number): string | null {
278
+ export function draftProposalBlock(userReplies: number, blockedAttempts = 0): string | null {
279
279
  if (userReplies > 0) return null;
280
- return "INTERVIEW FIRST — you have not received a single user reply since drafting started. Ask the user ONE sharp question about their objective (seed-specific, with a recommended default; challenge non-answers by offering concrete options), wait for the answer, and only then call the propose tool again. The Confirm dialog stays closed until the user has actually been heard.";
280
+ const base = "INTERVIEW FIRST — you have not received a single user reply since drafting started. Ask the user ONE sharp question about their objective (seed-specific, with a recommended default; challenge non-answers by offering concrete options), wait for the answer, and only then call the propose tool again. The Confirm dialog stays closed until the user has actually been heard.";
281
+ // v0.15.1 escape hatch: typed chat replies AND answered ask_user_question
282
+ // dialogs both count. If we have blocked 3+ proposals, the replies are
283
+ // arriving through a path this plugin cannot see — hand the user a manual
284
+ // unlock instead of manufacturing yet another interview round.
285
+ if (blockedAttempts >= 3) {
286
+ return base + " NOTE: proposals have been blocked repeatedly despite interviewing — the reply counter may not see your channel. Tell the user plainly: 'type any chat message (e.g. \"go on\") to unlock the Confirm dialog', wait for it, then propose again. Do NOT ask another interview question first.";
287
+ }
288
+ return base;
289
+ }
290
+
291
+ /**
292
+ * v0.15.1: an ask_user_question tool result counts as a user reply during
293
+ * drafting — dialog answers arrive as tool results, not chat messages.
294
+ * Answered = not cancelled (Esc) with at least one answer recorded.
295
+ */
296
+ export function askUserQuestionAnswered(toolName: string, details: unknown): boolean {
297
+ if (toolName !== "ask_user_question") return false;
298
+ if (!details || typeof details !== "object") return false;
299
+ const d = details as { answers?: unknown; cancelled?: unknown };
300
+ return d.cancelled === false && Array.isArray(d.answers) && d.answers.length > 0;
281
301
  }
282
302
 
283
303
  /**
@@ -39,6 +39,7 @@ import {
39
39
  takeAt,
40
40
  goalArgsNeedDrafting,
41
41
  buildSeedGrillMessage,
42
+ askUserQuestionAnswered,
42
43
  draftProposalBlock,
43
44
  type TaskProposal,
44
45
  validateTaskProposal,
@@ -110,6 +111,7 @@ let draftingTarget: "goal" | "list" | "loop" | null = null;
110
111
  // v0.14.0 drafting floor: user replies counted while drafting; the injected
111
112
  // seed prompt itself arrives as a user message — skip exactly that one.
112
113
  let draftingUserReplies = 0;
114
+ let draftingBlockedProposals = 0; // v0.15.1: stuck-gate escape hatch
113
115
  let draftingSeedInFlight = false;
114
116
 
115
117
  // Dedup set for token accounting (agent_end may replay seen messages).
@@ -432,7 +434,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
432
434
  const [file, label, tool] = prompts[target]!;
433
435
  ctx.ui.notify(
434
436
  seed
435
- ? `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm).`
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>.`
436
438
  : `${label} started. The agent will grill until the contract is concrete, then ${tool} opens a Confirm dialog. No work begins before confirmation.`,
437
439
  "info",
438
440
  );
@@ -458,6 +460,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
458
460
  try {
459
461
  extensionApi?.sendUserMessage(tmpl, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
460
462
  draftingUserReplies = 0;
463
+ draftingBlockedProposals = 0;
461
464
  draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
462
465
  } catch {
463
466
  draftingTarget = null;
@@ -478,6 +481,16 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
478
481
  if (route.name === "cancel") return cmdCancel(ctx);
479
482
  if (route.name === "tweak") return cmdTweak(route.rest, ctx);
480
483
  if (route.name === "archive") return cmdGoals(ctx);
484
+ // v0.16.0: /goal start <objective> — explicit skip-draft. Activates
485
+ // immediately, no interview, no "Done when:" heuristic. Symmetric
486
+ // with /loop start. The auditor infers the contract from the objective.
487
+ if (route.name === "start") {
488
+ if (!route.rest) {
489
+ ctx.ui.notify("Usage: /goal start <objective> — activates immediately, skipping the drafting interview. (Without start, an objective needs a 'Done when:' clause or it gets drafted first.)", "warning");
490
+ return;
491
+ }
492
+ return cmdSet(route.rest, ctx, true);
493
+ }
481
494
  }
482
495
  return cmdSet(route.kind === "set" ? route.text : "", ctx);
483
496
  }
@@ -486,7 +499,7 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
486
499
  // /goal: bypass drafting, start now (the only entry in v0.1.0)
487
500
  // =================================================================
488
501
 
489
- async function cmdSet(args: string, ctx: ExtensionContext): Promise<void> {
502
+ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): Promise<void> {
490
503
  let raw = args.trim();
491
504
  // Users naturally quote the objective ("/goal \"do X\""); strip one layer of
492
505
  // surrounding matching quotes so they don't leak into the goal text.
@@ -504,7 +517,8 @@ async function cmdSet(args: string, ctx: ExtensionContext): Promise<void> {
504
517
  // v0.11.0: a contract-less objective gets drafted, not activated raw —
505
518
  // the pi-goal-x lesson: arg + Enter is worse than a 5-minute draft.
506
519
  // Include an explicit "Done when: …" clause to activate instantly.
507
- if (goalArgsNeedDrafting(raw)) {
520
+ // v0.16.0: /goal start bypasses this by explicit user command.
521
+ if (!skipDraft && goalArgsNeedDrafting(raw)) {
508
522
  await startDrafting(ctx, "goal", raw);
509
523
  return;
510
524
  }
@@ -1420,7 +1434,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1420
1434
  };
1421
1435
  }
1422
1436
  // v0.14.0: the interview floor — no Confirm until the user replied.
1423
- const block = draftProposalBlock(draftingUserReplies);
1437
+ // v0.14.0: the interview floor — no Confirm until the user replied.
1438
+ if (draftingUserReplies === 0) draftingBlockedProposals++;
1439
+ const block = draftProposalBlock(draftingUserReplies, draftingBlockedProposals);
1424
1440
  if (block) {
1425
1441
  return { content: [{ type: "text", text: block }], details: {} };
1426
1442
  }
@@ -1525,7 +1541,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1525
1541
  };
1526
1542
  }
1527
1543
  // v0.14.0: the interview floor — no Confirm until the user replied.
1528
- const loopBlock = draftProposalBlock(draftingUserReplies);
1544
+ if (draftingUserReplies === 0) draftingBlockedProposals++;
1545
+ const loopBlock = draftProposalBlock(draftingUserReplies, draftingBlockedProposals);
1529
1546
  if (loopBlock) {
1530
1547
  return { content: [{ type: "text", text: loopBlock }], details: {} };
1531
1548
  }
@@ -2143,7 +2160,7 @@ export default function (pi: ExtensionAPI): void {
2143
2160
  // /loop — the metric loop (draft|start|status|stop)
2144
2161
  // /gla — the settings UI (+ scriptable key=value)
2145
2162
  pi.registerCommand("goal", {
2146
- description: "Set/draft a goal, or /goal status|pause|resume|cancel|tweak <text>|archive. Objectives without a 'Done when:' clause are grilled into a contract first (nothing activates until you confirm); include the clause to start instantly.",
2163
+ description: "Set/draft a goal, or /goal status|pause|resume|cancel|tweak <text>|archive|start <objective>. Objectives without a 'Done when:' clause are grilled into a contract first; include the clause or use /goal start to skip the interview and activate instantly.",
2147
2164
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdGoal(args, ctx); },
2148
2165
  });
2149
2166
  const settingsHandler = (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdSettings(args, ctx); };
@@ -2188,6 +2205,15 @@ export default function (pi: ExtensionAPI): void {
2188
2205
  draftingUserReplies++;
2189
2206
  });
2190
2207
 
2208
+ // v0.15.1: ask_user_question answers arrive as tool results, not chat
2209
+ // messages — count answered (non-cancelled) questionnaires as replies too.
2210
+ pi.on("tool_result", async (event: any) => {
2211
+ if (draftingTarget === null) return;
2212
+ if (askUserQuestionAnswered(String(event?.toolName ?? ""), event?.details)) {
2213
+ draftingUserReplies++;
2214
+ }
2215
+ });
2216
+
2191
2217
  pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
2192
2218
  rememberCtx(ctx);
2193
2219
  state = readState(ctx.cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",