toolroll 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serve.js CHANGED
@@ -58,11 +58,13 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
58
58
  import { workIndexPage, workCountsByProject, WorkIndexCursorError } from "./work-index.js";
59
59
  import { openWorkDecisionOf } from "./work-summary.js";
60
60
  import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
61
+ import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
61
62
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
62
63
  import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, publishingOf, saveMergeSettings, savePublishing, newestPullRequestOf, MERGE_METHODS } from './pull-request-flow.js';
63
64
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
64
65
  import { assignmentCatchUp } from './assignment-brief.js';
65
66
  import { assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
67
+ import { pullRequestFactOf, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
66
68
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
67
69
  import { ledgerBody } from "./ledger-view.js";
68
70
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -150,8 +152,8 @@ import { TEMPLATES, templateByName } from "./templates.js";
150
152
  import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, createWorkflowPreview, workflowPreview, launchWorkflow, saveWorkflowRecipe, RecipeError, prepareRecipeRun } from "./recipes.js";
151
153
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
152
154
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
153
- import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
154
- import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
155
+ import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
156
+ import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
155
157
  import { PRICED_BUILD_MODELS } from "./pricing.js";
156
158
  import { RESULT_TABS, RESULT_REVIEW_SCRIPT, isRevisionFeedback, evidenceResultStatusOf, evidenceShortenedWords, parseResultTab, resultLeadOf, evidenceProblemDetailsOf, evidenceHealthOf, resultFactsAttributes, resultFactsAttributeMap, resultReturnTarget, commentSourceKey, REQUEST_TOKEN, revisionBatchOf, parseRevisionBatch, revisionSourceOf, } from "./result-review.js";
157
159
  import { parseReport } from "./scout-report.js";
@@ -1727,6 +1729,7 @@ export function createDecisionServer(options) {
1727
1729
  url.pathname !== "/projects/browse" && url.pathname !== "/projects/github" && url.pathname !== "/workbench" &&
1728
1730
  url.pathname !== "/fleet" &&
1729
1731
  url.pathname !== "/chat" && url.pathname !== "/chat/mate/status" && url.pathname !== "/chat/task-status" && url.pathname !== "/chat/stream" &&
1732
+ !url.pathname.startsWith("/chat/demo/") &&
1730
1733
  !/^\/chat\/action\/[0-9]{1,15}$/.test(url.pathname) &&
1731
1734
  !url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
1732
1735
  !(url.pathname === "/board" && url.searchParams.get("scope") === "all");
@@ -3237,6 +3240,26 @@ export function createDecisionServer(options) {
3237
3240
  ...(fragments === null ? {} : { fragments }),
3238
3241
  }));
3239
3242
  }
