toolroll 0.9.12 → 0.9.13

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.
@@ -19,7 +19,7 @@ export function browserWorkActionHref(item) {
19
19
  if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(item.publicationUrl ?? ''))
20
20
  return item.publicationUrl;
21
21
  if (runId !== null && (action.code === 'open-pr' || action.code === 'retry-review')) {
22
- return `/review?result=${encodeURIComponent(taskId)}&run=${runId}${item.repo === null ? '' : '&project=' + encodeURIComponent(item.repo)}`;
22
+ return `/review?result=${encodeURIComponent(taskId)}&run=${runId}`;
23
23
  }
24
24
  if (runId !== null && (action.code === 'inspect-run' || action.code === 'reconcile-run'))
25
25
  return `/r/${runId}`;
@@ -11,6 +11,7 @@ import type { Ask } from './needs-you.js';
11
11
  import type { WorkIndexGroup } from './work-index.js';
12
12
  import type { AssignmentCard } from './assignment-ui.js';
13
13
  import type { TaskStatus } from './task-status.js';
14
+ import type { ResultActs } from './result-acts.js';
14
15
  import type { FirstRunStep, FirstTaskSuggestion, JourneyStep } from './first-run.js';
15
16
  export type BrowserProject = {
16
17
  name: string;
@@ -586,6 +587,23 @@ export type BrowserResultView = {
586
587
  problem: boolean;
587
588
  logHref: string | null;
588
589
  } | null;
590
+ /** The one ink act that resolves the result, the one outline act beside it, and why it can't be accepted yet (result-acts.ts). */
591
+ acts: ResultActs;
592
+ /** Run checks on this result's commit: the project's check, when it didn't run. */
593
+ runChecks: {
594
+ action: string;
595
+ level: "quick" | "full";
596
+ returnTo: string;
597
+ } | null;
598
+ /** The raw run record, under Details: its facts and the full record. */
599
+ record: {
600
+ build: number;
601
+ href: string;
602
+ facts: {
603
+ label: string;
604
+ value: string;
605
+ }[];
606
+ } | null;
589
607
  /** The signed scope; null when none was filed. */
590
608
  intent: {
591
609
  approval: string;
@@ -0,0 +1,44 @@
1
+ /** The result page's acts (2026-10-02), chosen in one place: exactly one ink
2
+ * act, the one that resolves the result's state, and never navigation; at
3
+ * most one outline act beside it. A result that can't be accepted says why
4
+ * in one line before them.
5
+ *
6
+ * everything met → Accept
7
+ * checks queued or running → Checks running (disabled), then Accept without checks
8
+ * checks didn't run, a check exists → Run checks, then Accept without checks
9
+ * report and changes disagree,
10
+ * or the proof is refuted → Request changes, then Accept (as allowed);
11
+ * never Accept as ink, even with no form here
12
+ * a Needs you form resolves it → that form (Build again, Confirm it stopped)
13
+ * saved notes or a failing PR → Revise / Draft a repair task
14
+ *
15
+ * Accepting means what it always meant: the same forms post to the same
16
+ * endpoints. This only decides which one is ink. */
17
+ export type ResultActKind = "accept" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "confirm-stopped" | "revise" | "draft-repair";
18
+ export type ResultActFacts = {
19
+ /** An Accept this page can post (Mark complete, or the person's acceptance); `ready`: every requirement met and checks passed. */
20
+ accept: {
21
+ ready: boolean;
22
+ } | null;
23
+ /** The checks didn't run, the project has one, and this person may run it. */
24
+ runChecks: boolean;
25
+ /** Checks on this result are queued or running. */
26
+ checksRunning: boolean;
27
+ /** Why it can't be accepted yet (cantAcceptYetOf), or null. */
28
+ blocked: string | null;
29
+ /** The feedback form is on this page. */
30
+ canRequest: boolean;
31
+ /** A Needs you form that resolves it here. */
32
+ need: "rebuild" | "confirm-stopped" | null;
33
+ /** Saved notes ready to become a revision, or a failing pull request's repair. */
34
+ next: "revise" | "draft-repair" | null;
35
+ };
36
+ export type ResultActs = {
37
+ /** The one ink act, or null when nothing waits on the person. */
38
+ primary: ResultActKind | null;
39
+ /** The one outline act beside it. */
40
+ secondary: ResultActKind | null;
41
+ /** The one line before the acts: why it can't be accepted yet. */
42
+ line: string | null;
43
+ };
44
+ export declare function resultActsOf(facts: ResultActFacts): ResultActs;
@@ -0,0 +1,36 @@
1
+ /** The result page's acts (2026-10-02), chosen in one place: exactly one ink
2
+ * act, the one that resolves the result's state, and never navigation; at
3
+ * most one outline act beside it. A result that can't be accepted says why
4
+ * in one line before them.
5
+ *
6
+ * everything met → Accept
7
+ * checks queued or running → Checks running (disabled), then Accept without checks
8
+ * checks didn't run, a check exists → Run checks, then Accept without checks
9
+ * report and changes disagree,
10
+ * or the proof is refuted → Request changes, then Accept (as allowed);
11
+ * never Accept as ink, even with no form here
12
+ * a Needs you form resolves it → that form (Build again, Confirm it stopped)
13
+ * saved notes or a failing PR → Revise / Draft a repair task
14
+ *
15
+ * Accepting means what it always meant: the same forms post to the same
16
+ * endpoints. This only decides which one is ink. */
17
+ export function resultActsOf(facts) {
18
+ const request = facts.canRequest ? "request-changes" : null;
19
+ const accept = facts.accept === null ? null : "accept";
20
+ if (facts.need !== null)
21
+ return { primary: facts.need, secondary: request, line: null };
22
+ if (facts.next !== null)
23
+ return { primary: facts.next, secondary: accept ?? request, line: null };
24
+ // Can't be accepted yet: Accept is never the ink act, even when the feedback form isn't here.
25
+ if (facts.blocked !== null)
26
+ return { primary: request, secondary: accept, line: facts.blocked };
27
+ if (facts.checksRunning)
28
+ return { primary: "checks-running", secondary: accept ?? request, line: null };
29
+ if (facts.accept === null)
30
+ return { primary: null, secondary: request, line: null };
31
+ if (facts.accept.ready)
32
+ return { primary: "accept", secondary: request, line: null };
33
+ if (facts.runChecks)
34
+ return { primary: "run-checks", secondary: "accept", line: null };
35
+ return { primary: "accept", secondary: request, line: null };
36
+ }
@@ -153,7 +153,7 @@ export function resultFactsFromHtml(html) {
153
153
  * lands on the run page. Matched exactly — never a bare open redirect. */
154
154
  export function resultReturnTarget(raw, runId) {
155
155
  if (raw !== null && raw !== undefined) {
156
- const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?$/.exec(raw);
156
+ const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?(?:&tab=(?:summary|changes|checks))?$/.exec(raw);
157
157
  if (review !== null && (review[1] === undefined || Number(review[1]) === runId))
158
158
  return raw;
159
159
  const chat = /^\/chat\?task=([A-Za-z0-9._~%-]{1,200})&result=([0-9]{1,15})(?:&conversation=[A-Za-z0-9-]{1,128})?$/.exec(raw);
package/dist/serve.d.ts CHANGED
@@ -558,6 +558,7 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
558
558
  run: number;
559
559
  action: string;
560
560
  rebuildable?: boolean;
561
+ refuted?: boolean;
561
562
  }): BrowserNeedAction | null;
562
563
  /** The facts region alone, for the open-run poll (A4). A finished run's
563
564
  * fragment says so instead of quietly growing forms (finding 5), and a run
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';
@@ -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, 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.
@@ -10975,7 +10989,7 @@ export function createDecisionServer(options) {
10975
10989
  const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
10976
10990
  if (!completed.ok)
10977
10991
  return taskScreen(response, who, taskId, completed.message, 409);
10978
- return redirect(response, reviewHref(taskId, Number(namedRun), taskRepoOf(ref.id)));
10992
+ return redirect(response, reviewHref(taskId, Number(namedRun)));
10979
10993
  }
10980
10994
  case "retry-review": {
10981
10995
  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 +12722,7 @@ ${THEME_MAPPING}
12708
12722
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
12709
12723
  /* Phone spacing steps (760px and narrower): side gutter, gap under the
12710
12724
  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;
12725
+ --so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
12712
12726
  }
12713
12727
  @media (prefers-color-scheme: dark) {
12714
12728
  :root:not([data-theme="light"]) {
@@ -18175,9 +18189,9 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
18175
18189
  return null;
18176
18190
  switch (status.action.kind) {
18177
18191
  case "open-result": return runId === null ? taskHref(taskId) : `/r/${runId}`;
18178
- case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId, repo);
18192
+ case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
18179
18193
  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));
18194
+ case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
18181
18195
  case "open-task":
18182
18196
  default: return taskHref(taskId);
18183
18197
  }
@@ -18211,7 +18225,7 @@ function workPage(chrome, data) {
18211
18225
  const rowHtml = (row) => {
18212
18226
  const target = row.primaryAction?.target;
18213
18227
  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)}`
18228
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
18215
18229
  : browserWorkActionHref(row);
18216
18230
  const action = actionHref === null || row.primaryAction === null ? '' : `<a class="work-action" data-primary-action href="${escape(actionHref)}">${escape(row.primaryAction.label)} →</a>`;
18217
18231
  const project = data.multiProject || row.repo === null ? `<span class="project-label">${row.repo === null ? 'Unplaced' : escape(projectName(row.repo))}</span>` : '';
@@ -18245,7 +18259,7 @@ function workPage(chrome, data) {
18245
18259
  rows: data.work.items.map(row => {
18246
18260
  const target = row.primaryAction?.target;
18247
18261
  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)}`
18262
+ ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
18249
18263
  : browserWorkActionHref(row);
18250
18264
  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
18265
  return {
@@ -21141,7 +21155,9 @@ export function orderChangedFiles(files, proofCitesPaths) {
21141
21155
  ? churn(b) - churn(a)
21142
21156
  : a.path.localeCompare(b.path));
21143
21157
  }
21144
- const reviewHref = (taskId, runId = null, repo = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}${repo === null ? "" : `&project=${encodeURIComponent(repo)}`}`;
21158
+ /** The one result page's address: the task and its run, never a project path
21159
+ * (the page finds the result's project itself and keeps the person's own). */
21160
+ const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
21145
21161
  /** The receipt's own proof-state word, so the cockpit and the task page
21146
21162
  * never disagree on the state's name or its precedence: the stored
21147
21163
  * verdict decides, whatever the run's outcome — a no-change run with a
@@ -21266,7 +21282,7 @@ function reviewCockpitPage(chrome, data) {
21266
21282
  const why = status === null
21267
21283
  ? reasons.length === 0 ? "" : `<span class="cockpit-why">${escape(reasons[0])}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
21268
21284
  : `<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"` : ""}>` +
21285
+ 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
21286
  `<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
21271
21287
  `<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
21288
  (row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
@@ -21295,7 +21311,7 @@ function reviewCockpitPage(chrome, data) {
21295
21311
  const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
21296
21312
  const reasons = row.priority.reasons;
21297
21313
  return {
21298
- title: row.title, href: reviewHref(row.taskId, row.runId, row.repo), at: row.completedAt,
21314
+ title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
21299
21315
  status: status === null ? null : { label: status.label, tone: status.tone },
21300
21316
  notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
21301
21317
  current: selected !== null && selected.taskId === row.taskId,
@@ -21377,6 +21393,15 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21377
21393
  problem: view.detail === null ? view.historyProblem : null,
21378
21394
  next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
21379
21395
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
21396
+ acts: { primary: null, secondary: null, line: null }, runChecks: null,
21397
+ // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
21398
+ record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
21399
+ { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
21400
+ { label: "Worker", value: run.runner },
21401
+ ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
21402
+ ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
21403
+ ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
21404
+ ] },
21380
21405
  };
21381
21406
  if (run === null || view.detail === null) {
21382
21407
  parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
@@ -21419,7 +21444,8 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21419
21444
  // The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
21420
21445
  const acceptsHere = complete !== null || panel.panel.need?.accept != null;
21421
21446
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
21422
- const decision = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21447
+ const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
21448
+ const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
21423
21449
  checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
21424
21450
  unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
21425
21451
  yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
@@ -21427,6 +21453,26 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21427
21453
  publishing: view.detail?.publishing ?? "other",
21428
21454
  proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
21429
21455
  }) };
21456
+ // Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
21457
+ const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
21458
+ // 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.
21459
+ const followUps = view.detail.followUps ?? null;
21460
+ const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
21461
+ const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
21462
+ (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
21463
+ // Back on this result's Checks tab, where #follow-ups shows the run it started.
21464
+ ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
21465
+ const need = panel.panel.need;
21466
+ const nextKind = selected.next?.kind;
21467
+ const acts = resultActsOf({
21468
+ accept: decision === null ? null : { ready: decision.ready },
21469
+ runChecks: runChecks !== null,
21470
+ checksRunning,
21471
+ blocked,
21472
+ canRequest: panel.panel.canRequest && panel.panel.request !== null,
21473
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
21474
+ next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
21475
+ });
21430
21476
  if (view.notes.length > 0) {
21431
21477
  parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
21432
21478
  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 +21481,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
21435
21481
  return {
21436
21482
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
21437
21483
  selected: {
21438
- ...selected, complete, decision, panel: panel.panel, contest,
21484
+ ...selected, complete, decision, panel: panel.panel, contest, acts, runChecks,
21439
21485
  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
21486
  },
21441
21487
  };
@@ -22344,7 +22390,7 @@ function resultPanelParts(detail, o) {
22344
22390
  // Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
22345
22391
  // On the result itself, "Review result" would link here: accepting it is what resolves it.
22346
22392
  const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
22347
- accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`,
22393
+ accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`, refuted: proof?.verdict === "refuted",
22348
22394
  // What Store.finalResultReason refuses: a verified, attested, accepted or published result.
22349
22395
  rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
22350
22396
  });
@@ -22762,7 +22808,7 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22762
22808
  if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
22763
22809
  return { label: status.need.action.label, ...rebuild };
22764
22810
  if (result !== undefined) {
22765
- if (status.need.key === "review-result" && !result.accepted && csrf !== "")
22811
+ if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
22766
22812
  return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
22767
22813
  accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
22768
22814
  // The task page sends the person here: never back to it. What this page can do leads — Accept
@@ -22772,8 +22818,9 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
22772
22818
  return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
22773
22819
  if (here && csrf !== "" && result.rebuildable === true)
22774
22820
  return { label: NEEDS.rebuild.action.label, ...rebuild };
22821
+ // Nothing here resolves it: no act at all, never a link back to the task (which sends the person here).
22775
22822
  if (status.need.key === "review-result" || here)
22776
- return { label: "Open the task", href: taskHref(assignment.rootId), confirm: null };
22823
+ return null;
22777
22824
  }
22778
22825
  const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
22779
22826
  ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
@@ -22802,7 +22849,7 @@ function resultNeedAction(need, o, canRequest) {
22802
22849
  function conciseOutcomeOf(conclusion) {
22803
22850
  const flat = oneLineOf(conclusion, 2_000);
22804
22851
  const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
22805
- const bounded = sentence.length <= 180 ? sentence : `${sentence.slice(0, 180).replace(/\s+\S*$/, "")}…`;
22852
+ const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
22806
22853
  return bounded.trim();
22807
22854
  }
22808
22855
  /** Exactly one primary act for the result (repair 2026-09-14), chosen from
package/dist/test-temp.js CHANGED
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
18
18
  "flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
19
19
  "proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
20
20
  "task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
21
- "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-",
21
+ "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-",
22
22
  // Playwright's browser profiles and artifacts.
23
23
  "playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
24
24
  ];
@@ -190,6 +190,10 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
190
190
  /** A finished result's display status in the shared headline's words: a
191
191
  * failed project check is Failed, a result missing its record needs a
192
192
  * person, every other saved result is Ready for review. Token and action stay. */
193
+ /** Why a result can't be accepted as it stands, in one plain line, or null
194
+ * when nothing refutes it (a person's acceptance settles it). The result page
195
+ * shows it before Request changes, its one ink act. */
196
+ export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
193
197
  /** The result page's own sentence when the decision is on it. */
194
198
  export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
195
199
  /** What the result page's Accept says, from facts it already shows: the
@@ -189,9 +189,9 @@ function storedResultStatusOf(result, publication) {
189
189
  label: "Result saved, but its record does not match",
190
190
  // A structural refutation is settled before the approved check is
191
191
  // weighed, so it says nothing about whether that check passed.
192
- detail: withPublication("What the agent reported doesn't match the changes it saved. This doesn't say whether the project check passed."),
192
+ detail: withPublication("What the agent reported doesn't match the changes it saved."),
193
193
  tone: "problem",
194
- action: { label: "Open the record", kind: "open-review" },
194
+ action: { label: "Open the result", kind: "open-review" },
195
195
  };
196
196
  }
197
197
  if (result.verdict === "short" && humanReview) {
@@ -312,6 +312,16 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
312
312
  /** A finished result's display status in the shared headline's words: a
313
313
  * failed project check is Failed, a result missing its record needs a
314
314
  * person, every other saved result is Ready for review. Token and action stay. */
315
+ /** Why a result can't be accepted as it stands, in one plain line, or null
316
+ * when nothing refutes it (a person's acceptance settles it). The result page
317
+ * shows it before Request changes, its one ink act. */
318
+ export function cantAcceptYetOf(verdict, reasons, accepted) {
319
+ if (accepted || verdict !== "refuted")
320
+ return null;
321
+ return evidenceProblemOf(verdict, reasons) === "checks-failed"
322
+ ? "Can't accept yet: the project's check failed on these changes."
323
+ : "Can't accept yet: what the agent reported doesn't match the changes it saved.";
324
+ }
315
325
  /** The result page's own sentence when the decision is on it. */
316
326
  export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
317
327
  export function acceptWordsOf(facts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"