pi-goal-list-loop-audit 0.35.13 → 0.35.32
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 +366 -0
- package/INSTALL.md +3 -3
- package/LIST-PHILOSOPHY.md +90 -0
- package/PLAN.md +316 -0
- package/README.md +108 -13
- package/docs/DESIGN-subagent-visibility.md +76 -0
- package/docs/DESIGN.md +59 -8
- package/docs/GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md +6 -1
- package/docs/INDEX.md +4 -0
- package/docs/VISION-ASSIST.md +2 -2
- package/extensions/auditor-extensions.ts +289 -0
- package/extensions/goal-agents-panel.ts +185 -0
- package/extensions/goal-commands.ts +120 -6
- package/extensions/goal-continuation.ts +45 -2
- package/extensions/goal-heartbeat.ts +176 -5
- package/extensions/goal-loop-auditor-process.ts +26 -0
- package/extensions/goal-loop-auditor.ts +3 -5
- package/extensions/goal-loop-backoff.ts +79 -0
- package/extensions/goal-loop-core.ts +97 -4
- package/extensions/goal-loop-display.ts +107 -8
- package/extensions/goal-loop-forever.ts +39 -1
- package/extensions/goal-loop-shield.ts +114 -25
- package/extensions/goal-loop.ts +66 -1
- package/extensions/goal-recovery.ts +8 -2
- package/extensions/goal-settings.ts +23 -0
- package/extensions/goal-state.ts +11 -0
- package/extensions/loops/goal-activation.ts +226 -25
- package/extensions/loops/goal-auditor-hooks.ts +39 -3
- package/extensions/loops/goal-list-queue.ts +17 -1
- package/extensions/loops/goal-orchestrator.ts +28 -1
- package/extensions/loops/goal-settings-ui.ts +96 -5
- package/extensions/loops/goal-tools.ts +82 -28
- package/extensions/loops/goal-ui.ts +105 -1
- package/extensions/loops/goal.ts +7 -7
- package/extensions/multi-model-picker.ts +45 -7
- package/extensions/settings-menu.ts +10 -0
- package/package.json +8 -3
- package/schemas/goal.schema.json +2 -0
- package/scripts/goal-auditor-launch.mjs +19 -1
- package/scripts/goal-auditor-worker.mjs +15 -0
- package/scripts/verify-auditor-extensions-offline.mjs +59 -0
|
@@ -247,6 +247,10 @@ import {
|
|
|
247
247
|
type MultiModelPickerResult,
|
|
248
248
|
} from "../multi-model-picker.js";
|
|
249
249
|
import { consumeRecoveryResume } from "../goal-recovery.js"; // decomposition step 3 (v0.34.111)
|
|
250
|
+
import {
|
|
251
|
+
discoverAuditorExtensions,
|
|
252
|
+
normalizeAuditorAllowedExtensions,
|
|
253
|
+
} from "../auditor-extensions.js";
|
|
250
254
|
import {
|
|
251
255
|
createGoalHeartbeat,
|
|
252
256
|
endSubagentHangProbe,
|
|
@@ -672,11 +676,25 @@ async function promptModelRef(
|
|
|
672
676
|
ctx: ExtensionContext,
|
|
673
677
|
title: string,
|
|
674
678
|
emptyLabel: string,
|
|
679
|
+
// v0.35.24 (note.md Next #1): auditor-slot parity with the main agent
|
|
680
|
+
// selector — forbidden-models policy filters BOTH the picker list and the
|
|
681
|
+
// typed escape hatch. Without it the user could pin a model the resolver
|
|
682
|
+
// would silently skip at audit time (auditor_model_fallback reason:
|
|
683
|
+
// "forbidden"), a dead-end selection the main selector never offers.
|
|
684
|
+
opts: { excludeRefs?: readonly string[] } = {},
|
|
675
685
|
): Promise<{ kind: "session" } | { kind: "ref"; ref: string } | undefined> {
|
|
686
|
+
const exclude = opts.excludeRefs ?? [];
|
|
687
|
+
const validateTypedRef = (ref: string): { kind: "session" } | { kind: "ref"; ref: string } | undefined => {
|
|
688
|
+
if (exclude.length > 0 && isForbiddenModel(ref, exclude)) {
|
|
689
|
+
ctx.ui.notify(`"${ref}" matches your forbidden-models policy — the picker refuses it. Adjust /glla → Forbidden models first.`, "warning");
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
return { kind: "ref", ref };
|
|
693
|
+
};
|
|
676
694
|
const inputFallback = async (): Promise<{ kind: "session" } | { kind: "ref"; ref: string } | undefined> => {
|
|
677
695
|
const v = await ctx.ui.input(title, "provider/model-id — empty keeps the default");
|
|
678
696
|
if (v === undefined) return undefined;
|
|
679
|
-
return v.trim() ?
|
|
697
|
+
return v.trim() ? validateTypedRef(v.trim()) : { kind: "session" };
|
|
680
698
|
};
|
|
681
699
|
if (typeof (ctx.ui as { custom?: unknown }).custom !== "function" || !ctx.modelRegistry) {
|
|
682
700
|
return inputFallback();
|
|
@@ -686,7 +704,7 @@ async function promptModelRef(
|
|
|
686
704
|
const models = ctx.modelRegistry
|
|
687
705
|
.getAvailable()
|
|
688
706
|
.filter((m: any) => ctx.modelRegistry.hasConfiguredAuth(m));
|
|
689
|
-
const items = buildModelPickItems(models, sessionLabel);
|
|
707
|
+
const items = buildModelPickItems(models, sessionLabel, { excludeRefs: exclude });
|
|
690
708
|
let factoryInvoked = false;
|
|
691
709
|
const pick = await ctx.ui.custom<ModelPickItem | undefined>((tui, theme, keybindings, done) => {
|
|
692
710
|
factoryInvoked = true;
|
|
@@ -701,7 +719,7 @@ async function promptModelRef(
|
|
|
701
719
|
// manual escape hatch — typed provider/model, validated like before
|
|
702
720
|
const v = await ctx.ui.input(title, emptyLabel);
|
|
703
721
|
if (v === undefined) return undefined;
|
|
704
|
-
return v.trim() ?
|
|
722
|
+
return v.trim() ? validateTypedRef(v.trim()) : { kind: "session" };
|
|
705
723
|
}
|
|
706
724
|
|
|
707
725
|
/** Multi-select counterpart to promptModelRef: returns an ordered string[] of
|
|
@@ -1089,7 +1107,11 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
1089
1107
|
return;
|
|
1090
1108
|
}
|
|
1091
1109
|
case "auditorModel": {
|
|
1092
|
-
|
|
1110
|
+
// v0.35.24 (note.md Next #1): parity with the main agent selector —
|
|
1111
|
+
// the auditor picker filters forbidden models out of the list AND
|
|
1112
|
+
// refuses typed policy matches, so a pin saved here is one the
|
|
1113
|
+
// resolver will actually honor.
|
|
1114
|
+
const pick = await promptModelRef(ctx, "Auditor model override", "provider/model-id — empty keeps the pi session model", { excludeRefs: loadSettings(ctx.cwd).forbiddenModels });
|
|
1093
1115
|
if (pick === undefined) return;
|
|
1094
1116
|
saveSettings("global", ctx.cwd, { auditorModel: pick.kind === "session" ? undefined : pick.ref });
|
|
1095
1117
|
// v0.31.4: thinking is chosen WITH the model (user: "we are setting
|
|
@@ -1146,12 +1168,81 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
1146
1168
|
return;
|
|
1147
1169
|
}
|
|
1148
1170
|
case "auditorModelFallback": {
|
|
1149
|
-
const pick = await promptModelRef(ctx, "Auditor fallback agent (runtime failure or same-session swap)", "provider/model-id — empty clears the fallback");
|
|
1171
|
+
const pick = await promptModelRef(ctx, "Auditor fallback agent (runtime failure or same-session swap)", "provider/model-id — empty clears the fallback", { excludeRefs: loadSettings(ctx.cwd).forbiddenModels });
|
|
1150
1172
|
if (pick === undefined) return;
|
|
1151
1173
|
saveSettings("global", ctx.cwd, { auditorModelFallback: pick.kind === "session" ? undefined : pick.ref });
|
|
1152
1174
|
if (pick.kind === "session") ctx.ui.notify("Auditor fallback cleared — a session on the pinned auditor model keeps that model.", "info");
|
|
1153
1175
|
return;
|
|
1154
1176
|
}
|
|
1177
|
+
case "auditorAllowedExtensions": {
|
|
1178
|
+
// v0.36.0 (GitHub issue: extension-based model providers in the
|
|
1179
|
+
// detached auditor). The picker lists every extension a normal pi
|
|
1180
|
+
// session would load, RESOLVED to concrete install paths (raw npm:/git:
|
|
1181
|
+
// specs are not directly loadable by the detached worker — see
|
|
1182
|
+
// extensions/auditor-extensions.ts); selected paths ride the auditor
|
|
1183
|
+
// request and the worker passes them as `pi --extension <path>` under
|
|
1184
|
+
// the still intact --no-extensions discovery off switch.
|
|
1185
|
+
const currentExts = normalizeAuditorAllowedExtensions(loadGlobalSettings().auditorAllowedExtensions);
|
|
1186
|
+
const discovered = discoverAuditorExtensions(os.homedir(), ctx.cwd);
|
|
1187
|
+
const discoveredSpecs = new Set(discovered.map((entry) => entry.spec));
|
|
1188
|
+
const extItems: ModelPickItem[] = [
|
|
1189
|
+
...discovered.map((entry) => ({
|
|
1190
|
+
kind: "model" as const,
|
|
1191
|
+
ref: entry.spec,
|
|
1192
|
+
label: `${entry.label} · ${entry.source}`,
|
|
1193
|
+
searchText: `${entry.spec} ${entry.label} ${entry.source}`,
|
|
1194
|
+
})),
|
|
1195
|
+
// Saved specs that discovery can no longer see (removed package,
|
|
1196
|
+
// another machine's home) still render so the user can deselect
|
|
1197
|
+
// them instead of a silently immortal settings entry.
|
|
1198
|
+
...currentExts.filter((spec) => !discoveredSpecs.has(spec)).map((spec) => ({
|
|
1199
|
+
kind: "model" as const,
|
|
1200
|
+
ref: spec,
|
|
1201
|
+
label: `${spec} · saved, not discovered here`,
|
|
1202
|
+
searchText: spec,
|
|
1203
|
+
})),
|
|
1204
|
+
];
|
|
1205
|
+
const extInputFallback = async (): Promise<string[] | undefined> => {
|
|
1206
|
+
const v = await ctx.ui.input(
|
|
1207
|
+
"Auditor allowed extensions — comma-separated pi extension specs",
|
|
1208
|
+
currentExts.length ? currentExts.join(",") : "npm:package-name or /path/to/extension",
|
|
1209
|
+
);
|
|
1210
|
+
if (v === undefined) return undefined;
|
|
1211
|
+
return normalizeAuditorAllowedExtensions(v.split(","));
|
|
1212
|
+
};
|
|
1213
|
+
let extFactoryInvoked = false;
|
|
1214
|
+
const extPick = extItems.length
|
|
1215
|
+
? await ctx.ui.custom<MultiModelPickerResult>((tui, theme, keybindings, done) => {
|
|
1216
|
+
extFactoryInvoked = true;
|
|
1217
|
+
return new MultiModelPickerComponent(
|
|
1218
|
+
{
|
|
1219
|
+
title: "Auditor allowed extensions — space toggles, enter confirms; empty = fully isolated auditor (default)",
|
|
1220
|
+
items: extItems,
|
|
1221
|
+
initialSelected: currentExts,
|
|
1222
|
+
unorderedSet: true,
|
|
1223
|
+
},
|
|
1224
|
+
() => tui.requestRender(),
|
|
1225
|
+
theme,
|
|
1226
|
+
keybindings,
|
|
1227
|
+
done,
|
|
1228
|
+
);
|
|
1229
|
+
})
|
|
1230
|
+
: undefined;
|
|
1231
|
+
let pickedExts: string[] | undefined;
|
|
1232
|
+
if (extPick === undefined && !extFactoryInvoked) pickedExts = await extInputFallback();
|
|
1233
|
+
else if (extPick === undefined) pickedExts = undefined;
|
|
1234
|
+
else pickedExts = Array.isArray(extPick) ? extPick : extPick.refs;
|
|
1235
|
+
if (pickedExts === undefined) return;
|
|
1236
|
+
const normalizedExts = normalizeAuditorAllowedExtensions(pickedExts);
|
|
1237
|
+
saveSettings("global", ctx.cwd, { auditorAllowedExtensions: normalizedExts.length ? normalizedExts : undefined });
|
|
1238
|
+
ctx.ui.notify(
|
|
1239
|
+
normalizedExts.length
|
|
1240
|
+
? `Auditor allowed extensions saved: ${normalizedExts.join(", ")}.`
|
|
1241
|
+
: "Auditor allowed extensions cleared — the detached auditor runs extension-less again (default).",
|
|
1242
|
+
"info",
|
|
1243
|
+
);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1155
1246
|
case "auditorSameSessionSwap": {
|
|
1156
1247
|
const v = await ctx.ui.select("Same-model swap — when the pinned auditor IS the session model, walk the fallback pin (a same-family model shares the executor's blind spots)", [
|
|
1157
1248
|
"on — the verifier differs from the executor (default)",
|
|
@@ -410,6 +410,12 @@ async function resolveDraftActivationConflict(ctx: ExtensionContext, incoming: O
|
|
|
410
410
|
return "proceed";
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
function verifyTaskMilestone(ctx: ExtensionContext, verificationContract?: string): { failedCommand?: string; output?: string; exitCode?: number } | null {
|
|
414
|
+
if (!verificationContract?.trim()) return null;
|
|
415
|
+
const result = runMechanicalPreAuditChecks(ctx.cwd, extractMechanicalCheckCommands(verificationContract));
|
|
416
|
+
return result.passed ? null : result;
|
|
417
|
+
}
|
|
418
|
+
|
|
413
419
|
function registerAgentTools(pi: any): void {
|
|
414
420
|
pi.registerTool(defineTool({
|
|
415
421
|
name: "complete_goal",
|
|
@@ -691,6 +697,7 @@ function registerAgentTools(pi: any): void {
|
|
|
691
697
|
verificationSummary: p.verificationSummary,
|
|
692
698
|
model: candidate.model,
|
|
693
699
|
thinkingLevel: (settings.auditorThinkingLevel ?? "high") as any, // may be "max" — pi ≥0.83 understands it; the dev-types predate it
|
|
700
|
+
allowedExtensions: settings.auditorAllowedExtensions,
|
|
694
701
|
runtime: { attemptId: () => newDetachedAuditJobAttemptId(completionClaim.attemptId!), logicalAttemptId: completionClaim.attemptId!, wallTimeoutMs: AUDITOR_WALL_TIMEOUT_MS },
|
|
695
702
|
onProgress: (progress) => {
|
|
696
703
|
publishDetachedAuditProgress(auditGeneration, auditGoalId, auditAttemptId, progress);
|
|
@@ -715,9 +722,10 @@ function registerAgentTools(pi: any): void {
|
|
|
715
722
|
let fallbackUsed = false;
|
|
716
723
|
// v0.35.7: Deterministic Fast-Fail Pre-Audit — if mechanical contract checks fail,
|
|
717
724
|
// fail in 200ms with raw output rather than burning 45s on an LLM audit pass.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
725
|
+
try {
|
|
726
|
+
const mechanicalCmds = extractMechanicalCheckCommands(auditGoal.verificationContract ?? "");
|
|
727
|
+
const mechanicalResult = runMechanicalPreAuditChecks(ctx.cwd, mechanicalCmds);
|
|
728
|
+
if (!mechanicalResult.passed) {
|
|
721
729
|
result = {
|
|
722
730
|
approved: false,
|
|
723
731
|
disapproved: true,
|
|
@@ -727,8 +735,7 @@ function registerAgentTools(pi: any): void {
|
|
|
727
735
|
regressionShieldPassed: true,
|
|
728
736
|
goalRevision: captureGoalRevision(auditGoal) ?? undefined,
|
|
729
737
|
};
|
|
730
|
-
|
|
731
|
-
try {
|
|
738
|
+
} else {
|
|
732
739
|
({ result, retriedOnce, fallbackUsed } = await runDetachedCompletionWithFallback(auditorCandidates, runAudit, {
|
|
733
740
|
shouldRetry: () => detachedAuditContext(auditGeneration, auditGoalId, auditAttemptId) !== null,
|
|
734
741
|
forbiddenRefs: settings.forbiddenModels,
|
|
@@ -754,12 +761,15 @@ function registerAgentTools(pi: any): void {
|
|
|
754
761
|
current.ui.notify(`Detached auditor failed on ${auditorCandidateLabel(from)} — retrying with ${auditorCandidateLabel(to)}. This is infrastructure, not a verdict.`, "warning");
|
|
755
762
|
},
|
|
756
763
|
}));
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
764
|
+
}
|
|
765
|
+
} finally {
|
|
766
|
+
// Deterministic pre-audit failures take the same cleanup path as a
|
|
767
|
+
// detached worker. Otherwise the in-flight latch survives forever
|
|
768
|
+
// and recovery/status surfaces claim an auditor is still running.
|
|
769
|
+
if (ownsDetachedAudit(auditGeneration, auditGoalId, auditAttemptId)) {
|
|
770
|
+
clearDetachedAuditProgress(auditGeneration, auditGoalId, auditAttemptId);
|
|
771
|
+
completionAuditInFlight = false;
|
|
772
|
+
completionAuditGeneration = null;
|
|
763
773
|
}
|
|
764
774
|
}
|
|
765
775
|
const auditContextAfterRun = freshCtxForGeneration(auditGeneration);
|
|
@@ -883,7 +893,21 @@ function registerAgentTools(pi: any): void {
|
|
|
883
893
|
? state.goal.completionSummary.replace(/\s+/g, " ")
|
|
884
894
|
: state.goal.objective;
|
|
885
895
|
const recap = displaySlice(recapSrc, 110);
|
|
886
|
-
archiveCurrentGoal(ctx, "complete", `auditor ${result.model} approved`);
|
|
896
|
+
const archived = archiveCurrentGoal(ctx, "complete", `auditor ${result.model} approved`);
|
|
897
|
+
if (!archived) {
|
|
898
|
+
// The archive helper preserves the live objective and emits the
|
|
899
|
+
// persistence warning. Stop here: an approved verdict is not a
|
|
900
|
+
// terminal success until the archive and state transition land.
|
|
901
|
+
updateGoal({
|
|
902
|
+
status: "paused",
|
|
903
|
+
pendingCompletion: undefined,
|
|
904
|
+
pauseKind: "blocked",
|
|
905
|
+
pauseReason: "completion approved but terminal archive persistence failed",
|
|
906
|
+
pauseSuggestedAction: `Fix .pi-glla disk access or resolve the archive fence, then ${activeGoalSurfaceCommand("resume")} and call complete_goal again.`,
|
|
907
|
+
}, ctx);
|
|
908
|
+
appendLedger(ctx.cwd, "goal_archive_failed_after_approval", { goalId: state.goal?.id, origin: "manual-verify", model: result.model });
|
|
909
|
+
return { content: [{ type: "text", text: "The auditor approved, but the terminal archive could not be persisted. The goal is paused; fix persistence, resume, and retry complete_goal." }], details: {} };
|
|
910
|
+
}
|
|
887
911
|
ctx.ui.notify(`✓ done: ${recap} — auditor ${result.model} approved.`, "info");
|
|
888
912
|
notifyExternal(ctx, `Goal complete (auditor approved): ${displaySlice(recapSrc, 120)}`);
|
|
889
913
|
return { content: [{ type: "text", text: `Goal approved by auditor ${result.model}.` }], details: {} };
|
|
@@ -1082,7 +1106,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1082
1106
|
void retryStoredCompletionAudit();
|
|
1083
1107
|
return;
|
|
1084
1108
|
}
|
|
1085
|
-
updateGoal({ status: "active", pauseKind: undefined, pauseResumeAt: undefined, pauseReason: undefined, pauseSuggestedAction: undefined }, fresh);
|
|
1109
|
+
updateGoal({ status: "active", pauseKind: undefined, pauseResumeAt: undefined, pauseReason: undefined, pauseSuggestedAction: undefined, autoResumedAt: new Date().toISOString(), autoResumedEvent: "auditor provider retry elapsed" }, fresh);
|
|
1086
1110
|
appendLedger(fresh.cwd, "goal_resumed", { via: "provider-retry" });
|
|
1087
1111
|
if (resolveEffectiveAggressiveSettings(loadSettings(fresh.cwd)).aggressiveMode) {
|
|
1088
1112
|
fresh.ui.notify("Auto-resume fired (event: auditor provider retry elapsed). Continue working.", "info");
|
|
@@ -1281,7 +1305,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1281
1305
|
pi.registerTool(defineTool({
|
|
1282
1306
|
name: "pause_goal",
|
|
1283
1307
|
label: "Pause goal",
|
|
1284
|
-
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.",
|
|
1308
|
+
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress. Pausing ABORTS the current turn immediately — after this call, stop; never keep working. 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 and pop a picker for the user. 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.",
|
|
1285
1309
|
parameters: Type.Object({
|
|
1286
1310
|
reason: Type.String({ description: "Why the work is paused" }),
|
|
1287
1311
|
suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
|
|
@@ -1296,6 +1320,10 @@ function registerAgentTools(pi: any): void {
|
|
|
1296
1320
|
const ctx = currentToolContext(execCtx);
|
|
1297
1321
|
if (!ctx) return staleToolResult();
|
|
1298
1322
|
const p = params as { reason: string; suggestedAction?: string; kind?: "decision" | "error" | "wait" | "blocked"; options?: string[]; recommended?: number; resumeAt?: string };
|
|
1323
|
+
// v0.35.15: a model that passes options but forgets kind="decision"
|
|
1324
|
+
// still gets the decision card — a non-empty options array IS the
|
|
1325
|
+
// decision intent; silently dropping it left the user with no picker.
|
|
1326
|
+
if (!p.kind && p.options && p.options.length > 0) p.kind = "decision";
|
|
1299
1327
|
if (!state.goal) return { content: [{ type: "text", text: "No active goal." }], details: {} };
|
|
1300
1328
|
// A late model/tool call from the previous turn must not overwrite a
|
|
1301
1329
|
// paused or auditing lifecycle. That race made a genuine stop look
|
|
@@ -1387,7 +1415,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1387
1415
|
try {
|
|
1388
1416
|
const hours = Math.round((resumeAtMs - Date.now()) / (60 * 60 * 1000));
|
|
1389
1417
|
ctx.ui.notify(
|
|
1390
|
-
`Pause scheduled for ~${hours}h. If the objective no longer matches your intent, run ${activeGoalSurfaceCommand("tweak")} to replace it now; otherwise ${activeGoalSurfaceCommand("resume")}
|
|
1418
|
+
`Pause scheduled for ~${hours}h. If the objective no longer matches your intent, run ${activeGoalSurfaceCommand("tweak")} to replace it now; otherwise the wait auto-continues shortly after its resume time (heartbeat backstop), and ${activeGoalSurfaceCommand("resume")} resumes immediately.`,
|
|
1391
1419
|
"info",
|
|
1392
1420
|
);
|
|
1393
1421
|
appendLedger(ctx.cwd, "pause_long_wait_offer_tweak", {
|
|
@@ -1400,12 +1428,29 @@ function registerAgentTools(pi: any): void {
|
|
|
1400
1428
|
/* best-effort */
|
|
1401
1429
|
}
|
|
1402
1430
|
}
|
|
1431
|
+
// v0.35.15: a pause ENDS the turn. Before this fix the tool result was
|
|
1432
|
+
// just text — pi kept the same turn running, the model kept working
|
|
1433
|
+
// past its own pause ("the agent moves on"), and the deferred decision
|
|
1434
|
+
// picker lost the race: the user was never shown the options. Abort
|
|
1435
|
+
// (the same mechanism /goal cancel and the zero-stream watchdog use)
|
|
1436
|
+
// so the session actually stops on an idle surface where the decision
|
|
1437
|
+
// picker can own the screen. The impossible-drop advance is the one
|
|
1438
|
+
// exception: the queue already moved to the next item and its
|
|
1439
|
+
// continuation owns this turn — aborting would kill the hand-off.
|
|
1440
|
+
if (!droppedImpossible) {
|
|
1441
|
+
try {
|
|
1442
|
+
ctx.abort();
|
|
1443
|
+
appendLedger(ctx.cwd, "pause_goal_aborted_turn", { goalId: state.goal?.id, kind: p.kind ?? "blocked" });
|
|
1444
|
+
} catch (abortError) {
|
|
1445
|
+
appendLedger(ctx.cwd, "pause_goal_abort_failed", { goalId: state.goal?.id, error: abortError instanceof Error ? abortError.message : String(abortError) });
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1403
1448
|
return {
|
|
1404
1449
|
content: [{
|
|
1405
1450
|
type: "text",
|
|
1406
1451
|
text: droppedImpossible
|
|
1407
1452
|
? "The list item was auto-dropped as impossible (blocked with no resume path) — the list moved on instead of stopping."
|
|
1408
|
-
: `Goal paused. ${activeGoalSurfaceCommand("resume")} to continue.`,
|
|
1453
|
+
: `Goal paused. The turn ends here — do NOT continue working. ${activeGoalSurfaceCommand("resume")} to continue.`,
|
|
1409
1454
|
}],
|
|
1410
1455
|
details: {},
|
|
1411
1456
|
};
|
|
@@ -1432,18 +1477,15 @@ function registerAgentTools(pi: any): void {
|
|
|
1432
1477
|
while (queue.length > 0) {
|
|
1433
1478
|
const t = queue.shift();
|
|
1434
1479
|
if (t.id === p.id && t.status !== "complete") {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
details: {},
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1480
|
+
const checkRes = verifyTaskMilestone(ctx, t.verificationContract);
|
|
1481
|
+
if (checkRes) {
|
|
1482
|
+
return {
|
|
1483
|
+
content: [{
|
|
1484
|
+
type: "text",
|
|
1485
|
+
text: `Task ${p.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}\n\nTask ${p.id} remains in_progress. Fix the failure before marking complete.`,
|
|
1486
|
+
}],
|
|
1487
|
+
details: {},
|
|
1488
|
+
};
|
|
1447
1489
|
}
|
|
1448
1490
|
t.status = "complete";
|
|
1449
1491
|
updateGoal({ taskList: tl }, ctx);
|
|
@@ -1477,6 +1519,18 @@ function registerAgentTools(pi: any): void {
|
|
|
1477
1519
|
while (queue.length > 0) {
|
|
1478
1520
|
const t = queue.shift();
|
|
1479
1521
|
if (t.id === p.id) {
|
|
1522
|
+
if (p.status === "complete") {
|
|
1523
|
+
const checkRes = verifyTaskMilestone(ctx, t.verificationContract);
|
|
1524
|
+
if (checkRes) {
|
|
1525
|
+
return {
|
|
1526
|
+
content: [{
|
|
1527
|
+
type: "text",
|
|
1528
|
+
text: `Task ${p.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}\n\nTask ${p.id} remains ${t.status}. Fix the failure before marking complete.`,
|
|
1529
|
+
}],
|
|
1530
|
+
details: {},
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1480
1534
|
t.status = p.status;
|
|
1481
1535
|
updateGoal({ taskList: tl }, ctx);
|
|
1482
1536
|
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
@@ -114,6 +114,7 @@ import {
|
|
|
114
114
|
isGoalRevisionCurrent,
|
|
115
115
|
nextHourlyPromptMs,
|
|
116
116
|
nextHourlyProbeMs,
|
|
117
|
+
supervisorPaused,
|
|
117
118
|
type ModelSwitchRecord,
|
|
118
119
|
type ListItem,
|
|
119
120
|
} from "../goal-loop-core.js";
|
|
@@ -129,6 +130,11 @@ import {
|
|
|
129
130
|
transitionDispatch,
|
|
130
131
|
type ContinuationDispatch,
|
|
131
132
|
} from "../goal-loop-dispatch.js";
|
|
133
|
+
import {
|
|
134
|
+
AUDITOR_QUIET_MS,
|
|
135
|
+
auditorDisplayPhase,
|
|
136
|
+
fmtElapsed,
|
|
137
|
+
} from "../goal-loop-display.js";
|
|
132
138
|
import {
|
|
133
139
|
createGoalContinuation,
|
|
134
140
|
scheduleContinuation,
|
|
@@ -257,6 +263,8 @@ import {
|
|
|
257
263
|
type HeartbeatDeps,
|
|
258
264
|
type HeartbeatFlags,
|
|
259
265
|
} from "../goal-heartbeat.js"; // decomposition step 4 (v0.34.112)
|
|
266
|
+
import { getSubagentAgentsSnapshot } from "../goal-heartbeat.js";
|
|
267
|
+
import { renderAgentsWidgetLine, type AgentsPanelRow } from "../goal-agents-panel.js";
|
|
260
268
|
import {
|
|
261
269
|
clearMainModelRecoveryTimer,
|
|
262
270
|
createGoalRecovery,
|
|
@@ -664,12 +672,23 @@ function refreshUI(ctx: ExtensionContext): void {
|
|
|
664
672
|
const extras = {
|
|
665
673
|
stalls: consecutiveStalls,
|
|
666
674
|
recent: recentActions,
|
|
675
|
+
// v0.35.29 (issue #15): ambient tracked-subagent segment — hidden at
|
|
676
|
+
// zero tracked children (renderAgentsWidgetLine returns undefined).
|
|
677
|
+
...(() => {
|
|
678
|
+
try {
|
|
679
|
+
const { agents } = getSubagentAgentsSnapshot();
|
|
680
|
+
const line = renderAgentsWidgetLine(agents as AgentsPanelRow[]);
|
|
681
|
+
return line ? { agents: { line } } : {};
|
|
682
|
+
} catch { return {}; }
|
|
683
|
+
})(),
|
|
684
|
+
...activity,
|
|
667
685
|
...activity,
|
|
668
686
|
turnPending: pendingContinuationDispatchRef() !== null,
|
|
669
687
|
auditorSilent: loadSettings(ctx.cwd).auditorSilent !== false,
|
|
670
688
|
auditorProgressSignals: settings.auditorProgressSignals !== false,
|
|
671
689
|
mainModelFallbacks: fallbackRefs,
|
|
672
690
|
modelProvenance,
|
|
691
|
+
...(lastAuditorQuietStretch ? { auditorQuietStretch: lastAuditorQuietStretch } : {}),
|
|
673
692
|
};
|
|
674
693
|
// Settings are loaded once for the remaining projections; the explicit
|
|
675
694
|
// auditorSilent expression preserves the source-level contract.
|
|
@@ -700,6 +719,57 @@ function scheduleUIRefresh(): void {
|
|
|
700
719
|
deferredUIRefresh.unref?.();
|
|
701
720
|
}
|
|
702
721
|
|
|
722
|
+
// v0.35.15: proactive auditor quiet reporting (runtime-only state).
|
|
723
|
+
// The seed complaint (2026-08-21): an auditor sat silent for 8 minutes and
|
|
724
|
+
// the user only learned about it afterwards from the status line — nothing
|
|
725
|
+
// ever NOTIFIED. The phase chip recolors at ~3 min, but a chip is not a
|
|
726
|
+
// report. The watcher below fires exactly ONE notify when a detached audit
|
|
727
|
+
// crosses into the quiet phase, then records the ended stretch so the
|
|
728
|
+
// footer can show "silent Xm then resumed" afterwards. `/glla pause`
|
|
729
|
+
// suppresses the notify (it is automatic machinery), never the recording.
|
|
730
|
+
let auditorQuietSince: number | null = null;
|
|
731
|
+
let auditorQuietNotified = false;
|
|
732
|
+
let lastAuditorQuietStretch: { ms: number; endedAt: number } | null = null;
|
|
733
|
+
|
|
734
|
+
/** Drive the quiet watcher once per UI tick. Returns a warning message to
|
|
735
|
+
* deliver, or null. Split out for testability. */
|
|
736
|
+
export function __auditorQuietWatchTick(now = Date.now()): string | null {
|
|
737
|
+
const g = state.goal;
|
|
738
|
+
if (!g || g.status !== "auditing") {
|
|
739
|
+
// No live audit — reset any open stretch so a stale silence cannot leak
|
|
740
|
+
// into a later goal's display.
|
|
741
|
+
if (auditorQuietSince !== null) {
|
|
742
|
+
const ms = now - auditorQuietSince;
|
|
743
|
+
if (ms >= AUDITOR_QUIET_MS) lastAuditorQuietStretch = { ms, endedAt: now };
|
|
744
|
+
auditorQuietSince = null;
|
|
745
|
+
auditorQuietNotified = false;
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
const phase = auditorDisplayPhase(g, latestAuditProgress, now);
|
|
750
|
+
if (phase === "quiet") {
|
|
751
|
+
// Entering quiet: anchor the stretch at the worker's last activity.
|
|
752
|
+
if (auditorQuietSince === null) {
|
|
753
|
+
auditorQuietSince = typeof latestAuditProgress?.lastActivityAt === "number"
|
|
754
|
+
? latestAuditProgress.lastActivityAt
|
|
755
|
+
: now;
|
|
756
|
+
}
|
|
757
|
+
if (!auditorQuietNotified && !supervisorPaused(state)) {
|
|
758
|
+
auditorQuietNotified = true;
|
|
759
|
+
return `glla: the detached auditor shows NO worker activity for ${fmtElapsed(now - auditorQuietSince)} — it may be stuck (or doing one long read). The status footer keeps counting. /glla pause freezes all supervisor machinery; /goal cancel discards the claim.`;
|
|
760
|
+
}
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
// Left the quiet phase: record the ended stretch for the footer summary.
|
|
764
|
+
if (auditorQuietSince !== null) {
|
|
765
|
+
const ms = now - auditorQuietSince;
|
|
766
|
+
if (ms >= AUDITOR_QUIET_MS) lastAuditorQuietStretch = { ms, endedAt: now };
|
|
767
|
+
auditorQuietSince = null;
|
|
768
|
+
}
|
|
769
|
+
auditorQuietNotified = false;
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
703
773
|
function startUITicker(): void {
|
|
704
774
|
if (uiTicker) return;
|
|
705
775
|
uiTicker = setInterval(() => {
|
|
@@ -707,7 +777,17 @@ function startUITicker(): void {
|
|
|
707
777
|
// v0.34.12: keep ticking during a timed wait-pause too — the status
|
|
708
778
|
// line counts down to resumeAt live (pully field request 2026-08-01).
|
|
709
779
|
const auditVisible = state.goal?.status === "auditing";
|
|
710
|
-
if (ctx
|
|
780
|
+
if (!ctx) return;
|
|
781
|
+
if (!(isSupervising() || auditVisible || (state.goal?.status === "paused" && !!state.goal.pauseResumeAt))) return;
|
|
782
|
+
// v0.35.15: proactive quiet-phase notification (once per stretch).
|
|
783
|
+
if (auditVisible) {
|
|
784
|
+
const warning = __auditorQuietWatchTick();
|
|
785
|
+
if (warning && !supervisorPaused(state)) {
|
|
786
|
+
try { ctx.ui.notify(warning, "warning"); } catch { /* stale handle — next tick retries the notify gate honestly */ }
|
|
787
|
+
scheduleUIRefresh();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
refreshUI(ctx);
|
|
711
791
|
}, 1_000);
|
|
712
792
|
uiTicker.unref?.();
|
|
713
793
|
}
|
|
@@ -803,6 +883,30 @@ export function __testOnlyRegisterAgentTools(pi: any): void {
|
|
|
803
883
|
export function __testOnlyRememberCtx(ctx: ExtensionContext): void {
|
|
804
884
|
rememberCtx(ctx);
|
|
805
885
|
}
|
|
886
|
+
|
|
887
|
+
/** v0.35.15 test-only: drive the auditor quiet watcher with a controlled
|
|
888
|
+
* progress snapshot + clock. Pass null progress to simulate no audit.
|
|
889
|
+
* Returns the tick's notify message (null = silent) exactly like the
|
|
890
|
+
* production ticker consumes it. */
|
|
891
|
+
export function __testOnlyAuditorQuietWatchTick(
|
|
892
|
+
progress: import("../goal-loop-display.js").AuditDisplayProgress | null,
|
|
893
|
+
now = Date.now(),
|
|
894
|
+
): string | null {
|
|
895
|
+
latestAuditProgress = progress;
|
|
896
|
+
return __auditorQuietWatchTick(now);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** v0.35.15 test-only: reset the quiet-watcher runtime state between tests. */
|
|
900
|
+
export function __testOnlyResetAuditorQuietWatch(): void {
|
|
901
|
+
auditorQuietSince = null;
|
|
902
|
+
auditorQuietNotified = false;
|
|
903
|
+
lastAuditorQuietStretch = null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** v0.35.15 test-only: the last ended quiet stretch the footer extras carry. */
|
|
907
|
+
export function __testOnlyLastAuditorQuietStretch(): { ms: number; endedAt: number } | null {
|
|
908
|
+
return lastAuditorQuietStretch;
|
|
909
|
+
}
|
|
806
910
|
// v0.32.1 (pi-goal-x's lesson — "recover from compacts smarter"): a compact
|
|
807
911
|
// leaves a RESUME DEBT, not just two fixed-offset settle probes that can both
|
|
808
912
|
// lose (field: hellhunter 4-min dangle 2026-07-31; polis stall same day).
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
} from "../goal-continuation.js";
|
|
48
48
|
export { __testOnlySetContinuationStartTimeout, __testOnlySetContinuationRetryBackoff } from "../goal-continuation.js";
|
|
49
49
|
|
|
50
|
-
import { resetLengthContinue } from "../length-continue.js";
|
|
51
50
|
import {
|
|
52
51
|
clearMainModelRecoveryTimer,
|
|
53
52
|
createGoalRecovery,
|
|
@@ -60,7 +59,7 @@ import {
|
|
|
60
59
|
type RecoveryDeps,
|
|
61
60
|
type RecoveryFlags,
|
|
62
61
|
} from "../goal-recovery.js";
|
|
63
|
-
import { createGoalHeartbeat,
|
|
62
|
+
import { createGoalHeartbeat, getSubagentAgentsSnapshot, type HeartbeatDeps, type HeartbeatFlags } from "../goal-heartbeat.js";
|
|
64
63
|
import { createGoalCommands, type CommandDeps, type CommandFlags } from "../goal-commands.js";
|
|
65
64
|
import { clearLoopTimer, createGoalLoop, type LoopDeps, type LoopFlags } from "../goal-loop.js";
|
|
66
65
|
|
|
@@ -154,6 +153,9 @@ const commandDeps: CommandDeps = {
|
|
|
154
153
|
persistState,
|
|
155
154
|
updateGoal,
|
|
156
155
|
setGoal,
|
|
156
|
+
// v0.35.29 (issue #15): /glla agents reads the heartbeat's tracked-subagent
|
|
157
|
+
// snapshot via injection (goal-commands must not import goal-heartbeat).
|
|
158
|
+
agentsSnapshot: () => getSubagentAgentsSnapshot(),
|
|
157
159
|
archiveCurrentGoal,
|
|
158
160
|
healGoalPolicy,
|
|
159
161
|
startDrafting,
|
|
@@ -404,10 +406,8 @@ createGoalHeartbeat(heartbeatFlags, heartbeatDeps);
|
|
|
404
406
|
|
|
405
407
|
|
|
406
408
|
export default function (pi: ExtensionAPI): void {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
startHeartbeat();
|
|
411
|
-
startUITicker();
|
|
409
|
+
// Factory evaluation can also happen inside pi-subagents child sessions.
|
|
410
|
+
// Registration must not claim the shared host API or start session timers;
|
|
411
|
+
// the admitted host session_start below owns both lifecycle resources.
|
|
412
412
|
registerGoalRuntime(pi);
|
|
413
413
|
}
|