toolroll 0.9.52 → 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 (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/board.d.ts +3 -19
  8. package/dist/board.js +4 -42
  9. package/dist/browser/workspace.js +18 -18
  10. package/dist/browser-workspace.d.ts +2 -1
  11. package/dist/builder.d.ts +9 -40
  12. package/dist/builder.js +50 -257
  13. package/dist/chat-actions.js +3 -14
  14. package/dist/chat-controls.d.ts +0 -4
  15. package/dist/chat-controls.js +0 -1
  16. package/dist/chat-decide.js +1 -3
  17. package/dist/chat-delivery.d.ts +0 -2
  18. package/dist/chat-delivery.js +0 -1
  19. package/dist/chat-quiet.js +2 -2
  20. package/dist/chat-task-actions.d.ts +1 -1
  21. package/dist/chat-task-actions.js +0 -2
  22. package/dist/claim.d.ts +79 -244
  23. package/dist/claim.js +688 -1023
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +10 -2
  26. package/dist/contracts/cli.d.ts +0 -13
  27. package/dist/contracts/cli.js +0 -2
  28. package/dist/contracts/console-api.d.ts +5 -10
  29. package/dist/contracts/console-api.js +4 -10
  30. package/dist/contracts/flow.d.ts +50 -0
  31. package/dist/contracts/flow.js +26 -1
  32. package/dist/contracts/lead-tools.d.ts +0 -13
  33. package/dist/contracts/lead-tools.js +3 -3
  34. package/dist/contracts/store-json.d.ts +1 -25
  35. package/dist/contracts/store-json.js +3 -13
  36. package/dist/converse.d.ts +1 -11
  37. package/dist/converse.js +6 -38
  38. package/dist/coordinator.d.ts +0 -1
  39. package/dist/coordinator.js +3 -5
  40. package/dist/demo.js +16 -26
  41. package/dist/desktop-update-gate.js +0 -1
  42. package/dist/desktop-update.js +1 -3
  43. package/dist/dispatch.d.ts +1 -1
  44. package/dist/dispatch.js +1 -1
  45. package/dist/dispose.d.ts +2 -3
  46. package/dist/dispose.js +17 -92
  47. package/dist/envelope.js +0 -3
  48. package/dist/exec.d.ts +0 -64
  49. package/dist/exec.js +3 -356
  50. package/dist/exhaustion.d.ts +22 -76
  51. package/dist/exhaustion.js +14 -140
  52. package/dist/export.js +4 -4
  53. package/dist/flow-engine.js +1 -1
  54. package/dist/flow-schedule.d.ts +154 -0
  55. package/dist/flow-schedule.js +321 -0
  56. package/dist/flow-share.js +1 -1
  57. package/dist/flow-triggers.d.ts +6 -1
  58. package/dist/flow-triggers.js +65 -10
  59. package/dist/flows-cli.js +8 -4
  60. package/dist/flows-ui.js +2 -0
  61. package/dist/flows.js +1 -1
  62. package/dist/guides.js +9 -29
  63. package/dist/invoke.d.ts +1 -24
  64. package/dist/invoke.js +17 -259
  65. package/dist/lead-status.js +2 -4
  66. package/dist/lead-voice.js +1 -1
  67. package/dist/mate-doors.d.ts +1 -2
  68. package/dist/mate-doors.js +12 -25
  69. package/dist/mate-tools.d.ts +2 -2
  70. package/dist/mate-tools.js +10 -22
  71. package/dist/modes.d.ts +6 -7
  72. package/dist/modes.js +7 -13
  73. package/dist/operate-remote-arguments.js +2 -6
  74. package/dist/operate-remote.js +6 -7
  75. package/dist/operate.d.ts +0 -6
  76. package/dist/operate.js +222 -2247
  77. package/dist/phase-routing.d.ts +13 -23
  78. package/dist/phase-routing.js +13 -35
  79. package/dist/plan-auto.js +2 -2
  80. package/dist/planner-source.js +2 -1
  81. package/dist/policy.d.ts +1 -4
  82. package/dist/policy.js +0 -8
  83. package/dist/process-recovery-eligibility.js +2 -3
  84. package/dist/project-delete.js +3 -15
  85. package/dist/proposal.d.ts +4 -24
  86. package/dist/proposal.js +3 -88
  87. package/dist/provider-auth.d.ts +3 -3
  88. package/dist/provider-auth.js +4 -6
  89. package/dist/provider.d.ts +11 -25
  90. package/dist/provider.js +4 -36
  91. package/dist/push.js +1 -1
  92. package/dist/recipe-ui.js +6 -6
  93. package/dist/recipes.d.ts +3 -3
  94. package/dist/recipes.js +27 -23
  95. package/dist/run-closeout.d.ts +1 -1
  96. package/dist/run-closeout.js +1 -4
  97. package/dist/scope.d.ts +11 -65
  98. package/dist/scope.js +17 -82
  99. package/dist/serve.js +15 -24
  100. package/dist/server/chat.js +7 -30
  101. package/dist/server/flows.js +11 -10
  102. package/dist/server/guards.js +0 -1
  103. package/dist/server/pages-assets.js +1 -1
  104. package/dist/server/pages.js +0 -2
  105. package/dist/server/people-tokens.js +0 -4
  106. package/dist/server/route-table.d.ts +1 -1
  107. package/dist/server/route-table.js +2 -17
  108. package/dist/server/settings.js +1 -8
  109. package/dist/server/shared.d.ts +28 -212
  110. package/dist/server/shared.js +71 -780
  111. package/dist/server/task-views.js +9 -76
  112. package/dist/server/tasks.d.ts +1 -50
  113. package/dist/server/tasks.js +36 -946
  114. package/dist/setup-guide.d.ts +2 -2
  115. package/dist/slack-chat.d.ts +0 -2
  116. package/dist/slack-chat.js +0 -1
  117. package/dist/store.d.ts +105 -1340
  118. package/dist/store.js +706 -4010
  119. package/dist/surface.js +10 -24
  120. package/dist/task-composer.d.ts +1 -1
  121. package/dist/task-composer.js +1 -1
  122. package/dist/task-control.d.ts +3 -10
  123. package/dist/task-control.js +2 -7
  124. package/dist/teammate-desk.js +1 -1
  125. package/dist/teammates-ui.js +1 -1
  126. package/dist/telegram-mate.d.ts +0 -3
  127. package/dist/telegram-mate.js +0 -1
  128. package/dist/templates.d.ts +11 -7
  129. package/dist/templates.js +12 -9
  130. package/dist/toolroll-update.d.ts +10 -0
  131. package/dist/toolroll-update.js +56 -4
  132. package/dist/work-index.js +2 -6
  133. package/dist/workspace-ui.js +1 -1
  134. package/package.json +1 -1
  135. package/scripts/recovery-canary.mjs +3 -3
  136. package/dist/contest.d.ts +0 -318
  137. package/dist/contest.js +0 -753
  138. package/dist/held.d.ts +0 -150
  139. package/dist/held.js +0 -799
  140. package/dist/liveness.d.ts +0 -30
  141. package/dist/liveness.js +0 -42
  142. package/dist/pricing.d.ts +0 -45
  143. package/dist/pricing.js +0 -77
  144. package/dist/routine.d.ts +0 -228
  145. package/dist/routine.js +0 -764
  146. package/dist/supervisor.mjs +0 -297
package/dist/operate.js CHANGED
@@ -42,7 +42,7 @@ import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
42
42
  import { origin, readMonitoring } from "./monitoring-settings.js";
43
43
  import { logEvent } from "./log.js";
44
44
  import { FLOW_EVERY_MS, flowHousekeeping, moveCards, moveCardsAfter } from "./flow-cadence.js";
45
- import { readHooksBase } from "./flow-triggers.js";
45
+ import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
46
46
  import { sendTeammateSummaries } from "./teammate-admin.js";
47
47
  import { runRequestedUndos, sendTeammateWeeklies } from "./teammate-week.js";
48
48
  import { refreshConnections } from "./mcp-connect.js";
@@ -83,10 +83,10 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
83
83
  *
84
84
  * **Nothing ever prompts.** There is no terminal on the other end at 3am.
85
85
  */
86
- import { homedir, hostname, tmpdir } from "node:os";
86
+ import { homedir, hostname } from "node:os";
87
87
  import { basename, dirname, join, resolve } from "node:path";
88
88
  import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
89
- 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";
90
90
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
91
91
  import { authorizePlanUnderMode } from "./plan-auto.js";
92
92
  import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
@@ -94,8 +94,8 @@ import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
94
94
  import { configPath, addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
95
95
  import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
96
96
  import { pushPass } from "./push.js";
97
- import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync, mkdirSync } from "node:fs";
98
- 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";
99
99
  import { claimActor, currentActor, parseLeadToken, withActor } from "./actor.js";
100
100
  import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
101
101
  import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
@@ -166,17 +166,15 @@ import { tally, spendLine } from "./summary.js";
166
166
  import { bodyHashOf, describePublicationGrant, observeChecks, publicationBody, publishPass, sweepMerges, } from "./publish.js";
167
167
  import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
168
168
  import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
169
- 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";
170
170
  import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
171
- import { attendedLivenessState } from "./liveness.js";
172
- import { HeldSessionCoordinator, sweepHeldOrphans } from "./held.js";
173
171
  import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
174
172
  import { builtIn, guarded } from "./backend.js";
175
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";
176
174
  import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
177
175
  import { serveMcp } from "./mcp.js";
178
176
  import { createInterface } from "node:readline";
179
- 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";
180
178
  import { presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
181
179
  import { WorktreePool } from "./worktree.js";
182
180
  import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
@@ -186,25 +184,23 @@ import { readExecutorOf } from "./read-executor.js";
186
184
  import { parseWorkView } from "./workspace-ui.js";
187
185
  import { taskWorkSummaryOf } from "./work-summary.js";
188
186
  import { assignmentOf, assignmentBrief, syncAssignmentHandoffs } from "./assignment.js";
189
- import { approveRoutine, describeRoutine, fireRoutine, refreshRoutineAgents, routineAgentsState, routineDigestOf, validateRoutineTerms, ROUTINE_NAME, } from "./routine.js";
190
- import { fileTaskProposal, fileRoutineProposal, validateTaskText } from "./proposal.js";
187
+ import { createScheduledFlow, describeSchedule, parseSchedule } from "./flow-schedule.js";
188
+ import { fileTaskProposal, validateTaskText } from "./proposal.js";
191
189
  import { TEMPLATES, templateByName } from "./templates.js";
192
- import { planTournament, planComparison, contestNoun, jointApprovalDigest, admitContest, crossReadyBarrier, finalizeContestant, recoverContests, maybeAggregate as contestMaybeAggregate, sweepContestCleanup, escalateOverdueContests } from "./contest.js";
193
190
  import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
194
- import { resolvePhaseAgent, resolveScopeProfile, resolveScopeChain, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
195
- 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";
196
193
  import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
197
194
  import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
198
195
  import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
199
196
  import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
200
197
  import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
201
198
  import { attestProvider, attestationOf, versionInRange } from "./attest.js";
202
- import { recognizesEligible } from "./exhaustion.js";
203
199
  import { build, proveApprovedProfile, } from "./builder.js";
204
200
  import { plan as planTask } from "./planner.js";
205
201
  import { attachTmux, elapsedWords, openInTmux, PEEK_TAIL_LINES, runPeek, snapshotLiveRuns } from "./peek-cli.js";
206
202
  import { scout as scoutTask } from "./scout.js";
207
- import { profileDigestOf, chainDigestOf, entryDigestOf } from "./scope.js";
203
+ import { profileDigestOf } from "./scope.js";
208
204
  import { PROVIDER_KEY_ENV, SUBSCRIPTION_CAPABLE, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
209
205
  import { run, terminateLiveProviders, run as execRun } from "./exec.js";
210
206
  import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
@@ -359,14 +355,8 @@ External trackers — build what a tracker nominates, under local approvals
359
355
  toolroll approver list
360
356
  toolroll task scope <id> --goal <what success is>
361
357
  [--not <text>] [--touches a,b] [--budget-usd <n>]
362
- [--race provider:model[,provider:model…]] [--race-count 2..4]
363
- [--race-per-usd <n>] [--race-total-usd <n>]
364
- [--compare provider:model[,provider:model…]] (labeled comparison — no dollar caps; needs a lane no budget can bound)
365
- a tournament races 2-4 agents on the
366
- task; you compare and pick one
367
358
  toolroll task approve <id> the yes — interactive, or
368
- --yes --digest <d> --as <you> --token <t> for scripts; a tournament
369
- approves both documents with one yes, on the joint fingerprint
359
+ --yes --digest <d> --as <you> --token <t> for scripts
370
360
  toolroll task requeue <id> --as <you> --token <t>
371
361
  exit a stall: incidents resolved,
372
362
  strikes cleared, queued again
@@ -374,8 +364,7 @@ External trackers — build what a tracker nominates, under local approvals
374
364
  run the approved check again on the
375
365
  last attempt's exact commit — a new
376
366
  attempt, no agent, saved result
377
- toolroll config set budgets [--build-usd <n>] [--race-per-usd <n>]
378
- [--race-total-usd <n>] [--race-agents 2..4] --as <you> --token <t>
367
+ toolroll config set budgets [--build-usd <n>] --as <you> --token <t>
379
368
  spend defaults new filings pre-fill
380
369
  from; config clear budgets resets
381
370
 
@@ -443,35 +432,19 @@ Capabilities — what the work needs, recorded and probed, never valued
443
432
  plan before building: an agent reads
444
433
  the repo, asks you questions, and
445
434
  proposes a scope you approve
446
- toolroll task route <id> [--risk routine|elevated|high]
447
- [--size small|medium|large] [--risky yes|no]
435
+ toolroll task route <id> [--size small|medium|large] [--risky yes|no]
448
436
  [--phase plan|build|repair --provider <p> [--model <m>] | --clear-phase <phase>]
449
437
  --as <you> --token <t> which agent plans, builds, and repairs
450
438
  this task, with the reason for
451
- each; declare its risk or override a
439
+ each; set its size or override a
452
440
  phase — approval seals the route
453
441
 
454
- Routines — tasks that fire on a schedule, each instance isolated
455
- toolroll template list common routines, shipped
456
- toolroll template show <name> the full prefill + what to edit
442
+ Templates — common work, shipped
443
+ toolroll template list | show <name>
457
444
  toolroll template apply <name> --repo <path> [--file]
458
- previews the exact filing; --file files it UNAPPROVED through the
459
- same door as a manual filing — a template carries no authority
460
-
461
- toolroll routine add <name> --repo <path> --goal <text>
462
- --schedule every:<min>|daily:<HH:MM>[@Zone]|weekly:<0-6>:<HH:MM>[@Zone] (UTC by default)
463
- [--not <text>] [--touches a,b] [--require kind:name,…] [--ceiling <usd>]
464
- [--budget-usd <n>] what each firing may spend
465
- toolroll routine approve <name> the step-up: approving means each
466
- firing builds WITHOUT asking, inside
467
- exactly the stated terms; editing any
468
- term voids the approval
469
- toolroll routine list | show <name>
470
- toolroll routine refresh <name> re-resolve the agents it freezes from
471
- today's configuration; approve again
472
- afterwards — nothing fires until then
473
- toolroll routine pause|resume <name>
474
- 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)
475
448
 
476
449
  Flows — processes cards move through; the console's rules
477
450
  toolroll flows list [--repo <path>] | show <id>
@@ -521,9 +494,10 @@ Agents — which provider and model each phase runs on
521
494
  the build it mends.
522
495
  toolroll config set <phase> --tier strong --provider <p> --model <m>
523
496
  [--repo <path>] --as <you> --token <t>
524
- the STRONG agent high-risk, strict,
525
- screenshot-proof, and automerge
526
- 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
527
501
  toolroll config set build --tier light --provider <p> --model <m>
528
502
  [--repo <path>] --as <you> --token <t>
529
503
  the fast agent small changes build
@@ -561,8 +535,8 @@ Agents — which provider and model each phase runs on
561
535
  approve the fast check Quick runs; verify show
562
536
  suggests one from the project's scripts
563
537
  Pass flags still win for one pass: --provider/--model,
564
- --plan-provider/--plan-model, --repair-model. A routine instance is
565
- 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.
566
540
  toolroll peek [<run-id>] [--tmux] watch live agents: one pane per
567
541
  open run — stage, clock, and what
568
542
  the agent is saying; digits focus,
@@ -630,8 +604,6 @@ export const TASK_ACTIONS = [
630
604
  export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
631
605
  export const CONFIG_ACTIONS = ["show", "set", "clear"];
632
606
  export const APPROVER_ACTIONS = ["list", "add"];
633
- export const ROUTINE_ACTIONS = ["list", "add", "show", "approve", "refresh", "pause", "resume", "run-now"];
634
- export const CONTEST_ACTIONS = ["show", "exclude"];
635
607
  export const PEOPLE_ACTIONS = ["list", "invite", "projects", "revoke"];
636
608
  export const KEYS_ACTIONS = ["status", "set", "clear", "verify", "auth"];
637
609
  /**
@@ -644,7 +616,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
644
616
  "allow", "selector", "paths", "credentials", "repo", "token", "capacity",
645
617
  "goal", "not", "touches", "acceptance", "candidate", "by", "digest", "as", "branch", "pool", "base", "model", "turns",
646
618
  "max", "cap", "probe", "kind", "expires", "cmd", "since", "repair-model",
647
- "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",
648
620
  "for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
649
621
  "say", "ceiling-usd", "cap-usd",
650
622
  "token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
@@ -659,8 +631,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
659
631
  "project-root", "schedule", "ceiling", "require",
660
632
  "provider", "plan-model", "plan-provider", "public-url", "editor",
661
633
  "command", "timeout-seconds", "setup-digest", "stop-grace", "title", "name", "every", "lines",
662
- "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",
663
- "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",
664
636
  "run", "containment", "agent",
665
637
  // onboard: the starter flows to switch on.
666
638
  "starter",
@@ -679,7 +651,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
679
651
  "token-name",
680
652
  ]);
681
653
  export const OPERATE_BOOLEAN_FLAGS = new Set([
682
- "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",
683
655
  "clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
684
656
  "no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
685
657
  "self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
@@ -898,7 +870,6 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
898
870
  ...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
899
871
  ...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
900
872
  ...(options.mateSeams === undefined ? {} : { mateSeams: options.mateSeams }),
901
- ...(options.heldCoordinator === undefined ? {} : { heldCoordinator: options.heldCoordinator }),
902
873
  ...(options.waitSleep === undefined ? {} : { waitSleep: options.waitSleep }),
903
874
  ...(options.releaseIo === undefined ? {} : { releaseIo: options.releaseIo }),
904
875
  ...(options.installBin === undefined ? {} : { installBin: options.installBin }),
@@ -1048,13 +1019,11 @@ function telegramCanDeliver(context, token) {
1048
1019
  }
1049
1020
  /** Ordinary paired text talks to the shared assistant: the same evidence
1050
1021
  * root the console reads results from, the same membership harness seam
1051
- * the CLI's chat uses, and this process's held-session supervisor for a
1052
- * 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. */
1053
1023
  function telegramConversation(context, options = {}) {
1054
1024
  return {
1055
1025
  evidenceRoot: context.evidenceRoot,
1056
1026
  ...(context.mateSeams?.subscriptionRunner === undefined ? {} : { subscriptionRunner: context.mateSeams.subscriptionRunner }),
1057
- ...(context.heldCoordinator === undefined ? {} : { held: context.heldCoordinator }),
1058
1027
  // Re-read on every card and every `/task`: the same console-url chat
1059
1028
  // links use, held to an https origin, and — inside `up`, where this
1060
1029
  // process also serves the console — equal to that console's own
@@ -1208,8 +1177,6 @@ async function dispatch(command, positional, flags, context) {
1208
1177
  return decideCommand(positional, flags, context);
1209
1178
  case "incident":
1210
1179
  return incidentCommand(positional, flags, context);
1211
- case "routine":
1212
- return routineCommand(positional, flags, context);
1213
1180
  case "flows":
1214
1181
  return flowsCommand(positional, flags, context);
1215
1182
  case "config":
@@ -1245,8 +1212,6 @@ async function dispatch(command, positional, flags, context) {
1245
1212
  return providersCommand(flags, context);
1246
1213
  case "template":
1247
1214
  return templateCommand(positional, flags, context);
1248
- case "contest":
1249
- return contestCommand(positional, flags, context);
1250
1215
  case "webhook":
1251
1216
  return webhookCommand(positional, flags, context);
1252
1217
  case "sync":
@@ -1465,14 +1430,6 @@ function claimCommand(positional, flags, context) {
1465
1430
  detail: result.detail,
1466
1431
  });
1467
1432
  }
1468
- if (result.reason === "attended-held") {
1469
- return fail(write, json, "claim", "attended-held", `an attended session holds this task for ${result.runner}`, EXIT.refused, {
1470
- runner: result.runner,
1471
- });
1472
- }
1473
- if (result.reason === "attended-only") {
1474
- 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);
1475
- }
1476
1433
  if (result.reason === "mode-ended") {
1477
1434
  return fail(write, json, "claim", "mode-ended", result.message, EXIT.refused);
1478
1435
  }
@@ -2065,11 +2022,9 @@ async function buildCommand(positional, flags, context) {
2065
2022
  // the same proof the tick's admission wears, refused in words here.
2066
2023
  // Every road stamps at insert (v48 integrity): a routed row's sealed
2067
2024
  // build leg, a pre-routing row's sealed profile, the bare word on a task
2068
- // with no scope — and a chain approval's base custody rides the same
2069
- // insert (the standalone road never resumes a parked chain tail; the
2070
- // tick's proven transfer does that).
2025
+ // with no scope.
2071
2026
  const standaloneModel = text(flags, "model");
2072
- 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 });
2073
2028
  if (authority !== null && !authority.ok) {
2074
2029
  await worktrees.release(leased.worktree.path, now);
2075
2030
  return fail(write, json, "build", "admission-refused", `${id}: ${authority.problem}`, EXIT.refused);
@@ -2099,7 +2054,6 @@ async function buildCommand(positional, flags, context) {
2099
2054
  worktree: leased.worktree.path,
2100
2055
  ...(authority === null ? {} : { provider: authority.stamp.provider, route: authority.stamp }),
2101
2056
  ...(standaloneModel === undefined ? {} : { model: standaloneModel }),
2102
- ...(store.approvedChainOf(id) === null ? {} : { custody: { kind: "base" } }),
2103
2057
  now,
2104
2058
  });
2105
2059
  }
@@ -2187,12 +2141,6 @@ async function buildCommand(positional, flags, context) {
2187
2141
  "Nothing has been pushed. Look at the branch before it goes anywhere.",
2188
2142
  ]);
2189
2143
  }
2190
- // ---- the unattended pass --------------------------------------------------
2191
- /** An attended authorization whose signed head the leased worktree no
2192
- * longer matches (final authority closure): thrown BEFORE admission so no
2193
- * run opens and no attempt is spent, reported as its own skip reason. */
2194
- class StaleAuthorization extends Error {
2195
- }
2196
2144
  /** How a worker's flow pass reaches outside: the tests' seams, or the real commands, network and folders. */
2197
2145
  function flowIoOf(context, pool, git, base) {
2198
2146
  const dir = dirname(context.databaseFile);
@@ -2270,7 +2218,7 @@ async function tickCommand(flags, context) {
2270
2218
  const repo = repoFrom(flags);
2271
2219
  // THE PRE-I/O MEMBERSHIP CHECK (MCP spec v6, round-4 finding 1): the
2272
2220
  // pass proves its canonical repo is in the runner's BOUND list before
2273
- // 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
2274
2222
  // authority on every runner road, not only at the claim.
2275
2223
  if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo))) {
2276
2224
  return fail(write, json, "tick", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
@@ -2308,29 +2256,9 @@ async function tickCommand(flags, context) {
2308
2256
  // in. Statuses land in the store; the gate inside the claim transaction is
2309
2257
  // what acts on them.
2310
2258
  await probeRepo(store, repo, runner, clock());
2311
- // Standing orders fire before the ready set is read, so a fresh instance
2312
- // joins THIS pass. dueRoutines only nominates; every proof — approval
2313
- // digest, pause, due, single-flight, budget — is re-made inside
2314
- // fireRoutine's own transaction, and skipped slots ledger and page
2315
- // themselves there. This loop just reports.
2316
- const routines = [];
2317
- for (const routine of buildsOnly ? [] : store.dueRoutines(repo, clock())) {
2318
- // The stop fence (audit IV-1): a signal that landed mid-pass stops
2319
- // every further admission — a routine not yet fired stays unfired.
2320
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
2321
- break;
2322
- const outcome = fireRoutine(store, routine.id, clock());
2323
- routines.push(outcome.ok
2324
- ? { routine: routine.name, outcome: "fired", taskId: outcome.taskId }
2325
- : {
2326
- routine: routine.name,
2327
- outcome: outcome.reason,
2328
- ...(outcome.detail === undefined ? {} : { detail: outcome.detail }),
2329
- });
2330
- }
2331
- // Flows move before the ready set is read, like routines: a card entering
2332
- // a build or research zone files its task now, and that task joins THIS
2333
- // 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.
2334
2262
  // Triggers first: a schedule, GitHub, Linear or another flow may start
2335
2263
  // cards, which then move in the same pass. Checking an outside service is
2336
2264
  // `gh` or one HTTPS request, never a model, and only when it is due.
@@ -2372,7 +2300,7 @@ async function tickCommand(flags, context) {
2372
2300
  // other builds may hold the worker for minutes.
2373
2301
  let settled = 0;
2374
2302
  const settleFinished = () => {
2375
- 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);
2376
2304
  settled = dispatched.length;
2377
2305
  if (finished.length === 0)
2378
2306
  return;
@@ -2381,24 +2309,10 @@ async function tickCommand(flags, context) {
2381
2309
  flowPass.filed.push(...after.filed);
2382
2310
  flowPass.problems.push(...after.problems);
2383
2311
  };
2384
- // Tournament housekeeping before the ordinary pass (stage 4): interrupted
2385
- // races recover by CAS, and an ANSWERED question re-admits its parked
2386
- // agent — fresh claim, fresh slot, remaining budget only, the SAME
2387
- // verified checkout on the SAME runner (finding 29's custody rule).
2388
- if (!buildsOnly)
2389
- recoverContests(store, clock());
2390
2312
  // Expired ceremony nonces are litter with a bound (round-3 finding 30):
2391
2313
  // the mint refuses past 50 open per approver, so the sweep keeps the
2392
2314
  // ceiling meaningful rather than letting dead rows consume it.
2393
2315
  store.sweepCeremonyNonces(clock());
2394
- // Decided tournaments give their checkouts back (stage 6) — this runner's
2395
- // custody only; a checkout that will not release cleanly is flagged and
2396
- // paged, never force-cleaned. Undecided ones escalate once at 14 days.
2397
- if (!buildsOnly) {
2398
- await sweepContestCleanup(store, path => worktrees.release(path, clock()), runner, clock());
2399
- escalateOverdueContests(store, clock());
2400
- }
2401
- const resumed = [];
2402
2316
  // v105: monthly budgets, read when first needed and again after any build (its spend counts): a used-up hard-stop
2403
2317
  // budget holds back new work billed to an API key, on every road below.
2404
2318
  let budgetGate = null;
@@ -2406,13 +2320,10 @@ async function tickCommand(flags, context) {
2406
2320
  const budgetWords = (hold) => budgetHoldWords(hold, monthOf(clock()).name);
2407
2321
  // Sprint 8: the organisation policy, asked before any claim on every road below: a provider or model it doesn't
2408
2322
  // allow never starts (the task waits, saying which rule and where to change it); terms above its permission ceiling
2409
- // 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).
2410
2324
  const policyHold = (agents) => {
2411
2325
  for (const agent of agents) {
2412
2326
  if ("profile" in agent) {
2413
- const refused = agent.attended === true ? store.attendedPolicyRefusal(agent.profile) : null;
2414
- if (refused !== null)
2415
- return refused;
2416
2327
  const verdict = store.runPolicy(agent.profile);
2417
2328
  if (!verdict.ok)
2418
2329
  return verdict.message;
@@ -2426,297 +2337,12 @@ async function tickCommand(flags, context) {
2426
2337
  }
2427
2338
  return null;
2428
2339
  };
2429
- for (const waiting of buildsOnly ? [] : store.contestsInStates(["decision-wait"])) {
2430
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
2431
- break;
2432
- // D1 belt-and-braces (external dispatch, finding 41): a mirror and a
2433
- // contest should never coexist; if one ever does, its race resumes
2434
- // NOTHING — no claim, no run, no worktree, no spend.
2435
- const waitingTaskId = store.externalIdFor(waiting.taskRef);
2436
- if (waitingTaskId !== null && store.mirrorByTask(waitingTaskId) !== null) {
2437
- resumed.push({ id: waitingTaskId, outcome: "skipped", reason: "external-race" });
2438
- continue;
2439
- }
2440
- // THE ANSWERED BATCH IS MARKED ACTIVE FIRST (Codex slice-B finding 2):
2441
- // resuming lanes one at a time let the FIRST finisher aggregate the
2442
- // contest while later answered lanes were still 'parked' — excluded
2443
- // from active, their decisions no longer open — stranding them in a
2444
- // contest that had already moved on. 'ready' counts as active, so
2445
- // aggregation waits for the whole batch. Any lane that bails before
2446
- // its build reverts to 'parked' so the next pass retries it.
2447
- const batch = [];
2448
- for (const racer of store.contestants(waiting.id).filter(one => one.state === "parked")) {
2449
- if (store.answeredDecisionForContestant(racer.id) === null)
2450
- continue;
2451
- if (store.casContestantState(racer.id, ["parked"], "ready", racer.generation)) {
2452
- batch.push({ racer });
2453
- }
2454
- }
2455
- for (const { racer } of batch) {
2456
- const backToParked = () => {
2457
- const current = store.getContestant(racer.id);
2458
- if (current !== null)
2459
- store.casContestantState(racer.id, ["ready"], "parked", current.generation);
2460
- };
2461
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true) {
2462
- backToParked();
2463
- continue;
2464
- }
2465
- const custody = racer.custody === null ? null : JSON.parse(racer.custody);
2466
- const taskId = store.externalIdFor(waiting.taskRef);
2467
- if (custody === null || custody.runner !== runner || taskId === null) {
2468
- backToParked();
2469
- continue;
2470
- }
2471
- const lanePolicy = policyHold([racer.profile == null ? { provider: racer.provider, model: racer.model } : { profile: racer.profile }]);
2472
- if (lanePolicy !== null) {
2473
- backToParked();
2474
- resumed.push({ id: taskId, outcome: "skipped", reason: "policy", detail: lanePolicy });
2475
- continue;
2476
- }
2477
- const laneBudget = budgetHold(waiting.taskRef, store.agentsFor([racer.provider]));
2478
- if (laneBudget.over !== null) {
2479
- backToParked();
2480
- resumed.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(laneBudget) });
2481
- continue;
2482
- }
2483
- // The sign-in pause holds a resumed lane too: it waits, parked, instead of failing.
2484
- const laneGate = signInGate(store, [racer.provider], clock());
2485
- if (laneGate.waiting !== null) {
2486
- backToParked();
2487
- resumed.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(laneGate.waiting) });
2488
- continue;
2489
- }
2490
- // Legacy comparison lanes carried an absolute clock. New profiles use
2491
- // a progress watchdog and may remain alive indefinitely while useful
2492
- // work is observable; only legacy approvals retain their cumulative
2493
- // wall-clock contract.
2494
- const remaining = waiting.kind === "comparison" ? null : racer.budgetMicrousd - racer.accountedMicrousd;
2495
- if (remaining !== null && remaining <= 0) {
2496
- laneGate.giveBack();
2497
- const current = store.getContestant(racer.id);
2498
- if (current !== null)
2499
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2500
- contestMaybeAggregate(store, waiting.id, clock());
2501
- resumed.push({ id: taskId, outcome: "skipped", reason: "over-ceiling" });
2502
- continue;
2503
- }
2504
- if (waiting.kind === "comparison") {
2505
- const laneProfile = racer.profile ?? contestantProfileOf(racer.provider, racer.model, racer.repairModel);
2506
- const clockCapMs = 3 * laneProfile.timeoutSeconds * 1000;
2507
- if (laneProfile.timeoutKind !== "idle" && store.contestantCumulativeMs(racer.id) >= clockCapMs) {
2508
- laneGate.giveBack();
2509
- const current = store.getContestant(racer.id);
2510
- if (current !== null)
2511
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2512
- contestMaybeAggregate(store, waiting.id, clock());
2513
- resumed.push({ id: taskId, outcome: "skipped", reason: "over-ceiling" });
2514
- continue;
2515
- }
2516
- }
2517
- const reclaimed = acquire(store, waiting.taskRef, runner, { now: clock(), token, ttlMs: leaseTtlMs });
2518
- if (!reclaimed.ok) {
2519
- laneGate.giveBack();
2520
- backToParked();
2521
- continue;
2522
- }
2523
- const freshContest = store.getContest(waiting.id);
2524
- if (freshContest === null || !store.casContestState(waiting.id, ["decision-wait", "racing"], "racing", freshContest.generation)) {
2525
- release(store, reclaimed.claim.leaseId, clock());
2526
- laneGate.giveBack();
2527
- backToParked();
2528
- continue;
2529
- }
2530
- store.stampContestLease(waiting.id, reclaimed.claim.leaseId, runner, text(flags, "incarnation") ?? null);
2531
- const leased = await worktrees.lease({ repo, branch: racer.branch, runner, taskRef: waiting.taskRef, now: clock() });
2532
- const headCheck = leased.ok ? await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path }) : null;
2533
- if (!leased.ok || (custody.head !== null && headCheck !== null && headCheck.stdout.trim() !== custody.head)) {
2534
- // The tree cannot be proved to be the one the agent left — stop the
2535
- // agent rather than cold-starting against a different history.
2536
- if (leased.ok)
2537
- await worktrees.release(leased.worktree.path, clock());
2538
- const current = store.getContestant(racer.id);
2539
- if (current !== null)
2540
- store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
2541
- store.setContestantCleanup(racer.id, "attention");
2542
- contestMaybeAggregate(store, waiting.id, clock());
2543
- release(store, reclaimed.claim.leaseId, clock());
2544
- resumed.push({ id: taskId, outcome: "failed", reason: "contest-custody" });
2545
- continue;
2546
- }
2547
- const [resumeSlot] = store.reserveExecutionSlots(runner, 1, clock());
2548
- const parkedRun = racer.activeRun;
2549
- // A contest lane spends under its race-approved profile and says so
2550
- // at insert (v48 integrity): the lane's proven profile digest, the
2551
- // exact pair it will spend as — the store's own answer, re-proved
2552
- // and bound to the lane inside the admission.
2553
- const laneStamp = store.laneAuthorityFor(racer.id);
2554
- if (laneStamp === null) {
2555
- await worktrees.release(leased.worktree.path, clock());
2556
- release(store, reclaimed.claim.leaseId, clock());
2557
- backToParked();
2558
- resumed.push({ id: taskId, outcome: "failed", reason: "admission-refused", detail: `contestant ${racer.id} is gone` });
2559
- continue;
2560
- }
2561
- // THE LANE ADMISSION (atomic authority closure): the resume opens on
2562
- // the lane under the contest's live custody — this lease, this
2563
- // runner, this watch incarnation, stamped on the contest just above
2564
- // — presenting the lane's stored sealed profile; value-shaped.
2565
- const admittedResume = store.admitContestLane({
2566
- taskRef: waiting.taskRef,
2567
- leaseId: reclaimed.claim.leaseId,
2568
- runner,
2569
- incarnation: text(flags, "incarnation") ?? null,
2570
- branch: racer.branch,
2571
- worktree: leased.worktree.path,
2572
- provider: racer.provider,
2573
- model: racer.model,
2574
- contestant: racer.id,
2575
- ...(parkedRun === null ? {} : { parentRun: parkedRun }),
2576
- now: clock(),
2577
- route: laneStamp,
2578
- });
2579
- if (!admittedResume.ok) {
2580
- await worktrees.release(leased.worktree.path, clock());
2581
- release(store, reclaimed.claim.leaseId, clock());
2582
- backToParked();
2583
- resumed.push({ id: taskId, outcome: "failed", reason: "admission-refused", detail: admittedResume.problem });
2584
- continue;
2585
- }
2586
- const resumeRun = admittedResume.runId;
2587
- // The lane's pointer moved to the resume INSIDE its admission (raw
2588
- // authority repair): from the parked attempt it continues, proved
2589
- // there — no release-then-claim window exists any more.
2590
- const afterClaim = store.getContestant(racer.id);
2591
- if (afterClaim !== null)
2592
- store.casContestantState(racer.id, ["ready"], "building", afterClaim.generation);
2593
- budgetGate = null; // this build's spend counts toward the next check
2594
- const resumeResult = await build(store, {
2595
- taskId,
2596
- taskRef: waiting.taskRef,
2597
- runner,
2598
- leaseId: reclaimed.claim.leaseId,
2599
- runnerToken: token,
2600
- runId: resumeRun,
2601
- evidenceRoot: context.evidenceRoot,
2602
- worktree: leased.worktree.path,
2603
- branch: racer.branch,
2604
- now: clock(),
2605
- clock,
2606
- provider: racer.provider,
2607
- // v24: the contestant's OWN sealed profile is the authority — the
2608
- // proof holds the lane to it (model, limits, permissions), so no
2609
- // flag-shaped overrides ride along.
2610
- contestProfile: racer.profile ?? contestantProfileOf(racer.provider, racer.model, racer.repairModel),
2611
- ...(remaining === null ? {} : { maxBudgetUsd: Math.min(remaining, laneBudget.remainingMicrousd ?? Infinity) / 1_000_000 }),
2612
- onProviderSpawn: (pid) => {
2613
- worktrees.recordProviderOccupancy(leased.worktree.path, runner, pid, leased.worktree.leaseEpoch);
2614
- if (resumeSlot !== undefined) {
2615
- const facts = { run: resumeRun, contestant: racer.id, incarnation: text(flags, "incarnation") ?? null, processGroup: pid };
2616
- if (!store.markSlotRunning(resumeSlot, facts, clock()) && !store.refreshSlotProcess(resumeSlot, facts))
2617
- throw new Error("the provider's execution slot no longer belongs to this attempt");
2618
- }
2619
- },
2620
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
2621
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
2622
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
2623
- });
2624
- const resumedRunRow = store.getRun(resumeRun);
2625
- const resumeMeasured = resumedRunRow === null || resumedRunRow.providerStartedAt === null
2626
- ? 0
2627
- : resumedRunRow.costUsd !== null
2628
- ? Math.round(resumedRunRow.costUsd * 1_000_000)
2629
- : null;
2630
- let resumedOutcome = "failed";
2631
- let resumedCommitted = false;
2632
- if (resumeResult.ok && resumeResult.parked !== undefined) {
2633
- resumedOutcome = "parked";
2634
- const asked = resumeResult.parked.decision;
2635
- const racerDecision = store.saveDecision({
2636
- run: resumeRun,
2637
- contestant: racer.id,
2638
- urgency: asked.urgency,
2639
- recap: asked.recap,
2640
- question: asked.question,
2641
- options: asked.options,
2642
- recommendation: asked.recommendation,
2643
- ...(asked.assignee === null ? {} : { assignee: asked.assignee }),
2644
- ...(asked.deadline === null ? {} : { deadline: asked.deadline }),
2645
- }, clock());
2646
- // A racing agent's question pages like any other (arc 3 finding 21):
2647
- // aggregation stays quiet ASSUMING this row already spoke.
2648
- store.enqueueNotification({
2649
- source: { run: resumeRun },
2650
- dedupeKey: `decision:${racerDecision}`,
2651
- kind: "decision",
2652
- subject: `${taskId} parked a decision (${contestNoun(waiting.kind)} agent)`,
2653
- body: `\`toolroll decide ${racerDecision}\``,
2654
- pushClass: "decision",
2655
- link: `/d/${racerDecision}`,
2656
- }, clock());
2657
- }
2658
- else if (resumeResult.ok) {
2659
- resumedOutcome = "built";
2660
- resumedCommitted = resumeResult.committed;
2661
- }
2662
- else if (resumeResult.reason === "stopped") {
2663
- resumedOutcome = "stopped";
2664
- }
2665
- if (resumedRunRow !== null && resumedRunRow.outcome === null) {
2666
- // The reason rides the resumed run too (Codex slice-B finding 7):
2667
- // both settlement paths, one honesty.
2668
- const resumeReason = !resumeResult.ok ? resumeResult.reason : undefined;
2669
- store.finishRun(resumeRun, {
2670
- outcome: resumedOutcome === "built" && !resumedCommitted ? "no-change" : resumedOutcome === "stopped" ? "failed" : resumedOutcome === "parked" ? "parked" : resumedOutcome,
2671
- committed: resumedCommitted,
2672
- ...(resumeReason === undefined ? {} : { reason: resumeReason }),
2673
- now: clock(),
2674
- });
2675
- }
2676
- if (resumedOutcome === "parked") {
2677
- // Custody refreshes on EVERY park (Codex slice-B finding 9): a
2678
- // re-parked lane whose custody still named the pre-resume head
2679
- // would falsely stop as contest-custody on its next answer.
2680
- const headNow = await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
2681
- const dirtyNow = await git("git", ["status", "--porcelain"], { cwd: leased.worktree.path });
2682
- store.setContestantCustody(racer.id, JSON.stringify({
2683
- branch: racer.branch,
2684
- head: headNow.code === 0 ? headNow.stdout.trim() : null,
2685
- runner,
2686
- dirty: dirtyNow.stdout.trim() !== "",
2687
- at: clock().toISOString(),
2688
- }));
2689
- }
2690
- await worktrees.release(leased.worktree.path, clock());
2691
- const resumedFinal = finalizeContestant(store, {
2692
- contestId: waiting.id,
2693
- contestantId: racer.id,
2694
- runId: resumeRun,
2695
- outcome: resumedOutcome,
2696
- measuredMicrousd: resumeMeasured,
2697
- slotId: resumeSlot ?? null,
2698
- }, clock());
2699
- resumed.push({ id: taskId, outcome: "contest", reason: resumedFinal.aggregated ?? "racing" });
2700
- }
2701
- }
2702
- // THE CHAIN RECONCILER (E3d, review finding 3): a crash between a run's
2703
- // disposition and its cycle resolution leaves an OPEN cycle with a
2704
- // concluded tail — resolve each through the SAME resolver the disposition
2705
- // uses, before any admission can re-tag or race it. Advancing here lands
2706
- // the cycle in pending-admission for THIS pass's chain admission below.
2707
- // One shared piece with the fault tests (F+G review, finding 5).
2708
- if (!buildsOnly)
2709
- store.reconcileStrandedChains(repo, clock());
2710
2340
  // The DISPATCH view of the queue (queue columns, v19): this runner's own
