pi-goal-list-loop-audit 0.34.48 → 0.34.49
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/extensions/loops/goal.ts +76 -12
- package/package.json +1 -1
- package/scripts/goal-auditor-worker.mjs +34 -8
package/extensions/loops/goal.ts
CHANGED
|
@@ -873,6 +873,7 @@ function tryAbsorbHostSuccessor(ctx: ExtensionContext, via: string): boolean {
|
|
|
873
873
|
staleTerminalDone = false;
|
|
874
874
|
sessionHandoffPending = false;
|
|
875
875
|
sessionGeneration++; // a dead generation's delayed callbacks must not fire into the new owner
|
|
876
|
+
clearDraftingState(); // the old interview belongs to the disposed generation
|
|
876
877
|
appendLedger(ctx.cwd, "session_rebind_via_live_ctx", { via, generation: sessionGeneration });
|
|
877
878
|
if (interruptedAudit) {
|
|
878
879
|
// The old generation's detached worker/result handler is now stale. Do
|
|
@@ -982,6 +983,19 @@ let draftingUserReplies = 0;
|
|
|
982
983
|
let draftingBlockedProposals = 0; // v0.15.1: stuck-gate escape hatch
|
|
983
984
|
let draftingSeedInFlight = false;
|
|
984
985
|
|
|
986
|
+
/** Drafting is ephemeral session state, not durable goal/list state. A stale
|
|
987
|
+
* seed or an in-flight Confirm must never leave the next MAIN session behind
|
|
988
|
+
* the old interview gate. */
|
|
989
|
+
function clearDraftingState(): void {
|
|
990
|
+
draftingTarget = null;
|
|
991
|
+
draftingUserReplies = 0;
|
|
992
|
+
draftingBlockedProposals = 0;
|
|
993
|
+
draftingSeedInFlight = false;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const DRAFT_SESSION_INTERRUPTED_MESSAGE =
|
|
997
|
+
"The drafting flow was interrupted by a pi session replacement. This is NOT a rejection — do not refine or re-propose from the old turn. Wait for a fresh session_start, then run the drafting command again.";
|
|
998
|
+
|
|
985
999
|
// Dedup set for token accounting (agent_end may replay seen messages).
|
|
986
1000
|
const countedTokenMessages = new Set<string>();
|
|
987
1001
|
const countedLoopTokenMessages = new Set<string>();
|
|
@@ -3580,7 +3594,14 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
|
|
|
3580
3594
|
// Drafting: /goal with no args → clarify → Confirm dialog → activate
|
|
3581
3595
|
// =================================================================
|
|
3582
3596
|
|
|
3583
|
-
async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string): Promise<
|
|
3597
|
+
async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string): Promise<boolean> {
|
|
3598
|
+
// A stale/handoff-bound MAIN cannot deliver the seed. Do not leave the
|
|
3599
|
+
// module in drafting mode: that orphaned gate makes later list_add and
|
|
3600
|
+
// propose_goal_draft calls look like user disapprovals until restart.
|
|
3601
|
+
if (sessionHandoffPending || extensionApiStale || staleTerminalDone || zombieStoodDown || probeExtensionApiStale()) {
|
|
3602
|
+
clearDraftingState();
|
|
3603
|
+
return false;
|
|
3604
|
+
}
|
|
3584
3605
|
draftingTarget = target;
|
|
3585
3606
|
const prompts: Record<string, [string, string, string]> = {
|
|
3586
3607
|
goal: ["goal-loop-draft.md", "Goal drafting", "propose_goal_draft"],
|
|
@@ -3594,12 +3615,6 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
3594
3615
|
: target === "loop"
|
|
3595
3616
|
? `${label}: a loop target needs a metric and a direction — the agent will help you design them first (nothing activates until you confirm). Skip the interview entirely: /loop start "<target>" (bare = infinite metricless) or /loop start "<target>" measure="<cmd>" direction=min|max [window=5] [max=50] [time=h] [tokens=n] [branch=1].`
|
|
3596
3617
|
: `${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>.`;
|
|
3597
|
-
ctx.ui.notify(
|
|
3598
|
-
seed
|
|
3599
|
-
? seededHint
|
|
3600
|
-
: `${label} started. The agent will grill until the contract is concrete, then ${tool} opens a Confirm dialog. No work begins before confirmation.`,
|
|
3601
|
-
"info",
|
|
3602
|
-
);
|
|
3603
3618
|
const tmplPath = path.resolve(__dirname, "..", "..", "prompts", file);
|
|
3604
3619
|
let tmpl: string;
|
|
3605
3620
|
try {
|
|
@@ -3634,12 +3649,30 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
3634
3649
|
}
|
|
3635
3650
|
}
|
|
3636
3651
|
try {
|
|
3637
|
-
|
|
3652
|
+
const wasStale = extensionApiStale;
|
|
3653
|
+
const sent = safeSteerUser(ctx, tmpl);
|
|
3654
|
+
if (!sent) {
|
|
3655
|
+
clearDraftingState();
|
|
3656
|
+
// safeSteerUser deliberately catches stale API errors, so its caller
|
|
3657
|
+
// must handle a false result explicitly rather than relying on catch.
|
|
3658
|
+
if (extensionApiStale && !wasStale) {
|
|
3659
|
+
appendLedger(ctx.cwd, "extension_api_stale", { where: "startDrafting seed" });
|
|
3660
|
+
ctx.ui.notify("glla: can't start the drafting interview — this session's extension handle is stale (pi session replacement). A fresh session_start will rebind it; if no replacement arrives, restart pi normally, then re-run the command.", "warning");
|
|
3661
|
+
}
|
|
3662
|
+
return false;
|
|
3663
|
+
}
|
|
3664
|
+
ctx.ui.notify(
|
|
3665
|
+
seed
|
|
3666
|
+
? seededHint
|
|
3667
|
+
: `${label} started. The agent will grill until the contract is concrete, then ${tool} opens a Confirm dialog. No work begins before confirmation.`,
|
|
3668
|
+
"info",
|
|
3669
|
+
);
|
|
3638
3670
|
draftingUserReplies = 0;
|
|
3639
3671
|
draftingBlockedProposals = 0;
|
|
3640
3672
|
draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
|
|
3673
|
+
return true;
|
|
3641
3674
|
} catch (err) {
|
|
3642
|
-
|
|
3675
|
+
clearDraftingState();
|
|
3643
3676
|
// v0.28.1 (E6): the seed send used to fail SILENTLY — the user pressed
|
|
3644
3677
|
// Enter on /goal and nothing happened. Now: loud, and stale handles get
|
|
3645
3678
|
// the honest restart guidance.
|
|
@@ -3650,6 +3683,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
3650
3683
|
} else {
|
|
3651
3684
|
ctx.ui.notify(`glla: couldn't start the drafting interview (${err instanceof Error ? err.message : String(err)}) — try again.`, "warning");
|
|
3652
3685
|
}
|
|
3686
|
+
return false;
|
|
3653
3687
|
}
|
|
3654
3688
|
}
|
|
3655
3689
|
|
|
@@ -3748,6 +3782,9 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
3748
3782
|
raw = raw.slice(1, -1).trim();
|
|
3749
3783
|
}
|
|
3750
3784
|
if (!raw) {
|
|
3785
|
+
// A stale MAIN cannot deliver the interview seed. Do not create an
|
|
3786
|
+
// orphaned drafting gate after the entry warning has fired.
|
|
3787
|
+
if (staleEntry) return;
|
|
3751
3788
|
await startDrafting(ctx, "goal");
|
|
3752
3789
|
return;
|
|
3753
3790
|
}
|
|
@@ -3760,6 +3797,7 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
3760
3797
|
// Include an explicit "Done when: …" clause to activate instantly.
|
|
3761
3798
|
// v0.16.0: /goal start bypasses this by explicit user command.
|
|
3762
3799
|
if (!skipDraft && goalArgsNeedDrafting(raw)) {
|
|
3800
|
+
if (staleEntry) return;
|
|
3763
3801
|
await startDrafting(ctx, "goal", raw);
|
|
3764
3802
|
return;
|
|
3765
3803
|
}
|
|
@@ -6115,7 +6153,7 @@ function registerAgentTools(pi: any): void {
|
|
|
6115
6153
|
const foreign2 = foreignToolGuard(execCtx);
|
|
6116
6154
|
if (foreign2) return { content: [{ type: "text", text: foreign2 }], details: {} };
|
|
6117
6155
|
const p = params as { objective: string; verificationContract?: string; items?: string[] };
|
|
6118
|
-
|
|
6156
|
+
let liveCtx = currentToolContext(execCtx);
|
|
6119
6157
|
if (!liveCtx) return staleToolResult();
|
|
6120
6158
|
if (draftingTarget !== "goal" && draftingTarget !== "list") {
|
|
6121
6159
|
return {
|
|
@@ -6123,6 +6161,14 @@ function registerAgentTools(pi: any): void {
|
|
|
6123
6161
|
details: {},
|
|
6124
6162
|
};
|
|
6125
6163
|
}
|
|
6164
|
+
const draftGeneration = sessionGeneration;
|
|
6165
|
+
const staleDraftEntry = draftingTarget === "list"
|
|
6166
|
+
? warnIfStaleAtEntry(liveCtx, "list drafting")
|
|
6167
|
+
: warnIfStaleAtEntry(liveCtx, "goal drafting");
|
|
6168
|
+
if (staleDraftEntry) {
|
|
6169
|
+
clearDraftingState();
|
|
6170
|
+
return { content: [{ type: "text", text: DRAFT_SESSION_INTERRUPTED_MESSAGE }], details: {} };
|
|
6171
|
+
}
|
|
6126
6172
|
// v0.28.14: one-active-thing EARLY guard — refuse the whole interview
|
|
6127
6173
|
// when a loop is live (the post-confirm backstop below stays: state
|
|
6128
6174
|
// can change mid-interview).
|
|
@@ -6148,8 +6194,6 @@ function registerAgentTools(pi: any): void {
|
|
|
6148
6194
|
details: {},
|
|
6149
6195
|
};
|
|
6150
6196
|
}
|
|
6151
|
-
// v0.28.1 (S3): honest staleness warning before any Confirm attempt.
|
|
6152
|
-
warnIfStaleAtEntry(liveCtx, "goal drafting");
|
|
6153
6197
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
6154
6198
|
if (p.items && p.items.length > 0) {
|
|
6155
6199
|
// v0.23.7: show ALL items in full — the user approves the whole
|
|
@@ -6167,9 +6211,16 @@ function registerAgentTools(pi: any): void {
|
|
|
6167
6211
|
"Confirm list batch",
|
|
6168
6212
|
`${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
|
|
6169
6213
|
);
|
|
6214
|
+
const afterConfirm = freshCtxForGeneration(draftGeneration);
|
|
6215
|
+
if (!afterConfirm) {
|
|
6216
|
+
clearDraftingState();
|
|
6217
|
+
return { content: [{ type: "text", text: DRAFT_SESSION_INTERRUPTED_MESSAGE }], details: {} };
|
|
6218
|
+
}
|
|
6219
|
+
liveCtx = afterConfirm;
|
|
6170
6220
|
if (c === "stale") {
|
|
6171
6221
|
// v0.28.1 (T1): a stale dialog is NOT a rejection — nothing was
|
|
6172
6222
|
// refused; the dialog simply can't render in a doomed process.
|
|
6223
|
+
clearDraftingState();
|
|
6173
6224
|
extensionApiStale = true;
|
|
6174
6225
|
appendLedger(liveCtx.cwd, "extension_api_stale", { where: "batch confirm" });
|
|
6175
6226
|
return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Wait for a fresh session_start, then re-run the drafting flow." }], details: {} };
|
|
@@ -6212,8 +6263,15 @@ function registerAgentTools(pi: any): void {
|
|
|
6212
6263
|
appendLedger(liveCtx.cwd, "draft_autoaccepted", { kind: isListDraft ? "list" : "goal", objective: p.objective.trim().slice(0, 200) });
|
|
6213
6264
|
} else {
|
|
6214
6265
|
const c = await confirmDraft(liveCtx, isListDraft ? "Confirm list item" : "Confirm goal", `${sanitizeDisplayText(p.objective.trim())}${sanitizeDisplayText(contractBlock)}${activationNote}`);
|
|
6266
|
+
const afterConfirm = freshCtxForGeneration(draftGeneration);
|
|
6267
|
+
if (!afterConfirm) {
|
|
6268
|
+
clearDraftingState();
|
|
6269
|
+
return { content: [{ type: "text", text: DRAFT_SESSION_INTERRUPTED_MESSAGE }], details: {} };
|
|
6270
|
+
}
|
|
6271
|
+
liveCtx = afterConfirm;
|
|
6215
6272
|
if (c === "stale") {
|
|
6216
6273
|
// v0.28.1 (T1): a stale dialog is NOT "Draft rejected by the user".
|
|
6274
|
+
clearDraftingState();
|
|
6217
6275
|
extensionApiStale = true;
|
|
6218
6276
|
appendLedger(liveCtx.cwd, "extension_api_stale", { where: "draft confirm" });
|
|
6219
6277
|
return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Wait for a fresh session_start, then re-run the drafting flow." }], details: {} };
|
|
@@ -6307,6 +6365,10 @@ function registerAgentTools(pi: any): void {
|
|
|
6307
6365
|
const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; branch?: boolean };
|
|
6308
6366
|
const liveCtx = currentToolContext(execCtx);
|
|
6309
6367
|
if (!liveCtx) return staleToolResult();
|
|
6368
|
+
if (warnIfStaleAtEntry(liveCtx, "loop drafting")) {
|
|
6369
|
+
clearDraftingState();
|
|
6370
|
+
return { content: [{ type: "text", text: DRAFT_SESSION_INTERRUPTED_MESSAGE }], details: {} };
|
|
6371
|
+
}
|
|
6310
6372
|
if (draftingTarget !== "loop") {
|
|
6311
6373
|
return {
|
|
6312
6374
|
content: [{ type: "text", text: "You cannot start or draft a loop — only the user can, from the slash bar (the Confirm is the product). Do NOT write draft files or wait for the user to say 'start' in chat; that dead-ends. Instead hand the user the exact command: /loop start \"<target>\" (bare = infinite metricless; add measure=\"<cmd>\" direction=min|max for a metric loop), or /loop respec to reconcile against the root spec, or /loop with no args to draft interactively." }],
|
|
@@ -8160,6 +8222,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
8160
8222
|
markSessionOwnerShutdown(ctx.cwd, shutdownReason);
|
|
8161
8223
|
writeSessionHandoff(ctx, shutdownReason);
|
|
8162
8224
|
sessionReplacementUntil = Date.now() + SESSION_REBIND_GRACE_MS;
|
|
8225
|
+
clearDraftingState();
|
|
8163
8226
|
clearSessionOwnedTimers();
|
|
8164
8227
|
toolsRegistered = false;
|
|
8165
8228
|
toolHealNotified = false;
|
|
@@ -8208,6 +8271,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
8208
8271
|
deadOwnerSession = null; // v0.34.25: a real session_start supersedes the silent-swap record
|
|
8209
8272
|
deadOwnerCwd = null;
|
|
8210
8273
|
sessionGeneration++;
|
|
8274
|
+
clearDraftingState();
|
|
8211
8275
|
// An auditor belonging to the disposed generation cannot block the fresh
|
|
8212
8276
|
// session's recovery gate; its finally block is generation-guarded too.
|
|
8213
8277
|
completionAuditInFlight = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.49",
|
|
4
4
|
"description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
|
@@ -109,12 +109,29 @@ function toolArgsPrefix(args) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function appendRecentOutput(recentOutput, text) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
function appendRecentOutput(recentOutput, reportLine, text, flush = false) {
|
|
113
|
+
// text_delta fragments are arbitrary provider chunks, not logical lines.
|
|
114
|
+
// Assemble them exactly like MAIN's cumulative assistant renderer; treating
|
|
115
|
+
// each fragment as a line made the HUD show one word or punctuation mark at
|
|
116
|
+
// a time (for example, `Audit summary` followed by `:`). The exact audit
|
|
117
|
+
// result remains in outputParts and is deliberately unaffected by this
|
|
118
|
+
// bounded display telemetry.
|
|
119
|
+
const combined = `${reportLine.value}${text}`;
|
|
120
|
+
const parts = combined.split("\n");
|
|
121
|
+
reportLine.value = parts.pop() ?? "";
|
|
122
|
+
for (const raw of parts) {
|
|
123
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
124
|
+
if (!line) continue;
|
|
125
|
+
recentOutput.push(line.length <= MAX_RECENT_OUTPUT_ITEM_CHARS
|
|
126
|
+
? line
|
|
127
|
+
: `${line.slice(0, MAX_RECENT_OUTPUT_ITEM_CHARS - 1)}…`);
|
|
128
|
+
}
|
|
129
|
+
if (flush && reportLine.value) {
|
|
130
|
+
const line = reportLine.value;
|
|
131
|
+
recentOutput.push(line.length <= MAX_RECENT_OUTPUT_ITEM_CHARS
|
|
132
|
+
? line
|
|
133
|
+
: `${line.slice(0, MAX_RECENT_OUTPUT_ITEM_CHARS - 1)}…`);
|
|
134
|
+
reportLine.value = "";
|
|
118
135
|
}
|
|
119
136
|
while (recentOutput.length > MAX_RECENT_OUTPUT_ITEMS) recentOutput.shift();
|
|
120
137
|
}
|
|
@@ -134,6 +151,7 @@ async function main() {
|
|
|
134
151
|
const toolCalls = [];
|
|
135
152
|
const recentOutput = [];
|
|
136
153
|
const outputParts = [];
|
|
154
|
+
const recentReportLine = { value: "" };
|
|
137
155
|
const activeTools = new Map();
|
|
138
156
|
let currentTool;
|
|
139
157
|
let currentToolArgs;
|
|
@@ -172,7 +190,12 @@ async function main() {
|
|
|
172
190
|
phase,
|
|
173
191
|
elapsedMs: Date.now() - startedAt,
|
|
174
192
|
...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
|
|
175
|
-
recentOutput:
|
|
193
|
+
recentOutput: [
|
|
194
|
+
...recentOutput.slice(-MAX_RECENT_OUTPUT_ITEMS),
|
|
195
|
+
...(recentReportLine.value ? [recentReportLine.value.length <= MAX_RECENT_OUTPUT_ITEM_CHARS
|
|
196
|
+
? recentReportLine.value
|
|
197
|
+
: `${recentReportLine.value.slice(0, MAX_RECENT_OUTPUT_ITEM_CHARS - 1)}…`] : []),
|
|
198
|
+
].slice(-MAX_RECENT_OUTPUT_ITEMS),
|
|
176
199
|
toolCalls: toolCalls.slice(-MAX_TOOL_CALLS),
|
|
177
200
|
...(currentTool ? { currentTool } : {}),
|
|
178
201
|
...(currentToolArgs ? { currentToolArgs } : {}),
|
|
@@ -187,6 +210,9 @@ async function main() {
|
|
|
187
210
|
const finish = async (ok, error = "") => {
|
|
188
211
|
if (finalized) return;
|
|
189
212
|
finalized = true;
|
|
213
|
+
// Preserve a final unterminated report line in the last progress snapshot
|
|
214
|
+
// without changing the exact result output used for verdict parsing.
|
|
215
|
+
appendRecentOutput(recentOutput, recentReportLine, "", true);
|
|
190
216
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
191
217
|
if (inactivityTimer) clearInterval(inactivityTimer);
|
|
192
218
|
if (pi && pi.exitCode === null) pi.kill("SIGTERM");
|
|
@@ -313,7 +339,7 @@ async function main() {
|
|
|
313
339
|
if (event.type === "message_update") {
|
|
314
340
|
if (update?.type === "text_delta" && typeof update.delta === "string") {
|
|
315
341
|
outputParts.push(update.delta);
|
|
316
|
-
appendRecentOutput(recentOutput, update.delta);
|
|
342
|
+
appendRecentOutput(recentOutput, recentReportLine, update.delta);
|
|
317
343
|
void progress(phase).catch(() => {});
|
|
318
344
|
} else {
|
|
319
345
|
void progress("thinking").catch(() => {});
|