toolroll 0.9.91 → 0.9.93

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 (124) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/approval-authority.d.ts +9 -1
  3. package/dist/approval-authority.js +15 -2
  4. package/dist/assignment-status.d.ts +1 -1
  5. package/dist/assignment-ui.js +2 -2
  6. package/dist/assignment.js +11 -3
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +30 -30
  9. package/dist/browser-crew.js +2 -2
  10. package/dist/browser-workspace.d.ts +3 -5
  11. package/dist/build-review.js +2 -0
  12. package/dist/builder.js +21 -2
  13. package/dist/chat-actions.d.ts +11 -3
  14. package/dist/chat-actions.js +47 -2
  15. package/dist/chat-decide.js +2 -0
  16. package/dist/chat-review.d.ts +2 -1
  17. package/dist/chat-review.js +2 -2
  18. package/dist/check-facts.d.ts +124 -0
  19. package/dist/check-facts.js +272 -0
  20. package/dist/check-report.d.ts +5 -0
  21. package/dist/check-report.js +4 -3
  22. package/dist/cli.js +5 -0
  23. package/dist/contracts/chat-actions.d.ts +10 -0
  24. package/dist/contracts/chat-actions.js +4 -0
  25. package/dist/contracts/console-api.d.ts +3 -2
  26. package/dist/contracts/console-api.js +5 -2
  27. package/dist/contracts/release-check.d.ts +25 -1
  28. package/dist/contracts/release-check.js +19 -0
  29. package/dist/contracts/route-results.d.ts +121 -0
  30. package/dist/contracts/route-results.js +71 -0
  31. package/dist/contracts/route.d.ts +343 -5
  32. package/dist/contracts/route.js +36 -7
  33. package/dist/contracts/store-json.d.ts +2 -0
  34. package/dist/desktop-update.d.ts +2 -2
  35. package/dist/desktop-update.js +13 -9
  36. package/dist/dispose.js +6 -0
  37. package/dist/eval-hooks.d.ts +55 -0
  38. package/dist/eval-hooks.js +227 -4
  39. package/dist/flaky.d.ts +17 -0
  40. package/dist/flaky.js +9 -0
  41. package/dist/flow-engine.js +1 -1
  42. package/dist/flow-pull-request.js +1 -1
  43. package/dist/lead-doors.js +4 -2
  44. package/dist/lead-tools.js +10 -2
  45. package/dist/local-owner.d.ts +1 -1
  46. package/dist/local-owner.js +11 -0
  47. package/dist/memory-cli.d.ts +3 -0
  48. package/dist/memory-cli.js +3 -3
  49. package/dist/memory-pass.js +5 -1
  50. package/dist/model-choices.d.ts +7 -3
  51. package/dist/model-choices.js +26 -11
  52. package/dist/models-cli.d.ts +205 -0
  53. package/dist/models-cli.js +143 -4
  54. package/dist/models-ui.d.ts +57 -0
  55. package/dist/models-ui.js +44 -1
  56. package/dist/needs-you.d.ts +7 -7
  57. package/dist/needs-you.js +12 -10
  58. package/dist/operate-remote-arguments.js +4 -4
  59. package/dist/operate-remote.js +1 -1
  60. package/dist/operate.js +191 -23
  61. package/dist/phase-routing.d.ts +82 -2
  62. package/dist/phase-routing.js +132 -18
  63. package/dist/project-delete.d.ts +4 -0
  64. package/dist/project-delete.js +16 -4
  65. package/dist/project-memory.d.ts +8 -0
  66. package/dist/project-memory.js +20 -1
  67. package/dist/pull-request-flow.js +1 -1
  68. package/dist/push.d.ts +10 -3
  69. package/dist/push.js +22 -6
  70. package/dist/release-adapters.js +11 -1
  71. package/dist/release-approval.d.ts +191 -0
  72. package/dist/release-approval.js +382 -0
  73. package/dist/release-auto.js +22 -4
  74. package/dist/release-card.d.ts +73 -4
  75. package/dist/release-card.js +168 -23
  76. package/dist/release-cli.js +9 -1
  77. package/dist/release.d.ts +30 -0
  78. package/dist/release.js +42 -3
  79. package/dist/result-actions.d.ts +4 -0
  80. package/dist/result-actions.js +3 -0
  81. package/dist/result-acts.d.ts +3 -3
  82. package/dist/result-acts.js +1 -1
  83. package/dist/route-hooks.d.ts +54 -0
  84. package/dist/route-hooks.js +179 -0
  85. package/dist/route-results.d.ts +70 -0
  86. package/dist/route-results.js +340 -0
  87. package/dist/route-rules.d.ts +324 -0
  88. package/dist/route-rules.js +781 -0
  89. package/dist/route-savings.d.ts +136 -0
  90. package/dist/route-savings.js +193 -0
  91. package/dist/route-store.d.ts +300 -0
  92. package/dist/route-store.js +598 -0
  93. package/dist/scope.d.ts +3 -1
  94. package/dist/scope.js +4 -1
  95. package/dist/server/guards.js +1 -0
  96. package/dist/server/pages-assets.js +1 -1
  97. package/dist/server/render-chat.js +2 -2
  98. package/dist/server/render-release.d.ts +17 -0
  99. package/dist/server/render-release.js +84 -0
  100. package/dist/server/render-results.d.ts +0 -2
  101. package/dist/server/render-results.js +4 -6
  102. package/dist/server/render-tasks.d.ts +7 -0
  103. package/dist/server/render-tasks.js +16 -3
  104. package/dist/server/route-table.d.ts +1 -1
  105. package/dist/server/route-table.js +19 -1
  106. package/dist/server/settings.d.ts +2 -1
  107. package/dist/server/settings.js +87 -1
  108. package/dist/server/task-views.js +6 -0
  109. package/dist/server/tasks.d.ts +15 -0
  110. package/dist/server/tasks.js +105 -20
  111. package/dist/settings-proofs.js +2 -0
  112. package/dist/spend-ui.d.ts +11 -0
  113. package/dist/spend-ui.js +49 -2
  114. package/dist/stop-proof.d.ts +67 -0
  115. package/dist/stop-proof.js +293 -0
  116. package/dist/store.d.ts +106 -31
  117. package/dist/store.js +653 -128
  118. package/dist/surface.js +17 -5
  119. package/dist/task-outcome-cli.js +24 -2
  120. package/dist/toolroll-update-ui.js +2 -2
  121. package/dist/toolroll-update.js +4 -2
  122. package/dist/work-index.js +7 -9
  123. package/dist/work-summary.d.ts +2 -2
  124. package/package.json +1 -1
