toolroll 0.9.51 → 0.9.53

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 (164) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/backup.d.ts +8 -0
  8. package/dist/backup.js +10 -1
  9. package/dist/board.d.ts +3 -19
  10. package/dist/board.js +4 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-workspace.d.ts +3 -1
  14. package/dist/builder.d.ts +9 -40
  15. package/dist/builder.js +50 -257
  16. package/dist/chat-actions.js +3 -14
  17. package/dist/chat-controls.d.ts +0 -4
  18. package/dist/chat-controls.js +0 -1
  19. package/dist/chat-decide.js +1 -3
  20. package/dist/chat-delivery.d.ts +0 -2
  21. package/dist/chat-delivery.js +0 -1
  22. package/dist/chat-quiet.js +2 -2
  23. package/dist/chat-task-actions.d.ts +1 -1
  24. package/dist/chat-task-actions.js +0 -2
  25. package/dist/claim.d.ts +79 -244
  26. package/dist/claim.js +688 -1023
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +10 -2
  29. package/dist/contracts/cli.d.ts +0 -13
  30. package/dist/contracts/cli.js +0 -2
  31. package/dist/contracts/console-api.d.ts +5 -10
  32. package/dist/contracts/console-api.js +4 -10
  33. package/dist/contracts/flow.d.ts +50 -0
  34. package/dist/contracts/flow.js +26 -1
  35. package/dist/contracts/lead-tools.d.ts +0 -13
  36. package/dist/contracts/lead-tools.js +3 -3
  37. package/dist/contracts/store-json.d.ts +1 -25
  38. package/dist/contracts/store-json.js +3 -13
  39. package/dist/converse.d.ts +1 -11
  40. package/dist/converse.js +6 -38
  41. package/dist/coordinator.d.ts +0 -1
  42. package/dist/coordinator.js +3 -5
  43. package/dist/database-reclaim.d.ts +4 -0
  44. package/dist/database-reclaim.js +13 -0
  45. package/dist/demo.d.ts +1 -1
  46. package/dist/demo.js +17 -27
  47. package/dist/desktop-update-gate.js +0 -1
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/desktop-update.js +1 -3
  50. package/dist/dispatch.d.ts +1 -1
  51. package/dist/dispatch.js +1 -1
  52. package/dist/dispose.d.ts +2 -3
  53. package/dist/dispose.js +17 -92
  54. package/dist/envelope.js +0 -3
  55. package/dist/exec.d.ts +0 -64
  56. package/dist/exec.js +3 -356
  57. package/dist/exhaustion.d.ts +22 -76
  58. package/dist/exhaustion.js +14 -140
  59. package/dist/export.js +4 -4
  60. package/dist/flow-engine.js +1 -1
  61. package/dist/flow-schedule.d.ts +154 -0
  62. package/dist/flow-schedule.js +321 -0
  63. package/dist/flow-share.js +1 -1
  64. package/dist/flow-triggers.d.ts +6 -1
  65. package/dist/flow-triggers.js +65 -10
  66. package/dist/flows-cli.js +8 -4
  67. package/dist/flows-ui.js +2 -0
  68. package/dist/flows.js +1 -1
  69. package/dist/guides.js +9 -29
  70. package/dist/invoke.d.ts +1 -24
  71. package/dist/invoke.js +17 -259
  72. package/dist/lead-status.js +2 -4
  73. package/dist/lead-voice.js +1 -1
  74. package/dist/live-bus.d.ts +3 -3
  75. package/dist/live-bus.js +3 -3
  76. package/dist/mate-doors.d.ts +1 -2
  77. package/dist/mate-doors.js +12 -25
  78. package/dist/mate-tools.d.ts +2 -2
  79. package/dist/mate-tools.js +10 -22
  80. package/dist/modes.d.ts +6 -7
  81. package/dist/modes.js +7 -13
  82. package/dist/monitoring.js +32 -8
  83. package/dist/notification-delivery.d.ts +19 -0
  84. package/dist/notification-delivery.js +37 -0
  85. package/dist/operate-remote-arguments.js +2 -6
  86. package/dist/operate-remote.js +6 -7
  87. package/dist/operate.d.ts +0 -6
  88. package/dist/operate.js +283 -2378
  89. package/dist/phase-routing.d.ts +13 -23
  90. package/dist/phase-routing.js +13 -35
  91. package/dist/plan-auto.js +2 -2
  92. package/dist/planner-source.js +2 -1
  93. package/dist/policy.d.ts +1 -4
  94. package/dist/policy.js +0 -8
  95. package/dist/process-recovery-eligibility.js +2 -3
  96. package/dist/process-recovery-settlement.js +1 -0
  97. package/dist/project-delete.js +3 -15
  98. package/dist/proposal.d.ts +4 -24
  99. package/dist/proposal.js +3 -88
  100. package/dist/provider-auth.d.ts +3 -3
  101. package/dist/provider-auth.js +4 -6
  102. package/dist/provider.d.ts +11 -25
  103. package/dist/provider.js +4 -36
  104. package/dist/push.js +1 -1
  105. package/dist/recipe-ui.js +6 -6
  106. package/dist/recipes.d.ts +3 -3
  107. package/dist/recipes.js +27 -23
  108. package/dist/retention.js +6 -5
  109. package/dist/run-closeout.d.ts +1 -1
  110. package/dist/run-closeout.js +1 -4
  111. package/dist/scope.d.ts +11 -65
  112. package/dist/scope.js +17 -82
  113. package/dist/serve.js +16 -25
  114. package/dist/server/chat.js +7 -30
  115. package/dist/server/flows.js +11 -10
  116. package/dist/server/guards.js +0 -1
  117. package/dist/server/pages-assets.js +1 -1
  118. package/dist/server/pages.js +0 -2
  119. package/dist/server/people-tokens.js +0 -4
  120. package/dist/server/route-table.d.ts +1 -1
  121. package/dist/server/route-table.js +2 -17
  122. package/dist/server/settings.js +4 -9
  123. package/dist/server/shared.d.ts +29 -212
  124. package/dist/server/shared.js +73 -780
  125. package/dist/server/task-views.js +9 -76
  126. package/dist/server/tasks.d.ts +1 -50
  127. package/dist/server/tasks.js +36 -946
  128. package/dist/setup-guide.d.ts +2 -2
  129. package/dist/slack-chat.d.ts +0 -2
  130. package/dist/slack-chat.js +0 -1
  131. package/dist/store.d.ts +142 -1382
  132. package/dist/store.js +900 -4083
  133. package/dist/surface.js +10 -25
  134. package/dist/task-composer.d.ts +1 -1
  135. package/dist/task-composer.js +1 -1
  136. package/dist/task-control.d.ts +3 -10
  137. package/dist/task-control.js +2 -7
  138. package/dist/teammate-desk.js +1 -1
  139. package/dist/teammates-ui.js +1 -1
  140. package/dist/telegram-mate.d.ts +0 -3
  141. package/dist/telegram-mate.js +0 -1
  142. package/dist/templates.d.ts +11 -7
  143. package/dist/templates.js +12 -9
  144. package/dist/toolroll-update.d.ts +24 -3
  145. package/dist/toolroll-update.js +159 -16
  146. package/dist/webhooks.d.ts +10 -22
  147. package/dist/webhooks.js +43 -81
  148. package/dist/work-index.js +3 -7
  149. package/dist/workspace-revision.d.ts +28 -1
  150. package/dist/workspace-revision.js +276 -49
  151. package/dist/workspace-ui.js +1 -1
  152. package/package.json +1 -3
  153. package/scripts/recovery-canary.mjs +3 -3
  154. package/dist/contest.d.ts +0 -318
  155. package/dist/contest.js +0 -753
  156. package/dist/held.d.ts +0 -150
  157. package/dist/held.js +0 -799
  158. package/dist/liveness.d.ts +0 -30
  159. package/dist/liveness.js +0 -42
  160. package/dist/pricing.d.ts +0 -45
  161. package/dist/pricing.js +0 -77
  162. package/dist/routine.d.ts +0 -228
  163. package/dist/routine.js +0 -764
  164. package/dist/supervisor.mjs +0 -297
@@ -28,7 +28,6 @@ import {} from '../cli-http.js';
28
28
  import { CODING_SHIPPING_CSS } from '../coding-shipping-ui.js';
29
29
  import { CODING_CSS } from '../coding-ui.js';
30
30
  import { CodingWorkspace } from '../coding-workspace.js';
31
- import { contestNoun } from "../contest.js";
32
31
  import {} from "../contracts/console-api.js";
33
32
  import { connectionWords, hiddenFields } from "../control-ui.js";
34
33
  import { isSubscriptionChatProvider, PRICED_MODELS } from "../converse.js";
