toolroll 0.9.54 → 0.9.55

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 (199) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-polish.d.ts +5 -4
  19. package/dist/chat-polish.js +9 -10
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +26 -53
  22. package/dist/cli.d.ts +3 -1
  23. package/dist/cli.js +9 -3
  24. package/dist/coding-shipping-ui.d.ts +2 -1
  25. package/dist/coding-shipping-ui.js +6 -6
  26. package/dist/coding-ui.d.ts +3 -1
  27. package/dist/coding-ui.js +30 -29
  28. package/dist/contracts/cli.d.ts +1 -3
  29. package/dist/contracts/console-api.d.ts +1 -1
  30. package/dist/contracts/console-api.js +6 -5
  31. package/dist/contracts/health.d.ts +1 -3
  32. package/dist/control-ui.d.ts +5 -4
  33. package/dist/control-ui.js +9 -34
  34. package/dist/credentials-ui.d.ts +5 -4
  35. package/dist/credentials-ui.js +23 -24
  36. package/dist/demo-chat.d.ts +3 -3
  37. package/dist/demo-chat.js +29 -76
  38. package/dist/discord-settings.d.ts +3 -2
  39. package/dist/discord-settings.js +17 -42
  40. package/dist/envelope.js +10 -0
  41. package/dist/evidence-pack.d.ts +2 -1
  42. package/dist/evidence-pack.js +45 -49
  43. package/dist/export-ui.d.ts +7 -2
  44. package/dist/export-ui.js +4 -10
  45. package/dist/flow-gallery-ui.d.ts +4 -3
  46. package/dist/flow-gallery-ui.js +28 -43
  47. package/dist/flow-starters-ui.d.ts +2 -1
  48. package/dist/flow-starters-ui.js +12 -15
  49. package/dist/flow-triggers.d.ts +1 -0
  50. package/dist/flow-triggers.js +16 -14
  51. package/dist/flows-ui.d.ts +4 -3
  52. package/dist/flows-ui.js +19 -30
  53. package/dist/flows.js +2 -1
  54. package/dist/guides.js +108 -3
  55. package/dist/html.d.ts +76 -0
  56. package/dist/html.js +173 -0
  57. package/dist/integrations-ui.d.ts +2 -1
  58. package/dist/integrations-ui.js +19 -23
  59. package/dist/kits-ui.d.ts +5 -4
  60. package/dist/kits-ui.js +18 -20
  61. package/dist/knowledge-ui.d.ts +6 -5
  62. package/dist/knowledge-ui.js +26 -25
  63. package/dist/lead-context.d.ts +11 -1
  64. package/dist/lead-context.js +6 -8
  65. package/dist/ledger-view.d.ts +2 -1
  66. package/dist/ledger-view.js +27 -29
  67. package/dist/limits-ui.d.ts +2 -1
  68. package/dist/limits-ui.js +7 -7
  69. package/dist/live-bus.d.ts +50 -8
  70. package/dist/live-bus.js +133 -50
  71. package/dist/mcp-oauth.js +35 -36
  72. package/dist/models-ui.d.ts +2 -1
  73. package/dist/models-ui.js +25 -35
  74. package/dist/monitoring-ui.d.ts +4 -3
  75. package/dist/monitoring-ui.js +13 -26
  76. package/dist/openrouter-models.d.ts +2 -1
  77. package/dist/openrouter-models.js +5 -9
  78. package/dist/operate.js +11 -7
  79. package/dist/people-audit-ui.d.ts +2 -1
  80. package/dist/people-audit-ui.js +13 -13
  81. package/dist/policy-ui.d.ts +3 -2
  82. package/dist/policy-ui.js +17 -23
  83. package/dist/project-delete-ui.d.ts +4 -3
  84. package/dist/project-delete-ui.js +15 -27
  85. package/dist/pull-request-ui.d.ts +2 -1
  86. package/dist/pull-request-ui.js +17 -32
  87. package/dist/recipe-ui.d.ts +8 -7
  88. package/dist/recipe-ui.js +39 -32
  89. package/dist/release-adapters.d.ts +60 -0
  90. package/dist/release-adapters.js +435 -0
  91. package/dist/release-cli.d.ts +24 -0
  92. package/dist/release-cli.js +181 -0
  93. package/dist/release-gate.d.ts +80 -0
  94. package/dist/release-gate.js +150 -0
  95. package/dist/release-links.d.ts +29 -0
  96. package/dist/release-links.js +102 -0
  97. package/dist/release.d.ts +331 -0
  98. package/dist/release.js +550 -0
  99. package/dist/reply-shape.d.ts +2 -1
  100. package/dist/reply-shape.js +5 -5
  101. package/dist/repository-context-ui.d.ts +2 -1
  102. package/dist/repository-context-ui.js +2 -7
  103. package/dist/request-budget-ui.d.ts +2 -1
  104. package/dist/request-budget-ui.js +16 -16
  105. package/dist/result-review.d.ts +2 -3
  106. package/dist/result-review.js +3 -9
  107. package/dist/retention-ui.d.ts +7 -1
  108. package/dist/retention-ui.js +7 -11
  109. package/dist/serve.d.ts +11 -2
  110. package/dist/serve.js +69 -58
  111. package/dist/server/chat.d.ts +3 -6
  112. package/dist/server/chat.js +969 -1006
  113. package/dist/server/chrome.d.ts +291 -0
  114. package/dist/server/chrome.js +747 -0
  115. package/dist/server/console.css +2819 -0
  116. package/dist/server/flows.d.ts +2 -6
  117. package/dist/server/flows.js +840 -766
  118. package/dist/server/guards.d.ts +1 -1
  119. package/dist/server/guards.js +5 -2
  120. package/dist/server/handler-context.d.ts +1 -1
  121. package/dist/server/handler-context.js +1 -1
  122. package/dist/server/handler-registry.d.ts +8 -6
  123. package/dist/server/handler-registry.js +14 -2
  124. package/dist/server/http.d.ts +274 -0
  125. package/dist/server/http.js +239 -0
  126. package/dist/server/live.d.ts +34 -0
  127. package/dist/server/live.js +309 -0
  128. package/dist/server/pages.d.ts +2 -6
  129. package/dist/server/pages.js +808 -789
  130. package/dist/server/people-tokens.d.ts +1 -6
  131. package/dist/server/people-tokens.js +580 -572
  132. package/dist/server/remote.d.ts +4 -7
  133. package/dist/server/remote.js +37 -70
  134. package/dist/server/render-chat.d.ts +402 -0
  135. package/dist/server/render-chat.js +927 -0
  136. package/dist/server/render-pages.d.ts +535 -0
  137. package/dist/server/render-pages.js +1277 -0
  138. package/dist/server/render-people.d.ts +32 -0
  139. package/dist/server/render-people.js +121 -0
  140. package/dist/server/render-results.d.ts +744 -0
  141. package/dist/server/render-results.js +2237 -0
  142. package/dist/server/render-settings.d.ts +147 -0
  143. package/dist/server/render-settings.js +577 -0
  144. package/dist/server/render-tasks.d.ts +632 -0
  145. package/dist/server/render-tasks.js +2345 -0
  146. package/dist/server/request-context.d.ts +46 -0
  147. package/dist/server/request-context.js +17 -0
  148. package/dist/server/route-table.d.ts +25 -3
  149. package/dist/server/route-table.js +163 -75
  150. package/dist/server/runtime.d.ts +15 -7
  151. package/dist/server/runtime.js +7 -1
  152. package/dist/server/session.d.ts +138 -0
  153. package/dist/server/session.js +94 -0
  154. package/dist/server/settings.d.ts +1 -5
  155. package/dist/server/settings.js +2181 -2117
  156. package/dist/server/task-views.d.ts +11 -4
  157. package/dist/server/task-views.js +26 -22
  158. package/dist/server/tasks.d.ts +9 -9
  159. package/dist/server/tasks.js +1486 -1418
  160. package/dist/server-telemetry.d.ts +4 -3
  161. package/dist/server-telemetry.js +4 -8
  162. package/dist/skills-ui.d.ts +4 -3
  163. package/dist/skills-ui.js +14 -17
  164. package/dist/skills.d.ts +1 -1
  165. package/dist/skills.js +2 -1
  166. package/dist/slack-settings.d.ts +3 -2
  167. package/dist/slack-settings.js +17 -42
  168. package/dist/spend-ui.d.ts +2 -1
  169. package/dist/spend-ui.js +9 -32
  170. package/dist/sso-ui.d.ts +3 -2
  171. package/dist/sso-ui.js +26 -27
  172. package/dist/storage-ui.d.ts +2 -1
  173. package/dist/storage-ui.js +14 -33
  174. package/dist/surface.js +14 -3
  175. package/dist/task-composer.d.ts +2 -1
  176. package/dist/task-composer.js +7 -7
  177. package/dist/task-status.d.ts +4 -3
  178. package/dist/task-status.js +15 -16
  179. package/dist/team-http.d.ts +5 -9
  180. package/dist/team-http.js +5 -63
  181. package/dist/team-ui.d.ts +2 -1
  182. package/dist/team-ui.js +2 -5
  183. package/dist/teammates-ui.d.ts +5 -4
  184. package/dist/teammates-ui.js +61 -90
  185. package/dist/teams-settings.d.ts +3 -2
  186. package/dist/teams-settings.js +20 -31
  187. package/dist/telegram-settings.d.ts +3 -2
  188. package/dist/telegram-settings.js +14 -25
  189. package/dist/toolroll-update-ui.d.ts +3 -2
  190. package/dist/toolroll-update-ui.js +35 -39
  191. package/dist/tools-ui.d.ts +5 -2
  192. package/dist/tools-ui.js +31 -31
  193. package/dist/when-html.d.ts +11 -3
  194. package/dist/when-html.js +5 -6
  195. package/dist/workspace-ui.d.ts +4 -2
  196. package/dist/workspace-ui.js +10 -10
  197. package/package.json +1 -1
  198. package/dist/server/shared.d.ts +0 -3151
  199. package/dist/server/shared.js +0 -12001
