toolroll 0.9.5 โ†’ 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.
@@ -8,6 +8,7 @@ import type { Store } from './store.js';
8
8
  import type { WorkSummaryAccess } from './work-summary.js';
9
9
  import type { StatusTone } from './workspace-ui.js';
10
10
  import type { AssignmentCard } from './assignment-ui.js';
11
+ import type { TaskStatus } from './task-status.js';
11
12
  import type { FirstRunStep, FirstTaskSuggestion } from './first-run.js';
12
13
  export type BrowserProject = {
13
14
  name: string;
@@ -365,12 +366,6 @@ export type BrowserProjectsView = {
365
366
  };
366
367
  };
367
368
  export type BrowserResultTab = 'summary' | 'changes' | 'checks';
368
- export type BrowserResultChip = {
369
- tone: 'success' | 'danger' | 'warning' | 'info' | 'neutral';
370
- label: string;
371
- icon: 'check' | 'x' | null;
372
- title: string | null;
373
- };
374
369
  /** The shared result panel in parts. Each tab's content, the feedback
375
370
  * section and the learning card are the server's own HTML; the page script
376
371
  * binds to the same data attributes and ids (tabs, drafts, line notes). */
@@ -378,10 +373,8 @@ export type BrowserResultPanel = {
378
373
  attributes: Record<string, string>;
379
374
  heading: string;
380
375
  outcome: string;
381
- verdict: {
382
- chips: BrowserResultChip[];
383
- by: string | null;
384
- } | null;
376
+ /** The shared task status (task-status.ts): headline, sentence and detail rows. */
377
+ status: TaskStatus | null;
385
378
  reviewHistory: string | null;
386
379
  attention: string[];
387
380
  /** Requirements only a person can confirm, in plain words, and the one
@@ -6,7 +6,7 @@ export function acceptanceStatus(proof, accepted) {
6
6
  if (accepted)
7
7
  return manualReviewOnly(proof) ? "Accepted by a person" : "Accepted with an exception";
8
8
  if (manualReviewOnly(proof))
9
- return "Ready to inspect";
9
+ return "You check the remaining requirements";
10
10
  return proof === null ? "No checks recorded" : ({ verified: "Checks passed", attested: "Checks reported by the agent", short: "Some required material is missing", refuted: "Saved result conflicts with the approved scope" }[proof.verdict] ?? "Some required material is missing");
11
11
  }
12
12
  export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0) {
@@ -75,7 +75,7 @@ export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0
75
75
  accepted: acceptance !== null, acceptance: acceptance === null ? null : { at: acceptance.acceptedAt, note: brief(acceptance.note) },
76
76
  reasons: (recorded?.reasons ?? []).map(one => brief(one)), gate,
77
77
  criteriaTotal: matrix.length, nextCriterionOffset: offset + page.length < matrix.length ? offset + page.length : null,
78
- criteria: page.map(row => ({ id: row.id, requirement: brief(row.statement), state: row.state === "manual-review" ? "Human review required" : row.state,
78
+ criteria: page.map(row => ({ id: row.id, requirement: brief(row.statement), state: row.state === "manual-review" ? "You check" : row.state,
79
79
  details: row.detail.map(one => brief(one)), requiredEvidence: row.requiredEvidence,
80
80
  reviewer: row.review == null ? null : { judgement: row.review.judgement, note: brief(row.review.note) },
81
81
  evidence: row.answered.map(one => ({ kind: one.kind, reference: brief(one.ref) })),
@@ -7,6 +7,7 @@
7
7
  import { assignmentOf } from "./assignment.js";
8
8
  import { chatControlHref } from "./chat-controls.js";
9
9
  import { telegramProgressCard } from "./telegram-progress.js";
10
+ import { assignmentStatusFacts, headlineEmoji, taskStatusOf } from "./task-status.js";
10
11
  import { phoneText, projectLabel } from "./telegram-status.js";
11
12
  import { isLifecycleNotification } from "./store.js";
12
13
  /** What always makes a new message, even in quiet mode: a question or approval waiting, a failure or other
@@ -37,15 +38,16 @@ function cardRun(store, taskRef) {
37
38
  return store.runsFor(taskRef).find(run => run.role === "builder" && run.contestant === null) ?? null;
38
39
  }
39
40
  /** Before any attempt starts, the card says where the task stands in the words of its last lifecycle fact. */
41
+ const before = (headline) => ({ icon: headlineEmoji(headline), status: headline });
40
42
  const BEFORE_RUN = {
41
- "task-filed": { icon: "๐Ÿ“", status: "Filed" },
42
- "scope-approved": { icon: "โœ“", status: "Approved ยท Waiting to start" },
43
- "approval-withdrawn": { icon: "๐Ÿ“", status: "Approval withdrawn" },
44
- "task-held": { icon: "โธ", status: "Paused" },
45
- "task-released": { icon: "โœ“", status: "Waiting to start" },
46
- "task-queued": { icon: "โœ“", status: "Waiting to start" },
47
- "task-requeued": { icon: "โœ“", status: "Waiting to start" },
48
- "task-cancelled": { icon: "โน", status: "Cancelled" },
43
+ "task-filed": before("Queued"),
44
+ "scope-approved": before("Queued"),
45
+ "approval-withdrawn": before("Needs you"),
46
+ "task-held": before("Stopped"),
47
+ "task-released": before("Queued"),
48
+ "task-queued": before("Queued"),
49
+ "task-requeued": before("Queued"),
50
+ "task-cancelled": before("Stopped"),
49
51
  };
50
52
  function taskLine(store, taskRef, now, root) {
51
53
  const ref = store.refById(taskRef);
@@ -85,10 +87,8 @@ export function quietCardView(store, taskRefs, now, root) {
85
87
  export function readyPingView(store, run, taskId, project, now, root) {
86
88
  const card = telegramProgressCard(store, run, taskId, project, now, root);
87
89
  const [title = "", shown = ""] = card.entities.map(one => card.text.slice(one.offset, one.offset + one.length));
88
- // A saved result is Ready even in the moment before its worker lets go of the task.
89
- const saved = shown.endsWith(" Result saved");
90
- const heading = saved ? "โœ… Ready" : shown;
91
- const next = card.next !== "" ? card.next : saved ? "Next: open the result, then mark it complete or request changes." : "";
90
+ const heading = shown;
91
+ const next = card.next;
92
92
  const first = `${heading} ยท ${title}`;
93
93
  return { text: next === "" ? first : `${first}\n${next}`, entities: [{ type: "bold", offset: 0, length: heading.length }], link: card.link };
94
94
  }
@@ -120,18 +120,19 @@ export function eveningDigestText(store, account, now, root) {
120
120
  failed.set(row.taskId, title(row.taskId));
121
121
  }
122
122
  for (const taskId of new Set(facts.map(row => row.taskId).filter((one) => one !== null))) {
123
- let state = null;
123
+ let headline = null;
124
124
  try {
125
125
  const project = facts.find(row => row.taskId === taskId).project;
126
- state = assignmentOf(store, taskId, now, { principal: "operator", repos: [project] }, root)?.state ?? null;
126
+ const assignment = assignmentOf(store, taskId, now, { principal: "operator", repos: [project] }, root);
127
+ headline = assignment === null ? null : taskStatusOf(assignmentStatusFacts(assignment)).headline;
127
128
  }
128
129
  catch {
129
- state = null;
130
+ headline = null;
130
131
  }
131
- if (state === "ready-to-check")
132
- waits.set(taskId, `${title(taskId)} ยท Ready for review`);
133
- else if (state === "needs-decision")
134
- waits.set(taskId, `${title(taskId)} ยท Needs your decision`);
132
+ if (headline === "Ready for review" || headline === "Needs you")
133
+ waits.set(taskId, `${title(taskId)} ยท ${headline}`);
134
+ else if (headline === "Failed")
135
+ failed.set(taskId, title(taskId));
135
136
  }
136
137
  if (finished.size + failed.size + waits.size === 0)
137
138
  return null;
@@ -1,8 +1,3 @@
1
- /**
2
- * Chat in `toolroll demo`: the scripted lead's conversation (src/demo.ts),
3
- * rendered as plain server HTML with native forms. One small script keeps a
4
- * running build's progress live; without it the page still works by reload.
5
- */
6
1
  import type { DemoExchange } from "./demo.js";
7
2
  /** What a finished exchange's stored result holds, read back from its evidence. */
8
3
  export type DemoResultView = {
package/dist/demo-chat.js CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Chat in `toolroll demo`: the scripted lead's conversation (src/demo.ts),
3
+ * rendered as plain server HTML with native forms. One small script keeps a
4
+ * running build's progress live; without it the page still works by reload.
5
+ */
6
+ import { headlineOf } from "./task-status.js";
1
7
  const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
2
8
  const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3
9
  const hidden = (csrf) => `<input type="hidden" name="csrf" value="${esc(csrf)}">`;
@@ -52,9 +58,11 @@ function diffHtml(diff) {
52
58
  function resultCard(exchange, csrf, result) {
53
59
  if (result === null)
54
60
  return `<section class="card demo-result"><p class="meta">This result's saved evidence is unavailable.</p></section>`;
55
- const status = exchange.state === "complete" ? `<span class="badge demo-complete">Complete</span>`
56
- : exchange.state === "sent-back" ? `<span class="badge">Sent back</span>` : `<span class="badge demo-ready">Ready</span>`;
57
61
  const passed = result.checks.status === "passed";
62
+ // The shared headline (task-status.ts): the same words as every real task.
63
+ const headline = headlineOf({ stage: exchange.state === "complete" ? "complete" : exchange.state === "sent-back" ? "building" : "finished",
64
+ checks: { status: passed ? "passed" : "failed", exitCode: null, head: null } });
65
+ const status = `<span class="badge${headline === "Complete" ? " demo-complete" : headline === "Ready for review" ? " demo-ready" : ""}" data-headline="${headline}">${headline}</span>`;
58
66
  const summary = `${result.files} file${result.files === 1 ? "" : "s"} changed ยท +${result.additions} โˆ’${result.deletions} ยท ` +
59
67
  `<span class="${passed ? "demo-pass" : "demo-fail"}">${esc(result.checks.detail)}</span>`;
60
68
  const actions = exchange.state === "ready"
@@ -69,6 +69,13 @@ export type InstallationStatus = {
69
69
  command: string;
70
70
  since: string;
71
71
  }[];
72
+ /** The most urgent tasks with the shared headline and sentence (task-status.ts), as every other surface words them. */
73
+ tasks: {
74
+ task: string;
75
+ title: string;
76
+ headline: string;
77
+ sentence: string;
78
+ }[];
72
79
  };
73
80
  /** One indexed observation for `task wait`. `watchedRun` is the attempt the
74
81
  * caller first saw. Each poll follows the task's newest attempt from there, so
@@ -4,6 +4,7 @@ import { COMPLETION_ACTION } from "./result-completion.js";
4
4
  import { windowLabel } from "./provider-limits.js";
5
5
  import { openAuthPauses, signInCommand, signInReason } from "./provider-auth.js";
6
6
  import { BUILT_IN, checkSuitesOf } from "./store.js";
7
+ import { workIndexPage } from "./work-index.js";
7
8
  const maybeNumber = (value) => value === null || value === undefined || !Number.isInteger(Number(value)) ? null : Number(value);
8
9
  const CHECK_COLUMNS = "rc.status AS check_status, rc.exit_code AS check_exit_code, rc.suites_json AS check_suites_json";
9
10
  function checkOf(row) {
@@ -253,6 +254,8 @@ export function installationStatus(store, now) {
253
254
  observedAt: String(row["observed_at"]),
254
255
  })),
255
256
  signIn: pauses.map(one => ({ provider: one.provider, reason: signInReason(one), command: signInCommand(one), since: one.openedAt })),
257
+ tasks: workIndexPage(store, now, { principal: "operator", repos: null, includeUnplaced: true }, { limit: 8 }).items
258
+ .map(one => ({ task: one.rootId, title: one.title, headline: one.status.label, sentence: one.status.detail })),
256
259
  };
257
260
  }
258
261
  export function renderInstallationStatus(status) {
@@ -260,7 +263,13 @@ export function renderInstallationStatus(status) {
260
263
  // A paused provider comes first: it is the one thing a person must do.
261
264
  for (const one of status.signIn)
262
265
  lines.push(`${one.reason} โ€” run \`${one.command}\`. Its tasks wait until then.`);
263
- lines.push(status.running.count === 0 ? "Running: none" : `Running: ${status.running.count} โ€” ${status.running.tasks.map(one => `${one.task} (#${one.run}, ${one.phase})`).join(", ")}${status.running.count > status.running.tasks.length ? ", โ€ฆ" : ""}`);
266
+ // Each task's one headline, the same words as the console and chat cards.
267
+ if (status.tasks.length > 0) {
268
+ lines.push("Tasks:");
269
+ for (const one of status.tasks)
270
+ lines.push(` ${one.headline.padEnd(16)} ${one.title} (${one.task}) โ€” ${one.sentence}`);
271
+ }
272
+ lines.push(status.running.count === 0 ? "Building: none" : `Building: ${status.running.count} โ€” ${status.running.tasks.map(one => `${one.task} (#${one.run}, ${one.phase})`).join(", ")}${status.running.count > status.running.tasks.length ? ", โ€ฆ" : ""}`);
264
273
  lines.push(status.queued.count === 0 ? "Queued: none" : `Queued: ${status.queued.count} โ€” ${status.queued.tasks.map(one => `${one.task} (${one.reason})`).join(", ")}${status.queued.count > status.queued.tasks.length ? ", โ€ฆ" : ""}`);
265
274
  lines.push(status.waitingForReview.count === 0 ? "Ready for review: none" : `Ready for review: ${status.waitingForReview.count} โ€” ${status.waitingForReview.results.map(one => `${one.task} (#${one.run})`).join(", ")}${status.waitingForReview.count > status.waitingForReview.results.length ? ", โ€ฆ" : ""}`);
266
275
  if (status.releaseCheck === null) {
@@ -289,7 +298,7 @@ export function renderInstallationStatus(status) {
289
298
  lines.push(`Plan windows: ${provider} โ€” ${detail}`);
290
299
  }
291
300
  }
292
- return lines.slice(0, 12 + status.signIn.length);
301
+ return lines.slice(0, 12 + status.signIn.length + (status.tasks.length === 0 ? 0 : status.tasks.length + 1));
293
302
  }
294
303
  export function renderTaskWait(snapshot, outcome = snapshot.outcome) {
295
304
  const check = snapshot.check.status === "unknown" ? "checks unknown" : snapshot.check.status === "not-run" ? "checks not run" : `checks ${snapshot.check.status}${snapshot.check.exitCode === null ? "" : ` (exit ${snapshot.check.exitCode})`}`;
package/dist/serve.js CHANGED
@@ -64,6 +64,7 @@ import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequest
64
64
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
65
65
  import { assignmentCatchUp } from './assignment-brief.js';
66
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';
67
68
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
68
69
  import { ledgerBody } from "./ledger-view.js";
69
70
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -152,7 +153,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
152
153
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
153
154
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
154
155
  import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
155
- import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
156
+ import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
156
157
  import { PRICED_BUILD_MODELS } from "./pricing.js";
157
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";
158
159
  import { parseReport } from "./scout-report.js";
@@ -4907,8 +4908,6 @@ export function createDecisionServer(options) {
4907
4908
  const result = receipt === undefined && task.state === "done" && latest !== null && (latest.outcome === "built" || latest.outcome === "no-change")
4908
4909
  ? completionReceiptView(store, latest, store.artifactsFor(latest.id), evidenceRoot, reviewFactsFor(latest.id)) : receipt;
4909
4910
  const status = workStatusOf(facts, result == null ? undefined : receiptStatusOf(result));
4910
- if (store.lookupRef(task.id)?.revisionOf !== null && live !== null && status.tone === "live")
4911
- status.label = "Revising";
4912
4911
  const earlierLive = task.family === undefined ? [] : earlierLiveVersions(task.family, now);
4913
4912
  if (earlierLive.length > 0 && !status.views.includes("running"))
4914
4913
  status.views = [...status.views, "running"];
@@ -5495,8 +5494,12 @@ export function createDecisionServer(options) {
5495
5494
  const publishing = publishingOf(store, repo);
5496
5495
  const target = publishing.on ? publishing.base : null;
5497
5496
  const view = newestPullRequestOf(store, family === null ? [taskId] : family.versions.map(one => one.id));
5498
- if (view !== null)
5499
- 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
+ }
5500
5503
  if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
5501
5504
  return null;
5502
5505
  const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
@@ -11242,7 +11245,8 @@ export function editorFileHref(worktree, path, line) {
11242
11245
  * chat (v39, extending Priority 2's "one surface, six places" rule from
11243
11246
  * the verdict word to the per-criterion state). */
11244
11247
  function matrixStateBadge(state) {
11245
- 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";
11246
11250
  const word = state === "pass" ? "pass" : state === "missing" ? "missing" : state === "failed" ? "failed" : "manual review";
11247
11251
  return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(word)}</span>`;
11248
11252
  }
@@ -11328,7 +11332,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11328
11332
  if (matrix.length === 0)
11329
11333
  return "";
11330
11334
  const kinds = {
11331
- 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",
11332
11336
  };
11333
11337
  const evidenceLink = (a) => {
11334
11338
  const text = `<code>${escape(a.ref)}</code>`;
@@ -11344,8 +11348,9 @@ function criterionMatrixHtml(matrix, options = {}) {
11344
11348
  const state = row.state;
11345
11349
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
11346
11350
  const confirmed = row.assessment !== undefined && state === "pass";
11347
- const label = awaitingAssessment ? "Not assessed" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
11348
- 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";
11349
11354
  const warnings = [];
11350
11355
  // Only replace the known boilerplate. Other recorded failure details
11351
11356
  // remain verbatim, so concision cannot hide a different problem.
@@ -11978,6 +11983,10 @@ ${THEME_DARK}
11978
11983
  background: color-mix(in srgb, var(--muted) 58%, var(--card));
11979
11984
  color: color-mix(in srgb, var(--destructive) 68%, var(--foreground)); border-color: var(--border);
11980
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
+ }
11981
11990
  .badge-overdue {
11982
11991
  background: color-mix(in srgb, var(--muted) 58%, var(--card)); color: var(--foreground);
11983
11992
  border-color: var(--border);
@@ -12818,8 +12827,9 @@ ${THEME_DARK}
12818
12827
  .dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before,
12819
12828
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before {
12820
12829
  content: ""; display: inline-block; width: .45rem; height: .45rem; margin-right: .45rem; border-radius: 50%; vertical-align: .08rem;
12821
- background: var(--destructive);
12830
+ background: var(--so-warning, #ab6400);
12822
12831
  }
12832
+ .problem.dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before { background: var(--destructive); }
12823
12833
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before { background: var(--muted-foreground); }
12824
12834
  /* A review in flight (review fixes): the box reads neutral โ€” neither the
12825
12835
  green of a settled verdict nor the red of a problem โ€” with the dot in
@@ -13320,7 +13330,7 @@ ${THEME_DARK}
13320
13330
  .pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }
13321
13331
  .pull-request-head strong::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }
13322
13332
  .pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }
13323
- .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); }
13324
13334
  .pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }
13325
13335
  .pull-request-act .meta { flex: 1 1 100%; }
13326
13336
  .pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }
@@ -14513,7 +14523,7 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
14513
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}`;
14514
14524
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
14515
14525
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
14516
- 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}');
14517
14527
  /**
14518
14528
  * The board's liveness: fetch this page's own fragment on a timer and swap
14519
14529
  * it in place โ€” no flicker, no scroll reset, no long-lived stream to manage,
@@ -15975,7 +15985,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
15975
15985
  const dispatch = task.dispatch;
15976
15986
  rows.push(`<a class="chat-overview-item decision" href="${taskHref(task.rootId ?? task.id)}" data-dispatch-status="${escape(dispatch.code)}">` +
15977
15987
  `<span class="chat-overview-icon">${strokeIcon(`<path d="M12 8v4"/><path d="M12 16h.01"/><circle cx="12" cy="12" r="9"/>`)}</span>` +
15978
- `<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>` +
15979
15989
  `<span class="chat-overview-arrow" aria-hidden="true">โ†’</span></a>`);
15980
15990
  }
15981
15991
  for (const task of snapshot.tasks.filter(one => one.state === "failed").slice(0, Math.max(0, 3 - rows.length))) {
@@ -17212,6 +17222,11 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
17212
17222
  }
17213
17223
  /** The one status line every surface renders: a dot in the tone, the
17214
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
+ }
17215
17230
  function statusLineHtml(status, extra = "") {
17216
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>`;
17217
17232
  }
@@ -17265,7 +17280,7 @@ function workPage(chrome, data) {
17265
17280
  const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
17266
17281
  ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}${row.repo === null ? '' : '&project=' + encodeURIComponent(row.repo)}`
17267
17282
  : browserWorkActionHref(row);
17268
- 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;
17269
17284
  return {
17270
17285
  id: row.rootId, title: row.title, href: taskHref(row.rootId),
17271
17286
  project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
@@ -18331,7 +18346,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
18331
18346
  ? `<span class="badge">queued</span>`
18332
18347
  : `<span class="badge">reserved for ${escape(column)}</span>`;
18333
18348
  const chips = ` ${state}` +
18334
- `${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>`}`;
18335
18350
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
18336
18351
  `<input type="hidden" name="projectRevision" value="${revision}">` +
18337
18352
  `<input type="hidden" name="queueRevision" value="${queueRevision}">` +
@@ -18802,7 +18817,11 @@ function taskBodyParts(data) {
18802
18817
  // data-work-status is the shared projection's token (package 1) โ€”
18803
18818
  // the same one the Work row, chat, and cockpit carry for this task.
18804
18819
  const workToken = resultStatus?.token ?? data.dispatch?.code ?? "unknown";
18805
- 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)}">` +
18806
18825
  `<div class="dispatch-copy"><strong>${escape(inReview ? "Recorded checks" : title)}</strong><span class="meta">${detail}</span></div>${controls}</div>`;
18807
18826
  if (task.state !== "done") {
18808
18827
  const diagnosis = data.dispatch ?? null;
@@ -19689,7 +19708,13 @@ function taskBodyParts(data) {
19689
19708
  : ` ยท filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` ยท <span class="badge">scout</span>` : ""}</p>`;
19690
19709
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
19691
19710
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
19692
- 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);
19693
19718
  // The result takes over from the task status as soon as it is ready.
19694
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);
19695
19720
  // The exact-run control (v52), directly under the scheduler's answer:
@@ -19819,7 +19844,7 @@ function taskBodyParts(data) {
19819
19844
  approval: approvalHtml,
19820
19845
  lead: [
19821
19846
  { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
19822
- { key: "pull-request", html: data.pullRequest == null ? "" : pullRequestCardHtml(data.pullRequest, data.csrf) },
19847
+ { key: "pull-request", html: pullRequestCard },
19823
19848
  { key: "needs-scope", html: needsScopeCard }, { key: "progress", html: progressCard }, { key: "revisions", html: revisionLedgerCard },
19824
19849
  { key: "plan", html: planCard }, { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
19825
19850
  ].filter(one => one.html !== ""),
@@ -20071,11 +20096,11 @@ function cockpitStatusOf(view) {
20071
20096
  // The receipt's own status (package 3): the same projection the chat
20072
20097
  // receipt and the run page print, read from the shared detail.
20073
20098
  if (view.run === null || view.detail === null)
20074
- return view.assignment === null ? resultStatusOf(null, null) : assignmentPresentationOf(view.assignment).status;
20099
+ return view.assignment === null ? resultHeadlineOf(resultStatusOf(null, null)) : assignmentPresentationOf(view.assignment).status;
20075
20100
  const receiptStatus = receiptStatusOf(view.detail.receipt);
20076
20101
  if (view.assignment !== null)
20077
20102
  return assignmentStatusOf(assignmentWithEvidence(view.assignment, receiptStatus, view.run.id));
20078
- return receiptStatus;
20103
+ return resultHeadlineOf(receiptStatus);
20079
20104
  }
20080
20105
  /** Turn verifier records into one sentence a project owner can act on.
20081
20106
  * A command-not-found result means the check could not start, not that the
@@ -20368,7 +20393,8 @@ function pullRequestCardHtml(pr, csrf) {
20368
20393
  const view = pr.view;
20369
20394
  const url = safePrUrl(view.prUrl);
20370
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>`;
20371
- 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";
20372
20398
  const revision = view.revisionTask === null || view.state !== "failing" ? "" : ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
20373
20399
  const merged = view.mergeCommit === null ? "" : `<p class="meta">Merge commit <span class="mono">${escape(view.mergeCommit.slice(0, 12))}</span></p>`;
20374
20400
  const target = pr.target === null ? "the base branch" : pr.target;
@@ -21144,7 +21170,8 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
21144
21170
  lead +
21145
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>` +
21146
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>` : "") +
21147
- (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>` : "") +
21148
21175
  caveats +
21149
21176
  coverage +
21150
21177
  `<details class="receipt-details"><summary>Result details</summary>` +
@@ -21195,16 +21222,18 @@ function resultPanelParts(detail, o) {
21195
21222
  const { run, receipt, proof, terminal, handoff } = detail;
21196
21223
  const facts = receipt.facts;
21197
21224
  const current = detail.assignment?.receipt?.runId === run.id ? detail.assignment : null;
21198
- const presentation = current == null ? null : assignmentPresentationOf(current, { additionalAttention: [...facts.evidenceProblems, ...receipt.caveats] });
21199
- 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));
21200
21231
  const stored = receiptStatusOf(receipt, null);
21201
21232
  const humanReview = manualReviewOnly(proof === null ? null : { ...proof, verdict: proof.verdict ?? "" });
21202
21233
  const directAssessment = proof?.matrix.some(row => row.assessment !== undefined) === true;
21203
21234
  const awaitingGoalReview = directAssessment && proof?.reasons.length === 1 && proof.reasons[0] === GOAL_ASSESSMENT_PENDING;
21204
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();
21205
21236
  const physicalReasons = receipt.reasons.filter(reason => !assessmentReasons.has(reason));
21206
- if (directAssessment && status.token === "verification-needed" && proof?.verdict === "short" && proof.matrix.some(row => row.review?.judgement === "cannot-tell") && physicalReasons.length === 0)
21207
- status = { ...status, label: "More evidence needed", tone: "attention" };
21208
21237
  const runId = run.id;
21209
21238
  const shots = proof?.screenshots ?? [];
21210
21239
  const shown = shots.filter(one => one.problem === null);
@@ -21238,10 +21267,11 @@ function resultPanelParts(detail, o) {
21238
21267
  attention.push(...(handoff?.followUps ?? []).map(one => `Follow-up: ${one}`));
21239
21268
  // Publication risks stay in the open (repair 2026-09-14); the routine
21240
21269
  // publication fact lives with the build details below.
21241
- 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) {
21242
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.`);
21243
21273
  }
21244
- if (detail.ciFailing && publication !== null)
21274
+ if (detail.ciFailing && publication !== null && taskStatus === null)
21245
21275
  attention.push(`CI is failing on PR #${publication.prNumber} at the last check.`);
21246
21276
  // Requirements only a person can confirm: plain words and one Accept,
21247
21277
  // in neutral ink โ€” nothing failed. Accepting records the person's
@@ -21517,11 +21547,13 @@ function resultPanelParts(detail, o) {
21517
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)}>` +
21518
21548
  (o.back === null ? "" : `<p class="result-back"><a href="${escape(o.back.href)}" data-result-back>โ† ${escape(o.back.label)}</a></p>`) +
21519
21549
  (detail.history ?? "") +
21520
- `<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>`) +
21521
21554
  `<p class="result-summary">${escape(outcome)}</p>` +
21522
21555
  (current == null || o.place === "review" && (current.state === "ready-to-check" || current.state === "complete") ? "" :
21523
- `<div class="verdict" data-current-outcome>${verdictChipsHtml(current, facts)}` +
21524
- (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>`) +
21525
21557
  attentionHtml +
21526
21558
  youCheckHtml +
21527
21559
  action +
@@ -21536,7 +21568,7 @@ function resultPanelParts(detail, o) {
21536
21568
  const panel = {
21537
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) },
21538
21570
  heading, outcome,
21539
- verdict: current == null ? null : verdictChipsOf(current, facts),
21571
+ status: taskStatus,
21540
21572
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
21541
21573
  attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
21542
21574
  youCheck,
@@ -21551,40 +21583,6 @@ function resultPanelParts(detail, o) {
21551
21583
  };
21552
21584
  return { html, panel };
21553
21585
  }
21554
- const CHIP_ICONS = { check: `<path d="M20 6 9 17l-5-5"/>`, x: `<path d="M18 6 6 18M6 6l12 12"/>` };
21555
- /** The chips as data: the server list below and the rebuilt result page. */
21556
- function verdictChipsOf(current, facts) {
21557
- const chips = [];
21558
- const checks = current.receipt?.checks.status ?? null;
21559
- if (checks === "passed")
21560
- chips.push({ tone: "success", label: "Checks passed", icon: "check", title: null });
21561
- else if (checks === "failed")
21562
- chips.push({ tone: "danger", label: "Checks failed", icon: "x", title: null });
21563
- else if (checks === "unavailable")
21564
- chips.push({ tone: "warning", label: "Check unavailable", icon: null, title: null });
21565
- const publication = { none: ["neutral", "Branch only"], intended: ["info", "Publishing"], pushed: ["info", "Pushed"], opened: ["info", "Pull request open"], failed: ["danger", "Publish failed"] };
21566
- const [tone, label] = publication[facts.publicationState] ?? ["neutral", facts.publicationState];
21567
- chips.push({ tone, label, icon: null, title: facts.publicationWords });
21568
- const health = facts.evidenceHealth;
21569
- if (health.damaged > 0)
21570
- chips.push({ tone: "danger", label: "Damaged evidence", icon: null, title: null });
21571
- else if (health.missing > 0)
21572
- chips.push({ tone: "warning", label: "Missing evidence", icon: null, title: null });
21573
- else if (health.shortened > 0)
21574
- chips.push({ tone: "warning", label: "Partial output", icon: null, title: "Some saved output was shortened when stored" });
21575
- return { chips, by: current.completion === null ? null : current.completion.actor.replace(/^(?:operator|coordinator|lead):/, "") };
21576
- }
21577
- /** The verdict at a glance: checks, where the work lives, and whether any
21578
- * saved output is partial, as chips instead of sentences. The same facts
21579
- * stay available in words on the Checks tab and in Build details. */
21580
- function verdictChipsHtml(current, facts) {
21581
- const { chips, by } = verdictChipsOf(current, facts);
21582
- const items = chips.map(one => {
21583
- const label = `${one.icon === null ? "" : strokeIcon(CHIP_ICONS[one.icon])}${one.label}`;
21584
- return `<li class="verdict-chip verdict-chip--${one.tone}" title="${escape(one.title ?? label)}">${label}</li>`;
21585
- });
21586
- 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>`);
21587
- }
21588
21586
  /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
21589
21587
  * first sentence, cut at a word when it runs past 180 characters. The
21590
21588
  * full conclusion stays available behind the Summary's disclosure. */