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.
- package/CHANGELOG.md +8 -0
- package/README.md +55 -80
- package/dist/agentconfig.d.ts +11 -36
- package/dist/agentconfig.js +3 -62
- package/dist/approval-policy.d.ts +1 -1
- package/dist/approval-policy.js +1 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-decide.js +1 -3
- package/dist/chat-delivery.d.ts +0 -2
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-quiet.js +2 -2
- package/dist/chat-task-actions.d.ts +1 -1
- package/dist/chat-task-actions.js +0 -2
- package/dist/claim.d.ts +79 -244
- package/dist/claim.js +688 -1023
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -2
- package/dist/contracts/cli.d.ts +0 -13
- package/dist/contracts/cli.js +0 -2
- package/dist/contracts/console-api.d.ts +5 -10
- package/dist/contracts/console-api.js +4 -10
- package/dist/contracts/flow.d.ts +50 -0
- package/dist/contracts/flow.js +26 -1
- package/dist/contracts/lead-tools.d.ts +0 -13
- package/dist/contracts/lead-tools.js +3 -3
- package/dist/contracts/store-json.d.ts +1 -25
- package/dist/contracts/store-json.js +3 -13
- package/dist/converse.d.ts +1 -11
- package/dist/converse.js +6 -38
- package/dist/coordinator.d.ts +0 -1
- package/dist/coordinator.js +3 -5
- package/dist/demo.js +16 -26
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.js +1 -3
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.d.ts +2 -3
- package/dist/dispose.js +17 -92
- package/dist/envelope.js +0 -3
- package/dist/exec.d.ts +0 -64
- package/dist/exec.js +3 -356
- package/dist/exhaustion.d.ts +22 -76
- package/dist/exhaustion.js +14 -140
- package/dist/export.js +4 -4
- package/dist/flow-engine.js +1 -1
- package/dist/flow-schedule.d.ts +154 -0
- package/dist/flow-schedule.js +321 -0
- package/dist/flow-share.js +1 -1
- package/dist/flow-triggers.d.ts +6 -1
- package/dist/flow-triggers.js +65 -10
- package/dist/flows-cli.js +8 -4
- package/dist/flows-ui.js +2 -0
- package/dist/flows.js +1 -1
- package/dist/guides.js +9 -29
- package/dist/invoke.d.ts +1 -24
- package/dist/invoke.js +17 -259
- package/dist/lead-status.js +2 -4
- package/dist/lead-voice.js +1 -1
- package/dist/mate-doors.d.ts +1 -2
- package/dist/mate-doors.js +12 -25
- package/dist/mate-tools.d.ts +2 -2
- package/dist/mate-tools.js +10 -22
- package/dist/modes.d.ts +6 -7
- package/dist/modes.js +7 -13
- package/dist/operate-remote-arguments.js +2 -6
- package/dist/operate-remote.js +6 -7
- package/dist/operate.d.ts +0 -6
- package/dist/operate.js +222 -2247
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- package/dist/planner-source.js +2 -1
- package/dist/policy.d.ts +1 -4
- package/dist/policy.js +0 -8
- package/dist/process-recovery-eligibility.js +2 -3
- package/dist/project-delete.js +3 -15
- package/dist/proposal.d.ts +4 -24
- package/dist/proposal.js +3 -88
- package/dist/provider-auth.d.ts +3 -3
- package/dist/provider-auth.js +4 -6
- package/dist/provider.d.ts +11 -25
- package/dist/provider.js +4 -36
- package/dist/push.js +1 -1
- package/dist/recipe-ui.js +6 -6
- package/dist/recipes.d.ts +3 -3
- package/dist/recipes.js +27 -23
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +1 -4
- package/dist/scope.d.ts +11 -65
- package/dist/scope.js +17 -82
- package/dist/serve.js +15 -24
- package/dist/server/chat.js +7 -30
- package/dist/server/flows.js +11 -10
- package/dist/server/guards.js +0 -1
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/pages.js +0 -2
- package/dist/server/people-tokens.js +0 -4
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +2 -17
- package/dist/server/settings.js +1 -8
- package/dist/server/shared.d.ts +28 -212
- package/dist/server/shared.js +71 -780
- package/dist/server/task-views.js +9 -76
- package/dist/server/tasks.d.ts +1 -50
- package/dist/server/tasks.js +36 -946
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +0 -2
- package/dist/slack-chat.js +0 -1
- package/dist/store.d.ts +105 -1340
- package/dist/store.js +706 -4010
- package/dist/surface.js +10 -24
- package/dist/task-composer.d.ts +1 -1
- package/dist/task-composer.js +1 -1
- package/dist/task-control.d.ts +3 -10
- package/dist/task-control.js +2 -7
- package/dist/teammate-desk.js +1 -1
- package/dist/teammates-ui.js +1 -1
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +56 -4
- package/dist/work-index.js +2 -6
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
- package/scripts/recovery-canary.mjs +3 -3
- package/dist/contest.d.ts +0 -318
- package/dist/contest.js +0 -753
- package/dist/held.d.ts +0 -150
- package/dist/held.js +0 -799
- package/dist/liveness.d.ts +0 -30
- package/dist/liveness.js +0 -42
- package/dist/pricing.d.ts +0 -45
- package/dist/pricing.js +0 -77
- package/dist/routine.d.ts +0 -228
- package/dist/routine.js +0 -764
- 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
|
|
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,
|
|
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
|
|
98
|
-
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst,
|
|
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,
|
|
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,
|
|
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 {
|
|
190
|
-
import { fileTaskProposal,
|
|
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,
|
|
195
|
-
import {
|
|
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
|
|
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
|
|
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>]
|
|
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> [--
|
|
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;
|
|
439
|
+
each; set its size or override a
|
|
452
440
|
phase — approval seals the route
|
|
453
441
|
|
|
454
|
-
|
|
455
|
-
toolroll template list
|
|
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
|
|
459
|
-
|
|
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
|
|
525
|
-
screenshot-proof, and
|
|
526
|
-
routes reach for; never
|
|
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
|
|
565
|
-
|
|
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", "
|
|
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", "
|
|
663
|
-
"phase", "
|
|
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",
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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
|
-
//
|
|
2312
|
-
//
|
|
2313
|
-
//
|
|
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"
|
|
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)
|
|
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 = [
|
|
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
|
|
2752
|
-
|
|
2753
|
-
|
|
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
|
|
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
|
-
|
|
2800
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
2881
|
-
//
|
|
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 =
|
|
2927
|
-
? null
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
:
|
|
2938
|
-
|
|
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
|
|
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
|
|
2955
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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).
|
|
2985
|
-
//
|
|
2986
|
-
// (
|
|
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",
|
|
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 &&
|
|
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)
|
|
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
|
|
3036
|
-
|
|
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:
|
|
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.
|
|
3061
|
-
//
|
|
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 =
|
|
3067
|
-
? []
|
|
3068
|
-
:
|
|
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.
|
|
3098
|
-
const refused =
|
|
3099
|
-
|
|
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
|
-
//
|
|
3110
|
-
const
|
|
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
|
-
//
|
|
3119
|
-
|
|
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 =
|
|
3484
|
-
|
|
2734
|
+
const unrecorded = finalize(store, lease, {
|
|
2735
|
+
kind: "plan-failure",
|
|
3485
2736
|
runId: planRunId,
|
|
3486
2737
|
taskId: id,
|
|
3487
|
-
|
|
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 =
|
|
3524
|
-
|
|
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 =
|
|
3548
|
-
|
|
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 =
|
|
3582
|
-
|
|
2832
|
+
const sealedFailure = finalize(store, lease, {
|
|
2833
|
+
kind: "plan-failure",
|
|
3583
2834
|
runId: planRunId,
|
|
3584
2835
|
taskId: id,
|
|
3585
|
-
|
|
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,
|
|
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 =
|
|
3691
|
-
|
|
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 =
|
|
3710
|
-
|
|
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 =
|
|
3731
|
-
|
|
2981
|
+
const sealedFailure = finalize(store, lease, {
|
|
2982
|
+
kind: "scout-failure",
|
|
3732
2983
|
runId: scoutRunId,
|
|
3733
2984
|
taskId: id,
|
|
3734
|
-
|
|
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
|
-
//
|
|
3797
|
-
//
|
|
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 (
|
|
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:
|
|
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
|
|
3949
|
-
//
|
|
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
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
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
|
-
|
|
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
|
|
4414
|
-
outcome: "
|
|
4415
|
-
reason:
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
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 === "
|
|
4568
|
-
return succeed(write, json, "tick", { considered, dispatched,
|
|
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,
|
|
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;
|
|
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
|
-
//
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
6968
|
-
*
|
|
6969
|
-
*
|
|
6970
|
-
*
|
|
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
|
|
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(`
|
|
7100
|
-
write(` name ${template.
|
|
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
|
|
7171
|
-
const
|
|
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
|
-
|
|
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(`
|
|
5694
|
+
write(` flow ${draft.name}`);
|
|
7195
5695
|
write(` repo ${draft.repo}`);
|
|
7196
|
-
write(` schedule ${
|
|
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}
|
|
5703
|
+
write(` ceiling $${draft.costCeilingUsd} every 7 days`);
|
|
7204
5704
|
write("");
|
|
7205
|
-
write("Nothing was
|
|
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 =
|
|
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",
|
|
7222
|
-
|
|
7223
|
-
|
|
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
|
-
"
|
|
7226
|
-
` toolroll
|
|
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.
|
|
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
|
-
:
|
|
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 === "
|
|
11305
|
-
?
|
|
11306
|
-
: moved.reason === "
|
|
11307
|
-
?
|
|
11308
|
-
:
|
|
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>]
|
|
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
|
|
11575
|
-
//
|
|
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
|
-
|
|
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: `--
|
|
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> [--
|
|
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 =
|
|
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,
|
|
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
|
|
11773
|
-
//
|
|
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",
|
|
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 ${
|
|
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
|
|
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 \`
|
|
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
|
|
13325
|
-
* console-url`), CLI answers print it beside ids so
|
|
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) {
|