agentflight 0.4.0 → 0.4.2

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.
@@ -1,111 +1,316 @@
1
1
  export function renderHtmlReplay(input) {
2
- const risk = input.riskBadges[0] ?? "unknown";
3
2
  const readiness = input.reviewReadiness ?? "Unknown";
3
+ const verdict = classifyReadiness(readiness);
4
4
  return `<!doctype html>
5
5
  <html lang="en">
6
6
  <head>
7
7
  <meta charset="utf-8">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=1">
9
- <title>AgentFlight Replay ${escapeHtml(input.sessionId)}</title>
9
+ <title>AgentFlight Record ${escapeHtml(input.sessionId)}</title>
10
10
  <style>
11
11
  :root {
12
12
  color-scheme: light;
13
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
14
- --bg: oklch(0.982 0.006 248);
15
- --paper: oklch(0.996 0.003 248);
16
- --text: oklch(0.245 0.022 252);
17
- --muted: oklch(0.49 0.028 252);
18
- --faint: oklch(0.66 0.024 252);
19
- --line: oklch(0.865 0.014 252);
20
- --line-strong: oklch(0.74 0.026 252);
21
- --soft: oklch(0.955 0.008 248);
22
- --accent: oklch(0.47 0.105 252);
23
- --success: oklch(0.49 0.13 148);
24
- --success-bg: oklch(0.955 0.035 148);
25
- --danger: oklch(0.52 0.165 28);
26
- --danger-bg: oklch(0.955 0.032 28);
27
- --warning: oklch(0.57 0.115 78);
28
- --warning-bg: oklch(0.955 0.035 78);
29
- --mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
13
+ --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Consolas, "Liberation Mono", monospace;
15
+
16
+ --bg: oklch(0.984 0.004 255);
17
+ --paper: oklch(0.997 0.002 255);
18
+ --ink: oklch(0.265 0.018 258);
19
+ --chrome: oklch(0.515 0.022 258);
20
+ --faint: oklch(0.66 0.018 258);
21
+ --rule: oklch(0.905 0.008 258);
22
+ --rule-strong: oklch(0.81 0.014 258);
23
+ --soft: oklch(0.957 0.006 258);
24
+ --accent: oklch(0.5 0.092 262);
25
+
26
+ --ok: oklch(0.52 0.085 152);
27
+ --ok-bg: oklch(0.965 0.022 152);
28
+ --warn: oklch(0.52 0.092 74);
29
+ --warn-bg: oklch(0.964 0.034 78);
30
+ --danger: oklch(0.515 0.158 27);
31
+ --danger-bg: oklch(0.962 0.026 27);
30
32
  }
31
33
 
32
34
  * { box-sizing: border-box; }
33
- body { margin: 0; background: var(--bg); color: var(--text); }
34
- main { max-width: 1120px; margin: 0 auto; padding: 40px 28px 48px; }
35
- header { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; align-items: start; border-bottom: 1px solid var(--line); padding-bottom: 28px; margin-bottom: 24px; }
36
- h1 { font-size: 30px; line-height: 1.15; margin: 8px 0 12px; letter-spacing: 0; max-width: 26ch; }
37
- h2 { font-size: 17px; line-height: 1.25; margin: 0 0 14px; letter-spacing: 0; }
38
- p { max-width: 72ch; }
39
- .eyebrow { color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
40
- .meta { color: var(--muted); font-size: 13px; line-height: 1.45; }
41
- .header-status { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; max-width: 360px; }
42
- .badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
43
- .badge { border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; background: var(--paper); color: var(--text); font-size: 12px; font-weight: 650; line-height: 1.35; }
44
- .badge.risk-high, .badge.status-failed { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
45
- .badge.risk-medium { background: var(--warning-bg); border-color: var(--warning); color: oklch(0.39 0.095 78); }
46
- .badge.risk-low, .badge.status-passed, .badge.ready { background: var(--success-bg); border-color: var(--success); color: var(--success); }
47
- .badge.unknown { background: var(--soft); color: var(--muted); }
48
- .summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--paper); margin: 0 0 28px; }
49
- .summary-card { min-width: 0; padding: 15px 16px; border-right: 1px solid var(--line); }
50
- .summary-card:last-child { border-right: 0; }
51
- .summary-card span { color: var(--muted); display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; }
52
- .summary-card strong { display: block; font-size: 17px; line-height: 1.25; margin-top: 5px; overflow-wrap: anywhere; }
53
- .section { border-top: 1px solid var(--line); padding: 26px 0 4px; margin-top: 24px; }
54
- .section-header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 12px; }
55
- .section-header .meta { flex: 0 0 auto; }
56
- .timeline { display: grid; gap: 8px; }
57
- .timeline-item { display: grid; grid-template-columns: minmax(145px, 0.34fr) 14px minmax(0, 1fr); gap: 12px; align-items: start; padding: 10px 0; border-bottom: 1px solid var(--line); }
58
- .timeline-item:last-child { border-bottom: 0; }
59
- .timeline-time { color: var(--muted); font-family: var(--mono); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
60
- .timeline-dot { width: 9px; height: 9px; margin-top: 5px; border-radius: 999px; background: var(--line-strong); }
61
- .timeline-body strong { display: block; font-size: 14px; line-height: 1.35; }
62
- .timeline-type { color: var(--muted); font-family: var(--mono); font-size: 12px; margin-top: 2px; }
63
- .timeline-message { color: var(--text); margin-top: 6px; }
64
- .file-groups { display: grid; gap: 8px; }
65
- .file-group { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 14px; padding: 10px 0; border-bottom: 1px solid var(--line); }
66
- .file-group:last-child { border-bottom: 0; }
67
- .file-category { color: var(--text); font-size: 13px; font-weight: 700; }
68
- .file-list-inline { display: flex; flex-wrap: wrap; gap: 6px; }
69
- .changed-files { columns: 2; column-gap: 28px; margin: 0; padding: 0; list-style: none; }
70
- .changed-files li { break-inside: avoid; margin: 0 0 7px; }
71
- .verification-grid { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--paper); }
72
- .verification-card { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 14px; align-items: start; padding: 13px 16px; border-bottom: 1px solid var(--line); }
73
- .verification-card:last-child { border-bottom: 0; }
74
- .verification-status { align-self: start; justify-self: start; }
75
- .verification-command { min-width: 0; }
76
- details { margin-top: 8px; }
77
- summary { color: var(--muted); cursor: pointer; font-size: 12px; font-weight: 650; }
78
- .verification-paths { color: var(--muted); font-size: 12px; line-height: 1.55; margin-top: 6px; overflow-wrap: anywhere; }
79
- .recommendation { background: var(--paper); border: 1px solid var(--line); border-radius: 8px; padding: 16px 18px; }
80
- footer { color: var(--muted); font-size: 12px; margin-top: 30px; border-top: 1px solid var(--line); padding-top: 16px; }
81
- ul { margin: 0; padding-left: 22px; }
82
- li { margin: 6px 0; }
83
- code { background: var(--soft); border: 1px solid var(--line); border-radius: 5px; color: var(--text); font-family: var(--mono); font-size: 0.93em; padding: 2px 5px; overflow-wrap: anywhere; }
84
- @media (max-width: 860px) {
85
- main { padding: 28px 18px 40px; }
86
- header { grid-template-columns: 1fr; }
87
- .header-status { justify-content: flex-start; }
88
- .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
89
- .summary-card:nth-child(2) { border-right: 0; }
90
- .summary-card:nth-child(-n + 2) { border-bottom: 1px solid var(--line); }
91
- .timeline-item, .file-group, .verification-card { grid-template-columns: 1fr; gap: 6px; }
92
- .timeline-dot { display: none; }
93
- .changed-files { columns: 1; }
35
+ html { -webkit-text-size-adjust: 100%; }
36
+ body {
37
+ margin: 0;
38
+ background: var(--bg);
39
+ color: var(--ink);
40
+ font-family: var(--sans);
41
+ font-size: 15px;
42
+ line-height: 1.6;
43
+ -webkit-font-smoothing: antialiased;
44
+ text-rendering: optimizeLegibility;
45
+ }
46
+ main { max-width: 1040px; margin: 0 auto; padding: 56px 32px 64px; }
47
+ p { max-width: 70ch; margin: 0; }
48
+ a { color: var(--accent); }
49
+
50
+ /* technical chrome: mono labels, section markers, metadata */
51
+ .label {
52
+ font-family: var(--mono);
53
+ font-size: 11px;
54
+ font-weight: 600;
55
+ letter-spacing: 0.13em;
56
+ text-transform: uppercase;
57
+ color: var(--chrome);
58
+ }
59
+ .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
60
+
61
+ /* masthead */
62
+ .masthead {
63
+ display: grid;
64
+ grid-template-columns: minmax(0, 1fr) auto;
65
+ gap: 40px;
66
+ align-items: end;
67
+ padding-bottom: 26px;
68
+ border-bottom: 2px solid var(--ink);
69
+ }
70
+ .masthead-id .label { display: block; margin-bottom: 14px; }
71
+ h1 {
72
+ font-size: clamp(26px, 3.2vw, 33px);
73
+ line-height: 1.12;
74
+ letter-spacing: -0.014em;
75
+ font-weight: 640;
76
+ margin: 0 0 14px;
77
+ max-width: 24ch;
78
+ text-wrap: balance;
79
+ }
80
+ .ident {
81
+ font-family: var(--mono);
82
+ font-size: 12.5px;
83
+ color: var(--chrome);
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ gap: 4px 14px;
87
+ }
88
+
89
+ /* verdict: the single most important signal, calm unless attention is due */
90
+ .verdict { text-align: right; max-width: 340px; }
91
+ .verdict .label { display: block; margin-bottom: 8px; }
92
+ .verdict-line { display: inline-flex; align-items: center; gap: 9px; font-size: 17px; font-weight: 620; letter-spacing: -0.01em; }
93
+ .verdict-mark { width: 9px; height: 9px; border-radius: 2px; flex: none; background: var(--rule-strong); }
94
+ .verdict--ok .verdict-mark { background: var(--ok); }
95
+ .verdict--attention { color: var(--warn); }
96
+ .verdict--attention .verdict-mark { background: var(--warn); }
97
+ .verdict--blocked { color: var(--danger); }
98
+ .verdict--blocked .verdict-mark { background: var(--danger); }
99
+ .flags { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; margin-top: 12px; }
100
+ .flag {
101
+ font-family: var(--mono);
102
+ font-size: 11.5px;
103
+ letter-spacing: 0.02em;
104
+ padding: 3px 8px;
105
+ border: 1px solid var(--rule-strong);
106
+ border-radius: 3px;
107
+ color: var(--chrome);
108
+ background: var(--paper);
109
+ }
110
+ .flag.tone-high { border-color: var(--danger); color: var(--danger); background: var(--danger-bg); }
111
+ .flag.tone-medium { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
112
+
113
+ /* readout band: instrument readings, not metric cards */
114
+ .summary-grid {
115
+ display: flex;
116
+ flex-wrap: wrap;
117
+ gap: 0;
118
+ margin: 0 0 8px;
119
+ border-bottom: 1px solid var(--rule);
120
+ }
121
+ .reading {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 5px;
125
+ padding: 18px 30px 18px 0;
126
+ margin-right: 30px;
127
+ border-right: 1px solid var(--rule);
128
+ min-width: 0;
129
+ }
130
+ .reading:last-child { border-right: 0; margin-right: 0; }
131
+ .reading-value { font-size: 19px; font-weight: 600; letter-spacing: -0.01em; overflow-wrap: anywhere; }
132
+ .reading-value.tone-high { color: var(--danger); }
133
+ .reading-value.tone-medium { color: var(--warn); }
134
+
135
+ /* sections */
136
+ .section { padding-top: 38px; }
137
+ .section-head {
138
+ display: flex;
139
+ align-items: baseline;
140
+ justify-content: space-between;
141
+ gap: 16px;
142
+ padding-bottom: 12px;
143
+ margin-bottom: 18px;
144
+ border-bottom: 1px solid var(--rule);
145
+ }
146
+ .section-head h2 { all: unset; }
147
+ .section-head .label { display: block; }
148
+ .count { font-family: var(--mono); font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
149
+ .empty { color: var(--faint); font-size: 14px; }
150
+
151
+ /* timeline as a log spine */
152
+ .timeline { display: grid; }
153
+ .timeline-item {
154
+ display: grid;
155
+ grid-template-columns: minmax(132px, 0.28fr) 18px minmax(0, 1fr);
156
+ gap: 16px;
157
+ padding: 4px 0 18px;
158
+ }
159
+ .timeline-time { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--faint); line-height: 1.7; overflow-wrap: anywhere; }
160
+ .timeline-rail { position: relative; }
161
+ .timeline-rail::before {
162
+ content: "";
163
+ position: absolute;
164
+ left: 50%;
165
+ top: 0;
166
+ bottom: 0;
167
+ width: 1px;
168
+ transform: translateX(-50%);
169
+ background: var(--rule-strong);
170
+ }
171
+ .timeline-item:first-child .timeline-rail::before { top: 11px; }
172
+ .timeline-item:last-child .timeline-rail::before { bottom: auto; height: 11px; }
173
+ .timeline-node {
174
+ position: absolute;
175
+ left: 50%;
176
+ top: 11px;
177
+ width: 9px;
178
+ height: 9px;
179
+ border-radius: 999px;
180
+ transform: translate(-50%, -50%);
181
+ background: var(--paper);
182
+ border: 1.5px solid var(--rule-strong);
183
+ }
184
+ .node--start { background: var(--ink); border-color: var(--ink); }
185
+ .node--ok { background: var(--ok); border-color: var(--ok); }
186
+ .node--fail { background: var(--danger-bg); border-color: var(--danger); border-width: 2px; }
187
+ .node--snapshot { background: var(--paper); border-color: var(--accent); }
188
+ .node--report { background: var(--accent); border-color: var(--accent); }
189
+ .timeline-title { font-size: 14.5px; font-weight: 600; line-height: 1.4; }
190
+ .timeline-type { font-family: var(--mono); font-size: 11.5px; color: var(--faint); margin-top: 3px; letter-spacing: 0.01em; }
191
+ .timeline-message { margin-top: 7px; font-size: 14px; color: var(--ink); }
192
+
193
+ /* records: review focus + file groups */
194
+ .records { display: grid; }
195
+ .record {
196
+ display: grid;
197
+ grid-template-columns: 168px minmax(0, 1fr);
198
+ gap: 18px;
199
+ padding: 14px 0;
200
+ border-top: 1px solid var(--rule);
201
+ }
202
+ .record:first-child { border-top: 0; }
203
+ .record-key { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
204
+ .record-rank { font-family: var(--mono); font-size: 12px; color: var(--faint); }
205
+ .record-cat { font-size: 13.5px; font-weight: 650; }
206
+ .record-body { min-width: 0; display: flex; flex-direction: column; gap: 6px; }
207
+ .reason { color: var(--chrome); font-size: 13.5px; line-height: 1.55; }
208
+ .reason-strong { color: var(--ink); }
209
+ .tags { display: flex; flex-wrap: wrap; gap: 6px; }
210
+
211
+ /* verification evidence ledger */
212
+ .ledger { display: grid; }
213
+ .entry {
214
+ display: grid;
215
+ grid-template-columns: 78px minmax(0, 1fr);
216
+ gap: 18px;
217
+ align-items: start;
218
+ padding: 15px 0;
219
+ border-top: 1px solid var(--rule);
220
+ }
221
+ .entry:first-child { border-top: 0; }
222
+ .stamp {
223
+ font-family: var(--mono);
224
+ font-size: 12px;
225
+ font-weight: 700;
226
+ letter-spacing: 0.08em;
227
+ padding-top: 1px;
228
+ }
229
+ .stamp--passed { color: var(--ok); }
230
+ .stamp--failed { color: var(--danger); }
231
+ .entry-body { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
232
+ .entry-cmd { font-family: var(--mono); font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }
233
+ .entry-meta { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--faint); }
234
+ details { font-size: 12.5px; }
235
+ summary { color: var(--chrome); cursor: pointer; font-family: var(--mono); font-size: 12px; letter-spacing: 0.02em; width: max-content; }
236
+ summary:hover { color: var(--ink); }
237
+ .paths { font-family: var(--mono); font-size: 12px; color: var(--faint); line-height: 1.7; margin-top: 7px; overflow-wrap: anywhere; }
238
+
239
+ /* changed file list */
240
+ .files { columns: 2; column-gap: 36px; margin: 0; padding: 0; list-style: none; }
241
+ .files li { break-inside: avoid; margin: 0 0 8px; }
242
+
243
+ /* gaps + recommendation callout */
244
+ .gaps { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
245
+ .gaps li { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 14px; align-items: baseline; font-size: 14px; }
246
+ .gap-sev { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--chrome); }
247
+ .gap-sev.sev-blocking { color: var(--danger); }
248
+ .gap-sev.sev-warning { color: var(--warn); }
249
+
250
+ .callout {
251
+ margin-top: 22px;
252
+ padding: 18px 20px;
253
+ background: var(--soft);
254
+ border: 1px solid var(--rule);
255
+ border-radius: 6px;
256
+ }
257
+ .callout-state { font-weight: 650; font-size: 15px; }
258
+ .callout-reason { color: var(--chrome); font-size: 14px; margin-top: 4px; line-height: 1.55; }
259
+ .callout-next { margin-top: 10px; font-family: var(--mono); font-size: 13px; }
260
+ .callout-next .label { display: inline; margin-right: 8px; }
261
+
262
+ code {
263
+ font-family: var(--mono);
264
+ font-size: 0.92em;
265
+ background: var(--soft);
266
+ border: 1px solid var(--rule);
267
+ border-radius: 4px;
268
+ padding: 1px 5px;
269
+ overflow-wrap: anywhere;
270
+ }
271
+
272
+ footer {
273
+ margin-top: 52px;
274
+ padding-top: 18px;
275
+ border-top: 1px solid var(--rule);
276
+ font-family: var(--mono);
277
+ font-size: 11.5px;
278
+ letter-spacing: 0.04em;
279
+ color: var(--faint);
280
+ display: flex;
281
+ justify-content: space-between;
282
+ gap: 16px;
283
+ flex-wrap: wrap;
284
+ }
285
+
286
+ @media (max-width: 820px) {
287
+ main { padding: 36px 20px 48px; }
288
+ .masthead { grid-template-columns: 1fr; gap: 24px; align-items: start; }
289
+ .verdict { text-align: left; max-width: none; }
290
+ .verdict .flags { justify-content: flex-start; }
291
+ .reading { padding-right: 22px; margin-right: 22px; }
292
+ .timeline-item, .record, .entry { grid-template-columns: 1fr; gap: 6px; }
293
+ .timeline-rail { display: none; }
294
+ .files { columns: 1; }
295
+ .gaps li { grid-template-columns: 1fr; gap: 2px; }
94
296
  }
95
297
  </style>
96
298
  </head>
97
299
  <body>
98
300
  <main>
99
- <header>
100
- <div>
101
- <div class="eyebrow">AgentFlight Replay</div>
301
+ <header class="masthead">
302
+ <div class="masthead-id">
303
+ <span class="label">AgentFlight &middot; Flight Record</span>
102
304
  <h1>${escapeHtml(input.task)}</h1>
103
- <div class="meta">Session ${escapeHtml(input.sessionId)} &middot; Started ${escapeHtml(input.startedAt)}</div>
104
- <div class="badges">${input.riskBadges.map((badge) => renderBadge(badge, "risk")).join("")}</div>
305
+ <div class="ident">
306
+ <span>${escapeHtml(input.sessionId)}</span>
307
+ <span>started ${escapeHtml(input.startedAt)}</span>
308
+ </div>
105
309
  </div>
106
- <div class="header-status">
107
- ${renderBadge(readiness, "readiness")}
108
- ${renderBadge(risk, "risk")}
310
+ <div class="verdict verdict--${verdict.tone}">
311
+ <span class="label">Review readiness</span>
312
+ <span class="verdict-line"><span class="verdict-mark" aria-hidden="true"></span>${escapeHtml(readiness)}</span>
313
+ <div class="flags">${input.riskBadges.map((badge) => renderFlag(badge)).join("")}</div>
109
314
  </div>
110
315
  </header>
111
316
 
@@ -114,42 +319,34 @@ export function renderHtmlReplay(input) {
114
319
  ${renderReview(input.review)}
115
320
 
116
321
  <section class="section">
117
- <div class="section-header">
118
- <h2>Timeline</h2>
119
- <div class="meta">${escapeHtml(String(input.timeline.length))} events</div>
120
- </div>
322
+ <div class="section-head"><h2 class="label">Timeline</h2><span class="count">${escapeHtml(String(input.timeline.length))} events</span></div>
121
323
  ${renderTimeline(input.timeline)}
122
324
  </section>
123
325
 
124
326
  <section class="section">
125
- <div class="section-header">
126
- <h2>Changed File Groups</h2>
127
- <div class="meta">${escapeHtml(String(input.changedFileGroups?.length ?? 0))} groups</div>
128
- </div>
327
+ <div class="section-head"><h2 class="label">Changed File Groups</h2><span class="count">${escapeHtml(String(input.changedFileGroups?.length ?? 0))} groups</span></div>
129
328
  ${renderFileGroups(input.changedFileGroups ?? [])}
130
- <div class="section-header" style="margin-top: 22px;">
131
- <h2>Changed Files</h2>
132
- <div class="meta">${escapeHtml(String(input.changedFiles.length))} files</div>
133
- </div>
329
+ </section>
330
+
331
+ <section class="section">
332
+ <div class="section-head"><h2 class="label">Changed Files</h2><span class="count">${escapeHtml(String(input.changedFiles.length))} files</span></div>
134
333
  ${renderFileList(input.changedFiles)}
135
334
  </section>
136
335
 
137
336
  <section class="section">
138
- <div class="section-header">
139
- <h2>Verification Evidence</h2>
140
- <div class="meta">${escapeHtml(String(input.verificationEvidence.length))} runs</div>
141
- </div>
337
+ <div class="section-head"><h2 class="label">Verification Evidence</h2><span class="count">${escapeHtml(String(input.verificationEvidence.length))} runs</span></div>
142
338
  ${renderVerification(input.verificationEvidence)}
143
339
  </section>
144
340
 
145
341
  <section class="section">
146
- <div class="section-header">
147
- <h2>Recommendation</h2>
148
- </div>
149
- <div class="recommendation">${escapeHtml(input.recommendation)}</div>
342
+ <div class="section-head"><h2 class="label">Recommendation</h2></div>
343
+ <p>${escapeHtml(input.recommendation)}</p>
150
344
  </section>
151
345
 
152
- <footer>Generated by AgentFlight</footer>
346
+ <footer>
347
+ <span>Generated by AgentFlight</span>
348
+ <span>${escapeHtml(input.sessionId)}</span>
349
+ </footer>
153
350
  </main>
154
351
  </body>
155
352
  </html>
@@ -161,24 +358,25 @@ function renderSummary(input) {
161
358
  const risk = input.riskBadges[0] ?? "unknown";
162
359
  const readiness = input.reviewReadiness ?? "Unknown";
163
360
  return `<section class="summary-grid" aria-label="Session summary">
164
- ${renderSummaryCard("Risk", risk)}
165
- ${renderSummaryCard("Changed Files", String(input.changedFiles.length))}
166
- ${renderSummaryCard("Proof", `${passed} passed / ${failed} failed`)}
167
- ${renderSummaryCard("Readiness", readiness)}
361
+ ${renderReading("Risk", risk, riskTone(risk))}
362
+ ${renderReading("Changed Files", String(input.changedFiles.length))}
363
+ ${renderReading("Proof", `${passed} passed / ${failed} failed`)}
364
+ ${renderReading("Readiness", readiness)}
168
365
  </section>`;
169
366
  }
170
- function renderSummaryCard(label, value) {
171
- return `<div class="summary-card"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
367
+ function renderReading(label, value, tone = "") {
368
+ const toneClass = tone ? ` tone-${tone}` : "";
369
+ return `<div class="reading"><span class="label">${escapeHtml(label)}</span><span class="reading-value${toneClass}">${escapeHtml(value)}</span></div>`;
172
370
  }
173
371
  function renderTimeline(items) {
174
372
  if (items.length === 0)
175
- return "<p>No timeline events recorded.</p>";
373
+ return `<p class="empty">No timeline events recorded.</p>`;
176
374
  return `<div class="timeline">${items
177
375
  .map((item) => `<div class="timeline-item">
178
376
  <div class="timeline-time">${escapeHtml(item.timestamp)}</div>
179
- <div class="timeline-dot" aria-hidden="true"></div>
377
+ <div class="timeline-rail"><span class="timeline-node ${nodeClass(item.type)}" aria-hidden="true"></span></div>
180
378
  <div class="timeline-body">
181
- <strong>${escapeHtml(item.title)}</strong>
379
+ <div class="timeline-title">${escapeHtml(item.title)}</div>
182
380
  <div class="timeline-type">${escapeHtml(item.type)}</div>
183
381
  ${item.message ? `<div class="timeline-message">${escapeHtml(item.message)}</div>` : ""}
184
382
  </div>
@@ -189,61 +387,55 @@ function renderReview(review) {
189
387
  if (!review)
190
388
  return "";
191
389
  return `<section class="section">
192
- <div class="section-header">
193
- <h2>Review Focus</h2>
194
- <div class="meta">${escapeHtml(String(review.focus.length))} files</div>
195
- </div>
390
+ <div class="section-head"><h2 class="label">Review Focus</h2><span class="count">${escapeHtml(String(review.focus.length))} files</span></div>
196
391
  ${renderReviewFocus(review.focus)}
197
- <div class="section-header" style="margin-top: 22px;">
198
- <h2>Proof Gaps</h2>
199
- <div class="meta">${escapeHtml(String(review.proofGaps.length))} gaps</div>
200
- </div>
392
+ <div class="section-head" style="margin-top: 32px;"><h2 class="label">Proof Gaps</h2><span class="count">${escapeHtml(String(review.proofGaps.length))} gaps</span></div>
201
393
  ${renderProofGaps(review.proofGaps)}
202
- <div class="recommendation" style="margin-top: 16px;">
203
- <strong>${escapeHtml(review.readiness.label)}</strong><br>
204
- ${escapeHtml(review.readiness.reason)}<br>
205
- Next: ${escapeHtml(review.readiness.nextAction)}
394
+ <div class="callout">
395
+ <div class="callout-state">${escapeHtml(review.readiness.label)}</div>
396
+ <div class="callout-reason">${escapeHtml(review.readiness.reason)}</div>
397
+ <div class="callout-next"><span class="label">Next</span>${escapeHtml(review.readiness.nextAction)}</div>
206
398
  </div>
207
399
  </section>`;
208
400
  }
209
401
  function renderReviewFocus(items) {
210
402
  if (items.length === 0)
211
- return "<p>No changed files to review.</p>";
212
- return `<div class="file-groups">${items
213
- .map((item) => `<div class="file-group"><div class="file-category">#${escapeHtml(String(item.rank))} ${escapeHtml(item.category)}</div><div><code>${escapeHtml(item.file)}</code><div class="meta">${escapeHtml(item.reasons.join("; "))}</div><div class="meta">${escapeHtml(item.suggestedReviewerFocus)}</div>${item.suggestedCommand ? `<div class="meta">Suggested proof: <code>${escapeHtml(item.suggestedCommand)}</code></div>` : ""}</div></div>`)
403
+ return `<p class="empty">No changed files to review.</p>`;
404
+ return `<div class="records">${items
405
+ .map((item) => `<div class="record"><div class="record-key"><span class="record-rank">#${escapeHtml(String(item.rank))}</span><span class="record-cat">${escapeHtml(item.category)}</span></div><div class="record-body"><code>${escapeHtml(item.file)}</code><div class="reason"><span class="reason-strong">Why:</span> ${escapeHtml(item.reasons.join("; "))}</div><div class="reason">${escapeHtml(item.suggestedReviewerFocus)}</div>${item.suggestedCommand ? `<div class="reason">Suggested proof: <code>${escapeHtml(item.suggestedCommand)}</code></div>` : ""}</div></div>`)
214
406
  .join("")}</div>`;
215
407
  }
216
408
  function renderProofGaps(gaps) {
217
409
  if (gaps.length === 0)
218
- return "<p>No proof gaps detected.</p>";
219
- return `<ul>${gaps
220
- .map((gap) => `<li><strong>${escapeHtml(gap.severity)}</strong>: ${escapeHtml(gap.message)}${gap.suggestedCommand ? ` <code>agentflight verify -- ${escapeHtml(gap.suggestedCommand)}</code>` : ""}</li>`)
410
+ return `<p class="empty">No proof gaps detected.</p>`;
411
+ return `<ul class="gaps">${gaps
412
+ .map((gap) => `<li><span class="gap-sev sev-${escapeHtml(gap.severity.toLowerCase())}">${escapeHtml(gap.severity)}</span><span>${escapeHtml(gap.message)}${gap.suggestedCommand ? ` <code>agentflight verify -- ${escapeHtml(gap.suggestedCommand)}</code>` : ""}</span></li>`)
221
413
  .join("")}</ul>`;
222
414
  }
223
415
  function renderFileGroups(groups) {
224
416
  if (groups.length === 0)
225
- return "<p>No changed file groups detected.</p>";
226
- return `<div class="file-groups">${groups
227
- .map((group) => `<div class="file-group"><div class="file-category">${escapeHtml(group.category)}</div><div class="file-list-inline">${group.files.map((file) => `<code>${escapeHtml(file)}</code>`).join("")}</div></div>`)
417
+ return `<p class="empty">No changed file groups detected.</p>`;
418
+ return `<div class="records">${groups
419
+ .map((group) => `<div class="record"><div class="record-key"><span class="record-cat">${escapeHtml(group.category)}</span></div><div class="record-body"><div class="tags">${group.files.map((file) => `<code>${escapeHtml(file)}</code>`).join("")}</div></div></div>`)
228
420
  .join("")}</div>`;
229
421
  }
230
422
  function renderFileList(files) {
231
423
  if (files.length === 0)
232
- return "<p>No changed files detected.</p>";
233
- return `<ul class="changed-files">${files.map((file) => `<li><code>${escapeHtml(file)}</code></li>`).join("")}</ul>`;
424
+ return `<p class="empty">No changed files detected.</p>`;
425
+ return `<ul class="files">${files.map((file) => `<li><code>${escapeHtml(file)}</code></li>`).join("")}</ul>`;
234
426
  }
235
427
  function renderVerification(evidence) {
236
428
  if (evidence.length === 0)
237
- return "<p>No verification evidence recorded.</p>";
238
- return `<div class="verification-grid">${evidence
239
- .map((item) => `<div class="verification-card ${escapeHtml(item.status)}">
240
- <div class="verification-status">${renderBadge(item.status, "status")}</div>
241
- <div class="verification-command">
242
- <code>${escapeHtml(item.command)}</code>
243
- <div class="meta">Exit ${escapeHtml(String(item.exitCode ?? "unknown"))} &middot; ${escapeHtml(String(item.durationMs))}ms</div>
429
+ return `<p class="empty">No verification evidence recorded.</p>`;
430
+ return `<div class="ledger">${evidence
431
+ .map((item) => `<div class="entry">
432
+ <div class="stamp stamp--${escapeHtml(item.status)}">${escapeHtml(stampText(item.status))}</div>
433
+ <div class="entry-body">
434
+ <div class="entry-cmd">${escapeHtml(item.command)}</div>
435
+ <div class="entry-meta">exit ${escapeHtml(String(item.exitCode ?? "unknown"))} &middot; ${escapeHtml(String(item.durationMs))}ms</div>
244
436
  <details>
245
437
  <summary>Evidence files</summary>
246
- <div class="verification-paths">
438
+ <div class="paths">
247
439
  <div>stdout: ${escapeHtml(item.stdoutPath)}</div>
248
440
  <div>stderr: ${escapeHtml(item.stderrPath)}</div>
249
441
  </div>
@@ -252,16 +444,48 @@ function renderVerification(evidence) {
252
444
  </div>`)
253
445
  .join("")}</div>`;
254
446
  }
255
- function renderBadge(value, kind) {
256
- const normalized = value.toLowerCase().replaceAll(/\s+/g, "-");
257
- const className = kind === "readiness"
258
- ? normalized.startsWith("ready")
259
- ? "ready"
260
- : "unknown"
261
- : kind === "status"
262
- ? `status-${normalized}`
263
- : `risk-${normalized}`;
264
- return `<span class="badge ${escapeHtml(className)}">${escapeHtml(value)}</span>`;
447
+ function classifyReadiness(value) {
448
+ const normalized = value.toLowerCase();
449
+ if (normalized.startsWith("ready"))
450
+ return { tone: "ok" };
451
+ if (normalized.includes("not ready") || normalized.includes("block"))
452
+ return { tone: "blocked" };
453
+ if (normalized.includes("needs") || normalized.includes("verif"))
454
+ return { tone: "attention" };
455
+ return { tone: "neutral" };
456
+ }
457
+ function riskTone(value) {
458
+ const normalized = value.toLowerCase();
459
+ if (normalized === "high")
460
+ return "high";
461
+ if (normalized === "medium")
462
+ return "medium";
463
+ return "";
464
+ }
465
+ function renderFlag(value) {
466
+ const tone = riskTone(value);
467
+ const toneClass = tone ? ` tone-${tone}` : "";
468
+ return `<span class="flag${toneClass}">${escapeHtml(value)}</span>`;
469
+ }
470
+ function nodeClass(type) {
471
+ if (type.includes("passed"))
472
+ return "node--ok";
473
+ if (type.includes("failed"))
474
+ return "node--fail";
475
+ if (type.includes("snapshot"))
476
+ return "node--snapshot";
477
+ if (type.includes("session_started"))
478
+ return "node--start";
479
+ if (type.includes("replay") || type.includes("report") || type.includes("resume"))
480
+ return "node--report";
481
+ return "node--default";
482
+ }
483
+ function stampText(status) {
484
+ if (status === "passed")
485
+ return "PASS";
486
+ if (status === "failed")
487
+ return "FAIL";
488
+ return status.toUpperCase();
265
489
  }
266
490
  function escapeHtml(value) {
267
491
  return value