@@ -176,6 +176,7 @@ export function createSharedGuards(runtime) {
176
176
  const task = matchTaskPath(url.pathname, '(?:/[a-z-]+)?$');
177
177
  return task === null ? null : runtime.store.lookupRef(task.taskId)?.repo ?? null;
178
178
  }
179
+ case 'task-ref': return Number.isSafeInteger(id) && id > 0 ? runtime.store.refForId(id)?.repo ?? null : null;
179
180
  case 'run': return runRepo(id);
180
181
  case 'decision': return runRepo(runtime.store.getDecision(id)?.run);
181
182
  case 'incident': return runRepo(runtime.store.openIncidents().find(one => one.id === id)?.run);
@@ -39,7 +39,7 @@ const PWA_MANIFEST = JSON.stringify({
39
39
  // paths — the payload URL is data, revalidated, never handed raw to the
40
40
  // browser (arc 3 finding 5).
41
41
  const PWA_WORKER = `// Toolroll — push only; deliberately NO fetch handler (no offline cache of an authenticated console).
42
- const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/];
42
+ const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/, /^\\/approve\\/[0-9]+$/];
43
43
  self.addEventListener("push", function (event) {
44
44
  var data = {};
45
45
  try { data = event.data ? event.data.json() : {}; } catch (e) {}
@@ -34,7 +34,7 @@ import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, str
34
34
  import { chatResultHref, projectChatHref, taskChatHref, taskHref } from "./http.js";
35
35
  import {} from "./render-pages.js";
36
36
  import { completionReceiptCard } from "./render-results.js";
37
- import { agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
37
+ import { routeEvidenceHtml, agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
38
38
  /** The installation fact that the lead was turned on by default, once (its value: the provider). */
39
39
  export const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
40
40
  /** The installation fact that the phone card was put away. */
@@ -51,7 +51,7 @@ export function taskChatContext(focus) {
51
51
  // conversation never hides which agent runs.
52
52
  focus.route === null
53
53
  ? ""
54
- : html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p><div class="agents-badges">${agentsBadgesHtml(focus.route)}</div>${focus.route.projection === null ? "" : html `${agentsAvailabilityHtml(focus.route.projection)}${agentsWhyHtml(focus.route.projection)}`}<p class="meta"><a href="${taskHref(focus.id)}#agents">Change agents on the task →</a></p></div>`}<div class="task-chat-overview-actions"><a class="task-chat-overview-link" href="${taskHref(focus.id)}">Open full overview →</a></div></aside>`;
54
+ : html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p>${routeEvidenceHtml(focus.route)}<div class="agents-badges">${agentsBadgesHtml(focus.route)}</div>${focus.route.projection === null ? "" : html `${agentsAvailabilityHtml(focus.route.projection)}${agentsWhyHtml(focus.route.projection)}`}<p class="meta"><a href="${taskHref(focus.id)}#agents">Change agents on the task →</a></p></div>`}<div class="task-chat-overview-actions"><a class="task-chat-overview-link" href="${taskHref(focus.id)}">Open full overview →</a></div></aside>`;
55
55
  }
56
56
  export function taskChatApproval(focus, csrf) {
57
57
  const approval = focus.approval;
@@ -0,0 +1,17 @@
1
+ import type { ApprovalFacts } from "../design/kit.js";
2
+ import { type ReleaseApprovalCard } from "../release-approval.js";
3
+ import { type Chrome, type Screen } from "./chrome.js";
4
+ export type ReleaseSheetData = {
5
+ card: ReleaseApprovalCard;
6
+ /** The single-use approval nonce for this person, task and card digest ("" when nothing can be approved here). */
7
+ nonce: string;
8
+ facts: ApprovalFacts;
9
+ /** This computer's local owner: releases need a full sign-in first (scope.ts localApprovalEligible). */
10
+ local: {
11
+ message: string;
12
+ action: string;
13
+ href: string;
14
+ } | null;
15
+ problem: string | null;
16
+ };
17
+ export declare function releaseSheetPage(chrome: Chrome, data: ReleaseSheetData): Screen;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The release approval sheet (docs/plans/trusted-release-reports.md, slice A, A7): one screen with what the person is
3
+ * approving, GitHub CI now, what approving does, what changed and every term, then one Approve release button. The
4
+ * page only shows: the approval POST (`/t/<id>/approve`, `sheet=release`) proves the person, the card and the scope
5
+ * again (release-approval.ts approveReleaseCard).
6
+ */
7
+ import { html, joinHtml } from "../html.js";
8
+ import { ciWords, releaseConsequence } from "../release-approval.js";
9
+ import { checkAccessWords } from "../check-access.js";
10
+ import { projectName } from "../project.js";
11
+ import { screen } from "./chrome.js";
12
+ import { approvalTerms, approveForm, chip, disclose, doc, docHeader, facts, infoLine, note, notice, slots, btn } from "./kit.js";
13
+ import { taskHref } from "./http.js";
14
+ /** The change list, folded: every commit when it is whole, or why it isn't. */
15
+ function changesHtml(card) {
16
+ const changes = card.card.changes;
17
+ const id = card.seal.identity;
18
+ if (changes.commits === null) {
19
+ return note(changes.baselineUnreadable === true ? "What changed couldn't be read: the last verified release's commit can't be read."
20
+ : `What changed couldn't be read${changes.why === undefined ? "" : ` (${changes.why})`}. See pull request #${id.pr} on GitHub.`);
21
+ }
22
+ const head = changes.baseline === null ? `Pull request #${id.pr}'s commits` : `Since the last verified release (${changes.baseline.slice(0, 7)})`;
23
+ const list = changes.commits.length === 0 ? note("No commits.")
24
+ : html `<ul class="k-list">${changes.commits.map(one => html `<li><code class="k-mono">${one.sha.slice(0, 7)}</code> ${one.subject}</li>`)}</ul>`;
25
+ return disclose(`What changed: ${changes.commits.length} commit${changes.commits.length === 1 ? "" : "s"}`, html `<p class="k-note">${head}</p>${list}`);
26
+ }
27
+ /** Every signed term, before the button. */
28
+ function termsHtml(card) {
29
+ return disclose("Every term", facts(card.terms.map(line => [line.label, html `<span class="k-mono">${line.value}</span>`])), true);
30
+ }
31
+ export function releaseSheetPage(chrome, data) {
32
+ const card = data.card;
33
+ const id = card.seal.identity;
34
+ const title = `Release ${id.packageVersion}`;
35
+ const summary = facts([
36
+ ["Pull request", `#${id.pr}`],
37
+ ["Branch", html `<code class="k-mono">${id.branch}</code>`],
38
+ ["Commit", html `<code class="k-mono">${id.approvedSha}</code>`],
39
+ ["Project", projectName(id.repo)],
40
+ ["Checks", checkAccessWords(id.access)],
41
+ ]);
42
+ const consequence = releaseConsequence(card);
43
+ const ci = ciWords(card.ci, id.approvedSha);
44
+ const notRunning = card.stopped === null ? "" : infoLine(html `The release isn't running (it stopped at ${card.stopped.step}). After you approve, run <code class="k-mono">toolroll release ${id.branch}</code>.`);
45
+ let status, text, acts, terms, wait = false;
46
+ if (card.approved !== null) {
47
+ status = html `${chip("Approved", "done")}<h2>Release approved</h2>`;
48
+ text = `Approved by ${card.approved.by}. ${ci}`;
49
+ acts = btn("Open the task", { variant: "outline", href: taskHref(card.taskId) });
50
+ terms = "";
51
+ }
52
+ else if (card.refusal !== null) {
53
+ status = html `${chip("Can't approve", "failed")}<h2>Can't approve this release now</h2>`;
54
+ text = card.refusal;
55
+ acts = btn("Open the task", { variant: "outline", href: taskHref(card.taskId) });
56
+ terms = "";
57
+ }
58
+ else if (data.local !== null) {
59
+ wait = true;
60
+ status = html `${chip("Needs you", "needs")}<h2>Ready for your approval</h2>`;
61
+ text = `${ci} Releases need a full sign-in on this computer.`;
62
+ acts = btn(data.local.action, { variant: "primary", href: data.local.href });
63
+ terms = data.local.message;
64
+ }
65
+ else {
66
+ wait = true;
67
+ status = html `${chip("Needs you", "needs")}<h2>Ready for your approval</h2>`;
68
+ text = ci;
69
+ acts = data.nonce === "" ? "" : approveForm({
70
+ action: `${taskHref(card.taskId)}/approve`, kind: "standard", facts: data.facts, label: "Approve release", primary: true,
71
+ before: html `<section class="k-sec"><h3>What approving does</h3>${joinHtml(consequence.map(line => html `<p class="k-note">${line}</p>`))}${termsHtml(card)}</section>`,
72
+ form: { hidden: { nonce: data.nonce, digest: card.digest, sheet: "release" }, attrs: { class: "k-form", "data-release-approve": card.digest } },
73
+ });
74
+ terms = approvalTerms("standard", data.facts);
75
+ }
76
+ // What, then what changed, then the one decision: its status, CI now, what approving does, every term, the button.
77
+ const formShown = acts !== "" && data.nonce !== "" && card.approved === null && card.refusal === null && data.local === null;
78
+ const body = doc(docHeader({ title, back: [taskHref(card.taskId), "Task"], meta: `${projectName(id.repo)} · pull request #${id.pr}` }),
79
+ // A refused approval says why first, above the card it now shows.
80
+ data.problem === null ? "" : notice(data.problem), summary, changesHtml(card), slots({ wait, attrs: { "data-release-sheet": card.taskId }, status, text, acts, terms }), notRunning,
81
+ // When the form isn't shown, the consequence and the terms are still on the page.
82
+ formShown ? "" : html `<section class="k-sec"><h3>What approving does</h3>${joinHtml(consequence.map(line => html `<p class="k-note">${line}</p>`))}</section>${termsHtml(card)}`);
83
+ return screen(title, body, { chrome });
84
+ }
@@ -707,8 +707,6 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
707
707
  rebuildable?: boolean;