@@ -0,0 +1,2237 @@
1
+ /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
+ import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from "../assignment-presentation.js";
3
+ import { assignmentActionHref, assignmentStatusOf, assignmentWithEvidence } from "../assignment-ui.js";
4
+ import { owedAcceptance } from "../assignment.js";
5
+ import {} from "../browser-workspace.js";
6
+ import { CHECK_LEVEL_WORDS, liveQuickCommand } from "../check-levels.js";
7
+ import { LIMITS } from "../decision.js";
8
+ import { readVerifiedArtifact, readVerifiedProofForRun, reportShotsOf } from "../evidence.js";
9
+ import { isInternalErrorReason, NEEDS, runReasonWords } from "../needs-you.js";
10
+ import {} from "../phase-routing.js";
11
+ import { projectName } from "../project.js";
12
+ import { coverageStateWords, coverageWords, dispatchStatusToken, GOAL_ASSESSMENT_PENDING, manualReviewCriterionOf, manualReviewOnly, passFraction, personCheckWords, plainReasonWords, reviewConflict, semanticCoverage } from "../proof.js";
13
+ import {} from "../pull-request-flow.js";
14
+ import { qualityModeTitle } from "../quality.js";
15
+ import { acceptWithChecksOf, resultActsOf } from "../result-acts.js";
16
+ import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
17
+ import { ADD_TESTS_ACTION, followUpChecksOf } from "../result-follow-ups.js";
18
+ import { evidenceHealthOf, evidenceProblemDetailsOf, evidenceResultStatusOf, isRevisionFeedback, RESULT_REVIEW_SCRIPT, RESULT_TABS, resultFactsAttributeMap, resultLeadOf, revisionBatchOf, revisionSourceOf } from "../result-review.js";
19
+ import { findingWords } from "../review-switch.js";
20
+ import {} from "../scope.js";
21
+ import { parseReport } from "../scout-report.js";
22
+ import {} from "../store.js";
23
+ import { runCostWords } from "../summary.js";
24
+ import { demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
25
+ import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
26
+ import { createHash, randomBytes } from "node:crypto";
27
+ import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
28
+ import { chatResultHref, reviewHref, taskChatHref, taskHref } from "./http.js";
29
+ import { attributes, html, htmlString, joinHtml, postForm } from "../html.js";
30
+ import { publicationFactsOf, statusActionHref, statusLineHtml } from "./render-tasks.js";
31
+ export const RUNS_PAGE = 50;
32
+ /**
33
+ * A vscode://file href, or null (arc 6, finding 2): the link exists only
34
+ * when every part is provably tame — an absolute, control-free worktree;
35
+ * a relative, single-line path whose segments contain no empty, dot,
36
+ * dot-dot, or backslash components (so lexical resolution stays below the
37
+ * worktree); a line inside the same 1..1,000,000 range the comment form
38
+ * enforces. Encoding failures return null — a file row degrades to plain
39
+ * text, never to a 500. The scheme is a constant, never data.
40
+ */
41
+ export function editorFileHref(worktree, path, line) {
42
+ if (!worktree.startsWith("/") || /[\u0000-\u001f\u007f]/.test(worktree))
43
+ return null;
44
+ if (path === "" || /[\u0000-\u001f\u007f]/.test(path) || path.startsWith("/") || path.includes("\\"))
45
+ return null;
46
+ const segments = path.split("/");
47
+ if (segments.some(segment => segment === "" || segment === "." || segment === ".."))
48
+ return null;
49
+ const rootSegments = worktree.replace(/\/+$/, "").split("/");
50
+ if (rootSegments.some(segment => segment === "." || segment === ".."))
51
+ return null;
52
+ try {
53
+ const root = rootSegments.map(segment => encodeURIComponent(segment)).join("/");
54
+ const file = segments.map(segment => encodeURIComponent(segment)).join("/");
55
+ const at = line !== undefined && line !== null && Number.isInteger(line) && line >= 1 && line <= 1_000_000 ? `:${line}` : "";
56
+ return `vscode://file${root}/${file}${at}`;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /** The execution profile in plain words (v24): what the password signs
63
+ * says WHAT RUNS — provider, exact model, permissions, and the real
64
+ * bounds — or says honestly that it cannot yet. */
65
+ /** The state badges the criterion-to-evidence matrix renders with — one
66
+ * shared vocabulary, so a "failed" row reads the same shade of trouble on
67
+ * the task page, the run page, the board, done, builds, the inbox, and
68
+ * chat (v39, extending Priority 2's "one surface, six places" rule from
69
+ * the verdict word to the per-criterion state). */
70
+ export function matrixStateBadge(state) {
71
+ // Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
72
+ const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
73
+ // The same words as the Checks tab and the card's Requirements row (task-status.ts).
74
+ return html `<span class="badge ${cls}" data-matrix-state="${state}">${requirementWordOf({ state })}</span>`;
75
+ }
76
+ /** v40: the bounded repair chain's own line — presented as the user-facing
77
+ * recovery it represents, without exposing the internal workflow name.
78
+ * Renders on the task page and the run page identically. */
79
+ export function repairChainHtml(chain) {
80
+ if (chain === null)
81
+ return html ``;
82
+ const basisWords = chain.basis === "mode" ? "approved automatically" : "ready for your approval";
83
+ const outcomeWords = chain.outcome === "drafted"
84
+ ? chain.draftTask === null ? "No fix could be prepared" : `Targeted fix ${basisWords}`
85
+ : chain.outcome === "resolved"
86
+ ? `Completed on attempt ${chain.attempt}`
87
+ : chain.outcome === "attempts-spent"
88
+ ? `Stopped after ${chain.attempt} attempt${chain.attempt === 1 ? "" : "s"}`
89
+ : chain.outcome === "no-progress"
90
+ ? "Stopped because two attempts made no progress"
91
+ : "Stopped because the evidence may conflict with the approved work";
92
+ const link = chain.draftTask === null ? "" : html ` <a href="${taskHref(chain.draftTask)}">${chain.draftTask}</a>`;
93
+ const details = chain.unresolved.length === 0
94
+ ? ""
95
+ : html `<details><summary>What it is fixing</summary><p class="meta">${chain.unresolved.join(", ")}</p></details>`;
96
+ return html `<div class="card repair-chain" data-repair-outcome="${chain.outcome}"><p class="row"><strong>Automatic recovery</strong> — ${outcomeWords}${link}</p>${details}</div>`;
97
+ }
98
+ /**
99
+ * The bounded review-retry panel (v50), shared by the task page and the
100
+ * result cockpit so both say the same thing: which attempt is running or
101
+ * queued, how the latest one ended, every root attempt on record, and the
102
+ * ONE explicit act — a Retry review form — rendered only while the store's
103
+ * allowance admits it and this session may ask (an approver's cookie
104
+ * session). Every other state shows the same control disabled, in words,
105
+ * so a person always sees why nothing more will happen by itself. Empty
106
+ * when no review was ever asked for.
107
+ */
108
+ export function reviewRetryPanel(_taskId, _sourceRun, retry, _options) {
109
+ if (retry === null || retry.state === "unrequested")
110
+ return html ``;
111
+ const attempts = retry.attempts.map(one => html `<li data-review-attempt="${one.attempt}" data-review-outcome="${one.outcome ?? "open"}"><a href="/r/${one.runId}">Run #${one.runId}</a> · ${one.outcome ?? "unfinished"}${one.reason === null ? "" : html ` · ${one.reason}`}</li>`);
112
+ return html `<details class="review-history" data-review-state="${retry.state}"><summary>Previous assessments</summary><p class="meta">Saved history; no separate review will be started.</p>${attempts.length === 0 ? "" : html `<ol aria-label="previous assessments">${attempts}</ol>`}</details>`;
113
+ }
114
+ /** A one-line summary of the matrix for list rows too dense for the full
115
+ * table (done, builds, board, inbox) — "2/3 criteria", plus a worst-state
116
+ * badge so trouble is visible without opening the row. `[]` renders
117
+ * nothing. */
118
+ export function criterionMatrixSummary(matrix) {
119
+ if (matrix.length === 0)
120
+ return html ``;
121
+ const { passed, total } = passFraction(matrix);
122
+ const worst = matrix.some(row => row.state === "missing" || row.state === "failed")
123
+ ? "failed"
124
+ : matrix.some(row => row.state === "manual-review")
125
+ ? "manual-review"
126
+ : "pass";
127
+ return worst === "pass"
128
+ ? html ` <span class="badge badge-done">${passed}/${total} criteria</span>`
129
+ : html `${matrixStateBadge(worst)} <span class="badge">${passed}/${total} criteria</span>`;
130
+ }
131
+ export const CHECK_LOG_CAPTURE = /^sh -c "(.+)" \(exit \d+(?:, timed out)?\)$/;
132
+ export function evidenceLinksFor(artifacts) {
133
+ const map = new Map();
134
+ for (const artifact of artifacts) {
135
+ if (artifact.kind === "screenshot") {
136
+ const path = SCREENSHOT_CAPTURE.exec(artifact.capture)?.[1];
137
+ if (path !== undefined)
138
+ map.set(`screenshot:${path}`, artifact.id);
139
+ }
140
+ else if (artifact.kind === "check-log") {
141
+ const command = CHECK_LOG_CAPTURE.exec(artifact.capture)?.[1];
142
+ if (command !== undefined)
143
+ map.set(`check:${command}`, artifact.id);
144
+ }
145
+ else if (artifact.kind === "terminal-diff") {
146
+ map.set("changed-path:*", artifact.id);
147
+ }
148
+ }
149
+ return map;
150
+ }
151
+ /** Read the approved requirement first; inspect its saved evidence on demand.
152
+ * Machine results, reviewer judgements and missing context remain separate.
153
+ * Warnings stay outside the disclosure, including on compact surfaces. */
154
+ export function criterionMatrixHtml(matrix, options = {}) {
155
+ if (matrix.length === 0)
156
+ return html ``;
157
+ const kinds = {
158
+ check: "Checks", "changed-path": "Changed files", screenshot: "Screenshots", "manual-review": "You check",
159
+ };
160
+ const evidenceLink = (a) => {
161
+ const text = html `<code>${a.ref}</code>`;
162
+ const anchor = a.kind === "changed-path" ? options.fileAnchors?.get(a.ref) : undefined;
163
+ if (anchor !== undefined)
164
+ return html `<a href="#${anchor}">${text}</a>`;
165
+ const artifactId = options.links?.get(`${a.kind}:${a.ref}`) ?? (a.kind === "changed-path" ? options.links?.get("changed-path:*") : undefined);
166
+ return artifactId !== undefined && options.runId !== undefined
167
+ ? html `<a href="/r/${options.runId}/evidence/${artifactId}">${text}</a>`
168
+ : text;
169
+ };
170
+ const rows = matrix.map((row, index) => {
171
+ const state = row.state;
172
+ const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
173
+ const confirmed = row.assessment !== undefined && state === "pass";
174
+ // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
175
+ // The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
176
+ const label = requirementWordOf(row, options.verdict);
177
+ const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
178
+ const warnings = [];
179
+ // Only replace the known boilerplate. Other recorded failure details
180
+ // remain verbatim, so concision cannot hide a different problem.
181
+ const detail = row.detail.filter(line => !(row.assessment && row.review && line === `${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`) && !(state === "manual-review" && /^criterion "[^"\n]+" requires manual-review evidence — an operator must accept it before this can verify$/.test(line)));
182
+ if (!awaitingAssessment && state !== "pass" && detail.length > 0)
183
+ warnings.push(html `<ul class="requirement-issues">${detail.map(line => html `<li>${line}</li>`)}</ul>`);
184
+ const review = row.review;
185
+ if (review !== null && review.judgement !== "upholds") {
186
+ warnings.push(html `<div class="requirement-warning" data-review-judgement="${review.judgement}"><strong>${review.judgement === "contradicts" ? "Reviewer found a problem" : "Reviewer could not confirm this"}</strong><p>${review.note}</p></div>`);
187
+ }
188
+ const coverage = row.coverage;
189
+ if (coverage !== undefined && (coverage.state === "gap" || coverage.gaps.length > 0)) {
190
+ warnings.push(html `<div class="requirement-warning" data-context-coverage="${coverage.state}"><strong>Review context is missing</strong>${coverage.gaps.length === 0 ? html `<p>The saved context cannot support this requirement.</p>` : html `<ul>${coverage.gaps.map(gap => html `<li>${gap}</li>`)}</ul>`}${coverage.priorSupport === "invalid" ? html `<p>The earlier review no longer supports this requirement.</p>` : ""}</div>`);
191
+ }
192
+ const answered = row.answered ?? [];
193
+ const groups = Object.entries(kinds).flatMap(([kind, name]) => {
194
+ const refs = answered.filter(one => one.kind === kind);
195
+ return refs.length === 0 ? [] : [html `<div class="requirement-evidence-group"><strong>${name} · ${refs.length}</strong><ul>${refs.map(ref => html `<li>${evidenceLink(ref)}</li>`)}</ul></div>`];
196
+ });
197
+ const reviewDetails = review === null ? "" : html `<div class="requirement-evidence-group"><strong>Previous assessment</strong><p>${review.judgement === "upholds" ? html `${review.note} ` : ""}<span class="meta">${review.author}</span></p></div>`;
198
+ return html `<li class="requirement" data-criterion-id="${row.id}">${[
199
+ html `<div class="requirement-heading"><span>Requirement ${index + 1}</span><span class="badge ${cls}" data-matrix-state="${state}">${label}</span></div>`,
200
+ html `<p class="requirement-statement">${row.statement}</p>`,
201
+ review?.judgement === "upholds" && !confirmed ? html `<p class="requirement-review" data-review-judgement="upholds">Reviewer confirmed</p>` : "",
202
+ warnings,
203
+ html `<details class="requirement-evidence"><summary>View evidence</summary><div class="requirement-evidence-body">`,
204
+ html `<p class="meta">Required: ${row.requiredEvidence.map(kind => kinds[kind]).join(", ") || "No evidence types specified"}</p>`,
205
+ answered.length === 0 ? html `<p class="meta">No evidence was submitted for this requirement.</p>` : groups,
206
+ state !== "pass" || detail.length === 0 ? "" : html `<div class="requirement-evidence-group"><strong>Verification notes</strong><ul>${detail.map(line => html `<li>${line}</li>`)}</ul></div>`,
207
+ reviewDetails,
208
+ coverage === undefined ? "" : html `<p class="meta"${coverage.state === "gap" ? "" : html ` data-context-coverage="${coverage.state}"`}>Review context: ${coverageStateWords(coverage)}</p>`,
209
+ coverage?.assets === undefined ? "" : html `<p class="meta" data-context-assets>${coverage.assets}</p>`,
210
+ html `<p class="meta">Requirement ID: <code>${row.id}</code></p></div></details></li>`,
211
+ ]}`;
212
+ });
213
+ return html `<div class="result-section criterion-matrix"><strong>Requirements · ${matrix.length}</strong><ol class="requirement-list">${rows}</ol></div>`;
214
+ }
215
+ /** One policy summary beneath the requirements. Per-requirement concerns
216
+ * are already visible in the matrix; do not repeat their full text here. */
217
+ export function semanticCoverageHtml(matrix, qualityMode) {
218
+ if (!matrix.some(row => row.review != null || row.assessment !== undefined))
219
+ return html ``;
220
+ const coverage = semanticCoverage(matrix, qualityMode);
221
+ return html `<details class="result-section semantic-coverage" data-semantic-coverage="${coverage.satisfied === true ? "satisfied" : coverage.satisfied === null ? "unsettled" : "unsatisfied"}" data-coverage-policy="${coverage.policy}"><summary>Previous assessment</summary><p class="meta">The saved assessment confirmed ${coverage.upheld.length} of ${coverage.total} requirements.</p></details>`;
222
+ }
223
+ /** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
224
+ /** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
225
+ export const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
226
+ export const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
227
+ /** A same-site page address with a refusal code added, before any fragment. */
228
+ export function withRefusal(href, code) {
229
+ const at = href.indexOf("#");
230
+ const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
231
+ return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
232
+ }
233
+ // ---- the review cockpit (Priority 5) ----------------------------------------
234
+ // The field parallelizes generation and lets review pile up (M8.19); this
235
+ // page compresses it. Read-only by design — ranked advice and deep links,
236
+ // no merge button, because the PR is the terminus and the person merges
237
+ // on GitHub.
238
+ /** The queue reads at most this many completed tasks — the store's own
239
+ * page ceiling, admission bound before it. */
240
+ export const REVIEW_QUEUE_CAP = 100;
241
+ /** The one return road the comment endpoint honors besides its own run
242
+ * page: the cockpit's selected-result link, exactly. */
243
+ export const REVIEW_RETURN = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}$/;
244
+ export const PRIORITY_LABELS = { 0: "needs action", 1: "review", 2: "no flags" };
245
+ export function reviewPriorityOf(row) {
246
+ if (row.assignment != null && (row.assignment.receipt?.runId ?? null) === row.runId) {
247
+ const presentation = assignmentPresentationOf(row.assignment);
248
+ const band = row.assignment.state === "needs-decision" ? 0 : row.assignment.state === "ready-to-check" ? 1 : 2;
249
+ return { band, label: PRIORITY_LABELS[band], reasons: band === 2 ? [] : [presentation.status.label] };
250
+ }
251
+ const reasons = [];
252
+ let band = 2;
253
+ const raise = (to, why) => {
254
+ if (to < band)
255
+ band = to;
256
+ reasons.push(why);
257
+ };
258
+ if (row.runId === null) {
259
+ raise(1, "no build record");
260
+ return { band, label: PRIORITY_LABELS[band], reasons };
261
+ }
262
+ const ids = (predicate) => row.proofMatrix.filter(predicate).map(one => one.id).join(", ");
263
+ const contradicted = ids(one => one.review?.judgement === "contradicts");
264
+ const broken = ids(one => one.state === "failed" || one.state === "missing");
265
+ const manual = ids(one => one.state === "manual-review");
266
+ const humanReview = manualReviewOnly({ verdict: row.proofVerdict ?? "", reasons: row.proofReasons ?? [], matrix: row.proofMatrix });
267
+ if (row.proofVerdict === "refuted") {
268
+ raise(row.proofAccepted ? 1 : 0, row.proofAccepted ? "conflicting evidence — accepted with exception" : "conflicting evidence");
269
+ }
270
+ else if (humanReview) {
271
+ raise(1, row.proofAccepted ? "accepted after human review" : "human review needed");
272
+ }
273
+ else if (row.proofVerdict === "short") {
274
+ raise(row.proofAccepted ? 1 : 0, row.proofAccepted ? "missing evidence — accepted with exception" : "missing evidence");
275
+ }
276
+ if (contradicted !== "")
277
+ raise(row.proofAccepted ? 1 : 0, `reviewer raised a concern with ${contradicted}`);
278
+ if (broken !== "")
279
+ raise(row.proofAccepted ? 1 : 0, `missing or failed evidence for ${broken}`);
280
+ if (row.ciFailing)
281
+ raise(0, "CI failing on its pull request — observed, not inferred");
282
+ if (row.publicationState === "failed")
283
+ raise(1, "publication failed — the branch never reached its remote");
284
+ if (manual !== "" && !row.proofAccepted && !humanReview)
285
+ raise(1, `manual review needed for ${manual}`);
286
+ if (row.proofVerdict === null && row.outcome !== "no-change")
287
+ raise(1, "no verification result");
288
+ return { band, label: PRIORITY_LABELS[band], reasons };
289
+ }
290
+ /** Ranked, stable: band first, then newest completion, then task id — the
291
+ * same input always yields the same queue, whoever loads it. */
292
+ export function rankReviewQueue(rows) {
293
+ return rows
294
+ .map(row => ({ ...row, priority: reviewPriorityOf(row) }))
295
+ .sort((a, b) => a.priority.band !== b.priority.band
296
+ ? a.priority.band - b.priority.band
297
+ : a.ciFailing !== b.ciFailing
298
+ ? Number(b.ciFailing) - Number(a.ciFailing)
299
+ : a.completedAt !== b.completedAt
300
+ ? b.completedAt.localeCompare(a.completedAt)
301
+ : a.taskId.localeCompare(b.taskId));
302
+ }
303
+ /** Whether a changed path sits inside the scope's signed "touches" — an
304
+ * exact file, a directory prefix (with or without its slash), or a plain
305
+ * `*` / `**` glob. Touches are advisory in the scope; here they only
306
+ * decide which files wear the "outside the signed touches" flag. */
307
+ export function withinSignedTouches(path, touches) {
308
+ return touches.some(raw => {
309
+ const touch = raw.trim().replace(/^\.\//, "");
310
+ if (touch === "")
311
+ return false;
312
+ if (touch.includes("*"))
313
+ return touchGlob(touch).test(path);
314
+ const dir = touch.endsWith("/") ? touch : `${touch}/`;
315
+ return path === touch || path.startsWith(dir);
316
+ });
317
+ }
318
+ /** A signed touch's glob as a regular expression, gitignore-style: `*`
319
+ * stays inside one segment; `**` followed by `/` spans zero or more
320
+ * directories, so `src/**` followed by `/*.ts` matches `src/a.ts` as well
321
+ * as `src/nested/a.ts` (v2 review, comment 2); a bare `**` spans anything.
322
+ * Whatever the glob names, its contents are inside it too. */
323
+ export function touchGlob(raw) {
324
+ // A trailing slash names a directory; the contents clause below covers it.
325
+ const touch = raw.replace(/\/+$/, "");
326
+ let pattern = "";
327
+ for (let at = 0; at < touch.length;) {
328
+ if (touch.startsWith("**/", at)) {
329
+ pattern += "(?:[^/]*/)*";
330
+ at += 3;
331
+ }
332
+ else if (touch.startsWith("**", at)) {
333
+ pattern += ".*";
334
+ at += 2;
335
+ }
336
+ else if (touch[at] === "*") {
337
+ pattern += "[^/]*";
338
+ at += 1;
339
+ }
340
+ else {
341
+ pattern += touch[at].replace(/[.+?^${}()|[\]\\]/g, "\\$&");
342
+ at += 1;
343
+ }
344
+ }
345
+ return new RegExp(`^${pattern}(?:/.*)?$`);
346
+ }
347
+ /** A stable, attribute-safe anchor for one file of the sealed patch —
348
+ * derived from the path's bytes, never from the path's characters, so a
349
+ * hostile file name can neither break the id nor escape the attribute. */
350
+ export function diffFileAnchor(path) {
351
+ return `diff-file-${createHash("sha256").update(path, "utf8").digest("hex").slice(0, 16)}`;
352
+ }
353
+ /** Paths whose churn a reviewer reads first — dependency manifests and
354
+ * locks, CI workflows, container and schema definitions, migrations, the
355
+ * environment files secrets live in, and files NAMED for credentials.
356
+ * The credential words match only as whole `-`/`_`/`.`-delimited pieces
357
+ * of the file name (v2 review, comment 4): `auth.ts`, `api-token.ts`,
358
+ * `secrets.json` count; `author.ts`, `tokenizer.ts`, and `.envelope.ts`
359
+ * are ordinary names and do not. `permission` is not a credential word
360
+ * at all — `permissions-ui.tsx` is a screen, not a secret. */
361
+ export const SENSITIVE_PATH = /(^|\/)(\.github|migrations?)\/|(^|\/)(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|Dockerfile|\.env(\.[^/]+)?|schema[^/]*\.(sql|prisma)|(?:[^/]*[-_.])?(auth|secrets?|tokens?|credentials?)(?:[-_.][^/]*)?)$/i;
362
+ export const LARGE_CHANGE_LINES = 200;
363
+ /** The changed-file list's own review priority: outside the signed
364
+ * touches first, then files a machine cannot diff or a criterion never
365
+ * cited, then the rest by churn. Presentation only — the sealed patch
366
+ * keeps its own order beneath, byte for byte. */
367
+ export function reviewFilePriority(file, proofCitesPaths) {
368
+ const reasons = [];
369
+ let band = 2;
370
+ const raise = (to, why) => {
371
+ if (to < band)
372
+ band = to;
373
+ reasons.push(why);
374
+ };
375
+ if (file.outsideTouches)
376
+ raise(0, "outside approved paths");
377
+ if (file.additions === null || file.deletions === null)
378
+ raise(1, "binary file — preview unavailable");
379
+ if (SENSITIVE_PATH.test(file.path))
380
+ raise(1, "dependencies, CI, schema, or credentials");
381
+ if (proofCitesPaths && !file.cited)
382
+ raise(1, "not referenced by a requirement");
383
+ if (file.additions !== null && file.deletions !== null && file.additions + file.deletions >= LARGE_CHANGE_LINES)
384
+ raise(1, "a large change");
385
+ if (file.anchor === null)
386
+ raise(1, "not in the recorded diff — see the full diff");
387
+ return { band, label: PRIORITY_LABELS[band], reasons };
388
+ }
389
+ export function orderChangedFiles(files, proofCitesPaths) {
390
+ const churn = (file) => (file.additions ?? 0) + (file.deletions ?? 0);
391
+ return files
392
+ .map(file => ({ ...file, priority: reviewFilePriority(file, proofCitesPaths) }))
393
+ .sort((a, b) => a.priority.band !== b.priority.band
394
+ ? a.priority.band - b.priority.band
395
+ : churn(a) !== churn(b)
396
+ ? churn(b) - churn(a)
397
+ : a.path.localeCompare(b.path));
398
+ }
399
+ /** The receipt's own proof-state word, so the cockpit and the task page
400
+ * never disagree on the state's name or its precedence: the stored
401
+ * verdict decides, whatever the run's outcome — a no-change run with a
402
+ * refuted proof reads as refuted, never as "no change needed". The one
403
+ * word the receipt never needs is "No build record": a manual completion
404
+ * has no receipt to share it with. */
405
+ /** The cockpit's headline status: the shared projection over the same
406
+ * verdict, acceptance, and publication rows the task page reads. */
407
+ export function cockpitStatusOf(view) {
408
+ // The receipt's own status (package 3): the same projection the chat
409
+ // receipt and the run page print, read from the shared detail.
410
+ if (view.run === null || view.detail === null)
411
+ return view.assignment === null ? resultHeadlineOf(resultStatusOf(null, null)) : assignmentPresentationOf(view.assignment).status;
412
+ const receiptStatus = receiptStatusOf(view.detail.receipt);
413
+ if (view.assignment !== null)
414
+ return assignmentStatusOf(assignmentWithEvidence(view.assignment, receiptStatus, view.run.id));
415
+ return resultHeadlineOf(receiptStatus);
416
+ }
417
+ /** Turn verifier records into one sentence a project owner can act on.
418
+ * A command-not-found result means the check could not start, not that the
419
+ * product itself failed. The stored record remains unchanged. */
420
+ export function verificationExplanation(verdict, reasons) {
421
+ const plain = reasons.map(reason => {
422
+ if (reason === "the approved verification command passed after the approved setup command ran"
423
+ || reason === "the approved verification command passed after the approved setup command restored project dependencies") {
424
+ return "Toolroll ran the approved setup automatically, then the project check passed.";
425
+ }
426
+ if (reason === "the approved verification command could not start because a required project executable was unavailable and no approved recovery was enabled"
427
+ || reason === "the approved verification command could not start because a project dependency was unavailable and no approved recovery was enabled") {
428
+ return "The project check couldn't start because a required project executable was missing. Automatic recovery wasn't enabled for this check.";
429
+ }
430
+ if (reason === "automatic recovery stopped because the approved setup changed after self-healing was authorized"
431
+ || reason === "automatic recovery stopped because its approved setup or project-check settings changed"
432
+ || reason === "automatic recovery stopped because the project setup or check changed") {
433
+ return "Automatic recovery stopped because the project setup or check changed. Review and reapprove automatic recovery before retrying.";
434
+ }
435
+ if (reason === "the approved setup command failed during automatic recovery"
436
+ || reason === "the approved setup command could not restore the project dependencies") {
437
+ return "The approved project setup failed, so automatic recovery stopped before retrying the project check.";
438
+ }
439
+ if (reason === "automatic recovery stopped because the setup command changed tracked files after the build") {
440
+ return "Automatic recovery stopped because project setup changed files after the build. Review those changes before retrying.";
441
+ }
442
+ if (reason === "automatic recovery stopped because tracked files no longer matched the built result") {
443
+ return "Automatic recovery stopped because files changed after the build was saved. Review those changes before retrying.";
444
+ }
445
+ if (reason === "automatic recovery stopped because the checkout moved away from the built commit") {
446
+ return "Automatic recovery stopped because Toolroll found a different project version than the one it built. Review the build log before retrying.";
447
+ }
448
+ if (reason === "automatic recovery stopped because Toolroll could not confirm that the built checkout was unchanged") {
449
+ return "Automatic recovery stopped because Toolroll couldn't confirm that no files changed after the build was saved. Review the build log, then try again.";
450
+ }
451
+ if (reason === "the required project executable was still unavailable after replaying the approved setup command"
452
+ || reason === "project dependencies were still unavailable after replaying the approved setup command") {
453
+ return "Automatic recovery ran once, but the required project executable was still missing.";
454
+ }
455
+ if (reason === "the retried verification command timed out after automatic recovery") {
456
+ return "Automatic recovery ran the approved setup, but the retried project check timed out.";
457
+ }
458
+ if (reason === "the retried verification command could not be started after automatic recovery") {
459
+ return "Automatic recovery ran the approved setup, but Toolroll still couldn't start the project check.";
460
+ }
461
+ if (reason === "automatic recovery stopped because this worker no longer owned the build") {
462
+ return "Automatic recovery stopped because this worker no longer owned the build. A current worker can retry safely.";
463
+ }
464
+ if (reason === "the approved verification command could not be run") {
465
+ return "Toolroll couldn't run the project check.";
466
+ }
467
+ const recoveredFailure = /^the repository's approved verification command exited (-?[0-9]+) after the approved setup command was replayed$/.exec(reason);
468
+ if (recoveredFailure !== null) {
469
+ return `Automatic recovery ran the approved setup, but the project check still failed (exit ${Number(recoveredFailure[1])}).`;
470
+ }
471
+ const exit = /^the repository's approved verification command exited (-?[0-9]+)$/.exec(reason);
472
+ if (exit !== null) {
473
+ const code = Number(exit[1]);
474
+ return code === 127
475
+ ? "Toolroll couldn't run the project check because a required command wasn't available."
476
+ : `The project check failed (exit ${code}).`;
477
+ }
478
+ if (reason === "the sealed diff is unavailable or truncated; the claimed changed paths cannot be verified against it") {
479
+ return "The recorded changes were incomplete, so they could not be verified.";
480
+ }
481
+ const plainWords = plainReasonWords(reason);
482
+ if (plainWords !== reason)
483
+ return plainWords;
484
+ const sentence = reason.trim();
485
+ return sentence === "" ? "" : `${sentence[0]?.toUpperCase() ?? ""}${sentence.slice(1)}${/[.!?]$/.test(sentence) ? "" : "."}`;
486
+ }).filter(Boolean);
487
+ if (plain.length > 0)
488
+ return [...new Set(plain)].join(" ");
489
+ if (verdict === "verified")
490
+ return "Toolroll independently verified this result.";
491
+ if (verdict === "attested")
492
+ return "The agent supplied evidence, but no independent project check was available.";
493
+ if (verdict === "short")
494
+ return "Some approved requirements still need evidence.";
495
+ if (verdict === "refuted")
496
+ return "Recorded evidence conflicts with this result.";
497
+ return "No verification result is available for this build.";
498
+ }
499
+ export function verificationRecovered(reasons) {
500
+ return reasons.some(reason => reason === "the approved verification command passed after the approved setup command ran"
501
+ || reason === "the approved verification command passed after the approved setup command restored project dependencies");
502
+ }
503
+ /** Whether a reviewer can annotate this result's diff here: the shared
504
+ * detail's own answer (a verified, non-empty sealed patch read by a
505
+ * session that holds a CSRF token) — decided once, in `resultDetailOf`. */
506
+ export function canAnnotateDiff(view, csrf) {
507
+ return view !== null && view.detail !== null && csrf !== "" && view.detail.canAnnotate;
508
+ }
509
+ export function reviewCockpitPage(chrome, data) {
510
+ const { queue, selected, csrf } = data;
511
+ const elevated = queue.filter(one => one.priority.band < 2).length;
512
+ const queueRows = queue.length === 0
513
+ ? html `<p class="meta">No results in this review list.</p>`
514
+ : html `<ol class="cockpit-queue-list">${joinHtml(queue
515
+ .map(row => {
516
+ const current = selected !== null && selected.taskId === row.taskId;
517
+ const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
518
+ const reasons = row.priority.reasons;
519
+ const why = status === null
520
+ ? reasons.length === 0 ? "" : html `<span class="cockpit-why">${reasons[0]}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
521
+ : html `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
522
+ return html `<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${reviewHref(row.taskId, row.runId)}"${current ? html ` aria-current="page"` : ""}>${[
523
+ html `<span class="cockpit-row-head"><strong>${row.title}</strong></span>`,
524
+ html `<span class="cockpit-row-meta">${whenTime(row.completedAt)}${row.runId === null ? " · no build" : row.outcome === "no-change" ? " · no change" : ""}${row.prNumber === null ? "" : ` · PR #${row.prNumber}`}</span>`,
525
+ row.historyProblem ? html `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why,
526
+ status !== null && row.ciFailing ? html `<span class="cockpit-why">CI is failing</span>` : "",
527
+ ]}</a></li>`;
528
+ }), "\n")}</ol>`;
529
+ const queuePane = html `<aside class="cockpit-queue" aria-label="review queue"><h2>Recent results <span class="lane-count">${queue.length}</span></h2>${elevated > 0 || queue.length >= data.queueCap ? html `<p class="meta cockpit-queue-hint">${elevated === 0 ? "" : `${elevated} ${elevated === 1 ? "needs" : "need"} your attention`}${queue.length >= data.queueCap ? `${elevated > 0 ? ". " : ""}Showing the newest ${data.queueCap}; older results still open from their task` : ""}</p>` : ""}${queueRows}</aside>`;
530
+ const missingNote = data.missing === null
531
+ ? ""
532
+ : html `<p class="problem">No completed task <span class="mono">${data.missing}</span> is in view here — it may not be finished, or it is outside this console's projects. ${selected === null ? "" : "Showing the top of the queue instead."}</p>`;
533
+ const beyondNote = !data.beyondQueue || selected === null
534
+ ? ""
535
+ : html `<p class="meta cockpit-beyond" data-cockpit-beyond="1">This result is not in the current review list.</p>`;
536
+ const detailParts = selected === null ? null : reviewCockpitDetailParts(selected, csrf, data.noted, data.canRetryReview, data.tab, data.user);
537
+ const detail = detailParts === null ? html `<section class="cockpit-detail"><p class="meta">Nothing to review yet.</p></section>` : detailParts.html;
538
+ // The rebuilt page (shadcn/ui): the selected result at full width, the
539
+ // list one tap away in the header.
540
+ const view = {
541
+ kind: "result",
542
+ results: queue.map(row => {
543
+ const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
544
+ const reasons = row.priority.reasons;
545
+ return {
546
+ title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
547
+ status: status === null ? null : { label: status.label, tone: status.tone },
548
+ notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
549
+ current: selected !== null && selected.taskId === row.taskId,
550
+ needsYou: row.priority.band < 2,
551
+ };
552
+ }),
553
+ attention: elevated,
554
+ capped: queue.length >= data.queueCap ? data.queueCap : null,
555
+ missing: data.missing === null ? null : `No completed task ${data.missing} is in view here — it may not be finished, or it is outside this console's projects.`,
556
+ beyond: data.beyondQueue && selected !== null,
557
+ selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
558
+ };
559
+ return screen("review", joinHtml([
560
+ selected === null ? html `<h1>Results</h1>` : "",
561
+ missingNote,
562
+ beyondNote,
563
+ data.refusal == null || selected === null ? "" : html `<p class="problem" data-result-refusal>${data.refusal}</p>`,
564
+ html `<div class="cockpit">${queuePane}${detail}</div>`,
565
+ ], "\n"), {
566
+ chrome,
567
+ workspace: { view },
568
+ functional: { script: reviewEvidenceScript() + (selected !== null && selected.detail !== null ? RESULT_REVIEW_SCRIPT : ""), fetches: false },
569
+ });
570
+ }
571
+ /** The selected result: intent → proof → changes → publication → acts,
572
+ * one scan path, every fact labeled by its source. */
573
+ export function reviewCockpitDetail(view, csrf, noted, canRetryReview = false, tab = "summary", user = "") {
574
+ return reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user).html;
575
+ }
576
+ /** The selected result's HTML and, for the rebuilt page, the same result
577
+ * as data: every form and panel body is the one the HTML carries. */
578
+ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user) {
579
+ const parts = [];
580
+ const run = view.run;
581
+ const proof = view.detail?.proof ?? null;
582
+ const accepted = proof?.accepted !== null && proof?.accepted !== undefined;
583
+ const canAnnotate = canAnnotateDiff(view, csrf);
584
+ const status = cockpitStatusOf(view);
585
+ // Header: what this is, its verdict word, and why it sits where it does.
586
+ parts.push(joinHtml([
587
+ html `<header class="cockpit-head" data-review-task="${view.taskId}">`,
588
+ html `<h1>${view.title}</h1>`,
589
+ html `<p class="meta">${run === null ? "No build" : `Build #${run.id}`} · <a href="${taskHref(view.taskId)}">Open task</a>${projectChip(view.repo)}</p>`,
590
+ html `<p class="cockpit-chips">${statusLineHtml(status)}</p>`,
591
+ html `</header>`,
592
+ ]));
593
+ if (view.detail === null && view.historyProblem !== null)
594
+ parts.push(html `<p class="problem" data-history-problem>${view.historyProblem} <a href="${taskHref(view.taskId)}">Open task</a></p>`);
595
+ // The primary next act — exactly one road, chosen from the state.
596
+ parts.push(reviewNextAction(view, csrf, accepted, canAnnotate));
597
+ // Approved intent: the signed scope's words, the plan's approach.
598
+ const intent = view.intent;
599
+ let intentView = null;
600
+ if (intent === null) {
601
+ parts.push(html `<section class="card cockpit-section" data-cockpit-section="intent"><h3>Approved scope</h3><p class="meta">No scope was filed for this task, so there is no approved goal or boundary to review.</p></section>`);
602
+ }
603
+ else {
604
+ // The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
605
+ const approvalWords = intent.approval.approved
606
+ ? `Approved by ${intent.approvedBy ?? "an operator"}`
607
+ : intent.approval.reason === "changed"
608
+ ? "The scope changed after approval. The words below are the current text, not the signed one."
609
+ : "Never approved. The result was built without a signed scope.";
610
+ const approval = intent.approval.approved
611
+ ? html `Approved by ${intent.approvedBy ?? "an operator"} · ${whenTime(intent.approval.at)}`
612
+ : approvalWords;
613
+ const body = joinHtml([
614
+ html `<p class="recap" style="margin-top:.25rem"><strong>Goal</strong> ${intent.goal}</p>`,
615
+ intent.outOfScope === null ? html `<p class="meta">No boundary was stated</p>` : html `<p class="recap"><strong>Not this</strong> ${intent.outOfScope}</p>`,
616
+ intent.touches.length === 0
617
+ ? html `<p class="meta">No expected paths were signed — every changed file reads as in bounds</p>`
618
+ : html `<p class="row"><span class="meta">Expected to touch</span> ${joinHtml(intent.touches.map(one => html `<span class="mono">${one}</span>`), " ")}</p>`,
619
+ view.plan === null
620
+ ? ""
621
+ : html `<p class="meta">Plan revision ${view.plan.revision} · <span class="mono" title="${view.plan.sha256}">${view.plan.sha256.slice(0, 12)}…</span>${view.plan.approach === null ? "" : ` — ${view.plan.approach}`}</p>`,
622
+ ]);
623
+ intentView = { approval: approvalWords, approvedAt: intent.approval.approved ? intent.approval.at ?? null : null, html: htmlString(body) };
624
+ parts.push(html `<details class="card cockpit-section cockpit-disclosure" data-cockpit-section="intent"><summary><h3>Approved scope<small>What this build was asked to do · ${approval}</small></h3><span class="cockpit-disclosure-action">View</span></summary><div class="cockpit-disclosure-body">${body}</div></details>`);
625
+ }
626
+ const selected = {
627
+ taskId: view.taskId, title: view.title, project: view.repo === null ? null : projectName(view.repo), build: run === null ? null : run.id,
628
+ taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
629
+ status: { label: status.label, tone: status.tone, token: status.token },
630
+ problem: view.detail === null ? view.historyProblem : null,
631
+ next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
632
+ notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
633
+ acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
634
+ // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
635
+ record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
636
+ { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
637
+ { label: "Worker", value: run.runner },
638
+ ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
639
+ ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
640
+ ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
641
+ ] },
642
+ };
643
+ if (run === null || view.detail === null) {
644
+ parts.push(html `<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>Verification</h3><p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p><p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
645
+ return { html: html `<section class="cockpit-detail">${joinHtml(parts, "\n")}</section>`, selected: { ...selected, noRun: "This task has no finished build record, so there are no captured changes or checks to review." } };
646
+ }
647
+ // The result itself (package 3): the same panel the run page and the
648
+ // chat's result view render — Summary / Changes / Checks with Request
649
+ // changes beside it. The cockpit adds its own acts under Checks: the
650
+ // v50 review-retry panel and the accept-with-exception form.
651
+ const extraChecks = reviewRetryPanel(view.taskId, run.id, view.reviewRetry, { csrf, canAct: false, returnTo: null });
652
+ const here = reviewHref(view.taskId);
653
+ const assignment = view.assignment === null ? null : assignmentWithEvidence(view.assignment, receiptStatusOf(view.detail.receipt), run.id);
654
+ const checks = assignment?.receipt?.checks;
655
+ if (checks !== undefined)
656
+ parts.push(html `<p class="${checks.status === "failed" || checks.status === "unavailable" ? "problem" : "meta"}" data-actual-checks="${checks.status}">${checks.detail}${checks.logArtifactId === null ? "" : html ` <a href="/r/${run.id}/evidence/${checks.logArtifactId}">Open check output</a>`}</p>`);
657
+ if (assignment?.completion != null)
658
+ parts.push(html `<p class="meta" data-result-completed>Marked complete by ${assignment.completion.actor.replace(/^operator:/, "")}.</p>`);
659
+ const panel = resultPanelParts(view.detail, {
660
+ place: "review",
661
+ tab,
662
+ csrf,
663
+ user,
664
+ noted,
665
+ requestToken: randomBytes(16).toString("hex"),
666
+ hrefFor: one => `${here}&run=${run.id}${one === "summary" ? "" : `&tab=${one}`}`,
667
+ returnTo: `${here}&run=${run.id}`,
668
+ back: null,
669
+ extraChecks,
670
+ headStatus: false,
671
+ action: false,
672
+ });
673
+ parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
674
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
675
+ const youCheck = panel.panel.youCheck;
676
+ // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
677
+ // report that doesn't match its changes asks for), the same request records it first.
678
+ const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
679
+ : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
680
+ const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
681
+ ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
682
+ if (complete !== null)
683
+ parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
684
+ // The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
685
+ const acceptsHere = complete !== null || panel.panel.need?.accept != null || youCheck?.accept != null;
686
+ const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
687
+ const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
688
+ const base = !acceptsHere ? null : acceptWordsOf({
689
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
690
+ unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
691
+ action: complete !== null ? "complete" : "accept",
692
+ publishing: view.detail?.publishing ?? "other",
693
+ proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
694
+ });
695
+ // Refuted: plain Accept and finish stays as allowed, in outline, and the one line before the acts (acts.line) says why.
696
+ const settled = base === null || blocked === null ? base : { ...base, label: "Accept and finish", ready: false };
697
+ const decision = settled === null ? null : { sentence: RESULT_DECISION_SENTENCE, ...settled, ...acceptWithChecksOf(settled, { unanswered, notRight: [] }),
698
+ base: { label: settled.label, ready: settled.ready, why: settled.why } };
699
+ // Run checks: no check ran on this result (or its saved one can't be read), the project has one, and an approver may run it.
700
+ const followUps = view.detail.followUps ?? null;
701
+ const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
702
+ const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
703
+ (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
704
+ // Back on this result's Checks tab, where #follow-ups shows the run it started.
705
+ ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
706
+ const need = panel.panel.need;
707
+ const nextKind = selected.next?.kind;
708
+ // A failed build: what went wrong is the card, Retry the act; a link to this same page goes nowhere, so it isn't one.
709
+ const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
710
+ link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
711
+ ...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
712
+ const actFacts = failure !== null
713
+ ? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
714
+ : {
715
+ accept: settled === null ? null : { ready: settled.ready },
716
+ runChecks: runChecks !== null,
717
+ checksRunning,
718
+ blocked,
719
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
720
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
721
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
722
+ unanswered: settled === null ? 0 : unanswered.length,
723
+ notRight: 0,
724
+ };
725
+ const acts = resultActsOf(actFacts);
726
+ // A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
727
+ // match the changes (not a failed check), that is the headline.
728
+ const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
729
+ const patch = view.detail?.terminal?.patch ?? null;
730
+ const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
731
+ // Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
732
+ const hunkStart = new Map();
733
+ const changes = new Map(files.map(file => {
734
+ const shown = file.hunks[0]?.lines.filter(line => line.newLine !== null && line.kind !== "meta") ?? [];
735
+ if (shown[0]?.newLine != null)
736
+ hunkStart.set(file.path, shown[0].newLine);
737
+ const added = shown.filter(line => line.kind === "addition");
738
+ const lines = (added.length > 0 ? added : shown).map(line => line.newLine);
739
+ return [file.path, lines.length === 0 ? null : { from: Math.min(...lines), to: Math.max(...lines) }];
740
+ }));
741
+ const changesHref = `${here}&run=${run.id}&tab=changes`;
742
+ const found = reportMismatchesOf(proof.reasons, proof.proofProblem === null ? proof.matrix : [], changes);
743
+ // The saved report itself can't be read: that is the evidence problem, said once.
744
+ if (proof.proofProblem !== null)
745
+ found.push({ text: `The saved report can't be read: ${proof.proofProblem}`, path: null, lines: null, inChanges: null, note: null, reason: proof.proofProblem });
746
+ if (found.length === 0)
747
+ return null;
748
+ return {
749
+ headline: evidenceProblemOf(proof.verdict, proof.reasons) === "mismatched" ? MISMATCH_HEADLINE : null,
750
+ rows: found.map(one => ({
751
+ text: one.text, path: one.path, absent: one.inChanges === false,
752
+ lines: one.lines === null ? null : one.lines.from === one.lines.to ? `line ${one.lines.from}` : `lines ${one.lines.from}–${one.lines.to}`,
753
+ href: one.path !== null && one.inChanges === true ? `${changesHref}#${diffFileAnchor(one.path)}${hunkStart.has(one.path) ? `-L${hunkStart.get(one.path)}` : ""}`
754
+ : one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${here}&run=${run.id}#report-note-${one.note}` : null,
755
+ noteLabel: one.path === null && one.note !== null ? `The report, note ${one.note}` : null,
756
+ })),
757
+ said: [...new Set([...proof.reasons, ...found.map(one => one.reason)].flatMap(reason => [reason, plainReasonWords(reason)]))],
758
+ };
759
+ })();
760
+ if (view.notes.length > 0) {
761
+ parts.push(html `<section class="card cockpit-section" data-cockpit-section="notes"><h3>Operator notes</h3>${joinHtml(view.notes.map(one => html `<p class="row"><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span> ${one.note}</p>`), "\n")}</section>`);
762
+ }
763
+ return {
764
+ html: html `<section class="cockpit-detail">${joinHtml(parts, "\n")}</section>`,
765
+ selected: {
766
+ ...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, acts, actFacts, runChecks, mismatch, failure,
767
+ // A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
768
+ // Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
769
+ panel: (() => {
770
+ // A Chat link to this same result would lead back here: it isn't one.
771
+ const elsewhere = (href) => href !== null && !href.endsWith("#follow-ups") && !new RegExp(`^/chat\\?task=[^&]+&result=${run.id}(?:&|$)`).test(href);
772
+ const shown = panel.panel.status === null ? null : view.demo === true ? demoChecksOf(panel.panel.status) : panel.panel.status;
773
+ const missed = proof === null || proof.proofProblem !== null ? 0 : requirementsOf(proof.matrix)?.missed ?? 0;
774
+ const status = shown === null ? null : { ...shown, details: shown.details.map(one => ({ ...one,
775
+ href: elsewhere(one.href) ? one.href : null, action: one.action !== null && elsewhere(one.action.href) ? one.action : null })) };
776
+ return failure === null ? { ...panel.panel, status }
777
+ : { ...panel.panel, outcome: failure.line, need: null, youCheck: null, status: status === null ? null : { ...status, headline: "Failed", tone: "danger", sentence: failure.line, need: null,
778
+ // Read as Failed, the Requirements row counts what it missed, as the task's card does.
779
+ details: status.details.map(one => one.key !== "requirements" || missed === 0 ? one : { ...one, text: `${missed} missed`, mark: "failed", action: null }) },
780
+ // What it missed is said once, in plain words above; the recorded wording stays under Details.
781
+ attention: panel.panel.attention.filter(one => !(proof?.reasons ?? []).includes(one) && !(proof?.matrix ?? []).some(row => row.detail.includes(one))) };
782
+ })(),
783
+ ...(failure === null ? {} : { status: { label: "Failed", tone: "problem", token: "failed" } }),
784
+ checks: checks === undefined ? null : { detail: checks.detail, problem: checks.status === "failed" || checks.status === "unavailable", logHref: checks.logArtifactId === null ? null : `/r/${run.id}/evidence/${checks.logArtifactId}` },
785
+ },
786
+ };
787
+ }
788
+ /** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
789
+ * (owedAcceptance), so the form asks for exactly what the completion requires. */
790
+ export function owedAcceptanceOf(receipt) {
791
+ const owed = owedAcceptance(receipt);
792
+ return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
793
+ }
794
+ export function completionForm(taskId, runId, digest, pullRequestTo = null, accept = null) {
795
+ // Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
796
+ // its reason, asked for right here), the same post records it first.
797
+ const reason = accept?.note == null ? "" : html `<label class="meta" for="complete-reason-${runId}">${ACCEPT_NEEDS_REASON}</label><input type="text" id="complete-reason-${runId}" name="note" maxlength="500" required placeholder="${accept.note}">`;
798
+ const options = { attrs: { class: "card result-complete" }, hidden: { receipt: digest, run: runId, accept: accept === null ? null : "1" } };
799
+ if (pullRequestTo !== null) {
800
+ // With pull requests set up: the PR is the primary road, a bare finish the quiet one beside it.
801
+ return postForm(`${taskHref(taskId)}/complete`, html `${reason}<p class="meta">Finishes the task. A pull request opens on ${pullRequestTo} from this exact commit when you ask for one.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Accept and finish</button></div>`, options);
802
+ }
803
+ return postForm(`${taskHref(taskId)}/complete`, html `${reason}<p class="meta">Finishes the task. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Accept and finish</button>`, options);
804
+ }
805
+ /** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
806
+ * behind the password when checks passed, or Open a pull request for a result completed without one. */
807
+ export function pullRequestCardHtml(pr, csrf) {
808
+ if (pr.view === null) {
809
+ if (pr.offer === null || csrf === "")
810
+ return html ``;
811
+ return html `<section class="card pull-request" id="merge" data-pull-request="none"><div class="pull-request-head"><strong>No pull request</strong></div><p class="meta">This result was completed without one.</p>${postForm(`${taskHref(pr.offer.taskId)}/complete`, html `<button type="submit" class="secondary">Open a pull request</button>`, { attrs: { class: "pull-request-act" }, hidden: { receipt: pr.offer.digest, run: pr.offer.runId, publish: "1" } })}</section>`;
812
+ }
813
+ const view = pr.view;
814
+ const url = safePrUrl(view.prUrl);
815
+ const link = view.prNumber === null ? "" : url === null ? html `<span class="mono">PR #${view.prNumber}</span>` : html `<a href="${url}" class="mono" rel="noreferrer" target="_blank">PR #${view.prNumber}</a>`;
816
+ // A pull request's trouble never undoes the result: amber, never red.
817
+ const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" ? "problem" : "waiting";
818
+ const revision = view.revisionTask === null || view.state !== "failing" ? "" : html ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
819
+ const merged = view.mergeCommit === null ? "" : html `<p class="meta">Merge commit <span class="mono">${view.mergeCommit.slice(0, 12)}</span></p>`;
820
+ const target = pr.target === null ? "the base branch" : pr.target;
821
+ const act = !view.canMerge || csrf === "" || view.prNumber === null ? "" :
822
+ postForm(`${taskHref(pr.taskId)}/merge`, html `<p class="meta">${view.mergeMethod === "squash" ? "Squash-merges" : view.mergeMethod === "rebase" ? "Rebase-merges" : "Merges"} PR #${view.prNumber} into ${target} and deletes its branch.</p><label>Password<input type="password" name="token" autocomplete="current-password" required></label>${view.fullCheck === null ? html `<button type="submit">Merge</button>` : html `<input type="hidden" name="anyway" value="1"><button type="submit" class="secondary">Merge anyway</button>`}`, { attrs: { class: "pull-request-act", "data-merge-form": true }, hidden: { run: view.runId } });
823
+ return html `<section class="card pull-request pull-request--${tone}" id="merge" data-pull-request="${view.state}"><div class="pull-request-head"><strong>${view.label}</strong>${link}</div><p class="meta">${view.detail}${revision}</p>${merged}${act}</section>`;
824
+ }
825
+ /** Exactly one primary road per result, chosen from its state; the
826
+ * others stay reachable from their own sections. Every form posts to the
827
+ * endpoint that already owns the act, with the session's CSRF token; a
828
+ * bearer session (no token) sees the road named, never a form. */
829
+ export function reviewNextAction(view, csrf, _accepted, _canAnnotate) {
830
+ const next = reviewNextActionParts(view, csrf);
831
+ return next === null ? html `` : html `<div class="card cockpit-next" data-next-action="${next.kind}"><div><strong>${next.title}</strong><span class="meta">${next.detail}</span></div>${next.control}</div>`;
832
+ }
833
+ /** The next action for the browser workspace's JSON: its control as markup text. */
834
+ export function reviewNextActionOf(view, csrf) {
835
+ const next = reviewNextActionParts(view, csrf);
836
+ return next === null ? null : { ...next, control: htmlString(next.control) };
837
+ }
838
+ function reviewNextActionParts(view, csrf) {
839
+ const run = view.run;
840
+ const detail = view.detail;
841
+ const card = (kind, title, detailWords, control) => ({ kind, title, detail: detailWords, control });
842
+ if (run === null || detail === null) {
843
+ return card("inspect-task", "No build to review", "This task was marked complete without a build record.", html `<a class="button-link" href="${taskHref(view.taskId)}">Open the task</a>`);
844
+ }
845
+ if (detail.ciFailing && csrf !== "") {
846
+ return card("draft-repair", "CI is failing on its pull request", "Toolroll confirmed the failure. Draft one repair task, then approve it before it runs.", postForm(`/r/${run.id}/draft-repair`, html `<button type="submit">Draft a repair task</button>`));
847
+ }
848
+ if (detail.comments.length > 0 && csrf !== "") {
849
+ return card("revise", `${detail.comments.length} note${detail.comments.length === 1 ? "" : "s"} ready`, "Create one revision from these notes. You approve it before it runs.", postForm(`/r/${run.id}/revise`, html `${revisionSealFields(detail.comments, detail.sourceDigest)}<button type="submit">Revise</button>`, { returnTo: reviewHref(view.taskId) }));
850
+ }
851
+ return null;
852
+ }
853
+ /**
854
+ * Plain words for the machine's own vocabulary — no internal token ever
855
+ * reaches a page. Every map here has a generic fallback: a kind a newer
856
+ * daemon invents degrades to honest generic prose, never to its raw name.
857
+ */
858
+ export const PHASE_WORDS = {
859
+ "agent-running": "agent working",
860
+ "validating-handoff": "checking the handoff",
861
+ "correcting-proof": "correcting evidence",
862
+ "capturing-evidence": "capturing evidence",
863
+ committing: "committing",
864
+ };
865
+ /** An agent's phase on the home card, from the machine's own vocabulary. */
866
+ export function homePhaseWords(run) {
867
+ if (run.role === "planner")
868
+ return "Planning";
869
+ if (run.role === "reviewer")
870
+ return "Reviewing";
871
+ if (run.role === "scout")
872
+ return "Investigating";
873
+ const words = { "agent-running": "Writing the change", "validating-handoff": "Checking its handoff", "capturing-evidence": "Saving its evidence",
874
+ committing: "Committing", "verifying-proof": "Running the checks", "correcting-proof": "Correcting its evidence" };
875
+ return run.phase === null ? "Starting" : words[run.phase] ?? "Working";
876
+ }
877
+ export function phaseWords(phase) {
878
+ return PHASE_WORDS[phase] ?? "the agent is working";
879
+ }
880
+ /** A recorded reason in one plain line; machine output (a stack trace, a path, "Error:" text) waits on the run record. */
881
+ export function reasonWords(reason) {
882
+ return isInternalErrorReason(reason) ? "stopped with an internal error" : oneLineOf(runReasonWords(reason.trim().split(/\r?\n/, 1)[0] ?? ""), 140);
883
+ }
884
+ export const INCIDENT_WORDS = {
885
+ "malformed-decision": "the agent's question was malformed",
886
+ "attempts-exhausted": "failed too many times in a row",
887
+ "commit-failure": "the commit failed",
888
+ "malformed-plan": "the plan was malformed",
889
+ "plan-attempts-exhausted": "planning failed too many times",
890
+ "malformed-report": "the scout's report was malformed",
891
+ };
892
+ export function incidentWords(kind) {
893
+ return INCIDENT_WORDS[kind] ?? "something went wrong — the run records have the detail";
894
+ }
895
+ export const EVIDENCE_WORDS = {
896
+ diff: "Diff",
897
+ status: "Build status",
898
+ "park-payload": "Question record",
899
+ plan: "Plan",
900
+ "terminal-diff": "Final diff",
901
+ "diff-stat": "Change summary",
902
+ "base-tree": "Starting files",
903
+ handoff: "Agent handoff",
904
+ "revision-brief": "Revision brief",
905
+ report: "Report",
906
+ proof: "Agent proof",
907
+ "check-log": "Check log",
908
+ screenshot: "Screenshot",
909
+ "structured-output": "Agent response",
910
+ "plan-contract": "Plan contract",
911
+ "review-context": "Review context",
912
+ };
913
+ /** Byte counts as people read them. */
914
+ export function humanBytes(bytes) {
915
+ if (bytes < 1024)
916
+ return `${bytes} B`;
917
+ if (bytes < 1024 * 1024)
918
+ return `${Math.round(bytes / 1024)} KB`;
919
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
920
+ }
921
+ export function evidenceWords(kind) {
922
+ return EVIDENCE_WORDS[kind] ?? "a stored record";
923
+ }
924
+ /** The badge for a run's outcome. A null outcome reads "running" ONLY when
925
+ * the caller proved the run's lease is the task's current live claim — an
926
+ * orphaned run keeps saying what actually became of it. */
927
+ export function runOutcomeBadge(run, live) {
928
+ if (!live && run.role === "planner" && run.reason === "plan-drafted") {
929
+ return html `<span class="badge">Planned</span>`;
930
+ }
931
+ if (!live && run.reason === "interrupted")
932
+ return html `<span class="badge">Interrupted</span>`;
933
+ return live
934
+ ? html `<span class="badge badge-running">Running</span>`
935
+ : html `<span class="badge badge-${run.outcome ?? "cut"}">${sentenceCase(run.outcome ?? "never finished")}</span>`;
936
+ }
937
+ export const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
938
+ export function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
939
+ const list = rows.length === 0
940
+ ? html `<p class="meta">No builds yet \u2014 they appear once an approved task is dispatched.</p>`
941
+ : joinHtml(rows
942
+ .map(run => {
943
+ const verdict = verdicts.get(run.id)?.verdict ?? null;
944
+ const needsVerification = (verdict === "short" || verdict === "refuted") && !accepted.has(run.id)
945
+ ? html ` <span class="badge badge-failed">${verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
946
+ : "";
947
+ return html `<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> <a href="${taskHref(run.taskId)}" class="mono">${run.taskId}</a> ${[
948
+ runOutcomeBadge(run, liveIds.has(run.id)),
949
+ run.qualityMode === "strict" ? html ` <span class="badge">Strict review</span>` : "",
950
+ needsVerification,
951
+ criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []),
952
+ run.provider === "claude" ? "" : html ` <span class="meta mono">${run.provider}</span>`,
953
+ html `<span class="right meta mono">${whenTime(run.startedAt)}${run.providerStartedAt === null && run.tokensIn === null && run.tokensOut === null && run.costUsd === null ? "" : ` \u00b7 ${runCostWords(run, liveIds.has(run.id))}`}</span></p>`,
954
+ ]}`;
955
+ }), "\n");
956
+ const older = nextCursor === null ? "" : html `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
957
+ return screen("builds", joinHtml([html `<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), html `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older], "\n"), { chrome });
958
+ }
959
+ /** What a scout found, each with its link and picture, then any screenshot no item shows. Every link is the cited
960
+ * http(s) address the report parser admitted; every picture is served from verified evidence. */
961
+ export function reportItemsHtml(items, shots, runId) {
962
+ const picture = (shot) => shot.artifactId === null
963
+ ? html `<p class="meta">Screenshot unavailable: ${shot.problem ?? "it can't be shown"}</p>`
964
+ : html `<a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}" loading="lazy"><span>${shot.caption}</span></a>`;
965
+ const link = (url) => html `<a href="${url}" rel="noopener noreferrer nofollow" target="_blank" class="report-link">${url}</a>`;
966
+ const shown = new Set(items.map(one => one.image).filter((one) => one !== null));
967
+ const loose = shots.filter(one => !shown.has(one.file));
968
+ if (items.length === 0 && loose.length === 0)
969
+ return html ``;
970
+ return html `<div class="report-items" data-report-items="${items.length}">${items.length === 0 ? "" : html `<p><strong>What it found</strong></p><ol>${items.map(item => {
971
+ const shot = item.image === null ? undefined : shots.find(one => one.file === item.image);
972
+ return html `<li data-report-item><p><strong>${item.title}</strong></p><p class="meta">${item.why}</p><p class="meta">${link(item.url)}</p>${shot === undefined ? "" : html `<div class="receipt-visuals">${picture(shot)}</div>`}</li>`;
973
+ })}</ol>`}${loose.length === 0 ? "" : html `<p><strong>Screenshots</strong></p><div class="receipt-visuals" aria-label="the scout's screenshots">${loose.map(picture)}</div>`}</div>`;
974
+ }
975
+ export function runReportView(artifacts, root) {
976
+ const artifact = [...artifacts].reverse().find(one => one.kind === "report");
977
+ if (artifact === undefined)
978
+ return null;
979
+ let read;
980
+ try {
981
+ read = readVerifiedArtifact(root, artifact);
982
+ }
983
+ catch {
984
+ return { ok: false, artifactId: artifact.id, problem: "the report file could not be read" };
985
+ }
986
+ if (!read.ok)
987
+ return { ok: false, artifactId: artifact.id, problem: read.problem };
988
+ const parsed = parseReport(read.content.toString("utf8"), { stored: true });
989
+ if (!parsed.ok)
990
+ return { ok: false, artifactId: artifact.id, problem: artifact.truncated ? "the report was shortened at storage and cannot be read; the missing part was never captured" : "the stored report is not a report this console can read" };
991
+ return { ok: true, artifactId: artifact.id, title: parsed.report.title, summary: parsed.report.summary, document: parsed.report.report, followUps: parsed.report.followUps.length, truncated: artifact.truncated,
992
+ items: parsed.report.items, shots: reportShotsOf(artifacts, root, artifact.run, parsed.report.images) };
993
+ }
994
+ /** The shared facts (package 3), computed ONCE from the same verified
995
+ * records every surface reads: the head from the sealed diff summary when
996
+ * it verifies (else the run record, and the source is named), the signed
997
+ * criteria passed from the machine's matrix, the proof's caveats, every
998
+ * evidence problem in words, and the observed publication state. */
999
+ export function sharedResultFactsOf(run, proof, terminal, handoff, report, publication) {
1000
+ const stat = terminal?.stat ?? null;
1001
+ const statOk = stat !== null && !("problem" in stat);
1002
+ const passed = proof === null || proof.matrix.length === 0 ? null : passFraction(proof.matrix);
1003
+ const patch = terminal?.patch ?? null;
1004
+ const evidenceProblems = evidenceProblemDetailsOf({
1005
+ proofProblem: proof?.proofProblem ?? null,
1006
+ diff: patch === null ? null : "problem" in patch ? { problem: patch.problem } : { truncated: patch.truncated },
1007
+ stat: stat === null ? null : "problem" in stat ? { problem: stat.problem } : { filesTruncated: stat.filesTruncated },
1008
+ checkLog: proof?.checkLog === null || proof?.checkLog === undefined
1009
+ ? null
1010
+ : proof.checkLog.problem === null ? { truncated: proof.checkLog.truncated } : { problem: proof.checkLog.problem },
1011
+ screenshots: proof?.screenshots ?? [],
1012
+ uncapturedScreenshots: proof?.uncapturedScreenshots ?? [],
1013
+ report: report === null ? null : report.ok ? { ok: true, truncated: report.truncated } : { problem: report.problem },
1014
+ reportExpected: run.role === "scout",
1015
+ handoffPresent: handoff !== null,
1016
+ outcome: run.outcome,
1017
+ });
1018
+ return {
1019
+ runId: run.id,
1020
+ base: statOk ? stat.base : run.baseRevision,
1021
+ head: statOk ? stat.head : run.headRevision,
1022
+ headSource: statOk ? "sealed diff" : run.headRevision === null ? null : "run record",
1023
+ checks: passed === null ? null : { passed: passed.passed, total: passed.total },
1024
+ caveats: proof?.proof?.caveats ?? [],
1025
+ evidenceProblems: evidenceProblems.map(one => one.words),
1026
+ evidenceHealth: evidenceHealthOf(evidenceProblems),
1027
+ publicationState: publication === null ? "none" : publication.state,
1028
+ publicationWords: receiptPublicationWords(publication),
1029
+ };
1030
+ }
1031
+ export function proofBundleView(store, run, artifacts, root) {
1032
+ const verdictRow = store.proofVerdictFor(run.id);
1033
+ const acceptanceRow = store.proofAcceptance(run.id);
1034
+ const proofView = readVerifiedProofForRun(store, root, run.id);
1035
+ const checkLogArtifact = artifacts.find(one => one.kind === "check-log") ?? null;
1036
+ const screenshotArtifacts = artifacts.filter(one => one.kind === "screenshot");
1037
+ if (verdictRow === null && proofView === null && checkLogArtifact === null && screenshotArtifacts.length === 0) {
1038
+ return null;
1039
+ }
1040
+ const proof = proofView !== null && proofView.ok ? proofView.proof : null;
1041
+ const captionFor = (path) => proof?.screenshots.find(one => one.path === path)?.caption ?? path;
1042
+ const screenshots = screenshotArtifacts.flatMap((artifact) => {
1043
+ const path = SCREENSHOT_CAPTURE.exec(artifact.capture)?.[1];
1044
+ if (path === undefined)
1045
+ return [];
1046
+ // The bytes are re-verified at render (workspace package 3): a shot
1047
+ // whose file is gone or altered is named as unavailable, never shown
1048
+ // as validated visual proof.
1049
+ let problem = null;
1050
+ try {
1051
+ const read = readVerifiedArtifact(root, artifact);
1052
+ if (!read.ok)
1053
+ problem = read.problem;
1054
+ }
1055
+ catch {
1056
+ problem = "the file could not be read";
1057
+ }
1058
+ return [{ path, caption: captionFor(path), artifactId: artifact.id, problem }];
1059
+ });
1060
+ const stored = new Set(screenshots.map(one => one.path));
1061
+ const uncapturedScreenshots = (proof?.screenshots ?? []).map(one => one.path).filter(path => !stored.has(path));
1062
+ let checkLog = null;
1063
+ if (checkLogArtifact !== null) {
1064
+ // Re-verified at render: a log whose bytes no longer hash to their
1065
+ // record is a named evidence problem, never shown as output.
1066
+ let read;
1067
+ try {
1068
+ read = readVerifiedArtifact(root, checkLogArtifact);
1069
+ }
1070
+ catch {
1071
+ read = { ok: false, problem: "the file could not be read" };
1072
+ }
1073
+ const sizes = { bytesOriginal: checkLogArtifact.bytesOriginal, bytesStored: checkLogArtifact.bytesStored };
1074
+ checkLog = read.ok
1075
+ ? { text: read.content.toString("utf8"), artifactId: checkLogArtifact.id, truncated: checkLogArtifact.truncated, problem: null, ...sizes }
1076
+ : { text: "", artifactId: checkLogArtifact.id, truncated: checkLogArtifact.truncated, problem: read.problem, ...sizes };
1077
+ }
1078
+ return {
1079
+ verdict: verdictRow?.verdict ?? null,
1080
+ reasons: verdictRow?.reasons ?? [],
1081
+ accepted: acceptanceRow === null ? null : { by: acceptanceRow.approver, note: acceptanceRow.note, at: acceptanceRow.acceptedAt },
1082
+ proof: proof === null
1083
+ ? null
1084
+ : {
1085
+ criteria: proof.criteria.map(one => ({ statement: one.statement, verdict: one.verdict, how: one.how })),
1086
+ checks: proof.checks,
1087
+ caveats: proof.caveats,
1088
+ },
1089
+ proofProblem: proofView !== null && !proofView.ok ? proofView.problem : null,
1090
+ checkLog,
1091
+ screenshots,
1092
+ uncapturedScreenshots,
1093
+ matrix: verdictRow?.matrix ?? [],
1094
+ // A matrix row links only to records that still verify (repair
1095
+ // 2026-09-14): a damaged screenshot or check log is named as a
1096
+ // problem above, never offered as a download.
1097
+ matrixLinks: (() => {
1098
+ const damaged = new Set([...screenshots.filter(one => one.problem !== null).map(one => one.artifactId), ...(checkLog !== null && checkLog.problem !== null ? [checkLog.artifactId] : [])]);
1099
+ return new Map([...evidenceLinksFor(artifacts)].filter(([, id]) => !damaged.has(id)));
1100
+ })(),
1101
+ machineVerdict: verdictRow?.machineVerdict ?? null,
1102
+ qualityMode: run.qualityMode ?? "default",
1103
+ repairChain: store.repairChainFor(run.id) ?? (() => {
1104
+ const ref = store.refById(run.taskRef);
1105
+ return ref === null ? null : store.repairChainForDraft(ref.externalId);
1106
+ })(),
1107
+ };
1108
+ }
1109
+ export function completionReceiptView(store, run, artifacts, root, review = null) {
1110
+ const handoff = structuredHandoffView(artifacts, root);
1111
+ const proof = proofBundleView(store, run, artifacts, root);
1112
+ const terminal = terminalDiffView(artifacts, root);
1113
+ const report = runReportView(artifacts, root);
1114
+ const stat = terminal?.stat ?? null;
1115
+ const coverage = semanticCoverage(proof?.matrix ?? [], run.qualityMode ?? "default");
1116
+ const publication = publicationFactsOf(store.publicationForRun(run.id));
1117
+ return {
1118
+ runId: run.id,
1119
+ role: run.role,
1120
+ outcome: run.outcome,
1121
+ summary: run.role === "scout" ? report?.ok ? report.summary : "The report needs attention." : handoff?.conclusion ?? run.handoff,
1122
+ verdict: proof?.verdict ?? null,
1123
+ reasons: proof?.reasons ?? [],
1124
+ accepted: proof?.accepted !== null && proof?.accepted !== undefined,
1125
+ recordComplete: handoff !== null && terminal !== null,
1126
+ publication,
1127
+ review,
1128
+ matrix: proof?.matrix ?? [],
1129
+ diff: stat === null || "problem" in stat
1130
+ ? stat
1131
+ : {
1132
+ fileCount: stat.fileCount,
1133
+ additions: stat.additions,
1134
+ deletions: stat.deletions,
1135
+ binaryCount: stat.binaryCount,
1136
+ filesTruncated: stat.filesTruncated,
1137
+ },
1138
+ screenshots: proof?.screenshots ?? [],
1139
+ caveats: proof?.proof?.caveats ?? [],
1140
+ coverage: coverage.contextGaps.length > 0 || (proof?.matrix ?? []).some(row => row.review != null || row.assessment !== undefined) ? coverageWords(coverage) : [],
1141
+ coverageSecondary: coverage.contextGaps.length === 0 && (coverage.satisfied === true || (coverage.satisfied === null && !coverage.required)),
1142
+ report,
1143
+ facts: sharedResultFactsOf(run, proof, terminal, handoff, report, publication),
1144
+ };
1145
+ }
1146
+ export const REVIEW_DIFF_LINE_CAP = 4_000;
1147
+ /** Parse only the stable structure Git's unified patch format guarantees.
1148
+ * Unknown metadata remains visible, and a patch that cannot be structured
1149
+ * falls back to the sealed raw record—presentation never becomes proof. */
1150
+ export function parseReviewDiff(text) {
1151
+ const files = [];
1152
+ let file = null;
1153
+ let hunk = null;
1154
+ let oldLine = 0;
1155
+ let newLine = 0;
1156
+ let rendered = 0;
1157
+ let linesTruncated = false;
1158
+ const pathOf = (raw) => {
1159
+ const withoutTimestamp = raw.split("\t", 1)[0]?.trim() ?? raw.trim();
1160
+ let decoded = withoutTimestamp;
1161
+ if (decoded.startsWith('"') && decoded.endsWith('"')) {
1162
+ try {
1163
+ decoded = JSON.parse(decoded);
1164
+ }
1165
+ catch {
1166
+ decoded = decoded.slice(1, -1);
1167
+ }
1168
+ }
1169
+ return decoded === "/dev/null" ? decoded : decoded.replace(/^[ab]\//, "");
1170
+ };
1171
+ for (const raw of text.replace(/\r\n/g, "\n").split("\n")) {
1172
+ if (raw.startsWith("diff --git ")) {
1173
+ const at = raw.lastIndexOf(" b/");
1174
+ file = { path: at === -1 ? "changed file" : pathOf(raw.slice(at + 1)), oldPath: null, meta: [raw], hunks: [] };
1175
+ files.push(file);
1176
+ hunk = null;
1177
+ continue;
1178
+ }
1179
+ if (file === null)
1180
+ continue;
1181
+ if (raw.startsWith("--- ")) {
1182
+ file.oldPath = pathOf(raw.slice(4));
1183
+ file.meta.push(raw);
1184
+ continue;
1185
+ }
1186
+ if (raw.startsWith("+++ ")) {
1187
+ const nextPath = pathOf(raw.slice(4));
1188
+ if (nextPath !== "/dev/null")
1189
+ file.path = nextPath;
1190
+ file.meta.push(raw);
1191
+ continue;
1192
+ }
1193
+ const hunkHeader = /^@@ -([0-9]+)(?:,[0-9]+)? \+([0-9]+)(?:,[0-9]+)? @@(.*)$/.exec(raw);
1194
+ if (hunkHeader !== null) {
1195
+ oldLine = Number(hunkHeader[1]);
1196
+ newLine = Number(hunkHeader[2]);
1197
+ hunk = { header: raw, lines: [] };
1198
+ file.hunks.push(hunk);
1199
+ continue;
1200
+ }
1201
+ if (hunk === null) {
1202
+ if (raw !== "")
1203
+ file.meta.push(raw);
1204
+ continue;
1205
+ }
1206
+ if (rendered >= REVIEW_DIFF_LINE_CAP) {
1207
+ linesTruncated = true;
1208
+ continue;
1209
+ }
1210
+ rendered += 1;
1211
+ if (raw.startsWith("+") && !raw.startsWith("+++")) {
1212
+ hunk.lines.push({ kind: "addition", text: raw.slice(1), oldLine: null, newLine });
1213
+ newLine += 1;
1214
+ }
1215
+ else if (raw.startsWith("-") && !raw.startsWith("---")) {
1216
+ hunk.lines.push({ kind: "deletion", text: raw.slice(1), oldLine, newLine: null });
1217
+ oldLine += 1;
1218
+ }
1219
+ else if (raw.startsWith(" ")) {
1220
+ hunk.lines.push({ kind: "context", text: raw.slice(1), oldLine, newLine });
1221
+ oldLine += 1;
1222
+ newLine += 1;
1223
+ }
1224
+ else {
1225
+ hunk.lines.push({ kind: "meta", text: raw, oldLine: null, newLine: null });
1226
+ }
1227
+ }
1228
+ return { files, linesTruncated };
1229
+ }
1230
+ export function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
1231
+ const parsed = parseReviewDiff(patch.text);
1232
+ const structured = parsed.files.some(file => file.hunks.length > 0);
1233
+ if (!structured) {
1234
+ return html `<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary><pre class="mono" style="overflow-x:auto">${patch.text}</pre></details>${storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated)}`;
1235
+ }
1236
+ const stats = stat !== null && !("problem" in stat) ? new Map(stat.files.map(one => [one.path, one])) : new Map();
1237
+ const annotateIcon = strokeIcon(html `<path d="M21 15a2 2 0 0 1-2 2H8l-5 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 10h8"/>`);
1238
+ const files = parsed.files.map((one, fileIndex) => {
1239
+ const counts = stats.get(one.path);
1240
+ const countWords = counts === undefined
1241
+ ? ""
1242
+ : counts.additions === null || counts.deletions === null
1243
+ ? html `<span class="diff-file-counts">binary</span>`
1244
+ : html `<span class="diff-file-counts"><b>+${counts.additions}</b><i>−${counts.deletions}</i></span>`;
1245
+ const fileAnchor = anchors.get(one.path);
1246
+ // Each change is a link target of its own (#<file anchor>-L<first new line>): a result names the exact lines.
1247
+ const hunks = one.hunks.map(hunk => {
1248
+ const first = hunk.lines.find(line => line.newLine !== null && line.kind !== "meta")?.newLine ?? null;
1249
+ return html `<section class="diff-hunk"${fileAnchor === undefined || first === null ? "" : html ` id="${fileAnchor}-L${first}"`}><div class="diff-hunk-head">${hunk.header}</div><div class="diff-lines">${hunk.lines.map(line => {
1250
+ const lineNumber = line.newLine ?? line.oldLine;
1251
+ const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
1252
+ const annotate = !commentable || lineNumber === null || line.kind === "meta"
1253
+ ? html `<span class="diff-annotate-space"></span>`
1254
+ : html `<button type="button" class="diff-annotate pick-line" data-path="${one.path}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${one.path}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
1255
+ const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
1256
+ return html `<div class="diff-line diff-${line.kind}">${annotate}<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span><code><b aria-hidden="true">${marker}</b>${line.text}</code></div>`;
1257
+ })}</div></section>`;
1258
+ });
1259
+ const anchor = anchors.get(one.path);
1260
+ return html `<details class="diff-file"${fileIndex === 0 ? " open" : ""}${anchor === undefined ? "" : html ` id="${anchor}"`}><summary><span class="diff-file-name">${one.path}</span>${countWords}</summary>${one.oldPath !== null && one.oldPath !== "/dev/null" && one.oldPath !== one.path ? html `<p class="diff-rename meta">from ${one.oldPath}</p>` : ""}${hunks}</details>`;
1261
+ });
1262
+ return html `<div class="diff-review" data-review-diff>${commentable
1263
+ ? html `<div class="diff-review-bar"><span class="meta">${parsed.files.length} changed file${parsed.files.length === 1 ? "" : "s"}</span><div class="diff-modes" role="group" aria-label="diff mode"><button type="button" data-diff-mode="view" aria-pressed="true">View</button><button type="button" data-diff-mode="annotate" aria-pressed="false">Annotate</button></div></div><p class="diff-review-help">Select a line, then describe what should change. Nothing is revised until you create the revision below.</p>`
1264
+ : ""}${files}${patch.truncated
1265
+ ? html `<p class="diff-cut">The sealed diff was shortened at storage: the rest of the change was never captured, here or in the download.</p>`
1266
+ : parsed.linesTruncated ? html `<p class="diff-cut">This visual diff is shortened. Review the sealed patch before approving.</p>` : ""}</div>${storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated)}`;
1267
+ }
1268
+ /** The check log as stored (repair 2026-09-14): its output behind a
1269
+ * disclosure when the bytes verify — a shortened log says so in the
1270
+ * summary and its download is the stored part; a log that no longer
1271
+ * verifies is a named problem with no output and no download. */
1272
+ export function checkLogHtml(log, runId, extra) {
1273
+ if (log.problem !== null) {
1274
+ return html `<p class="problem" data-check-log="damaged"${extra}>The check log no longer verifies (${log.problem}). Its output is not shown, and there is nothing to download.</p>`;
1275
+ }
1276
+ return html `<details data-check-log="${log.truncated ? "shortened" : "ok"}"${extra}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${
1277
+ // Each line is a link target (#check-log-L<n>): a failed task names the exact line its check ended on.
1278
+ joinHtml(log.text.split("\n").map((line, index) => html `<span id="check-log-L${index + 1}">${line}</span>`), "\n")}</pre></details>${storedDownloadLink(runId, log.artifactId, "check log", log.truncated)}`;
1279
+ }
1280
+ /** The download link for a stored record (repair 2026-09-14): a shortened
1281
+ * record's link says it holds the stored part only — never "the full"
1282
+ * bytes that were cut at storage and exist nowhere. */
1283
+ export function storedDownloadLink(runId, artifactId, what, truncated) {
1284
+ return html `<p class="meta"><a href="/r/${runId}/evidence/${artifactId}">${truncated ? `Download the stored part of the ${what} (shortened at storage — not the full ${what})` : `Download the ${what}`}</a></p>`;
1285
+ }
1286
+ /** Render the terminal diff card: stat and capture health first, the bounded
1287
+ * patch beneath a fold. `editor` (arc 6) links file rows to vscode:// on the
1288
+ * reviewing device; `commentable` adds a per-file "comment" button the
1289
+ * page's prefill script reads — a real button, keyboard-reachable, separate
1290
+ * from the link so the two never fight over one click (finding 4). */
1291
+ export function terminalDiffCard(view, runId, editor = null, commentable = false) {
1292
+ const parts = [html `<h2>What changed</h2>`];
1293
+ if (view.stat === null) {
1294
+ parts.push(html `<p class="meta">No change summary was captured for this build</p>`);
1295
+ }
1296
+ else if ("problem" in view.stat) {
1297
+ parts.push(html `<p class="meta">Stat: ${view.stat.problem}</p>`);
1298
+ }
1299
+ else {
1300
+ const s = view.stat;
1301
+ const zero = s.fileCount === 0;
1302
+ parts.push(html `<p class="row"><span class="mono">${s.base.slice(0, 12)} → ${s.head.slice(0, 12)}</span> — ${zero
1303
+ ? "no changes, verified"
1304
+ : `${s.fileCount} file(s) · +${s.additions} −${s.deletions}` +
1305
+ (s.binaryCount > 0 ? ` · ${s.binaryCount} binary` : "") +
1306
+ (s.filesTruncated ? " · file list cut, counts complete" : "")}</p>`);
1307
+ if (!zero) {
1308
+ const fileName = (path) => {
1309
+ const href = editor === null ? null : editorFileHref(editor.worktree, path);
1310
+ return href === null ? path : html `<a href="${href}">${path}</a>`;
1311
+ };
1312
+ parts.push(html `<div class="evidence">${joinHtml(s.files
1313
+ .slice(0, 40)
1314
+ .map(file => html `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${file.renamedFrom})`} <span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>${commentable ? html ` <button type="button" class="pick-file" data-path="${file.path}">Comment</button>` : ""}</p>`), "\n")}${s.files.length > 40 ? html `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : ""}</div>`);
1315
+ if (editor !== null) {
1316
+ parts.push(html `<p class="meta">File links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing</p>`);
1317
+ }
1318
+ }
1319
+ }
1320
+ if (view.patch === null) {
1321
+ parts.push(html `<p class="meta">The final diff was not captured for this build</p>`);
1322
+ }
1323
+ else if ("problem" in view.patch) {
1324
+ parts.push(html `<p class="meta">Patch: ${view.patch.problem}</p>`);
1325
+ }
1326
+ else if (view.patch.text.trim() === "") {
1327
+ parts.push(html `<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
1328
+ }
1329
+ else {
1330
+ parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
1331
+ }
1332
+ return joinHtml(parts, "\n");
1333
+ }
1334
+ /**
1335
+ * The evidence bundle (Priority 2): the closed verdict first — the one
1336
+ * sentence every other surface agrees with — then criteria, the agent's
1337
+ * declared checks, the plane's own re-run (labeled "re-run here", never
1338
+ * confused with the agent's own claim), caveats, and every validated
1339
+ * screenshot as a thumbnail linking to the full image. Renders nothing
1340
+ * when the run predates the proof system.
1341
+ */
1342
+ export function evidenceBundleCard(view, run, publication = null, review = null) {
1343
+ if (view === null)
1344
+ return html ``;
1345
+ const runId = run.id;
1346
+ const parts = [html `<h2>Verification and evidence</h2>`];
1347
+ if (view.verdict !== null) {
1348
+ // The same status line every other surface renders for this run
1349
+ // (workspace package 1), beside the verdict's own explanation.
1350
+ const status = resultStatusOf({ runId, role: run.role, outcome: run.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted !== null, review }, publication);
1351
+ parts.push(html `<p class="row" data-proof-verdict="${dispatchStatusToken(view.verdict)}">${statusLineHtml(status)} <span class="meta">${verificationExplanation(view.verdict, view.reasons)}</span></p>`);
1352
+ }
1353
+ if (view.accepted !== null) {
1354
+ parts.push(html `<p class="meta">Accepted with an exception by <span class="mono">${view.accepted.by}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${view.accepted.note}`}</p>`);
1355
+ }
1356
+ parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
1357
+ parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
1358
+ if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
1359
+ parts.push(html `<p class="meta">An independent review found conflicting evidence.</p>`);
1360
+ }
1361
+ parts.push(repairChainHtml(view.repairChain));
1362
+ if (view.proofProblem !== null) {
1363
+ parts.push(html `<p class="meta">Verification details are unavailable: ${view.proofProblem}</p>`);
1364
+ }
1365
+ else if (view.proof !== null) {
1366
+ if (view.proof.criteria.length > 0) {
1367
+ parts.push(html `<div class="result-section"><strong>Acceptance criteria</strong><ul>${view.proof.criteria
1368
+ .map(one => html `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${sentenceCase(one.verdict)}</span> ${one.statement} <span class="meta">— ${one.how}</span></li>`)}</ul></div>`);
1369
+ }
1370
+ if (view.proof.checks.length > 0) {
1371
+ parts.push(html `<div class="result-section"><strong>Checks reported by the agent</strong><ul>${view.proof.checks.map(one => html `<li><span class="mono">${one.command}</span> <span class="meta">(exit ${one.exitCode}) — ${one.summary}</span></li>`)}</ul></div>`);
1372
+ }
1373
+ if (view.proof.caveats.length > 0) {
1374
+ parts.push(html `<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => html `<li>${one}</li>`)}</ul></div>`);
1375
+ }
1376
+ }
1377
+ if (view.checkLog !== null)
1378
+ parts.push(checkLogHtml(view.checkLog, runId, html ``));
1379
+ if (view.screenshots.length > 0) {
1380
+ parts.push(html `<div class="result-section"><strong>Screenshots</strong>${joinHtml(view.screenshots
1381
+ .map(shot => html `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}" style="max-width:12rem;max-height:9rem;border-radius:var(--radius);border:1px solid var(--border)"></a> <span class="meta">${shot.caption} · <span class="mono">${shot.path}</span></span></p>`), "\n")}</div>`);
1382
+ }
1383
+ return parts.length === 1 ? html `` : joinHtml(parts, "\n");
1384
+ }
1385
+ /** A calm, scan-first result receipt for the task and its focused chat.
1386
+ * The full ledger remains one click away; this card carries only the facts
1387
+ * needed to decide whether to inspect, discuss, or move on. */
1388
+ /** The receipt's proof-state word and tone, from the stored verdict and
1389
+ * the operator's acceptance alone — shared with the review cockpit's
1390
+ * header chip so the two surfaces never name one state differently. */
1391
+ /** The receipt's facts as the shared projection reads them. */
1392
+ /** A result's follow-ups: checks run on its commit since it finished, and its tests task. */
1393
+ export function followUpsFor(store, evidenceRoot, run, now) {
1394
+ if (run.role !== "builder" || run.finishedAt === null || run.headRevision === null)
1395
+ return null;
1396
+ const repo = store.refById(run.taskRef)?.repo ?? null;
1397
+ const tests = store.handle.prepare("SELECT outcome FROM action_ledger WHERE run_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(run.id, ADD_TESTS_ACTION);
1398
+ return { repo, checks: followUpChecksOf(store, run.id, now, evidenceRoot), testsTask: tests === undefined ? null : String(tests["outcome"]),
1399
+ quick: repo !== null && liveQuickCommand(store, repo) !== null, full: repo !== null && store.liveVerifyCommand(repo) !== null };
1400
+ }
1401
+ export const FOLLOW_UP_WORDS = { waiting: "waiting for a worker", running: "running", passed: "passed", failed: "failed", "not-run": "didn't run" };
1402
+ /** Run checks and Add tests, under the result's Checks: what ran since, then the two acts. */
1403
+ export function followUpsHtml(followUps, runId, o) {
1404
+ // A batch check names every result it ran with and the exact commit it tested (batch-checks.ts).
1405
+ const batchHow = (one) => {
1406
+ const peers = one.batch?.members.filter(member => member.run !== runId).map(member => member.task) ?? [];
1407
+ return one.batch?.mode === "together" && peers.length > 0 ? ` together with ${peers.join(", ")}`
1408
+ : one.batch?.mode === "split" ? " on its own after its batch failed" : one.batch?.mode === "conflict" ? " on its own (it conflicted with another result)" : "";
1409
+ };
1410
+ const rows = followUps.checks.map(one => html `<li data-follow-up-check="${one.state}"${one.why === "batch" ? html ` data-batch="${one.batch?.mode ?? "waiting"}"` : ""}>${CHECK_LEVEL_WORDS[one.level]} checks ${one.why === "batch" && one.state === "waiting" ? "waiting to check with other results" : FOLLOW_UP_WORDS[one.state]}${batchHow(one)}${one.exitCode === null || one.state === "passed" ? "" : ` (exit ${one.exitCode})`} on <span class="mono">${(one.tested ?? one.head).slice(0, 7)}</span><span class="meta"> · ${one.why === "pull-request" ? "for the pull request" : one.why === "batch" ? "batch check" : one.actor}${one.note === null ? "" : ` · ${one.note}`}</span>${one.logArtifactId === null ? "" : html ` <a href="/r/${runId}/evidence/${one.logArtifactId}">Log</a>`}</li>`);
1411
+ const canCheck = followUps.quick || followUps.full;
1412
+ const checkActs = !canCheck || o.csrf === "" ? null : postForm(`/r/${runId}/checks`, followUps.quick && followUps.full ? html `<button type="submit" name="level" value="quick">Run quick checks</button><button type="submit" name="level" value="full" class="secondary">Run full checks</button>`
1413
+ : html `<button type="submit" name="level" value="${followUps.quick ? "quick" : "full"}">Run checks</button>`, { attrs: { class: "follow-up-act" }, returnTo: o.returnTo });
1414
+ const tests = followUps.testsTask !== null ? html `<p class="meta">Tests task: <a href="${taskHref(followUps.testsTask)}">${followUps.testsTask}</a></p>`
1415
+ : o.csrf === "" ? null : postForm(`/r/${runId}/add-tests`, html `<button type="submit" class="secondary">Add tests</button>`, { attrs: { class: "follow-up-act" }, returnTo: o.returnTo });
1416
+ return html `<section class="result-section follow-ups" id="follow-ups" data-follow-ups><strong>Follow-ups</strong>${rows.length === 0 ? "" : html `<ul class="follow-up-list">${rows}</ul>`}${canCheck || followUps.repo === null ? "" : html `<p class="meta">No approved check to run. <a href="/settings/checks?repo=${encodeURIComponent(followUps.repo)}">Set one up</a>.</p>`}${checkActs === null && tests === null ? "" : html `<div class="follow-up-acts">${checkActs}${tests}</div>`}</section>`;
1417
+ }
1418
+ export function receiptStatusOf(view, review = view.review) {
1419
+ // The evidence health rides the status (repair 2026-09-14): a result
1420
+ // whose stored records are damaged is never called ready on the strength
1421
+ // of a verdict recorded before the damage.
1422
+ return evidenceResultStatusOf({ runId: view.runId, role: view.role, outcome: view.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted, recordComplete: view.recordComplete, review }, view.publication, view.facts.evidenceHealth);
1423
+ }
1424
+ export function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(view), assignment = null, headline = true) {
1425
+ const current = assignment?.receipt?.runId === view.runId ? assignment : null;
1426
+ const presentation = current === null ? null : assignmentPresentationOf(current);
1427
+ const status = presentation?.status ?? standing;
1428
+ // The verdict on record, whatever review is in flight: the criteria
1429
+ // count's source label reads from it, never from the review's tone.
1430
+ const stored = receiptStatusOf(view, null);
1431
+ const inReview = REVIEW_TOKENS.has(status.token);
1432
+ const facts = view.facts;
1433
+ const shown = view.screenshots.filter(one => one.problem === null);
1434
+ const unavailable = view.screenshots.length - shown.length;
1435
+ const diff = view.diff === null
1436
+ ? "Change summary unavailable"
1437
+ : "problem" in view.diff
1438
+ ? "Change summary unavailable"
1439
+ : view.diff.fileCount === 0
1440
+ ? "No repository changes"
1441
+ : `${view.diff.fileCount} file${view.diff.fileCount === 1 ? "" : "s"} · +${view.diff.additions} −${view.diff.deletions}` +
1442
+ (view.diff.binaryCount > 0 ? ` · ${view.diff.binaryCount} binary` : "") +
1443
+ (view.diff.filesTruncated ? " · list shortened" : "");
1444
+ const criteria = facts.checks === null
1445
+ ? "No requirements set"
1446
+ : `${facts.checks.passed}/${facts.checks.total} requirements met`;
1447
+ // The deliverable leads (package 3): a scout's report by its title, UI
1448
+ // work by its validated screenshots. Unverifiable shots are counted as
1449
+ // unavailable, never shown, never called validated.
1450
+ const lead = view.report !== null
1451
+ ? view.report.ok
1452
+ ? html `<p class="receipt-report"><strong>${view.report.title}</strong> <span class="meta">${oneLineOf(view.report.summary, 240)}</span></p>`
1453
+ : html `<p class="receipt-report problem">The report cannot be shown: ${view.report.problem}.</p>`
1454
+ : shown.length === 0
1455
+ ? ""
1456
+ : html `<div class="receipt-visuals" aria-label="validated screenshots">${shown
1457
+ .slice(0, 4)
1458
+ .map(shot => html `<a class="receipt-shot" href="/r/${view.runId}/evidence/${shot.artifactId}"><img src="/r/${view.runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>`)}</div>`;
1459
+ // Caveats and evidence problems stay in the open, ahead of the counts
1460
+ // and any readiness words below them.
1461
+ const attention = current === null ? [...facts.evidenceProblems, ...view.caveats] : [];
1462
+ const caveats = attention.length === 0
1463
+ ? ""
1464
+ : html `<div class="receipt-caveats" data-result-attention="${attention.length}"><strong>Before you move on</strong><ul>${attention.map(one => html `<li>${plainReasonWords(one)}</li>`)}</ul></div>`;
1465
+ // v51: semantic coverage, distinct from the machine proof word above —
1466
+ // the same lines the CLI prints, so chat and terminal cannot disagree.
1467
+ // Nothing owed (optional and unsettled, or satisfied) folds behind a
1468
+ // disclosure; a strict shortfall or a context gap stays in the open.
1469
+ const coverage = view.coverage.length === 0
1470
+ ? ""
1471
+ : current !== null || view.coverageSecondary
1472
+ ? html `<details class="receipt-coverage" data-semantic-coverage="secondary"><summary>Previous assessment</summary><ul>${view.coverage.map(one => html `<li>${one}</li>`)}</ul></details>`
1473
+ : html `<div class="receipt-coverage" data-semantic-coverage=""><strong>Previous assessment</strong><ul>${view.coverage.map(one => html `<li>${one}</li>`)}</ul></div>`;
1474
+ const resultHref = status.token === "review-failed" ? reviewHref(taskId) : place === "chat" ? chatResultHref(taskId, view.runId, status.action?.kind === "open-review" ? "checks" : "summary") : statusActionHref(status, taskId, view.runId, view.publication?.prUrl ?? null) ?? `/r/${view.runId}`;
1475
+ return joinHtml([
1476
+ html `<section class="card completion-receipt" data-card-kind="result-receipt"${attributes(resultFactsAttributeMap(facts))}>`,
1477
+ html `<div class="receipt-head"><div><h2>${receiptHeadingOf(view.outcome, view.publication, view.role)}</h2></div>`,
1478
+ html `${headline ? statusLineHtml(status) : ""}</div>`,
1479
+ // The agent's handoff is its narrative, labeled as such; the
1480
+ // publication line is the observed record — never "shipped".
1481
+ lead,
1482
+ html `<p class="receipt-summary">${conciseOutcomeOf(view.summary ?? (view.outcome === "no-change" ? "The agent found that no repository change was needed." : "The build finished without a concise handoff."))}</p>`,
1483
+ headline ? html `<div class="receipt-actions"><a class="button-link" href="${resultHref}" data-open-result data-primary-action>${status.action?.label ?? "Open result"}</a></div>` : "",
1484
+ // A pull request that couldn't open never undoes the result: an amber note, not red.
1485
+ view.publication?.state === "failed" ? html `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "",
1486
+ caveats,
1487
+ coverage,
1488
+ html `<details class="receipt-details"><summary>${STATUS_MORE}</summary>`,
1489
+ view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? html `<p class="recap">${view.summary}</p>` : "",
1490
+ html `<p class="receipt-publication" data-receipt-publication="${facts.publicationState}">${facts.publicationWords}</p>`,
1491
+ // A review in flight is the receipt's primary status too (its chip
1492
+ // above); the detail — the earlier verdict as history (review fixes,
1493
+ // finding 4) — is secondary, behind a native disclosure that reads the
1494
+ // same words on the task page and in chat.
1495
+ inReview ? html `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${status.token}">${status.detail}</p></details>` : "",
1496
+ // The matrix count is the proof's own citation; it reads as verified
1497
+ // only when the machine verified the result (workspace package 1).
1498
+ html `<div class="receipt-facts"><span><strong>${criteria}</strong><small>${stored.tone === "problem" ? html `the agent's own claim — not checked` : stored.token === "agent-attested" ? html `the agent's own claim, no project check` : "against the approved scope"}</small></span>`,
1499
+ html `<span><strong>${diff}</strong><small>${facts.head === null ? "no commit recorded" : `head ${facts.head.slice(0, 12)} · ${facts.headSource ?? ""}`}</small></span>`,
1500
+ html `<span><strong>${view.screenshots.length} screenshot${view.screenshots.length === 1 ? "" : "s"}</strong><small>${view.screenshots.length === 0 ? "none required or captured" : unavailable === 0 ? "validated visual proof" : `${unavailable} unavailable — not validated`}</small></span></div>`,
1501
+ html `<div class="receipt-actions">`,
1502
+ place === "task"
1503
+ ? html `<a href="${taskChatHref(taskId)}">Discuss in chat →</a>`
1504
+ : html `<a href="/r/${view.runId}">Full build record →</a>`,
1505
+ // The cockpit (Priority 5): the same records, arranged for a reviewer.
1506
+ html `<a href="${reviewHref(taskId)}">Open result →</a>`,
1507
+ html `</div></details></section>`,
1508
+ ]);
1509
+ }
1510
+ /** The revision form's binding (repair 2026-09-14): the exact ids of the
1511
+ * notes this page displays and the source terms it was rendered against.
1512
+ * The server seals these ids and no others. */
1513
+ export function revisionSealFields(comments, sourceDigest) {
1514
+ return html `<input type="hidden" name="batch" value="${revisionBatchOf(comments)}"><input type="hidden" name="source" value="${revisionSourceOf(sourceDigest)}">`;
1515
+ }
1516
+ /** Older machine-checked results saved a mechanical sentence; say the same
1517
+ * facts in plain words (the version id stays, shortened). */
1518
+ export function plainConclusionOf(conclusion) {
1519
+ const prepared = /^Prepared candidate ([0-9a-f]{7,40}) was checked out by the machine; no agent ran\. (The branch already matched it\.|The sealed diff spans this task's base to that candidate\.)/.exec(conclusion);
1520
+ if (prepared === null)
1521
+ return conclusion;
1522
+ const rest = conclusion.slice(prepared[0].length).trim();
1523
+ return `Checked the prepared version ${prepared[1].slice(0, 7)}; no agent ran. ${prepared[2].startsWith("The branch") ? "The branch already matched it." : "The changes cover everything since the task started."}${rest === "" ? "" : ` ${rest}`}`;
1524
+ }
1525
+ export function resultPanelHtml(detail, o) {
1526
+ return resultPanelParts(detail, o).html;
1527
+ }
1528
+ /** The panel's HTML and the same panel in parts, for the rebuilt result page. */
1529
+ export function resultPanelParts(detail, o) {
1530
+ const { run, receipt, proof, terminal, handoff } = detail;
1531
+ const facts = receipt.facts;
1532
+ const current = detail.assignment?.receipt?.runId === run.id ? detail.assignment : null;
1533
+ const followUps = detail.followUps ?? null;
1534
+ const resultLinks = { result: o.hrefFor("summary"), checks: o.hrefFor("checks"), pullRequest: `${taskHref(detail.rootId ?? detail.taskId)}#merge`,
1535
+ ...(followUps !== null && (followUps.quick || followUps.full) && o.csrf !== "" ? { runChecks: `${o.hrefFor("checks")}#follow-ups` } : {}) };
1536
+ const presentation = current == null ? null : assignmentPresentationOf(current, { additionalAttention: [...facts.evidenceProblems, ...receipt.caveats],
1537
+ ...(detail.publication === null ? {} : { pullRequest: pullRequestFactOf({ ...detail.publication, lastCheckState: detail.ciFailing ? "failing" : detail.publication.lastCheckState }) }),
1538
+ evidence: { damaged: facts.evidenceHealth.damaged, missing: facts.evidenceHealth.missing, shortened: facts.evidenceHealth.shortened }, links: resultLinks });
1539
+ const taskStatus = presentation?.taskStatus ?? null;
1540
+ const status = presentation?.status ?? resultHeadlineOf(receiptStatusOf(receipt));
1541
+ const stored = receiptStatusOf(receipt, null);
1542
+ const humanReview = manualReviewOnly(proof === null ? null : { ...proof, verdict: proof.verdict ?? "" });
1543
+ // Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
1544
+ // On the result itself, "Review result" would link here: accepting it is what resolves it.
1545
+ const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
1546
+ accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`, refuted: proof?.verdict === "refuted",
1547
+ // What Store.finalResultReason refuses: a verified, attested, accepted or published result.
1548
+ rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
1549
+ });
1550
+ const directAssessment = proof?.matrix.some(row => row.assessment !== undefined) === true;
1551
+ const awaitingGoalReview = directAssessment && proof?.reasons.length === 1 && proof.reasons[0] === GOAL_ASSESSMENT_PENDING;
1552
+ const assessmentReasons = directAssessment ? new Set(proof.matrix.flatMap(row => row.review ? [`${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`] : [])) : new Set();
1553
+ const physicalReasons = receipt.reasons.filter(reason => !assessmentReasons.has(reason));
1554
+ const runId = run.id;
1555
+ const shots = proof?.screenshots ?? [];
1556
+ const shown = shots.filter(one => one.problem === null);
1557
+ const stat = terminal?.stat ?? null;
1558
+ const statOk = stat !== null && !("problem" in stat);
1559
+ const patch = terminal?.patch ?? null;
1560
+ const patchOk = patch !== null && !("problem" in patch);
1561
+ const lead = resultLeadOf({ role: run.role, report: receipt.report !== null, screenshots: shown.length, diff: (patchOk && patch.text.trim() !== "") || (statOk && stat.fileCount > 0) });
1562
+ const tabHref = (tab) => o.hrefFor(tab);
1563
+ // ---- attention: problems and caveats, ahead of every readiness word ----
1564
+ const publication = detail.publication;
1565
+ const prUrl = publication === null ? null : safePrUrl(publication.prUrl);
1566
+ const attention = [];
1567
+ // The visible status already says verification is needed. Only omit the
1568
+ // generic repeat; failed checks, specific reasons and damaged evidence stay.
1569
+ const missingVerdictNamed = o.headStatus !== false && status.token === "verification-needed" && receipt.verdict === null && receipt.reasons.length === 0;
1570
+ if (current == null && !humanReview && !awaitingGoalReview && (!directAssessment || physicalReasons.length > 0) && !missingVerdictNamed && stored.token !== "evidence-damaged" && (stored.tone === "problem" || stored.tone === "attention"))
1571
+ attention.push(verificationExplanation(receipt.verdict, physicalReasons));
1572
+ // Exact criterion assessment notes are already shown in Requirements;
1573
+ // do not repeat them as a second current attention message.
1574
+ if (presentation !== null)
1575
+ attention.push(...presentation.attention.map(one => one.detail).filter(detail => !assessmentReasons.has(detail)));
1576
+ if (current == null)
1577
+ attention.push(...facts.evidenceProblems);
1578
+ if (detail.outsideTouches.length > 0)
1579
+ attention.push(`${detail.outsideTouches.length} changed file${detail.outsideTouches.length === 1 ? "" : "s"} outside the approved paths: ${detail.outsideTouches.join(", ")}.`);
1580
+ // A failed caveat can already be quoted in full by its verification
1581
+ // reason. Keep that explanation once and retain every other caveat.
1582
+ if (current == null)
1583
+ attention.push(...receipt.caveats.filter(caveat => !historicalAssessmentReason(caveat) && !attention.some(problem => problem.includes(caveat))));
1584
+ attention.push(...(handoff?.followUps ?? []).map(one => `Follow-up: ${one}`));
1585
+ // The automatic review: a HIGH that did not send the work back, or a review
1586
+ // that could not finish, is the person's to see. (Said once when the
1587
+ // current assignment already carries it.)
1588
+ const automatic = detail.automaticReview ?? null;
1589
+ if (automatic !== null && automatic.sentBackAs === null) {
1590
+ const reviewed = [...(automatic.state === "not-reviewed" ? [`Not reviewed: ${automatic.reason ?? "the automatic review did not finish"}.`] : []), ...automatic.high.map(one => `Review: ${findingWords(one)}`)];
1591
+ for (const line of reviewed)
1592
+ if (!attention.includes(line))
1593
+ attention.push(line);
1594
+ }
1595
+ // Publication risks stay in the open (repair 2026-09-14); the routine
1596
+ // publication fact lives with the build details below.
1597
+ // With the shared status, the pull request is its own quiet row (its reason under Details).
1598
+ if (publication !== null && publication.state === "failed" && taskStatus === null) {
1599
+ attention.push(`Publication failed after ${publication.attempts} attempt${publication.attempts === 1 ? "" : "s"}${publication.lastError === null ? "" : ` — ${oneLineOf(publication.lastError, 200)}`}. No pull request or merge is recorded.`);
1600
+ }
1601
+ if (detail.ciFailing && publication !== null && taskStatus === null)
1602
+ attention.push(`CI is failing on PR #${publication.prNumber} at the last check.`);
1603
+ // Requirements only a person can confirm: plain words and one Accept,
1604
+ // in neutral ink — nothing failed. Accepting records the person's
1605
+ // decision and leaves the recorded checks as they are.
1606
+ const personChecks = proof === null || proof.accepted !== null ? [] : [
1607
+ ...proof.matrix.filter(row => row.state === "manual-review").map(row => personCheckWords(row.statement)),
1608
+ ...(proof.matrix.length > 0 ? [] : proof.reasons.filter(reason => manualReviewCriterionOf(reason) !== null).map(() => personCheckWords(null))),
1609
+ ];
1610
+ const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
1611
+ const youCheck = personChecks.length === 0 ? null : {
1612
+ lines: [...new Set(personChecks)],
1613
+ items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
1614
+ accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
1615
+ };
1616
+ const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
1617
+ const youCheckHtml = youCheck === null ? "" :
1618
+ html `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => html `<li>${one}</li>`)}</ul>${youCheckForm === null ? "" : postForm(youCheckForm.action, html `<button type="submit" data-accept-result>Accept</button>`, { returnTo: youCheckForm.returnTo, hidden: { run: youCheckForm.run } })}</div>`;
1619
+ const attentionHtml = attention.length === 0
1620
+ ? ""
1621
+ : html `<div class="result-attention" data-result-attention="${attention.length}"><ul>${attention.map(one => html `<li>${plainReasonWords(one)}</li>`)}</ul></div>`;
1622
+ // ---- one outcome, one action (repair 2026-09-14) ------------------------
1623
+ // The outcome is the handoff's first sentence, bounded; the agent's full
1624
+ // account waits behind a disclosure in Summary so nothing is said twice.
1625
+ const conclusion = plainConclusionOf(receipt.summary ?? (run.outcome === "no-change" ? "The agent found that no repository change was needed." : "The build finished without a concise handoff."));
1626
+ const outcome = conciseOutcomeOf(conclusion);
1627
+ // A failing pull request keeps its repair draft beside the need's action.
1628
+ const action = o.action === false ? "" : need !== null ? html `${resultNeedAction(need, o, detail.canAnnotate)}${detail.ciFailing && o.csrf !== "" ? resultPrimaryAction(detail, o, prUrl) : ""}` : resultPrimaryAction(detail, o, prUrl);
1629
+ // ---- summary: the deliverable first ----------------------------------
1630
+ const summaryParts = [];
1631
+ const report = receipt.report;
1632
+ if (lead === "report") {
1633
+ if (report === null) {
1634
+ summaryParts.push(html `<p class="problem" data-result-report="missing">This investigation stored no report.</p>`);
1635
+ }
1636
+ else if (!report.ok) {
1637
+ summaryParts.push(html `<p class="problem" data-result-report="problem">The report cannot be shown: ${report.problem}.</p>`);
1638
+ }
1639
+ else {
1640
+ // Escaped text in a fenced block, never markup, never a page: the
1641
+ // download serves the exact stored bytes as text. The summary is
1642
+ // already the outcome line above (and, when shortened there, waits
1643
+ // in "What the agent reported"), so the article never repeats it.
1644
+ summaryParts.push(html `<article class="result-report" data-result-report="ok"><h3>${report.title}</h3>${reportItemsHtml(report.items, report.shots, runId)}<pre class="recap plan-doc">${report.document}</pre><p class="meta"><a href="/r/${runId}/evidence/${report.artifactId}">${report.truncated ? "Download the stored part of the report (shortened at storage — not the full report)" : "Download the report"}</a>${report.followUps === 0 ? "" : html ` · ${report.followUps} proposed follow-up${report.followUps === 1 ? "" : "s"} on <a href="${taskHref(detail.rootId ?? detail.taskId)}">the task</a>`}</p></article>`);
1645
+ }
1646
+ }
1647
+ else if (lead === "screenshots") {
1648
+ summaryParts.push(html `<div class="receipt-visuals result-visuals" aria-label="validated screenshots">${shown
1649
+ .map(shot => html `<a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>`)}</div>`);
1650
+ }
1651
+ const unavailableShots = shots.filter(one => one.problem !== null);
1652
+ if (lead === "changes") {
1653
+ // Code work: the changed files ARE the deliverable; the diff is one tap away.
1654
+ summaryParts.push(html `<p class="result-files-lead">${detail.files.length === 0 ? "" : html `${joinHtml(detail.files.slice(0, 6).map(one => html `<span class="mono">${one.path}</span>`), ", ")}${detail.files.length > 6 ? ` and ${detail.files.length - 6} more` : ""} — `}<a href="${tabHref("changes")}" data-result-goto="changes">see the diff</a></p>`);
1655
+ }
1656
+ if (lead === "summary" && run.outcome === "no-change")
1657
+ summaryParts.push(html `<p class="meta">The agent found that no repository change was needed${statOk && stat.fileCount === 0 ? "; the sealed diff is empty" : ""}.</p>`);
1658
+ // The agent's own account — its full conclusion (when the outcome line
1659
+ // shortened it), what it says it changed, and how it checked — on demand.
1660
+ const agentAccount = [
1661
+ ...(conclusion !== outcome ? [html `<p class="recap">${conclusion}</p>`] : []),
1662
+ ...(handoff !== null && handoff.changes.length > 0 ? [html `<ul class="result-changes">${handoff.changes.map(one => html `<li>${one}</li>`)}</ul>`] : []),
1663
+ ...(handoff !== null && handoff.verification.length > 0 ? [html `<p class="meta">Checked by the agent:</p><ul class="result-changes" data-cockpit-source="agent-words">${handoff.verification.map(one => html `<li>${one}</li>`)}</ul>`] : []),
1664
+ // The report's own notes, each a link target (#report-note-<n>) a mismatch can point at.
1665
+ ...(proof?.proof == null || proof.proof.caveats.length === 0 ? [] : [html `<p class="meta">Its notes:</p><ol class="result-changes" data-report-notes>${proof.proof.caveats.map((one, index) => html `<li id="report-note-${index + 1}">${one}</li>`)}</ol>`]),
1666
+ ];
1667
+ if (agentAccount.length > 0)
1668
+ summaryParts.push(html `<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount}</details>`);
1669
+ // MEDIUM and LOW never block: suggested follow-ups, on request.
1670
+ if (automatic !== null && automatic.followUps.length > 0) {
1671
+ summaryParts.push(html `<details class="result-notes" data-review-followups="${automatic.followUps.length}"><summary>Suggested follow-ups · ${automatic.followUps.length}</summary><ul class="result-changes">${automatic.followUps.map(one => html `<li><span class="badge">${one.severity === "MEDIUM" ? "Medium" : "Low"}</span> <span class="mono">${`${one.file}:${one.line}`}</span> ${one.scenario}</li>`)}</ul></details>`);
1672
+ }
1673
+ const evidenceSources = [
1674
+ ...(report !== null ? [report.ok ? `verified report${report.truncated ? " (shortened)" : ""}` : "report (unverifiable)"] : []),
1675
+ ...(patch !== null ? [patchOk ? `sealed diff${patch.truncated ? " (shortened)" : ""}` : "diff (unverifiable)"] : []),
1676
+ ...(proof === null ? [] : proof.proofProblem !== null ? ["proof (unreadable)"] : proof.proof !== null ? ["agent proof"] : []),
1677
+ ...(proof?.checkLog !== null && proof?.checkLog !== undefined ? [proof.checkLog.problem !== null ? "check log (unverifiable)" : `check log${proof.checkLog.truncated ? " (shortened)" : ""}`] : []),
1678
+ ...(shots.length > 0 ? [`${shown.length} validated screenshot${shown.length === 1 ? "" : "s"}${unavailableShots.length > 0 ? `, ${unavailableShots.length} unavailable` : ""}`] : []),
1679
+ ];
1680
+ const publicationHtml = html `<p class="result-publication" data-receipt-publication="${facts.publicationState}">${facts.publicationWords}${publication === null || publication.prNumber === null
1681
+ ? ""
1682
+ : html ` ${prUrl === null ? html `<span class="mono">PR #${publication.prNumber}</span>` : html `<a href="${prUrl}">PR #${publication.prNumber}</a>`}<span class="meta" data-ci-observed="${detail.ciFailing ? "failing" : (publication.lastCheckState ?? "none")}"> · ${detail.ciFailing ? "CI failing at the last check" : publication.lastCheckState === "passing" ? html `CI passing, observed ${whenTime(publication.lastCheckAt)}` : publication.lastCheckState === "running" ? "CI still running at the last check" : "no CI checks found — verify on GitHub"}</span>`}</p>`;
1683
+ const agent = runAgentWords(detail.route);
1684
+ const links = [];
1685
+ if (o.place !== "run")
1686
+ links.push(html `<a href="/r/${runId}">Full build record →</a>`);
1687
+ if (o.place !== "review")
1688
+ links.push(html `<a href="${reviewHref(detail.taskId)}">Open result →</a>`);
1689
+ if (o.place !== "chat")
1690
+ links.push(html `<a href="${taskChatHref(detail.rootId ?? detail.taskId)}">Discuss in chat →</a>`);
1691
+ links.push(html `<a href="${taskHref(detail.rootId ?? detail.taskId)}">Task overview →</a>`);
1692
+ // Technical facts on demand: build, agent, exact commits, evidence
1693
+ // sources, the publication record, and the other pages for this result.
1694
+ summaryParts.push(joinHtml([
1695
+ html `<details class="result-details" data-result-details><summary>Build details</summary>`,
1696
+ unavailableShots.length === 0 ? "" : html `<ul class="result-unavailable meta" data-result-unavailable="${unavailableShots.length}">${unavailableShots.map(one => html `<li>Screenshot unavailable — <span class="mono">${one.path}</span>: ${one.problem ?? ""}</li>`)}</ul>`,
1697
+ html `<dl class="result-facts">`,
1698
+ html `<div><dt>Build</dt><dd>#${runId} · ${run.runner}${agent === null ? ` · ${run.provider}` : ""}${run.finishedAt === null ? "" : html ` · ${whenTime(run.finishedAt)}`}</dd></div>`,
1699
+ agent === null ? "" : html `<div><dt>Agent</dt><dd>${agent}</dd></div>`,
1700
+ html `<div><dt>Commits</dt><dd>${facts.head === null
1701
+ ? facts.base === null ? "no commit recorded" : html `<span class="mono">${facts.base.slice(0, 12)}</span> <span class="meta">base · no head recorded</span>`
1702
+ : html `<span class="mono">${facts.base === null ? "" : `${facts.base.slice(0, 12)} → `}${facts.head.slice(0, 12)}</span> <span class="meta">from the ${facts.headSource ?? "record"}</span>`}</dd></div>`,
1703
+ html `<div><dt>Evidence</dt><dd>${evidenceSources.length === 0 ? "nothing was captured" : evidenceSources.join(" · ")}</dd></div>`,
1704
+ html `<div><dt>Publication</dt><dd>${publicationHtml}</dd></div>`,
1705
+ html `</dl>`,
1706
+ html `<p class="result-links meta">${links}</p>`,
1707
+ html `</details>`,
1708
+ ]));
1709
+ // ---- changes -----------------------------------------------------------
1710
+ const changeParts = [];
1711
+ if (terminal === null) {
1712
+ changeParts.push(html `<p class="meta">No final diff or change summary was captured for this build${run.role === "scout" ? " — an investigation changes nothing in the repository" : ""}.</p>`);
1713
+ }
1714
+ else {
1715
+ if (stat === null)
1716
+ changeParts.push(html `<p class="meta">No change summary was captured.</p>`);
1717
+ else if (!statOk)
1718
+ changeParts.push(html `<p class="problem">Change summary unavailable: ${stat.problem}.</p>`);
1719
+ else {
1720
+ changeParts.push(html `<p class="row result-stat"><span class="mono">${stat.base.slice(0, 12)} → ${stat.head.slice(0, 12)}</span> — ${stat.fileCount === 0
1721
+ ? "no changes, verified"
1722
+ : `${stat.fileCount} file${stat.fileCount === 1 ? "" : "s"} · +${stat.additions} −${stat.deletions}${stat.binaryCount > 0 ? ` · ${stat.binaryCount} binary` : ""}${stat.filesTruncated ? " · file list cut, counts complete" : ""}`}</p>`);
1723
+ }
1724
+ if (detail.outsideTouches.length > 0) {
1725
+ changeParts.push(html `<p class="problem cockpit-drift" data-cockpit-drift="${detail.outsideTouches.length}"><strong>${detail.outsideTouches.length} changed file${detail.outsideTouches.length === 1 ? "" : "s"} outside the approved paths</strong> — ${joinHtml(detail.outsideTouches.map(path => html `<span class="mono">${path}</span>`), ", ")}. Review these files before accepting the result.</p>`);
1726
+ }
1727
+ if (detail.files.length > 0) {
1728
+ const fileName = (file) => {
1729
+ const href = detail.editor === null ? null : editorFileHref(detail.editor.worktree, file.path);
1730
+ if (href !== null)
1731
+ return html `<a class="mono" href="${href}">${file.path}</a>`;
1732
+ return file.anchor === null ? html `<span class="mono">${file.path}</span>` : html `<a class="mono" href="#${file.anchor}">${file.path}</a>`;
1733
+ };
1734
+ changeParts.push(html `<ol class="cockpit-files result-files">${detail.files
1735
+ .map(file => {
1736
+ const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
1737
+ const flags = html `${file.outsideTouches ? html ` <span class="badge badge-failed" data-outside-touches="1">Outside touches</span>` : ""}${file.cited ? html ` <span class="badge badge-done">Cited</span>` : ""}`;
1738
+ const why = file.priority.reasons.length === 0 ? "" : html ` <span class="meta">— ${file.priority.reasons.join("; ")}</span>`;
1739
+ return html `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : html ` <span class="meta">(was ${file.renamedFrom})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? html ` <button type="button" class="pick-file" data-path="${file.path}">Comment</button>` : ""}${why}</li>`;
1740
+ })}</ol>${detail.editor === null ? "" : html `<p class="meta">File links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing.</p>`}`);
1741
+ }
1742
+ if (patch === null)
1743
+ changeParts.push(html `<p class="meta">The final diff was not captured for this build.</p>`);
1744
+ else if (!patchOk)
1745
+ changeParts.push(html `<p class="problem">Diff unavailable: ${patch.problem}.</p>`);
1746
+ else if (patch.text.trim() === "")
1747
+ changeParts.push(html `<p class="meta">Empty diff — captured successfully, nothing changed.</p>`);
1748
+ else
1749
+ changeParts.push(reviewDiffHtml(patch, stat, runId, detail.canAnnotate, detail.fileAnchors));
1750
+ }
1751
+ // ---- checks --------------------------------------------------------------
1752
+ const checkParts = [];
1753
+ const reviewerNotes = detail.reviewerFindings.map(one => html `<li><span class="badge${one.severity === "problem" ? " badge-failed" : ""}">${one.severity ?? "note"}</span> ${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> `}${one.note} <span class="meta">— ${one.author}</span>${!isRevisionFeedback(one) && detail.canAnnotate && o.csrf !== "" ? html ` <button type="button" class="pick-file" data-path="${one.path ?? ""}" data-line="${one.line ?? ""}" data-review-note="${one.note}">Request change</button>` : ""}</li>`);
1754
+ if (proof === null && reviewerNotes.length > 0)
1755
+ checkParts.push(html `<div class="result-section" data-cockpit-source="reviewer"><strong>Previous assessment</strong><ul>${reviewerNotes}</ul></div>`);
1756
+ if (run.outcome === "no-change" && !directAssessment)
1757
+ checkParts.push(html `<p class="meta">The build concluded that no repository change was needed. A no-change conclusion owes no proof — its handoff and machine-captured diff are the record.</p>`);
1758
+ if (proof === null) {
1759
+ if (run.outcome !== "no-change")
1760
+ checkParts.push(html `<p class="meta" data-proof-verdict="none">This build has no verification result or captured evidence. Review its recorded changes yourself.</p>`);
1761
+ }
1762
+ else {
1763
+ if (proof.verdict !== null && !directAssessment) {
1764
+ // The same status line the panel leads with (one status per surface,
1765
+ // workspace package 1), beside the machine verdict's own explanation.
1766
+ checkParts.push(html `<p class="row result-verdict" data-proof-verdict="${dispatchStatusToken(proof.verdict)}"><span class="meta">At completion: ${humanReview ? "Human review required for the requirements below." : verificationExplanation(proof.verdict, proof.reasons)}</span></p>`);
1767
+ }
1768
+ else if (proof.verdict === null && run.outcome !== "no-change") {
1769
+ checkParts.push(html `<p class="meta" data-proof-verdict="none">No verification result is available for this build.</p>`);
1770
+ }
1771
+ if (proof.accepted !== null)
1772
+ checkParts.push(html `<p class="meta">${humanReview ? "Accepted after human review" : "Accepted with an exception"} by <span class="mono">${proof.accepted.by}</span> · ${whenTime(proof.accepted.at)}${proof.accepted.note === null ? "" : ` — ${proof.accepted.note}`}</p>`);
1773
+ if (proof.matrix.length === 0) {
1774
+ checkParts.push(detail.signedCriteria > 0 ? html `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : html `<p class="meta">This scope signed no acceptance checks.</p>`);
1775
+ }
1776
+ else {
1777
+ checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
1778
+ }
1779
+ checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
1780
+ if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
1781
+ checkParts.push(html `<p class="meta">An independent review found conflicting evidence.</p>`);
1782
+ checkParts.push(repairChainHtml(proof.repairChain));
1783
+ if (detail.reviewerFindings.length > 0) {
1784
+ checkParts.push(html `<div class="result-section" data-cockpit-source="reviewer"><strong>Independent review</strong><ul>${reviewerNotes}</ul></div>`);
1785
+ }
1786
+ if (proof.proofProblem !== null)
1787
+ checkParts.push(html `<p class="problem">Verification details are unavailable: ${proof.proofProblem}</p>`);
1788
+ checkParts.push(proof.checkLog === null ? html `<p class="meta" data-cockpit-source="machine">${current?.receipt?.checks.level === "off" ? "Checks were off for this build." : "No automated check was configured for this build."}</p>` : checkLogHtml(proof.checkLog, runId, html ` data-cockpit-source="machine"`));
1789
+ checkParts.push(proof.proof === null || proof.proof.checks.length === 0
1790
+ ? directAssessment ? html `` : html `<p class="meta" data-cockpit-source="agent">The agent reported no checks.</p>`
1791
+ : html `<details class="cockpit-proof-group" data-cockpit-source="agent"><summary>Agent checks · ${proof.proof.checks.length}</summary><div class="result-section"><ul>${proof.proof.checks.map(one => html `<li><span class="mono">${one.command}</span> <span class="meta">(exit ${one.exitCode}) — ${one.summary}</span></li>`)}</ul></div></details>`);
1792
+ const screenshotRequired = proof.matrix.some(one => one.requiredEvidence.includes("screenshot"));
1793
+ checkParts.push(shots.length === 0
1794
+ ? directAssessment && !screenshotRequired ? html `` : html `<p class="meta" data-cockpit-source="screenshots">No screenshots${screenshotRequired ? " were captured, although one was required" : " were needed"}.</p>`
1795
+ : lead === "screenshots"
1796
+ ? html `<p class="meta" data-cockpit-source="screenshots">${shown.length} validated screenshot${shown.length === 1 ? "" : "s"} shown in Summary${unavailableShots.length > 0 ? `; ${unavailableShots.length} unavailable` : ""}.</p>`
1797
+ : html `<details class="cockpit-proof-group" data-cockpit-source="screenshots"><summary>Screenshots · ${shown.length}${unavailableShots.length > 0 ? ` (${unavailableShots.length} unavailable)` : ""}</summary><div class="receipt-visuals" aria-label="validated screenshots">${shown.map(shot => html `<a class="receipt-shot" href="/r/${runId}/evidence/${shot.artifactId}"><img src="/r/${runId}/evidence/${shot.artifactId}" alt="${shot.caption}"><span>${shot.caption}</span></a>`)}</div></details>`);
1798
+ checkParts.push(receipt.caveats.length === 0
1799
+ ? directAssessment ? html `` : html `<p class="meta" data-cockpit-source="caveats">No caveats were reported.</p>`
1800
+ : html `<details class="cockpit-proof-group" data-cockpit-source="caveats"><summary>Agent caveats · ${receipt.caveats.length}</summary><ul>${receipt.caveats.map(one => html `<li>${one}</li>`)}</ul></details>`);
1801
+ }
1802
+ if (followUps !== null && run.role === "builder")
1803
+ checkParts.push(followUpsHtml(followUps, runId, o));
1804
+ // The handoff's own account of its checks — the agent's words, labeled
1805
+ // as such, whether or not a proof exists.
1806
+ if (!directAssessment && handoff !== null && handoff.verification.length > 0)
1807
+ checkParts.push(html `<div class="result-section" data-cockpit-source="agent-words"><strong>The agent's own account</strong><ul>${handoff.verification.map(one => html `<li>${one}</li>`)}</ul></div>`);
1808
+ if (o.extraChecks !== undefined)
1809
+ checkParts.push(o.extraChecks);
1810
+ // ---- request changes: both feedback styles, one sealed road ------------
1811
+ const requestParts = [html `<h3 class="so-sr-only">What should change?</h3>`];
1812
+ const pathWords = (path, line) => {
1813
+ const shownPath = `${path}${line === null ? "" : `:${line}`}`;
1814
+ const href = detail.editor === null ? null : editorFileHref(detail.editor.worktree, path, line);
1815
+ return href === null ? html `<span class="mono">${shownPath}</span> ` : html `<a class="mono" href="${href}">${shownPath}</a> `;
1816
+ };
1817
+ if (detail.comments.length > 0) {
1818
+ requestParts.push(html `<p class="meta">Saved for later · ${detail.comments.length}</p><div class="diff-comments" data-result-notes="${detail.comments.length}">${detail.comments.map(one => html `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div>`);
1819
+ if (o.csrf !== "" && !detail.canAnnotate) {
1820
+ requestParts.push(postForm(`/r/${runId}/revise`, html `${revisionSealFields(detail.comments, detail.sourceDigest)}<div><strong>${detail.comments.length} note${detail.comments.length === 1 ? "" : "s"} ready</strong></div><button type="submit">Request changes</button>`, { attrs: { class: "card revision-from-comments" }, returnTo: o.returnTo }));
1821
+ }
1822
+ }
1823
+ if ((detail.pastComments?.length ?? 0) > 0) {
1824
+ requestParts.push(html `<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => html `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`)}</div></details>`);
1825
+ }
1826
+ for (const revision of detail.revisions) {
1827
+ // The child's standing is the shared status projection's own words
1828
+ // (repair 2026-09-14): current exact approval and actual activity.
1829
+ requestParts.push(html `<p class="result-revision" data-result-revision="${revision.id}" data-result-revision-approved="${revision.approved ? "1" : "0"}" data-tone="${revision.tone}"><strong>Revision</strong> <a href="${taskHref(revision.id)}">${revision.title}</a> <span class="meta">· ${revision.standing}</span></p>`);
1830
+ }
1831
+ if (o.csrf === "") {
1832
+ requestParts.push(html `<p class="meta">Sign in with a browser session to request changes.</p>`);
1833
+ }
1834
+ else if (!detail.canAnnotate) {
1835
+ requestParts.push(html `<p class="meta" data-result-feedback="unavailable">${terminal === null ? "This result has no sealed diff to attach notes to." : "The sealed diff no longer verifies, so notes cannot attach to it."} <a href="${taskChatHref(detail.rootId ?? detail.taskId)}">Discuss in chat →</a></p>`);
1836
+ }
1837
+ else {
1838
+ requestParts.push(html `<details class="result-request-open result-request-form"${detail.comments.length > 0 ? " open" : ""}><summary>What should change?</summary>${postForm(`/r/${runId}/comment`, joinHtml([
1839
+ revisionSealFields(detail.comments, detail.sourceDigest),
1840
+ html `<input type="hidden" data-recorded-requests value="${[...detail.comments, ...(detail.pastComments ?? [])].filter(one => one.sourceKey?.startsWith(`review:${o.user}:`)).map(one => one.sourceKey.slice(`review:${o.user}:`.length)).join(",")}">`,
1841
+ html `<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Describe the change…" aria-label="review comment" aria-describedby="comment-note-limit"${o.noted ? " autofocus" : ""}></textarea>`,
1842
+ html `<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>`,
1843
+ html `<details class="result-pin"><summary>Attach to a file or line</summary>`,
1844
+ html `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>`,
1845
+ html `<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>`,
1846
+ html `<div class="result-feedback-actions"><button type="submit" name="intent" value="revise" data-request-changes>Request changes</button><button type="submit" name="intent" value="note" class="quiet" data-save-feedback>Save for later</button></div>`,
1847
+ ]), { attrs: { class: "diff-comment-form", id: "comment-form" }, returnTo: o.returnTo, hidden: { tab: o.tab, request: o.requestToken } })}</details>`);
1848
+ }
1849
+ // ---- the panel ---------------------------------------------------------
1850
+ const tabCounts = {
1851
+ summary: "",
1852
+ changes: statOk ? (stat.fileCount === 0 ? "none" : `${stat.fileCount} file${stat.fileCount === 1 ? "" : "s"}`) : terminal === null ? "none" : "",
1853
+ checks: "",
1854
+ };
1855
+ const tabs = html `<nav class="result-tabs" role="tablist" aria-label="result views">${RESULT_TABS.map(tab => html `<a role="tab" href="${tabHref(tab.key)}" data-result-tab="${tab.key}" aria-selected="${tab.key === o.tab ? "true" : "false"}"${tab.key === o.tab ? "" : html ` tabindex="-1"`}>${tab.key === "checks" && proof?.matrix.some(row => row.assessment !== undefined) ? "Requirements" : tab.label}${tabCounts[tab.key] === "" ? "" : html `<span class="count">${tabCounts[tab.key]}</span>`}</a>`)}</nav>`;
1856
+ const view = (tab, parts) => html `<div class="result-view" role="tabpanel" data-result-view="${tab}"${tab === o.tab ? "" : " hidden"}>${joinHtml(parts, "\n")}</div>`;
1857
+ const heading = receiptHeadingOf(run.outcome, receipt.publication, run.role);
1858
+ const panelHtml = joinHtml([
1859
+ html `<section class="card result-panel" id="result" data-result-panel data-result-place="${o.place}" data-result-lead="${lead}" data-result-task="${detail.rootId ?? detail.taskId}" data-result-user="${o.user}"${attributes(resultFactsAttributeMap(facts))}>`,
1860
+ o.back === null ? "" : html `<p class="result-back"><a href="${o.back.href}" data-result-back>← ${o.back.label}</a></p>`,
1861
+ (detail.history ?? ""),
1862
+ // One headline: with the shared status, its words lead (task-status.ts); "Changes saved" would compete with them.
1863
+ taskStatus !== null && o.headStatus !== false
1864
+ ? html `<header class="result-head"><div data-headline-tone="${taskStatus.tone}"><h2 class="status-headline" data-work-status="${status.token}"><i aria-hidden="true"></i>${taskStatus.headline}</h2><p class="status-sentence">${taskStatus.sentence}</p></div></header>`
1865
+ : html `<header class="result-head"><div><h2>${heading}</h2></div>${o.headStatus === false ? "" : statusLineHtml(status)}</header>`,
1866
+ html `<p class="result-summary">${outcome}</p>`,
1867
+ current == null || o.place === "review" && (current.state === "ready-to-check" || current.state === "complete") ? "" :
1868
+ html `<div class="verdict" data-current-outcome data-headline="${taskStatus.headline}">${statusDetailsHtml(taskStatus)}${statusWhyHtml(taskStatus)}</div>`,
1869
+ attentionHtml,
1870
+ youCheckHtml,
1871
+ action,
1872
+ REVIEW_TOKENS.has(status.token) ? html `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${status.token}">${status.detail}</p></details>` : "",
1873
+ tabs,
1874
+ view("summary", summaryParts),
1875
+ view("changes", changeParts),
1876
+ view("checks", checkParts),
1877
+ (detail.learning ?? ""),
1878
+ detail.skillTest ? "" : html `<section class="result-request" id="request-changes">${joinHtml(requestParts, "\n")}</section>`,
1879
+ html `</section>`,
1880
+ ]);
1881
+ const panel = {
1882
+ attributes: { "data-result-panel": "", "data-result-place": o.place, "data-result-lead": lead, "data-result-task": detail.rootId ?? detail.taskId, "data-result-user": o.user, ...resultFactsAttributeMap(facts) },
1883
+ heading, outcome,
1884
+ status: taskStatus,
1885
+ reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
1886
+ attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
1887
+ youCheck,
1888
+ // The same counts as the Requirements row (a refuted report verifies none of them, and says Unverified).
1889
+ requirements: proof === null || proof.verdict === "refuted" ? null : requirementsOf(proof.matrix),
1890
+ limits: attention.filter(one => shortenedMaterialReason(one)),
1891
+ tabs: RESULT_TABS.map(tab => ({ key: tab.key, label: tab.key === "checks" && proof?.matrix.some(row => row.assessment !== undefined) ? "Requirements" : tab.label, count: tabCounts[tab.key], href: o.hrefFor(tab.key), active: tab.key === o.tab })),
1892
+ views: [{ key: "summary", html: htmlString(joinHtml(summaryParts, "\n")) }, { key: "changes", html: htmlString(joinHtml(changeParts, "\n")) }, { key: "checks", html: htmlString(joinHtml(checkParts, "\n")) }],
1893
+ history: detail.history === undefined ? "" : htmlString(detail.history),
1894
+ learning: detail.learning === undefined ? "" : htmlString(detail.learning),
1895
+ request: detail.skillTest ? null : htmlString(joinHtml(requestParts, "\n")),
1896
+ canRequest: detail.canAnnotate && o.csrf !== "",
1897
+ need,
1898
+ requestQuiet: detail.canAnnotate && o.csrf !== "" && detail.comments.length === 0 && (detail.pastComments?.length ?? 0) === 0 && detail.revisions.length === 0,
1899
+ };
1900
+ return { html: panelHtml, panel };
1901
+ }
1902
+ /** Changed lines shown beside one "You check this one" item: at most this many per file, three files. */
1903
+ export const CHECK_EXCERPT_LINES = 12;
1904
+ /** Each requirement only a person can confirm, with the evidence to judge
1905
+ * it by, inline: the changed lines in the files it cites (citing none, the
1906
+ * change's first file, labelled as not cited), the screenshots it cites
1907
+ * (none cited: every validated one), and the agent's own note. */
1908
+ export function personCheckItems(proof, patchText, shots, runId) {
1909
+ const rows = proof.matrix.filter(row => row.state === "manual-review");
1910
+ if (rows.length === 0)
1911
+ return proof.reasons.some(reason => manualReviewCriterionOf(reason) !== null)
1912
+ ? [{ id: "you-check", statement: "", words: personCheckWords(null), note: null, excerpts: [], shots: [] }] : [];
1913
+ const diff = patchText === null ? null : parseReviewDiff(patchText);
1914
+ const excerptOf = (file, cited) => {
1915
+ const changed = file.hunks.flatMap(hunk => hunk.lines).filter((line) => line.kind === "addition" || line.kind === "deletion");
1916
+ return { path: file.path, cited, lines: changed.slice(0, CHECK_EXCERPT_LINES).map(line => ({ kind: line.kind, line: line.newLine ?? line.oldLine, text: line.text })), more: Math.max(0, changed.length - CHECK_EXCERPT_LINES) };
1917
+ };
1918
+ return rows.map(row => {
1919
+ const refs = row.answered ?? [];
1920
+ const paths = refs.filter(one => one.kind === "changed-path").map(one => one.ref);
1921
+ const files = diff === null ? [] : paths.length > 0 ? diff.files.filter(file => paths.includes(file.path)).map(file => excerptOf(file, true))
1922
+ : diff.files.slice(0, 1).map(file => excerptOf(file, false));
1923
+ const cited = refs.filter(one => one.kind === "screenshot").map(one => one.ref);
1924
+ const pictures = cited.length > 0 ? shots.filter(shot => cited.includes(shot.path)) : shots;
1925
+ const notes = refs.filter(one => one.kind === "manual-review").map(one => one.ref.trim()).filter(one => one !== "");
1926
+ return {
1927
+ id: row.id, statement: row.statement, words: personCheckWords(row.statement), note: notes.length === 0 ? null : notes.join(" "),
1928
+ excerpts: files.filter(one => one.lines.length > 0).slice(0, 3),
1929
+ shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption })),
1930
+ };
1931
+ });
1932
+ }
1933
+ /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
1934
+ * stopped behind the password. Null under every other headline. */
1935
+ export function needActionOf(assignment, status, csrf, returnTo, result) {
1936
+ if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
1937
+ return null;
1938
+ const action = assignment.primaryAction;
1939
+ const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
1940
+ // Built to an earlier plan: Build again, in place (the task page's requeue).
1941
+ if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
1942
+ return { label: status.need.action.label, ...rebuild };
1943
+ if (result !== undefined) {
1944
+ if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
1945
+ return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
1946
+ accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
1947
+ // The task page sends the person here: never back to it. What this page can do leads — Accept
1948
+ // a check only a person can make, or Build again a result that may run again.
1949
+ const here = (action?.code === "open-result" || action?.code === "inspect-run") && action.target.runId === result.run;
1950
+ if (here && csrf !== "" && !result.accepted && result.humanReview)
1951
+ return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
1952
+ if (here && csrf !== "" && result.rebuildable === true)
1953
+ return { label: NEEDS.rebuild.action.label, ...rebuild };
1954
+ // Nothing here resolves it: no act at all, never a link back to the task (which sends the person here).
1955
+ if (status.need.key === "review-result" || here)
1956
+ return null;
1957
+ }
1958
+ const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
1959
+ ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
1960
+ return { label: status.need.action.label, href: assignmentActionHref(assignment) ?? taskHref(assignment.rootId), confirm };
1961
+ }
1962
+ /** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
1963
+ export const needsCheck = (assignment) => assignment.need != null && "key" in assignment.need && assignment.need.key === "check-stopped";
1964
+ /** The server page's form of the same: the need's action first, Request changes as the quiet second. */
1965
+ export function resultNeedAction(need, o, canRequest) {
1966
+ const control = need.accept != null
1967
+ ? postForm(need.accept.action, html `${need.accept.note === null ? "" : html `<input type="text" name="note" maxlength="500" required placeholder="${need.accept.note}" aria-label="${need.accept.note}">`}<button type="submit" data-primary-action data-accept-result style="min-height:44px">${need.label}</button>`, { attrs: { class: "accept-result" }, returnTo: need.accept.returnTo, hidden: { run: need.accept.run } })
1968
+ : need.rebuild != null
1969
+ ? postForm(need.rebuild.action, html `<button type="submit" data-primary-action data-rebuild style="min-height:44px">${need.label}</button>`, { attrs: { class: "rebuild" } })
1970
+ : need.confirm !== null
1971
+ ? postForm(need.confirm.action, html `${need.confirm.checked === true ? html `<label><input type="checkbox" name="checked" value="yes" required> Nothing from build #${need.confirm.run} is running</label>` : ""}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><button type="submit" style="min-height:44px">${need.label}</button>`, { attrs: { id: "confirm-stopped", class: "confirm-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
1972
+ : html `<a class="button-link" href="${need.href ?? "#"}" data-primary-action>${need.label}</a>`;
1973
+ return html `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? html `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
1974
+ }
1975
+ /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
1976
+ * first sentence, cut at a word when it runs past 180 characters. The
1977
+ * full conclusion stays available behind the Summary's disclosure. */
1978
+ export function conciseOutcomeOf(conclusion) {
1979
+ const flat = oneLineOf(conclusion, 2_000);
1980
+ const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
1981
+ const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
1982
+ return bounded.trim();
1983
+ }
1984
+ /** Exactly one primary act for the result (repair 2026-09-14), chosen from
1985
+ * its state: draft the CI repair, create the revision the notes are
1986
+ * waiting for, open the pull request, or request changes. A bearer session
1987
+ * (no token) gets no control; the cockpit renders its own next-action
1988
+ * card instead (`action: false`). */
1989
+ export function resultPrimaryAction(detail, o, prUrl) {
1990
+ const wrap = (kind, control) => html `<div class="result-action" data-result-action="${kind}">${control}</div>`;
1991
+ if (detail.ciFailing && o.csrf !== "") {
1992
+ return wrap("draft-repair", postForm(`/r/${detail.run.id}/draft-repair`, html `<button type="submit">Draft a repair task</button>`));
1993
+ }
1994
+ if (detail.comments.length > 0 && o.csrf !== "")
1995
+ return wrap("revise", html `<a class="result-feedback-link" href="#request-changes">Review saved notes</a>`);
1996
+ if (detail.publication !== null && detail.publication.prNumber !== null && prUrl !== null)
1997
+ return wrap("open-pr", html `<a class="button-link" href="${prUrl}">Open PR #${detail.publication.prNumber}</a>`);
1998
+ if (detail.assignment == null && detail.proof?.matrix.some(row => row.assessment !== undefined) && detail.proof.reasons.includes(GOAL_ASSESSMENT_PENDING))
1999
+ return wrap("open-result", html `<a class="button-link" href="${reviewHref(detail.taskId)}">Open result</a>`);
2000
+ if (detail.assignment == null && detail.proof?.matrix.some(row => row.assessment !== undefined && row.review?.judgement === "cannot-tell"))
2001
+ return wrap("review-evidence", html `<a class="button-link" href="${o.hrefFor("checks")}">Review evidence</a>`);
2002
+ if (detail.canAnnotate && o.csrf !== "")
2003
+ return wrap("request-changes", html `<a class="result-feedback-link" href="#request-changes">Request changes</a>`);
2004
+ return html ``;
2005
+ }
2006
+ /** The run's facts as rows — one renderer for the page and its live
2007
+ * fragment (A4). Elapsed ticks client-side while the run is open. */
2008
+ /** The run's route provenance in plain words (v47): the exact agent it
2009
+ * spent as, in which role, and how that agent was chosen. */
2010
+ export function runAgentWords(route) {
2011
+ if (route === null || route === undefined)
2012
+ return null;
2013
+ const role = route.phase === "plan" ? "planner" : route.phase === "build" ? "builder" : route.phase === "repair" ? "repair" : "reviewer";
2014
+ const how = route.chosen === "fallback"
2015
+ ? "the approved fallback"
2016
+ : route.chosen === "override"
2017
+ ? "chosen by an approver"
2018
+ : route.chosen === "pinned"
2019
+ ? "pinned"
2020
+ : route.chosen === "legacy"
2021
+ ? "from the approved profile"
2022
+ : "recommended";
2023
+ return `${route.provider}${route.model === null ? "" : ` · ${route.model}`} as the ${role} — ${how}`;
2024
+ }
2025
+ export function runFactsRows(run, taskId, live, route = null) {
2026
+ const facts = [
2027
+ ["task", taskId, true],
2028
+ ["role", run.role],
2029
+ ["agent", runAgentWords(route)],
2030
+ ["quality", qualityModeTitle(run.qualityMode ?? "default")],
2031
+ ["outcome", live ? "running" : (run.outcome ?? "never finished")],
2032
+ ["phase", live && run.phase !== null ? phaseWords(run.phase) : null],
2033
+ ["reason", run.reason === null ? null : isInternalErrorReason(run.reason) ? "stopped with an internal error (below)" : reasonWords(run.reason)],
2034
+ ["runner", run.runner, true],
2035
+ ["branch", run.branch, true],
2036
+ ["model", run.model, true],
2037
+ ["starting point", run.baseRevision === null ? null : `${run.baseRevision.slice(0, 12)}…`, true],
2038
+ ["ended at", run.headRevision === null ? null : `${run.headRevision.slice(0, 12)}…`, true],
2039
+ ["started", when(run.startedAt), true],
2040
+ ["finished", when(run.finishedAt), true],
2041
+ ["provider started", when(run.providerStartedAt), true],
2042
+ ["tokens in", run.tokensIn === null ? null : run.tokensIn.toLocaleString(), true],
2043
+ ["tokens out", run.tokensOut === null ? null : run.tokensOut.toLocaleString(), true],
2044
+ // Auth is part of the economic fact: Claude's subscription harness
2045
+ // reports an API-price equivalent, not a separate API-key charge.
2046
+ [
2047
+ "usage",
2048
+ run.providerStartedAt === null && run.tokensIn === null && run.tokensOut === null && run.costUsd === null ? null : runCostWords(run, live),
2049
+ run.costUsd !== null && run.authMode !== "subscription",
2050
+ ],
2051
+ ];
2052
+ const elapsed = live && run.startedAt !== null
2053
+ ? html `<p class="row"><span class="meta" style="min-width:8.5rem">elapsed</span> <time class="mono" data-elapsed-since="${run.startedAt}"></time></p>`
2054
+ : "";
2055
+ return html `${joinHtml(facts
2056
+ .filter((fact) => fact[1] !== null && fact[1] !== "")
2057
+ .map(([label, value, mono]) => html `<p class="row"><span class="meta" style="min-width:8.5rem">${label}</span> <span${mono === true ? html ` class="mono"` : ""}>${value}</span></p>`), "\n")}${elapsed}${
2058
+ // Machine output (a stack trace, a path, "Error:" text) reads as an internal error everywhere else; its detail is here, as recorded.
2059
+ run.reason !== null && isInternalErrorReason(run.reason)
2060
+ ? html `<p class="row"><span class="meta" style="min-width:8.5rem">recorded error</span></p><pre class="mono" id="run-reason-detail" style="white-space:pre-wrap;overflow-wrap:anywhere">${run.reason}</pre>`
2061
+ : ""}`;
2062
+ }
2063
+ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
2064
+ const rows = runFactsRows(run, taskId, running, route);
2065
+ // The live peek region (A2): the poller fills it only on a serve that
2066
+ // asserted its runner. Without the assertion the section still appears
2067
+ // for a running build and says honestly why it is empty \u2014 a page that
2068
+ // silently lacked the region while the task screen promised a live view
2069
+ // read as broken (round-4, A1).
2070
+ const peek = !running
2071
+ ? ""
2072
+ : peekable
2073
+ ? html `<h2>What is changing right now</h2><div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div><p class="meta" id="run-peek-stamp"></p>`
2074
+ : html `<h2>What is changing right now</h2><p class="meta">The live file view is off \u2014 start serve with ${"--runner <name>"} naming this machine's worker, and it appears here</p>`;
2075
+ // The live transcript (arc 1): the agent's own words, streamed to a file
2076
+ // beside the run and polled as raw text. Honesty stated on the surface:
2077
+ // this is display only, and the machine running the agent could alter it.
2078
+ const transcript = !running
2079
+ ? ""
2080
+ : !peekable
2081
+ ? html `<h2>What the agent is saying</h2><p class="meta">The live transcript is off \u2014 start serve with ${"--runner <name>"} naming this machine's worker, and it appears here</p>`
2082
+ : run.provider !== "claude"
2083
+ ? html `<h2>What the agent is saying</h2><p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${run.provider}</p>`
2084
+ : html `<h2>What the agent is saying</h2><p class="meta">Display only \u2014 this is not evidence, and the machine running the agent could alter it</p><pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre><p class="meta" id="live-transcript-state"></p>`;
2085
+ const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
2086
+ const resultList = (label, items) => items.length === 0
2087
+ ? ""
2088
+ : html `<div class="result-section"><strong>${label}</strong><ul>${items.map(one => html `<li>${one}</li>`)}</ul></div>`;
2089
+ const handoff = resultSummary === null
2090
+ ? ""
2091
+ : html `<section class="card result-card"><h2>Result</h2><p class="recap">${resultSummary}</p>${structuredHandoff === null
2092
+ ? ""
2093
+ : [resultList("completed", structuredHandoff.changes),
2094
+ resultList("checks reported by the agent", structuredHandoff.verification),
2095
+ resultList("follow-up", structuredHandoff.followUps)]}</section>`;
2096
+ const evidence = artifacts.length === 0
2097
+ ? ""
2098
+ : html `<details class="evidence-files"><summary>Saved files <span class="meta">(${artifacts.length})</span></summary><ul>${artifacts
2099
+ .map(artifact => html `<li><a href="/r/${run.id}/evidence/${artifact.id}">${evidenceWords(artifact.kind)}</a> <span class="meta" title="${artifact.bytesStored} bytes stored${artifact.truncated ? ` of ${artifact.bytesOriginal}` : ""}">${artifact.truncated ? `${humanBytes(artifact.bytesStored)} of ${humanBytes(artifact.bytesOriginal)} · partial` : humanBytes(artifact.bytesStored)}</span></li>`)}</ul></details>`;
2100
+ // Review comments on the immutable terminal diff (M6.8): listed, added,
2101
+ // and sealed into ONE unapproved revision task. The seal is deliberately
2102
+ // plain — the ceremony lives on the revision task's approval screen,
2103
+ // which restates the batch; this button only creates the unapproved task.
2104
+ const hasTerminalDiff = terminal !== null && terminal.patch !== null && !("problem" in terminal.patch);
2105
+ const commentPathWords = (path, line) => {
2106
+ const shown = `${path}${line === null ? "" : `:${line}`}`;
2107
+ const href = editor === null ? null : editorFileHref(editor.worktree, path, line);
2108
+ return href === null
2109
+ ? html `<span class="mono">${shown}</span> `
2110
+ : html `<a class="mono" href="${href}">${shown}</a> `;
2111
+ };
2112
+ const commentRows = comments
2113
+ .map(one => html `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : commentPathWords(one.path, one.line)}${one.note}</p><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span></div>`);
2114
+ const commentForm = csrf === "" || !hasTerminalDiff
2115
+ ? null
2116
+ : postForm(`/r/${run.id}/comment`, html `<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label><label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div><label>Change requested<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Explain what should change and why" aria-label="review comment" aria-describedby="comment-note-limit"${noted ? " autofocus" : ""}></textarea></label><span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span><button type="submit">Add annotation</button>`, { attrs: { class: "card diff-comment-form", id: "comment-form" } });
2117
+ // The device-side half of the editor-link activation (arc 6, finding 1):
2118
+ // rendered only when the server capability exists and this run belongs
2119
+ // to this machine's runner — the person at the browser flips it.
2120
+ const editorToggleForm = editorToggle === null || csrf === ""
2121
+ ? ""
2122
+ : postForm("/session/editor-links", html `<button type="submit">${editorToggle.on ? "stop opening files in VS Code from this device" : "open files in VS Code from this device"}</button><span class="meta"> — only useful when this browser runs on the machine that holds the worktrees</span>`, { attrs: { class: "row" }, returnTo: `/r/${run.id}`, hidden: { on: editorToggle.on ? "0" : "1" } });
2123
+ const reviseForm = csrf === "" || comments.length === 0
2124
+ ? ""
2125
+ : postForm(`/r/${run.id}/revise`, html `${revisionSealFields(comments, sourceDigest)}<div><strong>${comments.length} annotation${comments.length === 1 ? "" : "s"} ready</strong><span class="meta">Creates one revision carrying this exact batch. You review its scope before anything builds.</span></div><button type="submit">Create revision from annotations</button>`, { attrs: { class: "card revision-from-comments" } });
2126
+ // CI repair, suggestion-first (M8.18): the observed red episode earns a
2127
+ // button; the button drafts ONE unapproved task; a person approves it.
2128
+ const repairCard = ciRepair === null || csrf === ""
2129
+ ? ""
2130
+ : html `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>${postForm(`/r/${run.id}/draft-repair`, html `<button type="submit">Draft a repair task</button><span class="meta"> — one unapproved task; you approve its scope before anything builds</span>`)}</div>`;
2131
+ const reviewCard = html `${commentRows.length === 0 && commentForm === null ? "" : html `<h2 id="review">Review and revise</h2><p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>${commentRows.length === 0 ? "" : html `<div class="diff-comments">${joinHtml(commentRows, "\n")}</div>`}${commentForm}${reviseForm}${editorToggleForm}`}${repairCard}`;
2132
+ const noteRows = joinHtml(notes.map(one => html `<p class="row"><span class="meta">${one.author} · ${whenTime(one.createdAt)}</span> ${one.note}</p>`), "\n");
2133
+ const noteForm = csrf === ""
2134
+ ? null
2135
+ : postForm(`/r/${run.id}/note`, html `<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem"><button type="submit">Add note</button>`, { attrs: { class: "row" } });
2136
+ const notesCard = notes.length === 0 && noteForm === null ? "" : html `<h2>Operator notes</h2>${noteRows}${noteForm}`;
2137
+ // Outcome first (UI polish 2026-09-13): a finished build leads with its
2138
+ // result, proof, and diff; the machine facts fold under "Build details".
2139
+ // A live build keeps the facts open — they are what a watcher polls.
2140
+ const facts = running
2141
+ ? html `<div id="run-facts">${rows}</div><p class="meta" id="run-facts-stamp"></p>`
2142
+ : html `<details class="run-facts-details"><summary>Run record<span class="meta">${[run.runner, run.provider, run.outcome ?? "never finished"].join(" · ")}</span></summary><div id="run-facts">${rows}</div></details>`;
2143
+ // A finished result (package 3) is the ONE shared panel — Summary /
2144
+ // Changes / Checks with Request changes beside it — the same markup the
2145
+ // review cockpit and the chat's result view render. Every other run
2146
+ // (live, failed, interrupted) keeps its record-by-record page.
2147
+ const resultPanel = result === null
2148
+ ? null
2149
+ : resultPanelHtml(result.detail, {
2150
+ place: "run",
2151
+ tab: result.tab,
2152
+ csrf,
2153
+ user: result.user,
2154
+ noted,
2155
+ requestToken: result.requestToken,
2156
+ hrefFor: one => (one === "summary" ? `/r/${run.id}` : `/r/${run.id}?tab=${one}`),
2157
+ returnTo: `/r/${run.id}`,
2158
+ back: null,
2159
+ });
2160
+ return screen(`build #${run.id}`, joinHtml([
2161
+ html `<h1>Build #${run.id} <span class="meta"><a href="${taskHref(taskId)}">${taskId}</a></span></h1>`,
2162
+ running ? facts : "",
2163
+ transcript,
2164
+ peek,
2165
+ ...(resultPanel === null
2166
+ ? [
2167
+ handoff,
2168
+ evidenceBundleCard(proofBundle, run, publicationFactsOf(publicationRow), review),
2169
+ terminal === null ? "" : terminalDiffCard(terminal, run.id, editor, commentForm !== null),
2170
+ reviewCard,
2171
+ ]
2172
+ : [resultPanel, editorToggleForm]),
2173
+ running ? "" : facts,
2174
+ evidence,
2175
+ notesCard,
2176
+ ], "\n"), {
2177
+ chrome,
2178
+ // One composed functional script (arc 4 contract): the pollers when the
2179
+ // run is live, the result panel's tabs/draft script or the legacy
2180
+ // comment prefill when a form exists. Neither fetches — they earn
2181
+ // neither connect-src nor the noscript refresh.
2182
+ ...(liveScript === undefined && commentForm === null && resultPanel === null
2183
+ ? {}
2184
+ : {
2185
+ functional: {
2186
+ script: (liveScript ?? "") + (resultPanel !== null ? RESULT_REVIEW_SCRIPT : commentForm === null ? "" : prefillScript()),
2187
+ fetches: liveScript !== undefined,
2188
+ },
2189
+ }),
2190
+ });
2191
+ }
2192
+ /**
2193
+ * Click-to-prefill (arc 6, finding 4): client-side FORM mutation, named as
2194
+ * such — normal viewing stays the default; "Annotate" reveals line pins.
2195
+ * A pin or file button copies its target into the comment form and focuses
2196
+ * the note field. No fetch, endpoint, or submit: comments still leave
2197
+ * through the same CSRF'd form POST. Reads data attributes, writes input
2198
+ * values, and flips presentational state — never markup. The note counter
2199
+ * (follow-up on build 1540) reads the textarea's own maxlength — the
2200
+ * server's LIMITS.note — into the helper text, so the two cannot drift.
2201
+ */
2202
+ export function prefillScript() {
2203
+ return (`(function(){var form=document.getElementById("comment-form");if(!form)return;var review=document.querySelector("[data-review-diff]");` +
2204
+ `var noteBox=form.querySelector("[name=note]"),limit=document.getElementById("comment-note-limit");if(noteBox&&limit&&noteBox.maxLength>0){` +
2205
+ `function tally(){limit.textContent=noteBox.value.length===0?"up to "+noteBox.maxLength+" characters":noteBox.value.length+" of "+noteBox.maxLength+" characters";}tally();noteBox.addEventListener("input",tally);}` +
2206
+ `if(review){review.setAttribute("data-mode","view");review.addEventListener("click",function(ev){` +
2207
+ `var mode=ev.target&&ev.target.closest?ev.target.closest("button[data-diff-mode]"):null;if(!mode)return;` +
2208
+ `var value=mode.getAttribute("data-diff-mode")==="annotate"?"annotate":"view";review.setAttribute("data-mode",value);` +
2209
+ `review.querySelectorAll("button[data-diff-mode]").forEach(function(one){one.setAttribute("aria-pressed",String(one===mode));});});}` +
2210
+ `document.addEventListener("click",function(ev){` +
2211
+ `var button=ev.target&&ev.target.closest?ev.target.closest("button.pick-file,button.pick-line"):null;if(!button)return;` +
2212
+ `var path=form.querySelector("[name=path]");var line=form.querySelector("[name=line]");var note=form.querySelector("[name=note]");` +
2213
+ `if(path)path.value=button.getAttribute("data-path")||"";` +
2214
+ `if(line)line.value=button.getAttribute("data-line")||"";` +
2215
+ `form.scrollIntoView({behavior:"smooth",block:"center"});if(note)note.focus();});})();`);
2216
+ }
2217
+ /** The primary review act opens the Checks view it names. The anchor
2218
+ * still lands on the result without JavaScript (the server honours
2219
+ * `?tab=checks`); this only removes the reload. */
2220
+ export function reviewEvidenceScript() {
2221
+ return (`(function(){var link=document.querySelector("[data-open-evidence]"),tab=document.querySelector('[data-result-tab="checks"]'),panel=document.getElementById("result");` +
2222
+ `if(!link||!tab||!panel)return;link.addEventListener("click",function(ev){ev.preventDefault();tab.click();panel.scrollIntoView({behavior:"smooth",block:"start"});});})();`);
2223
+ }
2224
+ /** The facts region alone, for the open-run poll (A4). A finished run's
2225
+ * fragment says so instead of quietly growing forms (finding 5), and a run
2226
+ * that stopped being the task's live claim says so too — both carry the
2227
+ * stop marker, so an open tab quits refetching a dead build (round-4
2228
+ * finding 15). */
2229
+ export function runFactsFragment(run, taskId, live, route = null) {
2230
+ if (run.outcome !== null) {
2231
+ return html `<p class="meta" data-region-stop>finished — <a href="/r/${run.id}">reload for the final record</a></p>`;
2232
+ }
2233
+ if (!live) {
2234
+ return html `<p class="meta" data-region-stop>this build stopped without finishing — <a href="/r/${run.id}">reload for the record</a></p>`;
2235
+ }
2236
+ return runFactsRows(run, taskId, live, route);
2237
+ }