pi-goal-list-loop-audit 0.38.55 → 0.38.56
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/CHANGELOG.md +53 -0
- package/docs/INDEX.md +1 -1
- package/extensions/completion-summary.ts +103 -0
- package/extensions/goal-commands.ts +42 -6
- package/extensions/goal-loop-forever.ts +10 -1
- package/extensions/goal-loop.ts +63 -37
- package/extensions/loops/goal-tools.ts +2 -0
- package/extensions/start-context.ts +41 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.56 — Refine-and-resume loops, chatter-resolving tweaks (2026-09-16)
|
|
4
|
+
|
|
5
|
+
### Context-inferred tweak objectives
|
|
6
|
+
|
|
7
|
+
`/goal tweak`, `/list tweak`, and the bare-tweak dialog resolve chatter
|
|
8
|
+
against the session context instead of adopting it (field 2026-09-16 —
|
|
9
|
+
the VidPro ledger recorded `objective:"ok adjsut it"`). Self-contained
|
|
10
|
+
text still adopts verbatim; an acknowledgment (`go`, `ok adjust it`)
|
|
11
|
+
resolves via the discussed context and the confirm shows
|
|
12
|
+
CURRENT → INFERRED; with nothing clear discussed, a guidance message
|
|
13
|
+
names the shape and nothing changes. An explicit `Done when:` contract
|
|
14
|
+
stays verbatim — explicit intent wins. Raw chatter never reaches the
|
|
15
|
+
ledger on any adoption path (including the plain-`/goal` conflict
|
|
16
|
+
update, which funnels through the same resolver).
|
|
17
|
+
|
|
18
|
+
### Loop-sweep refinability
|
|
19
|
+
|
|
20
|
+
`/loop refine` works on resumable-held loops (field 2026-09-16 loop
|
|
21
|
+
sweep): the hint queues, then the byte-identical resume path runs —
|
|
22
|
+
refine-and-resume (owner choice), with wrong-branch and active-goal
|
|
23
|
+
guards keeping their voices so the loop stays held when they fire. Dead
|
|
24
|
+
loops stay refused with the honest path (`/loop start` for ended runs,
|
|
25
|
+
the agent `propose_loop_refine` repair for broken measures — no hint is
|
|
26
|
+
queued onto a loop that can never run again). The agent tool predicate
|
|
27
|
+
accepts parked loops too, so both surfaces agree (parity matrix pinned;
|
|
28
|
+
broken-measure stays tool-only by design). `stop`/`pause`/`finish`/
|
|
29
|
+
`status`/`resume` already handled held loops gracefully — sweep found no
|
|
30
|
+
further dead spots.
|
|
31
|
+
|
|
32
|
+
### Paused-goal tweak
|
|
33
|
+
|
|
34
|
+
`/goal tweak` accepts a paused goal instead of stranding it behind a
|
|
35
|
+
resume round-trip (field 2026-09-16, the VidPro park: tweak bounced
|
|
36
|
+
with "No active goal to tweak" while the goal sat paused). Mirrors
|
|
37
|
+
`/list tweak`, which is paused-only: the tweak applies, the goal stays
|
|
38
|
+
paused with its pause reason intact, nothing is scheduled, and the
|
|
39
|
+
result points at `/goal resume`. The nothing-to-tweak refusal now reads
|
|
40
|
+
"No goal to tweak. /goal <objective> to start one."
|
|
41
|
+
|
|
42
|
+
### Structured-long summaries
|
|
43
|
+
|
|
44
|
+
A section-structured `Outcome` (2+ `##`–`####` headers, tables) now
|
|
45
|
+
renders in full as the card's `### Summary` section — a working document
|
|
46
|
+
is no longer truncated into a status ping (field 2026-09-16, the
|
|
47
|
+
keyword-review close). The card answers two questions, informative over
|
|
48
|
+
long: Summary carries what happened plus the horizon (slices 2–3),
|
|
49
|
+
`Next` still names only the single immediate move (slice 1). Structure
|
|
50
|
+
is the price of length: unstructured values keep today's budgets and
|
|
51
|
+
the headline echo stays short. Delivery-only (terminal card +
|
|
52
|
+
archive human layer) with a 100k pathological guard pointing at the
|
|
53
|
+
archive; headline, verification, recap, and every recycled per-tick
|
|
54
|
+
payload stay bounded.
|
|
55
|
+
|
|
3
56
|
## 0.38.55 — Full-parity terminal card, lenient auditor, agent resume (2026-09-16)
|
|
4
57
|
|
|
5
58
|
### Full-parity terminal card
|
package/docs/INDEX.md
CHANGED
|
@@ -16,7 +16,7 @@ Policy contracts and recent changes live in the `audit/` directory of the
|
|
|
16
16
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
17
17
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
18
18
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
19
|
-
v0.35.14–v0.38.
|
|
19
|
+
v0.35.14–v0.38.56 continue through the supervisor freeze (`/glla pause`),
|
|
20
20
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
21
21
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
22
22
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -289,6 +289,83 @@ export const RICH_OBJECTIVE_ECHO_CHARS = 80;
|
|
|
289
289
|
export const RICH_TABLE_GROUP_THRESHOLD = 4;
|
|
290
290
|
/** v0.38.50: evidence tokens per finding cell — density, not a dump. */
|
|
291
291
|
export const RICH_EVIDENCE_TOKENS_PER_FINDING = 4;
|
|
292
|
+
/** Structured-long doctrine (field 2026-09-16 — the keyword-review close):
|
|
293
|
+
* a section-structured label value is a working document, not a status
|
|
294
|
+
* ping, and truncating it destroys its purpose. Structure is the price
|
|
295
|
+
* of length: at least this many `##`–`####` section headers keeps the
|
|
296
|
+
* value on the structured path; anything else keeps today's budgets.
|
|
297
|
+
* Delivery-only: the terminal card and the archive human layer. Recycled
|
|
298
|
+
* per-tick payloads (continuation, handoff, checkpoint) stay bounded. */
|
|
299
|
+
export const RICH_STRUCTURED_MIN_HEADERS = 2;
|
|
300
|
+
/** Structured-long guard against pathological megabytes: the tool call
|
|
301
|
+
* carries no length bound, so the structured path still ends — with an
|
|
302
|
+
* honest truncation note pointing at the archive, never silent soup. */
|
|
303
|
+
export const RICH_STRUCTURED_VALUE_BUDGET = 100_000;
|
|
304
|
+
|
|
305
|
+
/** Count markdown section headers (`##`–`####` with text) in raw text.
|
|
306
|
+
* H1 (`# `) is a document title, not a section — it does not count. */
|
|
307
|
+
export function countSectionHeaders(text: string): number {
|
|
308
|
+
const matches = (text ?? "").match(/^#{2,4}\s+\S/gm);
|
|
309
|
+
return matches ? matches.length : 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function isSectionStructured(text: string): boolean {
|
|
313
|
+
return countSectionHeaders(text) >= RICH_STRUCTURED_MIN_HEADERS;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const STRUCTURED_LABELS = ["Outcome:", "Changed:", "Evidence:", "Tests:", "Unresolved:", "Next:"];
|
|
317
|
+
|
|
318
|
+
/** Raw (newline-preserving) label value for the structured path. Labels
|
|
319
|
+
* anchor to line starts and the last restatement wins — the same
|
|
320
|
+
* doctrine as labelPositions on the flattened path (v0.38.45). Residual
|
|
321
|
+
* ambiguity (a structured doc quoting `Changed:` at a line start) cuts
|
|
322
|
+
* the value early and the detector usually falls back to clipped
|
|
323
|
+
* rendering — doubt resolves to today's behavior, never to invention. */
|
|
324
|
+
export function rawLabelValue(text: string, label: string): string | null {
|
|
325
|
+
const source = text ?? "";
|
|
326
|
+
if (!source) return null;
|
|
327
|
+
const openings: number[] = [];
|
|
328
|
+
const openPattern = new RegExp(`^${label}`, "gim");
|
|
329
|
+
for (const match of source.matchAll(openPattern)) openings.push(match.index ?? 0);
|
|
330
|
+
if (openings.length === 0) return null;
|
|
331
|
+
const start = openings[openings.length - 1]! + label.length;
|
|
332
|
+
const restPattern = new RegExp(`^(${STRUCTURED_LABELS.filter((entry) => entry !== label).join("|")})`, "gim");
|
|
333
|
+
restPattern.lastIndex = start;
|
|
334
|
+
const next = restPattern.exec(source);
|
|
335
|
+
const end = next ? (next.index ?? source.length) : source.length;
|
|
336
|
+
const value = source.slice(start, end).trim();
|
|
337
|
+
return value || null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Machine-token strip for structured lines. chatSafeDetailValue also
|
|
341
|
+
* collapses multi-space runs, which would damage markdown table
|
|
342
|
+
* alignment padding — structured lines keep their spacing and drop
|
|
343
|
+
* only the archive-only tokens. */
|
|
344
|
+
function stripMachineTokens(line: string): string {
|
|
345
|
+
return line
|
|
346
|
+
.replace(/(?:\/var)?\/tmp\/\S+/g, "")
|
|
347
|
+
.replace(/\S+\.tgz\b/g, "")
|
|
348
|
+
.replace(/\(\s*\)/g, "")
|
|
349
|
+
.replace(/\s+$/u, "");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Full Outcome body for the `### Summary` card section, or null when
|
|
353
|
+
* the value is not section-structured (caller keeps today's budgets).
|
|
354
|
+
* Newlines and tables survive; machine paths stay archive-only; the
|
|
355
|
+
* pathological guard ends with an honest pointer, never silent soup. */
|
|
356
|
+
export function structuredSummaryLines(summary: string | undefined, label = "Outcome:"): string[] | null {
|
|
357
|
+
const raw = rawLabelValue(summary ?? "", label);
|
|
358
|
+
if (!raw || !isSectionStructured(raw)) return null;
|
|
359
|
+
const lines = raw.split("\n").map(stripMachineTokens);
|
|
360
|
+
while (lines.length > 0 && !(lines[0] ?? "").trim()) lines.shift();
|
|
361
|
+
while (lines.length > 0 && !(lines[lines.length - 1] ?? "").trim()) lines.pop();
|
|
362
|
+
let text = lines.join("\n");
|
|
363
|
+
const units = [...text];
|
|
364
|
+
if (units.length > RICH_STRUCTURED_VALUE_BUDGET) {
|
|
365
|
+
text = `${units.slice(0, RICH_STRUCTURED_VALUE_BUDGET).join("")}\n\n… (truncated for chat — full text in the archived record.)`;
|
|
366
|
+
}
|
|
367
|
+
return text.split("\n");
|
|
368
|
+
}
|
|
292
369
|
|
|
293
370
|
export interface RichTerminalParts {
|
|
294
371
|
/** v0.38.55 (full parity): verdict banner — `## Done — auditor
|
|
@@ -303,6 +380,11 @@ export interface RichTerminalParts {
|
|
|
303
380
|
/** v0.38.55 (full parity): final repository state section lines.
|
|
304
381
|
* Empty when the state could not be read (absent stays absent). */
|
|
305
382
|
repoLines: string[];
|
|
383
|
+
/** Structured-long body (field 2026-09-16): full Outcome text with
|
|
384
|
+
* newlines preserved, rendered as `### Summary`. Empty unless the
|
|
385
|
+
* value earned the structured path — never a second copy of the
|
|
386
|
+
* clipped headline echo. */
|
|
387
|
+
summaryLines: string[];
|
|
306
388
|
}
|
|
307
389
|
|
|
308
390
|
function escapeTableCell(value: string): string {
|
|
@@ -472,6 +554,10 @@ export function buildRichTerminalParts(args: {
|
|
|
472
554
|
* tree cleanliness) from the render call site, which owns cwd access.
|
|
473
555
|
* Absent keeps the section out — never invented. */
|
|
474
556
|
repoState?: string[];
|
|
557
|
+
/** Structured-long body lines from structuredSummaryLines. Absent
|
|
558
|
+
* keeps the `### Summary` section out (unstructured summaries keep
|
|
559
|
+
* today's headline-echo shape). */
|
|
560
|
+
summaryLines?: string[];
|
|
475
561
|
}): RichTerminalParts {
|
|
476
562
|
const { findings, tests, next } = partitionRichDetails(args.details);
|
|
477
563
|
const outcome = args.outcome;
|
|
@@ -573,6 +659,7 @@ export function buildRichTerminalParts(args: {
|
|
|
573
659
|
tableLines,
|
|
574
660
|
nextLines,
|
|
575
661
|
repoLines: args.repoState ?? [],
|
|
662
|
+
summaryLines: args.summaryLines ?? [],
|
|
576
663
|
};
|
|
577
664
|
}
|
|
578
665
|
|
|
@@ -587,6 +674,14 @@ export function composeRichTerminalLines(parts: RichTerminalParts): string[] {
|
|
|
587
674
|
const lines = [parts.banner ?? parts.headline, ""];
|
|
588
675
|
if (parts.headline !== lines[0]) lines.push(parts.headline, "");
|
|
589
676
|
if (parts.durationLine) lines.push(parts.durationLine, "");
|
|
677
|
+
// Structured-long (field 2026-09-16): the full Outcome body rides its
|
|
678
|
+
// own section between the headline and the findings — findings-first
|
|
679
|
+
// order is preserved (findings, verification, Next keep their relative
|
|
680
|
+
// order), the headline echo stays short, and the one-action Next rule
|
|
681
|
+
// is untouched.
|
|
682
|
+
if (parts.summaryLines.length > 0) {
|
|
683
|
+
lines.push("### Summary", ...parts.summaryLines, "");
|
|
684
|
+
}
|
|
590
685
|
if (parts.findingLines.length > 0) {
|
|
591
686
|
lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
|
|
592
687
|
}
|
|
@@ -611,6 +706,7 @@ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath:
|
|
|
611
706
|
const facts: CompletionSummaryFacts = { goal, status, archivePath };
|
|
612
707
|
const summary = resolveCompletionSummary(facts, goal.completionSummary).summary;
|
|
613
708
|
const brief = humanCompletionBrief(summary, 140, RICH_FULL_VALUE_BUDGET);
|
|
709
|
+
const structured = structuredSummaryLines(summary);
|
|
614
710
|
const history = goal.auditHistory ?? [];
|
|
615
711
|
const latest = history[history.length - 1];
|
|
616
712
|
const approval = latest
|
|
@@ -628,6 +724,7 @@ export function buildRichArchiveSection(goal: Goal, status: Status, archivePath:
|
|
|
628
724
|
gates: gateRows,
|
|
629
725
|
// v0.38.55: aborted records never wear a Done banner/headline.
|
|
630
726
|
kind: status === "complete" ? "Done" : "Aborted",
|
|
727
|
+
...(structured ? { summaryLines: structured } : {}),
|
|
631
728
|
});
|
|
632
729
|
return [
|
|
633
730
|
...composeRichTerminalLines(parts),
|
|
@@ -778,6 +875,11 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
778
875
|
140,
|
|
779
876
|
RICH_FULL_VALUE_BUDGET,
|
|
780
877
|
);
|
|
878
|
+
// Structured-long (field 2026-09-16): a section-structured Outcome
|
|
879
|
+
// earns the full `### Summary` section on the terminal card (and the
|
|
880
|
+
// archive human layer). Headline echo, verification, Next, recap, and
|
|
881
|
+
// every recycled payload keep their bounds.
|
|
882
|
+
const structured = structuredSummaryLines(resolveCompletionSummary(facts, candidate).summary);
|
|
781
883
|
// v0.38.37 (audit 2026-09-08): the deliberate non-do comes from the
|
|
782
884
|
// agent's complete_goal leftOut claim — never invented. Filler ("none")
|
|
783
885
|
// drops via the same briefValueContent filter; absent stays absent.
|
|
@@ -821,6 +923,7 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
|
|
|
821
923
|
groups: input.findingGroups,
|
|
822
924
|
gates: input.gateRows,
|
|
823
925
|
...(input.repoState ? { repoState: input.repoState } : {}),
|
|
926
|
+
...(structured ? { summaryLines: structured } : {}),
|
|
824
927
|
});
|
|
825
928
|
const chatBody = composeRichTerminalLines(richParts);
|
|
826
929
|
const transcriptBody = composeRichTerminalLines(richParts);
|
|
@@ -39,7 +39,7 @@ import { formatGllaVersion } from "./glla-version.js";
|
|
|
39
39
|
import { cmdGllaOwner, cmdGllaTakeover } from "./state-root-owner.js";
|
|
40
40
|
import { AUDIT_JOB_CLEANUP_MIN_AGE_MS, cancelDetachedGoalCompletionAuditor, cleanupDeadAuditJobs, inspectAuditJobHealth, DEFAULT_AUDITOR_STALL_MS, DEFAULT_AUDITOR_TOOL_TIMEOUT_MS } from "./goal-loop-auditor-process.js";
|
|
41
41
|
import { releaseAuditorSurface } from "./loops/goal-auditor-surface.js";
|
|
42
|
-
import { inferStartFromSession, type StartContextInference } from "./start-context.js";
|
|
42
|
+
import { inferStartFromSession, resolveTweakReplacement, type StartContextInference } from "./start-context.js";
|
|
43
43
|
|
|
44
44
|
/** Child pi sessions live under the shared session store, munged by cwd
|
|
45
45
|
* (verified layout: ~/.pi/agent/sessions/--home-user-proj--/*.jsonl). */
|
|
@@ -755,11 +755,17 @@ export async function cmdTweak(
|
|
|
755
755
|
&& current?.policy === "list"
|
|
756
756
|
&& (current.status === "active" || current.status === "auditing");
|
|
757
757
|
const expectedStatus = mode === "list" ? "paused" : "active";
|
|
758
|
-
|
|
758
|
+
// Field 2026-09-16 (the VidPro park): /goal tweak accepts a paused goal
|
|
759
|
+
// instead of stranding it behind a resume round-trip. Mirrors /list
|
|
760
|
+
// tweak, which is paused-only and stays paused.
|
|
761
|
+
const tweakable = mode === "list"
|
|
762
|
+
? current?.status === expectedStatus
|
|
763
|
+
: current?.status === "active" || current?.status === "paused";
|
|
764
|
+
if (!current || (!tweakable && !liveListConflict) || (mode === "list" && current.policy !== "list")) {
|
|
759
765
|
ctx.ui.notify(
|
|
760
766
|
mode === "list"
|
|
761
767
|
? "No paused list item to tweak. /list tweak <replacement objective, optional 'Done when: ...' clause>"
|
|
762
|
-
: "No
|
|
768
|
+
: "No goal to tweak. /goal <objective> to start one.",
|
|
763
769
|
"info",
|
|
764
770
|
);
|
|
765
771
|
return false;
|
|
@@ -797,7 +803,29 @@ export async function cmdTweak(
|
|
|
797
803
|
}
|
|
798
804
|
raw = v.trim();
|
|
799
805
|
}
|
|
800
|
-
|
|
806
|
+
// Field 2026-09-16 (VidPro ledger — `/goal tweak` adopted "ok adjsut it"
|
|
807
|
+
// as the objective): chatter is a pointer at the session context, not a
|
|
808
|
+
// replacement. Resolve it against what was actually discussed; guide when
|
|
809
|
+
// nothing clear was discussed. Raw chatter never reaches the ledger.
|
|
810
|
+
// This sits in cmdTweak because all adoption paths funnel here: /goal
|
|
811
|
+
// tweak, /list tweak, and the plain-/goal "Update current objective"
|
|
812
|
+
// conflict choice (via updateWholeObjectiveFromConflict).
|
|
813
|
+
const replacement = resolveTweakReplacement(raw, ctx.sessionManager);
|
|
814
|
+
let adopted = raw;
|
|
815
|
+
let inferredNote = "";
|
|
816
|
+
if (replacement.kind === "inferred") {
|
|
817
|
+
adopted = replacement.text;
|
|
818
|
+
inferredNote = ` (inferred from ${replacement.source === "recent-context" ? "recent discussion" : "your message"} — yours read "${displaySlice(raw, 80)}"; cancel and re-run with exact wording if this is wrong)`;
|
|
819
|
+
} else if (replacement.kind === "unresolvable") {
|
|
820
|
+
const surface = mode === "list" ? "list item" : "goal";
|
|
821
|
+
let guidance = `That reads as an acknowledgment rather than a replacement ("${displaySlice(raw, 80)}") — nothing changed. The ${surface} keeps: ${displaySlice(current.objective, 120)}. Say what it should become as full replacement text.`;
|
|
822
|
+
if (replacement.reason === "ambiguous" && replacement.candidates.length > 0) {
|
|
823
|
+
guidance += ` It could mean: ${replacement.candidates.slice(0, 2).map((c) => `"${displaySlice(c, 100)}"`).join(" / ")} — re-run with one in full.`;
|
|
824
|
+
}
|
|
825
|
+
ctx.ui.notify(guidance, "info");
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const proposed = extractVerificationContract(adopted);
|
|
801
829
|
const newObjective = proposed.objective;
|
|
802
830
|
if (!newObjective) {
|
|
803
831
|
// (a bare input of "Done when: ..." only yields this — the replacement
|
|
@@ -833,7 +861,7 @@ export async function cmdTweak(
|
|
|
833
861
|
try {
|
|
834
862
|
confirmed = await ctx.ui.confirm(
|
|
835
863
|
mode === "list" ? "Tweak list item?" : "Tweak goal?",
|
|
836
|
-
`CURRENT:\n${sanitizeDisplayText(current.objective)}\n\nNEW:\n${sanitizeDisplayText(newObjective)}` +
|
|
864
|
+
`CURRENT:\n${sanitizeDisplayText(current.objective)}\n\nNEW${inferredNote}:\n${sanitizeDisplayText(newObjective)}` +
|
|
837
865
|
(hasNewContract
|
|
838
866
|
? `\n\nNew contract:\n${sanitizeDisplayText(proposed.verificationContract)}`
|
|
839
867
|
: clearsContract
|
|
@@ -862,7 +890,7 @@ export async function cmdTweak(
|
|
|
862
890
|
return false;
|
|
863
891
|
}
|
|
864
892
|
const priorProvenance = latest.objectiveProvenance;
|
|
865
|
-
const userSeeds = [...(priorProvenance?.userSeeds ?? []),
|
|
893
|
+
const userSeeds = [...(priorProvenance?.userSeeds ?? []), adopted].slice(-10);
|
|
866
894
|
const replacingLiveList = liveListConflict
|
|
867
895
|
&& latest.policy === "list"
|
|
868
896
|
&& (latest.status === "active" || latest.status === "auditing");
|
|
@@ -921,6 +949,14 @@ export async function cmdTweak(
|
|
|
921
949
|
ctx.ui.notify("List item tweaked; it remains paused. /list resume to continue.", "info");
|
|
922
950
|
return true;
|
|
923
951
|
}
|
|
952
|
+
// A paused goal stays paused — the tweak must not wake the loop (field
|
|
953
|
+
// 2026-09-16: the VidPro park re-parked on every wake, which is the
|
|
954
|
+
// hamster wheel in the report). The user resumes explicitly when the
|
|
955
|
+
// blocker clears.
|
|
956
|
+
if (latest.status === "paused") {
|
|
957
|
+
ctx.ui.notify("Goal tweaked; it remains paused. /goal resume to continue.", "info");
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
924
960
|
ctx.ui.notify("Goal tweaked. The loop continues against the new objective.", "info");
|
|
925
961
|
scheduleContinuation(ctx, true);
|
|
926
962
|
return true;
|
|
@@ -57,7 +57,12 @@ export function isLifecycleHeldLoopReason(reason?: string): boolean {
|
|
|
57
57
|
/** A stopped loop can be respecified without discarding its history when the
|
|
58
58
|
* stop is a recoverable work failure or an explicit time/token window. Max
|
|
59
59
|
* iterations and clean/user stops remain terminal until a fresh `/loop start`.
|
|
60
|
-
|
|
60
|
+
* Field 2026-09-16 (loop sweep): parked loops (user pause, restore-hold,
|
|
61
|
+
* zero-stream abort, user-stop-with-em-dash) are refinable too — parity with
|
|
62
|
+
* the slash-side RESUMABLE_STOP so both surfaces agree on what can be
|
|
63
|
+
* rescoped. `measure command broken` stays tool-only by design: the agent
|
|
64
|
+
* fixes the measure here, while slash resume must not restart a broken
|
|
65
|
+
* loop until it is fixed. */
|
|
61
66
|
export function isRefinableStoppedLoopReason(reason?: string): boolean {
|
|
62
67
|
return !!reason && (
|
|
63
68
|
reason.startsWith("time bound reached")
|
|
@@ -68,6 +73,10 @@ export function isRefinableStoppedLoopReason(reason?: string): boolean {
|
|
|
68
73
|
|| reason.startsWith("measure command broken —")
|
|
69
74
|
|| reason.startsWith("provider errors —")
|
|
70
75
|
|| reason.startsWith("stalled:")
|
|
76
|
+
|| reason === HELD_ON_RESTORE
|
|
77
|
+
|| reason.startsWith("paused by user (/loop pause)")
|
|
78
|
+
|| reason.startsWith("stopped: automatic zero-stream abort")
|
|
79
|
+
|| reason.startsWith("stopped by user —")
|
|
71
80
|
);
|
|
72
81
|
}
|
|
73
82
|
|
package/extensions/goal-loop.ts
CHANGED
|
@@ -215,6 +215,46 @@ function isLoopActive(): boolean {
|
|
|
215
215
|
return !!state.loop?.active;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Resumable-stop predicate, hoisted to module scope (field 2026-09-16,
|
|
219
|
+
// loop sweep): /loop resume and /loop refine share it so a held loop is
|
|
220
|
+
// refinable wherever it is resumable.
|
|
221
|
+
// v0.29.20: plain plateau stops are resumable too — pre-gate plateaus
|
|
222
|
+
// could be false (hegemon/polis stopped 2026-07-31 with open findings
|
|
223
|
+
// on 429-dead turns), and an explicit resume is the user's call; the
|
|
224
|
+
// v0.29.19 gate + re-armed counters make the resumed run honest.
|
|
225
|
+
export const RESUMABLE_STOP = (r?: string): boolean =>
|
|
226
|
+
r === HELD_ON_RESTORE ||
|
|
227
|
+
!!r?.startsWith("provider errors —") ||
|
|
228
|
+
!!r?.startsWith("stopped by user —") ||
|
|
229
|
+
!!r?.startsWith("plateau —") ||
|
|
230
|
+
!!r?.startsWith("stalled:") ||
|
|
231
|
+
!!r?.startsWith("stuck —") ||
|
|
232
|
+
!!r?.startsWith("time bound reached") ||
|
|
233
|
+
!!r?.startsWith("token budget exhausted") ||
|
|
234
|
+
// v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
|
|
235
|
+
// moved" stop message promises "/loop resume retries or /loop stop",
|
|
236
|
+
// but this predicate never matched that prefix — the promised command
|
|
237
|
+
// answered "No held loop to resume" and, with propose_loop_refine
|
|
238
|
+
// gated on an ACTIVE loop, the only recovery was /loop stop + a fresh
|
|
239
|
+
// start discarding iteration history. Same class as the v0.35.25
|
|
240
|
+
// issue-#14 zombie prefix (fixed there, missed for this brand-new
|
|
241
|
+
// prefix). Resuming re-arms the counters; if the metric is still dead
|
|
242
|
+
// it re-stops loudly after its window — and a measure-changing
|
|
243
|
+
// propose_loop_refine (usable again once resumed) re-scopes the era so
|
|
244
|
+
// the never-moved grace re-arms.
|
|
245
|
+
!!r?.startsWith("metric never moved —") ||
|
|
246
|
+
// v0.35.25 (issue #14): abortZombieRun parks with
|
|
247
|
+
// "stopped: automatic zero-stream abort — … (/loop resume to retry)"
|
|
248
|
+
// and its user-facing message PROMISES that resume command. The
|
|
249
|
+
// predicate it lands in never matched the prefix, so /loop resume
|
|
250
|
+
// answered "No held loop to resume" and the preserved iteration,
|
|
251
|
+
// best value, and history were unreachable without re-drafting.
|
|
252
|
+
!!r?.startsWith("stopped: automatic zero-stream abort") ||
|
|
253
|
+
// v0.38.27 (ported from Bjynt's PR #46): /loop pause is a soft-hold
|
|
254
|
+
// parallel to /goal pause. The loop stays resumable; finishLoopGit
|
|
255
|
+
// was skipped, so resume picks up iteration/best/history verbatim.
|
|
256
|
+
!!r?.startsWith("paused by user (/loop pause)");
|
|
257
|
+
|
|
218
258
|
async function resolveLoopStartConflict(ctx: ExtensionContext, target: string): Promise<boolean> {
|
|
219
259
|
const current = liveObjectives(state);
|
|
220
260
|
if (current.length === 0) return true;
|
|
@@ -994,42 +1034,6 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
994
1034
|
return;
|
|
995
1035
|
}
|
|
996
1036
|
const stored = state.loop;
|
|
997
|
-
// v0.29.20: plain plateau stops are resumable too — pre-gate plateaus
|
|
998
|
-
// could be false (hegemon/polis stopped 2026-07-31 with open findings
|
|
999
|
-
// on 429-dead turns), and an explicit resume is the user's call; the
|
|
1000
|
-
// v0.29.19 gate + re-armed counters make the resumed run honest.
|
|
1001
|
-
const RESUMABLE_STOP = (r?: string): boolean =>
|
|
1002
|
-
r === HELD_ON_RESTORE ||
|
|
1003
|
-
!!r?.startsWith("provider errors —") ||
|
|
1004
|
-
!!r?.startsWith("stopped by user —") ||
|
|
1005
|
-
!!r?.startsWith("plateau —") ||
|
|
1006
|
-
!!r?.startsWith("stalled:") ||
|
|
1007
|
-
!!r?.startsWith("stuck —") ||
|
|
1008
|
-
!!r?.startsWith("time bound reached") ||
|
|
1009
|
-
!!r?.startsWith("token budget exhausted") ||
|
|
1010
|
-
// v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
|
|
1011
|
-
// moved" stop message promises "/loop resume retries or /loop stop",
|
|
1012
|
-
// but this predicate never matched that prefix — the promised command
|
|
1013
|
-
// answered "No held loop to resume" and, with propose_loop_refine
|
|
1014
|
-
// gated on an ACTIVE loop, the only recovery was /loop stop + a fresh
|
|
1015
|
-
// start discarding iteration history. Same class as the v0.35.25
|
|
1016
|
-
// issue-#14 zombie prefix (fixed there, missed for this brand-new
|
|
1017
|
-
// prefix). Resuming re-arms the counters; if the metric is still dead
|
|
1018
|
-
// it re-stops loudly after its window — and a measure-changing
|
|
1019
|
-
// propose_loop_refine (usable again once resumed) re-scopes the era so
|
|
1020
|
-
// the never-moved grace re-arms.
|
|
1021
|
-
!!r?.startsWith("metric never moved —") ||
|
|
1022
|
-
// v0.35.25 (issue #14): abortZombieRun parks with
|
|
1023
|
-
// "stopped: automatic zero-stream abort — … (/loop resume to retry)"
|
|
1024
|
-
// and its user-facing message PROMISES that resume command. The
|
|
1025
|
-
// predicate it lands in never matched the prefix, so /loop resume
|
|
1026
|
-
// answered "No held loop to resume" and the preserved iteration,
|
|
1027
|
-
// best value, and history were unreachable without re-drafting.
|
|
1028
|
-
!!r?.startsWith("stopped: automatic zero-stream abort") ||
|
|
1029
|
-
// v0.38.27 (ported from Bjynt's PR #46): /loop pause is a soft-hold
|
|
1030
|
-
// parallel to /goal pause. The loop stays resumable; finishLoopGit
|
|
1031
|
-
// was skipped, so resume picks up iteration/best/history verbatim.
|
|
1032
|
-
!!r?.startsWith("paused by user (/loop pause)");
|
|
1033
1037
|
if (stored && !stored.active && RESUMABLE_STOP(stored.stopReason)) {
|
|
1034
1038
|
// Branch-mode stop returns HEAD to originalBranch. Refuse a resume from
|
|
1035
1039
|
// there rather than letting the next tick commit loop work to the
|
|
@@ -1202,10 +1206,25 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1202
1206
|
// queues the operator's suggestion into the next iteration's prompt.
|
|
1203
1207
|
// ("polish" accepted as an alias: the widget footer advertised it
|
|
1204
1208
|
// before the command existed — now it does.)
|
|
1205
|
-
|
|
1209
|
+
// Field 2026-09-16 (loop sweep): a resumable-held loop is refinable —
|
|
1210
|
+
// the hint rides the next iteration whenever it resumes. A dead loop
|
|
1211
|
+
// (user stop/finish/maxed) stays refused with the honest fresh-start
|
|
1212
|
+
// path; queuing onto it would promise a prompt that never comes.
|
|
1213
|
+
const stored = state.loop;
|
|
1214
|
+
if (!stored) {
|
|
1206
1215
|
ctx.ui.notify("No active loop to refine — /loop start first.", "warning");
|
|
1207
1216
|
return;
|
|
1208
1217
|
}
|
|
1218
|
+
if (!stored.active && !RESUMABLE_STOP(stored.stopReason)) {
|
|
1219
|
+
// A broken measure is fixed from the agent side, not by restarting:
|
|
1220
|
+
// point there instead of at a fresh start.
|
|
1221
|
+
if (stored.stopReason?.startsWith("measure command broken —")) {
|
|
1222
|
+
ctx.ui.notify("That loop stopped on a broken measure — the hint was not queued. Have the agent repair it via propose_loop_refine (it test-runs the new measure), then /loop resume.", "warning");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
ctx.ui.notify(`That loop ended (${stored.stopReason ?? "stopped"}) — the hint was not queued. /loop start begins a fresh run.`, "warning");
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1209
1228
|
const hint = rest.trim();
|
|
1210
1229
|
if (!hint) {
|
|
1211
1230
|
ctx.ui.notify("Usage: /loop refine <what the spec should capture better> — the suggestion rides the next iteration's prompt; the agent proposes via propose_loop_refine and you confirm.", "info");
|
|
@@ -1214,6 +1233,13 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1214
1233
|
state.loop!.refineHint = hint.slice(0, 300);
|
|
1215
1234
|
persistState(ctx);
|
|
1216
1235
|
appendLedger(ctx.cwd, "loop_refine_hint", { iteration: state.loop!.iteration, hint: state.loop!.refineHint });
|
|
1236
|
+
if (!stored.active) {
|
|
1237
|
+
// Refine-and-resume (owner choice): the hint is queued first, then
|
|
1238
|
+
// the byte-identical resume path runs — wrong-branch and active-goal
|
|
1239
|
+
// guards keep their voices and the loop stays held when they fire.
|
|
1240
|
+
ctx.ui.notify("Refine hint queued — it rides the next iteration's prompt. Resuming the held loop now.", "info");
|
|
1241
|
+
return cmdLoop("resume", ctx);
|
|
1242
|
+
}
|
|
1217
1243
|
ctx.ui.notify("Refine hint queued — it rides the next iteration's prompt.", "info");
|
|
1218
1244
|
return;
|
|
1219
1245
|
}
|
|
@@ -524,6 +524,8 @@ function registerAgentTools(pi: any): void {
|
|
|
524
524
|
"Outcome: <what was delivered> · Changed: <files/behavior/decision> · Evidence: <key commit/report/result> · " +
|
|
525
525
|
"Tests: <bounded commands + pass/fail, or `not run — <reason>`> · Unresolved: <remaining risk, or `none`> · " +
|
|
526
526
|
"Next: <one follow-up hint, or `none`>. " +
|
|
527
|
+
"A section-structured Outcome (##/### headers) renders in full as the card Summary section — " +
|
|
528
|
+
"structure is the price of length. " +
|
|
527
529
|
"See audit/COMPLETION-SUMMARY-POLICY-2026-08-19.md and docs/DESIGN-long-running-supervision.md. Free-form prose is allowed but discouraged; incomplete input receives a recorded-facts-only fallback at terminalization.",
|
|
528
530
|
})),
|
|
529
531
|
verificationSummary: Type.Optional(Type.String({
|
|
@@ -297,3 +297,44 @@ export function inferStartFromSession(sessionManager: unknown): StartContextInfe
|
|
|
297
297
|
currentPromptTruncated: window.currentPromptTruncated,
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
|
+
|
|
301
|
+
/** Acknowledgment openers: the user is responding to a proposal, not
|
|
302
|
+
* stating an objective. `\b` keeps "goals"/"going" out. */
|
|
303
|
+
const ACK_PREFIX_RE = /^(?:ok(?:ay)?|yes|yeah|yep|sure|go|fine|great|perfect|thanks?|thank\s+you)\b[\s,—–-]*/i;
|
|
304
|
+
|
|
305
|
+
/** Field 2026-09-16 (VidPro ledger: `/goal tweak` adopted "ok adjsut it"
|
|
306
|
+
* as the objective): chatter is an acknowledgment / pronoun / bare vague
|
|
307
|
+
* action — possibly behind an ack opener ("ok adjust it") — never a
|
|
308
|
+
* self-contained objective. Deliberately NOT length-based: short-but-real
|
|
309
|
+
* objectives ("fix typo in X") pass through untouched. Questions pass
|
|
310
|
+
* through too — only the acknowledgment shapes divert. */
|
|
311
|
+
export function isChatterReplacement(text: string): boolean {
|
|
312
|
+
const clean = normalizeWhitespace(text ?? "");
|
|
313
|
+
if (!clean) return false;
|
|
314
|
+
if (GENERIC_REPLY_RE.test(clean) || PRONOUN_ONLY_RE.test(clean) || VAGUE_ACTION_RE.test(clean)) return true;
|
|
315
|
+
const stripped = clean.replace(ACK_PREFIX_RE, "").trim();
|
|
316
|
+
if (stripped !== clean) return stripped ? classifyCandidate(stripped) !== "clear" : true;
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export type TweakReplacement =
|
|
321
|
+
| { kind: "verbatim" }
|
|
322
|
+
| { kind: "inferred"; text: string; source: "current-prompt" | "recent-context" }
|
|
323
|
+
| { kind: "unresolvable"; reason: "ambiguous" | "none"; candidates: string[] };
|
|
324
|
+
|
|
325
|
+
/** Resolve a tweak/update replacement against what was actually discussed.
|
|
326
|
+
* Self-contained text is adopted verbatim (today's path, byte-identical).
|
|
327
|
+
* Chatter ("go fix it", "ok adjsut it") resolves via the session
|
|
328
|
+
* context into the discussed objective — the user pointed at the context,
|
|
329
|
+
* so the context supplies the words. When nothing clear was discussed,
|
|
330
|
+
* the caller guides instead of adopting garbage: raw chatter must never
|
|
331
|
+
* land in the ledger as an objective again. */
|
|
332
|
+
export function resolveTweakReplacement(raw: string, sessionManager: unknown): TweakReplacement {
|
|
333
|
+
if (!isChatterReplacement(raw)) return { kind: "verbatim" };
|
|
334
|
+
const inference = inferStartFromSession(sessionManager);
|
|
335
|
+
if (inference.kind === "clear") return { kind: "inferred", text: inference.objective, source: inference.source };
|
|
336
|
+
if (inference.kind === "ambiguous") {
|
|
337
|
+
return { kind: "unresolvable", reason: "ambiguous", candidates: inference.candidates };
|
|
338
|
+
}
|
|
339
|
+
return { kind: "unresolvable", reason: "none", candidates: [] };
|
|
340
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.56",
|
|
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": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|