708
708
  refuted?: boolean;
709
709
  }): BrowserNeedAction | null;
710
- /** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
711
- export declare const needsCheck: (assignment: AssignmentSnapshot) => boolean;
712
710
  /** The server page's form of the same: the need's action first, Request changes as the quiet second. */
713
711
  export declare function resultNeedAction(need: BrowserNeedAction, o: ResultPanelOptions, canRequest: boolean): Html;
714
712
  /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
@@ -731,7 +731,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
731
731
  checksRunning,
732
732
  blocked,
733
733
  canRequest: panel.panel.canRequest && panel.panel.request !== null,
734
- need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
734
+ need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "recheck-stopped" : null,
735
735
  next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
736
736
  unanswered: settled === null ? 0 : unanswered.length,
737
737
  notRight: 0,
@@ -2009,12 +2009,10 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
2009
2009
  if (status.need.key === "review-result" || here)
2010
2010
  return null;
2011
2011
  }
2012
- const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
2013
- ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
2012
+ const confirm = action?.code === "recheck-stopped" && action.target.runId !== null && csrf !== ""
2013
+ ? { action: `${taskHref(assignment.rootId)}/recheck-stopped`, run: action.target.runId, returnTo } : null;
2014
2014
  return { label: status.need.action.label, href: assignmentActionHref(assignment) ?? taskHref(assignment.rootId), confirm };
