toolroll 0.9.52 → 0.9.54

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 (187) hide show
  1. package/CHANGELOG.md +16 -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/board.d.ts +3 -19
  8. package/dist/board.js +4 -42
  9. package/dist/browser/workspace.js +18 -18
  10. package/dist/browser-workspace.d.ts +2 -1
  11. package/dist/builder.d.ts +9 -40
  12. package/dist/builder.js +50 -257
  13. package/dist/chat-actions.js +3 -14
  14. package/dist/chat-ask.js +8 -8
  15. package/dist/chat-controls.d.ts +0 -4
  16. package/dist/chat-controls.js +0 -1
  17. package/dist/chat-core.d.ts +171 -0
  18. package/dist/chat-core.js +288 -0
  19. package/dist/chat-decide.d.ts +10 -8
  20. package/dist/chat-decide.js +27 -24
  21. package/dist/chat-delivery-state.d.ts +51 -75
  22. package/dist/chat-delivery-state.js +140 -170
  23. package/dist/chat-delivery.d.ts +21 -4
  24. package/dist/chat-delivery.js +313 -195
  25. package/dist/chat-flow.d.ts +29 -1
  26. package/dist/chat-flow.js +24 -50
  27. package/dist/chat-messages.d.ts +319 -0
  28. package/dist/chat-messages.js +841 -0
  29. package/dist/chat-migration.d.ts +30 -0
  30. package/dist/chat-migration.js +802 -0
  31. package/dist/chat-quiet.d.ts +3 -0
  32. package/dist/chat-quiet.js +7 -2
  33. package/dist/chat-task-actions.d.ts +1 -1
  34. package/dist/chat-task-actions.js +0 -2
  35. package/dist/claim.d.ts +79 -244
  36. package/dist/claim.js +688 -1023
  37. package/dist/cli.d.ts +2 -0
  38. package/dist/cli.js +10 -2
  39. package/dist/contracts/chat-content.d.ts +71 -2
  40. package/dist/contracts/chat-content.js +22 -3
  41. package/dist/contracts/chat-tables.d.ts +11 -4
  42. package/dist/contracts/chat-tables.js +165 -87
  43. package/dist/contracts/cli.d.ts +0 -13
  44. package/dist/contracts/cli.js +0 -2
  45. package/dist/contracts/console-api.d.ts +5 -10
  46. package/dist/contracts/console-api.js +4 -10
  47. package/dist/contracts/discord-callback.js +2 -1
  48. package/dist/contracts/flow.d.ts +50 -0
  49. package/dist/contracts/flow.js +26 -1
  50. package/dist/contracts/lead-tools.d.ts +0 -13
  51. package/dist/contracts/lead-tools.js +3 -3
  52. package/dist/contracts/slack-callback.js +3 -2
  53. package/dist/contracts/store-json.d.ts +1 -29
  54. package/dist/contracts/store-json.js +4 -17
  55. package/dist/contracts/teams-callback.js +2 -1
  56. package/dist/converse.d.ts +1 -11
  57. package/dist/converse.js +6 -38
  58. package/dist/coordinator.d.ts +0 -1
  59. package/dist/coordinator.js +3 -5
  60. package/dist/demo.js +16 -26
  61. package/dist/desktop-update-gate.js +0 -1
  62. package/dist/desktop-update.js +1 -3
  63. package/dist/discord-chat.js +19 -38
  64. package/dist/discord-settings.js +4 -13
  65. package/dist/discord.js +4 -14
  66. package/dist/dispatch.d.ts +1 -1
  67. package/dist/dispatch.js +1 -1
  68. package/dist/dispose.d.ts +2 -3
  69. package/dist/dispose.js +17 -92
  70. package/dist/envelope.js +0 -3
  71. package/dist/exec.d.ts +0 -64
  72. package/dist/exec.js +3 -356
  73. package/dist/exhaustion.d.ts +22 -76
  74. package/dist/exhaustion.js +14 -140
  75. package/dist/export.js +8 -8
  76. package/dist/flow-engine.js +1 -1
  77. package/dist/flow-schedule.d.ts +154 -0
  78. package/dist/flow-schedule.js +321 -0
  79. package/dist/flow-share.js +1 -1
  80. package/dist/flow-triggers.d.ts +6 -1
  81. package/dist/flow-triggers.js +65 -10
  82. package/dist/flows-cli.js +8 -4
  83. package/dist/flows-ui.js +2 -0
  84. package/dist/flows.js +1 -1
  85. package/dist/guides.js +9 -29
  86. package/dist/invoke.d.ts +1 -24
  87. package/dist/invoke.js +17 -259
  88. package/dist/lead-status.js +2 -4
  89. package/dist/lead-voice.js +1 -1
  90. package/dist/mate-doors.d.ts +1 -2
  91. package/dist/mate-doors.js +12 -25
  92. package/dist/mate-tools.d.ts +2 -2
  93. package/dist/mate-tools.js +13 -24
  94. package/dist/modes.d.ts +19 -8
  95. package/dist/modes.js +24 -14
  96. package/dist/notification-rows.d.ts +30 -0
  97. package/dist/notification-rows.js +102 -0
  98. package/dist/operate-remote-arguments.js +2 -6
  99. package/dist/operate-remote.js +6 -7
  100. package/dist/operate.d.ts +0 -6
  101. package/dist/operate.js +226 -2248
  102. package/dist/phase-routing.d.ts +13 -23
  103. package/dist/phase-routing.js +13 -35
  104. package/dist/plan-auto.js +2 -2
  105. package/dist/plane-review.js +5 -11
  106. package/dist/planner-source.js +2 -1
  107. package/dist/policy.d.ts +1 -4
  108. package/dist/policy.js +0 -8
  109. package/dist/process-recovery-eligibility.js +2 -3
  110. package/dist/project-delete.js +24 -41
  111. package/dist/proposal.d.ts +4 -24
  112. package/dist/proposal.js +3 -88
  113. package/dist/provider-auth.d.ts +3 -3
  114. package/dist/provider-auth.js +4 -6
  115. package/dist/provider.d.ts +11 -25
  116. package/dist/provider.js +4 -36
  117. package/dist/push.js +1 -1
  118. package/dist/recipe-ui.js +6 -6
  119. package/dist/recipes.d.ts +3 -3
  120. package/dist/recipes.js +27 -23
  121. package/dist/result-shots.js +2 -2
  122. package/dist/retention.js +1 -1
  123. package/dist/run-closeout.d.ts +1 -1
  124. package/dist/run-closeout.js +1 -4
  125. package/dist/scope.d.ts +11 -65
  126. package/dist/scope.js +17 -82
  127. package/dist/serve.js +15 -24
  128. package/dist/server/chat.js +7 -30
  129. package/dist/server/flows.js +11 -10
  130. package/dist/server/guards.js +0 -1
  131. package/dist/server/pages-assets.js +1 -1
  132. package/dist/server/pages.js +0 -2
  133. package/dist/server/people-tokens.js +0 -4
  134. package/dist/server/route-table.d.ts +1 -1
  135. package/dist/server/route-table.js +2 -17
  136. package/dist/server/settings.js +6 -12
  137. package/dist/server/shared.d.ts +28 -212
  138. package/dist/server/shared.js +71 -780
  139. package/dist/server/task-views.js +9 -76
  140. package/dist/server/tasks.d.ts +1 -50
  141. package/dist/server/tasks.js +36 -946
  142. package/dist/setup-guide.d.ts +2 -2
  143. package/dist/slack-api.d.ts +5 -1
  144. package/dist/slack-api.js +2 -2
  145. package/dist/slack-chat.d.ts +9 -6
  146. package/dist/slack-chat.js +29 -53
  147. package/dist/slack-settings.js +6 -15
  148. package/dist/slack.js +6 -16
  149. package/dist/store.d.ts +200 -1702
  150. package/dist/store.js +811 -5439
  151. package/dist/surface.js +10 -24
  152. package/dist/task-composer.d.ts +1 -1
  153. package/dist/task-composer.js +1 -1
  154. package/dist/task-control.d.ts +3 -10
  155. package/dist/task-control.js +2 -7
  156. package/dist/teammate-desk.js +1 -1
  157. package/dist/teammate-question.js +12 -12
  158. package/dist/teammates-ui.js +1 -1
  159. package/dist/teams-chat.js +12 -9
  160. package/dist/teams-settings.js +2 -2
  161. package/dist/teams.js +4 -5
  162. package/dist/telegram-flow.js +5 -5
  163. package/dist/telegram-mate.d.ts +0 -3
  164. package/dist/telegram-mate.js +0 -1
  165. package/dist/telegram.js +4 -4
  166. package/dist/templates.d.ts +11 -7
  167. package/dist/templates.js +12 -9
  168. package/dist/toolroll-update.d.ts +20 -0
  169. package/dist/toolroll-update.js +104 -5
  170. package/dist/work-index.js +2 -6
  171. package/dist/workspace-revision.js +3 -1
  172. package/dist/workspace-ui.js +1 -1
  173. package/package.json +1 -1
  174. package/scripts/recovery-canary.mjs +3 -3
  175. package/dist/contest.d.ts +0 -318
  176. package/dist/contest.js +0 -753
  177. package/dist/held.d.ts +0 -150
  178. package/dist/held.js +0 -799
  179. package/dist/liveness.d.ts +0 -30
  180. package/dist/liveness.js +0 -42
  181. package/dist/pricing.d.ts +0 -45
  182. package/dist/pricing.js +0 -77
  183. package/dist/routine.d.ts +0 -228
  184. package/dist/routine.js +0 -764
  185. package/dist/slack-state.d.ts +0 -13
  186. package/dist/slack-state.js +0 -10
  187. package/dist/supervisor.mjs +0 -297
@@ -10,7 +10,6 @@ import { codingHandoffPreview, createCodingHandoff } from '../coding-handoff.js'
10
10
  import { codingShippingHtml } from '../coding-shipping-ui.js';
11
11
  import { codingWorkspaceHtml, codingWorkspaceScript } from '../coding-ui.js';
12
12
  import { CodingActionError } from '../coding-workspace.js';
13
- import { abandonContest, buildPickView, computePickPlan, finalizeContestPick, nonceHashOf, pickTupleDigest, planComparison, planTournament } from "../contest.js";
14
13
  import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
15
14
  import { hasForbiddenControls, validateNote } from "../decision.js";
16
15
  import { diagnoseTaskDispatch } from "../dispatch.js";
@@ -19,16 +18,15 @@ import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../
19
18
  import { computeGaps } from "../gaps.js";
20
19
  import { limitsView } from "../limits-ui.js";
21
20
  import { readLiveWindow } from "../live.js";
22
- import { modeTermsFromJson } from "../modes.js";
23
21
  import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
24
22
  import { REMOTE_MESSAGES } from "../operate-remote.js";
25
23
  import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
