toolroll 0.9.51 → 0.9.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/backup.d.ts +8 -0
  8. package/dist/backup.js +10 -1
  9. package/dist/board.d.ts +3 -19
  10. package/dist/board.js +4 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-workspace.d.ts +3 -1
  14. package/dist/builder.d.ts +9 -40
  15. package/dist/builder.js +50 -257
  16. package/dist/chat-actions.js +3 -14
  17. package/dist/chat-controls.d.ts +0 -4
  18. package/dist/chat-controls.js +0 -1
  19. package/dist/chat-decide.js +1 -3
  20. package/dist/chat-delivery.d.ts +0 -2
  21. package/dist/chat-delivery.js +0 -1
  22. package/dist/chat-quiet.js +2 -2
  23. package/dist/chat-task-actions.d.ts +1 -1
  24. package/dist/chat-task-actions.js +0 -2
  25. package/dist/claim.d.ts +79 -244
  26. package/dist/claim.js +688 -1023
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +10 -2
  29. package/dist/contracts/cli.d.ts +0 -13
  30. package/dist/contracts/cli.js +0 -2
  31. package/dist/contracts/console-api.d.ts +5 -10
  32. package/dist/contracts/console-api.js +4 -10
  33. package/dist/contracts/flow.d.ts +50 -0
  34. package/dist/contracts/flow.js +26 -1
  35. package/dist/contracts/lead-tools.d.ts +0 -13
  36. package/dist/contracts/lead-tools.js +3 -3
  37. package/dist/contracts/store-json.d.ts +1 -25
  38. package/dist/contracts/store-json.js +3 -13
  39. package/dist/converse.d.ts +1 -11
  40. package/dist/converse.js +6 -38
  41. package/dist/coordinator.d.ts +0 -1
  42. package/dist/coordinator.js +3 -5
  43. package/dist/database-reclaim.d.ts +4 -0
  44. package/dist/database-reclaim.js +13 -0
  45. package/dist/demo.d.ts +1 -1
  46. package/dist/demo.js +17 -27
  47. package/dist/desktop-update-gate.js +0 -1
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/desktop-update.js +1 -3
  50. package/dist/dispatch.d.ts +1 -1
  51. package/dist/dispatch.js +1 -1
  52. package/dist/dispose.d.ts +2 -3
  53. package/dist/dispose.js +17 -92
  54. package/dist/envelope.js +0 -3
  55. package/dist/exec.d.ts +0 -64
  56. package/dist/exec.js +3 -356
  57. package/dist/exhaustion.d.ts +22 -76
  58. package/dist/exhaustion.js +14 -140
  59. package/dist/export.js +4 -4
  60. package/dist/flow-engine.js +1 -1
  61. package/dist/flow-schedule.d.ts +154 -0
  62. package/dist/flow-schedule.js +321 -0
  63. package/dist/flow-share.js +1 -1
  64. package/dist/flow-triggers.d.ts +6 -1
  65. package/dist/flow-triggers.js +65 -10
  66. package/dist/flows-cli.js +8 -4
  67. package/dist/flows-ui.js +2 -0
  68. package/dist/flows.js +1 -1
  69. package/dist/guides.js +9 -29
  70. package/dist/invoke.d.ts +1 -24
  71. package/dist/invoke.js +17 -259
  72. package/dist/lead-status.js +2 -4
  73. package/dist/lead-voice.js +1 -1
  74. package/dist/live-bus.d.ts +3 -3
  75. package/dist/live-bus.js +3 -3
  76. package/dist/mate-doors.d.ts +1 -2
  77. package/dist/mate-doors.js +12 -25
  78. package/dist/mate-tools.d.ts +2 -2
  79. package/dist/mate-tools.js +10 -22
  80. package/dist/modes.d.ts +6 -7
  81. package/dist/modes.js +7 -13
  82. package/dist/monitoring.js +32 -8
  83. package/dist/notification-delivery.d.ts +19 -0
  84. package/dist/notification-delivery.js +37 -0
  85. package/dist/operate-remote-arguments.js +2 -6
  86. package/dist/operate-remote.js +6 -7
  87. package/dist/operate.d.ts +0 -6
  88. package/dist/operate.js +283 -2378
  89. package/dist/phase-routing.d.ts +13 -23
  90. package/dist/phase-routing.js +13 -35
  91. package/dist/plan-auto.js +2 -2
  92. package/dist/planner-source.js +2 -1
  93. package/dist/policy.d.ts +1 -4
  94. package/dist/policy.js +0 -8
  95. package/dist/process-recovery-eligibility.js +2 -3
  96. package/dist/process-recovery-settlement.js +1 -0
  97. package/dist/project-delete.js +3 -15
  98. package/dist/proposal.d.ts +4 -24
  99. package/dist/proposal.js +3 -88
  100. package/dist/provider-auth.d.ts +3 -3
  101. package/dist/provider-auth.js +4 -6
  102. package/dist/provider.d.ts +11 -25
  103. package/dist/provider.js +4 -36
  104. package/dist/push.js +1 -1
  105. package/dist/recipe-ui.js +6 -6
  106. package/dist/recipes.d.ts +3 -3
  107. package/dist/recipes.js +27 -23
  108. package/dist/retention.js +6 -5
  109. package/dist/run-closeout.d.ts +1 -1
  110. package/dist/run-closeout.js +1 -4
  111. package/dist/scope.d.ts +11 -65
  112. package/dist/scope.js +17 -82
  113. package/dist/serve.js +16 -25
  114. package/dist/server/chat.js +7 -30
  115. package/dist/server/flows.js +11 -10
  116. package/dist/server/guards.js +0 -1
  117. package/dist/server/pages-assets.js +1 -1
  118. package/dist/server/pages.js +0 -2
  119. package/dist/server/people-tokens.js +0 -4
  120. package/dist/server/route-table.d.ts +1 -1
  121. package/dist/server/route-table.js +2 -17
  122. package/dist/server/settings.js +4 -9
  123. package/dist/server/shared.d.ts +29 -212
  124. package/dist/server/shared.js +73 -780
  125. package/dist/server/task-views.js +9 -76
  126. package/dist/server/tasks.d.ts +1 -50
  127. package/dist/server/tasks.js +36 -946
  128. package/dist/setup-guide.d.ts +2 -2
  129. package/dist/slack-chat.d.ts +0 -2
  130. package/dist/slack-chat.js +0 -1
  131. package/dist/store.d.ts +142 -1382
  132. package/dist/store.js +900 -4083
  133. package/dist/surface.js +10 -25
  134. package/dist/task-composer.d.ts +1 -1
  135. package/dist/task-composer.js +1 -1
  136. package/dist/task-control.d.ts +3 -10
  137. package/dist/task-control.js +2 -7
  138. package/dist/teammate-desk.js +1 -1
  139. package/dist/teammates-ui.js +1 -1
  140. package/dist/telegram-mate.d.ts +0 -3
  141. package/dist/telegram-mate.js +0 -1
  142. package/dist/templates.d.ts +11 -7
  143. package/dist/templates.js +12 -9
  144. package/dist/toolroll-update.d.ts +24 -3
  145. package/dist/toolroll-update.js +159 -16
  146. package/dist/webhooks.d.ts +10 -22
  147. package/dist/webhooks.js +43 -81
  148. package/dist/work-index.js +3 -7
  149. package/dist/workspace-revision.d.ts +28 -1
  150. package/dist/workspace-revision.js +276 -49
  151. package/dist/workspace-ui.js +1 -1
  152. package/package.json +1 -3
  153. package/scripts/recovery-canary.mjs +3 -3
  154. package/dist/contest.d.ts +0 -318
  155. package/dist/contest.js +0 -753
  156. package/dist/held.d.ts +0 -150
  157. package/dist/held.js +0 -799
  158. package/dist/liveness.d.ts +0 -30
  159. package/dist/liveness.js +0 -42
  160. package/dist/pricing.d.ts +0 -45
  161. package/dist/pricing.js +0 -77
  162. package/dist/routine.d.ts +0 -228
  163. package/dist/routine.js +0 -764
  164. package/dist/supervisor.mjs +0 -297
package/dist/operate.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { notificationClaimHeld, claimNotifications, finalizeNotification, notificationDestination } from "./notification-delivery.js";
1
2
  import { adapterPolicy } from "./server/route-policy.js";
2
3
  import { UNSENT_REPLY_MS } from "./telegram-settings.js";
3
4
  import { checkPublicCommand } from "./public-check.js";
@@ -41,14 +42,12 @@ import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
41
42
  import { origin, readMonitoring } from "./monitoring-settings.js";
42
43
  import { logEvent } from "./log.js";
43
44
  import { FLOW_EVERY_MS, flowHousekeeping, moveCards, moveCardsAfter } from "./flow-cadence.js";
44
- import { readHooksBase } from "./flow-triggers.js";
45
+ import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
45
46
  import { sendTeammateSummaries } from "./teammate-admin.js";
46
47
  import { runRequestedUndos, sendTeammateWeeklies } from "./teammate-week.js";
47
48
  import { refreshConnections } from "./mcp-connect.js";
48
49
  import { followDiscord } from "./discord.js";
49
50
  import { followTeams } from "./teams.js";
50
- import { loadDiscordCredentials } from "./discord-api.js";
51
- import { loadSlackCredentials } from "./slack-api.js";
52
51
  import { followSlack } from "./slack.js";
53
52
  import { validateScopeText } from "./task-text.js";
54
53
  import { runMemoryCommand } from "./memory-cli.js";
@@ -84,10 +83,10 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
84
83
  *
85
84
  * **Nothing ever prompts.** There is no terminal on the other end at 3am.
86
85
  */
87
- import { homedir, hostname, tmpdir } from "node:os";
86
+ import { homedir, hostname } from "node:os";
88
87
  import { basename, dirname, join, resolve } from "node:path";
89
88
  import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
90
- import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
89
+ import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
91
90
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
92
91
  import { authorizePlanUnderMode } from "./plan-auto.js";
93
92
  import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
@@ -95,8 +94,8 @@ import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
95
94
  import { configPath, addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
96
95
  import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
97
96
  import { pushPass } from "./push.js";
98
- import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync, mkdirSync } from "node:fs";
99
- import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, namedPath, taskBranches } from "./names.js";
97
+ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
98
+ import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
100
99
  import { claimActor, currentActor, parseLeadToken, withActor } from "./actor.js";
101
100
  import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
102
101
  import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
@@ -167,17 +166,15 @@ import { tally, spendLine } from "./summary.js";
167
166
  import { bodyHashOf, describePublicationGrant, observeChecks, publicationBody, publishPass, sweepMerges, } from "./publish.js";
168
167
  import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
169
168
  import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
170
- import { acquire, acquireFallback, acquireIfReady, completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, finalizePlanFenced, finalizePlanFailureFenced, finalizeScoutFenced, finalizeScoutFailureFenced, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, acquireContinuation, } from "./claim.js";
169
+ import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
171
170
  import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
172
- import { attendedLivenessState } from "./liveness.js";
173
- import { HeldSessionCoordinator, sweepHeldOrphans } from "./held.js";
174
171
  import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
175
172
  import { builtIn, guarded } from "./backend.js";
176
173
  import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverDead, recoveredAnything, acquireWatchLeaseAuthed, heartbeatWatchLeaseAuthed, addRunnerReposAuthed, removeRunnerRepoAuthed, registerRunnerIfIdle, retireRunnerIfCurrent, normalizeRunnerName, validRunnerName, canonicalRepos, RUNNER_NAME_MAX, RUNNER_CAPACITY_MAX, parseRunnerCapacity, } from "./runner.js";
177
174
  import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
178
175
  import { serveMcp } from "./mcp.js";
179
176
  import { createInterface } from "node:readline";