@@ -61,7 +60,6 @@ import { ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "../lead-about.js";
61
60
  import { LEAD_CONTEXT_CSS } from '../lead-context.js';
62
61
  import { LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "../lead-identity.js";
63
62
  import { LIMITS_CSS, limitsHtml } from "../limits-ui.js";
64
- import { attendedLivenessState } from "../liveness.js";
65
63
  import { PROPOSAL_WAIT_REASON, proposalActGate } from "../mate-doors.js";
66
64
  import { MATE_MESSAGE_MAX_CHARS } from "../mate.js";
67
65
  import { MOBILE_VIEWPORT_SCRIPT } from "../mobile-viewport.js";
@@ -73,17 +71,16 @@ import { cloneGithubRepo, listGithubRepos, previewGithubRepo } from "../onboard.
73
71
  import {} from "../operate-remote.js";
74
72
  import { parseBaseTreeSnapshot } from "../peek.js";
75
73
  import { PEOPLE_AUDIT_CSS } from "../people-audit-ui.js";
76
- import { agentsSummary, chosenWords, isRiskLevel, makesNoPlan, postureWords, projectRoute, RISK_CHOICES, RISK_LEVELS, riskConsequence, riskTitle, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
74
+ import { agentsSummary, chosenWords, makesNoPlan, postureWords, projectRoute, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
77
75
  import { parseExecutionPlanDocument } from "../plan.js";
78
76
  import {} from "../planner-source.js";
79
77
  import { POLICY_CSS } from "../policy-ui.js";
80
- import { PRICED_BUILD_MODELS } from "../pricing.js";
81
78
  import { PROJECT_DELETE_CSS } from "../project-delete-ui.js";
82
79
  import { projectName } from "../project.js";
83
80
  import { coverageStateWords, coverageWords, dispatchStatusToken, GOAL_ASSESSMENT_PENDING, manualReviewCriterionOf, manualReviewOnly, passFraction, personCheckWords, plainReasonWords, reviewConflict, semanticCoverage } from "../proof.js";
84
81
  import { providerName } from "../provider-auth.js";
85
82
  import {} from "../provider-connection.js";
86
- import { PROVIDER_IDS } from "../provider.js";
83
+ import {} from "../provider.js";
87
84
  import {} from '../pull-request-flow.js';
88
85
  import { PULL_REQUEST_SETTINGS_CSS } from '../pull-request-ui.js';
89
86
  import { qualityModeTitle } from "../quality.js";
@@ -98,9 +95,9 @@ import { evidenceHealthOf, evidenceProblemDetailsOf, evidenceResultStatusOf, evi
98
95
  import { RESULT_SHOT_CHOICES } from "../result-shots.js";
99
96
  import { RETENTION_CSS } from "../retention-ui.js";
100
97
  import { findingWords } from "../review-switch.js";
101
- import { describeSchedule, parseSchedule, routineAgentsState } from "../routine.js";
98
+ import { describeSchedule, parseSchedule } from "../flow-schedule.js";
102
99
  import { isAlive as runnerAlive } from "../runner.js";
103
- import { acceptanceToLines, approvalOf, chainFromJson, scopeAuthorityOf } from "../scope.js";
100
+ import { acceptanceToLines, approvalOf, scopeAuthorityOf } from "../scope.js";
104
101
  import { parseReport } from "../scout-report.js";
105
102
  import { ASSISTANTS } from "../setup-guide.js";
106
103
  import { SKILLS_CSS } from "../skills-ui.js";
@@ -711,7 +708,7 @@ export function approvalAmendmentHtml(view) {
711
708
  * post to the same route, and the task page and chat card share it.
712
709
  */
713
710
  export function approvalSheetHtml(input) {
714
- const { scope, route, raceTerms } = input;
711
+ const { scope, route } = input;
715
712
  // A machine-drafted repair appends its brief to the signed goal; the row
716
713
  // shows the goal and one sentence says what the repair fixes. The whole
717
714
  // signed goal stays in Details.
@@ -742,14 +739,12 @@ export function approvalSheetHtml(input) {
742
739
  const legOf = (phase) => legs.find(one => one.phase === phase);
743
740
  const who = [];
744
741
  const builder = legOf("build");
745
- if (raceTerms === null) {
746
- if (builder !== undefined)
747
- who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
748
- else if (route?.legacy != null)
749
- who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
750
- else if (scope.profile != null)
751
- who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
752
- }
742
+ if (builder !== undefined)
743
+ who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
744
+ else if (route?.legacy != null)
745
+ who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
746
+ else if (scope.profile != null)
747
+ who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
753
748
  const planner = legOf("plan");
754
749
  // A small change makes no plan: no planner is named (a person's chosen planner still is).
755
750
  const sized = route?.projection?.size ?? null;
@@ -762,26 +757,17 @@ export function approvalSheetHtml(input) {
762
757
  const allowed = permissionPlainWords(scope.profile);
763
758
  if (allowed !== null)
764
759
  allowing.push(allowed);
765
- if (raceTerms === null)
766
- allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
760
+ allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
767
761
  const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
768
762
  if (earlier !== null)
769
763
  allowing.push(earlier);
770
764
  const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
771
765
  const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
772
766
  (yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
773
- const race = raceTerms === null
774
- ? ""
775
- : `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
776
- (raceTerms.kind === "comparison"
777
- ? "No dollar cap; you compare the results and pick one.</p>"
778
- : `Each may spend ${money(raceTerms.perAgentBudgetMicrousd)} plus a ${money(raceTerms.overrunReserveMicrousd)} reserve, ${money(raceTerms.totalBudgetMicrousd)} in all; you compare the results and pick one.</p>`);
779
767
  const after = input.deliverable === "report"
780
768
  ? "An agent investigates without changing the repository. You'll hear when its report is ready."
781
- : raceTerms !== null
782
- ? "Each agent starts in its own branch. You'll hear when the results are ready to compare."
783
- : "An agent starts in its own branch. You'll hear when it's ready to review.";
784
- const submit = raceTerms === null ? "Approve & start" : raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament";
769
+ : "An agent starts in its own branch. You'll hear when it's ready to review.";
770
+ const submit = "Approve & start";
785
771
  // Everything else the digest binds, one tap away.
786
772
  const detail = (title, body, attrs = "") => body === "" ? "" : `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
787
773
  const revisionDetail = input.revision === null ? "" : detail("Earlier build", `<div class="revision-card" data-revision-feedback>` +
@@ -802,7 +788,7 @@ export function approvalSheetHtml(input) {
802
788
  : projection === null
803
789
  ? route.legacy === null ? "" : `<p>${escape(agentsSummaryWords(route))}</p>`
804
790
  : `<p>${escape(projection.summary)}</p>` +
805
- `<p class="meta">${escape(agentsRiskWords(projection))}. Uses ${escape(projection.postureWords)}.</p>` +
791
+ `<p class="meta">Uses ${escape(projection.postureWords)}.</p>` +
806
792
  (projection.demands.length === 0 ? "" : `<ul class="meta">${projection.demands.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`) +
807
793
  `<dl class="approval-roles">${projection.legs.map(leg => `<div><dt>${escape(ROLE_NOUN[leg.phase])}</dt><dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="meta">${escape(chosenWords(leg))}</span>` +
808
794
  `<ul>${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd></div>`).join("")}</dl>` +
@@ -884,7 +870,6 @@ export function approvalSheetHtml(input) {
884
870
  row("Done when", doneWhen) +
885
871
  `</dl>` +
886
872
  approvalAmendmentHtml(contract) +
887
- race +
888
873
  // The size, said once and plainly beside who builds: "Small change: fast model, no plan".
889
874
  (projection === null ? "" : sizeLineHtml(projection)) +
890
875
  (who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
@@ -1013,17 +998,7 @@ export function profileWords(scope) {
1013
998
  : profile.provider === "gemini"
1014
999
  ? `<p class="meta">Runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
1015
1000
  : `<p class="meta">Runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
1016
- // The fallback chain rides EVERY surface these words sign (F+G review,
1017
- // finding 2): the digest binds the whole chain, so the password form —
1018
- // task page and /next alike — states every entry, credential included.
1019
- const chain = chainFromJson(scope.proposedChainJson ?? null);
1020
- const chainLine = chain === null || chain.length < 2
1021
- ? ""
1022
- : `<p class="meta">If its subscription runs out: ${chain
1023
- .slice(1)
1024
- .map(one => `falls back to <span class="mono">${escape(one.profile.provider)} · ${escape(one.profile.model)}</span> — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
1025
- .join("; ")}</p>`;
1026
- return base + chainLine;
1001
+ return base;
1027
1002
  }
1028
1003
  export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
1029
1004
  export function agentsStandingWords(view) {
@@ -1060,16 +1035,15 @@ export function agentsSummaryWords(view) {
1060
1035
  return `${view.legacy.provider} · ${view.legacy.model} builds and repairs (repair model ${view.legacy.repairModel}); the planner and reviewer come from configuration at run time`;
1061
1036
  return view.problem ?? "the agents cannot be read";
1062
1037
  }
1063
- export function agentsRiskWords(projection) {
1064
- const consequence = projection.risk === "routine" && projection.size?.risky
1065
- ? riskConsequence("elevated")
1066
- : riskConsequence(projection.risk);
1067
- return `${projection.riskTitle}: ${consequence}`;
1038
+ /** A risk badge only when there is something to say: a risky change, or an
1039
+ * older route sealed at elevated or high risk. */
1040
+ export function riskBadgeHtml(projection) {
1041
+ return projection.riskTitle === "Routine" ? "" : `<span class="badge">${escape(sentenceCase(projection.riskTitle))}</span>`;
1068
1042
  }
1069
- /** Neutral metadata badges: risk, posture, and standing. */
1043
+ /** Neutral metadata badges: risk (when not routine), posture, and standing. */
1070
1044
  export function agentsBadgesHtml(view) {
1071
1045
  const p = view.projection;
1072
- return (`<span class="badge">${escape(sentenceCase(p?.riskTitle ?? riskTitle(view.riskLevel)))}</span>` +
1046
+ return ((p === null ? "" : riskBadgeHtml(p)) +
1073
1047
  (p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
1074
1048
  `<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
1075
1049
  }
@@ -1118,39 +1092,12 @@ export function agentsCeremonyHtml(view) {
1118
1092
  return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
1119
1093
  sizeLineHtml(view.projection) +
1120
1094
  `<p class="agents-summary">${escape(view.projection.summary)}</p>` +
1121
- `<div class="agents-badges"><span class="badge">${escape(sentenceCase(view.projection.riskTitle))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
1122
- `<p class="meta agents-risk-line">${escape(agentsRiskWords(view.projection))}.</p>` +
1095
+ `<div class="agents-badges">${riskBadgeHtml(view.projection)}<span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
1123
1096
  agentsWhyHtml(view.projection) +
1124
1097
  `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
1125
1098
  }
1126
- /** A standing order's agents (v48): the four-role route its approval
1127
- * froze (or will freeze), in the same concise block a task's ceremony
1128
- * shows — a firing can never be re-routed by a later configuration
1129
- * change, and the page says so. */
1130
- export function routineAgentsHtml(routine, approved, ceremony = false) {
1131
- const agents = routineAgentsState(routine);
1132
- const route = (approved ? routine.approvedRoute : routine.route) ?? null;
1133
- if (route === null || agents.state !== "frozen" && agents.state !== "pending") {
1134
- // The closed door, with its one road: no agents to restate means no
1135
- // yes to give — the words say which fact closed it and what opens it.
1136
- const words = agents.state === "unreadable"
1137
- ? `the agents on file cannot be read back (${agents.problem ?? "corrupt snapshot"}) — refresh the agents from today's configuration, read them, and approve this standing order again; until then nothing fires`
1138
- : agents.state === "unfrozen"
1139
- ? "approved before agents were frozen — refresh the agents, read the exact planner, builder, repair, and reviewer it then names, and approve this standing order again; until then nothing fires"
1140
- : agents.state === "unverified"
1141
- ? `the frozen agents do not verify (${agents.problem ?? "the approved snapshot is not whole"}) — refresh the agents from today's configuration, read them, and approve this standing order again; until then nothing fires`
1142
- : `not resolved — ${agents.problem ?? "this standing order does not name an exact agent for every role"}; configure the project's agents, then refresh and approve again`;
1143
- return `<div class="agents-ceremony agents-closed"><p class="approval-label">agents</p><p class="agents-summary">${escape(words)}</p></div>`;
1144
- }
1145
- const projection = projectRoute(route, () => null);
1146
- return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
1147
- `<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
1148
- `<div class="agents-badges"><span class="badge">${escape(sentenceCase(projection.riskTitle))}</span><span class="badge">${escape(sentenceCase(postureWords(route)))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
1149
- (ceremony ? agentsWhyHtml(projection) : "") +
1150
- `<p class="meta">${approved ? "Every firing runs on exactly these agents; a configuration change cannot re-route it." : "Approving freezes exactly these agents for every firing; a configuration change afterwards cannot re-route one."}</p></div>`);
1151
- }
1152
1099
  /** The runtime limits the sealed profile binds — permissions, turn and
1153
- * time bounds, repairs, the fallback chain — restated on the ceremony as
1100
+ * time bounds, repairs — restated on the ceremony as
1154
1101
  * a CLOSED disclosure (v48): a term the yes covers, one tap away, never a
1155
1102
  * wall of switches between the reader and the password. An unresolved
1156
1103
  * profile still speaks in the open: that is a refusal, not a detail. */
@@ -1178,11 +1125,10 @@ export function agentsCardHtml(taskId, view, csrf, canEdit) {
1178
1125
  (canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">Clear</button></form>` : "") +
1179
1126
  `</li>`)
1180
1127
  .join("")}</ul>`;
1181
- // The controls (v48): a risk choice that says what each level does, and
1128
+ // The controls (v48): a size choice that says what each size does, and
1182
1129
  // — per role — ONLY the configured, role-valid agents the operator may
1183
1130
  // pick from. Nothing is typed free-hand, no command line is quoted; the
1184
1131
  // reasons above already say why the current agents were chosen.
1185
- const riskGuide = `<dl class="agents-risk-guide">${RISK_CHOICES.map(one => `<div><dt>${escape(one.title)}</dt><dd>${escape(one.consequence)}</dd></div>`).join("")}</dl>`;
1186
1132
  const roleForms = ROUTE_PHASES.filter(phase => phase !== "review").map(phase => {
1187
1133
  // Selectable choices are the role's own configured agents; a current
1188
1134
  // agent the configuration no longer names is DISPLAY-ONLY — said
@@ -1204,15 +1150,11 @@ export function agentsCardHtml(taskId, view, csrf, canEdit) {
1204
1150
  : !view.editable
1205
1151
  ? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
1206
1152
  : `<details class="agents-change"><summary>Change agents</summary>` +
1207
- `<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
1208
- `<label>Risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
1209
- `<button type="submit" class="secondary">Set risk</button></form>` +
1210
1153
  `<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
1211
1154
  `<label>Size<select name="size" aria-label="task size">${TASK_SIZES.map(one => `<option value="${one}"${one === (p?.size?.size ?? "medium") ? " selected" : ""}>${escape(sizeConsequence(one, false))}</option>`).join("")}</select></label>` +
1212
1155
  // The hidden "no" says the form showed the box: unticked means not risky; a request without either keeps the flag.
1213
1156
  `<input type="hidden" name="risky" value="no"><label class="agents-risky"><input type="checkbox" name="risky" value="yes"${p?.size?.risky === true ? " checked" : ""}> Risky</label>` +
1214
1157
  `<button type="submit" class="secondary">Set size</button></form>` +
1215
- riskGuide +
1216
1158
  `<div class="agents-role-forms">${roleForms.join("")}</div>` +
1217
1159
  `<p class="meta">Only agents you have configured are offered; each choice is recorded as you. ${view.replanOnPlanChange ? "Changing the planner asks for a new plan — the drafted one is not relabeled. " : ""}An approval given under the earlier agents needs renewing.</p>` +
1218
1160
  overrides +
@@ -1986,8 +1928,6 @@ ${THEME_DARK}
1986
1928
  .filters strong { background: var(--muted); color: var(--foreground); }
1987
1929
  @media (hover: hover) and (pointer: fine) { .filters a:hover { color: var(--foreground); } }
1988
1930
 
1989
- /* A picked tournament result: marked in the built green, not magenta. */
1990
- .card.picked { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }
1991
1931
  .seal {
1992
1932
  display: inline-block; font-family: var(--font-mono); font-size: .75rem;
1993
1933
  background: var(--muted); border: 1px solid var(--border);
@@ -2483,13 +2423,8 @@ ${THEME_DARK}
2483
2423
  .agents-clear button { padding-inline: .8rem; font-size: .72rem; }
2484
2424
  .agents-ceremony { margin: .5rem 0 0; }
2485
2425
  .agents-ceremony .agents-badges { margin-top: .35rem; }
2486
- .agents-risk-line { margin: .4rem 0 0; }
2487
2426
  .agents-runtime { margin-top: .5rem; }
2488
2427
  .agents-runtime > summary { display: flex; align-items: center; min-height: 2.75rem; padding: .35rem .2rem; cursor: pointer; font-size: .8rem; font-weight: 600; }
2489
- .agents-risk-guide { display: grid; gap: .3rem; margin: .5rem 0 0; font-size: .76rem; line-height: 1.45; }
2490
- .agents-risk-guide div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; }
2491
- .agents-risk-guide dt { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
2492
- .agents-risk-guide dd { margin: 0; color: var(--muted-foreground); overflow-wrap: anywhere; }
2493
2428
  .agents-role-forms { display: grid; gap: .45rem; margin-top: .6rem; }
2494
2429
  .agents-role-row { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; align-items: center; }
2495
2430
  .agents-role-name { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
@@ -2517,7 +2452,7 @@ ${THEME_DARK}
2517
2452
  .approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
2518
2453
  .approval-sheet.approve-form > :not(input) { margin: 0; }
2519
2454
  .approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
2520
- .approval-revision, .approval-note, .approval-race { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
2455
+ .approval-revision, .approval-note { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
2521
2456
  .approval-note { color: var(--muted-foreground); }
2522
2457
  .approval-rows { display: grid; gap: 14px; margin: 0; }
2523
2458
  .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
@@ -2686,7 +2621,6 @@ ${THEME_DARK}
2686
2621
  }
2687
2622
  details.section[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }
2688
2623
  @media (hover: hover) and (pointer: fine) { details.section > summary:hover h2 { color: var(--foreground); } }
2689
- details.more-agents { margin: .75rem 0 0; }
2690
2624
  .task-layout > .task-main { min-width: 0; }
2691
2625
  .task-rail { position: sticky; top: 1rem; }
2692
2626
  .task-rail .card { margin-top: .75rem; }
@@ -3812,7 +3746,7 @@ ${THEME_DARK}
3812
3746
  .task-options-grid .wide, .task-options-grid .permission-field { grid-column: auto; }
3813
3747
  .agents-form { grid-template-columns: 1fr; }
3814
3748
  .agents-roles { grid-template-columns: 1fr; }
3815
- .agents-risk-guide div, .agents-role-row { grid-template-columns: 1fr; }
3749
+ .agents-role-row { grid-template-columns: 1fr; }
3816
3750
  .approval-card { padding: 1rem; }
3817
3751
  /* The orientation block says the wait on a phone; the kicker would say it twice. */
3818
3752
  .approval-card .approval-kicker { display: none; }
@@ -4059,19 +3993,6 @@ button { min-height: 44px; }
4059
3993
  }
4060
3994
  }
4061
3995
 
4062
- /* The tournament comparison: an at-a-glance table and side-by-side cards. */
4063
- .scroll-x { overflow-x: auto; max-width: 100%; }
4064
- .contest-glance { border-collapse: collapse; font-size: .8125rem; min-width: 34rem; margin: .75rem 0; }
4065
- .contest-glance th, .contest-glance td { text-align: left; padding: .375rem .75rem .375rem 0; vertical-align: top; border-bottom: 1px solid var(--border); }
4066
- .contest-glance th { font-weight: 600; }
4067
- .contest-glance tr:last-child td { border-bottom: none; }
4068
- .contest-compare { display: grid; grid-template-columns: 1fr; gap: .75rem; align-items: start; }
4069
- @media (min-width: 1100px) {
4070
- .content > main:has(.contest-compare) { max-width: none; }
4071
- .contest-compare { grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr)); }
4072
- }
4073
- .contest-compare .card { margin: 0; }
4074
-
4075
3996
  /* The per-file comment button: a small real button beside a diff row. */
4076
3997
  button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
4077
3998
 
@@ -4221,22 +4142,11 @@ export function transcriptScript(path, elementId = "live-transcript") {
4221
4142
  `function later(){setTimeout(go,2000);}go();})();`);
4222
4143
  }
4223
4144
  /**
4224
- * The attended chrome layer: the jump palette and elapsed tickers. Pure
4145
+ * The chrome layer: the jump palette and elapsed tickers. Pure
4225
4146
  * navigation — no key ever posts, so the palette cannot approve anything;
4226
4147
  * ceremonies stay POST + password + CSRF, untouched. Reads its index from
4227
4148
  * a non-executable JSON script tag rendered by the same authorized page.
4228
4149
  */
4229
- /** The attended beat (v28): parameterless, cookie + same-origin proven
4230
- * server-side, renewal-only — any console page keeps the signed-in
4231
- * approver's own sessions live; a hidden tab pauses honestly. Cheap
4232
- * no-op when nothing is open. Shipped ALONE on sensitive pages. */
4233
- export function beatScript(enabled = true) {
4234
- if (!enabled)
4235
- return "";
4236
- return (`(function(){var beat=function(){if(document.hidden)return;` +
4237
- `fetch("/session/attended-beats",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""}).catch(function(){});};` +
4238
- `beat();setInterval(beat,15000);})();`);
4239
- }
4240
4150
  /** The rail's presentation-only state is safe beside a password ceremony:
4241
4151
  * it never reads a field or sends a request, and keeps the chat collapsible
4242
4152
  * from its very first screen while the palette and global keys remain absent. */
@@ -4250,9 +4160,8 @@ export function sidebarScript() {
4250
4160
  `setSide(sideCollapsed);sideToggle.addEventListener("click",function(){var next=!app.classList.contains("sidebar-collapsed");setSide(next);` +
4251
4161
  `try{localStorage.setItem("standing-orders:sidebar-collapsed",next?"1":"0");}catch(e){}});}})();`);
4252
4162
  }
4253
- export function chromeScript(beats = true) {
4254
- return (beatScript(beats) +
4255
- sidebarScript() +
4163
+ export function chromeScript() {
4164
+ return (sidebarScript() +
4256
4165
  `(function(){` +
4257
4166
  // The app-icon badge (Phase 2E): the page's server-rendered waiting
4258
4167
  // count is authoritative over any stale push — synced on every chrome
@@ -4338,7 +4247,7 @@ export function chromeScript(beats = true) {
4338
4247
  `if(ev.key==="?"){toggleHelp();ev.preventDefault();return;}` +
4339
4248
  `if(ev.key==="j"||ev.key==="k"){rove(ev.key==="j"?1:-1,ev);return;}` +
4340
4249
  `if(pending==="g"){pending=null;` +
4341
- `var map={b:"/board",i:"/",w:"/workbench",r:"/routines",d:"/done",q:"/board?view=order",f:"/fleet",t:"/tasks",a:"/activity",p:"/projects"};` +
4250
+ `var map={b:"/board",i:"/",w:"/workbench",d:"/done",q:"/board?view=order",f:"/fleet",t:"/tasks",a:"/activity",p:"/projects"};` +
4342
4251
  `if(map[ev.key]){go(map[ev.key]);ev.preventDefault();}return;}` +
4343
4252
  `if(ev.key==="g"){pending="g";setTimeout(function(){pending=null;},800);}});` +
4344
4253
  `})();`);
@@ -4376,7 +4285,6 @@ export const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="
4376
4285
  `<tr><td><kbd>g</kbd> then <kbd>q</kbd></td><td>go to the queue</td></tr>` +
4377
4286
  `<tr><td><kbd>g</kbd> then <kbd>f</kbd></td><td>go to the fleet</td></tr>` +
4378
4287
  `<tr><td><kbd>g</kbd> then <kbd>w</kbd></td><td>go to the workbench</td></tr>` +
4379
- `<tr><td><kbd>g</kbd> then <kbd>r</kbd></td><td>go to the routines</td></tr>` +
4380
4288
  `<tr><td><kbd>g</kbd> then <kbd>t</kbd></td><td>go to the task list</td></tr>` +
4381
4289
  `<tr><td><kbd>g</kbd> then <kbd>a</kbd></td><td>go to the activity view</td></tr>` +
4382
4290
  `<tr><td><kbd>g</kbd> then <kbd>p</kbd></td><td>go to the projects</td></tr>` +
@@ -4903,9 +4811,8 @@ export function systemPage(chrome, data) {
4903
4811
  const dot = age < 5 * 60_000 ? "dot-ok" : age < 60 * 60_000 ? "dot-warn" : "dot-off";
4904
4812
  const said = age < 5 * 60_000 ? "alive" : age < 60 * 60_000 ? `quiet ${Math.round(age / 60_000)}m` : "not heard from";
4905
4813
  const busy = data.building.filter(claim => claim.runner === one.name).length;
4906
- const sessions = data.heldSessions?.get(one.name) ?? 0;
4907
4814
  return (`<div class="stat-card"><span class="k"><span class="dot ${dot}"></span>${escape(one.name)}</span>` +
4908
- `<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building${sessions > 0 ? ` \u00b7 ${sessions} attended session${sessions === 1 ? "" : "s"} (uncapped by Toolroll — each is an agent + a supervisor process; OS limits apply)` : ""}</span></div>`);
4815
+ `<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building</span></div>`);
4909
4816
  });
4910
4817
  const worktreeCards = data.worktrees.map(tree => {
4911
4818
  const leased = tree.leasedAt !== null && tree.releasedAt === null;
@@ -4948,7 +4855,7 @@ export function systemPage(chrome, data) {
4948
4855
  const agentsCard = `<h2>Agents</h2>` +
4949
4856
  `<p class="meta">Which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
4950
4857
  `<div class="card">${agentLines}` +
4951
- `<p class="meta">Repair always stays on the provider that built — only its model can differ. A routine pins its agent the moment it fires; nothing after that can re-route it.</p>` +
4858
+ `<p class="meta">Repair always stays on the provider that built — only its model can differ. A schedule's task filed under an approval moved from a routine is pinned to the agents approved then.</p>` +
4952
4859
  `</div>`;
4953
4860
  return screen("system", [
4954
4861
  `<h1>System</h1>`,
@@ -5023,7 +4930,7 @@ export function boardBody(data, ciRed) {
5023
4930
  };
5024
4931
  // The card's facts: mono key–value pairs under the title (board pass) —
5025
4932
  // task, worker, runtime — the same grammar on every lane, so the eye
5026
- // learns one card. Chips carry the words (project, routine, reservation).
4933
+ // learns one card. Chips carry the words (project, reservation).
5027
4934
  const facts = (rows) => rows.length === 0
5028
4935
  ? ""
5029
4936
  : `<span class="facts">${rows
@@ -5041,7 +4948,6 @@ export function boardBody(data, ciRed) {
5041
4948
  ...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince)]]),
5042
4949
  ]) +
5043
4950
  chips([
5044
- card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
5045
4951
  chip(card.repo).trim(),
5046
4952
  ]) +
5047
4953
  `</a>`;
@@ -5070,7 +4976,6 @@ export function boardBody(data, ciRed) {
5070
4976
  ? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
5071
4977
  : "",
5072
4978
  card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
5073
- card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
5074
4979
  chip(card.repo).trim(),
5075
4980
  ]) +
5076
4981
  `</a>`;
@@ -5136,15 +5041,6 @@ export function boardBody(data, ciRed) {
5136
5041
  ` \u2014 every project this server serves, each card wearing its project`
5137
5042
  : `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
5138
5043
  `</p>`;
5139
- // The tracks: standing orders as rows under the lanes \u2014 the heartbeat
5140
- // below the one-off pipeline. Only routines with something to say render;
5141
- // the full list lives at /routines.
5142
- const tracksSection = data.tracks.length === 0
5143
- ? ""
5144
- : `<section class="tracks"><h2><a href="/routines">routines</a></h2>` +
5145
- `<p class="hint">each dot is one firing, oldest first \u2014 the full list is under routines</p>` +
5146
- data.tracks.map(track => trackRow(track, data.all)).join("\n") +
5147
- `</section>`;
5148
5044
  const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
5149
5045
  const deltaLine = data.delta === null
5150
5046
  ? ""
@@ -5169,7 +5065,6 @@ export function boardBody(data, ciRed) {
5169
5065
  lane("building", "building", "one agent per card, in its own workspace", building),
5170
5066
  `<details class="lane lane-done"${data.done.length === 0 ? "" : " open"}><summary><h2><a href="/done">done recently</a></h2></summary><p class="hint">the most recent \u2014 the full list is under done</p>${doneCards}</details>`,
5171
5067
  `</div>`,
5172
- tracksSection,
5173
5068
  ].join("\n");
5174
5069
  }
5175
5070
  /** Completed work: one row per done task, its final run and PR attached. */
@@ -5201,72 +5096,13 @@ export function donePage(chrome, rows, ciRed) {
5201
5096
  list,
5202
5097
  ].join("\n"), { chrome });
5203
5098
  }
5204
- export const routineHref = (id) => `/routines/${id}`;
5205
- export function routineStatus(routine) {
5206
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
5207
- if (routine.paused)
5208
- return { text: "paused", badge: "badge" };
5209
- if (!approved) {
5210
- return {
5211
- text: routine.approvedAt === null ? "awaiting approval" : "edited — approve again",
5212
- badge: "badge badge-failed",
5213
- };
5214
- }
5215
- // An approval that froze no readable agents is not live (v48): its
5216
- // firings wait until the agents are refreshed and approved again.
5217
- const agents = routineAgentsState(routine);
5218
- if (agents.state !== "frozen")
5219
- return { text: "agents not frozen — refresh and approve again", badge: "badge badge-failed" };
5220
- return { text: "live", badge: "badge badge-running" };
5221
- }
5222
- /**
5223
- * The run-history strip: the last firings as dots, oldest on the left like
5224
- * a CI history. Fired slots wear their instance's fate; skipped slots are
5225
- * hollow — recorded absence, not silence.
5226
- */
5227
- export function trackStrip(fires) {
5228
- const dots = [...fires].reverse().map(fire => {
5229
- const slot = fire.scheduledFor.replace(/^manual:/, "");
5230
- if (fire.outcome === "skipped") {
5231
- return `<span class="fire fire-skip" title="${escape(slot)} — skipped: ${escape(fire.reason ?? "")}"></span>`;
5232
- }
5233
- const state = fire.instanceState;
5234
- const kind = state === "done" ? "fire-ok" : state === "failed" || state === "cancelled" ? "fire-bad" : "fire-live";
5235
- const title = `${slot} — ${fire.instanceTaskId ?? "instance"}${state === null ? "" : ` (${state})`}`;
5236
- return fire.instanceTaskId === null
5237
- ? `<span class="fire ${kind}" title="${escape(title)}"></span>`
5238
- : `<a class="fire ${kind}" href="${taskHref(fire.instanceTaskId)}" title="${escape(title)}"></a>`;
5239
- });
5240
- return `<span class="track-strip">${dots.join("")}</span>`;
5241
- }
5242
- /** One track row — shared by the board's tracks section and /routines. */
5243
- export function trackRow(track, all) {
5244
- const { routine, fires, spend, blocker } = track;
5245
- const status = routineStatus(routine);
5246
- const schedule = parseSchedule(routine.schedule);
5247
- const latest = fires.find(fire => fire.outcome === "fired");
5248
- return (`<div class="card track-row">` +
5249
- `<p><a href="${routineHref(routine.id)}"><strong>${escape(routine.name)}</strong></a> ` +
5250
- `<span class="${status.badge}">${escape(status.text)}</span>` +
5251
- `${all ? ` <span class="badge">${escape(projectName(routine.repo))}</span>` : ""}` +
5252
- `<span class="right meta">${escape(schedule === null ? routine.schedule : describeSchedule(schedule))}</span></p>` +
5253
- `<p class="meta">${escape(routine.goal.length > 110 ? routine.goal.slice(0, 110) + "…" : routine.goal)}</p>` +
5254
- `<p>${trackStrip(fires)}` +
5255
- `<span class="right meta">$${spend.costUsd.toFixed(2)} this week${spend.unmeasuredRuns > 0 ? ` — measured on ${spend.totalRuns - spend.unmeasuredRuns} of ${spend.totalRuns} builds` : ""}${routine.costCeilingUsd === null ? "" : ` of $${routine.costCeilingUsd.toFixed(2)}`}</span></p>` +
5256
- (blocker !== null
5257
- ? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
5258
- : latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
5259
- ? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
5260
- : "") +
5261
- `</div>`);
5262
- }
5263
5099
  export function chatMoney(microusd) {
5264
5100
  return microusd === null ? "unknown" : `$${(microusd / 1_000_000).toFixed(2)}`;
5265
5101
  }
5266
- /** The browser's one-click approval binds not only the signed scope but the
5267
- * exact advisory plan revision and any race terms shown beside it. The scope
5268
- * digest remains the durable authority; this composite makes a stale open
5269
- * tab fail when somebody edits the plan before approval. */
5102
+ /** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
5103
+ export function nonceHashOf(value) {
5104
+ return createHash("sha256").update(value).digest("hex");
5105
+ }
5270
5106
  /** The approve POST's refusal, in words a person can act on. */
5271
5107
  export function approveRefusalWords(reason) {
5272
5108
  if (reason === "requester" || reason === "person-required")
@@ -5277,14 +5113,19 @@ export function approveRefusalWords(reason) {
5277
5113
  return "not approved: the scope cannot name an exact agent for every role — fix the agent setup and re-file it";
5278
5114
  return `not approved: ${reason}`;
5279
5115
  }
5280
- export function approvalFormDigest(scopeDigest, raceDigest, planSha) {
5281
- if (raceDigest === null && planSha === null)
5116
+ /** The browser's one-click approval binds not only the signed scope but the
5117
+ * exact advisory plan revision shown beside it. The scope digest remains the
5118
+ * durable authority; this composite makes a stale open tab fail when somebody
5119
+ * edits the plan before approval. The empty middle slot keeps the bytes of
5120
+ * forms already open (it once held other terms, always empty here). */
5121
+ export function approvalFormDigest(scopeDigest, planSha) {
5122
+ if (planSha === null)
5282
5123
  return scopeDigest;
5283
5124
  return createHash("sha256")
5284
5125
  .update("standing-orders/browser-approval/v1\0", "utf8")
5285
5126
  .update(scopeDigest, "utf8")
5286
5127
  .update("\0", "utf8")
5287
- .update(raceDigest ?? "", "utf8")
5128
+ .update("", "utf8")
5288
5129
  .update("\0", "utf8")
5289
5130
  .update(planSha ?? "", "utf8")
5290
5131
  .digest("hex");
@@ -5431,7 +5272,7 @@ export function resumeCeremonyPage(chrome, data) {
5431
5272
  `<div class="card resume-ceremony">`,
5432
5273
  `<p class="row">Lifts this stop’s hold. Other holds stay in place.</p>`,
5433
5274
  `<p class="row">Continues saved work${control.committed ? " and its commit" : ""} under the current approved scope. The next attempt needs fresh evidence; the earlier handoff cannot count as a new result.</p>`,
5434
- `<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
5275
+ `<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, verification, and review limits still apply.</p>`,
5435
5276
  `<details><summary>Stop record and saved work</summary><p>Run #${control.run} was stopped by <span class="mono">${escape(control.stop.requestedBy)}</span> at ${escape(stopWhen(control.stop.requestedAt))}${control.stop.settledAt === null ? "" : ` and settled ${escape(stopWhen(control.stop.settledAt))} (${escape(control.stop.settlement ?? "?")})`}.</p>${control.worktree === null ? "" : `<p class="mono">${escape(control.worktree)}</p>`}</details>`,
5436
5277
  data.approved ? "" : `<p class="row problem-words"><strong>The scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
5437
5278
  data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
@@ -5505,12 +5346,11 @@ export function taskChatApproval(focus, csrf) {
5505
5346
  revision: approval.revision,
5506
5347
  revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
5507
5348
  repairChain: approval.repairChain,
5508
- raceTerms: approval.raceTerms,
5509
5349
  route: focus.route,
5510
5350
  coordinator: approval.coordinator,
5511
5351
  deliverable: approval.deliverable,
5512
5352
  // Edit plan opens the task page's sheet with its fields open for editing.
5513
- editHref: approval.raceTerms !== null ? `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}` : `${taskHref(focus.id)}?edit=plan#plan-editor`,
5353
+ editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
5514
5354
  notNowHref: "/chat",
5515
5355
  sticky: false,
5516
5356
  earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
@@ -5978,12 +5818,11 @@ export function chatPage(chrome, data) {
5978
5818
  for (const one of candidates) {
5979
5819
  const draft = one.draft;
5980
5820
  parts.push(`<div class="card">` +
5981
- `<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
5821
+ `<p><strong>${escape(draft.title)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
5982
5822
  `<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
5983
5823
  `<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
5984
5824
  (draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
5985
5825
  (draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
5986
- (draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
5987
5826
  `<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
5988
5827
  `<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
5989
5828
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
@@ -6173,11 +6012,10 @@ export function proposalCardParts(view, csrf, inert, decision, returnTo = null)
6173
6012
  }
6174
6013
  else if (view.kind === "agents") {
6175
6014
  // The agents card (v48): exactly what changes, in the same words the
6176
- // task page uses — the role, the exact agent, what the risk level
6177
- // does — and the consequence for the standing approval.
6015
+ // task page uses — the role, the exact agent, the size — and the
6016
+ // consequence for the standing approval.
6178
6017
  const taskTitle = text("taskTitle") || task;
6179
6018
  const role = text("role");
6180
- const risk = text("risk");
6181
6019
  const clear = payload["clear"] === true;
6182
6020
  const agentWords = text("provider") === "" ? "" : `${text("provider")} · ${text("model")}`;
6183
6021
  const size = text("size");
@@ -6186,15 +6024,13 @@ export function proposalCardParts(view, csrf, inert, decision, returnTo = null)
6186
6024
  ? `Run <a href="${taskHref(task)}">${escape(taskTitle)}</a>'s ${escape(role)} on <span class="mono">${escape(agentWords)}</span>`
6187
6025
  : role !== "" && clear
6188
6026
  ? `Let the recommended ${escape(role)} stand for <a href="${taskHref(task)}">${escape(taskTitle)}</a>`
6189
- : risk === "" && size !== ""
6027
+ : size !== ""
6190
6028
  ? `Treat <a href="${taskHref(task)}">${escape(taskTitle)}</a> as a ${escape(sizeLine)}`
6191
- : `Declare <a href="${taskHref(task)}">${escape(taskTitle)}</a> ${escape(riskTitle((isRiskLevel(risk) ? risk : "routine")).toLowerCase())}`;
6192
- const riskWords = isRiskLevel(risk) ? `${riskTitle(risk)}: ${riskConsequence(risk)}.` : "";
6029
+ : `Change the agents for <a href="${taskHref(task)}">${escape(taskTitle)}</a>`;
6193
6030
  what =
6194
6031
  `<h3>${heading}</h3>` +
6195
6032
  (text("why") === "" ? "" : `<p class="proposal-summary">${escape(text("why"))}</p>`) +
6196
- facts(["agents now", escape(text("before"))], ["risk", risk === "" ? "" : `<strong>${escape(riskTitle(risk))}</strong>${role !== "" && isRiskLevel(risk) ? "" : ` — ${escape(riskConsequence(risk))}`}`], ["size", size === "" ? "" : escape(sizeConsequence(size, payload["risky"] === true))], ["role", role === "" ? "" : `${escape(role)} → ${clear ? "the recommendation" : `<span class="mono">${escape(agentWords)}</span>`}`], ["project", `<span class="mono">${escape(repoId)}</span>`]) +
6197
- (role !== "" && riskWords !== "" ? `<p class="meta">${escape(riskWords)}</p>` : "") +
6033
+ facts(["agents now", escape(text("before"))], ["size", size === "" ? "" : escape(sizeConsequence(size, payload["risky"] === true))], ["role", role === "" ? "" : `${escape(role)} → ${clear ? "the recommendation" : `<span class="mono">${escape(agentWords)}</span>`}`], ["project", `<span class="mono">${escape(repoId)}</span>`]) +
6198
6034
  `<p class="meta proposal-disclosure">Recorded under your name when you confirm. ${text("approval") === "approved" ? "The current approval no longer covers the task afterwards — approve it again on the task." : "The next approval seals these agents."}</p>`;
6199
6035
  }
6200
6036
  else if (view.kind === "scope") {
@@ -6593,149 +6429,6 @@ export function matePage(chrome, data) {
6593
6429
  },
6594
6430
  });
6595
6431
  }
6596
- export function routinesPage(chrome, tracks, form) {
6597
- const fill = form.values === undefined ? form.prefill ?? null : { name: form.values.get("name") ?? "", goal: form.values.get("goal") ?? "", not: form.values.get("not") ?? "", touches: form.values.get("touches") ?? "", schedule: form.values.get("schedule") ?? "", acceptance: form.values.get("acceptance") ?? "" };
6598
- const capture = chrome.project === null
6599
- ? ""
6600
- : [
6601
- `<h2>File a standing order</h2>`,
6602
- form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
6603
- `<form method="post" action="/routines/add" class="card">`,
6604
- `<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
6605
- `<input type="hidden" name="projectRevision" value="${form.revision}">`,
6606
- fill === null
6607
- ? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
6608
- .map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
6609
- .join(" · ")}</p>`
6610
- : `<p class="meta">${form.values === undefined ? "pre-filled from a template — edit anything" : "Your entries are kept below"}; nothing fires until you approve the standing order</p>`,
6611
- `<label>Name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
6612
- `<label>Goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
6613
- `<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
6614
- `<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
6615
- `<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
6616
- scheduleEditorHtml(fill?.schedule ?? null, form.values),
6617
- `<label>Budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
6618
- `<button type="submit">File it \u2192 approve the standing order next</button>`,
6619
- `<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
6620
- `</form>`,
6621
- ].join("\n");
6622
- const list = tracks.length === 0
6623
- ? `<p class="meta">No routines${chrome.project === null ? " — open a project to file one" : " in this project yet — file one below; nothing fires until you approve it"}.</p>`
6624
- : tracks.map(track => trackRow(track, chrome.project === null)).join("\n");
6625
- return screen("routines", [
6626
- `<h1>Routines</h1>`,
6627
- `<p class="hint">scheduled work — anything needing a person appears in the inbox</p><p><a class="new-task" href="/recipes">Create a workflow from a recipe →</a></p>`,
6628
- list,
6629
- capture,
6630
- ].join("\n"), { chrome, functional: { script: scheduleEditorScript() } });
6631
- }
6632
- export function routineScreenPage(chrome, data) {
6633
- const { routine, fires } = data;
6634
- const status = routineStatus(routine);
6635
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
6636
- const schedule = parseSchedule(routine.schedule);
6637
- const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
6638
- const terms = `<div class="card">` +
6639
- `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
6640
- `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
6641
- `<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
6642
- (routine.acceptance.length === 0
6643
- ? ""
6644
- : `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
6645
- .map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
6646
- .join("")}</ul>`) +
6647
- `<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
6648
- routineAgentsHtml(routine, approved) +
6649
- `<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
6650
- `<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
6651
- `<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
6652
- `</div>`;
6653
- // THE CONSENT DOOR (v48): the password and the approve act exist only
6654
- // where an exact, readable route can be restated — never over agents
6655
- // that are unresolved, unreadable, or never frozen. Those states get the
6656
- // one recovery act instead: refresh the agents, then approve again.
6657
- const agents = routineAgentsState(routine);
6658
- // One plain-language act with accessible, neutral controls: the form is
6659
- // named by its title, the button described by the reason — no password,
6660
- // no danger verb, nothing to type.
6661
- const refreshForm = `<form method="post" action="${routineHref(routine.id)}/refresh" class="card approve-form agents-recovery" id="agents-recovery" aria-labelledby="agents-recovery-title">` +
6662
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
6663
- `<p id="agents-recovery-title"><strong>${escape(agents.state === "unfrozen"
6664
- ? "This standing order was approved before its agents were frozen — nothing fires until you approve it again."
6665
- : agents.state === "unreadable"
6666
- ? "The agents on file for this standing order cannot be read — nothing approves or fires until they are refreshed."
6667
- : agents.state === "unverified"
6668
- ? "The agents this standing order's approval froze do not verify — nothing fires until they are refreshed and approved again."
6669
- : "This standing order cannot be approved yet: it does not name an exact agent for every role.")}</strong></p>` +
6670
- `<p id="agents-recovery-why" class="recap">${escape(agents.state === "unresolved" && agents.problem !== null
6671
- ? `${agents.problem}. Configure the project's agents (config set <phase> --provider … --model …), then refresh.`
6672
- : "Refreshing reads today's configured agents into this order and shows you exactly who would plan, build, repair, and review each firing. Nothing is approved by refreshing — the password step comes after, on this page.")}</p>` +
6673
- `<button type="submit" aria-describedby="agents-recovery-why">Refresh agents</button></form>`;
6674
- const approveForm = approved
6675
- ? agents.refresh ? refreshForm : ""
6676
- : !agents.approvable
6677
- ? refreshForm
6678
- : [
6679
- `<form method="post" action="${routineHref(routine.id)}/approve" class="card approve-form">`,
6680
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
6681
- `<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
6682
- `<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
6683
- `<p><strong>Approve this standing order:</strong></p>`,
6684
- `<p class="recap">Each firing creates a task under exactly the terms above and BUILDS IT WITHOUT ASKING — ${escape(scheduleSaid)}, until you pause it. Questions and failures still reach you like any other work.</p>`,
6685
- // The agents the yes freezes (v48): the same concise block every
6686
- // approval shows, before the password.
6687
- routineAgentsHtml(routine, false, true),
6688
- `<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
6689
- `<button type="submit">Approve this routine</button>`,
6690
- `</form>`,
6691
- ].join("\n");
6692
- const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
6693
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
6694
- `<button type="submit"${danger ? ' class="danger"' : ""}>${label}</button></form>`;
6695
- const runNowForm = approved && !routine.paused && agents.state === "frozen"
6696
- ? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
6697
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
6698
- `<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
6699
- `<button type="submit">Run now</button></form>`
6700
- : "";
6701
- const acts = `<div class="card">` +
6702
- (routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
6703
- (runNowForm === "" ? "" : " " + runNowForm) +
6704
- `<p class="meta">${routine.paused ? "resuming fires again at the next due slot" : "pausing stops firing instantly; a running instance finishes"}${runNowForm === "" ? "" : " · run now spawns an extra instance without touching the schedule — spend outside the schedule takes your password again"}</p>` +
6705
- `</div>`;
6706
- const ledger = fires.length === 0
6707
- ? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${whenTime(routine.nextFireAt)}` : ""}.</p>`
6708
- : fires
6709
- .map(fire => {
6710
- const said = fire.outcome === "fired"
6711
- ? fire.instanceTaskId === null
6712
- ? "fired"
6713
- : `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(sentenceCase(fire.instanceState))}</span>`}`
6714
- : `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
6715
- const slot = fire.scheduledFor.replace(/^manual:/, "");
6716
- return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
6717
- })
6718
- .join("\n");
6719
- return screen(`routine · ${routine.name}`, [
6720
- `<h1>${escape(routine.name)} <span class="${status.badge}">${escape(status.text)}</span>` +
6721
- `<span class="meta"> · ${escape(projectName(routine.repo))}</span></h1>`,
6722
- data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
6723
- `<p>${trackStrip(fires)}<span class="right meta">$${data.spend.costUsd.toFixed(2)} this week${data.spend.unmeasuredRuns > 0 ? ` — measured on ${data.spend.totalRuns - data.spend.unmeasuredRuns} of ${data.spend.totalRuns} builds` : ""}</span></p>`,
6724
- data.blocker !== null
6725
- ? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
6726
- : "",
6727
- approved && !routine.paused && routine.nextFireAt !== null
6728
- ? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
6729
- : "",
6730
- "<h2>The standing order</h2>",
6731
- terms,
6732
- approveForm,
6733
- "<h2>Acts</h2>",
6734
- acts,
6735
- "<h2>Firings</h2>",
6736
- ledger,
6737
- ].join("\n"), { chrome });
6738
- }
6739
6432
  export function homePage(chrome, data) {
6740
6433
  const { summary } = data;
6741
6434
  // The night's harvest is the page's reason to exist — strong figures in a
@@ -6803,9 +6496,8 @@ export function homePage(chrome, data) {
6803
6496
  const dot = age < 5 * 60_000 ? "dot-ok" : age < 60 * 60_000 ? "dot-warn" : "dot-off";
6804
6497
  const said = age < 5 * 60_000 ? "alive" : age < 60 * 60_000 ? `quiet ${Math.round(age / 60_000)}m` : "not heard from";
6805
6498
  const busy = data.building.filter(claim => claim.runner === one.name).length;
6806
- const sessions = data.heldSessions?.get(one.name) ?? 0;
6807
6499
  return (`<div class="stat-card"><span class="k"><span class="dot ${dot}"></span>${escape(one.name)}</span>` +
6808
- `<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building${sessions > 0 ? ` \u00b7 ${sessions} attended session${sessions === 1 ? "" : "s"} (uncapped by Toolroll — each is an agent + a supervisor process; OS limits apply)` : ""}</span></div>`);
6500
+ `<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building</span></div>`);
6809
6501
  });
6810
6502
  const worktreeCards = data.worktrees.map(tree => {
6811
6503
  const leased = tree.leasedAt !== null && tree.releasedAt === null;
@@ -7005,9 +6697,9 @@ export function workPage(chrome, data) {
7005
6697
  (data.previous ? `<a class="button-link" href="${escape(href(data.view))}">First page</a>` : '') +
7006
6698
  (data.work.nextCursor === null ? '' : `<a class="button-link" rel="next" href="${escape(href(data.view, data.work.nextCursor))}">Next page</a>`) + `</nav>`;
7007
6699
  const tools = `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">` +
7008
- [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ['/routines', 'Routines'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
6700
+ [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
7009
6701
  .map(([path, label]) => `<a href="${path}">${label}</a>`).join('') + `</nav></details>`;
7010
- const toolLinks = [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ['/routines', 'Routines'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
6702
+ const toolLinks = [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
7011
6703
  const view = {
7012
6704
  kind: 'tasks',
7013
6705
  tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
@@ -7379,228 +7071,6 @@ export function portfolioOverview(data) {
7379
7071
  data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
7380
7072
  ].join("\n");
7381
7073
  }
7382
- /** Dollars for a screen: micro-USD stated as money, unknowables in words. */
7383
- export function contestDollars(microusd) {
7384
- return `$${(microusd / 1_000_000).toFixed(2)}`;
7385
- }
7386
- export const CONTEST_STATE_WORDS = {
7387
- dispatching: "the agents are being set up",
7388
- racing: "the agents are working right now",
7389
- "pick-wait": "every agent has finished — compare the results and pick one",
7390
- "decision-wait": "an agent asked a question — answer it from the task screen and the tournament continues",
7391
- picked: "decided — one result was picked",
7392
- abandoned: "abandoned — nothing was picked; every agent's work is kept",
7393
- interrupted: "interrupted — the machine running it went away; decide what happens next",
7394
- exhausted: "finished with nothing to pick — decide what happens next",
7395
- };
7396
- /**
7397
- * The comparison screen: every agent's result side by side — outcome, cost,
7398
- * questions, conclusion, and the verified diff — with a pick button only on
7399
- * results the evidence supports. Plain words throughout: tournament,
7400
- * agents, results. The pick itself happens on a separate confirmation
7401
- * screen whose form this page can only reach through a POST.
7402
- */
7403
- export function contestPage(chrome, data) {
7404
- const { contest, agents } = data.view;
7405
- const picking = contest.state === "pick-wait";
7406
- const abandonable = ["pick-wait", "exhausted", "interrupted", "decision-wait"].includes(contest.state);
7407
- // ONE summary per agent (arc 6, finding 7): the at-a-glance table and the
7408
- // cards below both render from this object, so the two can never tell a
7409
- // pick two different stories. Every diff state keeps its own words —
7410
- // verified-zero, changes, missing, and capture problems are not the same
7411
- // fact and are never collapsed into "no diff".
7412
- const summarize = (agent) => {
7413
- const { contestant, run } = agent;
7414
- const outcome = run === null
7415
- ? "never produced a finished attempt"
7416
- : run.outcome === "built"
7417
- ? "finished with changes"
7418
- : run.outcome === "no-change"
7419
- ? "concluded no change was needed"
7420
- : run.outcome === "parked"
7421
- ? "waiting on an answer"
7422
- : run.outcome === null
7423
- ? (data.liveRuns?.has(run.id) === true ? "still working" : "stopped without finishing")
7424
- : run.outcome === "failed"
7425
- ? "failed"
7426
- : run.outcome === "refused"
7427
- ? "refused — a gate said no"
7428
- : "stopped";
7429
- const minutes = run === null || run.finishedAt === null
7430
- ? null
7431
- : Math.max(1, Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 60_000));
7432
- const diff = data.diffs.get(contestant.id) ?? null;
7433
- const diffWords = diff === null || diff.stat === null
7434
- ? "no change summary"
7435
- : "problem" in diff.stat
7436
- ? "summary capture failed"
7437
- : diff.stat.fileCount === 0
7438
- ? "no changes, verified"
7439
- : `${diff.stat.fileCount} file(s) · +${diff.stat.additions} −${diff.stat.deletions}`;
7440
- return {
7441
- agent,
7442
- contestant,
7443
- run,
7444
- winner: contest.winnerContestant === contestant.id,
7445
- outcome,
7446
- minutes,
7447
- asked: data.questions.get(contestant.id) ?? 0,
7448
- // Money words are KIND words (slice B, E3): a comparison lane never
7449
- // had a reservation, so reservation language would lie in both
7450
- // directions — unmeasured lanes say tokens instead.
7451
- cost: (() => {
7452
- if (contest.kind !== "comparison") {
7453
- return contestant.unknownSpend
7454
- ? `${contestDollars(contestant.accountedMicrousd)} — the exact figure was unknowable, so the full reservation was charged`
7455
- : contestDollars(contestant.accountedMicrousd);
7456
- }
7457
- // The WHOLE lineage speaks (Codex slice-B finding 6): main attempt,
7458
- // resumes, and repairs — a newest-run read under-reports every
7459
- // park cycle.
7460
- const rollup = data.rollups.get(contestant.id) ?? { costMicrousd: 0, tokensIn: 0, tokensOut: 0, measuredRuns: 0, totalRuns: 0 };
7461
- return contestant.unknownSpend
7462
- ? `tokens only${rollup.tokensIn + rollup.tokensOut > 0 ? ` (${(rollup.tokensIn + rollup.tokensOut).toLocaleString()} across ${rollup.totalRuns} run${rollup.totalRuns === 1 ? "" : "s"})` : ""} — this harness reports no dollars`
7463
- : `${contestDollars(rollup.costMicrousd)} measured${rollup.measuredRuns < rollup.totalRuns ? ` on ${rollup.measuredRuns} of ${rollup.totalRuns} runs` : ""}`;
7464
- })(),
7465
- diff,
7466
- diffWords,
7467
- };
7468
- };
7469
- const summaries = agents.map(summarize);
7470
- // The at-a-glance table: one COLUMN per agent, the same derived facts as
7471
- // the cards. Wide content scrolls in its own box (the arc-4 rule).
7472
- const glance = summaries.length < 2
7473
- ? ""
7474
- : `<div class="scroll-x"><table class="contest-glance">` +
7475
- `<tr><td></td>${summaries.map(one => `<th>Agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">Picked</span>` : ""}</th>`).join("")}</tr>` +
7476
- `<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
7477
- `<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
7478
- `<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
7479
- `<tr><td class="meta">questions</td>${summaries.map(one => `<td>${one.asked}</td>`).join("")}</tr>` +
7480
- `<tr><td class="meta">cost</td>${summaries.map(one => `<td>${escape(one.cost)}</td>`).join("")}</tr>` +
7481
- `<tr><td></td>${summaries.map(one => `<td>${one.run === null ? "" : `<a href="/r/${one.run.id}">the build</a>`}</td>`).join("")}</tr>` +
7482
- `</table></div>`;
7483
- const agentCard = (summary) => {
7484
- const { contestant, run, winner, outcome, minutes, asked, cost, diff } = summary;
7485
- const agent = summary.agent;
7486
- const parts = [
7487
- `<div class="card${winner ? " picked" : ""}">`,
7488
- `<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
7489
- `${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
7490
- `<p class="row">${escape(outcome)}` +
7491
- `${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
7492
- `${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
7493
- `${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
7494
- `<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
7495
- run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
7496
- diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
7497
- ];
7498
- if (picking) {
7499
- if (agent.pickable) {
7500
- parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">Pick this result…</button>`, `</form>`, `<p class="meta">Picking continues to a confirmation screen — nothing happens yet</p>`);
7501
- }
7502
- else {
7503
- parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
7504
- }
7505
- }
7506
- parts.push(`</div>`);
7507
- return parts.filter(one => one !== "").join("\n");
7508
- };
7509
- return screen("tournament", [
7510
- `<h1>${contestNoun(contest.kind)}</h1>`,
7511
- `<p class="meta">${agents.length} agents raced on <a href="${taskHref(data.taskId)}">${escape(data.taskTitle)}</a> — ` +
7512
- `only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
7513
- data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
7514
- `<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
7515
- contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
7516
- contest.kind === "comparison"
7517
- ? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
7518
- `${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
7519
- : `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
7520
- `${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
7521
- glance,
7522
- `<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
7523
- abandonable
7524
- ? [
7525
- `<div class="card">`,
7526
- `<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
7527
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
7528
- `<input type="hidden" name="act" value="abandon">`,
7529
- `<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
7530
- `</form>`,
7531
- `<p class="meta">Abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
7532
- `</div>`,
7533
- ].join("\n")
7534
- : "",
7535
- ].filter(one => one !== "").join("\n"), { chrome });
7536
- }
7537
- /**
7538
- * The confirmation screen a POST minted: it restates, in full, exactly what
7539
- * the password will authorize — the identified result, the money, and the
7540
- * one publication consequence — over a single-use nonce bound to that
7541
- * restatement. If anything shifts underneath before the yes, the pick
7542
- * refuses rather than landing on the moved thing.
7543
- */
7544
- export function contestCeremonyPage(chrome, data) {
7545
- const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
7546
- if (data.kind === "abandon") {
7547
- return screen("tournament", [
7548
- `<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
7549
- `<div class="card">`,
7550
- `<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
7551
- `<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
7552
- `<p class="row">— every agent's branch and evidence is kept; nothing is deleted and nothing is published</p>`,
7553
- data.contestKind === "comparison"
7554
- ? `<p class="row">— the ${escape(contestDollars(data.totalMicrousd))} measured so far stays on the record</p>`
7555
- : `<p class="row">— the ${escape(contestDollars(data.totalMicrousd))} already charged stays charged</p>`,
7556
- `</div>`,
7557
- `<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
7558
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
7559
- `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
7560
- `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
7561
- `<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
7562
- `</form>`,
7563
- back,
7564
- ].join("\n"), { chrome });
7565
- }
7566
- const agent = data.chosen;
7567
- if (agent === undefined || agent.run === null)
7568
- return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
7569
- const run = agent.run;
7570
- return screen("tournament", [
7571
- `<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
7572
- `<div class="card">`,
7573
- `<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
7574
- `<p class="row">its result becomes the outcome of <strong>${escape(data.taskTitle)}</strong> — the task is marked done, keyed to <a href="/r/${run.id}">this build</a></p>`,
7575
- run.outcome === "no-change"
7576
- ? `<p class="row">the result is a verified <strong>no change</strong> — the agent concluded nothing needed doing, and its checkout still matches the starting point</p>`
7577
- : `<p class="row">the changes live on branch <span class="mono">${escape(run.branch ?? "?")}</span>` +
7578
- `${run.headRevision === null ? "" : `, ending at <span class="mono">${escape(run.headRevision.slice(0, 12))}</span>`}</p>`,
7579
- agent.diff === null
7580
- ? ""
7581
- : `<p class="row">the diff being picked: <span class="mono">${escape(agent.diff.sha256.slice(0, 12))}</span> · ${agent.diff.bytesStored} bytes, verified</p>`,
7582
- data.publication === null || data.publication === undefined
7583
- ? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
7584
- : `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
7585
- `<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
7586
- data.contestKind === "comparison"
7587
- ? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
7588
- `; the comparison measured ${escape(contestDollars(data.totalMicrousd))} across the lanes that report dollars</p>`
7589
- : `<p class="row">this agent was charged ${escape(contestDollars(agent.contestant.accountedMicrousd))}` +
7590
- `${agent.contestant.unknownSpend ? " (its full reservation — the exact figure was unknowable)" : ""}` +
7591
- `; the tournament charged ${escape(contestDollars(data.totalMicrousd))} in total</p>`,
7592
- `<p class="row">the other ${data.agents - 1} agent${data.agents - 1 === 1 ? "'s result is" : "s' results are"} not used — their branches and evidence are kept for reference</p>`,
7593
- `</div>`,
7594
- `<form method="post" action="/contest/${data.contestId}/pick" class="card approve-form">`,
7595
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
7596
- `<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
7597
- `<input type="hidden" name="choice" value="${agent.contestant.id}">`,
7598
- `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
7599
- `<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
7600
- `</form>`,
7601
- back,
7602
- ].filter(one => one !== "").join("\n"), { chrome });
7603
- }
7604
7074
  /**
7605
7075
  * owner/name from a git remote URL — FULLY ANCHORED https/ssh github.com
7606
7076
  * forms only (ghlist review, finding 2): a foreign host carrying
@@ -8037,7 +7507,7 @@ export const NAV_ICONS = {
8037
7507
  * The workspace shell (package 1): three primary rows (chat, work,
8038
7508
  * projects) above two accordion groups. Work tools is where work is
8039
7509
  * arranged, planned, and scheduled — the board, its order view, the
8040
- * flat task list, recipes, routines, the portfolio, and the action
7510
+ * flat task list, recipes, the portfolio, and the action
8041
7511
  * ledger. Settings is who and what the fleet runs on — fleet,
8042
7512
  * requirements, people, operating mode, system — plus the agent-defaults
8043
7513
  * page where the console offers it. Both draw from the same two lists on
@@ -8052,7 +7522,6 @@ export function workToolRows(scoped = false, _offersChat = false, offersCode = f
8052
7522
  { key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
8053
7523
  { key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
8054
7524
  { key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
8055
- { key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
8056
7525
  { key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
8057
7526
  { key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
8058
7527
  { key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
@@ -8071,7 +7540,7 @@ export function settingsRows(scoped = false, offersSettings = false) {
8071
7540
  return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
8072
7541
  }
8073
7542
  /** Which accordion group opens by default for a given active page. */
8074
- export const TOOL_KEYS = new Set(["code", "inbox", "board", "queue", "tasks", "workbench", "recipes", "routines", "ledger", "spend"]);
7543
+ export const TOOL_KEYS = new Set(["code", "inbox", "board", "queue", "tasks", "workbench", "recipes", "ledger", "spend"]);
8075
7544
  export const SETTINGS_KEYS = new Set(["settings", "fleet", "caps", "people", "mode", "system"]);
8076
7545
  /** The builds screen's views (reduction pass §1): done, the review queue,
8077
7546
  * and activity are ways of looking at builds, not destinations. */
@@ -8469,14 +7938,14 @@ export function revisionLineageWords(lineage) {
8469
7938
  }
8470
7939
  else if (lineage.terms !== null) {
8471
7940
  const terms = lineage.terms;
8472
- words.push(`inherited terms, as they stand now: ${riskTitle(terms.riskLevel)} · ${qualityModeTitle(terms.qualityMode)} quality · ` +
7941
+ words.push(`inherited terms, as they stand now: ${qualityModeTitle(terms.qualityMode)} quality · ` +
8473
7942
  `${terms.permissionMode === "bypassPermissions" ? "full access" : "auto permissions"} · ` +
8474
7943
  `${terms.budgetMicrousd === null ? "no attempt cap" : `$${(terms.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`} · ` +
8475
7944
  `${terms.exclusions ? "its exclusions" : "no exclusions"} · ${terms.touches === 0 ? "any path" : `${terms.touches} path limit${terms.touches === 1 ? "" : "s"}`} · ` +
8476
7945
  `${terms.criteria} criteri${terms.criteria === 1 ? "on" : "a"}${terms.routeOverrides === 0 ? "" : ` · ${terms.routeOverrides} agent override${terms.routeOverrides === 1 ? "" : "s"}`}`);
8477
7946
  }
8478
- words.push("re-resolved for this approval: the agents route and the fallback chain — a yes on the source never covers them");
8479
- words.push("never inherited: the source's approval, attended sessions, publication and merge grants");
7947
+ words.push("re-resolved for this approval: the agents route — a yes on the source never covers it");
7948
+ words.push("never inherited: the source's approval, publication and merge grants");
8480
7949
  if (lineage.repair !== null) {
8481
7950
  const repair = lineage.repair;
8482
7951
  words.push(`repair chain rooted at ${repair.rootTask}: ${repair.attemptsUsed} attempt${repair.attemptsUsed === 1 ? "" : "s"} used` +
@@ -8745,16 +8214,6 @@ export function taskBodyParts(data) {
8745
8214
  }
8746
8215
  return box("problem", "Dispatch unknown", "Refresh this task before relying on its scheduler state.", "unknown");
8747
8216
  })();
8748
- const contest = data.contest ?? null;
8749
- const contestCard = contest === null
8750
- ? ""
8751
- : [
8752
- `<div class="card">`,
8753
- `<p><strong>${contestNoun(contest.kind)}</strong> <span class="meta">${contest.agents} agents on this task</span></p>`,
8754
- `<p class="row">${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</p>`,
8755
- `<p class="row"><a href="/contest/${contest.id}">${contest.state === "pick-wait" ? "compare the results and pick one" : `see the ${contestNoun(contest.kind)}`}</a></p>`,
8756
- `</div>`,
8757
- ].join("\n");
8758
8217
  // Operator steering (arc 1): notes for the agent, each wearing exactly
8759
8218
  // where it stands — waiting, attached, proven delivered, or superseded.
8760
8219
  const steering = data.steering ?? [];
@@ -8771,7 +8230,7 @@ export function taskBodyParts(data) {
8771
8230
  .map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)} · ${escape(steerState(one))}</span> ` +
8772
8231
  `${escape(one.note)}</p>`)
8773
8232
  .join("\n");
8774
- const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled" || (data.contest ?? null) !== null
8233
+ const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
8775
8234
  ? ""
8776
8235
  : `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
8777
8236
  `<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
@@ -8800,18 +8259,6 @@ export function taskBodyParts(data) {
8800
8259
  scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
8801
8260
  scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
8802
8261
  `<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
8803
- // The fallback chain, on the card the yes reads (Layer F): the
8804
- // digest binds the WHOLE chain, so every entry — credential
8805
- // included — is said before anyone signs.
8806
- (() => {
8807
- const chain = chainFromJson(scope.proposedChainJson ?? null);
8808
- if (chain === null || chain.length < 2)
8809
- return "";
8810
- return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
8811
- .slice(1)
8812
- .map(one => `falls back to ${escape(one.profile.provider)} (${escape(one.profile.model)}) — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
8813
- .join("; ")}</p>`;
8814
- })(),
8815
8262
  acceptanceCeremonyHtml(scope.acceptance),
8816
8263
  approval.approved
8817
8264
  ? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
@@ -8943,7 +8390,6 @@ export function taskBodyParts(data) {
8943
8390
  revision: data.revision == null || "problem" in data.revision ? null : data.revision,
8944
8391
  revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
8945
8392
  repairChain: data.repairChain ?? null,
8946
- raceTerms: data.raceTerms ?? null,
8947
8393
  route: data.route,
8948
8394
  coordinator: data.coordinator ?? null,
8949
8395
  deliverable: data.deliverable ?? "branch",
@@ -8951,55 +8397,10 @@ export function taskBodyParts(data) {
8951
8397
  notNowHref: "/work",
8952
8398
  sticky: data.dispatch?.action === "approve-scope",
8953
8399
  earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
8954
- // A tournament's terms live in the full scope editor; every other plan edits in place.
8955
- edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
8400
+ edit: data.csrf === "" ? null
8956
8401
  : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
8957
8402
  open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
8958
8403
  });
8959
- // The attended road (Phase 2E): beside the approval, never replacing it.
8960
- // The mint button leads to the CONFIRM screen where every term renders
8961
- // and the password signs; an open authorization shows its state, its
8962
- // revoke, and — through the page script — the liveness beat that IS
8963
- // "while you watch".
8964
- const attended = data.attended ?? null;
8965
- const attendedCard = attended === null
8966
- ? ""
8967
- : attended.open !== null
8968
- ? [
8969
- `<div class="card" data-attended="${escape(attended.open.id)}">`,
8970
- `<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
8971
- `<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
8972
- `<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
8973
- `<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
8974
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
8975
- `<button type="submit">Revoke — stop the session</button>`,
8976
- `</form>`,
8977
- `</div>`,
8978
- ].join("\n")
8979
- : attended.canMint
8980
- ? [
8981
- `<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
8982
- `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
8983
- `<p><strong>Or run it once while you watch</strong></p>`,
8984
- `<p class="meta">No approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
8985
- attended.mint === undefined || attended.mint.models.length <= 1
8986
- ? ""
8987
- : `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
8988
- attended.mint.models
8989
- .map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
8990
- .join("") +
8991
- `</select></label>`,
8992
- attended.mint === undefined
8993
- ? ""
8994
- : `<label>Permissions<select name="posture">` +
8995
- `<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
8996
- `<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
8997
- `<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
8998
- `</select></label>`,
8999
- `<button type="submit">Read the terms</button>`,
9000
- `</form>`,
9001
- ].join("\n")
9002
- : "";
9003
8404
  const scopeForm = [
9004
8405
  `<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
9005
8406
  `<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
@@ -9040,50 +8441,6 @@ export function taskBodyParts(data) {
9040
8441
  qualityModeChoices("quality-mode", selected) +
9041
8442
  `<p class="meta permission-note">This choice is signed into the scope. Inspect the saved work and actual check results when it is ready. Publication and deployment need their own authorization.</p></fieldset>`;
9042
8443
  })(),
9043
- (() => {
9044
- // The tournament controls (operator request): how many agents compete,
9045
- // on which model, under which dollars. "One agent" is the ordinary
9046
- // path; anything more files race terms beside the scope, and the ONE
9047
- // approval above restates and covers both.
9048
- const defaults = data.spendDefaults ?? null;
9049
- const terms = data.raceTerms ?? null;
9050
- const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
9051
- const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
9052
- const perPrefill = terms !== null
9053
- ? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
9054
- : defaults?.racePerAgentMicrousd != null
9055
- ? (defaults.racePerAgentMicrousd / 1_000_000).toFixed(2)
9056
- : "";
9057
- const totalPrefill = terms !== null
9058
- ? (terms.totalBudgetMicrousd / 1_000_000).toFixed(2)
9059
- : defaults?.raceTotalMicrousd != null
9060
- ? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
9061
- : "";
9062
- return [
9063
- `<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
9064
- `<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
9065
- `<select name="race-count">` +
9066
- `<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
9067
- [2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
9068
- `</select></label>`,
9069
- `<label>Competing model <select name="race-model">` +
9070
- PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
9071
- `</select></label>`,
9072
- `<label>Each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
9073
- `<label>The whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
9074
- // The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
9075
- // provider, exact model required — no dollar fields exist. Blank
9076
- // rows are unused; filling any row files a comparison INSTEAD of a
9077
- // tournament, and mixing the two refuses in words.
9078
- `<p class="meta" style="margin-top:.75rem">or compare different agents side by side — no dollar caps; each agent's clock is its bound, and spend lands measured only where the harness reports dollars:</p>`,
9079
- ...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
9080
- `<option value="">—</option>` +
9081
- PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
9082
- `</select></label>` +
9083
- `<label>Its exact model<input type="text" name="compare-model-${lane}" value="${escape(scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
9084
- `</details>`,
9085
- ].join("\n");
9086
- })(),
9087
8444
  `<button type="submit">Save scope</button>`,
9088
8445
  `</form></details>`,
9089
8446
  ].join("\n");
@@ -9509,7 +8866,6 @@ export function taskBodyParts(data) {
9509
8866
  // (slice 1c) rides beside the main column on wide screens and above it
9510
8867
  // on narrow ones.
9511
8868
  `<div class="task-layout"><div class="task-main">`,
9512
- contestCard,
9513
8869
  attemptPanel,
9514
8870
  data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
9515
8871
  ? ""
@@ -9531,7 +8887,7 @@ export function taskBodyParts(data) {
9531
8887
  section("scope",
9532
8888
  // The recipe road rides with the scope it reuses (UI polish
9533
8889
  // 2026-09-13), off the title-to-action path.
9534
- ["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), attendedCard, scopeForm].join("\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
8890
+ ["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), scopeForm].join("\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
9535
8891
  dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
9536
8892
  section("holds", holds, true, data.holds.length),
9537
8893
  cancelAct,
@@ -9754,7 +9110,7 @@ export function taskBodyParts(data) {
9754
9110
  { key: "history", html: data.history ?? "" }, { key: "control", html: stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true) }, { key: "problem", html: problemHtml },
9755
9111
  { key: "pull-request", html: pullRequestCard },
9756
9112
  { key: "needs-scope", html: needsScopeCard },
9757
- { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
9113
+ { key: "mirror", html: mirrorCard }, { key: "attempt", html: attemptPanel },
9758
9114
  ].filter(one => one.html !== ""),
9759
9115
  // Open questions are answered in the thread.
9760
9116
  questions: "",
@@ -9789,11 +9145,7 @@ export function taskBody(data) {
9789
9145
  return taskBodyParts(data).html;
9790
9146
  }
9791
9147
  export function taskPage(chrome, data) {
9792
- // The beat moved to the chrome layer (v28): every console page keeps the
9793
- // approver's sessions live; this page no longer carries its own.
9794
- //
9795
- // The sensitive-page composition guard (slice 1c): a live attended
9796
- // attempt can coexist with an unapproved scope, so this page may carry a
9148
+ // The sensitive-page composition guard (slice 1c): this page may carry a
9797
9149
  // password ceremony. sendScreen() would keep a functional script on such
9798
9150
  // a page — so the decision is made HERE, from the rendered body itself:
9799
9151
  // when the body (or the chrome's list pane) shows a password input, the
@@ -10238,7 +9590,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
10238
9590
  taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
10239
9591
  status: { label: status.label, tone: status.tone, token: status.token },
10240
9592
  problem: view.detail === null ? view.historyProblem : null,
10241
- next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
9593
+ next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
10242
9594
  notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
10243
9595
  acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
10244
9596
  // The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
@@ -10282,8 +9634,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
10282
9634
  headStatus: false,
10283
9635
  action: false,
10284
9636
  });
10285
- const contest = view.contest === null ? "" : `<p class="row"><a href="/contest/${view.contest.id}">${view.contest.state === "pick-wait" ? `compare the ${contestNoun(view.contest.kind)} and pick →` : `the ${contestNoun(view.contest.kind)} (${escape(view.contest.state)}) →`}</a></p>`;
10286
- parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
9637
+ parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + `</div>`);
10287
9638
  const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
10288
9639
  const youCheck = panel.panel.youCheck;
10289
9640
  // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
@@ -10378,7 +9729,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
10378
9729
  return {
10379
9730
  html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
10380
9731
  selected: {
10381
- ...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, contest, acts, actFacts, runChecks, mismatch, failure,
9732
+ ...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, acts, actFacts, runChecks, mismatch, failure,
10382
9733
  // A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
10383
9734
  // Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
10384
9735
  panel: (() => {
@@ -10461,9 +9812,6 @@ export function reviewNextActionOf(view, csrf) {
10461
9812
  if (run === null || detail === null) {
10462
9813
  return card("inspect-task", "No build to review", "This task was marked complete without a build record.", `<a class="button-link" href="${taskHref(view.taskId)}">Open the task</a>`);
10463
9814
  }
10464
- if (view.contest !== null && view.contest.state === "pick-wait") {
10465
- return card("compare-contest", `Compare the ${contestNoun(view.contest.kind)}`, `${view.contest.agents} agents finished — compare their results side by side and pick one.`, `<a class="button-link" href="/contest/${view.contest.id}">Compare results</a>`);
10466
- }
10467
9815
  if (detail.ciFailing && csrf !== "") {
10468
9816
  return card("draft-repair", "CI is failing on its pull request", "Toolroll confirmed the failure. Draft one repair task, then approve it before it runs.", `<form method="post" action="/r/${run.id}/draft-repair"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit">Draft a repair task</button></form>`);
10469
9817
  }
@@ -11773,42 +11121,8 @@ export function runFactsRows(run, taskId, live, route = null) {
11773
11121
  ? `<p class="row"><span class="meta" style="min-width:8.5rem">recorded error</span></p><pre class="mono" id="run-reason-detail" style="white-space:pre-wrap;overflow-wrap:anywhere">${escape(run.reason)}</pre>`
11774
11122
  : ""));
11775
11123
  }
11776
- export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, heldTurns = null, continueOffer = null, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
11124
+ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
11777
11125
  const rows = runFactsRows(run, taskId, running, route);
11778
- // The conversation (Phase 2E, v2 S1g): every stdin injection as the
11779
- // ledger records it — author named for operator turns, machine turns
11780
- // say so — and the TURN BOX while the session is held. An unconfirmed
11781
- // turn says honestly that the agent may or may not have seen it.
11782
- const turnWords = (turn) => turn.state === "settled"
11783
- ? ""
11784
- : turn.state === "uncertain"
11785
- ? " · unconfirmed — the agent may or may not have seen this; its cost is counted at worst case"
11786
- : turn.state === "cancelled"
11787
- ? " · never reached the agent"
11788
- : " · the agent is working on this";
11789
- const conversation = heldTurns === null
11790
- ? ""
11791
- : `<h2>Conversation</h2>` +
11792
- (heldTurns.turns.length === 0
11793
- ? `<p class="meta">Nothing said yet</p>`
11794
- : heldTurns.turns
11795
- .map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
11796
- ? escape(turn.author ?? "operator")
11797
- : turn.sourceKind === "brief"
11798
- ? "the brief"
11799
- : turn.sourceKind === "answer"
11800
- ? "your answer"
11801
- : "repair (machine)"} · ${whenTime(turn.recordedAt)}${turnWords(turn)}</span> ${escape(oneLineOf(turn.text, 240))}</p>`)
11802
- .join("\n")) +
11803
- (heldTurns.open && csrf !== ""
11804
- ? `<form method="post" action="/r/${run.id}/turn" class="row">` +
11805
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
11806
- `<input type="text" name="text" maxlength="500" placeholder="say something to the agent — it reads this as its next instruction, inside the approved scope" aria-label="turn" style="width:100%;max-width:34rem">` +
11807
- `<button type="submit">Send</button></form>` +
11808
- `<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
11809
- : heldTurns.open
11810
- ? ""
11811
- : `<p class="meta">The session has ended — the record above is complete</p>`);
11812
11126
  // The live peek region (A2): the poller fills it only on a serve that
11813
11127
  // asserted its runner. Without the assertion the section still appears
11814
11128
  // for a running build and says honestly why it is empty \u2014 a page that
@@ -11927,19 +11241,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
11927
11241
  `<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem">` +
11928
11242
  `<button type="submit">Add note</button></form>`;
11929
11243
  const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
11930
- // Continuation (Phase 2E, A4): a finished attempt offers a watched
11931
- // follow-up — the text you type here enters the SIGNED terms on the
11932
- // confirm screen; nothing runs until your password agrees to exactly it.
11933
- const continueCard = continueOffer === null || csrf === ""
11934
- ? ""
11935
- : `<details class="result-request-open continue-watch"><summary>Continue while you watch</summary>` +
11936
- `<form method="post" action="${taskHref(continueOffer.taskId)}/attend-preview" class="card">` +
11937
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
11938
- `<input type="hidden" name="parent" value="${run.id}">` +
11939
- `<p class="meta">Picks up from where this attempt stopped.</p>` +
11940
- `<label>What next<textarea name="followup" rows="2" maxlength="2000" placeholder="What should the agent do next, within the same scope…"></textarea></label>` +
11941
- `<button type="submit">Review the terms</button>` +
11942
- `</form></details>`;
11943
11244
  // Outcome first (UI polish 2026-09-13): a finished build leads with its
11944
11245
  // result, proof, and diff; the machine facts fold under "Build details".
11945
11246
  // A live build keeps the facts open — they are what a watcher polls.
@@ -11966,7 +11267,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
11966
11267
  return screen(`build #${run.id}`, [
11967
11268
  `<h1>Build #${run.id} <span class="meta"><a href="${taskHref(taskId)}">${escape(taskId)}</a></span></h1>`,
11968
11269
  running ? facts : "",
11969
- conversation,
11970
11270
  transcript,
11971
11271
  peek,
11972
11272
  ...(resultPanel === null
@@ -11977,7 +11277,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
11977
11277
  reviewCard,
11978
11278
  ]
11979
11279
  : [resultPanel, editorToggleForm]),
11980
- continueCard,
11981
11280
  running ? "" : facts,
11982
11281
  evidence,
11983
11282
  notesCard,
@@ -12313,6 +11612,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
12313
11612
  kind: "settings",
12314
11613
  said: problem,
12315
11614
  groups: settingsGroups(messaging, telegramFailing),
11615
+ ...(messaging?.legacyWarning ? { legacyWebhookWarning: messaging.legacyWarning } : {}),
12316
11616
  theme,
12317
11617
  accent: requestContext.getStore()?.accent ?? DEFAULT_ACCENT,
12318
11618
  accentPresets: ACCENT_PRESETS,
@@ -12346,6 +11646,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
12346
11646
  };
12347
11647
  return screen("Settings", [
12348
11648
  "<h1>Settings</h1>",
11649
+ messaging?.legacyWarning ? `<p data-legacy-webhooks>Legacy webhooks are deprecated. Connect <a href="/settings/slack" style="display:inline-flex;min-height:44px;align-items:center">Slack</a> or <a href="/settings/discord" style="display:inline-flex;min-height:44px;align-items:center">Discord</a> in Chat settings.</p>` : "",
12349
11650
  settingsTiles(view.groups),
12350
11651
  phone === null ? "" : phoneSetupHtml(phone),
12351
11652
  appearanceCard(csrf),
@@ -12592,9 +11893,6 @@ export function nextPage(chrome, data) {
12592
11893
  `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
12593
11894
  `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
12594
11895
  (scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
12595
- (data.raceTerms === null
12596
- ? ""
12597
- : `<p><strong>${data.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</strong></p><p class="meta">${data.raceTerms.n} agents build independently: ${data.raceTerms.agents.map(one => `${escape(one.provider)} · ${escape(one.model)}`).join(" vs ")}.</p>`) +
12598
11896
  // The AGENTS the yes freezes (v48): the same concise block the task
12599
11897
  // page and chat sign under, before the password — runtime limits one
12600
11898
  // tap away, never in the way.
@@ -12696,11 +11994,6 @@ export async function form(request, cap = BODY_CAP) {
12696
11994
  }
12697
11995
  return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
12698
11996
  }
12699
- /** Attended liveness in words for the task card — reads the durable clock. */
12700
- export function attendedWatchWords(lastBeatAt, now, absoluteExpiry) {
12701
- const state = attendedLivenessState(lastBeatAt === null ? null : Date.parse(lastBeatAt), now.getTime(), Date.parse(absoluteExpiry));
12702
- return state === "live" ? "watching" : state === "grace" ? "watching (a beat behind)" : state === "expired" ? "expired" : "not watching — reopen this page to resume";
12703
- }
12704
11997
  /** One display line, bounded — turn text is data, never layout. */
12705
11998
  export function oneLineOf(text, cap) {
12706
11999
  const flat = text.replace(/\s+/g, " ").trim();