toolroll 0.9.12 → 0.9.14

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
@@ -56,6 +56,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
56
56
  import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
57
57
  import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
58
58
  import { learningHtml } from "./workspace-ui.js";
59
+ import { resultActsOf } from "./result-acts.js";
59
60
  import { createSessionEndpoint } from './session-server.js';
60
61
  import { handleTeamHttp } from './team-http.js';
61
62
  import { teamWorkspaceHtml } from './team-ui.js';
@@ -74,7 +75,7 @@ import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
74
75
  import { assignmentCatchUp } from './assignment-brief.js';
75
76
  import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
76
77
  import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
77
- import { ASKS, ASK_LABEL, NEEDS } from './needs-you.js';
78
+ import { ASKS, ASK_LABEL, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, isInternalErrorReason, latestFinishedAttempt, runReasonWords } from './needs-you.js';
78
79
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
79
80
  import { ledgerBody } from "./ledger-view.js";
80
81
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -163,7 +164,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
163
164
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
164
165
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
165
166
  import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
166
- import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
167
+ import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, lastErrorLineOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
167
168
  import { PRICED_BUILD_MODELS } from "./pricing.js";
168
169
  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";
169
170
  import { parseReport } from "./scout-report.js";
@@ -1761,7 +1762,9 @@ export function createDecisionServer(options) {
1761
1762
  }
1762
1763
  // Exact result links carry their own read context. Check the stored
1763
1764
  // placement against both account and instance access before using it;
1764
- // viewing a result never changes the session's selected project.
1765
+ // viewing a result never changes the session's selected project, and
1766
+ // the page's project switch keeps showing the person's own choice.
1767
+ const chosenProject = project;
1765
1768
  if (url.pathname === "/review" && url.searchParams.has("result")) {
1766
1769
  const wanted = url.searchParams.get("result") ?? "";
1767
1770
  const ref = wanted.length > 0 && wanted.length <= 64 && !hasForbiddenControls(wanted) ? store.lookupRef(wanted) : null;
@@ -2390,11 +2393,14 @@ export function createDecisionServer(options) {
2390
2393
  const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
2391
2394
  const csrf = who.via === "cookie" ? who.session.csrf : "";
2392
2395
  const expectedRun = url.searchParams.get("run");
2396
+ // An exact link to a result this person can't see (or that never existed) reads the same either way.
2397
+ if (expectedRun !== null && wantedId !== null && chosen === null)
2398
+ return refuse(response, who, 404, "No such result in your projects.", "/work");
2393
2399
  if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
2394
2400
  return sendScreen(response, 409, screen("Result changed", '<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>', { chrome: chromeFor(project, "runs") }));
2395
2401
  }
2396
2402
  const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
2397
- const reviewPage = reviewCockpitPage(chromeFor(project, "runs"), {
2403
+ const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
2398
2404
  queue: ranked,
2399
2405
  queueCap: REVIEW_QUEUE_CAP,
2400
2406
  selected,
@@ -3020,6 +3026,14 @@ export function createDecisionServer(options) {
3020
3026
  }
3021
3027
  return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
3022
3028
  }
3029
+ // One result page (2026-10-02): a builder's finished result opens on
3030
+ // its task's result page, titled with the task. The raw run record
3031
+ // waits there under Details, and here with ?record=1.
3032
+ if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
3033
+ completedRowFor(taskId, null)?.runId === found.id) {
3034
+ const tab = parseResultTab(url.searchParams.get("tab"));
3035
+ return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
3036
+ }
3023
3037
  // Pollers exist only for a LIVE run — an orphaned null-outcome run
3024
3038
  // would otherwise be refetched forever (finding 15). The nonce is
3025
3039
  // sendScreen's business now.
@@ -5881,6 +5895,33 @@ export function createDecisionServer(options) {
5881
5895
  return null;
5882
5896
  return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
5883
5897
  }
5898
+ /** What went wrong with the latest finished attempt (whatever its outcome, never an older failure), in one line: a
5899
+ * failing check's last error line (linked to that line in its saved log), else the attempt's recorded reason in plain
5900
+ * words, else that no reason was recorded. A reason that is machine output reads as an internal error, its detail
5901
+ * behind a link to that line of the attempt's record; nothing links to this task's own page. */
5902
+ function failureOf(runs, assignment) {
5903
+ const last = latestFinishedAttempt(runs);
5904
+ if (last === null)
5905
+ return { line: NO_REASON_RECORDED, link: null };
5906
+ const receipt = assignment?.receipt?.runId === last.id ? assignment.receipt : null;
5907
+ if (receipt !== null && receipt.checks.status === "failed" && receipt.checks.logArtifactId !== null) {
5908
+ const artifact = store.artifactsFor(last.id).find(one => one.id === receipt.checks.logArtifactId) ?? null;
5909
+ let read = null;
5910
+ try {
5911
+ read = artifact === null ? null : readVerifiedArtifact(evidenceRoot, artifact);
5912
+ }
5913
+ catch {
5914
+ read = null;
5915
+ }
5916
+ const found = read !== null && read.ok ? lastErrorLineOf(read.content.toString("utf8")) : null;
5917
+ if (found !== null) {
5918
+ return { line: oneLineOf(found.text, 140), link: { label: `Check output, line ${found.line}`,
5919
+ href: `${reviewHref(receipt.taskId)}&run=${last.id}&tab=checks#check-log-L${found.line}` } };
5920
+ }
5921
+ }
5922
+ return { line: failedAttemptSentence(last.reason),
5923
+ link: isInternalErrorReason(last.reason) ? { label: "The recorded error", href: `/r/${last.id}?record=1#run-reason-detail` } : null };
5924
+ }
5884
5925
  function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
5885
5926
  const admittedFamily = familyOf(taskId);
5886
5927
  if (admittedFamily?.problem != null)
@@ -5903,6 +5944,8 @@ export function createDecisionServer(options) {
5903
5944
  versions: family.versions.map(one => ({ id: one.id, title: one.title, state: one.state })),
5904
5945
  runs: family.versions.flatMap(version => version.id === data.task.id ? data.runs.map(run => ({ ...run, taskId: version.id })) : store.runsFor(version.refId).map(run => ({ ...run, taskId: version.id }))) };
5905
5946
  }
