pi-goal-list-loop-audit 0.28.23 → 0.28.25
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.
|
@@ -162,6 +162,8 @@ export interface HeartbeatInput {
|
|
|
162
162
|
/** Milliseconds since the last observed agent activity. */
|
|
163
163
|
msSinceActivity: number;
|
|
164
164
|
stallMs?: number;
|
|
165
|
+
/** v0.28.25: consecutive stall refires so far — spaces refires exponentially. */
|
|
166
|
+
consecutiveStalls?: number;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
/** Should the heartbeat re-fire the continuation right now? */
|
|
@@ -169,7 +171,15 @@ export function shouldHeartbeatRefire(input: HeartbeatInput): boolean {
|
|
|
169
171
|
if (!input.supervising) return false;
|
|
170
172
|
if (!input.sessionIdle) return false;
|
|
171
173
|
if (input.timerPending) return false;
|
|
172
|
-
|
|
174
|
+
// v0.28.25: exponential spacing between stall refires — 1m, 2m, 4m, 8m
|
|
175
|
+
// (cap 8×). Field-observed in junk-runner: the flat 60s gate burned all
|
|
176
|
+
// 5 refires in ~4 minutes into a just-compacted session, pausing a
|
|
177
|
+
// resumable goal instead of giving the provider/queue time to recover.
|
|
178
|
+
// noteActivity() runs at each refire, so msSinceActivity measures the
|
|
179
|
+
// silence SINCE the last refire — scaling the threshold scales the gap.
|
|
180
|
+
const stallMs = input.stallMs ?? HEARTBEAT_STALL_MS;
|
|
181
|
+
const scale = 2 ** Math.min(input.consecutiveStalls ?? 0, 3);
|
|
182
|
+
return input.msSinceActivity >= stallMs * scale;
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
/**
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -463,6 +463,25 @@ function startUITicker(): void {
|
|
|
463
463
|
// and escalated loudly past 5 minutes.
|
|
464
464
|
let continuationRearmStreak = 0;
|
|
465
465
|
let loopRearmStreak = 0;
|
|
466
|
+
// v0.28.24: post-compaction grace — a just-replaced session gets 3 minutes
|
|
467
|
+
// to settle (queue drain, provider recovery) before stall counting resumes.
|
|
468
|
+
// Field-observed in junk-runner: a 196k-token compact finished, then the
|
|
469
|
+
// heartbeat burned all 5 stall refires in the next 5 minutes into a session
|
|
470
|
+
// whose turn trigger was still dead — pausing a resumable goal 4 minutes
|
|
471
|
+
// after the compact instead of giving pi room to recover.
|
|
472
|
+
let compactionGraceUntil = 0;
|
|
473
|
+
const COMPACTION_GRACE_MS = 3 * 60_000;
|
|
474
|
+
// v0.28.25: provider-error retry cadence. Field-observed in dracon-utilities
|
|
475
|
+
// (kimi, 19-session fleet on one provider account): a "concurrent request
|
|
476
|
+
// limit" 403 storm got 5 retries BACK-TO-BACK (delay 0 after each errored
|
|
477
|
+
// turn — the session is idle at agent_end, so scheduleContinuation fired
|
|
478
|
+
// instantly) and the brake then cycled on a flat 60s cooldown for 1h 38m.
|
|
479
|
+
// The condition clears on a minutes-to-fleet scale, not milliseconds:
|
|
480
|
+
// ladder the inter-error retries (5s, 15s, 45s, 90s, 3m — the 5-retry
|
|
481
|
+
// budget now spans ~5.5m) and escalate the brake cooldown per consecutive
|
|
482
|
+
// brake (1m, 2m, 4m, 8m, 16m cap). A successful turn resets both.
|
|
483
|
+
const ERROR_RETRY_LADDER_MS = [5_000, 15_000, 45_000, 90_000, 180_000];
|
|
484
|
+
let errorBrakeStreak = 0;
|
|
466
485
|
const SEND_REARM_LEDGER_EVERY = 600; // 600 × 50ms = 30s
|
|
467
486
|
const SEND_REARM_ESCALATE_AT = 6000; // 6000 × 50ms = 5 minutes
|
|
468
487
|
|
|
@@ -545,6 +564,10 @@ function heartbeatTick(): void {
|
|
|
545
564
|
return;
|
|
546
565
|
}
|
|
547
566
|
const sessionIdle = idle && !pending;
|
|
567
|
+
// v0.28.24: post-compaction grace — the whole stall/refire/watchdog
|
|
568
|
+
// machinery below stays quiet for 3 minutes while the replaced session
|
|
569
|
+
// settles (latch watchdog, wedge alert, refire counting all resume after).
|
|
570
|
+
if (Date.now() < compactionGraceUntil) return;
|
|
548
571
|
// v0.26.5: pending-latch watchdog — a queued continuation whose turn
|
|
549
572
|
// trigger was dropped (field-observed post-compaction: continuation
|
|
550
573
|
// ACCEPTED at compact+0s, then 22 minutes of silence). The stuck latch
|
|
@@ -580,6 +603,7 @@ function heartbeatTick(): void {
|
|
|
580
603
|
timerPending: continuationTimer !== null || loopTimer !== null,
|
|
581
604
|
msSinceActivity: Date.now() - lastActivityAt,
|
|
582
605
|
stallMs: HEARTBEAT_STALL_MS,
|
|
606
|
+
consecutiveStalls,
|
|
583
607
|
});
|
|
584
608
|
// Wedge alert (v0.23.2): session BUSY but silent for the threshold —
|
|
585
609
|
// the classic hung-command case (a test suite that never exits holds
|
|
@@ -676,7 +700,7 @@ function freshCtx(): ExtensionContext | null {
|
|
|
676
700
|
}
|
|
677
701
|
}
|
|
678
702
|
|
|
679
|
-
function scheduleContinuation(ctx: ExtensionContext, force = false): void {
|
|
703
|
+
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
680
704
|
if (!isActionableGoal()) return;
|
|
681
705
|
rememberCtx(ctx);
|
|
682
706
|
const goalId = state.goal!.id;
|
|
@@ -684,7 +708,7 @@ function scheduleContinuation(ctx: ExtensionContext, force = false): void {
|
|
|
684
708
|
clearContinuationTimer();
|
|
685
709
|
let delay = 0;
|
|
686
710
|
try {
|
|
687
|
-
delay = ctx.isIdle() && !ctx.hasPendingMessages() ? 0 : BACKOFF_IDLE_RETRY_MS;
|
|
711
|
+
delay = delayMs ?? (ctx.isIdle() && !ctx.hasPendingMessages() ? 0 : BACKOFF_IDLE_RETRY_MS);
|
|
688
712
|
} catch {
|
|
689
713
|
return;
|
|
690
714
|
}
|
|
@@ -1205,10 +1229,10 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
1205
1229
|
// v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
|
|
1206
1230
|
// fresh session LOADS it (held by default since v0.28.21), and tell the truth instead of "starting now".
|
|
1207
1231
|
updateGoal({ interruptedAt: nowIso(), interruptedReason: "created in a stale session" }, ctx);
|
|
1208
|
-
ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi, then /goal resume (v0.28.21: session loads no longer auto-start by default)
|
|
1232
|
+
ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi, then /goal resume (v0.28.21: session loads no longer auto-start by default).`, "warning");
|
|
1209
1233
|
return;
|
|
1210
1234
|
}
|
|
1211
|
-
ctx.ui.notify(`Goal started: ${shortObj(goal.objective)} — the auditor will verify on completion
|
|
1235
|
+
ctx.ui.notify(`Goal started: ${shortObj(goal.objective)} — the auditor will verify on completion.`, "info");
|
|
1212
1236
|
scheduleContinuation(ctx, true);
|
|
1213
1237
|
}
|
|
1214
1238
|
|
|
@@ -1219,8 +1243,7 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
1219
1243
|
}
|
|
1220
1244
|
const g = state.goal;
|
|
1221
1245
|
const lines = [
|
|
1222
|
-
|
|
1223
|
-
`Objective: ${g.objective}`,
|
|
1246
|
+
`${statusLabel(g.status)}: ${g.objective}`,
|
|
1224
1247
|
// v0.24.7: name WHERE the work came from — a queue item is not a goal.
|
|
1225
1248
|
...(g.policy === "list" ? [`Source: /list queue (${listQueue().length} waiting) — /list to manage`] : []),
|
|
1226
1249
|
`Auto-continue: ${g.autoContinue ? "on" : "off"}`,
|
|
@@ -1240,10 +1263,10 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
1240
1263
|
// v0.22.7: name WHAT was paused — a list item resumes through /list.
|
|
1241
1264
|
if (state.goal.policy === "list") {
|
|
1242
1265
|
const queued = listQueue().length;
|
|
1243
|
-
ctx.ui.notify(`List item ${state.goal.
|
|
1266
|
+
ctx.ui.notify(`List item "${shortObj(state.goal.objective)}" paused${queued > 0 ? ` (${queued} waiting in the list)` : ""}. /list resume to continue.`, "info");
|
|
1244
1267
|
return;
|
|
1245
1268
|
}
|
|
1246
|
-
ctx.ui.notify(`Goal ${state.goal.
|
|
1269
|
+
ctx.ui.notify(`Goal "${shortObj(state.goal.objective)}" paused. /goal resume to continue.`, "info");
|
|
1247
1270
|
}
|
|
1248
1271
|
|
|
1249
1272
|
async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
@@ -2732,7 +2755,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2732
2755
|
pi.registerTool(defineTool({
|
|
2733
2756
|
name: "pause_goal",
|
|
2734
2757
|
label: "Pause goal",
|
|
2735
|
-
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress. When the user must CHOOSE between options, pass kind=\"decision\" with the options list (recommended = 1-based index of the best one) — decision pauses render as a prominent DECISION NEEDED card. Time-gated waits (retry at a specific time) use kind=\"wait\" with resumeAt (ISO). Operational failures use kind=\"error\".",
|
|
2758
|
+
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress. When the user must CHOOSE between options, pass kind=\"decision\" with the options list (recommended = 1-based index of the best one) — decision pauses render as a prominent DECISION NEEDED card. Time-gated waits (retry at a specific time) use kind=\"wait\" with resumeAt (ISO). Operational failures use kind=\"error\". VOCABULARY (v0.28.24): decision options and reasons must reference REAL commands only — /goal resume, /goal cancel, /goal tweak \"<new text>\", /list remove N, /list next, /list resume, /loop stop, /loop resume. These all act on the ACTIVE goal/item: there is NO /goal drop and NO command takes a goal id. Never show goal ids to the user — name the thing ('the active goal', 'list item \"<short name>\"'); ids are internal plumbing the user cannot act on.",
|
|
2736
2759
|
parameters: Type.Object({
|
|
2737
2760
|
reason: Type.String({ description: "Why the work is paused" }),
|
|
2738
2761
|
suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
|
|
@@ -4420,6 +4443,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4420
4443
|
rememberCtx(ctx);
|
|
4421
4444
|
if (!isSupervising()) return;
|
|
4422
4445
|
appendLedger(ctx.cwd, "session_compact", {});
|
|
4446
|
+
// v0.28.24: a compaction is LEGITIMATE busy time — reset the send-rearm
|
|
4447
|
+
// storm streaks (π-web nearly escalated a "send-retry storm" pause during
|
|
4448
|
+
// a 3.5-minute compact) and open the post-compaction stall grace.
|
|
4449
|
+
continuationRearmStreak = 0;
|
|
4450
|
+
loopRearmStreak = 0;
|
|
4451
|
+
compactionGraceUntil = Date.now() + COMPACTION_GRACE_MS;
|
|
4423
4452
|
const settle = setTimeout(() => {
|
|
4424
4453
|
const c = freshCtx();
|
|
4425
4454
|
if (!c) return;
|
|
@@ -4592,7 +4621,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4592
4621
|
// the auto-resume the marker promised.
|
|
4593
4622
|
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
|
4594
4623
|
ctx.ui.notify(
|
|
4595
|
-
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"}
|
|
4624
|
+
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"}: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}${wasInterrupted ? " — auto-resumed after the stale-handle interrupt" : ""}`,
|
|
4596
4625
|
"info",
|
|
4597
4626
|
);
|
|
4598
4627
|
// v0.28.4 (P3): skip nudge accounting for the first recovery turns.
|
|
@@ -4611,14 +4640,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4611
4640
|
pauseSuggestedAction: resumeHint,
|
|
4612
4641
|
}, ctx);
|
|
4613
4642
|
ctx.ui.notify(
|
|
4614
|
-
`${isListItem ? "List item" : "Goal"} held on restore
|
|
4643
|
+
`${isListItem ? "List item" : "Goal"} held on restore: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} waiting in the list)` : ""} — ${resumeCmd} to continue.`,
|
|
4615
4644
|
"info",
|
|
4616
4645
|
);
|
|
4617
4646
|
}
|
|
4618
4647
|
} else if (state.goal && state.goal.status === "active") {
|
|
4619
4648
|
// Active but autoContinue off: nothing auto-fires — just surface it.
|
|
4620
4649
|
ctx.ui.notify(
|
|
4621
|
-
`Restored ${state.goal.policy === "list" ? "list item" : "goal"}
|
|
4650
|
+
`Restored ${state.goal.policy === "list" ? "list item" : "goal"}: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
4622
4651
|
"info",
|
|
4623
4652
|
);
|
|
4624
4653
|
} else if ((!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") && listQueue().length > 0) {
|
|
@@ -4644,7 +4673,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4644
4673
|
pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
|
|
4645
4674
|
}, ctx);
|
|
4646
4675
|
ctx.ui.notify(
|
|
4647
|
-
`Goal
|
|
4676
|
+
`Goal held — a loop also exists; one active thing at a time. /loop to resume the loop, or /loop stop then /goal resume.`,
|
|
4648
4677
|
"info",
|
|
4649
4678
|
);
|
|
4650
4679
|
maybeDecisionPopup(ctx);
|
|
@@ -4804,33 +4833,46 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4804
4833
|
if (consecutiveErrorIterations >= 5) {
|
|
4805
4834
|
// v0.28.5 (E8): carry the REAL error text — the pause used to say
|
|
4806
4835
|
// literally "5 consecutive errors: error" (stopReason, not the
|
|
4807
|
-
// provider error). And give transient flakes ONE
|
|
4808
|
-
// (
|
|
4836
|
+
// provider error). And give transient flakes ONE auto-resume per brake
|
|
4837
|
+
// (escalating cooldown, reason re-checked) — the E8 incident lost 1.5h to a
|
|
4809
4838
|
// 60-second provider hiccup waiting on a manual /goal resume.
|
|
4810
4839
|
const detail = text.trim() ? ` (last: ${text.trim().replace(/\s+/g, " ").slice(0, 160)})` : "";
|
|
4811
4840
|
const reason = `5 consecutive errors${detail}`;
|
|
4841
|
+
// v0.28.25: the cooldown escalates per CONSECUTIVE brake — a fleet-wide
|
|
4842
|
+
// 403 window is not cleared by re-braking every 60 seconds.
|
|
4843
|
+
const cooldownMs = 60_000 * 2 ** Math.min(errorBrakeStreak, 4);
|
|
4844
|
+
const cooldownMin = Math.round(cooldownMs / 60_000);
|
|
4845
|
+
errorBrakeStreak++;
|
|
4812
4846
|
updateGoal({
|
|
4813
4847
|
status: "paused",
|
|
4814
4848
|
pauseKind: "wait",
|
|
4815
|
-
pauseResumeAt: new Date(Date.now() +
|
|
4849
|
+
pauseResumeAt: new Date(Date.now() + cooldownMs).toISOString(),
|
|
4816
4850
|
pauseReason: reason,
|
|
4817
|
-
pauseSuggestedAction:
|
|
4851
|
+
pauseSuggestedAction: `Transient provider flake? The goal auto-resumes once in ${cooldownMin}m if still paused for this reason — or /goal resume now.`,
|
|
4818
4852
|
}, ctx);
|
|
4819
4853
|
ctx.ui.notify(`Goal paused: ${reason}.`, "warning");
|
|
4820
4854
|
notifyExternal(ctx, `Goal paused: ${reason}.`);
|
|
4821
4855
|
appendLedger(ctx.cwd, "goal_paused", { reason });
|
|
4822
|
-
scheduleQuotaRetry(ctx,
|
|
4856
|
+
scheduleQuotaRetry(ctx, cooldownMs / 1000, reason, () => {
|
|
4823
4857
|
// Re-check: only auto-resume if STILL paused for the error brake
|
|
4824
4858
|
// (a user /goal pause during the window is not stomped).
|
|
4825
4859
|
if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("5 consecutive errors")) {
|
|
4826
4860
|
updateGoal({ status: "active" }, ctx);
|
|
4827
4861
|
appendLedger(ctx.cwd, "goal_resumed", { via: "error-brake-retry" });
|
|
4828
|
-
ctx.ui.notify("Auto-resumed after the 5-error brake (
|
|
4862
|
+
ctx.ui.notify("Auto-resumed after the 5-error brake (cooldown elapsed).", "info");
|
|
4829
4863
|
scheduleContinuation(ctx, true);
|
|
4830
4864
|
}
|
|
4831
4865
|
}, "5 consecutive errors — auto-retry");
|
|
4832
4866
|
return;
|
|
4833
4867
|
}
|
|
4868
|
+
// v0.28.25: under the brake, the retry rides the exponential ladder —
|
|
4869
|
+
// NOT the immediate scheduleContinuation at the bottom of this handler
|
|
4870
|
+
// (an errored turn leaves the session idle, so the default delay is 0:
|
|
4871
|
+
// exactly how 5 retries fired back-to-back in dracon-utilities).
|
|
4872
|
+
const retryDelayMs = ERROR_RETRY_LADDER_MS[Math.min(consecutiveErrorIterations - 1, ERROR_RETRY_LADDER_MS.length - 1)];
|
|
4873
|
+
appendLedger(ctx.cwd, "error_retry_backoff", { attempt: consecutiveErrorIterations, delayMs: retryDelayMs });
|
|
4874
|
+
scheduleContinuation(ctx, true, retryDelayMs);
|
|
4875
|
+
return;
|
|
4834
4876
|
} else if (stopReason === "aborted") {
|
|
4835
4877
|
// v0.28.5 (E8): user aborts are not provider errors. Separate brake,
|
|
4836
4878
|
// honest message, and NO auto-resume — aborting five turns in a row
|
|
@@ -4851,6 +4893,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4851
4893
|
} else {
|
|
4852
4894
|
consecutiveErrorIterations = 0;
|
|
4853
4895
|
consecutiveAbortIterations = 0;
|
|
4896
|
+
errorBrakeStreak = 0; // v0.28.25: a healthy turn clears the brake cooldown
|
|
4854
4897
|
}
|
|
4855
4898
|
|
|
4856
4899
|
// No wall-clock cap by design: a goal ends via completion, explicit
|
package/extensions/reviewer.ts
CHANGED
|
@@ -111,17 +111,84 @@ export function stripCodeSpans(text: string): string {
|
|
|
111
111
|
.replace(/`[^`\n]*`/g, " ");
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** v0.28.24: join hard-wrapped lines before classification. Completion
|
|
115
|
+
* summaries and transcripts arrive wrapped at ~70 cols, and line-by-line
|
|
116
|
+
* extraction sliced findings at the wrap point (field-observed in
|
|
117
|
+
* hellhunter: a list item whose ENTIRE objective was "Run a post-completion
|
|
118
|
+
* regression scan on the hellhunter codebase to" — the first visual line of
|
|
119
|
+
* a wrapped paragraph, enqueued by the convert-findings-to-list cascade).
|
|
120
|
+
* A line that doesn't end a sentence continues on the next line unless that
|
|
121
|
+
* line starts a new list item or heading. */
|
|
122
|
+
export function unwrapHardWrappedLines(text: string): string {
|
|
123
|
+
const lines = text.split("\n");
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const prev = out[out.length - 1];
|
|
127
|
+
const startsNewItem = /^\s*([-*•>]|\d+[.)]|#)/.test(line);
|
|
128
|
+
// v0.28.24: join only when the continuation starts LOWERCASE — the
|
|
129
|
+
// mid-sentence signal. Punctuation-less standalone items ("TODO: fix x")
|
|
130
|
+
// start uppercase/keyword and must NOT merge with the next item.
|
|
131
|
+
const continuesSentence = /^[a-z]/.test(line.trimStart());
|
|
132
|
+
if (
|
|
133
|
+
prev !== undefined &&
|
|
134
|
+
prev.trim().length > 0 &&
|
|
135
|
+
line.trim().length > 0 &&
|
|
136
|
+
!startsNewItem &&
|
|
137
|
+
continuesSentence &&
|
|
138
|
+
!/[.!?:;)"'\]]$/.test(prev.trimEnd())
|
|
139
|
+
) {
|
|
140
|
+
out[out.length - 1] = `${prev.trimEnd()} ${line.trimStart()}`;
|
|
141
|
+
} else {
|
|
142
|
+
out.push(line);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** v0.28.24: a candidate ending in a dangling connector is a wrap/parse
|
|
149
|
+
* fragment, not a finding ("…codebase to", "…the settings and"). */
|
|
150
|
+
const DANGLING_END = /\s(to|and|or|but|the|a|an|of|for|with|in|on|at|that|which|into|from|by|is|are|was|were|be|been|so|if|then|than|as|nor|yet|per|via)$/i;
|
|
151
|
+
|
|
152
|
+
/** v0.28.24: cut at a clause boundary, never mid-word — the finding text IS
|
|
153
|
+
* the item's user-facing name once enqueued. */
|
|
154
|
+
export function cutAtClauseBoundary(s: string, max: number): string {
|
|
155
|
+
if (s.length <= max) return s;
|
|
156
|
+
const window = s.slice(0, max);
|
|
157
|
+
const clause = Math.max(
|
|
158
|
+
window.lastIndexOf(". "),
|
|
159
|
+
window.lastIndexOf("! "),
|
|
160
|
+
window.lastIndexOf("? "),
|
|
161
|
+
window.lastIndexOf("; "),
|
|
162
|
+
window.lastIndexOf(", "),
|
|
163
|
+
window.lastIndexOf(" — "),
|
|
164
|
+
window.lastIndexOf(": "),
|
|
165
|
+
);
|
|
166
|
+
if (clause >= Math.floor(max * 0.4)) return window.slice(0, clause + 1).trimEnd();
|
|
167
|
+
const space = window.lastIndexOf(" ");
|
|
168
|
+
return (space > 0 ? window.slice(0, space) : window).trimEnd();
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
/** Scan source texts line-by-line for finding-shaped content. Code
|
|
115
|
-
* spans are stripped first (v0.26.4) — findings live in prose.
|
|
116
|
-
|
|
172
|
+
* spans are stripped first (v0.26.4) — findings live in prose. Hard-wrapped
|
|
173
|
+
* lines are joined (v0.28.24) — findings are sentence-shaped, not
|
|
174
|
+
* visual-line-shaped. `completedObjective` (v0.28.24) dedupes findings that
|
|
175
|
+
* merely restate the just-completed goal (exact-match dedupe at v0.28.16 was
|
|
176
|
+
* too narrow — duplicates arrive as prefixes/paraphrases). */
|
|
177
|
+
export function extractFindings(sources: Array<{ name: string; text: string }>, max: number, completedObjective?: string): Finding[] {
|
|
117
178
|
const out: Finding[] = [];
|
|
118
179
|
const seen = new Set<string>();
|
|
180
|
+
const completedNorm = completedObjective ? normalizeObjective(completedObjective) : "";
|
|
119
181
|
for (const { name, text } of sources) {
|
|
120
|
-
for (const line of stripCodeSpans(text).split("\n")) {
|
|
182
|
+
for (const line of unwrapHardWrappedLines(stripCodeSpans(text)).split("\n")) {
|
|
121
183
|
const cls = classifyFindingText(line);
|
|
122
184
|
if (!cls) continue;
|
|
123
|
-
const clean = line.trim().replace(/^[-*>\s\[\]x]+/, "")
|
|
185
|
+
const clean = cutAtClauseBoundary(line.trim().replace(/^[-*>\s\[\]x]+/, ""), 200);
|
|
124
186
|
if (clean.length < 8 || seen.has(clean)) continue;
|
|
187
|
+
if (DANGLING_END.test(clean) || /[,;:\u2014-]$/.test(clean)) continue; // v0.28.24: wrap/parse fragment
|
|
188
|
+
if (completedNorm) {
|
|
189
|
+
const nf = normalizeObjective(clean);
|
|
190
|
+
if (nf.length >= 24 && (completedNorm.startsWith(nf) || nf.startsWith(completedNorm))) continue; // v0.28.24: restates the completed goal
|
|
191
|
+
}
|
|
125
192
|
seen.add(clean);
|
|
126
193
|
out.push({ text: clean, source: name, class: cls });
|
|
127
194
|
if (out.length >= max) return out;
|
|
@@ -260,7 +327,7 @@ export function runReviewer(
|
|
|
260
327
|
}
|
|
261
328
|
}
|
|
262
329
|
|
|
263
|
-
const findings = extractFindings(deps.sources, config.maxFindingsPerReview);
|
|
330
|
+
const findings = extractFindings(deps.sources, config.maxFindingsPerReview, source.objective);
|
|
264
331
|
const bugs = findings.filter((f) => f.class === "bug" || f.class === "refactor");
|
|
265
332
|
const architectural = findings.filter((f) => f.class === "architectural");
|
|
266
333
|
const strategic = findings.filter((f) => f.class === "strategic");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.25",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
|
@@ -144,6 +144,8 @@ When the goal is genuinely blocked and you cannot make progress without user inp
|
|
|
144
144
|
pause_goal({reason: "...", suggestedAction: "..."})
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
When the user must CHOOSE between paths, use `pause_goal` with `kind="decision"`, an `options` list, and `recommended` (1-based index) — a prominent decision card renders and the user picks. **Vocabulary rules for reasons and options (v0.28.24):** reference only REAL commands — `/goal resume`, `/goal cancel`, `/goal tweak "<new text>"`, `/list remove N`, `/list next`, `/list resume`, `/loop stop`, `/loop resume` — all act on the ACTIVE goal/item; there is **no `/goal drop`** and **no command takes a goal id**. Never show goal ids (`20260729065635-gbtxsm`) in user-facing text — name the thing instead ("the active goal", "list item 'regression scan'"); ids are internal plumbing the user cannot act on.
|
|
148
|
+
|
|
147
149
|
## HARD RULES
|
|
148
150
|
|
|
149
151
|
- **Do not modify the objective silently.** The objective is the user's; if it has drifted from what makes sense, use `complete_goal`'s `newObjective` at completion time, or `pause_goal` and propose a `/goal tweak` mid-flight — never just work on something else and claim the original.
|