@zanii/blackbox 0.0.0-stage → 0.2.0
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/LICENSE +202 -0
- package/README.md +109 -2
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +73 -0
- package/dist/analysis/detectors.d.ts +36 -0
- package/dist/analysis/detectors.js +339 -0
- package/dist/analysis/faults.d.ts +9 -0
- package/dist/analysis/faults.js +250 -0
- package/dist/analysis/index.d.ts +68 -0
- package/dist/analysis/index.js +388 -0
- package/dist/analysis/landing.d.ts +25 -0
- package/dist/analysis/landing.js +225 -0
- package/dist/analysis/memory.d.ts +13 -0
- package/dist/analysis/memory.js +33 -0
- package/dist/analysis/waste.d.ts +29 -0
- package/dist/analysis/waste.js +79 -0
- package/dist/approvals/index.d.ts +11 -0
- package/dist/approvals/index.js +27 -0
- package/dist/approvals/warnings.d.ts +2 -0
- package/dist/approvals/warnings.js +28 -0
- package/dist/attest/index.d.ts +17 -0
- package/dist/attest/index.js +106 -0
- package/dist/authority/index.d.ts +24 -0
- package/dist/authority/index.js +77 -0
- package/dist/billing/index.d.ts +99 -0
- package/dist/billing/index.js +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1057 -0
- package/dist/client/index.d.ts +146 -0
- package/dist/client/index.js +210 -0
- package/dist/compliance/index.d.ts +41 -0
- package/dist/compliance/index.js +96 -0
- package/dist/cost/index.d.ts +133 -0
- package/dist/cost/index.js +293 -0
- package/dist/data/index.d.ts +191 -0
- package/dist/data/index.js +762 -0
- package/dist/directives/index.d.ts +35 -0
- package/dist/directives/index.js +80 -0
- package/dist/drills/index.d.ts +43 -0
- package/dist/drills/index.js +101 -0
- package/dist/duty/index.d.ts +21 -0
- package/dist/duty/index.js +68 -0
- package/dist/fleet/index.d.ts +141 -0
- package/dist/fleet/index.js +454 -0
- package/dist/hooks/ai-sdk.d.ts +42 -0
- package/dist/hooks/ai-sdk.js +62 -0
- package/dist/hooks/claude-agent-sdk.d.ts +14 -0
- package/dist/hooks/claude-agent-sdk.js +70 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/langchain-agent.d.ts +69 -0
- package/dist/hooks/langchain-agent.js +163 -0
- package/dist/hooks/langchain.d.ts +41 -0
- package/dist/hooks/langchain.js +216 -0
- package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
- package/dist/hooks/langgraph-checkpoint.js +73 -0
- package/dist/hooks/memory.d.ts +17 -0
- package/dist/hooks/memory.js +64 -0
- package/dist/hooks/openai-agents.d.ts +6 -0
- package/dist/hooks/openai-agents.js +40 -0
- package/dist/hooks/protect.d.ts +7 -0
- package/dist/hooks/protect.js +39 -0
- package/dist/hooks/providers.d.ts +16 -0
- package/dist/hooks/providers.js +149 -0
- package/dist/hooks/shared.d.ts +11 -0
- package/dist/hooks/shared.js +39 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/investigate/index.d.ts +66 -0
- package/dist/investigate/index.js +119 -0
- package/dist/mcp-server/index.d.ts +85 -0
- package/dist/mcp-server/index.js +216 -0
- package/dist/mcp-wrap/index.d.ts +17 -0
- package/dist/mcp-wrap/index.js +170 -0
- package/dist/money/index.d.ts +114 -0
- package/dist/money/index.js +622 -0
- package/dist/occurrence/index.d.ts +108 -0
- package/dist/occurrence/index.js +168 -0
- package/dist/ocsf/index.d.ts +22 -0
- package/dist/ocsf/index.js +168 -0
- package/dist/otlp/index.d.ts +24 -0
- package/dist/otlp/index.js +143 -0
- package/dist/packs/index.d.ts +48 -0
- package/dist/packs/index.js +343 -0
- package/dist/policy/delta.d.ts +11 -0
- package/dist/policy/delta.js +39 -0
- package/dist/policy/drafts.d.ts +34 -0
- package/dist/policy/drafts.js +129 -0
- package/dist/policy/index.d.ts +47 -0
- package/dist/policy/index.js +154 -0
- package/dist/precog/index.d.ts +96 -0
- package/dist/precog/index.js +167 -0
- package/dist/precog/intervention.d.ts +22 -0
- package/dist/precog/intervention.js +44 -0
- package/dist/precog/normal.d.ts +31 -0
- package/dist/precog/normal.js +89 -0
- package/dist/preflight/index.d.ts +11 -0
- package/dist/preflight/index.js +19 -0
- package/dist/ratings/index.d.ts +21 -0
- package/dist/ratings/index.js +48 -0
- package/dist/reconcile/claude-code.d.ts +19 -0
- package/dist/reconcile/claude-code.js +220 -0
- package/dist/reconcile/codex.d.ts +5 -0
- package/dist/reconcile/codex.js +191 -0
- package/dist/reconcile/index.d.ts +19 -0
- package/dist/reconcile/index.js +50 -0
- package/dist/reconcile/record.d.ts +49 -0
- package/dist/reconcile/record.js +225 -0
- package/dist/reconcile/shared.d.ts +65 -0
- package/dist/reconcile/shared.js +113 -0
- package/dist/replay/index.d.ts +11 -0
- package/dist/replay/index.js +64 -0
- package/dist/replay/repair.d.ts +10 -0
- package/dist/replay/repair.js +62 -0
- package/dist/session/drain.d.ts +13 -0
- package/dist/session/drain.js +35 -0
- package/dist/session/index.d.ts +275 -0
- package/dist/session/index.js +681 -0
- package/dist/undo/index.d.ts +45 -0
- package/dist/undo/index.js +212 -0
- package/dist/verify/anchor.d.ts +54 -0
- package/dist/verify/anchor.js +77 -0
- package/dist/verify/chain.d.ts +27 -0
- package/dist/verify/chain.js +105 -0
- package/dist/verify/envelope.d.ts +28 -0
- package/dist/verify/envelope.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/weather/index.d.ts +24 -0
- package/dist/weather/index.js +45 -0
- package/dist/workspace-receipt/index.d.ts +15 -0
- package/dist/workspace-receipt/index.js +121 -0
- package/package.json +56 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// Detectors (spec/findings.md §4): deterministic rules over the gateway's record, so they work for
|
|
2
|
+
// any agent, instrumented or not. Mirrors sdks/python/src/zanii_blackbox/analysis/detectors.py.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { priceCall, priceFor, promptTokens, tokensOf } from "../cost/index.js";
|
|
5
|
+
import { callsOf } from "../reconcile/record.js";
|
|
6
|
+
import { canonical, isObj } from "../reconcile/shared.js";
|
|
7
|
+
const VOLATILE = new Set([
|
|
8
|
+
"timestamp",
|
|
9
|
+
"ts",
|
|
10
|
+
"time",
|
|
11
|
+
"date",
|
|
12
|
+
"request_id",
|
|
13
|
+
"requestId",
|
|
14
|
+
"nonce",
|
|
15
|
+
"uuid",
|
|
16
|
+
"trace_id",
|
|
17
|
+
]);
|
|
18
|
+
const WINDOW = 20;
|
|
19
|
+
function normalise(value) {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.map(normalise);
|
|
22
|
+
if (isObj(value)) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [k, v] of Object.entries(value))
|
|
25
|
+
if (!VOLATILE.has(k))
|
|
26
|
+
out[k] = normalise(v);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return typeof value === "string" ? value.replaceAll("\\", "/") : value;
|
|
30
|
+
}
|
|
31
|
+
/** T1.4: equivalent arguments give the same fingerprint (volatile keys dropped, paths normalised). */
|
|
32
|
+
export function fingerprint(args) {
|
|
33
|
+
return createHash("sha256")
|
|
34
|
+
.update(canonical(normalise(args ?? null)))
|
|
35
|
+
.digest("hex")
|
|
36
|
+
.slice(0, 16);
|
|
37
|
+
}
|
|
38
|
+
export function toolCallsOf(calls) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const c of [...calls].sort((a, b) => a.endSeq - b.endSeq)) {
|
|
41
|
+
for (const [, b] of [...c.blocks].sort(([x], [y]) => x - y))
|
|
42
|
+
if (b.type === "tool_use" && typeof b.name === "string")
|
|
43
|
+
out.push({
|
|
44
|
+
seq: c.endSeq,
|
|
45
|
+
tool: b.name,
|
|
46
|
+
fp: fingerprint(b.input ?? {}),
|
|
47
|
+
args: b.input ?? {},
|
|
48
|
+
...(typeof b.id === "string" ? { id: b.id } : {}),
|
|
49
|
+
});
|
|
50
|
+
for (const item of c.items) {
|
|
51
|
+
if (item.type === "function_call" && typeof item.name === "string") {
|
|
52
|
+
let args = item.arguments;
|
|
53
|
+
if (typeof args === "string")
|
|
54
|
+
try {
|
|
55
|
+
args = JSON.parse(args);
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
out.push({
|
|
59
|
+
seq: c.endSeq,
|
|
60
|
+
tool: item.name,
|
|
61
|
+
fp: fingerprint(args),
|
|
62
|
+
args,
|
|
63
|
+
...(typeof item.call_id === "string" ? { id: item.call_id } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (item.type === "custom_tool_call" && typeof item.name === "string")
|
|
67
|
+
out.push({
|
|
68
|
+
seq: c.endSeq,
|
|
69
|
+
tool: item.name,
|
|
70
|
+
fp: fingerprint(item.input),
|
|
71
|
+
args: item.input,
|
|
72
|
+
...(typeof item.call_id === "string" ? { id: item.call_id } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Tool results the agent sent back: Anthropic tool_result blocks and Chat Completions `role: "tool"`
|
|
80
|
+
* messages (the first carrying each id), MCP tool.result. Chat Completions has no error flag, so its
|
|
81
|
+
* results count as not errors.
|
|
82
|
+
*/
|
|
83
|
+
export function toolResultsOf(calls, lines) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const c of calls) {
|
|
87
|
+
const messages = Array.isArray(c.request?.messages) ? c.request.messages.filter(isObj) : [];
|
|
88
|
+
for (const m of messages) {
|
|
89
|
+
if (m.role === "tool" && typeof m.tool_call_id === "string") {
|
|
90
|
+
if (!seen.has(m.tool_call_id)) {
|
|
91
|
+
seen.add(m.tool_call_id);
|
|
92
|
+
out.push({ seq: c.requestSeq, error: false, id: m.tool_call_id });
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(m.content))
|
|
97
|
+
continue;
|
|
98
|
+
for (const b of m.content.filter(isObj))
|
|
99
|
+
if (b.type === "tool_result" &&
|
|
100
|
+
typeof b.tool_use_id === "string" &&
|
|
101
|
+
!seen.has(b.tool_use_id)) {
|
|
102
|
+
seen.add(b.tool_use_id);
|
|
103
|
+
out.push({ seq: c.requestSeq, error: b.is_error === true, id: b.tool_use_id });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const e = JSON.parse(line);
|
|
109
|
+
if (e.kind === "tool.result" && typeof e.meta.is_error === "boolean")
|
|
110
|
+
out.push({ seq: e.seq, error: e.meta.is_error });
|
|
111
|
+
}
|
|
112
|
+
return out.sort((a, b) => a.seq - b.seq);
|
|
113
|
+
}
|
|
114
|
+
function segments(request) {
|
|
115
|
+
const out = [
|
|
116
|
+
["system", canonical(request.system ?? request.instructions ?? null)],
|
|
117
|
+
["tools", canonical(request.tools ?? null)],
|
|
118
|
+
];
|
|
119
|
+
const messages = Array.isArray(request.messages)
|
|
120
|
+
? request.messages
|
|
121
|
+
: Array.isArray(request.input)
|
|
122
|
+
? request.input
|
|
123
|
+
: [];
|
|
124
|
+
for (const [i, m] of messages.entries())
|
|
125
|
+
out.push([`message:${i}`, canonical(m)]);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** T1.5: the first prefix segment that differs; "none" when the earlier request is a prefix of the later. */
|
|
129
|
+
function changedSegment(before, after) {
|
|
130
|
+
const a = segments(before);
|
|
131
|
+
const b = segments(after);
|
|
132
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
133
|
+
if (a[i]?.[1] !== b[i]?.[1])
|
|
134
|
+
return b[i][0];
|
|
135
|
+
return b.length >= a.length ? "none" : a[b.length][0];
|
|
136
|
+
}
|
|
137
|
+
const f = (code, severity, ref, detail) => ({ code, source: "detectors", severity, ref, detail });
|
|
138
|
+
export function detectors(lines, options) {
|
|
139
|
+
const calls = callsOf(lines, options.bodies);
|
|
140
|
+
const byEnd = [...calls].sort((a, b) => a.endSeq - b.endSeq);
|
|
141
|
+
const tools = toolCallsOf(calls);
|
|
142
|
+
const results = toolResultsOf(calls, lines);
|
|
143
|
+
const keys = tools.map((t) => `${t.tool}\n${t.fp}`);
|
|
144
|
+
const first = lines[0] ? JSON.parse(lines[0]) : null;
|
|
145
|
+
const open = first?.kind === "session.open" ? first.meta : {};
|
|
146
|
+
const out = [];
|
|
147
|
+
// D1: the 3rd / 5th occurrence of the same call within the last 20 tool calls.
|
|
148
|
+
const d1 = new Set();
|
|
149
|
+
const repeat = [];
|
|
150
|
+
keys.forEach((key, i) => {
|
|
151
|
+
const count = keys.slice(Math.max(0, i - WINDOW + 1), i + 1).filter((k) => k === key).length;
|
|
152
|
+
repeat.push(count >= 2);
|
|
153
|
+
for (const [at, severity] of [
|
|
154
|
+
[3, "caution"],
|
|
155
|
+
[5, "warning"],
|
|
156
|
+
])
|
|
157
|
+
if (count === at && !d1.has(`${key}\n${at}`)) {
|
|
158
|
+
d1.add(`${key}\n${at}`);
|
|
159
|
+
const t = tools[i];
|
|
160
|
+
out.push(f("D1_REPEAT_CALL", severity, { tool: t.tool, fingerprint: t.fp, count: at }, `The same ${t.tool} call ${at} times in the last ${WINDOW} tool calls.`));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// D2: an n-gram (n = 2..4) repeated 3 times back to back, not one repeated call.
|
|
164
|
+
const inLoop = new Array(keys.length).fill(false);
|
|
165
|
+
for (let i = 0; i < keys.length;) {
|
|
166
|
+
let found = 0;
|
|
167
|
+
for (let n = 2; n <= 4 && !found; n++) {
|
|
168
|
+
if (i + 3 * n > keys.length)
|
|
169
|
+
break;
|
|
170
|
+
const gram = keys.slice(i, i + n);
|
|
171
|
+
if (new Set(gram).size === 1)
|
|
172
|
+
continue;
|
|
173
|
+
const same = (at) => keys.slice(at, at + n).every((k, j) => k === gram[j]);
|
|
174
|
+
if (same(i + n) && same(i + 2 * n))
|
|
175
|
+
found = n;
|
|
176
|
+
}
|
|
177
|
+
if (found) {
|
|
178
|
+
out.push(f("D2_ACTION_LOOP", "caution", { n: found, first_seq: tools[i].seq }, `A sequence of ${found} tool calls repeated 3 times in a row.`));
|
|
179
|
+
for (let j = i; j < i + 3 * found; j++)
|
|
180
|
+
inLoop[j] = true;
|
|
181
|
+
i += 3 * found;
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
// D3: 3+ error results in a row, once per streak.
|
|
187
|
+
let streak = 0;
|
|
188
|
+
let streakStart = 0;
|
|
189
|
+
for (const r of results) {
|
|
190
|
+
if (!r.error) {
|
|
191
|
+
streak = 0;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (streak === 0)
|
|
195
|
+
streakStart = r.seq;
|
|
196
|
+
streak++;
|
|
197
|
+
if (streak === 3)
|
|
198
|
+
out.push(f("D3_ERROR_STREAK", "caution", { first_seq: streakStart }, "Three tool results in a row were errors."));
|
|
199
|
+
}
|
|
200
|
+
// D4: input grew on each of the last 5 calls and exceeds 60% of the context window.
|
|
201
|
+
const priced = byEnd.filter((c) => c.usage);
|
|
202
|
+
const inputs = priced.map((c) => {
|
|
203
|
+
return promptTokens(tokensOf(c.usage));
|
|
204
|
+
});
|
|
205
|
+
for (let j = 5; j < priced.length; j++) {
|
|
206
|
+
const c = priced[j];
|
|
207
|
+
const window = c.model && options.prices
|
|
208
|
+
? priceFor(c.model, options.prices.table, c.provider)?.price.context_window
|
|
209
|
+
: undefined;
|
|
210
|
+
const rising = inputs
|
|
211
|
+
.slice(j - 5, j + 1)
|
|
212
|
+
.every((v, k, a) => k === 0 || v > a[k - 1]);
|
|
213
|
+
if (window && rising && inputs[j] * 10 > window * 6) {
|
|
214
|
+
out.push(f("D4_CONTEXT_BLOAT", "caution", { first_seq: c.endSeq }, "The context has grown on every call and is past 60% of the model's window."));
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// D5: cached input drops to zero; which prefix segment changed (T1.5).
|
|
219
|
+
for (let j = 1; j < priced.length; j++) {
|
|
220
|
+
const prev = priced[j - 1];
|
|
221
|
+
const cur = priced[j];
|
|
222
|
+
if (prev.model !== cur.model || !prev.request || !cur.request)
|
|
223
|
+
continue;
|
|
224
|
+
if (tokensOf(prev.usage).cache_read > 0 && tokensOf(cur.usage).cache_read === 0) {
|
|
225
|
+
const changed = changedSegment(prev.request, cur.request);
|
|
226
|
+
out.push(f("D5_CACHE_BREAK", "advisory", { seq: cur.endSeq, changed }, changed === "none"
|
|
227
|
+
? "The prompt cache expired: nothing in the prefix changed."
|
|
228
|
+
: `The prompt cache broke: ${changed} changed.`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// MODEL_DEPRECATED: a call to a model the price table marks deprecated (spec/cost.md §1), once
|
|
232
|
+
// per model, in the order they're first called.
|
|
233
|
+
if (options.prices) {
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
for (const c of byEnd) {
|
|
236
|
+
if (!c.model || seen.has(c.model))
|
|
237
|
+
continue;
|
|
238
|
+
const match = priceFor(c.model, options.prices.table, c.provider);
|
|
239
|
+
if (match?.price.status !== "deprecated")
|
|
240
|
+
continue;
|
|
241
|
+
seen.add(c.model);
|
|
242
|
+
out.push(f("MODEL_DEPRECATED", "advisory", { seq: c.endSeq, model: c.model }, `${c.model} is deprecated: plan the move to its successor before the provider retires it.`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// A person may raise a limit mid-session (spec/approvals.md §4): the latest grant wins.
|
|
246
|
+
const granted = grantedLimits(lines);
|
|
247
|
+
// D6: running cost crosses 80% / 100% of the budget (the session's own, else the default).
|
|
248
|
+
const budget = granted.budget ??
|
|
249
|
+
(typeof open.budget_micro_usd === "number" && open.budget_micro_usd > 0
|
|
250
|
+
? open.budget_micro_usd
|
|
251
|
+
: options.budgetMicroUsd);
|
|
252
|
+
if (options.prices && budget && budget > 0) {
|
|
253
|
+
let micro = 0;
|
|
254
|
+
const crossed = new Set();
|
|
255
|
+
for (const c of priced) {
|
|
256
|
+
// the cost report's own pricing (spec/cost.md): tiers, provider scope, reported costs
|
|
257
|
+
const priced = priceCall({ ...c, usage: c.usage }, options.prices.table);
|
|
258
|
+
if (priced.micro_usd === null)
|
|
259
|
+
continue;
|
|
260
|
+
micro += priced.micro_usd;
|
|
261
|
+
for (const [threshold, severity] of [
|
|
262
|
+
[80, "caution"],
|
|
263
|
+
[100, "warning"],
|
|
264
|
+
])
|
|
265
|
+
if (!crossed.has(threshold) && micro * 100 >= threshold * budget) {
|
|
266
|
+
crossed.add(threshold);
|
|
267
|
+
out.push(f("D6_SPEND", severity,
|
|
268
|
+
// a raised budget is a new one: its crossings are new findings
|
|
269
|
+
granted.budget !== undefined
|
|
270
|
+
? { threshold, budget_micro_usd: budget }
|
|
271
|
+
: { threshold }, `The session has spent ${threshold}% of its budget.`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// CALL_LIMIT: more model calls than the session's max_llm_calls (spec/control.md §2).
|
|
276
|
+
const limit = granted.calls ?? open.max_llm_calls;
|
|
277
|
+
if (typeof limit === "number" &&
|
|
278
|
+
Number.isSafeInteger(limit) &&
|
|
279
|
+
limit >= 0 &&
|
|
280
|
+
calls.length > limit)
|
|
281
|
+
out.push(f("CALL_LIMIT", "warning", { limit }, `The session made more than its ${limit} allowed model calls.`));
|
|
282
|
+
// RETRY_STORM: 3+ responses in a row with 429 or ≥ 500.
|
|
283
|
+
let storm = 0;
|
|
284
|
+
let stormStart = 0;
|
|
285
|
+
for (const c of byEnd) {
|
|
286
|
+
if (c.status === null)
|
|
287
|
+
continue;
|
|
288
|
+
if (c.status === 429 || c.status >= 500) {
|
|
289
|
+
if (storm === 0)
|
|
290
|
+
stormStart = c.endSeq;
|
|
291
|
+
storm++;
|
|
292
|
+
if (storm === 3)
|
|
293
|
+
out.push(f("RETRY_STORM", "caution", { first_seq: stormStart }, "Three provider errors (429 / 5xx) in a row."));
|
|
294
|
+
}
|
|
295
|
+
else
|
|
296
|
+
storm = 0;
|
|
297
|
+
}
|
|
298
|
+
// DANGLING_TOOL_CALL: a tool_use whose result never came back (judged at close).
|
|
299
|
+
if (options.now === Number.POSITIVE_INFINITY) {
|
|
300
|
+
const answered = new Set(results.flatMap((r) => (r.id ? [r.id] : [])));
|
|
301
|
+
for (const c of byEnd)
|
|
302
|
+
for (const [, b] of [...c.blocks].sort(([x], [y]) => x - y))
|
|
303
|
+
if (b.type === "tool_use" && typeof b.id === "string" && !answered.has(b.id))
|
|
304
|
+
out.push(f("DANGLING_TOOL_CALL", "advisory", { tool_use_id: b.id }, "A tool call's result never reached the model; the tool may have run."));
|
|
305
|
+
}
|
|
306
|
+
// LOST_COMMS: the session went silent (squawk 7600).
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
const e = JSON.parse(line);
|
|
309
|
+
if (e.kind === "session.lost_contact")
|
|
310
|
+
out.push(f("LOST_COMMS", "warning", { seq: e.seq }, "The agent stopped sending heartbeats: lost contact."));
|
|
311
|
+
}
|
|
312
|
+
// C_CUSUM over tool-call steps.
|
|
313
|
+
const errorIds = new Set(results.filter((r) => r.error && r.id).map((r) => r.id));
|
|
314
|
+
let s = 0;
|
|
315
|
+
for (let i = 0; i < tools.length; i++) {
|
|
316
|
+
const t = tools[i];
|
|
317
|
+
const score = (repeat[i] ? 1 : 0) + (inLoop[i] ? 1 : 0) + (t.id && errorIds.has(t.id) ? 1 : 0);
|
|
318
|
+
s = Math.max(0, s + score - 0.5);
|
|
319
|
+
if (s > 4) {
|
|
320
|
+
out.push(f("C_CUSUM", "caution", { seq: t.seq }, "The run has been going wrong step after step (repeats, loops, errors)."));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
/** The latest budget and call limit a person granted (spec/approvals.md §4), if any. */
|
|
327
|
+
export function grantedLimits(lines) {
|
|
328
|
+
const out = {};
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
const e = JSON.parse(line);
|
|
331
|
+
if (e.kind !== "control" || e.meta.action !== "permission")
|
|
332
|
+
continue;
|
|
333
|
+
if (Number.isSafeInteger(e.meta.budget_micro_usd))
|
|
334
|
+
out.budget = e.meta.budget_micro_usd;
|
|
335
|
+
if (Number.isSafeInteger(e.meta.max_llm_calls))
|
|
336
|
+
out.calls = e.meta.max_llm_calls;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Fault {
|
|
2
|
+
fault: string;
|
|
3
|
+
squawk?: string;
|
|
4
|
+
en: string;
|
|
5
|
+
ar: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const FAULTS: Readonly<Record<string, Fault>>;
|
|
8
|
+
/** The fault for a finding code; unknown codes get a generic entry, never an exception. */
|
|
9
|
+
export declare function faultOf(code: string): Fault;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// The fault catalog (spec/control.md §1), embedded from spec/faults/v1.json. Mirrors analysis/faults.py.
|
|
2
|
+
// Generated from the spec file; faults.test.ts checks they match exactly. Edit the spec, then regenerate.
|
|
3
|
+
export const FAULTS = {
|
|
4
|
+
BYPASS: {
|
|
5
|
+
fault: "BBX-7500",
|
|
6
|
+
squawk: "HIJACK",
|
|
7
|
+
en: "Model call around the gateway",
|
|
8
|
+
ar: "استدعاء نموذج خارج البوابة",
|
|
9
|
+
},
|
|
10
|
+
LOST_COMMS: {
|
|
11
|
+
fault: "BBX-7600",
|
|
12
|
+
squawk: "LOST_COMMS",
|
|
13
|
+
en: "Lost contact with the agent",
|
|
14
|
+
ar: "انقطاع الاتصال بالوكيل",
|
|
15
|
+
},
|
|
16
|
+
EMERGENCY: { fault: "BBX-7700", squawk: "EMERGENCY", en: "Emergency stop", ar: "إيقاف طارئ" },
|
|
17
|
+
MISSING_LOCAL: { fault: "BBX-1101", en: "Deleted from the agent's log", ar: "حُذف من سجل الوكيل" },
|
|
18
|
+
ALTERED: { fault: "BBX-1102", en: "Altered in the agent's log", ar: "عُدّل في سجل الوكيل" },
|
|
19
|
+
EXTRA_LOCAL: { fault: "BBX-1103", en: "Not seen by the gateway", ar: "لم تشاهده البوابة" },
|
|
20
|
+
LOCAL_GAP: { fault: "BBX-1104", en: "Gap in the agent's log", ar: "فجوة في سجل الوكيل" },
|
|
21
|
+
RECEIPT_MISSING: {
|
|
22
|
+
fault: "BBX-1105",
|
|
23
|
+
en: "Workspace receipt missing",
|
|
24
|
+
ar: "إيصال مساحة العمل مفقود",
|
|
25
|
+
},
|
|
26
|
+
UNATTRIBUTED_CHANGE: {
|
|
27
|
+
fault: "BBX-1106",
|
|
28
|
+
en: "File change outside any tool call",
|
|
29
|
+
ar: "تغيير في الملفات خارج أي استدعاء أداة",
|
|
30
|
+
},
|
|
31
|
+
SDK_SILENT: {
|
|
32
|
+
fault: "BBX-1201",
|
|
33
|
+
en: "The SDK stopped reporting",
|
|
34
|
+
ar: "توقّفت حزمة SDK عن الإبلاغ",
|
|
35
|
+
},
|
|
36
|
+
D1_REPEAT_CALL: { fault: "BBX-2101", en: "Repeated tool call", ar: "تكرار استدعاء الأداة" },
|
|
37
|
+
D2_ACTION_LOOP: { fault: "BBX-2102", en: "Action loop", ar: "حلقة إجراءات متكررة" },
|
|
38
|
+
D3_ERROR_STREAK: { fault: "BBX-2103", en: "Error streak", ar: "سلسلة أخطاء متتالية" },
|
|
39
|
+
D4_CONTEXT_BLOAT: { fault: "BBX-2104", en: "Context bloat", ar: "تضخّم السياق" },
|
|
40
|
+
D5_CACHE_BREAK: {
|
|
41
|
+
fault: "BBX-2105",
|
|
42
|
+
en: "Prompt cache break",
|
|
43
|
+
ar: "انقطاع ذاكرة التخزين المؤقت للموجّه",
|
|
44
|
+
},
|
|
45
|
+
RETRY_STORM: { fault: "BBX-2106", en: "Retry storm", ar: "عاصفة إعادة المحاولة" },
|
|
46
|
+
DANGLING_TOOL_CALL: {
|
|
47
|
+
fault: "BBX-2107",
|
|
48
|
+
en: "Tool result never returned",
|
|
49
|
+
ar: "نتيجة أداة لم تُعَد إلى النموذج",
|
|
50
|
+
},
|
|
51
|
+
C_CUSUM: {
|
|
52
|
+
fault: "BBX-2108",
|
|
53
|
+
en: "Run degrading step after step",
|
|
54
|
+
ar: "تدهور التشغيل خطوة بعد خطوة",
|
|
55
|
+
},
|
|
56
|
+
ORPHANED_TOOL_CALL: {
|
|
57
|
+
fault: "BBX-2109",
|
|
58
|
+
en: "A tool call never finished",
|
|
59
|
+
ar: "استدعاء أداة لم يكتمل",
|
|
60
|
+
},
|
|
61
|
+
EXIT_DURABILITY: {
|
|
62
|
+
fault: "BBX-2202",
|
|
63
|
+
en: "Side effects in a run that saves only on exit",
|
|
64
|
+
ar: "آثار جانبية في تشغيل لا يحفظ حالته إلا عند الخروج",
|
|
65
|
+
},
|
|
66
|
+
CHECKPOINT_REPLAY: {
|
|
67
|
+
fault: "BBX-2201",
|
|
68
|
+
en: "Side effect repeated on resume",
|
|
69
|
+
ar: "تكرار أثر جانبي عند الاستئناف",
|
|
70
|
+
},
|
|
71
|
+
D6_SPEND: { fault: "BBX-3101", en: "Spend limit", ar: "حدّ الإنفاق" },
|
|
72
|
+
CALL_LIMIT: {
|
|
73
|
+
fault: "BBX-3102",
|
|
74
|
+
en: "Model-call limit reached",
|
|
75
|
+
ar: "بلوغ حدّ استدعاءات النموذج",
|
|
76
|
+
},
|
|
77
|
+
MODEL_DEPRECATED: { fault: "BBX-3103", en: "Deprecated model", ar: "نموذج متوقّف الدعم" },
|
|
78
|
+
PLAN_DEVIATION: {
|
|
79
|
+
fault: "BBX-4101",
|
|
80
|
+
en: "Deviation from the flight plan",
|
|
81
|
+
ar: "انحراف عن خطة الرحلة",
|
|
82
|
+
},
|
|
83
|
+
PLAN_VIOLATION: { fault: "BBX-4102", en: "Flight plan limit broken", ar: "خرق حدود خطة الرحلة" },
|
|
84
|
+
PLAN_INVALID: {
|
|
85
|
+
fault: "BBX-4104",
|
|
86
|
+
en: "Flight plan filed by the SDK is invalid",
|
|
87
|
+
ar: "خطة رحلة غير صالحة مرسلة من الـ SDK",
|
|
88
|
+
},
|
|
89
|
+
STERILE_VIOLATION: {
|
|
90
|
+
fault: "BBX-4103",
|
|
91
|
+
en: "A tool outside a sterile phase's allowed list",
|
|
92
|
+
ar: "أداة خارج القائمة المسموحة في مرحلة حرجة",
|
|
93
|
+
},
|
|
94
|
+
FALSE_CLAIM: { fault: "BBX-4201", en: "False completion claim", ar: "ادعاء إنجاز غير صحيح" },
|
|
95
|
+
FALSE_SUCCESS: {
|
|
96
|
+
fault: "BBX-4203",
|
|
97
|
+
en: "A success the record contradicts",
|
|
98
|
+
ar: "نجاح مُعلن يناقضه السجل",
|
|
99
|
+
},
|
|
100
|
+
VERIFY_REGRESSION: {
|
|
101
|
+
fault: "BBX-4204",
|
|
102
|
+
en: "A check that passed now fails",
|
|
103
|
+
ar: "فحص نجح سابقًا ثم فشل",
|
|
104
|
+
},
|
|
105
|
+
CLAIM_UNVERIFIED: {
|
|
106
|
+
fault: "BBX-4202",
|
|
107
|
+
en: "Completion claim can't be verified",
|
|
108
|
+
ar: "تعذّر التحقق من ادعاء الإنجاز",
|
|
109
|
+
},
|
|
110
|
+
POLICY_DENIED: {
|
|
111
|
+
fault: "BBX-5101",
|
|
112
|
+
en: "Tool call refused by policy",
|
|
113
|
+
ar: "رُفض استدعاء الأداة وفق السياسة",
|
|
114
|
+
},
|
|
115
|
+
POLICY_VIOLATION: {
|
|
116
|
+
fault: "BBX-5102",
|
|
117
|
+
en: "Tool use against policy",
|
|
118
|
+
ar: "استخدام أداة مخالف للسياسة",
|
|
119
|
+
},
|
|
120
|
+
POLICY_AUDIT: {
|
|
121
|
+
fault: "BBX-5103",
|
|
122
|
+
en: "An audit-only policy rule would have acted",
|
|
123
|
+
ar: "كانت قاعدة سياسة قيد التدقيق سترفض استدعاء الأداة",
|
|
124
|
+
},
|
|
125
|
+
NEAR_MISS: {
|
|
126
|
+
fault: "BBX-6101",
|
|
127
|
+
en: "Near miss reported by the agent",
|
|
128
|
+
ar: "حادثة وشيكة أبلغ عنها الوكيل",
|
|
129
|
+
},
|
|
130
|
+
TOKEN_MISUSE: {
|
|
131
|
+
fault: "BBX-7401",
|
|
132
|
+
en: "The session token was sent onward",
|
|
133
|
+
ar: "أُرسل رمز الجلسة إلى جهة أخرى",
|
|
134
|
+
},
|
|
135
|
+
REVOKED_MEMORY_READ: {
|
|
136
|
+
fault: "BBX-7101",
|
|
137
|
+
en: "A revoked memory was used",
|
|
138
|
+
ar: "استُخدمت ذاكرة ملغاة",
|
|
139
|
+
},
|
|
140
|
+
TOOL_OUTPUT_MISMATCH: {
|
|
141
|
+
fault: "BBX-7201",
|
|
142
|
+
en: "Tool output doesn't match a re-run",
|
|
143
|
+
ar: "مخرجات الأداة لا تطابق إعادة التشغيل",
|
|
144
|
+
},
|
|
145
|
+
ABNORMAL_RUN: {
|
|
146
|
+
fault: "BBX-7302",
|
|
147
|
+
en: "A run unlike this agent's successful runs",
|
|
148
|
+
ar: "مسار تشغيل غير مألوف لهذا الوكيل",
|
|
149
|
+
},
|
|
150
|
+
PRECOG_RISK: {
|
|
151
|
+
fault: "BBX-7301",
|
|
152
|
+
en: "Likely to fail, predicted early",
|
|
153
|
+
ar: "من المرجح أن تفشل، بتنبؤ مبكر",
|
|
154
|
+
},
|
|
155
|
+
AUTOMATION_SURPRISE: {
|
|
156
|
+
fault: "BBX-8101",
|
|
157
|
+
en: "The agent acted after it lost the controls",
|
|
158
|
+
ar: "تصرّف الوكيل بعد أن فقد السيطرة",
|
|
159
|
+
},
|
|
160
|
+
TAKEOVER: {
|
|
161
|
+
fault: "BBX-8102",
|
|
162
|
+
en: "A person took the controls",
|
|
163
|
+
ar: "تولّى شخص القيادة",
|
|
164
|
+
},
|
|
165
|
+
PAUSED: { fault: "BBX-8103", en: "Paused by the operator", ar: "إيقاف مؤقت من المشغّل" },
|
|
166
|
+
DIRECTIVE: {
|
|
167
|
+
fault: "BBX-8201",
|
|
168
|
+
en: "An airworthiness directive applies to this fleet",
|
|
169
|
+
ar: "ينطبق توجيه صلاحية على هذا الأسطول",
|
|
170
|
+
},
|
|
171
|
+
UNDO_FAILED: {
|
|
172
|
+
fault: "BBX-8301",
|
|
173
|
+
en: "An undo didn't go through",
|
|
174
|
+
ar: "لم ينجح التراجع",
|
|
175
|
+
},
|
|
176
|
+
APPROVAL_REJECTED: {
|
|
177
|
+
fault: "BBX-8401",
|
|
178
|
+
en: "A second person refused the action",
|
|
179
|
+
ar: "رفض شخص ثانٍ الإجراء",
|
|
180
|
+
},
|
|
181
|
+
APPROVAL_TIMEOUT: {
|
|
182
|
+
fault: "BBX-8402",
|
|
183
|
+
en: "No second person confirmed in time",
|
|
184
|
+
ar: "لم يؤكد شخص ثانٍ في الوقت المحدد",
|
|
185
|
+
},
|
|
186
|
+
APPROVAL_PENDING: {
|
|
187
|
+
fault: "BBX-8403",
|
|
188
|
+
en: "An action waits for a second person",
|
|
189
|
+
ar: "إجراء بانتظار شخص ثانٍ",
|
|
190
|
+
},
|
|
191
|
+
PREFLIGHT_DEGRADED: {
|
|
192
|
+
fault: "BBX-8501",
|
|
193
|
+
en: "Started with equipment missing",
|
|
194
|
+
ar: "بدأ مع نقص في المعدات",
|
|
195
|
+
},
|
|
196
|
+
DUTY_LIMIT: {
|
|
197
|
+
fault: "BBX-8601",
|
|
198
|
+
en: "Duty limit reached: hand over",
|
|
199
|
+
ar: "بلغ حد العمل: سلّم المهمة",
|
|
200
|
+
},
|
|
201
|
+
TYPE_UNRATED: {
|
|
202
|
+
fault: "BBX-8801",
|
|
203
|
+
en: "Flying a model it isn't rated on",
|
|
204
|
+
ar: "يعمل على نموذج غير مؤهل له",
|
|
205
|
+
},
|
|
206
|
+
DATA_LEFT_REGION: {
|
|
207
|
+
fault: "BBX-9101",
|
|
208
|
+
en: "Personal data sent outside the data region",
|
|
209
|
+
ar: "بيانات شخصية أُرسلت خارج منطقة البيانات",
|
|
210
|
+
},
|
|
211
|
+
DATA_REGION_UNKNOWN: {
|
|
212
|
+
fault: "BBX-9102",
|
|
213
|
+
en: "Personal data sent to a model of unknown region",
|
|
214
|
+
ar: "بيانات شخصية أُرسلت إلى نموذج منطقته غير معروفة",
|
|
215
|
+
},
|
|
216
|
+
DATA_TO_UNAPPROVED_DESTINATION: {
|
|
217
|
+
fault: "BBX-9103",
|
|
218
|
+
en: "Data sent to a destination its rules don't allow",
|
|
219
|
+
ar: "بيانات أُرسلت إلى وجهة لا تسمح بها قواعدها",
|
|
220
|
+
},
|
|
221
|
+
DATA_CANARY_TRIPPED: {
|
|
222
|
+
fault: "BBX-9104",
|
|
223
|
+
en: "A canary value left the gateway",
|
|
224
|
+
ar: "قيمة إنذار خرجت من البوابة",
|
|
225
|
+
},
|
|
226
|
+
CONSENT_WITHDRAWN_USE: {
|
|
227
|
+
fault: "BBX-9105",
|
|
228
|
+
en: "Data used after consent was withdrawn",
|
|
229
|
+
ar: "استُخدمت بيانات بعد سحب الموافقة",
|
|
230
|
+
},
|
|
231
|
+
UNEVIDENCED_SIDE_EFFECT: {
|
|
232
|
+
fault: "BBX-9201",
|
|
233
|
+
en: "A change the touched system gave no proof of",
|
|
234
|
+
ar: "تغيير لم يقدّم النظام المعني دليلاً عليه",
|
|
235
|
+
},
|
|
236
|
+
TOOL_WITNESS_INVALID: {
|
|
237
|
+
fault: "BBX-9202",
|
|
238
|
+
en: "A tool's signature doesn't verify",
|
|
239
|
+
ar: "توقيع الأداة غير صالح",
|
|
240
|
+
},
|
|
241
|
+
EGRESS_OPEN: {
|
|
242
|
+
fault: "BBX-9301",
|
|
243
|
+
en: "The agent can reach the internet around the gateway",
|
|
244
|
+
ar: "يستطيع الوكيل الوصول إلى الإنترنت متجاوزاً البوابة",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
/** The fault for a finding code; unknown codes get a generic entry, never an exception. */
|
|
248
|
+
export function faultOf(code) {
|
|
249
|
+
return FAULTS[code] ?? { fault: "BBX-0000", en: code, ar: code };
|
|
250
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LoadedPrices } from "../cost/index.ts";
|
|
2
|
+
import { type DutyLimits } from "../duty/index.ts";
|
|
3
|
+
import { type CompiledPolicy } from "../policy/index.ts";
|
|
4
|
+
import { type PrecogModel } from "../precog/index.ts";
|
|
5
|
+
import type { Envelope, Json } from "../verify/index.ts";
|
|
6
|
+
export { type ClaimVerdict, type CriterionResult, checkFlightPlan, type Landing, landing, } from "./landing.ts";
|
|
7
|
+
export { OUTCOMES, type Outcome, type OutcomeRollup, rollup, type WasteReason, type WasteReport, waste, } from "./waste.ts";
|
|
8
|
+
export type Severity = "advisory" | "caution" | "warning";
|
|
9
|
+
export interface Finding {
|
|
10
|
+
code: string;
|
|
11
|
+
source: string;
|
|
12
|
+
severity: Severity;
|
|
13
|
+
ref: {
|
|
14
|
+
[key: string]: string | number | boolean;
|
|
15
|
+
};
|
|
16
|
+
detail?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AnalyzeOptions {
|
|
19
|
+
/** Body bytes by hash (SDK events carry their ids in the body; requests and responses are bodies). */
|
|
20
|
+
bodies: (hash: string) => Uint8Array | undefined;
|
|
21
|
+
/** For the detectors: context windows (D4) and cost (D6). */
|
|
22
|
+
prices?: LoadedPrices;
|
|
23
|
+
budgetMicroUsd?: number;
|
|
24
|
+
/** spec/duty.md §1: the server's default duty limits (a flight plan's win). */
|
|
25
|
+
duty?: DutyLimits;
|
|
26
|
+
/** spec/policy.md: the compiled tool policy, for POLICY_DENIED / POLICY_VIOLATION. */
|
|
27
|
+
policy?: CompiledPolicy;
|
|
28
|
+
/** spec/precog.md §3: the early-failure model, for PRECOG_RISK (needs `prices`). */
|
|
29
|
+
precog?: PrecogModel;
|
|
30
|
+
/** Epoch ms; +Infinity = judge everything (a session being closed). Default: now. */
|
|
31
|
+
now?: number;
|
|
32
|
+
graceMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface Ev extends Envelope {
|
|
35
|
+
/** The parsed body, for kinds whose body is JSON (sdk.event). */
|
|
36
|
+
data?: {
|
|
37
|
+
[key: string]: Json;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Parses the lines once, attaching SDK event bodies. */
|
|
41
|
+
export declare function eventsOf(lines: readonly string[], bodies: AnalyzeOptions["bodies"]): Ev[];
|
|
42
|
+
/** Every analysis, in spec order. */
|
|
43
|
+
export declare function analyze(lines: readonly string[], options: AnalyzeOptions): Finding[];
|
|
44
|
+
/** spec/attestation.md §3: a re-run that printed something else than the model was given. */
|
|
45
|
+
export declare function attestationFindings(events: readonly Ev[]): Finding[];
|
|
46
|
+
/** spec/fleet.md §3: the agent's own near-miss reports. */
|
|
47
|
+
export declare function nearMisses(events: readonly Ev[]): Finding[];
|
|
48
|
+
/** A stable key for dedup: code + ref with sorted keys. */
|
|
49
|
+
export declare function findingKey(f: {
|
|
50
|
+
code: string;
|
|
51
|
+
ref: {
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
}): string;
|
|
55
|
+
export declare function dualWitness(events: Ev[], now: number, graceMs: number): Finding[];
|
|
56
|
+
export declare function checkpointReplay(events: Ev[]): Finding[];
|
|
57
|
+
/** N4 (idea R9): a tool call with no result, judged at close (`now` is +Infinity): a crash
|
|
58
|
+
* mid-call. */
|
|
59
|
+
export declare function orphanedToolCalls(events: Ev[], now: number): Finding[];
|
|
60
|
+
/** N4 (idea R5): a LangGraph run in `exit` durability that ran tools. Once per session. */
|
|
61
|
+
export declare function exitDurability(events: Ev[]): Finding[];
|
|
62
|
+
/** N6 (idea S3): the gateway refused a request that carried the session's own token. */
|
|
63
|
+
export declare function tokenMisuse(events: Ev[]): Finding[];
|
|
64
|
+
/** Stage 2 R3: a `verify` check that passed, then failed later in the run. */
|
|
65
|
+
export declare function verifyRegression(events: Ev[]): Finding[];
|
|
66
|
+
/** Stage 2 R4, judged at close: a run labelled (or claimed) a success that its own record contradicts:
|
|
67
|
+
* a check's last result failed, or the last tool call failed with nothing succeeding after it. */
|
|
68
|
+
export declare function falseSuccess(events: Ev[], now: number): Finding[];
|