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