dsh-llm-verifier 0.3.0 → 0.4.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.
Files changed (97) hide show
  1. package/README.md +133 -9
  2. package/lib/auto-B5V49e-A.js +1517 -0
  3. package/lib/auto-B5V49e-A.js.map +1 -0
  4. package/lib/{caller-DJd_FlrQ.js → caller-DR_C7ueP.js} +154 -20
  5. package/lib/caller-DR_C7ueP.js.map +1 -0
  6. package/lib/caller.js +2 -2
  7. package/lib/client.js +804 -29
  8. package/lib/client.js.map +1 -1
  9. package/lib/core.js +226 -3
  10. package/lib/core.js.map +1 -1
  11. package/lib/index.js +1014 -1337
  12. package/lib/index.js.map +1 -1
  13. package/lib/replay.js +157 -0
  14. package/lib/replay.js.map +1 -0
  15. package/lib/types/auto.js +1 -1
  16. package/lib/types/auto.js.map +1 -1
  17. package/lib/types/caller.d.ts +65 -0
  18. package/lib/types/caller.d.ts.map +1 -1
  19. package/lib/types/caller.js +113 -5
  20. package/lib/types/caller.js.map +1 -1
  21. package/lib/types/client-i18n.d.ts +163 -6
  22. package/lib/types/client-i18n.d.ts.map +1 -1
  23. package/lib/types/client-i18n.js +137 -26
  24. package/lib/types/client-i18n.js.map +1 -1
  25. package/lib/types/client.d.ts +30 -0
  26. package/lib/types/client.d.ts.map +1 -1
  27. package/lib/types/client.js +94 -9
  28. package/lib/types/client.js.map +1 -1
  29. package/lib/types/config.d.ts +33 -0
  30. package/lib/types/config.d.ts.map +1 -1
  31. package/lib/types/config.js +17 -2
  32. package/lib/types/config.js.map +1 -1
  33. package/lib/types/core.d.ts +84 -0
  34. package/lib/types/core.d.ts.map +1 -1
  35. package/lib/types/core.js +222 -2
  36. package/lib/types/core.js.map +1 -1
  37. package/lib/types/criteria.d.ts +33 -0
  38. package/lib/types/criteria.d.ts.map +1 -0
  39. package/lib/types/criteria.js +67 -0
  40. package/lib/types/criteria.js.map +1 -0
  41. package/lib/types/decisions.d.ts +84 -0
  42. package/lib/types/decisions.d.ts.map +1 -0
  43. package/lib/types/decisions.js +209 -0
  44. package/lib/types/decisions.js.map +1 -0
  45. package/lib/types/engine.d.ts +54 -1
  46. package/lib/types/engine.d.ts.map +1 -1
  47. package/lib/types/engine.js +175 -12
  48. package/lib/types/engine.js.map +1 -1
  49. package/lib/types/index.d.ts +1 -0
  50. package/lib/types/index.d.ts.map +1 -1
  51. package/lib/types/index.js +392 -38
  52. package/lib/types/index.js.map +1 -1
  53. package/lib/types/replay.d.ts +88 -0
  54. package/lib/types/replay.d.ts.map +1 -0
  55. package/lib/types/replay.js +136 -0
  56. package/lib/types/replay.js.map +1 -0
  57. package/lib/types/router.d.ts +29 -5
  58. package/lib/types/router.d.ts.map +1 -1
  59. package/lib/types/router.js +283 -46
  60. package/lib/types/router.js.map +1 -1
  61. package/lib/types/statistics.d.ts +10 -1
  62. package/lib/types/statistics.d.ts.map +1 -1
  63. package/lib/types/statistics.js +35 -7
  64. package/lib/types/statistics.js.map +1 -1
  65. package/lib/types/top-logprobs.d.ts +36 -0
  66. package/lib/types/top-logprobs.d.ts.map +1 -1
  67. package/lib/types/top-logprobs.js +41 -14
  68. package/lib/types/top-logprobs.js.map +1 -1
  69. package/package.json +6 -1
  70. package/scripts/eval-replay.mjs +118 -0
  71. package/src/auto.ts +1 -1
  72. package/src/caller.test.ts +62 -1
  73. package/src/caller.ts +124 -4
  74. package/src/client-i18n.ts +137 -25
  75. package/src/client.test.ts +21 -0
  76. package/src/client.tsx +145 -7
  77. package/src/config.test.ts +32 -0
  78. package/src/config.ts +49 -2
  79. package/src/core.test.ts +95 -0
  80. package/src/core.ts +217 -2
  81. package/src/criteria.test.ts +60 -0
  82. package/src/criteria.ts +76 -0
  83. package/src/decisions.test.ts +150 -0
  84. package/src/decisions.ts +244 -0
  85. package/src/engine.test.ts +131 -0
  86. package/src/engine.ts +167 -10
  87. package/src/index.test.ts +278 -8
  88. package/src/index.ts +373 -37
  89. package/src/replay.test.ts +82 -0
  90. package/src/replay.ts +155 -0
  91. package/src/router.test.ts +240 -1
  92. package/src/router.ts +298 -48
  93. package/src/statistics.test.ts +49 -1
  94. package/src/statistics.ts +44 -7
  95. package/src/top-logprobs.test.ts +28 -0
  96. package/src/top-logprobs.ts +56 -8
  97. package/lib/caller-DJd_FlrQ.js.map +0 -1
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseStatisticsRecords, replayDecisionScores, sweepThresholds } from './replay.ts'
3
+
4
+ const statistics = JSON.stringify({
5
+ version: 1,
6
+ records: [
7
+ { toolName: 'verifier_current_session', startedAt: 1, success: true, verdict: { phase: 'final', score: 1, baselineScore: 0, winner: 'A', criteria: [{ id: 'a', score: 1 }, { id: 'b', score: 1 }], threshold: 0.65 } },
8
+ { toolName: 'verifier_current_session', startedAt: 2, success: true, verdict: { phase: 'final', score: 0.6, baselineScore: 0, winner: 'A', criteria: [{ id: 'a', score: 1 }, { id: 'b', score: 0.2 }], threshold: 0.65 } },
9
+ { toolName: 'verifier_current_session', startedAt: 3, success: false },
10
+ { toolName: 'verifier_compare', startedAt: 4, success: true, verdict: { phase: 'compare', score: 0.9, scoreB: 0.1, winner: 'A' } },
11
+ ],
12
+ })
13
+
14
+ describe('parseStatisticsRecords', () => {
15
+ it('reads the verdict fields a sweep needs and survives a malformed document', () => {
16
+ const records = parseStatisticsRecords(statistics)
17
+ expect(records).toHaveLength(4)
18
+ expect(records[0]).toMatchObject({ toolName: 'verifier_current_session', score: 1, winner: 'A' })
19
+ expect(records[0]!.criteria).toEqual([{ id: 'a', score: 1 }, { id: 'b', score: 1 }])
20
+ // A failed invocation has no verdict at all: it must still parse, with no score.
21
+ expect(records[2]).toMatchObject({ success: false, criteria: [] })
22
+ expect(records[2]!.score).toBeUndefined()
23
+ expect(parseStatisticsRecords('not json')).toEqual([])
24
+ expect(parseStatisticsRecords('{"version":1}')).toEqual([])
25
+ })
26
+ })
27
+
28
+ describe('sweepThresholds', () => {
29
+ it('reports what each threshold decides, including the per-criterion floor', () => {
30
+ const records = parseStatisticsRecords(statistics)
31
+ const rows = sweepThresholds(records, [0.5, 0.65, 0.9])
32
+ // Only the two session acceptances are gate decisions: compare uses another threshold.
33
+ expect(rows.every(row => row.total === 3)).toBe(true)
34
+ // At 0.5 the second record's mean (0.6) clears the bar, but its criterion b = 0.2 does not:
35
+ // that is exactly the failure the mean alone used to hide.
36
+ expect(rows.find(row => row.threshold === 0.5)).toMatchObject({ accepted: 1, rejectedVerdict: 0, rejectedMean: 0, rejectedCriterion: 1, unscored: 1 })
37
+ // At 0.65 the mean itself is short, so the floor never comes into play for that record.
38
+ expect(rows.find(row => row.threshold === 0.65)).toMatchObject({ accepted: 1, rejectedMean: 1, rejectedCriterion: 0, unscored: 1 })
39
+ expect(rows.find(row => row.threshold === 0.9)).toMatchObject({ accepted: 1, rejectedVerdict: 0, rejectedMean: 1, rejectedCriterion: 0, unscored: 1 })
40
+ })
41
+
42
+ it('bounds the sweep at the extremes instead of producing NaN', () => {
43
+ const records = parseStatisticsRecords(statistics)
44
+ // A threshold of 0 disables the floor as well: every scored acceptance passes (the one
45
+ // unscored record is never an acceptance, at any threshold).
46
+ expect(sweepThresholds(records, [0])[0]).toMatchObject({ accepted: 2, rejectedCriterion: 0, unscored: 1 })
47
+ expect(sweepThresholds(records, [1])[0]).toMatchObject({ accepted: 1, rejectedMean: 1 })
48
+ expect(sweepThresholds([], [0.65])[0]).toMatchObject({ total: 0, accepted: 0 })
49
+ })
50
+ })
51
+
52
+ describe('replayDecisionScores', () => {
53
+ it('re-parses a stored explicit-tag answer and flags drift and unparseable answers', () => {
54
+ const rows = replayDecisionScores([
55
+ // K is the 10th letter: (20 - 10) - 1 over 19 = 9/19.
56
+ { label: 'clean', channel: 'explicit-tag', output: '<score_A> K </score_A>\n<score_B> T </score_B>', score: 9 / 19 },
57
+ { label: 'changed', channel: 'explicit-tag', output: '<score_A> A </score_A>\n<score_B> T </score_B>', score: 0.5 },
58
+ // A tag that is present but carries no valid letter is a real parser failure.
59
+ { label: 'broken', channel: 'explicit-tag', output: '<score_A> ZZ </score_A>', score: 0.5 },
60
+ { label: 'distribution', channel: 'top-logprobs', output: '<score_A> A </score_A>', score: 0.83 },
61
+ ])
62
+ expect(rows[0]).toMatchObject({ mode: 'match', reparsed: 9 / 19 })
63
+ expect(rows[1]).toMatchObject({ mode: 'drift', reparsed: 1 })
64
+ expect(rows[2]!.mode).toBe('unreadable')
65
+ // A top-logprobs score is an expectation over a distribution the snapshot does not keep.
66
+ expect(rows[3]).toMatchObject({ mode: 'drift', stored: 0.83 })
67
+ })
68
+
69
+ it('replays a progress answer against the last checkpoint, inverted, and ignores non-scoring calls', () => {
70
+ const rows = replayDecisionScores([
71
+ // Track stores the LAST checkpoint and progresses A(=0) .. T(=1): <c2> T </c2> is 1.0.
72
+ { label: 'progress repeat 1/3', channel: 'explicit-tag', output: '<c1> K </c1>\n<c2> T </c2>', score: 1 },
73
+ { label: 'progress repeat 2/3', channel: 'explicit-tag', output: '<c1> K </c1>\n<c2> T </c2>', score: 0.5 },
74
+ // A route classification answers strict JSON — no score tag exists by design, and counting
75
+ // it as unreadable made every routing snapshot look like a regression.
76
+ { label: 'route classify', channel: 'explicit-tag', output: '{"kind":"none","confidence":0.9}', score: undefined },
77
+ ])
78
+ expect(rows[0]).toMatchObject({ mode: 'match', reparsed: 1 })
79
+ expect(rows[1]).toMatchObject({ mode: 'drift', reparsed: 1 })
80
+ expect(rows[2]!.mode).toBe('not-scored')
81
+ })
82
+ })
package/src/replay.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { extractScore } from './core.ts'
2
+ import { sessionAccepted } from './auto.ts'
3
+
4
+ /** One persisted invocation, reduced to the fields an acceptance decision depends on. */
5
+ export interface ReplayInvocation {
6
+ toolName: string
7
+ startedAt: number
8
+ success: boolean
9
+ score?: number
10
+ baselineScore?: number
11
+ winner?: 'A' | 'B' | 'tie'
12
+ criteria: Array<{ id: string; score: number }>
13
+ }
14
+
15
+ function numberAt(row: Record<string, unknown>, key: string): number | undefined {
16
+ const value = row[key]
17
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
18
+ }
19
+
20
+ /**
21
+ * Read one topic's statistics file into the records a threshold sweep can replay.
22
+ *
23
+ * Loose on purpose, exactly like the store's own loader: a record written by an older or newer
24
+ * plugin must not abort the replay. Records that carry no verdict (failed invocations) come back
25
+ * with no score, and the sweep reports them as unscored rather than as acceptances.
26
+ * @param text - raw `verifier/statistics-v1.json`.
27
+ * @returns The parsed invocations; an unreadable document yields an empty list.
28
+ */
29
+ export function parseStatisticsRecords(text: string): ReplayInvocation[] {
30
+ let document: unknown
31
+ try { document = JSON.parse(text) } catch { return [] }
32
+ const records = typeof document === 'object' && document !== null ? (document as { records?: unknown }).records : undefined
33
+ if (!Array.isArray(records)) return []
34
+ const out: ReplayInvocation[] = []
35
+ for (const entry of records) {
36
+ if (typeof entry !== 'object' || entry === null) continue
37
+ const row = entry as Record<string, unknown>
38
+ const verdict = typeof row.verdict === 'object' && row.verdict !== null ? row.verdict as Record<string, unknown> : {}
39
+ const criteria: Array<{ id: string; score: number }> = []
40
+ for (const item of Array.isArray(verdict.criteria) ? verdict.criteria : []) {
41
+ if (typeof item !== 'object' || item === null) continue
42
+ const criterion = item as Record<string, unknown>
43
+ if (typeof criterion.id !== 'string') continue
44
+ const score = numberAt(criterion, 'score')
45
+ if (score !== undefined) criteria.push({ id: criterion.id, score })
46
+ }
47
+ const score = numberAt(verdict, 'score')
48
+ const baselineScore = numberAt(verdict, 'baselineScore')
49
+ const winner = verdict.winner === 'A' || verdict.winner === 'B' || verdict.winner === 'tie' ? verdict.winner : undefined
50
+ out.push({
51
+ toolName: typeof row.toolName === 'string' ? row.toolName : '',
52
+ startedAt: numberAt(row, 'startedAt') ?? 0,
53
+ success: row.success === true,
54
+ criteria,
55
+ ...(score !== undefined ? { score } : {}),
56
+ ...(baselineScore !== undefined ? { baselineScore } : {}),
57
+ ...(winner !== undefined ? { winner } : {}),
58
+ })
59
+ }
60
+ return out
61
+ }
62
+
63
+ /** Why a recorded acceptance failed at one threshold, so a sweep shows WHAT the threshold decides. */
64
+ export interface ThresholdRow {
65
+ threshold: number
66
+ total: number
67
+ accepted: number
68
+ /** Losing to the empty-work baseline (or an unreadable verdict) can never pass. */
69
+ rejectedVerdict: number
70
+ /** The mean itself was below the threshold. */
71
+ rejectedMean: number
72
+ /** The mean passed but at least one criterion was below the threshold. */
73
+ rejectedCriterion: number
74
+ /** The record has no usable score (a failed invocation). */
75
+ unscored: number
76
+ }
77
+
78
+ /**
79
+ * Re-score every recorded session acceptance at each threshold with the LIVE acceptance rule.
80
+ *
81
+ * This is the tuning tool the plan calls for: `autoVerifyThreshold` (and the per-criterion
82
+ * floor that {@link sessionAccepted} applies) is currently a hand-picked number with no evidence
83
+ * behind it. The stored verdict already carries the session score, the baseline score, the winner
84
+ * and the per-criterion scores — everything the gate looked at — so a sweep costs no model calls.
85
+ *
86
+ * Only `verifier_current_session` records are gate decisions; routed compare/select/track
87
+ * verdicts use different thresholds and are skipped.
88
+ * @param invocations - records from {@link parseStatisticsRecords}.
89
+ * @param thresholds - thresholds to test, typically 0.5..0.9.
90
+ * @returns One row per threshold, in the given order.
91
+ */
92
+ export function sweepThresholds(invocations: readonly ReplayInvocation[], thresholds: readonly number[]): ThresholdRow[] {
93
+ const gateRecords = invocations.filter(record => record.toolName === 'verifier_current_session')
94
+ return thresholds.map(threshold => {
95
+ const row: ThresholdRow = { threshold, total: gateRecords.length, accepted: 0, rejectedVerdict: 0, rejectedMean: 0, rejectedCriterion: 0, unscored: 0 }
96
+ for (const record of gateRecords) {
97
+ if (record.score === undefined || record.winner === undefined) { row.unscored += 1; continue }
98
+ if (sessionAccepted({ score: record.score, winner: record.winner, criteria: record.criteria }, threshold)) { row.accepted += 1; continue }
99
+ if (record.winner !== 'A') row.rejectedVerdict += 1
100
+ else if (record.score < threshold) row.rejectedMean += 1
101
+ else row.rejectedCriterion += 1
102
+ }
103
+ return row
104
+ })
105
+ }
106
+
107
+ /** One captured judge call re-parsed through the current scorer. */
108
+ export interface DecisionReplayRow {
109
+ label: string
110
+ channel: string
111
+ stored?: number
112
+ reparsed?: number
113
+ /**
114
+ * `not-scored` means the answer carries no score tag at all — a route classification, or any
115
+ * other non-scoring call. Counting those as failures made the report cry wolf on every
116
+ * `track` and `verifier_route_classify` snapshot.
117
+ */
118
+ mode: 'match' | 'drift' | 'unreadable' | 'not-scored'
119
+ }
120
+
121
+ /**
122
+ * Re-parse captured judge answers with the current score parser.
123
+ *
124
+ * A snapshot stores the raw answer, so the parser can be replayed offline: a pairwise call must
125
+ * parse back to exactly the score it produced, and a drift means the parser (or the prompt format)
126
+ * changed under a stored answer. A top-logprobs call's stored score is an expectation over a token
127
+ * distribution the snapshot does not keep, so a text-channel re-parse is expected to differ and is
128
+ * reported as drift — read those rows as informational, not as regressions.
129
+ *
130
+ * The tag depends on the call: pairwise judging answers `<score_A>`, while progress judging answers
131
+ * `<c1>..<cN>` and the trace stores the LAST checkpoint, inverted (progress runs A = nothing done
132
+ * .. T = certainly done, the reverse of the pairwise scale). A call with neither tag is a route
133
+ * classification, i.e. not a score at all.
134
+ * @param calls - captured calls with their raw answers.
135
+ * @returns One row per call.
136
+ */
137
+ export function replayDecisionScores(calls: ReadonlyArray<{ label: string; channel: string; output: string; score?: number }>): DecisionReplayRow[] {
138
+ return calls.map(call => {
139
+ const completion = { text: call.output, tokens: [], positions: [] }
140
+ const progressTags = [...call.output.matchAll(/<c(\d+)>/gu)].map(match => '<c' + match[1] + '>')
141
+ const tagged = call.output.includes('<score_A>') || progressTags.length > 0
142
+ let reparsed: number | undefined
143
+ if (tagged) {
144
+ try {
145
+ reparsed = call.output.includes('<score_A>')
146
+ ? extractScore(completion, '<score_A>')
147
+ : 1 - extractScore(completion, progressTags[progressTags.length - 1]!)
148
+ } catch { reparsed = undefined }
149
+ }
150
+ const head = { label: call.label, channel: call.channel, ...(call.score !== undefined ? { stored: call.score } : {}) }
151
+ if (!tagged) return { ...head, mode: 'not-scored' as const }
152
+ if (reparsed === undefined) return { ...head, mode: 'unreadable' as const }
153
+ return { ...head, reparsed, mode: call.score !== undefined && Math.abs(call.score - reparsed) < 1e-9 ? 'match' as const : 'drift' as const }
154
+ })
155
+ }
@@ -13,10 +13,18 @@ function tool(value: ReturnType<typeof session>, name: string, id: string, text:
13
13
  value.append('tool/call', { turn, step, callId: id as never, name, arguments: '{}' })
14
14
  value.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text }], isError: false }) }, { surfaceOp: 'append' })
