toolroll 0.9.67 → 0.9.69
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 +70 -0
- package/dist/acp-client.js +1 -3
- package/dist/agent-board.d.ts +0 -1
- package/dist/agent-board.js +0 -1
- package/dist/agent-fence.d.ts +58 -0
- package/dist/agent-fence.js +214 -19
- package/dist/agentconfig.d.ts +1 -1
- package/dist/agentconfig.js +6 -2
- package/dist/batch-checks.js +14 -26
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +31 -29
- package/dist/builder.d.ts +9 -0
- package/dist/builder.js +30 -27
- package/dist/canonical-json.d.ts +14 -0
- package/dist/canonical-json.js +36 -0
- package/dist/chat-actions.js +1 -2
- package/dist/chat-admission.d.ts +19 -0
- package/dist/chat-admission.js +22 -0
- package/dist/chat-channel.js +1 -1
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-follow.d.ts +17 -0
- package/dist/chat-follow.js +32 -0
- package/dist/chat-messages.js +0 -1
- package/dist/chat-settings-page.d.ts +23 -0
- package/dist/chat-settings-page.js +41 -0
- package/dist/check-access.d.ts +54 -0
- package/dist/check-access.js +80 -0
- package/dist/claim.js +3 -6
- package/dist/claude-print.d.ts +12 -0
- package/dist/claude-print.js +34 -0
- package/dist/cli-http.d.ts +0 -7
- package/dist/cli-http.js +0 -21
- package/dist/cli.d.ts +3 -10
- package/dist/cli.js +15 -24
- package/dist/coding-provider.js +2 -1
- package/dist/coding-workspace.js +1 -1
- package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
- package/dist/connect-cli.js +331 -0
- package/dist/containment.d.ts +0 -2
- package/dist/containment.js +2 -6
- package/dist/contracts/chat-actions.d.ts +0 -1
- package/dist/contracts/chat-content.d.ts +0 -4
- package/dist/contracts/chat-content.js +0 -2
- package/dist/contracts/chat-tables.d.ts +0 -1
- package/dist/contracts/cli.d.ts +0 -62
- package/dist/contracts/cli.js +0 -4
- package/dist/contracts/coding-workspace.js +1 -1
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/contract.js +1 -1
- package/dist/contracts/flow-card.d.ts +0 -1
- package/dist/contracts/flow-propose.d.ts +0 -1
- package/dist/contracts/flow-send.js +1 -1
- package/dist/contracts/gateway-tools.d.ts +0 -1
- package/dist/contracts/integration-metadata.js +1 -1
- package/dist/contracts/lead-tools.d.ts +0 -1
- package/dist/contracts/memory-pass.d.ts +0 -1
- package/dist/contracts/memory-pass.js +1 -1
- package/dist/contracts/plan.js +1 -1
- package/dist/contracts/project-knowledge.js +1 -1
- package/dist/contracts/project-skills.d.ts +0 -1
- package/dist/contracts/proof.js +1 -1
- package/dist/contracts/recipes.js +1 -1
- package/dist/contracts/scope.js +1 -1
- package/dist/contracts/scout-report.js +1 -1
- package/dist/contracts/teams-callback.d.ts +0 -1
- package/dist/converse.d.ts +0 -6
- package/dist/converse.js +0 -19
- package/dist/daemon.js +3 -2
- package/dist/decision.d.ts +1 -3
- package/dist/deprecations.d.ts +12 -4
- package/dist/deprecations.js +27 -4
- package/dist/desktop-update-recovery.js +1 -1
- package/dist/desktop-update.js +3 -2
- package/dist/discord-chat.js +4 -15
- package/dist/discord-settings.d.ts +2 -6
- package/dist/discord-settings.js +10 -38
- package/dist/discord.js +12 -23
- package/dist/evidence.d.ts +0 -1
- package/dist/evidence.js +0 -3
- package/dist/exec.d.ts +0 -9
- package/dist/exec.js +3 -81
- package/dist/flow-actions.js +0 -3
- package/dist/flow-draft.d.ts +1 -3
- package/dist/flow-draft.js +15 -28
- package/dist/flow-scripts.d.ts +1 -8
- package/dist/flow-scripts.js +0 -13
- package/dist/flow-send.js +1 -1
- package/dist/flow-share.d.ts +0 -2
- package/dist/flow-share.js +1 -6
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +0 -1
- package/dist/flow-triggers.js +1 -2
- package/dist/flow-unattended.js +1 -12
- package/dist/flows.js +1 -1
- package/dist/health-cli.d.ts +2 -2
- package/dist/health-cli.js +1 -1
- package/dist/invoke.d.ts +39 -5
- package/dist/invoke.js +181 -24
- package/dist/is-record.d.ts +2 -0
- package/dist/is-record.js +2 -0
- package/dist/lead-doors.d.ts +1 -5
- package/dist/lead-doors.js +0 -4
- package/dist/lead-tools.js +3 -6
- package/dist/live-detail.d.ts +0 -1
- package/dist/live-detail.js +0 -6
- package/dist/live-terminal.js +2 -1
- package/dist/live.d.ts +0 -1
- package/dist/live.js +1 -2
- package/dist/mcp-core.js +0 -2
- package/dist/memory-pass.js +1 -1
- package/dist/model-catalog.js +1 -1
- package/dist/modes.js +1 -12
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +456 -750
- package/dist/password-bearer.d.ts +0 -2
- package/dist/password-bearer.js +0 -4
- package/dist/peek-cli.js +2 -1
- package/dist/peek.d.ts +2 -26
- package/dist/peek.js +2 -168
- package/dist/phase-routing.d.ts +0 -4
- package/dist/phase-routing.js +2 -21
- package/dist/planner-source.d.ts +2 -1
- package/dist/planner-source.js +2 -12
- package/dist/planner.js +2 -14
- package/dist/process-tree.d.ts +0 -2
- package/dist/process-tree.js +0 -10
- package/dist/project-delete-ui.d.ts +16 -0
- package/dist/project-delete-ui.js +60 -1
- package/dist/project-delete.d.ts +0 -2
- package/dist/project-delete.js +1 -3
- package/dist/project-learning.d.ts +0 -14
- package/dist/project-skills.d.ts +0 -1
- package/dist/project-skills.js +0 -14
- package/dist/project-tools.js +2 -10
- package/dist/provider.d.ts +0 -17
- package/dist/provider.js +0 -26
- package/dist/pulls.d.ts +0 -2
- package/dist/pulls.js +0 -9
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +15 -5
- package/dist/remote-exec.d.ts +3 -2
- package/dist/remote-exec.js +3 -2
- package/dist/repos.d.ts +0 -5
- package/dist/repos.js +2 -10
- package/dist/repository-context.d.ts +0 -2
- package/dist/repository-context.js +0 -4
- package/dist/restart-certification.d.ts +0 -2
- package/dist/restart-certification.js +1 -5
- package/dist/result-follow-ups.d.ts +1 -4
- package/dist/result-follow-ups.js +11 -21
- package/dist/result-review.d.ts +0 -2
- package/dist/result-review.js +0 -2
- package/dist/retention.d.ts +0 -1
- package/dist/retention.js +0 -1
- package/dist/review-context.d.ts +0 -1
- package/dist/review-context.js +1 -12
- package/dist/reviewer.d.ts +0 -16
- package/dist/reviewer.js +0 -18
- package/dist/run-closeout.js +2 -1
- package/dist/scope.d.ts +9 -0
- package/dist/scope.js +13 -18
- package/dist/scout-browser.js +2 -1
- package/dist/serve.js +11 -2
- package/dist/server/console.css +5 -133
- package/dist/server/flows.js +2 -2
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +5 -0
- package/dist/server/pages.js +2 -2
- package/dist/server/people-tokens.js +3 -4
- package/dist/server/remote.js +1 -6
- package/dist/server/render-chat.d.ts +9 -2
- package/dist/server/render-chat.js +53 -7
- package/dist/server/render-results.d.ts +3 -6
- package/dist/server/render-results.js +4 -9
- package/dist/server/render-tasks.js +6 -6
- package/dist/server/route-table.d.ts +0 -4
- package/dist/server/route-table.js +1 -6
- package/dist/server/settings.js +60 -23
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +5 -3
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills.d.ts +0 -6
- package/dist/skills.js +0 -8
- package/dist/slack-chat.d.ts +1 -7
- package/dist/slack-chat.js +5 -18
- package/dist/slack-settings.d.ts +2 -6
- package/dist/slack-settings.js +10 -38
- package/dist/slack.js +12 -23
- package/dist/sleep.d.ts +2 -0
- package/dist/sleep.js +2 -0
- package/dist/store.d.ts +29 -75
- package/dist/store.js +89 -180
- package/dist/structured-output.d.ts +0 -1
- package/dist/structured-output.js +0 -1
- package/dist/subagent-work.d.ts +0 -2
- package/dist/subagent-work.js +0 -2
- package/dist/subagents.d.ts +1 -3
- package/dist/subagents.js +20 -33
- package/dist/subscription-chat.d.ts +1 -3
- package/dist/subscription-chat.js +2 -3
- package/dist/surface.js +2 -1
- package/dist/task-sizing.d.ts +1 -4
- package/dist/task-sizing.js +3 -10
- package/dist/teams-chat.js +4 -11
- package/dist/teams.js +12 -19
- package/dist/telegram-lead.js +2 -2
- package/dist/telegram.js +1 -1
- package/dist/token-notices.d.ts +0 -11
- package/dist/token-notices.js +1 -5
- package/dist/tool-secret-store.d.ts +0 -3
- package/dist/tool-secret-store.js +2 -11
- package/dist/toolroll-update.d.ts +0 -2
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +0 -1
- package/dist/verification-evidence.js +0 -1
- package/dist/webhooks.d.ts +0 -1
- package/dist/webhooks.js +0 -1
- package/dist/workspace-ui.d.ts +0 -8
- package/dist/workspace-ui.js +1 -6
- package/package.json +1 -1
- package/dist/contracts/native-census.d.ts +0 -42
- package/dist/contracts/native-census.js +0 -36
- package/dist/process-recovery-anchor.d.ts +0 -75
- package/dist/process-recovery-anchor.js +0 -188
- package/dist/process-recovery-coalition.d.ts +0 -25
- package/dist/process-recovery-coalition.js +0 -105
- package/dist/process-recovery-eligibility.d.ts +0 -179
- package/dist/process-recovery-eligibility.js +0 -174
- package/dist/process-recovery-native.d.ts +0 -161
- package/dist/process-recovery-native.js +0 -595
- package/dist/process-recovery-provenance.d.ts +0 -163
- package/dist/process-recovery-provenance.js +0 -292
- package/dist/process-recovery-services.d.ts +0 -56
- package/dist/process-recovery-services.js +0 -191
- package/dist/process-recovery-settlement.d.ts +0 -32
- package/dist/process-recovery-settlement.js +0 -133
- package/dist/process-recovery.d.ts +0 -41
- package/dist/process-recovery.js +0 -91
- package/dist/session-brief.d.ts +0 -8
- package/dist/session-brief.js +0 -24
- package/dist/session-cli.d.ts +0 -42
- package/dist/session-cli.js +0 -271
- package/dist/session-contract.d.ts +0 -183
- package/dist/session-contract.js +0 -122
- package/dist/session-remote.d.ts +0 -19
- package/dist/session-remote.js +0 -86
- package/dist/session-service.d.ts +0 -22
- package/dist/session-service.js +0 -157
- package/dist/team-cli.js +0 -692
package/dist/operate.js
CHANGED
|
@@ -9,7 +9,7 @@ import { checkPublicCommand } from "./public-check.js";
|
|
|
9
9
|
import { leadNameOf } from "./lead-identity.js";
|
|
10
10
|
import { maybeTriggerRepair } from "./dispose.js";
|
|
11
11
|
import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
|
|
12
|
-
import { fileAddTestsTask,
|
|
12
|
+
import { fileAddTestsTask, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
|
|
13
13
|
import { runBatchChecks } from "./batch-checks.js";
|
|
14
14
|
import { BATCH_HINT, BATCH_WINDOW_MS, projectBatchChecks, setProjectBatchChecks } from "./batch-policy.js";
|
|
15
15
|
import { buildReviewPass } from "./build-review.js";
|
|
@@ -97,13 +97,13 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
97
97
|
import { homedir, hostname } from "node:os";
|
|
98
98
|
import { basename, dirname, join, resolve } from "node:path";
|
|
99
99
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
100
|
-
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN,
|
|
100
|
+
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
|
|
101
101
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
102
102
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
103
103
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
104
104
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
105
105
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
106
|
-
import {
|
|
106
|
+
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
|
|
107
107
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
108
108
|
import { pushPass } from "./push.js";
|
|
109
109
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
|
|
@@ -112,6 +112,8 @@ import { claimActor, currentActor, withActor } from "./actor.js";
|
|
|
112
112
|
import { parseLeadToken } from "./api-tokens.js";
|
|
113
113
|
import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
|
|
114
114
|
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
115
|
+
import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
|
|
116
|
+
import { CHECK_ACCESS_WARNINGS, checkAccessRunsAs, checkAccessWords, isCheckAccess, signInHostsMissing, signInsUnavailableWords, widerThan } from "./check-access.js";
|
|
115
117
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
116
118
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
117
119
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -120,7 +122,7 @@ import { spawn as spawnChild } from "node:child_process";
|
|
|
120
122
|
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
121
123
|
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
122
124
|
import { auditCommand } from "./audit-cli.js";
|
|
123
|
-
import { closedQuestionWords, hasDisguisedText,
|
|
125
|
+
import { closedQuestionWords, hasDisguisedText, validateNote } from "./decision.js";
|
|
124
126
|
import { readVerifiedArtifact, readVerifiedReport, storeEvidence } from "./evidence.js";
|
|
125
127
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges, encodePlannerSource, plannerSourceOf } from "./planner-source.js";
|
|
126
128
|
/**
|
|
@@ -162,7 +164,7 @@ function planContractLines(store, evidenceRoot, taskId) {
|
|
|
162
164
|
];
|
|
163
165
|
}
|
|
164
166
|
import { verdictWords as proofVerdictWords, matrixWords, semanticCoverage, coverageWords } from "./proof.js";
|
|
165
|
-
import { probeRepo
|
|
167
|
+
import { probeRepo } from "./probe.js";
|
|
166
168
|
import { diagnoseTaskDispatch, diagnosisIsDispatchable, scopeApprovedForDispatch, } from "./dispatch.js";
|
|
167
169
|
import { createDecisionServer } from "./serve.js";
|
|
168
170
|
import { readTailnetNames } from "./tailnet.js";
|
|
@@ -178,7 +180,7 @@ import { activeRemote, REMOTE_MESSAGES, REMOTE_REFUSED_FLAGS, REMOTE_SCOPES, Rem
|
|
|
178
180
|
import { projectAuthority } from "./project-access.js";
|
|
179
181
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
180
182
|
import { tally, spendLine } from "./summary.js";
|
|
181
|
-
import {
|
|
183
|
+
import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } from "./publish.js";
|
|
182
184
|
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
183
185
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
184
186
|
import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
|
|
@@ -189,7 +191,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
189
191
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
190
192
|
import { serveMcp } from "./mcp.js";
|
|
191
193
|
import { createInterface } from "node:readline";
|
|
192
|
-
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken,
|
|
194
|
+
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin } from "./scope.js";
|
|
193
195
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
194
196
|
import { WorktreePool } from "./worktree.js";
|
|
195
197
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -217,7 +219,7 @@ import { plan as planTask } from "./planner.js";
|
|
|
217
219
|
import { attachTmux, elapsedWords, openInTmux, PEEK_TAIL_LINES, runPeek, snapshotLiveRuns } from "./peek-cli.js";
|
|
218
220
|
import { scout as scoutTask } from "./scout.js";
|
|
219
221
|
import { profileDigestOf } from "./scope.js";
|
|
220
|
-
import { PROVIDER_KEY_ENV,
|
|
222
|
+
import { PROVIDER_KEY_ENV, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
|
|
221
223
|
import { run, terminateLiveProviders, run as execRun } from "./exec.js";
|
|
222
224
|
import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
223
225
|
import { readPulls } from "./pulls.js";
|
|
@@ -265,6 +267,9 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
265
267
|
where the project's builds may write: only their checkout, caches and temp
|
|
266
268
|
(workspace, the default), or anywhere outside Toolroll's fenced folders;
|
|
267
269
|
an approver changes it (new scopes seal it; approved ones keep theirs)
|
|
270
|
+
toolroll project check-access --repo <p> [sandboxed|sign-ins|unsandboxed]
|
|
271
|
+
how far the project's checks reach: sandboxed (the default), with model
|
|
272
|
+
and GitHub sign-ins, or not sandboxed; an instance operator widens it
|
|
268
273
|
toolroll config unconfined-agents [allow|refuse]
|
|
269
274
|
whether builds run where this machine can't confine them (instance operator)
|
|
270
275
|
toolroll project concurrency [<n>] --repo <p>
|
|
@@ -916,6 +921,8 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
|
|
|
916
921
|
...(options.flowTriggerIo === undefined ? {} : { flowTriggerIo: options.flowTriggerIo }),
|
|
917
922
|
...(options.flowStepIo === undefined ? {} : { flowStepIo: options.flowStepIo }),
|
|
918
923
|
...(options.flowEveryMs === undefined ? {} : { flowEveryMs: options.flowEveryMs }),
|
|
924
|
+
...(options.platform === undefined ? {} : { platform: options.platform }),
|
|
925
|
+
...(options.fenceAvailable === undefined ? {} : { fenceAvailable: options.fenceAvailable }),
|
|
919
926
|
...(options.flowFetch === undefined ? {} : { flowFetch: options.flowFetch }),
|
|
920
927
|
...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
|
|
921
928
|
...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
|
|
@@ -1171,6 +1178,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1171
1178
|
return projectNetworkCommand(positional, flags, context);
|
|
1172
1179
|
if (positional[0] === "writes")
|
|
1173
1180
|
return projectWritesCommand(positional, flags, context);
|
|
1181
|
+
if (positional[0] === "check-access")
|
|
1182
|
+
return projectCheckAccessCommand(positional, flags, context);
|
|
1174
1183
|
if (positional[0] === "checks")
|
|
1175
1184
|
return projectChecksCommand(positional, flags, context);
|
|
1176
1185
|
if (positional[0] === "demo")
|
|
@@ -1356,9 +1365,8 @@ async function readyCommand(flags, context) {
|
|
|
1356
1365
|
if (backendName !== BUILT_IN) {
|
|
1357
1366
|
const repo = repoFrom(flags);
|
|
1358
1367
|
const backend = openBackend(backendName, store, repo);
|
|
1359
|
-
if (backend === null)
|
|
1368
|
+
if (backend === null)
|
|
1360
1369
|
return fail(write, json, "ready", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
1361
|
-
}
|
|
1362
1370
|
const result = await backend.listReady();
|
|
1363
1371
|
if (!result.ok)
|
|
1364
1372
|
return fail(write, json, "ready", result.reason, result.message, EXIT.failed);
|
|
@@ -1443,17 +1451,14 @@ function claimCommand(positional, flags, context) {
|
|
|
1443
1451
|
// protects those leases would be protecting the wrong thing. This is what
|
|
1444
1452
|
// "auth from the first commit" is for — it is only cheap now.
|
|
1445
1453
|
const token = text(flags, "token");
|
|
1446
|
-
if (token === undefined)
|
|
1454
|
+
if (token === undefined)
|
|
1447
1455
|
return fail(write, json, "claim", "usage", "--token proves the runner is who it says", EXIT.usage);
|
|
1448
|
-
}
|
|
1449
1456
|
const auth = authenticate(store, runner, token);
|
|
1450
|
-
if (!auth.ok)
|
|
1457
|
+
if (!auth.ok)
|
|
1451
1458
|
return fail(write, json, "claim", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
1452
|
-
}
|
|
1453
1459
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
1454
|
-
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1460
|
+
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1455
1461
|
return fail(write, json, "claim", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
1456
|
-
}
|
|
1457
1462
|
const ttl = readTtl(flags);
|
|
1458
1463
|
if (ttl === null)
|
|
1459
1464
|
return fail(write, json, "claim", "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
@@ -1475,9 +1480,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1475
1480
|
// tasks" while letting the asker declare which those are is not a rule.
|
|
1476
1481
|
origin: store.originOf(backend, id),
|
|
1477
1482
|
});
|
|
1478
|
-
if (!verdict.ok)
|
|
1483
|
+
if (!verdict.ok)
|
|
1479
1484
|
return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
|
|
1480
|
-
}
|
|
1481
1485
|
}
|
|
1482
1486
|
const ref = store.refFor(backend, id);
|
|
1483
1487
|
const result = acquire(store, ref.id, runner, {
|
|
@@ -1489,9 +1493,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1489
1493
|
if (!result.ok) {
|
|
1490
1494
|
// Two distinct refusals: somebody HOLDS it right now, or it is
|
|
1491
1495
|
// RESERVED for a specific worker whoever asks (queue columns, v19).
|
|
1492
|
-
if (result.reason === "reserved")
|
|
1496
|
+
if (result.reason === "reserved")
|
|
1493
1497
|
return fail(write, json, "claim", "reserved", `${id} is reserved for ${result.reservedFor} — only that worker takes it`, EXIT.refused, { reservedFor: result.reservedFor });
|
|
1494
|
-
}
|
|
1495
1498
|
if (result.reason === "external") {
|
|
1496
1499
|
const said = {
|
|
1497
1500
|
"stale-mirror": "this tracker item has not been seen recently — run `toolroll sync` first",
|
|
@@ -1503,24 +1506,18 @@ function claimCommand(positional, flags, context) {
|
|
|
1503
1506
|
detail: result.detail,
|
|
1504
1507
|
});
|
|
1505
1508
|
}
|
|
1506
|
-
if (result.reason === "mode-ended")
|
|
1509
|
+
if (result.reason === "mode-ended")
|
|
1507
1510
|
return fail(write, json, "claim", "mode-ended", result.message, EXIT.refused);
|
|
1508
|
-
|
|
1509
|
-
if (result.reason === "unauthenticated") {
|
|
1511
|
+
if (result.reason === "unauthenticated")
|
|
1510
1512
|
return fail(write, json, "claim", "unauthenticated", describeAuth(result.detail, runner), EXIT.refused);
|
|
1511
|
-
|
|
1512
|
-
if (result.reason === "unplaced") {
|
|
1513
|
+
if (result.reason === "unplaced")
|
|
1513
1514
|
return fail(write, json, "claim", "unplaced", "this task has no repository — preserve its history and file a replacement with `task add <title> --repo <path>`, then approve the new scope", EXIT.refused);
|
|
1514
|
-
|
|
1515
|
-
if (result.reason === "unauthorized-repo") {
|
|
1515
|
+
if (result.reason === "unauthorized-repo")
|
|
1516
1516
|
return fail(write, json, "claim", "unauthorized-repo", `this runner is not bound to ${result.repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
1517
|
-
|
|
1518
|
-
if (result.reason === "coordinator-filed") {
|
|
1517
|
+
if (result.reason === "coordinator-filed")
|
|
1519
1518
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1520
|
-
|
|
1521
|
-
if (result.reason === "sweep-limit") {
|
|
1519
|
+
if (result.reason === "sweep-limit")
|
|
1522
1520
|
return fail(write, json, "claim", "sweep-limit", `its sweep already has ${result.limit} running at once — it starts when one finishes`, EXIT.refused, { sweep: result.sweep, limit: result.limit });
|
|
1523
|
-
}
|
|
1524
1521
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1525
1522
|
}
|
|
1526
1523
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1640,9 +1637,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1640
1637
|
return fail(write, json, "runner register", "usage", "`runner register <name> --repo <path> --as <you> --token <t>` — minting runner authority is an operator act", EXIT.usage);
|
|
1641
1638
|
}
|
|
1642
1639
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1643
|
-
if (!authenticated.ok)
|
|
1640
|
+
if (!authenticated.ok)
|
|
1644
1641
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1645
|
-
}
|
|
1646
1642
|
const repos = context.repoList ?? [];
|
|
1647
1643
|
if (repos.length === 0) {
|
|
1648
1644
|
return fail(write, json, "runner register", "usage", "--repo names at least one repository this runner may build — authority binds at the mint", EXIT.usage);
|
|
@@ -1652,9 +1648,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1652
1648
|
// a worker, no paste step. The file must not already exist: a second
|
|
1653
1649
|
// mint never silently overwrites a credential a running watch holds.
|
|
1654
1650
|
const tokenFile = text(flags, "token-file");
|
|
1655
|
-
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1651
|
+
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1656
1652
|
return fail(write, json, "runner register", "token-file-exists", `${tokenFile} already exists — remove it first if that worker is gone, or name another file`, EXIT.refused);
|
|
1657
|
-
}
|
|
1658
1653
|
const { runner, token, reclaimed } = register(store, {
|
|
1659
1654
|
name,
|
|
1660
1655
|
host: hostname(),
|
|
@@ -1694,13 +1689,11 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1694
1689
|
}
|
|
1695
1690
|
if (action === "heartbeat") {
|
|
1696
1691
|
const token = text(flags, "token");
|
|
1697
|
-
if (name === undefined || token === undefined)
|
|
1692
|
+
if (name === undefined || token === undefined)
|
|
1698
1693
|
return fail(write, json, "runner heartbeat", "usage", "`runner heartbeat <name> --token <token>`", EXIT.usage);
|
|
1699
|
-
}
|
|
1700
1694
|
const result = heartbeatRunner(store, name, token, now);
|
|
1701
|
-
if (!result.ok)
|
|
1695
|
+
if (!result.ok)
|
|
1702
1696
|
return fail(write, json, "runner heartbeat", result.reason, describeAuth(result.reason, name), EXIT.refused);
|
|
1703
|
-
}
|
|
1704
1697
|
return succeed(write, json, "runner heartbeat", { runner: result.runner }, () => [
|
|
1705
1698
|
`${name} checked in.`,
|
|
1706
1699
|
]);
|
|
@@ -1735,22 +1728,16 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1735
1728
|
return EXIT.ok;
|
|
1736
1729
|
}
|
|
1737
1730
|
if (action === "retire") {
|
|
1738
|
-
if (name === undefined)
|
|
1731
|
+
if (name === undefined)
|
|
1739
1732
|
return fail(write, json, "runner retire", "usage", "which runner?", EXIT.usage);
|
|
1740
|
-
}
|
|
1741
1733
|
// Retiring is revoking authority — the same operator act as minting it.
|
|
1742
1734
|
const acting = await askCredentials(flags, context);
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1747
|
-
if (!authenticated.ok) {
|
|
1748
|
-
return fail(write, json, "runner retire", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1749
|
-
}
|
|
1735
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1736
|
+
if (acting === null || !verified?.ok)
|
|
1737
|
+
return refuseApprover(context, "runner retire", verified, "`runner retire <name> --as <you> --token <t>` — revoking runner authority is an operator act", name);
|
|
1750
1738
|
const retired = store.retireRunner(name, now, mutationFrom(flags, now));
|
|
1751
|
-
if (!retired)
|
|
1739
|
+
if (!retired)
|
|
1752
1740
|
return fail(write, json, "runner retire", "unknown", `no runner \`${name}\``, EXIT.refused);
|
|
1753
|
-
}
|
|
1754
1741
|
return succeed(write, json, "runner retire", { name }, () => [`${name} is retired.`]);
|
|
1755
1742
|
}
|
|
1756
1743
|
if (action === "bind") {
|
|
@@ -1766,17 +1753,15 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1766
1753
|
return fail(write, json, "runner bind", "usage", "`runner bind <name> --repo <path> --as <you> --token <t>` — binding repositories is an operator act", EXIT.usage);
|
|
1767
1754
|
}
|
|
1768
1755
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1769
|
-
if (!authenticated.ok)
|
|
1756
|
+
if (!authenticated.ok)
|
|
1770
1757
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1771
|
-
}
|
|
1772
1758
|
const repos = context.repoList ?? [];
|
|
1773
1759
|
if (repos.length === 0) {
|
|
1774
1760
|
return fail(write, json, "runner bind", "usage", "--repo names at least one repository", EXIT.usage);
|
|
1775
1761
|
}
|
|
1776
1762
|
const bound = store.bindRunnerRepos(name, canonicalRepos(repos), now);
|
|
1777
|
-
if (!bound.ok)
|
|
1763
|
+
if (!bound.ok)
|
|
1778
1764
|
return fail(write, json, "runner bind", bound.reason, describeAuth(bound.reason, name), EXIT.refused);
|
|
1779
|
-
}
|
|
1780
1765
|
return succeed(write, json, "runner bind", { name, repos: bound.repos }, () => [
|
|
1781
1766
|
`${name} may build in:`,
|
|
1782
1767
|
...bound.repos.map(one => ` ${one}`),
|
|
@@ -1787,22 +1772,18 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1787
1772
|
// ledger as before → after. It never stops running work: claims already
|
|
1788
1773
|
// held keep going, and the new number applies at the next claim.
|
|
1789
1774
|
const given = positional[2];
|
|
1790
|
-
if (name === undefined || given === undefined || positional.length > 3)
|
|
1775
|
+
if (name === undefined || given === undefined || positional.length > 3)
|
|
1791
1776
|
return fail(write, json, "runner capacity", "usage", "`runner capacity <name> <n> --as <you> --token <t>`", EXIT.usage);
|
|
1792
|
-
}
|
|
1793
1777
|
const capacity = parseRunnerCapacity(given, { allowZero: true });
|
|
1794
|
-
if (capacity === null)
|
|
1778
|
+
if (capacity === null)
|
|
1795
1779
|
return fail(write, json, "runner capacity", "usage", `capacity is a whole number of tasks, 0 to ${RUNNER_CAPACITY_MAX}; 0 stops new builds`, EXIT.usage);
|
|
1796
|
-
}
|
|
1797
1780
|
const acting = await askCredentials(flags, context);
|
|
1798
1781
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1799
|
-
if (acting === null ||
|
|
1782
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
1800
1783
|
return fail(write, json, "runner capacity", "refused", "An instance operator changes a worker's capacity: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
1801
|
-
}
|
|
1802
1784
|
const changed = store.setRunnerCapacity(name, capacity, acting.name, now);
|
|
1803
|
-
if (!changed.ok)
|
|
1785
|
+
if (!changed.ok)
|
|
1804
1786
|
return fail(write, json, "runner capacity", changed.reason, changed.reason === "retired" ? `${name} is retired` : `no runner \`${name}\``, EXIT.refused);
|
|
1805
|
-
}
|
|
1806
1787
|
const running = store.liveClaimCount(name, now);
|
|
1807
1788
|
if (changed.after === 0) {
|
|
1808
1789
|
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
@@ -1852,9 +1833,8 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1852
1833
|
return fail(write, json, "coordinator mint", "usage", "`coordinator mint <name> --repo <path> --as <you> --token <t>` — minting filing authority is an operator act", EXIT.usage);
|
|
1853
1834
|
}
|
|
1854
1835
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1855
|
-
if (!authenticated.ok)
|
|
1836
|
+
if (!authenticated.ok)
|
|
1856
1837
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1857
|
-
}
|
|
1858
1838
|
const repos = context.repoList ?? [];
|
|
1859
1839
|
if (repos.length === 0) {
|
|
1860
1840
|
return fail(write, json, "coordinator mint", "usage", "--repo names at least one repository this coordinator may file into", EXIT.usage);
|
|
@@ -1892,17 +1872,12 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1892
1872
|
]);
|
|
1893
1873
|
}
|
|
1894
1874
|
if (action === "revoke") {
|
|
1895
|
-
if (name === undefined)
|
|
1875
|
+
if (name === undefined)
|
|
1896
1876
|
return fail(write, json, "coordinator revoke", "usage", "which coordinator? (`coordinator list` shows cids)", EXIT.usage);
|
|
1897
|
-
}
|
|
1898
1877
|
const acting = await askCredentials(flags, context);
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1903
|
-
if (!authenticated.ok) {
|
|
1904
|
-
return fail(write, json, "coordinator revoke", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1905
|
-
}
|
|
1878
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1879
|
+
if (acting === null || !verified?.ok)
|
|
1880
|
+
return refuseApprover(context, "coordinator revoke", verified, "`coordinator revoke <cid> --as <you> --token <t>` — revoking is an operator act", name);
|
|
1906
1881
|
const revoked = revokeCoordinator(store, name, acting.name, now);
|
|
1907
1882
|
if (!revoked.ok) {
|
|
1908
1883
|
const said = revoked.reason === "unknown" ? `no coordinator \`${name}\` — \`coordinator list\` shows cids` : "already revoked";
|
|
@@ -2066,19 +2041,16 @@ async function buildCommand(positional, flags, context) {
|
|
|
2066
2041
|
const runner = text(flags, "runner");
|
|
2067
2042
|
const token = text(flags, "token");
|
|
2068
2043
|
const branch = text(flags, "branch");
|
|
2069
|
-
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2044
|
+
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2070
2045
|
return fail(write, json, "build", "usage", "`toolroll build <id> --runner <name> --token <t> --branch <b> --repo <path>`", EXIT.usage);
|
|
2071
|
-
}
|
|
2072
2046
|
const auth = authenticate(store, runner, token);
|
|
2073
|
-
if (!auth.ok)
|
|
2047
|
+
if (!auth.ok)
|
|
2074
2048
|
return fail(write, json, "build", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2075
|
-
}
|
|
2076
2049
|
const repo = repoFrom(flags);
|
|
2077
2050
|
// Standalone build proves membership BEFORE leasing anything (MCP spec
|
|
2078
2051
|
// v6, round-4 finding 2): the caller's --repo is a claim, not authority.
|
|
2079
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2052
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2080
2053
|
return fail(write, json, "build", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2081
|
-
}
|
|
2082
2054
|
// Only a task that exists: a lookup must never register an id before the task is filed (its ledger history would be the task's).
|
|
2083
2055
|
if (store.getTask(id) === null)
|
|
2084
2056
|
return fail(write, json, "build", "unknown-task", `No task ${id}.`, EXIT.refused);
|
|
@@ -2093,9 +2065,8 @@ async function buildCommand(positional, flags, context) {
|
|
|
2093
2065
|
now,
|
|
2094
2066
|
...(text(flags, "base") === undefined ? {} : { base: text(flags, "base") }),
|
|
2095
2067
|
});
|
|
2096
|
-
if (!leased.ok)
|
|
2068
|
+
if (!leased.ok)
|
|
2097
2069
|
return fail(write, json, "build", leased.reason, leased.message, EXIT.refused);
|
|
2098
|
-
}
|
|
2099
2070
|
// The standalone road records its attempt and carries its exact lease the
|
|
2100
2071
|
// same as tick's — a park sealed here goes through the same fenced
|
|
2101
2072
|
// transaction, because a gate one road bypasses is a suggestion. No claim
|
|
@@ -2274,22 +2245,19 @@ async function tickCommand(flags, context) {
|
|
|
2274
2245
|
return demoFence;
|
|
2275
2246
|
const runner = text(flags, "runner");
|
|
2276
2247
|
const token = text(flags, "token");
|
|
2277
|
-
if (runner === undefined || token === undefined)
|
|
2248
|
+
if (runner === undefined || token === undefined)
|
|
2278
2249
|
return fail(write, json, "tick", "usage", "`toolroll tick --runner <name> --token <t> --repo <path> [--max <n>] [--base <ref>]`", EXIT.usage);
|
|
2279
|
-
}
|
|
2280
2250
|
// Heartbeat rather than bare auth: a pass that is about to hold leases for
|
|
2281
2251
|
// half an hour should also be on record as alive.
|
|
2282
2252
|
const auth = heartbeatRunner(store, runner, token, clock());
|
|
2283
|
-
if (!auth.ok)
|
|
2253
|
+
if (!auth.ok)
|
|
2284
2254
|
return fail(write, json, "tick", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2285
|
-
}
|
|
2286
2255
|
if (updateAdmissionPaused(store.raw()))
|
|
2287
2256
|
return fail(write, json, "tick", "updating", UPDATE_PAUSED, EXIT.refused);
|
|
2288
2257
|
const maxGiven = text(flags, "max");
|
|
2289
2258
|
const maxAsked = maxGiven === undefined ? 1 : Number(maxGiven);
|
|
2290
|
-
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2259
|
+
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2291
2260
|
return fail(write, json, "tick", "usage", "--max takes a whole number of tasks", EXIT.usage);
|
|
2292
|
-
}
|
|
2293
2261
|
// The service's build lanes (project-concurrency.ts): a lane over its
|
|
2294
2262
|
// project's fair share still runs its housekeeping but starts no
|
|
2295
2263
|
// unattended build this pass; an extra lane only builds.
|
|
@@ -2297,17 +2265,15 @@ async function tickCommand(flags, context) {
|
|
|
2297
2265
|
const buildsOnly = flags.has("builds-only");
|
|
2298
2266
|
const syncAgeGiven = text(flags, "sync-max-age");
|
|
2299
2267
|
const syncMaxAgeMs = syncAgeGiven === undefined ? SYNC_MAX_AGE_MS : Number(syncAgeGiven) * 1000;
|
|
2300
|
-
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2268
|
+
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2301
2269
|
return fail(write, json, "tick", "usage", "--sync-max-age takes whole seconds", EXIT.usage);
|
|
2302
|
-
}
|
|
2303
2270
|
const repo = repoFrom(flags);
|
|
2304
2271
|
// THE PRE-I/O MEMBERSHIP CHECK (MCP spec v6, round-4 finding 1): the
|
|
2305
2272
|
// pass proves its canonical repo is in the runner's BOUND list before
|
|
2306
2273
|
// any git access, probe, or schedule fires. The --repo flag stops being
|
|
2307
2274
|
// authority on every runner road, not only at the claim.
|
|
2308
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2275
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2309
2276
|
return fail(write, json, "tick", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2310
|
-
}
|
|
2311
2277
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
2312
2278
|
const model = text(flags, "model");
|
|
2313
2279
|
const repairModel = text(flags, "repair-model");
|
|
@@ -2342,9 +2308,8 @@ async function tickCommand(flags, context) {
|
|
|
2342
2308
|
let base = text(flags, "base");
|
|
2343
2309
|
if (base === undefined) {
|
|
2344
2310
|
const head = await git("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd: repo });
|
|
2345
|
-
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2311
|
+
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2346
2312
|
return fail(write, json, "tick", "git", `${repo} has no branch checked out — say --base explicitly`, EXIT.refused);
|
|
2347
|
-
}
|
|
2348
2313
|
base = head.stdout.trim();
|
|
2349
2314
|
}
|
|
2350
2315
|
// Probes run at every checkpoint (§3): a key revoked overnight is caught
|
|
@@ -3669,9 +3634,8 @@ async function briefCommand(flags, context) {
|
|
|
3669
3634
|
// (§6): the last watch episode's window and runner, instead of "the last
|
|
3670
3635
|
// 24 hours" — which can mix two windows, or none.
|
|
3671
3636
|
const episode = flags.has("latest-watch") ? store.latestWatchEpisode(repo) : null;
|
|
3672
|
-
if (flags.has("latest-watch") && episode === null)
|
|
3637
|
+
if (flags.has("latest-watch") && episode === null)
|
|
3673
3638
|
return fail(write, json, "brief", "no-watch", "no watch episode recorded for this repo yet — run `toolroll watch` first", EXIT.refused);
|
|
3674
|
-
}
|
|
3675
3639
|
const since = episode?.startedAt ??
|
|
3676
3640
|
text(flags, "since") ??
|
|
3677
3641
|
new Date(clock().getTime() - 24 * 60 * 60_000).toISOString();
|
|
@@ -3841,13 +3805,11 @@ async function decideCommand(positional, flags, context) {
|
|
|
3841
3805
|
return EXIT.refused;
|
|
3842
3806
|
}
|
|
3843
3807
|
const id = Number(idText);
|
|
3844
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
3808
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
3845
3809
|
return fail(write, json, "decide", "usage", "`toolroll decide [<id>] [--choose <option>]`", EXIT.usage);
|
|
3846
|
-
}
|
|
3847
3810
|
const decision = store.getDecision(id);
|
|
3848
|
-
if (decision === null)
|
|
3811
|
+
if (decision === null)
|
|
3849
3812
|
return fail(write, json, "decide", "unknown-decision", `no decision ${id}`, EXIT.refused);
|
|
3850
|
-
}
|
|
3851
3813
|
const choice = text(flags, "choose");
|
|
3852
3814
|
if (choice === undefined) {
|
|
3853
3815
|
const evidence = store.evidenceFor(id);
|
|
@@ -3892,14 +3854,10 @@ async function decideCommand(positional, flags, context) {
|
|
|
3892
3854
|
return EXIT.ok;
|
|
3893
3855
|
}
|
|
3894
3856
|
const acting = await askCredentials(flags, context);
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3857
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
3858
|
+
if (acting === null || !verified?.ok)
|
|
3859
|
+
return refuseApprover(context, "decide", verified, "answering takes `--as <you> --token <t>` — who decided is recorded, not asserted", String(id));
|
|
3898
3860
|
const { name: asWho, token } = acting;
|
|
3899
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
3900
|
-
if (!authenticated.ok) {
|
|
3901
|
-
return fail(write, json, "decide", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
3902
|
-
}
|
|
3903
3861
|
const note = text(flags, "note");
|
|
3904
3862
|
const answered = store.answerDecision({ id, choice, by: asWho, via: "cli", ...(note === undefined ? {} : { note }) }, clock(), mutationFrom(flags, clock()));
|
|
3905
3863
|
if (!answered.ok) {
|
|
@@ -3999,9 +3957,8 @@ async function serveCommand(flags, context) {
|
|
|
3999
3957
|
const port = Number(portGiven ?? 4180);
|
|
4000
3958
|
// Validated like demo's --port — a NaN handed to listen() surfaced as a
|
|
4001
3959
|
// baffling bind failure instead of a usage answer (round-4 finding 11).
|
|
4002
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
3960
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
4003
3961
|
return fail(write, json, "serve", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
4004
|
-
}
|
|
4005
3962
|
const host = text(flags, "host") ?? "127.0.0.1";
|
|
4006
3963
|
const allow = text(flags, "allow-host");
|
|
4007
3964
|
// The authorization ceiling: --repo (comma-separable) names repos this
|
|
@@ -4020,12 +3977,10 @@ async function serveCommand(flags, context) {
|
|
|
4020
3977
|
// allow-list of one, and which is meaningless without the machine's
|
|
4021
3978
|
// runner assertion — links bind to runs this machine owns.
|
|
4022
3979
|
const editor = text(flags, "editor");
|
|
4023
|
-
if (editor !== undefined && editor !== "vscode")
|
|
3980
|
+
if (editor !== undefined && editor !== "vscode")
|
|
4024
3981
|
return fail(write, json, "serve", "usage", "--editor supports: vscode", EXIT.usage);
|
|
4025
|
-
|
|
4026
|
-
if (editor !== undefined && localRunner === undefined) {
|
|
3982
|
+
if (editor !== undefined && localRunner === undefined)
|
|
4027
3983
|
return fail(write, json, "serve", "usage", "--editor needs --runner <name> — links bind to the worktrees this machine's runner owns", EXIT.usage);
|
|
4028
|
-
}
|
|
4029
3984
|
// The first-account road (setup review): with no approver, the login
|
|
4030
3985
|
// page offers to create one, gated by this code — printed below, once.
|
|
4031
3986
|
const setupCode = store.listApprovers().length === 0 ? String(randomInt(100_000, 1_000_000)) : undefined;
|
|
@@ -4076,18 +4031,13 @@ async function serveCommand(flags, context) {
|
|
|
4076
4031
|
async function requeueTask(positional, flags, context) {
|
|
4077
4032
|
const { store, write, json, clock } = context;
|
|
4078
4033
|
const [id] = positional;
|
|
4079
|
-
if (id === undefined)
|
|
4034
|
+
if (id === undefined)
|
|
4080
4035
|
return fail(write, json, "task requeue", "usage", "`toolroll task requeue <id> --as <you> --token <t>`", EXIT.usage);
|
|
4081
|
-
}
|
|
4082
4036
|
const acting = await askCredentials(flags, context);
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4037
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4038
|
+
if (acting === null || !verified?.ok)
|
|
4039
|
+
return refuseApprover(context, "task requeue", verified, "requeueing takes `--as <you> --token <t>` — who overrode the stall is recorded, not asserted", id);
|
|
4086
4040
|
const { name: asWho, token } = acting;
|
|
4087
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4088
|
-
if (!authenticated.ok) {
|
|
4089
|
-
return fail(write, json, "task requeue", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4090
|
-
}
|
|
4091
4041
|
const result = store.requeueTask(id, asWho, clock());
|
|
4092
4042
|
if (!result.ok) {
|
|
4093
4043
|
const why = {
|
|
@@ -4116,17 +4066,12 @@ async function requeueTask(positional, flags, context) {
|
|
|
4116
4066
|
async function regateTaskCommand(positional, flags, context) {
|
|
4117
4067
|
const { store, write, json, clock } = context;
|
|
4118
4068
|
const [id] = positional;
|
|
4119
|
-
if (id === undefined)
|
|
4069
|
+
if (id === undefined)
|
|
4120
4070
|
return fail(write, json, "task regate", "usage", "`toolroll task regate <id> --as <you> --token <t>`", EXIT.usage);
|
|
4121
|
-
}
|
|
4122
4071
|
const acting = await askCredentials(flags, context);
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
4127
|
-
if (!authenticated.ok) {
|
|
4128
|
-
return fail(write, json, "task regate", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4129
|
-
}
|
|
4072
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4073
|
+
if (acting === null || !verified?.ok)
|
|
4074
|
+
return refuseApprover(context, "task regate", verified, "rerunning the check takes `--as <you> --token <t>` — the rerun scope is approved in your name", id);
|
|
4130
4075
|
const result = regateTask(store, id, clock(), { kind: "operator", name: acting.name, token: acting.token });
|
|
4131
4076
|
if (!result.ok)
|
|
4132
4077
|
return fail(write, json, "task regate", result.reason, result.message, EXIT.refused);
|
|
@@ -4143,49 +4088,39 @@ async function regateTaskCommand(positional, flags, context) {
|
|
|
4143
4088
|
async function planTaskCommand(positional, flags, context) {
|
|
4144
4089
|
const { store, write, json, clock } = context;
|
|
4145
4090
|
const [id] = positional;
|
|
4146
|
-
if (id === undefined)
|
|
4091
|
+
if (id === undefined)
|
|
4147
4092
|
return fail(write, json, "task plan", "usage", "`toolroll task plan <id> --as <you> --token <t>`", EXIT.usage);
|
|
4148
|
-
}
|
|
4149
4093
|
const acting = await askCredentials(flags, context);
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
if (!authenticated.ok) {
|
|
4155
|
-
return fail(write, json, "task plan", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4156
|
-
}
|
|
4157
|
-
if (store.getTask(id) === null) {
|
|
4094
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4095
|
+
if (acting === null || !verified?.ok)
|
|
4096
|
+
return refuseApprover(context, "task plan", verified, "planning takes `--as <you> --token <t>` — it dispatches an agent that spends", id);
|
|
4097
|
+
if (store.getTask(id) === null)
|
|
4158
4098
|
return fail(write, json, "task plan", "refused", `no task ${id}`, EXIT.refused);
|
|
4159
|
-
}
|
|
4160
4099
|
const ref = store.refFor(BUILT_IN, id);
|
|
4161
4100
|
// Plan pins (P2/C7): a named pair binds the PLAN phase only, refused
|
|
4162
4101
|
// while a live plan claim is spending on the previous answer.
|
|
4163
4102
|
const pinProvider = text(flags, "provider");
|
|
4164
4103
|
const pinModel = text(flags, "model");
|
|
4165
4104
|
if (pinProvider !== undefined) {
|
|
4166
|
-
if (!isProviderId(pinProvider))
|
|
4105
|
+
if (!isProviderId(pinProvider))
|
|
4167
4106
|
return fail(write, json, "task plan", "usage", `unknown provider \`${pinProvider}\``, EXIT.usage);
|
|
4168
|
-
}
|
|
4169
4107
|
// A plan pin is an exact pair (v47): the route freezes model ids, and
|
|
4170
4108
|
// a pin with no model would bind nothing exact.
|
|
4171
|
-
if (pinModel === undefined || pinModel === "")
|
|
4109
|
+
if (pinModel === undefined || pinModel === "")
|
|
4172
4110
|
return fail(write, json, "task plan", "usage", "a plan pin names an exact --model with --provider — approvals bind exact routing", EXIT.usage);
|
|
4173
|
-
}
|
|
4174
4111
|
const valid = validateSpec({ provider: pinProvider, model: pinModel });
|
|
4175
4112
|
if (!valid.ok)
|
|
4176
4113
|
return fail(write, json, "task plan", "invalid", valid.problem, EXIT.usage);
|
|
4177
4114
|
const pinned = store.setPlanPins(ref.id, pinProvider, pinModel, clock());
|
|
4178
|
-
if (!pinned.ok)
|
|
4115
|
+
if (!pinned.ok)
|
|
4179
4116
|
return fail(write, json, "task plan", "refused", "a planner holds this task right now — the pin would reroute a live spend", EXIT.refused);
|
|
4180
|
-
}
|
|
4181
4117
|
}
|
|
4182
4118
|
else if (pinModel !== undefined) {
|
|
4183
4119
|
return fail(write, json, "task plan", "usage", "--model rides --provider for a plan pin", EXIT.usage);
|
|
4184
4120
|
}
|
|
4185
4121
|
const result = store.requestPlan(ref.id, clock());
|
|
4186
|
-
if (!result.ok)
|
|
4122
|
+
if (!result.ok)
|
|
4187
4123
|
return fail(write, json, "task plan", "refused", result.reason, EXIT.refused);
|
|
4188
|
-
}
|
|
4189
4124
|
const autoPlan = authorizePlanUnderMode(store, id, acting.name, clock());
|
|
4190
4125
|
return succeed(write, json, "task plan", { id, autoPlan, ...(pinProvider === undefined ? {} : { planProvider: pinProvider, planModel: pinModel ?? null }) }, () => [
|
|
4191
4126
|
`${id} will be planned before it is built: the next pass dispatches a planner${pinProvider === undefined ? "" : ` on ${pinProvider}${pinModel === undefined ? "" : ` \u00b7 ${pinModel}`}`}.`,
|
|
@@ -4224,9 +4159,8 @@ async function reviewTaskCommand(positional, flags, context) {
|
|
|
4224
4159
|
return fail(write, json, "task review", "usage", "--run and --all require --brief.", EXIT.usage);
|
|
4225
4160
|
const [runText] = positional;
|
|
4226
4161
|
const runId = Number(runText ?? "");
|
|
4227
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4162
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4228
4163
|
return fail(write, json, "task review", "usage", "`toolroll task review <run-id>`", EXIT.usage);
|
|
4229
|
-
}
|
|
4230
4164
|
// Read-only: the run's one automatic review, when it had one.
|
|
4231
4165
|
const review = buildReviewOf(store, runId);
|
|
4232
4166
|
if (review !== null) {
|
|
@@ -4261,26 +4195,19 @@ async function incidentCommand(positional, flags, context) {
|
|
|
4261
4195
|
write(" → toolroll incident resolve <id> --as <you> --token <t>");
|
|
4262
4196
|
return EXIT.refused;
|
|
4263
4197
|
}
|
|
4264
|
-
if (action !== "resolve")
|
|
4198
|
+
if (action !== "resolve")
|
|
4265
4199
|
return fail(write, json, "incident", "usage", "`toolroll incident [list|resolve <id>]`", EXIT.usage);
|
|
4266
|
-
}
|
|
4267
4200
|
const id = Number(idText);
|
|
4268
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
4201
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
4269
4202
|
return fail(write, json, "incident resolve", "usage", "`toolroll incident resolve <id> --as <you> --token <t>`", EXIT.usage);
|
|
4270
|
-
}
|
|
4271
4203
|
const acting = await askCredentials(flags, context);
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4204
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4205
|
+
if (acting === null || !verified?.ok)
|
|
4206
|
+
return refuseApprover(context, "incident resolve", verified, "resolving takes `--as <you> --token <t>` — who looked is recorded, not asserted", String(id));
|
|
4275
4207
|
const { name: asWho, token } = acting;
|
|
4276
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4277
|
-
if (!authenticated.ok) {
|
|
4278
|
-
return fail(write, json, "incident resolve", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
4279
|
-
}
|
|
4280
4208
|
const resolved = store.resolveIncident(id, asWho, clock());
|
|
4281
|
-
if (!resolved)
|
|
4209
|
+
if (!resolved)
|
|
4282
4210
|
return fail(write, json, "incident resolve", "unknown-or-resolved", `no unresolved incident ${id}`, EXIT.refused);
|
|
4283
|
-
}
|
|
4284
4211
|
return succeed(write, json, "incident resolve", { id, by: asWho }, () => [
|
|
4285
4212
|
`Resolved incident ${id}, as ${asWho}. The task's hold is lifted; the next tick may take it.`,
|
|
4286
4213
|
]);
|
|
@@ -4314,21 +4241,18 @@ async function webhookCommand(positional, _flags, context) {
|
|
|
4314
4241
|
return EXIT.ok;
|
|
4315
4242
|
}
|
|
4316
4243
|
if (action === "primary") {
|
|
4317
|
-
if (which === undefined || !isMessagingChannel(which))
|
|
4244
|
+
if (which === undefined || !isMessagingChannel(which))
|
|
4318
4245
|
return fail(write, json, "webhook primary", "usage", "primary is one of telegram, slack, discord", EXIT.usage);
|
|
4319
|
-
}
|
|
4320
4246
|
const primary = effectivePrimary(process.env, dir, telegramConfigured);
|
|
4321
|
-
if (!primary.configured.includes(which))
|
|
4247
|
+
if (!primary.configured.includes(which))
|
|
4322
4248
|
return fail(write, json, "webhook primary", "unconfigured", `${which} is not configured — set it up first, then choose it`, EXIT.refused);
|
|
4323
|
-
}
|
|
4324
4249
|
savePrimary(dir, which);
|
|
4325
4250
|
return succeed(write, json, "webhook primary", { primary: which }, () => [
|
|
4326
4251
|
`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."}`,
|
|
4327
4252
|
]);
|
|
4328
4253
|
}
|
|
4329
|
-
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4254
|
+
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4330
4255
|
return fail(write, json, "webhook", "usage", "`toolroll webhook [status|primary <service>|set console-url <url>]`", EXIT.usage);
|
|
4331
|
-
}
|
|
4332
4256
|
const saved = saveConsoleUrl(dir, value);
|
|
4333
4257
|
if (!saved.ok)
|
|
4334
4258
|
return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
|
|
@@ -4344,9 +4268,8 @@ function resumeProviderCommand(positional, flags, context) {
|
|
|
4344
4268
|
const { store, write, json } = context;
|
|
4345
4269
|
const command = "providers resume";
|
|
4346
4270
|
const provider = positional[0];
|
|
4347
|
-
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4271
|
+
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4348
4272
|
return fail(write, json, command, "usage", `Use \`toolroll providers resume <${PROVIDER_IDS.join("|")}>\`.`, EXIT.usage);
|
|
4349
|
-
}
|
|
4350
4273
|
const who = text(flags, "as") ?? process.env["USER"] ?? "operator";
|
|
4351
4274
|
const lifted = liftAuthPause(store, provider, "person", who, context.clock());
|
|
4352
4275
|
if (lifted === null)
|
|
@@ -4440,14 +4363,12 @@ async function providersCommand(flags, context) {
|
|
|
4440
4363
|
if (flags.has("report")) {
|
|
4441
4364
|
const runnerName = text(flags, "runner");
|
|
4442
4365
|
const runnerToken = text(flags, "token");
|
|
4443
|
-
if (runnerName === undefined || runnerToken === undefined)
|
|
4366
|
+
if (runnerName === undefined || runnerToken === undefined)
|
|
4444
4367
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4445
|
-
}
|
|
4446
4368
|
const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }), process.env, PROVIDER_IDS, provider => store.acpAttestationFor(provider, runnerName));
|
|
4447
4369
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4448
|
-
if (!reported.ok)
|
|
4370
|
+
if (!reported.ok)
|
|
4449
4371
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
4450
|
-
}
|
|
4451
4372
|
recorded = observed;
|
|
4452
4373
|
}
|
|
4453
4374
|
if (json) {
|
|
@@ -4594,9 +4515,8 @@ async function agentsCommand(positional, flags, context) {
|
|
|
4594
4515
|
async function keysCommand(positional, flags, context) {
|
|
4595
4516
|
const { write, json } = context;
|
|
4596
4517
|
const [action, provider, ...rest] = positional;
|
|
4597
|
-
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4518
|
+
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4598
4519
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4599
|
-
}
|
|
4600
4520
|
if (action === "status") {
|
|
4601
4521
|
// ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
|
|
4602
4522
|
const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
|
|
@@ -4622,12 +4542,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4622
4542
|
write(" → `keys auth <provider> subscription|api-key` switches; stored keys are kept either way");
|
|
4623
4543
|
return EXIT.ok;
|
|
4624
4544
|
}
|
|
4625
|
-
if (provider === undefined || !isProviderId(provider))
|
|
4545
|
+
if (provider === undefined || !isProviderId(provider))
|
|
4626
4546
|
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
|
|
4627
|
-
|
|
4628
|
-
if (!isKeyedProvider(provider)) {
|
|
4547
|
+
if (!isKeyedProvider(provider))
|
|
4629
4548
|
return fail(write, json, `keys ${action}`, "refused", `${ACP_AGENTS[provider].name} signs in its own way (\`${ACP_AGENTS[provider].signIn}\`) — Toolroll keeps no key for it`, EXIT.refused);
|
|
4630
|
-
}
|
|
4631
4549
|
if (action === "clear") {
|
|
4632
4550
|
const cleared = clearProviderKey(provider);
|
|
4633
4551
|
const mode = readAuthMode(provider);
|
|
@@ -4641,21 +4559,18 @@ async function keysCommand(positional, flags, context) {
|
|
|
4641
4559
|
}
|
|
4642
4560
|
if (action === "verify") {
|
|
4643
4561
|
const stored = readProviderKey(provider);
|
|
4644
|
-
if (stored === null)
|
|
4562
|
+
if (stored === null)
|
|
4645
4563
|
return fail(write, json, "keys verify", "refused", `no stored ${provider} key to verify`, EXIT.refused);
|
|
4646
|
-
}
|
|
4647
4564
|
const verdict = await verifyProviderKey(provider, stored);
|
|
4648
4565
|
return succeed(write, json, "keys verify", { provider, verdict }, () => [verdictWords(provider, verdict)]);
|
|
4649
4566
|
}
|
|
4650
4567
|
if (action === "auth") {
|
|
4651
4568
|
const [wanted] = rest;
|
|
4652
|
-
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4569
|
+
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4653
4570
|
return fail(write, json, "keys auth", "usage", `\`toolroll keys auth ${provider} subscription|api-key\``, EXIT.usage);
|
|
4654
|
-
}
|
|
4655
4571
|
const set = setAuthMode(provider, wanted);
|
|
4656
|
-
if (!set.ok)
|
|
4572
|
+
if (!set.ok)
|
|
4657
4573
|
return fail(write, json, "keys auth", "refused", `${provider} has no subscription login — it is api-key only`, EXIT.refused);
|
|
4658
|
-
}
|
|
4659
4574
|
return succeed(write, json, "keys auth", { provider, mode: wanted }, () => [
|
|
4660
4575
|
wanted === "subscription"
|
|
4661
4576
|
? `${provider} now uses its own login; its stored key is kept as the fallback you can switch to.`
|
|
@@ -4673,18 +4588,16 @@ async function keysCommand(positional, flags, context) {
|
|
|
4673
4588
|
}
|
|
4674
4589
|
}
|
|
4675
4590
|
else {
|
|
4676
|
-
if (process.stdin.isTTY)
|
|
4591
|
+
if (process.stdin.isTTY)
|
|
4677
4592
|
return fail(write, json, "keys set", "usage", "pipe the key in (\`toolroll keys set gemini < key.txt\` or via --key-file) — a key on an argv is visible to every process list", EXIT.usage);
|
|
4678
|
-
}
|
|
4679
4593
|
const chunks = [];
|
|
4680
4594
|
for await (const chunk of process.stdin)
|
|
4681
4595
|
chunks.push(chunk);
|
|
4682
4596
|
value = Buffer.concat(chunks).toString("utf8");
|
|
4683
4597
|
}
|
|
4684
4598
|
const saved = saveProviderKey(provider, value);
|
|
4685
|
-
if (!saved.ok)
|
|
4599
|
+
if (!saved.ok)
|
|
4686
4600
|
return fail(write, json, "keys set", "implausible", "that does not look like an API key — check the paste", EXIT.refused);
|
|
4687
|
-
}
|
|
4688
4601
|
// Save is instant (shape-checked); verification is a live credential
|
|
4689
4602
|
// check that spends no tokens — skip it with --no-verify for an offline
|
|
4690
4603
|
// machine or a provider you cannot reach right now.
|
|
@@ -4704,9 +4617,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4704
4617
|
async function peopleCommand(positional, flags, context) {
|
|
4705
4618
|
const { store, write, json, clock } = context;
|
|
4706
4619
|
const [action, ...rest] = positional;
|
|
4707
|
-
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4620
|
+
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4708
4621
|
return fail(write, json, "people", "usage", `unknown \`people ${action ?? ""}\` — try ${PEOPLE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4709
|
-
}
|
|
4710
4622
|
if (action === "list") {
|
|
4711
4623
|
const accounts = store.accountFacts();
|
|
4712
4624
|
const invites = store.openInvites(clock());
|
|
@@ -4728,14 +4640,12 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4728
4640
|
return EXIT.ok;
|
|
4729
4641
|
}
|
|
4730
4642
|
const acting = await askCredentials(flags, context);
|
|
4731
|
-
if (acting === null)
|
|
4643
|
+
if (acting === null)
|
|
4732
4644
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4733
|
-
}
|
|
4734
4645
|
// Inviting and widening access are credential acts; revoking only narrows.
|
|
4735
4646
|
const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
4736
|
-
if (!authenticated.ok)
|
|
4647
|
+
if (!authenticated.ok)
|
|
4737
4648
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4738
|
-
}
|
|
4739
4649
|
const repoGiven = text(flags, "repo");
|
|
4740
4650
|
const allProjects = flags.has("all-projects");
|
|
4741
4651
|
const noProjects = flags.has("no-projects");
|
|
@@ -4756,9 +4666,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4756
4666
|
}
|
|
4757
4667
|
if (action === "invite") {
|
|
4758
4668
|
const roleFlag = text(flags, "role") ?? "viewer";
|
|
4759
|
-
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4669
|
+
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4760
4670
|
return fail(write, json, "people invite", "usage", "--role viewer|approver (viewer is the default)", EXIT.usage);
|
|
4761
|
-
}
|
|
4762
4671
|
if (noProjects)
|
|
4763
4672
|
return fail(write, json, "people invite", "usage", "An invitation needs --repo <path> or all-project access.", EXIT.usage);
|
|
4764
4673
|
const minted = store.mintInvite(roleFlag, acting.name, clock(), undefined, projects);
|
|
@@ -4769,9 +4678,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4769
4678
|
]);
|
|
4770
4679
|
}
|
|
4771
4680
|
const [name] = rest;
|
|
4772
|
-
if (name === undefined || name.trim() === "")
|
|
4681
|
+
if (name === undefined || name.trim() === "")
|
|
4773
4682
|
return fail(write, json, "people revoke", "usage", "`toolroll people revoke <name> --as <you> --token <t>`", EXIT.usage);
|
|
4774
|
-
}
|
|
4775
4683
|
const severed = store.revokeAccount(name.trim(), acting.name, clock());
|
|
4776
4684
|
if (!severed.ok) {
|
|
4777
4685
|
const words = severed.reason === "last-approver"
|
|
@@ -4836,16 +4744,14 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4836
4744
|
async function modeCommand(positional, flags, context) {
|
|
4837
4745
|
const { store, write, json, clock } = context;
|
|
4838
4746
|
const [action] = positional;
|
|
4839
|
-
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4747
|
+
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4840
4748
|
return fail(write, json, "mode", "usage", `unknown \`mode ${action ?? ""}\` — try ${MODE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4841
|
-
}
|
|
4842
4749
|
const repoGiven = text(flags, "repo");
|
|
4843
4750
|
// The same canonical string every other road stores for the repository
|
|
4844
4751
|
// (a symlinked path — /var vs /private/var — must name the same mode).
|
|
4845
4752
|
const repo = repoGiven === undefined ? undefined : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4846
|
-
if (repo === undefined)
|
|
4753
|
+
if (repo === undefined)
|
|
4847
4754
|
return fail(write, json, "mode", "usage", "`mode` needs --repo <canonical path> — a mode is per-repository", EXIT.usage);
|
|
4848
|
-
}
|
|
4849
4755
|
const now = clock();
|
|
4850
4756
|
if (action === "show") {
|
|
4851
4757
|
const mode = store.activeMode(repo, now);
|
|
@@ -4860,14 +4766,12 @@ async function modeCommand(positional, flags, context) {
|
|
|
4860
4766
|
]);
|
|
4861
4767
|
}
|
|
4862
4768
|
const acting = await askCredentials(flags, context);
|
|
4863
|
-
if (acting === null)
|
|
4769
|
+
if (acting === null)
|
|
4864
4770
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4865
|
-
}
|
|
4866
4771
|
// Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
|
|
4867
4772
|
const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4868
|
-
if (!authed.ok)
|
|
4773
|
+
if (!authed.ok)
|
|
4869
4774
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4870
|
-
}
|
|
4871
4775
|
if (action === "revoke") {
|
|
4872
4776
|
// Lowering authority is one click for ANY approver (v4 doctrine).
|
|
4873
4777
|
const revoked = store.revokeMode(repo, acting.name, "operator", now);
|
|
@@ -4877,13 +4781,11 @@ async function modeCommand(positional, flags, context) {
|
|
|
4877
4781
|
}
|
|
4878
4782
|
// set: RAISING authority — the password ceremony above already ran.
|
|
4879
4783
|
const nameGiven = text(flags, "name") ?? "standard";
|
|
4880
|
-
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4784
|
+
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4881
4785
|
return fail(write, json, "mode set", "usage", "--name is standard or hands-off", EXIT.usage);
|
|
4882
|
-
}
|
|
4883
4786
|
const daysGiven = Number(text(flags, "days") ?? "1");
|
|
4884
|
-
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4787
|
+
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4885
4788
|
return fail(write, json, "mode set", "usage", `--days is 1 to ${MODE_MAX_DAYS} (a mode always expires)`, EXIT.usage);
|
|
4886
|
-
}
|
|
4887
4789
|
const expiry = new Date(now.getTime() + daysGiven * 24 * 60 * 60_000).toISOString();
|
|
4888
4790
|
const terms = presetTerms(nameGiven, expiry);
|
|
4889
4791
|
// Explicit term overrides ride the same flags (all optional):
|
|
@@ -4895,12 +4797,10 @@ async function modeCommand(positional, flags, context) {
|
|
|
4895
4797
|
terms.autoApproveFiling = true;
|
|
4896
4798
|
if (flag(flags, "plan-auto"))
|
|
4897
4799
|
terms.planAuto = true;
|
|
4898
|
-
if (["review-auto", "review-retry-auto", "repair-auto", "repair-max-attempts"].some(name => flags.has(name)))
|
|
4800
|
+
if (["review-auto", "review-retry-auto", "repair-auto", "repair-max-attempts"].some(name => flags.has(name)))
|
|
4899
4801
|
return fail(write, json, "mode set", "invalid", "Separate model review and automatic correction loops have been removed; finished work goes to the lead or user.", EXIT.refused);
|
|
4900
|
-
|
|
4901
|
-
if (terms.planAuto && !terms.autoApproveFiling) {
|
|
4802
|
+
if (terms.planAuto && !terms.autoApproveFiling)
|
|
4902
4803
|
return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
|
|
4903
|
-
}
|
|
4904
4804
|
// Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
|
|
4905
4805
|
// The signature names every chat app it covers: a grant signed before that named Telegram alone, and stays so.
|
|
4906
4806
|
if (flag(flags, "chat-approve")) {
|
|
@@ -4908,9 +4808,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4908
4808
|
terms.chatApproveChats = CHAT_APPROVE_ALL;
|
|
4909
4809
|
}
|
|
4910
4810
|
// Automerge requires a live merge-capable grant on the repo (D1).
|
|
4911
|
-
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4811
|
+
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4912
4812
|
return fail(write, json, "mode set", "no-grant", "automerge needs a merge-capable publication grant on this repo — file one first", EXIT.refused);
|
|
4913
|
-
}
|
|
4914
4813
|
const digest = modeDigestOf(terms);
|
|
4915
4814
|
const id = store.signMode({ repo, name: nameGiven, termsJson: modeTermsJson(terms), digest, signedBy: acting.name, absoluteExpiry: expiry, publication: terms.publication }, now);
|
|
4916
4815
|
return succeed(write, json, "mode set", { repo, id, digest, terms }, () => [
|
|
@@ -4921,9 +4820,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4921
4820
|
async function configCommand(positional, flags, context) {
|
|
4922
4821
|
const { store, write, json, clock } = context;
|
|
4923
4822
|
const [action, phase] = positional;
|
|
4924
|
-
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4823
|
+
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4925
4824
|
return fail(write, json, "config", "usage", `unknown \`config ${action}\` — try ${CONFIG_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4926
|
-
}
|
|
4927
4825
|
const repoGiven = text(flags, "repo");
|
|
4928
4826
|
const scope = repoGiven === undefined ? INSTALLATION_SCOPE : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4929
4827
|
if (action === undefined || action === "show") {
|
|
@@ -4992,21 +4890,18 @@ async function configCommand(positional, flags, context) {
|
|
|
4992
4890
|
}
|
|
4993
4891
|
return EXIT.ok;
|
|
4994
4892
|
}
|
|
4995
|
-
if (action !== "set" && action !== "clear")
|
|
4893
|
+
if (action !== "set" && action !== "clear")
|
|
4996
4894
|
return fail(write, json, "config", "usage", "`toolroll config [show|set <phase> --provider <p> [--model <m>]|clear <phase>] [--repo <path>] --as <you> --token <t>`", EXIT.usage);
|
|
4997
|
-
}
|
|
4998
4895
|
// Global dollar thresholds (v15, operator request): defaults for filings
|
|
4999
4896
|
// and an installation-wide backstop. Authenticated like every spend
|
|
5000
4897
|
// routing act; the per-task digest stays the authority.
|
|
5001
4898
|
if (phase === "budgets") {
|
|
5002
4899
|
const acting = await askCredentials(flags, context);
|
|
5003
|
-
if (acting === null)
|
|
4900
|
+
if (acting === null)
|
|
5004
4901
|
return fail(write, json, `config ${action}`, "usage", "changing spend defaults takes `--as <you> --token <t>`", EXIT.usage);
|
|
5005
|
-
}
|
|
5006
4902
|
const authedBudget = authenticateApprover(store, acting.name, acting.token);
|
|
5007
|
-
if (!authedBudget.ok)
|
|
4903
|
+
if (!authedBudget.ok)
|
|
5008
4904
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
5009
|
-
}
|
|
5010
4905
|
if (action === "clear") {
|
|
5011
4906
|
store.setSpendDefaults({ buildPerRunMicrousd: null }, acting.name, clock());
|
|
5012
4907
|
return succeed(write, json, "config clear", { budgets: null }, () => ["Spend defaults cleared — filings state their own numbers again."]);
|
|
@@ -5019,9 +4914,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5019
4914
|
return Number.isFinite(value) && value > 0 ? Math.round(value * 1_000_000) : false;
|
|
5020
4915
|
};
|
|
5021
4916
|
const build = parseUsd("build-usd");
|
|
5022
|
-
if (build === false)
|
|
4917
|
+
if (build === false)
|
|
5023
4918
|
return fail(write, json, "config set", "usage", "--build-usd is a positive dollar amount", EXIT.usage);
|
|
5024
|
-
}
|
|
5025
4919
|
store.setSpendDefaults({ buildPerRunMicrousd: build }, acting.name, clock());
|
|
5026
4920
|
return succeed(write, json, "config set", { budgets: store.getSpendDefaults() }, () => [
|
|
5027
4921
|
"Spend defaults set. New filings pre-fill from these; every approval still restates its own numbers:",
|
|
@@ -5033,17 +4927,14 @@ async function configCommand(positional, flags, context) {
|
|
|
5033
4927
|
// Installation-scoped only — a per-repo chat would splinter the spend
|
|
5034
4928
|
// ceiling that makes the ledger a ceiling at all.
|
|
5035
4929
|
if (phase === "chat") {
|
|
5036
|
-
if (repoGiven !== undefined)
|
|
4930
|
+
if (repoGiven !== undefined)
|
|
5037
4931
|
return fail(write, json, `config ${action}`, "usage", "chat is installation-scoped — no --repo", EXIT.usage);
|
|
5038
|
-
}
|
|
5039
4932
|
const acting = await askCredentials(flags, context);
|
|
5040
|
-
if (acting === null)
|
|
4933
|
+
if (acting === null)
|
|
5041
4934
|
return fail(write, json, `config ${action}`, "usage", "changing chat spend takes `--as <you> --token <t>`", EXIT.usage);
|
|
5042
|
-
}
|
|
5043
4935
|
const authedChat = authenticateApprover(store, acting.name, acting.token);
|
|
5044
|
-
if (!authedChat.ok)
|
|
4936
|
+
if (!authedChat.ok)
|
|
5045
4937
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
5046
|
-
}
|
|
5047
4938
|
if (action === "clear") {
|
|
5048
4939
|
store.clearChatConfig();
|
|
5049
4940
|
return succeed(write, json, "config clear", { chat: null }, () => ["Chat is off — the config row is gone."]);
|
|
@@ -5052,31 +4943,25 @@ async function configCommand(positional, flags, context) {
|
|
|
5052
4943
|
const requestedModel = text(flags, "model");
|
|
5053
4944
|
const weeklyUsd = text(flags, "weekly-usd");
|
|
5054
4945
|
const dailyGiven = text(flags, "daily-turns");
|
|
5055
|
-
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription")
|
|
4946
|
+
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription")
|
|
5056
4947
|
return fail(write, json, "config set", "usage", "chat provider is one of claude-subscription, codex-subscription, anthropic-api, openrouter-api", EXIT.usage);
|
|
5057
|
-
}
|
|
5058
4948
|
const model = requestedModel ?? (isSubscriptionChatProvider(provider) ? "default" : "");
|
|
5059
|
-
if (!validModelId(model))
|
|
4949
|
+
if (!validModelId(model))
|
|
5060
4950
|
return fail(write, json, "config set", "usage", "--model must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes", EXIT.usage);
|
|
5061
|
-
}
|
|
5062
4951
|
const priced = isDirectChatProvider(provider) ? livePin(store, provider, model) ?? priceOf(model) : null;
|
|
5063
|
-
if (isDirectChatProvider(provider) && priced === null)
|
|
4952
|
+
if (isDirectChatProvider(provider) && priced === null)
|
|
5064
4953
|
return fail(write, json, "config set", "unpriced-model", `chat reserves worst-case spend up front, so the model needs a price — run "toolroll models check" to load live prices, or pick one priced today: ${PRICED_MODELS.join(", ")}`, EXIT.refused);
|
|
5065
|
-
}
|
|
5066
4954
|
const weekly = Number(weeklyUsd);
|
|
5067
|
-
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
4955
|
+
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
5068
4956
|
return fail(write, json, "config set", "usage", "--weekly-usd <dollars> is required — chat without a ceiling is not configured, it is unbounded", EXIT.usage);
|
|
5069
|
-
}
|
|
5070
4957
|
const daily = dailyGiven === undefined ? 50 : Number(dailyGiven);
|
|
5071
|
-
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
4958
|
+
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
5072
4959
|
return fail(write, json, "config set", "usage", "--daily-turns is a whole number between 1 and 1000", EXIT.usage);
|
|
5073
|
-
}
|
|
5074
4960
|
// The CLI pins from the compiled table (the console additionally offers
|
|
5075
4961
|
// OpenRouter's live catalog — priced by the party that bills it).
|
|
5076
4962
|
const pinned = isDirectChatProvider(provider) ? priced : { inMicrousd: 0, outMicrousd: 0 };
|
|
5077
|
-
if (pinned === null)
|
|
4963
|
+
if (pinned === null)
|
|
5078
4964
|
return fail(write, json, "config set", "unpriced-model", `no compiled price for ${model}`, EXIT.refused);
|
|
5079
|
-
}
|
|
5080
4965
|
store.setChatConfig({
|
|
5081
4966
|
provider,
|
|
5082
4967
|
model,
|
|
@@ -5097,42 +4982,33 @@ async function configCommand(positional, flags, context) {
|
|
|
5097
4982
|
: []),
|
|
5098
4983
|
]);
|
|
5099
4984
|
}
|
|
5100
|
-
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
4985
|
+
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5101
4986
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5102
|
-
}
|
|
5103
4987
|
const acting = await askCredentials(flags, context);
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5108
|
-
if (!authenticated.ok) {
|
|
5109
|
-
return fail(write, json, `config ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, phase), EXIT.refused);
|
|
5110
|
-
}
|
|
4988
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4989
|
+
if (acting === null || !verified?.ok)
|
|
4990
|
+
return refuseApprover(context, `config ${action}`, verified, "changing spend routing takes `--as <you> --token <t>`", phase);
|
|
5111
4991
|
// The STRONG tier (v47): a second, named row per phase the routing policy
|
|
5112
4992
|
// reaches for when size, quality, evidence, or publication demand it.
|
|
5113
4993
|
// Authenticated and audited exactly like the routine row; existing
|
|
5114
4994
|
// approvals are untouched — a sealed route never re-resolves.
|
|
5115
4995
|
// The LIGHT tier (v2): the fast agent a small change builds on.
|
|
5116
4996
|
const tierGiven = text(flags, "tier");
|
|
5117
|
-
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
4997
|
+
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
5118
4998
|
return fail(write, json, `config ${action}`, "usage", "--tier is `light` or `strong` — the routine tier is the plain phase row", EXIT.usage);
|
|
5119
|
-
}
|
|
5120
4999
|
// A light planner would never run (a small change makes no plan): refused rather than kept and ignored.
|
|
5121
5000
|
// Clearing one left from before stays possible.
|
|
5122
|
-
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5001
|
+
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5123
5002
|
return fail(write, json, `config ${action}`, "usage", "--tier light is for build only — a small change makes no plan, and repairs resume the builder's session", EXIT.usage);
|
|
5124
|
-
}
|
|
5125
5003
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5126
5004
|
// has more room runs.
|
|
5127
5005
|
if (flag(flags, "also")) {
|
|
5128
|
-
if (phase !== "plan" && phase !== "build")
|
|
5006
|
+
if (phase !== "plan" && phase !== "build")
|
|
5129
5007
|
return fail(write, json, `config ${action}`, "usage", "--also is for plan or build — repairs follow the build's provider", EXIT.usage);
|
|
5130
|
-
}
|
|
5131
5008
|
const tier = (tierGiven ?? "routine");
|
|
5132
5009
|
const alsoProvider = text(flags, "provider");
|
|
5133
|
-
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5010
|
+
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5134
5011
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5135
|
-
}
|
|
5136
5012
|
if (action === "clear") {
|
|
5137
5013
|
const removed = store.clearPhaseTierAlternates(scope, phase, tier, alsoProvider ?? null);
|
|
5138
5014
|
return succeed(write, json, "config clear", { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
@@ -5140,9 +5016,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5140
5016
|
]);
|
|
5141
5017
|
}
|
|
5142
5018
|
const alsoModel = text(flags, "model");
|
|
5143
|
-
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5019
|
+
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5144
5020
|
return fail(write, json, "config set", "usage", "--also names an exact --provider and --model — approvals bind exact routing", EXIT.usage);
|
|
5145
|
-
}
|
|
5146
5021
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5147
5022
|
if (!alsoValid.ok)
|
|
5148
5023
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
@@ -5175,18 +5050,15 @@ async function configCommand(positional, flags, context) {
|
|
|
5175
5050
|
]);
|
|
5176
5051
|
}
|
|
5177
5052
|
const providerGiven = text(flags, "provider");
|
|
5178
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5053
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5179
5054
|
return fail(write, json, "config set", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5180
|
-
}
|
|
5181
5055
|
const modelGiven = text(flags, "model") ?? null;
|
|
5182
5056
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
5183
|
-
if (!valid.ok)
|
|
5057
|
+
if (!valid.ok)
|
|
5184
5058
|
return fail(write, json, "config set", "invalid", valid.problem, EXIT.usage);
|
|
5185
|
-
}
|
|
5186
5059
|
if (tierGiven === "light") {
|
|
5187
|
-
if (modelGiven === null)
|
|
5060
|
+
if (modelGiven === null)
|
|
5188
5061
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5189
|
-
}
|
|
5190
5062
|
store.setPhaseTierConfig(scope, phase, "light", providerGiven, modelGiven, acting.name, clock());
|
|
5191
5063
|
return succeed(write, json, "config set", { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5192
5064
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
@@ -5194,12 +5066,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5194
5066
|
]);
|
|
5195
5067
|
}
|
|
5196
5068
|
if (tierGiven === "strong") {
|
|
5197
|
-
if (phase === "review" && providerGiven === "gemini")
|
|
5069
|
+
if (phase === "review" && providerGiven === "gemini")
|
|
5198
5070
|
return fail(write, json, "config set", "invalid", "gemini has no isolation posture for the review phase yet — name claude or codex as the strong reviewer", EXIT.usage);
|
|
5199
|
-
|
|
5200
|
-
if (modelGiven === null) {
|
|
5071
|
+
if (modelGiven === null)
|
|
5201
5072
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5202
|
-
}
|
|
5203
5073
|
store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
|
|
5204
5074
|
return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5205
5075
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
@@ -5238,9 +5108,8 @@ async function setupCommand(positional, flags, context) {
|
|
|
5238
5108
|
const action = positional[0] ?? "show";
|
|
5239
5109
|
const repo = text(flags, "repo");
|
|
5240
5110
|
if (action === "show") {
|
|
5241
|
-
if (repo === undefined)
|
|
5111
|
+
if (repo === undefined)
|
|
5242
5112
|
return fail(write, json, "setup show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5243
|
-
}
|
|
5244
5113
|
const live = store.liveWorktreeSetup(repo);
|
|
5245
5114
|
if (json) {
|
|
5246
5115
|
write(envelopeJson({ ok: true, command: "setup show", repo, setup: live }));
|
|
@@ -5251,20 +5120,14 @@ async function setupCommand(positional, flags, context) {
|
|
|
5251
5120
|
: `${repo} runs before every agent:\n ${live.command}\n timeout ${Math.round(live.timeoutMs / 1000)}s · digest ${live.digest} · approved by ${live.approvedBy} at ${live.approvedAt}`);
|
|
5252
5121
|
return EXIT.ok;
|
|
5253
5122
|
}
|
|
5254
|
-
if (action !== "set" && action !== "clear")
|
|
5123
|
+
if (action !== "set" && action !== "clear")
|
|
5255
5124
|
return fail(write, json, "setup", "usage", "`toolroll setup [show|set --command <cmd> [--timeout-seconds <n>] --yes|clear] --repo <path> --as <you> --token <t>`", EXIT.usage);
|
|
5256
|
-
|
|
5257
|
-
if (repo === undefined) {
|
|
5125
|
+
if (repo === undefined)
|
|
5258
5126
|
return fail(write, json, `setup ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5259
|
-
}
|
|
5260
5127
|
const acting = await askCredentials(flags, context);
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5265
|
-
if (!authenticated.ok) {
|
|
5266
|
-
return fail(write, json, `setup ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5267
|
-
}
|
|
5128
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5129
|
+
if (acting === null || !verified?.ok)
|
|
5130
|
+
return refuseApprover(context, `setup ${action}`, verified, "an approved setup runs unattended in every future worktree — changing it takes `--as <you> --token <t>`", repo);
|
|
5268
5131
|
if (action === "clear") {
|
|
5269
5132
|
const cleared = store.clearWorktreeSetup(repo, acting.name, clock());
|
|
5270
5133
|
return succeed(write, json, "setup clear", { repo, cleared }, () => [
|
|
@@ -5272,25 +5135,21 @@ async function setupCommand(positional, flags, context) {
|
|
|
5272
5135
|
]);
|
|
5273
5136
|
}
|
|
5274
5137
|
const command = text(flags, "command");
|
|
5275
|
-
if (command === undefined || command.trim() === "")
|
|
5138
|
+
if (command === undefined || command.trim() === "")
|
|
5276
5139
|
return fail(write, json, "setup set", "usage", "--command <cmd> is what every fresh checkout will run", EXIT.usage);
|
|
5277
|
-
|
|
5278
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5140
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5279
5141
|
return fail(write, json, "setup set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5280
|
-
}
|
|
5281
5142
|
// Literal credentials never become standing rows (audit IV-5): a command
|
|
5282
5143
|
// that embeds a token is stored forever in plain text. Reference an
|
|
5283
5144
|
// environment variable the runner already exports instead. `$TOKEN` is
|
|
5284
5145
|
// a reference and passes; `=secret123` is a value and refuses.
|
|
5285
5146
|
const credentialShaped = /([A-Za-z0-9_-]*(?:token|secret|password|passwd|apikey|api_key|authorization|bearer|credential)[A-Za-z0-9_-]*\s*[=:]\s*)(?![$"']?\$)\S+/i.test(command) ||
|
|
5286
5147
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5287
|
-
if (credentialShaped)
|
|
5148
|
+
if (credentialShaped)
|
|
5288
5149
|
return fail(write, json, "setup set", "credential-shaped", "the command appears to embed a credential — reference an environment variable the runner exports (e.g. $NPM_TOKEN) instead of a literal value", EXIT.usage);
|
|
5289
|
-
}
|
|
5290
5150
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5291
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5151
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5292
5152
|
return fail(write, json, "setup set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5293
|
-
}
|
|
5294
5153
|
if (flags.get("yes") !== true) {
|
|
5295
5154
|
if (json) {
|
|
5296
5155
|
write(envelopeJson({ ok: false, command: "setup set", reason: "unconfirmed", repo, setupCommand: command, timeoutSeconds }));
|
|
@@ -5330,9 +5189,8 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5330
5189
|
const action = positional[0] ?? "show";
|
|
5331
5190
|
const repo = text(flags, "repo");
|
|
5332
5191
|
if (action === "show") {
|
|
5333
|
-
if (repo === undefined)
|
|
5192
|
+
if (repo === undefined)
|
|
5334
5193
|
return fail(write, json, "verify show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5335
|
-
}
|
|
5336
5194
|
const live = store.liveVerifyCommand(repo);
|
|
5337
5195
|
const quick = liveQuickCommand(store, repo);
|
|
5338
5196
|
const level = projectCheckLevel(store, repo);
|
|
@@ -5372,24 +5230,18 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5372
5230
|
...(wanted !== "full" ? ["The full check still runs when a pull request opens, and Merge waits for it unless a person merges anyway."] : []),
|
|
5373
5231
|
]);
|
|
5374
5232
|
}
|
|
5375
|
-
if (action !== "set" && action !== "clear")
|
|
5233
|
+
if (action !== "set" && action !== "clear")
|
|
5376
5234
|
return fail(write, json, "verify", "usage", "`toolroll verify [show|level quick|full|off|set --command <cmd>|--quick <cmd> [--timeout-seconds <n>] [--self-heal --setup-digest <shown>] --yes|clear [--level quick]] --repo <path> --as <you> --token <t>`", EXIT.usage);
|
|
5377
|
-
}
|
|
5378
5235
|
// The quick command is its own approved grant beside the full one.
|
|
5379
5236
|
const quickValue = text(flags, "quick");
|
|
5380
5237
|
const quickClear = action === "clear" && text(flags, "level") === "quick";
|
|
5381
5238
|
const grantKey = repo === undefined ? undefined : quickValue !== undefined || quickClear ? quickVerifyKey(repo) : repo;
|
|
5382
|
-
if (repo === undefined)
|
|
5239
|
+
if (repo === undefined)
|
|
5383
5240
|
return fail(write, json, `verify ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5384
|
-
}
|
|
5385
5241
|
const acting = await askCredentials(flags, context);
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5390
|
-
if (!authenticated.ok) {
|
|
5391
|
-
return fail(write, json, `verify ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5392
|
-
}
|
|
5242
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5243
|
+
if (acting === null || !verified?.ok)
|
|
5244
|
+
return refuseApprover(context, `verify ${action}`, verified, "an approved verification command runs unattended after every future build — changing it takes `--as <you> --token <t>`", repo);
|
|
5393
5245
|
if (action === "clear") {
|
|
5394
5246
|
const cleared = store.clearVerifyCommand(grantKey, acting.name, clock());
|
|
5395
5247
|
if (quickClear)
|
|
@@ -5401,38 +5253,30 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5401
5253
|
]);
|
|
5402
5254
|
}
|
|
5403
5255
|
const command = quickValue ?? text(flags, "command");
|
|
5404
|
-
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5256
|
+
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5405
5257
|
return fail(write, json, "verify set", "usage", "give the quick command with --quick \"<cmd>\" or the full one with --command, not both", EXIT.usage);
|
|
5406
|
-
|
|
5407
|
-
if (quickValue !== undefined && flags.get("self-heal") === true) {
|
|
5258
|
+
if (quickValue !== undefined && flags.get("self-heal") === true)
|
|
5408
5259
|
return fail(write, json, "verify set", "usage", "--self-heal applies to the full command only", EXIT.usage);
|
|
5409
|
-
|
|
5410
|
-
if (command === undefined || command.trim() === "") {
|
|
5260
|
+
if (command === undefined || command.trim() === "")
|
|
5411
5261
|
return fail(write, json, "verify set", "usage", "--command <cmd> is what the plane re-runs after every commit", EXIT.usage);
|
|
5412
|
-
|
|
5413
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5262
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5414
5263
|
return fail(write, json, "verify set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5415
|
-
}
|
|
5416
5264
|
// Same rule as an approved setup command (audit IV-5): a command that
|
|
5417
5265
|
// embeds a literal credential is stored forever in plain text.
|
|
5418
5266
|
const credentialShaped = /([A-Za-z0-9_-]*(?:token|secret|password|passwd|apikey|api_key|authorization|bearer|credential)[A-Za-z0-9_-]*\s*[=:]\s*)(?![$"']?\$)\S+/i.test(command) ||
|
|
5419
5267
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5420
|
-
if (credentialShaped)
|
|
5268
|
+
if (credentialShaped)
|
|
5421
5269
|
return fail(write, json, "verify set", "credential-shaped", "the command appears to embed a credential — reference an environment variable the runner exports (e.g. $NPM_TOKEN) instead of a literal value", EXIT.usage);
|
|
5422
|
-
}
|
|
5423
5270
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5424
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5271
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5425
5272
|
return fail(write, json, "verify set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5426
|
-
}
|
|
5427
5273
|
const selfHeal = flags.get("self-heal") === true;
|
|
5428
5274
|
const expectedSetupDigest = text(flags, "setup-digest");
|
|
5429
|
-
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5275
|
+
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5430
5276
|
return fail(write, json, "verify set", "usage", "--setup-digest is only valid with --self-heal", EXIT.usage);
|
|
5431
|
-
}
|
|
5432
5277
|
const recoverySetup = selfHeal ? store.liveWorktreeSetup(repo) : null;
|
|
5433
|
-
if (selfHeal && recoverySetup === null)
|
|
5278
|
+
if (selfHeal && recoverySetup === null)
|
|
5434
5279
|
return fail(write, json, "verify set", "setup-required", "self-healing needs an approved project setup first — set the exact preparation command, then approve verification again with --self-heal", EXIT.refused);
|
|
5435
|
-
}
|
|
5436
5280
|
if (flags.get("yes") !== true) {
|
|
5437
5281
|
if (json) {
|
|
5438
5282
|
write(envelopeJson({
|
|
@@ -5476,12 +5320,10 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5476
5320
|
}
|
|
5477
5321
|
return EXIT.refused;
|
|
5478
5322
|
}
|
|
5479
|
-
if (selfHeal && expectedSetupDigest === undefined)
|
|
5323
|
+
if (selfHeal && expectedSetupDigest === undefined)
|
|
5480
5324
|
return fail(write, json, "verify set", "setup-digest-required", `confirm self-healing with --setup-digest ${recoverySetup?.digest} exactly as previewed`, EXIT.refused);
|
|
5481
|
-
|
|
5482
|
-
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest) {
|
|
5325
|
+
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest)
|
|
5483
5326
|
return fail(write, json, "verify set", "stale-approval", `the approved setup changed after preview — expected ${expectedSetupDigest}, current ${recoverySetup?.digest}; preview verification again before approving`, EXIT.refused);
|
|
5484
|
-
}
|
|
5485
5327
|
const saved = store.setVerifyCommand({
|
|
5486
5328
|
repo: grantKey,
|
|
5487
5329
|
command,
|
|
@@ -5547,13 +5389,9 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5547
5389
|
}
|
|
5548
5390
|
if (action === "grant" || action === "clear") {
|
|
5549
5391
|
const acting = await askCredentials(flags, context);
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5554
|
-
if (!authenticated.ok) {
|
|
5555
|
-
return fail(write, json, `intake ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5556
|
-
}
|
|
5392
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5393
|
+
if (acting === null || !verified?.ok)
|
|
5394
|
+
return refuseApprover(context, `intake ${action}`, verified, "an intake grant is standing authority — it takes `--as <you> --token <t>`", repo);
|
|
5557
5395
|
if (action === "clear") {
|
|
5558
5396
|
const cleared = store.clearIntakeGrant(repo, acting.name, clock());
|
|
5559
5397
|
return succeed(write, json, "intake clear", { repo, cleared }, () => [
|
|
@@ -5562,12 +5400,10 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5562
5400
|
}
|
|
5563
5401
|
const github = text(flags, "github");
|
|
5564
5402
|
const label = text(flags, "label");
|
|
5565
|
-
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5403
|
+
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5566
5404
|
return fail(write, json, "intake grant", "usage", "--github <owner/name> names the repository on GitHub", EXIT.usage);
|
|
5567
|
-
|
|
5568
|
-
if (label === undefined || !LABEL_SHAPE.test(label)) {
|
|
5405
|
+
if (label === undefined || !LABEL_SHAPE.test(label))
|
|
5569
5406
|
return fail(write, json, "intake grant", "usage", "--label <label> names the exact label that nominates an issue", EXIT.usage);
|
|
5570
|
-
}
|
|
5571
5407
|
const reviewersRaw = text(flags, "reviewers");
|
|
5572
5408
|
const reviewers = reviewersRaw === undefined
|
|
5573
5409
|
? null
|
|
@@ -5598,13 +5434,11 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5598
5434
|
`Granted: issues in ${github} labeled "${label}" become unapproved proposals for ${repo}.`,
|
|
5599
5435
|
]);
|
|
5600
5436
|
}
|
|
5601
|
-
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5437
|
+
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5602
5438
|
return fail(write, json, "intake", "usage", "`toolroll intake [show|grant|clear|preview|run|pr-comments] --repo <path> …`", EXIT.usage);
|
|
5603
|
-
}
|
|
5604
5439
|
const grant = store.liveIntakeGrant(repo);
|
|
5605
|
-
if (grant === null)
|
|
5440
|
+
if (grant === null)
|
|
5606
5441
|
return fail(write, json, `intake ${action}`, "no-grant", `no intake grant for ${repo} — \`toolroll intake grant\` states the terms`, EXIT.refused);
|
|
5607
|
-
}
|
|
5608
5442
|
if (action === "pr-comments") {
|
|
5609
5443
|
// PR review comments become LOCAL diff comments (M8.17): own PRs only
|
|
5610
5444
|
// (the publication table IS the list of ours), named reviewers only,
|
|
@@ -5612,9 +5446,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5612
5446
|
// shared validator. Nothing here authorizes a spawn — ingested
|
|
5613
5447
|
// comments wait on the run page for the same one-tap seal and the
|
|
5614
5448
|
// same scope approval as comments typed in the console.
|
|
5615
|
-
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5449
|
+
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5616
5450
|
return fail(write, json, "intake pr-comments", "no-reviewers", "the grant names no reviewers — `intake grant --reviewers <logins>` is the authority for this", EXIT.refused);
|
|
5617
|
-
}
|
|
5618
5451
|
const publications = store
|
|
5619
5452
|
.openedPublications()
|
|
5620
5453
|
.filter(one => one.prNumber !== null &&
|
|
@@ -5695,9 +5528,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5695
5528
|
}
|
|
5696
5529
|
const limit = Math.min(Math.max(Number(text(flags, "limit") ?? "50"), 1), 200);
|
|
5697
5530
|
const asked = await gh("gh", ["issue", "list", "--repo", grant.github, "--label", grant.label, "--state", "open", "--limit", String(limit), "--json", "number,title,updatedAt"], { timeoutMs: 20_000 });
|
|
5698
|
-
if (asked.code !== 0)
|
|
5531
|
+
if (asked.code !== 0)
|
|
5699
5532
|
return fail(write, json, `intake ${action}`, "github-unreachable", `gh could not list ${grant.github}: ${(asked.stderr.split("\n")[0] ?? "").slice(0, 200)}`, EXIT.failed);
|
|
5700
|
-
}
|
|
5701
5533
|
let issues;
|
|
5702
5534
|
try {
|
|
5703
5535
|
issues = JSON.parse(asked.stdout);
|
|
@@ -5818,16 +5650,13 @@ function templateCommand(positional, flags, context) {
|
|
|
5818
5650
|
write("`toolroll template show <name>` · `template apply <name> --repo <path>`");
|
|
5819
5651
|
return EXIT.ok;
|
|
5820
5652
|
}
|
|
5821
|
-
if (action !== "show" && action !== "apply")
|
|
5653
|
+
if (action !== "show" && action !== "apply")
|
|
5822
5654
|
return fail(write, json, `template ${action}`, "usage", "`toolroll template list | show <name> | apply <name> --repo <path> [--file]`", EXIT.usage);
|
|
5823
|
-
|
|
5824
|
-
if (name === undefined) {
|
|
5655
|
+
if (name === undefined)
|
|
5825
5656
|
return fail(write, json, `template ${action}`, "usage", "which template? `toolroll template list` names them", EXIT.usage);
|
|
5826
|
-
}
|
|
5827
5657
|
const template = templateByName(name);
|
|
5828
|
-
if (template === null)
|
|
5658
|
+
if (template === null)
|
|
5829
5659
|
return fail(write, json, `template ${action}`, "unknown", `no template named ${name} — \`toolroll template list\``, EXIT.refused);
|
|
5830
|
-
}
|
|
5831
5660
|
if (action === "show") {
|
|
5832
5661
|
if (json) {
|
|
5833
5662
|
write(envelopeJson({ ok: true, command: "template show", template }));
|
|
@@ -5868,13 +5697,11 @@ function templateCommand(positional, flags, context) {
|
|
|
5868
5697
|
return EXIT.ok;
|
|
5869
5698
|
}
|
|
5870
5699
|
// apply
|
|
5871
|
-
if (template.kind === "recipe")
|
|
5700
|
+
if (template.kind === "recipe")
|
|
5872
5701
|
return fail(write, json, "template apply", "recipe", `${template.name} cannot be applied: ${template.why} \`toolroll template show ${template.name}\` walks the ceremonies.`, EXIT.refused);
|
|
5873
|
-
}
|
|
5874
5702
|
const repoGiven = text(flags, "repo");
|
|
5875
|
-
if (repoGiven === undefined)
|
|
5703
|
+
if (repoGiven === undefined)
|
|
5876
5704
|
return fail(write, json, "template apply", "usage", "which repository? --repo <path> — a template never guesses where work lands", EXIT.usage);
|
|
5877
|
-
}
|
|
5878
5705
|
const goal = text(flags, "goal") ?? template.goal;
|
|
5879
5706
|
const outOfScope = text(flags, "not") ?? template.outOfScope;
|
|
5880
5707
|
const touches = text(flags, "touches") === undefined
|
|
@@ -6015,22 +5842,18 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6015
5842
|
const runnerName = text(flags, "runner");
|
|
6016
5843
|
const inlineToken = text(flags, "token");
|
|
6017
5844
|
const tokenPath = text(flags, "token-file");
|
|
6018
|
-
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
5845
|
+
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
6019
5846
|
return fail(write, json, "daemon install", "usage", "choose one credential source: --token or --token-file", EXIT.usage);
|
|
6020
|
-
}
|
|
6021
5847
|
const token = inlineToken ?? readTokenFile(tokenPath);
|
|
6022
|
-
if (runnerName === undefined || token === undefined)
|
|
5848
|
+
if (runnerName === undefined || token === undefined)
|
|
6023
5849
|
return fail(write, json, "daemon install", "usage", "`toolroll daemon install --runner <name> (--token <t> | --token-file <path>) --repo <path>` (plus any watch flags to bake in)", EXIT.usage);
|
|
6024
|
-
}
|
|
6025
5850
|
const auth = authenticate(store, runnerName, token);
|
|
6026
|
-
if (!auth.ok)
|
|
5851
|
+
if (!auth.ok)
|
|
6027
5852
|
return fail(write, json, "daemon install", auth.reason, describeAuth(auth.reason, runnerName), EXIT.refused);
|
|
6028
|
-
}
|
|
6029
5853
|
const heartbeatBefore = store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6030
5854
|
const located = await resolveBin();
|
|
6031
|
-
if (located === null)
|
|
5855
|
+
if (located === null)
|
|
6032
5856
|
return fail(write, json, "daemon install", "no-bin", "`toolroll` is not on the PATH the service would use — run `toolroll link` first, or pass --bin <absolute path>", EXIT.refused);
|
|
6033
|
-
}
|
|
6034
5857
|
const watchFlags = [];
|
|
6035
5858
|
for (const name of ["pool", "model", "repair-model", "provider", "plan-model", "plan-provider", "max", "turns", "tick-every", "bridge-every", "reconcile-every", "max-open-decisions", "containment"]) {
|
|
6036
5859
|
const value = text(flags, name);
|
|
@@ -6067,15 +5890,13 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6067
5890
|
return EXIT.ok;
|
|
6068
5891
|
}
|
|
6069
5892
|
const installed = await installReplacingLegacy(plan, token, legacy, supervise);
|
|
6070
|
-
if (!installed.ok)
|
|
5893
|
+
if (!installed.ok)
|
|
6071
5894
|
return fail(write, json, "daemon install", "supervisor", installed.message, EXIT.failed);
|
|
6072
|
-
}
|
|
6073
5895
|
// The legacy watch beat for this runner until it stopped: only a beat after that proves the new one.
|
|
6074
5896
|
const readySince = legacy === null ? heartbeatBefore : store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6075
5897
|
const started = await daemonStatus(plan, supervise);
|
|
6076
|
-
if (started.state !== "running")
|
|
5898
|
+
if (started.state !== "running")
|
|
6077
5899
|
return fail(write, json, "daemon install", "not-running", `the service was installed but did not stay running (${started.detail}) — read ${plan.logPath}; no work will be claimed until this is fixed`, EXIT.failed);
|
|
6078
|
-
}
|
|
6079
5900
|
// A supervisor PID is necessary but not sufficient: macOS can leave a
|
|
6080
5901
|
// process stuck behind a protected-folder access check before it ever
|
|
6081
5902
|
// opens the queue. The credentialed runner heartbeat is the end-to-end
|
|
@@ -6725,15 +6546,13 @@ async function watchCommand(flags, context) {
|
|
|
6725
6546
|
return demoFence;
|
|
6726
6547
|
const runner = text(flags, "runner");
|
|
6727
6548
|
const token = text(flags, "token") ?? readTokenFile(text(flags, "token-file"));
|
|
6728
|
-
if (runner === undefined || token === undefined)
|
|
6549
|
+
if (runner === undefined || token === undefined)
|
|
6729
6550
|
return fail(write, json, "watch", "usage", "`toolroll watch --runner <name> --token <t>|--token-file <path> --repo <path> [--for <ms>]`", EXIT.usage);
|
|
6730
|
-
}
|
|
6731
6551
|
// Passes built from these flags authenticate with the resolved token.
|
|
6732
6552
|
flags.set("token", token);
|
|
6733
6553
|
const auth = authenticate(store, runner, token);
|
|
6734
|
-
if (!auth.ok)
|
|
6554
|
+
if (!auth.ok)
|
|
6735
6555
|
return fail(write, json, "watch", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
6736
|
-
}
|
|
6737
6556
|
const repo = repoFrom(flags);
|
|
6738
6557
|
// The envelope contract holds for long commands too (Codex M5-M8 audit,
|
|
6739
6558
|
// C-1): in --json mode every progress line goes to stderr, and stdout
|
|
@@ -7028,9 +6847,8 @@ async function onboardCommand(flags, context) {
|
|
|
7028
6847
|
const seams = context.onboardSeams ?? {};
|
|
7029
6848
|
const portGiven = text(flags, "port");
|
|
7030
6849
|
const port = Number(portGiven ?? 4180);
|
|
7031
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6850
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7032
6851
|
return fail(write, json, "onboard", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7033
|
-
}
|
|
7034
6852
|
// The console where `up` last served, unless --port says otherwise.
|
|
7035
6853
|
const served = portGiven === undefined ? readUpConsoleUrl(dirname(context.databaseFile)) : null;
|
|
7036
6854
|
const home = seams.home ?? homedir();
|
|
@@ -7123,32 +6941,26 @@ async function upCommand(flags, context) {
|
|
|
7123
6941
|
// 1. Validation, before anything at all.
|
|
7124
6942
|
const portGiven = text(flags, "port");
|
|
7125
6943
|
const port = Number(portGiven ?? 4180);
|
|
7126
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6944
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7127
6945
|
return fail(write, json, "up", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7128
|
-
}
|
|
7129
6946
|
const editorGiven = text(flags, "editor");
|
|
7130
|
-
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
6947
|
+
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
7131
6948
|
return fail(write, json, "up", "usage", "--editor supports: vscode", EXIT.usage);
|
|
7132
|
-
}
|
|
7133
6949
|
const forGiven = text(flags, "for");
|
|
7134
|
-
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
6950
|
+
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
7135
6951
|
return fail(write, json, "up", "usage", "--for takes a positive whole number of milliseconds", EXIT.usage);
|
|
7136
|
-
}
|
|
7137
6952
|
const capacityGiven = text(flags, "capacity");
|
|
7138
6953
|
const capacityFlag = capacityGiven === undefined ? null : parseRunnerCapacity(capacityGiven);
|
|
7139
|
-
if (capacityGiven !== undefined && capacityFlag === null)
|
|
6954
|
+
if (capacityGiven !== undefined && capacityFlag === null)
|
|
7140
6955
|
return fail(write, json, "up", "usage", `--capacity is a whole number of tasks, 1 to ${RUNNER_CAPACITY_MAX}`, EXIT.usage);
|
|
7141
|
-
}
|
|
7142
6956
|
const runnerFlag = text(flags, "runner");
|
|
7143
|
-
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
6957
|
+
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
7144
6958
|
return fail(write, json, "up", "usage", `a worker name is 1–${RUNNER_NAME_MAX} characters with no control characters`, EXIT.usage);
|
|
7145
|
-
}
|
|
7146
6959
|
// --host / --allow-host (setup review): `up` reaches a phone over a
|
|
7147
6960
|
// tailnet exactly as `serve` does, so one process is the whole install.
|
|
7148
6961
|
const hostFlag = text(flags, "host") ?? "127.0.0.1";
|
|
7149
|
-
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
6962
|
+
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
7150
6963
|
return fail(write, json, "up", "usage", "--host is an address or a name", EXIT.usage);
|
|
7151
|
-
}
|
|
7152
6964
|
const allowFlag = text(flags, "allow-host");
|
|
7153
6965
|
// --public-url: `up` serves a real domain exactly as `serve` does (one process is the whole team server).
|
|
7154
6966
|
const publicUrlFlag = text(flags, "public-url");
|
|
@@ -7166,9 +6978,8 @@ async function upCommand(flags, context) {
|
|
|
7166
6978
|
// person must repeat on every start.
|
|
7167
6979
|
const registryPath = registryPathOf(context);
|
|
7168
6980
|
const loadedRegistry = await loadProjectRegistry(registryPath);
|
|
7169
|
-
if ("error" in loadedRegistry)
|
|
6981
|
+
if ("error" in loadedRegistry)
|
|
7170
6982
|
return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
|
|
7171
|
-
}
|
|
7172
6983
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7173
6984
|
const rootInputs = (text(flags, "project-root") ?? "")
|
|
7174
6985
|
.split(",")
|
|
@@ -7177,9 +6988,8 @@ async function upCommand(flags, context) {
|
|
|
7177
6988
|
const explicitRoots = [];
|
|
7178
6989
|
for (const input of rootInputs) {
|
|
7179
6990
|
const canonical = canonicalProject(input);
|
|
7180
|
-
if (canonical === null)
|
|
6991
|
+
if (canonical === null)
|
|
7181
6992
|
return fail(write, json, "up", "project-root", `${input} is not a directory this machine can use`, EXIT.refused);
|
|
7182
|
-
}
|
|
7183
6993
|
if (!explicitRoots.includes(canonical))
|
|
7184
6994
|
explicitRoots.push(canonical);
|
|
7185
6995
|
}
|
|
@@ -7216,9 +7026,8 @@ async function upCommand(flags, context) {
|
|
|
7216
7026
|
if (explicitRepos.length > 0) {
|
|
7217
7027
|
for (const input of explicitRepos) {
|
|
7218
7028
|
const root = await proveRepo(input);
|
|
7219
|
-
if (root === null)
|
|
7029
|
+
if (root === null)
|
|
7220
7030
|
return fail(write, json, "up", "not-a-repository", `${input} is not inside a git repository — choose a repository, set a projects folder once with \`--project-root <dir>\`, or try the sandbox with \`toolroll demo\``, EXIT.refused);
|
|
7221
|
-
}
|
|
7222
7031
|
if (!repos.includes(root))
|
|
7223
7032
|
repos.push(root);
|
|
7224
7033
|
pinnedRepos.add(root);
|
|
@@ -7231,9 +7040,8 @@ async function upCommand(flags, context) {
|
|
|
7231
7040
|
if (cwdRepo !== null)
|
|
7232
7041
|
pinnedRepos.add(cwdRepo);
|
|
7233
7042
|
}
|
|
7234
|
-
if (repos.length === 0 && projectRoots.length === 0)
|
|
7043
|
+
if (repos.length === 0 && projectRoots.length === 0)
|
|
7235
7044
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
7236
|
-
}
|
|
7237
7045
|
// A person at a terminal reads three lines (onboarding); the folders are for --verbose, a service log, or an agent.
|
|
7238
7046
|
const quietStart = !json && !flags.has("verbose") && (context.upSeams?.terminal ?? process.stdout.isTTY === true) && !underAgent(context.upSeams?.env ?? process.env);
|
|
7239
7047
|
if (!quietStart) {
|
|
@@ -7249,9 +7057,8 @@ async function upCommand(flags, context) {
|
|
|
7249
7057
|
probe.once("error", () => resolveProbe(false));
|
|
7250
7058
|
probe.listen(port, "127.0.0.1", () => resolveProbe(true));
|
|
7251
7059
|
});
|
|
7252
|
-
if (!reserved)
|
|
7060
|
+
if (!reserved)
|
|
7253
7061
|
return fail(write, json, "up", "port-busy", `port ${port} is taken — another \`up\` or \`serve\` may already be running; stop it, or pick a different --port`, EXIT.refused);
|
|
7254
|
-
}
|
|
7255
7062
|
// Under a coding agent nothing is asked and no secret is printed, even in a real terminal: what is shown lands in
|
|
7256
7063
|
// its transcript.
|
|
7257
7064
|
const agentRunning = underAgent(context.upSeams?.env ?? process.env);
|
|
@@ -7262,9 +7069,8 @@ async function upCommand(flags, context) {
|
|
|
7262
7069
|
try {
|
|
7263
7070
|
// 4. The approver (findings 2/8/17/21/27/29/32).
|
|
7264
7071
|
const approverPlan = await resolveUpApprover(context, flags, canPrompt);
|
|
7265
|
-
if ("refusal" in approverPlan)
|
|
7072
|
+
if ("refusal" in approverPlan)
|
|
7266
7073
|
return fail(write, json, "up", "login", approverPlan.refusal, EXIT.refused);
|
|
7267
|
-
}
|
|
7268
7074
|
approver = approverPlan;
|
|
7269
7075
|
for (const note of approverPlan.notes)
|
|
7270
7076
|
progress(note);
|
|
@@ -7272,9 +7078,8 @@ async function upCommand(flags, context) {
|
|
|
7272
7078
|
// recovering runner authority on an UNVERIFIED operator identity was a
|
|
7273
7079
|
// registration road around the ceremony. No verified password, no
|
|
7274
7080
|
// runner — the login file or another approver restores the road.
|
|
7275
|
-
if (!approverPlan.verified)
|
|
7081
|
+
if (!approverPlan.verified)
|
|
7276
7082
|
return fail(write, json, "up", "login", `${approverPlan.approver}'s password could not be verified — a runner only registers under a proven operator. Type it correctly, restore ${UP_LOGIN_FILE}, or have another approver re-add you.`, EXIT.refused);
|
|
7277
|
-
}
|
|
7278
7083
|
// 5. The runner, through the atomic door (findings 1/16/26/31), with
|
|
7279
7084
|
// the suffix budget (finding 24) for generated names only.
|
|
7280
7085
|
const base = runnerFlag ?? normalizeRunnerName(hostname());
|
|
@@ -7759,9 +7564,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7759
7564
|
if (demoFence !== null)
|
|
7760
7565
|
return demoFence;
|
|
7761
7566
|
const [channel, action] = positional;
|
|
7762
|
-
if (channel !== "telegram")
|
|
7567
|
+
if (channel !== "telegram")
|
|
7763
7568
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status|digest|retry]`", EXIT.usage);
|
|
7764
|
-
}
|
|
7765
7569
|
if (action === "retry") {
|
|
7766
7570
|
// Replies already written and fenced on the pairing like every send: this only moves their next attempt to now.
|
|
7767
7571
|
const retried = store.retryTelegramReplies(null, clock());
|
|
@@ -7777,9 +7581,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7777
7581
|
removed ? "Token file removed." : "There was no token file to remove.",
|
|
7778
7582
|
]);
|
|
7779
7583
|
}
|
|
7780
|
-
if (value === undefined)
|
|
7584
|
+
if (value === undefined)
|
|
7781
7585
|
return fail(write, json, "bridge token", "usage", "`toolroll bridge telegram token <bot-token>` (from @BotFather), or --clear", EXIT.usage);
|
|
7782
|
-
}
|
|
7783
7586
|
const saved = saveBotToken(context.telegramTokenFile, value);
|
|
7784
7587
|
if (!saved.ok)
|
|
7785
7588
|
return fail(write, json, "bridge token", "bad-token", saved.message, EXIT.refused);
|
|
@@ -7806,13 +7609,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7806
7609
|
]);
|
|
7807
7610
|
}
|
|
7808
7611
|
const parsed = /^([1-9][0-9]{0,3})(m|h)$/.exec(every);
|
|
7809
|
-
if (parsed === null)
|
|
7612
|
+
if (parsed === null)
|
|
7810
7613
|
return fail(write, json, "bridge digest", "usage", "--every takes minutes or hours: 30m, 2h, 24h — or --off", EXIT.usage);
|
|
7811
|
-
}
|
|
7812
7614
|
const everyMs = Number(parsed[1]) * (parsed[2] === "h" ? 3_600_000 : 60_000);
|
|
7813
|
-
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7615
|
+
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7814
7616
|
return fail(write, json, "bridge digest", "usage", "the digest cadence is between 5 minutes and 7 days", EXIT.usage);
|
|
7815
|
-
}
|
|
7816
7617
|
store.setTelegramDigest(everyMs, "cli", clock());
|
|
7817
7618
|
return succeed(write, json, "bridge digest", { everyMs }, () => [
|
|
7818
7619
|
`Digest every ${digestWords(everyMs)}. Routine facts are held and sent together; decisions and anything that needs you now still page at once.`,
|
|
@@ -7847,18 +7648,15 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7847
7648
|
}
|
|
7848
7649
|
if (action === "pair" || action === "unpair") {
|
|
7849
7650
|
const acting = await askCredentials(flags, context);
|
|
7850
|
-
if (acting === null)
|
|
7651
|
+
if (acting === null)
|
|
7851
7652
|
return fail(write, json, `bridge ${action}`, "usage", "`--as <you> --token <your password>` — pairing hands your authority to a chat, so it takes your credential", EXIT.usage);
|
|
7852
|
-
}
|
|
7853
7653
|
const { name: asWho, token } = acting;
|
|
7854
7654
|
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7855
|
-
if (!authenticated.ok)
|
|
7655
|
+
if (!authenticated.ok)
|
|
7856
7656
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7857
|
-
}
|
|
7858
7657
|
if (action === "unpair") {
|
|
7859
|
-
if (source === null)
|
|
7658
|
+
if (source === null)
|
|
7860
7659
|
return fail(write, json, "bridge unpair", "no-token", "no bot token, so no bot to unpair", EXIT.refused);
|
|
7861
|
-
}
|
|
7862
7660
|
const revoked = store.unpairTelegram(source.botId, asWho, clock());
|
|
7863
7661
|
return succeed(write, json, "bridge unpair", { revoked }, () => [
|
|
7864
7662
|
revoked
|
|
@@ -7877,13 +7675,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7877
7675
|
"chat only, and the chat will answer as you — treat it accordingly.",
|
|
7878
7676
|
]);
|
|
7879
7677
|
}
|
|
7880
|
-
if (action !== undefined)
|
|
7678
|
+
if (action !== undefined)
|
|
7881
7679
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status]`", EXIT.usage);
|
|
7882
|
-
}
|
|
7883
7680
|
// The pass.
|
|
7884
|
-
if (source === null)
|
|
7681
|
+
if (source === null)
|
|
7885
7682
|
return fail(write, json, "bridge", "no-token", `no bot token — set ${TOKEN_ENV}, run \`toolroll bridge telegram token <t>\`, or use the serve settings card`, EXIT.refused);
|
|
7886
|
-
}
|
|
7887
7683
|
const fenced = networkFence(context.telegramEnv ?? (context.telegramTransport === undefined ? process.env : {}));
|
|
7888
7684
|
if (fenced !== null)
|
|
7889
7685
|
return fail(write, json, "bridge", "network-fenced", networkFenceLine(fenced), EXIT.refused);
|
|
@@ -7948,9 +7744,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7948
7744
|
clock,
|
|
7949
7745
|
...(flags.has("inbound-only") ? { deliver: false } : {}),
|
|
7950
7746
|
});
|
|
7951
|
-
if (!passed.ok)
|
|
7747
|
+
if (!passed.ok)
|
|
7952
7748
|
return fail(write, json, "bridge", passed.reason, passed.message, EXIT.refused);
|
|
7953
|
-
}
|
|
7954
7749
|
const { report } = passed;
|
|
7955
7750
|
const broke = report.problems.length > 0;
|
|
7956
7751
|
const idle = report.sent === 0 && report.answered === 0 && report.paired === 0 && (report.statusReplies ?? 0) === 0
|
|
@@ -7960,12 +7755,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7960
7755
|
...(report.backlog ? ["Telegram still holds more updates than one pass's budget — run it again."] : []),
|
|
7961
7756
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
7962
7757
|
];
|
|
7963
|
-
if (broke)
|
|
7758
|
+
if (broke)
|
|
7964
7759
|
return fail(write, json, "bridge", "telegram-transport", lines().join("\n"), EXIT.failed, { report });
|
|
7965
|
-
|
|
7966
|
-
if (idle) {
|
|
7760
|
+
if (idle)
|
|
7967
7761
|
return fail(write, json, "bridge", "idle", "nothing to send, nothing arrived", EXIT.refused, { report });
|
|
7968
|
-
}
|
|
7969
7762
|
return succeed(write, json, "bridge", { report }, lines);
|
|
7970
7763
|
}
|
|
7971
7764
|
// ---- publication -----------------------------------------------------------
|
|
@@ -7993,14 +7786,12 @@ async function publishCommand(positional, flags, context) {
|
|
|
7993
7786
|
const [action] = positional;
|
|
7994
7787
|
// Bare `publish` IS an action: the publication pass (push branches,
|
|
7995
7788
|
// open PRs, sweep merges) — only a NAMED unknown action refuses.
|
|
7996
|
-
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7789
|
+
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7997
7790
|
return fail(write, json, "publish", "usage", `unknown \`publish ${action}\` — try ${PUBLISH_ACTIONS.join(", ")}, or bare \`publish\` for the publication pass`, EXIT.usage);
|
|
7998
|
-
}
|
|
7999
7791
|
if (action === "setup") {
|
|
8000
7792
|
const mergeMethod = text(flags, "merge-method") ?? "squash";
|
|
8001
|
-
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
7793
|
+
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
8002
7794
|
return fail(write, json, "publish setup", "usage", "--merge-method is squash, merge or rebase", EXIT.usage);
|
|
8003
|
-
}
|
|
8004
7795
|
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
8005
7796
|
if (!checked.ok)
|
|
8006
7797
|
return fail(write, json, "publish setup", checked.reason, checked.message, EXIT.refused);
|
|
@@ -8022,26 +7813,20 @@ async function publishCommand(positional, flags, context) {
|
|
|
8022
7813
|
return EXIT.refused;
|
|
8023
7814
|
}
|
|
8024
7815
|
const acting = await askCredentials(flags, context);
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token, repo);
|
|
8029
|
-
if (!authenticated.ok) {
|
|
8030
|
-
return fail(write, json, "publish setup", authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8031
|
-
}
|
|
7816
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, repo);
|
|
7817
|
+
if (acting === null || !verified?.ok)
|
|
7818
|
+
return refuseApprover(context, "publish setup", verified, "turning on pull requests takes --as <you> --token <your password>", acting?.name);
|
|
8032
7819
|
savePublishing(store, plan, acting.name, settings, clock());
|
|
8033
7820
|
return succeed(write, json, "publish setup", { publishing: publishingOf(store, repo) }, () => ["Pull requests are on.", ...terms]);
|
|
8034
7821
|
}
|
|
8035
7822
|
if (action === "grant") {
|
|
8036
7823
|
const github = text(flags, "github");
|
|
8037
|
-
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
7824
|
+
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
8038
7825
|
return fail(write, json, "publish grant", "usage", "`--github <owner/name>` is required, exactly", EXIT.usage);
|
|
8039
|
-
}
|
|
8040
7826
|
const wantsMerge = flags.has("allow-merge");
|
|
8041
7827
|
const mergeMethod = text(flags, "merge-method");
|
|
8042
|
-
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
7828
|
+
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
8043
7829
|
return fail(write, json, "publish grant", "usage", "--allow-merge names its method: --merge-method squash|merge|rebase", EXIT.usage);
|
|
8044
|
-
}
|
|
8045
7830
|
const spec = {
|
|
8046
7831
|
repo,
|
|
8047
7832
|
githubRepo: github,
|
|
@@ -8082,14 +7867,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8082
7867
|
return EXIT.refused;
|
|
8083
7868
|
}
|
|
8084
7869
|
const acting = await askCredentials(flags, context);
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
7870
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7871
|
+
if (acting === null || !verified?.ok)
|
|
7872
|
+
return refuseApprover(context, "publish grant", verified, "granting takes --as <you> --token <your password> — pushing your repos is a person's yes", acting?.name);
|
|
8088
7873
|
const { name: asWho, token } = acting;
|
|
8089
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8090
|
-
if (!authenticated.ok) {
|
|
8091
|
-
return fail(write, json, "publish grant", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8092
|
-
}
|
|
8093
7874
|
store.savePublicationGrant({ ...spec, grantedBy: asWho }, clock());
|
|
8094
7875
|
return succeed(write, json, "publish grant", { granted: true, grant: spec }, () => [
|
|
8095
7876
|
`Granted by ${asWho}:`,
|
|
@@ -8099,14 +7880,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8099
7880
|
}
|
|
8100
7881
|
if (action === "revoke") {
|
|
8101
7882
|
const acting = await askCredentials(flags, context);
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
7883
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7884
|
+
if (acting === null || !verified?.ok)
|
|
7885
|
+
return refuseApprover(context, "publish revoke", verified, "`--as <you> --token <your password>`", acting?.name);
|
|
8105
7886
|
const { name: asWho, token } = acting;
|
|
8106
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8107
|
-
if (!authenticated.ok) {
|
|
8108
|
-
return fail(write, json, "publish revoke", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8109
|
-
}
|
|
8110
7887
|
const revoked = store.revokePublicationGrant(repo, asWho, clock());
|
|
8111
7888
|
return succeed(write, json, "publish revoke", { revoked }, () => [
|
|
8112
7889
|
revoked
|
|
@@ -8131,25 +7908,18 @@ async function publishCommand(positional, flags, context) {
|
|
|
8131
7908
|
if (action === "merge" || action === "refire") {
|
|
8132
7909
|
const prGiven = positional[1];
|
|
8133
7910
|
const pr = Number(prGiven);
|
|
8134
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7911
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8135
7912
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8136
|
-
}
|
|
8137
7913
|
const acting = await askCredentials(flags, context);
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8142
|
-
if (!authenticated.ok) {
|
|
8143
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8144
|
-
}
|
|
7914
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7915
|
+
if (acting === null || !verified?.ok)
|
|
7916
|
+
return refuseApprover(context, `publish ${action}`, verified, "authorizing a merge takes `--as <you> --token <t>` — who said yes is recorded, not asserted", acting?.name);
|
|
8145
7917
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8146
|
-
if (publication === undefined)
|
|
7918
|
+
if (publication === undefined)
|
|
8147
7919
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8148
|
-
}
|
|
8149
7920
|
const intent = store.mergeIntentFor(publication.id);
|
|
8150
|
-
if (intent === null)
|
|
7921
|
+
if (intent === null)
|
|
8151
7922
|
return fail(write, json, `publish ${action}`, "no-intent", `PR #${pr} has no merge intent yet — the sweep writes one when a merge-capable grant covers it`, EXIT.refused);
|
|
8152
|
-
}
|
|
8153
7923
|
if (action === "merge") {
|
|
8154
7924
|
// The EXACT-INTENT ceremony (E1): the yes covers this head and no
|
|
8155
7925
|
// other — a moved head refuses rather than authorizing the unseen.
|
|
@@ -8178,21 +7948,15 @@ async function publishCommand(positional, flags, context) {
|
|
|
8178
7948
|
if (action === "unblock" || action === "rearm") {
|
|
8179
7949
|
const prGiven = positional[1];
|
|
8180
7950
|
const pr = Number(prGiven);
|
|
8181
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7951
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8182
7952
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8183
|
-
}
|
|
8184
7953
|
const acting = await askCredentials(flags, context);
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8189
|
-
if (!authenticated.ok) {
|
|
8190
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8191
|
-
}
|
|
7954
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7955
|
+
if (acting === null || !verified?.ok)
|
|
7956
|
+
return refuseApprover(context, `publish ${action}`, verified, `${action === "unblock" ? "lifting a repair hold" : "re-arming a refused merge"} takes \`--as <you> --token <t>\` — who decided is recorded, not asserted`, acting?.name);
|
|
8192
7957
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8193
|
-
if (publication === undefined)
|
|
7958
|
+
if (publication === undefined)
|
|
8194
7959
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8195
|
-
}
|
|
8196
7960
|
if (action === "unblock") {
|
|
8197
7961
|
const lifted = store.liftMergeBlocker(publication.id, acting.name, context.clock());
|
|
8198
7962
|
store.resolveEpisodes(`merge-attn:${publication.id}`, context.clock());
|
|
@@ -8210,9 +7974,8 @@ async function publishCommand(positional, flags, context) {
|
|
|
8210
7974
|
: `PR #${pr} had no refused merge to re-arm.`,
|
|
8211
7975
|
]);
|
|
8212
7976
|
}
|
|
8213
|
-
if (action !== undefined)
|
|
7977
|
+
if (action !== undefined)
|
|
8214
7978
|
return fail(write, json, "publish", "usage", "`toolroll publish [grant|revoke|status|unblock|rearm]`", EXIT.usage);
|
|
8215
|
-
}
|
|
8216
7979
|
// The pass.
|
|
8217
7980
|
const report = await publishPass(store, {
|
|
8218
7981
|
repo,
|
|
@@ -8234,12 +7997,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8234
7997
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
8235
7998
|
...merges.problems.map(problem => ` merge: ${problem}`),
|
|
8236
7999
|
];
|
|
8237
|
-
if (report.problems.length > 0)
|
|
8000
|
+
if (report.problems.length > 0)
|
|
8238
8001
|
return fail(write, json, "publish", "publish-problems", lines().join("\n"), EXIT.failed, { report });
|
|
8239
|
-
|
|
8240
|
-
if (idle) {
|
|
8002
|
+
if (idle)
|
|
8241
8003
|
return fail(write, json, "publish", "idle", "nothing owed — no pending publications", EXIT.refused, { report });
|
|
8242
|
-
}
|
|
8243
8004
|
return succeed(write, json, "publish", { report }, lines);
|
|
8244
8005
|
}
|
|
8245
8006
|
// ---- the outbox -----------------------------------------------------------
|
|
@@ -8263,9 +8024,8 @@ async function peekCommand(positional, flags, context) {
|
|
|
8263
8024
|
}
|
|
8264
8025
|
const linesGiven = text(flags, "lines");
|
|
8265
8026
|
const lines = linesGiven === undefined ? 12 : Number(linesGiven);
|
|
8266
|
-
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8027
|
+
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8267
8028
|
return fail(write, json, "peek", "usage", `--lines is a whole number from 1 to ${PEEK_TAIL_LINES}`, EXIT.usage);
|
|
8268
|
-
}
|
|
8269
8029
|
const interactive = !json && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
8270
8030
|
if (flags.has("tmux")) {
|
|
8271
8031
|
if (!interactive)
|
|
@@ -8322,9 +8082,8 @@ async function outboxCommand(positional, flags, context) {
|
|
|
8322
8082
|
if (demoFence !== null)
|
|
8323
8083
|
return demoFence;
|
|
8324
8084
|
const command = text(flags, "cmd");
|
|
8325
|
-
if (command === undefined)
|
|
8085
|
+
if (command === undefined)
|
|
8326
8086
|
return fail(write, json, "outbox deliver", "usage", "--cmd says how: it runs once per notification, reading $TOOLROLL_KIND, $TOOLROLL_SUBJECT, $TOOLROLL_BODY", EXIT.usage);
|
|
8327
|
-
}
|
|
8328
8087
|
// The exact command owns a leased destination receipt, including across concurrent CLI invocations.
|
|
8329
8088
|
const owner = `outbox-${randomUUID()}`;
|
|
8330
8089
|
// Push delivery has independent receipts and must keep working if this command fails.
|
|
@@ -8397,28 +8156,23 @@ async function enrollCommand(positional, flags, context) {
|
|
|
8397
8156
|
const repo = positional[0] === undefined ? process.cwd() : resolve(positional[0]);
|
|
8398
8157
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
8399
8158
|
const mutations = readMutations(flags);
|
|
8400
|
-
if (mutations === null)
|
|
8159
|
+
if (mutations === null)
|
|
8401
8160
|
return fail(write, json, "enroll", "usage", `--allow takes ${MUTATION_CLASSES.join(", ")}`, EXIT.usage);
|
|
8402
|
-
}
|
|
8403
8161
|
const selectorFlag = text(flags, "selector") ?? "ours";
|
|
8404
|
-
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8162
|
+
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8405
8163
|
return fail(write, json, "enroll", "usage", "--selector is `ours` or `all`", EXIT.usage);
|
|
8406
|
-
}
|
|
8407
8164
|
const paths = readPaths(flags, backend);
|
|
8408
|
-
if (paths.length === 0)
|
|
8165
|
+
if (paths.length === 0)
|
|
8409
8166
|
return fail(write, json, "enroll", "usage", `--paths says what may be written for backend \`${backend}\``, EXIT.usage);
|
|
8410
|
-
}
|
|
8411
8167
|
// External dispatch (v20): its own explicit yes, never in any default.
|
|
8412
8168
|
// A dispatch grant binds EXACTLY ONE remote repository and mints this
|
|
8413
8169
|
// plane's marker identity.
|
|
8414
8170
|
const wantsDispatch = flags.has("allow-dispatch");
|
|
8415
8171
|
const dispatchRepo = text(flags, "github");
|
|
8416
|
-
if (wantsDispatch && backend !== "github-issues")
|
|
8172
|
+
if (wantsDispatch && backend !== "github-issues")
|
|
8417
8173
|
return fail(write, json, "enroll", "usage", "--allow-dispatch is a github-issues authority in this release", EXIT.usage);
|
|
8418
|
-
|
|
8419
|
-
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo))) {
|
|
8174
|
+
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo)))
|
|
8420
8175
|
return fail(write, json, "enroll", "usage", "--allow-dispatch binds exactly one tracker: name it with `--github <owner/name>`", EXIT.usage);
|
|
8421
|
-
}
|
|
8422
8176
|
const grant = await proposeGrant({
|
|
8423
8177
|
repo,
|
|
8424
8178
|
backend,
|
|
@@ -8509,17 +8263,14 @@ function grantsCommand(context) {
|
|
|
8509
8263
|
return EXIT.ok;
|
|
8510
8264
|
}
|
|
8511
8265
|
function checkProgressCommand(positional, context) {
|
|
8512
|
-
if (positional.length !== 1 || !/^[1-9][0-9]{0,14}$/.test(positional[0] ?? ""))
|
|
8266
|
+
if (positional.length !== 1 || !/^[1-9][0-9]{0,14}$/.test(positional[0] ?? ""))
|
|
8513
8267
|
return fail(context.write, context.json, "check-progress", "usage", "use check-progress <run>", EXIT.usage);
|
|
8514
|
-
}
|
|
8515
8268
|
const run = Number(positional[0]);
|
|
8516
|
-
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8269
|
+
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8517
8270
|
return fail(context.write, context.json, "check-progress", "unknown-run", "no such run", EXIT.refused);
|
|
8518
|
-
}
|
|
8519
8271
|
const progress = context.store.checkProgress(run);
|
|
8520
|
-
if (progress === null)
|
|
8272
|
+
if (progress === null)
|
|
8521
8273
|
return fail(context.write, context.json, "check-progress", "no-progress", "this run has no saved check progress", EXIT.refused);
|
|
8522
|
-
}
|
|
8523
8274
|
return succeed(context.write, context.json, "check-progress", { run, progress }, () => [progress.line]);
|
|
8524
8275
|
}
|
|
8525
8276
|
function revokeCommand(positional, flags, context) {
|
|
@@ -8535,9 +8286,8 @@ function revokeCommand(positional, flags, context) {
|
|
|
8535
8286
|
if (revoked && outgoing?.dispatch === true && outgoing.remoteRepo != null) {
|
|
8536
8287
|
void ghDispatchAdapter().deleteMarker(outgoing.remoteRepo);
|
|
8537
8288
|
}
|
|
8538
|
-
if (!revoked)
|
|
8289
|
+
if (!revoked)
|
|
8539
8290
|
return fail(write, json, "revoke", "no-grant", `${repo} was not enrolled for ${backend}`, EXIT.refused);
|
|
8540
|
-
}
|
|
8541
8291
|
return succeed(write, json, "revoke", { repo, backend }, () => [
|
|
8542
8292
|
`Revoked. ${backend} in ${repo} is read-only again.`,
|
|
8543
8293
|
]);
|
|
@@ -8675,14 +8425,12 @@ async function waitTask(positional, flags, context) {
|
|
|
8675
8425
|
if (!allowed.has(name))
|
|
8676
8426
|
return fail(context.write, context.json, command, "usage", `--${name} is not a task wait option.`, EXIT.usage);
|
|
8677
8427
|
}
|
|
8678
|
-
if (positional.length !== 1)
|
|
8428
|
+
if (positional.length !== 1)
|
|
8679
8429
|
return fail(context.write, context.json, command, "usage", "Use `toolroll task wait <id> [--timeout <seconds>]`.", EXIT.usage);
|
|
8680
|
-
}
|
|
8681
8430
|
const givenTimeout = text(flags, "timeout");
|
|
8682
8431
|
const timeoutSeconds = givenTimeout === undefined ? null : Number(givenTimeout);
|
|
8683
|
-
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8432
|
+
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8684
8433
|
return fail(context.write, context.json, command, "usage", "--timeout must be zero or a positive number of seconds.", EXIT.usage);
|
|
8685
|
-
}
|
|
8686
8434
|
const task = positional[0];
|
|
8687
8435
|
const startedAt = Date.now();
|
|
8688
8436
|
const timeoutMs = timeoutSeconds === null ? null : timeoutSeconds * 1_000;
|
|
@@ -8836,15 +8584,13 @@ async function addTask(positional, flags, context) {
|
|
|
8836
8584
|
// A scout task lives in the built-in backend (v4 review, finding 2):
|
|
8837
8585
|
// the deliverable is a column on OUR task_ref, stamped in the same
|
|
8838
8586
|
// transaction as the filing — a tracker's item has no such promise.
|
|
8839
|
-
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8587
|
+
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8840
8588
|
return fail(write, json, "task add", "usage", "--report files a scout task in the built-in backend only — drop --backend", EXIT.usage);
|
|
8841
|
-
}
|
|
8842
8589
|
if (backendName !== BUILT_IN) {
|
|
8843
8590
|
const repo = repoFrom(flags);
|
|
8844
8591
|
const backend = openBackend(backendName, store, repo);
|
|
8845
|
-
if (backend === null)
|
|
8592
|
+
if (backend === null)
|
|
8846
8593
|
return fail(write, json, "task add", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
8847
|
-
}
|
|
8848
8594
|
const created = await backend.create({ title });
|
|
8849
8595
|
if (!created.ok) {
|
|
8850
8596
|
// A denial is a refusal, not a breakage: the tool worked exactly as
|
|
@@ -8890,9 +8636,8 @@ async function addTask(positional, flags, context) {
|
|
|
8890
8636
|
const id = text(flags, "id") ?? (activeRemote() === null ? slug(title, now) : `task-${randomBytes(16).toString("hex")}`);
|
|
8891
8637
|
// Re-filing: the new task replaces an earlier one, which is cancelled and reads "Replaced by <id>".
|
|
8892
8638
|
const replaces = text(flags, "replaces");
|
|
8893
|
-
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8639
|
+
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8894
8640
|
return fail(write, json, "task add", "unknown-task", `no task \`${replaces}\` to replace`, EXIT.refused);
|
|
8895
|
-
}
|
|
8896
8641
|
noteActingPerson(flags, context);
|
|
8897
8642
|
// The existence check goes *inside* the replayed body. Outside it, a retry
|
|
8898
8643
|
// with the same key hits "already exists" and reports failure for a task the
|
|
@@ -8907,9 +8652,8 @@ async function addTask(positional, flags, context) {
|
|
|
8907
8652
|
// branch task.
|
|
8908
8653
|
return { ok: true, task: store.createTask({ id, title, ...(flags.has("report") ? { deliverable: "report" } : {}) }, now) };
|
|
8909
8654
|
}, result => result.ok);
|
|
8910
|
-
if (!outcome.ok)
|
|
8655
|
+
if (!outcome.ok)
|
|
8911
8656
|
return fail(write, json, "task add", "exists", `\`${id}\` already exists`, EXIT.refused);
|
|
8912
|
-
}
|
|
8913
8657
|
const filedId = outcome.task.id;
|
|
8914
8658
|
const remote = activeRemote();
|
|
8915
8659
|
if (remote !== null && filedId !== id) {
|
|
@@ -8926,9 +8670,8 @@ async function addTask(positional, flags, context) {
|
|
|
8926
8670
|
const placedIn = text(flags, "repo");
|
|
8927
8671
|
if (placedIn !== undefined) {
|
|
8928
8672
|
const placed = store.placeTask(store.refFor(BUILT_IN, filedId).id, canonicalProject(placedIn) ?? resolve(placedIn));
|
|
8929
|
-
if (typeof placed === "object" && !placed.ok)
|
|
8673
|
+
if (typeof placed === "object" && !placed.ok)
|
|
8930
8674
|
return fail(write, json, "task add", "scoped", "this task already has a scope — placement is immutable once somebody could have approved it", EXIT.refused);
|
|
8931
|
-
}
|
|
8932
8675
|
}
|
|
8933
8676
|
if (replaces !== undefined)
|
|
8934
8677
|
store.setTaskState(replaces, "cancelled", now, {}, undefined, outcome.task.id);
|
|
@@ -9029,17 +8772,14 @@ function requireTask(positional, flags, context) {
|
|
|
9029
8772
|
const { store, write, json, now } = context;
|
|
9030
8773
|
const id = positional[0];
|
|
9031
8774
|
const given = text(flags, "cap");
|
|
9032
|
-
if (id === undefined || given === undefined)
|
|
8775
|
+
if (id === undefined || given === undefined)
|
|
9033
8776
|
return fail(write, json, "task require", "usage", "`toolroll task require <id> --cap <kind:name>[,<kind:name>]` — or --cap none to clear", EXIT.usage);
|
|
9034
|
-
|
|
9035
|
-
if (store.getTask(id) === null) {
|
|
8777
|
+
if (store.getTask(id) === null)
|
|
9036
8778
|
return fail(write, json, "task require", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9037
|
-
}
|
|
9038
8779
|
const keys = given === "none" ? [] : given.split(",").map(one => one.trim()).filter(Boolean);
|
|
9039
8780
|
for (const key of keys) {
|
|
9040
|
-
if (parseCapabilityKey(key) === null)
|
|
8781
|
+
if (parseCapabilityKey(key) === null)
|
|
9041
8782
|
return fail(write, json, "task require", "usage", `\`${key}\` is not a capability key — say kind:name, like env:SUPABASE_KEY or cli:gh`, EXIT.usage);
|
|
9042
|
-
}
|
|
9043
8783
|
}
|
|
9044
8784
|
store.setRequirements(store.refFor(BUILT_IN, id).id, keys, mutationFrom(flags, now));
|
|
9045
8785
|
return succeed(write, json, "task require", { id, requirements: keys }, () => [
|
|
@@ -9062,21 +8802,18 @@ async function capCommand(positional, flags, context) {
|
|
|
9062
8802
|
const [action, name] = positional;
|
|
9063
8803
|
const repo = repoFrom(flags);
|
|
9064
8804
|
if (action === "add") {
|
|
9065
|
-
if (name === undefined)
|
|
8805
|
+
if (name === undefined)
|
|
9066
8806
|
return fail(write, json, "cap add", "usage", "`toolroll cap add <name> [--kind env|cli|mcp|ci|other] [--probe <cmd>] [--expires <iso>]`", EXIT.usage);
|
|
9067
|
-
}
|
|
9068
8807
|
const kind = (text(flags, "kind") ?? "env");
|
|
9069
|
-
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
8808
|
+
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
9070
8809
|
return fail(write, json, "cap add", "usage", "--kind takes env, cli, mcp, ci or other", EXIT.usage);
|
|
9071
|
-
}
|
|
9072
8810
|
// An env capability can have its probe synthesized from a fixed template
|
|
9073
8811
|
// — but only over a validated identifier, because the name lands inside
|
|
9074
8812
|
// a shell line. Anything else the operator writes explicitly.
|
|
9075
8813
|
let probe = text(flags, "probe") ?? null;
|
|
9076
8814
|
if (probe === null && kind === "env") {
|
|
9077
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
8815
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
9078
8816
|
return fail(write, json, "cap add", "usage", `\`${name}\` is not an environment variable name — give --probe explicitly`, EXIT.usage);
|
|
9079
|
-
}
|
|
9080
8817
|
probe = `test -n "$${name}"`;
|
|
9081
8818
|
}
|
|
9082
8819
|
const expires = text(flags, "expires") ?? null;
|
|
@@ -9161,16 +8898,14 @@ async function capCommand(positional, flags, context) {
|
|
|
9161
8898
|
if (action === "probe") {
|
|
9162
8899
|
const only = positional.slice(1);
|
|
9163
8900
|
for (const key of only) {
|
|
9164
|
-
if (parseCapabilityKey(key) === null)
|
|
8901
|
+
if (parseCapabilityKey(key) === null)
|
|
9165
8902
|
return fail(write, json, "cap probe", "usage", `\`${key}\` is not a capability key — say kind:name`, EXIT.usage);
|
|
9166
|
-
}
|
|
9167
8903
|
}
|
|
9168
8904
|
const outcomes = await probeRepo(store, repo, `operator@${hostname()}`, clock(), {
|
|
9169
8905
|
...(only.length === 0 ? {} : { only: new Set(only) }),
|
|
9170
8906
|
});
|
|
9171
|
-
if (outcomes.length === 0)
|
|
8907
|
+
if (outcomes.length === 0)
|
|
9172
8908
|
return fail(write, json, "cap probe", "empty", `nothing to probe for ${repo}`, EXIT.refused);
|
|
9173
|
-
}
|
|
9174
8909
|
const unverified = outcomes.filter(one => one.status !== "verified");
|
|
9175
8910
|
const lines = () => outcomes.map(one => ` ${`${one.kind}:${one.name}`.padEnd(32)} ${one.status}${one.detail === undefined ? "" : ` ${one.detail}`}`);
|
|
9176
8911
|
if (json) {
|
|
@@ -9191,9 +8926,8 @@ function listTasks(flags, context) {
|
|
|
9191
8926
|
const rawView = text(flags, 'view');
|
|
9192
8927
|
const rawLimit = text(flags, 'limit');
|
|
9193
8928
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
9194
|
-
if (wanted !== undefined && !STATES.includes(wanted))
|
|
8929
|
+
if (wanted !== undefined && !STATES.includes(wanted))
|
|
9195
8930
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
9196
|
-
}
|
|
9197
8931
|
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed'].includes(rawView) ||
|
|
9198
8932
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
9199
8933
|
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running or completed; --limit takes 1–100', EXIT.usage);
|
|
@@ -9444,19 +9178,14 @@ async function acceptTaskProof(positional, flags, context) {
|
|
|
9444
9178
|
if (task === null)
|
|
9445
9179
|
return fail(write, json, "task accept", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9446
9180
|
const acting = await askCredentials(flags, context);
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9451
|
-
if (!authenticated.ok) {
|
|
9452
|
-
return fail(write, json, "task accept", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
9453
|
-
}
|
|
9181
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9182
|
+
if (acting === null || !verified?.ok)
|
|
9183
|
+
return refuseApprover(context, "task accept", verified, "accepting incomplete proof is a person's act — it takes `--as <you> --token <t>`", id);
|
|
9454
9184
|
const ref = store.refFor(BUILT_IN, id);
|
|
9455
9185
|
// v40 fix: a reviewer run is never the attempt whose proof is accepted.
|
|
9456
9186
|
const latest = store.runsFor(ref.id).find(one => one.finishedAt !== null && (one.role === "builder" || one.role === "scout"));
|
|
9457
|
-
if (latest === undefined)
|
|
9187
|
+
if (latest === undefined)
|
|
9458
9188
|
return fail(write, json, "task accept", "no-run", `${id} has no finished attempt to accept`, EXIT.refused);
|
|
9459
|
-
}
|
|
9460
9189
|
const rawNote = text(flags, "note");
|
|
9461
9190
|
let note = null;
|
|
9462
9191
|
if (rawNote !== undefined && rawNote.trim() !== "") {
|
|
@@ -9481,9 +9210,8 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9481
9210
|
const { store, write, json, clock } = context;
|
|
9482
9211
|
const [runText] = positional;
|
|
9483
9212
|
const runId = Number(runText ?? "");
|
|
9484
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9213
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9485
9214
|
return fail(write, json, "task repair", "usage", "`toolroll task repair <run-id> [--yes] --as <you> --token <t>`", EXIT.usage);
|
|
9486
|
-
}
|
|
9487
9215
|
let chain = store.repairChainFor(runId);
|
|
9488
9216
|
if (chain === null && flags.get("yes") === true) {
|
|
9489
9217
|
const acting = await askCredentials(flags, context);
|
|
@@ -9500,12 +9228,10 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9500
9228
|
chain = store.repairChainFor(runId);
|
|
9501
9229
|
}
|
|
9502
9230
|
}
|
|
9503
|
-
if (chain === null)
|
|
9231
|
+
if (chain === null)
|
|
9504
9232
|
return fail(write, json, "task repair", "unknown-task", `run ${runId} has no drafted repair — a repair is only drafted after a review names unmet criteria`, EXIT.refused);
|
|
9505
|
-
|
|
9506
|
-
if (chain.draftTask === null) {
|
|
9233
|
+
if (chain.draftTask === null)
|
|
9507
9234
|
return fail(write, json, "task repair", "refused", `no draft was ever made for run ${runId} — the chain stopped at ${chain.outcome} before drafting one`, EXIT.refused);
|
|
9508
|
-
}
|
|
9509
9235
|
const draftId = chain.draftTask;
|
|
9510
9236
|
const yes = flags.get("yes") === true;
|
|
9511
9237
|
if (!yes) {
|
|
@@ -9516,21 +9242,15 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9516
9242
|
]);
|
|
9517
9243
|
}
|
|
9518
9244
|
const acting = await askCredentials(flags, context);
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9523
|
-
if (!authenticated.ok) {
|
|
9524
|
-
return fail(write, json, "task repair", authenticated.reason, describeApproveFailure(authenticated.reason, draftId), EXIT.refused);
|
|
9525
|
-
}
|
|
9245
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9246
|
+
if (acting === null || !verified?.ok)
|
|
9247
|
+
return refuseApprover(context, "task repair", verified, "approving a repair draft is a person's act — it takes `--as <you> --token <t>`", draftId);
|
|
9526
9248
|
const draftScope = store.getScope(draftId);
|
|
9527
|
-
if (draftScope === null)
|
|
9249
|
+
if (draftScope === null)
|
|
9528
9250
|
return fail(write, json, "task repair", "no-scope", `${draftId} has no scope to approve`, EXIT.refused);
|
|
9529
|
-
}
|
|
9530
9251
|
const approved = approve(store, draftId, acting.name, clock(), draftScope.digest, acting.token);
|
|
9531
|
-
if (!approved.ok)
|
|
9252
|
+
if (!approved.ok)
|
|
9532
9253
|
return fail(write, json, "task repair", approved.reason, approved.reason === "policy" ? `${draftId}: ${approved.message}` : describeApproveFailure(approved.reason, draftId), EXIT.refused);
|
|
9533
|
-
}
|
|
9534
9254
|
return succeed(write, json, "task repair", { run: runId, draft: draftId, approvedBy: acting.name }, () => [
|
|
9535
9255
|
`Approved: ${draftId} (attempt ${chain.attempt}) will build on the next dispatch.`,
|
|
9536
9256
|
]);
|
|
@@ -9538,19 +9258,16 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9538
9258
|
async function stateTask(positional, flags, context) {
|
|
9539
9259
|
const { store, write, json, now } = context;
|
|
9540
9260
|
const [id, state] = positional;
|
|
9541
|
-
if (id === undefined || state === undefined)
|
|
9261
|
+
if (id === undefined || state === undefined)
|
|
9542
9262
|
return fail(write, json, "task state", "usage", "`toolroll task state <id> <state>`", EXIT.usage);
|
|
9543
|
-
|
|
9544
|
-
if (!STATES.includes(state)) {
|
|
9263
|
+
if (!STATES.includes(state))
|
|
9545
9264
|
return fail(write, json, "task state", "usage", `state is one of ${STATES.join(", ")}`, EXIT.usage);
|
|
9546
|
-
}
|
|
9547
9265
|
const backendName = text(flags, "backend") ?? BUILT_IN;
|
|
9548
9266
|
if (backendName !== BUILT_IN) {
|
|
9549
9267
|
const repo = repoFrom(flags);
|
|
9550
9268
|
const backend = openBackend(backendName, store, repo);
|
|
9551
|
-
if (backend === null)
|
|
9269
|
+
if (backend === null)
|
|
9552
9270
|
return fail(write, json, "task state", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
9553
|
-
}
|
|
9554
9271
|
const moved = await backend.setState(id, state);
|
|
9555
9272
|
if (!moved.ok) {
|
|
9556
9273
|
const code = moved.reason === "denied" || moved.reason === "unsupported" ? EXIT.refused : EXIT.failed;
|
|
@@ -9601,9 +9318,8 @@ function askTask(positional, flags, context) {
|
|
|
9601
9318
|
const { store, write, json, now } = context;
|
|
9602
9319
|
const [id, ...extra] = positional;
|
|
9603
9320
|
const person = text(flags, "person"), why = text(flags, "why")?.trim();
|
|
9604
|
-
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9321
|
+
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9605
9322
|
return fail(write, json, "task ask", "usage", "`toolroll task ask <id> --person <name> --why \"what they need to do\"`", EXIT.usage);
|
|
9606
|
-
}
|
|
9607
9323
|
if (why.length > 500 || /[\u0000-\u001f\u007f]/.test(why))
|
|
9608
9324
|
return fail(write, json, "task ask", "usage", "--why is at most 500 plain characters on one line", EXIT.usage);
|
|
9609
9325
|
noteActingPerson(flags, context);
|
|
@@ -9632,9 +9348,8 @@ async function leadCommand(positional, flags, context) {
|
|
|
9632
9348
|
return fail(write, json, command, "refused", "A lead cannot mint lead tokens. The person it acts for runs this with their password.", EXIT.refused);
|
|
9633
9349
|
const acting = await askCredentials(flags, context);
|
|
9634
9350
|
const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
9635
|
-
if (acting === null ||
|
|
9351
|
+
if (acting === null || !verified?.ok)
|
|
9636
9352
|
return fail(write, json, command, "unauthenticated", "A lead token is minted behind your password: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
9637
|
-
}
|
|
9638
9353
|
const now = context.clock();
|
|
9639
9354
|
if (flags.has("revoke")) {
|
|
9640
9355
|
const ended = store.revokeLeadCredentials(acting.name, acting.name, now);
|
|
@@ -9688,13 +9403,11 @@ function blockTask(positional, flags, context) {
|
|
|
9688
9403
|
const { store, write, json } = context;
|
|
9689
9404
|
const id = positional[0];
|
|
9690
9405
|
const on = text(flags, "on");
|
|
9691
|
-
if (id === undefined || on === undefined)
|
|
9406
|
+
if (id === undefined || on === undefined)
|
|
9692
9407
|
return fail(write, json, "task block", "usage", "`toolroll task block <id> --on <id>`", EXIT.usage);
|
|
9693
|
-
}
|
|
9694
9408
|
for (const each of [id, on]) {
|
|
9695
|
-
if (store.getTask(each) === null)
|
|
9409
|
+
if (store.getTask(each) === null)
|
|
9696
9410
|
return fail(write, json, "task block", "unknown-task", `no task \`${each}\``, EXIT.refused);
|
|
9697
|
-
}
|
|
9698
9411
|
}
|
|
9699
9412
|
const result = store.addEdge(id, on, mutationFrom(flags, context.now));
|
|
9700
9413
|
if (!result.ok)
|
|
@@ -9712,20 +9425,17 @@ async function steerTask(positional, flags, context) {
|
|
|
9712
9425
|
const { store, write, json } = context;
|
|
9713
9426
|
const id = positional[0];
|
|
9714
9427
|
const note = text(flags, "note");
|
|
9715
|
-
if (id === undefined || note === undefined)
|
|
9428
|
+
if (id === undefined || note === undefined)
|
|
9716
9429
|
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; a running build's agent reads the note when it next checks for notes, otherwise the next attempt's brief quotes it, fenced, inside the approved scope", EXIT.usage);
|
|
9717
|
-
}
|
|
9718
9430
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9719
9431
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
9720
9432
|
// but wrong is not-an-approver — the same taxonomy as every ceremony.
|
|
9721
9433
|
const acting = await askCredentials(flags, context);
|
|
9722
|
-
if (acting === null)
|
|
9434
|
+
if (acting === null)
|
|
9723
9435
|
return fail(write, json, "task steer", "usage", "steering takes `--as <you> --token <t>` — anonymous notes never reach an agent's brief", EXIT.usage);
|
|
9724
|
-
}
|
|
9725
9436
|
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9726
|
-
if (!authed.ok)
|
|
9437
|
+
if (!authed.ok)
|
|
9727
9438
|
return fail(write, json, "task steer", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9728
|
-
}
|
|
9729
9439
|
// --now: Pause and steer — the running Claude build's turn stops, then this note continues it.
|
|
9730
9440
|
if (flags.get("now") === true)
|
|
9731
9441
|
return pauseRunningBuild("task steer", id, note, verifiedAuthor(acting.name), flags, context);
|
|
@@ -9828,16 +9538,13 @@ function unblockTask(positional, flags, context) {
|
|
|
9828
9538
|
const { store, write, json } = context;
|
|
9829
9539
|
const id = positional[0];
|
|
9830
9540
|
const on = text(flags, "on");
|
|
9831
|
-
if (id === undefined || on === undefined)
|
|
9541
|
+
if (id === undefined || on === undefined)
|
|
9832
9542
|
return fail(write, json, "task unblock", "usage", "`toolroll task unblock <id> --on <id>`", EXIT.usage);
|
|
9833
|
-
|
|
9834
|
-
if (store.getTask(id) === null) {
|
|
9543
|
+
if (store.getTask(id) === null)
|
|
9835
9544
|
return fail(write, json, "task unblock", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9836
|
-
}
|
|
9837
9545
|
const result = store.removeEdge(id, on, mutationFrom(flags, context.now));
|
|
9838
|
-
if (!result.ok)
|
|
9546
|
+
if (!result.ok)
|
|
9839
9547
|
return fail(write, json, "task unblock", "not-waiting", `${id} was not waiting on ${on}`, EXIT.refused);
|
|
9840
|
-
}
|
|
9841
9548
|
return succeed(write, json, "task unblock", { blocked: id, blocker: on }, () => [
|
|
9842
9549
|
`${id} no longer waits for ${on}.`,
|
|
9843
9550
|
]);
|
|
@@ -9850,9 +9557,8 @@ function unblockTask(positional, flags, context) {
|
|
|
9850
9557
|
function nextTask(positional, flags, context) {
|
|
9851
9558
|
const { store, write, json } = context;
|
|
9852
9559
|
const id = positional[0];
|
|
9853
|
-
if (id === undefined)
|
|
9560
|
+
if (id === undefined)
|
|
9854
9561
|
return fail(write, json, "task next", "usage", "`toolroll task next <id> [--undo]`", EXIT.usage);
|
|
9855
|
-
}
|
|
9856
9562
|
if (flags.has("undo")) {
|
|
9857
9563
|
const cleared = store.clearTaskPriority(id, mutationFrom(flags, context.now));
|
|
9858
9564
|
if (!cleared.ok)
|
|
@@ -9886,9 +9592,8 @@ function assignTask(positional, flags, context) {
|
|
|
9886
9592
|
const id = positional[0];
|
|
9887
9593
|
const runner = text(flags, "runner");
|
|
9888
9594
|
const anyone = flags.has("anyone");
|
|
9889
|
-
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9595
|
+
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9890
9596
|
return fail(write, json, "task assign", "usage", "`toolroll task assign <id> --runner <name> | --anyone`", EXIT.usage);
|
|
9891
|
-
}
|
|
9892
9597
|
const moved = store.moveTask({ taskId: id, toRunner: runner ?? null, beforeTaskId: null }, context.clock(), mutationFrom(flags, context.now));
|
|
9893
9598
|
if (!moved.ok) {
|
|
9894
9599
|
const message = moved.reason === "unknown-task"
|
|
@@ -9918,13 +9623,11 @@ function assignTask(positional, flags, context) {
|
|
|
9918
9623
|
async function reopenTask(positional, flags, context) {
|
|
9919
9624
|
const { store, write, json } = context;
|
|
9920
9625
|
const id = positional[0];
|
|
9921
|
-
if (id === undefined)
|
|
9626
|
+
if (id === undefined)
|
|
9922
9627
|
return fail(write, json, "task reopen", "usage", "`toolroll task reopen <id> --as <you> --token <t>`", EXIT.usage);
|
|
9923
|
-
}
|
|
9924
9628
|
const acting = await askCredentials(flags, context);
|
|
9925
|
-
if (acting === null)
|
|
9629
|
+
if (acting === null)
|
|
9926
9630
|
return fail(write, json, "task reopen", "usage", "reopening takes `--as <you> --token <t>` — who resumed external work is recorded, not asserted", EXIT.usage);
|
|
9927
|
-
}
|
|
9928
9631
|
const reopened = store.reopenMirror(id, acting.name, context.clock());
|
|
9929
9632
|
if (!reopened.ok) {
|
|
9930
9633
|
const said = {
|
|
@@ -9957,9 +9660,8 @@ async function syncCommand(flags, context) {
|
|
|
9957
9660
|
.listGrants()
|
|
9958
9661
|
.filter(grant => grant.dispatch === true && grant.remoteRepo != null)
|
|
9959
9662
|
.filter(grant => only === undefined || grant.repo === resolve(only));
|
|
9960
|
-
if (grants.length === 0)
|
|
9663
|
+
if (grants.length === 0)
|
|
9961
9664
|
return fail(write, json, "sync", "no-grant", "no tracker has a dispatch grant — `toolroll enroll <repo> --backend github-issues --github <owner/name> --allow-dispatch` states the terms", EXIT.refused);
|
|
9962
|
-
}
|
|
9963
9665
|
const adapter = ghDispatchAdapter();
|
|
9964
9666
|
const reports = [];
|
|
9965
9667
|
for (const grant of grants) {
|
|
@@ -9992,12 +9694,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
9992
9694
|
const { store, write, json, now } = context;
|
|
9993
9695
|
const id = positional[0];
|
|
9994
9696
|
const goal = text(flags, "goal");
|
|
9995
|
-
if (id === undefined || goal === undefined)
|
|
9697
|
+
if (id === undefined || goal === undefined)
|
|
9996
9698
|
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);
|
|
9997
|
-
|
|
9998
|
-
if (store.getTask(id) === null) {
|
|
9699
|
+
if (store.getTask(id) === null)
|
|
9999
9700
|
return fail(write, json, "task scope", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10000
|
-
}
|
|
10001
9701
|
// v39: this road bypasses proposeGuarded (a trusted local operator, not a
|
|
10002
9702
|
// web form re-authenticating staleness) but not the rubric requirement —
|
|
10003
9703
|
// every scope-producing road signs one, this one included.
|
|
@@ -10012,12 +9712,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10012
9712
|
// v69: a prepared commit. The machine checks it out as the attempt and
|
|
10013
9713
|
// runs the gate and the review; no agent is dispatched for it.
|
|
10014
9714
|
const candidateGiven = text(flags, "candidate");
|
|
10015
|
-
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
9715
|
+
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
10016
9716
|
return fail(write, json, "task scope", "usage", "--candidate is the full 40-character commit hash the machine will check out", EXIT.usage);
|
|
10017
|
-
|
|
10018
|
-
if (acceptanceParse.criteria.length === 0) {
|
|
9717
|
+
if (acceptanceParse.criteria.length === 0)
|
|
10019
9718
|
return fail(write, json, "task scope", "acceptance-required", "--acceptance named no valid criteria", EXIT.usage);
|
|
10020
|
-
}
|
|
10021
9719
|
const touches = (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(Boolean);
|
|
10022
9720
|
const badText = validateScopeText({ goal, outOfScope: text(flags, "not") ?? null, touches });
|
|
10023
9721
|
if (badText !== null)
|
|
@@ -10029,9 +9727,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10029
9727
|
: defaults?.buildPerRunMicrousd != null
|
|
10030
9728
|
? defaults.buildPerRunMicrousd / 1_000_000
|
|
10031
9729
|
: null;
|
|
10032
|
-
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
9730
|
+
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
10033
9731
|
return fail(write, json, "task scope", "bad-budget", "--budget-usd is a positive dollar amount", EXIT.usage);
|
|
10034
|
-
}
|
|
10035
9732
|
// v24 routing flags (foundations finding 5 — these used to be silently
|
|
10036
9733
|
// swallowed by the global parser): explicit flags resolve HERE, and an
|
|
10037
9734
|
// explicit ask that cannot resolve refuses rather than filing unresolved.
|
|
@@ -10040,9 +9737,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10040
9737
|
if (routingAsked) {
|
|
10041
9738
|
const scopeRef = store.lookupRef(id);
|
|
10042
9739
|
const resolvedRouting = resolveScopeProfile(store, scopeRef?.repo ?? null, scopeRef === null ? undefined : { agentProvider: scopeRef.agentProvider, agentModel: scopeRef.agentModel }, { provider: text(flags, "provider"), model: text(flags, "model"), repairModel: text(flags, "repair-model") });
|
|
10043
|
-
if (!resolvedRouting.ok)
|
|
9740
|
+
if (!resolvedRouting.ok)
|
|
10044
9741
|
return fail(write, json, "task scope", resolvedRouting.reason, resolvedRouting.problem, EXIT.usage);
|
|
10045
|
-
}
|
|
10046
9742
|
explicitProfile = resolvedRouting.profile;
|
|
10047
9743
|
}
|
|
10048
9744
|
// C1/M3, the credentialed-CLI road: when --as/--token authenticate the
|
|
@@ -10118,9 +9814,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10118
9814
|
}
|
|
10119
9815
|
return { ok: true, scope: proposed, sealedUnderMode, modeRefusedCoordinator };
|
|
10120
9816
|
}));
|
|
10121
|
-
if (!filed.ok)
|
|
9817
|
+
if (!filed.ok)
|
|
10122
9818
|
return fail(write, json, "task scope", filed.reason, filed.message, EXIT.refused);
|
|
10123
|
-
}
|
|
10124
9819
|
const scope = filed.scope;
|
|
10125
9820
|
if (filed.modeRefusedCoordinator === true) {
|
|
10126
9821
|
return succeed(write, json, "task scope", { scope, approvedUnderMode: false, quarantined: true }, () => [
|
|
@@ -10170,12 +9865,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10170
9865
|
async function routeTaskCommand(positional, flags, context) {
|
|
10171
9866
|
const { store, write, json, clock } = context;
|
|
10172
9867
|
const id = positional[0];
|
|
10173
|
-
if (id === undefined)
|
|
9868
|
+
if (id === undefined)
|
|
10174
9869
|
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);
|
|
10175
|
-
|
|
10176
|
-
if (store.getTask(id) === null) {
|
|
9870
|
+
if (store.getTask(id) === null)
|
|
10177
9871
|
return fail(write, json, "task route", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10178
|
-
}
|
|
10179
9872
|
const ref = store.refFor(BUILT_IN, id);
|
|
10180
9873
|
const phaseGiven = text(flags, "phase");
|
|
10181
9874
|
const clearGiven = text(flags, "clear-phase");
|
|
@@ -10221,36 +9914,29 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10221
9914
|
if (!editing)
|
|
10222
9915
|
return show();
|
|
10223
9916
|
const acting = await askCredentials(flags, context);
|
|
10224
|
-
if (acting === null)
|
|
9917
|
+
if (acting === null)
|
|
10225
9918
|
return fail(write, json, "task route", "usage", "changing a route takes `--as <you> --token <t>` — it reroutes spend", EXIT.usage);
|
|
10226
|
-
|
|
10227
|
-
if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
|
|
9919
|
+
if (sizeGiven !== undefined && !isTaskSize(sizeGiven))
|
|
10228
9920
|
return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
|
|
10229
|
-
|
|
10230
|
-
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no") {
|
|
9921
|
+
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no")
|
|
10231
9922
|
return fail(write, json, "task route", "usage", "--risky is yes or no", EXIT.usage);
|
|
10232
|
-
}
|
|
10233
9923
|
// A person's size outranks the classifier's; an unnamed half keeps what the task has.
|
|
10234
9924
|
const sizeEdit = sizeGiven === undefined && riskyGiven === undefined ? undefined : {
|
|
10235
9925
|
size: sizeGiven ?? ref.sizing?.size ?? "medium",
|
|
10236
9926
|
risky: riskyGiven === undefined ? ref.sizing?.risky ?? false : riskyGiven === "yes",
|
|
10237
9927
|
};
|
|
10238
|
-
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
9928
|
+
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
10239
9929
|
return fail(write, json, "task route", "usage", "say --phase … --provider … --model … OR --clear-phase …, not both", EXIT.usage);
|
|
10240
|
-
}
|
|
10241
9930
|
const phase = phaseGiven ?? clearGiven;
|
|
10242
|
-
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
9931
|
+
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
10243
9932
|
return fail(write, json, "task route", "usage", `--${phaseGiven !== undefined ? "phase" : "clear-phase"} is one of ${ROUTE_PHASES.join(", ")}`, EXIT.usage);
|
|
10244
|
-
}
|
|
10245
9933
|
if (phaseGiven === "review")
|
|
10246
9934
|
return fail(write, json, "task route", "model-review-retired", "Separate model review has been removed; no reviewer route is needed.", EXIT.refused);
|
|
10247
9935
|
if (phaseGiven !== undefined) {
|
|
10248
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
9936
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
10249
9937
|
return fail(write, json, "task route", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
10250
|
-
|
|
10251
|
-
if (modelGiven === undefined || modelGiven === "") {
|
|
9938
|
+
if (modelGiven === undefined || modelGiven === "")
|
|
10252
9939
|
return fail(write, json, "task route", "usage", "an override names an exact --model — approvals bind exact routing", EXIT.usage);
|
|
10253
|
-
}
|
|
10254
9940
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
10255
9941
|
if (!valid.ok)
|
|
10256
9942
|
return fail(write, json, "task route", "invalid", valid.problem, EXIT.usage);
|
|
@@ -10268,9 +9954,8 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10268
9954
|
...(digestGiven === undefined ? {} : { expectDigest: digestGiven === "none" ? null : digestGiven }),
|
|
10269
9955
|
}, clock());
|
|
10270
9956
|
if (!edited.ok) {
|
|
10271
|
-
if (edited.reason === "unauthenticated")
|
|
9957
|
+
if (edited.reason === "unauthenticated")
|
|
10272
9958
|
return fail(write, json, "task route", edited.detail, describeApproveFailure(edited.detail, id), EXIT.refused);
|
|
10273
|
-
}
|
|
10274
9959
|
const code = { "no-task": "unknown-task", "live-claim": "claimed", changed: "changed", nothing: "usage", "not-configured": "not-configured" };
|
|
10275
9960
|
return fail(write, json, "task route", code[edited.reason], `${id}: ${edited.detail}`, edited.reason === "nothing" ? EXIT.usage : EXIT.refused);
|
|
10276
9961
|
}
|
|
@@ -10286,18 +9971,15 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10286
9971
|
async function approveTask(positional, flags, context) {
|
|
10287
9972
|
const { store, write, json, now } = context;
|
|
10288
9973
|
const id = positional[0];
|
|
10289
|
-
if (id === undefined)
|
|
9974
|
+
if (id === undefined)
|
|
10290
9975
|
return fail(write, json, "task approve", "usage", "which task?", EXIT.usage);
|
|
10291
|
-
}
|
|
10292
9976
|
const scope = store.getScope(id);
|
|
10293
|
-
if (scope === null)
|
|
9977
|
+
if (scope === null)
|
|
10294
9978
|
return fail(write, json, "task approve", "no-scope", `${id} has no scope to approve — write one first`, EXIT.refused);
|
|
10295
|
-
}
|
|
10296
9979
|
// Console parity (v47): a plan leg changed after a draft asks for a real
|
|
10297
9980
|
// re-plan — nothing approves the obsolete draft while the planner is owed.
|
|
10298
|
-
if (store.lookupRef(id)?.plan === "requested")
|
|
9981
|
+
if (store.lookupRef(id)?.plan === "requested")
|
|
10299
9982
|
return fail(write, json, "task approve", "planning", `${id} is waiting on a planner — approval is blocked until the drafted plan lands`, EXIT.refused);
|
|
10300
|
-
}
|
|
10301
9983
|
let saw = text(flags, "digest");
|
|
10302
9984
|
let { name: asWho, token } = credentialsFrom(flags, context);
|
|
10303
9985
|
let confirmedAloud = false;
|
|
@@ -10346,9 +10028,8 @@ async function approveTask(positional, flags, context) {
|
|
|
10346
10028
|
// Unconfirmed is "no, not yet" — exit 3 in both modes (round-4 finding 10).
|
|
10347
10029
|
return EXIT.refused;
|
|
10348
10030
|
}
|
|
10349
|
-
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10031
|
+
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10350
10032
|
return fail(write, json, "task approve", "usage", "approving non-interactively takes --yes --digest <d> --as <you> --token <t>", EXIT.usage);
|
|
10351
|
-
}
|
|
10352
10033
|
// The digest is named rather than assumed, so an operator who read one scope
|
|
10353
10034
|
// cannot approve a different one that replaced it while they were reading.
|
|
10354
10035
|
// The credential is required for a different reason: an agent that can run
|
|
@@ -10409,9 +10090,8 @@ export const PROPOSALS_ACTIONS = ["list", "confirm", "dismiss"];
|
|
|
10409
10090
|
async function proposalsCommand(positional, flags, context) {
|
|
10410
10091
|
const { store, write, json, now } = context;
|
|
10411
10092
|
const [action = "list", idGiven] = positional;
|
|
10412
|
-
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10093
|
+
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10413
10094
|
return fail(write, json, "proposals", "usage", `unknown \`proposals ${action}\` — try ${PROPOSALS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10414
|
-
}
|
|
10415
10095
|
store.sweepCoordinatorProposals(now);
|
|
10416
10096
|
let repos;
|
|
10417
10097
|
if (context.repoList !== undefined && context.repoList.length > 0) {
|
|
@@ -10488,9 +10168,8 @@ async function chatCommand(flags, context) {
|
|
|
10488
10168
|
if (flags.has("follow") && flags.has("no-follow"))
|
|
10489
10169
|
return fail(write, json, "chat", "usage", "Choose --follow or --no-follow.", EXIT.usage);
|
|
10490
10170
|
const credentials = await askCredentials(flags, context);
|
|
10491
|
-
if (credentials === null)
|
|
10171
|
+
if (credentials === null)
|
|
10492
10172
|
return fail(write, json, "chat", "usage", "the lead takes your name and password — `--as <you>` and the hidden prompt; `--token <t>` only where a script must (it lands in shell history)", EXIT.usage);
|
|
10493
|
-
}
|
|
10494
10173
|
const ceilingGiven = text(flags, "ceiling-usd");
|
|
10495
10174
|
// The ceiling: the `--repo` list, or the ENROLLED registry beside the
|
|
10496
10175
|
// database (slice-2 review, finding 5) — never the opened-project history.
|
|
@@ -10646,9 +10325,8 @@ async function flowsCommand(positional, flags, context) {
|
|
|
10646
10325
|
async function approverCommand(positional, flags, context) {
|
|
10647
10326
|
const { store, write, json, now } = context;
|
|
10648
10327
|
const [action, name] = positional;
|
|
10649
|
-
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10328
|
+
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10650
10329
|
return fail(write, json, "approver", "usage", `unknown \`approver ${action}\` — try ${APPROVER_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10651
|
-
}
|
|
10652
10330
|
if (action === "list" || action === undefined) {
|
|
10653
10331
|
const approvers = store.listApprovers();
|
|
10654
10332
|
if (json) {
|
|
@@ -10665,9 +10343,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10665
10343
|
return EXIT.ok;
|
|
10666
10344
|
}
|
|
10667
10345
|
if (action === "add") {
|
|
10668
|
-
if (name === undefined)
|
|
10346
|
+
if (name === undefined)
|
|
10669
10347
|
return fail(write, json, "approver add", "usage", "an approver needs a name", EXIT.usage);
|
|
10670
|
-
}
|
|
10671
10348
|
let password = text(flags, "password");
|
|
10672
10349
|
const bootstrap = store.listApprovers().length === 0;
|
|
10673
10350
|
let by;
|
|
@@ -10680,9 +10357,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10680
10357
|
const chosen = await askHidden(`password for ${name} (enter to auto-generate): `);
|
|
10681
10358
|
if (chosen !== "") {
|
|
10682
10359
|
const again = await askHidden("again: ");
|
|
10683
|
-
if (again !== chosen)
|
|
10360
|
+
if (again !== chosen)
|
|
10684
10361
|
return fail(write, json, "approver add", "mismatch", "the two passwords did not match", EXIT.refused);
|
|
10685
|
-
}
|
|
10686
10362
|
password = chosen;
|
|
10687
10363
|
}
|
|
10688
10364
|
}
|
|
@@ -10719,17 +10395,14 @@ function holdTask(positional, flags, context) {
|
|
|
10719
10395
|
const { store, write, json, now } = context;
|
|
10720
10396
|
const id = positional[0];
|
|
10721
10397
|
const reason = text(flags, "reason");
|
|
10722
|
-
if (id === undefined || reason === undefined)
|
|
10398
|
+
if (id === undefined || reason === undefined)
|
|
10723
10399
|
return fail(write, json, "task hold", "usage", "`toolroll task hold <id> --reason <why>`", EXIT.usage);
|
|
10724
|
-
|
|
10725
|
-
if (store.getTask(id) === null) {
|
|
10400
|
+
if (store.getTask(id) === null)
|
|
10726
10401
|
return fail(write, json, "task hold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10727
|
-
}
|
|
10728
10402
|
const untilText = text(flags, "until");
|
|
10729
10403
|
const until = untilText === undefined ? null : new Date(untilText);
|
|
10730
|
-
if (until !== null && Number.isNaN(until.getTime()))
|
|
10404
|
+
if (until !== null && Number.isNaN(until.getTime()))
|
|
10731
10405
|
return fail(write, json, "task hold", "usage", "--until takes a date, e.g. 2026-08-12T09:00:00Z", EXIT.usage);
|
|
10732
|
-
}
|
|
10733
10406
|
store.hold(store.refFor(BUILT_IN, id).id, reason, until, now, mutationFrom(flags, now));
|
|
10734
10407
|
return succeed(write, json, "task hold", { id, reason, until: until?.toISOString() ?? null }, () => [
|
|
10735
10408
|
`${id} is on hold${until === null ? "" : ` until ${until.toISOString()}`}: ${reason}`,
|
|
@@ -10740,9 +10413,8 @@ function unholdTask(positional, flags, context) {
|
|
|
10740
10413
|
const id = positional[0];
|
|
10741
10414
|
if (id === undefined)
|
|
10742
10415
|
return fail(write, json, "task unhold", "usage", "which task?", EXIT.usage);
|
|
10743
|
-
if (store.getTask(id) === null)
|
|
10416
|
+
if (store.getTask(id) === null)
|
|
10744
10417
|
return fail(write, json, "task unhold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10745
|
-
}
|
|
10746
10418
|
const lifted = store.unhold(store.refFor(BUILT_IN, id).id, mutationFrom(flags, context.now));
|
|
10747
10419
|
if (!lifted)
|
|
10748
10420
|
return fail(write, json, "task unhold", "not-held", `${id} was not on hold`, EXIT.refused);
|
|
@@ -10762,19 +10434,14 @@ async function stopTaskCommand(positional, flags, context) {
|
|
|
10762
10434
|
const id = positional[0];
|
|
10763
10435
|
const runText = text(flags, "run");
|
|
10764
10436
|
const runId = Number(runText ?? "");
|
|
10765
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10437
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10766
10438
|
return fail(write, json, "task stop", "usage", "`toolroll task stop <id> --run <run-id> --as <you> --token <t>` — the stop names one exact attempt", EXIT.usage);
|
|
10767
|
-
}
|
|
10768
10439
|
if (store.getTask(id) === null)
|
|
10769
10440
|
return fail(write, json, "task stop", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10770
10441
|
const acting = await askCredentials(flags, context);
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10775
|
-
if (!authenticated.ok) {
|
|
10776
|
-
return fail(write, json, "task stop", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10777
|
-
}
|
|
10442
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
10443
|
+
if (acting === null || !verified?.ok)
|
|
10444
|
+
return refuseApprover(context, "task stop", verified, "stopping takes `--as <you> --token <t>` — it is an operator's act and is recorded against your name", id);
|
|
10778
10445
|
const asked = requestTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli" }, clock());
|
|
10779
10446
|
if (!asked.ok)
|
|
10780
10447
|
return fail(write, json, "task stop", asked.reason, asked.detail, EXIT.refused, { run: runId });
|
|
@@ -10802,26 +10469,20 @@ async function resumeTaskCommand(positional, flags, context) {
|
|
|
10802
10469
|
const id = positional[0];
|
|
10803
10470
|
const runText = text(flags, "run");
|
|
10804
10471
|
const runId = Number(runText ?? "");
|
|
10805
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10472
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10806
10473
|
return fail(write, json, "task resume", "usage", "`toolroll task resume <id> --run <run-id> --as <you> --token <t>` — the resume names the exact stopped attempt", EXIT.usage);
|
|
10807
|
-
}
|
|
10808
10474
|
if (store.getTask(id) === null)
|
|
10809
10475
|
return fail(write, json, "task resume", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10810
10476
|
const acting = await askCredentials(flags, context);
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10815
|
-
if (!authenticated.ok) {
|
|
10816
|
-
return fail(write, json, "task resume", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10817
|
-
}
|
|
10477
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
10478
|
+
if (acting === null || !verified?.ok)
|
|
10479
|
+
return refuseApprover(context, "task resume", verified, "resuming takes `--as <you> --token <t>` — it lets the next pass spend again", id);
|
|
10818
10480
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
10819
10481
|
const worktrees = new WorktreePool(store, { root: pool });
|
|
10820
10482
|
const resumed = resumeTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli", occupied: path => worktrees.inUse(path) }, clock());
|
|
10821
10483
|
if (!resumed.ok) {
|
|
10822
|
-
if (resumed.reason === "review")
|
|
10484
|
+
if (resumed.reason === "review")
|
|
10823
10485
|
return fail(write, json, "task resume", "review", `${resumed.detail} — separate model review has been removed; inspect the saved source result`, EXIT.refused, { run: runId });
|
|
10824
|
-
}
|
|
10825
10486
|
return fail(write, json, "task resume", resumed.reason, resumed.detail, EXIT.refused, { run: runId });
|
|
10826
10487
|
}
|
|
10827
10488
|
const control = taskControlOf(store, store.refFor(BUILT_IN, id).id, clock());
|
|
@@ -10850,21 +10511,18 @@ async function runCommand(positional, flags, context) {
|
|
|
10850
10511
|
const why = text(flags, "why")?.trim();
|
|
10851
10512
|
if (id === undefined || extra.length > 0 || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(runId))
|
|
10852
10513
|
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
10853
|
-
if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why))
|
|
10514
|
+
if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why))
|
|
10854
10515
|
return fail(write, json, command, "usage", "say why in one line with --why (at most 300 characters) — it is kept in the ledger", EXIT.usage);
|
|
10855
|
-
}
|
|
10856
10516
|
const run = store.getRun(runId);
|
|
10857
10517
|
if (run === null)
|
|
10858
10518
|
return fail(write, json, command, "unknown-run", `no run #${runId}`, EXIT.refused);
|
|
10859
10519
|
const acting = await askCredentials(flags, context);
|
|
10860
|
-
if (acting === null)
|
|
10520
|
+
if (acting === null)
|
|
10861
10521
|
return fail(write, json, command, "usage", "settling takes `--as <you> --token <t>` — it is an approver's act and is recorded against your name", EXIT.usage);
|
|
10862
|
-
}
|
|
10863
10522
|
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10864
10523
|
const repo = store.refById(run.taskRef)?.repo ?? null;
|
|
10865
|
-
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10524
|
+
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10866
10525
|
return fail(write, json, command, "not-an-approver", "that is not an approver for this run's project, or the token does not match", EXIT.refused);
|
|
10867
|
-
}
|
|
10868
10526
|
const settled = store.settleRunWitnessesByApprover({ runId, by: acting.name, why }, clock());
|
|
10869
10527
|
if (!settled.ok)
|
|
10870
10528
|
return fail(write, json, command, settled.reason, settled.detail, EXIT.refused, { run: runId });
|
|
@@ -10891,9 +10549,7 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10891
10549
|
if (positional.length > 1 || repoFlag === undefined)
|
|
10892
10550
|
return fail(context.write, context.json, command, "usage", "Use project rules --repo <project path> [--not-requester on|off] [--protect none|project] [--protect-paths \"infra/**,migrations/**\"].", EXIT.usage);
|
|
10893
10551
|
// The projects Settings shows: those with work, and those registered (enrolled) with none yet.
|
|
10894
|
-
const
|
|
10895
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
10896
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag));
|
|
10552
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: false });
|
|
10897
10553
|
if (repo === undefined)
|
|
10898
10554
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10899
10555
|
const current = context.store.approvalRules(repo);
|
|
@@ -10913,9 +10569,8 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10913
10569
|
return fail(context.write, context.json, command, "usage", paths.problem, EXIT.usage);
|
|
10914
10570
|
const acting = await askCredentials(flags, context);
|
|
10915
10571
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
10916
|
-
if (acting === null ||
|
|
10572
|
+
if (acting === null || !verified?.ok || !context.store.isInstanceOperator(acting.name))
|
|
10917
10573
|
return fail(context.write, context.json, command, "refused", "An instance operator changes approval rules: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
10918
|
-
}
|
|
10919
10574
|
const next = {
|
|
10920
10575
|
notRequester: onOff === undefined ? current.notRequester : onOff === "on",
|
|
10921
10576
|
protectProject: protect === undefined ? (paths !== null && paths.ok ? false : current.protectProject) : protect === "project",
|
|
@@ -10938,12 +10593,9 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10938
10593
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
|
|
10939
10594
|
const repoFlag = text(flags, "repo");
|
|
10940
10595
|
const local = flags.get("local");
|
|
10941
|
-
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none"))
|
|
10596
|
+
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none"))
|
|
10942
10597
|
return fail(context.write, context.json, command, "usage", "Use project network --repo <project path> [--allow host,…] [--remove host,…] [--open | --allowlist] [--local any|none].", EXIT.usage);
|
|
10943
|
-
}
|
|
10944
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10945
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10946
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10598
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
10947
10599
|
if (repo === undefined)
|
|
10948
10600
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10949
10601
|
const current = context.store.projectNetwork(repo);
|
|
@@ -10973,9 +10625,8 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10973
10625
|
return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
|
|
10974
10626
|
const acting = await askCredentials(flags, context);
|
|
10975
10627
|
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10976
|
-
if (acting === null ||
|
|
10628
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
10977
10629
|
return fail(context.write, context.json, command, "refused", "An approver for this project changes what its builds may reach, with their password: pass --as and --token.", EXIT.refused);
|
|
10978
|
-
}
|
|
10979
10630
|
const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
|
|
10980
10631
|
return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
|
|
10981
10632
|
...words(changed.after),
|
|
@@ -10996,12 +10647,9 @@ async function projectWritesCommand(positional, flags, context) {
|
|
|
10996
10647
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project writes option.`, EXIT.usage);
|
|
10997
10648
|
const repoFlag = text(flags, "repo");
|
|
10998
10649
|
const wanted = positional[1];
|
|
10999
|
-
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted)))
|
|
10650
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted)))
|
|
11000
10651
|
return fail(context.write, context.json, command, "usage", "Use project writes --repo <project path> [workspace | anywhere].", EXIT.usage);
|
|
11001
|
-
}
|
|
11002
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11003
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11004
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10652
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11005
10653
|
if (repo === undefined)
|
|
11006
10654
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11007
10655
|
const words = (one) => [`${repo}: builds write ${writesWords(one)}.`];
|
|
@@ -11010,9 +10658,8 @@ async function projectWritesCommand(positional, flags, context) {
|
|
|
11010
10658
|
return succeed(context.write, context.json, command, { repo, writes: current }, () => words(current));
|
|
11011
10659
|
const acting = await askCredentials(flags, context);
|
|
11012
10660
|
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
11013
|
-
if (acting === null ||
|
|
10661
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11014
10662
|
return fail(context.write, context.json, command, "refused", "An approver for this project changes where its builds may write, with their password: pass --as and --token.", EXIT.refused);
|
|
11015
|
-
}
|
|
11016
10663
|
const changed = context.store.setProjectWrites(repo, wanted, acting.name, context.clock());
|
|
11017
10664
|
return succeed(context.write, context.json, command, { repo, before: changed.before, writes: changed.after }, () => [
|
|
11018
10665
|
...words(changed.after),
|
|
@@ -11025,9 +10672,8 @@ async function unconfinedAgentsCommand(positional, flags, context) {
|
|
|
11025
10672
|
const command = "config unconfined-agents";
|
|
11026
10673
|
const { store, write, json } = context;
|
|
11027
10674
|
const wanted = positional[1];
|
|
11028
|
-
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
10675
|
+
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
11029
10676
|
return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
|
|
11030
|
-
}
|
|
11031
10677
|
const words = (allowed) => [allowed
|
|
11032
10678
|
? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
|
|
11033
10679
|
: "Builds this machine can't confine at the OS don't start."];
|
|
@@ -11035,9 +10681,8 @@ async function unconfinedAgentsCommand(positional, flags, context) {
|
|
|
11035
10681
|
return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
|
|
11036
10682
|
const acting = await askCredentials(flags, context);
|
|
11037
10683
|
const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
|
|
11038
|
-
if (acting === null ||
|
|
10684
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11039
10685
|
return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
|
|
11040
|
-
}
|
|
11041
10686
|
store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
|
|
11042
10687
|
return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
|
|
11043
10688
|
}
|
|
@@ -11051,9 +10696,7 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11051
10696
|
const repoFlag = text(flags, "repo");
|
|
11052
10697
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11053
10698
|
return fail(context.write, context.json, command, "usage", "Use project concurrency [<n>] --repo <project path>.", EXIT.usage);
|
|
11054
|
-
const
|
|
11055
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11056
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10699
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11057
10700
|
if (repo === undefined)
|
|
11058
10701
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11059
10702
|
const workers = context.store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(repo));
|
|
@@ -11069,9 +10712,8 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11069
10712
|
return fail(context.write, context.json, command, "usage", `Builds at once is a whole number from 1 to ${RUNNER_CAPACITY_MAX}.`, EXIT.usage);
|
|
11070
10713
|
const acting = await askCredentials(flags, context);
|
|
11071
10714
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11072
|
-
if (acting === null ||
|
|
10715
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11073
10716
|
return fail(context.write, context.json, command, "refused", "An approver for this project changes how many tasks build at once: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11074
|
-
}
|
|
11075
10717
|
const changed = saveProjectConcurrency(context.databaseFile, repo, n);
|
|
11076
10718
|
context.store.recordProjectConcurrency(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11077
10719
|
context.store.bumpWake();
|
|
@@ -11090,12 +10732,9 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11090
10732
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project checks option.`, EXIT.usage);
|
|
11091
10733
|
const repoFlag = text(flags, "repo");
|
|
11092
10734
|
const wanted = flags.get("batch");
|
|
11093
|
-
if (positional.length > 1 || repoFlag === undefined || (wanted !== undefined && wanted !== "on" && wanted !== "off"))
|
|
10735
|
+
if (positional.length > 1 || repoFlag === undefined || (wanted !== undefined && wanted !== "on" && wanted !== "off"))
|
|
11094
10736
|
return fail(context.write, context.json, command, "usage", "Use project checks [--batch on|off] --repo <project path>.", EXIT.usage);
|
|
11095
|
-
}
|
|
11096
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11097
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11098
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10737
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11099
10738
|
if (repo === undefined)
|
|
11100
10739
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11101
10740
|
const level = projectCheckLevel(context.store, repo).level;
|
|
@@ -11109,9 +10748,8 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11109
10748
|
}
|
|
11110
10749
|
const acting = await askCredentials(flags, context);
|
|
11111
10750
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token, repo);
|
|
11112
|
-
if (acting === null ||
|
|
10751
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11113
10752
|
return fail(context.write, context.json, command, "refused", "An approver for this project changes batch checks: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11114
|
-
}
|
|
11115
10753
|
const on = wanted === "on";
|
|
11116
10754
|
const changed = setProjectBatchChecks(context.store, repo, on, acting.name, context.clock());
|
|
11117
10755
|
return succeed(context.write, context.json, command, { repo, batch: on, before: changed.before, changed: changed.changed, windowMinutes: BATCH_WINDOW_MS / 60_000, level }, () => [
|
|
@@ -11130,9 +10768,7 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11130
10768
|
const repoFlag = text(flags, "repo");
|
|
11131
10769
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11132
10770
|
return fail(context.write, context.json, command, "usage", "Use project demo [<url>|off] --repo <project path>.", EXIT.usage);
|
|
11133
|
-
const
|
|
11134
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11135
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10771
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11136
10772
|
if (repo === undefined)
|
|
11137
10773
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11138
10774
|
const words = (url) => url === null ? `${repo}: no demo; scouts screenshot public pages only.` : `${repo}: scouts may also screenshot ${url}.`;
|
|
@@ -11146,9 +10782,8 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11146
10782
|
return fail(context.write, context.json, command, "usage", "The demo is an http or https address without a sign-in, or off.", EXIT.usage);
|
|
11147
10783
|
const acting = await askCredentials(flags, context);
|
|
11148
10784
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11149
|
-
if (acting === null ||
|
|
10785
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11150
10786
|
return fail(context.write, context.json, command, "refused", "An approver for this project sets its demo: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11151
|
-
}
|
|
11152
10787
|
const changed = saveProjectDemo(context.databaseFile, repo, url);
|
|
11153
10788
|
context.store.recordProjectDemo(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11154
10789
|
return succeed(context.write, context.json, command, { repo, before: changed.before, demoUrl: changed.after }, () => [words(changed.after)]);
|
|
@@ -11188,9 +10823,8 @@ async function projectDeleteCommand(positional, flags, context) {
|
|
|
11188
10823
|
const store = context.store;
|
|
11189
10824
|
const acting = await askCredentials(flags, context);
|
|
11190
10825
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11191
|
-
if (acting === null ||
|
|
10826
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11192
10827
|
return fail(context.write, context.json, command, "refused", "An instance operator deletes projects: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11193
|
-
}
|
|
11194
10828
|
const holdings = projectHoldings(store, repo);
|
|
11195
10829
|
const running = projectRunning(store, repo, context.clock());
|
|
11196
10830
|
if (running.length > 0)
|
|
@@ -11293,9 +10927,8 @@ async function restoreCommand(file, positional, flags, write, json, now) {
|
|
|
11293
10927
|
catch (error) {
|
|
11294
10928
|
return fail(write, json, command, "failed", `The restore failed: ${describe(error)}. The database is unchanged.`, EXIT.failed);
|
|
11295
10929
|
}
|
|
11296
|
-
if (!report.ok)
|
|
10930
|
+
if (!report.ok)
|
|
11297
10931
|
return fail(write, json, command, report.refusals[0]?.reason ?? "refused", ["Nothing was restored.", ...report.refusals.map(one => ` ${one.words}`)].join("\n"), EXIT.refused, { report });
|
|
11298
|
-
}
|
|
11299
10932
|
const checked = `schema ${report.schemaVersion}, ledger chain verified (${report.ledger?.entries ?? 0} entries)`;
|
|
11300
10933
|
return succeed(write, json, command, { report }, () => dryRun
|
|
11301
10934
|
? [`Dry run: ${report.file} can be restored (${checked}). Nothing was changed.`]
|
|
@@ -11356,9 +10989,8 @@ async function budgetCommand(positional, flags, context) {
|
|
|
11356
10989
|
const target = chosen[0];
|
|
11357
10990
|
const acting = await askCredentials(flags, context);
|
|
11358
10991
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11359
|
-
if (acting === null ||
|
|
10992
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11360
10993
|
return fail(context.write, context.json, command, "refused", "An instance operator sets budgets: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11361
|
-
}
|
|
11362
10994
|
const known = target.scope === "installation" || (target.scope === "project" ? store.knownRepos().includes(target.key)
|
|
11363
10995
|
: target.scope === "person" ? store.accountFacts().some(one => one.name === target.key && one.revokedAt === null)
|
|
11364
10996
|
: store.handle.prepare("SELECT 1 FROM subagent WHERE id = ?").get(Number(target.key)) !== undefined);
|
|
@@ -11389,18 +11021,16 @@ async function notificationsCommand(positional, flags, context) {
|
|
|
11389
11021
|
return fail(context.write, context.json, command, "usage", `--${name} is not a notifications option.`, EXIT.usage);
|
|
11390
11022
|
const usage = "Use notifications quiet, notifications all, notifications digest <HH:MM>|off, notifications screenshots off|first|all, or notifications mute|unmute --repo <project>.";
|
|
11391
11023
|
const valued = action === "digest" || action === "screenshots";
|
|
11392
|
-
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest", "screenshots", "mute", "unmute"].includes(action)) || valued !== (value !== undefined) || (muting && (value !== undefined || text(flags, "repo") === undefined)))
|
|
11024
|
+
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest", "screenshots", "mute", "unmute"].includes(action)) || valued !== (value !== undefined) || (muting && (value !== undefined || text(flags, "repo") === undefined)))
|
|
11393
11025
|
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11394
|
-
}
|
|
11395
11026
|
if (action === "screenshots" && !RESULT_SCREENSHOTS.includes(value))
|
|
11396
11027
|
return fail(context.write, context.json, command, "usage", "Screenshots with results is off, first (the first one) or all (up to 4).", EXIT.usage);
|
|
11397
11028
|
if (action === "digest" && value !== "off" && !isDigestTime(value))
|
|
11398
11029
|
return fail(context.write, context.json, command, "usage", "The digest time is HH:MM on a 24-hour clock, like 18:30, or off.", EXIT.usage);
|
|
11399
11030
|
const acting = await askCredentials(flags, context);
|
|
11400
11031
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11401
|
-
if (acting === null ||
|
|
11032
|
+
if (acting === null || !verified?.ok)
|
|
11402
11033
|
return fail(context.write, context.json, command, "refused", "Notifications are set per person: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11403
|
-
}
|
|
11404
11034
|
const now = context.clock();
|
|
11405
11035
|
if (muting) {
|
|
11406
11036
|
const typed = text(flags, "repo");
|
|
@@ -11458,17 +11088,15 @@ async function retentionCommand(positional, flags, context) {
|
|
|
11458
11088
|
if (action !== "set")
|
|
11459
11089
|
return fail(context.write, context.json, command, "usage", "Use retention show, retention preview or retention set <kind> <period>.", EXIT.usage);
|
|
11460
11090
|
const [kind, period, ...extra] = rest;
|
|
11461
|
-
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11091
|
+
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11462
11092
|
return fail(context.write, context.json, command, "usage", `Use retention set <${RETENTION_KINDS.map(one => one.kind).join("|")}> <days, like 1d, 90d or 1y, or forever>.`, EXIT.usage);
|
|
11463
|
-
}
|
|
11464
11093
|
const days = parsePeriod(period);
|
|
11465
11094
|
if (days === undefined)
|
|
11466
11095
|
return fail(context.write, context.json, command, "usage", `A period is forever, or ${MIN_DAYS} day to 10 years (1d, 90d, 12w, 1y).`, EXIT.usage);
|
|
11467
11096
|
const acting = await askCredentials(flags, context);
|
|
11468
11097
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11469
|
-
if (acting === null ||
|
|
11098
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11470
11099
|
return fail(context.write, context.json, command, "refused", "An instance operator sets retention: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11471
|
-
}
|
|
11472
11100
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
11473
11101
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
11474
11102
|
}
|
|
@@ -11757,9 +11385,8 @@ async function policyCommand(positional, flags, context) {
|
|
|
11757
11385
|
return fail(context.write, context.json, command, "usage", "Say what to set: --providers, --models, --tools or --ceiling.", EXIT.usage);
|
|
11758
11386
|
const acting = await askCredentials(flags, context);
|
|
11759
11387
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11760
|
-
if (acting === null ||
|
|
11388
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11761
11389
|
return fail(context.write, context.json, command, "refused", "An instance operator sets the policy: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11762
|
-
}
|
|
11763
11390
|
const current = store.orgPolicy();
|
|
11764
11391
|
const listFlag = (name, now) => {
|
|
11765
11392
|
if (!flags.has(name))
|
|
@@ -11797,9 +11424,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11797
11424
|
for (const name of flags.keys())
|
|
11798
11425
|
if (!allowed.has(name))
|
|
11799
11426
|
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
11800
|
-
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11427
|
+
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11801
11428
|
return fail(context.write, context.json, command, "usage", "Use ledger verify, ledger checkpoint or ledger export --from <day> --to <day>.", EXIT.usage);
|
|
11802
|
-
}
|
|
11803
11429
|
const store = context.store;
|
|
11804
11430
|
if (action === "verify") {
|
|
11805
11431
|
// The command walks the whole chain, every time.
|
|
@@ -11819,9 +11445,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11819
11445
|
if (action === "checkpoint") {
|
|
11820
11446
|
const acting = await askCredentials(flags, context);
|
|
11821
11447
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11822
|
-
if (acting === null ||
|
|
11448
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11823
11449
|
return fail(context.write, context.json, command, "refused", "An instance operator makes checkpoints: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11824
|
-
}
|
|
11825
11450
|
const made = store.ledgerCheckpoint(acting.name, context.clock());
|
|
11826
11451
|
if (made === null)
|
|
11827
11452
|
return fail(context.write, context.json, command, "empty", "The ledger has no entries yet.", EXIT.refused);
|
|
@@ -11925,6 +11550,23 @@ function databaseFailure(write, json, command, file, error) {
|
|
|
11925
11550
|
return fail(write, json, command, "database", describe(error), EXIT.failed);
|
|
11926
11551
|
return fail(write, json, command, "database-busy", `The database ${file} stayed busy for ${CONCURRENT_WRITER_WAIT_MS / 1_000} seconds. Wait for the other Toolroll process to finish, then try again.`, EXIT.failed);
|
|
11927
11552
|
}
|
|
11553
|
+
/**
|
|
11554
|
+
* The refusal every password-checked CLI act shares: no name or password given is a usage error with the act's own
|
|
11555
|
+
* hint; a check that failed says why, about `subject`. The check itself stays at each call site.
|
|
11556
|
+
*/
|
|
11557
|
+
function refuseApprover(context, command, verified, usage, subject) {
|
|
11558
|
+
if (verified === null)
|
|
11559
|
+
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11560
|
+
const reason = verified.ok ? "" : verified.reason;
|
|
11561
|
+
return fail(context.write, context.json, command, reason, describeApproveFailure(reason, subject ?? ""), EXIT.refused);
|
|
11562
|
+
}
|
|
11563
|
+
/** The project `--repo` names among those Toolroll knows: the database's, the listed projects' (when `listed`) and the
|
|
11564
|
+
* registry's, matched as given, resolved, or (when `canonical`) canonical. */
|
|
11565
|
+
async function knownProject(context, repoFlag, match) {
|
|
11566
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11567
|
+
const known = [...new Set([...context.store.knownRepos(), ...(match.listed ? context.store.listProjects().map(one => one.path) : []), ...("error" in registered ? [] : registered.repos)])];
|
|
11568
|
+
return known.find(one => one === repoFlag || one === resolve(repoFlag) || (match.canonical && one === canonicalProject(repoFlag)));
|
|
11569
|
+
}
|
|
11928
11570
|
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11929
11571
|
/** The value flags a verb reads under a global switch's name, for parseOperateArgs. */
|
|
11930
11572
|
export function ownValueFlags(command, argv) {
|
|
@@ -11934,3 +11576,67 @@ export function ownValueFlags(command, argv) {
|
|
|
11934
11576
|
return PROJECT_NETWORK_VALUE_FLAGS;
|
|
11935
11577
|
return undefined;
|
|
11936
11578
|
}
|
|
11579
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11580
|
+
/**
|
|
11581
|
+
* `project check-access --repo <p> [sandboxed|sign-ins|unsandboxed]` (check-access.ts): how far the project's setup
|
|
11582
|
+
* and check commands reach beyond the check fence. Widening is an instance operator's act with a password typed now
|
|
11583
|
+
* (never a remembered sign-in or a single sign-on recheck); any approver for the project may narrow it back to
|
|
11584
|
+
* sandboxed. New scopes seal it; approved ones keep theirs. The ledger keeps before → after.
|
|
11585
|
+
*/
|
|
11586
|
+
async function projectCheckAccessCommand(positional, flags, context) {
|
|
11587
|
+
const command = "project check-access";
|
|
11588
|
+
const allowed = new Set(["repo", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
11589
|
+
for (const name of flags.keys())
|
|
11590
|
+
if (!allowed.has(name))
|
|
11591
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project check-access option.`, EXIT.usage);
|
|
11592
|
+
const repoFlag = text(flags, "repo");
|
|
11593
|
+
const wanted = positional[1];
|
|
11594
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isCheckAccess(wanted)))
|
|
11595
|
+
return fail(context.write, context.json, command, "usage", "Use project check-access --repo <project path> [sandboxed | sign-ins | unsandboxed].", EXIT.usage);
|
|
11596
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11597
|
+
if (repo === undefined)
|
|
11598
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11599
|
+
const current = context.store.projectCheckAccess(repo);
|
|
11600
|
+
const platform = context.platform ?? process.platform;
|
|
11601
|
+
// How it actually runs here: with no OS fence and unconfined running allowed, without a sandbox whatever is saved; on
|
|
11602
|
+
// a saved sign-ins level (off for now) runs sandboxed. Said, never hidden.
|
|
11603
|
+
const fenced = context.fenceAvailable?.() ?? (macosFenceAvailable() || linuxFenceAvailable());
|
|
11604
|
+
const runsAs = (one) => checkAccessRunsAs(one, { platform, fenced, unconfinedAllowed: context.store.unfencedAgentsAllowed() });
|
|
11605
|
+
const here = runsAs(current);
|
|
11606
|
+
const words = (one) => {
|
|
11607
|
+
if (runsAs(one) === "unsandboxed" && one !== "unsandboxed")
|
|
11608
|
+
return [`${repo}: saved as ${checkAccessWords(one)}, but this machine can't sandbox checks, so they run without a sandbox.`];
|
|
11609
|
+
if (runsAs(one) !== one)
|
|
11610
|
+
return [`${repo}: saved with model and GitHub sign-ins, but its checks run sandboxed: sign-ins can't be shared with checks safely yet.`];
|
|
11611
|
+
const missing = one === "sign-ins" ? signInHostsMissing(context.store.projectNetwork(repo)) : [];
|
|
11612
|
+
return [`${repo}: checks run ${checkAccessWords(one)}.`, ...(one === "sandboxed" ? [] : [CHECK_ACCESS_WARNINGS[one]]),
|
|
11613
|
+
// Never added for it: the operator widens the network separately, with their own password.
|
|
11614
|
+
...(missing.length === 0 ? [] : [`Model sign-ins need these hosts in this project's network allowlist: ${missing.join(", ")}. Add them with: toolroll project network --repo ${repo} --allow ${missing.join(",")}`])];
|
|
11615
|
+
};
|
|
11616
|
+
if (wanted === undefined)
|
|
11617
|
+
return succeed(context.write, context.json, command, { repo, checkAccess: current, ...(here === current ? {} : { runsAs: here }), ...(here === "sign-ins" ? { missingHosts: signInHostsMissing(context.store.projectNetwork(repo)) } : {}) }, () => words(current));
|
|
11618
|
+
// Not available yet (check-access.ts signInsAvailable): refused before any password is asked for, nothing written.
|
|
11619
|
+
const unavailable = wanted === "sign-ins" ? signInsUnavailableWords(platform) : null;
|
|
11620
|
+
if (unavailable !== null)
|
|
11621
|
+
return fail(context.write, context.json, command, "refused", unavailable, EXIT.refused);
|
|
11622
|
+
const widening = widerThan(wanted, current);
|
|
11623
|
+
const acting = await askCredentials(flags, context);
|
|
11624
|
+
const proved = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token, repo);
|
|
11625
|
+
if (widening) {
|
|
11626
|
+
// Wider reach exposes the operator's own sign-ins or machine: an instance operator, with a password typed for this.
|
|
11627
|
+
if (acting === null || proved === null || !proved.ok || proved.method !== "password-now" || !context.store.isInstanceOperator(acting.name)) {
|
|
11628
|
+
return fail(context.write, context.json, command, "refused", "An instance operator widens what a project's checks reach, with their password typed now: pass --as and --token.", EXIT.refused);
|
|
11629
|
+
}
|
|
11630
|
+
}
|
|
11631
|
+
else if (acting === null || proved === null || !proved.ok || !credentialProof(proved.method) || !context.store.accountCanAccess(acting.name, repo)) {
|
|
11632
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project narrows what its checks reach, with their password: pass --as and --token.", EXIT.refused);
|
|
11633
|
+
}
|
|
11634
|
+
// The level is compared again inside the write: a change made while the password was asked for can't widen it here.
|
|
11635
|
+
const changed = context.store.setProjectCheckAccess(repo, wanted, acting.name, context.clock(), { may: widening ? "widen" : "narrow" });
|
|
11636
|
+
if (changed === null)
|
|
11637
|
+
return fail(context.write, context.json, command, "refused", "The project's check access changed meanwhile, and this would now widen it: an instance operator widens it, with their password typed now.", EXIT.refused);
|
|
11638
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, checkAccess: changed.after }, () => [
|
|
11639
|
+
...words(changed.after),
|
|
11640
|
+
"Scopes written from now on use this; scopes already approved keep what they were approved with.",
|
|
11641
|
+
]);
|
|
11642
|
+
}
|