26
- import { isRiskLevel, isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
24
+ import { isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
27
25
  import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
28
26
  import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
29
27
  import { levelOfProfile } from "../policy.js";
30
28
  import { authorizedProject, canonicalProject, projectName, rowVisible } from "../project.js";
31
- import { fileRoutineProposal, fileTaskProposal } from "../proposal.js";
29
+ import { fileTaskProposal } from "../proposal.js";
32
30
  import { isProviderId, validateSpec } from "../provider.js";
33
31
  import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
34
32
  import { isQualityMode } from "../quality.js";
@@ -36,21 +34,19 @@ import { createResultRevision, requestResultChanges } from "../result-actions.js
36
34
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
37
35
  import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
38
36
  import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
39
- import { approveRoutine, fireRoutine, refreshRoutineAgents, routineAgentsState } from "../routine.js";
40
37
  import { isAlive as runnerAlive } from "../runner.js";
41
- import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, attendedDigestOf, attendedTermsJson, canonicalProfileJson, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, profileDigestOf, profileFromJson, proposeGuarded } from "../scope.js";
38
+ import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
42
39
  import { teammateNames as teammateNamesOf } from "../spend.js";
43
40
  import { verifiedAuthor } from "../store.js";
44
41
  import { runCostWords } from "../summary.js";
45
- import { composerSchedule } from "../task-composer.js";
46
42
  import { requestTaskStop, resumeTaskStop } from "../task-control.js";
47
43
  import { templateByName } from "../templates.js";
48
44
  import { TEXT_LIMITS } from "../text-limits.js";
49
45
  import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
50
46
  import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
51
- import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest, approveRefusalWords, attendedWatchWords, boardBody, chatReturnWithSaid, checkProgressHtml, consentDoorOf, contestCeremonyPage, contestPage, decisionPage, donePage, editorFileHref, escape, inboxFingerprints, inboxPage, matchTaskPath, newTaskPage, nextPage, oneLineOf, parseInboxTab, PermissionsWouldChange, proofBundleView, QUEUE_FRONT, QUEUE_VIEW, queueBody, queueScript, rankReviewQueue, redirect, refuse, regionScript, requestContext, requirementsFromEditor, respond, RESULT_REFUSALS, resumeDigestOf, REVIEW_QUEUE_CAP, reviewCockpitPage, reviewHref, reviewPriorityOf, routineScreenPage, routinesPage, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, safeChatReturn, safeReturn, SAFETY, screen, TASK_STATES, taskChatHref, taskHref, taskOf, tasksPage, transcriptScript, whenTime, withRefusal, workPage } from "./shared.js";
47
+ import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest, nonceHashOf, approveRefusalWords, boardBody, chatReturnWithSaid, checkProgressHtml, consentDoorOf, decisionPage, donePage, editorFileHref, escape, inboxFingerprints, inboxPage, matchTaskPath, newTaskPage, nextPage, oneLineOf, parseInboxTab, PermissionsWouldChange, proofBundleView, QUEUE_FRONT, QUEUE_VIEW, queueBody, queueScript, rankReviewQueue, redirect, refuse, regionScript, requestContext, requirementsFromEditor, respond, RESULT_REFUSALS, resumeDigestOf, REVIEW_QUEUE_CAP, reviewCockpitPage, reviewHref, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, safeChatReturn, safeReturn, SAFETY, screen, TASK_STATES, taskChatHref, taskHref, taskOf, tasksPage, transcriptScript, whenTime, withRefusal, workPage } from "./shared.js";
52
48
  export function createTasksHandlers(runtime) {
53
- const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, allowedHost, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, matePrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
49
+ const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, matePrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
54
50
  async function get(ctx) {
55
51
  const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
56
52
  if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
@@ -273,8 +269,7 @@ export function createTasksHandlers(runtime) {
273
269
  const ref = store.lookupRef(item.approval.taskId);
274
270
  const scope = store.getScope(item.approval.taskId);
275
271
  const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
276
- const raceTerms = ref === null ? null : store.activeTournamentTerms(ref.id);
277
- const approvalDigest = approvalFormDigest(item.approval.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
272
+ const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
278
273
  // The same agents block the task page and chat sign under (v48),
279
274
  // and the same consent door: closed, it mints no nonce.
280
275
  const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
@@ -284,7 +279,6 @@ export function createTasksHandlers(runtime) {
284
279
  planDocument: planView?.document ?? null,
285
280
  planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
286
281
  approvalDigest,
287
- raceTerms,
288
282
  deliverable: ref?.deliverable ?? "branch",
289
283
  route,
290
284
  csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
@@ -292,7 +286,7 @@ export function createTasksHandlers(runtime) {
292
286
  }
293
287
  return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
294
288
  item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
295
- approvalDigest: null, raceTerms: null, route: null,
289
+ approvalDigest: null, route: null,
296
290
  remaining: remaining.length, skipped: [...skipped], now,
297
291
  }));
298
292
  }
@@ -373,14 +367,8 @@ export function createTasksHandlers(runtime) {
373
367
  repairChain,
374
368
  });
375
369
  });
