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 +4 -1
- package/extensions/goal-loop-core.ts +25 -5
- package/extensions/loops/goal.ts +32 -6
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
/**
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|