15
15
  }
16
+ /** The full-output evidence block of one rendered step, i.e. everything before its digest. */
17
+ function evidenceBlock(step: string): string {
18
+ const start = step.indexOf('Latest observed tool output')
19
+ if (start < 0) return ''
20
+ const rest = step.slice(start)
21
+ const end = rest.indexOf('\n\n')
22
+ return end < 0 ? rest : rest.slice(0, end)
23
+ }
16
24
  function assistant(value: ReturnType<typeof session>, text: string, turn = 1, step = 1) {
17
25
  value.append('assistant/message', { turn, step, message: createAssistantMessage({ content: [{ type: 'text', text }], source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } }) }, { surfaceOp: 'append' })
18
26
  }
19
- const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxPerTask: 5, maxPerSession: 20, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
27
+ const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxRoutePerTask: 5, maxRoutePerSession: 20, maxFinalPerTask: 2, maxFinalPerSession: 8, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
20
28
  const envelope = (count: number) => JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'auth', candidates: Array.from({ length: count }, (_, i) => ({ id: String(i + 1), label: 'C' + (i + 1), status: 'completed', content: 'candidate ' + (i + 1) })) })
21
29
 
22
30
  describe('production structured routing', () => {
@@ -162,6 +170,193 @@ describe('production structured routing', () => {
162
170
  expect(decision.steps[1]).not.toContain('counts:')
163
171
  })
164
172
 
173
+ it('never attaches a deliverable presentation as checkpoint evidence', () => {
174
+ // Regression (live session, four routes in a row): `present` is the last call of a
175
+ // turn, so the newest "observed tool output" reaching the judge was the wrapper's
176
+ // `presented: 1` — the paths the agent had just declared — while the typecheck and
177
+ // test run sat one call earlier. The judge's own rule ("a state without real
178
+ // verification should not exceed K") then capped the newest checkpoint at exactly
179
+ // K = 52.6% against a 0.8 threshold, so every route steered "continue the
180
+ // unfinished work" for work that was finished and verified, until the per-task
181
+ // route budget ran out.
182
+ const value = session()
183
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
184
+ tool(value, 'pwsh', 'verify', 'TYPECHECK_EXIT=0\nTests 309 passed')
185
+ value.append('tool/call', { turn: 1, step: 1, callId: 'wrap' as never, name: 'run_code', arguments: '{}' })
186
+ value.append('tool/ptc-dispatch' as never, { rootCallId: 'wrap', subCallId: 'wrap:ptc:1', name: 'present', arguments: '{}', isError: false, content: [{ type: 'text', text: 'Presented C:\\repo\\src\\mapper.ts' }] } as never)
187
+ value.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: 'wrap' as never, content: [{ type: 'text', text: 'presented: 1' }], isError: false }) }, { surfaceOp: 'append' })
188
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
189
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
190
+ expect(decision?.kind).toBe('track')
191
+ if (decision?.kind !== 'track') return
192
+ // The verification run is older than the presentation and must still win the
193
+ // evidence slot; the presentation survives only as one line of digest context.
194
+ const step = decision.steps[1]!
195
+ expect(evidenceBlock(step)).toContain('309 passed')
196
+ expect(evidenceBlock(step)).not.toContain('Presented C:')
197
+ expect(evidenceBlock(step)).not.toContain('presented: 1')
198
+ expect(step).toContain('Recent tool results')
199
+ expect(step).toContain('present: Presented C:')
200
+ })
201
+
202
+ it('shows the newest verification run when the newest output is not one', () => {
203
+ // Regression (live session): the full suite passed, then 88 tool calls closed issues
204
+ // and printed a status summary. The newest checkpoint carried only that summary, so
205
+ // the judge applied its own "no real verification" ceiling — exactly K = 52.6%
206
+ // against a 0.8 threshold — and the route steered "continue" for finished work.
207
+ const value = session()
208
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
209
+ tool(value, 'pwsh', 'verify', 'Test Files 10 passed (10)\n Tests 86 passed (86)')
210
+ tool(value, 'pwsh', 'close', '1517 closed completed')
211
+ tool(value, 'pwsh', 'status', '--- ahead/behind origin/dev ---\n0\t0\nlib-artifact-check: OK')
212
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
213
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
214
+ expect(decision?.kind).toBe('track')
215
+ if (decision?.kind !== 'track') return
216
+ const step = decision.steps[1]!
217
+ // The newest output stays the primary evidence...
218
+ expect(step).toContain('lib-artifact-check: OK')
219
+ // ...and the run it was hiding comes back, with how much happened after it.
220
+ expect(step).toContain('Latest observed verification run')
221
+ expect(step).toContain('86 passed')
222
+ expect(step).toContain('tool result(s) since')
223
+ // Historical checkpoints never grow this block: they describe past states.
224
+ expect(decision.steps[0]).not.toContain('Latest observed verification run')
225
+ })
226
+
227
+ it('does not repeat a verification run that is already the newest output', () => {
228
+ const value = session()
229
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
230
+ tool(value, 'pwsh', 'verify', 'Test Files 1 passed (1)\n Tests 9 passed (9)')
231
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
232
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
233
+ expect(decision?.kind).toBe('track')
234
+ if (decision?.kind !== 'track') return
235
+ expect(decision.steps[1]).toContain('Latest observed tool output at routing time')
236
+ expect(decision.steps[1]).not.toContain('Latest observed verification run')
237
+ })
238
+
239
+ it('leaves the checkpoints alone when the task never verified anything', () => {
240
+ const value = session()
241
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
242
+ tool(value, 'pwsh', 'edit', 'The file has been updated successfully.')
243
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
244
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
245
+ expect(decision?.kind).toBe('track')
246
+ if (decision?.kind !== 'track') return
247
+ expect(decision.steps[1]).toContain('The file has been updated successfully.')
248
+ expect(decision.steps[1]).not.toContain('Latest observed verification run')
249
+ })
250
+
251
+ it('keeps the extra verification block inside the checkpoint caps', () => {
252
+ const value = session()
253
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
254
+ tool(value, 'pwsh', 'verify', 'Test Files 10 passed (10)\n' + 'T'.repeat(4000))
255
+ tool(value, 'pwsh', 'tail', 'Z'.repeat(4000))
256
+ assistant(value, 'Q'.repeat(4000))
257
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
258
+ for (const maxItemChars of [128, 200, 500, 2000, 20000]) {
259
+ const maxInputChars = Math.max(60000, maxItemChars * 2)
260
+ const decision = analyzeStructuredRoute(value.events, 8, maxItemChars, maxInputChars)
261
+ expect(decision, 'maxItemChars=' + maxItemChars).toMatchObject({ kind: 'track' })
262
+ if (decision?.kind !== 'track') continue
263
+ for (const step of decision.steps) expect(step.length, 'maxItemChars=' + maxItemChars).toBeLessThanOrEqual(maxItemChars)
264
+ expect(decision.steps.reduce((sum, step) => sum + step.length, 0), 'maxItemChars=' + maxItemChars).toBeLessThanOrEqual(maxInputChars)
265
+ expect(boundDecision(decision, { ...policy, maxItemChars, maxInputChars }), 'maxItemChars=' + maxItemChars).toBeDefined()
266
+ }
267
+ })
268
+
269
+ it('never attaches coordination output as checkpoint evidence', () => {
270
+ // The same rule as `present`, applied to the rest of the agent's own control
271
+ // surface: every one of these runs AFTER the work, so as the newest result it hides
272
+ // the verification run and the judge caps the checkpoint at K — the ceiling its own
273
+ // prompt sets for "no real verification" — against the 0.8 progress threshold.
274
+ const value = session()
275
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
276
+ tool(value, 'pwsh', 'verify', 'TYPECHECK_EXIT=0\nTests 309 passed')
277
+ tool(value, 'job_list', 'jobs', '(no background jobs)')
278
+ tool(value, 'job_kill', 'kill', 'requested cancellation of job job-1')
279
+ tool(value, 'send_message', 'send', '{"messageId":"m1"}')
280
+ tool(value, 'list_subagent_models', 'models', 'antigravity/gemini-3.8-flash — Gemini 3.8 Flash')
281
+ // A previous verdict is not observed work output either: grading progress from it
282
+ // would be the judge grading itself.
283
+ tool(value, 'verifier_current_session', 'verdict', '{"winner":"A","score":1,"baselineScore":0,"threshold":0.65}')
284
+ tool(value, 'subagent', 'child', 'started subagent 042f004d-fe19-4ccb-8b98-f08a00b32e87')
285
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
286
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
287
+ expect(decision?.kind).toBe('track')
288
+ if (decision?.kind !== 'track') return
289
+ const step = decision.steps[1]!
290
+ expect(evidenceBlock(step)).toContain('309 passed')
291
+ for (const marker of ['(no background jobs)', 'requested cancellation', 'messageId', 'Gemini 3.8 Flash', '"baselineScore"', 'started subagent']) {
292
+ expect(evidenceBlock(step), marker).not.toContain(marker)
293
+ }
294
+ // The digest still shows them as one-line context — that tail is what the judge has
295
+ // to weigh — but a previous verdict never appears at all.
296
+ expect(step).toContain('Recent tool results')
297
+ expect(step).toContain('(no background jobs)')
298
+ expect(step).not.toContain('verifier_current_session')
299
+ })
300
+
301
+ it('summarises the recent calls in one line each, newest last', () => {
302
+ // The verification block says HOW MUCH happened after the run; the digest says WHAT,
303
+ // so a judge can tell "only issue bookkeeping since" from "twelve writes since".
304
+ const value = session()
305
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
306
+ tool(value, 'pwsh', 'verify', 'Test Files 3 passed (3)\n Tests 12 passed (12)')
307
+ tool(value, 'pwsh', 'close', '1517 closed completed')
308
+ tool(value, 'todo_write', 'todos', 'Updated todo list: 0 pending, 0 in progress, 13 completed.')
309
+ tool(value, 'present', 'present', 'Presented C:\\repo\\src\\mapper.ts')
310
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
311
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
312
+ expect(decision?.kind).toBe('track')
313
+ if (decision?.kind !== 'track') return
314
+ const step = decision.steps[1]!
315
+ const digest = step.slice(step.indexOf('Recent tool results'))
316
+ expect(digest).toContain('pwsh: 1517 closed completed')
317
+ expect(digest).toContain('todo_write: Updated todo list')
318
+ expect(digest).toContain('present: Presented C:')
319
+ // Newest last, and the two calls already rendered in full are marked, not repeated.
320
+ expect(digest.indexOf('1517 closed completed')).toBeLessThan(digest.indexOf('present: Presented C:'))
321
+ expect(digest).toContain('[shown above]')
322
+ // Historical checkpoints describe past states and never grow the digest.
323
+ expect(decision.steps[0]).not.toContain('Recent tool results')
324
+ })
325
+
326
+ it('keeps a foreground subagent report as checkpoint evidence', () => {
327
+ // Only the start acknowledgement is coordination. A foreground child returns the
328
+ // report it was asked for, which for delegated work is the deliverable itself.
329
+ const value = session()
330
+ value.append('todo/write', { todos: [{ content: 'Investigate', status: 'in_progress' }, { content: 'Report', status: 'pending' }] })
331
+ tool(value, 'subagent', 'child', 'Report: the mapper drops image parts when resolveRequestImages is undefined.')
332
+ value.append('todo/write', { todos: [{ content: 'Investigate', status: 'completed' }, { content: 'Report', status: 'completed' }] })
333
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
334
+ expect(decision?.kind).toBe('track')
335
+ if (decision?.kind !== 'track') return
336
+ expect(decision.steps[1]).toContain('the mapper drops image parts')
337
+ })
338
+
339
+ it('treats a wrapper that only started a background child as coordination', () => {
340
+ // In the PTC preset the picked result is the wrapper's own text, so the
341
+ // acknowledgement has to be recognised on the dispatch as well.
342
+ const value = session()
343
+ tool(value, 'pwsh', 'verify', 'all tests passed: 91 passed')
344
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'in_progress' }, { content: 'Test', status: 'pending' }] })
345
+ value.append('tool/call', { turn: 1, step: 1, callId: 'wrap' as never, name: 'run_code', arguments: '{}' })
346
+ value.append('tool/ptc-dispatch' as never, { rootCallId: 'wrap', subCallId: 'wrap:ptc:1', name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'started subagent 042f004d' }] } as never)
347
+ value.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: 'wrap' as never, content: [{ type: 'text', text: 'started subagent 042f004d' }], isError: false }) }, { surfaceOp: 'append' })
348
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'completed' }] })
349
+ const decision = analyzeStructuredRoute(value.events, 8, 20000, 60000)
350
+ expect(decision?.kind).toBe('track')
351
+ if (decision?.kind !== 'track') return
352
+ const step = decision.steps[1]!
353
+ expect(evidenceBlock(step)).toContain('all tests passed')
354
+ expect(evidenceBlock(step)).not.toContain('started subagent')
355
+ // It is still visible as digest context: the point is not to hide the call, it is to
356
+ // stop an acknowledgement from occupying the evidence slot.
357
+ expect(step).toContain('started subagent')
358
+ })
359
+
165
360
  it('keeps a wrapper that dispatched real work', () => {
166
361
  const value = session()
167
362
  value.append('tool/call', { turn: 1, step: 1, callId: 'wrap' as never, name: 'run_code', arguments: '{}' })
@@ -236,10 +431,18 @@ describe('semantic evidence references', () => {
236
431
  tool(value, 'subagent', 'a', 'candidate A from a real subagent')
237
432
  tool(value, 'create_goal', 'goal', '{"goal":{"id":"g1","phase":"active"}}')
238
433
  tool(value, 'skill', 'skill', 'loaded review skill body')
434
+ tool(value, 'present', 'present', 'Presented C:\\repo\\src\\mapper.ts')
435
+ tool(value, 'job_list', 'jobs', '(no background jobs)')
436
+ tool(value, 'verifier_current_session', 'verdict', '{"winner":"A","score":1}')
437
+ tool(value, 'subagent', 'child-bg', 'started subagent 042f004d')
239
438
  const prompt = buildSemanticRoutePrompt('pick the better one', value.events, 8, 20000, 60000)
240
439
  expect(prompt).toContain('candidate A from a real subagent')
241
440
  expect(prompt).not.toContain('"phase":"active"')
242
441
  expect(prompt).not.toContain('loaded review skill body')
442
+ expect(prompt).not.toContain('Presented C:')
443
+ expect(prompt).not.toContain('(no background jobs)')
444
+ expect(prompt).not.toContain('"winner":"A"')
445
+ expect(prompt).not.toContain('started subagent')
243
446
  })
244
447
 
245
448
  it('rejects a semantic decision that cites bookkeeping evidence', () => {
@@ -396,6 +599,36 @@ describe('transactional router state', () => {
396
599
  const final = router.reserve(agent, 'final', 'final', 4, policy)!
397
600
  expect(router.commit(agent, final)).toBe(true); expect(router.finalRequired(agent)).toBeUndefined()
398
601
  })
602
+ it('reserves final-verification budget independently of routing attempts', () => {
603
+ const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
604
+ // Routing gets exactly its own cap: exhausting it must not refuse the armed final gate.
605
+ const budget = { ...policy, maxRoutePerTask: 1, maxFinalPerTask: 2 }
606
+ const first = router.reserve(agent, 'track', 'route-1', 1, budget)
607
+ expect(first).toBeDefined()
608
+ expect(router.commit(agent, first!, 7)).toBe(true)
609
+ expect(router.finalRequired(agent)).toBe(7)
610
+ expect(router.reserve(agent, 'track', 'route-2', 1, budget)).toBeUndefined()
611
+ expect(router.budgetExhausted(agent, 1, budget)).toBe(true)
612
+ const final = router.reserve(agent, 'final', 'final-1', 4, budget)
613
+ expect(final).toBeDefined()
614
+ expect(router.commit(agent, final!)).toBe(true)
615
+ expect(router.finalRequired(agent)).toBeUndefined()
616
+ })
617
+ it('prefers the final gate after a track route clears the completion threshold', () => {
618
+ const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
619
+ expect(router.finalPreferred(agent)).toBe(false)
620
+ const track = router.reserve(agent, 'track', 'track-pass', 3, policy)!
621
+ expect(router.commit(agent, track, 9)).toBe(true)
622
+ router.preferFinal(agent)
623
+ expect(router.finalPreferred(agent)).toBe(true)
624
+ // Reserving the final attempt consumes the preference, and a failed acceptance must
625
+ // release routing again rather than disabling it for the rest of the task.
626
+ const final = router.reserve(agent, 'final', 'final', 4, policy)!
627
+ expect(router.finalPreferred(agent)).toBe(false)
628
+ router.fail(agent, final, false)
629
+ expect(router.finalPreferred(agent)).toBe(false)
630
+ expect(router.reserve(agent, 'track', 'route-after-failure', 1, policy)).toBeDefined()
631
+ })
399
632
  it('does not arm final verification when a plan pre-review passes', () => {
400
633
  const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
401
634
  const plan = router.reserve(agent, 'plan_review', 'plan', 1, policy)!
@@ -442,6 +675,12 @@ describe('transactional router state', () => {
442
675
  expect(routedRepeats(select, 3)).toBe(3)
443
676
  expect(routedRepeats(track, 1)).toBe(1)
444
677
  expect(routedRepeats(track, 3)).toBe(3)
678
+ // track carries its own count: with the explicit-tag channel one call samples ONE
679
+ // letter (5.3% of the A-T scale), so repeats are averaged to keep sampling noise from
680
+ // flipping the progress curve between bands.
681
+ expect(routedRepeats(track, 1, 3)).toBe(3)
682
+ expect(routedRepeats(compare, 1, 3)).toBe(2)
683
+ expect(routedRepeats(select, 1, 3)).toBe(1)
445
684
  })
446
685
 
447
686
  it('estimates routed calls from the real tournament shape', () => {