376
- // Instances belong to their track row, not the main lanes — the board
377
- // is for one-off work; tracks are the heartbeat. The one exception is
378
- // attention: anything needing a person surfaces, wearing its routine.
379
- const laneCards = [...cards.filter(card => card.routineName === null || card.lane === "attention"), ...unverifiedCards];
380
- const tracks = store
381
- .routineTracks(all ? null : project, now, admission)
382
- .filter(track => visible(track.routine.repo));
383
- const body = boardBody({ cards: laneCards, tracks, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
370
+ const laneCards = [...cards, ...unverifiedCards];
371
+ const body = boardBody({ cards: laneCards, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
384
372
  if (url.searchParams.get("fragment") === "1") {
385
373
  // The live region alone — the in-page swapper's diet. Same auth,
386
374
  // same ceiling, no shell, no scripts (finding 2).
@@ -411,7 +399,7 @@ export function createTasksHandlers(runtime) {
411
399
  // The review cockpit (Priority 5): every visible COMPLETED task,
412
400
  // ranked by review priority, with one selected result projected
413
401
  // from the records the task, run, and done pages already read —
414
- // scope, plan, run, artifacts, verdict, contest, publication. The
402
+ // scope, plan, run, artifacts, verdict, publication. The
415
403
  // ranking is a labeled presentation aid: it never rewrites the
416
404
  // stored verdict, and the sealed patch downloads exactly as stored.
417
405
  // Admission binds BEFORE the SQL limit (the done page's own rule),
@@ -527,8 +515,8 @@ export function createTasksHandlers(runtime) {
527
515
  const csrf = who.via === "cookie" ? who.session.csrf : "";
528
516
  const revision = who.via === "cookie" ? who.session.projectRevision : 0;
529
517
  // Column headers read one thing beyond the queue snapshot: live claims
530
- // in THIS project, per worker (the capacity is global; attended claims
531
- // may exceed it), so the header names both and never a ratio.
518
+ // in THIS project, per worker (the capacity is global), so the header
519
+ // names both and never a ratio.
532
520
  if (url.searchParams.get("fragment") === "1") {
533
521
  return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
534
522
  }
@@ -722,26 +710,7 @@ export function createTasksHandlers(runtime) {
722
710
  found.runner === options.localRunner &&
723
711
  who.via === "cookie"
724
712
  ? { on: who.session.editorLinks === true }
725
- : null, url.searchParams.get("noted") !== null, (() => {
726
- const held = store.heldSessionOf(found.id);
727
- if (held === null)
728
- return null;
729
- const authorization = store.readAuthorization(held.authorizationId);
730
- return {
731
- turns: store.sessionTurnsOf(found.id),
732
- open: held.endedAt === null && held.state === "open",
733
- state: authorization === null
734
- ? "session record"
735
- : attendedWatchWords(authorization.lastBeatAt, now, authorization.absoluteExpiry),
736
- cap: authorization?.maxSessionTurns ?? 0,
737
- };
738
- })(), options.attended !== undefined &&
739
- who.via === "cookie" &&
740
- !store.isDemo() &&
741
- (found.outcome === "built" || found.outcome === "no-change") &&
742
- store.openAuthorizationFor(found.taskRef) === null
743
- ? { taskId }
744
- : null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
713
+ : null, url.searchParams.get("noted") !== null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
745
714
  // The shared result presentation (package 3) for a finished result.
746
715
  !running && (found.outcome === "built" || found.outcome === "no-change")
747
716
  ? { detail: resultDetailOf(found, who, now), tab: parseResultTab(url.searchParams.get("tab")), user: who.name, requestToken: randomBytes(16).toString("hex") }
@@ -751,44 +720,9 @@ export function createTasksHandlers(runtime) {
751
720
  if (runArtifact !== null) {
752
721
  return runEvidence(response, Number(runArtifact[1]), Number(runArtifact[2]));
753
722
  }
754
- const contestPath = /^\/contest\/([0-9]{1,15})$/.exec(url.pathname);
755
- if (contestPath !== null) {
756
- return contestScreen(response, who, Number(contestPath[1]), null, 200);
757
- }
758
- if (url.pathname === "/routines") {
759
- // The ceiling row by row, exactly as everywhere: a routine placed in
760
- // a repo this server may not serve does not exist here.
761
- const tracks = store
762
- .routineTracks(project, now)
763
- .filter(track => visible(track.routine.repo));
764
- // ?template=<name> pre-fills the filing form from the shipped
765
- // library — a pre-filled form, same guarded submission path.
766
- const fromTemplate = url.searchParams.get("template");
767
- const picked = fromTemplate === null ? null : templateByName(fromTemplate);
768
- return sendScreen(response, 200, routinesPage(chromeFor(project, "routines"), tracks, {
769
- csrf: who.via === "cookie" ? who.session.csrf : "",
770
- revision: who.via === "cookie" ? who.session.projectRevision : 0,
771
- problem: null,
772
- prefill: picked !== null && picked.kind === "routine"
773
- ? {
774
- name: picked.routineName,
775
- goal: picked.goal,
776
- not: picked.outOfScope ?? "",
777
- touches: picked.touches.join(", "),
778
- schedule: picked.schedule,
779
- acceptance: acceptanceToLines(picked.acceptance).join("\n"),
780
- }
781
- : null,
782
- }));
783
- }
784
- const routineScreen = /^\/routines\/([0-9]{1,15})$/.exec(url.pathname);
785
- if (routineScreen !== null) {
786
- const routine = store.getRoutine(Number(routineScreen[1]));
787
- if (routine === null || !visible(routine.repo)) {
788
- return refuse(response, who, 404, "no such routine", "/routines");
789
- }
790
- return routinePage(response, who, routine.id, null, 200);
791
- }
723
+ // v115: routines became scheduled flows; an old link to the list or to one routine lands on the flows page.
724
+ if (url.pathname === "/routines" || /^\/routines\/[0-9]{1,15}$/.test(url.pathname))
725
+ return redirect(response, "/flows");
792
726
  const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
793
727
  if (one !== null) {
794
728
  const decision = store.getDecision(Number(one[1]));
@@ -811,17 +745,6 @@ export function createTasksHandlers(runtime) {
811
745
  }
812
746
  async function post(ctx) {
813
747
  const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
814
- // The attended beat answers BEFORE the shared mutation guard (v28): it
815
- // carries no parameters, so there is no csrf token to check — its OWN
816
- // guard is complete and STRICTER for the browsers this console
817
- // supports: cookie session, form content-type (form() enforced it),
818
- // and `Sec-Fetch-Site: same-origin`, which cross-site POSTs cannot
819
- // send and same-origin fetch always does. Renewal-only: an attacker
820
- // who somehow posted could only keep the operator's OWN sessions from
821
- // lapsing — no mint, no spend, no read.
822
- if (url.pathname === "/session/attended-beats") {
823
- return attendedBeats(who, request, response, clock());
824
- }
825
748
  if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
826
749
  const body = readForm(posted, CONSOLE_FORMS.code);
827
750
  const wantsJson = request.headers.accept?.includes('application/json') === true;
@@ -1060,7 +983,7 @@ export function createTasksHandlers(runtime) {
1060
983
  };
1061
984
  const moveReason = (reason) => reason === "stale"
1062
985
  ? "the queue moved underneath you — it just reloaded"
1063
- : reason === "claimed" || reason === "contest-open"
986
+ : reason === "claimed"
1064
987
  ? "that task is being taken right now — it keeps its claim"
1065
988
  : reason === "worker-retired"
1066
989
  ? "that worker is retired — drag its work elsewhere, or register the name again"
@@ -1098,11 +1021,11 @@ export function createTasksHandlers(runtime) {
1098
1021
  const now = clock();
1099
1022
  const snapshot = store.queueScoped(ref.repo, now);
1100
1023
  const self = snapshot.find(one => one.id === taskId);
1101
- // A claim or a contest can land after the form rendered WITHOUT
1024
+ // A claim can land after the form rendered WITHOUT
1102
1025
  // bumping queueRevision, so the snapshot — not the form — decides
1103
1026
  // whether the card is still free. A taken or vanished card is the
1104
1027
  // typed refusal, never a silent no-op that would skip moveTask()'s
1105
- // own claimed/contest recheck.
1028
+ // own claimed recheck.
1106
1029
  if (self === undefined)
1107
1030
  return respondMove(409, moveReason("unknown-task"));
1108
1031
  if (self.taken)
@@ -1173,87 +1096,6 @@ export function createTasksHandlers(runtime) {
1173
1096
  }
1174
1097
  return redirect(response, decisionBack);
1175
1098
  }
1176
- const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
1177
- if (contestAct !== null) {
1178
- if (who.via !== "cookie")
1179
- return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
1180
- const body = readForm(posted, CONSOLE_FORMS.contest);
1181
- const contestId = Number(contestAct[1]);
1182
- const verb = contestAct[2];
1183
- const data = contestData(contestId);
1184
- if (data === null)
1185
- return refuse(response, who, 404, "no such tournament", "/board");
1186
- const { view } = data;
1187
- if (verb === "arm") {
1188
- // The ceremony's nonce is minted by THIS POST, never by a GET
1189
- // (round-3 finding 30): a prefetched or crawled page must not mint
1190
- // anything. The response is the ceremony form itself, carrying the
1191
- // nonce value; its hash lives in a durable row bound to the exact
1192
- // tuple digest being restated.
1193
- const abandoning = body.get("act") === "abandon";
1194
- if (abandoning) {
1195
- if (!["pick-wait", "exhausted", "interrupted", "decision-wait"].includes(view.contest.state)) {
1196
- return contestScreen(response, who, contestId, "this tournament is not in a state an operator can abandon", 409);
1197
- }
1198
- const digest = pickTupleDigest(view, { abandon: true }, { grant: null, head: null });
1199
- const nonceValue = randomBytes(18).toString("base64url");
1200
- const minted = store.mintCeremonyNonce({ hash: nonceHashOf(nonceValue), approver: who.name, subject: "contest-abandon", subjectId: contestId, digest, ttlMs: 15 * 60_000 }, now);
1201
- if (!minted.ok)
1202
- return contestScreen(response, who, contestId, "too many unfinished confirmations are open — finish or let them expire", 429);
1203
- return sendScreen(response, 200, contestCeremonyPage(chromeFor(who.via === "cookie" ? who.session.project : null, "tasks"), {
1204
- kind: "abandon", contestKind: view.contest.kind, contestId, taskId: data.taskId, taskTitle: data.taskTitle,
1205
- agents: view.agents.length, totalMicrousd: data.totalMicrousd, anyUnknown: data.anyUnknown,
1206
- nonceValue, csrf: who.via === "cookie" ? who.session.csrf : "",
1207
- }));
1208
- }
1209
- if (view.contest.state !== "pick-wait") {
1210
- return contestScreen(response, who, contestId, "this tournament is not waiting for a pick", 409);
1211
- }
1212
- const choice = Number(body.get("choice") ?? "");
1213
- const plan = computePickPlan(store, view, choice, data.repo, data.refOrigin);
1214
- if (!plan.ok)
1215
- return contestScreen(response, who, contestId, plan.message, 409);
1216
- const nonceValue = randomBytes(18).toString("base64url");
1217
- const minted = store.mintCeremonyNonce({ hash: nonceHashOf(nonceValue), approver: who.name, subject: "contest-pick", subjectId: contestId, digest: plan.digest, ttlMs: 15 * 60_000 }, now);
1218
- if (!minted.ok)
1219
- return contestScreen(response, who, contestId, "too many unfinished confirmations are open — finish or let them expire", 429);
1220
- return sendScreen(response, 200, contestCeremonyPage(chromeFor(who.via === "cookie" ? who.session.project : null, "tasks"), {
1221
- kind: "pick", contestKind: view.contest.kind, contestId, taskId: data.taskId, taskTitle: data.taskTitle,
1222
- agents: view.agents.length, totalMicrousd: data.totalMicrousd, anyUnknown: data.anyUnknown,
1223
- chosen: plan.chosen,
1224
- publication: plan.publishable && plan.grant !== null && plan.chosen.run.branch !== null ? { githubRepo: plan.grant.githubRepo, branch: plan.chosen.run.branch, draft: plan.grant.draft } : null,
1225
- nonceValue, csrf: who.via === "cookie" ? who.session.csrf : "",
1226
- }));
1227
- }
1228
- // pick and abandon: the password, typed again, plus the nonce the arm
1229
- // POST minted. The store consumes the nonce conditionally inside the
1230
- // same transaction that moves the tournament — replay finds it gone.
1231
- const token = body.get("token") ?? "";
1232
- if (!authenticateApprover(who, token).ok) {
1233
- return contestScreen(response, who, contestId, "that decision takes your password, typed again", 403);
1234
- }
1235
- const nonceValue = body.get("nonce") ?? "";
1236
- if (nonceValue === "")
1237
- return contestScreen(response, who, contestId, "that confirmation form was incomplete — start again", 400);
1238
- if (verb === "pick") {
1239
- const choice = Number(body.get("choice") ?? "");
1240
- const picked = finalizeContestPick(store, {
1241
- contestId, contestantId: choice, approver: who.name, nonceValue,
1242
- evidenceRoot, repo: data.repo, taskId: data.taskId, refOrigin: data.refOrigin,
1243
- }, now);
1244
- if (!picked.ok)
1245
- return contestScreen(response, who, contestId, picked.message, 409);
1246
- return redirect(response, `/contest/${contestId}`);
1247
- }
1248
- const gone = abandonContest(store, { contestId, approver: who.name, nonceValue, evidenceRoot, taskId: data.taskId }, now);
1249
- if (!gone.ok)
1250
- return contestScreen(response, who, contestId, gone.message, 409);
1251
- return redirect(response, `/contest/${contestId}`);
1252
- }
1253
- const attendAct = matchTaskPath(url.pathname, "/(attend-preview|attend|attend-revoke)$");
1254
- if (attendAct !== null) {
1255
- return attendMutation(response, who, attendAct.taskId, attendAct.verb, readForm(posted, CONSOLE_FORMS.attend), now);
1256
- }
1257
1099
  const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
1258
1100
  if (act !== null && act.verb === "confirm-stopped") {
1259
1101
  const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
@@ -1312,79 +1154,6 @@ export function createTasksHandlers(runtime) {
1312
1154
  if (act !== null) {
1313
1155
  return taskMutation(response, who, act.taskId, act.verb, posted, now);
1314
1156
  }
1315
- if (url.pathname === "/routines/add") {
1316
- const body = readForm(posted, CONSOLE_FORMS.routinesAdd);
1317
- // A standing order files into the OPEN project — never a typed path,
1318
- // so the ceiling question never even arises — and lands on its own
1319
- // screen where the approval step-up already lives: filing is cheap,
1320
- // the yes is the ceremony.
1321
- const project = projectOf(who, request);
1322
- if (project === undefined || project === null) {
1323
- return refuse(response, who, 403, "open a project first — a standing order lives somewhere specific");
1324
- }
1325
- if (who.via === "cookie") {
1326
- const seen = body.get("projectRevision");
1327
- if (seen !== null && seen !== String(who.session.projectRevision)) {
1328
- return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/routines");
1329
- }
1330
- }
1331
- const name = (body.get("name") ?? "").trim();
1332
- const ceilingGiven = (body.get("ceiling") ?? "").trim();
1333
- // Root mode proves the CURRENT project exactly (repo onboarding,
1334
- // finding 24): canonicalize, authorize, and pass [canonical] as the
1335
- // admitted list — the same discipline as task filing, so a routine
1336
- // can land in a fresh clone too.
1337
- const routineRootMode = !unscopedMode && ceiling.roots.length > 0;
1338
- let routineRepo = project;
1339
- let routineAdmitted = unscopedMode ? null : admissionList() ?? [];
1340
- if (routineRootMode) {
1341
- const canonical = (await authorizedProject(liveCeiling(), project)) ? canonicalProject(project) : null;
1342
- if (canonical === null || canonical === undefined) {
1343
- return refuse(response, who, 403, "the open project is outside what this server was configured to show", "/routines");
1344
- }
1345
- routineRepo = canonical;
1346
- routineAdmitted = [canonical];
1347
- }
1348
- // One filing door for every surface (Codex adoption review, finding
1349
- // 7): the service validates, canonicalizes, digests, and stamps
1350
- // provenance; the admission list makes the ceiling explicit even
1351
- // though `project` was already proved inside it.
1352
- const scheduled = body.has("repeat") ? composerSchedule(body.sent) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
1353
- if (!scheduled.ok || scheduled.schedule === null) {
1354
- return sendScreen(response, 400, routinesPage(chromeFor(project, "routines"), store.routineTracks(project, now).filter(track => visible(track.routine.repo)), {
1355
- csrf: who.via === "cookie" ? who.session.csrf : "", revision: who.via === "cookie" ? who.session.projectRevision : 0,
1356
- problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body.sent,
1357
- }));
1358
- }
1359
- const created = fileRoutineProposal(store, {
1360
- name,
1361
- repo: routineRepo,
1362
- goal: (body.get("goal") ?? "").trim(),
1363
- outOfScope: (body.get("not") ?? "").trim() || null,
1364
- touches: (body.get("touches") ?? "").split(/[\n,]/).map(one => one.trim()).filter(one => one !== ""),
1365
- acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
1366
- requirements: [],
1367
- schedule: scheduled.schedule,
1368
- costCeilingUsd: ceilingGiven === "" ? null : Number(ceilingGiven),
1369
- filedVia: "console", createdBy: who.name,
1370
- ...(routineAdmitted === null ? {} : { admittedRepos: routineAdmitted }),
1371
- }, now);
1372
- if (created.ok && routineRootMode)
1373
- store.upsertProject(routineRepo, projectName(routineRepo), now);
1374
- if (!created.ok) {
1375
- const tracks = store.routineTracks(project, now).filter(track => visible(track.routine.repo));
1376
- return sendScreen(response, created.reason === "duplicate" ? 409 : 400, routinesPage(chromeFor(project, "routines"), tracks, {
1377
- csrf: who.via === "cookie" ? who.session.csrf : "",
1378
- revision: who.via === "cookie" ? who.session.projectRevision : 0,
1379
- problem: created.message, values: body.sent,
1380
- }));
1381
- }
1382
- return redirect(response, `/routines/${created.id}`);
1383
- }
1384
- const routineAct = /^\/routines\/([0-9]{1,15})\/(approve|refresh|pause|resume|run-now)$/.exec(url.pathname);
1385
- if (routineAct !== null) {
1386
- return routineMutation(response, who, Number(routineAct[1]), routineAct[2], readForm(posted, CONSOLE_FORMS.routine), now);
1387
- }
1388
1157
  const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
1389
1158
  if (runNote !== null) {
1390
1159
  const body = readForm(posted, CONSOLE_FORMS.runNote);
@@ -1505,43 +1274,6 @@ export function createTasksHandlers(runtime) {
1505
1274
  const back = body.get("return") ?? "/";
1506
1275
  return redirect(response, /^\/[a-z0-9/_-]*$/i.test(back) ? back : "/");
1507
1276
  }
1508
- const turnAct = /^\/r\/([0-9]{1,15})\/turn$/.exec(url.pathname);
1509
- if (turnAct !== null) {
1510
- const body = readForm(posted, CONSOLE_FORMS.turn);
1511
- // The operator's turn (Phase 2E, v2 S1g): cookie-only — a watching
1512
- // person, never a bearer machine — and every hard gate (custody,
1513
- // lease, cap, budget, open decision, single flight) re-proves
1514
- // ATOMICALLY inside the recording transaction. Words here only map
1515
- // the refusal tokens to sentences.
1516
- if (who.via !== "cookie")
1517
- return refuse(response, who, 403, "turns are a browser session's act");
1518
- const id = Number(turnAct[1]);
1519
- const found = store.getRun(id);
1520
- if (found === null || !visible(taskRepoOf(found.taskRef))) {
1521
- return refuse(response, who, 404, "no such run");
1522
- }
1523
- const text = (body.get("text") ?? "").trim();
1524
- if (text === "" || text.length > 500) {
1525
- return refuse(response, who, 400, "a turn is 1 to 500 characters", `/r/${id}`);
1526
- }
1527
- const coordinator = options.attended?.coordinator;
1528
- if (coordinator === undefined)
1529
- return refuse(response, who, 409, "this console is not holding the session", `/r/${id}`);
1530
- const injected = coordinator.injectOperatorTurn(id, who.name, text);
1531
- if (!injected.ok) {
1532
- const words = {
1533
- "no-held-session": "the session is not held here anymore",
1534
- fenced: "the session is winding down — nothing more reaches it",
1535
- "turn-open": "the agent is still working on the last message — wait for it to settle",
1536
- "turn-cap": "the session's message cap is reached — authorize a new session for more",
1537
- "budget-exhausted": "the session's budget is spent",
1538
- "decision-open": "answer the waiting question first — it is on this page",
1539
- "write-failed": "the message could not reach the agent — it was not charged",
1540
- };
1541
- return refuse(response, who, 409, words[injected.reason] ?? injected.reason, `/r/${id}`);
1542
- }
1543
- return redirect(response, `/r/${id}`);
1544
- }
1545
1277
  const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
1546
1278
  if (revise !== null) {
1547
1279
  const body = readForm(posted, CONSOLE_FORMS.revise);
@@ -1806,259 +1538,7 @@ export function createTasksHandlers(runtime) {
1806
1538
  peekBySession.set(sessionKey, left);
1807
1539
  }
1808
1540
  }
1809
- function contestScreen(response, who, contestId, problem, status) {
1810
- const data = contestData(contestId);
1811
- if (data === null)
1812
- return refuse(response, who, 404, "no such tournament", "/board");
1813
- const paneProject = who.via === "cookie" ? who.session.project : null;
1814
- const diffs = new Map();
1815
- for (const agent of data.view.agents) {
1816
- if (agent.run !== null)
1817
- diffs.set(agent.contestant.id, terminalDiffView(store.artifactsFor(agent.run.id), evidenceRoot));
1818
- }
1819
- return sendScreen(response, status, contestPage(chromeFor(paneProject, "tasks"), {
1820
- ...data,
1821
- diffs,
1822
- csrf: who.via === "cookie" ? who.session.csrf : "",
1823
- problem,
1824
- }));
1825
- }
1826
- /**
1827
- * Routine verbs. The ceiling check is independent of authorizeMutation
1828
- * (Codex round 2, finding 7): the routine's repository is resolved
1829
- * server-side and proved against this server's configuration before any
1830
- * verb runs, whatever the request named.
1831
- */
1832
- function routineMutation(response, who, routineId, verb, body, now) {
1833
- const routine = store.getRoutine(routineId);
1834
- if (routine === null || !visible(routine.repo)) {
1835
- return refuse(response, who, 404, "no such routine", "/routines");
1836
- }
1837
- switch (verb) {
1838
- case "approve": {
1839
- if (who.via !== "cookie")
1840
- return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
1841
- // Step-up, identical to a scope's: the session got you here; only
1842
- // the password agrees. The digest names what was seen.
1843
- const digest = body.get("digest") ?? "";
1844
- const token = body.get("token") ?? "";
1845
- const nonce = body.get("nonce") ?? "";
1846
- if (!consumeApprovalNonce(nonce, who.name, `routine:${routine.id}`, digest)) {
1847
- return routinePage(response, who, routineId, "that approval form is stale — read it again", 409);
1848
- }
1849
- if (token === "" && !hasFreshIdentitySignIn(who.name)) {
1850
- return routinePage(response, who, routineId, "approval requires your password, typed again", 400);
1851
- }
1852
- const approved = approveRoutine(store, routineId, who.name, now, digest, token);
1853
- if (!approved.ok) {
1854
- const status = approved.reason === "changed" ? 409 : 403;
1855
- const words = approved.reason === "profile-unresolved"
1856
- ? "not approved: the routine cannot name an exact agent for every role — configure the project's agents, then file the standing order again"
1857
- : approved.reason === "requester"
1858
- ? "You made this standing order, and this project needs someone else to approve it."
1859
- : `not approved: ${approved.reason}`;
1860
- return routinePage(response, who, routineId, words, status);
1861
- }
1862
- return redirect(response, `/routines/${routineId}`);
1863
- }
1864
- case "refresh": {
1865
- // THE RECOVERY ROAD (v48): re-resolve the agents from today's
1866
- // configuration and file them as the order's working agents. This
1867
- // approves nothing — the page then shows the exact agents and asks
1868
- // for the password again.
1869
- const refreshed = refreshRoutineAgents(store, routineId, now);
1870
- if (!refreshed.ok) {
1871
- return routinePage(response, who, routineId, `agents not refreshed: ${refreshed.problem}`, 409);
1872
- }
1873
- return redirect(response, `/routines/${routineId}`);
1874
- }
1875
- case "pause":
1876
- case "resume": {
1877
- store.setRoutinePaused(routineId, verb === "pause", now);
1878
- return redirect(response, `/routines/${routineId}`);
1879
- }
1880
- case "run-now": {
1881
- if (who.via !== "cookie")
1882
- return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
1883
- // Step-up (Codex Phase C review, M3): run-now is spend outside the
1884
- // approved schedule, so a session alone cannot ask for it — the
1885
- // password is typed again, like an approval.
1886
- const token = body.get("token") ?? "";
1887
- if (token === "" && !hasFreshIdentitySignIn(who.name)) {
1888
- return routinePage(response, who, routineId, "run now requires your password, typed again", 400);
1889
- }
1890
- const authenticated = authenticateApprover(who, token);
1891
- if (!authenticated.ok) {
1892
- return routinePage(response, who, routineId, "that is not your password", 403);
1893
- }
1894
- const outcome = fireRoutine(store, routineId, now, { manual: true });
1895
- if (!outcome.ok) {
1896
- return routinePage(response, who, routineId, `not fired: ${outcome.detail ?? outcome.reason}`, 409);
1897
- }
1898
- return redirect(response, `/routines/${routineId}`);
1899
- }
1900
- default:
1901
- return refuse(response, who, 404, "That action isn't available here.", `/routines/${routineId}`);
1902
- }
1903
- }
1904
1541
  // ---- mutations -----------------------------------------------------------
1905
- /** v28: the console-scoped liveness beat — see the route note in
1906
- * handlePost. Supersedes v2 S2f's per-page id binding, reversed
1907
- * KNOWINGLY: with parallel sessions one foregrounded tab per session is
1908
- * impossible, and page-binding was attention theater over what was
1909
- * always renewal-of-use. Approver-bound; never extends cookies, never
1910
- * touches absolute expiry, never mints. */
1911
- function attendedBeats(who, request, response, now) {
1912
- if (who.via !== "cookie")
1913
- return refuse(response, who, 403, "watching is a browser session's act");
1914
- // Round-2 finding 2's named miss: renewal is an approver's act — a
1915
- // viewer's open tab keeps nothing alive.
1916
- if (who.role !== "approver")
1917
- return refuse(response, who, 403, "your login can watch, not keep sessions alive");
1918
- if (request.headers["sec-fetch-site"] !== "same-origin") {
1919
- return refuse(response, who, 403, "the beat only answers this console's own pages");
1920
- }
1921
- // Belt with the braces: a PRESENT Origin/Referer must also name this
1922
- // server — the same allowlist the shared guard applies.
1923
- const namedOrigin = typeof request.headers.origin === "string" && request.headers.origin !== "null"
1924
- ? request.headers.origin
1925
- : typeof request.headers.referer === "string"
1926
- ? request.headers.referer
1927
- : null;
1928
- if (namedOrigin !== null && !allowedHost(namedOrigin.replace(/^https?:[/][/]/, "").split("/")[0])) {
1929
- return refuse(response, who, 403, "origin not allowed");
1930
- }
1931
- const attendedRunner = options.attended?.runner;
1932
- let beaten = 0;
1933
- if (attendedRunner !== undefined) {
1934
- for (const open of store.openAuthorizationsOf(attendedRunner)) {
1935
- if (open.approver !== who.name)
1936
- continue;
1937
- if (Date.parse(open.absoluteExpiry) <= now.getTime())
1938
- continue;
1939
- // Only terms SIGNED for console-wide renewal are renewed here
1940
- // (round-1 finding 2): a legacy page-bound signature never
1941
- // silently acquires the wider mode — it simply lapses.
1942
- try {
1943
- const signedMode = JSON.parse(open.termsJson).attentionMode;
1944
- if (signedMode !== "console-visible")
1945
- continue;
1946
- }
1947
- catch {
1948
- continue;
1949
- }
1950
- const beatRef = store.refForId(open.taskRef);
1951
- if (beatRef === null || !visible(beatRef.repo))
1952
- continue;
1953
- store.beatAuthorization(open.id, now);
1954
- if (open.attemptRun !== null)
1955
- options.attended?.coordinator?.poke(open.attemptRun);
1956
- beaten++;
1957
- }
1958
- }
1959
- return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, beaten }));
1960
- }
1961
- async function attendMutation(response, who, taskId, verb, body, now) {
1962
- const ref = store.lookupRef(taskId);
1963
- if (ref === null || store.getTask(taskId) === null || !visible(ref.repo)) {
1964
- return refuse(response, who, 404, "no such task", "/tasks");
1965
- }
1966
- if (who.via !== "cookie")
1967
- return refuse(response, who, 403, "watching is a browser session's act");
1968
- if (store.isDemo())
1969
- return refuse(response, who, 403, "the demo authorizes nothing");
1970
- if (verb === "attend-revoke") {
1971
- const open = store.openAuthorizationFor(ref.id);
1972
- if (open === null)
1973
- return refuse(response, who, 409, "nothing to revoke", taskHref(taskId));
1974
- store.closeAuthorization(open.id, "revoked", now);
1975
- if (open.attemptRun !== null)
1976
- options.attended?.coordinator?.poke(open.attemptRun);
1977
- return redirect(response, taskHref(taskId));
1978
- }
1979
- const parentGiven = body.get("parent");
1980
- const followupGiven = body.get("followup");
1981
- const inputs = {
1982
- minutes: Number(body.get("minutes") ?? "60"),
1983
- turns: Number(body.get("turns") ?? "20"),
1984
- budgetMicrousd: Number(body.get("budget") ?? String(store.getScope(taskId)?.budgetMicrousd ?? 2_000_000)),
1985
- ...(body.get("expiry") === null ? {} : { expiry: body.get("expiry") }),
1986
- ...(parentGiven === null || parentGiven === "" ? {} : { parent: Number(parentGiven) }),
1987
- ...(followupGiven === null ? {} : { followup: followupGiven }),
1988
- ...(body.get("model") === null ? {} : { model: body.get("model") }),
1989
- ...(body.get("posture") === null ? {} : { posture: body.get("posture") }),
1990
- };
1991
- if (store.openAuthorizationFor(ref.id) !== null) {
1992
- return refuse(response, who, 409, "an authorization is already open — revoke it first", taskHref(taskId));
1993
- }
1994
- const live = await liveAttendedTerms(taskId, inputs, now);
1995
- if (!live.ok)
1996
- return refuse(response, who, 409, live.problem, taskHref(taskId));
1997
- // Sprint 8: an attended session runs at exactly what the person signs, so the organisation policy refuses it
1998
- // outright (a provider, a model, or a posture above the permission ceiling) before anyone signs.
1999
- {
2000
- const pinned = profileFromJson(live.terms.profileJson);
2001
- const refused = pinned === null ? null : store.attendedPolicyRefusal(pinned);
2002
- if (refused !== null)
2003
- return refuse(response, who, 403, refused, taskHref(taskId));
2004
- }
2005
- const digest = attendedDigestOf(live.terms);
2006
- if (verb === "attend-preview") {
2007
- const nonce = mintApprovalNonce(who.name, `attend-${taskId}`, digest);
2008
- return attendConfirmScreen(response, who, live.terms, inputs, digest, nonce, who.session.csrf);
2009
- }
2010
- // attend: the yes. The nonce proves THIS form; the digest re-derived
2011
- // from live state proves the world held between reading and signing.
2012
- const nonce = body.get("nonce") ?? "";
2013
- if (!consumeApprovalNonce(nonce, who.name, `attend-${taskId}`, body.get("digest") ?? "")) {
2014
- return refuse(response, who, 409, "that form is stale — read it again", taskHref(taskId));
2015
- }
2016
- if (digest !== (body.get("digest") ?? "")) {
2017
- return refuse(response, who, 409, "the world moved while you were reading (scope, model, or head) — read it again", taskHref(taskId));
2018
- }
2019
- const token = body.get("token") ?? "";
2020
- let basis;
2021
- if (token === "" && !hasFreshIdentitySignIn(who.name)) {
2022
- // Quick mint (C2/M4): no password typed — valid ONLY when a live
2023
- // mode with quickMint was signed by THIS session's person. The mint
2024
- // transaction re-proves it; this pre-check only shapes the refusal.
2025
- const mode = ref.repo === null ? null : store.activeMode(ref.repo, now);
2026
- const modeTerms = mode === null ? null : modeTermsFromJson(mode.termsJson);
2027
- if (mode === null || modeTerms === null || !modeTerms.quickMint || mode.signedBy !== who.name) {
2028
- return refuse(response, who, 403, "authorizing takes your password, typed again", taskHref(taskId));
2029
- }
2030
- basis = { kind: "mode", digest: mode.digest };
2031
- }
2032
- else if (!authenticateApprover(who, token).ok) {
2033
- return refuse(response, who, 403, "authorizing takes your password, typed again", taskHref(taskId));
2034
- }
2035
- const minted = store.mintAttendedAuthorization({
2036
- id: randomUUID(),
2037
- taskRef: ref.id,
2038
- approver: who.name,
2039
- runner: live.terms.runner,
2040
- runnerGeneration: live.terms.runnerGeneration,
2041
- compositeDigest: digest,
2042
- termsJson: attendedTermsJson(live.terms),
2043
- maxSessionTurns: live.terms.maxSessionTurns,
2044
- budgetMicrousd: live.terms.budgetMicrousd,
2045
- ...(live.terms.parentRun == null ? {} : { parentRun: live.terms.parentRun }),
2046
- ...(live.terms.followup == null ? {} : { followup: live.terms.followup }),
2047
- absoluteExpiry: live.terms.absoluteExpiry,
2048
- ...(basis === undefined ? {} : { basis }),
2049
- now,
2050
- });
2051
- if (!minted.ok) {
2052
- return refuse(response, who, minted.reason === "authorization-open" ? 409 : 403, minted.reason === "mode-ended"
2053
- ? "the mode that covered quick minting has ended — your password, typed again, still works"
2054
- : minted.reason === "approval-rules"
2055
- ? "this project's approval rules need someone else's approval (or two people's) — a watched run can't stand in for it"
2056
- : "an authorization is already open — revoke it first", taskHref(taskId));
2057
- }
2058
- // The first beat is the mint itself: the person is visibly here.
2059
- store.beatAuthorization(minted.authorization.id, now);
2060
- return redirect(response, taskHref(taskId));
2061
- }
2062
1542
  function taskMutation(response, who, taskId, verb, posted, now) {
2063
1543
  const ref = store.lookupRef(taskId);
2064
1544
  if (ref === null || store.getTask(taskId) === null) {
@@ -2079,13 +1559,11 @@ export function createTasksHandlers(runtime) {
2079
1559
  // The session IS the verified principal here — cookie + CSRF proved it.
2080
1560
  const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now);
2081
1561
  if (!filed.ok) {
2082
- const said = filed.reason === "contest-open"
2083
- ? "agents are racing on this task — steering waits until the tournament settles"
2084
- : filed.reason === "task-finished"
2085
- ? "this task is finished — a note has no next attempt to reach"
2086
- : filed.reason === "invalid-note"
2087
- ? (filed.problem ?? "that note will not store")
2088
- : "no such task";
1562
+ const said = filed.reason === "task-finished"
1563
+ ? "this task is finished — a note has no next attempt to reach"
1564
+ : filed.reason === "invalid-note"
1565
+ ? (filed.problem ?? "that note will not store")
1566
+ : "no such task";
2089
1567
  return taskScreen(response, who, taskId, said, 400);
2090
1568
  }
2091
1569
  return redirect(response, taskHref(taskId));
@@ -2107,7 +1585,7 @@ export function createTasksHandlers(runtime) {
2107
1585
  // Chains are scheduling, not authority (chains-and-next review,
2108
1586
  // finding 2): the edge decides WHEN the ready set admits the task;
2109
1587
  // approval still decides WHAT may build. Both ends are re-proved
2110
- // here — existence, ceiling, and the tournament guard — and the
1588
+ // here — existence and ceiling — and the
2111
1589
  // cycle refusal comes from the store's own closure check.
2112
1590
  const on = (body.get("on") ?? "").trim();
2113
1591
  const blocker = on === "" ? null : store.getTask(on);
@@ -2115,9 +1593,6 @@ export function createTasksHandlers(runtime) {
2115
1593
  if (blocker === null || blockerRef === null || !visible(blockerRef.repo)) {
2116
1594
  return taskScreen(response, who, taskId, "that task to wait for does not exist here", 404);
2117
1595
  }
2118
- if (store.openContestFor(ref.id) !== null) {
2119
- return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
2120
- }
2121
1596
  const added = store.addEdge(taskId, on);
2122
1597
  if (!added.ok) {
2123
1598
  return taskScreen(response, who, taskId, `could not make ${taskId} wait for ${on} — ${added.reason}`, 409);
@@ -2127,9 +1602,6 @@ export function createTasksHandlers(runtime) {
2127
1602
  case "unblock": {
2128
1603
  const body = readForm(posted, CONSOLE_FORMS.taskBlock);
2129
1604
  const on = (body.get("on") ?? "").trim();
2130
- if (store.openContestFor(ref.id) !== null) {
2131
- return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
2132
- }
2133
1605
  const removed = store.removeEdge(taskId, on);
2134
1606
  if (!removed.ok) {
2135
1607
  return taskScreen(response, who, taskId, `${taskId} was not waiting on ${on}`, 409);
@@ -2144,9 +1616,6 @@ export function createTasksHandlers(runtime) {
2144
1616
  if (blockerTask === null || (blockerTask.state !== "failed" && blockerTask.state !== "cancelled")) {
2145
1617
  return taskScreen(response, who, taskId, "what this task waits for changed — refresh the page and choose again", 409);
2146
1618
  }
2147
- if (store.openContestFor(ref.id) !== null) {
2148
- return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish before changing its dependencies", 409);
2149
- }
2150
1619
  if (operation === "retry") {
2151
1620
  const blockerRef = store.lookupRef(blocker);
2152
1621
  if (blockerTask.state !== "failed" || blockerRef === null || !visible(blockerRef.repo)) {
@@ -2194,9 +1663,7 @@ export function createTasksHandlers(runtime) {
2194
1663
  ? "only queued work can move up — this task is not waiting in the queue"
2195
1664
  : moved.reason === "claimed"
2196
1665
  ? "this task is being built right now — it needs no place in line"
2197
- : moved.reason === "contest-open"
2198
- ? "a tournament is running on this task — let it finish, then pick or abandon it"
2199
- : "the queue rank could not be raised";
1666
+ : "the queue rank could not be raised";
2200
1667
  return taskScreen(response, who, taskId, said, 409);
2201
1668
  }
2202
1669
  return redirect(response, taskHref(taskId));
@@ -2217,7 +1684,6 @@ export function createTasksHandlers(runtime) {
2217
1684
  "not-latched": "the tracker never closed this — there is nothing to reopen",
2218
1685
  "not-seen-open": "the tracker has not been seen open again since the close — reopen it there first; the next sync notices",
2219
1686
  claimed: "this task is being built right now",
2220
- "contest-open": "a tournament is open on this task — decide it first",
2221
1687
  held: "a hold stands — lift it first",
2222
1688
  "question-open": "an unanswered question stands — answer or close it first",
2223
1689
  "incident-open": "an unresolved incident stands — resolve it first",
@@ -2414,16 +1880,15 @@ export function createTasksHandlers(runtime) {
2414
1880
  }
2415
1881
  case "route": {
2416
1882
  const body = readForm(posted, CONSOLE_FORMS.taskRoute);
2417
- // THE AGENTS EDIT (v47): an approver's session declares the risk,
1883
+ // THE AGENTS EDIT (v47): an approver's session sets the size,
2418
1884
  // overrides one role to an exact agent, or clears an override — ONE
2419
1885
  // authenticated transaction in the store that CAS-checks the scope
2420
1886
  // digest the form rendered (empty = "I saw no scope"), records the
2421
1887
  // change under this name, reconciles the planner pin, re-files the
2422
1888
  // scope so a sealed route goes visibly stale, and re-requests a
2423
1889
  // drafted plan when the planner changed. The viewer gate above
2424
- // already refused a watcher; a live claim or a tournament refuses
1890
+ // already refused a watcher; a live claim refuses
2425
1891
  // inside the transaction.
2426
- const riskGiven = body.get("risk");
2427
1892
  const sizeGiven = body.get("size");
2428
1893
  const riskyGiven = body.getAll("risky");
2429
1894
  const phaseGiven = body.get("phase");
@@ -2436,8 +1901,6 @@ export function createTasksHandlers(runtime) {
2436
1901
  const providerGiven = agentGiven === "" ? body.get("provider") : agentGiven.slice(0, agentGiven.indexOf("|") === -1 ? agentGiven.length : agentGiven.indexOf("|"));
2437
1902
  const modelGiven = agentGiven === "" ? (body.get("model") ?? "").trim() : agentGiven.indexOf("|") === -1 ? "" : agentGiven.slice(agentGiven.indexOf("|") + 1).trim();
2438
1903
  const sawDigest = body.get("sawDigest");
2439
- if (riskGiven !== null && !isRiskLevel(riskGiven))
2440
- return taskScreen(response, who, taskId, "risk is routine, elevated, or high", 400);
2441
1904
  const phase = phaseGiven ?? clearGiven;
2442
1905
  if (phase !== null && !ROUTE_PHASES.includes(phase))
2443
1906
  return taskScreen(response, who, taskId, "the role is planner, builder, or revision", 400);
@@ -2450,7 +1913,7 @@ export function createTasksHandlers(runtime) {
2450
1913
  size: sizeGiven !== null && isTaskSize(sizeGiven) ? sizeGiven : ref.sizing?.size ?? "medium",
2451
1914
  risky: riskyGiven.includes("yes") ? true : riskyGiven.includes("no") ? false : ref.sizing?.risky ?? false,
2452
1915
  };
2453
- if (riskGiven === null && phase === null && sizeEdit === undefined)
1916
+ if (phase === null && sizeEdit === undefined)
2454
1917
  return taskScreen(response, who, taskId, "nothing to change about the agents", 400);
2455
1918
  if (phaseGiven !== null) {
2456
1919
  if (providerGiven === null || !isProviderId(providerGiven))
@@ -2473,7 +1936,6 @@ export function createTasksHandlers(runtime) {
2473
1936
  const account = store.accountOf(who.name);
2474
1937
  return account !== null && account.revokedAt === null && account.role === "approver" ? { ok: true } : { ok: false, reason: "your login is no longer an approver" };
2475
1938
  },
2476
- ...(riskGiven === null ? {} : { risk: riskGiven }),
2477
1939
  // A person's size replaces the classifier's; the risky flag rides with it.
2478
1940
  ...(sizeEdit === undefined ? {} : { size: sizeEdit }),
2479
1941
  ...(phase === null
@@ -2532,75 +1994,17 @@ export function createTasksHandlers(runtime) {
2532
1994
  if (Number.isNaN(exactBudget))
2533
1995
  return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
2534
1996
  const inPlace = body.has("requirement-new");
2535
- // The tournament controls (operator request): a count of 2–4 files
2536
- // race terms BESIDE the scope — validated and priced BEFORE anything
2537
- // saves, so a bad tournament never half-lands on a good scope.
2538
- const raceCountGiven = (body.get("race-count") ?? "").trim();
2539
- // The comparison lanes (slice B): any filled row files a comparison
2540
- // INSTEAD of a tournament — planned and refused BEFORE the save,
2541
- // filed INSIDE the same transaction (round-6 finding 5 discipline).
2542
- const comparisonLanes = [1, 2, 3, 4]
2543
- .map(lane => ({
2544
- provider: (body.get(`compare-provider-${lane}`) ?? "").trim(),
2545
- model: (body.get(`compare-model-${lane}`) ?? "").trim(),
2546
- permissionMode,
2547
- }))
2548
- .filter(lane => lane.provider !== "" || lane.model !== "");
2549
- let plannedComparison = null;
2550
- if (comparisonLanes.length > 0) {
2551
- if (raceCountGiven !== "") {
2552
- return taskScreen(response, who, taskId, "a tournament and a comparison are different ceremonies — file one or the other", 400);
2553
- }
2554
- if (store.mirrorByTask(taskId) !== null) {
2555
- return taskScreen(response, who, taskId, "external work compares in a follow-up release — file the comparison on a local task", 409);
2556
- }
2557
- plannedComparison = planComparison({ agents: comparisonLanes });
2558
- if (!plannedComparison.ok) {
2559
- return taskScreen(response, who, taskId, `comparison not filed: ${plannedComparison.message}`, 400);
2560
- }
2561
- }
2562
- let plannedRace = null;
2563
- if (raceCountGiven !== "") {
2564
- const count = Number(raceCountGiven);
2565
- const model = body.get("race-model") ?? "";
2566
- if (!Number.isInteger(count) || count < 2 || count > 4) {
2567
- return taskScreen(response, who, taskId, "a tournament races 2 to 4 agents", 400);
2568
- }
2569
- const perUsd = Number((body.get("race-per-usd") ?? "").trim());
2570
- const totalUsd = Number((body.get("race-total-usd") ?? "").trim());
2571
- const planned = planTournament({
2572
- agents: Array.from({ length: count }, () => ({ provider: "claude", model, permissionMode })),
2573
- perAgentBudgetUsd: perUsd,
2574
- totalBudgetUsd: totalUsd,
2575
- });
2576
- if (!planned.ok) {
2577
- return taskScreen(response, who, taskId, `tournament not filed: ${planned.message}`, 400);
2578
- }
2579
- plannedRace = planned.plan;
2580
- }
2581
- // Refusals that must hold ATOMICALLY with the save (round-6 finding
2582
- // 5): a race request refused AFTER proposeGuarded would still have
2583
- // rewritten the scope — so proposal, the attended exclusion, and
2584
- // race-term filing share one transaction, and every refusal inside
2585
- // it rolls the whole act back.
2586
- if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
2587
- return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
2588
- }
2589
1997
  let saved;
2590
1998
  try {
2591
1999
  saved = store.transact(() => {
2592
- if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
2593
- return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
2594
- }
2595
2000
  // C1/M3: the signer's own credentialed filing auto-approves —
2596
2001
  // coverage is asked INSIDE this transaction, the escalated
2597
2002
  // default and budget ride the filing, and the seal commits
2598
- // atomically with it. Plain scopes only: a tournament or
2599
- // comparison keeps its own human ceremony.
2003
+ // atomically with it.
2600
2004
  // COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
2601
2005
  // bearer credentials are for machines, and a machine road must
2602
2006
  // never spend the signer's mode.
2603
- const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
2007
+ const coverage = who.via === "cookie"
2604
2008
  ? modeFilingCoverage(store, ref.repo, who.name, now)
2605
2009
  : null;
2606
2010
  const before = store.getScope(taskId);
@@ -2653,37 +2057,6 @@ export function createTasksHandlers(runtime) {
2653
2057
  }
2654
2058
  }
2655
2059
  }
2656
- if (plannedComparison !== null && plannedComparison.ok) {
2657
- store.fileTournamentTerms({
2658
- taskRef: ref.id,
2659
- kind: "comparison",
2660
- raceDigest: plannedComparison.plan.comparisonDigest,
2661
- agents: plannedComparison.plan.agents,
2662
- perAgentBudgetMicrousd: 0,
2663
- overrunReserveMicrousd: 0,
2664
- totalBudgetMicrousd: 0,
2665
- priceVersion: 0,
2666
- publicationPolicy: plannedComparison.plan.publicationPolicy,
2667
- }, now);
2668
- return { ok: true };
2669
- }
2670
- if (plannedRace === null) {
2671
- // Switching back to "one agent" withdraws a standing race OR
2672
- // comparison — the deactivated row survives as history, and
2673
- // the approval card returns to the scope alone.
2674
- store.retractTournamentTerms(ref.id);
2675
- return { ok: true };
2676
- }
2677
- store.fileTournamentTerms({
2678
- taskRef: ref.id,
2679
- raceDigest: plannedRace.raceDigest,
2680
- agents: plannedRace.agents,
2681
- perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
2682
- overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
2683
- totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
2684
- priceVersion: plannedRace.priceVersion,
2685
- publicationPolicy: plannedRace.publicationPolicy,
2686
- }, now);
2687
2060
  return { ok: true };
2688
2061
  });
2689
2062
  }
@@ -2737,9 +2110,8 @@ export function createTasksHandlers(runtime) {
2737
2110
  const scopeRow = store.getScope(taskId);
2738
2111
  if (scopeRow === null)
2739
2112
  return approvalProblem("this task has no scope to approve", 409);
2740
- const raceTerms = store.activeTournamentTerms(ref.id);
2741
2113
  const planView = planViewOf(ref.id);
2742
- const expectedDigest = approvalFormDigest(scopeRow.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
2114
+ const expectedDigest = approvalFormDigest(scopeRow.digest, planView?.sha256 ?? null);
2743
2115
  if (digest !== expectedDigest) {
2744
2116
  return approvalProblem("the scope or plan changed while this form was open — read the latest version and approve again", 409);
2745
2117
  }
@@ -2754,29 +2126,6 @@ export function createTasksHandlers(runtime) {
2754
2126
  return approvalProblem(`approval is blocked: ${view.problem}`, 409);
2755
2127
  }
2756
2128
  }
2757
- // A tournament task's yes covers BOTH documents (finding 31): the
2758
- // form bound the joint fingerprint, and the scope and race terms
2759
- // approve together, in one transaction, or not at all.
2760
- if (raceTerms !== null) {
2761
- const both = store.transact(() => {
2762
- const scopeApproved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
2763
- if (!scopeApproved.ok)
2764
- return scopeApproved;
2765
- if (!store.approveTournamentTerms(raceTerms.id, who.name, raceTerms.raceDigest, now)) {
2766
- throw new Error("the race terms changed while you were reading — nothing was approved");
2767
- }
2768
- return scopeApproved;
2769
- });
2770
- if (!both.ok) {
2771
- if (both.reason === "second-approver")
2772
- return approvalProblem(gateWords({ verdict: "vote", have: both.have, need: 2, already: both.already }), 200);
2773
- if (both.reason === "policy")
2774
- return approvalProblem(both.message, 403);
2775
- const status = both.reason === "changed" || both.reason === "unrouted" ? 409 : 403;
2776
- return approvalProblem(approveRefusalWords(both.reason), status);
2777
- }
2778
- return redirect(response, approvalBack);
2779
- }
2780
2129
  const approved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
2781
2130
  if (!approved.ok) {
2782
2131
  // v102: a first yes on protected work is recorded, not refused — the page says who else is needed.
@@ -2889,14 +2238,13 @@ export function createTasksHandlers(runtime) {
2889
2238
  const named = (body.get("run") ?? "").trim();
2890
2239
  if (!/^[0-9]{1,15}$/.test(named))
2891
2240
  return taskScreen(response, who, taskId, "which attempt? the stop names one exact run", 400);
2892
- const asked = requestTaskStop(store, { taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web", held: options.attended?.coordinator }, now);
2241
+ const asked = requestTaskStop(store, { taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web" }, now);
2893
2242
  if (!asked.ok) {
2894
2243
  const words = {
2895
2244
  "no-run": `there is no run #${escape(named)}`,
2896
2245
  "wrong-task": `run #${escape(named)} is not one of this task's attempts`,
2897
2246
  finished: `run #${escape(named)} already ended before the stop — nothing rewrites a finished attempt; reload to see the current state`,
2898
2247
  "not-live": `run #${escape(named)} is not the attempt holding this task now — reload and decide against the current attempt`,
2899
- tournament: "this run is a racing lane — stop the tournament through its own pick or abandon controls",
2900
2248
  publication: `run #${escape(named)} already admitted its publication — an external request in flight is not recalled by a stop`,
2901
2249
  };
2902
2250
  return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, 409);
@@ -3076,8 +2424,8 @@ export function createTasksHandlers(runtime) {
3076
2424
  function reviewCockpitViewOf(row, who, now) {
3077
2425
  const ref = store.lookupRef(row.taskId);
3078
2426
  const scope = store.getScope(row.taskId);
3079
- // The result run, read once: the contest road and the shared result
3080
- // detail below both hang off this one record.
2427
+ // The result run, read once: the shared result detail below hangs off
2428
+ // this one record.
3081
2429
  const run = row.runId === null ? null : store.getRun(row.runId);
3082
2430
  const intent = scope === null
3083
2431
  ? null
@@ -3102,13 +2450,6 @@ export function createTasksHandlers(runtime) {
3102
2450
  approach: parsed.ok ? oneLineOf(parsed.document.approach, 600) : null,
3103
2451
  };
3104
2452
  })();
3105
- const contestOf = () => {
3106
- // Either road to a tournament: the result run was one of its
3107
- // contestants, or the task still owes an operator a comparison.
3108
- const viaRun = run?.contestant === null || run?.contestant === undefined ? null : store.getContestant(run.contestant);
3109
- const contest = viaRun === null ? (ref === null ? null : store.contestNeedingOperator(ref.id)) : store.getContest(viaRun.contest);
3110
- return contest === null ? null : { id: contest.id, state: contest.state, kind: contest.kind, agents: store.contestants(contest.id).length };
3111
- };
3112
2453
  const base = {
3113
2454
  taskId: row.taskId,
3114
2455
  title: row.title,
@@ -3119,7 +2460,6 @@ export function createTasksHandlers(runtime) {
3119
2460
  assignment: row.assignment ?? null,
3120
2461
  intent,
3121
2462
  plan,
3122
- contest: contestOf(),
3123
2463
  ...(store.isDemo() ? { demo: true } : {}),
3124
2464
  };
3125
2465
  if (run === null) {
@@ -3202,32 +2542,6 @@ export function createTasksHandlers(runtime) {
3202
2542
  });
3203
2543
  response.end(read.content);
3204
2544
  }
3205
- /** The routine screen: the standing order restated, its verbs, its ledger. */
3206
- function routinePage(response, who, routineId, problem, status) {
3207
- const routine = store.getRoutine(routineId);
3208
- if (routine === null || !visible(routine.repo)) {
3209
- return refuse(response, who, 404, "no such routine", "/routines");
3210
- }
3211
- // Same rule as scope approval: the nonce exists only where the exact
3212
- // terms are restated, bound to who saw which digest of which order —
3213
- // and only where a yes could bind them (v48): an order whose agents
3214
- // are unresolved, unreadable, or never frozen gets the recovery road,
3215
- // never a password field.
3216
- const nonce = who.via === "cookie" && routineAgentsState(routine).approvable
3217
- ? mintApprovalNonce(who.name, `routine:${routine.id}`, routine.digest)
3218
- : "";
3219
- const paneProject = restricted() ? routine.repo : who.via === "cookie" ? who.session.project : null;
3220
- return sendScreen(response, status, routineScreenPage(chromeFor(paneProject, "routines"), {
3221
- routine,
3222
- fires: store.routineFires(routine.id, 14),
3223
- spend: store.routineSpend(routine.id, new Date(clock().getTime() - 7 * 24 * 60 * 60_000).toISOString()),
3224
- blocker: store.routineBlocker(routine.id, clock()),
3225
- csrf: who.via === "cookie" ? who.session.csrf : "",
3226
- nonce,
3227
- problem,
3228
- now: clock(),
3229
- }));
3230
- }
3231
2545
  /** The full guard list (v3 §4) — run on every request, hit or miss. */
3232
2546
  function peekGuards(runId) {
3233
2547
  if (options.localRunner === undefined || options.poolRoot === undefined) {
@@ -3290,215 +2604,6 @@ export function createTasksHandlers(runtime) {
3290
2604
  }
3291
2605
  return { ok: true, admit: { run, worktree: run.worktree, epoch: row.leaseEpoch, entries: snapshot } };
3292
2606
  }
3293
- /**
3294
- * The tournament's comparison data: the pick view plus everything the
3295
- * page states — the task behind it, per-agent question counts, and the
3296
- * money totals. The ceiling is proved here, on the server-resolved repo,
3297
- * whatever the request named.
3298
- */
3299
- function contestData(contestId) {
3300
- const view = buildPickView(store, evidenceRoot, contestId);
3301
- if (view === null)
3302
- return null;
3303
- const ref = store.refForId(view.contest.taskRef);
3304
- if (ref === null || (ref.repo !== null && !visible(ref.repo)))
3305
- return null;
3306
- const found = store.getTask(ref.externalId);
3307
- if (found === null)
3308
- return null;
3309
- // Question counts follow run lineage: every run belonging to the agent,
3310
- // not just its final one — a parked question is part of its story.
3311
- const byRun = new Map();
3312
- for (const one of store.runsFor(view.contest.taskRef)) {
3313
- if (one.contestant !== null)
3314
- byRun.set(one.id, one.contestant);
3315
- }
3316
- const questions = new Map();
3317
- for (const decision of store.decisionsForTask(view.contest.taskRef)) {
3318
- const owner = byRun.get(decision.run);
3319
- if (owner !== undefined)
3320
- questions.set(owner, (questions.get(owner) ?? 0) + 1);
3321
- }
3322
- return {
3323
- rollups: new Map(view.agents.map(agent => [agent.contestant.id, store.contestantSpendRollup(agent.contestant.id)])),
3324
- view,
3325
- taskId: ref.externalId,
3326
- taskTitle: found.title,
3327
- repo: ref.repo,
3328
- refOrigin: ref.origin,
3329
- questions,
3330
- // An interrupted tournament's agents are STOPPED, not "still
3331
- // working" — their run records stay unfinished, so the card must
3332
- // prove liveness the same way every other surface does (round-4
3333
- // finding 16).
3334
- liveRuns: liveRunIds(view.agents.flatMap(agent => (agent.run === null ? [] : [agent.run]))),
3335
- totalMicrousd: view.agents.reduce((sum, agent) => sum + agent.contestant.accountedMicrousd, 0),
3336
- anyUnknown: view.agents.some(agent => agent.contestant.unknownSpend),
3337
- };
3338
- }
3339
- /**
3340
- * Assemble the LIVE rendered terms of one watched attempt, or say in
3341
- * words why there are none (Phase 2E, ruling 12). Everything here is
3342
- * re-read at confirm time — the digest is the proof the world held.
3343
- */
3344
- async function liveAttendedTerms(taskId, inputs, now) {
3345
- if (options.attended === undefined)
3346
- return { ok: false, problem: "this console cannot hold a session — start it with `toolroll up`" };
3347
- const ref = store.lookupRef(taskId);
3348
- if (ref === null)
3349
- return { ok: false, problem: "no such task" };
3350
- const scope = store.getScope(taskId);
3351
- if (scope === null)
3352
- return { ok: false, problem: "file a scope first — the terms come from it" };
3353
- // CONTINUATION (A4): the parent attempt and the follow-up enter the
3354
- // SIGNED terms; the head is the parent's accepted commit per outcome;
3355
- // a moving publication blocks; failed parents are refused outright for
3356
- // now (a dirty preserved tree cannot promise a clean continuation) —
3357
- // stated, not hidden.
3358
- let continuation = null;
3359
- if (inputs.parent !== undefined) {
3360
- const parent = store.getRun(inputs.parent);
3361
- if (parent === null || parent.taskRef !== ref.id)
3362
- return { ok: false, problem: "no such finished attempt on this task" };
3363
- const followup = (inputs.followup ?? "").trim();
3364
- if (followup === "" || followup.length > 2000) {
3365
- return { ok: false, problem: "say what to do next — 1 to 2000 characters" };
3366
- }
3367
- if (parent.outcome !== "built" && parent.outcome !== "no-change") {
3368
- return { ok: false, problem: "only a built or no-change attempt can be continued — for a failed one, file a follow-up task" };
3369
- }
3370
- const accepted = parent.outcome === "built" ? parent.headRevision : parent.baseRevision;
3371
- if (accepted === null)
3372
- return { ok: false, problem: "the attempt's accepted head was never recorded — file a follow-up task" };
3373
- const blocked = store.continuationBlockOf(parent.id);
3374
- if (blocked !== null)
3375
- return { ok: false, problem: blocked };
3376
- continuation = { parentRun: parent.id, followup, head: accepted };
3377
- }
3378
- else if (approvalOf(scope).approved) {
3379
- return { ok: false, problem: "the scope is approved — it already dispatches unattended" };
3380
- }
3381
- let pinned = scope.profileState === "resolved" ? (scope.profile ?? null) : null;
3382
- if (pinned === null) {
3383
- return { ok: false, problem: "the scope cannot say exactly what runs — name a model (task scope --model, or config set build)" };
3384
- }
3385
- if (pinned.provider !== "claude") {
3386
- return { ok: false, problem: `${pinned.provider} cannot hold a watched session yet — this road is claude-only for now` };
3387
- }
3388
- // THE MINT PICKER (P1/C7): a chosen model or posture rebuilds the
3389
- // WHOLE ClaudeProfile — the digest signs the profile that actually
3390
- // runs, never a partial edit. Absent choices keep the scope's pin.
3391
- const chosenModel = (inputs.model ?? "").trim();
3392
- const chosenPosture = inputs.posture === "bypassPermissions"
3393
- ? "bypassPermissions"
3394
- : inputs.posture === "auto"
3395
- ? "auto"
3396
- : inputs.posture === "acceptEdits"
3397
- ? "acceptEdits"
3398
- : null;
3399
- if (chosenModel !== "" || chosenPosture !== null) {
3400
- pinned = {
3401
- ...pinned,
3402
- ...(chosenModel === "" ? {} : { model: chosenModel }),
3403
- ...(chosenPosture === null ? {} : { permissionArgv: chosenPosture }),
3404
- };
3405
- }
3406
- if (store.activeTournamentTerms(ref.id) !== null) {
3407
- return { ok: false, problem: "this task races a tournament — one attempt cannot authorize N racers" };
3408
- }
3409
- const repo = ref.repo;
3410
- if (repo === null)
3411
- return { ok: false, problem: "place the task in a repository first — the terms pin the exact head" };
3412
- const head = continuation !== null ? continuation.head : await options.attended.headOf(repo);
3413
- if (head === null)
3414
- return { ok: false, problem: `the head of ${repo} cannot be read right now` };
3415
- const minutes = Math.max(5, Math.min(240, Math.floor(inputs.minutes) || 60));
3416
- const turns = Math.max(1, Math.min(100, Math.floor(inputs.turns) || 20));
3417
- const budget = Math.max(100_000, Math.min(50_000_000, Math.floor(inputs.budgetMicrousd) || 2_000_000));
3418
- return {
3419
- ok: true,
3420
- terms: {
3421
- taskId,
3422
- attentionMode: "console-visible",
3423
- scopeDigest: scope.digest,
3424
- profileDigest: profileDigestOf(pinned),
3425
- profileJson: canonicalProfileJson(pinned),
3426
- repo,
3427
- runner: options.attended.runner,
3428
- runnerGeneration: 0,
3429
- head,
3430
- maxSessionTurns: turns,
3431
- budgetMicrousd: budget,
3432
- turnTimeoutSeconds: pinned.timeoutSeconds,
3433
- ...(continuation === null ? {} : { parentRun: continuation.parentRun, followup: continuation.followup }),
3434
- // The expiry is FIXED at preview and carried through the confirm —
3435
- // a timestamp recomputed at signing time would change the digest
3436
- // every millisecond and make the proof unmatchable. The confirm's
3437
- // bound check keeps a stale form honest.
3438
- absoluteExpiry: inputs.expiry !== undefined &&
3439
- Date.parse(inputs.expiry) > now.getTime() &&
3440
- Date.parse(inputs.expiry) <= now.getTime() + 241 * 60_000
3441
- ? inputs.expiry
3442
- : new Date(now.getTime() + minutes * 60_000).toISOString(),
3443
- },
3444
- };
3445
- }
3446
- /** The signed form, rendered from EXACT terms — what you read is what the password signs. */
3447
- function attendConfirmScreen(response, who, terms, inputs, digest, nonce, csrf) {
3448
- const scope = store.getScope(terms.taskId);
3449
- // Quick mint (M4): the mode signature substitutes for the password —
3450
- // the confirm screen still shows EVERY term; only the credential line
3451
- // changes. The mint transaction re-proves the mode either way.
3452
- const quickRef = store.lookupRef(terms.taskId);
3453
- const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
3454
- const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
3455
- const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
3456
- const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
3457
- `<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
3458
- `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
3459
- `<input type="hidden" name="nonce" value="${escape(nonce)}">` +
3460
- `<input type="hidden" name="digest" value="${escape(digest)}">` +
3461
- `<input type="hidden" name="minutes" value="${inputs.minutes}">` +
3462
- `<input type="hidden" name="turns" value="${inputs.turns}">` +
3463
- `<input type="hidden" name="budget" value="${inputs.budgetMicrousd}">` +
3464
- `<input type="hidden" name="expiry" value="${escape(terms.absoluteExpiry)}">` +
3465
- (inputs.model === undefined ? "" : `<input type="hidden" name="model" value="${escape(inputs.model)}">`) +
3466
- (inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
3467
- (terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
3468
- (terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
3469
- `<p><strong>Your password signs exactly this:</strong></p>` +
3470
- `<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
3471
- `<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
3472
- `<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
3473
- (() => {
3474
- const profile = JSON.parse(terms.profileJson).profile;
3475
- const posture = profile?.permissionArgv === "bypassPermissions"
3476
- ? "FULL permissions — claude runs with --dangerously-skip-permissions; nothing asks"
3477
- : profile?.permissionArgv === "auto"
3478
- ? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
3479
- : "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
3480
- return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
3481
- })() +
3482
- (terms.parentRun == null
3483
- ? ""
3484
- : `<p class="meta">Continues</p><p class="recap" style="margin-top:0">attempt <a href="/r/${terms.parentRun}" class="mono">#${terms.parentRun}</a>, from exactly where it finished</p>` +
3485
- `<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
3486
- `<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
3487
- `<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
3488
- `<p class="meta">Spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
3489
- `<p class="meta">Conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
3490
- `<p class="meta">While you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
3491
- (quick
3492
- ? `<p class="meta">Your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
3493
- : `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
3494
- `<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
3495
- `</form>` +
3496
- `<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
3497
- // The chrome binds to the AUTHORITY repository (surfaces round 1,
3498
- // finding 5): the banner on this screen is the mode that would cover
3499
- // a quick mint here, never the session's open-project filter.
3500
- return sendScreen(response, 200, screen(`attend \u00b7 ${terms.taskId}`, body, { chrome: chromeFor(terms.repo, "tasks") }));
3501
- }
3502
2607
  /** The live subset of a bounded run page \u2014 one indexed lookup per row. */
3503
2608
  function liveRunIds(rows) {
3504
2609
  const live = new Set();
@@ -3524,23 +2629,15 @@ export function createTasksHandlers(runtime) {
3524
2629
  { id: "runs", domain: "tasks", stage: "console", method: "GET", handle: get },
3525
2630
  { id: "run.page", domain: "tasks", stage: "console", method: "GET", handle: get },
3526
2631
  { id: "run.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
3527
- { id: "contest.page", domain: "tasks", stage: "console", method: "GET", handle: get },
3528
2632
  { id: "routines", domain: "tasks", stage: "console", method: "GET", handle: get },
3529
2633
  { id: "routine.page", domain: "tasks", stage: "console", method: "GET", handle: get },
3530
2634
  { id: "decision.page", domain: "tasks", stage: "console", method: "GET", handle: get },
3531
2635
  { id: "decision.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
3532
- { id: "session.attended-beats", domain: "tasks", stage: "console", method: "POST", handle: post },
3533
2636
  { id: "code.act", domain: "tasks", stage: "console", method: "POST", handle: post },
3534
2637
  { id: "tasks.add", domain: "tasks", stage: "console", method: "POST", handle: post },
3535
2638
  { id: "queue.move", domain: "tasks", stage: "console", method: "POST", handle: post },
3536
2639
  { id: "queue.note", domain: "tasks", stage: "console", method: "POST", handle: post },
3537
2640
  { id: "decision.answer", domain: "tasks", stage: "console", method: "POST", handle: post },
3538
- { id: "contest.act.arm", domain: "tasks", stage: "console", method: "POST", handle: post },
3539
- { id: "contest.act.pick", domain: "tasks", stage: "console", method: "POST", handle: post },
3540
- { id: "contest.act.abandon", domain: "tasks", stage: "console", method: "POST", handle: post },
3541
- { id: "task.attend.attend-preview", domain: "tasks", stage: "console", method: "POST", handle: post },
3542
- { id: "task.attend.attend", domain: "tasks", stage: "console", method: "POST", handle: post },
3543
- { id: "task.attend.attend-revoke", domain: "tasks", stage: "console", method: "POST", handle: post },
3544
2641
  { id: "task.act.hold", domain: "tasks", stage: "console", method: "POST", handle: post },
3545
2642
  { id: "task.act.unhold", domain: "tasks", stage: "console", method: "POST", handle: post },
3546
2643
  { id: "task.act.requeue", domain: "tasks", stage: "console", method: "POST", handle: post },
@@ -3564,16 +2661,9 @@ export function createTasksHandlers(runtime) {
3564
2661
  { id: "task.act.resume-arm", domain: "tasks", stage: "console", method: "POST", handle: post },
3565
2662
  { id: "task.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
3566
2663
  { id: "task.instance-act", domain: "tasks", stage: "console", method: "POST", handle: post },
3567
- { id: "routines.add", domain: "tasks", stage: "console", method: "POST", handle: post },
3568
- { id: "routine.act.approve", domain: "tasks", stage: "console", method: "POST", handle: post },
3569
- { id: "routine.act.refresh", domain: "tasks", stage: "console", method: "POST", handle: post },
3570
- { id: "routine.act.pause", domain: "tasks", stage: "console", method: "POST", handle: post },
3571
- { id: "routine.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
3572
- { id: "routine.act.run-now", domain: "tasks", stage: "console", method: "POST", handle: post },
3573
2664
  { id: "run.act", domain: "tasks", stage: "console", method: "POST", handle: post },
3574
2665
  { id: "session.editor-links", domain: "tasks", stage: "console", method: "POST", handle: post },
3575
- { id: "run.turn", domain: "tasks", stage: "console", method: "POST", handle: post },
3576
2666
  { id: "incident.resolve", domain: "tasks", stage: "console", method: "POST", handle: post },
3577
2667
  ];
3578
- return { registrations, get, post, runListPane, peekFragment, contestScreen, routineMutation, attendedBeats, attendMutation, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, routinePage, peekGuards, contestData, liveAttendedTerms, attendConfirmScreen, liveRunIds };
2668
+ return { registrations, get, post, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3579
2669
  }