pi-goal-list-loop-audit 0.34.49 → 0.34.57
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/README.md +51 -27
- package/docs/DESIGN.md +57 -0
- package/docs/RELEASING.md +6 -0
- package/extensions/goal-loop-auditor-process.ts +23 -5
- package/extensions/goal-loop-auditor.ts +94 -21
- package/extensions/goal-loop-core.ts +104 -5
- package/extensions/goal-loop-display.ts +49 -13
- package/extensions/loops/goal.ts +344 -260
- package/extensions/main-model-recovery.ts +33 -9
- package/package.json +1 -1
- package/scripts/goal-auditor-worker.mjs +48 -5
package/README.md
CHANGED
|
@@ -248,33 +248,47 @@ Activity is otherwise intentionally honest:
|
|
|
248
248
|
| `QUEUED` | A continuation is waiting to start; no work is fabricated. |
|
|
249
249
|
| `IDLE` | The durable item remains active, but no recent work is observed. |
|
|
250
250
|
| `auditor …` | A detached, extension-less verifier is queued, running, quiet, or waiting for its verdict. |
|
|
251
|
-
| `QUOTA WALL` | The provider rejected the request for a quota/plan window; saved work is waiting for a durable probe. |
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
plan is not a real
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
251
|
+
| `QUOTA WALL` | The provider rejected the request for a quota/plan window (by its own wording); saved work is waiting for a durable probe. |
|
|
252
|
+
|
|
253
|
+
## Provider failures: one retry envelope, bounded (v0.34.51)
|
|
254
|
+
|
|
255
|
+
Error text is **not trusted** to pick a retry policy: we only know that an
|
|
256
|
+
error came, and provider messages vary. Every main-model failure — quota,
|
|
257
|
+
rate limit, billing/credits, auth, transient, or unclassifiable — rides the
|
|
258
|
+
same durable recovery envelope `15m → 30m → 1h → 2h → 4h → 5h` (probe cap
|
|
259
|
+
5h, automatic window 24h, then an explicit `/goal resume`/`/list resume`/
|
|
260
|
+
`/loop resume` starts a fresh window). The only failures that do not auto-retry
|
|
261
|
+
are the ones identified by *positive evidence* as futile: context/output-token
|
|
262
|
+
limits and user aborts (`non-recoverable`), plus auditor watchdog timeouts
|
|
263
|
+
(a hanging verification command will hang again — the stored claim waits for
|
|
264
|
+
an explicit resume).
|
|
265
|
+
A provider hint (`retry_after`/`reset_at`) is honored when it fits the
|
|
266
|
+
five-hour probe budget; a week-long hint is shown and held instead of
|
|
267
|
+
scheduling a hidden week-long timer. With global `autoResume=on`, pending
|
|
268
|
+
probes survive a session reload. For continuous work, configure ordered
|
|
269
|
+
**Main model backups** in `/glla` using a model from a different provider or
|
|
270
|
+
billing/quota pool — another model on the same exhausted plan is not a real
|
|
271
|
+
fallback.
|
|
272
|
+
|
|
273
|
+
**Quota walls engage fast** (v0.34.57): a surfaced long-lived failure
|
|
274
|
+
(quota / billing / auth) records a 30-minute knowledge window; a send-rearm
|
|
275
|
+
storm inside that window escalates into the recovery envelope after **3
|
|
276
|
+
minutes** of failed sends instead of the generic 15 — a wedge right after a
|
|
277
|
+
quota wall is almost always the same wall. Transient (5xx/stream/network)
|
|
278
|
+
failures never record the signal and keep the fast error ladder. The
|
|
279
|
+
envelope is armed by configuration: an empty `mainModelFallbacks` list means
|
|
280
|
+
"park and probe the same model" rather than switching pools — the
|
|
281
|
+
never-switch posture is a first-class policy, not an accident.
|
|
282
|
+
|
|
283
|
+
Classification still exists, but it only *labels*: the card and badge show
|
|
284
|
+
what the provider said (quota wall, billing, rate limit) so the reason is
|
|
285
|
+
diagnosable, and `QUOTA WALL` is only shown when the provider's own words say
|
|
286
|
+
quota — ambiguous prose is never relabeled. The raw provider message stays in
|
|
287
|
+
the ledger/durable state; the card shows the classified reason and recovery
|
|
288
|
+
action. Detached-auditor failures get the same treatment: any infrastructure
|
|
289
|
+
error on a stored completion claim pauses the goal with a durable bounded
|
|
290
|
+
one-shot retry (`auditor retry: …`), and only the plan's 24h horizon stops
|
|
291
|
+
automatic probes.
|
|
278
292
|
|
|
279
293
|
The quota-specific card hides raw provider JSON while preserving it in durable
|
|
280
294
|
state and the ledger:
|
|
@@ -337,6 +351,16 @@ warning says no replacement arrived, restart pi normally and let the saved
|
|
|
337
351
|
mux dependency. The legacy `autoReloadOnStale` and `autoRecovery` fields remain
|
|
338
352
|
only as deprecated settings-file compatibility fields; they are ignored.
|
|
339
353
|
|
|
354
|
+
**A stale handle never mutates** (v0.34.51–v0.34.54): every `/list` mutation
|
|
355
|
+
(add/remove/next/clear/cancel) and the bare `/glla` settings surface probe at
|
|
356
|
+
entry and refuse with the standard recovery message on a stale extension
|
|
357
|
+
context — a session that cannot announce or run its writes must not make them.
|
|
358
|
+
Mutating `/glla` actions (wipe/cancel/reviewer/postaudit/tooloverride) leave a
|
|
359
|
+
`settings_mutation_refused_stale` ledger trail; read-only surfaces (`/list
|
|
360
|
+
show`, `/goal status`) stay usable with the warning. Once the replacement
|
|
361
|
+
`session_start` arrives, both surfaces render cleanly with no stale residue
|
|
362
|
+
(the lifecycle-recovery harness proves the two-phase contract).
|
|
363
|
+
|
|
340
364
|
**User aborts mean STOP** (v0.29.4): Esc-aborting a turn stands the chain
|
|
341
365
|
down with a named notify (`/goal resume` to continue) — it does NOT count
|
|
342
366
|
toward stall warnings, does NOT auto re-fire, and the stand-down survives
|
package/docs/DESIGN.md
CHANGED
|
@@ -197,6 +197,63 @@ architectural decisions that changed the SHAPE of the system:
|
|
|
197
197
|
cycle. Manual model selection cancels it; goal/list/loop cancellation clears
|
|
198
198
|
its timer and durable state.
|
|
199
199
|
|
|
200
|
+
## Addendum v0.34.48–v0.34.56 (lifecycle/recovery hardening — the stale-handle era)
|
|
201
|
+
|
|
202
|
+
Between the v0.34.31 recovery envelope and the v0.34.57 quota fast-engagement, the
|
|
203
|
+
recovery story hardened around the **stale extension handle** — the field-observed
|
|
204
|
+
shape (hegemon/polis 2026-07-26+) where pi invalidates the extension API on session
|
|
205
|
+
replacement without delivering a successor `session_start`:
|
|
206
|
+
|
|
207
|
+
- **Honest stale entry everywhere** (v0.34.51/52): `warnIfStaleAtEntry` probes at
|
|
208
|
+
entry (since v0.28.1), but the probe's return value used to be discarded by the
|
|
209
|
+
mutation paths. Now every `/list` mutation (add/remove/next/clear/cancel) and the
|
|
210
|
+
bare `/glla` settings surface refuse on a stale handle with the standard recovery
|
|
211
|
+
message — a session that cannot announce or run its writes must not make them.
|
|
212
|
+
Mutating `/glla` actions (wipe/cancel/reviewer/postaudit/tooloverride) refuse with
|
|
213
|
+
a `settings_mutation_refused_stale` ledger trail; read-only surfaces stay usable
|
|
214
|
+
with the warning. This is the plugin side of the missing-replacement contract: fail
|
|
215
|
+
closed, never guess, never pretend success.
|
|
216
|
+
- **Settings routing clarity** (v0.34.53): `/list settings` is a verb, handled
|
|
217
|
+
explicitly BEFORE the natural-language dump fallthrough — a ledgered redirect to
|
|
218
|
+
`/glla`, never a drafting seed. `/list add settings …` remains the only way an item
|
|
219
|
+
literally named "settings" enters the queue.
|
|
220
|
+
- **Lifecycle-recovery harness** (v0.34.54): a behavioral suite proves the two-phase
|
|
221
|
+
contract — stale handle: `/list show` warns and does not pretend, settings refuse
|
|
222
|
+
wholesale; fresh `session_start`: both render cleanly with no stale residue.
|
|
223
|
+
- **Command-registration collision model** (v0.34.55): pi's `resolveRegisteredCommands`
|
|
224
|
+
flattens extensions in load order and suffixes EVERY registration of a duplicated
|
|
225
|
+
command name (`name:1`, `name:2`, …) — the bare name becomes owned by nobody and
|
|
226
|
+
dispatch stops routing it while a collision exists. The model is read from the
|
|
227
|
+
installed pi core (hermetic, never modified) and auto-records the routing table to
|
|
228
|
+
`audit/command-registration-routing.md`, making collisions reproducible and
|
|
229
|
+
diagnosable without touching pi.
|
|
230
|
+
- **Unmatched telemetry stays unmatched** (v0.34.56): tool starts/ends without a
|
|
231
|
+
counterpart are represented as explicitly unmatched facts, never falsely paired —
|
|
232
|
+
the report surface stays truthful (the AuditProgress/AuditorProgress dual-interface
|
|
233
|
+
rule: display evidence-gates on `unmatchedStarts + unmatchedEnds > 0`).
|
|
234
|
+
- **Uniform retry envelope, no text-trust** (v0.34.51): error text is not trusted to
|
|
235
|
+
pick a retry policy. Quota, billing, auth, transient, and unknown failures all ride
|
|
236
|
+
ONE bounded durable envelope `15m → 30m → 1h → 2h → 4h → 5h` (cap 5h, automatic
|
|
237
|
+
window 24h); classification only labels the display. The billing-hold special case
|
|
238
|
+
is removed (`main_model_billing_hold` is legacy). Positive-evidence futile classes
|
|
239
|
+
(context/output-token limits, user aborts) plus auditor watchdog timeouts never
|
|
240
|
+
auto-retry; provider hints are honored only within the 5h probe budget.
|
|
241
|
+
|
|
242
|
+
## Addendum v0.34.57 (quota walls engage recovery fast)
|
|
243
|
+
|
|
244
|
+
- **Knowledge-window escalation**: a surfaced long-lived failure (quota /
|
|
245
|
+
billing / auth) records a 30-minute knowledge window. A send-rearm storm
|
|
246
|
+
inside that window escalates into the recovery envelope after 3 minutes of
|
|
247
|
+
failed sends (plus the unchanged 5-minute activity silence gate) instead of
|
|
248
|
+
the generic 15 minutes — a wedge right after a quota wall is almost always
|
|
249
|
+
the same wall, and blind re-sends into it are pure waste.
|
|
250
|
+
- **Transient failures stay fast**: 5xx/stream/network failures are
|
|
251
|
+
short-lived by definition and never record the knowledge signal; they keep
|
|
252
|
+
the 5s→3m error ladder and the pi-core retry budget.
|
|
253
|
+
- **Armed by configuration**: the envelope is inert without
|
|
254
|
+
`mainModelFallbacks` (rotation) — an empty list means "park and probe the
|
|
255
|
+
same model" instead of switching pools.
|
|
256
|
+
|
|
200
257
|
## Addendum v0.4.0 (completion)
|
|
201
258
|
|
|
202
259
|
- **Auditor compaction enabled** (flaw #3 — the last open one). Safety:
|
package/docs/RELEASING.md
CHANGED
|
@@ -17,6 +17,12 @@ the repository.
|
|
|
17
17
|
|
|
18
18
|
## Release checklist
|
|
19
19
|
|
|
20
|
+
Accumulated changes since the last release live under an `## Unreleased`
|
|
21
|
+
section at the top of `CHANGELOG.md` (with the in-repo milestone labels such
|
|
22
|
+
as `### 0.34.51`); the release commit renames that section to the released
|
|
23
|
+
version. Do not invent version headers for work that was never tagged —
|
|
24
|
+
untagged work stays under `Unreleased` until the release commit.
|
|
25
|
+
|
|
20
26
|
```bash
|
|
21
27
|
npm version <major.minor.patch> --no-git-tag-version
|
|
22
28
|
npm run release:check
|
|
@@ -39,7 +39,15 @@ export interface AuditorProgress {
|
|
|
39
39
|
currentTool?: string;
|
|
40
40
|
currentToolArgs?: string;
|
|
41
41
|
currentToolStartedAt?: number;
|
|
42
|
+
/** v0.34.56: the toolCallId of the open start (undefined when the start
|
|
43
|
+
* event carried none — the missing-toolCallId shape). */
|
|
44
|
+
currentToolId?: string;
|
|
42
45
|
toolCalls: Array<{ name: string; argsPrefix: string; finishedAt: number }>;
|
|
46
|
+
/** v0.34.56: explicitly unmatched tool starts/ends — see
|
|
47
|
+
* applyToolExecutionEvent (goal-loop-auditor.ts) and the worker's mirror
|
|
48
|
+
* in scripts/goal-auditor-worker.mjs. Never dropped, never falsely paired. */
|
|
49
|
+
unmatchedToolStarts: Array<{ name: string; argsPrefix: string; startedAt: number; toolCallId?: string }>;
|
|
50
|
+
unmatchedToolEnds: Array<{ toolCallId?: string; toolName?: string; at: number }>;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export type AuditorModel = string | { provider: string; id: string };
|
|
@@ -129,6 +137,10 @@ interface AuditorProgressFile {
|
|
|
129
137
|
currentTool?: string;
|
|
130
138
|
currentToolArgs?: string;
|
|
131
139
|
currentToolStartedAt?: number;
|
|
140
|
+
/** v0.34.56: explicitly unmatched tool telemetry facts (see
|
|
141
|
+
* applyToolExecutionEvent in goal-loop-auditor.ts). */
|
|
142
|
+
unmatchedToolStarts?: AuditorProgress["unmatchedToolStarts"];
|
|
143
|
+
unmatchedToolEnds?: AuditorProgress["unmatchedToolEnds"];
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
export interface AuditorProcessRuntime {
|
|
@@ -224,9 +236,13 @@ function asProgress(file: AuditorProgressFile, startedAt: number): AuditorProgre
|
|
|
224
236
|
...(file.lastActivityAt !== undefined ? { lastActivityAt: file.lastActivityAt } : {}),
|
|
225
237
|
recentOutput: file.recentOutput,
|
|
226
238
|
toolCalls: file.toolCalls,
|
|
239
|
+
unmatchedToolStarts: file.unmatchedToolStarts ?? [],
|
|
240
|
+
unmatchedToolEnds: file.unmatchedToolEnds ?? [],
|
|
227
241
|
...(file.currentTool ? { currentTool: file.currentTool } : {}),
|
|
228
242
|
...(file.currentToolArgs ? { currentToolArgs: file.currentToolArgs } : {}),
|
|
229
243
|
...(file.currentToolStartedAt ? { currentToolStartedAt: file.currentToolStartedAt } : {}),
|
|
244
|
+
...(file.unmatchedToolStarts ? { unmatchedToolStarts: file.unmatchedToolStarts } : {}),
|
|
245
|
+
...(file.unmatchedToolEnds ? { unmatchedToolEnds: file.unmatchedToolEnds } : {}),
|
|
230
246
|
};
|
|
231
247
|
}
|
|
232
248
|
|
|
@@ -355,17 +371,19 @@ export async function runDetachedGoalCompletionAuditor(args: {
|
|
|
355
371
|
if (parsed.approved && args.goal.verificationContract?.trim()) {
|
|
356
372
|
const shield = checkRegressionShield(output, args.goal.verificationContract);
|
|
357
373
|
if (!shield.passed) {
|
|
358
|
-
|
|
374
|
+
// The auditor's semantic verdict was approval; the separate
|
|
375
|
+
// regression shield blocked acceptance because the report did
|
|
376
|
+
// not cite every contract item. Keep that outcome distinct from
|
|
377
|
+
// both a work disapproval and infrastructure failure.
|
|
359
378
|
return {
|
|
360
|
-
approved:
|
|
361
|
-
error: `regression_shield: approved but ${why}`,
|
|
379
|
+
approved: true, disapproved: false, output, model, thinkingLevel,
|
|
362
380
|
regressionShieldPassed: false, regressionShieldMissing: shield.missingItems,
|
|
363
381
|
};
|
|
364
382
|
}
|
|
365
|
-
args.onProgress?.({ phase: "complete", elapsedMs: now() - startedAt, recentOutput: output.split("\n").filter(Boolean).slice(-8), toolCalls: result.toolCalls });
|
|
383
|
+
args.onProgress?.({ phase: "complete", elapsedMs: now() - startedAt, recentOutput: output.split("\n").filter(Boolean).slice(-8), toolCalls: result.toolCalls, unmatchedToolStarts: [], unmatchedToolEnds: [] });
|
|
366
384
|
return { approved: true, disapproved: false, output, model, thinkingLevel, regressionShieldPassed: true };
|
|
367
385
|
}
|
|
368
|
-
args.onProgress?.({ phase: "complete", elapsedMs: now() - startedAt, recentOutput: output.split("\n").filter(Boolean).slice(-8), toolCalls: result.toolCalls });
|
|
386
|
+
args.onProgress?.({ phase: "complete", elapsedMs: now() - startedAt, recentOutput: output.split("\n").filter(Boolean).slice(-8), toolCalls: result.toolCalls, unmatchedToolStarts: [], unmatchedToolEnds: [] });
|
|
369
387
|
return { approved: parsed.approved, disapproved: parsed.disapproved, impossible: parsed.impossible, impossibleReason: parsed.impossibleReason, output, model, thinkingLevel };
|
|
370
388
|
} catch (error) {
|
|
371
389
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return infra(model, thinkingLevel, `invalid auditor result: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -67,8 +67,80 @@ export interface AuditProgress {
|
|
|
67
67
|
currentTool?: string;
|
|
68
68
|
currentToolArgs?: string;
|
|
69
69
|
currentToolStartedAt?: number;
|
|
70
|
+
/** v0.34.56: the toolCallId of the open start (undefined when the start
|
|
71
|
+
* event carried none — the missing-toolCallId shape). */
|
|
72
|
+
currentToolId?: string;
|
|
70
73
|
// Tool-call history for regression_shield:
|
|
71
74
|
toolCalls: Array<{ name: string; argsPrefix: string; finishedAt: number }>;
|
|
75
|
+
/** v0.34.56: tool_execution_start events whose end never arrived (the
|
|
76
|
+
* start was replaced by a later start with a different/absent id). These
|
|
77
|
+
* are EXPLICIT unmatched facts — never silently re-paired with a wrong
|
|
78
|
+
* end. */
|
|
79
|
+
unmatchedToolStarts: Array<{ name: string; argsPrefix: string; startedAt: number; toolCallId?: string }>;
|
|
80
|
+
/** v0.34.56: tool_execution_end events that provably do not close the
|
|
81
|
+
* open start (wrong/absent id, or no open start at all). Represented
|
|
82
|
+
* explicitly instead of being dropped or falsely paired. */
|
|
83
|
+
unmatchedToolEnds: Array<{ toolCallId?: string; toolName?: string; at: number }>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type AuditorToolExecutionEvent =
|
|
87
|
+
| { type: "tool_execution_start"; toolCallId?: string; toolName: string; args?: unknown }
|
|
88
|
+
| { type: "tool_execution_end"; toolCallId?: string; toolName?: string };
|
|
89
|
+
|
|
90
|
+
/** v0.34.56: tool-execution pairing, extracted pure so the telemetry is
|
|
91
|
+
* regression-testable. Truth rules:
|
|
92
|
+
*
|
|
93
|
+
* - A start while a start is open with a different (or absent) id means the
|
|
94
|
+
* open start never received its end — record it in unmatchedToolStarts as
|
|
95
|
+
* an explicitly unmatched fact, then adopt the new start.
|
|
96
|
+
* - An end pairs ONLY when: both sides carry the SAME id, or both sides are
|
|
97
|
+
* anonymous (the id-less serial stream — the only pairing an id-less
|
|
98
|
+
* stream can support).
|
|
99
|
+
* - An end with a different id than the open start, an id'd end against an
|
|
100
|
+
* anonymous start, an anonymous end against an id'd start, or an end with
|
|
101
|
+
* no open start at all: recorded in unmatchedToolEnds. NEVER paired with
|
|
102
|
+
* the wrong start, NEVER silently dropped.
|
|
103
|
+
*/
|
|
104
|
+
export function applyToolExecutionEvent(
|
|
105
|
+
telemetry: AuditProgress,
|
|
106
|
+
event: AuditorToolExecutionEvent,
|
|
107
|
+
now: number,
|
|
108
|
+
): AuditProgress {
|
|
109
|
+
if (event.type === "tool_execution_start") {
|
|
110
|
+
const sameId = event.toolCallId !== undefined && event.toolCallId === telemetry.currentToolId;
|
|
111
|
+
if (telemetry.currentTool !== undefined && !sameId) {
|
|
112
|
+
telemetry.unmatchedToolStarts.push({
|
|
113
|
+
name: telemetry.currentTool,
|
|
114
|
+
argsPrefix: telemetry.currentToolArgs ?? "",
|
|
115
|
+
startedAt: telemetry.currentToolStartedAt ?? now,
|
|
116
|
+
toolCallId: telemetry.currentToolId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
telemetry.currentTool = event.toolName;
|
|
120
|
+
telemetry.currentToolArgs = typeof event.args === "object" && event.args !== null
|
|
121
|
+
? JSON.stringify(event.args).slice(0, 120)
|
|
122
|
+
: String(event.args ?? "").slice(0, 120);
|
|
123
|
+
telemetry.currentToolStartedAt = now;
|
|
124
|
+
telemetry.currentToolId = event.toolCallId;
|
|
125
|
+
return telemetry;
|
|
126
|
+
}
|
|
127
|
+
const open = telemetry.currentTool;
|
|
128
|
+
if (open !== undefined) {
|
|
129
|
+
const idMatches = event.toolCallId !== undefined && telemetry.currentToolId !== undefined && event.toolCallId === telemetry.currentToolId;
|
|
130
|
+
const bothAnonymous = event.toolCallId === undefined && telemetry.currentToolId === undefined;
|
|
131
|
+
if (idMatches || bothAnonymous) {
|
|
132
|
+
telemetry.toolCalls.push({ name: open, argsPrefix: telemetry.currentToolArgs ?? "", finishedAt: now });
|
|
133
|
+
telemetry.currentTool = undefined;
|
|
134
|
+
telemetry.currentToolArgs = undefined;
|
|
135
|
+
telemetry.currentToolStartedAt = undefined;
|
|
136
|
+
telemetry.currentToolId = undefined;
|
|
137
|
+
return telemetry;
|
|
138
|
+
}
|
|
139
|
+
// The end provably does not close the open start — the start stays open
|
|
140
|
+
// (it has not ended) and the end is recorded as an unmatched fact.
|
|
141
|
+
}
|
|
142
|
+
telemetry.unmatchedToolEnds.push({ toolCallId: event.toolCallId, toolName: event.toolName, at: now });
|
|
143
|
+
return telemetry;
|
|
72
144
|
}
|
|
73
145
|
|
|
74
146
|
export type AuditorProgressCallback = (progress: AuditProgress) => void;
|
|
@@ -223,6 +295,8 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
223
295
|
return { approved: false, disapproved: false, output: "", model: "(unset)", thinkingLevel, error: "no model (session model also unset)" };
|
|
224
296
|
}
|
|
225
297
|
const toolCalls: AuditProgress["toolCalls"] = [];
|
|
298
|
+
const unmatchedToolStarts: AuditProgress["unmatchedToolStarts"] = [];
|
|
299
|
+
const unmatchedToolEnds: AuditProgress["unmatchedToolEnds"] = [];
|
|
226
300
|
|
|
227
301
|
try {
|
|
228
302
|
const startedAt = Date.now();
|
|
@@ -231,6 +305,8 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
231
305
|
phase: "running",
|
|
232
306
|
elapsedMs: 0,
|
|
233
307
|
toolCalls,
|
|
308
|
+
unmatchedToolStarts,
|
|
309
|
+
unmatchedToolEnds,
|
|
234
310
|
};
|
|
235
311
|
function emitProgress(): void {
|
|
236
312
|
progress.elapsedMs = Date.now() - startedAt;
|
|
@@ -291,26 +367,24 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
291
367
|
if (typeof msg === "string") streamError = msg.slice(0, 300);
|
|
292
368
|
}
|
|
293
369
|
if (event.type === "tool_execution_start") {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
:
|
|
298
|
-
|
|
370
|
+
// v0.34.56: pairing lives in the pure applyToolExecutionEvent — the
|
|
371
|
+
// telemetry regression test drives it directly.
|
|
372
|
+
applyToolExecutionEvent(progress, {
|
|
373
|
+
type: "tool_execution_start",
|
|
374
|
+
toolCallId: (anyEvent.toolCallId as string | undefined) ?? undefined,
|
|
375
|
+
toolName: event.toolName,
|
|
376
|
+
args: anyEvent.args,
|
|
377
|
+
}, Date.now());
|
|
299
378
|
progress.phase = "tool_executing";
|
|
300
379
|
emitProgress();
|
|
301
380
|
return;
|
|
302
381
|
}
|
|
303
382
|
if (event.type === "tool_execution_end") {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
progress.currentTool = undefined;
|
|
312
|
-
progress.currentToolArgs = undefined;
|
|
313
|
-
progress.currentToolStartedAt = undefined;
|
|
383
|
+
applyToolExecutionEvent(progress, {
|
|
384
|
+
type: "tool_execution_end",
|
|
385
|
+
toolCallId: (anyEvent.toolCallId as string | undefined) ?? undefined,
|
|
386
|
+
toolName: anyEvent.toolName as string | undefined,
|
|
387
|
+
}, Date.now());
|
|
314
388
|
progress.phase = "running";
|
|
315
389
|
emitProgress();
|
|
316
390
|
return;
|
|
@@ -429,16 +503,15 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
429
503
|
if (approved && args.goal.verificationContract?.trim()) {
|
|
430
504
|
const shield = checkRegressionShield(output, args.goal.verificationContract);
|
|
431
505
|
if (!shield.passed) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
506
|
+
// Keep the auditor's semantic approval distinct from the
|
|
507
|
+
// orchestrator-side shield block. This is neither a disapproval of
|
|
508
|
+
// the work nor an infrastructure failure.
|
|
435
509
|
return {
|
|
436
|
-
approved:
|
|
437
|
-
disapproved:
|
|
510
|
+
approved: true,
|
|
511
|
+
disapproved: false,
|
|
438
512
|
output,
|
|
439
513
|
model: modelLabel(model),
|
|
440
514
|
thinkingLevel,
|
|
441
|
-
error: `regression_shield: approved but ${why}`,
|
|
442
515
|
regressionShieldPassed: false,
|
|
443
516
|
regressionShieldMissing: shield.missingItems,
|
|
444
517
|
};
|
|
@@ -35,6 +35,26 @@ export type Status =
|
|
|
35
35
|
|
|
36
36
|
export type Policy = "goal" | "list"; // v0.3.0: "loop".
|
|
37
37
|
|
|
38
|
+
/** User-facing controls whose command root follows the active goal policy. */
|
|
39
|
+
export type ModeCommand = "pause" | "tweak" | "resume";
|
|
40
|
+
|
|
41
|
+
/** Return the command root for a supervised work surface. */
|
|
42
|
+
export function workCommandRoot(mode: Policy | "loop" | undefined): "/goal" | "/list" | "/loop" {
|
|
43
|
+
if (mode === "list") return "/list";
|
|
44
|
+
if (mode === "loop") return "/loop";
|
|
45
|
+
return "/goal";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build a mode-correct pause/tweak/resume command for a goal or list item. */
|
|
49
|
+
export function modeCommand(mode: Policy | undefined, command: ModeCommand): string {
|
|
50
|
+
return `${workCommandRoot(mode)} ${command}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build a command for a goal, list item, or metric loop. */
|
|
54
|
+
export function workCommand(mode: Policy | "loop" | undefined, command: string): string {
|
|
55
|
+
return `${workCommandRoot(mode)} ${command}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
export interface Task {
|
|
39
59
|
id: string;
|
|
40
60
|
title: string;
|
|
@@ -114,6 +134,26 @@ export interface AuditVerdict {
|
|
|
114
134
|
regressionShieldMissing?: string[];
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
/** The display classification for one stored auditor result. Keep semantic
|
|
138
|
+
* verdicts separate from operational failures: a shield-blocked approval is
|
|
139
|
+
* not a disapproval, and an infrastructure error is not a verdict at all. */
|
|
140
|
+
export type AuditVerdictLabel =
|
|
141
|
+
| "approved"
|
|
142
|
+
| "disapproved"
|
|
143
|
+
| "impossible"
|
|
144
|
+
| "shield-blocked"
|
|
145
|
+
| "infrastructure failure"
|
|
146
|
+
| "no verdict";
|
|
147
|
+
|
|
148
|
+
export function auditVerdictLabel(v: Pick<AuditVerdict, "approved" | "disapproved" | "impossible" | "error" | "regressionShieldPassed">): AuditVerdictLabel {
|
|
149
|
+
if (v.approved && v.regressionShieldPassed === false) return "shield-blocked";
|
|
150
|
+
if (v.approved) return "approved";
|
|
151
|
+
if (v.impossible) return "impossible";
|
|
152
|
+
if (v.disapproved) return "disapproved";
|
|
153
|
+
if (v.error) return "infrastructure failure";
|
|
154
|
+
return "no verdict";
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
/**
|
|
118
158
|
* Sum token usage across assistant messages, counting each message once.
|
|
119
159
|
* `agent_end` events may include already-seen history, so callers pass a
|
|
@@ -307,6 +347,45 @@ export function listMutationBlocked(draftingTarget: string | null): boolean {
|
|
|
307
347
|
export const LIST_DRAFTING_BLOCK_MESSAGE =
|
|
308
348
|
"LIST DRAFTING IN PROGRESS — do not add items one by one. Decompose the request into an items[] array and call propose_goal_draft ONCE: the user confirms the whole batch in a single dialog. list_add / list_activate work again after the drafting session ends.";
|
|
309
349
|
|
|
350
|
+
/**
|
|
351
|
+
* v0.34.51: /list subcommands that persist or activate work — refused on a
|
|
352
|
+
* stale extension handle by cmdList's entry probe. Read-only verbs (show,
|
|
353
|
+
* depth, status) stay available so the user can still inspect the queue
|
|
354
|
+
* while the lifecycle replacement is pending. The natural-language dump
|
|
355
|
+
* fallthrough is always mutating and is gated separately at the call site.
|
|
356
|
+
*/
|
|
357
|
+
export const LIST_MUTATING_SUBCOMMANDS = new Set([
|
|
358
|
+
"audit",
|
|
359
|
+
"tweak",
|
|
360
|
+
"pause",
|
|
361
|
+
"resume",
|
|
362
|
+
"add",
|
|
363
|
+
"import",
|
|
364
|
+
"clear",
|
|
365
|
+
"cancel",
|
|
366
|
+
"next",
|
|
367
|
+
"remove",
|
|
368
|
+
"rm",
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* v0.34.52: /glla verbs that open a settings surface or persist config —
|
|
373
|
+
* refused on a stale extension handle by cmdSettings' entry probe, mirroring
|
|
374
|
+
* the /list gate. Bare /glla (verb "ui") is the settings entry itself: every
|
|
375
|
+
* table choice writes state. wipe/reset/cancel/resume/reviewer/postaudit/
|
|
376
|
+
* tooloverride mutate directly. Read-only surfaces (status, log, stats,
|
|
377
|
+
* audits) and the unknown-action notice stay available for inspection.
|
|
378
|
+
*/
|
|
379
|
+
export const SETTINGS_MUTATING_ACTIONS = new Set([
|
|
380
|
+
"wipe",
|
|
381
|
+
"reset",
|
|
382
|
+
"cancel",
|
|
383
|
+
"resume",
|
|
384
|
+
"reviewer",
|
|
385
|
+
"postaudit",
|
|
386
|
+
"tooloverride",
|
|
387
|
+
]);
|
|
388
|
+
|
|
310
389
|
/**
|
|
311
390
|
* Route natural-language text handed to `/list` with no subcommand verb
|
|
312
391
|
* (v0.18.0). The user typed a dump — "fix x, do y, write docs" — not a
|
|
@@ -725,7 +804,7 @@ export function renderGoalMarkdown(goal: Goal): string {
|
|
|
725
804
|
lines.push("## Audit history");
|
|
726
805
|
lines.push("");
|
|
727
806
|
for (const v of goal.auditHistory) {
|
|
728
|
-
lines.push(`- ${v.at} — ${v
|
|
807
|
+
lines.push(`- ${v.at} — ${auditVerdictLabel(v)} — \`${v.model}\``);
|
|
729
808
|
}
|
|
730
809
|
lines.push("");
|
|
731
810
|
}
|
|
@@ -899,15 +978,27 @@ export function draftContractItemCount(normalized: string): number {
|
|
|
899
978
|
* module so tests exercise THIS function, not a copy (the pre-0.23.7 test
|
|
900
979
|
* re-implemented it and silently went stale).
|
|
901
980
|
*/
|
|
902
|
-
export function extractVerificationContract(raw: string): { objective: string; verificationContract: string } {
|
|
981
|
+
export function extractVerificationContract(raw: string): { objective: string; verificationContract: string; explicitClear: boolean } {
|
|
903
982
|
// Line-based first: a marker at line start begins the contract block.
|
|
904
983
|
const lines = raw.split("\n");
|
|
905
984
|
let mode: "obj" | "verify" = "obj";
|
|
906
985
|
const objParts: string[] = [];
|
|
907
986
|
const verifyParts: string[] = [];
|
|
987
|
+
// v0.34.51: a BARE contract marker ("Done when:" with nothing after it) is
|
|
988
|
+
// an explicit CLEAR signal — the caller wipes the stored contract instead
|
|
989
|
+
// of preserving or replacing it. The bare marker line is consumed, never
|
|
990
|
+
// kept as contract text; later lines after a bare marker still belong to
|
|
991
|
+
// the contract block.
|
|
992
|
+
let explicitClear = false;
|
|
993
|
+
const MARKER_START = /^\s*(?:done when|verified when|verify|verification|done)\b[^:]*:/i;
|
|
908
994
|
for (const line of lines) {
|
|
909
|
-
|
|
995
|
+
const m = line.match(MARKER_START);
|
|
996
|
+
if (m) {
|
|
910
997
|
mode = "verify";
|
|
998
|
+
if (!line.slice(m[0].length).trim()) {
|
|
999
|
+
explicitClear = true;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
911
1002
|
}
|
|
912
1003
|
if (mode === "obj") objParts.push(line);
|
|
913
1004
|
else verifyParts.push(line);
|
|
@@ -923,9 +1014,16 @@ export function extractVerificationContract(raw: string): { objective: string; v
|
|
|
923
1014
|
if (m) {
|
|
924
1015
|
objective = (m[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
925
1016
|
verificationContract = (m[2] ?? "").trim();
|
|
1017
|
+
} else if (!explicitClear) {
|
|
1018
|
+
// Trailing bare marker mid-line: "Do x. Done when:" — explicit clear.
|
|
1019
|
+
const empty = raw.match(/^(.*?)\s+(?:done when|verified when|verify|verification)\b[^:]*:\s*$/is);
|
|
1020
|
+
if (empty) {
|
|
1021
|
+
objective = (empty[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
1022
|
+
explicitClear = true;
|
|
1023
|
+
}
|
|
926
1024
|
}
|
|
927
1025
|
}
|
|
928
|
-
return { objective, verificationContract };
|
|
1026
|
+
return { objective, verificationContract, explicitClear };
|
|
929
1027
|
}
|
|
930
1028
|
|
|
931
1029
|
/**
|
|
@@ -1440,7 +1538,8 @@ export function formatGoalAuditHistory(goal: { id: string; auditHistory?: Array<
|
|
|
1440
1538
|
if (history.length === 0) return "(no audits on this goal yet)";
|
|
1441
1539
|
return history
|
|
1442
1540
|
.map((v) => {
|
|
1443
|
-
const
|
|
1541
|
+
const verdict = auditVerdictLabel(v);
|
|
1542
|
+
const glyph = verdict === "approved" ? "✔" : verdict === "shield-blocked" ? "🛡" : verdict === "impossible" ? "⛔" : verdict === "disapproved" ? "✖" : "⚠";
|
|
1444
1543
|
const day = String(v.at ?? "").slice(5, 16).replace("T", " ");
|
|
1445
1544
|
const elapsed = v.durationMs ? ` · ${Math.round(v.durationMs / 60000)}m` : "";
|
|
1446
1545
|
const firstLine = (String(v.report ?? "").split("\n").find((l: string) => l.trim()) ?? "").trim().slice(0, 80);
|