5947
+ // What went wrong reads the family's latest finished attempt, as the Tasks row does.
5948
+ presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
5906
5949
  const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
5907
5950
  const page = taskPage(paneProject === null && !unscopedMode
5908
5951
  ? chromeFor(paneProject, "tasks")
@@ -10432,10 +10475,21 @@ export function createDecisionServer(options) {
10432
10475
  return redirect(response, taskHref(taskId));
10433
10476
  }
10434
10477
  case "requeue": {
10478
+ // Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
10479
+ const note = (body.get("note") ?? "").trim();
10480
+ if (note !== "" && who.via !== "cookie")
10481
+ return refuse(response, who, 403, "steering is a browser session's act");
10482
+ if (note !== "" && !validateNote(note).ok)
10483
+ return taskScreen(response, who, taskId, "that note will not store, so nothing was retried", 400);
10435
10484
  const requeued = store.requeueTask(taskId, who.name, now);
10436
10485
  if (!requeued.ok) {
10437
10486
  return taskScreen(response, who, taskId, `not requeued: ${requeued.reason}`, 409);
10438
10487
  }
10488
+ if (note !== "") {
10489
+ const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), note, now);
10490
+ if (!filed.ok)
10491
+ return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
10492
+ }
10439
10493
  // Allow-listed return only — never an arbitrary URL from the form.
10440
10494
  return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
10441
10495
  }
@@ -10975,7 +11029,7 @@ export function createDecisionServer(options) {
10975
11029
  const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
10976
11030
  if (!completed.ok)
10977
11031
  return taskScreen(response, who, taskId, completed.message, 409);
10978
- return redirect(response, reviewHref(taskId, Number(namedRun), taskRepoOf(ref.id)));
11032
+ return redirect(response, reviewHref(taskId, Number(namedRun)));
10979
11033
  }
10980
11034
  case "retry-review": {
10981
11035
  return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
@@ -12708,7 +12762,7 @@ ${THEME_MAPPING}
12708
12762
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
12709
12763
  /* Phone spacing steps (760px and narrower): side gutter, gap under the
12710
12764
  header, gap between page blocks, between rows, and inside a card. */
12711
- --so-phone-gutter: 14px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
12765
+ --so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
12712
12766
  }
12713
12767
  @media (prefers-color-scheme: dark) {
12714
12768
  :root:not([data-theme="light"]) {
@@ -18175,9 +18229,9 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
18175
18229
  return null;
18176
18230
  switch (status.action.kind) {
18177
18231
  case "open-result": return runId === null ? taskHref(taskId) : `/r/${runId}`;
18178
- case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId, repo);
18232
+ case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
18179
18233
  case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
18180
- case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId, repo));
18234
+ case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
18181
18235
  case "open-task":