3243
+ // `toolroll demo`: Chat is the scripted lead, never a model.
3244
+ if (url.pathname === "/chat/demo/live") {
3245
+ const lead = demoLeadHere();
3246
+ if (lead === null || who.via !== "cookie")
3247
+ return respond(response, 404, "application/json", JSON.stringify({ ok: false }));
3248
+ const exchanges = lead.exchanges();
3249
+ return respond(response, 200, "application/json", JSON.stringify({
3250
+ version: lead.version(), working: exchanges.some(one => one.state === "working"),
3251
+ html: demoThreadHtml(exchanges, who.session.csrf, demoResultView),
3252
+ }));
3253
+ }
3254
+ if (url.pathname === "/chat" && demoLeadHere() !== null && who.via === "cookie" &&
3255
+ !["task", "result", "project", "conversation", "team", "proposal"].some(name => url.searchParams.has(name))) {
3256
+ const lead = demoLeadHere();
3257
+ const said = url.searchParams.get("said")?.slice(0, 300) ?? null;
3258
+ return sendScreen(response, 200, screen("Chat", demoChatHtml({ exchanges: lead.exchanges(), csrf: who.session.csrf, version: lead.version(), problem: said, resultOf: demoResultView }), {
3259
+ chrome: chromeFor(null, "chat", undefined, "all"),
3260
+ functional: { script: DEMO_CHAT_SCRIPT, fetches: true },
3261
+ }));
3262
+ }
3240
3263
  if (url.pathname === "/chat") {
3241
3264
  // Cookie sessions only (Codex v3 review, change 7): drafts live in
3242
3265
  // THIS session's memory; a bearer caller has nowhere to keep them.
@@ -4146,6 +4169,42 @@ export function createDecisionServer(options) {
4146
4169
  /** The frozen explicit repo list, digested canonically (sorted) — every
4147
4170
  * candidate binds to it and filing re-proves it (v2 new finding 5). */
4148
4171
  const chatCeilingDigest = (repos = managedRepos()) => createHash("sha256").update([...repos].sort().join("\n")).digest("hex");
4172
+ /** The scripted lead answers only inside a demo database. */
4173
+ function demoLeadHere() {
4174
+ return options.demoLead !== undefined && store.isDemo() ? options.demoLead : null;
4175
+ }
4176
+ /** A finished demo exchange's result, read back from the evidence its run actually stored. */
4177
+ function demoResultView(exchange) {
4178
+ if (exchange.runId === null || exchange.taskId === null)
4179
+ return null;
4180
+ const artifacts = store.artifactsFor(exchange.runId);
4181
+ const text = (kind) => {
4182
+ const artifact = artifacts.find(one => one.kind === kind);
4183
+ if (artifact === undefined)
4184
+ return null;
4185
+ const read = readVerifiedArtifact(evidenceRoot, artifact);
4186
+ return read.ok ? read.content.toString("utf8") : null;
4187
+ };
4188
+ const shot = artifacts.find(one => one.kind === "screenshot");
4189
+ const stat = (() => { try {
4190
+ return JSON.parse(text("diff-stat") ?? "null");
4191
+ }
4192
+ catch {
4193
+ return null;
4194
+ } })();
4195
+ const assignment = assignmentOf(store, exchange.taskId, clock(), { principal: "operator", repos: managedRepos() }, evidenceRoot);
4196
+ const log = text("check-log");
4197
+ return {
4198
+ diff: text("terminal-diff"),
4199
+ checkLog: log === null ? null : log.slice(log.indexOf("$ ") === -1 ? 0 : log.indexOf("$ ")),
4200
+ checks: { status: assignment?.receipt?.checks.status ?? "unavailable", detail: assignment?.receipt?.checks.detail ?? "Saved checks are unavailable." },
4201
+ screenshot: shot === undefined ? null : { href: `/r/${exchange.runId}/evidence/${shot.id}`, caption: exchange.plan.screenshot.caption },
4202
+ additions: stat?.additions ?? 0,
4203
+ deletions: stat?.deletions ?? 0,
4204
+ files: stat?.fileCount ?? 0,
4205
+ taskHref: taskHref(exchange.taskId),
4206
+ };
4207
+ }
4149
4208
  /** Every condition re-proved per request — the render and the POST each
4150
4209
  * ask again; nothing is cached into authority. */
4151
4210
  function chatEnablement() {
@@ -4503,7 +4562,7 @@ export function createDecisionServer(options) {
4503
4562
  s = { ...s, workspace: { ...s.workspace, view: { ...s.workspace.view, stepUp: { label: ssoFacts.label, fresh: ssoFacts.fresh, confirmHref: `/login/sso?reauth=1&return=${encodeURIComponent(back)}` } } } };
4504
4563
  }
4505
4564
  const teamEntry = requestContext.getStore()?.returnTo;
4506
- if (teamEntry?.startsWith('/chat') && !s.workspace?.team && !teamEntry.includes('task=') && !teamEntry.includes('proposal=')) {
4565
+ if (teamEntry?.startsWith('/chat') && !s.workspace?.team && !teamEntry.includes('task=') && !teamEntry.includes('proposal=') && demoLeadHere() === null) {
4507
4566
  s.body = '<p class="team-entry"><a href="/chat?team=1">Open team chat</a></p>' + s.body;
4508
4567
  if (s.workspace?.conversation)
4509
4568
  s.workspace.controlsHtml = '<p><a href="/chat?team=1">Open team chat</a></p>' + (s.workspace.controlsHtml ?? '');
@@ -4524,7 +4583,7 @@ export function createDecisionServer(options) {
4524
4583
  const extras = s.workspace ?? {};
4525
4584
  const notices = [...(extras.notices ?? [])];
4526
4585
  if (s.chrome.demo)
4527
- notices.unshift('Demo workspace — synthetic tasks. External work is disabled.');
4586
+ notices.unshift(DEMO_BANNER);
4528
4587
  if (s.chrome.modeBanner)
4529
4588
  notices.push(s.chrome.modeBanner.words);
4530
4589
  let crew = { crew: [], crewTruncated: false };
@@ -4849,8 +4908,6 @@ export function createDecisionServer(options) {
4849
4908
  const result = receipt === undefined && task.state === "done" && latest !== null && (latest.outcome === "built" || latest.outcome === "no-change")
4850
4909
  ? completionReceiptView(store, latest, store.artifactsFor(latest.id), evidenceRoot, reviewFactsFor(latest.id)) : receipt;
4851
4910
  const status = workStatusOf(facts, result == null ? undefined : receiptStatusOf(result));
4852
- if (store.lookupRef(task.id)?.revisionOf !== null && live !== null && status.tone === "live")
4853
- status.label = "Revising";
4854
4911
  const earlierLive = task.family === undefined ? [] : earlierLiveVersions(task.family, now);
4855
4912
  if (earlierLive.length > 0 && !status.views.includes("running"))
4856
4913
  status.views = [...status.views, "running"];
@@ -5437,8 +5494,12 @@ export function createDecisionServer(options) {
5437
5494
  const publishing = publishingOf(store, repo);
5438
5495
  const target = publishing.on ? publishing.base : null;
5439
5496
  const view = newestPullRequestOf(store, family === null ? [taskId] : family.versions.map(one => one.id));
5440
- if (view !== null)
5441
- return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target };
5497
+ if (view !== null) {
5498
+ const publication = store.publicationForRun(view.runId);
5499
+ const fact = publication === null ? null : { ...pullRequestFactOf({ ...publication, merged: view.state === "merged" }),
5500
+ note: view.state === "merged" ? `${view.detail}${view.mergeCommit === null ? "" : ` Merge commit ${view.mergeCommit.slice(0, 12)}.`}` : null };
5501
+ return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target, fact };
5502
+ }
5442
5503
  if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
5443
5504
  return null;
5444
5505
  const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
@@ -5922,6 +5983,43 @@ export function createDecisionServer(options) {
5922
5983
  const denied = authorizeMutation(request, who, body);
5923
5984
  if (denied !== null)
5924
5985
  return refuse(response, who, denied.status, denied.message);
5986
+ // The scripted demo lead's acts: ask, approve, change a plan, complete or send back a result.
5987
+ const demoAct = /^\/chat\/demo\/(?:ask|([0-9]{1,9})\/(approve|change|revise|complete))$/.exec(url.pathname);
5988
+ if (demoAct !== null) {
5989
+ const lead = demoLeadHere();
5990
+ if (lead === null || who.via !== "cookie")
5991
+ return refuse(response, who, 404, "There's no page at this address.", "/chat");
5992
+ if (who.role !== "approver")
5993
+ return redirect(response, `/chat?said=${encodeURIComponent("Your login can watch. Approving and completing is an approver's act.")}`);
5994
+ const now = clock();
5995
+ if (demoAct[1] === undefined) {
5996
+ const message = (body.get("message") ?? "").trim();
5997
+ if (message === "")
5998
+ return redirect(response, `/chat?said=${encodeURIComponent("Type a request first.")}`);
5999
+ const asked = lead.ask(message, now);
6000
+ return redirect(response, `/chat#demo-${asked.id}`);
6001
+ }
6002
+ const id = Number(demoAct[1]);
6003
+ const note = body.get("note") ?? "";
6004
+ const done = demoAct[2] === "approve" ? lead.approve(id, now)
6005
+ : demoAct[2] === "change" ? lead.change(id, note, now)
6006
+ : demoAct[2] === "revise" ? lead.requestChanges(id, note, now)
6007
+ : lead.complete(id, now, (taskId, runId) => {
6008
+ const principal = matePrincipal(who);
6009
+ if (principal === null)
6010
+ return { ok: false, message: "Your access changed. Sign in again." };
6011
+ const current = assignmentOf(store, taskId, now, { principal: "operator", repos: principal.repos }, evidenceRoot);
6012
+ if (current?.receipt?.runId !== runId)
6013
+ return { ok: false, message: "This result changed. Reload before marking it complete." };
6014
+ const completed = checkAssignmentAsOperator(store, taskId, current.receipt.digest, principal, now, evidenceRoot);
6015
+ return completed.ok ? { ok: true } : { ok: false, message: completed.message };
6016
+ });
6017
+ bustBadge();
6018
+ if (!done.ok)
6019
+ return redirect(response, `/chat?said=${encodeURIComponent(done.message)}`);
6020
+ const latest = lead.exchanges().at(-1);
6021
+ return redirect(response, demoAct[2] === "change" || demoAct[2] === "revise" ? `/chat#demo-${latest?.id ?? id}` : `/chat#demo-${id}-work`);
6022
+ }
5925
6023
  // A person says a provider's sign-in works again: its sign-in pause
5926
6024
  // lifts, its tasks may start, and one short message says so.
5927
6025
  const resumeProvider = /^\/providers\/([a-z]+)\/resume$/.exec(url.pathname);
@@ -10220,7 +10318,7 @@ export function createDecisionServer(options) {
10220
10318
  note = validated.note;
10221
10319
  }
10222
10320
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
10223
- return redirect(response, taskHref(taskId));
10321
+ return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
10224
10322
  }
10225
10323
  case "complete": {
10226
10324
  if (who.via !== "cookie" || who.role !== "approver")
@@ -11147,7 +11245,8 @@ export function editorFileHref(worktree, path, line) {
11147
11245
  * chat (v39, extending Priority 2's "one surface, six places" rule from
11148
11246
  * the verdict word to the per-criterion state). */
11149
11247
  function matrixStateBadge(state) {
11150
- const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-failed" : state === "missing" ? "badge-failed" : "badge-manual-review";
11248
+ // Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
11249
+ const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
11151
11250
  const word = state === "pass" ? "pass" : state === "missing" ? "missing" : state === "failed" ? "failed" : "manual review";
11152
11251
  return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(word)}</span>`;
11153
11252
  }
@@ -11233,7 +11332,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11233
11332
  if (matrix.length === 0)
11234
11333
  return "";
11235
11334
  const kinds = {
11236
- check: "Checks", "changed-path": "Changed files", screenshot: "Screenshots", "manual-review": "Human review",
11335
+ check: "Checks", "changed-path": "Changed files", screenshot: "Screenshots", "manual-review": "You check",
11237
11336
  };
11238
11337
  const evidenceLink = (a) => {
11239
11338
  const text = `<code>${escape(a.ref)}</code>`;
@@ -11249,8 +11348,9 @@ function criterionMatrixHtml(matrix, options = {}) {
11249
11348
  const state = row.state;
11250
11349
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
11251
11350
  const confirmed = row.assessment !== undefined && state === "pass";
11252
- const label = awaitingAssessment ? "Not assessed" : confirmed ? "Confirmed" : state === "pass" ? "Evidence checks passed" : state === "manual-review" ? "Human review" : state === "missing" ? "Evidence missing" : "Evidence failed";
11253
- const cls = awaitingAssessment ? "" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-failed";
11351
+ // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
11352
+ const label = awaitingAssessment ? "Met" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
11353
+ const cls = awaitingAssessment ? "badge-done" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-note";
11254
11354
  const warnings = [];
11255
11355
  // Only replace the known boilerplate. Other recorded failure details
11256
11356
  // remain verbatim, so concision cannot hide a different problem.
@@ -11648,6 +11748,8 @@ function agentsStripHtml(view, taskId) {
11648
11748
  return `<p class="task-chat-agents">${escape(agentsSummaryWords(view))} <a href="${taskHref(taskId)}#agents">Review agent setup</a></p>`;
11649
11749
  return `<details class="task-chat-agents"><summary>Agent setup</summary><p>${escape(agentsSummaryWords(view))}</p><a href="${taskHref(taskId)}#agents">View agents</a></details>`;
11650
11750
  }
11751
+ /** The demo's one-line promise, on every page. */
11752
+ const DEMO_BANNER = "Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends.";
11651
11753
  /** Every character that could open a tag or an attribute, dead at the sink. */
11652
11754
  function escape(text) {
11653
11755
  return text
@@ -11881,6 +11983,10 @@ ${THEME_DARK}
11881
11983
  background: color-mix(in srgb, var(--muted) 58%, var(--card));
11882
11984
  color: color-mix(in srgb, var(--destructive) 68%, var(--foreground)); border-color: var(--border);
11883
11985
  }
11986
+ .badge-note {
11987
+ background: color-mix(in srgb, var(--muted) 58%, var(--card));
11988
+ color: var(--so-warning, #ab6400); border-color: var(--border);
11989
+ }
11884
11990
  .badge-overdue {
11885
11991
  background: color-mix(in srgb, var(--muted) 58%, var(--card)); color: var(--foreground);
11886
11992
  border-color: var(--border);
@@ -12599,6 +12705,66 @@ ${THEME_DARK}
12599
12705
  padding: .375rem .9rem; font-size: .8125rem; color: var(--muted-foreground);
12600
12706
  }
12601
12707
  .banner .badge { margin-right: .5rem; }
12708
+ /* Chat in \`toolroll demo\`: the scripted lead's conversation. */
12709
+ .demo-chat { display: flex; flex-direction: column; gap: 1rem; max-width: 46rem; margin: 0 auto; padding-bottom: 1rem; }
12710
+ .demo-thread { display: flex; flex-direction: column; gap: 1.25rem; }
12711
+ .demo-turn { display: flex; flex-direction: column; gap: .75rem; scroll-margin-top: 1rem; }
12712
+ .demo-said p { margin: 0; }
12713
+ .demo-you { align-self: flex-end; max-width: min(34rem, 85%); background: var(--so-ink); color: var(--so-paper); border-radius: .875rem .875rem .25rem .875rem; padding: .55rem .85rem; overflow-wrap: anywhere; }
12714
+ .demo-lead { max-width: 40rem; }
12715
+ .demo-who { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin-bottom: .15rem !important; }
12716
+ .demo-chat .card { margin: 0; padding: 1rem 1.125rem; }
12717
+ .demo-chat .card h2 { font-size: .9375rem; margin: .1rem 0 .4rem; }
12718
+ .demo-chat .card h3 { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin: .75rem 0 .25rem; }
12719
+ .demo-chat .card ul { margin: 0; padding-left: 1.1rem; }
12720
+ .demo-chat .card li { margin: .15rem 0; }
12721
+ .demo-chat .card > p { margin: .25rem 0; }
12722
+ .demo-kicker { font-size: .75rem; color: var(--so-muted); margin: 0 !important; }
12723
+ .demo-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .5rem; margin-top: .9rem; }
12724
+ .demo-actions form { margin: 0; }
12725
+ .demo-actions button, .demo-more > summary { min-height: 2.5rem; }
12726
+ .demo-chat button.demo-primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
12727
+ .demo-chat details.demo-more { flex: 1 1 12rem; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
12728
+ .demo-chat details.demo-more[open] { padding-bottom: 0; }
12729
+ .demo-chat .demo-more > summary { display: inline-flex; align-items: center; color: var(--so-ink); padding: .35rem .85rem; border: 1px solid var(--so-input-line); border-radius: calc(var(--radius) - 3px); background: var(--so-paper); font-weight: 500; font-size: .8125rem; cursor: pointer; list-style: none; }
12730
+ .demo-more > summary::-webkit-details-marker { display: none; }
12731
+ .demo-more[open] > summary { margin-bottom: .5rem; }
12732
+ .demo-more form, .demo-composer { display: flex; flex-direction: column; gap: .4rem; }
12733
+ .demo-more label { font-size: .8125rem; font-weight: 500; }
12734
+ .demo-more textarea, .demo-composer textarea { width: 100%; font: inherit; font-size: 1rem; resize: vertical; }
12735
+ .demo-more form button { align-self: flex-start; }
12736
+ .demo-steps { display: flex; flex-wrap: wrap; gap: .4rem 1rem; list-style: none; padding: 0 !important; margin: .4rem 0 !important; }
12737
+ .demo-steps li { display: inline-flex; align-items: center; gap: .4rem; color: var(--so-muted); font-size: .8125rem; margin: 0 !important; }
12738
+ .demo-steps li::before { content: ""; width: .55rem; height: .55rem; border-radius: 50%; border: 1.5px solid currentColor; }
12739
+ .demo-steps li.done { color: var(--so-ink); }
12740
+ .demo-steps li.done::before { background: var(--so-success); border-color: var(--so-success); }
12741
+ .demo-steps li.now { color: var(--so-ink); font-weight: 600; }
12742
+ .demo-steps li.now::before { border-color: var(--so-info); background: var(--so-info); animation: demo-pulse 1.2s ease-in-out infinite; }
12743
+ @keyframes demo-pulse { 50% { opacity: .35; } }
12744
+ @media (prefers-reduced-motion: reduce) { .demo-steps li.now::before { animation: none; } }
12745
+ .demo-state { display: flex; align-items: center; gap: .5rem; margin: 0 !important; }
12746
+ .demo-chat .badge.demo-ready { background: var(--so-info-soft); color: var(--so-info); }
12747
+ .demo-chat .badge.demo-complete { background: var(--so-success-soft); color: var(--so-success); }
12748
+ .demo-pass { color: var(--so-success); font-weight: 500; }
12749
+ .demo-fail { color: var(--so-danger); font-weight: 500; }
12750
+ .demo-chat details.demo-evidence { margin: .5rem 0 0; padding: 0; border: 0; border-top: 1px solid var(--so-line); border-radius: 0; background: none; box-shadow: none; }
12751
+ .demo-evidence > summary { min-height: 2.5rem; display: list-item; padding-block: .55rem; cursor: pointer; font-weight: 500; font-size: .8125rem; }
12752
+ .demo-evidence pre { margin: 0 0 .5rem; max-height: 22rem; overflow: auto; padding: .6rem .75rem; border-radius: .5rem; background: var(--so-raised); font: 400 .75rem/1.55 var(--font-mono); white-space: pre; overflow-wrap: normal; }
12753
+ .demo-diff span { display: block; min-width: max-content; }
12754
+ .demo-diff-add { background: var(--so-success-soft); color: var(--so-success); }
12755
+ .demo-diff-del { background: var(--so-danger-soft); color: var(--so-danger); }
12756
+ .demo-diff-hunk, .demo-diff-meta { color: var(--so-muted); }
12757
+ .demo-diff-file { font-weight: 600; }
12758
+ .demo-evidence figure { margin: 0 0 .5rem; }
12759
+ .demo-evidence img { display: block; width: 100%; height: auto; border: 1px solid var(--so-line); border-radius: .5rem; }
12760
+ .demo-evidence figcaption { margin-top: .35rem; }
12761
+ .demo-hint { padding: 1.25rem 0 .25rem; }
12762
+ .demo-hint-title { font-size: .9375rem; font-weight: 600; margin: 0 0 .75rem; }
12763
+ .demo-suggestions { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }
12764
+ .demo-suggestions button { min-height: 2.5rem; border-radius: 999px; padding: .35rem .95rem; }
12765
+ .demo-composer { position: sticky; bottom: 0; padding: .75rem 0 .25rem; background: var(--so-paper); flex-direction: row; align-items: flex-end; }
12766
+ .demo-chat .demo-composer textarea { flex: 1 1 auto; width: auto; min-width: 0; min-height: 2.75rem; }
12767
+ .demo-chat .demo-composer button { flex: none; width: auto; min-height: 2.75rem; padding-inline: 1.1rem; }
12602
12768
  .banner a { color: var(--muted-foreground); }
12603
12769
  .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }
12604
12770
 
@@ -12661,8 +12827,9 @@ ${THEME_DARK}
12661
12827
  .dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before,
12662
12828
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before {
12663
12829
  content: ""; display: inline-block; width: .45rem; height: .45rem; margin-right: .45rem; border-radius: 50%; vertical-align: .08rem;
12664
- background: var(--destructive);
12830
+ background: var(--so-warning, #ab6400);
12665
12831
  }
12832
+ .problem.dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before { background: var(--destructive); }
12666
12833
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before { background: var(--muted-foreground); }
12667
12834
  /* A review in flight (review fixes): the box reads neutral — neither the
12668
12835
  green of a settled verdict nor the red of a problem — with the dot in
@@ -13024,6 +13191,9 @@ ${THEME_DARK}
13024
13191
  .result-notes .recap { margin: .25rem 0 .4rem; font-size: .875rem; }
13025
13192
  .result-attention { margin: .75rem 0 .25rem; padding: .7rem .85rem; border-left: 1px solid var(--warning); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: var(--warning-soft); font-size: .8125rem; }
13026
13193
  .result-attention strong { display: block; font-size: .78rem; }
13194
+ .result-you-check { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .75rem; margin: .75rem 0; padding: .65rem .85rem; border-radius: calc(var(--radius) - 3px); background: var(--muted); font-size: .8125rem; }
13195
+ .result-you-check ul { flex: 1 1 14rem; min-width: 0; margin: 0; padding: 0; list-style: none; }
13196
+ .result-you-check form { margin: 0; }
13027
13197
  .result-attention ul { margin: .3rem 0 0; padding-left: 1.15rem; }
13028
13198
  .result-attention li { margin: .15rem 0; overflow-wrap: anywhere; }
13029
13199
  .result-tabs { display: flex; gap: .25rem; margin: .9rem 0 .75rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
@@ -13160,7 +13330,7 @@ ${THEME_DARK}
13160
13330
  .pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }
13161
13331
  .pull-request-head strong::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }
13162
13332
  .pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }
13163
- .pull-request--problem .pull-request-head strong::before { background: var(--destructive, #b91c1c); }
13333
+ .pull-request--problem .pull-request-head strong::before { background: var(--so-warning, #ab6400); }
13164
13334
  .pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }
13165
13335
  .pull-request-act .meta { flex: 1 1 100%; }
13166
13336
  .pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }
@@ -14353,7 +14523,7 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
14353
14523
  const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
14354
14524
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
14355
14525
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
14356
- const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
14526
+ const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
14357
14527
  /**
14358
14528
  * The board's liveness: fetch this page's own fragment on a timer and swap
14359
14529
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -14754,7 +14924,7 @@ function shell(title, body, options = {}) {
14754
14924
  // must not pass as production (adoption review, finding 8; the FENCE is
14755
14925
  // the refuseDemo gate in operate.ts, this is the honest label).
14756
14926
  const demoBanner = (chrome.demo === true
14757
- ? `<div class="banner"><span class="badge">sandbox</span>demo data \u2014 nothing here spends money or reaches a remote</div>`
14927
+ ? `<div class="banner"><span class="badge">demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
14758
14928
  : "") +
14759
14929
  (chrome.modeBanner === undefined
14760
14930
  ? ""
@@ -15815,7 +15985,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
15815
15985
  const dispatch = task.dispatch;
15816
15986
  rows.push(`<a class="chat-overview-item decision" href="${taskHref(task.rootId ?? task.id)}" data-dispatch-status="${escape(dispatch.code)}">` +
15817
15987
  `<span class="chat-overview-icon">${strokeIcon(`<path d="M12 8v4"/><path d="M12 16h.01"/><circle cx="12" cy="12" r="9"/>`)}</span>` +
15818
- `<span class="chat-overview-copy"><strong>${escape(task.title)}</strong><span>${escape(projectOf(task.repoIndex))} · ${escape(task.id)} · ${escape(dispatch.summary.toLowerCase())}</span></span>` +
15988
+ `<span class="chat-overview-copy"><strong>${escape(task.title)}</strong><span>${escape(projectOf(task.repoIndex))} · ${escape(task.id)} · ${escape(dispatchHeadline(dispatch))}</span></span>` +
15819
15989
  `<span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
15820
15990
  }
15821
15991
  for (const task of snapshot.tasks.filter(one => one.state === "failed").slice(0, Math.max(0, 3 - rows.length))) {
@@ -17052,6 +17222,11 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
17052
17222
  }
17053
17223
  /** The one status line every surface renders: a dot in the tone, the
17054
17224
  * label, and the `data-work-status` token tests and CSS key off. */
17225
+ /** A not-yet-finished task's shared headline from its dispatch diagnosis (task-status.ts). */
17226
+ function dispatchHeadline(d) {
17227
+ const read = stageOfDispatch(d);
17228
+ return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }) }).headline;
17229
+ }
17055
17230
  function statusLineHtml(status, extra = "") {
17056
17231
  return `<span class="status-line" data-work-status="${escape(status.token)}" data-tone="${escape(status.tone)}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${escape(status.label)}</span>${extra}</span>`;
17057
17232
  }
@@ -17105,7 +17280,7 @@ function workPage(chrome, data) {
17105
17280
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
17106
17281
  ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
17107
17282
  : browserWorkActionHref(row);
17108
- const needsYouDetail = row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
17283
+ const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
17109
17284
  return {
17110
17285
  id: row.rootId, title: row.title, href: taskHref(row.rootId),
17111
17286
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
@@ -18171,7 +18346,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
18171
18346
  ? `<span class="badge">queued</span>`
18172
18347
  : `<span class="badge">reserved for ${escape(column)}</span>`;
18173
18348
  const chips = ` ${state}` +
18174
- `${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}">${escape(one.dispatch.summary.toLowerCase())}</a>`}`;
18349
+ `${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
18175
18350
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
18176
18351
  `<input type="hidden" name="projectRevision" value="${revision}">` +
18177
18352
  `<input type="hidden" name="queueRevision" value="${queueRevision}">` +
@@ -18449,7 +18624,7 @@ function milestoneProgressHtml(milestones) {
18449
18624
  .map(one => `<li class="milestone milestone-${one.state}"><span class="milestone-badge">${milestoneStateWord(one.state)}</span> ` +
18450
18625
  `${escape(one.description)}${one.note === null ? "" : ` <span class="meta">— ${escape(one.note)}</span>`}</li>`)
18451
18626
  .join("\n") +
18452
- `</ul><p class="meta milestone-progress-note">Live checkpoints from the agent. The task is only done after its proof passes.</p></section>`);
18627
+ `</ul><p class="meta milestone-progress-note">Live checkpoints from the agent.</p></section>`);
18453
18628
  }
18454
18629
  /** Adaptive execution plans (v44): the current plan revision's own reason
18455
18630
  * and evidence, any revision still awaiting an operator's accept/reject,
@@ -18642,7 +18817,11 @@ function taskBodyParts(data) {
18642
18817
  // data-work-status is the shared projection's token (package 1) —
18643
18818
  // the same one the Work row, chat, and cockpit carry for this task.
18644
18819
  const workToken = resultStatus?.token ?? data.dispatch?.code ?? "unknown";
18645
- const box = (kind, title, detail, status, controls = "") => `<div class="${kind === "problem" && !inReview ? "problem" : "answered"} dispatch-status" id="run-status" data-dispatch-status="${escape(status ?? title.toLowerCase().replace(/[^a-z0-9]+/g, "-"))}" data-work-status="${escape(workToken)}">` +
18820
+ // A diagnosis under Task options: the exact technical reason, one tap
18821
+ // away from the shared headline (task-status.ts). Red only when that
18822
+ // headline is Failed.
18823
+ const headline = status.label;
18824
+ const box = (kind, title, detail, code, controls = "") => `<div class="${kind === "problem" && !inReview && headline === "Failed" ? "problem" : "answered"} dispatch-status" id="run-status" data-dispatch-status="${escape(code ?? title.toLowerCase().replace(/[^a-z0-9]+/g, "-"))}" data-work-status="${escape(workToken)}">` +
18646
18825
  `<div class="dispatch-copy"><strong>${escape(inReview ? "Recorded checks" : title)}</strong><span class="meta">${detail}</span></div>${controls}</div>`;
18647
18826
  if (task.state !== "done") {
18648
18827
  const diagnosis = data.dispatch ?? null;
@@ -18726,7 +18905,7 @@ function taskBodyParts(data) {
18726
18905
  return `<details class="dispatch-proof-details"><summary>Previous assessment</summary><div class="dispatch-proof-body">` +
18727
18906
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
18728
18907
  (accepted === null ? "" : `<p class="meta">Accepted with an exception by ${escape(accepted.approver)}. Check results are unchanged.${accepted.note === null ? "" : ` ${escape(accepted.note)}`}</p>`) +
18729
- (proof.proofReasons.length === 0 ? "" : `<ul>${proof.proofReasons.map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
18908
+ (proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
18730
18909
  criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
18731
18910
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
18732
18911
  }
@@ -19529,7 +19708,13 @@ function taskBodyParts(data) {
19529
19708
  : ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">scout</span>` : ""}</p>`;
19530
19709
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
19531
19710
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
19532
- const assignmentOptions = data.assignment == null ? null : { workStatus: status, hideAction: (approveForm !== "" && data.dispatch?.action === "approve-scope") || pullRequestActs, problem: status.tone === "problem", diagnostics: [...(status.diagnostics ?? []), ...(status.tone === "problem" && status.detail !== data.assignment.detail ? [status] : [])] };
19711
+ const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
19712
+ const assignmentOptions = data.assignment == null ? null : { workStatus: status, hideAction: (approveForm !== "" && data.dispatch?.action === "approve-scope") || pullRequestActs, problem: status.tone === "problem",
19713
+ planning: data.dispatch?.code === "running" && data.dispatch.role === "planner", ...(pullRequestFact === undefined ? {} : { pullRequest: pullRequestFact }),
19714
+ diagnostics: [...(status.diagnostics ?? []), ...(status.tone === "problem" && status.detail !== data.assignment.detail ? [status] : [])] };
19715
+ // Say it once: a pull request that couldn't open, or merged, is the status
19716
+ // card's own row (its reason or merge commit under Details), not a second card.
19717
+ const pullRequestCard = data.pullRequest == null || (pullRequestFact !== undefined && (data.pullRequest.view?.state === "failed" || data.pullRequest.view?.state === "merged")) ? "" : pullRequestCardHtml(data.pullRequest, data.csrf);
19533
19718
  // The result takes over from the task status as soon as it is ready.
19534
19719
  const statusHtml = (data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && data.dispatch?.action === "approve-scope")) + checkProgressHtml(data.checkProgress ?? null);
19535
19720
  // The exact-run control (v52), directly under the scheduler's answer:
@@ -19659,7 +19844,7 @@ function taskBodyParts(data) {
19659
19844
  approval: approvalHtml,
19660
19845
  lead: [
19661
19846
  { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
19662
- { key: "pull-request", html: data.pullRequest == null ? "" : pullRequestCardHtml(data.pullRequest, data.csrf) },
19847
+ { key: "pull-request", html: pullRequestCard },
19663
19848
  { key: "needs-scope", html: needsScopeCard }, { key: "progress", html: progressCard }, { key: "revisions", html: revisionLedgerCard },
19664
19849
  { key: "plan", html: planCard }, { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
19665
19850
  ].filter(one => one.html !== ""),
@@ -19911,11 +20096,11 @@ function cockpitStatusOf(view) {
19911
20096
  // The receipt's own status (package 3): the same projection the chat
19912
20097
  // receipt and the run page print, read from the shared detail.
19913
20098
  if (view.run === null || view.detail === null)
19914
- return view.assignment === null ? resultStatusOf(null, null) : assignmentPresentationOf(view.assignment).status;
20099
+ return view.assignment === null ? resultHeadlineOf(resultStatusOf(null, null)) : assignmentPresentationOf(view.assignment).status;
19915
20100
  const receiptStatus = receiptStatusOf(view.detail.receipt);
19916
20101
  if (view.assignment !== null)
19917
20102
  return assignmentStatusOf(assignmentWithEvidence(view.assignment, receiptStatus, view.run.id));
19918
- return receiptStatus;
20103
+ return resultHeadlineOf(receiptStatus);
19919
20104
  }
19920
20105
  /** Turn verifier records into one sentence a project owner can act on.
19921
20106
  * A command-not-found result means the check could not start, not that the
@@ -19981,11 +20166,14 @@ function verificationExplanation(verdict, reasons) {
19981
20166
  if (reason === "the sealed diff is unavailable or truncated; the claimed changed paths cannot be verified against it") {
19982
20167
  return "The recorded changes were incomplete, so they could not be verified.";
19983
20168
  }
20169
+ const plainWords = plainReasonWords(reason);
20170
+ if (plainWords !== reason)
20171
+ return plainWords;
19984
20172
  const sentence = reason.trim();
19985
20173
  return sentence === "" ? "" : `${sentence[0]?.toUpperCase() ?? ""}${sentence.slice(1)}${/[.!?]$/.test(sentence) ? "" : "."}`;
19986
20174
  }).filter(Boolean);
19987
20175
  if (plain.length > 0)
19988
- return plain.join(" ");
20176
+ return [...new Set(plain)].join(" ");
19989
20177
  if (verdict === "verified")
19990
20178
  return "Toolroll independently verified this result.";
19991
20179
  if (verdict === "attested")
@@ -20205,7 +20393,8 @@ function pullRequestCardHtml(pr, csrf) {
20205
20393
  const view = pr.view;
20206
20394
  const url = safePrUrl(view.prUrl);
20207
20395
  const link = view.prNumber === null ? "" : url === null ? `<span class="mono">PR #${view.prNumber}</span>` : `<a href="${escape(url)}" class="mono" rel="noreferrer" target="_blank">PR #${view.prNumber}</a>`;
20208
- const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" || view.state === "closed" ? "problem" : "waiting";
20396
+ // A pull request's trouble never undoes the result: amber, never red.
20397
+ const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" ? "problem" : "waiting";
20209
20398
  const revision = view.revisionTask === null || view.state !== "failing" ? "" : ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
20210
20399
  const merged = view.mergeCommit === null ? "" : `<p class="meta">Merge commit <span class="mono">${escape(view.mergeCommit.slice(0, 12))}</span></p>`;
20211
20400
  const target = pr.target === null ? "the base branch" : pr.target;
@@ -20940,8 +21129,8 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20940
21129
  (view.diff.binaryCount > 0 ? ` · ${view.diff.binaryCount} binary` : "") +
20941
21130
  (view.diff.filesTruncated ? " · list shortened" : "");
20942
21131
  const criteria = facts.checks === null
20943
- ? "No signed rubric"
20944
- : `${facts.checks.passed}/${facts.checks.total} acceptance criteria passed`;
21132
+ ? "No requirements set"
21133
+ : `${facts.checks.passed}/${facts.checks.total} requirements met`;
20945
21134
  // The deliverable leads (package 3): a scout's report by its title, UI
20946
21135
  // work by its validated screenshots. Unverifiable shots are counted as
20947
21136
  // unavailable, never shown, never called validated.
@@ -20962,7 +21151,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20962
21151
  const attention = current === null ? [...facts.evidenceProblems, ...view.caveats] : [];
20963
21152
  const caveats = attention.length === 0
20964
21153
  ? ""
20965
- : `<div class="receipt-caveats" data-result-attention="${attention.length}"><strong>Before you move on</strong><ul>${attention.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
21154
+ : `<div class="receipt-caveats" data-result-attention="${attention.length}"><strong>Before you move on</strong><ul>${attention.map(one => `<li>${escape(plainReasonWords(one))}</li>`).join("")}</ul></div>`;
20966
21155
  // v51: semantic coverage, distinct from the machine proof word above —
20967
21156
  // the same lines the CLI prints, so chat and terminal cannot disagree.
20968
21157
  // Nothing owed (optional and unsettled, or satisfied) folds behind a
@@ -20981,7 +21170,8 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20981
21170
  lead +
20982
21171
  `<p class="receipt-summary">${escape(conciseOutcomeOf(view.summary ?? (view.outcome === "no-change" ? "The agent found that no repository change was needed." : "The build finished without a concise handoff.")))}</p>` +
20983
21172
  (headline ? `<div class="receipt-actions"><a class="button-link" href="${escape(resultHref)}" data-open-result data-primary-action>${escape(status.action?.label ?? "Open result")}</a></div>` : "") +
20984
- (view.publication?.state === "failed" ? `<p class="problem">${escape(facts.publicationWords)}</p>` : "") +
21173
+ // A pull request that couldn't open never undoes the result: an amber note, not red.
21174
+ (view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
20985
21175
  caveats +
20986
21176
  coverage +
20987
21177
  `<details class="receipt-details"><summary>Result details</summary>` +
@@ -20994,7 +21184,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20994
21184
  (inReview ? `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${escape(status.token)}">${escape(status.detail)}</p></details>` : "") +
20995
21185
  // The matrix count is the proof's own citation; it reads as verified
20996
21186
  // only when the machine verified the result (workspace package 1).
20997
- `<div class="receipt-facts"><span><strong>${escape(criteria)}</strong><small>${stored.tone === "problem" ? "cited by the agent — not verified" : stored.token === "agent-attested" ? "cited by the agent, no independent check" : "against the approved scope"}</small></span>` +
21187
+ `<div class="receipt-facts"><span><strong>${escape(criteria)}</strong><small>${stored.tone === "problem" ? "the agent's own claim — not checked" : stored.token === "agent-attested" ? "the agent's own claim, no project check" : "against the approved scope"}</small></span>` +
20998
21188
  `<span><strong>${escape(diff)}</strong><small>${facts.head === null ? "no commit recorded" : `head ${escape(facts.head.slice(0, 12))} · ${escape(facts.headSource ?? "")}`}</small></span>` +
20999
21189
  `<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>` +
21000
21190
  `<div class="receipt-actions">` +
@@ -21032,16 +21222,18 @@ function resultPanelParts(detail, o) {
21032
21222
  const { run, receipt, proof, terminal, handoff } = detail;
21033
21223
  const facts = receipt.facts;
21034
21224
  const current = detail.assignment?.receipt?.runId === run.id ? detail.assignment : null;
21035
- const presentation = current == null ? null : assignmentPresentationOf(current, { additionalAttention: [...facts.evidenceProblems, ...receipt.caveats] });
21036
- let status = presentation?.status ?? receiptStatusOf(receipt);
21225
+ const resultLinks = { result: o.hrefFor("summary"), checks: o.hrefFor("checks"), pullRequest: `${taskHref(detail.rootId ?? detail.taskId)}#merge` };
21226
+ const presentation = current == null ? null : assignmentPresentationOf(current, { additionalAttention: [...facts.evidenceProblems, ...receipt.caveats],
21227
+ ...(detail.publication === null ? {} : { pullRequest: pullRequestFactOf({ ...detail.publication, lastCheckState: detail.ciFailing ? "failing" : detail.publication.lastCheckState }) }),
21228
+ evidence: { damaged: facts.evidenceHealth.damaged, missing: facts.evidenceHealth.missing, shortened: facts.evidenceHealth.shortened }, links: resultLinks });
21229
+ const taskStatus = presentation?.taskStatus ?? null;
21230
+ const status = presentation?.status ?? resultHeadlineOf(receiptStatusOf(receipt));
21037
21231
  const stored = receiptStatusOf(receipt, null);
21038
21232
  const humanReview = manualReviewOnly(proof === null ? null : { ...proof, verdict: proof.verdict ?? "" });
21039
21233
  const directAssessment = proof?.matrix.some(row => row.assessment !== undefined) === true;
21040
21234
  const awaitingGoalReview = directAssessment && proof?.reasons.length === 1 && proof.reasons[0] === GOAL_ASSESSMENT_PENDING;
21041
21235
  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();
21042
21236
  const physicalReasons = receipt.reasons.filter(reason => !assessmentReasons.has(reason));
21043
- if (directAssessment && status.token === "verification-needed" && proof?.verdict === "short" && proof.matrix.some(row => row.review?.judgement === "cannot-tell") && physicalReasons.length === 0)
21044
- status = { ...status, label: "More evidence needed", tone: "attention" };
21045
21237
  const runId = run.id;
21046
21238
  const shots = proof?.screenshots ?? [];
21047
21239
  const shown = shots.filter(one => one.problem === null);
@@ -21075,14 +21267,31 @@ function resultPanelParts(detail, o) {
21075
21267
  attention.push(...(handoff?.followUps ?? []).map(one => `Follow-up: ${one}`));
21076
21268
  // Publication risks stay in the open (repair 2026-09-14); the routine
21077
21269
  // publication fact lives with the build details below.
21078
- if (publication !== null && publication.state === "failed") {
21270
+ // With the shared status, the pull request is its own quiet row (its reason under Details).
21271
+ if (publication !== null && publication.state === "failed" && taskStatus === null) {
21079
21272
  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.`);
21080
21273
  }
21081
- if (detail.ciFailing && publication !== null)
21274
+ if (detail.ciFailing && publication !== null && taskStatus === null)
21082
21275
  attention.push(`CI is failing on PR #${publication.prNumber} at the last check.`);
21276
+ // Requirements only a person can confirm: plain words and one Accept,
21277
+ // in neutral ink — nothing failed. Accepting records the person's
21278
+ // decision and leaves the recorded checks as they are.
21279
+ const personChecks = proof === null || proof.accepted !== null ? [] : [
21280
+ ...proof.matrix.filter(row => row.state === "manual-review").map(row => personCheckWords(row.statement)),
21281
+ ...(proof.matrix.length > 0 ? [] : proof.reasons.filter(reason => manualReviewCriterionOf(reason) !== null).map(() => personCheckWords(null))),
21282
+ ];
21283
+ const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
21284
+ const youCheck = personChecks.length === 0 ? null : {
21285
+ lines: [...new Set(personChecks)],
21286
+ accept: acceptable ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
21287
+ };
21288
+ const youCheckHtml = youCheck === null ? "" :
21289
+ `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
21290
+ (youCheck.accept === null ? "" : `<form method="post" action="${escape(youCheck.accept.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheck.accept.run}"><input type="hidden" name="return" value="${escape(youCheck.accept.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
21291
+ `</div>`;
21083
21292
  const attentionHtml = attention.length === 0
21084
21293
  ? ""
21085
- : `<div class="result-attention" data-result-attention="${attention.length}"><ul>${attention.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
21294
+ : `<div class="result-attention" data-result-attention="${attention.length}"><ul>${attention.map(one => `<li>${escape(plainReasonWords(one))}</li>`).join("")}</ul></div>`;
21086
21295
  // ---- one outcome, one action (repair 2026-09-14) ------------------------
21087
21296
  // The outcome is the handoff's first sentence, bounded; the agent's full
21088
21297
  // account waits behind a disclosure in Summary so nothing is said twice.
@@ -21338,14 +21547,17 @@ function resultPanelParts(detail, o) {
21338
21547
  const html = (`<section class="card result-panel" id="result" data-result-panel data-result-place="${o.place}" data-result-lead="${lead}" data-result-task="${escape(detail.rootId ?? detail.taskId)}" data-result-user="${escape(o.user)}"${resultFactsAttributes(facts)}>` +
21339
21548
  (o.back === null ? "" : `<p class="result-back"><a href="${escape(o.back.href)}" data-result-back>← ${escape(o.back.label)}</a></p>`) +
21340
21549
  (detail.history ?? "") +
21341
- `<header class="result-head"><div><h2>${escape(heading)}</h2></div>${o.headStatus === false ? "" : statusLineHtml(status.token === "verification-needed" && !directAssessment ? { ...status, label: "Verification needed" } : status)}</header>` +
21550
+ // One headline: with the shared status, its words lead (task-status.ts); "Changes saved" would compete with them.
21551
+ (taskStatus !== null && o.headStatus !== false
21552
+ ? `<header class="result-head"><div data-headline-tone="${taskStatus.tone}"><h2 class="status-headline" data-work-status="${escape(status.token)}"><i aria-hidden="true"></i>${escape(taskStatus.headline)}</h2><p class="status-sentence">${escape(taskStatus.sentence)}</p></div></header>`
21553
+ : `<header class="result-head"><div><h2>${escape(heading)}</h2></div>${o.headStatus === false ? "" : statusLineHtml(status)}</header>`) +
21342
21554
  `<p class="result-summary">${escape(outcome)}</p>` +
21343
21555
  (current == null || o.place === "review" && (current.state === "ready-to-check" || current.state === "complete") ? "" :
21344
- `<div class="verdict" data-current-outcome>${verdictChipsHtml(current, facts)}` +
21345
- (current.receipt?.checks.status === "failed" || current.receipt?.checks.status === "unavailable" ? `<p class="problem">${escape(current.detail)}</p>` : "") + `</div>`) +
21556
+ `<div class="verdict" data-current-outcome data-headline="${escape(taskStatus.headline)}">${statusDetailsHtml(taskStatus)}${statusWhyHtml(taskStatus)}</div>`) +
21557
+ attentionHtml +
21558
+ youCheckHtml +
21346
21559
  action +
21347
21560
  (REVIEW_TOKENS.has(status.token) ? `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${escape(status.token)}">${escape(status.detail)}</p></details>` : "") +
21348
- attentionHtml +
21349
21561
  tabs +
21350
21562
  view("summary", summaryParts) +
21351
21563
  view("changes", changeParts) +
@@ -21356,9 +21568,10 @@ function resultPanelParts(detail, o) {
21356
21568
  const panel = {
21357
21569
  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) },
21358
21570
  heading, outcome,
21359
- verdict: current == null ? null : verdictChipsOf(current, facts),
21571
+ status: taskStatus,
21360
21572
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
21361
- attention: attention.filter(one => !shortenedMaterialReason(one)),
21573
+ attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
21574
+ youCheck,
21362
21575
  limits: attention.filter(one => shortenedMaterialReason(one)),
21363
21576
  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 })),
21364
21577
  views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
@@ -21370,40 +21583,6 @@ function resultPanelParts(detail, o) {
21370
21583
  };
21371
21584
  return { html, panel };
21372
21585
  }
21373
- const CHIP_ICONS = { check: `<path d="M20 6 9 17l-5-5"/>`, x: `<path d="M18 6 6 18M6 6l12 12"/>` };
21374
- /** The chips as data: the server list below and the rebuilt result page. */
21375
- function verdictChipsOf(current, facts) {
21376
- const chips = [];
21377
- const checks = current.receipt?.checks.status ?? null;
21378
- if (checks === "passed")
21379
- chips.push({ tone: "success", label: "Checks passed", icon: "check", title: null });
21380
- else if (checks === "failed")
21381
- chips.push({ tone: "danger", label: "Checks failed", icon: "x", title: null });
21382
- else if (checks === "unavailable")
21383
- chips.push({ tone: "warning", label: "Check unavailable", icon: null, title: null });
21384
- const publication = { none: ["neutral", "Branch only"], intended: ["info", "Publishing"], pushed: ["info", "Pushed"], opened: ["info", "Pull request open"], failed: ["danger", "Publish failed"] };
21385
- const [tone, label] = publication[facts.publicationState] ?? ["neutral", facts.publicationState];
21386
- chips.push({ tone, label, icon: null, title: facts.publicationWords });
21387
- const health = facts.evidenceHealth;
21388
- if (health.damaged > 0)
21389
- chips.push({ tone: "danger", label: "Damaged evidence", icon: null, title: null });
21390
- else if (health.missing > 0)
21391
- chips.push({ tone: "warning", label: "Missing evidence", icon: null, title: null });
21392
- else if (health.shortened > 0)
21393
- chips.push({ tone: "warning", label: "Partial output", icon: null, title: "Some saved output was shortened when stored" });
21394
- return { chips, by: current.completion === null ? null : current.completion.actor.replace(/^(?:operator|coordinator|lead):/, "") };
21395
- }
21396
- /** The verdict at a glance: checks, where the work lives, and whether any
21397
- * saved output is partial, as chips instead of sentences. The same facts
21398
- * stay available in words on the Checks tab and in Build details. */
21399
- function verdictChipsHtml(current, facts) {
21400
- const { chips, by } = verdictChipsOf(current, facts);
21401
- const items = chips.map(one => {
21402
- const label = `${one.icon === null ? "" : strokeIcon(CHIP_ICONS[one.icon])}${one.label}`;
21403
- return `<li class="verdict-chip verdict-chip--${one.tone}" title="${escape(one.title ?? label)}">${label}</li>`;
21404
- });
21405
- return `<ul class="verdict-chips" aria-label="Result at a glance">${items.join("")}</ul>` + (by === null ? "" : `<p class="meta verdict-by">Completed by ${escape(by)}</p>`);
21406
- }
21407
21586
  /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
21408
21587
  * first sentence, cut at a word when it runs past 180 characters. The
21409
21588
  * full conclusion stays available behind the Summary's disclosure. */