2711
2341
  // reserved work first, then the shared queue; work reserved for other
2712
2342
  // workers is absent. The claim primitive re-proves the reservation.
2713
2343
  const ready = store.listReady(clock(), runner);
2714
2344
  const considered = ready.length;
2715
- const dispatched = [...resumed];
2716
- // Expired attended authorizations close durably each pass (round-6
2717
- // finding 8): the partial unique frees, and the claim gates stop
2718
- // honoring corpses.
2719
- store.sweepExpiredAuthorizations(clock());
2345
+ const dispatched = [];
2720
2346
  // The stale-scan (dispatch v3, finding 20): mirrors the courtesy filter
2721
2347
  // kept out of ready are REPORTED here, typed, with a paged episode —
2722
2348
  // an undispatakable tracker item is a 9am fact, not a silent absence.
@@ -2748,15 +2374,9 @@ async function tickCommand(flags, context) {
2748
2374
  settleFinished();
2749
2375
  untakenTrial?.giveBack();
2750
2376
  untakenTrial = null;
2751
- // The build budget governs UNATTENDED admissions (round-1 finding 4):
2752
- // once it is spent, the pass keeps SCANNING for attended
2753
- // authorizations — operator-invoked sessions launch regardless —
2754
- // while declining every further ordinary admission.
2755
- if (built >= max) {
2756
- const openAuth = store.openAuthorizationFor(ref.id);
2757
- if (openAuth === null || openAuth.runner !== runner || openAuth.attemptRun !== null)
2758
- continue;
2759
- }
2377
+ // The build budget: once it is spent, no further admission this pass.
2378
+ if (built >= max)
2379
+ continue;
2760
2380
  // The stop fence (audit IV-1): checked before every claim. The build
2761
2381
  // already in flight finishes under its own bounds; nothing NEW is
2762
2382
  // admitted once the operator has said stop.
@@ -2768,91 +2388,17 @@ async function tickCommand(flags, context) {
2768
2388
  dispatched.push({ id, outcome: "skipped", reason: "other-repo" });
2769
2389
  continue;
2770
2390
  }
2771
- // EVERY live fallback cycle defers the ordinary road (Codex E3d review,
2772
- // finding 5) — a pending admission belongs to the chain pass below, and
2773
- // an open cycle's custody moves only through proven roads, never an
2774
- // in-passing re-tag. The ONE exception: an open cycle whose tail PARKED
2775
- // — the paused lineage — proceeds, and the new run takes custody through
2776
- // the proven parked-resume transfer after it is created.
2777
- const liveCycle = store.fallbackCycleFor(ref.id);
2778
- const parkedChainTail = liveCycle !== null && liveCycle.state === "open" && liveCycle.tailRun !== null
2779
- ? store.getRun(liveCycle.tailRun)
2780
- : null;
2781
- if (liveCycle !== null && !(parkedChainTail !== null && parkedChainTail.outcome === "parked")) {
2782
- dispatched.push({ id, outcome: "skipped", reason: "fallback-active" });
2783
- continue;
2784
- }
2785
2391
  // A plan the operator asked for dispatches a PLANNER — the one
2786
2392
  // legitimate spend on a task with no approved scope. Everything else
2787
- // unapproved is a person's pending decision: skip, not refuse — EXCEPT
2788
- // the attended road (Phase 2, v6 W1): a live attended authorization
2789
- // naming THIS runner is authority for one watched attempt, and the
2790
- // skip for everything short of that is its own typed word, never the
2791
- // generic `unapproved` the round-5 review caught masking it.
2393
+ // unapproved is a person's pending decision: skip, not refuse.
2792
2394
  const scopeApproved = scopeApprovedForDispatch(store, ref.id, clock());
2793
2395
  const wantsPlan = ref.plan === "requested" && !scopeApproved;
2794
2396
  // A report task with an approved scope dispatches a SCOUT (mate arc
2795
2397
  // §10): the same approval, the planner's read-only road, a report back.
2796
2398
  const wantsScout = ref.deliverable === "report" && scopeApproved;
2797
- let attendedDispatch = null;
2798
2399
  if (!wantsPlan && !scopeApproved) {
2799
- const open = store.openAuthorizationFor(ref.id);
2800
- const watching = open === null
2801
- ? null
2802
- : attendedLivenessState(open.lastBeatAt === null ? null : Date.parse(open.lastBeatAt), clock().getTime(), Date.parse(open.absoluteExpiry));
2803
- if (open !== null &&
2804
- open.runner === runner &&
2805
- (watching === "live" || watching === "grace") &&
2806
- open.attemptRun === null &&
2807
- context.heldCoordinator !== undefined) {
2808
- // v28: sessions are unbounded by default; an operator-set cap
2809
- // skips FURTHER launches in words. The gauge is durable custody
2810
- // rows, never the in-process map — a restarted up with orphans
2811
- // pending must count them.
2812
- if (context.maxHeldSessions !== undefined && store.openHeldSessionCount(runner) >= context.maxHeldSessions) {
2813
- dispatched.push({
2814
- id,
2815
- outcome: "skipped",
2816
- reason: "session-cap",
2817
- detail: `this machine holds ${store.openHeldSessionCount(runner)} of ${context.maxHeldSessions} attended sessions — end one, or raise --max-held-sessions`,
2818
- });
2819
- continue;
2820
- }
2821
- attendedDispatch = open;
2822
- }
2823
- else if (open !== null) {
2824
- dispatched.push({ id, outcome: "skipped", reason: "attended-only" });
2825
- continue;
2826
- }
2827
- else {
2828
- dispatched.push({ id, outcome: "skipped", reason: "unapproved" });
2829
- continue;
2830
- }
2831
- }
2832
- // TOURNAMENT TERMS FIRST (foundations finding 8's reorder): an approved
2833
- // race's admission is governed by its own fingerprinted terms, and pass
2834
- // flags must not be able to shape it — so the terms are discovered
2835
- // before any flag-shaped resolution runs, and a raced task's build
2836
- // resolution ignores the flags outright.
2837
- const racedAhead = wantsPlan || wantsScout || attendedDispatch !== null ? null : store.activeTournamentTerms(ref.id);
2838
- // The attended spec comes from the authorization's PINNED terms — the
2839
- // courtesy half of the proof; the coordinator's transaction re-proves
2840
- // byte-for-byte at the actual HEAD (v6 W1).
2841
- let attendedSpec = null;
2842
- if (attendedDispatch !== null) {
2843
- try {
2844
- const terms = JSON.parse(attendedDispatch.termsJson);
2845
- const pinned = profileFromJson(typeof terms.profileJson === "string" ? terms.profileJson : null);
2846
- if (pinned !== null)
2847
- attendedSpec = { provider: pinned.provider, model: pinned.model, digest: profileDigestOf(pinned), profile: pinned };
2848
- }
2849
- catch {
2850
- attendedSpec = null;
2851
- }
2852
- if (attendedSpec === null) {
2853
- dispatched.push({ id, outcome: "skipped", reason: "attended-only", detail: "the authorization's pinned profile cannot be read" });
2854
- continue;
2855
- }
2400
+ dispatched.push({ id, outcome: "skipped", reason: "unapproved" });
2401
+ continue;
2856
2402
  }
2857
2403
  // The phase agent, resolved BEFORE anything is claimed and snapshotted
2858
2404
  // into the run: pin > flags > project > installation > default. Planner
@@ -2860,14 +2406,14 @@ async function tickCommand(flags, context) {
2860
2406
  // when no plan-specific flag is given.
2861
2407
  // THE ROUTE (v47): the sealed route when the approval stands, else the
2862
2408
  // working proposed route, else — with no scope yet — a live
2863
- // 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
2864
2410
  // proven to predate routing is the LEGACY road (its sealed profile,
2865
2411
  // then flags and configuration); a routed row whose route cannot be
2866
2412
  // read FAILS CLOSED here, in words. Dispatch resolves each phase from
2867
2413
  // ITS leg: the plan leg for a planner (pass flags may restate it, never
2868
2414
  // contradict it), the SEALED build leg for every routed approval
2869
2415
  // (mutable configuration cannot reroute an approved build).
2870
- const taskRoute = attendedSpec !== null || racedAhead !== null ? null : routeOfTask(store, id, ref, clock());
2416
+ const taskRoute = routeOfTask(store, id, ref, clock());
2871
2417
  if (taskRoute !== null && taskRoute.kind === "unreadable") {
2872
2418
  dispatched.push({ id, outcome: "skipped", reason: "agent-config", detail: taskRoute.problem });
2873
2419
  continue;
@@ -2877,9 +2423,8 @@ async function tickCommand(flags, context) {
2877
2423
  // authority closure): a planner on a filed, unapproved scope runs
2878
2424
  // under the WORKING route only as the whole scope proves — exact raw
2879
2425
  // terms (a proposed-via marker this code never writes), a resolved
2880
- // profile, a whole fallback chain (an unresolved `[]` is none), route
2881
- // parity with the signed risk, the digest, and the live auth mode
2882
- // 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
2883
2428
  // task in words before any claim; the admission and the spawn ask the
2884
2429
  // same question again.
2885
2430
  if (wantsPlan && route !== null && route.source === "proposed") {
@@ -2891,21 +2436,6 @@ async function tickCommand(flags, context) {
2891
2436
  }
2892
2437
  const planLeg = route === null ? null : legOf(route.route, "plan");
2893
2438
  const sealedBuildLeg = route !== null && route.source === "approved" ? legOf(route.route, "build") : null;
2894
- // A parked chain tail PAST the base (v48 authority repair): its successor resumes the
2895
- // entry's custody and spends as that entry — the exact approved pair
2896
- // under `fallback` provenance — never as the sealed build leg. An
2897
- // entry the approved chain no longer carries under the tail's digest
2898
- // is refused here, in words, before any claim moves.
2899
- let parkedEntry = null;
2900
- if (parkedChainTail !== null && parkedChainTail.chainIndex != null && parkedChainTail.chainIndex > 0 && !wantsPlan && !wantsScout && attendedSpec === null && racedAhead === null) {
2901
- const chain = store.approvedChainOf(id);
2902
- const entry = chain === null ? undefined : chain[parkedChainTail.chainIndex];
2903
- if (entry === undefined || entryDigestOf(entry) !== parkedChainTail.entryDigest) {
2904
- 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` });
2905
- continue;
2906
- }
2907
- parkedEntry = { index: parkedChainTail.chainIndex, provider: entry.profile.provider, model: entry.profile.model };
2908
- }
2909
2439
  const planFlagged = planProvider !== undefined || planModel !== undefined || providerFlag !== undefined || model !== undefined;
2910
2440
  if (wantsPlan && planLeg !== null && planFlagged) {
2911
2441
  // Pass flags cannot contradict the task's plan leg: a flag that names
@@ -2923,48 +2453,40 @@ async function tickCommand(flags, context) {
2923
2453
  continue;
2924
2454
  }
2925
2455
  }
2926
- const resolution = attendedSpec !== null
2927
- ? null
2928
- : wantsPlan
2929
- ? planLeg !== null
2930
- ? resolvePhaseAgent(store, "plan", repo, { provider: planLeg.provider, model: planLeg.model })
2931
- : resolvePhaseAgent(store, "plan", repo, {
2932
- // The legacy road (P2/C7 precedence): the task's plan PIN
2933
- // beats every flag and config row; flags beat config.
2934
- provider: ref.planProvider ?? planProvider ?? providerFlag,
2935
- model: ref.planProvider !== null ? (ref.planModel ?? undefined) : (planModel ?? model),
2936
- })
2937
- : racedAhead !== null
2938
- ? resolvePhaseAgent(store, "build", repo, {}, ref)
2939
- : parkedEntry !== null
2940
- ? resolvePhaseAgent(store, "build", repo, { provider: parkedEntry.provider, model: parkedEntry.model })
2941
- : sealedBuildLeg !== null
2942
- ? resolvePhaseAgent(store, "build", repo, { provider: sealedBuildLeg.provider, model: sealedBuildLeg.model }, ref)
2943
- : resolvePhaseAgent(store, "build", repo, { provider: providerFlag, model }, ref);
2944
- 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) {
2945
2469
  dispatched.push({ id, outcome: "skipped", reason: "agent-config", detail: resolution.problem });
2946
2470
  continue;
2947
2471
  }
2948
- const spec = resolution === null ? attendedSpec : resolution.spec;
2472
+ const spec = resolution.spec;
2949
2473
  // THE AUTH MODE, strictly, before any claim or row (atomic authority
2950
2474
  // closure): a present mode file for the provider this pass would spend
2951
2475
  // as that says neither word is a stated problem — the same reader the
2952
2476
  // filing, the seal, and the spawn use — and the task is skipped in
2953
2477
  // words with nothing opened, never dispatched to be refused later.
2954
- const modeProviders = racedAhead !== null ? racedAhead.agents.filter(agent => isProviderId(agent.provider)).map(agent => agent.provider) : [spec.provider];
2955
- const brokenMode = [...new Set(modeProviders)].map(one => readAuthModeStrict(one)).find(one => !one.ok);
2956
- if (brokenMode !== undefined && !brokenMode.ok) {
2478
+ const brokenMode = readAuthModeStrict(spec.provider);
2479
+ if (!brokenMode.ok) {
2957
2480
  dispatched.push({ id, outcome: "skipped", reason: "auth-mode", detail: brokenMode.problem });
2958
2481
  continue;
2959
2482
  }
2960
2483
  // THE SIGN-IN PAUSE: a provider whose sign-in stopped working takes no
2961
- // new work — no claim, no run, never a substitute (a lapsed login must not
2962
- // 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
2963
2485
  // person resumes it. Other providers keep working.
2964
2486
  // One task every AUTH_TRIAL_MS goes ahead as the trial: a sign-in check
2965
2487
  // can say "logged in" for a session that cannot refresh, so a real run
2966
2488
  // decides — it lifts the pause, or fails into the same incident.
2967
- const gate = signInGate(store, modeProviders, clock(), ref.id);
2489
+ const gate = signInGate(store, [spec.provider], clock(), ref.id);
2968
2490
  if (gate.waiting !== null) {
2969
2491
  dispatched.push({ id, outcome: "skipped", reason: "signed-out", detail: signInWords(gate.waiting) });
2970
2492
  continue;
@@ -2972,38 +2494,27 @@ async function tickCommand(flags, context) {
2972
2494
  // A trial whose task is skipped before its claim succeeds is given back
2973
2495
  // at the next task (or after the pass), so the next pass may take it.
2974
2496
  untakenTrial = gate;
2975
- // The leg is the authority: what resolved must BE the leg, exactly —
2976
- // the sealed build leg, or the parked fallback entry's own pair.
2977
- 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;
2978
2499
  if (governingLeg !== null && (spec.provider !== governingLeg.provider || spec.model !== governingLeg.model)) {
2979
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` });
2980
2501
  continue;
2981
2502
  }
2982
2503
  // Route provenance for the run this pass opens (v47): PRESENTED to the
2983
2504
  // admission transaction, from the leg that governs it — the store
2984
- // dictates nothing (v48 authority repair). A parked fallback entry's successor presents
2985
- // `fallback` under the sealed route — there is no chain-only digest
2986
- // (raw authority repair): with no sealed route it presents nothing,
2987
- // and the admission refuses in words.
2988
- // Every road presents at insert (v48 integrity): an attended session
2989
- // its pinned profile; a pre-routing row its sealed profile (or the
2990
- // 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
2991
2508
  // admission proves exactly what the row holds.
2992
2509
  const routeStamp = (phase) => {
2993
- if (attendedSpec !== null && phase === "build") {
2994
- return { routeDigest: `profile:${attendedSpec.digest}`, phase, provider: spec.provider, model: spec.model, chosen: "legacy" };
2995
- }
2996
2510
  // A task with NO scope holds no filed route (final authority
2997
2511
  // closure): its planner presents the bare word `legacy` for the pair
2998
2512
  // the live recommendation resolved — the recommendation chose the
2999
2513
  // agent, but nothing anybody filed is the authority it spends under.
3000
2514
  if (governingLeg === null || route === null || (phase === "plan" && route.source === "live")) {
3001
- 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 });
3002
2516
  return legacy !== null && legacy.ok ? legacy.stamp : null;
3003
2517
  }
3004
- if (parkedEntry !== null && phase === "build") {
3005
- return { routeDigest: routeDigestOf(route.route), phase, provider: spec.provider, model: spec.model, chosen: "fallback" };
3006
- }
3007
2518
  return { routeDigest: routeDigestOf(route.route), phase, provider: spec.provider, model: spec.model, chosen: governingLeg.chosen };
3008
2519
  };
3009
2520
  // THE LEGACY STALE CHECK, before any claim or row (v48 integrity): a
@@ -3012,7 +2523,7 @@ async function tickCommand(flags, context) {
3012
2523
  // must BE that pair. A disagreement used to open a row and refuse it
3013
2524
  // at the dispatch proof; now nothing opens — the task is held under
3014
2525
  // the same backoff the approval door lifts, and paged once.
3015
- if (!wantsPlan && !wantsScout && attendedSpec === null && racedAhead === null && route === null) {
2526
+ if (!wantsPlan && !wantsScout && route === null) {
3016
2527
  const legacyStamp = routeStamp("build");
3017
2528
  if (legacyStamp !== null && (legacyStamp.provider !== spec.provider || legacyStamp.model !== spec.model)) {
3018
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)`;
@@ -3024,30 +2535,16 @@ async function tickCommand(flags, context) {
3024
2535
  // THE READINESS HALT (v47): a provider THIS runner has reported
3025
2536
  // unavailable never claims and never spends — no substitution, no
3026
2537
  // second-best; the skip names the observation and the ways out.
3027
- // Unknown readiness passes (every existing gate still applies); a
3028
- // raced task halts if any lane's provider is unavailable, because a
3029
- // subset is a different contest than the one signed. The ONE road
3030
- // onward is an EXPLICITLY APPROVED fallback chain: when the sealed
3031
- // build leg's provider is unavailable, the task moves to the approved
3032
- // next entry — that one and no other — and the chain admission pass
3033
- // below runs it after re-proving everything; otherwise it fails closed.
2538
+ // Unknown readiness passes (every existing gate still applies).
3034
2539
  {
3035
- const providersToProve = racedAhead !== null ? racedAhead.agents.map(agent => agent.provider) : [spec.provider];
3036
- const unavailable = providersToProve
3037
- .map(candidate => store.runnerReadinessOf(runner, candidate))
3038
- .find(seen => seen !== null && seen.state === "unavailable");
3039
- if (unavailable !== undefined && unavailable !== null) {
2540
+ const unavailable = store.runnerReadinessOf(runner, spec.provider);
2541
+ if (unavailable !== null && unavailable.state === "unavailable") {
3040
2542
  const observed = `${unavailable.provider} is reported unavailable on ${runner} (${unavailable.reason}; observed ${unavailable.observedAt})`;
3041
- const onward = !wantsPlan && !wantsScout && racedAhead === null && attendedSpec === null && sealedBuildLeg !== null
3042
- ? store.skipUnavailablePrimary(ref.id, id, repo, runner, clock())
3043
- : null;
3044
2543
  dispatched.push({
3045
2544
  id,
3046
2545
  outcome: "skipped",
3047
2546
  reason: "provider-unavailable",
3048
- detail: onward !== null && onward.ok
3049
- ? `${observed} — moving to the approved fallback ${onward.next.provider} · ${onward.next.model}, the only substitution the approval allows; it is admitted next`
3050
- : `${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`,
3051
2548
  });
3052
2549
  continue;
3053
2550
  }
@@ -3057,23 +2554,14 @@ async function tickCommand(flags, context) {
3057
2554
  // worktree, no wake churn. The provider source is the AUTHORITATIVE
3058
2555
  // one per road: the sealed approval snapshot for ordinary builds (the
3059
2556
  // same snapshot the dispatch proof enforces), the plan resolver for
3060
- // planner runs. Attended work is claude-only and excluded; tournament
3061
- // contestants cannot be tier-2 today (the money gate refuses them at
3062
- // 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
3063
2559
  // existing approval refusals own that road, and attestation must
3064
2560
  // never mask them. The gateway re-checks before spawn; this skip only
3065
2561
  // keeps the normal road cheap.
3066
- const skipProviders = attendedSpec !== null
3067
- ? []
3068
- : racedAhead !== null
3069
- ? // The contest road (slice B): every lane's provider, so an
3070
- // out-of-range attested lane skips the WHOLE contest before
3071
- // any claim — running a subset is a different contest than
3072
- // the one the operator signed.
3073
- racedAhead.agents.filter(agent => isProviderId(agent.provider)).map(agent => agent.provider)
3074
- : wantsPlan
3075
- ? [spec.provider]
3076
- : [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);
3077
2565
  let unattestedLane = null;
3078
2566
  for (const candidate of new Set(skipProviders)) {
3079
2567
  if (attestationOf(candidate) === null)
@@ -3094,10 +2582,9 @@ async function tickCommand(flags, context) {
3094
2582
  }
3095
2583
  // Sprint 8: the organisation policy before the claim (build() looks again before spawn, and lowers).
3096
2584
  {
3097
- const sealed = wantsPlan || wantsScout ? null : store.approvedChainOf(id)?.[0]?.profile ?? store.getScope(id)?.approvedProfile ?? null;
3098
- const refused = racedAhead !== null ? policyHold(racedAhead.agents.map(agent => ({ provider: agent.provider, model: agent.model })))
3099
- : policyHold([attendedSpec !== null ? { profile: attendedSpec.profile, attended: true } : sealed !== null ? { profile: sealed }
3100
- : { 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" } : {}) }]);
3101
2588
  if (refused !== null) {
3102
2589
  dispatched.push({ id, outcome: "skipped", reason: "policy", detail: refused });
3103
2590
  continue;
@@ -3106,18 +2593,16 @@ async function tickCommand(flags, context) {
3106
2593
  // v105: a monthly budget that stops new API work, used up: the task waits in the queue until the month turns or the
3107
2594
  // budget is raised (the 100% alert went out; the task page and the spend page say why).
3108
2595
  // Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
3109
- // A chain's base entry says how it bills (a pinned key); otherwise each provider as it bills now.
3110
- const chainBase = wantsPlan || wantsScout ? undefined : store.approvedChainOf(id)?.[0];
3111
- 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]));
3112
2598
  if (budgeted.over !== null) {
3113
2599
  dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
3114
2600
  continue;
3115
2601
  }
3116
2602
  // THE DAILY RAIL (modes chain D4): a live mode's run cap reserves at
3117
2603
  // admission — atomic, so two watch loops cannot both slip under it.
3118
- // Attended sessions and raced tasks reserve on their own roads below;
3119
- // an ordinary/planner start reserves ONE here. No mode = no-op.
3120
- if (attendedDispatch === null && racedAhead === null && repo !== null) {
2604
+ // An ordinary/planner start reserves ONE here. No mode = no-op.
2605
+ if (repo !== null) {
3121
2606
  const railed = store.reserveModeRail(repo, 1, clock());
3122
2607
  if (!railed.ok) {
3123
2608
  dispatched.push({ id, outcome: "skipped", reason: railed.rail, detail: railed.detail });
@@ -3174,240 +2659,6 @@ async function tickCommand(flags, context) {
3174
2659
  continue;
3175
2660
  }
3176
2661
  const lease = claimed.claim.leaseId;
3177
- // A tournament rides this claim (stage 3b): approved race terms send N
3178
- // agents instead of one builder. Everything after admission either
3179
- // reaches the ready barrier for ALL agents or interrupts the whole
3180
- // tournament — a partial race is never dispatched (finding 19).
3181
- const raceTerms = racedAhead;
3182
- if (raceTerms !== null && raceTerms.approvedDigest === raceTerms.raceDigest) {
3183
- const admittedKind = raceTerms.kind;
3184
- // The rail reserves every lane at once (D4) — a tournament is N
3185
- // starts from one filing. Refused before any skeleton exists.
3186
- if (repo !== null) {
3187
- const railedRace = store.reserveModeRail(repo, raceTerms.n, clock());
3188
- if (!railedRace.ok) {
3189
- dispatched.push({ id, outcome: "skipped", reason: railedRace.rail, detail: railedRace.detail });
3190
- continue;
3191
- }
3192
- }
3193
- const scopeRow = store.getScope(id);
3194
- const admitted = admitContest(store, {
3195
- taskId: id,
3196
- taskRef: ref.id,
3197
- runner,
3198
- leaseId: lease,
3199
- incarnation: text(flags, "incarnation") ?? null,
3200
- scopeDigest: scopeRow?.digest ?? "",
3201
- scopeApproved: scopeRow !== null && scopeApprovedForDispatch(store, ref.id, clock()),
3202
- // 'tasks' capacity mode keeps the claim-counted contract; the
3203
- // slot ledger records regardless (finding 26).
3204
- capacity: null,
3205
- quotaBlocked: (provider, model) => store.quotaState(runner, provider, model, clock())?.state ?? null,
3206
- }, clock());
3207
- if (!admitted.ok) {
3208
- release(store, lease, clock());
3209
- dispatched.push({ id, outcome: "skipped", reason: admitted.reason });
3210
- continue;
3211
- }
3212
- const interrupt = async (why, leasedPaths) => {
3213
- const fresh = store.getContest(admitted.contestId);
3214
- if (fresh !== null)
3215
- store.casContestState(admitted.contestId, ["dispatching", "racing"], "interrupted", fresh.generation);
3216
- store.releaseSlotsForContest(admitted.contestId, clock());
3217
- for (const path of leasedPaths)
3218
- await worktrees.release(path, clock());
3219
- release(store, lease, clock());
3220
- dispatched.push({ id, outcome: "failed", reason: why });
3221
- broke++;
3222
- };
3223
- const baseRead = await git("git", ["rev-parse", "HEAD"], { cwd: repo });
3224
- if (baseRead.code !== 0) {
3225
- await interrupt("contest-base", []);
3226
- continue;
3227
- }
3228
- const baseSha = baseRead.stdout.trim();
3229
- store.stampContestDispatch(admitted.contestId, baseSha, store.liveWorktreeSetup(repo)?.digest ?? null);
3230
- const agents = store.contestants(admitted.contestId);
3231
- const prepared = [];
3232
- let prepFailed = false;
3233
- for (const [index, agent] of agents.entries()) {
3234
- const leased = await worktrees.lease({ repo, branch: agent.branch, runner, taskRef: ref.id, now: clock(), base: baseSha });
3235
- if (!leased.ok) {
3236
- prepFailed = true;
3237
- break;
3238
- }
3239
- store.setContestantWorktree(agent.id, leased.worktree.path);
3240
- // The lane's provenance rides its insert (v48 integrity): the
3241
- // race-approved profile it will be proved against, exactly — the
3242
- // store's own answer, re-proved and bound to the lane there.
3243
- const laneStamp = store.laneAuthorityFor(agent.id);
3244
- if (laneStamp === null) {
3245
- prepFailed = true;
3246
- break;
3247
- }
3248
- // THE LANE ADMISSION (atomic authority closure): under the custody
3249
- // admitContest stamped — this lease, runner, and incarnation.
3250
- const admittedLane = store.admitContestLane({
3251
- taskRef: ref.id,
3252
- leaseId: lease,
3253
- runner,
3254
- incarnation: text(flags, "incarnation") ?? null,
3255
- branch: agent.branch,
3256
- worktree: leased.worktree.path,
3257
- provider: agent.provider,
3258
- model: agent.model,
3259
- contestant: agent.id,
3260
- now: clock(),
3261
- route: laneStamp,
3262
- });
3263
- if (!admittedLane.ok) {
3264
- prepFailed = true;
3265
- break;
3266
- }
3267
- const contestantRun = admittedLane.runId;
3268
- // The lane's pointer was bound inside the insert above (raw
3269
- // authority repair); nothing claims it after the fact.
3270
- prepared.push({
3271
- contestantId: agent.id,
3272
- slotId: admitted.slotIds[index] ?? -1,
3273
- runId: contestantRun,
3274
- worktree: leased.worktree.path,
3275
- branch: agent.branch,
3276
- leaseEpoch: leased.worktree.leaseEpoch ?? null,
3277
- });
3278
- }
3279
- const freshContest = store.getContest(admitted.contestId);
3280
- if (prepFailed || freshContest === null || !crossReadyBarrier(store, freshContest, admitted.contestantIds)) {
3281
- await interrupt("contest-admission", prepared.map(one => one.worktree));
3282
- continue;
3283
- }
3284
- // Every agent is READY and nothing has spawned: cross into racing and
3285
- // spend. The builds run concurrently; the stop fence stops them all.
3286
- budgetGate = null; // the race's spend counts toward the next check
3287
- // v105: the lanes run at once, so they share what's left of a monthly budget.
3288
- const laneShare = budgeted.remainingMicrousd === null ? null : Math.max(1, Math.floor(budgeted.remainingMicrousd / Math.max(1, prepared.length)));
3289
- const settled = await Promise.allSettled(prepared.map(async (entry) => {
3290
- const agent = store.getContestant(entry.contestantId);
3291
- if (agent === null)
3292
- throw new Error("contestant vanished");
3293
- store.casContestantState(entry.contestantId, ["ready"], "building", agent.generation);
3294
- return build(store, {
3295
- taskId: id,
3296
- taskRef: ref.id,
3297
- runner,
3298
- leaseId: lease,
3299
- runId: entry.runId,
3300
- evidenceRoot: context.evidenceRoot,
3301
- worktree: entry.worktree,
3302
- branch: entry.branch,
3303
- now: clock(),
3304
- clock,
3305
- provider: agent.provider,
3306
- contestProfile: contestantProfileOf(agent.provider, agent.model, agent.repairModel),
3307
- // A comparison lane has no dollar cap — the sealed clock is the
3308
- // bound; only race lanes carry the harness stop (E1).
3309
- ...(agent.budgetMicrousd > 0 ? { maxBudgetUsd: Math.min(agent.budgetMicrousd, laneShare ?? Infinity) / 1_000_000 } : {}),
3310
- onProviderSpawn: pid => {
3311
- worktrees.recordProviderOccupancy(entry.worktree, runner, pid, entry.leaseEpoch);
3312
- const facts = { run: entry.runId, contestant: entry.contestantId, incarnation: text(flags, "incarnation") ?? null, processGroup: pid };
3313
- if (!store.markSlotRunning(entry.slotId, facts, clock()) && !store.refreshSlotProcess(entry.slotId, facts))
3314
- throw new Error("the provider's execution slot no longer belongs to this attempt");
3315
- },
3316
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
3317
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
3318
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
3319
- });
3320
- }));
3321
- let lastAggregate = null;
3322
- for (const [index, entry] of prepared.entries()) {
3323
- const outcome = settled[index];
3324
- const run = store.getRun(entry.runId);
3325
- const measured = run === null || run.providerStartedAt === null
3326
- ? 0
3327
- : run.costUsd !== null
3328
- ? Math.round(run.costUsd * 1_000_000)
3329
- : null;
3330
- let contestantOutcome = "failed";
3331
- let committed = false;
3332
- if (outcome !== undefined && outcome.status === "fulfilled") {
3333
- const result = outcome.value;
3334
- if (result.ok && result.parked !== undefined) {
3335
- contestantOutcome = "parked";
3336
- // The question still reaches the operator, tagged with its agent;
3337
- // decision-wait mechanics land in stage 4 — the card works today.
3338
- const asked = result.parked.decision;
3339
- const contestantDecision = store.saveDecision({
3340
- run: entry.runId,
3341
- contestant: entry.contestantId,
3342
- urgency: asked.urgency,
3343
- recap: asked.recap,
3344
- question: asked.question,
3345
- options: asked.options,
3346
- recommendation: asked.recommendation,
3347
- ...(asked.assignee === null ? {} : { assignee: asked.assignee }),
3348
- ...(asked.deadline === null ? {} : { deadline: asked.deadline }),
3349
- }, clock());
3350
- store.enqueueNotification({
3351
- source: { run: entry.runId },
3352
- dedupeKey: `decision:${contestantDecision}`,
3353
- kind: "decision",
3354
- subject: `${id} parked a decision (${contestNoun(admittedKind)} agent)`,
3355
- body: `\`toolroll decide ${contestantDecision}\``,
3356
- pushClass: "decision",
3357
- link: `/d/${contestantDecision}`,
3358
- }, clock());
3359
- }
3360
- else if (result.ok) {
3361
- contestantOutcome = "built";
3362
- committed = result.committed;
3363
- }
3364
- else if (result.reason === "stopped") {
3365
- contestantOutcome = "stopped";
3366
- }
3367
- }
3368
- if (run !== null && run.outcome === null) {
3369
- // The reason rides the run (slice B, E2 — kind-agnostic): "lane 3
3370
- // failed" with no words when a binary drifted out of its attested
3371
- // range is exactly the silence the attested runtime rules out.
3372
- const laneReason = outcome !== undefined && outcome.status === "fulfilled" && !outcome.value.ok
3373
- ? outcome.value.reason
3374
- : undefined;
3375
- store.finishRun(entry.runId, {
3376
- outcome: contestantOutcome === "built" && !committed ? "no-change" : contestantOutcome === "stopped" ? "failed" : contestantOutcome === "parked" ? "parked" : contestantOutcome,
3377
- committed,
3378
- ...(laneReason === undefined ? {} : { reason: laneReason }),
3379
- now: clock(),
3380
- });
3381
- }
3382
- if (contestantOutcome === "parked") {
3383
- // Custody (round-3 finding 29): who owns this checkout while the
3384
- // question waits, and what exact state it was left in — the
3385
- // resume verifies all of it before trusting the tree again.
3386
- const headNow = await git("git", ["rev-parse", "HEAD"], { cwd: entry.worktree });
3387
- const dirtyNow = await git("git", ["status", "--porcelain"], { cwd: entry.worktree });
3388
- store.setContestantCustody(entry.contestantId, JSON.stringify({
3389
- branch: entry.branch,
3390
- head: headNow.code === 0 ? headNow.stdout.trim() : null,
3391
- runner,
3392
- dirty: dirtyNow.stdout.trim() !== "",
3393
- at: clock().toISOString(),
3394
- }));
3395
- }
3396
- await worktrees.release(entry.worktree, clock());
3397
- const final = finalizeContestant(store, {
3398
- contestId: admitted.contestId,
3399
- contestantId: entry.contestantId,
3400
- runId: entry.runId,
3401
- outcome: contestantOutcome,
3402
- measuredMicrousd: measured,
3403
- slotId: entry.slotId >= 0 ? entry.slotId : null,
3404
- }, clock());
3405
- if (final.aggregated !== null)
3406
- lastAggregate = final.aggregated;
3407
- }
3408
- dispatched.push({ id, outcome: "contest", reason: lastAggregate ?? "racing" });
3409
- continue;
3410
- }
3411
2662
  if (wantsPlan) {
3412
2663
  // THE FILED REQUEST, FIRST (contract handoff, task 1): everything the
3413
2664
  // operator filed — scope, rubric, terms, revision brief, earlier
@@ -3480,11 +2731,11 @@ async function tickCommand(flags, context) {
3480
2731
  }
3481
2732
  catch (error) {
3482
2733
  await worktrees.release(planLeased.worktree.path, clock());
3483
- const unrecorded = finalizePlanFailureFenced(store, {
3484
- leaseId: lease,
2734
+ const unrecorded = finalize(store, lease, {
2735
+ kind: "plan-failure",
3485
2736
  runId: planRunId,
3486
2737
  taskId: id,
3487
- kind: "failure",
2738
+ failure: "failure",
3488
2739
  message: `the planner's source could not be recorded: ${error instanceof Error ? error.message : String(error)}`,
3489
2740
  now: clock(),
3490
2741
  });
@@ -3520,8 +2771,8 @@ async function tickCommand(flags, context) {
3520
2771
  });
3521
2772
  await worktrees.release(planLeased.worktree.path, clock());
3522
2773
  if (outcome.ok && "parked" in outcome) {
3523
- const sealed = finalizeParkFenced(store, {
3524
- leaseId: lease,
2774
+ const sealed = finalize(store, lease, {
2775
+ kind: "park",
3525
2776
  runId: planRunId,
3526
2777
  taskId: id,
3527
2778
  decision: outcome.parked.decision,
@@ -3544,8 +2795,8 @@ async function tickCommand(flags, context) {
3544
2795
  continue;
3545
2796
  }
3546
2797
  if (outcome.ok) {
3547
- const sealed = finalizePlanFenced(store, {
3548
- leaseId: lease,
2798
+ const sealed = finalize(store, lease, {
2799
+ kind: "plan",
3549
2800
  runId: planRunId,
3550
2801
  taskId: id,
3551
2802
  plan: outcome.drafted.plan,
@@ -3578,11 +2829,11 @@ async function tickCommand(flags, context) {
3578
2829
  }
3579
2830
  continue;
3580
2831
  }
3581
- const sealedFailure = finalizePlanFailureFenced(store, {
3582
- leaseId: lease,
2832
+ const sealedFailure = finalize(store, lease, {
2833
+ kind: "plan-failure",
3583
2834
  runId: planRunId,
3584
2835
  taskId: id,
3585
- kind: outcome.kind,
2836
+ failure: outcome.kind,
3586
2837
  ...(outcome.kind === "malformed" ? { malformed: outcome.reason === "malformed-decision" ? "decision" : "plan" } : {}),
3587
2838
  message: outcome.message,
3588
2839
  now: clock(),
@@ -3608,7 +2859,7 @@ async function tickCommand(flags, context) {
3608
2859
  const scopeRow = store.getScope(id);
3609
2860
  // Approvals bind exact routing for a scout exactly as for a build:
3610
2861
  // the pinned profile is proved BEFORE the workspace is leased.
3611
- const proof = proveApprovedProfile(scopeRow, null, {
2862
+ const proof = proveApprovedProfile(scopeRow, {
3612
2863
  provider: spec.provider,
3613
2864
  model: spec.model ?? undefined,
3614
2865
  maxTurns: undefined,
@@ -3687,8 +2938,8 @@ async function tickCommand(flags, context) {
3687
2938
  // never silently kept; `toolroll worktrees` lists it.
3688
2939
  const leftover = discarded.ok ? {} : { detail: `checkout kept: ${discarded.message}` };
3689
2940
  if (scouted.ok && "parked" in scouted) {
3690
- const sealed = finalizeParkFenced(store, {
3691
- leaseId: lease,
2941
+ const sealed = finalize(store, lease, {
2942
+ kind: "park",
3692
2943
  runId: scoutRunId,
3693
2944
  taskId: id,
3694
2945
  decision: scouted.parked.decision,
@@ -3706,8 +2957,8 @@ async function tickCommand(flags, context) {
3706
2957
  continue;
3707
2958
  }
3708
2959
  if (scouted.ok) {
3709
- const sealed = finalizeScoutFenced(store, {
3710
- leaseId: lease,
2960
+ const sealed = finalize(store, lease, {
2961
+ kind: "scout",
3711
2962
  runId: scoutRunId,
3712
2963
  taskId: id,
3713
2964
  report: scouted.reported.report,
@@ -3727,11 +2978,11 @@ async function tickCommand(flags, context) {
3727
2978
  }
3728
2979
  continue;
3729
2980
  }
3730
- const sealedFailure = finalizeScoutFailureFenced(store, {
3731
- leaseId: lease,
2981
+ const sealedFailure = finalize(store, lease, {
2982
+ kind: "scout-failure",
3732
2983
  runId: scoutRunId,
3733
2984
  taskId: id,
3734
- kind: scouted.kind,
2985
+ failure: scouted.kind,
3735
2986
  ...(scouted.kind === "malformed" ? { malformed: scouted.reason === "malformed-decision" ? "decision" : "report" } : {}),
3736
2987
  message: scouted.message,
3737
2988
  now: clock(),
@@ -3793,102 +3044,12 @@ async function tickCommand(flags, context) {
3793
3044
  // a row with no outcome — an attempt that vanished, visible by morning.
3794
3045
  // Its route provenance (v47) is written in the same admission
3795
3046
  // transaction; build() then refuses to spend as anything else.
3796
- // The chain custody for this run (E3b/E3d, atomic since the v48 authority repair): a parked
3797
- // chain tail hands custody to this successor through the PROVEN resume
3798
- // transfer, otherwise a chain approval opens its fresh cycle bound to
3799
- // this run — both proved and written IN the run's own insert. A
3800
- // single-profile approval — every task until an operator configures a
3801
- // fallback chain — binds nothing, so this is inert by default. A
3802
- // binding that cannot be proved, or a stamp the task's authority does
3803
- // 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.
3804
3049
  const buildStamp = routeStamp("build");
3805
3050
  let runId;
3806
3051
  try {
3807
- if (parkedEntry !== null && parkedChainTail !== null && liveCycle !== null && buildStamp !== null) {
3808
- // A parked FALLBACK tail's successor is admitted by the one
3809
- // fallback road (v48 integrity): every fact the tail's binding
3810
- // states is presented and re-proved — cycle, index, digest, auth
3811
- // mode, provider, exact model, repair binding, the sealed-profile
3812
- // mirror, the approved chain — with the parked run as the live
3813
- // tail, before any row exists.
3814
- const chain = store.approvedChainOf(id);
3815
- const entry = chain === null ? undefined : chain[parkedEntry.index];
3816
- const mirror = store.getScope(id)?.approvedProfile ?? null;
3817
- if (chain === null || entry === undefined || mirror === null || parkedChainTail.entryDigest == null || parkedChainTail.authMode == null || spec.model === null) {
3818
- throw new Error(`${id}: the parked attempt #${parkedChainTail.id}'s fallback binding cannot be restated against the approved chain — nothing resumes it`);
3819
- }
3820
- const admitted = store.admitFallback({
3821
- kind: "resume",
3822
- parkedRun: parkedChainTail.id,
3823
- cycleId: liveCycle.id,
3824
- expectCursor: parkedEntry.index,
3825
- expectTail: parkedChainTail.id,
3826
- entryDigest: parkedChainTail.entryDigest,
3827
- authMode: parkedChainTail.authMode,
3828
- repairModel: entry.profile.repairModel === "inherit" ? entry.profile.model : entry.profile.repairModel,
3829
- approved: { chainDigest: chainDigestOf(chain), profile: mirror },
3830
- run: {
3831
- taskRef: ref.id, leaseId: lease, runner, branch, worktree: leased.worktree.path, provider: spec.provider, model: spec.model,
3832
- // The recovered draft's lineage rides the insert (raw
3833
- // authority repair) — never a later stamp.
3834
- ...(leased.resumedFromRun === undefined ? {} : { recoveredFrom: leased.resumedFromRun }),
3835
- },
3836
- route: buildStamp,
3837
- }, clock());
3838
- if (!admitted.ok)
3839
- throw new Error(admitted.problem);
3840
- runId = admitted.runId;
3841
- }
3842
- else if (attendedDispatch !== null && attendedSpec !== null) {
3843
- // THE ATTENDED ADMISSION (atomic authority closure): the one
3844
- // watched attempt opens under exactly this authorization — named
3845
- // by id, runner, and generation — and the insert consumes its
3846
- // attempt and binds the row to it in one transaction.
3847
- if (buildStamp === null)
3848
- throw new Error(`${id}: the attended authorization's pinned profile presents no build authority`);
3849
- // THE SIGNED HEAD, BEFORE THE ATTEMPT IS SPENT (final authority
3850
- // closure): the authorization signed the exact commit the watched
3851
- // attempt would start from. The leased worktree's HEAD is read
3852
- // here and held to it BEFORE admission — a head that moved opens
3853
- // no run and spends no attempt; the authorization closes in the
3854
- // refusal's words so the operator sees why and may authorize
3855
- // again at today's head. The coordinator's final proof re-reads
3856
- // the same term against the captured base revision.
3857
- let signedHead = null;
3858
- try {
3859
- const terms = JSON.parse(attendedDispatch.termsJson);
3860
- signedHead = typeof terms.head === "string" && terms.head !== "" ? terms.head : null;
3861
- }
3862
- catch {
3863
- signedHead = null;
3864
- }
3865
- if (signedHead === null) {
3866
- store.closeAuthorization(attendedDispatch.id, "refused:stale-authorization", clock());
3867
- throw new StaleAuthorization(`${id}: the attended authorization ${attendedDispatch.id} signs no readable head — nothing opens under it`);
3868
- }
3869
- const headRead = await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
3870
- const headNow = headRead.code === 0 ? headRead.stdout.trim() : "";
3871
- if (headNow !== signedHead) {
3872
- store.closeAuthorization(attendedDispatch.id, "refused:stale-authorization", clock());
3873
- 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`);
3874
- }
3875
- const admittedAttended = store.admitAttended({
3876
- taskRef: ref.id,
3877
- leaseId: lease,
3878
- runner,
3879
- branch,
3880
- worktree: leased.worktree.path,
3881
- provider: spec.provider,
3882
- ...(spec.model === null ? {} : { model: spec.model }),
3883
- authorization: { id: attendedDispatch.id, runner: attendedDispatch.runner, generation: attendedDispatch.runnerGeneration },
3884
- now: clock(),
3885
- route: buildStamp,
3886
- });
3887
- if (!admittedAttended.ok)
3888
- throw new Error(admittedAttended.problem);
3889
- runId = admittedAttended.runId;
3890
- }
3891
- else if (leased.resumedFromRun !== undefined) {
3052
+ if (leased.resumedFromRun !== undefined) {
3892
3053
  // THE RECOVERED-DRAFT ADMISSION (atomic authority closure): the
3893
3054
  // fresh attempt inherits the interrupted attempt's draft, proved
3894
3055
  // this task's own interrupted builder in this very worktree.
@@ -3905,7 +3066,6 @@ async function tickCommand(flags, context) {
3905
3066
  recoveredFrom: leased.resumedFromRun,
3906
3067
  now: clock(),
3907
3068
  route: buildStamp,
3908
- custody: parkedChainTail !== null && liveCycle !== null ? { kind: "resume", parkedRun: parkedChainTail.id } : { kind: "base" },
3909
3069
  });
3910
3070
  if (!admittedRecovered.ok)
3911
3071
  throw new Error(admittedRecovered.problem);
@@ -3922,14 +3082,13 @@ async function tickCommand(flags, context) {
3922
3082
  ...(spec.model === null ? {} : { model: spec.model }),
3923
3083
  now: clock(),
3924
3084
  ...(buildStamp === null ? {} : { route: buildStamp }),
3925
- custody: parkedChainTail !== null && liveCycle !== null ? { kind: "resume", parkedRun: parkedChainTail.id } : { kind: "base" },
3926
3085
  });
3927
3086
  }
3928
3087
  }
3929
3088
  catch (error) {
3930
3089
  await worktrees.release(leased.worktree.path, clock());
3931
3090
  release(store, lease, clock());
3932
- 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) });
3933
3092
  continue;
3934
3093
  }
3935
3094
  if (leased.resumedFromRun !== undefined) {
@@ -3945,11 +3104,9 @@ async function tickCommand(flags, context) {
3945
3104
  const budgetLeft = budgeted.remainingMicrousd !== null && MONEY_CAPABILITIES[spec.provider].nativeDollarCapFlag !== null ? Math.max(1, budgeted.remainingMicrousd) : null;
3946
3105
  const capMicrousd = [scopeBudget, backstop, budgetLeft].reduce((least, one) => one === null ? least : least === null ? one : Math.min(least, one), null);
3947
3106
  if (capMicrousd !== null && MONEY_CAPABILITIES[spec.provider].nativeDollarCapFlag === null) {
3948
- // The run row and any chain cycle already exist — FINISH and RESOLVE
3949
- // them (E3d verify, R6): a refused-but-open run would defer a chain
3950
- // 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.
3951
3109
  store.finishRun(runId, { outcome: "refused", reason: "budget-unenforceable", now: clock() });
3952
- store.resolveChainOnRunEnd(ref.id, id, repo, runId, clock());
3953
3110
  release(store, lease, clock());
3954
3111
  await worktrees.release(leased.worktree.path, clock());
3955
3112
  dispatched.push({ id, outcome: "skipped", reason: "budget-unenforceable" });
@@ -3988,29 +3145,7 @@ async function tickCommand(flags, context) {
3988
3145
  ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
3989
3146
  ...(leased.resumedFromRun === undefined ? {} : { recoveredDraftRun: leased.resumedFromRun }),
3990
3147
  ...(leased.recoveryKind === undefined ? {} : { recoveredDraftKind: leased.recoveryKind }),
3991
- ...(attendedDispatch === null || context.heldCoordinator === undefined
3992
- ? {}
3993
- : {
3994
- attended: {
3995
- authorization: attendedDispatch,
3996
- coordinator: context.heldCoordinator,
3997
- upIncarnation: context.upIncarnation ?? "unknown",
3998
- socketDir: context.heldSocketDir ?? tmpdir(),
3999
- releaseWorktree: async (path) => worktrees.release(path, clock()),
4000
- dispose: { repo, origin: ref.origin, provider: spec.provider, model: spec.model },
4001
- ...(context.heldStarter === undefined ? {} : { starter: context.heldStarter }),
4002
- ...(context.heldGraceMs === undefined ? {} : { graceMs: context.heldGraceMs }),
4003
- ...(context.maxHeldSessions === undefined ? {} : { maxHeldSessions: context.maxHeldSessions }),
4004
- },
4005
- }),
4006
3148
  });
4007
- // THE HELD HANDOFF (Phase 2, v2 S0d): ownership transferred — the
4008
- // coordinator owns run, lease, and worktree; this pass releases and
4009
- // settles NOTHING and moves on. Nonblocking is the whole point.
4010
- if (result.ok && result.parked === undefined && result.held === true) {
4011
- dispatched.push({ id, outcome: "held", branch, worktree: leased.worktree.path });
4012
- continue;
4013
- }
4014
3149
  // Handed back either way; a tree with somebody's work in it comes back
4015
3150
  // unverified rather than cleaned, same as `build`.
4016
3151
  await worktrees.release(leased.worktree.path, clock());
@@ -4034,13 +3169,6 @@ async function tickCommand(flags, context) {
4034
3169
  evidenceRoot: context.evidenceRoot,
4035
3170
  clock,
4036
3171
  }, result);
4037
- // The chain step (E3c/E3d): EVERY concluded run resolves its cycle
4038
- // through the one resolver — success closes, an ordinary end closes, a
4039
- // parked tail stays open for repair, and a recognized eligible
4040
- // exhaustion advances to pending-admission (fail-closed at every gate,
4041
- // the live grant re-proved in its own transaction). Inert unless the
4042
- // task filed under an explicit chain — no cycle, fast no-op.
4043
- store.resolveChainOnRunEnd(ref.id, id, repo, runId, clock());
4044
3172
  switch (disposition.kind) {
4045
3173
  case "parked":
4046
3174
  dispatched.push({ id, outcome: "parked", reason: `decision:${disposition.decisionId}`, worktree: leased.worktree.path });
@@ -4062,453 +3190,51 @@ async function tickCommand(flags, context) {
4062
3190
  dispatched.push({ id, outcome: "failed", reason: "fenced", branch, worktree: leased.worktree.path });
4063
3191
  broke++;
4064
3192
  break;
4065
- case "skipped":
4066
- dispatched.push({ id, outcome: "skipped", reason: disposition.reason });
4067
- break;
4068
- case "fenced":
4069
- dispatched.push({ id, outcome: "failed", reason: "fenced", worktree: leased.worktree.path });
4070
- broke++;
4071
- break;
4072
- case "malformed":
4073
- dispatched.push({ id, outcome: "failed", reason: disposition.sealed ? "malformed-decision" : "fenced", worktree: leased.worktree.path });
4074
- broke++;
4075
- break;
4076
- case "failed":
4077
- dispatched.push({
4078
- id,
4079
- outcome: "failed",
4080
- reason: disposition.sealed
4081
- ? disposition.disposition === "auth-expired"
4082
- ? "auth-expired — requeued, sign-in needed"
4083
- : `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
4084
- : "fenced",
4085
- ...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
4086
- worktree: leased.worktree.path,
4087
- });
4088
- broke++;
4089
- break;
4090
- case "recorded":
4091
- // The standalone-only arm; unreachable under the tick policy.
4092
- break;
4093
- case "stopped":
4094
- // An operator's stop (v52): interrupted with its work preserved —
4095
- // not a break, not a strike; the task waits under the stop's hold.
4096
- dispatched.push({ id, outcome: "stopped", reason: `stop:${disposition.stopRun}`, branch, worktree: leased.worktree.path });
4097
- break;
4098
- case "handed-back":
4099
- // The service is stopping: the work is kept and the task requeued
4100
- // to resume on the next pass — not a break, not a strike.
4101
- dispatched.push({ id, outcome: "stopped", reason: "service-stop", detail: result.ok ? "" : result.message.slice(0, 200), branch, worktree: leased.worktree.path });
4102
- break;
4103
- case "invariant":
4104
- dispatched.push({ id, outcome: "failed", reason: disposition.reason });
4105
- broke++;
4106
- break;
4107
- }
4108
- }
4109
- untakenTrial?.giveBack();
4110
- // THE CHAIN ADMISSION PASS (E3d): cycles a recognized exhaustion advanced
4111
- // to pending-admission dispatch their NEXT approved entry here — the ONLY
4112
- // road that runs a fallback entry. Every authority is re-derived inside
4113
- // admitNextChainEntry (approved chain standing + digest match + LIVE
4114
- // paid-fallback grant + the single-use pending edge); this loop carries
4115
- // only claim, worktree, and rail, and its run then re-proves the
4116
- // chain-entry dispatch proof inside build() before any money moves.
4117
- for (const pending of buildsOnly ? [] : store.pendingChainAdmissions(repo)) {
4118
- settleFinished();
4119
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
4120
- break;
4121
- if (built >= max)
4122
- break;
4123
- // v105: a fallback billed to an API key spends dollars: a used-up budget holds it like any new work.
4124
- const chainEntry = store.approvedChainOf(pending.taskId)?.[pending.cursor];
4125
- const chainAgents = chainEntry === undefined ? [] : [{ provider: chainEntry.profile.provider, billing: chainEntry.authMode }];
4126
- // Sprint 8: a fallback entry the organisation policy doesn't allow never starts.
4127
- const chainPolicy = chainEntry === undefined ? null : policyHold([{ profile: chainEntry.profile }]);
4128
- if (chainPolicy !== null) {
4129
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "policy", detail: chainPolicy });
4130
- continue;
4131
- }
4132
- const chainBudget = budgetHold(pending.taskRef, chainAgents);
4133
- if (chainBudget.over !== null) {
4134
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget", detail: budgetWords(chainBudget) });
4135
- continue;
4136
- }
4137
- const railed = store.reserveModeRail(repo, 1, clock());
4138
- if (!railed.ok) {
4139
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: railed.rail, detail: railed.detail });
4140
- continue;
4141
- }
4142
- // A peek proves the entry EXISTS and carries an enforceable budget
4143
- // BEFORE any claim or run row exists (review findings 4/6); the
4144
- // admission below re-derives the entry as authority in its transaction.
4145
- const peek = store.approvedChainOf(pending.taskId)?.[pending.cursor];
4146
- if (peek === undefined) {
4147
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "fallback-unadmittable" });
4148
- continue;
4149
- }
4150
- const scopeBudget = store.getScope(pending.taskId)?.budgetMicrousd ?? null;
4151
- const backstop = store.getSpendDefaults()?.buildPerRunMicrousd ?? null;
4152
- const capMicrousd = scopeBudget === null ? backstop : backstop === null ? scopeBudget : Math.min(scopeBudget, backstop);
4153
- if (capMicrousd !== null && MONEY_CAPABILITIES[peek.profile.provider].nativeDollarCapFlag === null) {
4154
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget-unenforceable" });
4155
- continue;
4156
- }
4157
- // The sign-in pause holds a fallback entry too: the admission stays pending until its provider works.
4158
- const entryGate = signInGate(store, [peek.profile.provider], clock(), pending.taskRef);
4159
- if (entryGate.waiting !== null) {
4160
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(entryGate.waiting) });
4161
- continue;
4162
- }
4163
- // The FALLBACK claim (finding 4): every acquireIfReady gate — task
4164
- // state, non-backoff holds, blockers, approved scope + mode belt,
4165
- // capability, capacity, and quota keyed by the PINNED credential —
4166
- // with only the predecessor's backoff exempted.
4167
- const claimed = acquireFallback(store, pending.taskRef, runner, {
4168
- now: clock(),
4169
- token,
4170
- ttlMs: leaseTtlMs,
4171
- repo,
4172
- provider: peek.profile.provider,
4173
- model: peek.profile.model,
4174
- authMode: peek.authMode,
4175
- // The watch incarnation rides the claim (F+G review, finding 3): a
4176
- // daemon that dies after this claim — admitted or not — must be
4177
- // recoverable by its successor's incarnation takeover, exactly like
4178
- // the ordinary road.
4179
- ...(text(flags, "incarnation") === undefined ? {} : { incarnation: text(flags, "incarnation") }),
4180
- ...(text(flags, "max-open-decisions") === undefined
4181
- ? {}
4182
- : { maxOpenDecisions: Number(text(flags, "max-open-decisions")) }),
4183
- });
4184
- if (!claimed.ok) {
4185
- entryGate.giveBack();
4186
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: claimed.reason });
4187
- continue;
4188
- }
4189
- const lease = claimed.claim.leaseId;
4190
- const branch = await existingOrFirst(taskBranches(pending.taskId), async (one) => (await git("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${one}`], { cwd: repo })).code === 0);
4191
- const exists = await git("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: repo });
4192
- const leased = await worktrees.lease({
4193
- repo,
4194
- branch,
4195
- runner,
4196
- taskRef: pending.taskRef,
4197
- now: clock(),
4198
- ...(exists.code === 0 ? {} : { base }),
4199
- reclaim: { evidenceRoot: context.evidenceRoot },
4200
- });
4201
- if (!leased.ok) {
4202
- release(store, lease, clock());
4203
- dispatched.push({ id: pending.taskId, outcome: "failed", reason: leased.reason });
4204
- broke++;
4205
- continue;
4206
- }
4207
- const admitted = store.admitNextChainEntry(pending.cycleId, { leaseId: lease, runner, branch, worktree: leased.worktree.path, ...(leased.resumedFromRun === undefined ? {} : { recoveredFrom: leased.resumedFromRun }) }, clock());
4208
- if (!admitted.ok) {
4209
- await worktrees.release(leased.worktree.path, clock());
4210
- release(store, lease, clock());
4211
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: `fallback-${admitted.reason}`, ...(admitted.detail === undefined ? {} : { detail: admitted.detail }) });
4212
- continue;
4213
- }
4214
- if (leased.resumedFromRun !== undefined) {
4215
- 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());
4216
- }
4217
- // The effective cap, RE-DERIVED after admission (E3d verify, R6): the
4218
- // pre-claim value is a survey; a backstop set while the claim and
4219
- // worktree awaits ran must govern the spend that actually happens. The
4220
- // belt then re-proves capability against the ADMITTED provider — and on
4221
- // refusal the run is FINISHED and its cycle resolved, never abandoned.
4222
- const scopeBudgetNow = store.getScope(pending.taskId)?.budgetMicrousd ?? null;
4223
- const backstopNow = store.getSpendDefaults()?.buildPerRunMicrousd ?? null;
4224
- const budgetNow = chainEntry?.authMode === "api-key" ? budgetHold(pending.taskRef, chainAgents).remainingMicrousd : null;
4225
- const capNow = [scopeBudgetNow, backstopNow, budgetNow === null ? null : Math.max(1, budgetNow)]
4226
- .reduce((least, one) => one === null ? least : least === null ? one : Math.min(least, one), null);
4227
- if (capNow !== null && MONEY_CAPABILITIES[admitted.provider].nativeDollarCapFlag === null) {
4228
- store.finishRun(admitted.runId, { outcome: "refused", reason: "budget-unenforceable", now: clock() });
4229
- store.resolveChainOnRunEnd(pending.taskRef, admitted.taskId, repo, admitted.runId, clock());
4230
- release(store, lease, clock());
4231
- await worktrees.release(leased.worktree.path, clock());
4232
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget-unenforceable" });
4233
- continue;
4234
- }
4235
- budgetGate = null; // this build's spend counts toward the next check
4236
- const result = await build(store, {
4237
- taskId: pending.taskId,
4238
- taskRef: pending.taskRef,
4239
- runner,
4240
- leaseId: lease,
4241
- runnerToken: token,
4242
- runId: admitted.runId,
4243
- evidenceRoot: context.evidenceRoot,
4244
- worktree: leased.worktree.path,
4245
- branch,
4246
- now: clock(),
4247
- clock,
4248
- ...(capNow === null ? {} : { maxBudgetUsd: capNow / 1_000_000 }),
4249
- onProviderSpawn: pid => {
4250
- worktrees.recordProviderOccupancy(leased.worktree.path, runner, pid, leased.worktree.leaseEpoch);
4251
- },
4252
- provider: admitted.provider,
4253
- model: admitted.model,
4254
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
4255
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
4256
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
4257
- ...(leased.resumedFromRun === undefined ? {} : { recoveredDraftRun: leased.resumedFromRun }),
4258
- ...(leased.recoveryKind === undefined ? {} : { recoveredDraftKind: leased.recoveryKind }),
4259
- });
4260
- await worktrees.release(leased.worktree.path, clock());
4261
- const disposition = disposeBuildOutcome({
4262
- store,
4263
- policy: "tick",
4264
- leaseId: lease,
4265
- runId: admitted.runId,
4266
- taskId: pending.taskId,
4267
- taskRef: pending.taskRef,
4268
- runner,
4269
- repo,
4270
- branch,
4271
- origin: "ours",
4272
- provider: admitted.provider,
4273
- model: admitted.model,
4274
- worktreePath: leased.worktree.path,
4275
- evidenceRoot: context.evidenceRoot,
4276
- clock,
4277
- }, result);
4278
- // The same one resolver: a fallback entry that itself exhausts advances
4279
- // again; one that succeeds or ordinarily ends closes its cycle.
4280
- store.resolveChainOnRunEnd(pending.taskRef, pending.taskId, repo, admitted.runId, clock());
4281
- switch (disposition.kind) {
4282
- case "built":
4283
- dispatched.push({ id: pending.taskId, outcome: "built", committed: disposition.committed, branch, worktree: leased.worktree.path });
4284
- built++;
4285
- break;
4286
- case "stopped":
4287
- dispatched.push({ id: pending.taskId, outcome: "stopped", reason: `stop:${disposition.stopRun}`, branch, worktree: leased.worktree.path });
4288
- break;
4289
- case "handed-back":
4290
- dispatched.push({ id: pending.taskId, outcome: "stopped", reason: "service-stop", branch, worktree: leased.worktree.path });
4291
- break;
4292
- case "parked":
4293
- dispatched.push({ id: pending.taskId, outcome: "parked", reason: `decision:${disposition.decisionId}`, worktree: leased.worktree.path });
4294
- parked++;
4295
- break;
4296
- case "skipped":
4297
- dispatched.push({ id: pending.taskId, outcome: "skipped", reason: disposition.reason });
4298
- break;
4299
- default:
4300
- dispatched.push({ id: pending.taskId, outcome: "failed", reason: "fallback-attempt", worktree: leased.worktree.path });
4301
- broke++;
4302
- break;
4303
- }
4304
- }
4305
- // THE CONTINUATION PASS (Phase 2E, A4): open continuation authorizations
4306
- // named to this runner dispatch here — the finished parent task never
4307
- // re-enters the queue; the authorization is the claimable unit (v3 R7).
4308
- // Only a co-located coordinator can hold the session, and everything
4309
- // else (liveness, one attempt, the final proof at the parent's exact
4310
- // head) is re-proved on the way in.
4311
- if (context.heldCoordinator !== undefined && !buildsOnly) {
4312
- for (const continuation of store.openContinuationAuthorizations(runner)) {
4313
- if (context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true)
4314
- break;
4315
- const watching = attendedLivenessState(continuation.lastBeatAt === null ? null : Date.parse(continuation.lastBeatAt), clock().getTime(), Date.parse(continuation.absoluteExpiry));
4316
- if (watching !== "live" && watching !== "grace")
4317
- continue;
4318
- const parent = continuation.parentRun === null ? null : store.getRun(continuation.parentRun);
4319
- const parentRef = parent === null ? null : store.refForId(parent.taskRef);
4320
- if (parent === null || parentRef === null)
4321
- continue;
4322
- // A continuation continues WORK: a branchless parent (the reviewer
4323
- // role, v29) has no workspace to lease and can never be continued —
4324
- // and this guard is what keeps "null" out of `git worktree add`.
4325
- if (parent.branch === null)
4326
- continue;
4327
- const parentBranch = parent.branch;
4328
- if (parentRef.repo !== null && parentRef.repo !== repo)
4329
- continue;
4330
- const taskId = parentRef.externalId;
4331
- let pinned = null;
4332
- try {
4333
- const terms = JSON.parse(continuation.termsJson);
4334
- const profile = profileFromJson(typeof terms.profileJson === "string" ? terms.profileJson : null);
4335
- if (profile !== null)
4336
- pinned = { provider: profile.provider, model: profile.model, digest: profileDigestOf(profile), profile };
4337
- }
4338
- catch {
4339
- pinned = null;
4340
- }
4341
- if (pinned === null) {
4342
- dispatched.push({ id: taskId, outcome: "skipped", reason: "attended-only", detail: "the continuation's pinned profile cannot be read" });
4343
- continue;
4344
- }
4345
- // Sprint 8: an attended continuation runs at exactly its signed terms, or not at all.
4346
- const continuationPolicy = policyHold([{ profile: pinned.profile, attended: true }]);
4347
- if (continuationPolicy !== null) {
4348
- dispatched.push({ id: taskId, outcome: "skipped", reason: "policy", detail: continuationPolicy });
4349
- continue;
4350
- }
4351
- const continuationBudget = budgetHold(parent.taskRef, store.agentsFor([pinned.provider]));
4352
- if (continuationBudget.over !== null) {
4353
- dispatched.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(continuationBudget) });
4354
- continue;
4355
- }
4356
- // The sign-in pause holds a continuation too: its authorization stays open and it starts once the sign-in works.
4357
- const continuationGate = signInGate(store, [pinned.provider], clock(), parent.taskRef);
4358
- if (continuationGate.waiting !== null) {
4359
- dispatched.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(continuationGate.waiting) });
4360
- continue;
4361
- }
4362
- const claimed = acquireContinuation(store, continuation, runner, { now: clock(), token, ttlMs: leaseTtlMs });
4363
- if (!claimed.ok) {
4364
- continuationGate.giveBack();
4365
- dispatched.push({ id: taskId, outcome: "skipped", reason: claimed.reason, ...("message" in claimed ? { detail: claimed.message } : {}) });
4366
- continue;
4367
- }
4368
- const lease = claimed.claim.leaseId;
4369
- // The parent's branch, at the head the terms signed — a moved branch
4370
- // fails the final proof with words naming the head.
4371
- const leased = await worktrees.lease({ repo, branch: parentBranch, runner, taskRef: parent.taskRef, now: clock() });
4372
- if (!leased.ok) {
4373
- release(store, lease, clock());
4374
- dispatched.push({ id: taskId, outcome: "failed", reason: leased.reason });
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 });
4375
3198
  broke++;
4376
- continue;
4377
- }
4378
- // THE SIGNED HEAD, BEFORE THE ATTEMPT IS SPENT (final admission
4379
- // closure): the continuation's terms signed the exact commit the
4380
- // parent finished at, and the leased worktree's HEAD is read here
4381
- // and held to it BEFORE admitAttended — the same order the queue's
4382
- // attended road keeps. A head the terms do not sign, or one that
4383
- // moved, opens no run, spends no attempt, invokes no provider: the
4384
- // worktree and claim are released and the authorization closes in
4385
- // the refusal's words — one terminal refusal, recorded once — so
4386
- // the operator authorizes again at today's head.
4387
- let signedContinuationHead = null;
4388
- try {
4389
- const terms = JSON.parse(continuation.termsJson);
4390
- signedContinuationHead = typeof terms.head === "string" && terms.head !== "" ? terms.head : null;
4391
- }
4392
- catch {
4393
- signedContinuationHead = null;
4394
- }
4395
- const continuationHeadRead = signedContinuationHead === null ? null : await git("git", ["rev-parse", "HEAD"], { cwd: leased.worktree.path });
4396
- if (continuationHeadRead !== null && continuationHeadRead.code !== 0) {
4397
- // The worktree's HEAD could not be READ — the head did not move,
4398
- // so nothing terminal is said about the authorization: custody is
4399
- // released and this tick records the failure; the next one reads
4400
- // again under the same open authorization.
4401
- await worktrees.release(leased.worktree.path, clock());
4402
- release(store, lease, clock());
4403
- 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` });
3199
+ break;
3200
+ case "malformed":
3201
+ dispatched.push({ id, outcome: "failed", reason: disposition.sealed ? "malformed-decision" : "fenced", worktree: leased.worktree.path });
4404
3202
  broke++;
4405
- continue;
4406
- }
4407
- const continuationHeadNow = continuationHeadRead === null ? "" : continuationHeadRead.stdout.trim();
4408
- if (signedContinuationHead === null || continuationHeadNow !== signedContinuationHead) {
4409
- await worktrees.release(leased.worktree.path, clock());
4410
- release(store, lease, clock());
4411
- store.closeAuthorization(continuation.id, "refused:stale-authorization", clock());
3203
+ break;
3204
+ case "failed":
4412
3205
  dispatched.push({
4413
- id: taskId,
4414
- outcome: "skipped",
4415
- reason: "stale-authorization",
4416
- detail: signedContinuationHead === null
4417
- ? `${taskId}: the continuation authorization ${continuation.id} signs no readable head — nothing opens under it; no run opened, no attempt spent`
4418
- : `${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`,
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,
4419
3215
  });
4420
- continue;
4421
- }
4422
- // The continuation spends under the authorization's pinned profile
4423
- // and says so at insert (v48 integrity).
4424
- // THE ATTENDED ADMISSION (atomic authority closure): the continuation
4425
- // opens under exactly this authorization — id, runner, generation —
4426
- // continuing the finished parent it names; the insert consumes the
4427
- // one attempt and binds the row to it.
4428
- const admittedContinuation = store.admitAttended({
4429
- taskRef: parent.taskRef,
4430
- leaseId: lease,
4431
- runner,
4432
- branch: parentBranch,
4433
- worktree: leased.worktree.path,
4434
- parentRun: parent.id,
4435
- provider: pinned.provider,
4436
- ...(pinned.model === null ? {} : { model: pinned.model }),
4437
- authorization: { id: continuation.id, runner: continuation.runner, generation: continuation.runnerGeneration },
4438
- now: clock(),
4439
- route: { routeDigest: `profile:${pinned.digest}`, phase: "build", provider: pinned.provider, model: pinned.model, chosen: "legacy" },
4440
- });
4441
- if (!admittedContinuation.ok) {
4442
- await worktrees.release(leased.worktree.path, clock());
4443
- release(store, lease, clock());
4444
- dispatched.push({ id: taskId, outcome: "skipped", reason: "admission-refused", detail: admittedContinuation.problem });
4445
- continue;
4446
- }
4447
- const runId = admittedContinuation.runId;
4448
- budgetGate = null; // this build's spend counts toward the next check
4449
- const result = await build(store, {
4450
- taskId,
4451
- taskRef: parent.taskRef,
4452
- runner,
4453
- leaseId: lease,
4454
- runnerToken: token,
4455
- runId,
4456
- evidenceRoot: context.evidenceRoot,
4457
- worktree: leased.worktree.path,
4458
- branch: parentBranch,
4459
- now: clock(),
4460
- clock,
4461
- provider: pinned.provider,
4462
- ...(pinned.model === null ? {} : { model: pinned.model }),
4463
- ...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
4464
- ...(context.gitRunner === undefined ? {} : { git: context.gitRunner }),
4465
- ...(context.shouldStop === undefined ? {} : { shouldStop: context.shouldStop }),
4466
- attended: {
4467
- authorization: continuation,
4468
- coordinator: context.heldCoordinator,
4469
- upIncarnation: context.upIncarnation ?? "unknown",
4470
- socketDir: context.heldSocketDir ?? tmpdir(),
4471
- releaseWorktree: async (path) => worktrees.release(path, clock()),
4472
- dispose: { repo, origin: parentRef.origin, provider: pinned.provider, model: pinned.model, policy: "continuation" },
4473
- ...(context.heldStarter === undefined ? {} : { starter: context.heldStarter }),
4474
- ...(context.heldGraceMs === undefined ? {} : { graceMs: context.heldGraceMs }),
4475
- ...(context.maxHeldSessions === undefined ? {} : { maxHeldSessions: context.maxHeldSessions }),
4476
- },
4477
- });
4478
- if (result.ok && result.parked === undefined && result.held === true) {
4479
- dispatched.push({ id: taskId, outcome: "held", branch: parentBranch, worktree: leased.worktree.path });
4480
- continue;
4481
- }
4482
- // A refusal before the hold (stale proof, spawn failure): record it
4483
- // through the continuation policy — taskless, always — and release.
4484
- await worktrees.release(leased.worktree.path, clock());
4485
- const disposition = disposeBuildOutcome({
4486
- store,
4487
- policy: "continuation",
4488
- leaseId: lease,
4489
- runId,
4490
- taskId,
4491
- taskRef: parent.taskRef,
4492
- runner,
4493
- repo,
4494
- branch: parentBranch,
4495
- origin: parentRef.origin,
4496
- provider: pinned.provider,
4497
- model: pinned.model,
4498
- worktreePath: leased.worktree.path,
4499
- evidenceRoot: context.evidenceRoot,
4500
- clock,
4501
- }, result);
4502
- dispatched.push({
4503
- id: taskId,
4504
- outcome: disposition.kind === "built" ? "built" : disposition.kind === "stopped" ? "stopped" : "failed",
4505
- ...(disposition.kind === "stopped" ? { reason: `stop:${disposition.stopRun}` } : result.ok ? {} : { reason: result.reason }),
4506
- worktree: leased.worktree.path,
4507
- });
4508
- if (disposition.kind !== "built" && disposition.kind !== "stopped")
4509
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 });
3233
+ broke++;
3234
+ break;
4510
3235
  }
4511
3236
  }
3237
+ untakenTrial?.giveBack();
4512
3238
  // The one automatic review each finished build gets when its project's
4513
3239
  // switch is on, before the result is handed on below. Other queued review
4514
3240
  // asks close unrun. Reauthenticated first; only admitted projects.
@@ -4532,11 +3258,6 @@ async function tickCommand(flags, context) {
4532
3258
  ...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
4533
3259
  const summary = () => {
4534
3260
  const lines = [`Considered ${considered}, built ${built}, parked ${parked}, broke ${broke}.`];
4535
- for (const entry of routines) {
4536
- lines.push(entry.outcome === "fired"
4537
- ? ` routine ${entry.routine.padEnd(16)} fired ${entry.taskId ?? ""}`.trimEnd()
4538
- : ` routine ${entry.routine.padEnd(16)} skipped ${entry.detail ?? entry.outcome}`);
4539
- }
4540
3261
  for (const entry of dispatched) {
4541
3262
  const detail = entry.outcome === "built"
4542
3263
  ? entry.committed === true
@@ -4561,21 +3282,19 @@ async function tickCommand(flags, context) {
4561
3282
  return fail(write, json, "tick", "build-failed", `${broke} of ${dispatched.length} dispatched tasks broke`, EXIT.failed, {
4562
3283
  considered,
4563
3284
  dispatched,
4564
- routines,
4565
3285
  });
4566
3286
  }
4567
- if (built > 0 || parked > 0 || dispatched.some(one => one.outcome === "planned" || one.outcome === "reported" || one.outcome === "held" || one.outcome === "reviewed" || one.outcome === "not-reviewed")) {
4568
- 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);
4569
3289
  }
4570
3290
  if (considered === 0) {
4571
3291
  return fail(write, json, "tick", "empty", "nothing is ready", EXIT.refused, {
4572
3292
  considered,
4573
3293
  dispatched,
4574
- routines,
4575
3294
  ...flows,
4576
3295
  });
4577
3296
  }
4578
- 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 });
4579
3298
  }
4580
3299
  /**
4581
3300
  * The recovery sweep: everything an unattended stretch may have left behind, in one pass.
@@ -5094,7 +3813,6 @@ async function startConsole(options) {
5094
3813
  ...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
5095
3814
  ...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
5096
3815
  ...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
5097
- ...(options.attended === undefined ? {} : { attended: options.attended }),
5098
3816
  });
5099
3817
  await new Promise((resolve, reject) => {
5100
3818
  server.once("error", reject);
@@ -5213,9 +3931,6 @@ async function requeueTask(positional, flags, context) {
5213
3931
  if (id === undefined) {
5214
3932
  return fail(write, json, "task requeue", "usage", "`toolroll task requeue <id> --as <you> --token <t>`", EXIT.usage);
5215
3933
  }
5216
- const racingGuard = refuseWhileRacing(context, "task requeue", id);
5217
- if (racingGuard !== null)
5218
- return racingGuard;
5219
3934
  const acting = await askCredentials(flags, context);
5220
3935
  if (acting === null) {
5221
3936
  return fail(write, json, "task requeue", "usage", "requeueing takes `--as <you> --token <t>` — who overrode the stall is recorded, not asserted", EXIT.usage);
@@ -5530,10 +4245,6 @@ async function providersCommand(flags, context) {
5530
4245
  const lastSuccess = store.providerLastSuccess(id);
5531
4246
  const keyPresent = facts.requiresEnv === null ? null : (process.env[facts.requiresEnv] ?? "") !== "";
5532
4247
  const installedVersion = installed ? (version.stdout.trim().split("\n")[0] ?? "") : null;
5533
- const authMode = readAuthMode(id);
5534
- const attestation = attestationOf(id);
5535
- const versionProvenAtSpawn = installedVersion !== null && attestation !== null && versionInRange(installedVersion, attestation);
5536
- const exhaustionRecognized = versionProvenAtSpawn && recognizesEligible(id, installedVersion, authMode);
5537
4248
  report.push({
5538
4249
  provider: id,
5539
4250
  binary: facts.binary,
@@ -5544,19 +4255,6 @@ async function providersCommand(flags, context) {
5544
4255
  ...(keyPresent === null ? {} : { keyPresent, keyEnv: facts.requiresEnv }),
5545
4256
  measuresCost: facts.measuresCost,
5546
4257
  configuredPhases: configured.get(id) ?? [],
5547
- fallbackReadiness: {
5548
- authMode,
5549
- versionProvenAtSpawn,
5550
- exhaustionRecognized,
5551
- automaticSwitchArmed: exhaustionRecognized,
5552
- reason: exhaustionRecognized
5553
- ? "this build can recognize an exhausted credential for the installed provider version"
5554
- : attestation === null
5555
- ? "this provider does not yet prove its version at spawn, so exhaustion classification fails closed"
5556
- : !versionProvenAtSpawn
5557
- ? "the installed provider version is not inside this build's attested range"
5558
- : "no reviewed exhaustion fixture recognizes this auth mode at the installed provider version",
5559
- },
5560
4258
  // The audit: facts about the harness, reported before any of them is
5561
4259
  // enforced. What transport we read, whether a session can resume,
5562
4260
  // which init signal exists, what hermetic flag we deliberately do NOT
@@ -5619,14 +4317,10 @@ async function providersCommand(flags, context) {
5619
4317
  if (one["keyEnv"] !== undefined) {
5620
4318
  write(` ${String(one["keyEnv"])} ${one["keyPresent"] === true ? "present (not validated — presence is not authorization)" : "ABSENT — runs will fail until the runner exports it"}`);
5621
4319
  }
5622
- 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"}`);
5623
4321
  const phases = one["configuredPhases"];
5624
4322
  if (phases.length > 0)
5625
4323
  write(` configured ${phases.join(", ")} (installation)`);
5626
- const fallback = one["fallbackReadiness"];
5627
- write(` auto fallback ${fallback.automaticSwitchArmed
5628
- ? "armed — a configured and approved chain may advance after proven exhaustion"
5629
- : `not armed — ${fallback.reason}`}`);
5630
4324
  const audit = one["audit"];
5631
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}`}`);
5632
4326
  write(` resume ${audit.resume}`);
@@ -5964,10 +4658,6 @@ async function modeCommand(positional, flags, context) {
5964
4658
  if (terms.planAuto && !terms.autoApproveFiling) {
5965
4659
  return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
5966
4660
  }
5967
- // The paid-fallback grant (R8): NEVER a preset default — only this
5968
- // explicit flag lets an exhausted subscription switch to another account.
5969
- if (flag(flags, "allow-paid-fallback"))
5970
- terms.allowPaidFallback = true;
5971
4661
  // Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
5972
4662
  if (flag(flags, "chat-approve"))
5973
4663
  terms.chatApprove = true;
@@ -6002,9 +4692,8 @@ async function configCommand(positional, flags, context) {
6002
4692
  : { problem: answer.problem }),
6003
4693
  };
6004
4694
  });
6005
- const fallback = scope === INSTALLATION_SCOPE ? [] : store.fallbackConfig(scope);
6006
4695
  // The STRONG tier (v47): the named strongest agent per phase, which
6007
- // high-risk, strict, evidence-sensitive, and publication-sensitive
4696
+ // large, strict, evidence-sensitive, and publication-sensitive
6008
4697
  // routes reach for. Never inferred — absent means "the default, said".
6009
4698
  const candidates = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
6010
4699
  const strong = ["plan", "build", "repair", "review"].map(one => ({
@@ -6015,7 +4704,7 @@ async function configCommand(positional, flags, context) {
6015
4704
  // A light planner from before light became build-only is never used: said, with how to clear it.
6016
4705
  const unusedLight = [INSTALLATION_SCOPE, ...(scope === INSTALLATION_SCOPE ? [] : [scope])].filter(one => store.phaseTierConfig(one, "plan", "light") !== null);
6017
4706
  if (json) {
6018
- 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) }));
6019
4708
  return EXIT.ok;
6020
4709
  }
6021
4710
  write(`Effective phase agents${scope === INSTALLATION_SCOPE ? "" : ` for ${scope}`}:`);
@@ -6028,7 +4717,7 @@ async function configCommand(positional, flags, context) {
6028
4717
  }
6029
4718
  }
6030
4719
  write("");
6031
- 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):");
6032
4721
  for (const one of strong) {
6033
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}]`}`);
6034
4723
  }
@@ -6051,14 +4740,6 @@ async function configCommand(positional, flags, context) {
6051
4740
  }
6052
4741
  }
6053
4742
  write(" repair note: the repair PROVIDER always inherits the build it mends — only its model is configurable.");
6054
- if (fallback.length > 0) {
6055
- write("");
6056
- write(" if the build agent's subscription runs out, NEW approvals bind this fallback chain:");
6057
- fallback.forEach((one, i) => {
6058
- write(` ${i + 1}. ${one.provider} (${one.model}) — ${one.authMode === "subscription" ? "its subscription login" : "your API key"}`);
6059
- });
6060
- write(" it fires only when a signed mode allows the paid fallback (`mode set --allow-paid-fallback`).");
6061
- }
6062
4743
  if (installation.length === 0 && project.length === 0) {
6063
4744
  write(" nothing configured — every phase runs the default (claude).");
6064
4745
  write(" toolroll config set build --provider claude --model sonnet --as <you> --token <t>");
@@ -6081,7 +4762,7 @@ async function configCommand(positional, flags, context) {
6081
4762
  return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
6082
4763
  }
6083
4764
  if (action === "clear") {
6084
- store.setSpendDefaults({ buildPerRunMicrousd: null, racePerAgentMicrousd: null, raceTotalMicrousd: null }, acting.name, clock());
4765
+ store.setSpendDefaults({ buildPerRunMicrousd: null }, acting.name, clock());
6085
4766
  return succeed(write, json, "config clear", { budgets: null }, () => ["Spend defaults cleared — filings state their own numbers again."]);
6086
4767
  }
6087
4768
  const parseUsd = (flag) => {
@@ -6092,117 +4773,13 @@ async function configCommand(positional, flags, context) {
6092
4773
  return Number.isFinite(value) && value > 0 ? Math.round(value * 1_000_000) : false;
6093
4774
  };
6094
4775
  const build = parseUsd("build-usd");
6095
- const racePer = parseUsd("race-per-usd");
6096
- const raceTotal = parseUsd("race-total-usd");
6097
- // Name the flag that was bad — "budgets are positive dollar amounts"
6098
- // over four candidates left the caller diffing their own command line.
6099
- const badFlag = build === false ? "--build-usd" : racePer === false ? "--race-per-usd" : raceTotal === false ? "--race-total-usd" : null;
6100
- if (badFlag !== null || build === false || racePer === false || raceTotal === false) {
6101
- return fail(write, json, "config set", "usage", `${badFlag ?? "--build-usd"} is a positive dollar amount`, EXIT.usage);
6102
- }
6103
- // The default competing-agent count (operator request): applied only
6104
- // where a filing names one agent and no explicit count; a race digest
6105
- // always binds the actual lineup.
6106
- const agentsGiven = text(flags, "race-agents");
6107
- const raceAgents = agentsGiven === undefined ? null : Number(agentsGiven);
6108
- if (raceAgents !== null && (!Number.isInteger(raceAgents) || raceAgents < 2 || raceAgents > 4)) {
6109
- 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);
6110
- }
6111
- 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());
6112
4780
  return succeed(write, json, "config set", { budgets: store.getSpendDefaults() }, () => [
6113
4781
  "Spend defaults set. New filings pre-fill from these; every approval still restates its own numbers:",
6114
4782
  ...(build === null ? [] : [` each ordinary build attempt: $${(build / 1_000_000).toFixed(2)} (also the installation backstop)`]),
6115
- ...(racePer === null ? [] : [` each tournament agent: $${(racePer / 1_000_000).toFixed(2)}`]),
6116
- ...(raceTotal === null ? [] : [` each tournament total: $${(raceTotal / 1_000_000).toFixed(2)}`]),
6117
- ...(raceAgents === null ? [] : [` tournaments race ${raceAgents} agents unless a filing says otherwise`]),
6118
- ]);
6119
- }
6120
- // The FALLBACK CHAIN (Layer F): the ordered entries a repository's NEW
6121
- // approvals bind after the base agent. Per-repo only — a chain names
6122
- // credentials for one project's work — and inert on its own: it only
6123
- // ever fires when a signed mode also allows the paid fallback.
6124
- if (phase === "fallback") {
6125
- const acting = await askCredentials(flags, context);
6126
- if (acting === null) {
6127
- return fail(write, json, `config ${action}`, "usage", "configuring fallbacks takes `--as <you> --token <t>`", EXIT.usage);
6128
- }
6129
- const authedFallback = authenticateApprover(store, acting.name, acting.token);
6130
- if (!authedFallback.ok) {
6131
- return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
6132
- }
6133
- if (scope === INSTALLATION_SCOPE) {
6134
- return fail(write, json, `config ${action}`, "usage", "fallbacks are per repository — say --repo <path>", EXIT.usage);
6135
- }
6136
- if (action === "clear") {
6137
- const had = store.clearFallbackConfig(scope);
6138
- return succeed(write, json, "config clear", { repo: scope, fallback: null }, () => [
6139
- had
6140
- ? `${scope}: fallback chain cleared — new approvals bind the single configured agent again.`
6141
- : `${scope}: no fallback chain was configured.`,
6142
- ]);
6143
- }
6144
- const entriesGiven = text(flags, "entries");
6145
- if (entriesGiven === undefined) {
6146
- 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);
6147
- }
6148
- const entries = [];
6149
- for (const one of entriesGiven.split(",")) {
6150
- // First and LAST colon split the three parts (F+G review, finding 1):
6151
- // model ids legitimately carry colons (openrouter's ":free" suffixes),
6152
- // so the model is everything between the provider and the auth mode.
6153
- const trimmed = one.trim();
6154
- const firstColon = trimmed.indexOf(":");
6155
- const lastColon = trimmed.lastIndexOf(":");
6156
- if (firstColon === -1 || lastColon === firstColon) {
6157
- return fail(write, json, "config set", "usage", `"${trimmed}" is not provider:model:auth-mode`, EXIT.usage);
6158
- }
6159
- const provider = trimmed.slice(0, firstColon);
6160
- const model = trimmed.slice(firstColon + 1, lastColon);
6161
- const authMode = trimmed.slice(lastColon + 1);
6162
- if (!isProviderId(provider)) {
6163
- return fail(write, json, "config set", "usage", `unknown provider "${provider}" — one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
6164
- }
6165
- if (authMode !== "subscription" && authMode !== "api-key") {
6166
- return fail(write, json, "config set", "usage", `auth-mode is subscription or api-key, not "${authMode}"`, EXIT.usage);
6167
- }
6168
- if (authMode === "subscription" && !SUBSCRIPTION_CAPABLE[provider]) {
6169
- return fail(write, json, "config set", "usage", `${provider} has no subscription login — this entry must use api-key`, EXIT.usage);
6170
- }
6171
- // The model rides provider argv: the SAME argv-safety validation every
6172
- // other sealed model passes (never a leading dash or control bytes).
6173
- const argvSafe = validateSpec({ provider, model: model === "" ? null : model });
6174
- if (model === "" || !argvSafe.ok) {
6175
- 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);
6176
- }
6177
- entries.push({ provider, model, authMode });
6178
- }
6179
- if (entries.length < 1 || entries.length > 3) {
6180
- return fail(write, json, "config set", "usage", "1 to 3 fallback entries (the approval binds the whole chain)", EXIT.usage);
6181
- }
6182
- // The WHOLE chain must file against the CURRENT base (F+G review,
6183
- // finding 4): a fallback that duplicates the base — or another entry —
6184
- // refuses NOW, in words, not at some future filing. Set, prove through
6185
- // the same resolver filing uses, and restore the old config on refusal.
6186
- const previous = store.fallbackConfig(scope);
6187
- store.setFallbackConfig(scope, entries, acting.name, clock());
6188
- const baseAgent = resolvePhaseAgent(store, "build", scope, {});
6189
- if (baseAgent.ok) {
6190
- const proven = resolveScopeChain(store, scope, undefined, {}, readAuthMode(baseAgent.spec.provider));
6191
- // An UNRESOLVED base (no explicit routing configured yet) cannot be
6192
- // duplicate-checked — the filing-time resolver holds that line; only
6193
- // a chain that provably cannot file refuses here.
6194
- if (!proven.ok && proven.reason !== "base-unresolved") {
6195
- if (previous.length > 0)
6196
- store.setFallbackConfig(scope, previous, acting.name, clock());
6197
- else
6198
- store.clearFallbackConfig(scope);
6199
- return fail(write, json, "config set", "usage", `this chain cannot file: ${proven.problem}`, EXIT.usage);
6200
- }
6201
- }
6202
- return succeed(write, json, "config set", { repo: scope, fallback: entries }, () => [
6203
- `${scope}: fallback chain set — NEW approvals bind it; existing approvals are untouched.`,
6204
- ...entries.map((one, i) => ` ${i + 1}. ${one.provider} (${one.model}) — ${one.authMode === "subscription" ? "its subscription login" : "your API key"}`),
6205
- " it fires only when a signed mode allows the paid fallback: `toolroll mode set --allow-paid-fallback …`",
6206
4783
  ]);
6207
4784
  }
6208
4785
  // Chat is its OWN configuration, deliberately not a phase (Codex v3
@@ -6286,7 +4863,7 @@ async function configCommand(positional, flags, context) {
6286
4863
  return fail(write, json, `config ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, phase), EXIT.refused);
6287
4864
  }
6288
4865
  // The STRONG tier (v47): a second, named row per phase the routing policy
6289
- // reaches for when risk, quality, evidence, or publication demand it.
4866
+ // reaches for when size, quality, evidence, or publication demand it.
6290
4867
  // Authenticated and audited exactly like the routine row; existing
6291
4868
  // approvals are untouched — a sealed route never re-resolves.
6292
4869
  // The LIGHT tier (v2): the fast agent a small change builds on.
@@ -6380,7 +4957,7 @@ async function configCommand(positional, flags, context) {
6380
4957
  store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
6381
4958
  return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
6382
4959
  `strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
6383
- " 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.",
6384
4961
  ...(phase === "repair" ? [" a strong repair agent applies only when its provider is the build's — cross-provider repair does not exist."] : []),
6385
4962
  ]);
6386
4963
  }
@@ -6393,7 +4970,7 @@ async function configCommand(positional, flags, context) {
6393
4970
  warnings.push("OPENROUTER_API_KEY is not present in this environment — runs will fail until the runner exports it.");
6394
4971
  }
6395
4972
  if (providerGiven !== "claude") {
6396
- 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.`);
6397
4974
  }
6398
4975
  return succeed(write, json, "config set", { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
6399
4976
  `${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} now runs ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
@@ -6964,80 +5541,11 @@ async function intakeCommand(positional, flags, context) {
6964
5541
  ]);
6965
5542
  }
6966
5543
  /**
6967
- * `toolroll contest …` — the tournament from the terminal: `show`
6968
- * for the machine-readable state, `exclude` to stop a racing agent whose
6969
- * question you will not answer (authenticated: it cancels paid-for work
6970
- * and un-sticks the race). The pick itself stays a console ceremony.
6971
- */
6972
- function contestCommand(positional, flags, context) {
6973
- const { store, write, json, clock } = context;
6974
- const [action, idGiven, ordinalGiven] = positional;
6975
- if (action === undefined || !CONTEST_ACTIONS.includes(action)) {
6976
- return fail(write, json, "contest", "usage", `unknown \`contest ${action ?? ""}\` — try ${CONTEST_ACTIONS.join(", ")}`, EXIT.usage);
6977
- }
6978
- if (action === "show") {
6979
- const contest = store.getContest(Number(idGiven));
6980
- if (contest === null)
6981
- return fail(write, json, "contest show", "unknown", "no tournament or comparison with that id", EXIT.refused);
6982
- const agents = store.contestants(contest.id);
6983
- if (json) {
6984
- write(envelopeJson({ ok: true, command: "contest show", contest, agents }));
6985
- return EXIT.ok;
6986
- }
6987
- write(`${contestNoun(contest.kind)} #${contest.id} — ${contest.state}`);
6988
- for (const racer of agents) {
6989
- const money = contest.kind === "comparison"
6990
- ? racer.unknownSpend
6991
- ? "spend unmeasured (tokens only)"
6992
- : `$${(racer.measuredMicrousd / 1_000_000).toFixed(2)} measured`
6993
- : `charged $${(racer.accountedMicrousd / 1_000_000).toFixed(2)}${racer.unknownSpend ? " (exact figure unknown — charged the reserved worst case)" : ""}`;
6994
- write(` agent ${racer.ordinal}: ${racer.provider} · ${racer.model} — ${racer.state} · ${money}`);
6995
- }
6996
- return EXIT.ok;
6997
- }
6998
- if (action === "exclude") {
6999
- return (async () => {
7000
- const contest = store.getContest(Number(idGiven));
7001
- const ordinal = Number(ordinalGiven);
7002
- if (contest === null || !Number.isInteger(ordinal)) {
7003
- return fail(write, json, "contest exclude", "usage", "`toolroll contest exclude <tournament-id> <agent-number> --as <you> --token <t>`", EXIT.usage);
7004
- }
7005
- const acting = await askCredentials(flags, context);
7006
- if (acting === null) {
7007
- return fail(write, json, "contest exclude", "usage", "stopping a racing agent takes `--as <you> --token <t>`", EXIT.usage);
7008
- }
7009
- const authed = authenticateApprover(store, acting.name, acting.token);
7010
- if (!authed.ok) {
7011
- return fail(write, json, "contest exclude", "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
7012
- }
7013
- const racer = store.contestants(contest.id).find(one => one.ordinal === ordinal);
7014
- if (racer === undefined || racer.state !== "parked") {
7015
- return fail(write, json, "contest exclude", "not-waiting", "that agent is not waiting on an answer", EXIT.refused);
7016
- }
7017
- const question = store.openDecisionForContestant(racer.id);
7018
- const moved = store.transact(() => {
7019
- if (question !== null && !store.excludeDecision(question, acting.name, clock()))
7020
- return false;
7021
- if (!store.casContestantState(racer.id, ["parked"], "stopped", racer.generation))
7022
- return false;
7023
- contestMaybeAggregate(store, contest.id, clock());
7024
- return true;
7025
- });
7026
- if (!moved)
7027
- return fail(write, json, "contest exclude", "changed", "the tournament moved while you were reading — look again", EXIT.refused);
7028
- const after = store.getContest(contest.id);
7029
- return succeed(write, json, "contest exclude", { contest: after }, () => [
7030
- `Agent ${ordinal} stopped; its question is closed as excluded. The tournament is now ${after?.state ?? "?"}.`,
7031
- ]);
7032
- })();
7033
- }
7034
- return fail(write, json, "contest", "usage", "`toolroll contest show <id> | exclude <id> <agent-number>`", EXIT.usage);
7035
- }
7036
- /**
7037
- * `toolroll template …` — the shipped library of common standing
7038
- * orders (adoption track, step 2). A template is a pre-filled form:
7039
- * `apply` PREVIEWS by default and files only under `--file`, through the
7040
- * 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
7041
5549
  * (issue-intake, ci-babysitter) display existing ceremonies and cannot be
7042
5550
  * applied — the authority they would need is a separate authenticated act
7043
5551
  * a template must never perform (adoption review, finding 10).
@@ -7054,7 +5562,7 @@ function templateCommand(positional, flags, context) {
7054
5562
  }));
7055
5563
  return EXIT.ok;
7056
5564
  }
7057
- write("Templates — common routines you edit to fit. Nothing a template");
5565
+ write("Templates — common work you edit to fit. Nothing a template");
7058
5566
  write("files is approved; recipes only show existing ceremonies.");
7059
5567
  write("");
7060
5568
  for (const one of TEMPLATES) {
@@ -7096,8 +5604,8 @@ function templateCommand(positional, flags, context) {
7096
5604
  write(` title ${template.title}`);
7097
5605
  }
7098
5606
  else {
7099
- write(` files one routine (cannot fire until you approve its terms)`);
7100
- write(` name ${template.routineName}`);
5607
+ write(` makes a scheduled flow (nothing repeats until you turn the schedule on)`);
5608
+ write(` name ${template.title}`);
7101
5609
  write(` schedule ${template.schedule}`);
7102
5610
  }
7103
5611
  write(` goal ${template.goal}`);
@@ -7167,273 +5675,48 @@ function templateCommand(positional, flags, context) {
7167
5675
  ...(filedLink === null ? [] : [` ${filedLink}`]),
7168
5676
  ]);
7169
5677
  }
7170
- const routineName = text(flags, "name") ?? template.routineName;
7171
- 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;
7172
5681
  const ceilingGiven = text(flags, "ceiling");
7173
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 };
7174
5685
  if (!flags.has("file")) {
7175
- const draft = {
7176
- kind: "routine",
7177
- name: routineName,
7178
- repo: canonicalProject(repoGiven) ?? resolve(repoGiven),
7179
- goal,
7180
- outOfScope,
7181
- touches,
7182
- acceptance: template.acceptance,
7183
- requirements: template.requirements,
7184
- schedule,
7185
- costCeilingUsd,
7186
- filedVia: `template:${template.name}`,
7187
- };
5686
+ const draft = { kind: "scheduled flow", name: flowName, repo, schedule, ...terms, filedVia: `template:${template.name}` };
7188
5687
  if (json) {
7189
5688
  write(envelopeJson({ ok: false, command: "template apply", reason: "unconfirmed", draft }));
7190
5689
  return 3;
7191
5690
  }
7192
- 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):");
7193
5693
  write("");
7194
- write(` routine ${draft.name}`);
5694
+ write(` flow ${draft.name}`);
7195
5695
  write(` repo ${draft.repo}`);
7196
- write(` schedule ${draft.schedule}${draft.schedule.startsWith("every:10080") ? " (weekly)" : ""}`);
5696
+ write(` schedule ${readable === null ? schedule : describeSchedule(readable)}`);
7197
5697
  write(` goal ${draft.goal}`);
7198
5698
  if (draft.outOfScope !== null)
7199
5699
  write(` not ${draft.outOfScope}`);
7200
5700
  if (draft.touches.length > 0)
7201
5701
  write(` touches ${draft.touches.join(", ")}`);
7202
5702
  if (draft.costCeilingUsd !== null)
7203
- write(` ceiling $${draft.costCeilingUsd}/week`);
5703
+ write(` ceiling $${draft.costCeilingUsd} every 7 days`);
7204
5704
  write("");
7205
- 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.");
7206
5706
  return 3;
7207
5707
  }
7208
- const made = fileRoutineProposal(store, {
7209
- name: routineName,
7210
- repo: repoGiven,
7211
- goal,
7212
- outOfScope,
7213
- touches,
7214
- acceptance: template.acceptance,
7215
- requirements: template.requirements,
7216
- schedule,
7217
- costCeilingUsd,
7218
- filedVia: `template:${template.name}`,
7219
- }, 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());
7220
5709
  if (!made.ok)
7221
- return fail(write, json, "template apply", made.reason, made.message, made.reason === "duplicate" ? EXIT.refused : EXIT.usage);
7222
- return succeed(write, json, "template apply", { filed: "routine", id: made.id, approved: false }, () => [
7223
- `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}.`,
7224
5714
  "",
7225
- "UNAPPROVED — NO AUTHORITY GRANTED. It cannot fire until you approve the standing order:",
7226
- ` 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}`]),
7227
5718
  ]);
7228
5719
  }
7229
- /**
7230
- * `toolroll routine …` — standing orders. Filing one is cheap; the
7231
- * expensive act is the approval, which restates every term including "each
7232
- * firing builds without asking" and takes the approver's credential, same
7233
- * as a scope. Pausing needs no ceremony because stopping spend never does.
7234
- */
7235
- async function routineCommand(positional, flags, context) {
7236
- const { store, write, json, clock } = context;
7237
- const [action, name] = positional;
7238
- if (action !== undefined && !ROUTINE_ACTIONS.includes(action)) {
7239
- return fail(write, json, "routine", "usage", `unknown \`routine ${action}\` — try ${ROUTINE_ACTIONS.join(", ")}`, EXIT.usage);
7240
- }
7241
- if (action === undefined || action === "list") {
7242
- const repoFilter = text(flags, "repo");
7243
- const routines = store.listRoutines(repoFilter === undefined ? null : canonicalProject(repoFilter) ?? resolve(repoFilter));
7244
- if (json) {
7245
- write(envelopeJson({ ok: true, command: "routine list", routines }));
7246
- return EXIT.ok;
7247
- }
7248
- if (routines.length === 0) {
7249
- write("No routines. `toolroll routine add <name> --repo <path> --goal <text> --schedule every:60` files one.");
7250
- return EXIT.ok;
7251
- }
7252
- for (const routine of routines) {
7253
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
7254
- const status = routine.paused ? "paused" : approved ? "live" : "awaiting approval";
7255
- write(` ${routine.name.padEnd(20)} ${status.padEnd(18)} ${routine.schedule.padEnd(14)} ${routine.repo}`);
7256
- }
7257
- return EXIT.ok;
7258
- }
7259
- if (action === "add") {
7260
- if (name === undefined || !ROUTINE_NAME.test(name)) {
7261
- 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);
7262
- }
7263
- const repoGiven = text(flags, "repo");
7264
- const goal = text(flags, "goal");
7265
- const schedule = text(flags, "schedule");
7266
- if (repoGiven === undefined || goal === undefined || schedule === undefined) {
7267
- 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);
7268
- }
7269
- const acceptanceGiven = text(flags, "acceptance");
7270
- if (acceptanceGiven === undefined) {
7271
- 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);
7272
- }
7273
- const ceilingGiven = text(flags, "ceiling");
7274
- // --budget-usd on a routine caps EACH instance (v16): it becomes the
7275
- // instance scope's digest-bound budget term, enforced by the same
7276
- // native-cap plumbing as any other scope budget.
7277
- const perRunGiven = text(flags, "budget-usd");
7278
- if (perRunGiven !== undefined && (!Number.isFinite(Number(perRunGiven)) || Number(perRunGiven) <= 0)) {
7279
- return fail(write, json, "routine add", "bad-budget", "--budget-usd is a positive dollar amount — what each firing may spend", EXIT.usage);
7280
- }
7281
- // One filing door for every surface (Codex adoption review, finding 7):
7282
- // validation, canonicalization, digest, and provenance live in the
7283
- // service, not here.
7284
- const created = fileRoutineProposal(store, {
7285
- name,
7286
- repo: repoGiven,
7287
- goal,
7288
- outOfScope: text(flags, "not") ?? null,
7289
- touches: (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(one => one !== ""),
7290
- acceptance: acceptanceLinesToInput(splitAcceptanceRubric(acceptanceGiven)),
7291
- requirements: (text(flags, "require") ?? "").split(",").map(one => one.trim()).filter(one => one !== ""),
7292
- schedule,
7293
- costCeilingUsd: ceilingGiven === undefined ? null : Number(ceilingGiven),
7294
- ...(perRunGiven === undefined ? {} : { budgetPerRunMicrousd: Math.round(Number(perRunGiven) * 1_000_000) }),
7295
- filedVia: "cli",
7296
- }, clock());
7297
- if (!created.ok) {
7298
- return fail(write, json, "routine add", created.reason, created.message, created.reason === "duplicate" ? EXIT.refused : EXIT.usage);
7299
- }
7300
- const routine = store.getRoutine(created.id);
7301
- return succeed(write, json, "routine add", { routine }, () => [
7302
- `Filed ${name}. Nothing fires until somebody approves the standing order:`,
7303
- ...(routine === null ? [] : describeRoutine(routine)),
7304
- "",
7305
- ` toolroll routine approve ${name}`,
7306
- ]);
7307
- }
7308
- if (name === undefined) {
7309
- return fail(write, json, `routine ${action}`, "usage", "which routine? give its name", EXIT.usage);
7310
- }
7311
- const routine = store.routineByName(name);
7312
- if (routine === null) {
7313
- return fail(write, json, `routine ${action}`, "unknown", `no routine named ${name}`, EXIT.refused);
7314
- }
7315
- switch (action) {
7316
- case "show": {
7317
- const fires = store.routineFires(routine.id, 14);
7318
- if (json) {
7319
- write(envelopeJson({ ok: true, command: "routine show", routine, fires }));
7320
- return EXIT.ok;
7321
- }
7322
- const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
7323
- write(`${routine.name} — ${routine.paused ? "paused" : approved ? "live" : "awaiting approval"}`);
7324
- for (const line of describeRoutine(routine))
7325
- write(line);
7326
- if (routine.nextFireAt !== null && !routine.paused && approved)
7327
- write(` next fire ${routine.nextFireAt}`);
7328
- if (fires.length > 0) {
7329
- write("");
7330
- write(" recent firings, newest first:");
7331
- for (const fire of fires) {
7332
- const said = fire.outcome === "fired"
7333
- ? `${fire.instanceTaskId ?? "instance"}${fire.instanceState === null ? "" : ` (${fire.instanceState})`}${fire.reason === "manual" ? " (run now)" : ""}`
7334
- : `skipped — ${fire.reason ?? ""}`;
7335
- write(` ${fire.scheduledFor.replace(/^manual:/, "")} ${said}`);
7336
- }
7337
- }
7338
- return EXIT.ok;
7339
- }
7340
- case "approve": {
7341
- let saw = text(flags, "digest");
7342
- let { name: asWho, token } = credentialsFrom(flags, context);
7343
- let confirmedAloud = false;
7344
- if ((!flags.has("yes") || saw === undefined || asWho === undefined || token === undefined) && interactive() && !json) {
7345
- write(`Approving ${name} makes it a STANDING order:`);
7346
- write("");
7347
- for (const line of describeRoutine(routine))
7348
- write(line);
7349
- write("");
7350
- const agreed = await confirm("Approve exactly this standing order?");
7351
- if (!agreed) {
7352
- write("Nothing approved.");
7353
- return EXIT.refused;
7354
- }
7355
- saw ??= routine.digest;
7356
- const acting = await askCredentials(flags, context);
7357
- if (acting === null)
7358
- return fail(write, json, "routine approve", "usage", "approval needs who is agreeing", EXIT.usage);
7359
- asWho = acting.name;
7360
- token = acting.token;
7361
- confirmedAloud = true;
7362
- }
7363
- const armed = (flags.has("yes") || confirmedAloud) && saw !== undefined && asWho !== undefined && token !== undefined;
7364
- if (!armed) {
7365
- if (json) {
7366
- write(envelopeJson({ ok: false, command: "routine approve", reason: "unconfirmed", routine }));
7367
- return EXIT.refused;
7368
- }
7369
- write(`Would approve this standing order — every firing of it builds without asking:`);
7370
- write("");
7371
- for (const line of describeRoutine(routine))
7372
- write(line);
7373
- write("");
7374
- write("Nothing has been approved. Agree to exactly this with:");
7375
- write(` toolroll routine approve ${name} --yes --digest ${routine.digest} --as <you> --token <your password>`);
7376
- // A preview reached by omitting --yes is the answer "no, not yet" —
7377
- // exit 3 in both modes, matching the JSON path (round-4 finding 10).
7378
- return EXIT.refused;
7379
- }
7380
- const approved = approveRoutine(store, routine.id, asWho, clock(), saw, token);
7381
- if (!approved.ok) {
7382
- return fail(write, json, "routine approve", approved.reason, describeApproveFailure(approved.reason, name), EXIT.refused);
7383
- }
7384
- return succeed(write, json, "routine approve", { routine: approved.routine }, () => [
7385
- `Approved. ${name} fires on its schedule from now on; first at ${approved.routine.nextFireAt}.`,
7386
- `Pause it any time: toolroll routine pause ${name}`,
7387
- ]);
7388
- }
7389
- case "refresh": {
7390
- // THE RECOVERY ROAD (v48): re-resolve the agents from today's
7391
- // configuration and file them as the order's working agents. Nothing
7392
- // is approved here — the refreshed order waits for the yes.
7393
- const before = routineAgentsState(routine);
7394
- const refreshed = refreshRoutineAgents(store, routine.id, clock());
7395
- if (!refreshed.ok) {
7396
- return fail(write, json, "routine refresh", refreshed.reason, `${name}: ${refreshed.problem}`, EXIT.refused);
7397
- }
7398
- return succeed(write, json, "routine refresh", { routine: refreshed.routine, changed: refreshed.changed, before: before.state }, () => [
7399
- refreshed.changed
7400
- ? `Refreshed ${name}'s agents from today's configuration. Nothing is approved yet — read them and agree:`
7401
- : `${name} already names exactly these agents; nothing changed.`,
7402
- ...describeRoutine(refreshed.routine),
7403
- "",
7404
- ` toolroll routine approve ${name}`,
7405
- ]);
7406
- }
7407
- case "pause":
7408
- case "resume": {
7409
- store.setRoutinePaused(routine.id, action === "pause", clock());
7410
- return succeed(write, json, `routine ${action}`, { name }, () => [
7411
- action === "pause"
7412
- ? `${name} is paused — no firing until you resume it. Already-running instances finish.`
7413
- : `${name} resumed — the next due slot fires again.`,
7414
- ]);
7415
- }
7416
- case "run-now": {
7417
- const acting = await askCredentials(flags, context);
7418
- if (acting === null) {
7419
- return fail(write, json, "routine run-now", "usage", "run-now takes `--as <you> --token <t>` — it dispatches work that spends", EXIT.usage);
7420
- }
7421
- const authenticated = authenticateApprover(store, acting.name, acting.token);
7422
- if (!authenticated.ok) {
7423
- return fail(write, json, "routine run-now", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
7424
- }
7425
- const outcome = fireRoutine(store, routine.id, clock(), { manual: true });
7426
- if (!outcome.ok) {
7427
- return fail(write, json, "routine run-now", outcome.reason, outcome.detail ?? outcome.reason, EXIT.refused);
7428
- }
7429
- return succeed(write, json, "routine run-now", { taskId: outcome.taskId }, () => [
7430
- `Spawned ${outcome.taskId} — it builds on the next pass. The regular schedule is untouched.`,
7431
- ]);
7432
- }
7433
- default:
7434
- return fail(write, json, "routine", "usage", "`toolroll routine [add|list|show|approve|refresh|pause|resume|run-now]`", EXIT.usage);
7435
- }
7436
- }
7437
5720
  /** A credential from a 0600 file, for units that must not carry it inline. */
7438
5721
  function readTokenFile(path) {
7439
5722
  if (path === undefined)
@@ -7761,21 +6044,18 @@ async function runWatchLoop(args) {
7761
6044
  const slackFollower = followSlack({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7762
6045
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7763
6046
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7764
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7765
6047
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7766
6048
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "slack",
7767
6049
  }).catch(() => progress("watch: Slack stopped. Check Slack settings before reconnecting."));
7768
6050
  const teamsFollower = followTeams({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7769
6051
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7770
6052
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7771
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7772
6053
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7773
6054
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "teams",
7774
6055
  }).catch(() => progress("watch: Teams stopped. Check Teams settings before reconnecting."));
7775
6056
  const discordFollower = followDiscord({ store, dir: dirname(context.databaseFile), signal: followController.signal,
7776
6057
  readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
7777
6058
  ...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
7778
- ...(context.heldCoordinator ? { held: context.heldCoordinator } : {}),
7779
6059
  origin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: text(flags, "public-url") ?? null }),
7780
6060
  notifications: () => effectivePrimary(process.env, dirname(context.databaseFile), loadBotToken(process.env, context.telegramTokenFile) !== null).channel === "discord",
7781
6061
  }).catch(() => progress("watch: Discord stopped. Check Discord settings before reconnecting."));
@@ -8638,7 +6918,6 @@ async function upCommand(flags, context) {
8638
6918
  if ("error" in loadedRegistry) {
8639
6919
  return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
8640
6920
  }
8641
- let sweptHeldOrphans = false;
8642
6921
  const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
8643
6922
  const rootInputs = (text(flags, "project-root") ?? "")
8644
6923
  .split(",")
@@ -8752,18 +7031,6 @@ async function upCommand(flags, context) {
8752
7031
  for (let attempt = 1; attempt <= 5; attempt += 1) {
8753
7032
  const suffix = attempt === 1 ? "" : `-${attempt}`;
8754
7033
  const name = attempt === 1 ? base : `${base.slice(0, RUNNER_NAME_MAX - suffix.length)}${suffix}`;
8755
- // The held orphan fence runs BEFORE any recovery road (v6 W6): a
8756
- // crashed predecessor's held session is seized, killed through its
8757
- // supervisor, and settled — or paged — before registration's
8758
- // recovery may touch its run or worktree.
8759
- if (!sweptHeldOrphans) {
8760
- sweptHeldOrphans = true;
8761
- const swept = await sweepHeldOrphans(store, `up:${hostname()}:${process.pid}`, clock);
8762
- if (swept.fenced > 0)
8763
- progress(`fenced ${swept.fenced} orphaned held session(s) from a previous up`);
8764
- if (swept.paged > 0)
8765
- progress(`${swept.paged} held session(s) could not be stopped — see the inbox`);
8766
- }
8767
7034
  // The runner's authority is BOUND to the canonical roots this up
8768
7035
  // serves (MCP spec v6): the claim gate enforces membership, so an
8769
7036
  // unbound registration would deny-all its own dispatches.
@@ -8810,28 +7077,6 @@ async function upCommand(flags, context) {
8810
7077
  finally {
8811
7078
  await new Promise(done => probe.close(() => done()));
8812
7079
  }
8813
- // 6b. The held-session coordinator (Phase 2): one per up process, shared
8814
- // by the console and every watch loop through the context. Its socket
8815
- // directory is deliberately SHORT and flat — sun_path is unforgiving.
8816
- const heldDir = namedPath(homedir(), ["held"], { dot: true });
8817
- try {
8818
- mkdirSync(heldDir, { recursive: true, mode: 0o700 });
8819
- }
8820
- catch {
8821
- // The launch's own path check refuses with words if this failed.
8822
- }
8823
- context.heldCoordinator = new HeldSessionCoordinator();
8824
- context.upIncarnation = randomUUID();
8825
- context.heldSocketDir = heldDir;
8826
- // v28: sessions are unbounded unless the operator caps them.
8827
- const heldCap = text(flags, "max-held-sessions");
8828
- if (heldCap !== undefined) {
8829
- const cap = Number(heldCap);
8830
- if (!Number.isInteger(cap) || cap < 1) {
8831
- return fail(write, json, "up", "usage", "--max-held-sessions is a whole number of concurrent attended sessions, at least 1", EXIT.usage);
8832
- }
8833
- context.maxHeldSessions = cap;
8834
- }
8835
7080
  // 7. The console, on the just-released port. The tiny window between the
8836
7081
  // probe closing and this bind can lose a race; that failure tears down
8837
7082
  // cleanly below instead of leaving identities half-claimed silently.
@@ -8854,14 +7099,6 @@ async function upCommand(flags, context) {
8854
7099
  currentRepos: () => [...activeRepos],
8855
7100
  admittedRepos: () => [...activeRepos],
8856
7101
  upConsole: true,
8857
- attended: {
8858
- runner: runnerName,
8859
- coordinator: context.heldCoordinator,
8860
- headOf: async (repo) => {
8861
- const answer = await gitRun("git", ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: repo });
8862
- return answer.code === 0 ? answer.stdout.trim() : null;
8863
- },
8864
- },
8865
7102
  registryPath,
8866
7103
  projectRoots,
8867
7104
  ...(text(flags, "public-url") === undefined ? {} : { publicUrl: text(flags, "public-url") }),
@@ -9203,23 +7440,6 @@ async function upCommand(flags, context) {
9203
7440
  clearTimeout(graceTimer);
9204
7441
  process.removeListener("SIGINT", stop);
9205
7442
  process.removeListener("SIGTERM", stop);
9206
- // Held sessions fence before the runner retires (v2 S0f): bounded, and
9207
- // every controller settles conservatively or is paged.
9208
- if (context.heldCoordinator !== undefined) {
9209
- const unsettled = await context.heldCoordinator.close();
9210
- for (const runId of unsettled) {
9211
- // The shutdown deadline won: say so durably — the orphan sweep of
9212
- // the NEXT up owns the cleanup, and silence would contradict
9213
- // "settled conservatively or paged".
9214
- store.enqueueNotification({
9215
- source: { run: runId },
9216
- dedupeKey: `held-shutdown-unsettled:${runId}`,
9217
- kind: "attended-unsettled",
9218
- subject: `an attended session did not settle before shutdown (run #${runId})`,
9219
- 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.`,
9220
- }, clock());
9221
- }
9222
- }
9223
7443
  retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
9224
7444
  await new Promise(done => console_.server.close(() => done()));
9225
7445
  if (fatal !== null) {
@@ -10776,7 +8996,6 @@ function showTask(positional, context) {
10776
8996
  claim: currentClaim(store, ref.id, now),
10777
8997
  scope,
10778
8998
  approval: approvalOf(scope),
10779
- risk: ref.riskLevel ?? scope?.riskLevel ?? "routine",
10780
8999
  route: taskRouteView(routed, readiness),
10781
9000
  runs: runs.map(one => ({ ...one, route: store.runRoute(one.id) })),
10782
9001
  deliverable: ref.deliverable,
@@ -11174,9 +9393,6 @@ function blockTask(positional, flags, context) {
11174
9393
  return fail(write, json, "task block", "unknown-task", `no task \`${each}\``, EXIT.refused);
11175
9394
  }
11176
9395
  }
11177
- const racingGuard = refuseWhileRacing(context, "task block", id);
11178
- if (racingGuard !== null)
11179
- return racingGuard;
11180
9396
  const result = store.addEdge(id, on, mutationFrom(flags, context.now));
11181
9397
  if (!result.ok)
11182
9398
  return fail(write, json, "task block", "rejected", result.reason, EXIT.refused);
@@ -11213,9 +9429,7 @@ async function steerTask(positional, flags, context) {
11213
9429
  ? `no task \`${id}\``
11214
9430
  : filed.reason === "task-finished"
11215
9431
  ? `${id} is finished — a note has no next attempt to reach`
11216
- : filed.reason === "contest-open"
11217
- ? "agents are racing on this task — steering waits until the tournament settles"
11218
- : (filed.problem ?? "that note will not store");
9432
+ : (filed.problem ?? "that note will not store");
11219
9433
  return fail(write, json, "task steer", filed.reason, detail, EXIT.refused);
11220
9434
  }
11221
9435
  return succeed(write, json, "task steer", { task: id, note: filed.id }, () => [
@@ -11233,9 +9447,6 @@ function unblockTask(positional, flags, context) {
11233
9447
  if (store.getTask(id) === null) {
11234
9448
  return fail(write, json, "task unblock", "unknown-task", `no task \`${id}\``, EXIT.refused);
11235
9449
  }
11236
- const racingGuard = refuseWhileRacing(context, "task unblock", id);
11237
- if (racingGuard !== null)
11238
- return racingGuard;
11239
9450
  const result = store.removeEdge(id, on, mutationFrom(flags, context.now));
11240
9451
  if (!result.ok) {
11241
9452
  return fail(write, json, "task unblock", "not-waiting", `${id} was not waiting on ${on}`, EXIT.refused);
@@ -11271,9 +9482,7 @@ function nextTask(positional, flags, context) {
11271
9482
  ? `${id} is not queued — only queued work can move up`
11272
9483
  : moved.reason === "claimed"
11273
9484
  ? `${id} is being built right now — it needs no place in line`
11274
- : moved.reason === "contest-open"
11275
- ? "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)"
11276
- : "the queue rank could not be raised any further";
9485
+ : "the queue rank could not be raised any further";
11277
9486
  return fail(write, json, "task next", moved.reason, message, EXIT.refused);
11278
9487
  }
11279
9488
  return succeed(write, json, "task next", { task: id, priority: moved.priority }, () => [
@@ -11301,13 +9510,11 @@ function assignTask(positional, flags, context) {
11301
9510
  ? `${id} is not queued — only queued work can be reserved`
11302
9511
  : moved.reason === "claimed"
11303
9512
  ? `${id} is being built right now — it needs no reservation`
11304
- : moved.reason === "contest-open"
11305
- ? "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)"
11306
- : moved.reason === "no-such-worker"
11307
- ? `no worker named \`${runner}\` — \`toolroll runner list\` names them`
11308
- : moved.reason === "worker-retired"
11309
- ? `${runner} is retired — register the name again, or reserve for another worker`
11310
- : "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";
11311
9518
  return fail(write, json, "task assign", moved.reason, message, EXIT.refused);
11312
9519
  }
11313
9520
  return succeed(write, json, "task assign", { task: id, reservedFor: runner ?? null }, () => [
@@ -11338,7 +9545,6 @@ async function reopenTask(positional, flags, context) {
11338
9545
  "not-latched": `${id} was never closed on its tracker — there is nothing to reopen`,
11339
9546
  "not-seen-open": "the tracker has not been SEEN open again since the close — reopen it there, then `toolroll sync`",
11340
9547
  claimed: `${id} is being built right now`,
11341
- "contest-open": "a tournament is open on this task — decide it first",
11342
9548
  held: "a hold stands — lift it first (`task unhold`, or wait out the timer)",
11343
9549
  "question-open": "an unanswered question stands — answer or close it first (it is on the task page)",
11344
9550
  "incident-open": "an unresolved incident stands — resolve it first",
@@ -11400,7 +9606,7 @@ async function scopeTask(positional, flags, context) {
11400
9606
  const id = positional[0];
11401
9607
  const goal = text(flags, "goal");
11402
9608
  if (id === undefined || goal === undefined) {
11403
- 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);
11404
9610
  }
11405
9611
  if (store.getTask(id) === null) {
11406
9612
  return fail(write, json, "task scope", "unknown-task", `no task \`${id}\``, EXIT.refused);
@@ -11416,10 +9622,6 @@ async function scopeTask(positional, flags, context) {
11416
9622
  if (acceptanceParse.problems.length > 0) {
11417
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);
11418
9624
  }
11419
- const riskGiven = text(flags, "risk");
11420
- if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
11421
- return fail(write, json, "task scope", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
11422
- }
11423
9625
  // v69: a prepared commit. The machine checks it out as the attempt and
11424
9626
  // runs the gate and the review; no agent is dispatched for it.
11425
9627
  const candidateGiven = text(flags, "candidate");
@@ -11433,9 +9635,6 @@ async function scopeTask(positional, flags, context) {
11433
9635
  const badText = validateScopeText({ goal, outOfScope: text(flags, "not") ?? null, touches });
11434
9636
  if (badText !== null)
11435
9637
  return fail(write, json, "task scope", badText.reason, badText.message, EXIT.usage);
11436
- // A tournament rides the same filing (stage 3): --race names the agents,
11437
- // the dollar terms are REQUIRED, and everything lands unapproved — the
11438
- // one yes later covers scope AND race terms as a single fingerprint.
11439
9638
  const budgetGiven = text(flags, "budget-usd");
11440
9639
  const defaults = store.getSpendDefaults();
11441
9640
  const budgetUsd = budgetGiven !== undefined
@@ -11446,84 +9645,6 @@ async function scopeTask(positional, flags, context) {
11446
9645
  if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0)) {
11447
9646
  return fail(write, json, "task scope", "bad-budget", "--budget-usd is a positive dollar amount", EXIT.usage);
11448
9647
  }
11449
- const raceGiven = text(flags, "race");
11450
- const raceCountGiven = text(flags, "race-count");
11451
- // The comparison road (Phase 3 slice B): labeled lanes, no dollar terms,
11452
- // any registered provider — refused outright when every lane could hold
11453
- // a real budget (the discipline gate: race those instead).
11454
- const compareGiven = text(flags, "compare");
11455
- const permissionMode = store.refFor(BUILT_IN, id).permissionMode ?? store.permissionDefault().mode;
11456
- let plannedComparison = null;
11457
- if (compareGiven !== undefined) {
11458
- if (raceGiven !== undefined || raceCountGiven !== undefined || text(flags, "race-per-usd") !== undefined || text(flags, "race-total-usd") !== undefined) {
11459
- return fail(write, json, "task scope", "usage", "--compare and the --race flags are different ceremonies — file one or the other", EXIT.usage);
11460
- }
11461
- if (store.mirrorByTask(id) !== null) {
11462
- 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);
11463
- }
11464
- const lanes = compareGiven.split(",").map(one => {
11465
- const [provider = "", model = ""] = one.trim().split(":");
11466
- return { provider, model, permissionMode };
11467
- });
11468
- plannedComparison = planComparison({ agents: lanes });
11469
- if (!plannedComparison.ok) {
11470
- return fail(write, json, "task scope", plannedComparison.reason, plannedComparison.message, EXIT.refused);
11471
- }
11472
- }
11473
- let plannedRace = null;
11474
- if (raceCountGiven !== undefined && raceGiven === undefined) {
11475
- 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);
11476
- }
11477
- if (raceGiven !== undefined) {
11478
- // Explicit flags win; absent ones fall back to the configured defaults
11479
- // (operator request) — the digest binds the ACTUAL numbers either way,
11480
- // and the approval restates them. A budget that is simply MISSING is
11481
- // named as the missing flag here, before planTournament's generic
11482
- // positive-amount backstop turns it into a riddle (round-4 finding 11).
11483
- if (text(flags, "race-per-usd") === undefined && defaults?.racePerAgentMicrousd == null) {
11484
- 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);
11485
- }
11486
- if (text(flags, "race-total-usd") === undefined && defaults?.raceTotalMicrousd == null) {
11487
- 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);
11488
- }
11489
- const perUsd = Number(text(flags, "race-per-usd") ?? (defaults?.racePerAgentMicrousd == null ? Number.NaN : defaults.racePerAgentMicrousd / 1_000_000));
11490
- const totalUsd = Number(text(flags, "race-total-usd") ?? (defaults?.raceTotalMicrousd == null ? Number.NaN : defaults.raceTotalMicrousd / 1_000_000));
11491
- if (store.mirrorByTask(id) !== null) {
11492
- 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);
11493
- }
11494
- let agents = raceGiven.split(",").map(one => {
11495
- const [provider = "", model = ""] = one.trim().split(":");
11496
- return { provider, model, permissionMode };
11497
- });
11498
- // The competing-agent COUNT (operator request): an explicit --race-count
11499
- // replicates a single named agent; with several named agents it may only
11500
- // agree with the list — a count that contradicts an explicit lineup is a
11501
- // question, not an instruction. Absent both, the configured default
11502
- // count replicates a single agent; an explicit list is always itself.
11503
- if (raceCountGiven !== undefined) {
11504
- const count = Number(raceCountGiven);
11505
- if (!Number.isInteger(count) || count < 2 || count > 4) {
11506
- return fail(write, json, "task scope", "usage", "--race-count is how many agents compete: a whole number from 2 to 4", EXIT.usage);
11507
- }
11508
- if (agents.length === 1) {
11509
- agents = Array.from({ length: count }, () => ({ ...agents[0] }));
11510
- }
11511
- else if (agents.length !== count) {
11512
- 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);
11513
- }
11514
- }
11515
- else if (agents.length === 1 && defaults?.raceAgents != null) {
11516
- agents = Array.from({ length: defaults.raceAgents }, () => ({ ...agents[0] }));
11517
- }
11518
- plannedRace = planTournament({
11519
- agents,
11520
- perAgentBudgetUsd: perUsd,
11521
- totalBudgetUsd: totalUsd,
11522
- });
11523
- if (!plannedRace.ok) {
11524
- return fail(write, json, "task scope", plannedRace.reason, plannedRace.message, EXIT.refused);
11525
- }
11526
- }
11527
9648
  // v24 routing flags (foundations finding 5 — these used to be silently
11528
9649
  // swallowed by the global parser): explicit flags resolve HERE, and an
11529
9650
  // explicit ask that cannot resolve refuses rather than filing unresolved.
@@ -11537,11 +9658,6 @@ async function scopeTask(positional, flags, context) {
11537
9658
  }
11538
9659
  explicitProfile = resolvedRouting.profile;
11539
9660
  }
11540
- // The race branch shares ONE transaction with the scope save (round-6
11541
- // finding 5): the attended exclusion refuses BEFORE anything writes, and
11542
- // a filing failure rolls the proposal back — a refused race never leaves
11543
- // a rewritten scope behind it.
11544
- //
11545
9661
  // C1/M3, the credentialed-CLI road: when --as/--token authenticate the
11546
9662
  // MODE'S SIGNER and their live mode auto-approves filings, the scope
11547
9663
  // seals in the same transaction it files — plain scopes only, and the
@@ -11559,8 +9675,7 @@ async function scopeTask(positional, flags, context) {
11559
9675
  const previous = store.getScope(id);
11560
9676
  if (ref === null || ref.deliverable === "report" || candidateGiven !== undefined ||
11561
9677
  ref.sizing?.source === "person" || approvalOf(previous).approved ||
11562
- previous?.proposedVia === "coordinator" || store.filedViaOf(id)?.startsWith("mcp:") ||
11563
- ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null))
9678
+ previous?.proposedVia === "coordinator" || store.filedViaOf(id)?.startsWith("mcp:"))
11564
9679
  return null;
11565
9680
  const input = { title: store.getTask(id).title, goal, outOfScope: text(flags, "not") ?? null, touches };
11566
9681
  const sized = store.applySizing(id, heuristicSizing(input), { followPlanning: true }, now);
@@ -11571,22 +9686,12 @@ async function scopeTask(positional, flags, context) {
11571
9686
  // Wait for bounded classification before filing so any approval in the
11572
9687
  // transaction below binds the final size and route.
11573
9688
  await refinement;
11574
- // ONE replayed composite (surfaces round 1, finding 3): the filing, any
11575
- // 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 —
11576
9691
  // a replayed key returns the FIRST answer whole instead of re-sealing
11577
9692
  // whatever scope happens to be current.
11578
9693
  const filed = store.replay(mutationFrom(flags, now), "task-scope-filed", () => store.transact(() => {
11579
- if (plannedRace !== null && plannedRace.ok) {
11580
- const raceRef = store.refFor(BUILT_IN, id);
11581
- if (store.openAuthorizationFor(raceRef.id) !== null) {
11582
- return {
11583
- ok: false,
11584
- reason: "attended-open",
11585
- message: "an attended authorization is open on this task — revoke it before filing a tournament",
11586
- };
11587
- }
11588
- }
11589
- const coverage = plannedRace === null && plannedComparison === null && actor !== null
9694
+ const coverage = actor !== null
11590
9695
  ? modeFilingCoverage(store, store.refFor(BUILT_IN, id).repo, actor, now)
11591
9696
  : null;
11592
9697
  const proposed = propose(store, {
@@ -11596,8 +9701,6 @@ async function scopeTask(positional, flags, context) {
11596
9701
  outOfScope: text(flags, "not") ?? null,
11597
9702
  touches,
11598
9703
  acceptance: acceptanceParse.criteria,
11599
- // v47: the declared risk — a durable task choice the route reads.
11600
- ...(riskGiven === undefined ? {} : { riskLevel: riskGiven }),
11601
9704
  ...(budgetUsd !== null
11602
9705
  ? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
11603
9706
  : coverage?.defaultBudgetMicrousd != null
@@ -11625,42 +9728,6 @@ async function scopeTask(positional, flags, context) {
11625
9728
  sealedUnderMode = store.sealScopeApproval(id, actor, now, {}, { kind: "mode", modeDigest: coverage.digest });
11626
9729
  modeRefusedCoordinator = !sealedUnderMode;
11627
9730
  }
11628
- if (plannedRace !== null && plannedRace.ok) {
11629
- const plan = plannedRace.plan;
11630
- const raceRef = store.refFor(BUILT_IN, id);
11631
- store.fileTournamentTerms({
11632
- taskRef: raceRef.id,
11633
- raceDigest: plan.raceDigest,
11634
- agents: plan.agents,
11635
- perAgentBudgetMicrousd: plan.perAgentBudgetMicrousd,
11636
- overrunReserveMicrousd: plan.overrunReserveMicrousd,
11637
- totalBudgetMicrousd: plan.totalBudgetMicrousd,
11638
- priceVersion: plan.priceVersion,
11639
- publicationPolicy: plan.publicationPolicy,
11640
- }, now);
11641
- }
11642
- if (plannedComparison !== null && plannedComparison.ok) {
11643
- const plan = plannedComparison.plan;
11644
- const compareRef = store.refFor(BUILT_IN, id);
11645
- if (store.openAuthorizationFor(compareRef.id) !== null) {
11646
- return {
11647
- ok: false,
11648
- reason: "attended-open",
11649
- message: "an attended authorization is open on this task — revoke it before filing a comparison",
11650
- };
11651
- }
11652
- store.fileTournamentTerms({
11653
- taskRef: compareRef.id,
11654
- kind: "comparison",
11655
- raceDigest: plan.comparisonDigest,
11656
- agents: plan.agents,
11657
- perAgentBudgetMicrousd: 0,
11658
- overrunReserveMicrousd: 0,
11659
- totalBudgetMicrousd: 0,
11660
- priceVersion: 0,
11661
- publicationPolicy: plan.publicationPolicy,
11662
- }, now);
11663
- }
11664
9731
  return { ok: true, scope: proposed, sealedUnderMode, modeRefusedCoordinator };
11665
9732
  }));
11666
9733
  if (!filed.ok) {
@@ -11679,33 +9746,6 @@ async function scopeTask(positional, flags, context) {
11679
9746
  ...describeScope(scope),
11680
9747
  ]);
11681
9748
  }
11682
- if (plannedComparison !== null && plannedComparison.ok) {
11683
- const plan = plannedComparison.plan;
11684
- return succeed(write, json, "task scope", { scope, comparison: plan }, () => [
11685
- `Scope and comparison written for ${id}. Nothing builds until somebody approves BOTH, with one yes:`,
11686
- ...describeScope(scope),
11687
- "",
11688
- ...plan.laneWords.map(lane => ` ${lane}`),
11689
- " no dollar caps exist on a comparison — each agent runs until it finishes or stops making progress;",
11690
- " spend lands measured only where the harness reports dollars",
11691
- "",
11692
- ` toolroll task approve ${id} --yes`,
11693
- ]);
11694
- }
11695
- if (plannedRace !== null && plannedRace.ok) {
11696
- const plan = plannedRace.plan;
11697
- const worst = plan.perAgentReserveMicrousd.reduce((sum, reserve) => sum + plan.perAgentBudgetMicrousd + reserve, 0);
11698
- return succeed(write, json, "task scope", { scope, race: plan }, () => [
11699
- `Scope and tournament written for ${id}. Nothing builds until somebody approves BOTH, with one yes:`,
11700
- ...describeScope(scope),
11701
- "",
11702
- ` tournament: ${plan.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`,
11703
- ` each agent may spend $${(plan.perAgentBudgetMicrousd / 1_000_000).toFixed(2)}, plus its stated overrun reserve;` +
11704
- ` worst case $${(worst / 1_000_000).toFixed(2)} total`,
11705
- "",
11706
- ` toolroll task approve ${id} --yes`,
11707
- ]);
11708
- }
11709
9749
  return succeed(write, json, "task scope", { scope }, () => [
11710
9750
  `Scope written for ${id}. Nothing will build it until somebody approves it.`,
11711
9751
  ...describeScope(scope),
@@ -11724,8 +9764,8 @@ async function scopeTask(positional, flags, context) {
11724
9764
  /**
11725
9765
  * `toolroll task route <id>` (v47): the explainable phase route —
11726
9766
  * shown from the ONE projection every surface renders, and edited only by
11727
- * an approver: `--risk <routine|elevated|high>` declares the task's risk;
11728
- * `--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
11729
9769
  * (an exact pair — approvals bind exact routing) with attribution;
11730
9770
  * `--clear-phase <p>` removes one; `--digest <d>` names the scope digest
11731
9771
  * you read, so the edit lands only on what you saw. The edit is ONE
@@ -11739,13 +9779,12 @@ async function routeTaskCommand(positional, flags, context) {
11739
9779
  const { store, write, json, clock } = context;
11740
9780
  const id = positional[0];
11741
9781
  if (id === undefined) {
11742
- 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);
11743
9783
  }
11744
9784
  if (store.getTask(id) === null) {
11745
9785
  return fail(write, json, "task route", "unknown-task", `no task \`${id}\``, EXIT.refused);
11746
9786
  }
11747
9787
  const ref = store.refFor(BUILT_IN, id);
11748
- const riskGiven = text(flags, "risk");
11749
9788
  const phaseGiven = text(flags, "phase");
11750
9789
  const clearGiven = text(flags, "clear-phase");
11751
9790
  const providerGiven = text(flags, "provider");
@@ -11753,7 +9792,7 @@ async function routeTaskCommand(positional, flags, context) {
11753
9792
  const digestGiven = text(flags, "digest");
11754
9793
  const sizeGiven = text(flags, "size");
11755
9794
  const riskyGiven = text(flags, "risky");
11756
- const editing = riskGiven !== undefined || phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
9795
+ const editing = phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
11757
9796
  const show = () => {
11758
9797
  const scope = store.getScope(id);
11759
9798
  const current = store.refFor(BUILT_IN, id);
@@ -11761,7 +9800,7 @@ async function routeTaskCommand(positional, flags, context) {
11761
9800
  const readiness = store.readinessLookupFor(ref.repo, ref.assignedRunner, clock());
11762
9801
  const view = taskRouteView(routed, readiness);
11763
9802
  const projection = routed !== null && routed.kind === "route" ? projectRoute(routed.route, readiness) : null;
11764
- 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
11765
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"}`]
11766
9805
  : [
11767
9806
  ...(projection === null
@@ -11769,10 +9808,8 @@ async function routeTaskCommand(positional, flags, context) {
11769
9808
  : [
11770
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"}`,
11771
9810
  ...routeWords(projection),
11772
- // The declared risk, in the words the console and chat
11773
- // use — and the one CLI hint that belongs here, not in
11774
- // the route's own reasons: how a stronger tier is named.
11775
- ` 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.
11776
9813
  ...(projection.legs.some(leg => leg.reasons.some(reason => reason.startsWith("no stronger")))
11777
9814
  ? [` name a stronger agent once with \`config set <phase> --tier strong --provider … --model …\``]
11778
9815
  : []),
@@ -11795,9 +9832,6 @@ async function routeTaskCommand(positional, flags, context) {
11795
9832
  if (acting === null) {
11796
9833
  return fail(write, json, "task route", "usage", "changing a route takes `--as <you> --token <t>` — it reroutes spend", EXIT.usage);
11797
9834
  }
11798
- if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
11799
- return fail(write, json, "task route", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
11800
- }
11801
9835
  if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
11802
9836
  return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
11803
9837
  }
@@ -11835,7 +9869,6 @@ async function routeTaskCommand(positional, flags, context) {
11835
9869
  const authenticated = authenticateApprover(store, acting.name, acting.token);
11836
9870
  return authenticated.ok ? { ok: true } : { ok: false, reason: authenticated.reason };
11837
9871
  },
11838
- ...(riskGiven === undefined ? {} : { risk: riskGiven }),
11839
9872
  ...(sizeEdit === undefined ? {} : { size: sizeEdit }),
11840
9873
  ...(phase === undefined
11841
9874
  ? {}
@@ -11846,7 +9879,7 @@ async function routeTaskCommand(positional, flags, context) {
11846
9879
  if (edited.reason === "unauthenticated") {
11847
9880
  return fail(write, json, "task route", edited.detail, describeApproveFailure(edited.detail, id), EXIT.refused);
11848
9881
  }
11849
- 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" };
11850
9883
  return fail(write, json, "task route", code[edited.reason], `${id}: ${edited.detail}`, edited.reason === "nothing" ? EXIT.usage : EXIT.refused);
11851
9884
  }
11852
9885
  const code = show();
@@ -11888,14 +9921,6 @@ async function approveTask(positional, flags, context) {
11888
9921
  write(line);
11889
9922
  for (const line of planContractLines(store, context.evidenceRoot, id))
11890
9923
  write(line);
11891
- const interactiveRace = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
11892
- if (interactiveRace !== null) {
11893
- write("");
11894
- write(` AND it starts a tournament: ${interactiveRace.n} agents build this independently —`);
11895
- write(` ${interactiveRace.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`);
11896
- write(` each may spend $${(interactiveRace.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus its overrun reserve;`);
11897
- write(` the whole tournament is capped at $${(interactiveRace.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You pick the winner; only the winner publishes.`);
11898
- }
11899
9924
  write("");
11900
9925
  const agreed = await confirm("Approve exactly this?");
11901
9926
  if (!agreed) {
@@ -11924,14 +9949,8 @@ async function approveTask(positional, flags, context) {
11924
9949
  for (const line of planContractLines(store, context.evidenceRoot, id))
11925
9950
  write(line);
11926
9951
  write("");
11927
- const previewRace = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
11928
- if (previewRace !== null) {
11929
- write(` AND the tournament: ${previewRace.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`);
11930
- write(` each capped at $${(previewRace.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} + reserve, total $${(previewRace.totalBudgetMicrousd / 1_000_000).toFixed(2)}`);
11931
- write("");
11932
- }
11933
9952
  write("Nothing has been approved. Agree to this exact scope with:");
11934
- 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>`);
11935
9954
  // Unconfirmed is "no, not yet" — exit 3 in both modes (round-4 finding 10).
11936
9955
  return EXIT.refused;
11937
9956
  }
@@ -11943,38 +9962,6 @@ async function approveTask(positional, flags, context) {
11943
9962
  // The credential is required for a different reason: an agent that can run
11944
9963
  // these commands can read the digest out of `task show`, and an approval
11945
9964
  // nobody has to authenticate would let it agree to its own brief.
11946
- //
11947
- // A tournament task's yes covers BOTH documents (finding 31): the named
11948
- // digest is tournament-approval/v1 = H(scope, race), and the scope and
11949
- // the race terms approve together, in one transaction, or not at all.
11950
- const raceTerms = store.activeTournamentTerms(store.refFor(BUILT_IN, id).id);
11951
- if (raceTerms !== null) {
11952
- const joint = jointApprovalDigest(scope.digest, raceTerms.raceDigest);
11953
- if (saw !== joint && saw !== scope.digest) {
11954
- return fail(write, json, "task approve", "changed", `this task races a tournament — approve the JOINT fingerprint: ${joint}`, EXIT.refused);
11955
- }
11956
- if (saw === scope.digest && !confirmedAloud) {
11957
- 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);
11958
- }
11959
- const both = store.transact(() => {
11960
- const scopeApproved = approve(store, id, asWho, now, scope.digest, token, mutationFrom(flags, now));
11961
- if (!scopeApproved.ok)
11962
- return scopeApproved;
11963
- if (!store.approveTournamentTerms(raceTerms.id, asWho, raceTerms.raceDigest, now)) {
11964
- throw new Error("the race terms changed while you were reading — nothing was approved");
11965
- }
11966
- return scopeApproved;
11967
- });
11968
- if (!both.ok) {
11969
- return fail(write, json, "task approve", both.reason, both.reason === "policy" ? `${id}: ${both.message}` : describeApproveFailure(both.reason, id), EXIT.refused);
11970
- }
11971
- return succeed(write, json, "task approve", { scope: both.scope, race: raceTerms }, () => [
11972
- `Approved — scope AND tournament, with one yes. ${raceTerms.n} agents will build ${id} independently:`,
11973
- ...describeScope(both.scope),
11974
- ` ${raceTerms.agents.map(agent => `${agent.provider} · ${agent.model}`).join(" vs ")}`,
11975
- ` each may spend $${(raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus its overrun reserve; total cap $${(raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}`,
11976
- ]);
11977
- }
11978
9965
  const approved = approve(store, id, asWho, now, saw, token, mutationFrom(flags, now));
11979
9966
  if (!approved.ok) {
11980
9967
  if (approved.reason === "policy")
@@ -11993,7 +9980,7 @@ function describeApproveFailure(reason, id) {
11993
9980
  if (reason === "requester")
11994
9981
  return `you filed ${id}, and this project needs someone else to approve it`;
11995
9982
  if (reason === "person-required")
11996
- 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`;
11997
9984
  if (reason === "second-approver")
11998
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)`;
11999
9986
  if (reason === "no-approvers") {
@@ -12004,7 +9991,7 @@ function describeApproveFailure(reason, id) {
12004
9991
  if (reason === "unrouted")
12005
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`;
12006
9993
  if (reason === "profile-unresolved")
12007
- 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`;
12008
9995
  return `${id} has no scope to approve`;
12009
9996
  }
12010
9997
  /**
@@ -12354,9 +10341,6 @@ function unholdTask(positional, flags, context) {
12354
10341
  if (store.getTask(id) === null) {
12355
10342
  return fail(write, json, "task unhold", "unknown-task", `no task \`${id}\``, EXIT.refused);
12356
10343
  }
12357
- const racingGuard = refuseWhileRacing(context, "task unhold", id);
12358
- if (racingGuard !== null)
12359
- return racingGuard;
12360
10344
  const lifted = store.unhold(store.refFor(BUILT_IN, id).id, mutationFrom(flags, context.now));
12361
10345
  if (!lifted)
12362
10346
  return fail(write, json, "task unhold", "not-held", `${id} was not on hold`, EXIT.refused);
@@ -13304,14 +11288,6 @@ function text(flags, name) {
13304
11288
  function flag(flags, name) {
13305
11289
  return flags.get(name) === true || flags.get(name) === "true";
13306
11290
  }
13307
- /** A running tournament owns its task (round-1 finding 5): the generic
13308
- * doors refuse until it finishes, is picked, or is abandoned. */
13309
- function refuseWhileRacing(context, command, taskId) {
13310
- const ref = context.store.lookupRef(taskId);
13311
- if (ref === null || context.store.openContestFor(ref.id) === null)
13312
- return null;
13313
- 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);
13314
- }
13315
11291
  /**
13316
11292
  * The demo fence (Codex adoption review, finding 8): a database stamped as
13317
11293
  * a demo sandbox NEVER spends money or touches the world outside — no
@@ -13321,9 +11297,8 @@ function refuseWhileRacing(context, command, taskId) {
13321
11297
  * is the enforcement.
13322
11298
  */
13323
11299
  /**
13324
- * Deep links (attended A5): when a console URL is configured (`webhook set
13325
- * console-url`), CLI answers print it beside ids so the attended eye can
13326
- * 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
13327
11302
  * a guess, and stale guesses are worse than none.
13328
11303
  */
13329
11304
  function consoleLinkFor(context, path) {