pi-goal-list-loop-audit 0.38.24 → 0.38.26
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 +13 -0
- package/docs/INDEX.md +3 -2
- package/extensions/approval-render-store.ts +162 -0
- package/extensions/completion-summary.ts +93 -1
- package/extensions/loops/goal-activation.ts +16 -0
- package/extensions/loops/goal-auditor-hooks.ts +32 -28
- package/extensions/loops/goal-tools.ts +44 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.26 — approval-render store: spillway cap keeps undelivered renders (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Post-tag review of the v0.38.25 store: the sidecar cap could trim undelivered renders past 20 entries (non-positive slice budget). The cap now applies to delivered history only — undelivered renders are never dropped (self-draining: any user command replays them). Same 7 behavioral pins green; full gate 1986 pass / 0 fail.
|
|
7
|
+
|
|
8
|
+
## 0.38.25 — post-objective summary: canonical approval render, persist + replay, audit-goal counts (2026-09-07)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Field failure: goal `20260907131550-12ddoy` completed with a perfect six-label archive record, but the approval chat lines fired into a dead context (auditor verdict landed with no live turn) — record perfect, delivery silent. Cross-harness survey (`audit/POST-OBJECTIVE-SUMMARY-2026-09-07.md`): Codex CLI 0.147.0 and Claude Code both leave the final message to the agent and standardize only usage/session-persistence/resume; pi-goal-x (in-repo v0.26.1 source) fixes the report order (verdict → complete → task summary → detail) — validating the outcome-first shape. Nobody persists the render; GLLA now does.
|
|
12
|
+
- ONE canonical builder: `buildTerminalApprovalRender` (outcome + ≤2 details + approval + counts + record) feeds chat, transcript, external, and the persisted render on all three approval paths (detached auditor, manual verify, Esc-without-audit). `buildApprovalChatLines` gains an optional `counts` slot (backward compatible); the stale pre-verdict `Next:` stays stripped everywhere.
|
|
13
|
+
- Audit-goal counts line rides every render, built from durable state only: `— run: 42 turns · 17 file writes · 23 bash calls · auditor approved (1 verdict).` Absent facts are named as absent; the no-audit path says so honestly.
|
|
14
|
+
- Persist + replay: renders persist to `.pi-glla/pending-approval-renders.json` at archive time with `delivered: !isIdle()` (the probe fails toward undelivered — a duplicate beats a loss). All five command handlers (`/goal /glla /review /list /loop`) replay undelivered renders on live contact, fire-once fenced via `deliveredAt`, ledgered (`terminal_approval_render_persisted/_replayed`), corrupt-store safe. Coverage: 7 new behavioral pins + updated source-grep contract.
|
|
15
|
+
|
|
3
16
|
## 0.38.24 — audit pass: abort-latch send guards, ownership compare-and-swap, auditor inherit parity (2026-09-07)
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/docs/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
|
|
|
18
18
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
19
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
20
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14–v0.38.
|
|
21
|
+
v0.35.14–v0.38.26 continue through the supervisor freeze (`/glla pause`),
|
|
22
22
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
23
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
24
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -27,7 +27,8 @@ For shipped docs, the relevant entry points are:
|
|
|
27
27
|
extensions, bounded zero-stream retry containment, crash-safe persistence,
|
|
28
28
|
packed-artifact release verification, and the 2026-09-07 display/lifecycle/
|
|
29
29
|
settings audit pass (abort-latch send guards, ownership compare-and-swap,
|
|
30
|
-
auditor inherit/clear parity)
|
|
30
|
+
auditor inherit/clear parity) and the v0.38.25 post-objective summary
|
|
31
|
+
(canonical approval render, persist + replay, audit-goal counts line) — see CHANGELOG.md for the full trail.
|
|
31
32
|
- `../README.md` — what the plugin is, install, quickstart, and the
|
|
32
33
|
architectural guarantee (drafting + confirm + detached auditor).
|
|
33
34
|
- `../INSTALL.md` — source install / local development setup; the recommended
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { appendLedger, ensureDirs, nowIso, piGlaDir, runPersistStep } from "./goal-loop-core.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persisted terminal-approval renders (v0.38.25, post-objective summary).
|
|
7
|
+
*
|
|
8
|
+
* Field failure 2026-09-07: goal `20260907131550-12ddoy` completed with a
|
|
9
|
+
* perfect six-label archive record, but the approval chat lines fired into a
|
|
10
|
+
* dead context (auditor verdict landed with no live turn) — record perfect,
|
|
11
|
+
* delivery silent. The archive keeps facts; this sidecar keeps the RENDER.
|
|
12
|
+
*
|
|
13
|
+
* Contract: every terminal approval persists its human-sees lines here at
|
|
14
|
+
* archive time. Renders persisted from a live turn are marked delivered at
|
|
15
|
+
* once; renders persisted while the host is idle (`ctx.isIdle()`) stay
|
|
16
|
+
* undelivered until `replayUndeliveredApprovalRenders` delivers them on the
|
|
17
|
+
* next live contact (any user-invoked /goal /glla /review /list /loop
|
|
18
|
+
* command) and marks them delivered. Late verdicts follow the same path:
|
|
19
|
+
* persist first, deliver-or-replay — never silent.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface PendingApprovalRender {
|
|
23
|
+
goalId: string;
|
|
24
|
+
objective: string;
|
|
25
|
+
chatLines: string[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
deliveredAt?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Cap: the sidecar is a spillway, not a log. Delivered renders are kept
|
|
31
|
+
* briefly for inspection; undelivered ones are never dropped by the cap. */
|
|
32
|
+
const MAX_STORED_RENDERS = 20;
|
|
33
|
+
const MAX_REPLAY_PER_CONTACT = 5;
|
|
34
|
+
|
|
35
|
+
export function approvalRenderStorePath(cwd: string): string {
|
|
36
|
+
return path.join(piGlaDir(cwd), "pending-approval-renders.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isValidRender(entry: unknown): entry is PendingApprovalRender {
|
|
40
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
41
|
+
const e = entry as Record<string, unknown>;
|
|
42
|
+
return typeof e.goalId === "string" && e.goalId.length > 0
|
|
43
|
+
&& typeof e.objective === "string"
|
|
44
|
+
&& Array.isArray(e.chatLines)
|
|
45
|
+
&& e.chatLines.every((line) => typeof line === "string")
|
|
46
|
+
&& typeof e.createdAt === "string"
|
|
47
|
+
&& (e.deliveredAt === undefined || typeof e.deliveredAt === "string");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readRenders(cwd: string): PendingApprovalRender[] {
|
|
51
|
+
let raw: string;
|
|
52
|
+
try {
|
|
53
|
+
raw = fs.readFileSync(approvalRenderStorePath(cwd), "utf-8");
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed: unknown = JSON.parse(raw);
|
|
59
|
+
if (!Array.isArray(parsed)) throw new Error("not an array");
|
|
60
|
+
const valid = parsed.filter(isValidRender);
|
|
61
|
+
if (valid.length !== parsed.length) {
|
|
62
|
+
appendLedger(cwd, "terminal_approval_render_store_invalid", {
|
|
63
|
+
dropped: parsed.length - valid.length,
|
|
64
|
+
kept: valid.length,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return valid;
|
|
68
|
+
} catch {
|
|
69
|
+
appendLedger(cwd, "terminal_approval_render_store_invalid", { dropped: "all", kept: 0 });
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeRenders(cwd: string, renders: PendingApprovalRender[]): boolean {
|
|
75
|
+
const landed = runPersistStep("persistApprovalRender", () => {
|
|
76
|
+
ensureDirs(cwd);
|
|
77
|
+
fs.writeFileSync(approvalRenderStorePath(cwd), JSON.stringify(renders, null, 2), "utf-8");
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
return landed === true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Persist the rendered human-sees lines at archive time. `delivered`
|
|
84
|
+
* must be true only when the render went out on a provably live turn
|
|
85
|
+
* (callers pass `!ctx.isIdle()`); idle-persisted renders stay queued for
|
|
86
|
+
* replay. Never throws into orchestrator handlers. */
|
|
87
|
+
export function persistApprovalRender(cwd: string, render: {
|
|
88
|
+
goalId: string;
|
|
89
|
+
objective: string;
|
|
90
|
+
chatLines: string[];
|
|
91
|
+
delivered: boolean;
|
|
92
|
+
}): boolean {
|
|
93
|
+
const at = nowIso();
|
|
94
|
+
const existing = readRenders(cwd);
|
|
95
|
+
const entry: PendingApprovalRender = {
|
|
96
|
+
goalId: render.goalId,
|
|
97
|
+
objective: render.objective.slice(0, 300),
|
|
98
|
+
chatLines: render.chatLines,
|
|
99
|
+
createdAt: at,
|
|
100
|
+
...(render.delivered ? { deliveredAt: at } : {}),
|
|
101
|
+
};
|
|
102
|
+
// The cap trims DELIVERED history only: undelivered renders are never
|
|
103
|
+
// dropped (each is ~1KB and any user command replays them, so the
|
|
104
|
+
// undelivered tail is self-draining in practice).
|
|
105
|
+
const undelivered = existing.filter((e) => !e.deliveredAt);
|
|
106
|
+
const delivered = [...existing.filter((e) => e.deliveredAt), ...(render.delivered ? [entry] : [])];
|
|
107
|
+
const deliveredBudget = Math.max(0, MAX_STORED_RENDERS - undelivered.length - (render.delivered ? 0 : 1));
|
|
108
|
+
const next = [...undelivered, ...(render.delivered ? [] : [entry]), ...delivered.slice(-deliveredBudget)];
|
|
109
|
+
if (!writeRenders(cwd, next)) return false;
|
|
110
|
+
appendLedger(cwd, "terminal_approval_render_persisted", {
|
|
111
|
+
goalId: render.goalId,
|
|
112
|
+
delivered: render.delivered,
|
|
113
|
+
lines: render.chatLines.length,
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Deliver every undelivered render on a live contact, oldest first,
|
|
119
|
+
* fire-once fenced via deliveredAt. Callers are user-invoked command
|
|
120
|
+
* handlers — live by construction. Returns the replayed count; never
|
|
121
|
+
* throws. */
|
|
122
|
+
export function replayUndeliveredApprovalRenders(ctx: { cwd: string; ui: { notify: (message: string, type?: "info" | "warning" | "error") => void } }): number {
|
|
123
|
+
const renders = readRenders(ctx.cwd);
|
|
124
|
+
if (renders.length === 0) return 0;
|
|
125
|
+
const pending = renders.filter((e) => !e.deliveredAt).slice(0, MAX_REPLAY_PER_CONTACT);
|
|
126
|
+
if (pending.length === 0) return 0;
|
|
127
|
+
const at = nowIso();
|
|
128
|
+
let replayed = 0;
|
|
129
|
+
for (const entry of pending) {
|
|
130
|
+
try {
|
|
131
|
+
ctx.ui.notify(entry.chatLines.join("\n"), "info");
|
|
132
|
+
entry.deliveredAt = at;
|
|
133
|
+
replayed += 1;
|
|
134
|
+
appendLedger(ctx.cwd, "terminal_approval_render_replayed", {
|
|
135
|
+
goalId: entry.goalId,
|
|
136
|
+
createdAt: entry.createdAt,
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
// Leave undelivered for the next live contact; never break the
|
|
140
|
+
// command that triggered the replay.
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (replayed > 0) writeRenders(ctx.cwd, renders);
|
|
145
|
+
return replayed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Liveness probe for delivery marking. Unknown/throwing ⇒ idle ⇒
|
|
149
|
+
* undelivered ⇒ replayed on the next live contact. The safe direction is
|
|
150
|
+
* never-silent: a duplicate render on the next command beats a lost one. */
|
|
151
|
+
export function isApprovalContextIdle(ctx: { isIdle?: () => boolean }): boolean {
|
|
152
|
+
try {
|
|
153
|
+
return ctx.isIdle?.() ?? true;
|
|
154
|
+
} catch {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Test-only reset for store isolation. */
|
|
160
|
+
export function __testOnlyResetApprovalRenderStore(): void {
|
|
161
|
+
// No module-level cache — isolation is via cwd-scoped files.
|
|
162
|
+
}
|
|
@@ -173,21 +173,113 @@ export function withoutStaleNext(details: string[] | undefined): string[] {
|
|
|
173
173
|
* informing details (the full record lives in the archive and the
|
|
174
174
|
* transcript notice), then the approval trailer and the record pointer.
|
|
175
175
|
* Five 120-char label lines scan as soup, not a summary (field
|
|
176
|
-
* 2026-09-04); the stale Next never reaches the chat.
|
|
176
|
+
* 2026-09-04); the stale Next never reaches the chat.
|
|
177
|
+
* v0.38.25: optional `counts` audit-goal counts line rides between the
|
|
178
|
+
* approval trailer and the record pointer (the pointer stays last). */
|
|
177
179
|
export function buildApprovalChatLines(notice: {
|
|
178
180
|
outcome: string;
|
|
179
181
|
details: string[] | undefined;
|
|
180
182
|
approval: string;
|
|
181
183
|
record: string;
|
|
184
|
+
counts?: string;
|
|
182
185
|
}): string[] {
|
|
183
186
|
return [
|
|
184
187
|
`✓ done — ${notice.outcome}`,
|
|
185
188
|
...withoutStaleNext(notice.details).slice(0, 2),
|
|
186
189
|
notice.approval,
|
|
190
|
+
...(notice.counts ? [notice.counts] : []),
|
|
187
191
|
notice.record,
|
|
188
192
|
];
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/** v0.38.25: the audit-goal counts line. Compact execution + audit proof
|
|
196
|
+
* built ONLY from durable goal state — turns/file-writes/bash from
|
|
197
|
+
* telemetry, verdict counts from auditHistory. Absent facts are named as
|
|
198
|
+
* absent, never invented (the recorded-facts honesty rule). */
|
|
199
|
+
export function buildAuditCountsLine(goal: Goal, auditNote?: string): string {
|
|
200
|
+
const telemetry = goal.telemetry;
|
|
201
|
+
const run = telemetry
|
|
202
|
+
? `${telemetry.turns} turns · ${telemetry.fileWrites} file writes · ${telemetry.bashCalls} bash calls`
|
|
203
|
+
: "no execution telemetry was recorded";
|
|
204
|
+
const history = goal.auditHistory ?? [];
|
|
205
|
+
const latest = history.length > 0 ? history[history.length - 1] : undefined;
|
|
206
|
+
const audit = auditNote ?? (latest === undefined
|
|
207
|
+
? "no auditor verdict was recorded"
|
|
208
|
+
: (() => {
|
|
209
|
+
const verdict = latest.approved ? "approved" : latest.impossible ? "impossible" : latest.disapproved ? "disapproved" : "no verdict";
|
|
210
|
+
return `auditor ${verdict} (${history.length} verdict${history.length === 1 ? "" : "s"})`;
|
|
211
|
+
})());
|
|
212
|
+
return `— run: ${run} · ${audit}.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TerminalApprovalRenderInput {
|
|
216
|
+
goal: Goal;
|
|
217
|
+
status: Status;
|
|
218
|
+
stopReason?: string;
|
|
219
|
+
archivePath?: string;
|
|
220
|
+
completionSummary?: string;
|
|
221
|
+
/** Path-specific voice, e.g. `— auditor X approved …` or `— completed without audit (your choice).` */
|
|
222
|
+
approval: string;
|
|
223
|
+
/** Path-specific record pointer, e.g. `— record: <path>`. */
|
|
224
|
+
record: string;
|
|
225
|
+
/** Extra trailing lines (inspection-session pointer, …). */
|
|
226
|
+
extras?: string[];
|
|
227
|
+
/** Override for the counts line (e.g. the no-audit path). */
|
|
228
|
+
countsLine?: string;
|
|
229
|
+
/** Override for the audit half of the counts line. */
|
|
230
|
+
auditNote?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface TerminalApprovalRender {
|
|
234
|
+
/** The human-sees chat lines: outcome + ≤2 details + approval + counts + record (+ extras). */
|
|
235
|
+
chatLines: string[];
|
|
236
|
+
/** Compact single line for external notifies (pager/sound safe). */
|
|
237
|
+
recap: string;
|
|
238
|
+
/** Transcript-notice details: informing details (stale Next stripped) + approval. */
|
|
239
|
+
transcriptLines: string[];
|
|
240
|
+
/** The counts line riding the render. */
|
|
241
|
+
countsLine: string;
|
|
242
|
+
/** Brief outcome (chat line 1 without the `✓ done — ` prefix). */
|
|
243
|
+
outcome: string;
|
|
244
|
+
/** Approval trailer line (shared by chat + transcript surfaces). */
|
|
245
|
+
approval: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** v0.38.25: ONE canonical builder for every terminal approval surface —
|
|
249
|
+
* chat notify, transcript notice, external notify, and the persisted
|
|
250
|
+
* archive render. All approval paths (detached auditor, manual verify,
|
|
251
|
+
* Esc-without-audit) build the same voice from the same resolved facts so
|
|
252
|
+
* the surfaces cannot drift and a render can be persisted + replayed. */
|
|
253
|
+
export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput): TerminalApprovalRender {
|
|
254
|
+
const facts: CompletionSummaryFacts = {
|
|
255
|
+
goal: input.goal,
|
|
256
|
+
status: input.status,
|
|
257
|
+
stopReason: input.stopReason,
|
|
258
|
+
archivePath: input.archivePath,
|
|
259
|
+
};
|
|
260
|
+
const candidate = input.completionSummary ?? input.goal.completionSummary;
|
|
261
|
+
const recap = compactTerminalCompletionSummary(facts, candidate);
|
|
262
|
+
const brief = terminalHumanBrief(facts, candidate);
|
|
263
|
+
const countsLine = input.countsLine ?? buildAuditCountsLine(input.goal, input.auditNote);
|
|
264
|
+
return {
|
|
265
|
+
chatLines: [
|
|
266
|
+
...buildApprovalChatLines({
|
|
267
|
+
outcome: brief.outcome,
|
|
268
|
+
details: brief.details,
|
|
269
|
+
approval: input.approval,
|
|
270
|
+
record: input.record,
|
|
271
|
+
counts: countsLine,
|
|
272
|
+
}),
|
|
273
|
+
...(input.extras ?? []),
|
|
274
|
+
],
|
|
275
|
+
recap,
|
|
276
|
+
transcriptLines: [...withoutStaleNext(brief.details), input.approval],
|
|
277
|
+
countsLine,
|
|
278
|
+
outcome: brief.outcome,
|
|
279
|
+
approval: input.approval,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
191
283
|
/** Multi-line projection: one `Label: value` line per label with generous
|
|
192
284
|
* word-bounded values. This is the user-facing `✓ done` block — six short
|
|
193
285
|
* facts that stay scannable in chat. The single-line projection remains
|
|
@@ -124,6 +124,7 @@ isGoalRevisionCurrent,
|
|
|
124
124
|
type ModelSwitchRecord,
|
|
125
125
|
type ListItem,
|
|
126
126
|
} from "../goal-loop-core.js";
|
|
127
|
+
import { replayUndeliveredApprovalRenders } from "../approval-render-store.js";
|
|
127
128
|
import {
|
|
128
129
|
createContinuationDispatch,
|
|
129
130
|
dispatchMatchesOwner,
|
|
@@ -778,12 +779,18 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
778
779
|
handler: (args: string, ctx: ExtensionContext) => {
|
|
779
780
|
rememberCtx(ctx);
|
|
780
781
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
782
|
+
// v0.38.25: live contact — replay any approval render that landed
|
|
783
|
+
// with no live turn (persist-first, never silent).
|
|
784
|
+
replayUndeliveredApprovalRenders(ctx);
|
|
781
785
|
return cmdGoal(args, ctx);
|
|
782
786
|
},
|
|
783
787
|
});
|
|
784
788
|
const settingsHandler = (args: string, ctx: ExtensionContext) => {
|
|
785
789
|
rememberCtx(ctx);
|
|
786
790
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
791
|
+
// v0.38.25: live contact — replay any approval render that landed
|
|
792
|
+
// with no live turn (persist-first, never silent).
|
|
793
|
+
replayUndeliveredApprovalRenders(ctx);
|
|
787
794
|
return cmdSettings(args, ctx);
|
|
788
795
|
};
|
|
789
796
|
pi.registerCommand("glla", {
|
|
@@ -812,6 +819,9 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
812
819
|
handler: (args: string, ctx: ExtensionContext) => {
|
|
813
820
|
rememberCtx(ctx);
|
|
814
821
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
822
|
+
// v0.38.25: live contact — replay any approval render that landed
|
|
823
|
+
// with no live turn (persist-first, never silent).
|
|
824
|
+
replayUndeliveredApprovalRenders(ctx);
|
|
815
825
|
return cmdReview(args, ctx);
|
|
816
826
|
},
|
|
817
827
|
});
|
|
@@ -838,6 +848,9 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
838
848
|
handler: (args: string, ctx: ExtensionContext) => {
|
|
839
849
|
rememberCtx(ctx);
|
|
840
850
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
851
|
+
// v0.38.25: live contact — replay any approval render that landed
|
|
852
|
+
// with no live turn (persist-first, never silent).
|
|
853
|
+
replayUndeliveredApprovalRenders(ctx);
|
|
841
854
|
return cmdList(args, ctx);
|
|
842
855
|
},
|
|
843
856
|
});
|
|
@@ -859,6 +872,9 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
859
872
|
handler: (args: string, ctx: ExtensionContext) => {
|
|
860
873
|
rememberCtx(ctx);
|
|
861
874
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
875
|
+
// v0.38.25: live contact — replay any approval render that landed
|
|
876
|
+
// with no live turn (persist-first, never silent).
|
|
877
|
+
replayUndeliveredApprovalRenders(ctx);
|
|
862
878
|
return cmdLoop(args, ctx);
|
|
863
879
|
},
|
|
864
880
|
});
|
|
@@ -242,7 +242,8 @@ import {
|
|
|
242
242
|
pushCapped as pushRepetitionCapped,
|
|
243
243
|
} from "../goal-loop-repetition.js";
|
|
244
244
|
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
245
|
-
import {
|
|
245
|
+
import { buildTerminalApprovalRender, compactCompletionSummary, isGenericCompletionSummary, missingCompletionSummaryLabels } from "../completion-summary.js";
|
|
246
|
+
import { isApprovalContextIdle, persistApprovalRender } from "../approval-render-store.js";
|
|
246
247
|
import {
|
|
247
248
|
defaultAgentDir,
|
|
248
249
|
resolveEffectiveSubagentModel,
|
|
@@ -1420,23 +1421,29 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
1420
1421
|
// is the agent's completionSummary when captured; the objective is the
|
|
1421
1422
|
// fallback for legacy/aborted goals.
|
|
1422
1423
|
const terminalReason = `auditor ${result.model} approved (${origin})`;
|
|
1423
|
-
const recap = compactTerminalCompletionSummary({
|
|
1424
|
-
goal: state.goal,
|
|
1425
|
-
status: "complete",
|
|
1426
|
-
stopReason: terminalReason,
|
|
1427
|
-
archivePath: path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id),
|
|
1428
|
-
}, state.goal.completionSummary);
|
|
1429
|
-
// Computed pre-archive: archiveCurrentGoal clears state.goal.
|
|
1430
|
-
const brief = terminalHumanBrief({
|
|
1431
|
-
goal: state.goal,
|
|
1432
|
-
status: "complete",
|
|
1433
|
-
stopReason: terminalReason,
|
|
1434
|
-
archivePath: path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id),
|
|
1435
|
-
}, state.goal.completionSummary);
|
|
1436
1424
|
const approvalVia = `${origin === "manual" ? " on /goal verify" : origin === "session-recovery" ? " after session recovery" : " on the provider retry"}${fallbackUsed ? " after an auditor-model fallback" : ""}`;
|
|
1437
1425
|
// v0.38.20: the chat record pointer. Computed pre-archive like the
|
|
1438
|
-
//
|
|
1426
|
+
// render below (archiveCurrentGoal clears state.goal).
|
|
1439
1427
|
const approvalRecord = `— record: ${path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id)}`;
|
|
1428
|
+
const approvalArchivePath = path.relative(liveCtx.cwd, archivedGoalPath(liveCtx.cwd, state.goal.id)) || archivedGoalPath(liveCtx.cwd, state.goal.id);
|
|
1429
|
+
// v0.38.25: ONE canonical render for every approval surface (chat,
|
|
1430
|
+
// transcript, external, persisted). Computed pre-archive — the fence
|
|
1431
|
+
// clears state.goal — and persisted after the archive lands so a
|
|
1432
|
+
// verdict that lands with no live turn is replayed on the next live
|
|
1433
|
+
// contact instead of going silent (field 2026-09-07).
|
|
1434
|
+
const approvalRender = buildTerminalApprovalRender({
|
|
1435
|
+
goal: state.goal,
|
|
1436
|
+
status: "complete",
|
|
1437
|
+
stopReason: terminalReason,
|
|
1438
|
+
archivePath: approvalArchivePath,
|
|
1439
|
+
completionSummary: state.goal.completionSummary,
|
|
1440
|
+
approval: `— auditor ${result.model} approved${approvalVia}.`,
|
|
1441
|
+
record: approvalRecord,
|
|
1442
|
+
extras: inspectionSessionPath
|
|
1443
|
+
? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
|
|
1444
|
+
: [],
|
|
1445
|
+
});
|
|
1446
|
+
const approvalObjective = state.goal.objective;
|
|
1440
1447
|
const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`);
|
|
1441
1448
|
if (!archived) {
|
|
1442
1449
|
// archiveCurrentGoal already preserved the live record and warned the
|
|
@@ -1460,17 +1467,14 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
1460
1467
|
// PR #43: live inspection — the auditor's pi persisted a resumable
|
|
1461
1468
|
// session pinned inside the job dir. Point the user at it AFTER the
|
|
1462
1469
|
// audit (interactive attach only now; while running it was read-only).
|
|
1463
|
-
liveCtx.ui.notify(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
: []),
|
|
1472
|
-
].join("\n"), "info");
|
|
1473
|
-
notifyExternal(liveCtx, `Goal complete (auditor approved, ${origin}): ${recap}`);
|
|
1470
|
+
liveCtx.ui.notify(approvalRender.chatLines.join("\n"), "info");
|
|
1471
|
+
notifyExternal(liveCtx, `Goal complete (auditor approved, ${origin}): ${approvalRender.recap}`);
|
|
1472
|
+
persistApprovalRender(liveCtx.cwd, {
|
|
1473
|
+
goalId,
|
|
1474
|
+
objective: approvalObjective,
|
|
1475
|
+
chatLines: approvalRender.chatLines,
|
|
1476
|
+
delivered: !isApprovalContextIdle(liveCtx),
|
|
1477
|
+
});
|
|
1474
1478
|
// v0.38.18 (track 3): the toast above is ephemeral — without a
|
|
1475
1479
|
// transcript entry the session keeps narrating "waiting on the
|
|
1476
1480
|
// auditor's verdict" after the archive (junk-runner field). Deliver
|
|
@@ -1481,10 +1485,10 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
1481
1485
|
sendTerminalCompletionNotice(liveCtx, {
|
|
1482
1486
|
goalId,
|
|
1483
1487
|
generation,
|
|
1484
|
-
outcome:
|
|
1488
|
+
outcome: approvalRender.outcome,
|
|
1485
1489
|
// v0.38.20: the transcript keeps the informing details, but the
|
|
1486
1490
|
// stale pre-verdict `Next:` is stripped here too.
|
|
1487
|
-
details:
|
|
1491
|
+
details: approvalRender.transcriptLines,
|
|
1488
1492
|
});
|
|
1489
1493
|
}
|
|
1490
1494
|
return;
|
|
@@ -253,7 +253,8 @@ import {
|
|
|
253
253
|
pushCapped as pushRepetitionCapped,
|
|
254
254
|
} from "../goal-loop-repetition.js";
|
|
255
255
|
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
256
|
-
import {
|
|
256
|
+
import { buildTerminalApprovalRender, compactCompletionSummary, compactTerminalCompletionSummary } from "../completion-summary.js";
|
|
257
|
+
import { isApprovalContextIdle, persistApprovalRender } from "../approval-render-store.js";
|
|
257
258
|
import {
|
|
258
259
|
defaultAgentDir,
|
|
259
260
|
resolveEffectiveSubagentModel,
|
|
@@ -1217,11 +1218,18 @@ function registerAgentTools(pi: any): void {
|
|
|
1217
1218
|
const terminalGoal = state.goal;
|
|
1218
1219
|
if (!terminalGoal) return staleToolResult();
|
|
1219
1220
|
const terminalReason = "completed without audit (user choice after Esc)";
|
|
1220
|
-
const
|
|
1221
|
+
const escArchivePath = path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, terminalGoal.id)) || archivedGoalPath(ctx.cwd, terminalGoal.id);
|
|
1222
|
+
// v0.38.25: canonical render (same voice as every approval path).
|
|
1223
|
+
// Built from the terminalGoal local, which survives the archive
|
|
1224
|
+
// fence clearing state.goal.
|
|
1225
|
+
const escRender = buildTerminalApprovalRender({
|
|
1221
1226
|
goal: terminalGoal,
|
|
1222
1227
|
status: "complete",
|
|
1223
1228
|
stopReason: terminalReason,
|
|
1224
|
-
archivePath:
|
|
1229
|
+
archivePath: escArchivePath,
|
|
1230
|
+
approval: `— completed without audit (your choice).`,
|
|
1231
|
+
record: `— record: ${escArchivePath}`,
|
|
1232
|
+
auditNote: "completed without audit (your choice)",
|
|
1225
1233
|
});
|
|
1226
1234
|
if (!archiveCurrentGoal(ctx, "complete", terminalReason)) {
|
|
1227
1235
|
return {
|
|
@@ -1229,23 +1237,18 @@ function registerAgentTools(pi: any): void {
|
|
|
1229
1237
|
details: {},
|
|
1230
1238
|
};
|
|
1231
1239
|
}
|
|
1232
|
-
const brief = terminalHumanBrief({
|
|
1233
|
-
goal: terminalGoal,
|
|
1234
|
-
status: "complete",
|
|
1235
|
-
stopReason: terminalReason,
|
|
1236
|
-
archivePath: path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, terminalGoal.id)) || archivedGoalPath(ctx.cwd, terminalGoal.id),
|
|
1237
|
-
});
|
|
1238
1240
|
// v0.38.20: the command output keeps the informing details (stale
|
|
1239
1241
|
// `Next:` stripped); the chat notify is outcome + approval +
|
|
1240
1242
|
// record pointer like every other approval path.
|
|
1241
|
-
const briefBlock =
|
|
1242
|
-
ctx.ui.notify(
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1243
|
+
const briefBlock = escRender.transcriptLines.join("\n");
|
|
1244
|
+
ctx.ui.notify(escRender.chatLines.join("\n"), "info");
|
|
1245
|
+
notifyExternal(ctx, `Goal complete without audit (user choice): ${escRender.recap}`);
|
|
1246
|
+
persistApprovalRender(ctx.cwd, {
|
|
1247
|
+
goalId: terminalGoal.id,
|
|
1248
|
+
objective: terminalGoal.objective,
|
|
1249
|
+
chatLines: escRender.chatLines,
|
|
1250
|
+
delivered: !isApprovalContextIdle(ctx),
|
|
1251
|
+
});
|
|
1249
1252
|
return { content: [{ type: "text", text: `Goal marked complete without audit (user choice).\n\n${briefBlock}` }], details: {} };
|
|
1250
1253
|
}
|
|
1251
1254
|
scheduleContinuation(ctx, true);
|
|
@@ -1265,22 +1268,25 @@ function registerAgentTools(pi: any): void {
|
|
|
1265
1268
|
// saying "auditor approved" — pure process, no information
|
|
1266
1269
|
// (Screenshot_20260808_012905/013220/013515).
|
|
1267
1270
|
const terminalReason = `auditor ${result.model} approved`;
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
// Computed pre-archive: archiveCurrentGoal clears state.goal.
|
|
1275
|
-
const brief = terminalHumanBrief({
|
|
1271
|
+
const manualArchivePath = path.relative(ctx.cwd, archivedGoalPath(ctx.cwd, state.goal.id)) || archivedGoalPath(ctx.cwd, state.goal.id);
|
|
1272
|
+
// v0.38.20: captured pre-archive — archiveCurrentGoal clears
|
|
1273
|
+
// state.goal, so the record pointer must be computed here.
|
|
1274
|
+
const manualArchiveRecord = `— record: ${manualArchivePath}`;
|
|
1275
|
+
// v0.38.25: canonical render — same voice as the detached path.
|
|
1276
|
+
const manualRender = buildTerminalApprovalRender({
|
|
1276
1277
|
goal: state.goal,
|
|
1277
1278
|
status: "complete",
|
|
1278
1279
|
stopReason: terminalReason,
|
|
1279
|
-
archivePath:
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1280
|
+
archivePath: manualArchivePath,
|
|
1281
|
+
completionSummary: state.goal.completionSummary,
|
|
1282
|
+
approval: `— auditor ${result.model} approved.`,
|
|
1283
|
+
record: manualArchiveRecord,
|
|
1284
|
+
extras: inspectionSessionPath
|
|
1285
|
+
? [`Auditor session kept for review: pi --session ${inspectionSessionPath} (or pi --fork ${inspectionSessionPath}).`]
|
|
1286
|
+
: [],
|
|
1287
|
+
});
|
|
1288
|
+
const manualObjective = state.goal.objective;
|
|
1289
|
+
const manualGoalId = state.goal.id;
|
|
1284
1290
|
const archived = archiveCurrentGoal(ctx, "complete", terminalReason);
|
|
1285
1291
|
if (!archived) {
|
|
1286
1292
|
// The archive helper preserves the live objective and emits the
|
|
@@ -1299,17 +1305,14 @@ function registerAgentTools(pi: any): void {
|
|
|
1299
1305
|
// v0.38.20: same approval voice as the detached path — the stale
|
|
1300
1306
|
// pre-verdict `Next:` never reaches the chat.
|
|
1301
1307
|
// PR #43: append the kept inspection-session pointer when present.
|
|
1302
|
-
ctx.ui.notify(
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
: []),
|
|
1311
|
-
].join("\n"), "info");
|
|
1312
|
-
notifyExternal(ctx, `Goal complete (auditor approved): ${recap}`);
|
|
1308
|
+
ctx.ui.notify(manualRender.chatLines.join("\n"), "info");
|
|
1309
|
+
notifyExternal(ctx, `Goal complete (auditor approved): ${manualRender.recap}`);
|
|
1310
|
+
persistApprovalRender(ctx.cwd, {
|
|
1311
|
+
goalId: manualGoalId,
|
|
1312
|
+
objective: manualObjective,
|
|
1313
|
+
chatLines: manualRender.chatLines,
|
|
1314
|
+
delivered: !isApprovalContextIdle(ctx),
|
|
1315
|
+
});
|
|
1313
1316
|
return { content: [{ type: "text", text: `Goal approved by auditor ${result.model}.` }], details: {} };
|
|
1314
1317
|
}
|
|
1315
1318
|
|
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.26",
|
|
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",
|