180
- import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, profileFromJson, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
177
+ import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
181
178
  import { presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
182
179
  import { WorktreePool } from "./worktree.js";
183
180
  import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
@@ -187,25 +184,23 @@ import { readExecutorOf } from "./read-executor.js";
187
184
  import { parseWorkView } from "./workspace-ui.js";
188
185
  import { taskWorkSummaryOf } from "./work-summary.js";
189
186
  import { assignmentOf, assignmentBrief, syncAssignmentHandoffs } from "./assignment.js";
190
- import { approveRoutine, describeRoutine, fireRoutine, refreshRoutineAgents, routineAgentsState, routineDigestOf, validateRoutineTerms, ROUTINE_NAME, } from "./routine.js";
191
- import { fileTaskProposal, fileRoutineProposal, validateTaskText } from "./proposal.js";
187
+ import { createScheduledFlow, describeSchedule, parseSchedule } from "./flow-schedule.js";
188
+ import { fileTaskProposal, validateTaskText } from "./proposal.js";
192
189
  import { TEMPLATES, templateByName } from "./templates.js";
193
- import { planTournament, planComparison, contestNoun, jointApprovalDigest, admitContest, crossReadyBarrier, finalizeContestant, recoverContests, maybeAggregate as contestMaybeAggregate, sweepContestCleanup, escalateOverdueContests } from "./contest.js";
194
190
  import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
195
- import { resolvePhaseAgent, resolveScopeProfile, resolveScopeChain, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
196
- import { isRiskLevel, isTaskSize, legOf, projectRoute, riskConsequence, routeDigestOf, routeWords, RISK_LEVELS, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
191
+ import { resolvePhaseAgent, resolveScopeProfile, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
192
+ import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
197
193
  import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
198
194
  import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
199
195
  import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
200
- import { clearWebhook, effectivePrimary, isMessagingChannel, loadConsoleUrl, loadPrimary, loadWebhookTargets, phoneOrigin, saveConsoleUrl, savePrimary, saveWebhook, webhookPass, SLACK_ENV, DISCORD_ENV } from "./webhooks.js";
196
+ import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
201
197
  import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
202
198
  import { attestProvider, attestationOf, versionInRange } from "./attest.js";
203
- import { recognizesEligible } from "./exhaustion.js";
204
199
  import { build, proveApprovedProfile, } from "./builder.js";
205
200
  import { plan as planTask } from "./planner.js";
206
201
  import { attachTmux, elapsedWords, openInTmux, PEEK_TAIL_LINES, runPeek, snapshotLiveRuns } from "./peek-cli.js";
207
202
  import { scout as scoutTask } from "./scout.js";
208
- import { profileDigestOf, chainDigestOf, entryDigestOf } from "./scope.js";
203
+ import { profileDigestOf } from "./scope.js";
209
204
  import { PROVIDER_KEY_ENV, SUBSCRIPTION_CAPABLE, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
210
205
  import { run, terminateLiveProviders, run as execRun } from "./exec.js";
211
206
  import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
@@ -360,14 +355,8 @@ External trackers — build what a tracker nominates, under local approvals
360
355
  toolroll approver list
361
356
  toolroll task scope <id> --goal <what success is>
362
357
  [--not <text>] [--touches a,b] [--budget-usd <n>]
363
- [--race provider:model[,provider:model…]] [--race-count 2..4]
364
- [--race-per-usd <n>] [--race-total-usd <n>]
365
- [--compare provider:model[,provider:model…]] (labeled comparison — no dollar caps; needs a lane no budget can bound)
366
- a tournament races 2-4 agents on the
367
- task; you compare and pick one
368
358
  toolroll task approve <id> the yes — interactive, or
369
- --yes --digest <d> --as <you> --token <t> for scripts; a tournament
370
- approves both documents with one yes, on the joint fingerprint
359
+ --yes --digest <d> --as <you> --token <t> for scripts
371
360
  toolroll task requeue <id> --as <you> --token <t>
372
361
  exit a stall: incidents resolved,
373
362
  strikes cleared, queued again
@@ -375,8 +364,7 @@ External trackers — build what a tracker nominates, under local approvals
375
364
  run the approved check again on the
376
365
  last attempt's exact commit — a new
377
366
  attempt, no agent, saved result
378
- toolroll config set budgets [--build-usd <n>] [--race-per-usd <n>]
379
- [--race-total-usd <n>] [--race-agents 2..4] --as <you> --token <t>
367
+ toolroll config set budgets [--build-usd <n>] --as <you> --token <t>
380
368
  spend defaults new filings pre-fill
381
369
  from; config clear budgets resets
382
370
 
@@ -444,35 +432,19 @@ Capabilities — what the work needs, recorded and probed, never valued
444
432
  plan before building: an agent reads
445
433
  the repo, asks you questions, and
446
434
  proposes a scope you approve
447
- toolroll task route <id> [--risk routine|elevated|high]
448
- [--size small|medium|large] [--risky yes|no]
435
+ toolroll task route <id> [--size small|medium|large] [--risky yes|no]
449
436
  [--phase plan|build|repair --provider <p> [--model <m>] | --clear-phase <phase>]
450
437
  --as <you> --token <t> which agent plans, builds, and repairs
451
438
  this task, with the reason for
452
- each; declare its risk or override a
439
+ each; set its size or override a
453
440
  phase — approval seals the route
454
441
 
455
- Routines — tasks that fire on a schedule, each instance isolated
456
- toolroll template list common routines, shipped
457
- toolroll template show <name> the full prefill + what to edit
442
+ Templates — common work, shipped
443
+ toolroll template list | show <name>
458
444
  toolroll template apply <name> --repo <path> [--file]
459
- previews the exact filing; --file files it UNAPPROVED through the
460
- same door as a manual filing — a template carries no authority
461
-
462
- toolroll routine add <name> --repo <path> --goal <text>
463
- --schedule every:<min>|daily:<HH:MM>[@Zone]|weekly:<0-6>:<HH:MM>[@Zone] (UTC by default)
464
- [--not <text>] [--touches a,b] [--require kind:name,…] [--ceiling <usd>]
465
- [--budget-usd <n>] what each firing may spend
466
- toolroll routine approve <name> the step-up: approving means each
467
- firing builds WITHOUT asking, inside
468
- exactly the stated terms; editing any
469
- term voids the approval
470
- toolroll routine list | show <name>
471
- toolroll routine refresh <name> re-resolve the agents it freezes from
472
- today's configuration; approve again
473
- afterwards — nothing fires until then
474
- toolroll routine pause|resume <name>
475
- toolroll routine run-now <name> --as <you> --token <t>
445
+ previews the exact work; --file makes it UNAPPROVED — a task, or a
446
+ scheduled flow whose schedule starts paused (turn it on with
447
+ \`flows trigger resume\`; each run waits for approval)
476
448
 
477
449
  Flows — processes cards move through; the console's rules
478
450
  toolroll flows list [--repo <path>] | show <id>
@@ -522,9 +494,10 @@ Agents — which provider and model each phase runs on
522
494
  the build it mends.
523
495
  toolroll config set <phase> --tier strong --provider <p> --model <m>
524
496
  [--repo <path>] --as <you> --token <t>
525
- the STRONG agent high-risk, strict,
526
- screenshot-proof, and automerge
527
- routes reach for; never inferred
497
+ the STRONG agent large or risky,
498
+ strict, screenshot-proof, and
499
+ automerge routes reach for; never
500
+ inferred
528
501
  toolroll config set build --tier light --provider <p> --model <m>
529
502
  [--repo <path>] --as <you> --token <t>
530
503
  the fast agent small changes build
@@ -562,8 +535,8 @@ Agents — which provider and model each phase runs on
562
535
  approve the fast check Quick runs; verify show
563
536
  suggests one from the project's scripts
564
537
  Pass flags still win for one pass: --provider/--model,
565
- --plan-provider/--plan-model, --repair-model. A routine instance is
566
- pinned at fire time and ignores all of them.
538
+ --plan-provider/--plan-model, --repair-model. A task a schedule files
539
+ under an approval moved from a routine is pinned and ignores all of them.
567
540
  toolroll peek [<run-id>] [--tmux] watch live agents: one pane per
568
541
  open run — stage, clock, and what
569
542
  the agent is saying; digits focus,
@@ -575,21 +548,14 @@ Agents — which provider and model each phase runs on
575
548
  says REVIEW was not read)
576
549
 
577
550
  The outbox — facts that want a person, durably
578
- toolroll webhook set slack|discord <url>
579
- UI-only chat mirrors: every page a
580
- message with a console link; acting
581
- stays in the console. Delivers when
582
- Telegram is not configured.
583
551
  toolroll webhook set console-url <http://host:port>
584
552
  toolroll webhook primary telegram|slack|discord
585
553
  which service receives alerts when
586
554
  several are connected (asked once,
587
555
  the first time you add a second)
588
- toolroll webhook status | test | clear slack|discord
589
- toolroll outbox list [--all]
590
- toolroll outbox deliver --cmd <c> runs once per pending row, reading
591
- $TOOLROLL_KIND / _SUBJECT / _BODY;
592
- exit 0 delivered receipts, 1 any fail
556
+ toolroll webhook status
557
+ toolroll outbox list [--all] pending facts (--all: resolved too)
558
+ toolroll outbox deliver --cmd <cmd> deliver through an operator command
593
559
 
594
560
  Runners — the machines that may be given work
595
561
  toolroll runner register <name> [--capacity <n>] [--token-file <path>]
@@ -638,8 +604,6 @@ export const TASK_ACTIONS = [
638
604
  export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
639
605
  export const CONFIG_ACTIONS = ["show", "set", "clear"];
640
606
  export const APPROVER_ACTIONS = ["list", "add"];
641
- export const ROUTINE_ACTIONS = ["list", "add", "show", "approve", "refresh", "pause", "resume", "run-now"];
642
- export const CONTEST_ACTIONS = ["show", "exclude"];
643
607
  export const PEOPLE_ACTIONS = ["list", "invite", "projects", "revoke"];
644
608
  export const KEYS_ACTIONS = ["status", "set", "clear", "verify", "auth"];
645
609
  /**
@@ -652,7 +616,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
652
616
  "allow", "selector", "paths", "credentials", "repo", "token", "capacity",
653
617
  "goal", "not", "touches", "acceptance", "candidate", "by", "digest", "as", "branch", "pool", "base", "model", "turns",
654
618
  "max", "cap", "probe", "kind", "expires", "cmd", "since", "repair-model",
655
- "choose", "note", "max-open-decisions", "max-held-sessions", "name", "days", "publication", "auto-approve", "review-auto", "entries", "port", "host", "allow-host",
619
+ "choose", "note", "max-open-decisions", "name", "days", "publication", "auto-approve", "review-auto", "port", "host", "allow-host",
656
620
  "for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
657
621
  "say", "ceiling-usd", "cap-usd",
658
622
  "token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
@@ -667,8 +631,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
667
631
  "project-root", "schedule", "ceiling", "require",
668
632
  "provider", "plan-model", "plan-provider", "public-url", "editor",
669
633
  "command", "timeout-seconds", "setup-digest", "stop-grace", "title", "name", "every", "lines",
670
- "label", "reviewers", "limit", "role", "key-file", "weekly-usd", "daily-turns", "per-hour", "token-file", "race", "compare", "race-per-usd", "race-total-usd", "race-count", "race-agents", "budget-usd", "build-usd", "sync-max-age", "merge-method",
671
- "phase", "risk", "tier", "clear-phase", "size", "risky",
634
+ "label", "reviewers", "limit", "role", "key-file", "weekly-usd", "daily-turns", "per-hour", "token-file", "budget-usd", "build-usd", "sync-max-age", "merge-method",
635
+ "phase", "tier", "clear-phase", "size", "risky",
672
636
  "run", "containment", "agent",
673
637
  // onboard: the starter flows to switch on.
674
638
  "starter",
@@ -687,7 +651,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
687
651
  "token-name",
688
652
  ]);
689
653
  export const OPERATE_BOOLEAN_FLAGS = new Set([
690
- "json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
654
+ "json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
691
655
  "clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
692
656
  "no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
693
657
  "self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
@@ -906,7 +870,6 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
906
870
  ...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
907
871
  ...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
908
872
  ...(options.mateSeams === undefined ? {} : { mateSeams: options.mateSeams }),
909
- ...(options.heldCoordinator === undefined ? {} : { heldCoordinator: options.heldCoordinator }),
910
873
  ...(options.waitSleep === undefined ? {} : { waitSleep: options.waitSleep }),
911
874
  ...(options.releaseIo === undefined ? {} : { releaseIo: options.releaseIo }),
912
875
  ...(options.installBin === undefined ? {} : { installBin: options.installBin }),
@@ -1056,15 +1019,13 @@ function telegramCanDeliver(context, token) {
1056
1019
  }
1057
1020
  /** Ordinary paired text talks to the shared assistant: the same evidence
1058
1021
  * root the console reads results from, the same membership harness seam
1059
- * the CLI's chat uses, and this process's held-session supervisor for a
1060
- * confirmed stop. The pass, the follower and the watch all wire it. */
1022
+ * the CLI's chat uses. The pass, the follower and the watch all wire it. */
1061
1023
  function telegramConversation(context, options = {}) {
1062
1024
  return {
1063
1025
  evidenceRoot: context.evidenceRoot,
1064
1026
  ...(context.mateSeams?.subscriptionRunner === undefined ? {} : { subscriptionRunner: context.mateSeams.subscriptionRunner }),
1065
- ...(context.heldCoordinator === undefined ? {} : { held: context.heldCoordinator }),
1066
- // Re-read on every card and every `/task`: the same console-url the
1067
- // mirrors use, held to an https origin, and — inside `up`, where this
1027
+ // Re-read on every card and every `/task`: the same console-url chat
1028
+ // links use, held to an https origin, and — inside `up`, where this
1068
1029
  // process also serves the console — equal to that console's own
1069
1030
  // `--public-url`, or no link at all.
1070
1031
  phoneOrigin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: options.serverOrigin ?? null }),
@@ -1216,8 +1177,6 @@ async function dispatch(command, positional, flags, context) {
1216
1177
  return decideCommand(positional, flags, context);
1217
1178
  case "incident":
1218
1179
  return incidentCommand(positional, flags, context);
1219
- case "routine":
1220
- return routineCommand(positional, flags, context);
1221
1180
  case "flows":
1222
1181
  return flowsCommand(positional, flags, context);
1223
1182
  case "config":
@@ -1253,8 +1212,6 @@ async function dispatch(command, positional, flags, context) {
1253
1212
  return providersCommand(flags, context);
1254
1213
  case "template":
1255
1214
  return templateCommand(positional, flags, context);
1256
- case "contest":
1257
- return contestCommand(positional, flags, context);
1258
1215
  case "webhook":
1259
1216
  return webhookCommand(positional, flags, context);
1260
1217
  case "sync":
@@ -1473,14 +1430,6 @@ function claimCommand(positional, flags, context) {
1473
1430
  detail: result.detail,
1474
1431
  });
1475
1432
  }
1476
- if (result.reason === "attended-held") {
1477
- return fail(write, json, "claim", "attended-held", `an attended session holds this task for ${result.runner}`, EXIT.refused, {
1478
- runner: result.runner,
1479
- });
1480
- }
1481
- if (result.reason === "attended-only") {
1482
- return fail(write, json, "claim", "attended-only", "this task runs only while its operator watches — it needs a live attended authorization or a real approval", EXIT.refused);
1483
- }
1484
1433
  if (result.reason === "mode-ended") {
1485
1434
  return fail(write, json, "claim", "mode-ended", result.message, EXIT.refused);
1486
1435
  }
@@ -2073,11 +2022,9 @@ async function buildCommand(positional, flags, context) {
2073
2022
  // the same proof the tick's admission wears, refused in words here.
2074
2023
  // Every road stamps at insert (v48 integrity): a routed row's sealed
2075
2024
  // build leg, a pre-routing row's sealed profile, the bare word on a task
2076
- // with no scope — and a chain approval's base custody rides the same
2077
- // insert (the standalone road never resumes a parked chain tail; the
2078
- // tick's proven transfer does that).
2025
+ // with no scope.
2079
2026
  const standaloneModel = text(flags, "model");
2080
- const authority = store.routeAuthorityFor(ref.id, "builder", null, { provider: "claude", model: standaloneModel ?? null });
2027
+ const authority = store.routeAuthorityFor(ref.id, "builder", { provider: "claude", model: standaloneModel ?? null });
2081
2028
  if (authority !== null && !authority.ok) {
2082
2029
  await worktrees.release(leased.worktree.path, now);
2083
2030
  return fail(write, json, "build", "admission-refused", `${id}: ${authority.problem}`, EXIT.refused);
@@ -2107,7 +2054,6 @@ async function buildCommand(positional, flags, context) {
2107
2054
  worktree: leased.worktree.path,
2108
2055
  ...(authority === null ? {} : { provider: authority.stamp.provider, route: authority.stamp }),
2109
2056
  ...(standaloneModel === undefined ? {} : { model: standaloneModel }),
2110
- ...(store.approvedChainOf(id) === null ? {} : { custody: { kind: "base" } }),
2111
2057
  now,
2112
2058
  });
2113
2059
  }
@@ -2195,12 +2141,6 @@ async function buildCommand(positional, flags, context) {
2195
2141
  "Nothing has been pushed. Look at the branch before it goes anywhere.",
2196
2142
  ]);
2197
2143
  }
2198
- // ---- the unattended pass --------------------------------------------------
2199
- /** An attended authorization whose signed head the leased worktree no
2200
- * longer matches (final authority closure): thrown BEFORE admission so no
2201
- * run opens and no attempt is spent, reported as its own skip reason. */
2202
- class StaleAuthorization extends Error {
2203
- }
2204
2144
  /** How a worker's flow pass reaches outside: the tests' seams, or the real commands, network and folders. */
2205
2145
  function flowIoOf(context, pool, git, base) {
2206
2146
  const dir = dirname(context.databaseFile);
@@ -2278,7 +2218,7 @@ async function tickCommand(flags, context) {
2278
2218
  const repo = repoFrom(flags);
2279
2219
  // THE PRE-I/O MEMBERSHIP CHECK (MCP spec v6, round-4 finding 1): the
2280
2220
  // pass proves its canonical repo is in the runner's BOUND list before
2281
- // any git access, probe, or routine fires. The --repo flag stops being
2221
+ // any git access, probe, or schedule fires. The --repo flag stops being
2282
2222
  // authority on every runner road, not only at the claim.
2283
2223
  if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo))) {
2284
2224
  return fail(write, json, "tick", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
@@ -2316,29 +2256,9 @@ async function tickCommand(flags, context) {
2316
2256
  // in. Statuses land in the store; the gate inside the claim transaction is
2317
2257
  // what acts on them.
2318
2258
  await probeRepo(store, repo, runner, clock());
2319
- // Standing orders fire before the ready set is read, so a fresh instance
2320
- // joins THIS pass. dueRoutines only nominates; every proof — approval
2321
- // digest, pause, due, single-flight, budget — is re-made inside
2322
- // fireRoutine's own transaction, and skipped slots ledger and page
2323
- // themselves there. This loop just reports.
2324
- const routines = [];
2325
- for (const routine of buildsOnly ? [] : store.dueRoutines(repo, clock())) {
2326
- // The stop fence (audit IV-1): a signal that landed mid-pass stops
2327
- // every further admission — a routine not yet fired stays unfired.
2328
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
2329
- break;
2330
- const outcome = fireRoutine(store, routine.id, clock());
2331
- routines.push(outcome.ok
2332
- ? { routine: routine.name, outcome: "fired", taskId: outcome.taskId }
2333
- : {
2334
- routine: routine.name,
2335
- outcome: outcome.reason,
2336
- ...(outcome.detail === undefined ? {} : { detail: outcome.detail }),
2337
- });
2338
- }
2339
- // Flows move before the ready set is read, like routines: a card entering
2340
- // a build or research zone files its task now, and that task joins THIS
2341
- // pass. The engine is model-free; approvals the task needs still apply.
2259
+ // Flows move before the ready set is read: a card entering
2260
+ // a build or research zone files its task now (as does a schedule's
2261
+ // standing order, v115), and that task joins THIS pass. The engine is model-free; approvals the task needs still apply.
2342
2262
  // Triggers first: a schedule, GitHub, Linear or another flow may start
2343
2263
  // cards, which then move in the same pass. Checking an outside service is
2344
2264
  // `gh` or one HTTPS request, never a model, and only when it is due.
@@ -2380,7 +2300,7 @@ async function tickCommand(flags, context) {
2380
2300
  // other builds may hold the worker for minutes.
2381
2301
  let settled = 0;
2382
2302
  const settleFinished = () => {
2383
- const finished = dispatched.slice(settled).filter(one => one.outcome !== "skipped" && one.outcome !== "contest").map(one => one.id);
2303
+ const finished = dispatched.slice(settled).filter(one => one.outcome !== "skipped").map(one => one.id);
2384
2304
  settled = dispatched.length;
2385
2305
  if (finished.length === 0)
2386
2306
  return;
@@ -2389,24 +2309,10 @@ async function tickCommand(flags, context) {
2389
2309
  flowPass.filed.push(...after.filed);
2390
2310
  flowPass.problems.push(...after.problems);
2391
2311
  };
2392
- // Tournament housekeeping before the ordinary pass (stage 4): interrupted
2393
- // races recover by CAS, and an ANSWERED question re-admits its parked
2394
- // agent — fresh claim, fresh slot, remaining budget only, the SAME
2395
- // verified checkout on the SAME runner (finding 29's custody rule).
2396
- if (!buildsOnly)
2397
- recoverContests(store, clock());
2398
2312
  // Expired ceremony nonces are litter with a bound (round-3 finding 30):
2399
2313
  // the mint refuses past 50 open per approver, so the sweep keeps the
2400
2314
  // ceiling meaningful rather than letting dead rows consume it.
2401
2315
  store.sweepCeremonyNonces(clock());
2402
- // Decided tournaments give their checkouts back (stage 6) — this runner's
2403
- // custody only; a checkout that will not release cleanly is flagged and
2404
- // paged, never force-cleaned. Undecided ones escalate once at 14 days.
2405
- if (!buildsOnly) {
2406
- await sweepContestCleanup(store, path => worktrees.release(path, clock()), runner, clock());
2407
- escalateOverdueContests(store, clock());
2408
- }
2409
- const resumed = [];
2410
2316
  // v105: monthly budgets, read when first needed and again after any build (its spend counts): a used-up hard-stop
2411
2317
  // budget holds back new work billed to an API key, on every road below.
2412
2318
  let budgetGate = null;
@@ -2414,13 +2320,10 @@ async function tickCommand(flags, context) {
2414
2320
  const budgetWords = (hold) => budgetHoldWords(hold, monthOf(clock()).name);
2415
2321
  // Sprint 8: the organisation policy, asked before any claim on every road below: a provider or model it doesn't
2416
2322
  // allow never starts (the task waits, saying which rule and where to change it); terms above its permission ceiling
2417
- // start and run lowered (build() lowers them and says so), except an attended session's, signed at exactly those.
2323
+ // start and run lowered (build() lowers them and says so).
2418
2324
  const policyHold = (agents) => {
2419
2325
  for (const agent of agents) {
2420
2326
  if ("profile" in agent) {
2421
- const refused = agent.attended === true ? store.attendedPolicyRefusal(agent.profile) : null;
2422
- if (refused !== null)
2423
- return refused;
2424
2327
  const verdict = store.runPolicy(agent.profile);
2425
2328
  if (!verdict.ok)
2426
2329
  return verdict.message;
@@ -2434,297 +2337,12 @@ async function tickCommand(flags, context) {
2434
2337
  }
2435
2338
  return null;
2436
2339
  };
2437
- for (const waiting of buildsOnly ? [] : store.contestsInStates(["decision-wait"])) {
2438
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
2439
- break;
2440
- // D1 belt-and-braces (external dispatch, finding 41): a mirror and a
2441
- // contest should never coexist; if one ever does, its race resumes
2442
- // NOTHING — no claim, no run, no worktree, no spend.
2443
- const waitingTaskId = store.externalIdFor(waiting.taskRef);
2444
- if (waitingTaskId !== null && store.mirrorByTask(waitingTaskId) !== null) {
2445
- resumed.push({ id: waitingTaskId, outcome: "skipped", reason: "external-race" });
2446
- continue;
2447
- }
2448
- // THE ANSWERED BATCH IS MARKED ACTIVE FIRST (Codex slice-B finding 2):
2449
- // resuming lanes one at a time let the FIRST finisher aggregate the
2450
- // contest while later answered lanes were still 'parked' — excluded
2451
- // from active, their decisions no longer open — stranding them in a
2452
- // contest that had already moved on. 'ready' counts as active, so
2453
- // aggregation waits for the whole batch. Any lane that bails before
2454
- // its build reverts to 'parked' so the next pass retries it.
2455
- const batch = [];
2456
- for (const racer of store.contestants(waiting.id).filter(one => one.state === "parked")) {
2457
- if (store.answeredDecisionForContestant(racer.id) === null)
2458
- continue;
2459
- if (store.casContestantState(racer.id, ["parked"], "ready", racer.generation)) {
2460
- batch.push({ racer });
2461
- }
2462
- }
2463
- for (const { racer } of batch) {
2464
- const backToParked = () => {
2465
- const current = store.getContestant(racer.id);
2466
- if (current !== null)
2467
- store.casContestantState(racer.id, ["ready"], "parked", current.generation);
2468
- };
2469
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true) {
2470
- backToParked();
2471
- continue;
2472
- }
2473
- const custody = racer.custody === null ? null : JSON.parse(racer.custody);
2474
- const taskId = store.externalIdFor(waiting.taskRef);
2475
- if (custody === null || custody.runner !== runner || taskId === null) {
2476
- backToParked();
2477
- continue;
2478
- }
2479
- const lanePolicy = policyHold([racer.profile == null ? { provider: racer.provider, model: racer.model } : { profile: racer.profile }]);
2480
- if (lanePolicy !== null) {
2481
- backToParked();
2482
- resumed.push({ id: taskId, outcome: "skipped", reason: "policy", detail: lanePolicy });
2483
- continue;
2484
- }
2485
- const laneBudget = budgetHold(waiting.taskRef, store.agentsFor([racer.provider]));
2486
- if (laneBudget.over !== null) {
2487
- backToParked();
2488
- resumed.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(laneBudget) });
2489
- continue;
2490
- }
2491
- // The sign-in pause holds a resumed lane too: it waits, parked, instead of failing.
2492
- const laneGate = signInGate(store, [racer.provider], clock());
2493
- if (laneGate.waiting !== null) {
2494
- backToParked();
2495
- resumed.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(laneGate.waiting) });
2496
- continue;
2497
- }
2498
- // Legacy comparison lanes carried an absolute clock. New profiles use
2499
- // a progress watchdog and may remain alive indefinitely while useful
2500
- // work is observable; only legacy approvals retain their cumulative
2501
- // wall-clock contract.
2502
- const remaining = waiting.kind === "comparison" ? null : racer.budgetMicrousd - racer.accountedMicrousd;
2503
- if (remaining !== null && remaining <= 0) {
2504
- laneGate.giveBack();
2505
- const current = store.getContestant(racer.id);
2506
- if (current !== null)
2507
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2508
- contestMaybeAggregate(store, waiting.id, clock());
2509
- resumed.push({ id: taskId, outcome: "skipped", reason: "over-ceiling" });
2510
- continue;
2511
- }
2512
- if (waiting.kind === "comparison") {
2513
- const laneProfile = racer.profile ?? contestantProfileOf(racer.provider, racer.model, racer.repairModel);
2514
- const clockCapMs = 3 * laneProfile.timeoutSeconds * 1000;
2515
- if (laneProfile.timeoutKind !== "idle" && store.contestantCumulativeMs(racer.id) >= clockCapMs) {
2516
- laneGate.giveBack();
2517
- const current = store.getContestant(racer.id);
2518
- if (current !== null)
2519
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2520
- contestMaybeAggregate(store, waiting.id, clock());
2521
- resumed.push({ id: taskId, outcome: "skipped", reason: "over-ceiling" });
2522
- continue;
2523
- }
2524
- }
2525
- const reclaimed = acquire(store, waiting.taskRef, runner, { now: clock(), token, ttlMs: leaseTtlMs });
2526
- if (!reclaimed.ok) {
2527
- laneGate.giveBack();
2528
- backToParked();
2529
- continue;
2530
- }
2531
- const freshContest = store.getContest(waiting.id);
2532
- if (freshContest === null || !store.casContestState(waiting.id, ["decision-wait", "racing"], "racing", freshContest.generation)) {
2533
- release(store, reclaimed.claim.leaseId, clock());
2534
- laneGate.giveBack();
2535
- backToParked();
2536
- continue;
2537
- }
2538
- store.stampContestLease(waiting.id, reclaimed.claim.leaseId, runner, text(flags, "incarnation") ?? null);
2539
- const leased = await worktrees.lease({ repo, branch: racer.branch, runner, taskRef: waiting.taskRef, now: clock() });
2540
- const headCheck = leased.ok ? await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path }) : null;
2541
- if (!leased.ok || (custody.head !== null && headCheck !== null && headCheck.stdout.trim() !== custody.head)) {
2542
- // The tree cannot be proved to be the one the agent left — stop the
2543
- // agent rather than cold-starting against a different history.
2544
- if (leased.ok)
2545
- await worktrees.release(leased.worktree.path, clock());
2546
- const current = store.getContestant(racer.id);
2547
- if (current !== null)
2548
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2549
- store.setContestantCleanup(racer.id, "attention");
2550
- contestMaybeAggregate(store, waiting.id, clock());
2551
- release(store, reclaimed.claim.leaseId, clock());
2552
- resumed.push({ id: taskId, outcome: "failed", reason: "contest-custody" });
2553
- continue;
2554
- }
2555
- const [resumeSlot] = store.reserveExecutionSlots(runner, 1, clock());
2556
- const parkedRun = racer.activeRun;
2557
- // A contest lane spends under its race-approved profile and says so
2558
- // at insert (v48 integrity): the lane's proven profile digest, the
2559
- // exact pair it will spend as — the store's own answer, re-proved
2560
- // and bound to the lane inside the admission.
2561
- const laneStamp = store.laneAuthorityFor(racer.id);
2562
- if (laneStamp === null) {
2563
- await worktrees.release(leased.worktree.path, clock());
2564
- release(store, reclaimed.claim.leaseId, clock());
2565
- backToParked();
2566
- resumed.push({ id: taskId, outcome: "failed", reason: "admission-refused", detail: `contestant ${racer.id} is gone` });
2567
- continue;
2568
- }
2569
- // THE LANE ADMISSION (atomic authority closure): the resume opens on
2570
- // the lane under the contest's live custody — this lease, this
2571
- // runner, this watch incarnation, stamped on the contest just above
2572
- // — presenting the lane's stored sealed profile; value-shaped.
2573
- const admittedResume = store.admitContestLane({
2574
- taskRef: waiting.taskRef,
2575
- leaseId: reclaimed.claim.leaseId,
2576
- runner,
2577
- incarnation: text(flags, "incarnation") ?? null,
2578
- branch: racer.branch,
2579
- worktree: leased.worktree.path,
2580
- provider: racer.provider,
2581
- model: racer.model,
2582
- contestant: racer.id,
2583
- ...(parkedRun === null ? {} : { parentRun: parkedRun }),
2584
- now: clock(),
2585
- route: laneStamp,
2586
- });
2587
- if (!admittedResume.ok) {
2588
- await worktrees.release(leased.worktree.path, clock());
2589
- release(store, reclaimed.claim.leaseId, clock());
2590
- backToParked();
2591
- resumed.push({ id: taskId, outcome: "failed", reason: "admission-refused", detail: admittedResume.problem });
2592
- continue;
2593
- }
2594
- const resumeRun = admittedResume.runId;
2595
- // The lane's pointer moved to the resume INSIDE its admission (raw
2596
- // authority repair): from the parked attempt it continues, proved
2597
- // there — no release-then-claim window exists any more.
2598
- const afterClaim = store.getContestant(racer.id);
2599
- if (afterClaim !== null)
2600
- store.casContestantState(racer.id, ["ready"], "building", afterClaim.generation);
2601
- budgetGate = null; // this build's spend counts toward the next check
2602
- const resumeResult = await build(store, {
2603
- taskId,
2604
- taskRef: waiting.taskRef,
2605
- runner,
2606
- leaseId: reclaimed.claim.leaseId,
2607
- runnerToken: token,
2608
- runId: resumeRun,
2609
- evidenceRoot: context.evidenceRoot,
2610
- worktree: leased.worktree.path,
2611
- branch: racer.branch,
2612
- now: clock(),
2613
- clock,
2614
- provider: racer.provider,
2615
- // v24: the contestant's OWN sealed profile is the authority — the
2616
- // proof holds the lane to it (model, limits, permissions), so no
2617
- // flag-shaped overrides ride along.
2618
- contestProfile: racer.profile ?? contestantProfileOf(racer.provider, racer.model, racer.repairModel),
2619
- ...(remaining === null ? {} : { maxBudgetUsd: Math.min(remaining, laneBudget.remainingMicrousd ?? Infinity) / 1_000_000 }),
2620
- onProviderSpawn: (pid) => {
2621
- worktrees.recordProviderOccupancy(leased.worktree.path, runner, pid, leased.worktree.leaseEpoch);
2622
- if (resumeSlot !== undefined) {
2623
- const facts = { run: resumeRun, contestant: racer.id, incarnation: text(flags, "incarnation") ?? null, processGroup: pid };
2624
- if (!store.markSlotRunning(resumeSlot, facts, clock()) && !store.refreshSlotProcess(resumeSlot, facts))
2625
- throw new Error("the provider's execution slot no longer belongs to this attempt");
2626
- }
2627
- },
2628
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
2629
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
2630
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
2631
- });
2632
- const resumedRunRow = store.getRun(resumeRun);
2633
- const resumeMeasured = resumedRunRow === null || resumedRunRow.providerStartedAt === null
2634
- ? 0
2635
- : resumedRunRow.costUsd !== null
2636
- ? Math.round(resumedRunRow.costUsd * 1_000_000)
2637
- : null;
2638
- let resumedOutcome = "failed";
2639
- let resumedCommitted = false;
2640
- if (resumeResult.ok && resumeResult.parked !== undefined) {
2641
- resumedOutcome = "parked";
2642
- const asked = resumeResult.parked.decision;
2643
- const racerDecision = store.saveDecision({
2644
- run: resumeRun,
2645
- contestant: racer.id,
2646
- urgency: asked.urgency,
2647
- recap: asked.recap,
2648
- question: asked.question,
2649
- options: asked.options,
2650
- recommendation: asked.recommendation,
2651
- ...(asked.assignee === null ? {} : { assignee: asked.assignee }),
2652
- ...(asked.deadline === null ? {} : { deadline: asked.deadline }),
2653
- }, clock());
2654
- // A racing agent's question pages like any other (arc 3 finding 21):
2655
- // aggregation stays quiet ASSUMING this row already spoke.
2656
- store.enqueueNotification({
2657
- source: { run: resumeRun },
2658
- dedupeKey: `decision:${racerDecision}`,
2659
- kind: "decision",
2660
- subject: `${taskId} parked a decision (${contestNoun(waiting.kind)} agent)`,
2661
- body: `\`toolroll decide ${racerDecision}\``,
2662
- pushClass: "decision",
2663
- link: `/d/${racerDecision}`,
2664
- }, clock());
2665
- }
2666
- else if (resumeResult.ok) {
2667
- resumedOutcome = "built";
2668
- resumedCommitted = resumeResult.committed;
2669
- }
2670
- else if (resumeResult.reason === "stopped") {
2671
- resumedOutcome = "stopped";
2672
- }
2673
- if (resumedRunRow !== null && resumedRunRow.outcome === null) {
2674
- // The reason rides the resumed run too (Codex slice-B finding 7):
2675
- // both settlement paths, one honesty.
2676
- const resumeReason = !resumeResult.ok ? resumeResult.reason : undefined;
2677
- store.finishRun(resumeRun, {
2678
- outcome: resumedOutcome === "built" && !resumedCommitted ? "no-change" : resumedOutcome === "stopped" ? "failed" : resumedOutcome === "parked" ? "parked" : resumedOutcome,
2679
- committed: resumedCommitted,
2680
- ...(resumeReason === undefined ? {} : { reason: resumeReason }),
2681
- now: clock(),
2682
- });
2683
- }
2684
- if (resumedOutcome === "parked") {
2685
- // Custody refreshes on EVERY park (Codex slice-B finding 9): a
2686
- // re-parked lane whose custody still named the pre-resume head
2687
- // would falsely stop as contest-custody on its next answer.
2688
- const headNow = await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
2689
- const dirtyNow = await git("git", ["status", "--porcelain"], { cwd: leased.worktree.path });
2690
- store.setContestantCustody(racer.id, JSON.stringify({
2691
- branch: racer.branch,
2692
- head: headNow.code === 0 ? headNow.stdout.trim() : null,
2693
- runner,
2694
- dirty: dirtyNow.stdout.trim() !== "",
2695
- at: clock().toISOString(),
2696
- }));
2697
- }
2698
- await worktrees.release(leased.worktree.path, clock());
2699
- const resumedFinal = finalizeContestant(store, {
2700
- contestId: waiting.id,
2701
- contestantId: racer.id,
2702
- runId: resumeRun,
2703
- outcome: resumedOutcome,
2704
- measuredMicrousd: resumeMeasured,
2705
- slotId: resumeSlot ?? null,
2706
- }, clock());
2707
- resumed.push({ id: taskId, outcome: "contest", reason: resumedFinal.aggregated ?? "racing" });
2708
- }
2709
- }
2710
- // THE CHAIN RECONCILER (E3d, review finding 3): a crash between a run's
2711
- // disposition and its cycle resolution leaves an OPEN cycle with a
2712
- // concluded tail — resolve each through the SAME resolver the disposition
2713
- // uses, before any admission can re-tag or race it. Advancing here lands
2714
- // the cycle in pending-admission for THIS pass's chain admission below.
2715
- // One shared piece with the fault tests (F+G review, finding 5).
2716
- if (!buildsOnly)
2717
- store.reconcileStrandedChains(repo, clock());
2718
2340
  // The DISPATCH view of the queue (queue columns, v19): this runner's own
2719
2341
  // reserved work first, then the shared queue; work reserved for other
2720
2342
  // workers is absent. The claim primitive re-proves the reservation.
2721
2343
  const ready = store.listReady(clock(), runner);
2722
2344
  const considered = ready.length;
2723
- const dispatched = [...resumed];
2724
- // Expired attended authorizations close durably each pass (round-6
2725
- // finding 8): the partial unique frees, and the claim gates stop
2726
- // honoring corpses.
2727
- store.sweepExpiredAuthorizations(clock());
2345
+ const dispatched = [];
2728
2346
  // The stale-scan (dispatch v3, finding 20): mirrors the courtesy filter
2729
2347
  // kept out of ready are REPORTED here, typed, with a paged episode —
2730
2348
  // an undispatakable tracker item is a 9am fact, not a silent absence.
@@ -2756,15 +2374,9 @@ async function tickCommand(flags, context) {
2756
2374
  settleFinished();
2757
2375
  untakenTrial?.giveBack();
2758
2376
  untakenTrial = null;
2759
- // The build budget governs UNATTENDED admissions (round-1 finding 4):
2760
- // once it is spent, the pass keeps SCANNING for attended
2761
- // authorizations — operator-invoked sessions launch regardless —
2762
- // while declining every further ordinary admission.
2763
- if (built >= max) {
2764
- const openAuth = store.openAuthorizationFor(ref.id);
2765
- if (openAuth === null || openAuth.runner !== runner || openAuth.attemptRun !== null)
2766
- continue;
2767
- }
2377
+ // The build budget: once it is spent, no further admission this pass.
2378
+ if (built >= max)
2379
+ continue;
2768
2380
  // The stop fence (audit IV-1): checked before every claim. The build
2769
2381
  // already in flight finishes under its own bounds; nothing NEW is
2770
2382
  // admitted once the operator has said stop.
@@ -2776,91 +2388,17 @@ async function tickCommand(flags, context) {
2776
2388
  dispatched.push({ id, outcome: "skipped", reason: "other-repo" });
2777
2389
  continue;
2778
2390
  }
2779
- // EVERY live fallback cycle defers the ordinary road (Codex E3d review,
2780
- // finding 5) — a pending admission belongs to the chain pass below, and
2781
- // an open cycle's custody moves only through proven roads, never an
2782
- // in-passing re-tag. The ONE exception: an open cycle whose tail PARKED
2783
- // — the paused lineage — proceeds, and the new run takes custody through
2784
- // the proven parked-resume transfer after it is created.
2785
- const liveCycle = store.fallbackCycleFor(ref.id);
2786
- const parkedChainTail = liveCycle !== null && liveCycle.state === "open" && liveCycle.tailRun !== null
2787
- ? store.getRun(liveCycle.tailRun)
2788
- : null;
2789
- if (liveCycle !== null && !(parkedChainTail !== null && parkedChainTail.outcome === "parked")) {
2790
- dispatched.push({ id, outcome: "skipped", reason: "fallback-active" });
2791
- continue;
2792
- }
2793
2391
  // A plan the operator asked for dispatches a PLANNER — the one
2794
2392
  // legitimate spend on a task with no approved scope. Everything else
2795
- // unapproved is a person's pending decision: skip, not refuse — EXCEPT
2796
- // the attended road (Phase 2, v6 W1): a live attended authorization
2797
- // naming THIS runner is authority for one watched attempt, and the
2798
- // skip for everything short of that is its own typed word, never the
2799
- // generic `unapproved` the round-5 review caught masking it.
2393
+ // unapproved is a person's pending decision: skip, not refuse.
2800
2394
  const scopeApproved = scopeApprovedForDispatch(store, ref.id, clock());
2801
2395
  const wantsPlan = ref.plan === "requested" && !scopeApproved;
2802
2396
  // A report task with an approved scope dispatches a SCOUT (mate arc
2803
2397
  // §10): the same approval, the planner's read-only road, a report back.
2804
2398
  const wantsScout = ref.deliverable === "report" && scopeApproved;
2805
- let attendedDispatch = null;
2806
2399
  if (!wantsPlan && !scopeApproved) {
2807
- const open = store.openAuthorizationFor(ref.id);
2808
- const watching = open === null
2809
- ? null
2810
- : attendedLivenessState(open.lastBeatAt === null ? null : Date.parse(open.lastBeatAt), clock().getTime(), Date.parse(open.absoluteExpiry));
2811
- if (open !== null &&
2812
- open.runner === runner &&
2813
- (watching === "live" || watching === "grace") &&
2814
- open.attemptRun === null &&
2815
- context.heldCoordinator !== undefined) {
2816
- // v28: sessions are unbounded by default; an operator-set cap
2817
- // skips FURTHER launches in words. The gauge is durable custody
2818
- // rows, never the in-process map — a restarted up with orphans
2819
- // pending must count them.
2820
- if (context.maxHeldSessions !== undefined && store.openHeldSessionCount(runner) >= context.maxHeldSessions) {
2821
- dispatched.push({
2822
- id,
2823
- outcome: "skipped",
2824
- reason: "session-cap",
2825
- detail: `this machine holds ${store.openHeldSessionCount(runner)} of ${context.maxHeldSessions} attended sessions — end one, or raise --max-held-sessions`,
2826
- });
2827
- continue;
2828
- }
2829
- attendedDispatch = open;
2830
- }
2831
- else if (open !== null) {
2832
- dispatched.push({ id, outcome: "skipped", reason: "attended-only" });
2833
- continue;
2834
- }
2835
- else {
2836
- dispatched.push({ id, outcome: "skipped", reason: "unapproved" });
2837
- continue;
2838
- }
2839
- }
2840
- // TOURNAMENT TERMS FIRST (foundations finding 8's reorder): an approved
2841
- // race's admission is governed by its own fingerprinted terms, and pass
2842
- // flags must not be able to shape it — so the terms are discovered
2843
- // before any flag-shaped resolution runs, and a raced task's build
2844
- // resolution ignores the flags outright.
2845
- const racedAhead = wantsPlan || wantsScout || attendedDispatch !== null ? null : store.activeTournamentTerms(ref.id);
2846
- // The attended spec comes from the authorization's PINNED terms — the
2847
- // courtesy half of the proof; the coordinator's transaction re-proves
2848
- // byte-for-byte at the actual HEAD (v6 W1).
2849
- let attendedSpec = null;
2850
- if (attendedDispatch !== null) {
2851
- try {
2852
- const terms = JSON.parse(attendedDispatch.termsJson);
2853
- const pinned = profileFromJson(typeof terms.profileJson === "string" ? terms.profileJson : null);
2854
- if (pinned !== null)
2855
- attendedSpec = { provider: pinned.provider, model: pinned.model, digest: profileDigestOf(pinned), profile: pinned };
2856
- }
2857
- catch {
2858
- attendedSpec = null;
2859
- }
2860
- if (attendedSpec === null) {
2861
- dispatched.push({ id, outcome: "skipped", reason: "attended-only", detail: "the authorization's pinned profile cannot be read" });
2862
- continue;
2863
- }
2400
+ dispatched.push({ id, outcome: "skipped", reason: "unapproved" });
2401
+ continue;
2864
2402
  }
2865
2403
  // The phase agent, resolved BEFORE anything is claimed and snapshotted
2866
2404
  // into the run: pin > flags > project > installation > default. Planner
@@ -2868,14 +2406,14 @@ async function tickCommand(flags, context) {
2868
2406
  // when no plan-specific flag is given.
2869
2407
  // THE ROUTE (v47): the sealed route when the approval stands, else the
2870
2408
  // working proposed route, else — with no scope yet — a live
2871
- // recommendation from the task's own risk, overrides, and pins; a row
2409
+ // recommendation from the task's own size, overrides, and pins; a row
2872
2410
  // proven to predate routing is the LEGACY road (its sealed profile,
2873
2411
  // then flags and configuration); a routed row whose route cannot be
2874
2412
  // read FAILS CLOSED here, in words. Dispatch resolves each phase from
2875
2413
  // ITS leg: the plan leg for a planner (pass flags may restate it, never
2876
2414
  // contradict it), the SEALED build leg for every routed approval
2877
2415
  // (mutable configuration cannot reroute an approved build).
2878
- const taskRoute = attendedSpec !== null || racedAhead !== null ? null : routeOfTask(store, id, ref, clock());
2416
+ const taskRoute = routeOfTask(store, id, ref, clock());
2879
2417
  if (taskRoute !== null && taskRoute.kind === "unreadable") {
2880
2418
  dispatched.push({ id, outcome: "skipped", reason: "agent-config", detail: taskRoute.problem });
2881
2419
  continue;
@@ -2885,9 +2423,8 @@ async function tickCommand(flags, context) {
2885
2423
  // authority closure): a planner on a filed, unapproved scope runs
2886
2424
  // under the WORKING route only as the whole scope proves — exact raw
2887
2425
  // terms (a proposed-via marker this code never writes), a resolved
2888
- // profile, a whole fallback chain (an unresolved `[]` is none), route
2889
- // parity with the signed risk, the digest, and the live auth mode
2890
- // agreeing with a chain's pinned base mode. One disagreement skips the
2426
+ // profile, route parity with the signed risk, the digest, and the live
2427
+ // auth mode. One disagreement skips the
2891
2428
  // task in words before any claim; the admission and the spawn ask the
2892
2429
  // same question again.
2893
2430
  if (wantsPlan && route !== null && route.source === "proposed") {
@@ -2899,21 +2436,6 @@ async function tickCommand(flags, context) {
2899
2436
  }
2900
2437
  const planLeg = route === null ? null : legOf(route.route, "plan");
2901
2438
  const sealedBuildLeg = route !== null && route.source === "approved" ? legOf(route.route, "build") : null;
2902
- // A parked chain tail PAST the base (v48 authority repair): its successor resumes the
2903
- // entry's custody and spends as that entry — the exact approved pair
2904
- // under `fallback` provenance — never as the sealed build leg. An
2905
- // entry the approved chain no longer carries under the tail's digest
2906
- // is refused here, in words, before any claim moves.
2907
- let parkedEntry = null;
2908
- if (parkedChainTail !== null && parkedChainTail.chainIndex != null && parkedChainTail.chainIndex > 0 && !wantsPlan && !wantsScout && attendedSpec === null && racedAhead === null) {
2909
- const chain = store.approvedChainOf(id);
2910
- const entry = chain === null ? undefined : chain[parkedChainTail.chainIndex];
2911
- if (entry === undefined || entryDigestOf(entry) !== parkedChainTail.entryDigest) {
2912
- dispatched.push({ id, outcome: "skipped", reason: "stale-approval", detail: `${id}: the parked attempt #${parkedChainTail.id} is bound to fallback entry ${parkedChainTail.chainIndex}, which the approved chain no longer carries — nothing resumes it` });
2913
- continue;
2914
- }
2915
- parkedEntry = { index: parkedChainTail.chainIndex, provider: entry.profile.provider, model: entry.profile.model };
2916
- }
2917
2439
  const planFlagged = planProvider !== undefined || planModel !== undefined || providerFlag !== undefined || model !== undefined;
2918
2440
  if (wantsPlan && planLeg !== null && planFlagged) {
2919
2441
  // Pass flags cannot contradict the task's plan leg: a flag that names
@@ -2931,48 +2453,40 @@ async function tickCommand(flags, context) {
2931
2453
  continue;
2932
2454
  }
2933
2455
  }
2934
- const resolution = attendedSpec !== null
2935
- ? null
2936
- : wantsPlan
2937
- ? planLeg !== null
2938
- ? resolvePhaseAgent(store, "plan", repo, { provider: planLeg.provider, model: planLeg.model })
2939
- : resolvePhaseAgent(store, "plan", repo, {
2940
- // The legacy road (P2/C7 precedence): the task's plan PIN
2941
- // beats every flag and config row; flags beat config.
2942
- provider: ref.planProvider ?? planProvider ?? providerFlag,
2943
- model: ref.planProvider !== null ? (ref.planModel ?? undefined) : (planModel ?? model),
2944
- })
2945
- : racedAhead !== null
2946
- ? resolvePhaseAgent(store, "build", repo, {}, ref)
2947
- : parkedEntry !== null
2948
- ? resolvePhaseAgent(store, "build", repo, { provider: parkedEntry.provider, model: parkedEntry.model })
2949
- : sealedBuildLeg !== null
2950
- ? resolvePhaseAgent(store, "build", repo, { provider: sealedBuildLeg.provider, model: sealedBuildLeg.model }, ref)
2951
- : resolvePhaseAgent(store, "build", repo, { provider: providerFlag, model }, ref);
2952
- if (resolution !== null && !resolution.ok) {
2456
+ const resolution = wantsPlan
2457
+ ? planLeg !== null
2458
+ ? resolvePhaseAgent(store, "plan", repo, { provider: planLeg.provider, model: planLeg.model })
2459
+ : resolvePhaseAgent(store, "plan", repo, {
2460
+ // The legacy road (P2/C7 precedence): the task's plan PIN
2461
+ // beats every flag and config row; flags beat config.
2462
+ provider: ref.planProvider ?? planProvider ?? providerFlag,
2463
+ model: ref.planProvider !== null ? (ref.planModel ?? undefined) : (planModel ?? model),
2464
+ })
2465
+ : sealedBuildLeg !== null
2466
+ ? resolvePhaseAgent(store, "build", repo, { provider: sealedBuildLeg.provider, model: sealedBuildLeg.model }, ref)
2467
+ : resolvePhaseAgent(store, "build", repo, { provider: providerFlag, model }, ref);
2468
+ if (!resolution.ok) {
2953
2469
  dispatched.push({ id, outcome: "skipped", reason: "agent-config", detail: resolution.problem });
2954
2470
  continue;
2955
2471
  }
2956
- const spec = resolution === null ? attendedSpec : resolution.spec;
2472
+ const spec = resolution.spec;
2957
2473
  // THE AUTH MODE, strictly, before any claim or row (atomic authority
2958
2474
  // closure): a present mode file for the provider this pass would spend
2959
2475
  // as that says neither word is a stated problem — the same reader the
2960
2476
  // filing, the seal, and the spawn use — and the task is skipped in
2961
2477
  // words with nothing opened, never dispatched to be refused later.
2962
- const modeProviders = racedAhead !== null ? racedAhead.agents.filter(agent => isProviderId(agent.provider)).map(agent => agent.provider) : [spec.provider];
2963
- const brokenMode = [...new Set(modeProviders)].map(one => readAuthModeStrict(one)).find(one => !one.ok);
2964
- if (brokenMode !== undefined && !brokenMode.ok) {
2478
+ const brokenMode = readAuthModeStrict(spec.provider);
2479
+ if (!brokenMode.ok) {
2965
2480
  dispatched.push({ id, outcome: "skipped", reason: "auth-mode", detail: brokenMode.problem });
2966
2481
  continue;
2967
2482
  }
2968
2483
  // THE SIGN-IN PAUSE: a provider whose sign-in stopped working takes no
2969
- // new work — no claim, no run, never a substitute (a lapsed login must not
2970
- // reach a paid fallback) — until a run or check on it works again or a
2484
+ // new work — no claim, no run, never a substitute — until a run or check on it works again or a
2971
2485
  // person resumes it. Other providers keep working.
2972
2486
  // One task every AUTH_TRIAL_MS goes ahead as the trial: a sign-in check
2973
2487
  // can say "logged in" for a session that cannot refresh, so a real run
2974
2488
  // decides — it lifts the pause, or fails into the same incident.
2975
- const gate = signInGate(store, modeProviders, clock(), ref.id);
2489
+ const gate = signInGate(store, [spec.provider], clock(), ref.id);
2976
2490
  if (gate.waiting !== null) {
2977
2491
  dispatched.push({ id, outcome: "skipped", reason: "signed-out", detail: signInWords(gate.waiting) });
2978
2492
  continue;
@@ -2980,38 +2494,27 @@ async function tickCommand(flags, context) {
2980
2494
  // A trial whose task is skipped before its claim succeeds is given back
2981
2495
  // at the next task (or after the pass), so the next pass may take it.
2982
2496
  untakenTrial = gate;
2983
- // The leg is the authority: what resolved must BE the leg, exactly —
2984
- // the sealed build leg, or the parked fallback entry's own pair.
2985
- const governingLeg = wantsPlan ? planLeg : parkedEntry !== null ? { provider: parkedEntry.provider, model: parkedEntry.model, chosen: "fallback" } : sealedBuildLeg;
2497
+ // The leg is the authority: what resolved must BE the leg, exactly.
2498
+ const governingLeg = wantsPlan ? planLeg : sealedBuildLeg;
2986
2499
  if (governingLeg !== null && (spec.provider !== governingLeg.provider || spec.model !== governingLeg.model)) {
2987
2500
  dispatched.push({ id, outcome: "skipped", reason: "agent-config", detail: `${id}: the ${wantsPlan ? "plan" : "build"} leg names ${governingLeg.provider} · ${governingLeg.model} but resolution produced ${spec.provider} · ${spec.model ?? "(no model)"} — nothing substitutes` });
2988
2501
  continue;
2989
2502
  }
2990
2503
  // Route provenance for the run this pass opens (v47): PRESENTED to the
2991
2504
  // admission transaction, from the leg that governs it — the store
2992
- // dictates nothing (v48 authority repair). A parked fallback entry's successor presents
2993
- // `fallback` under the sealed route — there is no chain-only digest
2994
- // (raw authority repair): with no sealed route it presents nothing,
2995
- // and the admission refuses in words.
2996
- // Every road presents at insert (v48 integrity): an attended session
2997
- // its pinned profile; a pre-routing row its sealed profile (or the
2998
- // bare word on a task with no scope) — the store's own answer, so the
2505
+ // dictates nothing (v48 authority repair).
2506
+ // Every road presents at insert (v48 integrity): a pre-routing row its
2507
+ // sealed profile (or the bare word on a task with no scope) — the store's own answer, so the
2999
2508
  // admission proves exactly what the row holds.
3000
2509
  const routeStamp = (phase) => {
3001
- if (attendedSpec !== null && phase === "build") {
3002
- return { routeDigest: `profile:${attendedSpec.digest}`, phase, provider: spec.provider, model: spec.model, chosen: "legacy" };
3003
- }
3004
2510
  // A task with NO scope holds no filed route (final authority
3005
2511
  // closure): its planner presents the bare word `legacy` for the pair
3006
2512
  // the live recommendation resolved — the recommendation chose the
3007
2513
  // agent, but nothing anybody filed is the authority it spends under.
3008
2514
  if (governingLeg === null || route === null || (phase === "plan" && route.source === "live")) {
3009
- const legacy = store.routeAuthorityFor(ref.id, phase === "plan" ? "planner" : "builder", null, { provider: spec.provider, model: spec.model });
2515
+ const legacy = store.routeAuthorityFor(ref.id, phase === "plan" ? "planner" : "builder", { provider: spec.provider, model: spec.model });
3010
2516
  return legacy !== null && legacy.ok ? legacy.stamp : null;
3011
2517
  }
3012
- if (parkedEntry !== null && phase === "build") {
3013
- return { routeDigest: routeDigestOf(route.route), phase, provider: spec.provider, model: spec.model, chosen: "fallback" };
3014
- }
3015
2518
  return { routeDigest: routeDigestOf(route.route), phase, provider: spec.provider, model: spec.model, chosen: governingLeg.chosen };
3016
2519
  };
3017
2520
  // THE LEGACY STALE CHECK, before any claim or row (v48 integrity): a
@@ -3020,7 +2523,7 @@ async function tickCommand(flags, context) {
3020
2523
  // must BE that pair. A disagreement used to open a row and refuse it
3021
2524
  // at the dispatch proof; now nothing opens — the task is held under
3022
2525
  // the same backoff the approval door lifts, and paged once.
3023
- if (!wantsPlan && !wantsScout && attendedSpec === null && racedAhead === null && route === null) {
2526
+ if (!wantsPlan && !wantsScout && route === null) {
3024
2527
  const legacyStamp = routeStamp("build");
3025
2528
  if (legacyStamp !== null && (legacyStamp.provider !== spec.provider || legacyStamp.model !== spec.model)) {
3026
2529
  const message = `${id}: the sealed profile builds on ${legacyStamp.provider} · ${legacyStamp.model ?? "(no model)"} but today's routing resolved ${spec.provider} · ${spec.model ?? "(no model)"} — nothing runs on it (stale-approval)`;
@@ -3032,30 +2535,16 @@ async function tickCommand(flags, context) {
3032
2535
  // THE READINESS HALT (v47): a provider THIS runner has reported
3033
2536
  // unavailable never claims and never spends — no substitution, no
3034
2537
  // second-best; the skip names the observation and the ways out.
3035
- // Unknown readiness passes (every existing gate still applies); a
3036
- // raced task halts if any lane's provider is unavailable, because a
3037
- // subset is a different contest than the one signed. The ONE road
3038
- // onward is an EXPLICITLY APPROVED fallback chain: when the sealed
3039
- // build leg's provider is unavailable, the task moves to the approved
3040
- // next entry — that one and no other — and the chain admission pass
3041
- // below runs it after re-proving everything; otherwise it fails closed.
2538
+ // Unknown readiness passes (every existing gate still applies).
3042
2539
  {
3043
- const providersToProve = racedAhead !== null ? racedAhead.agents.map(agent => agent.provider) : [spec.provider];
3044
- const unavailable = providersToProve
3045
- .map(candidate => store.runnerReadinessOf(runner, candidate))
3046
- .find(seen => seen !== null && seen.state === "unavailable");
3047
- if (unavailable !== undefined && unavailable !== null) {
2540
+ const unavailable = store.runnerReadinessOf(runner, spec.provider);
2541
+ if (unavailable !== null && unavailable.state === "unavailable") {
3048
2542
  const observed = `${unavailable.provider} is reported unavailable on ${runner} (${unavailable.reason}; observed ${unavailable.observedAt})`;
3049
- const onward = !wantsPlan && !wantsScout && racedAhead === null && attendedSpec === null && sealedBuildLeg !== null
3050
- ? store.skipUnavailablePrimary(ref.id, id, repo, runner, clock())
3051
- : null;
3052
2543
  dispatched.push({
3053
2544
  id,
3054
2545
  outcome: "skipped",
3055
2546
  reason: "provider-unavailable",
3056
- detail: onward !== null && onward.ok
3057
- ? `${observed} — moving to the approved fallback ${onward.next.provider} · ${onward.next.model}, the only substitution the approval allows; it is admitted next`
3058
- : `${observed} — nothing substitutes for a routed provider${onward === null || onward.reason === "no-chain" ? "" : ` (${onward.detail})`}: override the phase with \`task route\` or restore the provider and report readiness again`,
2547
+ detail: `${observed} — nothing substitutes for a routed provider: override the phase with \`task route\` or restore the provider and report readiness again`,
3059
2548
  });
3060
2549
  continue;
3061
2550
  }
@@ -3065,23 +2554,14 @@ async function tickCommand(flags, context) {
3065
2554
  // worktree, no wake churn. The provider source is the AUTHORITATIVE
3066
2555
  // one per road: the sealed approval snapshot for ordinary builds (the
3067
2556
  // same snapshot the dispatch proof enforces), the plan resolver for
3068
- // planner runs. Attended work is claude-only and excluded; tournament
3069
- // contestants cannot be tier-2 today (the money gate refuses them at
3070
- // filing). A missing or malformed snapshot is NOT skipped here — the
2557
+ // planner runs. A missing
2558
+ // or malformed snapshot is NOT skipped here — the
3071
2559
  // existing approval refusals own that road, and attestation must
3072
2560
  // never mask them. The gateway re-checks before spawn; this skip only
3073
2561
  // keeps the normal road cheap.
3074
- const skipProviders = attendedSpec !== null
3075
- ? []
3076
- : racedAhead !== null
3077
- ? // The contest road (slice B): every lane's provider, so an
3078
- // out-of-range attested lane skips the WHOLE contest before
3079
- // any claim — running a subset is a different contest than
3080
- // the one the operator signed.
3081
- racedAhead.agents.filter(agent => isProviderId(agent.provider)).map(agent => agent.provider)
3082
- : wantsPlan
3083
- ? [spec.provider]
3084
- : [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
2562
+ const skipProviders = wantsPlan
2563
+ ? [spec.provider]
2564
+ : [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
3085
2565
  let unattestedLane = null;
3086
2566
  for (const candidate of new Set(skipProviders)) {
3087
2567
  if (attestationOf(candidate) === null)
@@ -3102,10 +2582,9 @@ async function tickCommand(flags, context) {
3102
2582
  }
3103
2583
  // Sprint 8: the organisation policy before the claim (build() looks again before spawn, and lowers).
3104
2584
  {
3105
- const sealed = wantsPlan || wantsScout ? null : store.approvedChainOf(id)?.[0]?.profile ?? store.getScope(id)?.approvedProfile ?? null;
3106
- const refused = racedAhead !== null ? policyHold(racedAhead.agents.map(agent => ({ provider: agent.provider, model: agent.model })))
3107
- : policyHold([attendedSpec !== null ? { profile: attendedSpec.profile, attended: true } : sealed !== null ? { profile: sealed }
3108
- : { provider: spec.provider, model: spec.model, ...(wantsPlan ? { session: "planning" } : wantsScout ? { session: "scouting" } : {}) }]);
2585
+ const sealed = wantsPlan || wantsScout ? null : store.getScope(id)?.approvedProfile ?? null;
2586
+ const refused = policyHold([sealed !== null ? { profile: sealed }
2587
+ : { provider: spec.provider, model: spec.model, ...(wantsPlan ? { session: "planning" } : wantsScout ? { session: "scouting" } : {}) }]);
3109
2588
  if (refused !== null) {
3110
2589
  dispatched.push({ id, outcome: "skipped", reason: "policy", detail: refused });
3111
2590
  continue;
@@ -3114,18 +2593,16 @@ async function tickCommand(flags, context) {
3114
2593
  // v105: a monthly budget that stops new API work, used up: the task waits in the queue until the month turns or the
3115
2594
  // budget is raised (the 100% alert went out; the task page and the spend page say why).
3116
2595
  // Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
3117
- // A chain's base entry says how it bills (a pinned key); otherwise each provider as it bills now.
3118
- const chainBase = wantsPlan || wantsScout ? undefined : store.approvedChainOf(id)?.[0];
3119
- const budgeted = budgetHold(ref.id, chainBase !== undefined ? [{ provider: chainBase.profile.provider, billing: chainBase.authMode }] : store.agentsFor([spec.provider, ...skipProviders]));
2596
+ // Each provider as it bills now.
2597
+ const budgeted = budgetHold(ref.id, store.agentsFor([spec.provider, ...skipProviders]));
3120
2598
  if (budgeted.over !== null) {
3121
2599
  dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
3122
2600
  continue;
3123
2601
  }
3124
2602
  // THE DAILY RAIL (modes chain D4): a live mode's run cap reserves at
3125
2603
  // admission — atomic, so two watch loops cannot both slip under it.
3126
- // Attended sessions and raced tasks reserve on their own roads below;
3127
- // an ordinary/planner start reserves ONE here. No mode = no-op.
3128
- if (attendedDispatch === null && racedAhead === null && repo !== null) {
2604
+ // An ordinary/planner start reserves ONE here. No mode = no-op.
2605
+ if (repo !== null) {
3129
2606
  const railed = store.reserveModeRail(repo, 1, clock());
3130
2607
  if (!railed.ok) {
3131
2608
  dispatched.push({ id, outcome: "skipped", reason: railed.rail, detail: railed.detail });
@@ -3182,240 +2659,6 @@ async function tickCommand(flags, context) {
3182
2659
  continue;
3183
2660
  }
3184
2661
  const lease = claimed.claim.leaseId;
3185
- // A tournament rides this claim (stage 3b): approved race terms send N
3186
- // agents instead of one builder. Everything after admission either
3187
- // reaches the ready barrier for ALL agents or interrupts the whole
3188
- // tournament — a partial race is never dispatched (finding 19).
3189
- const raceTerms = racedAhead;
3190
- if (raceTerms !== null && raceTerms.approvedDigest === raceTerms.raceDigest) {
3191
- const admittedKind = raceTerms.kind;
3192
- // The rail reserves every lane at once (D4) — a tournament is N
3193
- // starts from one filing. Refused before any skeleton exists.
3194
- if (repo !== null) {
3195
- const railedRace = store.reserveModeRail(repo, raceTerms.n, clock());
3196
- if (!railedRace.ok) {
3197
- dispatched.push({ id, outcome: "skipped", reason: railedRace.rail, detail: railedRace.detail });
3198
- continue;
3199
- }
3200
- }
3201
- const scopeRow = store.getScope(id);
3202
- const admitted = admitContest(store, {
3203
- taskId: id,
3204
- taskRef: ref.id,
3205
- runner,
3206
- leaseId: lease,
3207
- incarnation: text(flags, "incarnation") ?? null,
3208
- scopeDigest: scopeRow?.digest ?? "",
3209
- scopeApproved: scopeRow !== null && scopeApprovedForDispatch(store, ref.id, clock()),
3210
- // 'tasks' capacity mode keeps the claim-counted contract; the
3211
- // slot ledger records regardless (finding 26).
3212
- capacity: null,
3213
- quotaBlocked: (provider, model) => store.quotaState(runner, provider, model, clock())?.state ?? null,
3214
- }, clock());
3215
- if (!admitted.ok) {
3216
- release(store, lease, clock());
3217
- dispatched.push({ id, outcome: "skipped", reason: admitted.reason });
3218
- continue;
3219
- }
3220
- const interrupt = async (why, leasedPaths) => {
3221
- const fresh = store.getContest(admitted.contestId);
3222
- if (fresh !== null)
3223
- store.casContestState(admitted.contestId, ["dispatching", "racing"], "interrupted", fresh.generation);
3224
- store.releaseSlotsForContest(admitted.contestId, clock());
3225
- for (const path of leasedPaths)
3226
- await worktrees.release(path, clock());
3227
- release(store, lease, clock());
3228
- dispatched.push({ id, outcome: "failed", reason: why });
3229
- broke++;
3230
- };
3231
- const baseRead = await git("git", ["rev-parse", "HEAD"], { cwd: repo });
3232
- if (baseRead.code !== 0) {
3233
- await interrupt("contest-base", []);
3234
- continue;
3235
- }
3236
- const baseSha = baseRead.stdout.trim();
3237
- store.stampContestDispatch(admitted.contestId, baseSha, store.liveWorktreeSetup(repo)?.digest ?? null);
3238
- const agents = store.contestants(admitted.contestId);
3239
- const prepared = [];
3240
- let prepFailed = false;
3241
- for (const [index, agent] of agents.entries()) {
3242
- const leased = await worktrees.lease({ repo, branch: agent.branch, runner, taskRef: ref.id, now: clock(), base: baseSha });
3243
- if (!leased.ok) {
3244
- prepFailed = true;
3245
- break;
3246
- }
3247
- store.setContestantWorktree(agent.id, leased.worktree.path);
3248
- // The lane's provenance rides its insert (v48 integrity): the
3249
- // race-approved profile it will be proved against, exactly — the
3250
- // store's own answer, re-proved and bound to the lane there.
3251
- const laneStamp = store.laneAuthorityFor(agent.id);
3252
- if (laneStamp === null) {
3253
- prepFailed = true;
3254
- break;
3255
- }
3256
- // THE LANE ADMISSION (atomic authority closure): under the custody
3257
- // admitContest stamped — this lease, runner, and incarnation.
3258
- const admittedLane = store.admitContestLane({
3259
- taskRef: ref.id,
3260
- leaseId: lease,
3261
- runner,
3262
- incarnation: text(flags, "incarnation") ?? null,
3263
- branch: agent.branch,
3264
- worktree: leased.worktree.path,
3265
- provider: agent.provider,
3266
- model: agent.model,
3267
- contestant: agent.id,
3268
- now: clock(),
3269
- route: laneStamp,
3270
- });
3271
- if (!admittedLane.ok) {
3272
- prepFailed = true;
3273
- break;
3274
- }
3275
- const contestantRun = admittedLane.runId;
3276
- // The lane's pointer was bound inside the insert above (raw
3277
- // authority repair); nothing claims it after the fact.
3278
- prepared.push({
3279
- contestantId: agent.id,
3280
- slotId: admitted.slotIds[index] ?? -1,
3281
- runId: contestantRun,
3282
- worktree: leased.worktree.path,
3283
- branch: agent.branch,
3284
- leaseEpoch: leased.worktree.leaseEpoch ?? null,
3285
- });
3286
- }
3287
- const freshContest = store.getContest(admitted.contestId);
3288
- if (prepFailed || freshContest === null || !crossReadyBarrier(store, freshContest, admitted.contestantIds)) {
3289
- await interrupt("contest-admission", prepared.map(one => one.worktree));
3290
- continue;
3291
- }
3292
- // Every agent is READY and nothing has spawned: cross into racing and
3293
- // spend. The builds run concurrently; the stop fence stops them all.
3294
- budgetGate = null; // the race's spend counts toward the next check
3295
- // v105: the lanes run at once, so they share what's left of a monthly budget.
3296
- const laneShare = budgeted.remainingMicrousd === null ? null : Math.max(1, Math.floor(budgeted.remainingMicrousd / Math.max(1, prepared.length)));
3297
- const settled = await Promise.allSettled(prepared.map(async (entry) => {
3298
- const agent = store.getContestant(entry.contestantId);
3299
- if (agent === null)
3300
- throw new Error("contestant vanished");
3301
- store.casContestantState(entry.contestantId, ["ready"], "building", agent.generation);
3302
- return build(store, {
3303
- taskId: id,
3304
- taskRef: ref.id,
3305
- runner,
3306
- leaseId: lease,
3307
- runId: entry.runId,
3308
- evidenceRoot: context.evidenceRoot,
3309
- worktree: entry.worktree,
3310
- branch: entry.branch,
3311
- now: clock(),
3312
- clock,
3313
- provider: agent.provider,
3314
- contestProfile: contestantProfileOf(agent.provider, agent.model, agent.repairModel),
3315
- // A comparison lane has no dollar cap — the sealed clock is the
3316
- // bound; only race lanes carry the harness stop (E1).
3317
- ...(agent.budgetMicrousd > 0 ? { maxBudgetUsd: Math.min(agent.budgetMicrousd, laneShare ?? Infinity) / 1_000_000 } : {}),
3318
- onProviderSpawn: pid => {
3319
- worktrees.recordProviderOccupancy(entry.worktree, runner, pid, entry.leaseEpoch);
3320
- const facts = { run: entry.runId, contestant: entry.contestantId, incarnation: text(flags, "incarnation") ?? null, processGroup: pid };
3321
- if (!store.markSlotRunning(entry.slotId, facts, clock()) && !store.refreshSlotProcess(entry.slotId, facts))
3322
- throw new Error("the provider's execution slot no longer belongs to this attempt");
3323
- },
3324
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
3325
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
3326
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
3327
- });
3328
- }));
3329
- let lastAggregate = null;
3330
- for (const [index, entry] of prepared.entries()) {
3331
- const outcome = settled[index];
3332
- const run = store.getRun(entry.runId);
3333
- const measured = run === null || run.providerStartedAt === null
3334
- ? 0
3335
- : run.costUsd !== null
3336
- ? Math.round(run.costUsd * 1_000_000)
3337
- : null;
3338
- let contestantOutcome = "failed";
3339
- let committed = false;
3340
- if (outcome !== undefined && outcome.status === "fulfilled") {
3341
- const result = outcome.value;
3342
- if (result.ok && result.parked !== undefined) {
3343
- contestantOutcome = "parked";
3344
- // The question still reaches the operator, tagged with its agent;
3345
- // decision-wait mechanics land in stage 4 — the card works today.
3346
- const asked = result.parked.decision;
3347
- const contestantDecision = store.saveDecision({
3348
- run: entry.runId,
3349
- contestant: entry.contestantId,
3350
- urgency: asked.urgency,
3351
- recap: asked.recap,
3352
- question: asked.question,
3353
- options: asked.options,
3354
- recommendation: asked.recommendation,
3355
- ...(asked.assignee === null ? {} : { assignee: asked.assignee }),
3356
- ...(asked.deadline === null ? {} : { deadline: asked.deadline }),
3357
- }, clock());
3358
- store.enqueueNotification({
3359
- source: { run: entry.runId },
3360
- dedupeKey: `decision:${contestantDecision}`,
3361
- kind: "decision",
3362
- subject: `${id} parked a decision (${contestNoun(admittedKind)} agent)`,
3363
- body: `\`toolroll decide ${contestantDecision}\``,
3364
- pushClass: "decision",
3365
- link: `/d/${contestantDecision}`,
3366
- }, clock());
3367
- }
3368
- else if (result.ok) {
3369
- contestantOutcome = "built";
3370
- committed = result.committed;
3371
- }
3372
- else if (result.reason === "stopped") {
3373
- contestantOutcome = "stopped";
3374
- }
3375
- }
3376
- if (run !== null && run.outcome === null) {
3377
- // The reason rides the run (slice B, E2 — kind-agnostic): "lane 3
3378
- // failed" with no words when a binary drifted out of its attested
3379
- // range is exactly the silence the attested runtime rules out.
3380
- const laneReason = outcome !== undefined && outcome.status === "fulfilled" && !outcome.value.ok
3381
- ? outcome.value.reason
3382
- : undefined;
3383
- store.finishRun(entry.runId, {
3384
- outcome: contestantOutcome === "built" && !committed ? "no-change" : contestantOutcome === "stopped" ? "failed" : contestantOutcome === "parked" ? "parked" : contestantOutcome,
3385
- committed,
3386
- ...(laneReason === undefined ? {} : { reason: laneReason }),
3387
- now: clock(),
3388
- });
3389
- }
3390
- if (contestantOutcome === "parked") {
3391
- // Custody (round-3 finding 29): who owns this checkout while the
3392
- // question waits, and what exact state it was left in — the
3393
- // resume verifies all of it before trusting the tree again.
3394
- const headNow = await git("git", ["rev-parse", "HEAD"], { cwd: entry.worktree });
3395
- const dirtyNow = await git("git", ["status", "--porcelain"], { cwd: entry.worktree });
3396
- store.setContestantCustody(entry.contestantId, JSON.stringify({
3397
- branch: entry.branch,
3398
- head: headNow.code === 0 ? headNow.stdout.trim() : null,
3399
- runner,
3400
- dirty: dirtyNow.stdout.trim() !== "",
3401
- at: clock().toISOString(),
3402
- }));
3403
- }
3404
- await worktrees.release(entry.worktree, clock());
3405
- const final = finalizeContestant(store, {
3406
- contestId: admitted.contestId,
3407
- contestantId: entry.contestantId,
3408
- runId: entry.runId,
3409
- outcome: contestantOutcome,
3410
- measuredMicrousd: measured,
3411
- slotId: entry.slotId >= 0 ? entry.slotId : null,
3412
- }, clock());
3413
- if (final.aggregated !== null)
3414
- lastAggregate = final.aggregated;
3415
- }
3416
- dispatched.push({ id, outcome: "contest", reason: lastAggregate ?? "racing" });
3417
- continue;
3418
- }
3419
2662
  if (wantsPlan) {
3420
2663
  // THE FILED REQUEST, FIRST (contract handoff, task 1): everything the
3421
2664
  // operator filed — scope, rubric, terms, revision brief, earlier
@@ -3488,11 +2731,11 @@ async function tickCommand(flags, context) {
3488
2731
  }
3489
2732
  catch (error) {
3490
2733
  await worktrees.release(planLeased.worktree.path, clock());
3491
- const unrecorded = finalizePlanFailureFenced(store, {
3492
- leaseId: lease,
2734
+ const unrecorded = finalize(store, lease, {
2735
+ kind: "plan-failure",
3493
2736
  runId: planRunId,
3494
2737
  taskId: id,
3495
- kind: "failure",
2738
+ failure: "failure",
3496
2739
  message: `the planner's source could not be recorded: ${error instanceof Error ? error.message : String(error)}`,
3497
2740
  now: clock(),
3498
2741
  });
@@ -3528,8 +2771,8 @@ async function tickCommand(flags, context) {
3528
2771
  });
3529
2772
  await worktrees.release(planLeased.worktree.path, clock());
3530
2773
  if (outcome.ok && "parked" in outcome) {
3531
- const sealed = finalizeParkFenced(store, {
3532
- leaseId: lease,
2774
+ const sealed = finalize(store, lease, {
2775
+ kind: "park",
3533
2776
  runId: planRunId,
3534
2777
  taskId: id,
3535
2778
  decision: outcome.parked.decision,
@@ -3552,8 +2795,8 @@ async function tickCommand(flags, context) {
3552
2795
  continue;
3553
2796
  }
3554
2797
  if (outcome.ok) {
3555
- const sealed = finalizePlanFenced(store, {
3556
- leaseId: lease,
2798
+ const sealed = finalize(store, lease, {
2799
+ kind: "plan",
3557
2800
  runId: planRunId,
3558
2801
  taskId: id,
3559
2802
  plan: outcome.drafted.plan,
@@ -3586,11 +2829,11 @@ async function tickCommand(flags, context) {
3586
2829
  }
3587
2830
  continue;
3588
2831
  }
3589
- const sealedFailure = finalizePlanFailureFenced(store, {
3590
- leaseId: lease,
2832
+ const sealedFailure = finalize(store, lease, {
2833
+ kind: "plan-failure",
3591
2834
  runId: planRunId,
3592
2835
  taskId: id,
3593
- kind: outcome.kind,
2836
+ failure: outcome.kind,
3594
2837
  ...(outcome.kind === "malformed" ? { malformed: outcome.reason === "malformed-decision" ? "decision" : "plan" } : {}),
3595
2838
  message: outcome.message,
3596
2839
  now: clock(),
@@ -3616,7 +2859,7 @@ async function tickCommand(flags, context) {
3616
2859
  const scopeRow = store.getScope(id);
3617
2860
  // Approvals bind exact routing for a scout exactly as for a build:
3618
2861
  // the pinned profile is proved BEFORE the workspace is leased.
3619
- const proof = proveApprovedProfile(scopeRow, null, {
2862
+ const proof = proveApprovedProfile(scopeRow, {
3620
2863
  provider: spec.provider,
3621
2864
  model: spec.model ?? undefined,
3622
2865
  maxTurns: undefined,
@@ -3695,8 +2938,8 @@ async function tickCommand(flags, context) {
3695
2938
  // never silently kept; `toolroll worktrees` lists it.
3696
2939
  const leftover = discarded.ok ? {} : { detail: `checkout kept: ${discarded.message}` };
3697
2940
  if (scouted.ok && "parked" in scouted) {
3698
- const sealed = finalizeParkFenced(store, {
3699
- leaseId: lease,
2941
+ const sealed = finalize(store, lease, {
2942
+ kind: "park",
3700
2943
  runId: scoutRunId,
3701
2944
  taskId: id,
3702
2945
  decision: scouted.parked.decision,
@@ -3714,8 +2957,8 @@ async function tickCommand(flags, context) {
3714
2957
  continue;
3715
2958
  }
3716
2959
  if (scouted.ok) {
3717
- const sealed = finalizeScoutFenced(store, {
3718
- leaseId: lease,
2960
+ const sealed = finalize(store, lease, {
2961
+ kind: "scout",
3719
2962
  runId: scoutRunId,
3720
2963
  taskId: id,
3721
2964
  report: scouted.reported.report,
@@ -3735,11 +2978,11 @@ async function tickCommand(flags, context) {
3735
2978
  }
3736
2979
  continue;
3737
2980
  }
3738
- const sealedFailure = finalizeScoutFailureFenced(store, {
3739
- leaseId: lease,
2981
+ const sealedFailure = finalize(store, lease, {
2982
+ kind: "scout-failure",
3740
2983
  runId: scoutRunId,
3741
2984
  taskId: id,
3742
- kind: scouted.kind,
2985
+ failure: scouted.kind,
3743
2986
  ...(scouted.kind === "malformed" ? { malformed: scouted.reason === "malformed-decision" ? "decision" : "report" } : {}),
3744
2987
  message: scouted.message,
3745
2988
  now: clock(),
@@ -3801,102 +3044,12 @@ async function tickCommand(flags, context) {
3801
3044
  // a row with no outcome — an attempt that vanished, visible by morning.
3802
3045
  // Its route provenance (v47) is written in the same admission
3803
3046
  // transaction; build() then refuses to spend as anything else.
3804
- // The chain custody for this run (E3b/E3d, atomic since the v48 authority repair): a parked
3805
- // chain tail hands custody to this successor through the PROVEN resume
3806
- // transfer, otherwise a chain approval opens its fresh cycle bound to
3807
- // this run — both proved and written IN the run's own insert. A
3808
- // single-profile approval — every task until an operator configures a
3809
- // fallback chain — binds nothing, so this is inert by default. A
3810
- // binding that cannot be proved, or a stamp the task's authority does
3811
- // not admit, rolls the insert back: no row, no claim kept, said why.
3047
+ // A stamp the task's authority does not admit rolls the insert back:
3048
+ // no row, no claim kept, said why.
3812
3049
  const buildStamp = routeStamp("build");
3813
3050
  let runId;
3814
3051
  try {
3815
- if (parkedEntry !== null && parkedChainTail !== null && liveCycle !== null && buildStamp !== null) {
3816
- // A parked FALLBACK tail's successor is admitted by the one
3817
- // fallback road (v48 integrity): every fact the tail's binding
3818
- // states is presented and re-proved — cycle, index, digest, auth
3819
- // mode, provider, exact model, repair binding, the sealed-profile
3820
- // mirror, the approved chain — with the parked run as the live
3821
- // tail, before any row exists.
3822
- const chain = store.approvedChainOf(id);
3823
- const entry = chain === null ? undefined : chain[parkedEntry.index];
3824
- const mirror = store.getScope(id)?.approvedProfile ?? null;
3825
- if (chain === null || entry === undefined || mirror === null || parkedChainTail.entryDigest == null || parkedChainTail.authMode == null || spec.model === null) {
3826
- throw new Error(`${id}: the parked attempt #${parkedChainTail.id}'s fallback binding cannot be restated against the approved chain — nothing resumes it`);
3827
- }
3828
- const admitted = store.admitFallback({
3829
- kind: "resume",
3830
- parkedRun: parkedChainTail.id,
3831
- cycleId: liveCycle.id,
3832
- expectCursor: parkedEntry.index,
3833
- expectTail: parkedChainTail.id,
3834
- entryDigest: parkedChainTail.entryDigest,
3835
- authMode: parkedChainTail.authMode,
3836
- repairModel: entry.profile.repairModel === "inherit" ? entry.profile.model : entry.profile.repairModel,
3837
- approved: { chainDigest: chainDigestOf(chain), profile: mirror },
3838
- run: {
3839
- taskRef: ref.id, leaseId: lease, runner, branch, worktree: leased.worktree.path, provider: spec.provider, model: spec.model,
3840
- // The recovered draft's lineage rides the insert (raw
3841
- // authority repair) — never a later stamp.
3842
- ...(leased.resumedFromRun === undefined ? {} : { recoveredFrom: leased.resumedFromRun }),
3843
- },
3844
- route: buildStamp,
3845
- }, clock());
3846
- if (!admitted.ok)
3847
- throw new Error(admitted.problem);
3848
- runId = admitted.runId;
3849
- }
3850
- else if (attendedDispatch !== null && attendedSpec !== null) {
3851
- // THE ATTENDED ADMISSION (atomic authority closure): the one
3852
- // watched attempt opens under exactly this authorization — named
3853
- // by id, runner, and generation — and the insert consumes its
3854
- // attempt and binds the row to it in one transaction.
3855
- if (buildStamp === null)
3856
- throw new Error(`${id}: the attended authorization's pinned profile presents no build authority`);
3857
- // THE SIGNED HEAD, BEFORE THE ATTEMPT IS SPENT (final authority
3858
- // closure): the authorization signed the exact commit the watched
3859
- // attempt would start from. The leased worktree's HEAD is read
3860
- // here and held to it BEFORE admission — a head that moved opens
3861
- // no run and spends no attempt; the authorization closes in the
3862
- // refusal's words so the operator sees why and may authorize
3863
- // again at today's head. The coordinator's final proof re-reads
3864
- // the same term against the captured base revision.
3865
- let signedHead = null;
3866
- try {
3867
- const terms = JSON.parse(attendedDispatch.termsJson);
3868
- signedHead = typeof terms.head === "string" && terms.head !== "" ? terms.head : null;
3869
- }
3870
- catch {
3871
- signedHead = null;
3872
- }
3873
- if (signedHead === null) {
3874
- store.closeAuthorization(attendedDispatch.id, "refused:stale-authorization", clock());
3875
- throw new StaleAuthorization(`${id}: the attended authorization ${attendedDispatch.id} signs no readable head — nothing opens under it`);
3876
- }
3877
- const headRead = await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
3878
- const headNow = headRead.code === 0 ? headRead.stdout.trim() : "";
3879
- if (headNow !== signedHead) {
3880
- store.closeAuthorization(attendedDispatch.id, "refused:stale-authorization", clock());
3881
- throw new StaleAuthorization(`${id}: the head moved since the attended authorization ${attendedDispatch.id} was signed (${signedHead.slice(0, 12)} → ${headNow === "" ? "unreadable" : headNow.slice(0, 12)}) — no run opened, no attempt spent; authorize it again at today's head`);
3882
- }
3883
- const admittedAttended = store.admitAttended({
3884
- taskRef: ref.id,
3885
- leaseId: lease,
3886
- runner,
3887
- branch,
3888
- worktree: leased.worktree.path,
3889
- provider: spec.provider,
3890
- ...(spec.model === null ? {} : { model: spec.model }),
3891
- authorization: { id: attendedDispatch.id, runner: attendedDispatch.runner, generation: attendedDispatch.runnerGeneration },
3892
- now: clock(),
3893
- route: buildStamp,
3894
- });
3895
- if (!admittedAttended.ok)
3896
- throw new Error(admittedAttended.problem);
3897
- runId = admittedAttended.runId;
3898
- }
3899
- else if (leased.resumedFromRun !== undefined) {
3052
+ if (leased.resumedFromRun !== undefined) {
3900
3053
  // THE RECOVERED-DRAFT ADMISSION (atomic authority closure): the
3901
3054
  // fresh attempt inherits the interrupted attempt's draft, proved
3902
3055
  // this task's own interrupted builder in this very worktree.
@@ -3913,7 +3066,6 @@ async function tickCommand(flags, context) {
3913
3066
  recoveredFrom: leased.resumedFromRun,
3914
3067
  now: clock(),
3915
3068
  route: buildStamp,
3916
- custody: parkedChainTail !== null && liveCycle !== null ? { kind: "resume", parkedRun: parkedChainTail.id } : { kind: "base" },
3917
3069
  });
3918
3070
  if (!admittedRecovered.ok)
3919
3071
  throw new Error(admittedRecovered.problem);
@@ -3930,14 +3082,13 @@ async function tickCommand(flags, context) {
3930
3082
  ...(spec.model === null ? {} : { model: spec.model }),
3931
3083
  now: clock(),
3932
3084
  ...(buildStamp === null ? {} : { route: buildStamp }),
3933
- custody: parkedChainTail !== null && liveCycle !== null ? { kind: "resume", parkedRun: parkedChainTail.id } : { kind: "base" },
3934
3085
  });
3935
3086
  }
3936
3087
  }
3937
3088
  catch (error) {
3938
3089
  await worktrees.release(leased.worktree.path, clock());
3939
3090
  release(store, lease, clock());
3940
- dispatched.push({ id, outcome: "skipped", reason: error instanceof StaleAuthorization ? "stale-authorization" : "admission-refused", detail: error instanceof Error ? error.message : String(error) });
3091
+ dispatched.push({ id, outcome: "skipped", reason: "admission-refused", detail: error instanceof Error ? error.message : String(error) });
3941
3092
  continue;
3942
3093
  }
3943
3094
  if (leased.resumedFromRun !== undefined) {
@@ -3953,11 +3104,9 @@ async function tickCommand(flags, context) {
3953
3104
  const budgetLeft = budgeted.remainingMicrousd !== null && MONEY_CAPABILITIES[spec.provider].nativeDollarCapFlag !== null ? Math.max(1, budgeted.remainingMicrousd) : null;
3954
3105
  const capMicrousd = [scopeBudget, backstop, budgetLeft].reduce((least, one) => one === null ? least : least === null ? one : Math.min(least, one), null);
3955
3106
  if (capMicrousd !== null && MONEY_CAPABILITIES[spec.provider].nativeDollarCapFlag === null) {
3956
- // The run row and any chain cycle already exist — FINISH and RESOLVE
3957
- // them (E3d verify, R6): a refused-but-open run would defer a chain
3958
- // task forever and read as a vanished attempt everywhere else.
3107
+ // The run row already exists — FINISH it: a refused-but-open run
3108
+ // would read as a vanished attempt everywhere else.
3959
3109
  store.finishRun(runId, { outcome: "refused", reason: "budget-unenforceable", now: clock() });
3960
- store.resolveChainOnRunEnd(ref.id, id, repo, runId, clock());
3961
3110
  release(store, lease, clock());
3962
3111
  await worktrees.release(leased.worktree.path, clock());
3963
3112
  dispatched.push({ id, outcome: "skipped", reason: "budget-unenforceable" });
@@ -3996,29 +3145,7 @@ async function tickCommand(flags, context) {
3996
3145
  ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
3997
3146
  ...(leased.resumedFromRun === undefined ? {} : { recoveredDraftRun: leased.resumedFromRun }),
3998
3147
  ...(leased.recoveryKind === undefined ? {} : { recoveredDraftKind: leased.recoveryKind }),
3999
- ...(attendedDispatch === null || context.heldCoordinator === undefined
4000
- ? {}
4001
- : {
4002
- attended: {
4003
- authorization: attendedDispatch,
4004
- coordinator: context.heldCoordinator,
4005
- upIncarnation: context.upIncarnation ?? "unknown",
4006
- socketDir: context.heldSocketDir ?? tmpdir(),
4007
- releaseWorktree: async (path) => worktrees.release(path, clock()),
4008
- dispose: { repo, origin: ref.origin, provider: spec.provider, model: spec.model },
4009
- ...(context.heldStarter === undefined ? {} : { starter: context.heldStarter }),
4010
- ...(context.heldGraceMs === undefined ? {} : { graceMs: context.heldGraceMs }),
4011
- ...(context.maxHeldSessions === undefined ? {} : { maxHeldSessions: context.maxHeldSessions }),
4012
- },
4013
- }),
4014
3148
  });
4015
- // THE HELD HANDOFF (Phase 2, v2 S0d): ownership transferred — the
4016
- // coordinator owns run, lease, and worktree; this pass releases and
4017
- // settles NOTHING and moves on. Nonblocking is the whole point.
4018
- if (result.ok && result.parked === undefined && result.held === true) {
4019
- dispatched.push({ id, outcome: "held", branch, worktree: leased.worktree.path });
4020
- continue;
4021
- }
4022
3149
  // Handed back either way; a tree with somebody's work in it comes back
4023
3150
  // unverified rather than cleaned, same as `build`.
4024
3151
  await worktrees.release(leased.worktree.path, clock());
@@ -4042,13 +3169,6 @@ async function tickCommand(flags, context) {
4042
3169
  evidenceRoot: context.evidenceRoot,
4043
3170
  clock,
4044
3171
  }, result);
4045
- // The chain step (E3c/E3d): EVERY concluded run resolves its cycle
4046
- // through the one resolver — success closes, an ordinary end closes, a
4047
- // parked tail stays open for repair, and a recognized eligible
4048
- // exhaustion advances to pending-admission (fail-closed at every gate,
4049
- // the live grant re-proved in its own transaction). Inert unless the
4050
- // task filed under an explicit chain — no cycle, fast no-op.
4051
- store.resolveChainOnRunEnd(ref.id, id, repo, runId, clock());
4052
3172
  switch (disposition.kind) {
4053
3173
  case "parked":
4054
3174
  dispatched.push({ id, outcome: "parked", reason: `decision:${disposition.decisionId}`, worktree: leased.worktree.path });
@@ -4062,461 +3182,59 @@ async function tickCommand(flags, context) {
4062
3182
  dispatched.push({ id, outcome: "failed", reason: "external-closed", branch, worktree: leased.worktree.path });
4063
3183
  broke++;
4064
3184
  break;
4065
- case "built":
4066
- dispatched.push({ id, outcome: "built", committed: disposition.committed, branch, worktree: leased.worktree.path });
4067
- built++;
4068
- break;
4069
- case "built-fenced":
4070
- dispatched.push({ id, outcome: "failed", reason: "fenced", branch, worktree: leased.worktree.path });
4071
- broke++;
4072
- break;
4073
- case "skipped":
4074
- dispatched.push({ id, outcome: "skipped", reason: disposition.reason });
4075
- break;
4076
- case "fenced":
4077
- dispatched.push({ id, outcome: "failed", reason: "fenced", worktree: leased.worktree.path });
4078
- broke++;
4079
- break;
4080
- case "malformed":
4081
- dispatched.push({ id, outcome: "failed", reason: disposition.sealed ? "malformed-decision" : "fenced", worktree: leased.worktree.path });
4082
- broke++;
4083
- break;
4084
- case "failed":
4085
- dispatched.push({
4086
- id,
4087
- outcome: "failed",
4088
- reason: disposition.sealed
4089
- ? disposition.disposition === "auth-expired"
4090
- ? "auth-expired — requeued, sign-in needed"
4091
- : `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
4092
- : "fenced",
4093
- ...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
4094
- worktree: leased.worktree.path,
4095
- });
4096
- broke++;
4097
- break;
4098
- case "recorded":
4099
- // The standalone-only arm; unreachable under the tick policy.
4100
- break;
4101
- case "stopped":
4102
- // An operator's stop (v52): interrupted with its work preserved —
4103
- // not a break, not a strike; the task waits under the stop's hold.
4104
- dispatched.push({ id, outcome: "stopped", reason: `stop:${disposition.stopRun}`, branch, worktree: leased.worktree.path });
4105
- break;
4106
- case "handed-back":
4107
- // The service is stopping: the work is kept and the task requeued
4108
- // to resume on the next pass — not a break, not a strike.
4109
- dispatched.push({ id, outcome: "stopped", reason: "service-stop", detail: result.ok ? "" : result.message.slice(0, 200), branch, worktree: leased.worktree.path });
4110
- break;
4111
- case "invariant":
4112
- dispatched.push({ id, outcome: "failed", reason: disposition.reason });
4113
- broke++;
4114
- break;
4115
- }
4116
- }
4117
- untakenTrial?.giveBack();
4118
- // THE CHAIN ADMISSION PASS (E3d): cycles a recognized exhaustion advanced
4119
- // to pending-admission dispatch their NEXT approved entry here — the ONLY
4120
- // road that runs a fallback entry. Every authority is re-derived inside
4121
- // admitNextChainEntry (approved chain standing + digest match + LIVE
4122
- // paid-fallback grant + the single-use pending edge); this loop carries
4123
- // only claim, worktree, and rail, and its run then re-proves the
4124
- // chain-entry dispatch proof inside build() before any money moves.
4125
- for (const pending of buildsOnly ? [] : store.pendingChainAdmissions(repo)) {
4126
- settleFinished();
4127
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
4128
- break;
4129
- if (built >= max)
4130
- break;
4131
- // v105: a fallback billed to an API key spends dollars: a used-up budget holds it like any new work.
4132
- const chainEntry = store.approvedChainOf(pending.taskId)?.[pending.cursor];
4133
- const chainAgents = chainEntry === undefined ? [] : [{ provider: chainEntry.profile.provider, billing: chainEntry.authMode }];
4134
- // Sprint 8: a fallback entry the organisation policy doesn't allow never starts.
4135
- const chainPolicy = chainEntry === undefined ? null : policyHold([{ profile: chainEntry.profile }]);
4136
- if (chainPolicy !== null) {
4137
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "policy", detail: chainPolicy });
4138
- continue;
4139
- }
4140
- const chainBudget = budgetHold(pending.taskRef, chainAgents);
4141
- if (chainBudget.over !== null) {
4142
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget", detail: budgetWords(chainBudget) });
4143
- continue;
4144
- }
4145
- const railed = store.reserveModeRail(repo, 1, clock());
4146
- if (!railed.ok) {
4147
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: railed.rail, detail: railed.detail });
4148
- continue;
4149
- }
4150
- // A peek proves the entry EXISTS and carries an enforceable budget
4151
- // BEFORE any claim or run row exists (review findings 4/6); the
4152
- // admission below re-derives the entry as authority in its transaction.
4153
- const peek = store.approvedChainOf(pending.taskId)?.[pending.cursor];
4154
- if (peek === undefined) {
4155
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "fallback-unadmittable" });
4156
- continue;
4157
- }
4158
- const scopeBudget = store.getScope(pending.taskId)?.budgetMicrousd ?? null;
4159
- const backstop = store.getSpendDefaults()?.buildPerRunMicrousd ?? null;
4160
- const capMicrousd = scopeBudget === null ? backstop : backstop === null ? scopeBudget : Math.min(scopeBudget, backstop);
4161
- if (capMicrousd !== null && MONEY_CAPABILITIES[peek.profile.provider].nativeDollarCapFlag === null) {
4162
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget-unenforceable" });
4163
- continue;
4164
- }
4165
- // The sign-in pause holds a fallback entry too: the admission stays pending until its provider works.
4166
- const entryGate = signInGate(store, [peek.profile.provider], clock(), pending.taskRef);
4167
- if (entryGate.waiting !== null) {
4168
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(entryGate.waiting) });
4169
- continue;
4170
- }
4171
- // The FALLBACK claim (finding 4): every acquireIfReady gate — task
4172
- // state, non-backoff holds, blockers, approved scope + mode belt,
4173
- // capability, capacity, and quota keyed by the PINNED credential —
4174
- // with only the predecessor's backoff exempted.
4175
- const claimed = acquireFallback(store, pending.taskRef, runner, {
4176
- now: clock(),
4177
- token,
4178
- ttlMs: leaseTtlMs,
4179
- repo,
4180
- provider: peek.profile.provider,
4181
- model: peek.profile.model,
4182
- authMode: peek.authMode,
4183
- // The watch incarnation rides the claim (F+G review, finding 3): a
4184
- // daemon that dies after this claim — admitted or not — must be
4185
- // recoverable by its successor's incarnation takeover, exactly like
4186
- // the ordinary road.
4187
- ...(text(flags, "incarnation") === undefined ? {} : { incarnation: text(flags, "incarnation") }),
4188
- ...(text(flags, "max-open-decisions") === undefined
4189
- ? {}
4190
- : { maxOpenDecisions: Number(text(flags, "max-open-decisions")) }),
4191
- });
4192
- if (!claimed.ok) {
4193
- entryGate.giveBack();
4194
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: claimed.reason });
4195
- continue;
4196
- }
4197
- const lease = claimed.claim.leaseId;
4198
- const branch = await existingOrFirst(taskBranches(pending.taskId), async (one) => (await git("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${one}`], { cwd: repo })).code === 0);
4199
- const exists = await git("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: repo });
4200
- const leased = await worktrees.lease({
4201
- repo,
4202
- branch,
4203
- runner,
4204
- taskRef: pending.taskRef,
4205
- now: clock(),
4206
- ...(exists.code === 0 ? {} : { base }),
4207
- reclaim: { evidenceRoot: context.evidenceRoot },
4208
- });
4209
- if (!leased.ok) {
4210
- release(store, lease, clock());
4211
- dispatched.push({ id: pending.taskId, outcome: "failed", reason: leased.reason });
4212
- broke++;
4213
- continue;
4214
- }
4215
- const admitted = store.admitNextChainEntry(pending.cycleId, { leaseId: lease, runner, branch, worktree: leased.worktree.path, ...(leased.resumedFromRun === undefined ? {} : { recoveredFrom: leased.resumedFromRun }) }, clock());
4216
- if (!admitted.ok) {
4217
- await worktrees.release(leased.worktree.path, clock());
4218
- release(store, lease, clock());
4219
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: `fallback-${admitted.reason}`, ...(admitted.detail === undefined ? {} : { detail: admitted.detail }) });
4220
- continue;
4221
- }
4222
- if (leased.resumedFromRun !== undefined) {
4223
- store.addRunNote(admitted.runId, "Toolroll", `Recovered the ${leased.recoveryKind === "completed" ? "completed source draft" : "work-in-progress draft"} from interrupted attempt #${leased.resumedFromRun}. This fresh attempt is reviewing and verifying it; the safety patch is retained.`, clock());
4224
- }
4225
- // The effective cap, RE-DERIVED after admission (E3d verify, R6): the
4226
- // pre-claim value is a survey; a backstop set while the claim and
4227
- // worktree awaits ran must govern the spend that actually happens. The
4228
- // belt then re-proves capability against the ADMITTED provider — and on
4229
- // refusal the run is FINISHED and its cycle resolved, never abandoned.
4230
- const scopeBudgetNow = store.getScope(pending.taskId)?.budgetMicrousd ?? null;
4231
- const backstopNow = store.getSpendDefaults()?.buildPerRunMicrousd ?? null;
4232
- const budgetNow = chainEntry?.authMode === "api-key" ? budgetHold(pending.taskRef, chainAgents).remainingMicrousd : null;
4233
- const capNow = [scopeBudgetNow, backstopNow, budgetNow === null ? null : Math.max(1, budgetNow)]
4234
- .reduce((least, one) => one === null ? least : least === null ? one : Math.min(least, one), null);
4235
- if (capNow !== null && MONEY_CAPABILITIES[admitted.provider].nativeDollarCapFlag === null) {
4236
- store.finishRun(admitted.runId, { outcome: "refused", reason: "budget-unenforceable", now: clock() });
4237
- store.resolveChainOnRunEnd(pending.taskRef, admitted.taskId, repo, admitted.runId, clock());
4238
- release(store, lease, clock());
4239
- await worktrees.release(leased.worktree.path, clock());
4240
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget-unenforceable" });
4241
- continue;
4242
- }
4243
- budgetGate = null; // this build's spend counts toward the next check
4244
- const result = await build(store, {
4245
- taskId: pending.taskId,
4246
- taskRef: pending.taskRef,
4247
- runner,
4248
- leaseId: lease,
4249
- runnerToken: token,
4250
- runId: admitted.runId,
4251
- evidenceRoot: context.evidenceRoot,
4252
- worktree: leased.worktree.path,
4253
- branch,
4254
- now: clock(),
4255
- clock,
4256
- ...(capNow === null ? {} : { maxBudgetUsd: capNow / 1_000_000 }),
4257
- onProviderSpawn: pid => {
4258
- worktrees.recordProviderOccupancy(leased.worktree.path, runner, pid, leased.worktree.leaseEpoch);
4259
- },
4260
- provider: admitted.provider,
4261
- model: admitted.model,
4262
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
4263
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
4264
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
4265
- ...(leased.resumedFromRun === undefined ? {} : { recoveredDraftRun: leased.resumedFromRun }),
4266
- ...(leased.recoveryKind === undefined ? {} : { recoveredDraftKind: leased.recoveryKind }),
4267
- });
4268
- await worktrees.release(leased.worktree.path, clock());
4269
- const disposition = disposeBuildOutcome({
4270
- store,
4271
- policy: "tick",
4272
- leaseId: lease,
4273
- runId: admitted.runId,
4274
- taskId: pending.taskId,
4275
- taskRef: pending.taskRef,
4276
- runner,
4277
- repo,
4278
- branch,
4279
- origin: "ours",
4280
- provider: admitted.provider,
4281
- model: admitted.model,
4282
- worktreePath: leased.worktree.path,
4283
- evidenceRoot: context.evidenceRoot,
4284
- clock,
4285
- }, result);
4286
- // The same one resolver: a fallback entry that itself exhausts advances
4287
- // again; one that succeeds or ordinarily ends closes its cycle.
4288
- store.resolveChainOnRunEnd(pending.taskRef, pending.taskId, repo, admitted.runId, clock());
4289
- switch (disposition.kind) {
4290
- case "built":
4291
- dispatched.push({ id: pending.taskId, outcome: "built", committed: disposition.committed, branch, worktree: leased.worktree.path });
4292
- built++;
4293
- break;
4294
- case "stopped":
4295
- dispatched.push({ id: pending.taskId, outcome: "stopped", reason: `stop:${disposition.stopRun}`, branch, worktree: leased.worktree.path });
4296
- break;
4297
- case "handed-back":
4298
- dispatched.push({ id: pending.taskId, outcome: "stopped", reason: "service-stop", branch, worktree: leased.worktree.path });
4299
- break;
4300
- case "parked":
4301
- dispatched.push({ id: pending.taskId, outcome: "parked", reason: `decision:${disposition.decisionId}`, worktree: leased.worktree.path });
4302
- parked++;
4303
- break;
4304
- case "skipped":
4305
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: disposition.reason });
4306
- break;
4307
- default:
4308
- dispatched.push({ id: pending.taskId, outcome: "failed", reason: "fallback-attempt", worktree: leased.worktree.path });
4309
- broke++;
4310
- break;
4311
- }
4312
- }
4313
- // THE CONTINUATION PASS (Phase 2E, A4): open continuation authorizations
4314
- // named to this runner dispatch here — the finished parent task never
4315
- // re-enters the queue; the authorization is the claimable unit (v3 R7).
4316
- // Only a co-located coordinator can hold the session, and everything
4317
- // else (liveness, one attempt, the final proof at the parent's exact
4318
- // head) is re-proved on the way in.
4319
- if (context.heldCoordinator !== undefined && !buildsOnly) {
4320
- for (const continuation of store.openContinuationAuthorizations(runner)) {
4321
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
4322
- break;
4323
- const watching = attendedLivenessState(continuation.lastBeatAt === null ? null : Date.parse(continuation.lastBeatAt), clock().getTime(), Date.parse(continuation.absoluteExpiry));
4324
- if (watching !== "live" && watching !== "grace")
4325
- continue;
4326
- const parent = continuation.parentRun === null ? null : store.getRun(continuation.parentRun);
4327
- const parentRef = parent === null ? null : store.refForId(parent.taskRef);
4328
- if (parent === null || parentRef === null)
4329
- continue;
4330
- // A continuation continues WORK: a branchless parent (the reviewer
4331
- // role, v29) has no workspace to lease and can never be continued —
4332
- // and this guard is what keeps "null" out of `git worktree add`.
4333
- if (parent.branch === null)
4334
- continue;
4335
- const parentBranch = parent.branch;
4336
- if (parentRef.repo !== null && parentRef.repo !== repo)
4337
- continue;
4338
- const taskId = parentRef.externalId;
4339
- let pinned = null;
4340
- try {
4341
- const terms = JSON.parse(continuation.termsJson);
4342
- const profile = profileFromJson(typeof terms.profileJson === "string" ? terms.profileJson : null);
4343
- if (profile !== null)
4344
- pinned = { provider: profile.provider, model: profile.model, digest: profileDigestOf(profile), profile };
4345
- }
4346
- catch {
4347
- pinned = null;
4348
- }
4349
- if (pinned === null) {
4350
- dispatched.push({ id: taskId, outcome: "skipped", reason: "attended-only", detail: "the continuation's pinned profile cannot be read" });
4351
- continue;
4352
- }
4353
- // Sprint 8: an attended continuation runs at exactly its signed terms, or not at all.
4354
- const continuationPolicy = policyHold([{ profile: pinned.profile, attended: true }]);
4355
- if (continuationPolicy !== null) {
4356
- dispatched.push({ id: taskId, outcome: "skipped", reason: "policy", detail: continuationPolicy });
4357
- continue;
4358
- }
4359
- const continuationBudget = budgetHold(parent.taskRef, store.agentsFor([pinned.provider]));
4360
- if (continuationBudget.over !== null) {
4361
- dispatched.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(continuationBudget) });
4362
- continue;
4363
- }
4364
- // The sign-in pause holds a continuation too: its authorization stays open and it starts once the sign-in works.
4365
- const continuationGate = signInGate(store, [pinned.provider], clock(), parent.taskRef);
4366
- if (continuationGate.waiting !== null) {
4367
- dispatched.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(continuationGate.waiting) });
4368
- continue;
4369
- }
4370
- const claimed = acquireContinuation(store, continuation, runner, { now: clock(), token, ttlMs: leaseTtlMs });
4371
- if (!claimed.ok) {
4372
- continuationGate.giveBack();
4373
- dispatched.push({ id: taskId, outcome: "skipped", reason: claimed.reason, ...("message" in claimed ? { detail: claimed.message } : {}) });
4374
- continue;
4375
- }
4376
- const lease = claimed.claim.leaseId;
4377
- // The parent's branch, at the head the terms signed — a moved branch
4378
- // fails the final proof with words naming the head.
4379
- const leased = await worktrees.lease({ repo, branch: parentBranch, runner, taskRef: parent.taskRef, now: clock() });
4380
- if (!leased.ok) {
4381
- release(store, lease, clock());
4382
- dispatched.push({ id: taskId, outcome: "failed", reason: leased.reason });
4383
- broke++;
4384
- continue;
4385
- }
4386
- // THE SIGNED HEAD, BEFORE THE ATTEMPT IS SPENT (final admission
4387
- // closure): the continuation's terms signed the exact commit the
4388
- // parent finished at, and the leased worktree's HEAD is read here
4389
- // and held to it BEFORE admitAttended — the same order the queue's
4390
- // attended road keeps. A head the terms do not sign, or one that
4391
- // moved, opens no run, spends no attempt, invokes no provider: the
4392
- // worktree and claim are released and the authorization closes in
4393
- // the refusal's words — one terminal refusal, recorded once — so
4394
- // the operator authorizes again at today's head.
4395
- let signedContinuationHead = null;
4396
- try {
4397
- const terms = JSON.parse(continuation.termsJson);
4398
- signedContinuationHead = typeof terms.head === "string" && terms.head !== "" ? terms.head : null;
4399
- }
4400
- catch {
4401
- signedContinuationHead = null;
4402
- }
4403
- const continuationHeadRead = signedContinuationHead === null ? null : await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
4404
- if (continuationHeadRead !== null && continuationHeadRead.code !== 0) {
4405
- // The worktree's HEAD could not be READ — the head did not move,
4406
- // so nothing terminal is said about the authorization: custody is
4407
- // released and this tick records the failure; the next one reads
4408
- // again under the same open authorization.
4409
- await worktrees.release(leased.worktree.path, clock());
4410
- release(store, lease, clock());
4411
- dispatched.push({ id: taskId, outcome: "failed", reason: "head-unreadable", detail: `${taskId}: the leased worktree's HEAD could not be read (${continuationHeadRead.stderr.trim() || `git exited ${continuationHeadRead.code}`}) — the continuation authorization ${continuation.id} stays open; no run opened, no attempt spent` });
4412
- broke++;
4413
- continue;
4414
- }
4415
- const continuationHeadNow = continuationHeadRead === null ? "" : continuationHeadRead.stdout.trim();
4416
- if (signedContinuationHead === null || continuationHeadNow !== signedContinuationHead) {
4417
- await worktrees.release(leased.worktree.path, clock());
4418
- release(store, lease, clock());
4419
- store.closeAuthorization(continuation.id, "refused:stale-authorization", clock());
4420
- dispatched.push({
4421
- id: taskId,
4422
- outcome: "skipped",
4423
- reason: "stale-authorization",
4424
- detail: signedContinuationHead === null
4425
- ? `${taskId}: the continuation authorization ${continuation.id} signs no readable head — nothing opens under it; no run opened, no attempt spent`
4426
- : `${taskId}: the head moved since the continuation authorization ${continuation.id} was signed (${signedContinuationHead.slice(0, 12)} → ${continuationHeadNow === "" ? "empty" : continuationHeadNow.slice(0, 12)}) — no run opened, no attempt spent; authorize it again at today's head`,
4427
- });
4428
- continue;
4429
- }
4430
- // The continuation spends under the authorization's pinned profile
4431
- // and says so at insert (v48 integrity).
4432
- // THE ATTENDED ADMISSION (atomic authority closure): the continuation
4433
- // opens under exactly this authorization — id, runner, generation —
4434
- // continuing the finished parent it names; the insert consumes the
4435
- // one attempt and binds the row to it.
4436
- const admittedContinuation = store.admitAttended({
4437
- taskRef: parent.taskRef,
4438
- leaseId: lease,
4439
- runner,
4440
- branch: parentBranch,
4441
- worktree: leased.worktree.path,
4442
- parentRun: parent.id,
4443
- provider: pinned.provider,
4444
- ...(pinned.model === null ? {} : { model: pinned.model }),
4445
- authorization: { id: continuation.id, runner: continuation.runner, generation: continuation.runnerGeneration },
4446
- now: clock(),
4447
- route: { routeDigest: `profile:${pinned.digest}`, phase: "build", provider: pinned.provider, model: pinned.model, chosen: "legacy" },
4448
- });
4449
- if (!admittedContinuation.ok) {
4450
- await worktrees.release(leased.worktree.path, clock());
4451
- release(store, lease, clock());
4452
- dispatched.push({ id: taskId, outcome: "skipped", reason: "admission-refused", detail: admittedContinuation.problem });
4453
- continue;
4454
- }
4455
- const runId = admittedContinuation.runId;
4456
- budgetGate = null; // this build's spend counts toward the next check
4457
- const result = await build(store, {
4458
- taskId,
4459
- taskRef: parent.taskRef,
4460
- runner,
4461
- leaseId: lease,
4462
- runnerToken: token,
4463
- runId,
4464
- evidenceRoot: context.evidenceRoot,
4465
- worktree: leased.worktree.path,
4466
- branch: parentBranch,
4467
- now: clock(),
4468
- clock,
4469
- provider: pinned.provider,
4470
- ...(pinned.model === null ? {} : { model: pinned.model }),
4471
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
4472
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
4473
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
4474
- attended: {
4475
- authorization: continuation,
4476
- coordinator: context.heldCoordinator,
4477
- upIncarnation: context.upIncarnation ?? "unknown",
4478
- socketDir: context.heldSocketDir ?? tmpdir(),
4479
- releaseWorktree: async (path) => worktrees.release(path, clock()),
4480
- dispose: { repo, origin: parentRef.origin, provider: pinned.provider, model: pinned.model, policy: "continuation" },
4481
- ...(context.heldStarter === undefined ? {} : { starter: context.heldStarter }),
4482
- ...(context.heldGraceMs === undefined ? {} : { graceMs: context.heldGraceMs }),
4483
- ...(context.maxHeldSessions === undefined ? {} : { maxHeldSessions: context.maxHeldSessions }),
4484
- },
4485
- });
4486
- if (result.ok && result.parked === undefined && result.held === true) {
4487
- dispatched.push({ id: taskId, outcome: "held", branch: parentBranch, worktree: leased.worktree.path });
4488
- continue;
4489
- }
4490
- // A refusal before the hold (stale proof, spawn failure): record it
4491
- // through the continuation policy — taskless, always — and release.
4492
- await worktrees.release(leased.worktree.path, clock());
4493
- const disposition = disposeBuildOutcome({
4494
- store,
4495
- policy: "continuation",
4496
- leaseId: lease,
4497
- runId,
4498
- taskId,
4499
- taskRef: parent.taskRef,
4500
- runner,
4501
- repo,
4502
- branch: parentBranch,
4503
- origin: parentRef.origin,
4504
- provider: pinned.provider,
4505
- model: pinned.model,
4506
- worktreePath: leased.worktree.path,
4507
- evidenceRoot: context.evidenceRoot,
4508
- clock,
4509
- }, result);
4510
- dispatched.push({
4511
- id: taskId,
4512
- outcome: disposition.kind === "built" ? "built" : disposition.kind === "stopped" ? "stopped" : "failed",
4513
- ...(disposition.kind === "stopped" ? { reason: `stop:${disposition.stopRun}` } : result.ok ? {} : { reason: result.reason }),
4514
- worktree: leased.worktree.path,
4515
- });
4516
- if (disposition.kind !== "built" && disposition.kind !== "stopped")
3185
+ case "built":
3186
+ dispatched.push({ id, outcome: "built", committed: disposition.committed, branch, worktree: leased.worktree.path });
3187
+ built++;
3188
+ break;
3189
+ case "built-fenced":
3190
+ dispatched.push({ id, outcome: "failed", reason: "fenced", branch, worktree: leased.worktree.path });
3191
+ broke++;
3192
+ break;
3193
+ case "skipped":
3194
+ dispatched.push({ id, outcome: "skipped", reason: disposition.reason });
3195
+ break;
3196
+ case "fenced":
3197
+ dispatched.push({ id, outcome: "failed", reason: "fenced", worktree: leased.worktree.path });
3198
+ broke++;
3199
+ break;
3200
+ case "malformed":
3201
+ dispatched.push({ id, outcome: "failed", reason: disposition.sealed ? "malformed-decision" : "fenced", worktree: leased.worktree.path });
3202
+ broke++;
3203
+ break;
3204
+ case "failed":
3205
+ dispatched.push({
3206
+ id,
3207
+ outcome: "failed",
3208
+ reason: disposition.sealed
3209
+ ? disposition.disposition === "auth-expired"
3210
+ ? "auth-expired — requeued, sign-in needed"
3211
+ : `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
3212
+ : "fenced",
3213
+ ...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
3214
+ worktree: leased.worktree.path,
3215
+ });
3216
+ broke++;
3217
+ break;
3218
+ case "recorded":
3219
+ // The standalone-only arm; unreachable under the tick policy.
3220
+ break;
3221
+ case "stopped":
3222
+ // An operator's stop (v52): interrupted with its work preserved —
3223
+ // not a break, not a strike; the task waits under the stop's hold.
3224
+ dispatched.push({ id, outcome: "stopped", reason: `stop:${disposition.stopRun}`, branch, worktree: leased.worktree.path });
3225
+ break;
3226
+ case "handed-back":
3227
+ // The service is stopping: the work is kept and the task requeued
3228
+ // to resume on the next pass — not a break, not a strike.
3229
+ dispatched.push({ id, outcome: "stopped", reason: "service-stop", detail: result.ok ? "" : result.message.slice(0, 200), branch, worktree: leased.worktree.path });
3230
+ break;
3231
+ case "invariant":
3232
+ dispatched.push({ id, outcome: "failed", reason: disposition.reason });
4517
3233
  broke++;
3234
+ break;
4518
3235
  }
4519
3236
  }
3237
+ untakenTrial?.giveBack();
4520
3238
  // The one automatic review each finished build gets when its project's
4521
3239
  // switch is on, before the result is handed on below. Other queued review
4522
3240
  // asks close unrun. Reauthenticated first; only admitted projects.
@@ -4540,11 +3258,6 @@ async function tickCommand(flags, context) {
4540
3258
  ...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
4541
3259
  const summary = () => {
4542
3260
  const lines = [`Considered ${considered}, built ${built}, parked ${parked}, broke ${broke}.`];
4543
- for (const entry of routines) {
4544
- lines.push(entry.outcome === "fired"
4545
- ? ` routine ${entry.routine.padEnd(16)} fired ${entry.taskId ?? ""}`.trimEnd()
4546
- : ` routine ${entry.routine.padEnd(16)} skipped ${entry.detail ?? entry.outcome}`);
4547
- }
4548
3261
  for (const entry of dispatched) {
4549
3262
  const detail = entry.outcome === "built"
4550
3263
  ? entry.committed === true
@@ -4569,21 +3282,19 @@ async function tickCommand(flags, context) {
4569
3282
  return fail(write, json, "tick", "build-failed", `${broke} of ${dispatched.length} dispatched tasks broke`, EXIT.failed, {
4570
3283
  considered,
4571
3284
  dispatched,
4572
- routines,
4573
3285
  });
4574
3286
  }
4575
- if (built > 0 || parked > 0 || dispatched.some(one => one.outcome === "planned" || one.outcome === "reported" || one.outcome === "held" || one.outcome === "reviewed" || one.outcome === "not-reviewed")) {
4576
- return succeed(write, json, "tick", { considered, dispatched, routines, ...flows }, summary);
3287
+ if (built > 0 || parked > 0 || dispatched.some(one => one.outcome === "planned" || one.outcome === "reported" || one.outcome === "reviewed" || one.outcome === "not-reviewed")) {
3288
+ return succeed(write, json, "tick", { considered, dispatched, ...flows }, summary);
4577
3289
  }
4578
3290
  if (considered === 0) {
4579
3291
  return fail(write, json, "tick", "empty", "nothing is ready", EXIT.refused, {
4580
3292
  considered,
4581
3293
  dispatched,
4582
- routines,
4583
3294
  ...flows,
4584
3295
  });
4585
3296
  }
4586
- return fail(write, json, "tick", "nothing-dispatched", "everything ready is waiting on a person or held by somebody else", EXIT.refused, { considered, dispatched, routines, ...flows });
3297
+ return fail(write, json, "tick", "nothing-dispatched", "everything ready is waiting on a person or held by somebody else", EXIT.refused, { considered, dispatched, ...flows });
4587
3298
  }
4588
3299
  /**
4589
3300
  * The recovery sweep: everything an unattended stretch may have left behind, in one pass.
@@ -4922,7 +3633,7 @@ async function briefCommand(flags, context) {
4922
3633
  lines.push(` #${pull.number} ${pull.title}`);
4923
3634
  }
4924
3635
  if (pending.length > 0) {
4925
- lines.push(` ▸ OUTBOX ${pending.length} undelivered — toolroll outbox deliver --cmd …`);
3636
+ lines.push(` ▸ OUTBOX ${pending.length} undelivered — toolroll outbox list`);
4926
3637
  }
4927
3638
  if (decisions.length > 0) {
4928
3639
  const overdue = decisions.filter(one => one.state === "expired").length;
@@ -5102,7 +3813,6 @@ async function startConsole(options) {
5102
3813
  ...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
5103
3814
  ...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
5104
3815
  ...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
5105
- ...(options.attended === undefined ? {} : { attended: options.attended }),
5106
3816
  });
5107
3817
  await new Promise((resolve, reject) => {
5108
3818
  server.once("error", reject);
@@ -5221,9 +3931,6 @@ async function requeueTask(positional, flags, context) {
5221
3931
  if (id === undefined) {
5222
3932
  return fail(write, json, "task requeue", "usage", "`toolroll task requeue <id> --as <you> --token <t>`", EXIT.usage);
5223
3933
  }
5224
- const racingGuard = refuseWhileRacing(context, "task requeue", id);
5225
- if (racingGuard !== null)
5226
- return racingGuard;
5227
3934
  const acting = await askCredentials(flags, context);
5228
3935
  if (acting === null) {
5229
3936
  return fail(write, json, "task requeue", "usage", "requeueing takes `--as <you> --token <t>` — who overrode the stall is recorded, not asserted", EXIT.usage);
@@ -5429,18 +4136,16 @@ async function incidentCommand(positional, flags, context) {
5429
4136
  ]);
5430
4137
  }
5431
4138
  /**
5432
- * `toolroll webhook …` — Slack and Discord as UI-ONLY mirrors: every
5433
- * page is a message with a console link; acting stays in the console
5434
- * behind its own authentication. The URL is a credential: 0600 file
5435
- * beside the database, or the environment, never anywhere else.
4139
+ * `toolroll webhook status|primary|set console-url` — messaging settings:
4140
+ * which connected chat service receives alerts, and the console address
4141
+ * chat links open.
5436
4142
  */
5437
- async function webhookCommand(positional, flags, context) {
5438
- const { store, write, json, clock } = context;
4143
+ async function webhookCommand(positional, _flags, context) {
4144
+ const { write, json } = context;
5439
4145
  const dir = dirname(context.databaseFile);
5440
4146
  const [action, which, value] = positional;
5441
4147
  const telegramConfigured = loadBotToken(process.env, context.telegramTokenFile) !== null;
5442
4148
  if (action === undefined || action === "status") {
5443
- const targets = loadWebhookTargets(process.env, dir);
5444
4149
  const consoleUrl = loadConsoleUrl(process.env, dir);
5445
4150
  const primary = effectivePrimary(process.env, dir, telegramConfigured);
5446
4151
  if (json) {
@@ -5456,29 +4161,8 @@ async function webhookCommand(positional, flags, context) {
5456
4161
  write(` Choose: toolroll webhook primary telegram|slack|discord`);
5457
4162
  }
5458
4163
  write(` links ${consoleUrl ?? "NOT SET — messages will carry no console link; toolroll webhook set console-url http://host:port"}`);
5459
- if (targets.length === 0) {
5460
- write("");
5461
- write(" toolroll webhook set slack https://hooks.slack.com/services/…");
5462
- write(" toolroll webhook set discord https://discord.com/api/webhooks/…");
5463
- write(` (or export ${SLACK_ENV} / ${DISCORD_ENV})`);
5464
- }
5465
- write("");
5466
- write(" Mirrors deliver when Telegram is not configured; with a paired Telegram");
5467
- write(" chat, Telegram carries the page (it can hold buttons) and mirrors stay quiet.");
5468
4164
  return EXIT.ok;
5469
4165
  }
5470
- if (action === "test") {
5471
- const targets = loadWebhookTargets(process.env, dir);
5472
- if (targets.length === 0) {
5473
- return fail(write, json, "webhook test", "unconfigured", "no webhook configured — `toolroll webhook set slack|discord <url>`", EXIT.refused);
5474
- }
5475
- store.enqueueNotification({ source: { installation: true }, dedupeKey: `webhook-test:${clock().getTime()}`, kind: "test", subject: "toolroll webhook test", body: "If you can read this, the mirror works. Acting happens in the console." }, clock());
5476
- const report = await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock });
5477
- if (report.problems.length > 0) {
5478
- return fail(write, json, "webhook test", "delivery", report.problems.join("; "), EXIT.failed);
5479
- }
5480
- return succeed(write, json, "webhook test", { sent: report.sent }, () => [`Sent ${report.sent} message(s). Check the channel.`]);
5481
- }
5482
4166
  if (action === "primary") {
5483
4167
  if (which === undefined || !isMessagingChannel(which)) {
5484
4168
  return fail(write, json, "webhook primary", "usage", "primary is one of telegram, slack, discord", EXIT.usage);
@@ -5492,46 +4176,13 @@ async function webhookCommand(positional, flags, context) {
5492
4176
  `Alerts now go to ${which}. ${which === "telegram" ? "Buttons and replies work there as always." : "Telegram (if configured) still accepts taps and replies — it just stops sending alerts."}`,
5493
4177
  ]);
5494
4178
  }
5495
- if (action === "clear" && (which === "slack" || which === "discord")) {
5496
- clearWebhook(dir, which);
5497
- return succeed(write, json, "webhook clear", { which }, () => [`${which} mirror cleared.`]);
5498
- }
5499
- if (action !== "set" || which === undefined || value === undefined) {
5500
- return fail(write, json, "webhook", "usage", "`toolroll webhook [status|test|set slack|discord|console-url <value>|clear slack|discord]`", EXIT.usage);
4179
+ if (action !== "set" || which !== "console-url" || value === undefined) {
4180
+ return fail(write, json, "webhook", "usage", "`toolroll webhook [status|primary <service>|set console-url <url>]`", EXIT.usage);
5501
4181
  }
5502
- if (which === "console-url") {
5503
- const saved = saveConsoleUrl(dir, value);
5504
- if (!saved.ok)
5505
- return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
5506
- return succeed(write, json, "webhook set", { which }, () => [`Console links will open ${value.replace(/\/+$/, "")}.`]);
5507
- }
5508
- if (which !== "slack" && which !== "discord") {
5509
- return fail(write, json, "webhook set", "usage", "set what? slack, discord, or console-url", EXIT.usage);
5510
- }
5511
- const saved = saveWebhook(dir, which, value);
4182
+ const saved = saveConsoleUrl(dir, value);
5512
4183
  if (!saved.ok)
5513
4184
  return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
5514
- // The first moment more than one service exists is the moment to ask
5515
- // which one pages — once, right here, not at 3am when both fire.
5516
- const after = effectivePrimary(process.env, dir, telegramConfigured);
5517
- let chosen = null;
5518
- if (after.implicit && loadPrimary(process.env, dir) === null && after.configured.length > 1 && interactive() && !json) {
5519
- write(`You now have ${after.configured.join(" and ")} connected.`);
5520
- const answer = (await ask(`Which service should receive alerts? [${after.configured.join("/")}] `)).trim().toLowerCase();
5521
- if (isMessagingChannel(answer) && after.configured.includes(answer)) {
5522
- savePrimary(dir, answer);
5523
- chosen = answer;
5524
- }
5525
- else {
5526
- write(`Left unchosen — ${after.channel} receives alerts by default. Decide any time: toolroll webhook primary <service>`);
5527
- }
5528
- }
5529
- return succeed(write, json, "webhook set", { which, ...(chosen === null ? {} : { primary: chosen }) }, () => [
5530
- `${which} mirror configured — the URL lives in a private file beside the database.`,
5531
- ...(chosen === null ? [] : [`${chosen} carries the pages.`]),
5532
- ...(after.implicit && chosen === null && !interactive() ? [`Several services are configured — choose the pager: toolroll webhook primary <service>`] : []),
5533
- `Send yourself a proof: toolroll webhook test`,
5534
- ]);
4185
+ return succeed(write, json, "webhook set", { which }, () => [`Console links will open ${value.replace(/\/+$/, "")}.`]);
5535
4186
  }
5536
4187
  /**
5537
4188
  * `toolroll providers resume <provider>` — a person says the sign-in works
@@ -5594,10 +4245,6 @@ async function providersCommand(flags, context) {
5594
4245
  const lastSuccess = store.providerLastSuccess(id);
5595
4246
  const keyPresent = facts.requiresEnv === null ? null : (process.env[facts.requiresEnv] ?? "") !== "";
5596
4247
  const installedVersion = installed ? (version.stdout.trim().split("\n")[0] ?? "") : null;
5597
- const authMode = readAuthMode(id);
5598
- const attestation = attestationOf(id);
5599
- const versionProvenAtSpawn = installedVersion !== null && attestation !== null && versionInRange(installedVersion, attestation);
5600
- const exhaustionRecognized = versionProvenAtSpawn && recognizesEligible(id, installedVersion, authMode);
5601
4248
  report.push({
5602
4249
  provider: id,
5603
4250
  binary: facts.binary,
@@ -5608,19 +4255,6 @@ async function providersCommand(flags, context) {
5608
4255
  ...(keyPresent === null ? {} : { keyPresent, keyEnv: facts.requiresEnv }),
5609
4256
  measuresCost: facts.measuresCost,
5610
4257
  configuredPhases: configured.get(id) ?? [],
5611
- fallbackReadiness: {
5612
- authMode,
5613
- versionProvenAtSpawn,
5614
- exhaustionRecognized,
5615
- automaticSwitchArmed: exhaustionRecognized,
5616
- reason: exhaustionRecognized
5617
- ? "this build can recognize an exhausted credential for the installed provider version"
5618
- : attestation === null
5619
- ? "this provider does not yet prove its version at spawn, so exhaustion classification fails closed"
5620
- : !versionProvenAtSpawn
5621
- ? "the installed provider version is not inside this build's attested range"
5622
- : "no reviewed exhaustion fixture recognizes this auth mode at the installed provider version",
5623
- },
5624
4258
  // The audit: facts about the harness, reported before any of them is
5625
4259
  // enforced. What transport we read, whether a session can resume,
5626
4260
  // which init signal exists, what hermetic flag we deliberately do NOT
@@ -5683,14 +4317,10 @@ async function providersCommand(flags, context) {
5683
4317
  if (one["keyEnv"] !== undefined) {
5684
4318
  write(` ${String(one["keyEnv"])} ${one["keyPresent"] === true ? "present (not validated — presence is not authorization)" : "ABSENT — runs will fail until the runner exports it"}`);
5685
4319
  }
5686
- write(` cost ${one["measuresCost"] === true ? "measured in dollars per run" : "tokens only — runs land as UNMEASURED; ceilinged routines fail closed on them"}`);
4320
+ write(` cost ${one["measuresCost"] === true ? "measured in dollars per run" : "tokens only — runs land as UNMEASURED; schedules with a weekly limit fail closed on them"}`);
5687
4321
  const phases = one["configuredPhases"];
5688
4322
  if (phases.length > 0)
5689
4323
  write(` configured ${phases.join(", ")} (installation)`);
5690
- const fallback = one["fallbackReadiness"];
5691
- write(` auto fallback ${fallback.automaticSwitchArmed
5692
- ? "armed — a configured and approved chain may advance after proven exhaustion"
5693
- : `not armed — ${fallback.reason}`}`);
5694
4324
  const audit = one["audit"];
5695
4325
  write(` transport ${audit.transport}${audit.initSignal === "none" ? " — no init signal; a failed run cannot say whether the harness came up" : ` — init signal: ${audit.initSignal}`}`);
5696
4326
  write(` resume ${audit.resume}`);
@@ -6028,10 +4658,6 @@ async function modeCommand(positional, flags, context) {
6028
4658
  if (terms.planAuto && !terms.autoApproveFiling) {
6029
4659
  return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
6030
4660
  }
6031
- // The paid-fallback grant (R8): NEVER a preset default — only this
6032
- // explicit flag lets an exhausted subscription switch to another account.
6033
- if (flag(flags, "allow-paid-fallback"))
6034
- terms.allowPaidFallback = true;
6035
4661
  // Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
6036
4662
  if (flag(flags, "chat-approve"))
6037
4663
  terms.chatApprove = true;
@@ -6066,9 +4692,8 @@ async function configCommand(positional, flags, context) {
6066
4692
  : { problem: answer.problem }),
6067
4693
  };
6068
4694
  });
6069
- const fallback = scope === INSTALLATION_SCOPE ? [] : store.fallbackConfig(scope);
6070
4695
  // The STRONG tier (v47): the named strongest agent per phase, which
6071
- // high-risk, strict, evidence-sensitive, and publication-sensitive
4696
+ // large, strict, evidence-sensitive, and publication-sensitive
6072
4697
  // routes reach for. Never inferred — absent means "the default, said".
6073
4698
  const candidates = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
6074
4699
  const strong = ["plan", "build", "repair", "review"].map(one => ({
@@ -6079,7 +4704,7 @@ async function configCommand(positional, flags, context) {
6079
4704
  // A light planner from before light became build-only is never used: said, with how to clear it.
6080
4705
  const unusedLight = [INSTALLATION_SCOPE, ...(scope === INSTALLATION_SCOPE ? [] : [scope])].filter(one => store.phaseTierConfig(one, "plan", "light") !== null);
6081
4706
  if (json) {
6082
- write(envelopeJson({ ok: true, command: "config show", installation, project, resolved, fallback, strong, installationStrong: store.listPhaseTierConfig(INSTALLATION_SCOPE), projectStrong: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierConfig(scope), installationAlso: store.listPhaseTierAlternates(INSTALLATION_SCOPE), projectAlso: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierAlternates(scope) }));
4707
+ write(envelopeJson({ ok: true, command: "config show", installation, project, resolved, strong, installationStrong: store.listPhaseTierConfig(INSTALLATION_SCOPE), projectStrong: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierConfig(scope), installationAlso: store.listPhaseTierAlternates(INSTALLATION_SCOPE), projectAlso: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierAlternates(scope) }));
6083
4708
  return EXIT.ok;
6084
4709
  }
6085
4710
  write(`Effective phase agents${scope === INSTALLATION_SCOPE ? "" : ` for ${scope}`}:`);
@@ -6092,7 +4717,7 @@ async function configCommand(positional, flags, context) {
6092
4717
  }
6093
4718
  }
6094
4719
  write("");
6095
- write(" strong tier (high-risk, strict, screenshot-proof, and automerge routes reach for these):");
4720
+ write(" strong tier (large or risky, strict, screenshot-proof, and automerge routes reach for these):");
6096
4721
  for (const one of strong) {
6097
4722
  write(` ${one.phase.padEnd(8)} ${one.strong === null ? "none configured — such routes keep the default above and say so" : `${one.strong.provider}${one.strong.model === null ? " (harness default model)" : ` · ${one.strong.model}`} [${one.strong.source}]`}`);
6098
4723
  }
@@ -6115,14 +4740,6 @@ async function configCommand(positional, flags, context) {
6115
4740
  }
6116
4741
  }
6117
4742
  write(" repair note: the repair PROVIDER always inherits the build it mends — only its model is configurable.");
6118
- if (fallback.length > 0) {
6119
- write("");
6120
- write(" if the build agent's subscription runs out, NEW approvals bind this fallback chain:");
6121
- fallback.forEach((one, i) => {
6122
- write(` ${i + 1}. ${one.provider} (${one.model}) — ${one.authMode === "subscription" ? "its subscription login" : "your API key"}`);
6123
- });
6124
- write(" it fires only when a signed mode allows the paid fallback (`mode set --allow-paid-fallback`).");
6125
- }
6126
4743
  if (installation.length === 0 && project.length === 0) {
6127
4744
  write(" nothing configured — every phase runs the default (claude).");
6128
4745
  write(" toolroll config set build --provider claude --model sonnet --as <you> --token <t>");
@@ -6145,7 +4762,7 @@ async function configCommand(positional, flags, context) {
6145
4762
  return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
6146
4763
  }
6147
4764
  if (action === "clear") {
6148
- store.setSpendDefaults({ buildPerRunMicrousd: null, racePerAgentMicrousd: null, raceTotalMicrousd: null }, acting.name, clock());
4765
+ store.setSpendDefaults({ buildPerRunMicrousd: null }, acting.name, clock());
6149
4766
  return succeed(write, json, "config clear", { budgets: null }, () => ["Spend defaults cleared — filings state their own numbers again."]);
6150
4767
  }
6151
4768
  const parseUsd = (flag) => {
@@ -6156,117 +4773,13 @@ async function configCommand(positional, flags, context) {
6156
4773
  return Number.isFinite(value) && value > 0 ? Math.round(value * 1_000_000) : false;
6157
4774
  };
6158
4775
  const build = parseUsd("build-usd");
6159
- const racePer = parseUsd("race-per-usd");
6160
- const raceTotal = parseUsd("race-total-usd");
6161
- // Name the flag that was bad — "budgets are positive dollar amounts"
6162
- // over four candidates left the caller diffing their own command line.
6163
- const badFlag = build === false ? "--build-usd" : racePer === false ? "--race-per-usd" : raceTotal === false ? "--race-total-usd" : null;
6164
- if (badFlag !== null || build === false || racePer === false || raceTotal === false) {
6165
- return fail(write, json, "config set", "usage", `${badFlag ?? "--build-usd"} is a positive dollar amount`, EXIT.usage);
6166
- }
6167
- // The default competing-agent count (operator request): applied only
6168
- // where a filing names one agent and no explicit count; a race digest
6169
- // always binds the actual lineup.
6170
- const agentsGiven = text(flags, "race-agents");
6171
- const raceAgents = agentsGiven === undefined ? null : Number(agentsGiven);
6172
- if (raceAgents !== null && (!Number.isInteger(raceAgents) || raceAgents < 2 || raceAgents > 4)) {
6173
- return fail(write, json, "config set", "usage", "--race-agents is how many agents compete by default: a whole number from 2 to 4", EXIT.usage);
6174
- }
6175
- store.setSpendDefaults({ buildPerRunMicrousd: build, racePerAgentMicrousd: racePer, raceTotalMicrousd: raceTotal, raceAgents }, acting.name, clock());
4776
+ if (build === false) {
4777
+ return fail(write, json, "config set", "usage", "--build-usd is a positive dollar amount", EXIT.usage);
4778
+ }
4779
+ store.setSpendDefaults({ buildPerRunMicrousd: build }, acting.name, clock());
6176
4780
  return succeed(write, json, "config set", { budgets: store.getSpendDefaults() }, () => [
6177
4781
  "Spend defaults set. New filings pre-fill from these; every approval still restates its own numbers:",
6178
4782
  ...(build === null ? [] : [` each ordinary build attempt: $${(build / 1_000_000).toFixed(2)} (also the installation backstop)`]),
6179
- ...(racePer === null ? [] : [` each tournament agent: $${(racePer / 1_000_000).toFixed(2)}`]),
6180
- ...(raceTotal === null ? [] : [` each tournament total: $${(raceTotal / 1_000_000).toFixed(2)}`]),
6181
- ...(raceAgents === null ? [] : [` tournaments race ${raceAgents} agents unless a filing says otherwise`]),
6182
- ]);
6183
- }
6184
- // The FALLBACK CHAIN (Layer F): the ordered entries a repository's NEW
6185
- // approvals bind after the base agent. Per-repo only — a chain names
6186
- // credentials for one project's work — and inert on its own: it only
6187
- // ever fires when a signed mode also allows the paid fallback.
6188
- if (phase === "fallback") {
6189
- const acting = await askCredentials(flags, context);
6190
- if (acting === null) {
6191
- return fail(write, json, `config ${action}`, "usage", "configuring fallbacks takes `--as <you> --token <t>`", EXIT.usage);
6192
- }
6193
- const authedFallback = authenticateApprover(store, acting.name, acting.token);
6194
- if (!authedFallback.ok) {
6195
- return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
6196
- }
6197
- if (scope === INSTALLATION_SCOPE) {
6198
- return fail(write, json, `config ${action}`, "usage", "fallbacks are per repository — say --repo <path>", EXIT.usage);
6199
- }
6200
- if (action === "clear") {
6201
- const had = store.clearFallbackConfig(scope);
6202
- return succeed(write, json, "config clear", { repo: scope, fallback: null }, () => [
6203
- had
6204
- ? `${scope}: fallback chain cleared — new approvals bind the single configured agent again.`
6205
- : `${scope}: no fallback chain was configured.`,
6206
- ]);
6207
- }
6208
- const entriesGiven = text(flags, "entries");
6209
- if (entriesGiven === undefined) {
6210
- return fail(write, json, "config set", "usage", "`config set fallback --repo <path> --entries provider:model:auth-mode[,…]` — auth-mode is subscription or api-key; 1 to 3 entries", EXIT.usage);
6211
- }
6212
- const entries = [];
6213
- for (const one of entriesGiven.split(",")) {
6214
- // First and LAST colon split the three parts (F+G review, finding 1):
6215
- // model ids legitimately carry colons (openrouter's ":free" suffixes),
6216
- // so the model is everything between the provider and the auth mode.
6217
- const trimmed = one.trim();
6218
- const firstColon = trimmed.indexOf(":");
6219
- const lastColon = trimmed.lastIndexOf(":");
6220
- if (firstColon === -1 || lastColon === firstColon) {
6221
- return fail(write, json, "config set", "usage", `"${trimmed}" is not provider:model:auth-mode`, EXIT.usage);
6222
- }
6223
- const provider = trimmed.slice(0, firstColon);
6224
- const model = trimmed.slice(firstColon + 1, lastColon);
6225
- const authMode = trimmed.slice(lastColon + 1);
6226
- if (!isProviderId(provider)) {
6227
- return fail(write, json, "config set", "usage", `unknown provider "${provider}" — one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
6228
- }
6229
- if (authMode !== "subscription" && authMode !== "api-key") {
6230
- return fail(write, json, "config set", "usage", `auth-mode is subscription or api-key, not "${authMode}"`, EXIT.usage);
6231
- }
6232
- if (authMode === "subscription" && !SUBSCRIPTION_CAPABLE[provider]) {
6233
- return fail(write, json, "config set", "usage", `${provider} has no subscription login — this entry must use api-key`, EXIT.usage);
6234
- }
6235
- // The model rides provider argv: the SAME argv-safety validation every
6236
- // other sealed model passes (never a leading dash or control bytes).
6237
- const argvSafe = validateSpec({ provider, model: model === "" ? null : model });
6238
- if (model === "" || !argvSafe.ok) {
6239
- return fail(write, json, "config set", "usage", model === "" ? "each entry names an exact model — approvals bind exact routing" : argvSafe.ok ? "invalid entry" : argvSafe.problem, EXIT.usage);
6240
- }
6241
- entries.push({ provider, model, authMode });
6242
- }
6243
- if (entries.length < 1 || entries.length > 3) {
6244
- return fail(write, json, "config set", "usage", "1 to 3 fallback entries (the approval binds the whole chain)", EXIT.usage);
6245
- }
6246
- // The WHOLE chain must file against the CURRENT base (F+G review,
6247
- // finding 4): a fallback that duplicates the base — or another entry —
6248
- // refuses NOW, in words, not at some future filing. Set, prove through
6249
- // the same resolver filing uses, and restore the old config on refusal.
6250
- const previous = store.fallbackConfig(scope);
6251
- store.setFallbackConfig(scope, entries, acting.name, clock());
6252
- const baseAgent = resolvePhaseAgent(store, "build", scope, {});
6253
- if (baseAgent.ok) {
6254
- const proven = resolveScopeChain(store, scope, undefined, {}, readAuthMode(baseAgent.spec.provider));
6255
- // An UNRESOLVED base (no explicit routing configured yet) cannot be
6256
- // duplicate-checked — the filing-time resolver holds that line; only
6257
- // a chain that provably cannot file refuses here.
6258
- if (!proven.ok && proven.reason !== "base-unresolved") {
6259
- if (previous.length > 0)
6260
- store.setFallbackConfig(scope, previous, acting.name, clock());
6261
- else
6262
- store.clearFallbackConfig(scope);
6263
- return fail(write, json, "config set", "usage", `this chain cannot file: ${proven.problem}`, EXIT.usage);
6264
- }
6265
- }
6266
- return succeed(write, json, "config set", { repo: scope, fallback: entries }, () => [
6267
- `${scope}: fallback chain set — NEW approvals bind it; existing approvals are untouched.`,
6268
- ...entries.map((one, i) => ` ${i + 1}. ${one.provider} (${one.model}) — ${one.authMode === "subscription" ? "its subscription login" : "your API key"}`),
6269
- " it fires only when a signed mode allows the paid fallback: `toolroll mode set --allow-paid-fallback …`",
6270
4783
  ]);
6271
4784
  }
6272
4785
  // Chat is its OWN configuration, deliberately not a phase (Codex v3
@@ -6350,7 +4863,7 @@ async function configCommand(positional, flags, context) {
6350
4863
  return fail(write, json, `config ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, phase), EXIT.refused);
6351
4864
  }
6352
4865
  // The STRONG tier (v47): a second, named row per phase the routing policy
6353
- // reaches for when risk, quality, evidence, or publication demand it.
4866
+ // reaches for when size, quality, evidence, or publication demand it.
6354
4867
  // Authenticated and audited exactly like the routine row; existing
6355
4868
  // approvals are untouched — a sealed route never re-resolves.
6356
4869
  // The LIGHT tier (v2): the fast agent a small change builds on.
@@ -6444,7 +4957,7 @@ async function configCommand(positional, flags, context) {
6444
4957
  store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
6445
4958
  return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
6446
4959
  `strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
6447
- " high-risk, strict, screenshot-proof, and automerge routes filed from now on reach for it; sealed routes are untouched.",
4960
+ " large or risky, strict, screenshot-proof, and automerge routes filed from now on reach for it; sealed routes are untouched.",
6448
4961
  ...(phase === "repair" ? [" a strong repair agent applies only when its provider is the build's — cross-provider repair does not exist."] : []),
6449
4962
  ]);
6450
4963
  }
@@ -6457,7 +4970,7 @@ async function configCommand(positional, flags, context) {
6457
4970
  warnings.push("OPENROUTER_API_KEY is not present in this environment — runs will fail until the runner exports it.");
6458
4971
  }
6459
4972
  if (providerGiven !== "claude") {
6460
- warnings.push(`${providerGiven} does not report dollar cost: its runs land as UNMEASURED, and any routine with a cost ceiling fails closed on them by design.`);
4973
+ warnings.push(`${providerGiven} does not report dollar cost: its runs land as UNMEASURED, and any schedule with a weekly limit fails closed on them by design.`);
6461
4974
  }
6462
4975
  return succeed(write, json, "config set", { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
6463
4976
  `${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} now runs ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
@@ -7028,80 +5541,11 @@ async function intakeCommand(positional, flags, context) {
7028
5541
  ]);
7029
5542
  }
7030
5543
  /**
7031
- * `toolroll contest …` — the tournament from the terminal: `show`
7032
- * for the machine-readable state, `exclude` to stop a racing agent whose
7033
- * question you will not answer (authenticated: it cancels paid-for work
7034
- * and un-sticks the race). The pick itself stays a console ceremony.
7035
- */
7036
- function contestCommand(positional, flags, context) {
7037
- const { store, write, json, clock } = context;
7038
- const [action, idGiven, ordinalGiven] = positional;
7039
- if (action === undefined || !CONTEST_ACTIONS.includes(action)) {
7040
- return fail(write, json, "contest", "usage", `unknown \`contest ${action ?? ""}\` — try ${CONTEST_ACTIONS.join(", ")}`, EXIT.usage);
7041
- }
7042
- if (action === "show") {
7043
- const contest = store.getContest(Number(idGiven));
7044
- if (contest === null)
7045
- return fail(write, json, "contest show", "unknown", "no tournament or comparison with that id", EXIT.refused);
7046
- const agents = store.contestants(contest.id);
7047
- if (json) {
7048
- write(envelopeJson({ ok: true, command: "contest show", contest, agents }));
7049
- return EXIT.ok;
7050
- }
7051
- write(`${contestNoun(contest.kind)} #${contest.id} — ${contest.state}`);
7052
- for (const racer of agents) {
7053
- const money = contest.kind === "comparison"
7054
- ? racer.unknownSpend
7055
- ? "spend unmeasured (tokens only)"
7056
- : `$${(racer.measuredMicrousd / 1_000_000).toFixed(2)} measured`
7057
- : `charged $${(racer.accountedMicrousd / 1_000_000).toFixed(2)}${racer.unknownSpend ? " (exact figure unknown — charged the reserved worst case)" : ""}`;
7058
- write(` agent ${racer.ordinal}: ${racer.provider} · ${racer.model} — ${racer.state} · ${money}`);
7059
- }
7060
- return EXIT.ok;
7061
- }
7062
- if (action === "exclude") {
7063
- return (async () => {
7064
- const contest = store.getContest(Number(idGiven));
7065
- const ordinal = Number(ordinalGiven);
7066
- if (contest === null || !Number.isInteger(ordinal)) {
7067
- return fail(write, json, "contest exclude", "usage", "`toolroll contest exclude <tournament-id> <agent-number> --as <you> --token <t>`", EXIT.usage);
7068
- }
7069
- const acting = await askCredentials(flags, context);
7070
- if (acting === null) {
7071
- return fail(write, json, "contest exclude", "usage", "stopping a racing agent takes `--as <you> --token <t>`", EXIT.usage);
7072
- }
7073
- const authed = authenticateApprover(store, acting.name, acting.token);
7074
- if (!authed.ok) {
7075
- return fail(write, json, "contest exclude", "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
7076
- }
7077
- const racer = store.contestants(contest.id).find(one => one.ordinal === ordinal);
7078
- if (racer === undefined || racer.state !== "parked") {
7079
- return fail(write, json, "contest exclude", "not-waiting", "that agent is not waiting on an answer", EXIT.refused);
7080
- }
7081
- const question = store.openDecisionForContestant(racer.id);
7082
- const moved = store.transact(() => {
7083
- if (question !== null && !store.excludeDecision(question, acting.name, clock()))
7084
- return false;
7085
- if (!store.casContestantState(racer.id, ["parked"], "stopped", racer.generation))
7086
- return false;
7087
- contestMaybeAggregate(store, contest.id, clock());
7088
- return true;
7089
- });
7090
- if (!moved)
7091
- return fail(write, json, "contest exclude", "changed", "the tournament moved while you were reading — look again", EXIT.refused);
7092
- const after = store.getContest(contest.id);
7093
- return succeed(write, json, "contest exclude", { contest: after }, () => [
7094
- `Agent ${ordinal} stopped; its question is closed as excluded. The tournament is now ${after?.state ?? "?"}.`,
7095
- ]);
7096
- })();
7097
- }
7098
- return fail(write, json, "contest", "usage", "`toolroll contest show <id> | exclude <id> <agent-number>`", EXIT.usage);
7099
- }
7100
- /**
7101
- * `toolroll template …` — the shipped library of common standing
7102
- * orders (adoption track, step 2). A template is a pre-filled form:
7103
- * `apply` PREVIEWS by default and files only under `--file`, through the
7104
- * same one door as every manual filing, landing UNAPPROVED. Recipes
5544
+ * `toolroll template …` — the shipped library of common work (adoption
5545
+ * track, step 2). A template is a pre-filled form: `apply` PREVIEWS by
5546
+ * default and files only under `--file`, landing UNAPPROVED — a task through
5547
+ * the same one door as every manual filing, or a scheduled flow whose
5548
+ * schedule starts paused. Recipes
7105
5549
  * (issue-intake, ci-babysitter) display existing ceremonies and cannot be
7106
5550
  * applied — the authority they would need is a separate authenticated act
7107
5551
  * a template must never perform (adoption review, finding 10).
@@ -7118,7 +5562,7 @@ function templateCommand(positional, flags, context) {
7118
5562
  }));
7119
5563
  return EXIT.ok;
7120
5564
  }
7121
- write("Templates — common routines you edit to fit. Nothing a template");
5565
+ write("Templates — common work you edit to fit. Nothing a template");
7122
5566
  write("files is approved; recipes only show existing ceremonies.");
7123
5567
  write("");
7124
5568
  for (const one of TEMPLATES) {
@@ -7160,8 +5604,8 @@ function templateCommand(positional, flags, context) {
7160
5604
  write(` title ${template.title}`);
7161
5605
  }
7162
5606
  else {
7163
- write(` files one routine (cannot fire until you approve its terms)`);
7164
- write(` name ${template.routineName}`);
5607
+ write(` makes a scheduled flow (nothing repeats until you turn the schedule on)`);
5608
+ write(` name ${template.title}`);
7165
5609
  write(` schedule ${template.schedule}`);
7166
5610
  }
7167
5611
  write(` goal ${template.goal}`);
@@ -7231,273 +5675,48 @@ function templateCommand(positional, flags, context) {
7231
5675
  ...(filedLink === null ? [] : [` ${filedLink}`]),
7232
5676
  ]);
7233
5677
  }
7234
- const routineName = text(flags, "name") ?? template.routineName;
7235
- const schedule = text(flags, "schedule") ?? template.schedule;
5678
+ const flowName = text(flags, "name") ?? template.title;
5679
+ const scheduleGiven = text(flags, "schedule") ?? template.schedule;
5680
+ const schedule = parseSchedule(scheduleGiven) !== null ? scheduleGiven : scheduleFromWords(scheduleGiven) ?? scheduleGiven;
7236
5681
  const ceilingGiven = text(flags, "ceiling");
7237
5682
  const costCeilingUsd = ceilingGiven === undefined ? template.costCeilingUsd : Number(ceilingGiven);
5683
+ const repo = canonicalProject(repoGiven) ?? resolve(repoGiven);
5684
+ const terms = { goal, outOfScope, touches, requirements: template.requirements, acceptance: template.acceptance, budgetPerRunMicrousd: null, costCeilingUsd };
7238
5685
  if (!flags.has("file")) {
7239
- const draft = {
7240
- kind: "routine",
7241
- name: routineName,
7242
- repo: canonicalProject(repoGiven) ?? resolve(repoGiven),
7243
- goal,
7244
- outOfScope,
7245
- touches,
7246
- acceptance: template.acceptance,
7247
- requirements: template.requirements,
7248
- schedule,
7249
- costCeilingUsd,
7250
- filedVia: `template:${template.name}`,
7251
- };
5686
+ const draft = { kind: "scheduled flow", name: flowName, repo, schedule, ...terms, filedVia: `template:${template.name}` };
7252
5687
  if (json) {
7253
5688
  write(envelopeJson({ ok: false, command: "template apply", reason: "unconfirmed", draft }));
7254
5689
  return 3;
7255
5690
  }
7256
- write("Would file, exactly (edit with --name/--goal/--not/--touches/--schedule/--ceiling):");
5691
+ const readable = parseSchedule(schedule);
5692
+ write("Would make this scheduled flow (edit with --name/--goal/--not/--touches/--schedule/--ceiling):");
7257
5693
  write("");
7258
- write(` routine ${draft.name}`);
5694
+ write(` flow ${draft.name}`);
7259
5695
  write(` repo ${draft.repo}`);
7260
- write(` schedule ${draft.schedule}${draft.schedule.startsWith("every:10080") ? " (weekly)" : ""}`);
5696
+ write(` schedule ${readable === null ? schedule : describeSchedule(readable)}`);
7261
5697
  write(` goal ${draft.goal}`);
7262
5698
  if (draft.outOfScope !== null)
7263
5699
  write(` not ${draft.outOfScope}`);
7264
5700
  if (draft.touches.length > 0)
7265
5701
  write(` touches ${draft.touches.join(", ")}`);
7266
5702
  if (draft.costCeilingUsd !== null)
7267
- write(` ceiling $${draft.costCeilingUsd}/week`);
5703
+ write(` ceiling $${draft.costCeilingUsd} every 7 days`);
7268
5704
  write("");
7269
- write("Nothing was filed. Re-run with --file to file it — UNAPPROVED either way; it cannot fire until you approve it.");
5705
+ write("Nothing was made. Re-run with --file to make it. Nothing repeats until you turn the schedule on.");
7270
5706
  return 3;
7271
5707
  }
7272
- const made = fileRoutineProposal(store, {
7273
- name: routineName,
7274
- repo: repoGiven,
7275
- goal,
7276
- outOfScope,
7277
- touches,
7278
- acceptance: template.acceptance,
7279
- requirements: template.requirements,
7280
- schedule,
7281
- costCeilingUsd,
7282
- filedVia: `template:${template.name}`,
7283
- }, clock());
5708
+ const made = createScheduledFlow(store, { repo, name: flowName, stem: template.name, schedule, terms, by: context.principal?.account ?? "toolroll", filedBy: context.principal?.account ?? null }, clock());
7284
5709
  if (!made.ok)
7285
- return fail(write, json, "template apply", made.reason, made.message, made.reason === "duplicate" ? EXIT.refused : EXIT.usage);
7286
- return succeed(write, json, "template apply", { filed: "routine", id: made.id, approved: false }, () => [
7287
- `Filed routine ${routineName} from template ${template.name}.`,
5710
+ return fail(write, json, "template apply", "bad-terms", made.message, EXIT.usage);
5711
+ const flowLink = consoleLinkFor(context, `/flows/${made.flow}`);
5712
+ return succeed(write, json, "template apply", { filed: "scheduled flow", flow: made.flow, trigger: made.trigger, on: false, ...(flowLink === null ? {} : { links: { flow: flowLink } }) }, () => [
5713
+ `Made ${flowName}, flow #${made.flow}, from template ${template.name}.`,
7288
5714
  "",
7289
- "UNAPPROVED — NO AUTHORITY GRANTED. It cannot fire until you approve the standing order:",
7290
- ` toolroll routine approve ${routineName}`,
5715
+ "Nothing repeats until you turn the schedule on; each run then waits for approval under the project's rules:",
5716
+ ` toolroll flows trigger resume ${made.flow} ${made.trigger}`,
5717
+ ...(flowLink === null ? [] : [` ${flowLink}`]),
7291
5718
  ]);
7292
5719
  }
7293
- /**
7294
- * `toolroll routine …` — standing orders. Filing one is cheap; the
7295
- * expensive act is the approval, which restates every term including "each
7296
- * firing builds without asking" and takes the approver's credential, same
7297
- * as a scope. Pausing needs no ceremony because stopping spend never does.
7298
- */
7299
- async function routineCommand(positional, flags, context) {
7300
- const { store, write, json, clock } = context;
7301
- const [action, name] = positional;
7302
- if (action !== undefined && !ROUTINE_ACTIONS.includes(action)) {
7303
- return fail(write, json, "routine", "usage", `unknown \`routine ${action}\` — try ${ROUTINE_ACTIONS.join(", ")}`, EXIT.usage);
7304
- }
7305
- if (action === undefined || action === "list") {
7306
- const repoFilter = text(flags, "repo");
7307
- const routines = store.listRoutines(repoFilter === undefined ? null : canonicalProject(repoFilter) ?? resolve(repoFilter));
7308
- if (json) {
7309
- write(envelopeJson({ ok: true, command: "routine list", routines }));
7310
- return EXIT.ok;
7311
- }
7312
- if (routines.length === 0) {
7313
- write("No routines. `toolroll routine add <name> --repo <path> --goal <text> --schedule every:60` files one.");
7314
- return EXIT.ok;
7315
- }
7316
- for (const routine of routines) {
7317
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
7318
- const status = routine.paused ? "paused" : approved ? "live" : "awaiting approval";
7319
- write(` ${routine.name.padEnd(20)} ${status.padEnd(18)} ${routine.schedule.padEnd(14)} ${routine.repo}`);
7320
- }
7321
- return EXIT.ok;
7322
- }
7323
- if (action === "add") {
7324
- if (name === undefined || !ROUTINE_NAME.test(name)) {
7325
- return fail(write, json, "routine add", "usage", "a routine's name is lowercase letters, digits, and dashes — it becomes each instance's id", EXIT.usage);
7326
- }
7327
- const repoGiven = text(flags, "repo");
7328
- const goal = text(flags, "goal");
7329
- const schedule = text(flags, "schedule");
7330
- if (repoGiven === undefined || goal === undefined || schedule === undefined) {
7331
- return fail(write, json, "routine add", "usage", "`toolroll routine add <name> --repo <path> --goal <text> --schedule every:<min>|daily:<HH:MM>[@Zone]|weekly:<0-6>:<HH:MM>[@Zone] --acceptance <rubric> [--not <text>] [--touches a,b] [--require kind:name,…] [--ceiling <usd>] [--budget-usd <n>]`", EXIT.usage);
7332
- }
7333
- const acceptanceGiven = text(flags, "acceptance");
7334
- if (acceptanceGiven === undefined) {
7335
- return fail(write, json, "routine add", "acceptance-required", "a standing order needs at least one signed acceptance criterion — `--acceptance \"<statement>|<evidence,kinds>\"`, `;`-separated for more than one; evidence kinds are check, screenshot, changed-path, manual-review", EXIT.usage);
7336
- }
7337
- const ceilingGiven = text(flags, "ceiling");
7338
- // --budget-usd on a routine caps EACH instance (v16): it becomes the
7339
- // instance scope's digest-bound budget term, enforced by the same
7340
- // native-cap plumbing as any other scope budget.
7341
- const perRunGiven = text(flags, "budget-usd");
7342
- if (perRunGiven !== undefined && (!Number.isFinite(Number(perRunGiven)) || Number(perRunGiven) <= 0)) {
7343
- return fail(write, json, "routine add", "bad-budget", "--budget-usd is a positive dollar amount — what each firing may spend", EXIT.usage);
7344
- }
7345
- // One filing door for every surface (Codex adoption review, finding 7):
7346
- // validation, canonicalization, digest, and provenance live in the
7347
- // service, not here.
7348
- const created = fileRoutineProposal(store, {
7349
- name,
7350
- repo: repoGiven,
7351
- goal,
7352
- outOfScope: text(flags, "not") ?? null,
7353
- touches: (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(one => one !== ""),
7354
- acceptance: acceptanceLinesToInput(splitAcceptanceRubric(acceptanceGiven)),
7355
- requirements: (text(flags, "require") ?? "").split(",").map(one => one.trim()).filter(one => one !== ""),
7356
- schedule,
7357
- costCeilingUsd: ceilingGiven === undefined ? null : Number(ceilingGiven),
7358
- ...(perRunGiven === undefined ? {} : { budgetPerRunMicrousd: Math.round(Number(perRunGiven) * 1_000_000) }),
7359
- filedVia: "cli",
7360
- }, clock());
7361
- if (!created.ok) {
7362
- return fail(write, json, "routine add", created.reason, created.message, created.reason === "duplicate" ? EXIT.refused : EXIT.usage);
7363
- }
7364
- const routine = store.getRoutine(created.id);
7365
- return succeed(write, json, "routine add", { routine }, () => [
7366
- `Filed ${name}. Nothing fires until somebody approves the standing order:`,
7367
- ...(routine === null ? [] : describeRoutine(routine)),
7368
- "",
7369
- ` toolroll routine approve ${name}`,
7370
- ]);
7371
- }
7372
- if (name === undefined) {
7373
- return fail(write, json, `routine ${action}`, "usage", "which routine? give its name", EXIT.usage);
7374
- }
7375
- const routine = store.routineByName(name);
7376
- if (routine === null) {
7377
- return fail(write, json, `routine ${action}`, "unknown", `no routine named ${name}`, EXIT.refused);
7378
- }
7379
- switch (action) {
7380
- case "show": {
7381
- const fires = store.routineFires(routine.id, 14);
7382
- if (json) {
7383
- write(envelopeJson({ ok: true, command: "routine show", routine, fires }));
7384
- return EXIT.ok;
7385
- }
7386
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
7387
- write(`${routine.name} — ${routine.paused ? "paused" : approved ? "live" : "awaiting approval"}`);
7388
- for (const line of describeRoutine(routine))
7389
- write(line);
7390
- if (routine.nextFireAt !== null && !routine.paused && approved)
7391
- write(` next fire ${routine.nextFireAt}`);
7392
- if (fires.length > 0) {
7393
- write("");
7394
- write(" recent firings, newest first:");
7395
- for (const fire of fires) {
7396
- const said = fire.outcome === "fired"
7397
- ? `${fire.instanceTaskId ?? "instance"}${fire.instanceState === null ? "" : ` (${fire.instanceState})`}${fire.reason === "manual" ? " (run now)" : ""}`
7398
- : `skipped — ${fire.reason ?? ""}`;
7399
- write(` ${fire.scheduledFor.replace(/^manual:/, "")} ${said}`);
7400
- }
7401
- }
7402
- return EXIT.ok;
7403
- }
7404
- case "approve": {
7405
- let saw = text(flags, "digest");
7406
- let { name: asWho, token } = credentialsFrom(flags, context);
7407
- let confirmedAloud = false;
7408
- if ((!flags.has("yes") || saw === undefined || asWho === undefined || token === undefined) && interactive() && !json) {
7409
- write(`Approving ${name} makes it a STANDING order:`);
7410
- write("");
7411
- for (const line of describeRoutine(routine))
7412
- write(line);
7413
- write("");
7414
- const agreed = await confirm("Approve exactly this standing order?");
7415
- if (!agreed) {
7416
- write("Nothing approved.");
7417
- return EXIT.refused;
7418
- }
7419
- saw ??= routine.digest;
7420
- const acting = await askCredentials(flags, context);
7421
- if (acting === null)
7422
- return fail(write, json, "routine approve", "usage", "approval needs who is agreeing", EXIT.usage);
7423
- asWho = acting.name;
7424
- token = acting.token;
7425
- confirmedAloud = true;
7426
- }
7427
- const armed = (flags.has("yes") || confirmedAloud) && saw !== undefined && asWho !== undefined && token !== undefined;
7428
- if (!armed) {
7429
- if (json) {
7430
- write(envelopeJson({ ok: false, command: "routine approve", reason: "unconfirmed", routine }));
7431
- return EXIT.refused;
7432
- }
7433
- write(`Would approve this standing order — every firing of it builds without asking:`);
7434
- write("");
7435
- for (const line of describeRoutine(routine))
7436
- write(line);
7437
- write("");
7438
- write("Nothing has been approved. Agree to exactly this with:");
7439
- write(` toolroll routine approve ${name} --yes --digest ${routine.digest} --as <you> --token <your password>`);
7440
- // A preview reached by omitting --yes is the answer "no, not yet" —
7441
- // exit 3 in both modes, matching the JSON path (round-4 finding 10).
7442
- return EXIT.refused;
7443
- }
7444
- const approved = approveRoutine(store, routine.id, asWho, clock(), saw, token);
7445
- if (!approved.ok) {
7446
- return fail(write, json, "routine approve", approved.reason, describeApproveFailure(approved.reason, name), EXIT.refused);
7447
- }
7448
- return succeed(write, json, "routine approve", { routine: approved.routine }, () => [
7449
- `Approved. ${name} fires on its schedule from now on; first at ${approved.routine.nextFireAt}.`,
7450
- `Pause it any time: toolroll routine pause ${name}`,
7451
- ]);
7452
- }
7453
- case "refresh": {
7454
- // THE RECOVERY ROAD (v48): re-resolve the agents from today's
7455
- // configuration and file them as the order's working agents. Nothing
7456
- // is approved here — the refreshed order waits for the yes.
7457
- const before = routineAgentsState(routine);
7458
- const refreshed = refreshRoutineAgents(store, routine.id, clock());
7459
- if (!refreshed.ok) {
7460
- return fail(write, json, "routine refresh", refreshed.reason, `${name}: ${refreshed.problem}`, EXIT.refused);
7461
- }
7462
- return succeed(write, json, "routine refresh", { routine: refreshed.routine, changed: refreshed.changed, before: before.state }, () => [
7463
- refreshed.changed
7464
- ? `Refreshed ${name}'s agents from today's configuration. Nothing is approved yet — read them and agree:`
7465
- : `${name} already names exactly these agents; nothing changed.`,
7466
- ...describeRoutine(refreshed.routine),
7467
- "",
7468
- ` toolroll routine approve ${name}`,
7469
- ]);
7470
- }
7471
- case "pause":
7472
- case "resume": {
7473
- store.setRoutinePaused(routine.id, action === "pause", clock());
7474
- return succeed(write, json, `routine ${action}`, { name }, () => [
7475
- action === "pause"
7476
- ? `${name} is paused — no firing until you resume it. Already-running instances finish.`
7477
- : `${name} resumed — the next due slot fires again.`,
7478
- ]);
7479
- }
7480
- case "run-now": {
7481
- const acting = await askCredentials(flags, context);
7482
- if (acting === null) {
7483
- return fail(write, json, "routine run-now", "usage", "run-now takes `--as <you> --token <t>` — it dispatches work that spends", EXIT.usage);
7484
- }
7485
- const authenticated = authenticateApprover(store, acting.name, acting.token);
7486
- if (!authenticated.ok) {
7487
- return fail(write, json, "routine run-now", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
7488
- }
7489
- const outcome = fireRoutine(store, routine.id, clock(), { manual: true });
7490
- if (!outcome.ok) {
7491
- return fail(write, json, "routine run-now", outcome.reason, outcome.detail ?? outcome.reason, EXIT.refused);
7492
- }
7493
- return succeed(write, json, "routine run-now", { taskId: outcome.taskId }, () => [
7494
- `Spawned ${outcome.taskId} — it builds on the next pass. The regular schedule is untouched.`,
7495
- ]);
7496
- }
7497
- default:
7498
- return fail(write, json, "routine", "usage", "`toolroll routine [add|list|show|approve|refresh|pause|resume|run-now]`", EXIT.usage);
7499
- }
7500
- }
7501
5720
  /** A credential from a 0600 file, for units that must not carry it inline. */
7502
5721
  function readTokenFile(path) {
7503
5722
  if (path === undefined)
@@ -7825,21 +6044,18 @@ async function runWatchLoop(args) {
7825
6044
  const slackFollower = followSlack({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7826
6045
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7827
6046
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7828
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7829
6047
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7830
6048
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "slack",
7831
6049
  }).catch(() => progress("watch: Slack stopped. Check Slack settings before reconnecting."));
7832
6050
  const teamsFollower = followTeams({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7833
6051
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7834
6052
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7835
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7836
6053
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7837
6054
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "teams",
7838
6055
  }).catch(() => progress("watch: Teams stopped. Check Teams settings before reconnecting."));
7839
6056
  const discordFollower = followDiscord({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7840
6057
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7841
6058
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7842
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7843
6059
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7844
6060
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "discord",
7845
6061
  }).catch(() => progress("watch: Discord stopped. Check Discord settings before reconnecting."));
@@ -7865,6 +6081,7 @@ async function runWatchLoop(args) {
7865
6081
  let lastTick = 0;
7866
6082
  let lastBridge = 0;
7867
6083
  let lastPush = 0;
6084
+ let lastWebhook = 0;
7868
6085
  let ticks = 0;
7869
6086
  let built = 0;
7870
6087
  let brokeCount = 0;
@@ -8160,6 +6377,14 @@ async function runWatchLoop(args) {
8160
6377
  // Push is additive; a broken pass never stops the watch.
8161
6378
  }
8162
6379
  }
6380
+ if (now - lastWebhook >= bridgeEveryMs) {
6381
+ lastWebhook = now;
6382
+ const dir = dirname(context.databaseFile);
6383
+ const primary = effectivePrimary(process.env, dir, loadBotToken(process.env, context.telegramTokenFile) !== null);
6384
+ const targets = activeWebhookTargets(process.env, dir, primary.channel);
6385
+ if (targets.length > 0)
6386
+ await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
6387
+ }
8163
6388
  // The embedded follower owns the wire while it lives; the timer-driven
8164
6389
  // pass is the fallback shape for a watch started before a token existed.
8165
6390
  if (follower === null && now - lastBridge >= bridgeEveryMs) {
@@ -8175,12 +6400,6 @@ async function runWatchLoop(args) {
8175
6400
  quiet.length = 0;
8176
6401
  await bridgeCommand(["telegram"], passFlags(primary.channel === "telegram" ? {} : { "inbound-only": "1" }), quietContext);
8177
6402
  }
8178
- if (primary.channel !== null && primary.channel !== "telegram") {
8179
- const targets = loadWebhookTargets(process.env, dir).filter(one => one.kind === primary.channel && (one.kind !== "slack" || loadSlackCredentials(dir) === null) && (one.kind !== "discord" || loadDiscordCredentials(dir) === null));
8180
- if (targets.length > 0) {
8181
- await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
8182
- }
8183
- }
8184
6403
  }
8185
6404
  // Work-conserving: if the world moved while we worked — or we just
8186
6405
  // finished something that may have freed a dependent — go again now.
@@ -8194,7 +6413,7 @@ async function runWatchLoop(args) {
8194
6413
  // due, a signal lands, or the trial window (--for) ends. Reading one
8195
6414
  // integer from SQLite twice a second is the entire idle cost — no
8196
6415
  // token has anywhere to be spent from here.
8197
- const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs, deadline ?? Number.MAX_SAFE_INTEGER) - Date.now();
6416
+ const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs, lastWebhook + bridgeEveryMs, deadline ?? Number.MAX_SAFE_INTEGER) - Date.now();
8198
6417
  // Doze for the WHOLE idle window, waking early only for a signal or a
8199
6418
  // sequence move — not for one 500ms step and then another pass
8200
6419
  // (2026-09-05: that read as two ticks a second, all night, with
@@ -8699,7 +6918,6 @@ async function upCommand(flags, context) {
8699
6918
  if ("error" in loadedRegistry) {
8700
6919
  return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
8701
6920
  }
8702
- let sweptHeldOrphans = false;
8703
6921
  const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
8704
6922
  const rootInputs = (text(flags, "project-root") ?? "")
8705
6923
  .split(",")
@@ -8813,18 +7031,6 @@ async function upCommand(flags, context) {
8813
7031
  for (let attempt = 1; attempt <= 5; attempt += 1) {
8814
7032
  const suffix = attempt === 1 ? "" : `-${attempt}`;
8815
7033
  const name = attempt === 1 ? base : `${base.slice(0, RUNNER_NAME_MAX - suffix.length)}${suffix}`;
8816
- // The held orphan fence runs BEFORE any recovery road (v6 W6): a
8817
- // crashed predecessor's held session is seized, killed through its
8818
- // supervisor, and settled — or paged — before registration's
8819
- // recovery may touch its run or worktree.
8820
- if (!sweptHeldOrphans) {
8821
- sweptHeldOrphans = true;
8822
- const swept = await sweepHeldOrphans(store, `up:${hostname()}:${process.pid}`, clock);
8823
- if (swept.fenced > 0)
8824
- progress(`fenced ${swept.fenced} orphaned held session(s) from a previous up`);
8825
- if (swept.paged > 0)
8826
- progress(`${swept.paged} held session(s) could not be stopped — see the inbox`);
8827
- }
8828
7034
  // The runner's authority is BOUND to the canonical roots this up
8829
7035
  // serves (MCP spec v6): the claim gate enforces membership, so an
8830
7036
  // unbound registration would deny-all its own dispatches.
@@ -8871,28 +7077,6 @@ async function upCommand(flags, context) {
8871
7077
  finally {
8872
7078
  await new Promise(done => probe.close(() => done()));
8873
7079
  }
8874
- // 6b. The held-session coordinator (Phase 2): one per up process, shared
8875
- // by the console and every watch loop through the context. Its socket
8876
- // directory is deliberately SHORT and flat — sun_path is unforgiving.
8877
- const heldDir = namedPath(homedir(), ["held"], { dot: true });
8878
- try {
8879
- mkdirSync(heldDir, { recursive: true, mode: 0o700 });
8880
- }
8881
- catch {
8882
- // The launch's own path check refuses with words if this failed.
8883
- }
8884
- context.heldCoordinator = new HeldSessionCoordinator();
8885
- context.upIncarnation = randomUUID();
8886
- context.heldSocketDir = heldDir;
8887
- // v28: sessions are unbounded unless the operator caps them.
8888
- const heldCap = text(flags, "max-held-sessions");
8889
- if (heldCap !== undefined) {
8890
- const cap = Number(heldCap);
8891
- if (!Number.isInteger(cap) || cap < 1) {
8892
- return fail(write, json, "up", "usage", "--max-held-sessions is a whole number of concurrent attended sessions, at least 1", EXIT.usage);
8893
- }
8894
- context.maxHeldSessions = cap;
8895
- }
8896
7080
  // 7. The console, on the just-released port. The tiny window between the
8897
7081
  // probe closing and this bind can lose a race; that failure tears down
8898
7082
  // cleanly below instead of leaving identities half-claimed silently.
@@ -8915,14 +7099,6 @@ async function upCommand(flags, context) {
8915
7099
  currentRepos: () => [...activeRepos],
8916
7100
  admittedRepos: () => [...activeRepos],
8917
7101
  upConsole: true,
8918
- attended: {
8919
- runner: runnerName,
8920
- coordinator: context.heldCoordinator,
8921
- headOf: async (repo) => {
8922
- const answer = await gitRun("git", ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: repo });
8923
- return answer.code === 0 ? answer.stdout.trim() : null;
8924
- },
8925
- },
8926
7102
  registryPath,
8927
7103
  projectRoots,
8928
7104
  ...(text(flags, "public-url") === undefined ? {} : { publicUrl: text(flags, "public-url") }),
@@ -9264,23 +7440,6 @@ async function upCommand(flags, context) {
9264
7440
  clearTimeout(graceTimer);
9265
7441
  process.removeListener("SIGINT", stop);
9266
7442
  process.removeListener("SIGTERM", stop);
9267
- // Held sessions fence before the runner retires (v2 S0f): bounded, and
9268
- // every controller settles conservatively or is paged.
9269
- if (context.heldCoordinator !== undefined) {
9270
- const unsettled = await context.heldCoordinator.close();
9271
- for (const runId of unsettled) {
9272
- // The shutdown deadline won: say so durably — the orphan sweep of
9273
- // the NEXT up owns the cleanup, and silence would contradict
9274
- // "settled conservatively or paged".
9275
- store.enqueueNotification({
9276
- source: { run: runId },
9277
- dedupeKey: `held-shutdown-unsettled:${runId}`,
9278
- kind: "attended-unsettled",
9279
- subject: `an attended session did not settle before shutdown (run #${runId})`,
9280
- body: `The shutdown deadline passed before run #${runId}'s session finished fencing. The next \`toolroll up\` will fence and settle it; its worktree is preserved.`,
9281
- }, clock());
9282
- }
9283
- }
9284
7443
  retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
9285
7444
  await new Promise(done => console_.server.close(() => done()));
9286
7445
  if (fatal !== null) {
@@ -9804,18 +7963,6 @@ async function publishCommand(positional, flags, context) {
9804
7963
  return succeed(write, json, "publish", { report }, lines);
9805
7964
  }
9806
7965
  // ---- the outbox -----------------------------------------------------------
9807
- /**
9808
- * `toolroll outbox list|deliver` — reading and draining the durable
9809
- * outbox. Delivery runs an operator-supplied command once per pending row;
9810
- * the notification's text reaches it as environment variables, never
9811
- * substituted into the command line, because subjects and bodies quote
9812
- * things agents and repositories said and a shell must not meet those.
9813
- *
9814
- * toolroll outbox deliver --cmd 'curl -d "$TOOLROLL_SUBJECT" ntfy.sh/mine'
9815
- *
9816
- * Exit 0 when everything pending delivered (or nothing was pending);
9817
- * 1 when any delivery failed — a broken channel is breakage, not a "no".
9818
- */
9819
7966
  /**
9820
7967
  * `toolroll peek [<run>] [--tmux] [--lines <n>]` — watch live agents
9821
7968
  * in the terminal: one pane per open run with its stage and transcript
@@ -9865,6 +8012,11 @@ async function peekCommand(positional, flags, context) {
9865
8012
  }
9866
8013
  return runPeek(store, context.evidenceRoot, { ...(runId === undefined ? {} : { runId }), io: { stdout: process.stdout, stdin: process.stdin }, clock });
9867
8014
  }
8015
+ /**
8016
+ * `toolroll outbox list [--all]` — read the durable outbox: the facts that
8017
+ * want a person, pending until resolved. An operator command can also deliver
8018
+ * them with its own destination receipts.
8019
+ */
9868
8020
  async function outboxCommand(positional, flags, context) {
9869
8021
  const { store, write, json, clock } = context;
9870
8022
  const [action] = positional;
@@ -9876,17 +8028,12 @@ async function outboxCommand(positional, flags, context) {
9876
8028
  return EXIT.ok;
9877
8029
  }
9878
8030
  if (notifications.length === 0) {
9879
- write(wanted === "pending" ? "Nothing waiting to be delivered." : "The outbox is empty.");
8031
+ write(wanted === "pending" ? "Nothing pending." : "The outbox is empty.");
9880
8032
  return EXIT.ok;
9881
8033
  }
9882
8034
  for (const one of notifications) {
9883
- const state = one.deliveredAt !== null
9884
- ? `delivered ${one.deliveredAt}`
9885
- : one.attempts > 0
9886
- ? `pending, ${one.attempts} failed attempt(s): ${one.lastError ?? ""}`
9887
- : "pending";
9888
8035
  write(` #${one.id} ${one.kind.padEnd(18)} ${one.subject}`);
9889
- write(` ${state}`);
8036
+ write(` ${one.resolvedAt === null ? "pending" : `resolved ${one.resolvedAt}`}`);
9890
8037
  }
9891
8038
  return EXIT.ok;
9892
8039
  }
@@ -9898,14 +8045,9 @@ async function outboxCommand(positional, flags, context) {
9898
8045
  if (command === undefined) {
9899
8046
  return fail(write, json, "outbox deliver", "usage", "--cmd says how: it runs once per notification, reading $TOOLROLL_KIND, $TOOLROLL_SUBJECT, $TOOLROLL_BODY", EXIT.usage);
9900
8047
  }
9901
- // Claimed, not merely listed: the Telegram bridge drains this same
9902
- // outbox, and select-then-send-then-record from two deliverers pages a
9903
- // person twice. The claim is a short lease on the act of sending; a
9904
- // deliverer that dies mid-send leaves rows that unclaim by expiry.
8048
+ // The exact command owns a leased destination receipt, including across concurrent CLI invocations.
9905
8049
  const owner = `outbox-${randomUUID()}`;
9906
- // Push first, independently (arc 3 finding 8): its pair ledger does not
9907
- // depend on globally-undelivered notifications — Telegram or a webhook
9908
- // may already have stamped delivered_at.
8050
+ // Push delivery has independent receipts and must keep working if this command fails.
9909
8051
  let pushed = 0;
9910
8052
  try {
9911
8053
  pushed = (await pushPass(store, { configDir: dirname(context.databaseFile), clock })).accepted;
@@ -9913,7 +8055,7 @@ async function outboxCommand(positional, flags, context) {
9913
8055
  catch {
9914
8056
  // additive; the shell delivery below still runs
9915
8057
  }
9916
- const pending = store.claimDeliveries(owner, 2 * 60_000, clock());
8058
+ const pending = claimNotifications(store, notificationDestination("command", command), owner, clock());
9917
8059
  if (pending.length === 0) {
9918
8060
  return succeed(write, json, "outbox deliver", { delivered: 0, failed: 0, pushed }, () => [
9919
8061
  pushed > 0 ? `Nothing for the shell command; ${pushed} push(es) accepted.` : "Nothing waiting to be delivered.",
@@ -9922,6 +8064,10 @@ async function outboxCommand(positional, flags, context) {
9922
8064
  let delivered = 0;
9923
8065
  let failed = 0;
9924
8066
  for (const one of pending) {
8067
+ if (!notificationClaimHeld(store, one, owner, clock())) {
8068
+ failed++;
8069
+ continue;
8070
+ }
9925
8071
  const sent = await run("sh", ["-lc", command], {
9926
8072
  timeoutMs: 30_000,
9927
8073
  env: {
@@ -9933,14 +8079,16 @@ async function outboxCommand(positional, flags, context) {
9933
8079
  });
9934
8080
  if (sent.code === 0) {
9935
8081
  const receipt = sent.stdout.split("\n")[0]?.trim() ?? "";
9936
- store.finalizeDelivery(one.id, owner, { ok: true, receipt: receipt === "" ? null : receipt }, clock());
9937
- delivered++;
8082
+ if (finalizeNotification(store, one, owner, { ok: true, receipt: receipt === "" ? null : receipt }, clock()))
8083
+ delivered++;
8084
+ else
8085
+ failed++;
9938
8086
  }
9939
8087
  else {
9940
8088
  const error = sent.timedOut
9941
8089
  ? "timed out"
9942
8090
  : sent.stderr.split("\n")[0]?.trim() || `exit ${sent.code}`;
9943
- store.finalizeDelivery(one.id, owner, { ok: false, error }, clock());
8091
+ finalizeNotification(store, one, owner, { ok: false, error }, clock());
9944
8092
  failed++;
9945
8093
  }
9946
8094
  }
@@ -10171,6 +8319,7 @@ async function statusCommand(positional, flags, context) {
10171
8319
  // An update waiting, on what, and the action: before anything else, as it holds up new work.
10172
8320
  const waiting = waitingUpdate(context.databaseFile, run => context.store.stopQuiescenceProblem(run) !== null, context.clock());
10173
8321
  const updateWaiting = waiting === null ? {} : { updateWaiting: { app: waiting.app, version: waiting.version, stopped: waiting.stopped, run: waiting.run, on: waiting.on, action: waiting.action } };
8322
+ const legacyWebhookWarning = loadWebhookTargets(process.env, dirname(context.databaseFile)).length > 0 ? LEGACY_WEBHOOK_WARNING : null;
10174
8323
  const projects = projectBuildsStatus(context);
10175
8324
  const projectsLine = projects.length === 0 ? null
10176
8325
  : `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
@@ -10178,7 +8327,7 @@ async function statusCommand(positional, flags, context) {
10178
8327
  const unsent = context.store.unsentTelegramReplies(null, null, new Date(context.clock().getTime() - UNSENT_REPLY_MS));
10179
8328
  const unsentLine = unsentRepliesLine(unsent);
10180
8329
  const unsentReplies = unsent.length === 0 ? {} : { unsentReplies: unsent.map(one => ({ ...one, retry: "toolroll bridge telegram retry" })) };
10181
- return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting, ...unsentReplies }, () => [...(waiting === null ? [] : [waiting.words]), ...(unsentLine === null ? [] : [unsentLine]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
8330
+ return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting, ...unsentReplies, ...(legacyWebhookWarning === null ? {} : { legacyWebhookWarning }) }, () => [...(waiting === null ? [] : [waiting.words]), ...(unsentLine === null ? [] : [unsentLine]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine]), ...(legacyWebhookWarning === null ? [] : [legacyWebhookWarning])]);
10182
8331
  }
10183
8332
  function unsentRepliesLine(unsent) {
10184
8333
  if (unsent.length === 0)
@@ -10847,7 +8996,6 @@ function showTask(positional, context) {
10847
8996
  claim: currentClaim(store, ref.id, now),
10848
8997
  scope,
10849
8998
  approval: approvalOf(scope),
10850
- risk: ref.riskLevel ?? scope?.riskLevel ?? "routine",
10851
8999
  route: taskRouteView(routed, readiness),
10852
9000
  runs: runs.map(one => ({ ...one, route: store.runRoute(one.id) })),
10853
9001
  deliverable: ref.deliverable,
@@ -11245,9 +9393,6 @@ function blockTask(positional, flags, context) {
11245
9393
  return fail(write, json, "task block", "unknown-task", `no task \`${each}\``, EXIT.refused);
11246
9394
  }
11247
9395
  }
11248
- const racingGuard = refuseWhileRacing(context, "task block", id);
11249
- if (racingGuard !== null)
11250
- return racingGuard;
11251
9396
  const result = store.addEdge(id, on, mutationFrom(flags, context.now));
11252
9397
  if (!result.ok)
11253
9398
  return fail(write, json, "task block", "rejected", result.reason, EXIT.refused);
@@ -11284,9 +9429,7 @@ async function steerTask(positional, flags, context) {
11284
9429
  ? `no task \`${id}\``
11285
9430
  : filed.reason === "task-finished"
11286
9431
  ? `${id} is finished — a note has no next attempt to reach`
11287
- : filed.reason === "contest-open"
11288
- ? "agents are racing on this task — steering waits until the tournament settles"
11289
- : (filed.problem ?? "that note will not store");
9432
+ : (filed.problem ?? "that note will not store");
11290
9433
  return fail(write, json, "task steer", filed.reason, detail, EXIT.refused);
11291
9434
  }
11292
9435
  return succeed(write, json, "task steer", { task: id, note: filed.id }, () => [
@@ -11304,9 +9447,6 @@ function unblockTask(positional, flags, context) {
11304
9447
  if (store.getTask(id) === null) {
11305
9448
  return fail(write, json, "task unblock", "unknown-task", `no task \`${id}\``, EXIT.refused);
11306
9449
  }
11307
- const racingGuard = refuseWhileRacing(context, "task unblock", id);
11308
- if (racingGuard !== null)
11309
- return racingGuard;
11310
9450
  const result = store.removeEdge(id, on, mutationFrom(flags, context.now));
11311
9451
  if (!result.ok) {
11312
9452
  return fail(write, json, "task unblock", "not-waiting", `${id} was not waiting on ${on}`, EXIT.refused);
@@ -11342,9 +9482,7 @@ function nextTask(positional, flags, context) {
11342
9482
  ? `${id} is not queued — only queued work can move up`
11343
9483
  : moved.reason === "claimed"
11344
9484
  ? `${id} is being built right now — it needs no place in line`
11345
- : moved.reason === "contest-open"
11346
- ? "a tournament is running on this task — let it finish, then pick or abandon it from the tournament screen in the console (the task's page links to it)"
11347
- : "the queue rank could not be raised any further";
9485
+ : "the queue rank could not be raised any further";
11348
9486
  return fail(write, json, "task next", moved.reason, message, EXIT.refused);
11349
9487
  }
11350
9488
  return succeed(write, json, "task next", { task: id, priority: moved.priority }, () => [
@@ -11372,13 +9510,11 @@ function assignTask(positional, flags, context) {
11372
9510
  ? `${id} is not queued — only queued work can be reserved`
11373
9511
  : moved.reason === "claimed"
11374
9512
  ? `${id} is being built right now — it needs no reservation`
11375
- : moved.reason === "contest-open"
11376
- ? "a tournament is running on this task — let it finish, then pick or abandon it from the tournament screen in the console (the task's page links to it)"
11377
- : moved.reason === "no-such-worker"
11378
- ? `no worker named \`${runner}\` — \`toolroll runner list\` names them`
11379
- : moved.reason === "worker-retired"
11380
- ? `${runner} is retired — register the name again, or reserve for another worker`
11381
- : "the queue did not accept the move";
9513
+ : moved.reason === "no-such-worker"
9514
+ ? `no worker named \`${runner}\` — \`toolroll runner list\` names them`
9515
+ : moved.reason === "worker-retired"
9516
+ ? `${runner} is retired — register the name again, or reserve for another worker`
9517
+ : "the queue did not accept the move";
11382
9518
  return fail(write, json, "task assign", moved.reason, message, EXIT.refused);
11383
9519
  }
11384
9520
  return succeed(write, json, "task assign", { task: id, reservedFor: runner ?? null }, () => [
@@ -11409,7 +9545,6 @@ async function reopenTask(positional, flags, context) {
11409
9545
  "not-latched": `${id} was never closed on its tracker — there is nothing to reopen`,
11410
9546
  "not-seen-open": "the tracker has not been SEEN open again since the close — reopen it there, then `toolroll sync`",
11411
9547
  claimed: `${id} is being built right now`,
11412
- "contest-open": "a tournament is open on this task — decide it first",
11413
9548
  held: "a hold stands — lift it first (`task unhold`, or wait out the timer)",
11414
9549
  "question-open": "an unanswered question stands — answer or close it first (it is on the task page)",
11415
9550
  "incident-open": "an unresolved incident stands — resolve it first",
@@ -11471,7 +9606,7 @@ async function scopeTask(positional, flags, context) {
11471
9606
  const id = positional[0];
11472
9607
  const goal = text(flags, "goal");
11473
9608
  if (id === undefined || goal === undefined) {
11474
- return fail(write, json, "task scope", "usage", "`toolroll task scope <id> --goal <what success is> --acceptance <rubric> [--not <text>] [--touches a,b] [--candidate <commit>] [--budget-usd <n>] [--race provider:model[,provider:model…]] [--race-count 2..4] [--race-per-usd <n>] [--race-total-usd <n>]`", EXIT.usage);
9609
+ return fail(write, json, "task scope", "usage", "`toolroll task scope <id> --goal <what success is> --acceptance <rubric> [--not <text>] [--touches a,b] [--candidate <commit>] [--budget-usd <n>]`", EXIT.usage);
11475
9610
  }
11476
9611
  if (store.getTask(id) === null) {
11477
9612
  return fail(write, json, "task scope", "unknown-task", `no task \`${id}\``, EXIT.refused);
@@ -11487,10 +9622,6 @@ async function scopeTask(positional, flags, context) {
11487
9622
  if (acceptanceParse.problems.length > 0) {
11488
9623
  return fail(write, json, "task scope", "bad-acceptance", `${acceptanceParse.problems.map(p => p.message).join("; ")} — each criterion is \`<statement>|<evidence,kinds>\`, criteria are \`;\`-separated, and \`--acceptance plan\` asks the planner to write the rubric`, EXIT.usage);
11489
9624
  }
11490
- const riskGiven = text(flags, "risk");
11491
- if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
11492
- return fail(write, json, "task scope", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
11493
- }
11494
9625
  // v69: a prepared commit. The machine checks it out as the attempt and
11495
9626
  // runs the gate and the review; no agent is dispatched for it.
11496
9627
  const candidateGiven = text(flags, "candidate");
@@ -11504,9 +9635,6 @@ async function scopeTask(positional, flags, context) {
11504
9635
  const badText = validateScopeText({ goal, outOfScope: text(flags, "not") ?? null, touches });
11505
9636
  if (badText !== null)
11506
9637
  return fail(write, json, "task scope", badText.reason, badText.message, EXIT.usage);
11507
- // A tournament rides the same filing (stage 3): --race names the agents,
11508
- // the dollar terms are REQUIRED, and everything lands unapproved — the
11509
- // one yes later covers scope AND race terms as a single fingerprint.
11510
9638
  const budgetGiven = text(flags, "budget-usd");
11511
9639
  const defaults = store.getSpendDefaults();
11512
9640
  const budgetUsd = budgetGiven !== undefined
@@ -11517,84 +9645,6 @@ async function scopeTask(positional, flags, context) {
11517
9645
  if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0)) {
11518
9646
  return fail(write, json, "task scope", "bad-budget", "--budget-usd is a positive dollar amount", EXIT.usage);
11519
9647
  }
11520
- const raceGiven = text(flags, "race");
11521
- const raceCountGiven = text(flags, "race-count");
11522
- // The comparison road (Phase 3 slice B): labeled lanes, no dollar terms,
11523
- // any registered provider — refused outright when every lane could hold
11524
- // a real budget (the discipline gate: race those instead).
11525
- const compareGiven = text(flags, "compare");
11526
- const permissionMode = store.refFor(BUILT_IN, id).permissionMode ?? store.permissionDefault().mode;
11527
- let plannedComparison = null;
11528
- if (compareGiven !== undefined) {
11529
- if (raceGiven !== undefined || raceCountGiven !== undefined || text(flags, "race-per-usd") !== undefined || text(flags, "race-total-usd") !== undefined) {
11530
- return fail(write, json, "task scope", "usage", "--compare and the --race flags are different ceremonies — file one or the other", EXIT.usage);
11531
- }
11532
- if (store.mirrorByTask(id) !== null) {
11533
- return fail(write, json, "task scope", "external-race", "external work compares in a follow-up release — file the comparison on a local task", EXIT.refused);
11534
- }
11535
- const lanes = compareGiven.split(",").map(one => {
11536
- const [provider = "", model = ""] = one.trim().split(":");
11537
- return { provider, model, permissionMode };
11538
- });
11539
- plannedComparison = planComparison({ agents: lanes });
11540
- if (!plannedComparison.ok) {
11541
- return fail(write, json, "task scope", plannedComparison.reason, plannedComparison.message, EXIT.refused);
11542
- }
11543
- }
11544
- let plannedRace = null;
11545
- if (raceCountGiven !== undefined && raceGiven === undefined) {
11546
- return fail(write, json, "task scope", "usage", "--race-count needs --race to name the competing agent, e.g. `--race claude:claude-sonnet-5 --race-count 3`", EXIT.usage);
11547
- }
11548
- if (raceGiven !== undefined) {
11549
- // Explicit flags win; absent ones fall back to the configured defaults
11550
- // (operator request) — the digest binds the ACTUAL numbers either way,
11551
- // and the approval restates them. A budget that is simply MISSING is
11552
- // named as the missing flag here, before planTournament's generic
11553
- // positive-amount backstop turns it into a riddle (round-4 finding 11).
11554
- if (text(flags, "race-per-usd") === undefined && defaults?.racePerAgentMicrousd == null) {
11555
- return fail(write, json, "task scope", "bad-budget", "--race-per-usd is missing and no default is set — pass it, or set one with `toolroll config set budgets --race-per-usd <n>`", EXIT.usage);
11556
- }
11557
- if (text(flags, "race-total-usd") === undefined && defaults?.raceTotalMicrousd == null) {
11558
- return fail(write, json, "task scope", "bad-budget", "--race-total-usd is missing and no default is set — pass it, or set one with `toolroll config set budgets --race-total-usd <n>`", EXIT.usage);
11559
- }
11560
- const perUsd = Number(text(flags, "race-per-usd") ?? (defaults?.racePerAgentMicrousd == null ? Number.NaN : defaults.racePerAgentMicrousd / 1_000_000));
11561
- const totalUsd = Number(text(flags, "race-total-usd") ?? (defaults?.raceTotalMicrousd == null ? Number.NaN : defaults.raceTotalMicrousd / 1_000_000));
11562
- if (store.mirrorByTask(id) !== null) {
11563
- return fail(write, json, "task scope", "external-race", "external work races in a follow-up release — file the tournament on a local task", EXIT.refused);
11564
- }
11565
- let agents = raceGiven.split(",").map(one => {
11566
- const [provider = "", model = ""] = one.trim().split(":");
11567
- return { provider, model, permissionMode };
11568
- });
11569
- // The competing-agent COUNT (operator request): an explicit --race-count
11570
- // replicates a single named agent; with several named agents it may only
11571
- // agree with the list — a count that contradicts an explicit lineup is a
11572
- // question, not an instruction. Absent both, the configured default
11573
- // count replicates a single agent; an explicit list is always itself.
11574
- if (raceCountGiven !== undefined) {
11575
- const count = Number(raceCountGiven);
11576
- if (!Number.isInteger(count) || count < 2 || count > 4) {
11577
- return fail(write, json, "task scope", "usage", "--race-count is how many agents compete: a whole number from 2 to 4", EXIT.usage);
11578
- }
11579
- if (agents.length === 1) {
11580
- agents = Array.from({ length: count }, () => ({ ...agents[0] }));
11581
- }
11582
- else if (agents.length !== count) {
11583
- return fail(write, json, "task scope", "usage", `--race names ${agents.length} agents but --race-count says ${count} — make them agree, or name one agent and let the count replicate it`, EXIT.usage);
11584
- }
11585
- }
11586
- else if (agents.length === 1 && defaults?.raceAgents != null) {
11587
- agents = Array.from({ length: defaults.raceAgents }, () => ({ ...agents[0] }));
11588
- }
11589
- plannedRace = planTournament({
11590
- agents,
11591
- perAgentBudgetUsd: perUsd,
11592
- totalBudgetUsd: totalUsd,
11593
- });
11594
- if (!plannedRace.ok) {
11595
- return fail(write, json, "task scope", plannedRace.reason, plannedRace.message, EXIT.refused);
11596
- }
11597
- }
11598
9648
  // v24 routing flags (foundations finding 5 — these used to be silently
11599
9649
  // swallowed by the global parser): explicit flags resolve HERE, and an
11600
9650
  // explicit ask that cannot resolve refuses rather than filing unresolved.
@@ -11608,11 +9658,6 @@ async function scopeTask(positional, flags, context) {
11608
9658
  }
11609
9659
  explicitProfile = resolvedRouting.profile;
11610
9660
  }
11611
- // The race branch shares ONE transaction with the scope save (round-6
11612
- // finding 5): the attended exclusion refuses BEFORE anything writes, and
11613
- // a filing failure rolls the proposal back — a refused race never leaves
11614
- // a rewritten scope behind it.
11615
- //
11616
9661
  // C1/M3, the credentialed-CLI road: when --as/--token authenticate the
11617
9662
  // MODE'S SIGNER and their live mode auto-approves filings, the scope
11618
9663
  // seals in the same transaction it files — plain scopes only, and the
@@ -11630,8 +9675,7 @@ async function scopeTask(positional, flags, context) {
11630
9675
  const previous = store.getScope(id);
11631
9676
  if (ref === null || ref.deliverable === "report" || candidateGiven !== undefined ||
11632
9677
  ref.sizing?.source === "person" || approvalOf(previous).approved ||
11633
- previous?.proposedVia === "coordinator" || store.filedViaOf(id)?.startsWith("mcp:") ||
11634
- ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null))
9678
+ previous?.proposedVia === "coordinator" || store.filedViaOf(id)?.startsWith("mcp:"))
11635
9679
  return null;
11636
9680
  const input = { title: store.getTask(id).title, goal, outOfScope: text(flags, "not") ?? null, touches };
11637
9681
  const sized = store.applySizing(id, heuristicSizing(input), { followPlanning: true }, now);
@@ -11642,22 +9686,12 @@ async function scopeTask(positional, flags, context) {
11642
9686
  // Wait for bounded classification before filing so any approval in the
11643
9687
  // transaction below binds the final size and route.
11644
9688
  await refinement;
11645
- // ONE replayed composite (surfaces round 1, finding 3): the filing, any
11646
- // race/comparison terms, AND the mode seal record as a single operation —
9689
+ // ONE replayed composite (surfaces round 1, finding 3): the filing AND
9690
+ // the mode seal record as a single operation —
11647
9691
  // a replayed key returns the FIRST answer whole instead of re-sealing
11648
9692
  // whatever scope happens to be current.
11649
9693
  const filed = store.replay(mutationFrom(flags, now), "task-scope-filed", () => store.transact(() => {
11650
- if (plannedRace !== null && plannedRace.ok) {
11651
- const raceRef = store.refFor(BUILT_IN, id);
11652
- if (store.openAuthorizationFor(raceRef.id) !== null) {
11653
- return {
11654
- ok: false,
11655
- reason: "attended-open",
11656
- message: "an attended authorization is open on this task — revoke it before filing a tournament",
11657
- };
11658
- }
11659
- }
11660
- const coverage = plannedRace === null && plannedComparison === null && actor !== null
9694
+ const coverage = actor !== null
11661
9695
  ? modeFilingCoverage(store, store.refFor(BUILT_IN, id).repo, actor, now)
11662
9696
  : null;
11663
9697
  const proposed = propose(store, {
@@ -11667,8 +9701,6 @@ async function scopeTask(positional, flags, context) {
11667
9701
  outOfScope: text(flags, "not") ?? null,
11668
9702
  touches,
11669
9703
  acceptance: acceptanceParse.criteria,
11670
- // v47: the declared risk — a durable task choice the route reads.
11671
- ...(riskGiven === undefined ? {} : { riskLevel: riskGiven }),
11672
9704
  ...(budgetUsd !== null
11673
9705
  ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
11674
9706
  : coverage?.defaultBudgetMicrousd != null
@@ -11696,42 +9728,6 @@ async function scopeTask(positional, flags, context) {
11696
9728
  sealedUnderMode = store.sealScopeApproval(id, actor, now, {}, { kind: "mode", modeDigest: coverage.digest });
11697
9729
  modeRefusedCoordinator = !sealedUnderMode;
11698
9730
  }
11699
- if (plannedRace !== null && plannedRace.ok) {
11700
- const plan = plannedRace.plan;
11701
- const raceRef = store.refFor(BUILT_IN, id);
11702
- store.fileTournamentTerms({
11703
- taskRef: raceRef.id,
11704
- raceDigest: plan.raceDigest,
11705
- agents: plan.agents,
11706
- perAgentBudgetMicrousd: plan.perAgentBudgetMicrousd,
11707
- overrunReserveMicrousd: plan.overrunReserveMicrousd,
11708
- totalBudgetMicrousd: plan.totalBudgetMicrousd,
11709
- priceVersion: plan.priceVersion,
11710
- publicationPolicy: plan.publicationPolicy,
11711
- }, now);
11712
- }
11713
- if (plannedComparison !== null && plannedComparison.ok) {
11714
- const plan = plannedComparison.plan;
11715
- const compareRef = store.refFor(BUILT_IN, id);
11716
- if (store.openAuthorizationFor(compareRef.id) !== null) {
11717
- return {
11718
- ok: false,
11719
- reason: "attended-open",
11720
- message: "an attended authorization is open on this task — revoke it before filing a comparison",
11721
- };
11722
- }
11723
- store.fileTournamentTerms({
11724
- taskRef: compareRef.id,
11725
- kind: "comparison",
11726
- raceDigest: plan.comparisonDigest,
11727
- agents: plan.agents,
11728
- perAgentBudgetMicrousd: 0,
11729
- overrunReserveMicrousd: 0,
11730
- totalBudgetMicrousd: 0,
11731
- priceVersion: 0,
11732
- publicationPolicy: plan.publicationPolicy,
11733
- }, now);
11734
- }
11735
9731
  return { ok: true, scope: proposed, sealedUnderMode, modeRefusedCoordinator };
11736
9732
  }));
11737
9733
  if (!filed.ok) {
@@ -11750,33 +9746,6 @@ async function scopeTask(positional, flags, context) {
11750
9746
  ...describeScope(scope),
11751
9747
  ]);
11752
9748
  }
11753
- if (plannedComparison !== null && plannedComparison.ok) {
11754
- const plan = plannedComparison.plan;
11755
- return succeed(write, json, "task scope", { scope, comparison: plan }, () => [
11756
- `Scope and comparison written for ${id}. Nothing builds until somebody approves BOTH, with one yes:`,
11757
- ...describeScope(scope),
11758
- "",
11759
- ...plan.laneWords.map(lane => ` ${lane}`),
11760
- " no dollar caps exist on a comparison — each agent runs until it finishes or stops making progress;",
11761
- " spend lands measured only where the harness reports dollars",
11762
- "",
11763
- ` toolroll task approve ${id} --yes`,
11764
- ]);
11765
- }
11766
- if (plannedRace !== null && plannedRace.ok) {
11767
- const plan = plannedRace.plan;
11768
- const worst = plan.perAgentReserveMicrousd.reduce((sum, reserve) => sum + plan.perAgentBudgetMicrousd + reserve, 0);
11769
- return succeed(write, json, "task scope", { scope, race: plan }, () => [
11770
- `Scope and tournament written for ${id}. Nothing builds until somebody approves BOTH, with one yes:`,
11771
- ...describeScope(scope),
11772
- "",
11773
- ` tournament: ${plan.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`,
11774
- ` each agent may spend $${(plan.perAgentBudgetMicrousd / 1_000_000).toFixed(2)}, plus its stated overrun reserve;` +
11775
- ` worst case $${(worst / 1_000_000).toFixed(2)} total`,
11776
- "",
11777
- ` toolroll task approve ${id} --yes`,
11778
- ]);
11779
- }
11780
9749
  return succeed(write, json, "task scope", { scope }, () => [
11781
9750
  `Scope written for ${id}. Nothing will build it until somebody approves it.`,
11782
9751
  ...describeScope(scope),
@@ -11795,8 +9764,8 @@ async function scopeTask(positional, flags, context) {
11795
9764
  /**
11796
9765
  * `toolroll task route <id>` (v47): the explainable phase route —
11797
9766
  * shown from the ONE projection every surface renders, and edited only by
11798
- * an approver: `--risk <routine|elevated|high>` declares the task's risk;
11799
- * `--phase <p> --provider <p> --model <m>` records a per-phase override
9767
+ * an approver: `--size <small|medium|large>` and `--risky yes|no` set the
9768
+ * task's size; `--phase <p> --provider <p> --model <m>` records a per-phase override
11800
9769
  * (an exact pair — approvals bind exact routing) with attribution;
11801
9770
  * `--clear-phase <p>` removes one; `--digest <d>` names the scope digest
11802
9771
  * you read, so the edit lands only on what you saw. The edit is ONE
@@ -11810,13 +9779,12 @@ async function routeTaskCommand(positional, flags, context) {
11810
9779
  const { store, write, json, clock } = context;
11811
9780
  const id = positional[0];
11812
9781
  if (id === undefined) {
11813
- return fail(write, json, "task route", "usage", "`toolroll task route <id> [--risk <level>] [--phase <p> --provider <p> --model <m> | --clear-phase <p>] [--digest <d>] --as <you> --token <t>`", EXIT.usage);
9782
+ return fail(write, json, "task route", "usage", "`toolroll task route <id> [--size <s>] [--risky yes|no] [--phase <p> --provider <p> --model <m> | --clear-phase <p>] [--digest <d>] --as <you> --token <t>`", EXIT.usage);
11814
9783
  }
11815
9784
  if (store.getTask(id) === null) {
11816
9785
  return fail(write, json, "task route", "unknown-task", `no task \`${id}\``, EXIT.refused);
11817
9786
  }
11818
9787
  const ref = store.refFor(BUILT_IN, id);
11819
- const riskGiven = text(flags, "risk");
11820
9788
  const phaseGiven = text(flags, "phase");
11821
9789
  const clearGiven = text(flags, "clear-phase");
11822
9790
  const providerGiven = text(flags, "provider");
@@ -11824,7 +9792,7 @@ async function routeTaskCommand(positional, flags, context) {
11824
9792
  const digestGiven = text(flags, "digest");
11825
9793
  const sizeGiven = text(flags, "size");
11826
9794
  const riskyGiven = text(flags, "risky");
11827
- const editing = riskGiven !== undefined || phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
9795
+ const editing = phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
11828
9796
  const show = () => {
11829
9797
  const scope = store.getScope(id);
11830
9798
  const current = store.refFor(BUILT_IN, id);
@@ -11832,7 +9800,7 @@ async function routeTaskCommand(positional, flags, context) {
11832
9800
  const readiness = store.readinessLookupFor(ref.repo, ref.assignedRunner, clock());
11833
9801
  const view = taskRouteView(routed, readiness);
11834
9802
  const projection = routed !== null && routed.kind === "route" ? projectRoute(routed.route, readiness) : null;
11835
- return succeed(write, json, "task route", { id, risk: current.riskLevel ?? scope?.riskLevel ?? "routine", riskConsequence: riskConsequence(current.riskLevel ?? scope?.riskLevel ?? "routine"), size: current.sizing ?? null, source: routed !== null && routed.kind === "route" ? routed.source : routed?.kind ?? null, route: view, overrides: current.routeOverrides ?? [], digest: scope?.digest ?? null, approval: approvalOf(scope) }, () => routed === null
9803
+ return succeed(write, json, "task route", { id, size: current.sizing ?? null, source: routed !== null && routed.kind === "route" ? routed.source : routed?.kind ?? null, route: view, overrides: current.routeOverrides ?? [], digest: scope?.digest ?? null, approval: approvalOf(scope) }, () => routed === null
11836
9804
  ? [`${id}: no route can be recommended yet — ${scope === null ? "place the task in a repository and file a scope" : scope.unresolvedReason ?? "the phase configuration cannot resolve"}`]
11837
9805
  : [
11838
9806
  ...(projection === null
@@ -11840,10 +9808,8 @@ async function routeTaskCommand(positional, flags, context) {
11840
9808
  : [
11841
9809
  `${id}: route ${routed.kind === "route" && routed.source === "approved" ? "SEALED by the approval" : routed.kind === "route" && routed.source === "proposed" ? "proposed — the next approval seals it" : "recommended live — no scope filed yet"}`,
11842
9810
  ...routeWords(projection),
11843
- // The declared risk, in the words the console and chat
11844
- // use — and the one CLI hint that belongs here, not in
11845
- // the route's own reasons: how a stronger tier is named.
11846
- ` risk ${current.riskLevel ?? scope?.riskLevel ?? "routine"} — ${riskConsequence(current.riskLevel ?? scope?.riskLevel ?? "routine")}`,
9811
+ // The one CLI hint that belongs here, not in the
9812
+ // route's own reasons: how a stronger tier is named.
11847
9813
  ...(projection.legs.some(leg => leg.reasons.some(reason => reason.startsWith("no stronger")))
11848
9814
  ? [` name a stronger agent once with \`config set <phase> --tier strong --provider … --model …\``]
11849
9815
  : []),
@@ -11866,9 +9832,6 @@ async function routeTaskCommand(positional, flags, context) {
11866
9832
  if (acting === null) {
11867
9833
  return fail(write, json, "task route", "usage", "changing a route takes `--as <you> --token <t>` — it reroutes spend", EXIT.usage);
11868
9834
  }
11869
- if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
11870
- return fail(write, json, "task route", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
11871
- }
11872
9835
  if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
11873
9836
  return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
11874
9837
  }
@@ -11906,7 +9869,6 @@ async function routeTaskCommand(positional, flags, context) {
11906
9869
  const authenticated = authenticateApprover(store, acting.name, acting.token);
11907
9870
  return authenticated.ok ? { ok: true } : { ok: false, reason: authenticated.reason };
11908
9871
  },
11909
- ...(riskGiven === undefined ? {} : { risk: riskGiven }),
11910
9872
  ...(sizeEdit === undefined ? {} : { size: sizeEdit }),
11911
9873
  ...(phase === undefined
11912
9874
  ? {}
@@ -11917,7 +9879,7 @@ async function routeTaskCommand(positional, flags, context) {
11917
9879
  if (edited.reason === "unauthenticated") {
11918
9880
  return fail(write, json, "task route", edited.detail, describeApproveFailure(edited.detail, id), EXIT.refused);
11919
9881
  }
11920
- const code = { "no-task": "unknown-task", "live-claim": "claimed", "contest-open": "contest-open", changed: "changed", nothing: "usage", "not-configured": "not-configured" };
9882
+ const code = { "no-task": "unknown-task", "live-claim": "claimed", changed: "changed", nothing: "usage", "not-configured": "not-configured" };
11921
9883
  return fail(write, json, "task route", code[edited.reason], `${id}: ${edited.detail}`, edited.reason === "nothing" ? EXIT.usage : EXIT.refused);
11922
9884
  }
11923
9885
  const code = show();
@@ -11959,14 +9921,6 @@ async function approveTask(positional, flags, context) {
11959
9921
  write(line);
11960
9922
  for (const line of planContractLines(store, context.evidenceRoot, id))
11961
9923
  write(line);
11962
- const interactiveRace = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
11963
- if (interactiveRace !== null) {
11964
- write("");
11965
- write(` AND it starts a tournament: ${interactiveRace.n} agents build this independently —`);
11966
- write(` ${interactiveRace.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`);
11967
- write(` each may spend $${(interactiveRace.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus its overrun reserve;`);
11968
- write(` the whole tournament is capped at $${(interactiveRace.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You pick the winner; only the winner publishes.`);
11969
- }
11970
9924
  write("");
11971
9925
  const agreed = await confirm("Approve exactly this?");
11972
9926
  if (!agreed) {
@@ -11995,14 +9949,8 @@ async function approveTask(positional, flags, context) {
11995
9949
  for (const line of planContractLines(store, context.evidenceRoot, id))
11996
9950
  write(line);
11997
9951
  write("");
11998
- const previewRace = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
11999
- if (previewRace !== null) {
12000
- write(` AND the tournament: ${previewRace.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`);
12001
- write(` each capped at $${(previewRace.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} + reserve, total $${(previewRace.totalBudgetMicrousd / 1_000_000).toFixed(2)}`);
12002
- write("");
12003
- }
12004
9952
  write("Nothing has been approved. Agree to this exact scope with:");
12005
- write(` toolroll task approve ${id} --yes --digest ${previewRace === null ? scope.digest : jointApprovalDigest(scope.digest, previewRace.raceDigest)} --as <you> --token <your password>`);
9953
+ write(` toolroll task approve ${id} --yes --digest ${scope.digest} --as <you> --token <your password>`);
12006
9954
  // Unconfirmed is "no, not yet" — exit 3 in both modes (round-4 finding 10).
12007
9955
  return EXIT.refused;
12008
9956
  }
@@ -12014,38 +9962,6 @@ async function approveTask(positional, flags, context) {
12014
9962
  // The credential is required for a different reason: an agent that can run
12015
9963
  // these commands can read the digest out of `task show`, and an approval
12016
9964
  // nobody has to authenticate would let it agree to its own brief.
12017
- //
12018
- // A tournament task's yes covers BOTH documents (finding 31): the named
12019
- // digest is tournament-approval/v1 = H(scope, race), and the scope and
12020
- // the race terms approve together, in one transaction, or not at all.
12021
- const raceTerms = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
12022
- if (raceTerms !== null) {
12023
- const joint = jointApprovalDigest(scope.digest, raceTerms.raceDigest);
12024
- if (saw !== joint && saw !== scope.digest) {
12025
- return fail(write, json, "task approve", "changed", `this task races a tournament — approve the JOINT fingerprint: ${joint}`, EXIT.refused);
12026
- }
12027
- if (saw === scope.digest && !confirmedAloud) {
12028
- return fail(write, json, "task approve", "changed", `this task races a tournament — the yes must name the joint fingerprint ${joint}, which covers the race terms too`, EXIT.refused);
12029
- }
12030
- const both = store.transact(() => {
12031
- const scopeApproved = approve(store, id, asWho, now, scope.digest, token, mutationFrom(flags, now));
12032
- if (!scopeApproved.ok)
12033
- return scopeApproved;
12034
- if (!store.approveTournamentTerms(raceTerms.id, asWho, raceTerms.raceDigest, now)) {
12035
- throw new Error("the race terms changed while you were reading — nothing was approved");
12036
- }
12037
- return scopeApproved;
12038
- });
12039
- if (!both.ok) {
12040
- return fail(write, json, "task approve", both.reason, both.reason === "policy" ? `${id}: ${both.message}` : describeApproveFailure(both.reason, id), EXIT.refused);
12041
- }
12042
- return succeed(write, json, "task approve", { scope: both.scope, race: raceTerms }, () => [
12043
- `Approved — scope AND tournament, with one yes. ${raceTerms.n} agents will build ${id} independently:`,
12044
- ...describeScope(both.scope),
12045
- ` ${raceTerms.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`,
12046
- ` each may spend $${(raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus its overrun reserve; total cap $${(raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}`,
12047
- ]);
12048
- }
12049
9965
  const approved = approve(store, id, asWho, now, saw, token, mutationFrom(flags, now));
12050
9966
  if (!approved.ok) {
12051
9967
  if (approved.reason === "policy")
@@ -12064,7 +9980,7 @@ function describeApproveFailure(reason, id) {
12064
9980
  if (reason === "requester")
12065
9981
  return `you filed ${id}, and this project needs someone else to approve it`;
12066
9982
  if (reason === "person-required")
12067
- return `${id} is protected work: a person has to approve it, not an operating mode, a routine or an AI teammate`;
9983
+ return `${id} is protected work: a person has to approve it, not an operating mode, a schedule or an AI teammate`;
12068
9984
  if (reason === "second-approver")
12069
9985
  return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
12070
9986
  if (reason === "no-approvers") {
@@ -12075,7 +9991,7 @@ function describeApproveFailure(reason, id) {
12075
9991
  if (reason === "unrouted")
12076
9992
  return `${id} was filed before agent routing and its old approval no longer stands — an approval now names exactly which agent plans, builds, repairs, and reviews: file the scope again (\`task scope ${id} …\`) so it is routed under today's agents, then approve it`;
12077
9993
  if (reason === "profile-unresolved")
12078
- return `${id} cannot name an exact agent for every role — configure the project's agents (\`config set <phase> --provider … --model …\`), then \`routine refresh ${id}\` and approve it again`;
9994
+ return `${id} cannot name an exact agent for every role — configure the project's agents (\`config set <phase> --provider … --model …\`), then file the scope again (\`task scope ${id} …\`) and approve it`;
12079
9995
  return `${id} has no scope to approve`;
12080
9996
  }
12081
9997
  /**
@@ -12425,9 +10341,6 @@ function unholdTask(positional, flags, context) {
12425
10341
  if (store.getTask(id) === null) {
12426
10342
  return fail(write, json, "task unhold", "unknown-task", `no task \`${id}\``, EXIT.refused);
12427
10343
  }
12428
- const racingGuard = refuseWhileRacing(context, "task unhold", id);
12429
- if (racingGuard !== null)
12430
- return racingGuard;
12431
10344
  const lifted = store.unhold(store.refFor(BUILT_IN, id).id, mutationFrom(flags, context.now));
12432
10345
  if (!lifted)
12433
10346
  return fail(write, json, "task unhold", "not-held", `${id} was not on hold`, EXIT.refused);
@@ -12829,6 +10742,7 @@ async function backupCommand(positional, flags, context) {
12829
10742
  return fail(context.write, context.json, command, "failed", `The backup failed: ${made.error}`, EXIT.failed, { backup: made.id });
12830
10743
  return succeed(context.write, context.json, command, { backup: made }, () => [
12831
10744
  `Backed up to ${made.file} (${bytesWords(made.bytes)}).${made.removed === 0 ? "" : ` Removed ${made.removed} older ${made.removed === 1 ? "backup" : "backups"}.`}`,
10745
+ ...(made.checkpoint !== null && "problem" in made.checkpoint ? [`The activity log failed its tamper check, so it was not anchored: ${made.checkpoint.problem}`] : []),
12832
10746
  ]);
12833
10747
  }
12834
10748
  const settings = store.backupSettings();
@@ -13374,14 +11288,6 @@ function text(flags, name) {
13374
11288
  function flag(flags, name) {
13375
11289
  return flags.get(name) === true || flags.get(name) === "true";
13376
11290
  }
13377
- /** A running tournament owns its task (round-1 finding 5): the generic
13378
- * doors refuse until it finishes, is picked, or is abandoned. */
13379
- function refuseWhileRacing(context, command, taskId) {
13380
- const ref = context.store.lookupRef(taskId);
13381
- if (ref === null || context.store.openContestFor(ref.id) === null)
13382
- return null;
13383
- return fail(context.write, context.json, command, "contest-open", "a tournament is running on this task — let it finish, then pick or abandon it from the tournament screen in the console (the task's page links to it)", EXIT.refused);
13384
- }
13385
11291
  /**
13386
11292
  * The demo fence (Codex adoption review, finding 8): a database stamped as
13387
11293
  * a demo sandbox NEVER spends money or touches the world outside — no
@@ -13391,9 +11297,8 @@ function refuseWhileRacing(context, command, taskId) {
13391
11297
  * is the enforcement.
13392
11298
  */
13393
11299
  /**
13394
- * Deep links (attended A5): when a console URL is configured (`webhook set
13395
- * console-url`), CLI answers print it beside ids so the attended eye can
13396
- * jump. Absent configuration prints nothing — a link nobody configured is
11300
+ * Deep links: when a console URL is configured (`webhook set
11301
+ * console-url`), CLI answers print it beside ids so a reader can jump. Absent configuration prints nothing — a link nobody configured is
13397
11302
  * a guess, and stale guesses are worse than none.
13398
11303
  */
13399
11304
  function consoleLinkFor(context, path) {