18182
18236
  default: return taskHref(taskId);
18183
18237
  }
@@ -18211,7 +18265,7 @@ function workPage(chrome, data) {
18211
18265
  const rowHtml = (row) => {
18212
18266
  const target = row.primaryAction?.target;
18213
18267
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
18214
- ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
18268
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
18215
18269
  : browserWorkActionHref(row);
18216
18270
  const action = actionHref === null || row.primaryAction === null ? '' : `<a class="work-action" data-primary-action href="${escape(actionHref)}">${escape(row.primaryAction.label)} →</a>`;
18217
18271
  const project = data.multiProject || row.repo === null ? `<span class="project-label">${row.repo === null ? 'Unplaced' : escape(projectName(row.repo))}</span>` : '';
@@ -18245,7 +18299,7 @@ function workPage(chrome, data) {
18245
18299
  rows: data.work.items.map(row => {
18246
18300
  const target = row.primaryAction?.target;
18247
18301
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
18248
- ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
18302
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
18249
18303
  : browserWorkActionHref(row);
18250
18304
  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;
18251
18305
  return {
@@ -19814,7 +19868,8 @@ function taskBodyParts(data) {
19814
19868
  case "approve-scope": return ` <a href="#approve">Review and sign the exact scope</a>.`;
19815
19869
  case "answer-decision": return ` <a href="#decisions">Answer the waiting question</a>.`;
19816
19870
  case "unhold": return ` Use <strong>Remove hold</strong> when it can continue.`;
19817
- case "retry-task": return ` Review the incident, then use <strong>retry</strong> below.`;
19871
+ // The reason is the detail itself; Retry is the status card's own act, said once.
19872
+ case "retry-task": return "";
19818
19873
  case "repair-capability": return ` <a href="/caps">Repair the requirement</a>.`;
19819
19874
  case "repair-dependency":
19820
19875
  return blocker?.admitted === true
@@ -19857,7 +19912,8 @@ function taskBodyParts(data) {
19857
19912
  : `<p class="meta">This task resumes automatically when the builder reconnects. You do not need to file or approve it again.</p>`) +
19858
19913
  `<p><a href="/system">See connection status →</a></p></div></details>`);
19859
19914
  }
19860
- const href = taskRecoveryHref(task.id, diagnosis);
19915
+ // Never a link to this page's own acts: Retry sits on the status card and below.
19916
+ const href = diagnosis.action === "retry-task" ? null : taskRecoveryHref(task.id, diagnosis);
19861
19917
  return href === null ? "" : `<a class="button-link dispatch-action-link" href="${href}">${escape(dispatchActionLabel(diagnosis))}</a>`;
19862
19918
  })();
19863
19919
  const positive = diagnosis.code === "running" || diagnosis.code === "ready" || diagnosis.code === "planning-ready" || diagnosis.code === "scouting-ready";
@@ -20555,8 +20611,10 @@ function taskBodyParts(data) {
20555
20611
  // hold (the hold governs the NEXT start), and requeue refuses while a
20556
20612
  // claim is live — so the words say exactly when each becomes real.
20557
20613
  const canPlan = data.plan === null && !approval.approved && !data.claimed && task.state === "queued" && (data.coordinator === null || data.coordinator === undefined);
20558
- const primaryAct = stopControlsActive ? null : stalled && !data.claimed
20559
- ? { html: act("requeue", "retry — branch and workspace kept"), why: "resolves the incidents, clears the failed attempts, and queues the task again; the preserved branch and workspace are NOT erased", whyClass: "retry" }
20614
+ // A failed task's status card carries Retry itself (the same requeue); here it would be a second ink act.
20615
+ const cardRetries = task.state === "failed" && data.assignment != null && data.csrf !== "";
20616
+ const primaryAct = stopControlsActive || cardRetries ? null : stalled && !data.claimed
20617
+ ? { html: act("requeue", "Retry, keeping the branch and workspace"), why: "Resolves the incidents, clears the failed attempts and queues the task again. The branch and workspace are kept.", whyClass: "retry" }
20560
20618
  : canPlan
20561
20619
  ? { html: act("plan", "plan first"), why: "plan first sends an agent to read the repository, ask you questions, and propose a scope — nothing builds until you approve it", whyClass: "plan" }
20562
20620
  : data.plan === "requested"
@@ -20567,7 +20625,7 @@ function taskBodyParts(data) {
20567
20625
  const holdAct = `<form method="post" action="${taskHref(task.id)}/hold" class="inline act-hold">` +
20568
20626
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
20569
20627
  `<input type="text" name="reason" class="inline" placeholder="reason (optional)" aria-label="hold reason">` +
20570
- `<button type="submit">hold next attempt</button></form>`;
20628
+ `<button type="submit">Hold the next attempt</button></form>`;
20571
20629
  const canHold = task.state === "queued" || task.state === "running" || task.state === "failed";
20572
20630
  const actsBar = [
20573
20631
  `<span id="task-actions"></span><div class="acts-bar">`,
@@ -20584,7 +20642,7 @@ function taskBodyParts(data) {
20584
20642
  `</div>`,
20585
20643
  primaryAct === null ? "" : `<p class="meta acts-why acts-why-${primaryAct.whyClass}">${primaryAct.why}</p>`,
20586
20644
  data.claimed && !stopControlsActive
20587
- ? `<p class="meta acts-why">a worker is building this right now — <em>hold next attempt</em> stops the one after it; cancel waits for the current build to finish${stalled ? "; retry becomes available after this attempt finishes" : ""}</p>`
20645
+ ? `<p class="meta acts-why">a worker is building this right now — <em>Hold the next attempt</em> stops the one after it; cancel waits for the current build to finish${stalled ? "; retry becomes available after this attempt finishes" : ""}</p>`
20588
20646
  : "",
20589
20647
  ].join("\n");
20590
20648
  // Cancel gets the same ceremony as an irreversible answer: armed behind
@@ -20777,7 +20835,7 @@ function taskBodyParts(data) {
20777
20835
  : run.outcome === null ? "Attempt never finished"
20778
20836
  : `${noun[0].toUpperCase()}${noun.slice(1)} ${run.outcome}`;
20779
20837
  thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
20780
- title: `${title}${revision}`, text: run.handoff === null ? (run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
20838
+ title: `${title}${revision}`, text: run.handoff === null ? (run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
20781
20839
  link: href, html: "",
20782
20840
  more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: "Result details", html: previousResult.html } : null });
20783
20841
  }
@@ -20872,6 +20930,22 @@ function taskBodyParts(data) {
20872
20930
  // needs a person, then facts, then folds; the mechanics under Manage.
20873
20931
  const MANAGE = new Set(["steering", "waits-for", "holds"]);
20874
20932
  const finished = task.state === "done" || task.state === "cancelled";
20933
+ // Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
20934
+ // this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
20935
+ const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
20936
+ const failedCard = statusCard?.status.headline === "Failed";
20937
+ // A link to this very page, with no section to open, goes nowhere: a card never offers one.
20938
+ const bareSelfLink = (href) => {
20939
+ if (href.includes("#"))
20940
+ return false;
20941
+ const [path, query = ""] = href.split("?");
20942
+ const version = new URLSearchParams(query).get("version");
20943
+ return [data.assignment?.rootId, data.rootId, task.id].some(id => id != null && path === `/t/${encodeURIComponent(id)}`) && (version === null || version === task.id);
20944
+ };
20945
+ // Build again, in place, names its form after the card's own act.
20946
+ const rebuild = data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
20947
+ ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
20948
+ const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== "" ? { action: `${taskHref(task.id)}/requeue` } : null;
20875
20949
  const view = {
20876
20950
  kind: "task",
20877
20951
  id: task.id,
@@ -20883,13 +20957,15 @@ function taskBodyParts(data) {
20883
20957
  { label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
20884
20958
  ],
20885
20959
  version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
20886
- status: data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null,
20960
+ status: statusCard === null ? null : failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
20961
+ : statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard,
20887
20962
  statusHtml,
20963
+ failure: failedCard ? data.failure ?? null : null,
20964
+ retry,
20888
20965
  approval: approvalHtml,
20889
20966
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
20890
20967
  ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
20891
- rebuild: data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
20892
- ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null,
20968
+ rebuild,
20893
20969
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20894
20970
  lead: [
20895
20971
  { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
@@ -21141,7 +21217,9 @@ export function orderChangedFiles(files, proofCitesPaths) {
21141
21217
  ? churn(b) - churn(a)
21142
21218
  : a.path.localeCompare(b.path));
21143
21219
  }
21144
- const reviewHref = (taskId, runId = null, repo = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}${repo === null ? "" : `&project=${encodeURIComponent(repo)}`}`;
21220
+ /** The one result page's address: the task and its run, never a project path
21221
+ * (the page finds the result's project itself and keeps the person's own). */
21222
+ const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
21145
21223
  /** The receipt's own proof-state word, so the cockpit and the task page
21146
21224
  * never disagree on the state's name or its precedence: the stored
21147
21225
  * verdict decides, whatever the run's outcome — a no-change run with a
@@ -21266,7 +21344,7 @@ function reviewCockpitPage(chrome, data) {
21266
21344
  const why = status === null
21267
21345
  ? reasons.length === 0 ? "" : `<span class="cockpit-why">${escape(reasons[0])}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
21268
21346
  : `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
21269
- return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId, row.repo))}"${current ? ` aria-current="page"` : ""}>` +
21347
+ return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId))}"${current ? ` aria-current="page"` : ""}>` +
21270
21348
  `<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
21271
21349
  `<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>` +
21272
21350
  (row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
@@ -21295,7 +21373,7 @@ function reviewCockpitPage(chrome, data) {
21295
21373
  const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
21296
21374
  const reasons = row.priority.reasons;
21297
21375
  return {
21298
- title: row.title, href: reviewHref(row.taskId, row.runId, row.repo), at: row.completedAt,
21376
+ title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
21299
21377
  status: status === null ? null : { label: status.label, tone: status.tone },
21300
21378
  notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
21301
21379
  current: selected !== null && selected.taskId === row.taskId,
@@ -21377,6 +21455,15 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21377
21455
  problem: view.detail === null ? view.historyProblem : null,
21378
21456
  next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21379
21457
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21458
+ acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
21459
+ // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
21460
+ record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
21461
+ { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
21462
+ { label: "Worker", value: run.runner },
21463
+ ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
21464
+ ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
21465
+ ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
21466
+ ] },
21380
21467
  };
21381
21468
  if (run === null || view.detail === null) {
21382
21469
  parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
@@ -21419,7 +21506,8 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21419
21506
  // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21420
21507
  const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21421
21508
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21422
- const decision = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21509
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21510
+ const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21423
21511
  checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21424
21512
  unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21425
21513
  yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
@@ -21427,6 +21515,60 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21427
21515
  publishing: view.detail?.publishing ?? "other",
21428
21516
  proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21429
21517
  }) };
21518
+ // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21519
+ const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
21520
+ // 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.
21521
+ const followUps = view.detail.followUps ?? null;
21522
+ const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
21523
+ const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
21524
+ (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
21525
+ // Back on this result's Checks tab, where #follow-ups shows the run it started.
21526
+ ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21527
+ const need = panel.panel.need;
21528
+ const nextKind = selected.next?.kind;
21529
+ const acts = resultActsOf({
21530
+ accept: decision === null ? null : { ready: decision.ready },
21531
+ runChecks: runChecks !== null,
21532
+ checksRunning,
21533
+ blocked,
21534
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
21535
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21536
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21537
+ });
21538
+ // A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
21539
+ // match the changes (not a failed check), that is the headline.
21540
+ const mismatch = proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
21541
+ const patch = view.detail?.terminal?.patch ?? null;
21542
+ const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
21543
+ // Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
21544
+ const hunkStart = new Map();
21545
+ const changes = new Map(files.map(file => {
21546
+ const shown = file.hunks[0]?.lines.filter(line => line.newLine !== null && line.kind !== "meta") ?? [];
21547
+ if (shown[0]?.newLine != null)
21548
+ hunkStart.set(file.path, shown[0].newLine);
21549
+ const added = shown.filter(line => line.kind === "addition");
21550
+ const lines = (added.length > 0 ? added : shown).map(line => line.newLine);
21551
+ return [file.path, lines.length === 0 ? null : { from: Math.min(...lines), to: Math.max(...lines) }];
21552
+ }));
21553
+ const changesHref = `${here}&run=${run.id}&tab=changes`;
21554
+ const found = reportMismatchesOf(proof.reasons, proof.proofProblem === null ? proof.matrix : [], changes);
21555
+ // The saved report itself can't be read: that is the evidence problem, said once.
21556
+ if (proof.proofProblem !== null)
21557
+ found.push({ text: `The saved report can't be read: ${proof.proofProblem}`, path: null, lines: null, inChanges: null, note: null, reason: proof.proofProblem });
21558
+ if (found.length === 0)
21559
+ return null;
21560
+ return {
21561
+ headline: evidenceProblemOf(proof.verdict, proof.reasons) === "mismatched" ? MISMATCH_HEADLINE : null,
21562
+ rows: found.map(one => ({
21563
+ text: one.text, path: one.path, absent: one.inChanges === false,
21564
+ lines: one.lines === null ? null : one.lines.from === one.lines.to ? `line ${one.lines.from}` : `lines ${one.lines.from}–${one.lines.to}`,
21565
+ href: one.path !== null && one.inChanges === true ? `${changesHref}#${diffFileAnchor(one.path)}${hunkStart.has(one.path) ? `-L${hunkStart.get(one.path)}` : ""}`
21566
+ : one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${here}&run=${run.id}#report-note-${one.note}` : null,
21567
+ noteLabel: one.path === null && one.note !== null ? `The report, note ${one.note}` : null,
21568
+ })),
21569
+ said: [...new Set([...proof.reasons, ...found.map(one => one.reason)].flatMap(reason => [reason, plainReasonWords(reason)]))],
21570
+ };
21571
+ })();
21430
21572
  if (view.notes.length > 0) {
21431
21573
  parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21432
21574
  view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
@@ -21435,7 +21577,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21435
21577
  return {
21436
21578
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21437
21579
  selected: {
21438
- ...selected, complete, decision, panel: panel.panel, contest,
21580
+ ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks, mismatch,
21439
21581
  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}` },
21440
21582
  },
21441
21583
  };
@@ -21530,38 +21672,9 @@ function homePhaseWords(run) {
21530
21672
  function phaseWords(phase) {
21531
21673
  return PHASE_WORDS[phase] ?? "the agent is working";
21532
21674
  }
21533
- const REASON_WORDS = {
21534
- agent: "the agent failed",
21535
- "agent-reported": "the agent reported it could not finish",
21536
- "no-op": "nothing changed when something should have",
21537
- "no-handoff": "the agent stopped before handing off; its work was kept and it is being resumed",
21538
- "moved-head": "the branch moved underneath the build",
21539
- "moved-branch": "the branch moved underneath the build",
21540
- timeout: "ran out of time",
21541
- git: "a git step failed",
21542
- "malformed-decision": "the agent's question was malformed",
21543
- "malformed-plan": "the plan was malformed",
21544
- fenced: "another worker took the task over",
21545
- unapproved: "the scope was not approved",
21546
- "scope-changed": "the scope changed after approval",
21547
- capability: "a requirement was missing",
21548
- setup: "the workspace preparation step failed",
21549
- "provider-init": "the agent could not start",
21550
- "commit-failure": "the commit failed",
21551
- "protected-branch": "refused to touch a protected branch",
21552
- "wrong-branch": "the checkout was on the wrong branch",
21553
- "not-leased": "the lease was not valid",
21554
- "no-claim": "the lease was not valid",
21555
- "not-yours": "the lease was not valid",
21556
- "no-run-record": "the run record was missing",
21557
- "missing-mailbox": "the resume mailbox could not be read",
21558
- "unreadable-mailbox": "the resume mailbox could not be read",
21559
- "revision-brief": "the revision brief could not be read",
21560
- "repaired-park": "resumed from a parked question",
21561
- stopped: "stopped by the operator",
21562
- };
21675
+ /** A recorded reason in one plain line; machine output (a stack trace, a path, "Error:" text) waits on the run record. */
21563
21676
  function reasonWords(reason) {
21564
- return REASON_WORDS[reason] ?? "stopped — the build records have the detail";
21677
+ return isInternalErrorReason(reason) ? "stopped with an internal error" : oneLineOf(runReasonWords(reason.trim().split(/\r?\n/, 1)[0] ?? ""), 140);
21565
21678
  }
21566
21679
  const INCIDENT_WORDS = {
21567
21680
  "malformed-decision": "the agent's question was malformed",
@@ -22011,18 +22124,23 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
22011
22124
  : counts.additions === null || counts.deletions === null
22012
22125
  ? `<span class="diff-file-counts">binary</span>`
22013
22126
  : `<span class="diff-file-counts"><b>+${counts.additions}</b><i>−${counts.deletions}</i></span>`;
22014
- const hunks = one.hunks.map(hunk => `<section class="diff-hunk"><div class="diff-hunk-head">${escape(hunk.header)}</div>` +
22015
- `<div class="diff-lines">${hunk.lines.map(line => {
22016
- const lineNumber = line.newLine ?? line.oldLine;
22017
- const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
22018
- const annotate = !commentable || lineNumber === null || line.kind === "meta"
22019
- ? `<span class="diff-annotate-space"></span>`
22020
- : `<button type="button" class="diff-annotate pick-line" data-path="${escape(one.path)}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${escape(one.path)}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
22021
- const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
22022
- return (`<div class="diff-line diff-${line.kind}">${annotate}` +
22023
- `<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span>` +
22024
- `<code><b aria-hidden="true">${marker}</b>${escape(line.text)}</code></div>`);
22025
- }).join("")}</div></section>`).join("");
22127
+ const fileAnchor = anchors.get(one.path);
22128
+ // Each change is a link target of its own (#<file anchor>-L<first new line>): a result names the exact lines.
22129
+ const hunks = one.hunks.map(hunk => {
22130
+ const first = hunk.lines.find(line => line.newLine !== null && line.kind !== "meta")?.newLine ?? null;
22131
+ return `<section class="diff-hunk"${fileAnchor === undefined || first === null ? "" : ` id="${fileAnchor}-L${first}"`}><div class="diff-hunk-head">${escape(hunk.header)}</div>` +
22132
+ `<div class="diff-lines">${hunk.lines.map(line => {
22133
+ const lineNumber = line.newLine ?? line.oldLine;
22134
+ const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
22135
+ const annotate = !commentable || lineNumber === null || line.kind === "meta"
22136
+ ? `<span class="diff-annotate-space"></span>`
22137
+ : `<button type="button" class="diff-annotate pick-line" data-path="${escape(one.path)}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${escape(one.path)}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
22138
+ const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
22139
+ return (`<div class="diff-line diff-${line.kind}">${annotate}` +
22140
+ `<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span>` +
22141
+ `<code><b aria-hidden="true">${marker}</b>${escape(line.text)}</code></div>`);
22142
+ }).join("")}</div></section>`;
22143
+ }).join("");
22026
22144
  const anchor = anchors.get(one.path);
22027
22145
  return (`<details class="diff-file"${fileIndex === 0 ? " open" : ""}${anchor === undefined ? "" : ` id="${anchor}"`}>` +
22028
22146
  `<summary><span class="diff-file-name">${escape(one.path)}</span>${countWords}</summary>` +
@@ -22050,7 +22168,9 @@ function checkLogHtml(log, runId, attributes) {
22050
22168
  if (log.problem !== null) {
22051
22169
  return `<p class="problem" data-check-log="damaged"${attributes}>The check log no longer verifies (${escape(log.problem)}). Its output is not shown, and there is nothing to download.</p>`;
22052
22170
  }
22053
- return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${escape(log.text)}</pre></details>` +
22171
+ return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${
22172
+ // Each line is a link target (#check-log-L<n>): a failed task names the exact line its check ended on.
22173
+ log.text.split("\n").map((line, index) => `<span id="check-log-L${index + 1}">${escape(line)}</span>`).join("\n")}</pre></details>` +
22054
22174
  storedDownloadLink(runId, log.artifactId, "check log", log.truncated));
22055
22175
  }
22056
22176
  /** The download link for a stored record (repair 2026-09-14): a shortened
@@ -22344,7 +22464,7 @@ function resultPanelParts(detail, o) {
22344
22464
  // Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
22345
22465
  // On the result itself, "Review result" would link here: accepting it is what resolves it.
22346
22466
  const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
22347
- accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`,
22467
+ accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`, refuted: proof?.verdict === "refuted",
22348
22468
  // What Store.finalResultReason refuses: a verified, attested, accepted or published result.
22349
22469
  rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
22350
22470
  });
@@ -22466,6 +22586,8 @@ function resultPanelParts(detail, o) {
22466
22586
  ...(conclusion !== outcome ? [`<p class="recap">${escape(conclusion)}</p>`] : []),
22467
22587
  ...(handoff !== null && handoff.changes.length > 0 ? [`<ul class="result-changes">${handoff.changes.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
22468
22588
  ...(handoff !== null && handoff.verification.length > 0 ? [`<p class="meta">Checked by the agent:</p><ul class="result-changes" data-cockpit-source="agent-words">${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
22589
+ // The report's own notes, each a link target (#report-note-<n>) a mismatch can point at.
22590
+ ...(proof?.proof == null || proof.proof.caveats.length === 0 ? [] : [`<p class="meta">Its notes:</p><ol class="result-changes" data-report-notes>${proof.proof.caveats.map((one, index) => `<li id="report-note-${index + 1}">${escape(one)}</li>`).join("")}</ol>`]),
22469
22591
  ];
22470
22592
  if (agentAccount.length > 0)
22471
22593
  summaryParts.push(`<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount.join("")}</details>`);
@@ -22762,7 +22884,7 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22762
22884
  if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
22763
22885
  return { label: status.need.action.label, ...rebuild };
22764
22886
  if (result !== undefined) {
22765
- if (status.need.key === "review-result" && !result.accepted && csrf !== "")
22887
+ if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
22766
22888
  return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
22767
22889
  accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
22768
22890
  // The task page sends the person here: never back to it. What this page can do leads — Accept
@@ -22772,8 +22894,9 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22772
22894
  return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
22773
22895
  if (here && csrf !== "" && result.rebuildable === true)
22774
22896
  return { label: NEEDS.rebuild.action.label, ...rebuild };
22897
+ // Nothing here resolves it: no act at all, never a link back to the task (which sends the person here).
22775
22898
  if (status.need.key === "review-result" || here)
22776
- return { label: "Open the task", href: taskHref(assignment.rootId), confirm: null };
22899
+ return null;
22777
22900
  }
22778
22901
  const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
22779
22902
  ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
@@ -22802,7 +22925,7 @@ function resultNeedAction(need, o, canRequest) {
22802
22925
  function conciseOutcomeOf(conclusion) {
22803
22926
  const flat = oneLineOf(conclusion, 2_000);
22804
22927
  const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
22805
- const bounded = sentence.length <= 180 ? sentence : `${sentence.slice(0, 180).replace(/\s+\S*$/, "")}…`;
22928
+ const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
22806
22929
  return bounded.trim();
22807
22930
  }
22808
22931
  /** Exactly one primary act for the result (repair 2026-09-14), chosen from
@@ -22854,7 +22977,7 @@ function runFactsRows(run, taskId, live, route = null) {
22854
22977
  ["quality", qualityModeTitle(run.qualityMode ?? "default")],
22855
22978
  ["outcome", live ? "running" : (run.outcome ?? "never finished")],
22856
22979
  ["phase", live && run.phase !== null ? phaseWords(run.phase) : null],
22857
- ["reason", run.reason === null ? null : reasonWords(run.reason)],
22980
+ ["reason", run.reason === null ? null : isInternalErrorReason(run.reason) ? "stopped with an internal error (below)" : reasonWords(run.reason)],
22858
22981
  ["runner", run.runner, true],
22859
22982
  ["branch", run.branch, true],
22860
22983
  ["model", run.model, true],
@@ -22880,7 +23003,11 @@ function runFactsRows(run, taskId, live, route = null) {
22880
23003
  .filter((fact) => fact[1] !== null && fact[1] !== "")
22881
23004
  .map(([label, value, mono]) => `<p class="row"><span class="meta" style="min-width:8.5rem">${escape(label)}</span> ` +
22882
23005
  `<span${mono === true ? ` class="mono"` : ""}>${escape(value)}</span></p>`)
22883
- .join("\n") + elapsed);
23006
+ .join("\n") + elapsed +
23007
+ // Machine output (a stack trace, a path, "Error:" text) reads as an internal error everywhere else; its detail is here, as recorded.
23008
+ (run.reason !== null && isInternalErrorReason(run.reason)
23009
+ ? `<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">${escape(run.reason)}</pre>`
23010
+ : ""));
22884
23011
  }
22885
23012
  function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, heldTurns = null, continueOffer = null, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
22886
23013
  const rows = runFactsRows(run, taskId, running, route);
@@ -86,10 +86,12 @@ export type TaskStatusFacts = {
86
86
  report?: boolean;
87
87
  checks?: ChecksFact | null;
88
88
  pullRequest?: PullRequestFact | null;
89
+ /** `unverified`: the report is refuted, so no requirement counts as met however it was marked. */
89
90
  requirements?: {
90
91
  met: number;
91
92
  total: number;
92
93
  yours: number;
94
+ unverified?: boolean;
93
95
  } | null;
94
96
  evidence?: {
95
97
  shortened: number;
@@ -159,6 +161,8 @@ export declare function requirementsOf(matrix: readonly {
159
161
  } | undefined;
160
162
  review?: unknown;
161
163
  }[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
164
+ /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
165
+ export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
162
166
  /** Saved evidence health from the receipt's artifact list and its caveats. */
163
167
  export declare function evidenceOf(receipt: AssignmentSnapshot["receipt"], problems?: readonly string[]): NonNullable<TaskStatusFacts["evidence"]> | null;
164
168
  /** The status facts of one assignment, as every surface reads them. */