toolroll 0.9.74 → 0.9.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assignment.d.ts +3 -0
  3. package/dist/assignment.js +27 -8
  4. package/dist/browser/workspace.js +27 -27
  5. package/dist/browser-workspace.d.ts +1 -1
  6. package/dist/chat-acceptance.d.ts +4 -1
  7. package/dist/chat-acceptance.js +13 -3
  8. package/dist/cli.js +1 -1
  9. package/dist/coding-handoff.js +4 -3
  10. package/dist/coding-ui.js +2 -2
  11. package/dist/coding-workspace.js +16 -4
  12. package/dist/contracts/console-api.d.ts +5 -1
  13. package/dist/contracts/console-api.js +10 -0
  14. package/dist/control-ui.d.ts +3 -2
  15. package/dist/control-ui.js +7 -2
  16. package/dist/demo.js +5 -3
  17. package/dist/desktop-update.d.ts +3 -0
  18. package/dist/desktop-update.js +43 -9
  19. package/dist/first-run.d.ts +8 -5
  20. package/dist/first-run.js +39 -6
  21. package/dist/flow-card-task.js +3 -2
  22. package/dist/flow-unattended.d.ts +2 -0
  23. package/dist/flow-unattended.js +12 -1
  24. package/dist/integrations.d.ts +4 -3
  25. package/dist/integrations.js +5 -6
  26. package/dist/lead-contract.d.ts +1 -1
  27. package/dist/lead-contract.js +2 -2
  28. package/dist/lead-follow.js +3 -1
  29. package/dist/openrouter-models.d.ts +5 -2
  30. package/dist/openrouter-models.js +7 -4
  31. package/dist/operate.js +2 -0
  32. package/dist/project-learning.js +44 -6
  33. package/dist/project-tools.js +19 -1
  34. package/dist/pull-request-ui.d.ts +1 -1
  35. package/dist/pull-request-ui.js +5 -1
  36. package/dist/push.d.ts +2 -0
  37. package/dist/push.js +24 -19
  38. package/dist/read-only-request.d.ts +4 -0
  39. package/dist/read-only-request.js +15 -0
  40. package/dist/repository-context.js +6 -1
  41. package/dist/result-follow-ups.js +20 -1
  42. package/dist/result-review.js +5 -1
  43. package/dist/serve.js +162 -6
  44. package/dist/server/agents-live-filed.d.ts +17 -0
  45. package/dist/server/agents-live-filed.js +97 -0
  46. package/dist/server/agents-live-room.js +4 -1
  47. package/dist/server/chat.js +31 -23
  48. package/dist/server/http.d.ts +5 -0
  49. package/dist/server/last-checks.d.ts +17 -0
  50. package/dist/server/last-checks.js +22 -0
  51. package/dist/server/pages.js +18 -4
  52. package/dist/server/people-tokens.js +7 -4
  53. package/dist/server/render-pages.d.ts +2 -1
  54. package/dist/server/render-pages.js +28 -24
  55. package/dist/server/render-results.d.ts +11 -2
  56. package/dist/server/render-results.js +54 -12
  57. package/dist/server/render-settings.js +3 -3
  58. package/dist/server/render-tasks.js +7 -4
  59. package/dist/server/route-table.js +14 -1
  60. package/dist/server/runtime.d.ts +16 -1
  61. package/dist/server/settings.js +82 -29
  62. package/dist/server/task-views.js +0 -1
  63. package/dist/server/tasks.js +11 -2
  64. package/dist/storage-ui.d.ts +2 -0
  65. package/dist/storage-ui.js +1 -1
  66. package/dist/storage.js +2 -1
  67. package/dist/store.d.ts +3 -0
  68. package/dist/store.js +33 -9
  69. package/dist/subagent-work.js +3 -0
  70. package/dist/task-status.d.ts +28 -1
  71. package/dist/task-status.js +93 -11
  72. package/dist/teardown-git.d.ts +27 -1
  73. package/dist/teardown-git.js +185 -39
  74. package/dist/telegram-progress.js +4 -4
  75. package/dist/telegram-status.js +8 -3
  76. package/dist/tool-secret-store.js +8 -0
  77. package/dist/toolroll-update-ui.d.ts +2 -0
  78. package/dist/toolroll-update-ui.js +4 -3
  79. package/dist/toolroll-update.js +10 -9
  80. package/dist/work-index.js +29 -12
  81. package/dist/workspace-ui.d.ts +5 -0
  82. package/dist/workspace-ui.js +38 -10
  83. package/package.json +1 -1