2015
2015
  }
2016
- /** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
2017
- export const needsCheck = (assignment) => assignment.need != null && "key" in assignment.need && assignment.need.key === "check-stopped";
2018
2016
  /** The server page's form of the same: the need's action first, Request changes as the quiet second. */
2019
2017
  export function resultNeedAction(need, o, canRequest) {
2020
2018
  const control = need.accept != null
@@ -2022,7 +2020,7 @@ export function resultNeedAction(need, o, canRequest) {
2022
2020
  : need.rebuild != null
2023
2021
  ? postForm(need.rebuild.action, html `<button type="submit" data-primary-action data-rebuild style="min-height:44px">${need.label}</button>`, { attrs: { class: "rebuild" } })
2024
2022
  : need.confirm !== null
2025
- ? postForm(need.confirm.action, html `${need.confirm.checked === true ? html `<label><input type="checkbox" name="checked" value="yes" required> Nothing from build #${need.confirm.run} is running</label>` : ""}${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button type="submit" style="min-height:44px">${need.label}</button>`, { attrs: { id: "confirm-stopped", class: "confirm-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
2023
+ ? postForm(need.confirm.action, html `<button type="submit" data-primary-action style="min-height:44px">${need.label}</button>`, { attrs: { id: "recheck-stopped", class: "recheck-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
2026
2024
  : html `<a class="button-link" href="${need.href ?? "#"}" data-primary-action>${need.label}</a>`;
2027
2025
  return html `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? html `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
2028
2026
  }
@@ -220,7 +220,12 @@ export type RouteView = {
220
220
  * every option an exact pair the operator named once in configuration.
221
221
  * The form offers these and nothing else. */
222
222
  choices: Record<Phase, AgentChoice[]>;
223
+ /** Results routing (slice 2): "Why this model: …" when an earned rule selected the build leg (route-rules.ts
224
+ * selectionEvidenceWords), else absent. The selection is sealed; the counts read "so far". */
225
+ evidence?: string | null;
223
226
  };
227
+ /** The evidence line beside the agents, only when an earned rule chose the builder. */
228
+ export declare function routeEvidenceHtml(view: RouteView | null | undefined, className?: string): Html;
224
229
  export declare const ROLE_NOUN: Record<RouteProjection["legs"][number]["phase"], string>;
225
230
  export declare function agentsStandingWords(view: RouteView): string;
226
231
  /** The size line an approval leads its agents with — "Small change: fast
@@ -609,6 +614,8 @@ export declare function taskBodyParts(data: {
609
614
  qualityMode?: QualityMode | null;
610
615
  /** The phase route (v47) and whether this session may edit it. */
611
616
  route?: RouteView | null;
617
+ /** Results routing (slice 1): this task family's result record in words; null when it has none yet. */
618
+ resultRecord?: string | null;
612
619
  canEditRoute?: boolean;
613
620
  runs: Run[];
614
621
  /** Proof produced by the newest finished attempt. The two booleans are
@@ -50,7 +50,7 @@ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SEN
50
50
  import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
51
51
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
52
52
  import { coordinatorProposalsSection } from "./render-chat.js";
53
- import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
53
+ import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
54
54
  import {} from "./session.js";
55
55
  import { linkWords, originNote, requestName, requestWords } from "../task-source.js";
56
56
  export function contractChangeHtml(change) {
@@ -439,6 +439,7 @@ export function approvalSheetParts(input) {
439
439
  // The size, said once and plainly beside who builds: "Small change: fast model, no plan".
440
440
  projection === null ? "" : sizeLineHtml(projection),
441
441
  who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
442
+ routeEvidenceHtml(route, "approval-evidence"),
442
443
  consent,
443
444
  ]);
444
445
  return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and build again" : "Approve and start", after, allowing, passwordNote };
@@ -565,6 +566,14 @@ export function profileWords(scope) {
565
566
  const terms = sandboxTermsWords(profile);
566
567
  return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>${profile.checkAccess === undefined ? "" : html `<p class="meta" data-scope-checks>Checks: ${checkAccessWords(profile.checkAccess)}.</p>`}<p class="meta" data-scope-reads>Reads: ${terms.reads}.</p>${terms.mach === null ? "" : html `<p class="meta" data-scope-mach>${terms.mach.charAt(0).toUpperCase()}${terms.mach.slice(1)}.</p>`}`;
567
568
  }
569
+ /** The evidence line beside the agents, only when an earned rule chose the builder. */
570
+ export function routeEvidenceHtml(view, className = "meta") {
571
+ const words = view?.evidence ?? null;
572
+ if (words === null)
573
+ return html ``;
574
+ const split = words.indexOf(": ");
575
+ return html `<p class="${className} route-evidence" data-route-evidence><strong>${words.slice(0, split + 1)}</strong>${words.slice(split + 1)}</p>`;
576
+ }
568
577
  export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
569
578
  export function agentsStandingWords(view) {
570
579
  return view.kind === "legacy"
@@ -658,6 +667,7 @@ export function agentsCeremonyHtml(view) {
658
667
  html `<div class="agents-ceremony"><p class="approval-label">agents</p>`,
659
668
  sizeLineHtml(view.projection),
660
669
  html `<p class="agents-summary">${view.projection.summary}</p>`,
670
+ routeEvidenceHtml(view),
661
671
  html `<div class="agents-badges">${riskBadgeHtml(view.projection)}<span class="badge">${sentenceCase(view.projection.postureWords)}</span></div>`,
662
672
  agentsWhyHtml(view.projection),
663
673
  html `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`,
@@ -735,6 +745,7 @@ export function agentsCardHtml(taskId, view, canEdit) {
735
745
  html `<section class="card agents-card" id="agents" aria-label="agents">`,
736
746
  html `<div class="agents-head"><h3>Agents</h3><div class="agents-badges">${agentsBadgesHtml(view)}</div></div>`,
737
747
  html `<p class="agents-summary">${agentsSummaryWords(view)}</p>`,
748
+ routeEvidenceHtml(view),
738
749
  p === null
739
750
  ? view.kind === "unreadable"
740
751
  ? html `<p class="agents-halted">Nothing runs for this task until its scope is filed again and approved.</p>`
@@ -2280,6 +2291,8 @@ export function taskBodyParts(data) {
2280
2291
  review.push({ ...approvedScope, label: approvedScope.label === "Approved scope" ? "Approvals" : "Scope" });
2281
2292
  review.push({ label: "Who reviews", parts: [facts.find(one => one.label === "Approval rules")?.parts[0] ?? "Any approver in this project"] });
2282
2293
  review.push({ label: "Publishes as", parts: [publishesAs] });
2294
+ if (data.resultRecord != null)
2295
+ review.push({ label: "Result record", parts: [data.resultRecord] });
2283
2296
  const about = [];
2284
2297
  const filedFact = facts.find(one => one.label === "Filed by");
2285
2298
  about.push(filedFact ?? { label: "Filed by", parts: [filedBy ?? (data.origin != null ? "An automation" : data.filedVia == null ? "Not recorded" : `via ${data.filedVia}`)] });
@@ -2395,8 +2408,8 @@ export function taskBodyParts(data) {
2395
2408
  approve: data.archived != null || sheetInput === null || dependencyChoiceNeeded || data.csrf === "" ? null : approveKitOf(sheetInput, data.approvalFresh ?? null),
2396
2409
  approved: scope !== null && approval.approved ? approvedKitOf(scope, approval, data.approvalProof ?? null) : null,
2397
2410
  more,
2398
- confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
2399
- ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
2411
+ recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
2412
+ ? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
2400
2413
  rebuild,
2401
2414
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
2402
2415
  lead: [
@@ -22,7 +22,7 @@ export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "
22
22
  /** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
23
23
  export type RouteScope = "none" | "read" | "act" | "step-up";
24
24
  /** Where the request's project comes from. */
25
- export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
25
+ export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "task-ref" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
26
26
  /**
27
27
  * What a project-limited account may do here:
28
28
  * - `deny`: instance access only.
@@ -91,6 +91,11 @@ const LOCAL_POSTS = Object.freeze({
91
91
  "settings.lead-on": "choose-default-agent",
92
92
  // Choosing which open project the pages show: a view filter, never authorization.
93
93
  "projects.select": "read",
94
+ // Results routing: lowering only.
95
+ "settings.models-turn-off": "routing-turn-off",
96
+ "settings.models-not-now": "dismiss-suggestion",
97
+ "settings.models-suggest-off": "dismiss-suggestion",
98
+ "settings.models-stop": "trial-stop",
94
99
  "agents.seen": "mark-seen",
95
100
  "agents.watch": "watch-agent",
96
101
  });
@@ -281,6 +286,10 @@ get("tasks", "welcome", "/welcome", { limited: "collection", ...free });
281
286
  post("tasks", "welcome.sample", "/welcome/sample", { callers: ["cookie"], project: "none", ...approver("An approver adds projects.", "/welcome") });
282
287
  get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
283
288
  get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
289
+ // One-tap release approval (trusted-release-reports.md A6/A7): the sheet, and the push's opaque locator, which only
290
+ // redirects to it once the plane's own state says the task ref is a release in the person's projects.
291
+ get("tasks", "task.release", `^\\/t\\/${TASK}\\/release$`, { project: "task", limited: "resource", ...free, sample: "/t/one/release" });
292
+ get("tasks", "release.locate", `^\\/approve\\/${ID}$`, { project: "task-ref", limited: "resource", ...free, sample: "/approve/1" });
284
293
  get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
285
294
  get("tasks", "runs", "/runs", { limited: "collection", ...free }); // a redirect to /agents now
286
295
  get("pages", "menu", "/menu", { limited: "collection", ...free });
@@ -483,6 +492,13 @@ for (const one of ["check", "watch", "update"])
483
492
  // Passwordless session; model-choices.ts re-proves the session and standing inside its one transaction).
484
493
  for (const one of ["agent", "apply"])
485
494
  post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes models.", "/settings/models") });
495
+ // Earned rules (results routing): accepting, keeping a paused rule and switching suggestions on are an instance
496
+ // operator's browser step-up; turning a rule off, Not now and switching suggestions off only take authority away (a
497
+ // signed-in operator, or the local owner in exactly that act: route-store.ts proveRuleDoor re-checks it in the write).
498
+ for (const one of ["accept", "keep", "suggest-on", "try"])
499
+ post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes which models run.", "/settings/models") });
500
+ for (const one of ["turn-off", "not-now", "suggest-off", "stop"])
501
+ post("settings", `settings.models-${one}`, `/settings/models/${one}`, operator("An instance operator changes which models run.", "/settings/models"));
486
502
  // Any other models action answers the models area's own refusal.
487
503
  post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
488
504
  post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
@@ -544,7 +560,9 @@ post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", l
544
560
  post("tasks", "task.act.retry-review", `^\\/t\\/${TASK}\\/retry-review$`, { project: "task", limited: "resource", sample: "/t/one/retry-review" });
545
561
  post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete", ...approver("Only an approver can mark a result complete.") });
546
562
  post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
547
- post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
563
+ // Check again: Toolroll looks at this computer's processes for a finished build once more (stop-proof.ts). Nobody
564
+ // vouches that a build stopped, so no password: it only asks for the look the worker makes on its own anyway.
565
+ post("tasks", "task.act.recheck-stopped", `^\\/t\\/${TASK}\\/recheck-stopped$`, { project: "task", limited: "resource", sample: "/t/one/recheck-stopped", ...approver("Only an approver can have Toolroll check a build again.") });
548
566
  post("tasks", "task.act.stop-carrying", `^\\/t\\/${TASK}\\/stop-carrying$`, { project: "task", limited: "resource", sample: "/t/one/stop-carrying", ...approver("Only an approver can stop carrying a release.") });
549
567
  post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
550
568
  // Putting one task away and bringing it back (task-tidy.md): a person's act, so only a browser session.
@@ -1,5 +1,5 @@
1
1
  import type { ServerRuntime } from './runtime.js';
2
- import { type RoleView } from "../models-ui.js";
2
+ import { type RoleView, type SuggestedView } from "../models-ui.js";
3
3
  export declare function createSettingsHandlers(runtime: ServerRuntime): {
4
4
  registrations: import("./handler-registry.js").Registration[];
5
5
  modelsView: (canManage: boolean, csrf: string, now: Date, said: string | null, problem: string | null, query?: URLSearchParams) => {
@@ -71,5 +71,6 @@ export declare function createSettingsHandlers(runtime: ServerRuntime): {
71
71
  taskId: string;
72
72
  title: string;
73
73
  }[];
74
+ suggested: SuggestedView;
74
75
  };
75
76
  };
@@ -76,6 +76,7 @@ import { startConnect } from "../mcp-connect.js";
76
76
  import { decideProposal, listProposals, memoryStatus } from "../memory-pass.js";
77
77
  import { checkModels, isNewModel, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "../model-catalog.js";
78
78
  import { modelsHtml, modelsScript } from "../models-ui.js";
79
+ import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, modelName, ruleWords, rulesOf, setSuggestModels, startTrial, stopTrial, suggestionsFor, suggestionWords, suggestModelsOn, trialOffersFor, trialOfferWords, trialsOf, trialWords, turnOffRule } from "../route-rules.js";
79
80
  import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
80
81
  import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
81
82
  import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
@@ -118,6 +119,7 @@ import { slackSettingsHtml } from "../slack-settings.js";
118
119
  import { spendHtml, spendScript } from "../spend-ui.js";
119
120
  import { approvalFactsNow } from "./kit.js";
120
121
  import { spendReport, spendReportCsv, spendRows } from "../spend-report.js";
122
+ import { savingsReportOrError } from "../route-savings.js";
121
123
  import { limitsView } from "../limits-ui.js";
122
124
  import { budgetStates, monthNamed, spendItems, usd as spendUsd, subagentNames as subagentNamesOf } from "../spend.js";
123
125
  import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
@@ -185,7 +187,9 @@ export function createSettingsHandlers(runtime) {
185
187
  };
186
188
  const windows = limitsView(store.providerLimits(), [], { project: projectName, subagent: id => subagentNames.get(id) ?? `Subagent ${id}` }, now);
187
189
  const view = { month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1), current: current.name, report: spendReport(rows),
188
- budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow() };
190
+ budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow(),
191
+ // Suggestions and rules (results-routing slice 4): read-only, for every project this console shows.
192
+ savings: savingsReportOrError(store, consoleProjects(), now) };
189
193
  // A refusal for the open form shows in the form; anything else under the limits' heading.
190
194
  const said = { said: url.searchParams.get("said"), problem: edit === null ? url.searchParams.get("problem") : null };
191
195
  return sendScreen(response, 200, screen("Spend", spendHtml(view, said), { chrome: chromeFor(null, "spend"), functional: { script: spendScript() } }));
@@ -2565,6 +2569,7 @@ ${postForm("/settings/chat-approval/save", html `\
2565
2569
  chosen: one.chosen === null ? "its default" : /^(opus|sonnet|haiku|fable)$/.test(one.chosen) ? `floating ${one.chosen}` : one.chosen,
2566
2570
  ranOn: one.ranOn ?? "not reported" })),
2567
2571
  waiting: canManage ? store.scopesAwaitingApproval(null, 10, admissionList()).map(one => ({ taskId: one.taskId, title: one.title })) : [],
2572
+ suggested: suggestedView(repo, projects, query.get("suggestion"), now),
2568
2573
  };
2569
2574
  }
2570
2575
  /** Google's redirect back: the code becomes a saved refresh token, and a page sends the person back to Settings. */
@@ -2739,6 +2744,14 @@ ${postForm("/settings/chat-approval/save", html `\
2739
2744
  "settings.models-agent": modelsSave,
2740
2745
  "settings.models-apply": modelsApply,
2741
2746
  "settings.models-act": modelsAct("other"),
2747
+ "settings.models-accept": modelsRuleAct("accept"),
2748
+ "settings.models-keep": modelsRuleAct("keep"),
2749
+ "settings.models-suggest-on": modelsRuleAct("suggest-on"),
2750
+ "settings.models-turn-off": modelsRuleAct("turn-off"),
2751
+ "settings.models-not-now": modelsRuleAct("not-now"),
2752
+ "settings.models-suggest-off": modelsRuleAct("suggest-off"),
2753
+ "settings.models-try": modelsRuleAct("try"),
2754
+ "settings.models-stop": modelsRuleAct("stop"),
2742
2755
  "settings.knowledge-refresh": knowledgeRefresh,
2743
2756
  "settings.knowledge-proposal": knowledgeProposal,
2744
2757
  "settings.knowledge-decision": knowledgeDecision,
@@ -3005,5 +3018,78 @@ ${postForm("/settings/chat-approval/save", html `\
3005
3018
  return back("problem", turned.reason === "not-on" ? "Local mode is already off." : "Only an instance operator can turn off local mode.");
3006
3019
  return back("said", "Local mode is off. This computer's browser signs in with your password now.");
3007
3020
  }
3021
+ /** Settings → Models → Suggested: one project's offers, rules and switch; on the installation scope, every
3022
+ * project's offers and rules, each reviewed on its own project's page. */
3023
+ function suggestedView(repo, projects, asked, now) {
3024
+ const repos = repo === null ? projects : [repo];
3025
+ const offers = repos.flatMap(one => suggestionsFor(store, one, now));
3026
+ const sizeTitle = (size, name) => `${name} for ${size === "small" ? "small" : "routine"} tasks`;
3027
+ const reviewed = asked === null || repo === null ? null : offers.find(one => one.id === asked && one.repo === repo) ?? null;
3028
+ return {
3029
+ repo,
3030
+ offers: offers.map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, one.name), words: suggestionWords(one) })),
3031
+ review: asked === null || repo === null ? null : reviewed === null ? { stale: true, repo } : {
3032
+ id: reviewed.id, repo: reviewed.repo, title: `${sizeTitle(reviewed.cohortSize, reviewed.name)} instead of ${reviewed.configured.name}`,
3033
+ evidence: [suggestionWords(reviewed), reviewed.configuredEvidence.decided === 0 ? `${reviewed.configured.name}: no decided similar tasks here in the last 90 days.` : `${reviewed.configured.name}: ${reviewed.configuredEvidence.mature} of its last ${reviewed.configuredEvidence.decided} decided similar tasks accepted with checks passing.`],
3034
+ prices: `${reviewed.name} $${reviewed.prices.model.in} / $${reviewed.prices.model.out}, ${reviewed.configured.name} $${reviewed.prices.configured.in} / $${reviewed.prices.configured.out} per 1M tokens (listed now)`,
3035
+ terms: grantWords(reviewed.grant), save: `Use ${reviewed.name} for ${reviewed.cohortSize === "small" ? "small" : "routine"} tasks`,
3036
+ },
3037
+ rules: repos.flatMap(one => rulesOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, modelName(store, one.model)), words: ruleWords(store, one),
3038
+ state: one.state === "active" ? "active" : one.state === "paused" ? "paused" : "ended" })),
3039
+ suggest: repo === null ? null : { on: suggestModelsOn(store, repo) },
3040
+ trialOffers: repos.flatMap(one => trialOffersFor(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
3041
+ title: `Try ${sizeTitle(one.cohortSize, one.name)}`, words: trialOfferWords(one), save: `Try it on the next ${one.places}` })),
3042
+ trials: repos.flatMap(one => trialsOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
3043
+ title: `Trying ${sizeTitle(one.cohortSize, modelName(store, one.model))}`, words: trialWords(store, one), live: one.state === "trialling" || one.state === "paused" })),
3044
+ };
3045
+ }
3046
+ /**
3047
+ * Settings → Models → Suggested (results routing, slice 2): the earned-rule doors (route-rules.ts), re-proved inside
3048
+ * their own transaction. Accepting, keeping a paused rule and switching suggestions on are an instance operator's
3049
+ * step-up (the password, or a Passwordless session); turning a rule off or dropping it, Not now, and switching
3050
+ * suggestions off only take authority away, so a signed-in session does — or this computer's local owner, whose
3051
+ * route row narrows it to exactly that act.
3052
+ */
3053
+ function modelsRuleAct(kind) {
3054
+ return async (ctx) => {
3055
+ const { response, now, posted } = ctx;
3056
+ const who = browserCaller(ctx.who);
3057
+ const body = readForm(posted, CONSOLE_FORMS.modelsRules);
3058
+ const repo = body.get("scope") ?? "";
3059
+ const back = (key, words) => redirect(response, `/settings/models?scope=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#models-suggested`);
3060
+ if (["scope", "password", "id", "rule"].some(field => body.sent.getAll(field).length > 1))
3061
+ return back("problem", "That form was sent twice over. Nothing changed.");
3062
+ if (!consoleProjects().includes(repo) || !store.accountCanAccess(who.name, repo))
3063
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/models");
3064
+ let door;
3065
+ if (kind === "accept" || kind === "keep" || kind === "suggest-on" || kind === "try") {
3066
+ const proved = authenticateApprover(who, body.get("password") ?? "");
3067
+ if (!proved.ok)
3068
+ return back("problem", "Enter your Toolroll password to change which models run.");
3069
+ door = { kind: "person", person: modelsAuthority(who, proved.method) };
3070
+ }
3071
+ else {
3072
+ door = who.authority === "local-owner" ? { kind: "local-owner", actor: who.name } : { kind: "person", person: modelsAuthority(who, "session") };
3073
+ }
3074
+ const ruleId = /^[1-9][0-9]{0,9}$/.test(body.get("rule") ?? "") ? Number(body.get("rule")) : -1;
3075
+ const id = body.get("id") ?? "";
3076
+ const result = kind === "accept" ? acceptSuggestion(store, { repo, id }, door, now)
3077
+ : kind === "try" ? startTrial(store, { repo, id }, door, now)
3078
+ : kind === "stop" ? stopTrial(store, ruleId, door, now)
3079
+ : kind === "keep" ? keepRule(store, ruleId, door, now)
3080
+ : kind === "turn-off" ? turnOffRule(store, ruleId, door, now)
3081
+ : kind === "not-now" ? dismissSuggestion(store, { repo, id }, door, now)
3082
+ : setSuggestModels(store, repo, kind === "suggest-on", door);
3083
+ if (!result.ok)
3084
+ return back("problem", result.problem);
3085
+ return back("said", kind === "try" ? "Trial started. The next tasks of that size try the cheaper model; each waits for your approval."
3086
+ : kind === "stop" ? "Trial stopped. New tasks build on the configured model; tasks already approved keep their model."
3087
+ : kind === "accept" ? "Saved. New tasks of that size build on the suggested model; each is still approved by a person."
3088
+ : kind === "keep" ? "Kept. The rule is on again with its original terms."
3089
+ : kind === "turn-off" ? "Turned off. New tasks build on the configured model; tasks already approved keep their model."
3090
+ : kind === "not-now" ? "Not now. This suggestion stays quiet for 30 days."
3091
+ : kind === "suggest-on" ? "Suggestions are on for this project." : "Suggestions are off for this project.");
3092
+ };
3093
+ }
3008
3094
  return { registrations, modelsView };
3009
3095
  }
@@ -1,3 +1,5 @@
1
+ import { resultRecordWordsOf } from "../route-results.js";
2
+ import { selectionEvidenceWords } from "../route-rules.js";
1
3
  import { feedbackImageProblem } from "../result-actions.js";
2
4
  import { parseFeedbackImage } from "../result-review.js";
3
5
  import { releaseViewOf } from "../release-view.js";
@@ -473,6 +475,8 @@ export function createTaskViews(runtime) {
473
475
  // and the focused chat, with the readiness this task's runners report.
474
476
  route: routeView,
475
477
  canEditRoute: who.role === "approver",
478
+ // Results routing (slice 1): the family's saved record, or the one its rows give now; never a reason to fail the page.
479
+ resultRecord: resultRecordWordsOf(runtime.store, taskId, runtime.evidenceRoot, now),
476
480
  spendDefaults: runtime.store.getSpendDefaults(),
477
481
  permissionDefault: runtime.store.permissionDefault().mode,
478
482
  permissionMode: ref?.permissionMode ?? null,
@@ -571,6 +575,8 @@ export function createTaskViews(runtime) {
571
575
  replanOnPlanChange: ref.plan === "drafted" && scope !== null && !approvalOf(scope).approved,
572
576
  digest: scope?.digest ?? null,
573
577
  choices: agentChoicesFor(runtime.store, ref.repo, routed.kind === "route" ? routed.route : null),
578
+ // Results routing: why an earned rule's model builds it, from the records as they stand now.
579
+ evidence: routed.kind === "route" ? selectionEvidenceWords(runtime.store, routed.route, now) : null,
574
580
  };
575
581
  if (routed.kind === "route") {
576
582
  return { kind: "route", source: routed.source, projection: projectRoute(routed.route, runtime.store.readinessLookupFor(ref.repo, ref.assignedRunner, now)), legacy: null, problem: null, ...shared };
@@ -64,3 +64,18 @@ export declare function createAgentSignalHandlers(runtime: ServerRuntime): {
64
64
  "agents.seen": (ctx: HandlerContext) => Promise<void>;
65
65
  "agents.watch": (ctx: HandlerContext) => Promise<void>;
66
66
  };
67
+ /**
68
+ * One-tap release approval (docs/plans/trusted-release-reports.md, slice A): the sheet (`/t/<id>/release`), the push's
69
+ * opaque locator (`/approve/<task ref>`, a GET that only redirects to the sheet) and the sheet's approval, which the
70
+ * task approval POST hands here once its shared checks (cookie session, CSRF and the browser key at admission, the
71
+ * single-use nonce, a password or a standing proof) passed. The approval itself is the shared card wrapper around the
72
+ * unchanged approve() (release-approval.ts approveReleaseCard).
73
+ */
74
+ export declare function createReleaseSheetHandlers(runtime: ServerRuntime): {
75
+ routes: {
76
+ "task.release": (ctx: HandlerContext) => Promise<void>;
77
+ "release.locate": (ctx: HandlerContext) => Promise<void>;
78
+ };
79
+ sheetScreen: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, problem: string | null, status: number) => void;
80
+ approveFromSheet: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, cardDigest: string, token: string) => void;
81
+ };