skalpel 4.0.30 → 4.0.31
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/card.mjs +306 -0
- package/package.json +1 -1
- package/skalpel-setup.mjs +18 -0
package/card.mjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// card.mjs — THE CATCH CARD: `skalpel card`.
|
|
3
|
+
//
|
|
4
|
+
// THE PROBLEM (same root as verify-shadow / catches): your agent is both the executor AND the reporter
|
|
5
|
+
// of its own work, so when it says "done / tests pass" nothing independent re-checks. verify-shadow
|
|
6
|
+
// already closes that loop LIVE and logs the real verdict. THIS turns a single, already-verified catch
|
|
7
|
+
// into a clean, copy-pasteable artifact you can drop into Slack / a PR / X — so the moment the agent
|
|
8
|
+
// gets caught is shareable, and it's the AGENT that's on the hook, never you.
|
|
9
|
+
//
|
|
10
|
+
// WHAT IT IS: a one-command render of the MOST RECENT genuine catch (a GENUINE_FAIL row) that
|
|
11
|
+
// verify-shadow already wrote to ~/.skalpel/verify-shadow.log. The card carries ONLY what the real row
|
|
12
|
+
// holds: the agent's verbatim completion claim, the exact proof/probe command skalpel re-ran, the real
|
|
13
|
+
// verdict (FAIL — the proof exited non-zero; plus a real exit code / HTTP status / failing-count ONLY
|
|
14
|
+
// when the captured output literally contains one), the verbatim tail of the real output, and the real
|
|
15
|
+
// timestamp. Nothing else — no summaries, no aggregates, no cross-model claims, no minutes.
|
|
16
|
+
//
|
|
17
|
+
// HARD RED LINES (why this is safe + honest):
|
|
18
|
+
// • READ-ONLY over verify-shadow's OWN output. It only READS ~/.skalpel/verify-shadow.log. It NEVER
|
|
19
|
+
// re-runs a proof, spawns, builds, writes, deploys, or mutates anything, and opens no socket.
|
|
20
|
+
// • OPT-IN. User-invoked subcommand; prints and exits. The per-turn hook is untouched (byte-identical
|
|
21
|
+
// when the shadow is off — this file is never imported by the hook path).
|
|
22
|
+
// • VERIFIED CATCHES ONLY. It renders a row ONLY if that row is a GENUINE_FAIL (the agent's own
|
|
23
|
+
// test/build/lint really ran and exited non-zero). A HARNESS_ERROR (timeout / missing-script /
|
|
24
|
+
// wrong-cwd) is proof-machinery noise, never a catch, and is never rendered — we never cry wolf.
|
|
25
|
+
// • NO FABRICATED NUMBERS. Every value on the card traces to a real field of the real row. The banned
|
|
26
|
+
// "minutes saved / lost" metric never appears. An exit code / HTTP status / failing count is shown
|
|
27
|
+
// ONLY when the row's own captured output literally contains it. If there is no catch to show, it
|
|
28
|
+
// SAYS so honestly — it never invents one.
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { realpathSync } from "node:fs";
|
|
32
|
+
import { VERIFY_LOG_PATH, extractFailCount } from "./verify-shadow.mjs";
|
|
33
|
+
|
|
34
|
+
// Bound the read so a pathological log can never OOM us (verify-shadow already trims to ~1MB/500 lines).
|
|
35
|
+
const MAX_LOG_BYTES = 4 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
// ---------- palette (matches the statusline / setup) ----------
|
|
38
|
+
const R = "\x1b[38;2;233;38;31m"; // skalpel red
|
|
39
|
+
const D = "\x1b[2m";
|
|
40
|
+
const B = "\x1b[1m";
|
|
41
|
+
const X = "\x1b[0m";
|
|
42
|
+
|
|
43
|
+
// ---------- sanitize (values come from real captured output — strip any control/ANSI bytes) ----------
|
|
44
|
+
// The claim / proof / evidence are captured from the agent's transcript + test output; they must never
|
|
45
|
+
// be able to inject their own escape codes into the terminal or a pasted card. Strip ESC + other C0
|
|
46
|
+
// control chars (keep normal whitespace which we then collapse), and cap length.
|
|
47
|
+
function clean(s, n) {
|
|
48
|
+
const t = String(s == null ? "" : s)
|
|
49
|
+
// eslint-disable-next-line no-control-regex
|
|
50
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // ANSI CSI sequences
|
|
51
|
+
// eslint-disable-next-line no-control-regex
|
|
52
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ") // other control chars → space
|
|
53
|
+
.replace(/\s+/g, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
if (n && t.length > n) return t.slice(0, n - 1) + "…";
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------- (1) read + select the catch ----------
|
|
60
|
+
// A row is a genuine catch iff the agent's OWN proof really ran and exited non-zero. verify-shadow
|
|
61
|
+
// records that as outcome==="GENUINE_FAIL" (pass_fail==="FAIL"). HARNESS_ERROR / PASS / no-proof rows
|
|
62
|
+
// are never catches. We tolerate a future "ship-FAIL" label defensively but still exclude harness noise.
|
|
63
|
+
export function isGenuineCatch(row) {
|
|
64
|
+
if (!row || typeof row !== "object") return false;
|
|
65
|
+
if (row.outcome === "HARNESS_ERROR" || row.pass_fail === "HARNESS_ERROR") return false;
|
|
66
|
+
const failed = row.outcome === "GENUINE_FAIL" || row.pass_fail === "FAIL";
|
|
67
|
+
// A real catch must carry the two things the card indicts the agent with: what it claimed, and the
|
|
68
|
+
// proof that contradicts it. A row missing either can't be rendered honestly, so it isn't a catch.
|
|
69
|
+
return failed && !!row.proof_command && !!row.claim_text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// readCatches() → all genuine-catch rows in the shadow log, oldest→newest as written. Fail-open: any
|
|
73
|
+
// read/parse error yields [] (rendered as the honest "no catches yet" state).
|
|
74
|
+
export function readCatches(logPath = VERIFY_LOG_PATH) {
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = readFileSync(logPath, "utf8");
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
if (raw.length > MAX_LOG_BYTES) raw = raw.slice(-MAX_LOG_BYTES);
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const line of raw.split("\n")) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
let row;
|
|
86
|
+
try {
|
|
87
|
+
row = JSON.parse(line);
|
|
88
|
+
} catch {
|
|
89
|
+
continue; // partial/garbage line — skip
|
|
90
|
+
}
|
|
91
|
+
if (isGenuineCatch(row)) out.push(row);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// selectCatch({ id }) → the catch to render: the newest overall, or (with --id) the newest for that
|
|
97
|
+
// session id. Returns null when there is nothing genuine to show. `catches` is injectable for tests.
|
|
98
|
+
export function selectCatch({ id = null, catches = null, logPath = VERIFY_LOG_PATH } = {}) {
|
|
99
|
+
const rows = catches || readCatches(logPath);
|
|
100
|
+
if (!rows.length) return null;
|
|
101
|
+
const pool = id ? rows.filter((r) => r.session === id) : rows;
|
|
102
|
+
if (!pool.length) return null;
|
|
103
|
+
return pool[pool.length - 1]; // rows are append-ordered → last is newest
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------- (2) honest verdict extraction ----------
|
|
107
|
+
// A GENUINE_FAIL means, by verify-shadow's own classifyOutcome, that the proof PROCESS exited non-zero
|
|
108
|
+
// (not a timeout, not command-not-found, not a missing script — those are HARNESS_ERROR). So
|
|
109
|
+
// "exited non-zero" is always provably true for a catch row. Beyond that, we surface a SPECIFIC code
|
|
110
|
+
// ONLY when the real captured output literally printed one — never invented.
|
|
111
|
+
export function extractCode(evidence) {
|
|
112
|
+
const s = String(evidence || "");
|
|
113
|
+
// HTTP status first (auth/proxy/API proofs surface these): "HTTP/1.1 500", "status: 503",
|
|
114
|
+
// "responded with 429", "got 401". Only 1xx–5xx are treated as a status.
|
|
115
|
+
const http = s.match(
|
|
116
|
+
/\b(?:HTTP\/\d(?:\.\d)?\s+|status(?:\s+code)?[:\s]+|responded with\s+|returned\s+|got\s+)(\d{3})\b/i,
|
|
117
|
+
);
|
|
118
|
+
if (http) {
|
|
119
|
+
const n = Number.parseInt(http[1], 10);
|
|
120
|
+
if (n >= 100 && n < 600) return { kind: "http", value: n };
|
|
121
|
+
}
|
|
122
|
+
// Process exit code: "exit code 1", "exited with code 2", "Process exited with code 1".
|
|
123
|
+
const exit = s.match(/exit(?:ed)?(?:\s+with)?\s+(?:code\s+)?(\d{1,3})\b/i);
|
|
124
|
+
if (exit) {
|
|
125
|
+
const n = Number.parseInt(exit[1], 10);
|
|
126
|
+
if (Number.isFinite(n)) return { kind: "exit", value: n };
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// verdictPhrase(row) → the honest one-line verdict. Always truthful for a catch row.
|
|
132
|
+
function verdictPhrase(row) {
|
|
133
|
+
const code = extractCode(row.evidence);
|
|
134
|
+
const failN = extractFailCount(row.evidence);
|
|
135
|
+
const count = failN != null ? ` · ${failN} failing` : "";
|
|
136
|
+
if (code && code.kind === "http") return `FAIL — its own proof got HTTP ${code.value}${count}`;
|
|
137
|
+
if (code && code.kind === "exit") return `FAIL — its own proof exited ${code.value}${count}`;
|
|
138
|
+
return `FAIL — its own proof re-ran and exited non-zero${count}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Human timestamp: keep the real ISO instant (never fabricated); show it verbatim.
|
|
142
|
+
function stamp(ts) {
|
|
143
|
+
const s = clean(ts, 40);
|
|
144
|
+
return s || "unknown time";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------- (3a) plain-text card (no ANSI — the shareable artifact) ----------
|
|
148
|
+
export function renderPlainCard(row) {
|
|
149
|
+
const claim = clean(row.claim_text, 200) || "it's done";
|
|
150
|
+
const proof = clean(row.proof_command, 200) || "its own proof";
|
|
151
|
+
const evidence = clean(row.evidence, 200);
|
|
152
|
+
const L = [];
|
|
153
|
+
L.push("🔬 skalpel — verified catch");
|
|
154
|
+
L.push("");
|
|
155
|
+
L.push("The agent claimed:");
|
|
156
|
+
L.push(` "${claim}"`);
|
|
157
|
+
L.push("");
|
|
158
|
+
L.push("skalpel re-ran the agent's OWN proof, out-of-band:");
|
|
159
|
+
L.push(` $ ${proof}`);
|
|
160
|
+
L.push(` → ${verdictPhrase(row)}`);
|
|
161
|
+
if (evidence) {
|
|
162
|
+
L.push("");
|
|
163
|
+
L.push("real output (verbatim):");
|
|
164
|
+
L.push(` ${evidence}`);
|
|
165
|
+
}
|
|
166
|
+
L.push("");
|
|
167
|
+
L.push(`caught ${stamp(row.ts)}`);
|
|
168
|
+
L.push("The agent said done. Its own proof, re-run, disagreed.");
|
|
169
|
+
return L.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------- (3b) markdown card (Slack / PR / X) ----------
|
|
173
|
+
export function renderMarkdownCard(row) {
|
|
174
|
+
const claim = clean(row.claim_text, 200) || "it's done";
|
|
175
|
+
const proof = clean(row.proof_command, 200) || "its own proof";
|
|
176
|
+
const evidence = clean(row.evidence, 200);
|
|
177
|
+
const FENCE = "```";
|
|
178
|
+
const L = [];
|
|
179
|
+
L.push("**🔬 skalpel — verified catch**");
|
|
180
|
+
L.push("");
|
|
181
|
+
L.push(`> The agent claimed: _"${claim}"_`);
|
|
182
|
+
L.push("");
|
|
183
|
+
L.push("skalpel re-ran the agent's own proof, out-of-band:");
|
|
184
|
+
L.push(FENCE);
|
|
185
|
+
L.push(`$ ${proof}`);
|
|
186
|
+
L.push(`→ ${verdictPhrase(row)}`);
|
|
187
|
+
L.push(FENCE);
|
|
188
|
+
if (evidence) {
|
|
189
|
+
L.push("real output (verbatim):");
|
|
190
|
+
L.push(FENCE);
|
|
191
|
+
L.push(evidence);
|
|
192
|
+
L.push(FENCE);
|
|
193
|
+
}
|
|
194
|
+
L.push(`_Caught ${stamp(row.ts)}. The agent said done; its own proof, re-run, disagreed._`);
|
|
195
|
+
return L.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- (3c) styled terminal card ----------
|
|
199
|
+
function renderTerminalCard(row) {
|
|
200
|
+
const claim = clean(row.claim_text, 200) || "it's done";
|
|
201
|
+
const proof = clean(row.proof_command, 200) || "its own proof";
|
|
202
|
+
const evidence = clean(row.evidence, 180);
|
|
203
|
+
const L = [];
|
|
204
|
+
L.push("");
|
|
205
|
+
L.push(` ${B}🔬 skalpel${X} ${D}·${X} ${B}${R}verified catch${X}`);
|
|
206
|
+
L.push(` ${D}${"─".repeat(70)}${X}`);
|
|
207
|
+
L.push(` ${D}The agent claimed:${X}`);
|
|
208
|
+
L.push(` ${B}"${claim}"${X}`);
|
|
209
|
+
L.push("");
|
|
210
|
+
L.push(` ${D}skalpel re-ran the agent's OWN proof, out-of-band:${X}`);
|
|
211
|
+
L.push(` ${D}$${X} ${proof}`);
|
|
212
|
+
L.push(` ${R}${B}→ ${verdictPhrase(row)}${X}`);
|
|
213
|
+
if (evidence) {
|
|
214
|
+
L.push("");
|
|
215
|
+
L.push(` ${D}real output:${X} ${evidence}`);
|
|
216
|
+
}
|
|
217
|
+
L.push(` ${D}${"─".repeat(70)}${X}`);
|
|
218
|
+
L.push(
|
|
219
|
+
` ${D}caught ${stamp(row.ts)} — the agent said done; its own proof, re-run, disagreed.${X}`,
|
|
220
|
+
);
|
|
221
|
+
L.push("");
|
|
222
|
+
return L.join("\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------- (4) the honest empty state ----------
|
|
226
|
+
function renderNoCatch({ id }) {
|
|
227
|
+
const L = [];
|
|
228
|
+
L.push("");
|
|
229
|
+
L.push(` ${B}🔬 skalpel card${X}`);
|
|
230
|
+
L.push(` ${D}${"─".repeat(70)}${X}`);
|
|
231
|
+
if (id) {
|
|
232
|
+
L.push(` ${D}No verified catch found for session ${clean(id, 60)}.${X}`);
|
|
233
|
+
} else {
|
|
234
|
+
L.push(` ${D}No verified catch to show yet.${X}`);
|
|
235
|
+
}
|
|
236
|
+
L.push("");
|
|
237
|
+
L.push(
|
|
238
|
+
` ${D}A card is minted only from a REAL catch — a turn where your agent claimed "done"${X}`,
|
|
239
|
+
);
|
|
240
|
+
L.push(
|
|
241
|
+
` ${D}and skalpel re-ran its OWN proof and it genuinely FAILED. Nothing is invented.${X}`,
|
|
242
|
+
);
|
|
243
|
+
L.push("");
|
|
244
|
+
L.push(
|
|
245
|
+
` ${D}Turn the live shadow on with ${X}${B}SKALPEL_VERIFY_SHADOW=1${X}${D}, keep coding, and${X}`,
|
|
246
|
+
);
|
|
247
|
+
L.push(` ${D}run ${X}${B}skalpel card${X}${D} again the next time a claim doesn't hold up.${X}`);
|
|
248
|
+
L.push("");
|
|
249
|
+
return L.join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------- (5) top-level render (used by CLI + tests) ----------
|
|
253
|
+
// format ∈ { "terminal" (default), "md", "plain" }. Returns the string to print. `catches` injectable.
|
|
254
|
+
export function renderCard({ id = null, format = "terminal", catches = null } = {}) {
|
|
255
|
+
const row = selectCatch({ id, catches });
|
|
256
|
+
if (!row) return renderNoCatch({ id });
|
|
257
|
+
if (format === "md") return renderMarkdownCard(row);
|
|
258
|
+
if (format === "plain") return renderPlainCard(row);
|
|
259
|
+
// Default terminal view = the styled card, followed by a copy-pasteable markdown block for sharing.
|
|
260
|
+
return (
|
|
261
|
+
renderTerminalCard(row) +
|
|
262
|
+
"\n" +
|
|
263
|
+
` ${D}copy for Slack / a PR / X ${X}${D}(or: ${X}${B}skalpel card --md | pbcopy${X}${D}):${X}\n\n` +
|
|
264
|
+
renderMarkdownCard(row) +
|
|
265
|
+
"\n"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------- CLI ----------
|
|
270
|
+
function parseArgs(argv) {
|
|
271
|
+
let id = null;
|
|
272
|
+
let format = "terminal";
|
|
273
|
+
for (let i = 0; i < argv.length; i++) {
|
|
274
|
+
const a = argv[i];
|
|
275
|
+
if (a === "--last")
|
|
276
|
+
continue; // newest catch (the default) — accepted for explicitness
|
|
277
|
+
else if (a === "--id") {
|
|
278
|
+
id = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : null;
|
|
279
|
+
} else if (a.startsWith("--id=")) id = a.slice("--id=".length);
|
|
280
|
+
else if (a === "--md" || a === "--markdown") format = "md";
|
|
281
|
+
else if (a === "--plain" || a === "--text") format = "plain";
|
|
282
|
+
}
|
|
283
|
+
return { id, format };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const isMain = (() => {
|
|
287
|
+
try {
|
|
288
|
+
return (
|
|
289
|
+
Boolean(process.argv[1]) &&
|
|
290
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
291
|
+
);
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
|
|
297
|
+
if (isMain) {
|
|
298
|
+
try {
|
|
299
|
+
const { id, format } = parseArgs(process.argv.slice(2));
|
|
300
|
+
process.stdout.write(renderCard({ id, format }) + "\n");
|
|
301
|
+
} catch {
|
|
302
|
+
// READ-ONLY + fail-open: never throw at the user.
|
|
303
|
+
process.stdout.write("\n 🔬 skalpel — could not read your catch log on this machine.\n\n");
|
|
304
|
+
}
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
package/package.json
CHANGED
package/skalpel-setup.mjs
CHANGED
|
@@ -62,6 +62,7 @@ const KNOWN_SUBS = new Set([
|
|
|
62
62
|
"autopsy",
|
|
63
63
|
"xray",
|
|
64
64
|
"catches",
|
|
65
|
+
"card",
|
|
65
66
|
"__build",
|
|
66
67
|
"__verify-report",
|
|
67
68
|
]);
|
|
@@ -77,6 +78,7 @@ usage:
|
|
|
77
78
|
skalpel autopsy local, read-only receipt of your verified patterns
|
|
78
79
|
skalpel xray re-run your last session's own proof — did "done" hold up?
|
|
79
80
|
skalpel catches scan your own history for "done" claims over failing tests
|
|
81
|
+
skalpel card [--last|--id <id>] [--md] share one verified catch: the claim + its own failing proof
|
|
80
82
|
skalpel uninstall [--purge] remove the hooks + local data from this machine
|
|
81
83
|
|
|
82
84
|
options:
|
|
@@ -112,6 +114,11 @@ function validateArgv() {
|
|
|
112
114
|
process.exit(0);
|
|
113
115
|
}
|
|
114
116
|
if (BOOL_FLAGS.has(a)) continue; // valueless flag (e.g. `uninstall --purge`)
|
|
117
|
+
// `skalpel card` owns its own flags (--last / --id <id> / --md / --plain) and forwards them to
|
|
118
|
+
// card.mjs. They are NOT setup options, so setup must not reject them as unknown — pass any token
|
|
119
|
+
// after the `card` subcommand straight through (card.mjs validates its own flags). Global -v/-h
|
|
120
|
+
// above still win for a bare `skalpel -v`. Scoped to `card` so no other subcommand's contract shifts.
|
|
121
|
+
if (i > 0 && argv[0] === "card") continue;
|
|
115
122
|
if (a.startsWith("-")) {
|
|
116
123
|
console.error(`skalpel: unknown option \`${a}\`\n\n${USAGE}`);
|
|
117
124
|
process.exit(2);
|
|
@@ -878,6 +885,17 @@ async function main() {
|
|
|
878
885
|
spawnSync("node", [join(__dir, "catches.mjs"), ...argv.slice(1)], { stdio: "inherit" });
|
|
879
886
|
return;
|
|
880
887
|
}
|
|
888
|
+
// `skalpel card` — THE CATCH CARD: a LOCAL, READ-ONLY, ZERO-NETWORK render of ONE already-verified
|
|
889
|
+
// catch that the live shadow (verify-shadow) wrote to ~/.skalpel/verify-shadow.log — the agent's
|
|
890
|
+
// verbatim "done" claim, the exact proof command skalpel re-ran, the real FAIL verdict, and the
|
|
891
|
+
// verbatim output tail — as a clean, copy-pasteable card for Slack / a PR / X. It never re-runs a
|
|
892
|
+
// proof, never touches the network; renders only a GENUINE_FAIL row (never harness noise) and, if
|
|
893
|
+
// there's no catch to show, says so honestly. `--md`/`--plain` pick the export; `--id <session>`
|
|
894
|
+
// targets a specific session; `--last` (default) is the newest catch.
|
|
895
|
+
if (sub === "card") {
|
|
896
|
+
spawnSync("node", [join(__dir, "card.mjs"), ...argv.slice(1)], { stdio: "inherit" });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
881
899
|
// `skalpel __verify-report` — the claim-verification SHADOW instrument: reads the local
|
|
882
900
|
// verify-shadow.log and prints the three numbers (claim fire-rate, proof-reconstruction rate, and the
|
|
883
901
|
// headline MISMATCH rate = how often the agent claimed done but its OWN proof, re-run, failed).
|