@@ -28,6 +28,7 @@ import { agentLanesOf, agentRowOf, agentsBoardOf, agentFactsFor, laneWindowOf, s
28
28
  import { markAgentSeen } from "../agent-board.js";
29
29
  import { agentsLiveOf } from "../agents-live.js";
30
30
  import { readChildWindow } from "../live-children-read.js";
31
+ import { leadFiledLinks } from "./agents-live-filed.js";
31
32
  import { readMainWindow } from "../live-main-read.js";
32
33
  import { runLiveCommands } from "../store.js";
33
34
  import { prometheusMetrics } from "../metrics.js";
@@ -53,7 +54,7 @@ import { decisionAnswerScript, taskBody } from "./render-tasks.js";
53
54
  import {} from "./session.js";
54
55
  import { rereadBrowser } from "./live.js";
55
56
  export function createPagesHandlers(runtime) {
56
- const { clock, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
57
+ const { clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
57
58
  // The filesystem browser (operator request): pick a project folder by
58
59
  // looking, not typing. CONFINED to exactly what opening allows —
59
60
  // explicit --repo mode has an enumerable list and gets no browser at
@@ -123,7 +124,8 @@ export function createPagesHandlers(runtime) {
123
124
  const { who, response } = ctx;
124
125
  if (store.isDemo())
125
126
  return refuse(response, who, 403, "the sandbox never talks to GitHub");
126
- const listed = await (options.ghList ?? listGithubRepos)();
127
+ // The last listing (docs/plans/get-side-effects.md): List repositories posts; opening the page asks GitHub nothing.
128
+ const listed = lastChecks.known("github-repos")?.value ?? null;
127
129
  // What this machine already has, matched by the clone's own recorded
128
130
  // origin — registered projects first, then the immediate children of
129
131
  // the configured roots. Identities are compared case-insensitively
@@ -142,7 +144,7 @@ export function createPagesHandlers(runtime) {
142
144
  // Roots are walked ONLY when there are listed repos to match against
143
145
  // (finding 1), through an iterator that stops at 400 entries — a
144
146
  // directory with a million children costs 400 stats, not a scan.
145
- if (listed.ok && listed.repos.length > 0) {
147
+ if (listed !== null && listed.ok && listed.repos.length > 0) {
146
148
  for (const root of ceiling.roots) {
147
149
  const children = [];
148
150
  try {
@@ -182,6 +184,14 @@ export function createPagesHandlers(runtime) {
182
184
  openProject: who.via === "cookie" ? who.session.project ?? null : null,
183
185
  }));
184
186
  }
187
+ // List repositories / Refresh list: ask the GitHub CLI signed in on this machine, then show the page.
188
+ async function projectsGithubList(ctx) {
189
+ const { who, response } = ctx;
190
+ if (store.isDemo())
191
+ return refuse(response, who, 403, "the sandbox never talks to GitHub");
192
+ await lastChecks.run("github-repos", () => (options.ghList ?? listGithubRepos)());
193
+ return redirect(response, "/projects/github");
194
+ }
185
195
  async function projectsIndex(ctx) {
186
196
  const { url, who, response } = ctx;
187
197
  return void projectsScreen(response, who, url.searchParams.get("said"), 200, safeReturn(url.searchParams.get("return")));
@@ -1027,7 +1037,10 @@ export function createPagesHandlers(runtime) {
1027
1037
  if (again === null)
1028
1038
  return respond(response, 401, "application/json", JSON.stringify({ error: "session" }));
1029
1039
  const shown = tiles.filter(tile => { const run = store.getRun(tile.runId); return run !== null && canSee(run); });
1030
- return respond(response, 200, "application/json", JSON.stringify({ tiles: shown, leads: leadsStillAdmitted(runtime, again, leads) }));
1040
+ // Work the viewer's own lead filed nests under it (docs/plans/lead-dispatched-tiles.md): proved from the store now,
1041
+ // for the tiles already shown, and only ever for their owner.
1042
+ const filed = again.role === "approver" ? leadFiledLinks(store, again.name, shown.map(tile => tile.runId)) : new Map();
1043
+ return respond(response, 200, "application/json", JSON.stringify({ tiles: shown.map(tile => ({ ...tile, filedBy: filed.get(tile.runId) ?? null })), leads: leadsStillAdmitted(runtime, again, leads) }));
1031
1044
  }
1032
1045
  /** This console's key for the Live grid's read cursors: random, in memory only (never where a build can reach), so a
1033
1046
  * restart makes old cursors start over visibly. A cursor is state, never permission. */
@@ -1070,6 +1083,7 @@ export function createPagesHandlers(runtime) {
1070
1083
  const registrations = handlersOf("pages", {
1071
1084
  "projects.browse": projectsBrowse,
1072
1085
  "projects.github": projectsGithub,
1086
+ "projects.github-list": projectsGithubList,
1073
1087
  "projects.page": projectsIndex,
1074
1088
  "home": homeRedirect,
1075
1089
  "morning": morningAlias,
@@ -39,7 +39,7 @@ export function sameVisitCheck(authTime, startedAt, now) {
39
39
  return Math.min(Math.max(at, startedAt), now);
40
40
  }
41
41
  export function createPeopleHandlers(runtime) {
42
- const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
42
+ const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, providerKnown, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
43
43
  // v101: Settings → Sessions & tokens: where you're signed in, and your API tokens. An instance operator can see everyone's.
44
44
  async function sessionsPage(ctx) {
45
45
  const { url, who, response, project } = ctx;
@@ -353,9 +353,12 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
353
353
  const origin = consoleOrigin(request.headers.host);
354
354
  if (origin === null)
355
355
  return page(response, 400, loginPage("Sign in from this computer (localhost) or from your https address.", returnTo, ssoOffer()));
356
- const found = await providerFor(settings.issuer);
357
- if (!found.ok)
358
- return page(response, 502, loginPage(`${settings.label} isn't answering: ${found.said}`, returnTo, ssoOffer()));
356
+ // Discovered as the console starts and in the background (docs/plans/get-side-effects.md): following this link
357
+ // fetches nothing.
358
+ const known = providerKnown(settings.issuer);
359
+ if (known === null)
360
+ return page(response, 503, loginPage(`${settings.label} isn't answering yet. Try again in a moment.`, returnTo, ssoOffer()));
361
+ const found = { provider: known };
359
362
  const visit = newOidcVisit(), redirectUri = `${origin}${SSO_CALLBACK}`;
360
363
  for (const [key, one] of ssoVisits)
361
364
  if (one.expires < Date.now())
@@ -442,7 +442,8 @@ export declare function originUrlOf(repoPath: string): string | null;
442
442
  * action each. Names, descriptions, and paths are gh/filesystem DATA —
443
443
  * escaped at the sink like everything else. */
444
444
  export declare function githubReposPage(chrome: Chrome, data: {
445
- listed: ListOutcome;
445
+ /** The last listing; null until someone lists (a POST: listing asks GitHub, so a page visit never does). */
446
+ listed: ListOutcome | null;
446
447
  local: Map<string, string>;
447
448
  registered: Set<string>;
448
449
  /** Unused: postForm adds the request's CSRF field. Kept for callers. */
@@ -925,34 +925,38 @@ export function originUrlOf(repoPath) {
925
925
  export function githubReposPage(chrome, data) {
926
926
  const openForm = (path, label) => postForm("/projects/open", html `<button type="submit">${label}</button>`, { attrs: { class: "inline" }, hidden: { path } });
927
927
  const cloneForm = (nameWithOwner) => postForm("/projects/onboard-preview", html `<button type="submit">Clone here →</button>`, { attrs: { class: "inline" }, hidden: { repo: nameWithOwner, root: "0" } });
928
- const rows = !data.listed.ok
929
- ? html `<p class="meta">${data.listed.message}</p>`
930
- : data.listed.repos.length === 0
931
- ? html `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
932
- : joinHtml(data.listed.repos.map(repo => {
933
- const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
934
- const action = localPath !== null && data.openProject === localPath
935
- ? html `<span class="badge badge-done">Open now</span>`
936
- : localPath !== null
937
- ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
938
- : data.cloneReady
939
- ? cloneForm(repo.nameWithOwner)
940
- : html `<span class="meta">choose a projects folder first</span>`;
941
- return joinHtml([
942
- html `<div class="card project-card">`,
943
- html `<div class="row"><strong>${repo.nameWithOwner}</strong>${repo.isPrivate ? html ` <span class="badge">Private</span>` : null}`,
944
- html `<span class="right">${action}</span></div>`,
945
- localPath === null
946
- ? html `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? html ` · pushed ${whenTime(repo.updatedAt)}` : null}</p>`
947
- : html `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${localPath}</p>`,
948
- repo.description === "" ? null : html `<p class="meta">${repo.description}</p>`,
949
- html `</div>`,
950
- ], "\n");
951
- }), "\n");
928
+ const list = (label) => postForm("/projects/github", html `<button type="submit"${label === "Refresh list" ? html ` class="secondary"` : ""}>${label}</button>`, { attrs: { class: "inline" } });
929
+ const rows = data.listed === null
930
+ ? list("List repositories")
931
+ : !data.listed.ok
932
+ ? html `<p class="meta">${data.listed.message}</p>`
933
+ : data.listed.repos.length === 0
934
+ ? html `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
935
+ : joinHtml(data.listed.repos.map(repo => {
936
+ const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
937
+ const action = localPath !== null && data.openProject === localPath
938
+ ? html `<span class="badge badge-done">Open now</span>`
939
+ : localPath !== null
940
+ ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
941
+ : data.cloneReady
942
+ ? cloneForm(repo.nameWithOwner)
943
+ : html `<span class="meta">choose a projects folder first</span>`;
944
+ return joinHtml([
945
+ html `<div class="card project-card">`,
946
+ html `<div class="row"><strong>${repo.nameWithOwner}</strong>${repo.isPrivate ? html ` <span class="badge">Private</span>` : null}`,
947
+ html `<span class="right">${action}</span></div>`,
948
+ localPath === null
949
+ ? html `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? html ` · pushed ${whenTime(repo.updatedAt)}` : null}</p>`
950
+ : html `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${localPath}</p>`,
951
+ repo.description === "" ? null : html `<p class="meta">${repo.description}</p>`,
952
+ html `</div>`,
953
+ ], "\n");
954
+ }), "\n");
952
955
  return screen("projects", joinHtml([
953
956
  html `<h1>Your GitHub repositories</h1>`,
954
957
  html `<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
955
958
  rows,
959
+ data.listed === null ? null : list("Refresh list"),
956
960
  html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
957
961
  ], "\n"), { chrome });
958
962
  }
@@ -16,7 +16,7 @@ import { type BuildReviewView } from "../review-switch.js";
16
16
  import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
17
17
  import { type ReportItem } from "../scout-report.js";
18
18
  import { type Artifact, type DiffComment, type Publication, type RepairChainRow, type ReviewRetryState, type Run, type Store, type TaskState } from "../store.js";
19
- import { type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
19
+ import { type ChecksFact, type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
20
20
  import { type DisplayStatus, type PublicationFacts, type ReviewFacts } from "../workspace-ui.js";
21
21
  import { type Chrome, type Screen } from "./chrome.js";
22
22
  import { type Html } from "../html.js";
@@ -423,6 +423,9 @@ export type ProofBundleView = {
423
423
  repairChain: RepairChainRow | null;
424
424
  /** v51: the policy semantic coverage is read under. */
425
425
  qualityMode: "default" | "strict";
426
+ /** The saved approved check read now (task-status.ts approvedCheckOf), for a finished build; absent otherwise, and
427
+ * then no words here claim the check passed. */
428
+ checks?: ChecksFact | null;
426
429
  };
427
430
  /** The smallest complete answer to "what did this task deliver?". It is a
428
431
  * projection of the same sealed handoff, proof, screenshot, and diff records
@@ -471,6 +474,9 @@ export type CompletionReceiptView = {
471
474
  report: RunReportView | null;
472
475
  /** The facts every result surface prints identically (package 3). */
473
476
  facts: SharedResultFacts;
477
+ /** The saved approved check as read now (the receipt and any follow-up on the same commit), so the status words
478
+ * read the same answer as the Project checks row (task-status.ts approvedCheckOf). Absent on a scout's report. */
479
+ checks?: ChecksFact | null;
474
480
  };
475
481
  /** A scout run's report as this run stored it: verified and parsed, or
476
482
  * the reason it cannot be shown. The download link serves the exact
@@ -501,7 +507,10 @@ export declare function runReportView(artifacts: Artifact[], root: string): RunR
501
507
  * evidence problem in words, and the observed publication state. */
502
508
  export declare function sharedResultFactsOf(run: Run, proof: ProofBundleView | null, terminal: TerminalDiffView | null, handoff: StructuredHandoffView | null, report: RunReportView | null, publication: PublicationFacts): SharedResultFacts;
503
509
  export declare function proofBundleView(store: Store, run: Run, artifacts: Artifact[], root: string): ProofBundleView | null;
504
- export declare function completionReceiptView(store: Store, run: Run, artifacts: Artifact[], root: string, review?: ReviewFacts | null): CompletionReceiptView;
510
+ /** What the verdict recorded when the build finished, said beside what holds now: a recorded pass is restated only
511
+ * when the check read now passed; unread, it points to the result; unreadable or failed since, it says so. */
512
+ export declare function completionWords(verdict: ProofVerdict | null, reasons: readonly string[], checks: ChecksFact | null | undefined): string;
513
+ export declare function completionReceiptView(store: Store, run: Run, artifacts: Artifact[], root: string, review?: ReviewFacts | null, now?: Date): CompletionReceiptView;
505
514
  export type ReviewDiffLine = {
506
515
  kind: "context" | "addition" | "deletion" | "meta";
507
516
  text: string;
@@ -1,6 +1,7 @@
1
1
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from "../assignment-presentation.js";
2
2
  import { assignmentActionHref, assignmentStatusOf, assignmentWithEvidence } from "../assignment-ui.js";
3
- import { owedAcceptance } from "../assignment.js";
3
+ import { assignmentChecksForRun, owedAcceptance } from "../assignment.js";
4
+ import { releaseCoverageOf } from "../release-coverage.js";
4
5
  import {} from "../browser-workspace.js";
5
6
  import { CHECK_LEVEL_WORDS, liveQuickCommand } from "../check-levels.js";
6
7
  import { LIMITS } from "../decision.js";
@@ -20,7 +21,7 @@ import {} from "../scope.js";
20
21
  import { parseReport } from "../scout-report.js";
21
22
  import {} from "../store.js";
22
23
  import { runCostWords } from "../summary.js";
23
- import { checkBackingOf, CHECKED_AT_RELEASE, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
24
+ import { approvedCheckOf, approvedCheckStateOf, CHECK_COMMAND_CHANGED, CHECK_UNREADABLE, checkBackingOf, CHECKED_AT_RELEASE, checksFactOf, receiptChecksOf, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
24
25
  import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
25
26
  import { createHash, randomBytes } from "node:crypto";
26
27
  import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
@@ -650,9 +651,12 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
650
651
  ? `${host.base}&run=${run.id}${section === "summary" ? "" : `&section=${section}`}`
651
652
  : `${here}&run=${run.id}${section === "summary" ? "" : `&tab=${section}`}`;
652
653
  const assignment = view.assignment === null ? null : assignmentWithEvidence(view.assignment, receiptStatusOf(view.detail.receipt), run.id);
653
- const checks = assignment?.receipt?.checks;
654
+ const saved = assignment?.receipt?.checks;
655
+ // The approved check as the task page reads it (task-status.ts approvedCheckOf): one answer for this whole page.
656
+ const checks = saved === undefined || assignment?.receipt == null ? undefined
657
+ : { ...saved, ...(receiptChecksOf(assignment.receipt) ?? {}), detail: saved.detail };
654
658
  if (checks !== undefined)
655
- parts.push(html `<p class="${checks.status === "failed" || checks.status === "unavailable" ? "problem" : "meta"}" data-actual-checks="${checks.status}">${checks.detail}${checks.logArtifactId === null ? "" : html ` <a href="/r/${run.id}/evidence/${checks.logArtifactId}">Open check output</a>`}</p>`);
659
+ parts.push(html `<p class="${checks.status === "failed" || checks.status === "unavailable" ? "problem" : "meta"}" data-actual-checks="${checks.status}">${checks.status === "unavailable" && saved.cause === "command-changed" ? CHECK_COMMAND_CHANGED : html `${checks.status === "unavailable" && saved.status !== "unavailable" ? `${CHECK_UNREADABLE} ` : ""}${checks.detail}`}${checks.logArtifactId === null ? "" : html ` <a href="/r/${run.id}/evidence/${checks.logArtifactId}">Open check output</a>`}</p>`);
656
660
  if (assignment?.completion != null)
657
661
  parts.push(html `<p class="meta" data-result-completed>Marked complete by ${assignment.completion.actor.replace(/^operator:/, "")}.</p>`);
658
662
  const panel = resultPanelParts(view.detail, {
@@ -692,7 +696,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
692
696
  const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
693
697
  const base = !acceptsHere ? null : acceptWordsOf({
694
698
  // Checks Off that a release check already covers read as passed (release-coverage.ts).
695
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? checks.release != null ? "passed" : "off" : checks.status,
699
+ checks: checks === undefined ? null : { passed: "passed", failed: "failed", running: "running", "not-run": "not-run", off: "off", unreadable: "unavailable" }[approvedCheckStateOf(checks)],
696
700
  // Release-aware words (task-status.ts): a requirement waiting only on a release check, or covered by one, isn't unmet.
697
701
  unmet: matrix.filter(row => !["Met", "You check", CHECKED_AT_RELEASE].includes(requirementWordOf(row, null, checks === undefined ? undefined : checkBackingOf(checks)))).length,
698
702
  action: complete !== null ? "complete" : "accept",
@@ -1084,6 +1088,7 @@ export function proofBundleView(store, run, artifacts, root) {
1084
1088
  : { text: "", artifactId: checkLogArtifact.id, truncated: checkLogArtifact.truncated, problem: read.problem, ...sizes };
1085
1089
  }
1086
1090
  return {
1091
+ ...(run.finishedAt !== null && run.role !== "scout" && run.role !== "reviewer" && run.role !== "planner" ? { checks: savedChecksOf(store, root, run, new Date()) } : {}),
1087
1092
  verdict: verdictRow?.verdict ?? null,
1088
1093
  reasons: verdictRow?.reasons ?? [],
1089
1094
  accepted: acceptanceRow === null ? null : { by: acceptanceRow.approver, note: acceptanceRow.note, at: acceptanceRow.acceptedAt },
@@ -1114,7 +1119,32 @@ export function proofBundleView(store, run, artifacts, root) {
1114
1119
  })(),
1115
1120
  };
1116
1121
  }
1117
- export function completionReceiptView(store, run, artifacts, root, review = null) {
1122
+ /** What the verdict recorded when the build finished, said beside what holds now: a recorded pass is restated only
1123
+ * when the check read now passed; unread, it points to the result; unreadable or failed since, it says so. */
1124
+ export function completionWords(verdict, reasons, checks) {
1125
+ const recorded = verificationExplanation(verdict, reasons);
1126
+ if (verdict !== "verified")
1127
+ return recorded;
1128
+ const now = approvedCheckStateOf(approvedCheckOf(verdict, reasons, checks));
1129
+ if (checks !== undefined && (now === "passed" || (now === "running" && checks?.status === "passed")))
1130
+ return recorded;
1131
+ if (checks === undefined)
1132
+ return "Open the result to see its checks.";
1133
+ const said = now === "failed" ? `the approved check failed against it${checks?.exitCode == null ? "" : ` (exit ${checks.exitCode})`}.`
1134
+ : checks?.cause === "command-changed" ? "the check command changed since this check." : `${CHECK_UNREADABLE.charAt(0).toLowerCase()}${CHECK_UNREADABLE.slice(1)}`;
1135
+ return `At completion: ${recorded} Now: ${said} It doesn't count as passed.`;
1136
+ }
1137
+ /** A result's saved approved check as the assignment reads it (assignment.ts): the receipt, follow-ups on its commit,
1138
+ * and with checks Off the passing release check that contains it. */
1139
+ function savedChecksOf(store, root, run, now) {
1140
+ const own = checksFactOf(assignmentChecksForRun(store, root, run.id, now), run.headRevision);
1141
+ if (own.level !== "off" || own.status === "passed" || own.status === "failed")
1142
+ return own;
1143
+ const repo = store.refById(run.taskRef)?.repo ?? null;
1144
+ const release = releaseCoverageOf(store, root, repo, run.headRevision);
1145
+ return release == null ? own : { ...own, release };
1146
+ }
1147
+ export function completionReceiptView(store, run, artifacts, root, review = null, now = new Date()) {
1118
1148
  const handoff = structuredHandoffView(artifacts, root);
1119
1149
  const proof = proofBundleView(store, run, artifacts, root);
1120
1150
  const terminal = terminalDiffView(artifacts, root);
@@ -1149,6 +1179,8 @@ export function completionReceiptView(store, run, artifacts, root, review = null
1149
1179
  coverageSecondary: coverage.contextGaps.length === 0 && (coverage.satisfied === true || (coverage.satisfied === null && !coverage.required)),
1150
1180
  report,
1151
1181
  facts: sharedResultFactsOf(run, proof, terminal, handoff, report, publication),
1182
+ // The same reading the task page's Project checks row shows (assignment.ts), never the verdict alone.
1183
+ ...(run.role === "scout" ? {} : { checks: savedChecksOf(store, root, run, now) }),
1152
1184
  };
1153
1185
  }
1154
1186
  export const REVIEW_DIFF_LINE_CAP = 4_000;
@@ -1357,8 +1389,9 @@ export function evidenceBundleCard(view, run, publication = null, review = null)
1357
1389
  if (view.verdict !== null) {
1358
1390
  // The same status line every other surface renders for this run
1359
1391
  // (workspace package 1), beside the verdict's own explanation.
1360
- const status = resultStatusOf({ runId, role: run.role, outcome: run.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted !== null, review }, publication);
1361
- parts.push(html `<p class="row" data-proof-verdict="${dispatchStatusToken(view.verdict)}">${statusLineHtml(status)} <span class="meta">${verificationExplanation(view.verdict, view.reasons)}</span></p>`);
1392
+ const status = resultStatusOf({ runId, role: run.role, outcome: run.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted !== null, review,
1393
+ ...(view.checks === undefined ? {} : { checks: view.checks }) }, publication);
1394
+ parts.push(html `<p class="row" data-proof-verdict="${dispatchStatusToken(view.verdict)}">${statusLineHtml(status)} <span class="meta">${completionWords(view.verdict, view.reasons, view.checks)}</span></p>`);
1362
1395
  }
1363
1396
  if (view.accepted !== null) {
1364
1397
  parts.push(html `<p class="meta">Accepted with an exception by <span class="mono">${view.accepted.by}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${view.accepted.note}`}</p>`);
@@ -1429,7 +1462,8 @@ export function receiptStatusOf(view, review = view.review) {
1429
1462
  // The evidence health rides the status (repair 2026-09-14): a result
1430
1463
  // whose stored records are damaged is never called ready on the strength
1431
1464
  // of a verdict recorded before the damage.
1432
- return evidenceResultStatusOf({ runId: view.runId, role: view.role, outcome: view.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted, recordComplete: view.recordComplete, review }, view.publication, view.facts.evidenceHealth);
1465
+ return evidenceResultStatusOf({ runId: view.runId, role: view.role, outcome: view.outcome, verdict: view.verdict, reasons: view.reasons, accepted: view.accepted, recordComplete: view.recordComplete, review,
1466
+ ...(view.checks === undefined ? {} : { checks: view.checks }) }, view.publication, view.facts.evidenceHealth);
1433
1467
  }
1434
1468
  export function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(view), assignment = null, headline = true) {
1435
1469
  const current = assignment?.receipt?.runId === view.runId ? assignment : null;
@@ -1577,8 +1611,11 @@ export function resultPanelParts(detail, o) {
1577
1611
  // The visible status already says verification is needed. Only omit the
1578
1612
  // generic repeat; failed checks, specific reasons and damaged evidence stay.
1579
1613
  const missingVerdictNamed = o.headStatus !== false && status.token === "verification-needed" && receipt.verdict === null && receipt.reasons.length === 0;
1580
- if (current == null && !humanReview && !awaitingGoalReview && (!directAssessment || physicalReasons.length > 0) && !missingVerdictNamed && stored.token !== "evidence-damaged" && (stored.tone === "problem" || stored.tone === "attention"))
1581
- attention.push(verificationExplanation(receipt.verdict, physicalReasons));
1614
+ if (current == null && !humanReview && !awaitingGoalReview && (!directAssessment || physicalReasons.length > 0) && !missingVerdictNamed && stored.token !== "evidence-damaged" && (stored.tone === "problem" || stored.tone === "attention")) {
1615
+ // A recorded pass whose check can't be read now is said as what holds now, never as the verdict's pass.
1616
+ // A recorded pass the check read now can't stand behind (unreadable, or failed since) is said as what holds now.
1617
+ attention.push(stored.token === "check-unreadable" || (receipt.verdict === "verified" && stored.token === "checks-failed") ? stored.detail : verificationExplanation(receipt.verdict, physicalReasons));
1618
+ }
1582
1619
  // Exact criterion assessment notes are already shown in Requirements;
1583
1620
  // do not repeat them as a second current attention message.
1584
1621
  if (presentation !== null)
@@ -1773,7 +1810,12 @@ export function resultPanelParts(detail, o) {
1773
1810
  if (proof.verdict !== null && !directAssessment) {
1774
1811
  // The same status line the panel leads with (one status per surface,
1775
1812
  // workspace package 1), beside the machine verdict's own explanation.
1776
- checkParts.push(html `<p class="row result-verdict" data-proof-verdict="${dispatchStatusToken(proof.verdict)}"><span class="meta">At completion: ${humanReview ? "Human review required for the requirements below." : verificationExplanation(proof.verdict, proof.reasons)}</span></p>`);
1813
+ // A recorded pass the saved check can't show now is said so here too (task-status.ts approvedCheckOf).
1814
+ const shown = current?.receipt != null ? receiptChecksOf(current.receipt) : receipt.checks === undefined ? undefined : approvedCheckOf(proof.verdict, proof.reasons, receipt.checks);
1815
+ const unreadNow = proof.verdict === "verified" && shown !== undefined && shown?.status !== "passed" && !humanReview;
1816
+ // completionWords: the verdict at completion, and what holds now when they differ.
1817
+ const words = humanReview ? "At completion: Human review required for the requirements below." : unreadNow ? completionWords(proof.verdict, proof.reasons, shown) : `At completion: ${verificationExplanation(proof.verdict, proof.reasons)}`;
1818
+ checkParts.push(html `<p class="row result-verdict" data-proof-verdict="${dispatchStatusToken(proof.verdict)}"${unreadNow ? html ` data-check-now="${approvedCheckStateOf(shown) ?? "unreadable"}"` : ""}><span class="meta">${words}</span></p>`);
1777
1819
  }
1778
1820
  else if (proof.verdict === null && run.outcome !== "no-change") {
1779
1821
  checkParts.push(html `<p class="meta" data-proof-verdict="none">No verification result is available for this build.</p>`);
@@ -32,7 +32,7 @@ export function leadSettingsHtml(data) {
32
32
  const summary = config === null
33
33
  ? data.signedIn !== null
34
34
  ? html `<p>The lead is off.</p>${postForm("/settings/lead/on", html `<button type="submit">Use your ${data.signedIn} sign-in</button>`, { returnTo })}`
35
- : html `<p>The lead is off. Sign in an agent on this computer to turn it on:</p><p><code>${data.command}</code></p>`
35
+ : html `<p>The lead is off. Sign in an agent on this computer to turn it on:</p><p><code>${data.command}</code></p>${postForm("/lead/check", html `<button type="submit" class="secondary">Check again</button>`, { returnTo })}`
36
36
  : data.words !== null
37
37
  ? html `<p>The lead uses ${data.words}.</p><p class="meta">${config.model === "default" ? "Its default model" : `Model ${config.model}`} · up to ${config.dailyTurns} turns a day · no dollar spend. Every action it proposes waits for you to confirm it.</p>`
38
38
  : html `<p>The lead uses the ${config.provider === "anthropic-api" ? "Anthropic" : "OpenRouter"} API with ${config.model}.</p><p class="meta">Up to ${chatMoney(config.weeklyCeilingMicrousd)} a week · up to ${config.dailyTurns} turns a day.</p>`;
@@ -261,7 +261,7 @@ ${[
261
261
  html `<details class="provider-manage"><summary>Manage</summary>`,
262
262
  postForm("/settings/provider-key", joinHtml([
263
263
  "",
264
- one.connection === undefined ? "" : html `<p class="provider-connection"><strong>${connectionWords(one.connection)}</strong> ${[one.connection.email, one.connection.plan, one.connection.method].filter(Boolean).join(" · ")} · <a href="/settings?check-connection=${encodeURIComponent(one.provider)}#providers">Check again</a></p>`,
264
+ one.connection === undefined ? "" : html `<p class="provider-connection"><strong>${connectionWords(one.connection)}</strong> ${[one.connection.email, one.connection.plan, one.connection.method].filter(Boolean).join(" · ")} · <button type="submit" class="secondary" formaction="/settings/connection/check" formnovalidate>Check again</button></p>`,
265
265
  html `<p class="meta">${one.mode === "subscription" ? "Uses its own sign-in, so no API-key spend" : "Uses the API key"} · <span class="mono">${one.envName}</span> · ${one.set ? `key stored${one.updatedAt === null ? "" : ` ${one.updatedAt.slice(0, 10)}`}` : one.ambient ? "key in this server's environment" : "no key stored"}</p>`,
266
266
  one.subscriptionCapable
267
267
  ? html `<label>Sign-in<select name="auth-mode">\
@@ -372,7 +372,7 @@ ${one.retiredAt === null
372
372
  : one.set ? { tone: "ok", words: "API key saved" } : one.ambient ? { tone: "ok", words: "Key from this computer’s environment" } : { tone: "off", words: "Not set up" };
373
373
  return {
374
374
  provider: one.provider, name, tone: status.tone, words: status.words,
375
- connection: one.connection === undefined ? null : { words: connectionWords(one.connection), facts: [one.connection.email, one.connection.plan, one.connection.method].filter(Boolean).join(" · "), checkHref: `/settings?check-connection=${encodeURIComponent(one.provider)}#providers` },
375
+ connection: one.connection === undefined ? null : { words: connectionWords(one.connection), facts: [one.connection.email, one.connection.plan, one.connection.method].filter(Boolean).join(" · "), checkAction: "/settings/connection/check" },
376
376
  usage: `${one.mode === "subscription" ? "Uses its own sign-in, so no API-key spend" : "Uses the API key"} · ${one.set ? `key stored${one.updatedAt === null ? "" : ` ${one.updatedAt.slice(0, 10)}`}` : one.ambient ? "key in this server's environment" : "no key stored"}`,
377
377
  envName: one.envName, subscriptionCapable: one.subscriptionCapable, mode: one.mode, set: one.set,
378
378
  };
@@ -1437,6 +1437,11 @@ export function taskBodyParts(data) {
1437
1437
  if (resultStatus.token === "evidence-damaged") {
1438
1438
  return withReview(box("problem", resultStatus.label, html `<a href="/r/${proof.runId}">Build #${proof.runId}</a> finished. ${resultStatus.detail}${publicationWords}`, "evidence-damaged"), html `<div class="proof-review-actions"><a class="button-link" href="/r/${proof.runId}">Review the evidence problems</a></div>`, proofDetails);
1439
1439
  }
1440
+ // A recorded pass the check read now doesn't stand behind (unreadable, failed since, or not read here) never
1441
+ // reads "passed": the box says what holds now (task-status.ts approvedCheckOf).
1442
+ if (proof.proofVerdict === "verified" && !accepted && !(proof.receipt?.checks !== undefined && ["ready-to-review", "pr-opened", "merge-observed", "pr-closed"].includes(resultStatus.token))) {
1443
+ return withReview(box(resultStatus.tone === "problem" ? "problem" : "ok", resultStatus.label, html `<a href="/r/${proof.runId}">Build #${proof.runId}</a> finished. ${resultStatus.detail}${publicationWords}`, resultStatus.token), proofDetails);
1444
+ }
1440
1445
  if (proof.proofVerdict === "verified" && !accepted) {
1441
1446
  const recovered = verificationRecovered(proof.proofReasons);
1442
1447
  return withReview(box("ok", resultStatus.label, recovered
@@ -2364,10 +2369,8 @@ export function taskBodyParts(data) {
2364
2369
  journey: task.state === "cancelled" || data.status === undefined ? null : (() => {
2365
2370
  const planning = data.plan === "requested" && approveForm === "";
2366
2371
  const read = data.assignment != null ? assignmentStageOf(data.assignment, { token: data.status.token }, planning) : stageOfCode(data.status.token, { planning });
2367
- // The checks fact the status card shows, read as what happened: a "note" is a failure when its words say so.
2368
- const shown = statusCard?.status.details.find(one => one.key === "checks") ?? null;
2369
- const checks = shown === null ? null : shown.mark === "ok" ? "passed" : shown.mark === "running" ? "running"
2370
- : shown.mark === "failed" || /\bfailed\b/i.test(shown.text) ? "failed" : shown.mark === "none" ? "not-run" : "unread";
2372
+ // The one answer the status card's headline and Project checks row read (task-status.ts approvedCheckOf).
2373
+ const checks = statusCard?.status.check ?? null;
2371
2374
  const report = data.deliverable === "report" || data.report != null;
2372
2375
  return taskRoad(read, scope !== null && approvalOf(scope).approved, task.state === "done", { checks, report });
2373
2376
  })(),
@@ -237,6 +237,10 @@ for (const one of ["close", "remove"])
237
237
  post("tasks", `term.${one}`, `^\\/term\\/${TERMINAL}\\/${one}$`, { ...TERM, sample: `/term/${"a".repeat(32)}/${one}` });
238
238
  for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
239
239
  post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
240
+ // Reading a coding checkout runs git in a folder an agent writes, so it is a POST with the form token, never a page
241
+ // visit (docs/plans/get-side-effects.md): the session's changes, and the review-for-shipping preview.
242
+ post("tasks", "code.changes-read", `^\\/code\\/${SESSION}\\/changes$`, { project: "coding", limited: "self", callers: ["cookie"], sample: `/code/${"a".repeat(32)}/changes` });
243
+ post("tasks", "code.ship-preview", `^\\/code\\/${SESSION}\\/ship\\/preview$`, { project: "coding", limited: "self", callers: ["cookie"], sample: `/code/${"a".repeat(32)}/ship/preview` });
240
244
  // Any other coding action answers "not found" after the same access check.
241
245
  post("tasks", "code.act-other", "^\\/code(\\/.*)?$", { ...CODING_ACT, sample: "/code/unknown" });
242
246
  post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped", ...approver("Sign in to revise a test.", "/settings/skills") });
@@ -308,10 +312,12 @@ const SETTINGS_ACT_ROLES = {
308
312
  updates: operator("An instance operator updates Toolroll.", "/settings"),
309
313
  "updates/seen": operator("An instance operator updates Toolroll.", "/settings"),
310
314
  "updates/cancel": operator("An instance operator updates Toolroll.", "/settings"),
315
+ "updates/check": operator("An instance operator updates Toolroll.", "/settings"),
311
316
  retention: operator("An instance operator sets retention.", "/settings"),
312
317
  storage: operator("An instance operator looks after storage.", "/settings"),
313
318
  "storage/clean": operator("An instance operator looks after storage.", "/settings"),
314
319
  "storage/discard": operator("An instance operator looks after storage.", "/settings"),
320
+ "storage/check": operator("An instance operator looks after storage.", "/settings"),
315
321
  "backups/now": operator("An instance operator looks after backups.", "/settings"),
316
322
  backups: operator("An instance operator looks after backups.", "/settings"),
317
323
  data: operator("An instance operator exports data.", "/settings"),
@@ -320,7 +326,7 @@ const SETTINGS_ACT_ROLES = {
320
326
  "tools/connect": approver("Sign in as an approver to connect tools.", "/settings/tools"),
321
327
  "tools/change": approver("Sign in as an approver to manage tools.", "/settings/tools"),
322
328
  };
323
- for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
329
+ for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "updates/check", "retention", "storage", "storage/clean", "storage/discard", "storage/check", "pull-requests", "pull-requests/check", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
324
330
  post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}), ...SETTINGS_ACT_ROLES[one] });
325
331
  }
326
332
  post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
@@ -334,6 +340,9 @@ post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal",
334
340
  post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form", ...approver("Sign in as an approver to change project decisions.", "/settings/knowledge") });
335
341
  post("settings", "settings.knowledge-change", "/settings/knowledge/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to change project knowledge.", "/settings/knowledge") });
336
342
  post("settings", "settings.learning-change", "/settings/learning/change", { project: "form", limited: "unscoped" });
343
+ // A page shows the last check; these ask again (docs/plans/get-side-effects.md).
344
+ post("settings", "control.models-refresh", "/control/models-refresh", anyApprover("Project setup requires an approver."));
345
+ post("settings", "settings.connection-check", "/settings/connection/check", anyApprover("An approver checks a sign-in.", "/settings"));
337
346
  for (const one of ["setup-preview", "setup-approve", "instructions-preview", "instructions-approve"])
338
347
  post("settings", `control.${one}`, `/control/${one}`, { ...(one.endsWith("approve") ? { scope: "step-up" } : {}) });
339
348
  for (const [service, name] of [["slack", "Slack"], ["teams", "Teams"], ["discord", "Discord"]]) {
@@ -348,6 +357,7 @@ post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limi
348
357
  for (const one of ["messaging", "permission-default", "quality-default", "notifications", "notifications/mute", "telegram-digest", "provider-key", "provider-key-clear", "telegram-token", "email", "email-test", "email-read-test", "google", "google/disconnect"]) {
349
358
  post("settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`);
350
359
  }
360
+ post("pages", "projects.github-list", "/projects/github", { callers: ["cookie"], ...anyApprover("your login can watch — adding projects is an approver's act") });
351
361
  post("pages", "projects.select", "/projects/select", { project: "form-path", limited: "unscoped", viewer: true });
352
362
  post("pages", "projects.remove", "/projects/remove", { project: "form", ...operator("An instance operator removes projects.", "/settings/project") });
353
363
  post("pages", "projects.open", "/projects/open", { project: "form-path" });
@@ -397,6 +407,9 @@ post("pages", "projects.onboard-preview", "/projects/onboard-preview", { project
397
407
  post("pages", "projects.onboard-confirm", "/projects/onboard-confirm", { scope: "step-up", project: "none" });
398
408
  post("chat", "push.subscribe", "/push/subscribe", { scope: "step-up" });
399
409
  post("chat", "push.remove", "/push/remove");
410
+ // Ask this computer's agent CLIs again (docs/plans/get-side-effects.md): Chat's first run and Settings → Lead's Check
411
+ // now. GET /lead/status only reads the last answer.
412
+ post("chat", "lead.check", "/lead/check", { callers: ["cookie"] });
400
413
  post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("An approver puts this away.", "/chat"));
401
414
  const LEAD_WORDS = { identity: "An approver names their lead.", about: "An approver edits what their lead knows about them.", "promise/cancel": "An approver manages the lead's promises.", on: "An approver turns the lead on." };
402
415
  for (const one of ["identity", "about", "promise/cancel", "on"])
@@ -158,6 +158,8 @@ export interface ServerRuntime {
158
158
  source: "environment" | "stored";
159
159
  } | null;
160
160
  chatCatalog: () => Promise<import("../converse.js").CatalogModel[] | null>;
161
+ /** The catalog as last fetched: what a page shows. A page never fetches. */
162
+ chatCatalogKnown: () => import("../converse.js").CatalogModel[] | null;
161
163
  leadRecheckedAt: number;
162
164
  checkLocalAgents: (fresh?: boolean) => Promise<void>;
163
165
  localSignIn: {
@@ -167,7 +169,18 @@ export interface ServerRuntime {
167
169
  } | null;
168
170
  agentSignInCommand: () => string;
169
171
  connectionCheck: (provider: ProviderId, fresh?: boolean) => Promise<ProviderConnection>;
170
- modelCatalog: (key: string | null, refresh?: boolean) => Promise<OpenRouterModels>;
172
+ /** What the last sign-in check said: what a page shows. A page never checks (docs/plans/get-side-effects.md). */
173
+ connectionKnown: (provider: ProviderId) => ProviderConnection | null;
174
+ /** The last answer of each other check a page shows; a page only reads it. */
175
+ lastChecks: import("./last-checks.js").LastChecks;
176
+ checkRelease: () => Promise<{
177
+ version: string;
178
+ } | {
179
+ problem: string;
180
+ }>;
181
+ modelCatalog: ((key: string | null, refresh?: boolean) => Promise<OpenRouterModels>) & {
182
+ known: (key: string | null) => OpenRouterModels | null;
183
+ };
171
184
  modelSeams: CatalogSeams;
172
185
  toolsViewOf: (repo: string, codex: unknown[] | null, kit?: string | null, wanted?: string | null) => ToolsView;
173
186
  codexServers: (repo: string) => Promise<unknown[] | null>;
@@ -209,6 +222,8 @@ export interface ServerRuntime {
209
222
  provider: OidcProvider;
210
223
  at: number;
211
224
  } | null;
225
+ /** The identity provider as last discovered; a page never discovers it. */
226
+ providerKnown: (issuer: string) => import("../oidc.js").OidcProvider | null;
212
227
  providerFor: (issuer: string) => Promise<{
213
228
  ok: true;
214
229
  provider: OidcProvider;