pi-goal-list-loop-audit 0.35.4 → 0.35.13
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 +7666 -0
- package/INSTALL.md +377 -0
- package/README.md +26 -13
- package/docs/DESIGN.md +31 -4
- package/docs/INDEX.md +32 -7
- package/examples/example-objective.md +119 -0
- package/extensions/faulty-objective-recovery.ts +13 -2
- package/extensions/goal-commands.ts +50 -6
- package/extensions/goal-heartbeat.ts +25 -21
- package/extensions/goal-loop-auditor-process.ts +184 -3
- package/extensions/goal-loop-core.ts +108 -15
- package/extensions/goal-loop-display.ts +82 -0
- package/extensions/goal-loop-shield.ts +55 -0
- package/extensions/goal-recovery.ts +317 -7
- package/extensions/goal-settings.ts +31 -1
- package/extensions/loops/goal-activation.ts +74 -7
- package/extensions/loops/goal-auditor-hooks.ts +30 -55
- package/extensions/loops/goal-list-queue.ts +18 -5
- package/extensions/loops/goal-orchestrator.ts +5 -0
- package/extensions/loops/goal-runtime-globals.ts +2 -0
- package/extensions/loops/goal-session.ts +101 -21
- package/extensions/loops/goal-settings-ui.ts +122 -63
- package/extensions/loops/goal-tools.ts +110 -36
- package/extensions/loops/goal-ui.ts +59 -2
- package/extensions/main-model-recovery.ts +24 -1
- package/extensions/model-picker.ts +3 -1
- package/extensions/model-selector.ts +2 -1
- package/extensions/multi-model-picker.ts +64 -7
- package/extensions/settings-menu.ts +16 -0
- package/package.json +4 -1
- package/prompts/goal-loop-continuation.md +11 -8
- package/prompts/goal-loop-draft.md +5 -5
- package/schemas/goal.schema.json +17 -0
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
workCommandRoot,
|
|
40
40
|
appendLedger,
|
|
41
41
|
claimRecoveryNotice,
|
|
42
|
+
assignQueueOrder,
|
|
42
43
|
providerErrorPresentation,
|
|
43
44
|
sanitizeProviderAuditReport,
|
|
44
45
|
sanitizeProviderDisplayText,
|
|
@@ -59,10 +60,9 @@ import {
|
|
|
59
60
|
computeListDepth,
|
|
60
61
|
formatAuditLog,
|
|
61
62
|
formatGoalAuditHistory,
|
|
62
|
-
runWithInfraRetry,
|
|
63
|
-
isRetriableInfraError,
|
|
64
63
|
readAuditLog,
|
|
65
64
|
bumpGoalRevision,
|
|
65
|
+
captureGoalRevision,
|
|
66
66
|
stripThinkBlocks,
|
|
67
67
|
type AuditLogEntry,
|
|
68
68
|
ledgerPath,
|
|
@@ -177,13 +177,10 @@ import {
|
|
|
177
177
|
tickLengthContinue,
|
|
178
178
|
} from "../length-continue.js";
|
|
179
179
|
import {
|
|
180
|
-
classifyMainModelFailure,
|
|
181
180
|
mainModelAutoRetryUntil,
|
|
182
|
-
mainModelFailureDelayMs,
|
|
183
181
|
mainModelRetryDelayMs,
|
|
184
182
|
MAIN_MODEL_AUTO_RETRY_HORIZON_MS,
|
|
185
183
|
modelRef,
|
|
186
|
-
nextUntriedModelRef,
|
|
187
184
|
normalizeModelRefs,
|
|
188
185
|
sendStormEscalateMs,
|
|
189
186
|
splitModelRef,
|
|
@@ -221,6 +218,10 @@ import {
|
|
|
221
218
|
runDetachedGoalCompletionAuditor,
|
|
222
219
|
type AuditorProgress,
|
|
223
220
|
} from "../goal-loop-auditor-process.js";
|
|
221
|
+
import {
|
|
222
|
+
extractMechanicalCheckCommands,
|
|
223
|
+
runMechanicalPreAuditChecks,
|
|
224
|
+
} from "../goal-loop-shield.js";
|
|
224
225
|
import {
|
|
225
226
|
REPETITION,
|
|
226
227
|
isActuallyStuck,
|
|
@@ -339,6 +340,7 @@ import {
|
|
|
339
340
|
import {
|
|
340
341
|
addSingleItem,
|
|
341
342
|
autoNotifyCmd,
|
|
343
|
+
hydrateListQueueFromDisk,
|
|
342
344
|
cmdGoal,
|
|
343
345
|
cmdList,
|
|
344
346
|
cmdReview,
|
|
@@ -414,8 +416,26 @@ function registerAgentTools(pi: any): void {
|
|
|
414
416
|
label: "Complete goal",
|
|
415
417
|
description: "Mark the active goal as complete. Queues a detached auditor worker to verify without holding the main pi turn. Use only when the objective is genuinely satisfied.",
|
|
416
418
|
parameters: Type.Object({
|
|
417
|
-
|
|
418
|
-
|
|
419
|
+
// v0.34.136: completionSummary adopts the six-label recap from
|
|
420
|
+
// audit/COMPLETION-SUMMARY-POLICY-2026-08-19.md (Outcome / Changed
|
|
421
|
+
// / Evidence / Tests / Unresolved / Next). Every label is present
|
|
422
|
+
// even when its value is `none`; the durable archive preserves all
|
|
423
|
+
// six lines verbatim and the display/widget projection does NOT
|
|
424
|
+
// fabricate an auditor verdict.
|
|
425
|
+
completionSummary: Type.Optional(Type.String({
|
|
426
|
+
description:
|
|
427
|
+
"Six-label recap — one line per label, every label required (use `none` when empty): " +
|
|
428
|
+
"Outcome: <what was delivered> · Changed: <files/behavior/decision> · Evidence: <key commit/report/result> · " +
|
|
429
|
+
"Tests: <bounded commands + pass/fail, or `not run — <reason>`> · Unresolved: <remaining risk, or `none`> · " +
|
|
430
|
+
"Next: <one follow-up hint, or `none`>. " +
|
|
431
|
+
"See audit/COMPLETION-SUMMARY-POLICY-2026-08-19.md. Free-form prose is allowed but discouraged.",
|
|
432
|
+
})),
|
|
433
|
+
verificationSummary: Type.Optional(Type.String({
|
|
434
|
+
description:
|
|
435
|
+
"Per-item evidence for the verification contract. " +
|
|
436
|
+
"Stays independent of completionSummary: the detached auditor receives both fields separately " +
|
|
437
|
+
"and cross-checks the verification claim against real artifacts (see audit/COMPLETION-SUMMARY-POLICY-2026-08-19.md).",
|
|
438
|
+
})),
|
|
419
439
|
newObjective: Type.Optional(Type.String({ description: "v0.25.0 (contract item 15): when the work has legitimately shifted, pass the new objective here — it atomically replaces the goal objective AND the audit proceeds against the NEW objective in this same call. Do not use to dodge a legitimate disapproval; the auditor sees the change." })),
|
|
420
440
|
}),
|
|
421
441
|
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
@@ -649,11 +669,22 @@ function registerAgentTools(pi: any): void {
|
|
|
649
669
|
// every state/UI access below rebinds through the generation guard.
|
|
650
670
|
completionAuditInFlight = true;
|
|
651
671
|
completionAuditGeneration = auditGeneration;
|
|
652
|
-
latestAuditProgress = {
|
|
672
|
+
latestAuditProgress = {
|
|
673
|
+
label: "queued",
|
|
674
|
+
model: modelRef(auditorModel),
|
|
675
|
+
via: via ?? "unset",
|
|
676
|
+
lastEventAt: Date.now(),
|
|
677
|
+
};
|
|
653
678
|
refreshUI(ctx);
|
|
654
679
|
void (async () => {
|
|
655
|
-
const runAudit = (candidate: AuditorModelCandidate) =>
|
|
656
|
-
|
|
680
|
+
const runAudit = (candidate: AuditorModelCandidate) => {
|
|
681
|
+
latestAuditProgress = {
|
|
682
|
+
...(latestAuditProgress ?? {}),
|
|
683
|
+
model: modelRef(candidate.model),
|
|
684
|
+
via: candidate.via,
|
|
685
|
+
};
|
|
686
|
+
refreshUI(ctx);
|
|
687
|
+
return runDetachedGoalCompletionAuditor({
|
|
657
688
|
cwd: ctx.cwd,
|
|
658
689
|
goal: auditGoal,
|
|
659
690
|
completionSummary: finalSummary,
|
|
@@ -673,6 +704,7 @@ function registerAgentTools(pi: any): void {
|
|
|
673
704
|
appendLedger(current.cwd, "auditor_stalled", { goalId: auditGoalId, attemptId: auditAttemptId, ...info });
|
|
674
705
|
},
|
|
675
706
|
});
|
|
707
|
+
};
|
|
676
708
|
// v0.25.4 (post-audit fix): a retriable infra failure (stream error,
|
|
677
709
|
// auth blip — NOT user abort, NOT missing model) gets ONE automatic
|
|
678
710
|
// retry with backoff before we report "auditor infrastructure error
|
|
@@ -681,29 +713,53 @@ function registerAgentTools(pi: any): void {
|
|
|
681
713
|
let result: Awaited<ReturnType<typeof runAudit>>;
|
|
682
714
|
let retriedOnce = false;
|
|
683
715
|
let fallbackUsed = false;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
},
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
716
|
+
// v0.35.7: Deterministic Fast-Fail Pre-Audit — if mechanical contract checks fail,
|
|
717
|
+
// fail in 200ms with raw output rather than burning 45s on an LLM audit pass.
|
|
718
|
+
const mechanicalCmds = extractMechanicalCheckCommands(auditGoal.verificationContract ?? "");
|
|
719
|
+
const mechanicalResult = runMechanicalPreAuditChecks(ctx.cwd, mechanicalCmds);
|
|
720
|
+
if (!mechanicalResult.passed) {
|
|
721
|
+
result = {
|
|
722
|
+
approved: false,
|
|
723
|
+
disapproved: true,
|
|
724
|
+
impossible: false,
|
|
725
|
+
output: `<disapproved/>\n\nDeterministic Pre-Audit Fast-Fail: Mechanical contract check failed: \`${mechanicalResult.failedCommand}\` (exit code ${mechanicalResult.exitCode})\n\n<evidence>\n${mechanicalResult.output}\n</evidence>`,
|
|
726
|
+
model: "deterministic-pre-audit",
|
|
727
|
+
regressionShieldPassed: true,
|
|
728
|
+
goalRevision: captureGoalRevision(auditGoal) ?? undefined,
|
|
729
|
+
};
|
|
730
|
+
} else {
|
|
731
|
+
try {
|
|
732
|
+
({ result, retriedOnce, fallbackUsed } = await runDetachedCompletionWithFallback(auditorCandidates, runAudit, {
|
|
733
|
+
shouldRetry: () => detachedAuditContext(auditGeneration, auditGoalId, auditAttemptId) !== null,
|
|
734
|
+
forbiddenRefs: settings.forbiddenModels,
|
|
735
|
+
retryBaseMinutes: settings.mainModelRetryMinutes,
|
|
736
|
+
onRetry: (candidate: AuditorModelCandidate, err: string) => {
|
|
737
|
+
const current = detachedAuditContext(auditGeneration, auditGoalId, auditAttemptId);
|
|
738
|
+
if (!current) return;
|
|
739
|
+
const failureCopy = providerErrorPresentation(err, "completion");
|
|
740
|
+
latestAuditProgress = {
|
|
741
|
+
...(latestAuditProgress ?? {}),
|
|
742
|
+
model: modelRef(candidate.model),
|
|
743
|
+
via: candidate.via,
|
|
744
|
+
label: `${failureCopy.display} — retrying once`,
|
|
745
|
+
lastEventAt: Date.now(),
|
|
746
|
+
};
|
|
747
|
+
refreshUI(current);
|
|
748
|
+
appendLedger(current.cwd, "audit_infra_retry", { goalId: auditGoalId, model: auditorCandidateLabel(candidate), error: failureCopy.diagnostic.slice(0, 200), diagnostic: failureCopy.diagnostic });
|
|
749
|
+
},
|
|
750
|
+
onFallback: (from: AuditorModelCandidate, to: AuditorModelCandidate, err: string) => {
|
|
751
|
+
const current = detachedAuditContext(auditGeneration, auditGoalId, auditAttemptId);
|
|
752
|
+
if (!current) return;
|
|
753
|
+
appendLedger(current.cwd, "auditor_runtime_model_fallback", { goalId: auditGoalId, from: auditorCandidateLabel(from), to: auditorCandidateLabel(to), error: err.slice(0, 200) });
|
|
754
|
+
current.ui.notify(`Detached auditor failed on ${auditorCandidateLabel(from)} — retrying with ${auditorCandidateLabel(to)}. This is infrastructure, not a verdict.`, "warning");
|
|
755
|
+
},
|
|
756
|
+
}));
|
|
757
|
+
} finally {
|
|
758
|
+
if (ownsDetachedAudit(auditGeneration, auditGoalId, auditAttemptId)) {
|
|
759
|
+
clearDetachedAuditProgress(auditGeneration, auditGoalId, auditAttemptId);
|
|
760
|
+
completionAuditInFlight = false;
|
|
761
|
+
completionAuditGeneration = null;
|
|
762
|
+
}
|
|
707
763
|
}
|
|
708
764
|
}
|
|
709
765
|
const auditContextAfterRun = freshCtxForGeneration(auditGeneration);
|
|
@@ -1376,6 +1432,19 @@ function registerAgentTools(pi: any): void {
|
|
|
1376
1432
|
while (queue.length > 0) {
|
|
1377
1433
|
const t = queue.shift();
|
|
1378
1434
|
if (t.id === p.id && t.status !== "complete") {
|
|
1435
|
+
if (t.verificationContract) {
|
|
1436
|
+
const checkCmds = extractMechanicalCheckCommands(t.verificationContract);
|
|
1437
|
+
const checkRes = runMechanicalPreAuditChecks(ctx.cwd, checkCmds);
|
|
1438
|
+
if (!checkRes.passed) {
|
|
1439
|
+
return {
|
|
1440
|
+
content: [{
|
|
1441
|
+
type: "text",
|
|
1442
|
+
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.`,
|
|
1443
|
+
}],
|
|
1444
|
+
details: {},
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1379
1448
|
t.status = "complete";
|
|
1380
1449
|
updateGoal({ taskList: tl }, ctx);
|
|
1381
1450
|
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
@@ -1603,20 +1672,24 @@ function registerAgentTools(pi: any): void {
|
|
|
1603
1672
|
};
|
|
1604
1673
|
}
|
|
1605
1674
|
}
|
|
1675
|
+
hydrateListQueueFromDisk(liveCtx);
|
|
1606
1676
|
const extracted = parseListItemDeclaration(full);
|
|
1607
|
-
const item = {
|
|
1677
|
+
const item = assignQueueOrder([{
|
|
1608
1678
|
id: newGoalId(),
|
|
1609
1679
|
objective: extracted.objective,
|
|
1610
1680
|
...(extracted.agentRole ? { agentRole: extracted.agentRole } : {}),
|
|
1611
1681
|
verificationContract: extracted.verificationContract || undefined,
|
|
1612
1682
|
...(extracted.parallelSafe === undefined ? {} : { parallelSafe: extracted.parallelSafe }),
|
|
1613
1683
|
addedAt: nowIso(),
|
|
1614
|
-
}
|
|
1684
|
+
}], listQueue())[0]!;
|
|
1615
1685
|
// v0.34.61: disk-first — same invariant as addSingleItem. The list
|
|
1616
1686
|
// draft path was the second-missed place: previously the in-memory
|
|
1617
1687
|
// state mutated without a sidecar, so a torn-rename or post-mutation
|
|
1618
1688
|
// crash could drop the drafted item.
|
|
1619
|
-
writeQueueItemFile(liveCtx.cwd, item);
|
|
1689
|
+
const written = writeQueueItemFile(liveCtx.cwd, item);
|
|
1690
|
+
if (written.failed) {
|
|
1691
|
+
return { content: [{ type: "text", text: "The list draft could not be persisted; no in-memory queue mutation was applied. Fix disk access and retry." }], details: {} };
|
|
1692
|
+
}
|
|
1620
1693
|
replaceState({ ...state, list: [...listQueue(), item] });
|
|
1621
1694
|
persistState(liveCtx);
|
|
1622
1695
|
appendLedger(liveCtx.cwd, "list_added", { id: item.id, objective: item.objective, drafted: true });
|
|
@@ -2016,6 +2089,7 @@ function registerAgentTools(pi: any): void {
|
|
|
2016
2089
|
tasks: Type.Array(Type.Object({
|
|
2017
2090
|
title: Type.String(),
|
|
2018
2091
|
agentRole: Type.Optional(Type.Literal("designer", { description: "Route this task through the read-only Designer subagent before implementation." })),
|
|
2092
|
+
verificationContract: Type.Optional(Type.String({ description: "Optional verification gate command for this milestone before it can be marked complete." })),
|
|
2019
2093
|
subtasks: Type.Optional(Type.Array(Type.String())),
|
|
2020
2094
|
})),
|
|
2021
2095
|
}),
|
|
@@ -224,7 +224,7 @@ import {
|
|
|
224
224
|
textFingerprint,
|
|
225
225
|
pushCapped as pushRepetitionCapped,
|
|
226
226
|
} from "../goal-loop-repetition.js";
|
|
227
|
-
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
227
|
+
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress, type ModelProvenanceDisplay } from "../goal-loop-display.js";
|
|
228
228
|
import {
|
|
229
229
|
defaultAgentDir,
|
|
230
230
|
resolveEffectiveSubagentModel,
|
|
@@ -474,6 +474,8 @@ function isSupervising(): boolean {
|
|
|
474
474
|
|
|
475
475
|
let latestAuditProgress: AuditDisplayProgress | null = null;
|
|
476
476
|
let uiTicker: NodeJS.Timeout | null = null;
|
|
477
|
+
let deferredUIRefresh: NodeJS.Timeout | null = null;
|
|
478
|
+
const UI_REFRESH_SETTLE_MS = 50;
|
|
477
479
|
const LIVE_STREAM_PROOF_MS = 15_000;
|
|
478
480
|
|
|
479
481
|
/**
|
|
@@ -502,6 +504,11 @@ function publishDetachedAuditProgress(
|
|
|
502
504
|
const current = detachedAuditContext(generation, goalId, attemptId);
|
|
503
505
|
if (!current) return false;
|
|
504
506
|
latestAuditProgress = {
|
|
507
|
+
// Candidate identity is marked by the parent before the worker starts;
|
|
508
|
+
// preserve it across progress-file snapshots so the card never loses
|
|
509
|
+
// which model actually handled the audit.
|
|
510
|
+
model: latestAuditProgress?.model,
|
|
511
|
+
via: latestAuditProgress?.via,
|
|
505
512
|
currentTool: progress.currentTool,
|
|
506
513
|
currentToolArgs: progress.currentToolArgs,
|
|
507
514
|
currentToolStartedAt: progress.currentToolStartedAt,
|
|
@@ -634,7 +641,36 @@ function refreshUI(ctx: ExtensionContext): void {
|
|
|
634
641
|
const width = process.stdout.columns || 80;
|
|
635
642
|
const activity = displayActivityFor(ctx);
|
|
636
643
|
const settings = loadSettings(ctx.cwd);
|
|
637
|
-
const
|
|
644
|
+
const sessionRef = modelRef(ctx.model);
|
|
645
|
+
const recovery = state.mainModelRecovery;
|
|
646
|
+
const fallbackRefs = settings.mainModelFallbacks ?? [];
|
|
647
|
+
const skippedForbiddenRefs = [
|
|
648
|
+
...fallbackRefs.filter((ref) => isForbiddenModel(ref, settings.forbiddenModels)),
|
|
649
|
+
...(recovery?.skipped ?? []).filter((entry) => entry.reason === "forbidden").map((entry) => entry.ref),
|
|
650
|
+
];
|
|
651
|
+
const modelProvenance: ModelProvenanceDisplay = {
|
|
652
|
+
// Main-model pins are session-owned today; an explicit goal pin will be
|
|
653
|
+
// able to set primarySource:"pinned" without changing the renderer.
|
|
654
|
+
primary: recovery?.primary ?? sessionRef,
|
|
655
|
+
primarySource: "inherited",
|
|
656
|
+
fallbackRefs,
|
|
657
|
+
skippedForbiddenRefs,
|
|
658
|
+
handledTurn: state.lastModelRef ?? recovery?.active ?? sessionRef,
|
|
659
|
+
...(latestAuditProgress?.model ? {
|
|
660
|
+
handledAudit: latestAuditProgress.model,
|
|
661
|
+
handledAuditSource: latestAuditProgress.via,
|
|
662
|
+
} : {}),
|
|
663
|
+
};
|
|
664
|
+
const extras = {
|
|
665
|
+
stalls: consecutiveStalls,
|
|
666
|
+
recent: recentActions,
|
|
667
|
+
...activity,
|
|
668
|
+
turnPending: pendingContinuationDispatchRef() !== null,
|
|
669
|
+
auditorSilent: loadSettings(ctx.cwd).auditorSilent !== false,
|
|
670
|
+
auditorProgressSignals: settings.auditorProgressSignals !== false,
|
|
671
|
+
mainModelFallbacks: fallbackRefs,
|
|
672
|
+
modelProvenance,
|
|
673
|
+
};
|
|
638
674
|
// Settings are loaded once for the remaining projections; the explicit
|
|
639
675
|
// auditorSilent expression preserves the source-level contract.
|
|
640
676
|
ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress, Date.now(), theme, extras));
|
|
@@ -644,6 +680,26 @@ function refreshUI(ctx: ExtensionContext): void {
|
|
|
644
680
|
}
|
|
645
681
|
}
|
|
646
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Repaint once more after the current event/tool turn yields back to pi.
|
|
685
|
+
*
|
|
686
|
+
* `refreshUI()` is called synchronously when state is persisted, but pi can
|
|
687
|
+
* repaint the transcript/editor after that callback and restore the previous
|
|
688
|
+
* extension widget. The regular ticker is intentionally best-effort and can
|
|
689
|
+
* also be between session contexts at that boundary. A debounced next-turn
|
|
690
|
+
* repaint makes the durable state transition win without making the detached
|
|
691
|
+
* auditor or the main turn wait for UI work.
|
|
692
|
+
*/
|
|
693
|
+
function scheduleUIRefresh(): void {
|
|
694
|
+
if (deferredUIRefresh) return;
|
|
695
|
+
deferredUIRefresh = setTimeout(() => {
|
|
696
|
+
deferredUIRefresh = null;
|
|
697
|
+
const ctx = freshCtx();
|
|
698
|
+
if (ctx) refreshUI(ctx);
|
|
699
|
+
}, UI_REFRESH_SETTLE_MS);
|
|
700
|
+
deferredUIRefresh.unref?.();
|
|
701
|
+
}
|
|
702
|
+
|
|
647
703
|
function startUITicker(): void {
|
|
648
704
|
if (uiTicker) return;
|
|
649
705
|
uiTicker = setInterval(() => {
|
|
@@ -815,6 +871,7 @@ defineGoalRuntimeGlobal("noteToolCall", { get: () => noteToolCall });
|
|
|
815
871
|
defineGoalRuntimeGlobal("noteToolResult", { get: () => noteToolResult });
|
|
816
872
|
defineGoalRuntimeGlobal("displayActivityFor", { get: () => displayActivityFor });
|
|
817
873
|
defineGoalRuntimeGlobal("refreshUI", { get: () => refreshUI });
|
|
874
|
+
defineGoalRuntimeGlobal("scheduleUIRefresh", { get: () => scheduleUIRefresh });
|
|
818
875
|
defineGoalRuntimeGlobal("startUITicker", { get: () => startUITicker });
|
|
819
876
|
defineGoalRuntimeGlobal("loopRearmStreak", { get: () => loopRearmStreak, set: (v) => { loopRearmStreak = v as any; } });
|
|
820
877
|
defineGoalRuntimeGlobal("compactionGraceUntil", { get: () => compactionGraceUntil, set: (v) => { compactionGraceUntil = v as any; } });
|
|
@@ -7,11 +7,30 @@
|
|
|
7
7
|
|
|
8
8
|
export const MAIN_MODEL_MAX_RETRY_DELAY_MS = 5 * 60 * 60_000;
|
|
9
9
|
export const MAIN_MODEL_AUTO_RETRY_HORIZON_MS = 24 * 60 * 60_000;
|
|
10
|
+
export const DEFAULT_MAIN_MODEL_PRIMARY_PROBE_MINUTES = 15;
|
|
10
11
|
/** Keep a fallback chain useful and bounded even when settings are edited
|
|
11
12
|
* outside the UI. Ten alternatives is enough to cross providers/model pools
|
|
12
13
|
* without turning one failure into an unbounded registry walk. */
|
|
13
14
|
export const MAX_MAIN_MODEL_FALLBACKS = 10;
|
|
14
15
|
|
|
16
|
+
export type MainModelFailbackPolicy = "auto" | "sticky";
|
|
17
|
+
|
|
18
|
+
/** Failback is deliberately opt-out: a configured fallback is temporary cover
|
|
19
|
+
* unless the user explicitly keeps it sticky. Invalid/missing JSON values use
|
|
20
|
+
* the safe default rather than disabling recovery by accident. */
|
|
21
|
+
export function isMainModelFailbackAuto(policy: unknown): boolean {
|
|
22
|
+
return policy !== "sticky";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Convert the preferred-primary probe cadence to a timer delay. The settings
|
|
26
|
+
* UI accepts positive minutes; the runtime still clamps hand-edited values to
|
|
27
|
+
* a useful bounded default. */
|
|
28
|
+
export function mainModelPrimaryProbeDelayMs(minutes: unknown = DEFAULT_MAIN_MODEL_PRIMARY_PROBE_MINUTES): number {
|
|
29
|
+
const value = typeof minutes === "number" ? minutes : Number(minutes);
|
|
30
|
+
const safeMinutes = Number.isFinite(value) && value > 0 ? value : DEFAULT_MAIN_MODEL_PRIMARY_PROBE_MINUTES;
|
|
31
|
+
return Math.max(1_000, Math.round(safeMinutes * 60_000));
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
export type MainModelFailureKind = "rate-limit" | "quota" | "billing" | "auth" | "transient" | "unknown" | "non-recoverable" | "context-overflow";
|
|
16
35
|
|
|
17
36
|
export interface MainModelFailure {
|
|
@@ -109,7 +128,11 @@ export function classifyMainModelFailure(error: string | undefined, opts?: { isC
|
|
|
109
128
|
const raw = typeof error === "string" ? error.trim() : "";
|
|
110
129
|
const text = raw.toLowerCase();
|
|
111
130
|
if (!raw) return { kind: "unknown", raw };
|
|
112
|
-
|
|
131
|
+
// Auditor timeouts / watchdog stalls are transient infrastructure failures, not user aborts.
|
|
132
|
+
if (/^(?:auditor (?:exceeded|stalled)|.*(?:timed?\s*out|timeout|inactivity|no session activity))/i.test(raw)) {
|
|
133
|
+
return { kind: "transient", raw };
|
|
134
|
+
}
|
|
135
|
+
if (/^(?:auditor aborted\.?$|user (?:interrupt|abort)|cancelled by user)/i.test(raw) || /user interrupt/.test(text)) {
|
|
113
136
|
return { kind: "non-recoverable", raw };
|
|
114
137
|
}
|
|
115
138
|
if (/context|output[ -]?token|max_?tokens|length limit|too many tokens|prompt too large|context window/.test(text)) {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { fuzzyFilter, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
22
22
|
import type { SettingsMenuTheme, KeybindingsManagerLike } from "./settings-menu.ts";
|
|
23
23
|
|
|
24
|
-
export type ModelPickKind = "session" | "model" | "manual";
|
|
24
|
+
export type ModelPickKind = "session" | "inherit" | "model" | "manual";
|
|
25
25
|
|
|
26
26
|
export interface ModelPickItem {
|
|
27
27
|
kind: ModelPickKind;
|
|
@@ -33,6 +33,8 @@ export interface ModelPickItem {
|
|
|
33
33
|
searchText: string;
|
|
34
34
|
/** A preserved configured ref that cannot currently be selected. */
|
|
35
35
|
disabledReason?: string;
|
|
36
|
+
/** True for the explicit dynamic session-inheritance choice. */
|
|
37
|
+
inheritFromSession?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface RegistryModelLike {
|
|
@@ -41,7 +41,8 @@ import {
|
|
|
41
41
|
export type ModelScope =
|
|
42
42
|
| { kind: "session" }
|
|
43
43
|
| { kind: "subagent"; agentName: string }
|
|
44
|
-
| { kind: "drafter" }
|
|
44
|
+
| { kind: "drafter" }
|
|
45
|
+
| { kind: "auditor" };
|
|
45
46
|
|
|
46
47
|
export interface ModelSelectorDeps {
|
|
47
48
|
/** Read the fallback chain for a scope (returns the configured list of
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// UX:
|
|
15
15
|
// • Space (" ") toggles the highlighted item. Models are toggleable;
|
|
16
16
|
// session-row and manual-row both render but pressing space on them
|
|
17
|
-
// is a no-op (they're not model refs to pick).
|
|
17
|
+
// is a no-op (they're not model refs to pick). An optional explicit
|
|
18
|
+
// inherit-from-session row is a dynamic choice, distinct from both.
|
|
18
19
|
// • Tab enters/exits ORDER MODE: while active, ↑/↓ moves the highlighted
|
|
19
20
|
// chain row earlier/later in the try order (membership unchanged). The
|
|
20
21
|
// active chain row is highlighted in the summary pane. Browsing/search
|
|
@@ -47,13 +48,26 @@ export interface MultiModelPickerDeps {
|
|
|
47
48
|
maxVisibleRows?: number;
|
|
48
49
|
/** Maximum number of model refs that may be selected. Undefined = no cap. */
|
|
49
50
|
maxSelections?: number;
|
|
51
|
+
/** Add a distinct dynamic "inherit from session" choice row. */
|
|
52
|
+
includeInheritOption?: boolean;
|
|
53
|
+
/** Initial state for the explicit inherit choice. */
|
|
54
|
+
initialInheritFromSession?: boolean;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
export
|
|
57
|
+
export interface MultiModelPickerSelection {
|
|
58
|
+
refs: string[];
|
|
59
|
+
inheritFromSession: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The legacy string[] result remains the default; enabling the inherit row
|
|
63
|
+
* returns a selection object so callers cannot confuse inheritance with the
|
|
64
|
+
* old session-clear or manual-entry rows. */
|
|
65
|
+
export type MultiModelPickerResult = string[] | MultiModelPickerSelection | undefined;
|
|
53
66
|
|
|
54
67
|
export class MultiModelPickerComponent {
|
|
55
68
|
private readonly title: string;
|
|
56
69
|
private readonly items: ModelPickItem[];
|
|
70
|
+
private readonly includeInheritOption: boolean;
|
|
57
71
|
private readonly maxRows: number;
|
|
58
72
|
private readonly maxSelections: number | undefined;
|
|
59
73
|
private readonly currentRef: string | undefined;
|
|
@@ -66,6 +80,8 @@ export class MultiModelPickerComponent {
|
|
|
66
80
|
private selectedIdx = 0;
|
|
67
81
|
/** Ordered list of selected refs — toggle order, not list order. */
|
|
68
82
|
private readonly selection: string[];
|
|
83
|
+
/** Explicit dynamic choice; it never becomes a fake provider/model ref. */
|
|
84
|
+
private inheritFromSession: boolean;
|
|
69
85
|
/** Order mode: ↑/↓ move the chain instead of navigating the list. */
|
|
70
86
|
private orderMode = false;
|
|
71
87
|
/** Chain index the order-mode cursor sits on (follows the moved row). */
|
|
@@ -79,7 +95,16 @@ export class MultiModelPickerComponent {
|
|
|
79
95
|
done: (result: MultiModelPickerResult) => void,
|
|
80
96
|
) {
|
|
81
97
|
this.title = deps.title;
|
|
82
|
-
this.
|
|
98
|
+
this.includeInheritOption = deps.includeInheritOption === true || deps.items.some((item) => item.kind === "inherit");
|
|
99
|
+
const inheritItem: ModelPickItem = {
|
|
100
|
+
kind: "inherit",
|
|
101
|
+
label: "inherit from session — use the live session model",
|
|
102
|
+
searchText: "inherit from session live session model",
|
|
103
|
+
inheritFromSession: true,
|
|
104
|
+
};
|
|
105
|
+
this.items = this.includeInheritOption && !deps.items.some((item) => item.kind === "inherit")
|
|
106
|
+
? [inheritItem, ...deps.items]
|
|
107
|
+
: deps.items;
|
|
83
108
|
this.maxRows = deps.maxVisibleRows ?? 12;
|
|
84
109
|
this.maxSelections = deps.maxSelections !== undefined && Number.isInteger(deps.maxSelections) && deps.maxSelections >= 0
|
|
85
110
|
? deps.maxSelections
|
|
@@ -106,6 +131,7 @@ export class MultiModelPickerComponent {
|
|
|
106
131
|
initial.push(itemRef.get(key) ?? ref);
|
|
107
132
|
}
|
|
108
133
|
this.selection = this.maxSelections === undefined ? initial : initial.slice(0, this.maxSelections);
|
|
134
|
+
this.inheritFromSession = this.includeInheritOption && deps.initialInheritFromSession === true;
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
/** Current search query. Exposed for tests. */
|
|
@@ -123,6 +149,11 @@ export class MultiModelPickerComponent {
|
|
|
123
149
|
return [...this.selection];
|
|
124
150
|
}
|
|
125
151
|
|
|
152
|
+
/** Whether the explicit inherit choice is enabled. Exposed for tests. */
|
|
153
|
+
getInheritFromSession(): boolean {
|
|
154
|
+
return this.inheritFromSession;
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
/** Filtered items for the current query. Exposed for tests. */
|
|
127
158
|
filteredItems(): ModelPickItem[] {
|
|
128
159
|
if (!this.query.trim()) return this.items;
|
|
@@ -162,7 +193,15 @@ export class MultiModelPickerComponent {
|
|
|
162
193
|
|
|
163
194
|
private toggle(): void {
|
|
164
195
|
const it = this.selectedItem();
|
|
165
|
-
if (!it
|
|
196
|
+
if (!it) return;
|
|
197
|
+
if (it.kind === "inherit") {
|
|
198
|
+
if (this.includeInheritOption) {
|
|
199
|
+
this.inheritFromSession = !this.inheritFromSession;
|
|
200
|
+
this.refresh();
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (it.kind !== "model" || !it.ref) return;
|
|
166
205
|
const idx = this.selectionIndex(it.ref);
|
|
167
206
|
if (idx >= 0) {
|
|
168
207
|
// Removing a stale/blocked ref is always allowed: the user is fixing
|
|
@@ -234,6 +273,16 @@ export class MultiModelPickerComponent {
|
|
|
234
273
|
return idx >= 0 ? `[${idx + 1}]` : "[ ]";
|
|
235
274
|
}
|
|
236
275
|
|
|
276
|
+
private itemMarker(item: ModelPickItem): string {
|
|
277
|
+
if (item.kind === "inherit") return this.inheritFromSession ? "[inherit]" : "[ ]";
|
|
278
|
+
return this.orderLabel(item.ref);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private result(): MultiModelPickerResult {
|
|
282
|
+
if (!this.includeInheritOption) return [...this.selection];
|
|
283
|
+
return { refs: [...this.selection], inheritFromSession: this.inheritFromSession };
|
|
284
|
+
}
|
|
285
|
+
|
|
237
286
|
private itemForRef(ref: string): ModelPickItem | undefined {
|
|
238
287
|
const key = ref.toLowerCase();
|
|
239
288
|
return this.items.find((item) => item.kind === "model" && item.ref?.toLowerCase() === key);
|
|
@@ -249,6 +298,11 @@ export class MultiModelPickerComponent {
|
|
|
249
298
|
} else {
|
|
250
299
|
lines.push(this.theme.fg("muted", "configured try order (first eligible ref wins):"));
|
|
251
300
|
}
|
|
301
|
+
if (this.includeInheritOption) {
|
|
302
|
+
const inheritedRef = this.currentRef ?? "current session model";
|
|
303
|
+
const state = this.inheritFromSession ? "on" : "off";
|
|
304
|
+
lines.push(truncateToWidth(` inherit session ${inheritedRef} · ${state}`, w, "…"));
|
|
305
|
+
}
|
|
252
306
|
if (this.selection.length === 0) {
|
|
253
307
|
lines.push(this.theme.fg("dim", this.currentRef
|
|
254
308
|
? " — no backups; keep probing the current model"
|
|
@@ -276,6 +330,9 @@ export class MultiModelPickerComponent {
|
|
|
276
330
|
const count = `${this.selection.length}/${this.maxSelections}`;
|
|
277
331
|
lines.push(this.theme.fg(this.selection.length >= this.maxSelections ? "warning" : "muted", `selected: ${count}`));
|
|
278
332
|
}
|
|
333
|
+
if (this.includeInheritOption) {
|
|
334
|
+
lines.push(this.theme.fg("muted", `inherit from session: ${this.inheritFromSession ? "selected" : "not selected"}`));
|
|
335
|
+
}
|
|
279
336
|
lines.push(this.theme.fg("dim", this.orderMode
|
|
280
337
|
? "↑/↓ moves the highlighted backup · tab returns to browsing"
|
|
281
338
|
: "selected rows are tried top-to-bottom · tab = order mode"));
|
|
@@ -292,7 +349,7 @@ export class MultiModelPickerComponent {
|
|
|
292
349
|
for (let i = 0; i < window.length; i++) {
|
|
293
350
|
const idx = start + i;
|
|
294
351
|
const it = window[i]!;
|
|
295
|
-
const marker = this.
|
|
352
|
+
const marker = this.itemMarker(it);
|
|
296
353
|
const disabledReason = this.effectiveDisabledReason(it);
|
|
297
354
|
const disabled = disabledReason && !this.isSelected(it.ref) ? ` · ${disabledReason}` : "";
|
|
298
355
|
const row = truncateToWidth(`${marker} ${it.label}${disabled}`, w - 2, "…");
|
|
@@ -346,7 +403,7 @@ export class MultiModelPickerComponent {
|
|
|
346
403
|
return;
|
|
347
404
|
}
|
|
348
405
|
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
349
|
-
this.done(
|
|
406
|
+
this.done(this.result());
|
|
350
407
|
return;
|
|
351
408
|
}
|
|
352
409
|
if (this.keybindings.matches(data, "tui.select.cancel") || data === "\x1b") {
|
|
@@ -356,7 +413,7 @@ export class MultiModelPickerComponent {
|
|
|
356
413
|
return;
|
|
357
414
|
}
|
|
358
415
|
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
359
|
-
this.done(
|
|
416
|
+
this.done(this.result());
|
|
360
417
|
return;
|
|
361
418
|
}
|
|
362
419
|
if (this.keybindings.matches(data, "tui.select.cancel") || data === "\x1b") {
|
|
@@ -263,6 +263,22 @@ export function buildSettingsRows(
|
|
|
263
263
|
sourceText: src("hourlyRetryProbe"),
|
|
264
264
|
description: "adds a probe at :00:30 while any main-model recovery is parked; off disables only this extra ticker, not the configured retry ladder"
|
|
265
265
|
},
|
|
266
|
+
{
|
|
267
|
+
id: "mainModelFailback",
|
|
268
|
+
section: "main-agent",
|
|
269
|
+
label: "Primary failback policy",
|
|
270
|
+
valueText: show("mainModelFailback", "auto"),
|
|
271
|
+
sourceText: src("mainModelFailback"),
|
|
272
|
+
description: "auto: a healthy fallback periodically tests the preferred primary · sticky: stay on the fallback until manually changed",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "mainModelPrimaryProbeMinutes",
|
|
276
|
+
section: "main-agent",
|
|
277
|
+
label: "Primary probe minutes",
|
|
278
|
+
valueText: show("mainModelPrimaryProbeMinutes", "15"),
|
|
279
|
+
sourceText: src("mainModelPrimaryProbeMinutes"),
|
|
280
|
+
description: "minutes between preferred-primary failback probes while the fallback is serving",
|
|
281
|
+
},
|
|
266
282
|
);
|
|
267
283
|
|
|
268
284
|
// ── Drafter ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.13",
|
|
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",
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"schemas",
|
|
34
34
|
"scripts",
|
|
35
35
|
"docs",
|
|
36
|
+
"examples",
|
|
36
37
|
"README.md",
|
|
38
|
+
"INSTALL.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
37
40
|
"LICENSE"
|
|
38
41
|
],
|
|
39
42
|
"pi": {
|