toolroll 0.9.65 → 0.9.68
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 +93 -0
- package/README.md +20 -2
- 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 +69 -2
- package/dist/agent-fence.js +294 -11
- package/dist/agentconfig.js +4 -1
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +46 -0
- package/dist/browser/terminal.js +134 -23
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -29
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +56 -37
- 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/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 +18 -3
- package/dist/invoke.js +142 -17
- 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.d.ts +24 -13
- package/dist/live-terminal.js +81 -43
- 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 +2 -0
- package/dist/operate.js +436 -745
- 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 +6 -0
- package/dist/project-delete-ui.js +20 -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 +9 -0
- package/dist/release-adapters.js +81 -4
- package/dist/release.d.ts +7 -1
- package/dist/release.js +20 -7
- 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 +4 -8
- 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 +13 -0
- package/dist/scope.js +11 -18
- package/dist/scout-browser.js +2 -1
- package/dist/serve.js +3 -2
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +45 -4
- package/dist/server/console.css +60 -137
- package/dist/server/flows.js +2 -2
- package/dist/server/http.d.ts +2 -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-pages.d.ts +28 -3
- package/dist/server/render-pages.js +78 -9
- package/dist/server/render-results.d.ts +0 -6
- package/dist/server/render-results.js +3 -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 +43 -22
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +10 -7
- 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 +11 -74
- package/dist/store.js +36 -179
- 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/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/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- 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-broker.d.ts +12 -0
- package/dist/tool-broker.js +66 -2
- 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/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- 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/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +5 -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";
|
|
@@ -111,6 +111,7 @@ import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from
|
|
|
111
111
|
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
|
+
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
114
115
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
115
116
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
116
117
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -119,7 +120,7 @@ import { spawn as spawnChild } from "node:child_process";
|
|
|
119
120
|
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
120
121
|
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
121
122
|
import { auditCommand } from "./audit-cli.js";
|
|
122
|
-
import { closedQuestionWords, hasDisguisedText,
|
|
123
|
+
import { closedQuestionWords, hasDisguisedText, validateNote } from "./decision.js";
|
|
123
124
|
import { readVerifiedArtifact, readVerifiedReport, storeEvidence } from "./evidence.js";
|
|
124
125
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges, encodePlannerSource, plannerSourceOf } from "./planner-source.js";
|
|
125
126
|
/**
|
|
@@ -161,7 +162,7 @@ function planContractLines(store, evidenceRoot, taskId) {
|
|
|
161
162
|
];
|
|
162
163
|
}
|
|
163
164
|
import { verdictWords as proofVerdictWords, matrixWords, semanticCoverage, coverageWords } from "./proof.js";
|
|
164
|
-
import { probeRepo
|
|
165
|
+
import { probeRepo } from "./probe.js";
|
|
165
166
|
import { diagnoseTaskDispatch, diagnosisIsDispatchable, scopeApprovedForDispatch, } from "./dispatch.js";
|
|
166
167
|
import { createDecisionServer } from "./serve.js";
|
|
167
168
|
import { readTailnetNames } from "./tailnet.js";
|
|
@@ -177,7 +178,7 @@ import { activeRemote, REMOTE_MESSAGES, REMOTE_REFUSED_FLAGS, REMOTE_SCOPES, Rem
|
|
|
177
178
|
import { projectAuthority } from "./project-access.js";
|
|
178
179
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
179
180
|
import { tally, spendLine } from "./summary.js";
|
|
180
|
-
import {
|
|
181
|
+
import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } from "./publish.js";
|
|
181
182
|
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
182
183
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
183
184
|
import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
|
|
@@ -188,7 +189,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
188
189
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
189
190
|
import { serveMcp } from "./mcp.js";
|
|
190
191
|
import { createInterface } from "node:readline";
|
|
191
|
-
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken,
|
|
192
|
+
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin } from "./scope.js";
|
|
192
193
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
193
194
|
import { WorktreePool } from "./worktree.js";
|
|
194
195
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -216,7 +217,7 @@ import { plan as planTask } from "./planner.js";
|
|
|
216
217
|
import { attachTmux, elapsedWords, openInTmux, PEEK_TAIL_LINES, runPeek, snapshotLiveRuns } from "./peek-cli.js";
|
|
217
218
|
import { scout as scoutTask } from "./scout.js";
|
|
218
219
|
import { profileDigestOf } from "./scope.js";
|
|
219
|
-
import { PROVIDER_KEY_ENV,
|
|
220
|
+
import { PROVIDER_KEY_ENV, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
|
|
220
221
|
import { run, terminateLiveProviders, run as execRun } from "./exec.js";
|
|
221
222
|
import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
222
223
|
import { readPulls } from "./pulls.js";
|
|
@@ -260,6 +261,10 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
260
261
|
toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
|
|
261
262
|
what the project's builds may reach on the network; an approver changes it
|
|
262
263
|
(new scopes seal it; approved ones keep theirs)
|
|
264
|
+
toolroll project writes --repo <p> [workspace|anywhere]
|
|
265
|
+
where the project's builds may write: only their checkout, caches and temp
|
|
266
|
+
(workspace, the default), or anywhere outside Toolroll's fenced folders;
|
|
267
|
+
an approver changes it (new scopes seal it; approved ones keep theirs)
|
|
263
268
|
toolroll config unconfined-agents [allow|refuse]
|
|
264
269
|
whether builds run where this machine can't confine them (instance operator)
|
|
265
270
|
toolroll project concurrency [<n>] --repo <p>
|
|
@@ -688,6 +693,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
688
693
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
689
694
|
"self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
|
|
690
695
|
"html", "csv", "alerts-only",
|
|
696
|
+
// project network: what its builds may reach (--open | --allowlist).
|
|
697
|
+
"open", "allowlist",
|
|
691
698
|
// v105: the full export.
|
|
692
699
|
"zip",
|
|
693
700
|
// Settings → Integrations: the last checks, without checking again.
|
|
@@ -709,6 +716,9 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
709
716
|
// task steer: pause the running Claude build, then send the note.
|
|
710
717
|
"now",
|
|
711
718
|
]);
|
|
719
|
+
/** `project network` reads --remove <hosts> and --local any|none as values, where every other verb's --remove and
|
|
720
|
+
* --local are switches. */
|
|
721
|
+
const PROJECT_NETWORK_VALUE_FLAGS = new Set(["remove", "local"]);
|
|
712
722
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
713
723
|
const positional = [];
|
|
714
724
|
const flags = new Map();
|
|
@@ -767,7 +777,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
767
777
|
return withPasswordSource(CLI_PASSWORD_SOURCE, () => rememberedLogin.run({ login: null }, () => runOperateLocal(command, argv, write, options)));
|
|
768
778
|
}
|
|
769
779
|
async function runOperateLocal(command, argv, write, options = {}) {
|
|
770
|
-
const parsed = parseOperateArgs(argv, command
|
|
780
|
+
const parsed = parseOperateArgs(argv, ownValueFlags(command, argv));
|
|
771
781
|
// A parse error precedes the flags map, so JSON mode is read from the raw
|
|
772
782
|
// argv — the envelope contract holds even for the earliest refusal.
|
|
773
783
|
if ("error" in parsed)
|
|
@@ -979,7 +989,7 @@ export async function runOperateAs(argv, opts) {
|
|
|
979
989
|
const scope = REMOTE_SCOPES.get(row.invocation);
|
|
980
990
|
if (row.remote !== "yes" || scope === undefined)
|
|
981
991
|
return refuse("not-remote", REMOTE_MESSAGES.no);
|
|
982
|
-
const parsed = parseOperateArgs(rest, root
|
|
992
|
+
const parsed = parseOperateArgs(rest, ownValueFlags(root, rest));
|
|
983
993
|
if ("error" in parsed)
|
|
984
994
|
return refuse("usage", parsed.error);
|
|
985
995
|
// The parser's positionals must spell the same row (a value flag can't hide a subcommand).
|
|
@@ -1159,6 +1169,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1159
1169
|
return projectConcurrencyCommand(positional, flags, context);
|
|
1160
1170
|
if (positional[0] === "network")
|
|
1161
1171
|
return projectNetworkCommand(positional, flags, context);
|
|
1172
|
+
if (positional[0] === "writes")
|
|
1173
|
+
return projectWritesCommand(positional, flags, context);
|
|
1162
1174
|
if (positional[0] === "checks")
|
|
1163
1175
|
return projectChecksCommand(positional, flags, context);
|
|
1164
1176
|
if (positional[0] === "demo")
|
|
@@ -1344,9 +1356,8 @@ async function readyCommand(flags, context) {
|
|
|
1344
1356
|
if (backendName !== BUILT_IN) {
|
|
1345
1357
|
const repo = repoFrom(flags);
|
|
1346
1358
|
const backend = openBackend(backendName, store, repo);
|
|
1347
|
-
if (backend === null)
|
|
1359
|
+
if (backend === null)
|
|
1348
1360
|
return fail(write, json, "ready", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
1349
|
-
}
|
|
1350
1361
|
const result = await backend.listReady();
|
|
1351
1362
|
if (!result.ok)
|
|
1352
1363
|
return fail(write, json, "ready", result.reason, result.message, EXIT.failed);
|
|
@@ -1431,17 +1442,14 @@ function claimCommand(positional, flags, context) {
|
|
|
1431
1442
|
// protects those leases would be protecting the wrong thing. This is what
|
|
1432
1443
|
// "auth from the first commit" is for — it is only cheap now.
|
|
1433
1444
|
const token = text(flags, "token");
|
|
1434
|
-
if (token === undefined)
|
|
1445
|
+
if (token === undefined)
|
|
1435
1446
|
return fail(write, json, "claim", "usage", "--token proves the runner is who it says", EXIT.usage);
|
|
1436
|
-
}
|
|
1437
1447
|
const auth = authenticate(store, runner, token);
|
|
1438
|
-
if (!auth.ok)
|
|
1448
|
+
if (!auth.ok)
|
|
1439
1449
|
return fail(write, json, "claim", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
1440
|
-
}
|
|
1441
1450
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
1442
|
-
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1451
|
+
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1443
1452
|
return fail(write, json, "claim", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
1444
|
-
}
|
|
1445
1453
|
const ttl = readTtl(flags);
|
|
1446
1454
|
if (ttl === null)
|
|
1447
1455
|
return fail(write, json, "claim", "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
@@ -1463,9 +1471,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1463
1471
|
// tasks" while letting the asker declare which those are is not a rule.
|
|
1464
1472
|
origin: store.originOf(backend, id),
|
|
1465
1473
|
});
|
|
1466
|
-
if (!verdict.ok)
|
|
1474
|
+
if (!verdict.ok)
|
|
1467
1475
|
return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
|
|
1468
|
-
}
|
|
1469
1476
|
}
|
|
1470
1477
|
const ref = store.refFor(backend, id);
|
|
1471
1478
|
const result = acquire(store, ref.id, runner, {
|
|
@@ -1477,9 +1484,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1477
1484
|
if (!result.ok) {
|
|
1478
1485
|
// Two distinct refusals: somebody HOLDS it right now, or it is
|
|
1479
1486
|
// RESERVED for a specific worker whoever asks (queue columns, v19).
|
|
1480
|
-
if (result.reason === "reserved")
|
|
1487
|
+
if (result.reason === "reserved")
|
|
1481
1488
|
return fail(write, json, "claim", "reserved", `${id} is reserved for ${result.reservedFor} — only that worker takes it`, EXIT.refused, { reservedFor: result.reservedFor });
|
|
1482
|
-
}
|
|
1483
1489
|
if (result.reason === "external") {
|
|
1484
1490
|
const said = {
|
|
1485
1491
|
"stale-mirror": "this tracker item has not been seen recently — run `toolroll sync` first",
|
|
@@ -1491,24 +1497,18 @@ function claimCommand(positional, flags, context) {
|
|
|
1491
1497
|
detail: result.detail,
|
|
1492
1498
|
});
|
|
1493
1499
|
}
|
|
1494
|
-
if (result.reason === "mode-ended")
|
|
1500
|
+
if (result.reason === "mode-ended")
|
|
1495
1501
|
return fail(write, json, "claim", "mode-ended", result.message, EXIT.refused);
|
|
1496
|
-
|
|
1497
|
-
if (result.reason === "unauthenticated") {
|
|
1502
|
+
if (result.reason === "unauthenticated")
|
|
1498
1503
|
return fail(write, json, "claim", "unauthenticated", describeAuth(result.detail, runner), EXIT.refused);
|
|
1499
|
-
|
|
1500
|
-
if (result.reason === "unplaced") {
|
|
1504
|
+
if (result.reason === "unplaced")
|
|
1501
1505
|
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);
|
|
1502
|
-
|
|
1503
|
-
if (result.reason === "unauthorized-repo") {
|
|
1506
|
+
if (result.reason === "unauthorized-repo")
|
|
1504
1507
|
return fail(write, json, "claim", "unauthorized-repo", `this runner is not bound to ${result.repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
1505
|
-
|
|
1506
|
-
if (result.reason === "coordinator-filed") {
|
|
1508
|
+
if (result.reason === "coordinator-filed")
|
|
1507
1509
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1508
|
-
|
|
1509
|
-
if (result.reason === "sweep-limit") {
|
|
1510
|
+
if (result.reason === "sweep-limit")
|
|
1510
1511
|
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 });
|
|
1511
|
-
}
|
|
1512
1512
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1513
1513
|
}
|
|
1514
1514
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1628,9 +1628,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1628
1628
|
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);
|
|
1629
1629
|
}
|
|
1630
1630
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1631
|
-
if (!authenticated.ok)
|
|
1631
|
+
if (!authenticated.ok)
|
|
1632
1632
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1633
|
-
}
|
|
1634
1633
|
const repos = context.repoList ?? [];
|
|
1635
1634
|
if (repos.length === 0) {
|
|
1636
1635
|
return fail(write, json, "runner register", "usage", "--repo names at least one repository this runner may build — authority binds at the mint", EXIT.usage);
|
|
@@ -1640,9 +1639,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1640
1639
|
// a worker, no paste step. The file must not already exist: a second
|
|
1641
1640
|
// mint never silently overwrites a credential a running watch holds.
|
|
1642
1641
|
const tokenFile = text(flags, "token-file");
|
|
1643
|
-
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1642
|
+
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1644
1643
|
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);
|
|
1645
|
-
}
|
|
1646
1644
|
const { runner, token, reclaimed } = register(store, {
|
|
1647
1645
|
name,
|
|
1648
1646
|
host: hostname(),
|
|
@@ -1682,13 +1680,11 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1682
1680
|
}
|
|
1683
1681
|
if (action === "heartbeat") {
|
|
1684
1682
|
const token = text(flags, "token");
|
|
1685
|
-
if (name === undefined || token === undefined)
|
|
1683
|
+
if (name === undefined || token === undefined)
|
|
1686
1684
|
return fail(write, json, "runner heartbeat", "usage", "`runner heartbeat <name> --token <token>`", EXIT.usage);
|
|
1687
|
-
}
|
|
1688
1685
|
const result = heartbeatRunner(store, name, token, now);
|
|
1689
|
-
if (!result.ok)
|
|
1686
|
+
if (!result.ok)
|
|
1690
1687
|
return fail(write, json, "runner heartbeat", result.reason, describeAuth(result.reason, name), EXIT.refused);
|
|
1691
|
-
}
|
|
1692
1688
|
return succeed(write, json, "runner heartbeat", { runner: result.runner }, () => [
|
|
1693
1689
|
`${name} checked in.`,
|
|
1694
1690
|
]);
|
|
@@ -1723,22 +1719,16 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1723
1719
|
return EXIT.ok;
|
|
1724
1720
|
}
|
|
1725
1721
|
if (action === "retire") {
|
|
1726
|
-
if (name === undefined)
|
|
1722
|
+
if (name === undefined)
|
|
1727
1723
|
return fail(write, json, "runner retire", "usage", "which runner?", EXIT.usage);
|
|
1728
|
-
}
|
|
1729
1724
|
// Retiring is revoking authority — the same operator act as minting it.
|
|
1730
1725
|
const acting = await askCredentials(flags, context);
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1735
|
-
if (!authenticated.ok) {
|
|
1736
|
-
return fail(write, json, "runner retire", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1737
|
-
}
|
|
1726
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1727
|
+
if (acting === null || !verified?.ok)
|
|
1728
|
+
return refuseApprover(context, "runner retire", verified, "`runner retire <name> --as <you> --token <t>` — revoking runner authority is an operator act", name);
|
|
1738
1729
|
const retired = store.retireRunner(name, now, mutationFrom(flags, now));
|
|
1739
|
-
if (!retired)
|
|
1730
|
+
if (!retired)
|
|
1740
1731
|
return fail(write, json, "runner retire", "unknown", `no runner \`${name}\``, EXIT.refused);
|
|
1741
|
-
}
|
|
1742
1732
|
return succeed(write, json, "runner retire", { name }, () => [`${name} is retired.`]);
|
|
1743
1733
|
}
|
|
1744
1734
|
if (action === "bind") {
|
|
@@ -1754,17 +1744,15 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1754
1744
|
return fail(write, json, "runner bind", "usage", "`runner bind <name> --repo <path> --as <you> --token <t>` — binding repositories is an operator act", EXIT.usage);
|
|
1755
1745
|
}
|
|
1756
1746
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1757
|
-
if (!authenticated.ok)
|
|
1747
|
+
if (!authenticated.ok)
|
|
1758
1748
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1759
|
-
}
|
|
1760
1749
|
const repos = context.repoList ?? [];
|
|
1761
1750
|
if (repos.length === 0) {
|
|
1762
1751
|
return fail(write, json, "runner bind", "usage", "--repo names at least one repository", EXIT.usage);
|
|
1763
1752
|
}
|
|
1764
1753
|
const bound = store.bindRunnerRepos(name, canonicalRepos(repos), now);
|
|
1765
|
-
if (!bound.ok)
|
|
1754
|
+
if (!bound.ok)
|
|
1766
1755
|
return fail(write, json, "runner bind", bound.reason, describeAuth(bound.reason, name), EXIT.refused);
|
|
1767
|
-
}
|
|
1768
1756
|
return succeed(write, json, "runner bind", { name, repos: bound.repos }, () => [
|
|
1769
1757
|
`${name} may build in:`,
|
|
1770
1758
|
...bound.repos.map(one => ` ${one}`),
|
|
@@ -1775,22 +1763,18 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1775
1763
|
// ledger as before → after. It never stops running work: claims already
|
|
1776
1764
|
// held keep going, and the new number applies at the next claim.
|
|
1777
1765
|
const given = positional[2];
|
|
1778
|
-
if (name === undefined || given === undefined || positional.length > 3)
|
|
1766
|
+
if (name === undefined || given === undefined || positional.length > 3)
|
|
1779
1767
|
return fail(write, json, "runner capacity", "usage", "`runner capacity <name> <n> --as <you> --token <t>`", EXIT.usage);
|
|
1780
|
-
}
|
|
1781
1768
|
const capacity = parseRunnerCapacity(given, { allowZero: true });
|
|
1782
|
-
if (capacity === null)
|
|
1769
|
+
if (capacity === null)
|
|
1783
1770
|
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);
|
|
1784
|
-
}
|
|
1785
1771
|
const acting = await askCredentials(flags, context);
|
|
1786
1772
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1787
|
-
if (acting === null ||
|
|
1773
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
1788
1774
|
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);
|
|
1789
|
-
}
|
|
1790
1775
|
const changed = store.setRunnerCapacity(name, capacity, acting.name, now);
|
|
1791
|
-
if (!changed.ok)
|
|
1776
|
+
if (!changed.ok)
|
|
1792
1777
|
return fail(write, json, "runner capacity", changed.reason, changed.reason === "retired" ? `${name} is retired` : `no runner \`${name}\``, EXIT.refused);
|
|
1793
|
-
}
|
|
1794
1778
|
const running = store.liveClaimCount(name, now);
|
|
1795
1779
|
if (changed.after === 0) {
|
|
1796
1780
|
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
@@ -1840,9 +1824,8 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1840
1824
|
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);
|
|
1841
1825
|
}
|
|
1842
1826
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1843
|
-
if (!authenticated.ok)
|
|
1827
|
+
if (!authenticated.ok)
|
|
1844
1828
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1845
|
-
}
|
|
1846
1829
|
const repos = context.repoList ?? [];
|
|
1847
1830
|
if (repos.length === 0) {
|
|
1848
1831
|
return fail(write, json, "coordinator mint", "usage", "--repo names at least one repository this coordinator may file into", EXIT.usage);
|
|
@@ -1880,17 +1863,12 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1880
1863
|
]);
|
|
1881
1864
|
}
|
|
1882
1865
|
if (action === "revoke") {
|
|
1883
|
-
if (name === undefined)
|
|
1866
|
+
if (name === undefined)
|
|
1884
1867
|
return fail(write, json, "coordinator revoke", "usage", "which coordinator? (`coordinator list` shows cids)", EXIT.usage);
|
|
1885
|
-
}
|
|
1886
1868
|
const acting = await askCredentials(flags, context);
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1891
|
-
if (!authenticated.ok) {
|
|
1892
|
-
return fail(write, json, "coordinator revoke", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1893
|
-
}
|
|
1869
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1870
|
+
if (acting === null || !verified?.ok)
|
|
1871
|
+
return refuseApprover(context, "coordinator revoke", verified, "`coordinator revoke <cid> --as <you> --token <t>` — revoking is an operator act", name);
|
|
1894
1872
|
const revoked = revokeCoordinator(store, name, acting.name, now);
|
|
1895
1873
|
if (!revoked.ok) {
|
|
1896
1874
|
const said = revoked.reason === "unknown" ? `no coordinator \`${name}\` — \`coordinator list\` shows cids` : "already revoked";
|
|
@@ -2054,19 +2032,16 @@ async function buildCommand(positional, flags, context) {
|
|
|
2054
2032
|
const runner = text(flags, "runner");
|
|
2055
2033
|
const token = text(flags, "token");
|
|
2056
2034
|
const branch = text(flags, "branch");
|
|
2057
|
-
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2035
|
+
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2058
2036
|
return fail(write, json, "build", "usage", "`toolroll build <id> --runner <name> --token <t> --branch <b> --repo <path>`", EXIT.usage);
|
|
2059
|
-
}
|
|
2060
2037
|
const auth = authenticate(store, runner, token);
|
|
2061
|
-
if (!auth.ok)
|
|
2038
|
+
if (!auth.ok)
|
|
2062
2039
|
return fail(write, json, "build", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2063
|
-
}
|
|
2064
2040
|
const repo = repoFrom(flags);
|
|
2065
2041
|
// Standalone build proves membership BEFORE leasing anything (MCP spec
|
|
2066
2042
|
// v6, round-4 finding 2): the caller's --repo is a claim, not authority.
|
|
2067
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2043
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2068
2044
|
return fail(write, json, "build", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2069
|
-
}
|
|
2070
2045
|
// 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).
|
|
2071
2046
|
if (store.getTask(id) === null)
|
|
2072
2047
|
return fail(write, json, "build", "unknown-task", `No task ${id}.`, EXIT.refused);
|
|
@@ -2081,9 +2056,8 @@ async function buildCommand(positional, flags, context) {
|
|
|
2081
2056
|
now,
|
|
2082
2057
|
...(text(flags, "base") === undefined ? {} : { base: text(flags, "base") }),
|
|
2083
2058
|
});
|
|
2084
|
-
if (!leased.ok)
|
|
2059
|
+
if (!leased.ok)
|
|
2085
2060
|
return fail(write, json, "build", leased.reason, leased.message, EXIT.refused);
|
|
2086
|
-
}
|
|
2087
2061
|
// The standalone road records its attempt and carries its exact lease the
|
|
2088
2062
|
// same as tick's — a park sealed here goes through the same fenced
|
|
2089
2063
|
// transaction, because a gate one road bypasses is a suggestion. No claim
|
|
@@ -2262,22 +2236,19 @@ async function tickCommand(flags, context) {
|
|
|
2262
2236
|
return demoFence;
|
|
2263
2237
|
const runner = text(flags, "runner");
|
|
2264
2238
|
const token = text(flags, "token");
|
|
2265
|
-
if (runner === undefined || token === undefined)
|
|
2239
|
+
if (runner === undefined || token === undefined)
|
|
2266
2240
|
return fail(write, json, "tick", "usage", "`toolroll tick --runner <name> --token <t> --repo <path> [--max <n>] [--base <ref>]`", EXIT.usage);
|
|
2267
|
-
}
|
|
2268
2241
|
// Heartbeat rather than bare auth: a pass that is about to hold leases for
|
|
2269
2242
|
// half an hour should also be on record as alive.
|
|
2270
2243
|
const auth = heartbeatRunner(store, runner, token, clock());
|
|
2271
|
-
if (!auth.ok)
|
|
2244
|
+
if (!auth.ok)
|
|
2272
2245
|
return fail(write, json, "tick", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2273
|
-
}
|
|
2274
2246
|
if (updateAdmissionPaused(store.raw()))
|
|
2275
2247
|
return fail(write, json, "tick", "updating", UPDATE_PAUSED, EXIT.refused);
|
|
2276
2248
|
const maxGiven = text(flags, "max");
|
|
2277
2249
|
const maxAsked = maxGiven === undefined ? 1 : Number(maxGiven);
|
|
2278
|
-
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2250
|
+
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2279
2251
|
return fail(write, json, "tick", "usage", "--max takes a whole number of tasks", EXIT.usage);
|
|
2280
|
-
}
|
|
2281
2252
|
// The service's build lanes (project-concurrency.ts): a lane over its
|
|
2282
2253
|
// project's fair share still runs its housekeeping but starts no
|
|
2283
2254
|
// unattended build this pass; an extra lane only builds.
|
|
@@ -2285,17 +2256,15 @@ async function tickCommand(flags, context) {
|
|
|
2285
2256
|
const buildsOnly = flags.has("builds-only");
|
|
2286
2257
|
const syncAgeGiven = text(flags, "sync-max-age");
|
|
2287
2258
|
const syncMaxAgeMs = syncAgeGiven === undefined ? SYNC_MAX_AGE_MS : Number(syncAgeGiven) * 1000;
|
|
2288
|
-
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2259
|
+
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2289
2260
|
return fail(write, json, "tick", "usage", "--sync-max-age takes whole seconds", EXIT.usage);
|
|
2290
|
-
}
|
|
2291
2261
|
const repo = repoFrom(flags);
|
|
2292
2262
|
// THE PRE-I/O MEMBERSHIP CHECK (MCP spec v6, round-4 finding 1): the
|
|
2293
2263
|
// pass proves its canonical repo is in the runner's BOUND list before
|
|
2294
2264
|
// any git access, probe, or schedule fires. The --repo flag stops being
|
|
2295
2265
|
// authority on every runner road, not only at the claim.
|
|
2296
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2266
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2297
2267
|
return fail(write, json, "tick", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2298
|
-
}
|
|
2299
2268
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
2300
2269
|
const model = text(flags, "model");
|
|
2301
2270
|
const repairModel = text(flags, "repair-model");
|
|
@@ -2330,9 +2299,8 @@ async function tickCommand(flags, context) {
|
|
|
2330
2299
|
let base = text(flags, "base");
|
|
2331
2300
|
if (base === undefined) {
|
|
2332
2301
|
const head = await git("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd: repo });
|
|
2333
|
-
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2302
|
+
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2334
2303
|
return fail(write, json, "tick", "git", `${repo} has no branch checked out — say --base explicitly`, EXIT.refused);
|
|
2335
|
-
}
|
|
2336
2304
|
base = head.stdout.trim();
|
|
2337
2305
|
}
|
|
2338
2306
|
// Probes run at every checkpoint (§3): a key revoked overnight is caught
|
|
@@ -3657,9 +3625,8 @@ async function briefCommand(flags, context) {
|
|
|
3657
3625
|
// (§6): the last watch episode's window and runner, instead of "the last
|
|
3658
3626
|
// 24 hours" — which can mix two windows, or none.
|
|
3659
3627
|
const episode = flags.has("latest-watch") ? store.latestWatchEpisode(repo) : null;
|
|
3660
|
-
if (flags.has("latest-watch") && episode === null)
|
|
3628
|
+
if (flags.has("latest-watch") && episode === null)
|
|
3661
3629
|
return fail(write, json, "brief", "no-watch", "no watch episode recorded for this repo yet — run `toolroll watch` first", EXIT.refused);
|
|
3662
|
-
}
|
|
3663
3630
|
const since = episode?.startedAt ??
|
|
3664
3631
|
text(flags, "since") ??
|
|
3665
3632
|
new Date(clock().getTime() - 24 * 60 * 60_000).toISOString();
|
|
@@ -3829,13 +3796,11 @@ async function decideCommand(positional, flags, context) {
|
|
|
3829
3796
|
return EXIT.refused;
|
|
3830
3797
|
}
|
|
3831
3798
|
const id = Number(idText);
|
|
3832
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
3799
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
3833
3800
|
return fail(write, json, "decide", "usage", "`toolroll decide [<id>] [--choose <option>]`", EXIT.usage);
|
|
3834
|
-
}
|
|
3835
3801
|
const decision = store.getDecision(id);
|
|
3836
|
-
if (decision === null)
|
|
3802
|
+
if (decision === null)
|
|
3837
3803
|
return fail(write, json, "decide", "unknown-decision", `no decision ${id}`, EXIT.refused);
|
|
3838
|
-
}
|
|
3839
3804
|
const choice = text(flags, "choose");
|
|
3840
3805
|
if (choice === undefined) {
|
|
3841
3806
|
const evidence = store.evidenceFor(id);
|
|
@@ -3880,14 +3845,10 @@ async function decideCommand(positional, flags, context) {
|
|
|
3880
3845
|
return EXIT.ok;
|
|
3881
3846
|
}
|
|
3882
3847
|
const acting = await askCredentials(flags, context);
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3848
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
3849
|
+
if (acting === null || !verified?.ok)
|
|
3850
|
+
return refuseApprover(context, "decide", verified, "answering takes `--as <you> --token <t>` — who decided is recorded, not asserted", String(id));
|
|
3886
3851
|
const { name: asWho, token } = acting;
|
|
3887
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
3888
|
-
if (!authenticated.ok) {
|
|
3889
|
-
return fail(write, json, "decide", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
3890
|
-
}
|
|
3891
3852
|
const note = text(flags, "note");
|
|
3892
3853
|
const answered = store.answerDecision({ id, choice, by: asWho, via: "cli", ...(note === undefined ? {} : { note }) }, clock(), mutationFrom(flags, clock()));
|
|
3893
3854
|
if (!answered.ok) {
|
|
@@ -3987,9 +3948,8 @@ async function serveCommand(flags, context) {
|
|
|
3987
3948
|
const port = Number(portGiven ?? 4180);
|
|
3988
3949
|
// Validated like demo's --port — a NaN handed to listen() surfaced as a
|
|
3989
3950
|
// baffling bind failure instead of a usage answer (round-4 finding 11).
|
|
3990
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
3951
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
3991
3952
|
return fail(write, json, "serve", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
3992
|
-
}
|
|
3993
3953
|
const host = text(flags, "host") ?? "127.0.0.1";
|
|
3994
3954
|
const allow = text(flags, "allow-host");
|
|
3995
3955
|
// The authorization ceiling: --repo (comma-separable) names repos this
|
|
@@ -4008,12 +3968,10 @@ async function serveCommand(flags, context) {
|
|
|
4008
3968
|
// allow-list of one, and which is meaningless without the machine's
|
|
4009
3969
|
// runner assertion — links bind to runs this machine owns.
|
|
4010
3970
|
const editor = text(flags, "editor");
|
|
4011
|
-
if (editor !== undefined && editor !== "vscode")
|
|
3971
|
+
if (editor !== undefined && editor !== "vscode")
|
|
4012
3972
|
return fail(write, json, "serve", "usage", "--editor supports: vscode", EXIT.usage);
|
|
4013
|
-
|
|
4014
|
-
if (editor !== undefined && localRunner === undefined) {
|
|
3973
|
+
if (editor !== undefined && localRunner === undefined)
|
|
4015
3974
|
return fail(write, json, "serve", "usage", "--editor needs --runner <name> — links bind to the worktrees this machine's runner owns", EXIT.usage);
|
|
4016
|
-
}
|
|
4017
3975
|
// The first-account road (setup review): with no approver, the login
|
|
4018
3976
|
// page offers to create one, gated by this code — printed below, once.
|
|
4019
3977
|
const setupCode = store.listApprovers().length === 0 ? String(randomInt(100_000, 1_000_000)) : undefined;
|
|
@@ -4064,18 +4022,13 @@ async function serveCommand(flags, context) {
|
|
|
4064
4022
|
async function requeueTask(positional, flags, context) {
|
|
4065
4023
|
const { store, write, json, clock } = context;
|
|
4066
4024
|
const [id] = positional;
|
|
4067
|
-
if (id === undefined)
|
|
4025
|
+
if (id === undefined)
|
|
4068
4026
|
return fail(write, json, "task requeue", "usage", "`toolroll task requeue <id> --as <you> --token <t>`", EXIT.usage);
|
|
4069
|
-
}
|
|
4070
4027
|
const acting = await askCredentials(flags, context);
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4028
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4029
|
+
if (acting === null || !verified?.ok)
|
|
4030
|
+
return refuseApprover(context, "task requeue", verified, "requeueing takes `--as <you> --token <t>` — who overrode the stall is recorded, not asserted", id);
|
|
4074
4031
|
const { name: asWho, token } = acting;
|
|
4075
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4076
|
-
if (!authenticated.ok) {
|
|
4077
|
-
return fail(write, json, "task requeue", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4078
|
-
}
|
|
4079
4032
|
const result = store.requeueTask(id, asWho, clock());
|
|
4080
4033
|
if (!result.ok) {
|
|
4081
4034
|
const why = {
|
|
@@ -4104,17 +4057,12 @@ async function requeueTask(positional, flags, context) {
|
|
|
4104
4057
|
async function regateTaskCommand(positional, flags, context) {
|
|
4105
4058
|
const { store, write, json, clock } = context;
|
|
4106
4059
|
const [id] = positional;
|
|
4107
|
-
if (id === undefined)
|
|
4060
|
+
if (id === undefined)
|
|
4108
4061
|
return fail(write, json, "task regate", "usage", "`toolroll task regate <id> --as <you> --token <t>`", EXIT.usage);
|
|
4109
|
-
}
|
|
4110
4062
|
const acting = await askCredentials(flags, context);
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
4115
|
-
if (!authenticated.ok) {
|
|
4116
|
-
return fail(write, json, "task regate", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4117
|
-
}
|
|
4063
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4064
|
+
if (acting === null || !verified?.ok)
|
|
4065
|
+
return refuseApprover(context, "task regate", verified, "rerunning the check takes `--as <you> --token <t>` — the rerun scope is approved in your name", id);
|
|
4118
4066
|
const result = regateTask(store, id, clock(), { kind: "operator", name: acting.name, token: acting.token });
|
|
4119
4067
|
if (!result.ok)
|
|
4120
4068
|
return fail(write, json, "task regate", result.reason, result.message, EXIT.refused);
|
|
@@ -4131,49 +4079,39 @@ async function regateTaskCommand(positional, flags, context) {
|
|
|
4131
4079
|
async function planTaskCommand(positional, flags, context) {
|
|
4132
4080
|
const { store, write, json, clock } = context;
|
|
4133
4081
|
const [id] = positional;
|
|
4134
|
-
if (id === undefined)
|
|
4082
|
+
if (id === undefined)
|
|
4135
4083
|
return fail(write, json, "task plan", "usage", "`toolroll task plan <id> --as <you> --token <t>`", EXIT.usage);
|
|
4136
|
-
}
|
|
4137
4084
|
const acting = await askCredentials(flags, context);
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
if (!authenticated.ok) {
|
|
4143
|
-
return fail(write, json, "task plan", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4144
|
-
}
|
|
4145
|
-
if (store.getTask(id) === null) {
|
|
4085
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4086
|
+
if (acting === null || !verified?.ok)
|
|
4087
|
+
return refuseApprover(context, "task plan", verified, "planning takes `--as <you> --token <t>` — it dispatches an agent that spends", id);
|
|
4088
|
+
if (store.getTask(id) === null)
|
|
4146
4089
|
return fail(write, json, "task plan", "refused", `no task ${id}`, EXIT.refused);
|
|
4147
|
-
}
|
|
4148
4090
|
const ref = store.refFor(BUILT_IN, id);
|
|
4149
4091
|
// Plan pins (P2/C7): a named pair binds the PLAN phase only, refused
|
|
4150
4092
|
// while a live plan claim is spending on the previous answer.
|
|
4151
4093
|
const pinProvider = text(flags, "provider");
|
|
4152
4094
|
const pinModel = text(flags, "model");
|
|
4153
4095
|
if (pinProvider !== undefined) {
|
|
4154
|
-
if (!isProviderId(pinProvider))
|
|
4096
|
+
if (!isProviderId(pinProvider))
|
|
4155
4097
|
return fail(write, json, "task plan", "usage", `unknown provider \`${pinProvider}\``, EXIT.usage);
|
|
4156
|
-
}
|
|
4157
4098
|
// A plan pin is an exact pair (v47): the route freezes model ids, and
|
|
4158
4099
|
// a pin with no model would bind nothing exact.
|
|
4159
|
-
if (pinModel === undefined || pinModel === "")
|
|
4100
|
+
if (pinModel === undefined || pinModel === "")
|
|
4160
4101
|
return fail(write, json, "task plan", "usage", "a plan pin names an exact --model with --provider — approvals bind exact routing", EXIT.usage);
|
|
4161
|
-
}
|
|
4162
4102
|
const valid = validateSpec({ provider: pinProvider, model: pinModel });
|
|
4163
4103
|
if (!valid.ok)
|
|
4164
4104
|
return fail(write, json, "task plan", "invalid", valid.problem, EXIT.usage);
|
|
4165
4105
|
const pinned = store.setPlanPins(ref.id, pinProvider, pinModel, clock());
|
|
4166
|
-
if (!pinned.ok)
|
|
4106
|
+
if (!pinned.ok)
|
|
4167
4107
|
return fail(write, json, "task plan", "refused", "a planner holds this task right now — the pin would reroute a live spend", EXIT.refused);
|
|
4168
|
-
}
|
|
4169
4108
|
}
|
|
4170
4109
|
else if (pinModel !== undefined) {
|
|
4171
4110
|
return fail(write, json, "task plan", "usage", "--model rides --provider for a plan pin", EXIT.usage);
|
|
4172
4111
|
}
|
|
4173
4112
|
const result = store.requestPlan(ref.id, clock());
|
|
4174
|
-
if (!result.ok)
|
|
4113
|
+
if (!result.ok)
|
|
4175
4114
|
return fail(write, json, "task plan", "refused", result.reason, EXIT.refused);
|
|
4176
|
-
}
|
|
4177
4115
|
const autoPlan = authorizePlanUnderMode(store, id, acting.name, clock());
|
|
4178
4116
|
return succeed(write, json, "task plan", { id, autoPlan, ...(pinProvider === undefined ? {} : { planProvider: pinProvider, planModel: pinModel ?? null }) }, () => [
|
|
4179
4117
|
`${id} will be planned before it is built: the next pass dispatches a planner${pinProvider === undefined ? "" : ` on ${pinProvider}${pinModel === undefined ? "" : ` \u00b7 ${pinModel}`}`}.`,
|
|
@@ -4212,9 +4150,8 @@ async function reviewTaskCommand(positional, flags, context) {
|
|
|
4212
4150
|
return fail(write, json, "task review", "usage", "--run and --all require --brief.", EXIT.usage);
|
|
4213
4151
|
const [runText] = positional;
|
|
4214
4152
|
const runId = Number(runText ?? "");
|
|
4215
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4153
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4216
4154
|
return fail(write, json, "task review", "usage", "`toolroll task review <run-id>`", EXIT.usage);
|
|
4217
|
-
}
|
|
4218
4155
|
// Read-only: the run's one automatic review, when it had one.
|
|
4219
4156
|
const review = buildReviewOf(store, runId);
|
|
4220
4157
|
if (review !== null) {
|
|
@@ -4249,26 +4186,19 @@ async function incidentCommand(positional, flags, context) {
|
|
|
4249
4186
|
write(" → toolroll incident resolve <id> --as <you> --token <t>");
|
|
4250
4187
|
return EXIT.refused;
|
|
4251
4188
|
}
|
|
4252
|
-
if (action !== "resolve")
|
|
4189
|
+
if (action !== "resolve")
|
|
4253
4190
|
return fail(write, json, "incident", "usage", "`toolroll incident [list|resolve <id>]`", EXIT.usage);
|
|
4254
|
-
}
|
|
4255
4191
|
const id = Number(idText);
|
|
4256
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
4192
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
4257
4193
|
return fail(write, json, "incident resolve", "usage", "`toolroll incident resolve <id> --as <you> --token <t>`", EXIT.usage);
|
|
4258
|
-
}
|
|
4259
4194
|
const acting = await askCredentials(flags, context);
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4195
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4196
|
+
if (acting === null || !verified?.ok)
|
|
4197
|
+
return refuseApprover(context, "incident resolve", verified, "resolving takes `--as <you> --token <t>` — who looked is recorded, not asserted", String(id));
|
|
4263
4198
|
const { name: asWho, token } = acting;
|
|
4264
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4265
|
-
if (!authenticated.ok) {
|
|
4266
|
-
return fail(write, json, "incident resolve", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
4267
|
-
}
|
|
4268
4199
|
const resolved = store.resolveIncident(id, asWho, clock());
|
|
4269
|
-
if (!resolved)
|
|
4200
|
+
if (!resolved)
|
|
4270
4201
|
return fail(write, json, "incident resolve", "unknown-or-resolved", `no unresolved incident ${id}`, EXIT.refused);
|
|
4271
|
-
}
|
|
4272
4202
|
return succeed(write, json, "incident resolve", { id, by: asWho }, () => [
|
|
4273
4203
|
`Resolved incident ${id}, as ${asWho}. The task's hold is lifted; the next tick may take it.`,
|
|
4274
4204
|
]);
|
|
@@ -4302,21 +4232,18 @@ async function webhookCommand(positional, _flags, context) {
|
|
|
4302
4232
|
return EXIT.ok;
|
|
4303
4233
|
}
|
|
4304
4234
|
if (action === "primary") {
|
|
4305
|
-
if (which === undefined || !isMessagingChannel(which))
|
|
4235
|
+
if (which === undefined || !isMessagingChannel(which))
|
|
4306
4236
|
return fail(write, json, "webhook primary", "usage", "primary is one of telegram, slack, discord", EXIT.usage);
|
|
4307
|
-
}
|
|
4308
4237
|
const primary = effectivePrimary(process.env, dir, telegramConfigured);
|
|
4309
|
-
if (!primary.configured.includes(which))
|
|
4238
|
+
if (!primary.configured.includes(which))
|
|
4310
4239
|
return fail(write, json, "webhook primary", "unconfigured", `${which} is not configured — set it up first, then choose it`, EXIT.refused);
|
|
4311
|
-
}
|
|
4312
4240
|
savePrimary(dir, which);
|
|
4313
4241
|
return succeed(write, json, "webhook primary", { primary: which }, () => [
|
|
4314
4242
|
`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."}`,
|
|
4315
4243
|
]);
|
|
4316
4244
|
}
|
|
4317
|
-
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4245
|
+
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4318
4246
|
return fail(write, json, "webhook", "usage", "`toolroll webhook [status|primary <service>|set console-url <url>]`", EXIT.usage);
|
|
4319
|
-
}
|
|
4320
4247
|
const saved = saveConsoleUrl(dir, value);
|
|
4321
4248
|
if (!saved.ok)
|
|
4322
4249
|
return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
|
|
@@ -4332,9 +4259,8 @@ function resumeProviderCommand(positional, flags, context) {
|
|
|
4332
4259
|
const { store, write, json } = context;
|
|
4333
4260
|
const command = "providers resume";
|
|
4334
4261
|
const provider = positional[0];
|
|
4335
|
-
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4262
|
+
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4336
4263
|
return fail(write, json, command, "usage", `Use \`toolroll providers resume <${PROVIDER_IDS.join("|")}>\`.`, EXIT.usage);
|
|
4337
|
-
}
|
|
4338
4264
|
const who = text(flags, "as") ?? process.env["USER"] ?? "operator";
|
|
4339
4265
|
const lifted = liftAuthPause(store, provider, "person", who, context.clock());
|
|
4340
4266
|
if (lifted === null)
|
|
@@ -4428,14 +4354,12 @@ async function providersCommand(flags, context) {
|
|
|
4428
4354
|
if (flags.has("report")) {
|
|
4429
4355
|
const runnerName = text(flags, "runner");
|
|
4430
4356
|
const runnerToken = text(flags, "token");
|
|
4431
|
-
if (runnerName === undefined || runnerToken === undefined)
|
|
4357
|
+
if (runnerName === undefined || runnerToken === undefined)
|
|
4432
4358
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4433
|
-
}
|
|
4434
4359
|
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));
|
|
4435
4360
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4436
|
-
if (!reported.ok)
|
|
4361
|
+
if (!reported.ok)
|
|
4437
4362
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
4438
|
-
}
|
|
4439
4363
|
recorded = observed;
|
|
4440
4364
|
}
|
|
4441
4365
|
if (json) {
|
|
@@ -4582,9 +4506,8 @@ async function agentsCommand(positional, flags, context) {
|
|
|
4582
4506
|
async function keysCommand(positional, flags, context) {
|
|
4583
4507
|
const { write, json } = context;
|
|
4584
4508
|
const [action, provider, ...rest] = positional;
|
|
4585
|
-
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4509
|
+
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4586
4510
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4587
|
-
}
|
|
4588
4511
|
if (action === "status") {
|
|
4589
4512
|
// ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
|
|
4590
4513
|
const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
|
|
@@ -4610,12 +4533,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4610
4533
|
write(" → `keys auth <provider> subscription|api-key` switches; stored keys are kept either way");
|
|
4611
4534
|
return EXIT.ok;
|
|
4612
4535
|
}
|
|
4613
|
-
if (provider === undefined || !isProviderId(provider))
|
|
4536
|
+
if (provider === undefined || !isProviderId(provider))
|
|
4614
4537
|
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
|
|
4615
|
-
|
|
4616
|
-
if (!isKeyedProvider(provider)) {
|
|
4538
|
+
if (!isKeyedProvider(provider))
|
|
4617
4539
|
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);
|
|
4618
|
-
}
|
|
4619
4540
|
if (action === "clear") {
|
|
4620
4541
|
const cleared = clearProviderKey(provider);
|
|
4621
4542
|
const mode = readAuthMode(provider);
|
|
@@ -4629,21 +4550,18 @@ async function keysCommand(positional, flags, context) {
|
|
|
4629
4550
|
}
|
|
4630
4551
|
if (action === "verify") {
|
|
4631
4552
|
const stored = readProviderKey(provider);
|
|
4632
|
-
if (stored === null)
|
|
4553
|
+
if (stored === null)
|
|
4633
4554
|
return fail(write, json, "keys verify", "refused", `no stored ${provider} key to verify`, EXIT.refused);
|
|
4634
|
-
}
|
|
4635
4555
|
const verdict = await verifyProviderKey(provider, stored);
|
|
4636
4556
|
return succeed(write, json, "keys verify", { provider, verdict }, () => [verdictWords(provider, verdict)]);
|
|
4637
4557
|
}
|
|
4638
4558
|
if (action === "auth") {
|
|
4639
4559
|
const [wanted] = rest;
|
|
4640
|
-
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4560
|
+
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4641
4561
|
return fail(write, json, "keys auth", "usage", `\`toolroll keys auth ${provider} subscription|api-key\``, EXIT.usage);
|
|
4642
|
-
}
|
|
4643
4562
|
const set = setAuthMode(provider, wanted);
|
|
4644
|
-
if (!set.ok)
|
|
4563
|
+
if (!set.ok)
|
|
4645
4564
|
return fail(write, json, "keys auth", "refused", `${provider} has no subscription login — it is api-key only`, EXIT.refused);
|
|
4646
|
-
}
|
|
4647
4565
|
return succeed(write, json, "keys auth", { provider, mode: wanted }, () => [
|
|
4648
4566
|
wanted === "subscription"
|
|
4649
4567
|
? `${provider} now uses its own login; its stored key is kept as the fallback you can switch to.`
|
|
@@ -4661,18 +4579,16 @@ async function keysCommand(positional, flags, context) {
|
|
|
4661
4579
|
}
|
|
4662
4580
|
}
|
|
4663
4581
|
else {
|
|
4664
|
-
if (process.stdin.isTTY)
|
|
4582
|
+
if (process.stdin.isTTY)
|
|
4665
4583
|
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);
|
|
4666
|
-
}
|
|
4667
4584
|
const chunks = [];
|
|
4668
4585
|
for await (const chunk of process.stdin)
|
|
4669
4586
|
chunks.push(chunk);
|
|
4670
4587
|
value = Buffer.concat(chunks).toString("utf8");
|
|
4671
4588
|
}
|
|
4672
4589
|
const saved = saveProviderKey(provider, value);
|
|
4673
|
-
if (!saved.ok)
|
|
4590
|
+
if (!saved.ok)
|
|
4674
4591
|
return fail(write, json, "keys set", "implausible", "that does not look like an API key — check the paste", EXIT.refused);
|
|
4675
|
-
}
|
|
4676
4592
|
// Save is instant (shape-checked); verification is a live credential
|
|
4677
4593
|
// check that spends no tokens — skip it with --no-verify for an offline
|
|
4678
4594
|
// machine or a provider you cannot reach right now.
|
|
@@ -4692,9 +4608,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4692
4608
|
async function peopleCommand(positional, flags, context) {
|
|
4693
4609
|
const { store, write, json, clock } = context;
|
|
4694
4610
|
const [action, ...rest] = positional;
|
|
4695
|
-
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4611
|
+
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4696
4612
|
return fail(write, json, "people", "usage", `unknown \`people ${action ?? ""}\` — try ${PEOPLE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4697
|
-
}
|
|
4698
4613
|
if (action === "list") {
|
|
4699
4614
|
const accounts = store.accountFacts();
|
|
4700
4615
|
const invites = store.openInvites(clock());
|
|
@@ -4716,14 +4631,12 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4716
4631
|
return EXIT.ok;
|
|
4717
4632
|
}
|
|
4718
4633
|
const acting = await askCredentials(flags, context);
|
|
4719
|
-
if (acting === null)
|
|
4634
|
+
if (acting === null)
|
|
4720
4635
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4721
|
-
}
|
|
4722
4636
|
// Inviting and widening access are credential acts; revoking only narrows.
|
|
4723
4637
|
const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
4724
|
-
if (!authenticated.ok)
|
|
4638
|
+
if (!authenticated.ok)
|
|
4725
4639
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4726
|
-
}
|
|
4727
4640
|
const repoGiven = text(flags, "repo");
|
|
4728
4641
|
const allProjects = flags.has("all-projects");
|
|
4729
4642
|
const noProjects = flags.has("no-projects");
|
|
@@ -4744,9 +4657,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4744
4657
|
}
|
|
4745
4658
|
if (action === "invite") {
|
|
4746
4659
|
const roleFlag = text(flags, "role") ?? "viewer";
|
|
4747
|
-
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4660
|
+
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4748
4661
|
return fail(write, json, "people invite", "usage", "--role viewer|approver (viewer is the default)", EXIT.usage);
|
|
4749
|
-
}
|
|
4750
4662
|
if (noProjects)
|
|
4751
4663
|
return fail(write, json, "people invite", "usage", "An invitation needs --repo <path> or all-project access.", EXIT.usage);
|
|
4752
4664
|
const minted = store.mintInvite(roleFlag, acting.name, clock(), undefined, projects);
|
|
@@ -4757,9 +4669,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4757
4669
|
]);
|
|
4758
4670
|
}
|
|
4759
4671
|
const [name] = rest;
|
|
4760
|
-
if (name === undefined || name.trim() === "")
|
|
4672
|
+
if (name === undefined || name.trim() === "")
|
|
4761
4673
|
return fail(write, json, "people revoke", "usage", "`toolroll people revoke <name> --as <you> --token <t>`", EXIT.usage);
|
|
4762
|
-
}
|
|
4763
4674
|
const severed = store.revokeAccount(name.trim(), acting.name, clock());
|
|
4764
4675
|
if (!severed.ok) {
|
|
4765
4676
|
const words = severed.reason === "last-approver"
|
|
@@ -4824,16 +4735,14 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4824
4735
|
async function modeCommand(positional, flags, context) {
|
|
4825
4736
|
const { store, write, json, clock } = context;
|
|
4826
4737
|
const [action] = positional;
|
|
4827
|
-
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4738
|
+
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4828
4739
|
return fail(write, json, "mode", "usage", `unknown \`mode ${action ?? ""}\` — try ${MODE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4829
|
-
}
|
|
4830
4740
|
const repoGiven = text(flags, "repo");
|
|
4831
4741
|
// The same canonical string every other road stores for the repository
|
|
4832
4742
|
// (a symlinked path — /var vs /private/var — must name the same mode).
|
|
4833
4743
|
const repo = repoGiven === undefined ? undefined : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4834
|
-
if (repo === undefined)
|
|
4744
|
+
if (repo === undefined)
|
|
4835
4745
|
return fail(write, json, "mode", "usage", "`mode` needs --repo <canonical path> — a mode is per-repository", EXIT.usage);
|
|
4836
|
-
}
|
|
4837
4746
|
const now = clock();
|
|
4838
4747
|
if (action === "show") {
|
|
4839
4748
|
const mode = store.activeMode(repo, now);
|
|
@@ -4848,14 +4757,12 @@ async function modeCommand(positional, flags, context) {
|
|
|
4848
4757
|
]);
|
|
4849
4758
|
}
|
|
4850
4759
|
const acting = await askCredentials(flags, context);
|
|
4851
|
-
if (acting === null)
|
|
4760
|
+
if (acting === null)
|
|
4852
4761
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4853
|
-
}
|
|
4854
4762
|
// Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
|
|
4855
4763
|
const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4856
|
-
if (!authed.ok)
|
|
4764
|
+
if (!authed.ok)
|
|
4857
4765
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4858
|
-
}
|
|
4859
4766
|
if (action === "revoke") {
|
|
4860
4767
|
// Lowering authority is one click for ANY approver (v4 doctrine).
|
|
4861
4768
|
const revoked = store.revokeMode(repo, acting.name, "operator", now);
|
|
@@ -4865,13 +4772,11 @@ async function modeCommand(positional, flags, context) {
|
|
|
4865
4772
|
}
|
|
4866
4773
|
// set: RAISING authority — the password ceremony above already ran.
|
|
4867
4774
|
const nameGiven = text(flags, "name") ?? "standard";
|
|
4868
|
-
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4775
|
+
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4869
4776
|
return fail(write, json, "mode set", "usage", "--name is standard or hands-off", EXIT.usage);
|
|
4870
|
-
}
|
|
4871
4777
|
const daysGiven = Number(text(flags, "days") ?? "1");
|
|
4872
|
-
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4778
|
+
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4873
4779
|
return fail(write, json, "mode set", "usage", `--days is 1 to ${MODE_MAX_DAYS} (a mode always expires)`, EXIT.usage);
|
|
4874
|
-
}
|
|
4875
4780
|
const expiry = new Date(now.getTime() + daysGiven * 24 * 60 * 60_000).toISOString();
|
|
4876
4781
|
const terms = presetTerms(nameGiven, expiry);
|
|
4877
4782
|
// Explicit term overrides ride the same flags (all optional):
|
|
@@ -4883,12 +4788,10 @@ async function modeCommand(positional, flags, context) {
|
|
|
4883
4788
|
terms.autoApproveFiling = true;
|
|
4884
4789
|
if (flag(flags, "plan-auto"))
|
|
4885
4790
|
terms.planAuto = true;
|
|
4886
|
-
if (["review-auto", "review-retry-auto", "repair-auto", "repair-max-attempts"].some(name => flags.has(name)))
|
|
4791
|
+
if (["review-auto", "review-retry-auto", "repair-auto", "repair-max-attempts"].some(name => flags.has(name)))
|
|
4887
4792
|
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);
|
|
4888
|
-
|
|
4889
|
-
if (terms.planAuto && !terms.autoApproveFiling) {
|
|
4793
|
+
if (terms.planAuto && !terms.autoApproveFiling)
|
|
4890
4794
|
return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
|
|
4891
|
-
}
|
|
4892
4795
|
// Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
|
|
4893
4796
|
// The signature names every chat app it covers: a grant signed before that named Telegram alone, and stays so.
|
|
4894
4797
|
if (flag(flags, "chat-approve")) {
|
|
@@ -4896,9 +4799,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4896
4799
|
terms.chatApproveChats = CHAT_APPROVE_ALL;
|
|
4897
4800
|
}
|
|
4898
4801
|
// Automerge requires a live merge-capable grant on the repo (D1).
|
|
4899
|
-
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4802
|
+
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4900
4803
|
return fail(write, json, "mode set", "no-grant", "automerge needs a merge-capable publication grant on this repo — file one first", EXIT.refused);
|
|
4901
|
-
}
|
|
4902
4804
|
const digest = modeDigestOf(terms);
|
|
4903
4805
|
const id = store.signMode({ repo, name: nameGiven, termsJson: modeTermsJson(terms), digest, signedBy: acting.name, absoluteExpiry: expiry, publication: terms.publication }, now);
|
|
4904
4806
|
return succeed(write, json, "mode set", { repo, id, digest, terms }, () => [
|
|
@@ -4909,9 +4811,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4909
4811
|
async function configCommand(positional, flags, context) {
|
|
4910
4812
|
const { store, write, json, clock } = context;
|
|
4911
4813
|
const [action, phase] = positional;
|
|
4912
|
-
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4814
|
+
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4913
4815
|
return fail(write, json, "config", "usage", `unknown \`config ${action}\` — try ${CONFIG_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4914
|
-
}
|
|
4915
4816
|
const repoGiven = text(flags, "repo");
|
|
4916
4817
|
const scope = repoGiven === undefined ? INSTALLATION_SCOPE : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4917
4818
|
if (action === undefined || action === "show") {
|
|
@@ -4980,21 +4881,18 @@ async function configCommand(positional, flags, context) {
|
|
|
4980
4881
|
}
|
|
4981
4882
|
return EXIT.ok;
|
|
4982
4883
|
}
|
|
4983
|
-
if (action !== "set" && action !== "clear")
|
|
4884
|
+
if (action !== "set" && action !== "clear")
|
|
4984
4885
|
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);
|
|
4985
|
-
}
|
|
4986
4886
|
// Global dollar thresholds (v15, operator request): defaults for filings
|
|
4987
4887
|
// and an installation-wide backstop. Authenticated like every spend
|
|
4988
4888
|
// routing act; the per-task digest stays the authority.
|
|
4989
4889
|
if (phase === "budgets") {
|
|
4990
4890
|
const acting = await askCredentials(flags, context);
|
|
4991
|
-
if (acting === null)
|
|
4891
|
+
if (acting === null)
|
|
4992
4892
|
return fail(write, json, `config ${action}`, "usage", "changing spend defaults takes `--as <you> --token <t>`", EXIT.usage);
|
|
4993
|
-
}
|
|
4994
4893
|
const authedBudget = authenticateApprover(store, acting.name, acting.token);
|
|
4995
|
-
if (!authedBudget.ok)
|
|
4894
|
+
if (!authedBudget.ok)
|
|
4996
4895
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
4997
|
-
}
|
|
4998
4896
|
if (action === "clear") {
|
|
4999
4897
|
store.setSpendDefaults({ buildPerRunMicrousd: null }, acting.name, clock());
|
|
5000
4898
|
return succeed(write, json, "config clear", { budgets: null }, () => ["Spend defaults cleared — filings state their own numbers again."]);
|
|
@@ -5007,9 +4905,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5007
4905
|
return Number.isFinite(value) && value > 0 ? Math.round(value * 1_000_000) : false;
|
|
5008
4906
|
};
|
|
5009
4907
|
const build = parseUsd("build-usd");
|
|
5010
|
-
if (build === false)
|
|
4908
|
+
if (build === false)
|
|
5011
4909
|
return fail(write, json, "config set", "usage", "--build-usd is a positive dollar amount", EXIT.usage);
|
|
5012
|
-
}
|
|
5013
4910
|
store.setSpendDefaults({ buildPerRunMicrousd: build }, acting.name, clock());
|
|
5014
4911
|
return succeed(write, json, "config set", { budgets: store.getSpendDefaults() }, () => [
|
|
5015
4912
|
"Spend defaults set. New filings pre-fill from these; every approval still restates its own numbers:",
|
|
@@ -5021,17 +4918,14 @@ async function configCommand(positional, flags, context) {
|
|
|
5021
4918
|
// Installation-scoped only — a per-repo chat would splinter the spend
|
|
5022
4919
|
// ceiling that makes the ledger a ceiling at all.
|
|
5023
4920
|
if (phase === "chat") {
|
|
5024
|
-
if (repoGiven !== undefined)
|
|
4921
|
+
if (repoGiven !== undefined)
|
|
5025
4922
|
return fail(write, json, `config ${action}`, "usage", "chat is installation-scoped — no --repo", EXIT.usage);
|
|
5026
|
-
}
|
|
5027
4923
|
const acting = await askCredentials(flags, context);
|
|
5028
|
-
if (acting === null)
|
|
4924
|
+
if (acting === null)
|
|
5029
4925
|
return fail(write, json, `config ${action}`, "usage", "changing chat spend takes `--as <you> --token <t>`", EXIT.usage);
|
|
5030
|
-
}
|
|
5031
4926
|
const authedChat = authenticateApprover(store, acting.name, acting.token);
|
|
5032
|
-
if (!authedChat.ok)
|
|
4927
|
+
if (!authedChat.ok)
|
|
5033
4928
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
5034
|
-
}
|
|
5035
4929
|
if (action === "clear") {
|
|
5036
4930
|
store.clearChatConfig();
|
|
5037
4931
|
return succeed(write, json, "config clear", { chat: null }, () => ["Chat is off — the config row is gone."]);
|
|
@@ -5040,31 +4934,25 @@ async function configCommand(positional, flags, context) {
|
|
|
5040
4934
|
const requestedModel = text(flags, "model");
|
|
5041
4935
|
const weeklyUsd = text(flags, "weekly-usd");
|
|
5042
4936
|
const dailyGiven = text(flags, "daily-turns");
|
|
5043
|
-
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription")
|
|
4937
|
+
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription")
|
|
5044
4938
|
return fail(write, json, "config set", "usage", "chat provider is one of claude-subscription, codex-subscription, anthropic-api, openrouter-api", EXIT.usage);
|
|
5045
|
-
}
|
|
5046
4939
|
const model = requestedModel ?? (isSubscriptionChatProvider(provider) ? "default" : "");
|
|
5047
|
-
if (!validModelId(model))
|
|
4940
|
+
if (!validModelId(model))
|
|
5048
4941
|
return fail(write, json, "config set", "usage", "--model must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes", EXIT.usage);
|
|
5049
|
-
}
|
|
5050
4942
|
const priced = isDirectChatProvider(provider) ? livePin(store, provider, model) ?? priceOf(model) : null;
|
|
5051
|
-
if (isDirectChatProvider(provider) && priced === null)
|
|
4943
|
+
if (isDirectChatProvider(provider) && priced === null)
|
|
5052
4944
|
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);
|
|
5053
|
-
}
|
|
5054
4945
|
const weekly = Number(weeklyUsd);
|
|
5055
|
-
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
4946
|
+
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
5056
4947
|
return fail(write, json, "config set", "usage", "--weekly-usd <dollars> is required — chat without a ceiling is not configured, it is unbounded", EXIT.usage);
|
|
5057
|
-
}
|
|
5058
4948
|
const daily = dailyGiven === undefined ? 50 : Number(dailyGiven);
|
|
5059
|
-
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
4949
|
+
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
5060
4950
|
return fail(write, json, "config set", "usage", "--daily-turns is a whole number between 1 and 1000", EXIT.usage);
|
|
5061
|
-
}
|
|
5062
4951
|
// The CLI pins from the compiled table (the console additionally offers
|
|
5063
4952
|
// OpenRouter's live catalog — priced by the party that bills it).
|
|
5064
4953
|
const pinned = isDirectChatProvider(provider) ? priced : { inMicrousd: 0, outMicrousd: 0 };
|
|
5065
|
-
if (pinned === null)
|
|
4954
|
+
if (pinned === null)
|
|
5066
4955
|
return fail(write, json, "config set", "unpriced-model", `no compiled price for ${model}`, EXIT.refused);
|
|
5067
|
-
}
|
|
5068
4956
|
store.setChatConfig({
|
|
5069
4957
|
provider,
|
|
5070
4958
|
model,
|
|
@@ -5085,42 +4973,33 @@ async function configCommand(positional, flags, context) {
|
|
|
5085
4973
|
: []),
|
|
5086
4974
|
]);
|
|
5087
4975
|
}
|
|
5088
|
-
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
4976
|
+
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5089
4977
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5090
|
-
}
|
|
5091
4978
|
const acting = await askCredentials(flags, context);
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5096
|
-
if (!authenticated.ok) {
|
|
5097
|
-
return fail(write, json, `config ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, phase), EXIT.refused);
|
|
5098
|
-
}
|
|
4979
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
4980
|
+
if (acting === null || !verified?.ok)
|
|
4981
|
+
return refuseApprover(context, `config ${action}`, verified, "changing spend routing takes `--as <you> --token <t>`", phase);
|
|
5099
4982
|
// The STRONG tier (v47): a second, named row per phase the routing policy
|
|
5100
4983
|
// reaches for when size, quality, evidence, or publication demand it.
|
|
5101
4984
|
// Authenticated and audited exactly like the routine row; existing
|
|
5102
4985
|
// approvals are untouched — a sealed route never re-resolves.
|
|
5103
4986
|
// The LIGHT tier (v2): the fast agent a small change builds on.
|
|
5104
4987
|
const tierGiven = text(flags, "tier");
|
|
5105
|
-
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
4988
|
+
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
5106
4989
|
return fail(write, json, `config ${action}`, "usage", "--tier is `light` or `strong` — the routine tier is the plain phase row", EXIT.usage);
|
|
5107
|
-
}
|
|
5108
4990
|
// A light planner would never run (a small change makes no plan): refused rather than kept and ignored.
|
|
5109
4991
|
// Clearing one left from before stays possible.
|
|
5110
|
-
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
4992
|
+
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5111
4993
|
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);
|
|
5112
|
-
}
|
|
5113
4994
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5114
4995
|
// has more room runs.
|
|
5115
4996
|
if (flag(flags, "also")) {
|
|
5116
|
-
if (phase !== "plan" && phase !== "build")
|
|
4997
|
+
if (phase !== "plan" && phase !== "build")
|
|
5117
4998
|
return fail(write, json, `config ${action}`, "usage", "--also is for plan or build — repairs follow the build's provider", EXIT.usage);
|
|
5118
|
-
}
|
|
5119
4999
|
const tier = (tierGiven ?? "routine");
|
|
5120
5000
|
const alsoProvider = text(flags, "provider");
|
|
5121
|
-
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5001
|
+
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5122
5002
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5123
|
-
}
|
|
5124
5003
|
if (action === "clear") {
|
|
5125
5004
|
const removed = store.clearPhaseTierAlternates(scope, phase, tier, alsoProvider ?? null);
|
|
5126
5005
|
return succeed(write, json, "config clear", { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
@@ -5128,9 +5007,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5128
5007
|
]);
|
|
5129
5008
|
}
|
|
5130
5009
|
const alsoModel = text(flags, "model");
|
|
5131
|
-
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5010
|
+
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5132
5011
|
return fail(write, json, "config set", "usage", "--also names an exact --provider and --model — approvals bind exact routing", EXIT.usage);
|
|
5133
|
-
}
|
|
5134
5012
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5135
5013
|
if (!alsoValid.ok)
|
|
5136
5014
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
@@ -5163,18 +5041,15 @@ async function configCommand(positional, flags, context) {
|
|
|
5163
5041
|
]);
|
|
5164
5042
|
}
|
|
5165
5043
|
const providerGiven = text(flags, "provider");
|
|
5166
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5044
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5167
5045
|
return fail(write, json, "config set", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5168
|
-
}
|
|
5169
5046
|
const modelGiven = text(flags, "model") ?? null;
|
|
5170
5047
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
5171
|
-
if (!valid.ok)
|
|
5048
|
+
if (!valid.ok)
|
|
5172
5049
|
return fail(write, json, "config set", "invalid", valid.problem, EXIT.usage);
|
|
5173
|
-
}
|
|
5174
5050
|
if (tierGiven === "light") {
|
|
5175
|
-
if (modelGiven === null)
|
|
5051
|
+
if (modelGiven === null)
|
|
5176
5052
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5177
|
-
}
|
|
5178
5053
|
store.setPhaseTierConfig(scope, phase, "light", providerGiven, modelGiven, acting.name, clock());
|
|
5179
5054
|
return succeed(write, json, "config set", { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5180
5055
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
@@ -5182,12 +5057,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5182
5057
|
]);
|
|
5183
5058
|
}
|
|
5184
5059
|
if (tierGiven === "strong") {
|
|
5185
|
-
if (phase === "review" && providerGiven === "gemini")
|
|
5060
|
+
if (phase === "review" && providerGiven === "gemini")
|
|
5186
5061
|
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);
|
|
5187
|
-
|
|
5188
|
-
if (modelGiven === null) {
|
|
5062
|
+
if (modelGiven === null)
|
|
5189
5063
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5190
|
-
}
|
|
5191
5064
|
store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
|
|
5192
5065
|
return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5193
5066
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
@@ -5226,9 +5099,8 @@ async function setupCommand(positional, flags, context) {
|
|
|
5226
5099
|
const action = positional[0] ?? "show";
|
|
5227
5100
|
const repo = text(flags, "repo");
|
|
5228
5101
|
if (action === "show") {
|
|
5229
|
-
if (repo === undefined)
|
|
5102
|
+
if (repo === undefined)
|
|
5230
5103
|
return fail(write, json, "setup show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5231
|
-
}
|
|
5232
5104
|
const live = store.liveWorktreeSetup(repo);
|
|
5233
5105
|
if (json) {
|
|
5234
5106
|
write(envelopeJson({ ok: true, command: "setup show", repo, setup: live }));
|
|
@@ -5239,20 +5111,14 @@ async function setupCommand(positional, flags, context) {
|
|
|
5239
5111
|
: `${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}`);
|
|
5240
5112
|
return EXIT.ok;
|
|
5241
5113
|
}
|
|
5242
|
-
if (action !== "set" && action !== "clear")
|
|
5114
|
+
if (action !== "set" && action !== "clear")
|
|
5243
5115
|
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);
|
|
5244
|
-
|
|
5245
|
-
if (repo === undefined) {
|
|
5116
|
+
if (repo === undefined)
|
|
5246
5117
|
return fail(write, json, `setup ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5247
|
-
}
|
|
5248
5118
|
const acting = await askCredentials(flags, context);
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5253
|
-
if (!authenticated.ok) {
|
|
5254
|
-
return fail(write, json, `setup ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5255
|
-
}
|
|
5119
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5120
|
+
if (acting === null || !verified?.ok)
|
|
5121
|
+
return refuseApprover(context, `setup ${action}`, verified, "an approved setup runs unattended in every future worktree — changing it takes `--as <you> --token <t>`", repo);
|
|
5256
5122
|
if (action === "clear") {
|
|
5257
5123
|
const cleared = store.clearWorktreeSetup(repo, acting.name, clock());
|
|
5258
5124
|
return succeed(write, json, "setup clear", { repo, cleared }, () => [
|
|
@@ -5260,25 +5126,21 @@ async function setupCommand(positional, flags, context) {
|
|
|
5260
5126
|
]);
|
|
5261
5127
|
}
|
|
5262
5128
|
const command = text(flags, "command");
|
|
5263
|
-
if (command === undefined || command.trim() === "")
|
|
5129
|
+
if (command === undefined || command.trim() === "")
|
|
5264
5130
|
return fail(write, json, "setup set", "usage", "--command <cmd> is what every fresh checkout will run", EXIT.usage);
|
|
5265
|
-
|
|
5266
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5131
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5267
5132
|
return fail(write, json, "setup set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5268
|
-
}
|
|
5269
5133
|
// Literal credentials never become standing rows (audit IV-5): a command
|
|
5270
5134
|
// that embeds a token is stored forever in plain text. Reference an
|
|
5271
5135
|
// environment variable the runner already exports instead. `$TOKEN` is
|
|
5272
5136
|
// a reference and passes; `=secret123` is a value and refuses.
|
|
5273
5137
|
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) ||
|
|
5274
5138
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5275
|
-
if (credentialShaped)
|
|
5139
|
+
if (credentialShaped)
|
|
5276
5140
|
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);
|
|
5277
|
-
}
|
|
5278
5141
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5279
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5142
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5280
5143
|
return fail(write, json, "setup set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5281
|
-
}
|
|
5282
5144
|
if (flags.get("yes") !== true) {
|
|
5283
5145
|
if (json) {
|
|
5284
5146
|
write(envelopeJson({ ok: false, command: "setup set", reason: "unconfirmed", repo, setupCommand: command, timeoutSeconds }));
|
|
@@ -5318,9 +5180,8 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5318
5180
|
const action = positional[0] ?? "show";
|
|
5319
5181
|
const repo = text(flags, "repo");
|
|
5320
5182
|
if (action === "show") {
|
|
5321
|
-
if (repo === undefined)
|
|
5183
|
+
if (repo === undefined)
|
|
5322
5184
|
return fail(write, json, "verify show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5323
|
-
}
|
|
5324
5185
|
const live = store.liveVerifyCommand(repo);
|
|
5325
5186
|
const quick = liveQuickCommand(store, repo);
|
|
5326
5187
|
const level = projectCheckLevel(store, repo);
|
|
@@ -5360,24 +5221,18 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5360
5221
|
...(wanted !== "full" ? ["The full check still runs when a pull request opens, and Merge waits for it unless a person merges anyway."] : []),
|
|
5361
5222
|
]);
|
|
5362
5223
|
}
|
|
5363
|
-
if (action !== "set" && action !== "clear")
|
|
5224
|
+
if (action !== "set" && action !== "clear")
|
|
5364
5225
|
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);
|
|
5365
|
-
}
|
|
5366
5226
|
// The quick command is its own approved grant beside the full one.
|
|
5367
5227
|
const quickValue = text(flags, "quick");
|
|
5368
5228
|
const quickClear = action === "clear" && text(flags, "level") === "quick";
|
|
5369
5229
|
const grantKey = repo === undefined ? undefined : quickValue !== undefined || quickClear ? quickVerifyKey(repo) : repo;
|
|
5370
|
-
if (repo === undefined)
|
|
5230
|
+
if (repo === undefined)
|
|
5371
5231
|
return fail(write, json, `verify ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5372
|
-
}
|
|
5373
5232
|
const acting = await askCredentials(flags, context);
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5378
|
-
if (!authenticated.ok) {
|
|
5379
|
-
return fail(write, json, `verify ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5380
|
-
}
|
|
5233
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5234
|
+
if (acting === null || !verified?.ok)
|
|
5235
|
+
return refuseApprover(context, `verify ${action}`, verified, "an approved verification command runs unattended after every future build — changing it takes `--as <you> --token <t>`", repo);
|
|
5381
5236
|
if (action === "clear") {
|
|
5382
5237
|
const cleared = store.clearVerifyCommand(grantKey, acting.name, clock());
|
|
5383
5238
|
if (quickClear)
|
|
@@ -5389,38 +5244,30 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5389
5244
|
]);
|
|
5390
5245
|
}
|
|
5391
5246
|
const command = quickValue ?? text(flags, "command");
|
|
5392
|
-
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5247
|
+
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5393
5248
|
return fail(write, json, "verify set", "usage", "give the quick command with --quick \"<cmd>\" or the full one with --command, not both", EXIT.usage);
|
|
5394
|
-
|
|
5395
|
-
if (quickValue !== undefined && flags.get("self-heal") === true) {
|
|
5249
|
+
if (quickValue !== undefined && flags.get("self-heal") === true)
|
|
5396
5250
|
return fail(write, json, "verify set", "usage", "--self-heal applies to the full command only", EXIT.usage);
|
|
5397
|
-
|
|
5398
|
-
if (command === undefined || command.trim() === "") {
|
|
5251
|
+
if (command === undefined || command.trim() === "")
|
|
5399
5252
|
return fail(write, json, "verify set", "usage", "--command <cmd> is what the plane re-runs after every commit", EXIT.usage);
|
|
5400
|
-
|
|
5401
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5253
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5402
5254
|
return fail(write, json, "verify set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5403
|
-
}
|
|
5404
5255
|
// Same rule as an approved setup command (audit IV-5): a command that
|
|
5405
5256
|
// embeds a literal credential is stored forever in plain text.
|
|
5406
5257
|
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) ||
|
|
5407
5258
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5408
|
-
if (credentialShaped)
|
|
5259
|
+
if (credentialShaped)
|
|
5409
5260
|
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);
|
|
5410
|
-
}
|
|
5411
5261
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5412
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5262
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5413
5263
|
return fail(write, json, "verify set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5414
|
-
}
|
|
5415
5264
|
const selfHeal = flags.get("self-heal") === true;
|
|
5416
5265
|
const expectedSetupDigest = text(flags, "setup-digest");
|
|
5417
|
-
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5266
|
+
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5418
5267
|
return fail(write, json, "verify set", "usage", "--setup-digest is only valid with --self-heal", EXIT.usage);
|
|
5419
|
-
}
|
|
5420
5268
|
const recoverySetup = selfHeal ? store.liveWorktreeSetup(repo) : null;
|
|
5421
|
-
if (selfHeal && recoverySetup === null)
|
|
5269
|
+
if (selfHeal && recoverySetup === null)
|
|
5422
5270
|
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);
|
|
5423
|
-
}
|
|
5424
5271
|
if (flags.get("yes") !== true) {
|
|
5425
5272
|
if (json) {
|
|
5426
5273
|
write(envelopeJson({
|
|
@@ -5464,12 +5311,10 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5464
5311
|
}
|
|
5465
5312
|
return EXIT.refused;
|
|
5466
5313
|
}
|
|
5467
|
-
if (selfHeal && expectedSetupDigest === undefined)
|
|
5314
|
+
if (selfHeal && expectedSetupDigest === undefined)
|
|
5468
5315
|
return fail(write, json, "verify set", "setup-digest-required", `confirm self-healing with --setup-digest ${recoverySetup?.digest} exactly as previewed`, EXIT.refused);
|
|
5469
|
-
|
|
5470
|
-
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest) {
|
|
5316
|
+
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest)
|
|
5471
5317
|
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);
|
|
5472
|
-
}
|
|
5473
5318
|
const saved = store.setVerifyCommand({
|
|
5474
5319
|
repo: grantKey,
|
|
5475
5320
|
command,
|
|
@@ -5535,13 +5380,9 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5535
5380
|
}
|
|
5536
5381
|
if (action === "grant" || action === "clear") {
|
|
5537
5382
|
const acting = await askCredentials(flags, context);
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5542
|
-
if (!authenticated.ok) {
|
|
5543
|
-
return fail(write, json, `intake ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5544
|
-
}
|
|
5383
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5384
|
+
if (acting === null || !verified?.ok)
|
|
5385
|
+
return refuseApprover(context, `intake ${action}`, verified, "an intake grant is standing authority — it takes `--as <you> --token <t>`", repo);
|
|
5545
5386
|
if (action === "clear") {
|
|
5546
5387
|
const cleared = store.clearIntakeGrant(repo, acting.name, clock());
|
|
5547
5388
|
return succeed(write, json, "intake clear", { repo, cleared }, () => [
|
|
@@ -5550,12 +5391,10 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5550
5391
|
}
|
|
5551
5392
|
const github = text(flags, "github");
|
|
5552
5393
|
const label = text(flags, "label");
|
|
5553
|
-
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5394
|
+
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5554
5395
|
return fail(write, json, "intake grant", "usage", "--github <owner/name> names the repository on GitHub", EXIT.usage);
|
|
5555
|
-
|
|
5556
|
-
if (label === undefined || !LABEL_SHAPE.test(label)) {
|
|
5396
|
+
if (label === undefined || !LABEL_SHAPE.test(label))
|
|
5557
5397
|
return fail(write, json, "intake grant", "usage", "--label <label> names the exact label that nominates an issue", EXIT.usage);
|
|
5558
|
-
}
|
|
5559
5398
|
const reviewersRaw = text(flags, "reviewers");
|
|
5560
5399
|
const reviewers = reviewersRaw === undefined
|
|
5561
5400
|
? null
|
|
@@ -5586,13 +5425,11 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5586
5425
|
`Granted: issues in ${github} labeled "${label}" become unapproved proposals for ${repo}.`,
|
|
5587
5426
|
]);
|
|
5588
5427
|
}
|
|
5589
|
-
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5428
|
+
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5590
5429
|
return fail(write, json, "intake", "usage", "`toolroll intake [show|grant|clear|preview|run|pr-comments] --repo <path> …`", EXIT.usage);
|
|
5591
|
-
}
|
|
5592
5430
|
const grant = store.liveIntakeGrant(repo);
|
|
5593
|
-
if (grant === null)
|
|
5431
|
+
if (grant === null)
|
|
5594
5432
|
return fail(write, json, `intake ${action}`, "no-grant", `no intake grant for ${repo} — \`toolroll intake grant\` states the terms`, EXIT.refused);
|
|
5595
|
-
}
|
|
5596
5433
|
if (action === "pr-comments") {
|
|
5597
5434
|
// PR review comments become LOCAL diff comments (M8.17): own PRs only
|
|
5598
5435
|
// (the publication table IS the list of ours), named reviewers only,
|
|
@@ -5600,9 +5437,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5600
5437
|
// shared validator. Nothing here authorizes a spawn — ingested
|
|
5601
5438
|
// comments wait on the run page for the same one-tap seal and the
|
|
5602
5439
|
// same scope approval as comments typed in the console.
|
|
5603
|
-
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5440
|
+
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5604
5441
|
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);
|
|
5605
|
-
}
|
|
5606
5442
|
const publications = store
|
|
5607
5443
|
.openedPublications()
|
|
5608
5444
|
.filter(one => one.prNumber !== null &&
|
|
@@ -5683,9 +5519,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5683
5519
|
}
|
|
5684
5520
|
const limit = Math.min(Math.max(Number(text(flags, "limit") ?? "50"), 1), 200);
|
|
5685
5521
|
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 });
|
|
5686
|
-
if (asked.code !== 0)
|
|
5522
|
+
if (asked.code !== 0)
|
|
5687
5523
|
return fail(write, json, `intake ${action}`, "github-unreachable", `gh could not list ${grant.github}: ${(asked.stderr.split("\n")[0] ?? "").slice(0, 200)}`, EXIT.failed);
|
|
5688
|
-
}
|
|
5689
5524
|
let issues;
|
|
5690
5525
|
try {
|
|
5691
5526
|
issues = JSON.parse(asked.stdout);
|
|
@@ -5806,16 +5641,13 @@ function templateCommand(positional, flags, context) {
|
|
|
5806
5641
|
write("`toolroll template show <name>` · `template apply <name> --repo <path>`");
|
|
5807
5642
|
return EXIT.ok;
|
|
5808
5643
|
}
|
|
5809
|
-
if (action !== "show" && action !== "apply")
|
|
5644
|
+
if (action !== "show" && action !== "apply")
|
|
5810
5645
|
return fail(write, json, `template ${action}`, "usage", "`toolroll template list | show <name> | apply <name> --repo <path> [--file]`", EXIT.usage);
|
|
5811
|
-
|
|
5812
|
-
if (name === undefined) {
|
|
5646
|
+
if (name === undefined)
|
|
5813
5647
|
return fail(write, json, `template ${action}`, "usage", "which template? `toolroll template list` names them", EXIT.usage);
|
|
5814
|
-
}
|
|
5815
5648
|
const template = templateByName(name);
|
|
5816
|
-
if (template === null)
|
|
5649
|
+
if (template === null)
|
|
5817
5650
|
return fail(write, json, `template ${action}`, "unknown", `no template named ${name} — \`toolroll template list\``, EXIT.refused);
|
|
5818
|
-
}
|
|
5819
5651
|
if (action === "show") {
|
|
5820
5652
|
if (json) {
|
|
5821
5653
|
write(envelopeJson({ ok: true, command: "template show", template }));
|
|
@@ -5856,13 +5688,11 @@ function templateCommand(positional, flags, context) {
|
|
|
5856
5688
|
return EXIT.ok;
|
|
5857
5689
|
}
|
|
5858
5690
|
// apply
|
|
5859
|
-
if (template.kind === "recipe")
|
|
5691
|
+
if (template.kind === "recipe")
|
|
5860
5692
|
return fail(write, json, "template apply", "recipe", `${template.name} cannot be applied: ${template.why} \`toolroll template show ${template.name}\` walks the ceremonies.`, EXIT.refused);
|
|
5861
|
-
}
|
|
5862
5693
|
const repoGiven = text(flags, "repo");
|
|
5863
|
-
if (repoGiven === undefined)
|
|
5694
|
+
if (repoGiven === undefined)
|
|
5864
5695
|
return fail(write, json, "template apply", "usage", "which repository? --repo <path> — a template never guesses where work lands", EXIT.usage);
|
|
5865
|
-
}
|
|
5866
5696
|
const goal = text(flags, "goal") ?? template.goal;
|
|
5867
5697
|
const outOfScope = text(flags, "not") ?? template.outOfScope;
|
|
5868
5698
|
const touches = text(flags, "touches") === undefined
|
|
@@ -6003,22 +5833,18 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6003
5833
|
const runnerName = text(flags, "runner");
|
|
6004
5834
|
const inlineToken = text(flags, "token");
|
|
6005
5835
|
const tokenPath = text(flags, "token-file");
|
|
6006
|
-
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
5836
|
+
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
6007
5837
|
return fail(write, json, "daemon install", "usage", "choose one credential source: --token or --token-file", EXIT.usage);
|
|
6008
|
-
}
|
|
6009
5838
|
const token = inlineToken ?? readTokenFile(tokenPath);
|
|
6010
|
-
if (runnerName === undefined || token === undefined)
|
|
5839
|
+
if (runnerName === undefined || token === undefined)
|
|
6011
5840
|
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);
|
|
6012
|
-
}
|
|
6013
5841
|
const auth = authenticate(store, runnerName, token);
|
|
6014
|
-
if (!auth.ok)
|
|
5842
|
+
if (!auth.ok)
|
|
6015
5843
|
return fail(write, json, "daemon install", auth.reason, describeAuth(auth.reason, runnerName), EXIT.refused);
|
|
6016
|
-
}
|
|
6017
5844
|
const heartbeatBefore = store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6018
5845
|
const located = await resolveBin();
|
|
6019
|
-
if (located === null)
|
|
5846
|
+
if (located === null)
|
|
6020
5847
|
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);
|
|
6021
|
-
}
|
|
6022
5848
|
const watchFlags = [];
|
|
6023
5849
|
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"]) {
|
|
6024
5850
|
const value = text(flags, name);
|
|
@@ -6055,15 +5881,13 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6055
5881
|
return EXIT.ok;
|
|
6056
5882
|
}
|
|
6057
5883
|
const installed = await installReplacingLegacy(plan, token, legacy, supervise);
|
|
6058
|
-
if (!installed.ok)
|
|
5884
|
+
if (!installed.ok)
|
|
6059
5885
|
return fail(write, json, "daemon install", "supervisor", installed.message, EXIT.failed);
|
|
6060
|
-
}
|
|
6061
5886
|
// The legacy watch beat for this runner until it stopped: only a beat after that proves the new one.
|
|
6062
5887
|
const readySince = legacy === null ? heartbeatBefore : store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6063
5888
|
const started = await daemonStatus(plan, supervise);
|
|
6064
|
-
if (started.state !== "running")
|
|
5889
|
+
if (started.state !== "running")
|
|
6065
5890
|
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);
|
|
6066
|
-
}
|
|
6067
5891
|
// A supervisor PID is necessary but not sufficient: macOS can leave a
|
|
6068
5892
|
// process stuck behind a protected-folder access check before it ever
|
|
6069
5893
|
// opens the queue. The credentialed runner heartbeat is the end-to-end
|
|
@@ -6713,15 +6537,13 @@ async function watchCommand(flags, context) {
|
|
|
6713
6537
|
return demoFence;
|
|
6714
6538
|
const runner = text(flags, "runner");
|
|
6715
6539
|
const token = text(flags, "token") ?? readTokenFile(text(flags, "token-file"));
|
|
6716
|
-
if (runner === undefined || token === undefined)
|
|
6540
|
+
if (runner === undefined || token === undefined)
|
|
6717
6541
|
return fail(write, json, "watch", "usage", "`toolroll watch --runner <name> --token <t>|--token-file <path> --repo <path> [--for <ms>]`", EXIT.usage);
|
|
6718
|
-
}
|
|
6719
6542
|
// Passes built from these flags authenticate with the resolved token.
|
|
6720
6543
|
flags.set("token", token);
|
|
6721
6544
|
const auth = authenticate(store, runner, token);
|
|
6722
|
-
if (!auth.ok)
|
|
6545
|
+
if (!auth.ok)
|
|
6723
6546
|
return fail(write, json, "watch", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
6724
|
-
}
|
|
6725
6547
|
const repo = repoFrom(flags);
|
|
6726
6548
|
// The envelope contract holds for long commands too (Codex M5-M8 audit,
|
|
6727
6549
|
// C-1): in --json mode every progress line goes to stderr, and stdout
|
|
@@ -7016,9 +6838,8 @@ async function onboardCommand(flags, context) {
|
|
|
7016
6838
|
const seams = context.onboardSeams ?? {};
|
|
7017
6839
|
const portGiven = text(flags, "port");
|
|
7018
6840
|
const port = Number(portGiven ?? 4180);
|
|
7019
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6841
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7020
6842
|
return fail(write, json, "onboard", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7021
|
-
}
|
|
7022
6843
|
// The console where `up` last served, unless --port says otherwise.
|
|
7023
6844
|
const served = portGiven === undefined ? readUpConsoleUrl(dirname(context.databaseFile)) : null;
|
|
7024
6845
|
const home = seams.home ?? homedir();
|
|
@@ -7111,32 +6932,26 @@ async function upCommand(flags, context) {
|
|
|
7111
6932
|
// 1. Validation, before anything at all.
|
|
7112
6933
|
const portGiven = text(flags, "port");
|
|
7113
6934
|
const port = Number(portGiven ?? 4180);
|
|
7114
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6935
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7115
6936
|
return fail(write, json, "up", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7116
|
-
}
|
|
7117
6937
|
const editorGiven = text(flags, "editor");
|
|
7118
|
-
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
6938
|
+
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
7119
6939
|
return fail(write, json, "up", "usage", "--editor supports: vscode", EXIT.usage);
|
|
7120
|
-
}
|
|
7121
6940
|
const forGiven = text(flags, "for");
|
|
7122
|
-
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
6941
|
+
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
7123
6942
|
return fail(write, json, "up", "usage", "--for takes a positive whole number of milliseconds", EXIT.usage);
|
|
7124
|
-
}
|
|
7125
6943
|
const capacityGiven = text(flags, "capacity");
|
|
7126
6944
|
const capacityFlag = capacityGiven === undefined ? null : parseRunnerCapacity(capacityGiven);
|
|
7127
|
-
if (capacityGiven !== undefined && capacityFlag === null)
|
|
6945
|
+
if (capacityGiven !== undefined && capacityFlag === null)
|
|
7128
6946
|
return fail(write, json, "up", "usage", `--capacity is a whole number of tasks, 1 to ${RUNNER_CAPACITY_MAX}`, EXIT.usage);
|
|
7129
|
-
}
|
|
7130
6947
|
const runnerFlag = text(flags, "runner");
|
|
7131
|
-
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
6948
|
+
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
7132
6949
|
return fail(write, json, "up", "usage", `a worker name is 1–${RUNNER_NAME_MAX} characters with no control characters`, EXIT.usage);
|
|
7133
|
-
}
|
|
7134
6950
|
// --host / --allow-host (setup review): `up` reaches a phone over a
|
|
7135
6951
|
// tailnet exactly as `serve` does, so one process is the whole install.
|
|
7136
6952
|
const hostFlag = text(flags, "host") ?? "127.0.0.1";
|
|
7137
|
-
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
6953
|
+
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
7138
6954
|
return fail(write, json, "up", "usage", "--host is an address or a name", EXIT.usage);
|
|
7139
|
-
}
|
|
7140
6955
|
const allowFlag = text(flags, "allow-host");
|
|
7141
6956
|
// --public-url: `up` serves a real domain exactly as `serve` does (one process is the whole team server).
|
|
7142
6957
|
const publicUrlFlag = text(flags, "public-url");
|
|
@@ -7154,9 +6969,8 @@ async function upCommand(flags, context) {
|
|
|
7154
6969
|
// person must repeat on every start.
|
|
7155
6970
|
const registryPath = registryPathOf(context);
|
|
7156
6971
|
const loadedRegistry = await loadProjectRegistry(registryPath);
|
|
7157
|
-
if ("error" in loadedRegistry)
|
|
6972
|
+
if ("error" in loadedRegistry)
|
|
7158
6973
|
return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
|
|
7159
|
-
}
|
|
7160
6974
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7161
6975
|
const rootInputs = (text(flags, "project-root") ?? "")
|
|
7162
6976
|
.split(",")
|
|
@@ -7165,9 +6979,8 @@ async function upCommand(flags, context) {
|
|
|
7165
6979
|
const explicitRoots = [];
|
|
7166
6980
|
for (const input of rootInputs) {
|
|
7167
6981
|
const canonical = canonicalProject(input);
|
|
7168
|
-
if (canonical === null)
|
|
6982
|
+
if (canonical === null)
|
|
7169
6983
|
return fail(write, json, "up", "project-root", `${input} is not a directory this machine can use`, EXIT.refused);
|
|
7170
|
-
}
|
|
7171
6984
|
if (!explicitRoots.includes(canonical))
|
|
7172
6985
|
explicitRoots.push(canonical);
|
|
7173
6986
|
}
|
|
@@ -7204,9 +7017,8 @@ async function upCommand(flags, context) {
|
|
|
7204
7017
|
if (explicitRepos.length > 0) {
|
|
7205
7018
|
for (const input of explicitRepos) {
|
|
7206
7019
|
const root = await proveRepo(input);
|
|
7207
|
-
if (root === null)
|
|
7020
|
+
if (root === null)
|
|
7208
7021
|
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);
|
|
7209
|
-
}
|
|
7210
7022
|
if (!repos.includes(root))
|
|
7211
7023
|
repos.push(root);
|
|
7212
7024
|
pinnedRepos.add(root);
|
|
@@ -7219,9 +7031,8 @@ async function upCommand(flags, context) {
|
|
|
7219
7031
|
if (cwdRepo !== null)
|
|
7220
7032
|
pinnedRepos.add(cwdRepo);
|
|
7221
7033
|
}
|
|
7222
|
-
if (repos.length === 0 && projectRoots.length === 0)
|
|
7034
|
+
if (repos.length === 0 && projectRoots.length === 0)
|
|
7223
7035
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
7224
|
-
}
|
|
7225
7036
|
// A person at a terminal reads three lines (onboarding); the folders are for --verbose, a service log, or an agent.
|
|
7226
7037
|
const quietStart = !json && !flags.has("verbose") && (context.upSeams?.terminal ?? process.stdout.isTTY === true) && !underAgent(context.upSeams?.env ?? process.env);
|
|
7227
7038
|
if (!quietStart) {
|
|
@@ -7237,9 +7048,8 @@ async function upCommand(flags, context) {
|
|
|
7237
7048
|
probe.once("error", () => resolveProbe(false));
|
|
7238
7049
|
probe.listen(port, "127.0.0.1", () => resolveProbe(true));
|
|
7239
7050
|
});
|
|
7240
|
-
if (!reserved)
|
|
7051
|
+
if (!reserved)
|
|
7241
7052
|
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);
|
|
7242
|
-
}
|
|
7243
7053
|
// Under a coding agent nothing is asked and no secret is printed, even in a real terminal: what is shown lands in
|
|
7244
7054
|
// its transcript.
|
|
7245
7055
|
const agentRunning = underAgent(context.upSeams?.env ?? process.env);
|
|
@@ -7250,9 +7060,8 @@ async function upCommand(flags, context) {
|
|
|
7250
7060
|
try {
|
|
7251
7061
|
// 4. The approver (findings 2/8/17/21/27/29/32).
|
|
7252
7062
|
const approverPlan = await resolveUpApprover(context, flags, canPrompt);
|
|
7253
|
-
if ("refusal" in approverPlan)
|
|
7063
|
+
if ("refusal" in approverPlan)
|
|
7254
7064
|
return fail(write, json, "up", "login", approverPlan.refusal, EXIT.refused);
|
|
7255
|
-
}
|
|
7256
7065
|
approver = approverPlan;
|
|
7257
7066
|
for (const note of approverPlan.notes)
|
|
7258
7067
|
progress(note);
|
|
@@ -7260,9 +7069,8 @@ async function upCommand(flags, context) {
|
|
|
7260
7069
|
// recovering runner authority on an UNVERIFIED operator identity was a
|
|
7261
7070
|
// registration road around the ceremony. No verified password, no
|
|
7262
7071
|
// runner — the login file or another approver restores the road.
|
|
7263
|
-
if (!approverPlan.verified)
|
|
7072
|
+
if (!approverPlan.verified)
|
|
7264
7073
|
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);
|
|
7265
|
-
}
|
|
7266
7074
|
// 5. The runner, through the atomic door (findings 1/16/26/31), with
|
|
7267
7075
|
// the suffix budget (finding 24) for generated names only.
|
|
7268
7076
|
const base = runnerFlag ?? normalizeRunnerName(hostname());
|
|
@@ -7747,9 +7555,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7747
7555
|
if (demoFence !== null)
|
|
7748
7556
|
return demoFence;
|
|
7749
7557
|
const [channel, action] = positional;
|
|
7750
|
-
if (channel !== "telegram")
|
|
7558
|
+
if (channel !== "telegram")
|
|
7751
7559
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status|digest|retry]`", EXIT.usage);
|
|
7752
|
-
}
|
|
7753
7560
|
if (action === "retry") {
|
|
7754
7561
|
// Replies already written and fenced on the pairing like every send: this only moves their next attempt to now.
|
|
7755
7562
|
const retried = store.retryTelegramReplies(null, clock());
|
|
@@ -7765,9 +7572,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7765
7572
|
removed ? "Token file removed." : "There was no token file to remove.",
|
|
7766
7573
|
]);
|
|
7767
7574
|
}
|
|
7768
|
-
if (value === undefined)
|
|
7575
|
+
if (value === undefined)
|
|
7769
7576
|
return fail(write, json, "bridge token", "usage", "`toolroll bridge telegram token <bot-token>` (from @BotFather), or --clear", EXIT.usage);
|
|
7770
|
-
}
|
|
7771
7577
|
const saved = saveBotToken(context.telegramTokenFile, value);
|
|
7772
7578
|
if (!saved.ok)
|
|
7773
7579
|
return fail(write, json, "bridge token", "bad-token", saved.message, EXIT.refused);
|
|
@@ -7794,13 +7600,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7794
7600
|
]);
|
|
7795
7601
|
}
|
|
7796
7602
|
const parsed = /^([1-9][0-9]{0,3})(m|h)$/.exec(every);
|
|
7797
|
-
if (parsed === null)
|
|
7603
|
+
if (parsed === null)
|
|
7798
7604
|
return fail(write, json, "bridge digest", "usage", "--every takes minutes or hours: 30m, 2h, 24h — or --off", EXIT.usage);
|
|
7799
|
-
}
|
|
7800
7605
|
const everyMs = Number(parsed[1]) * (parsed[2] === "h" ? 3_600_000 : 60_000);
|
|
7801
|
-
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7606
|
+
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7802
7607
|
return fail(write, json, "bridge digest", "usage", "the digest cadence is between 5 minutes and 7 days", EXIT.usage);
|
|
7803
|
-
}
|
|
7804
7608
|
store.setTelegramDigest(everyMs, "cli", clock());
|
|
7805
7609
|
return succeed(write, json, "bridge digest", { everyMs }, () => [
|
|
7806
7610
|
`Digest every ${digestWords(everyMs)}. Routine facts are held and sent together; decisions and anything that needs you now still page at once.`,
|
|
@@ -7835,18 +7639,15 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7835
7639
|
}
|
|
7836
7640
|
if (action === "pair" || action === "unpair") {
|
|
7837
7641
|
const acting = await askCredentials(flags, context);
|
|
7838
|
-
if (acting === null)
|
|
7642
|
+
if (acting === null)
|
|
7839
7643
|
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);
|
|
7840
|
-
}
|
|
7841
7644
|
const { name: asWho, token } = acting;
|
|
7842
7645
|
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7843
|
-
if (!authenticated.ok)
|
|
7646
|
+
if (!authenticated.ok)
|
|
7844
7647
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7845
|
-
}
|
|
7846
7648
|
if (action === "unpair") {
|
|
7847
|
-
if (source === null)
|
|
7649
|
+
if (source === null)
|
|
7848
7650
|
return fail(write, json, "bridge unpair", "no-token", "no bot token, so no bot to unpair", EXIT.refused);
|
|
7849
|
-
}
|
|
7850
7651
|
const revoked = store.unpairTelegram(source.botId, asWho, clock());
|
|
7851
7652
|
return succeed(write, json, "bridge unpair", { revoked }, () => [
|
|
7852
7653
|
revoked
|
|
@@ -7865,13 +7666,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7865
7666
|
"chat only, and the chat will answer as you — treat it accordingly.",
|
|
7866
7667
|
]);
|
|
7867
7668
|
}
|
|
7868
|
-
if (action !== undefined)
|
|
7669
|
+
if (action !== undefined)
|
|
7869
7670
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status]`", EXIT.usage);
|
|
7870
|
-
}
|
|
7871
7671
|
// The pass.
|
|
7872
|
-
if (source === null)
|
|
7672
|
+
if (source === null)
|
|
7873
7673
|
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);
|
|
7874
|
-
}
|
|
7875
7674
|
const fenced = networkFence(context.telegramEnv ?? (context.telegramTransport === undefined ? process.env : {}));
|
|
7876
7675
|
if (fenced !== null)
|
|
7877
7676
|
return fail(write, json, "bridge", "network-fenced", networkFenceLine(fenced), EXIT.refused);
|
|
@@ -7936,9 +7735,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7936
7735
|
clock,
|
|
7937
7736
|
...(flags.has("inbound-only") ? { deliver: false } : {}),
|
|
7938
7737
|
});
|
|
7939
|
-
if (!passed.ok)
|
|
7738
|
+
if (!passed.ok)
|
|
7940
7739
|
return fail(write, json, "bridge", passed.reason, passed.message, EXIT.refused);
|
|
7941
|
-
}
|
|
7942
7740
|
const { report } = passed;
|
|
7943
7741
|
const broke = report.problems.length > 0;
|
|
7944
7742
|
const idle = report.sent === 0 && report.answered === 0 && report.paired === 0 && (report.statusReplies ?? 0) === 0
|
|
@@ -7948,12 +7746,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7948
7746
|
...(report.backlog ? ["Telegram still holds more updates than one pass's budget — run it again."] : []),
|
|
7949
7747
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
7950
7748
|
];
|
|
7951
|
-
if (broke)
|
|
7749
|
+
if (broke)
|
|
7952
7750
|
return fail(write, json, "bridge", "telegram-transport", lines().join("\n"), EXIT.failed, { report });
|
|
7953
|
-
|
|
7954
|
-
if (idle) {
|
|
7751
|
+
if (idle)
|
|
7955
7752
|
return fail(write, json, "bridge", "idle", "nothing to send, nothing arrived", EXIT.refused, { report });
|
|
7956
|
-
}
|
|
7957
7753
|
return succeed(write, json, "bridge", { report }, lines);
|
|
7958
7754
|
}
|
|
7959
7755
|
// ---- publication -----------------------------------------------------------
|
|
@@ -7981,14 +7777,12 @@ async function publishCommand(positional, flags, context) {
|
|
|
7981
7777
|
const [action] = positional;
|
|
7982
7778
|
// Bare `publish` IS an action: the publication pass (push branches,
|
|
7983
7779
|
// open PRs, sweep merges) — only a NAMED unknown action refuses.
|
|
7984
|
-
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7780
|
+
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7985
7781
|
return fail(write, json, "publish", "usage", `unknown \`publish ${action}\` — try ${PUBLISH_ACTIONS.join(", ")}, or bare \`publish\` for the publication pass`, EXIT.usage);
|
|
7986
|
-
}
|
|
7987
7782
|
if (action === "setup") {
|
|
7988
7783
|
const mergeMethod = text(flags, "merge-method") ?? "squash";
|
|
7989
|
-
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
7784
|
+
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
7990
7785
|
return fail(write, json, "publish setup", "usage", "--merge-method is squash, merge or rebase", EXIT.usage);
|
|
7991
|
-
}
|
|
7992
7786
|
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
7993
7787
|
if (!checked.ok)
|
|
7994
7788
|
return fail(write, json, "publish setup", checked.reason, checked.message, EXIT.refused);
|
|
@@ -8010,26 +7804,20 @@ async function publishCommand(positional, flags, context) {
|
|
|
8010
7804
|
return EXIT.refused;
|
|
8011
7805
|
}
|
|
8012
7806
|
const acting = await askCredentials(flags, context);
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token, repo);
|
|
8017
|
-
if (!authenticated.ok) {
|
|
8018
|
-
return fail(write, json, "publish setup", authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8019
|
-
}
|
|
7807
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, repo);
|
|
7808
|
+
if (acting === null || !verified?.ok)
|
|
7809
|
+
return refuseApprover(context, "publish setup", verified, "turning on pull requests takes --as <you> --token <your password>", acting?.name);
|
|
8020
7810
|
savePublishing(store, plan, acting.name, settings, clock());
|
|
8021
7811
|
return succeed(write, json, "publish setup", { publishing: publishingOf(store, repo) }, () => ["Pull requests are on.", ...terms]);
|
|
8022
7812
|
}
|
|
8023
7813
|
if (action === "grant") {
|
|
8024
7814
|
const github = text(flags, "github");
|
|
8025
|
-
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
7815
|
+
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
8026
7816
|
return fail(write, json, "publish grant", "usage", "`--github <owner/name>` is required, exactly", EXIT.usage);
|
|
8027
|
-
}
|
|
8028
7817
|
const wantsMerge = flags.has("allow-merge");
|
|
8029
7818
|
const mergeMethod = text(flags, "merge-method");
|
|
8030
|
-
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
7819
|
+
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
8031
7820
|
return fail(write, json, "publish grant", "usage", "--allow-merge names its method: --merge-method squash|merge|rebase", EXIT.usage);
|
|
8032
|
-
}
|
|
8033
7821
|
const spec = {
|
|
8034
7822
|
repo,
|
|
8035
7823
|
githubRepo: github,
|
|
@@ -8070,14 +7858,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8070
7858
|
return EXIT.refused;
|
|
8071
7859
|
}
|
|
8072
7860
|
const acting = await askCredentials(flags, context);
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
7861
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7862
|
+
if (acting === null || !verified?.ok)
|
|
7863
|
+
return refuseApprover(context, "publish grant", verified, "granting takes --as <you> --token <your password> — pushing your repos is a person's yes", acting?.name);
|
|
8076
7864
|
const { name: asWho, token } = acting;
|
|
8077
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8078
|
-
if (!authenticated.ok) {
|
|
8079
|
-
return fail(write, json, "publish grant", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8080
|
-
}
|
|
8081
7865
|
store.savePublicationGrant({ ...spec, grantedBy: asWho }, clock());
|
|
8082
7866
|
return succeed(write, json, "publish grant", { granted: true, grant: spec }, () => [
|
|
8083
7867
|
`Granted by ${asWho}:`,
|
|
@@ -8087,14 +7871,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8087
7871
|
}
|
|
8088
7872
|
if (action === "revoke") {
|
|
8089
7873
|
const acting = await askCredentials(flags, context);
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
7874
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7875
|
+
if (acting === null || !verified?.ok)
|
|
7876
|
+
return refuseApprover(context, "publish revoke", verified, "`--as <you> --token <your password>`", acting?.name);
|
|
8093
7877
|
const { name: asWho, token } = acting;
|
|
8094
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8095
|
-
if (!authenticated.ok) {
|
|
8096
|
-
return fail(write, json, "publish revoke", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8097
|
-
}
|
|
8098
7878
|
const revoked = store.revokePublicationGrant(repo, asWho, clock());
|
|
8099
7879
|
return succeed(write, json, "publish revoke", { revoked }, () => [
|
|
8100
7880
|
revoked
|
|
@@ -8119,25 +7899,18 @@ async function publishCommand(positional, flags, context) {
|
|
|
8119
7899
|
if (action === "merge" || action === "refire") {
|
|
8120
7900
|
const prGiven = positional[1];
|
|
8121
7901
|
const pr = Number(prGiven);
|
|
8122
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7902
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8123
7903
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8124
|
-
}
|
|
8125
7904
|
const acting = await askCredentials(flags, context);
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8130
|
-
if (!authenticated.ok) {
|
|
8131
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8132
|
-
}
|
|
7905
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7906
|
+
if (acting === null || !verified?.ok)
|
|
7907
|
+
return refuseApprover(context, `publish ${action}`, verified, "authorizing a merge takes `--as <you> --token <t>` — who said yes is recorded, not asserted", acting?.name);
|
|
8133
7908
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8134
|
-
if (publication === undefined)
|
|
7909
|
+
if (publication === undefined)
|
|
8135
7910
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8136
|
-
}
|
|
8137
7911
|
const intent = store.mergeIntentFor(publication.id);
|
|
8138
|
-
if (intent === null)
|
|
7912
|
+
if (intent === null)
|
|
8139
7913
|
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);
|
|
8140
|
-
}
|
|
8141
7914
|
if (action === "merge") {
|
|
8142
7915
|
// The EXACT-INTENT ceremony (E1): the yes covers this head and no
|
|
8143
7916
|
// other — a moved head refuses rather than authorizing the unseen.
|
|
@@ -8166,21 +7939,15 @@ async function publishCommand(positional, flags, context) {
|
|
|
8166
7939
|
if (action === "unblock" || action === "rearm") {
|
|
8167
7940
|
const prGiven = positional[1];
|
|
8168
7941
|
const pr = Number(prGiven);
|
|
8169
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7942
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8170
7943
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8171
|
-
}
|
|
8172
7944
|
const acting = await askCredentials(flags, context);
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8177
|
-
if (!authenticated.ok) {
|
|
8178
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8179
|
-
}
|
|
7945
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
7946
|
+
if (acting === null || !verified?.ok)
|
|
7947
|
+
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);
|
|
8180
7948
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8181
|
-
if (publication === undefined)
|
|
7949
|
+
if (publication === undefined)
|
|
8182
7950
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8183
|
-
}
|
|
8184
7951
|
if (action === "unblock") {
|
|
8185
7952
|
const lifted = store.liftMergeBlocker(publication.id, acting.name, context.clock());
|
|
8186
7953
|
store.resolveEpisodes(`merge-attn:${publication.id}`, context.clock());
|
|
@@ -8198,9 +7965,8 @@ async function publishCommand(positional, flags, context) {
|
|
|
8198
7965
|
: `PR #${pr} had no refused merge to re-arm.`,
|
|
8199
7966
|
]);
|
|
8200
7967
|
}
|
|
8201
|
-
if (action !== undefined)
|
|
7968
|
+
if (action !== undefined)
|
|
8202
7969
|
return fail(write, json, "publish", "usage", "`toolroll publish [grant|revoke|status|unblock|rearm]`", EXIT.usage);
|
|
8203
|
-
}
|
|
8204
7970
|
// The pass.
|
|
8205
7971
|
const report = await publishPass(store, {
|
|
8206
7972
|
repo,
|
|
@@ -8222,12 +7988,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8222
7988
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
8223
7989
|
...merges.problems.map(problem => ` merge: ${problem}`),
|
|
8224
7990
|
];
|
|
8225
|
-
if (report.problems.length > 0)
|
|
7991
|
+
if (report.problems.length > 0)
|
|
8226
7992
|
return fail(write, json, "publish", "publish-problems", lines().join("\n"), EXIT.failed, { report });
|
|
8227
|
-
|
|
8228
|
-
if (idle) {
|
|
7993
|
+
if (idle)
|
|
8229
7994
|
return fail(write, json, "publish", "idle", "nothing owed — no pending publications", EXIT.refused, { report });
|
|
8230
|
-
}
|
|
8231
7995
|
return succeed(write, json, "publish", { report }, lines);
|
|
8232
7996
|
}
|
|
8233
7997
|
// ---- the outbox -----------------------------------------------------------
|
|
@@ -8251,9 +8015,8 @@ async function peekCommand(positional, flags, context) {
|
|
|
8251
8015
|
}
|
|
8252
8016
|
const linesGiven = text(flags, "lines");
|
|
8253
8017
|
const lines = linesGiven === undefined ? 12 : Number(linesGiven);
|
|
8254
|
-
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8018
|
+
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8255
8019
|
return fail(write, json, "peek", "usage", `--lines is a whole number from 1 to ${PEEK_TAIL_LINES}`, EXIT.usage);
|
|
8256
|
-
}
|
|
8257
8020
|
const interactive = !json && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
8258
8021
|
if (flags.has("tmux")) {
|
|
8259
8022
|
if (!interactive)
|
|
@@ -8310,9 +8073,8 @@ async function outboxCommand(positional, flags, context) {
|
|
|
8310
8073
|
if (demoFence !== null)
|
|
8311
8074
|
return demoFence;
|
|
8312
8075
|
const command = text(flags, "cmd");
|
|
8313
|
-
if (command === undefined)
|
|
8076
|
+
if (command === undefined)
|
|
8314
8077
|
return fail(write, json, "outbox deliver", "usage", "--cmd says how: it runs once per notification, reading $TOOLROLL_KIND, $TOOLROLL_SUBJECT, $TOOLROLL_BODY", EXIT.usage);
|
|
8315
|
-
}
|
|
8316
8078
|
// The exact command owns a leased destination receipt, including across concurrent CLI invocations.
|
|
8317
8079
|
const owner = `outbox-${randomUUID()}`;
|
|
8318
8080
|
// Push delivery has independent receipts and must keep working if this command fails.
|
|
@@ -8385,28 +8147,23 @@ async function enrollCommand(positional, flags, context) {
|
|
|
8385
8147
|
const repo = positional[0] === undefined ? process.cwd() : resolve(positional[0]);
|
|
8386
8148
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
8387
8149
|
const mutations = readMutations(flags);
|
|
8388
|
-
if (mutations === null)
|
|
8150
|
+
if (mutations === null)
|
|
8389
8151
|
return fail(write, json, "enroll", "usage", `--allow takes ${MUTATION_CLASSES.join(", ")}`, EXIT.usage);
|
|
8390
|
-
}
|
|
8391
8152
|
const selectorFlag = text(flags, "selector") ?? "ours";
|
|
8392
|
-
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8153
|
+
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8393
8154
|
return fail(write, json, "enroll", "usage", "--selector is `ours` or `all`", EXIT.usage);
|
|
8394
|
-
}
|
|
8395
8155
|
const paths = readPaths(flags, backend);
|
|
8396
|
-
if (paths.length === 0)
|
|
8156
|
+
if (paths.length === 0)
|
|
8397
8157
|
return fail(write, json, "enroll", "usage", `--paths says what may be written for backend \`${backend}\``, EXIT.usage);
|
|
8398
|
-
}
|
|
8399
8158
|
// External dispatch (v20): its own explicit yes, never in any default.
|
|
8400
8159
|
// A dispatch grant binds EXACTLY ONE remote repository and mints this
|
|
8401
8160
|
// plane's marker identity.
|
|
8402
8161
|
const wantsDispatch = flags.has("allow-dispatch");
|
|
8403
8162
|
const dispatchRepo = text(flags, "github");
|
|
8404
|
-
if (wantsDispatch && backend !== "github-issues")
|
|
8163
|
+
if (wantsDispatch && backend !== "github-issues")
|
|
8405
8164
|
return fail(write, json, "enroll", "usage", "--allow-dispatch is a github-issues authority in this release", EXIT.usage);
|
|
8406
|
-
|
|
8407
|
-
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo))) {
|
|
8165
|
+
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo)))
|
|
8408
8166
|
return fail(write, json, "enroll", "usage", "--allow-dispatch binds exactly one tracker: name it with `--github <owner/name>`", EXIT.usage);
|
|
8409
|
-
}
|
|
8410
8167
|
const grant = await proposeGrant({
|
|
8411
8168
|
repo,
|
|
8412
8169
|
backend,
|
|
@@ -8497,17 +8254,14 @@ function grantsCommand(context) {
|
|
|
8497
8254
|
return EXIT.ok;
|
|
8498
8255
|
}
|
|
8499
8256
|
function checkProgressCommand(positional, context) {
|
|
8500
|
-
if (positional.length !== 1 || !/^[1-9][0-9]{0,14}$/.test(positional[0] ?? ""))
|
|
8257
|
+
if (positional.length !== 1 || !/^[1-9][0-9]{0,14}$/.test(positional[0] ?? ""))
|
|
8501
8258
|
return fail(context.write, context.json, "check-progress", "usage", "use check-progress <run>", EXIT.usage);
|
|
8502
|
-
}
|
|
8503
8259
|
const run = Number(positional[0]);
|
|
8504
|
-
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8260
|
+
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8505
8261
|
return fail(context.write, context.json, "check-progress", "unknown-run", "no such run", EXIT.refused);
|
|
8506
|
-
}
|
|
8507
8262
|
const progress = context.store.checkProgress(run);
|
|
8508
|
-
if (progress === null)
|
|
8263
|
+
if (progress === null)
|
|
8509
8264
|
return fail(context.write, context.json, "check-progress", "no-progress", "this run has no saved check progress", EXIT.refused);
|
|
8510
|
-
}
|
|
8511
8265
|
return succeed(context.write, context.json, "check-progress", { run, progress }, () => [progress.line]);
|
|
8512
8266
|
}
|
|
8513
8267
|
function revokeCommand(positional, flags, context) {
|
|
@@ -8523,9 +8277,8 @@ function revokeCommand(positional, flags, context) {
|
|
|
8523
8277
|
if (revoked && outgoing?.dispatch === true && outgoing.remoteRepo != null) {
|
|
8524
8278
|
void ghDispatchAdapter().deleteMarker(outgoing.remoteRepo);
|
|
8525
8279
|
}
|
|
8526
|
-
if (!revoked)
|
|
8280
|
+
if (!revoked)
|
|
8527
8281
|
return fail(write, json, "revoke", "no-grant", `${repo} was not enrolled for ${backend}`, EXIT.refused);
|
|
8528
|
-
}
|
|
8529
8282
|
return succeed(write, json, "revoke", { repo, backend }, () => [
|
|
8530
8283
|
`Revoked. ${backend} in ${repo} is read-only again.`,
|
|
8531
8284
|
]);
|
|
@@ -8663,14 +8416,12 @@ async function waitTask(positional, flags, context) {
|
|
|
8663
8416
|
if (!allowed.has(name))
|
|
8664
8417
|
return fail(context.write, context.json, command, "usage", `--${name} is not a task wait option.`, EXIT.usage);
|
|
8665
8418
|
}
|
|
8666
|
-
if (positional.length !== 1)
|
|
8419
|
+
if (positional.length !== 1)
|
|
8667
8420
|
return fail(context.write, context.json, command, "usage", "Use `toolroll task wait <id> [--timeout <seconds>]`.", EXIT.usage);
|
|
8668
|
-
}
|
|
8669
8421
|
const givenTimeout = text(flags, "timeout");
|
|
8670
8422
|
const timeoutSeconds = givenTimeout === undefined ? null : Number(givenTimeout);
|
|
8671
|
-
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8423
|
+
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8672
8424
|
return fail(context.write, context.json, command, "usage", "--timeout must be zero or a positive number of seconds.", EXIT.usage);
|
|
8673
|
-
}
|
|
8674
8425
|
const task = positional[0];
|
|
8675
8426
|
const startedAt = Date.now();
|
|
8676
8427
|
const timeoutMs = timeoutSeconds === null ? null : timeoutSeconds * 1_000;
|
|
@@ -8824,15 +8575,13 @@ async function addTask(positional, flags, context) {
|
|
|
8824
8575
|
// A scout task lives in the built-in backend (v4 review, finding 2):
|
|
8825
8576
|
// the deliverable is a column on OUR task_ref, stamped in the same
|
|
8826
8577
|
// transaction as the filing — a tracker's item has no such promise.
|
|
8827
|
-
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8578
|
+
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8828
8579
|
return fail(write, json, "task add", "usage", "--report files a scout task in the built-in backend only — drop --backend", EXIT.usage);
|
|
8829
|
-
}
|
|
8830
8580
|
if (backendName !== BUILT_IN) {
|
|
8831
8581
|
const repo = repoFrom(flags);
|
|
8832
8582
|
const backend = openBackend(backendName, store, repo);
|
|
8833
|
-
if (backend === null)
|
|
8583
|
+
if (backend === null)
|
|
8834
8584
|
return fail(write, json, "task add", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
8835
|
-
}
|
|
8836
8585
|
const created = await backend.create({ title });
|
|
8837
8586
|
if (!created.ok) {
|
|
8838
8587
|
// A denial is a refusal, not a breakage: the tool worked exactly as
|
|
@@ -8878,9 +8627,8 @@ async function addTask(positional, flags, context) {
|
|
|
8878
8627
|
const id = text(flags, "id") ?? (activeRemote() === null ? slug(title, now) : `task-${randomBytes(16).toString("hex")}`);
|
|
8879
8628
|
// Re-filing: the new task replaces an earlier one, which is cancelled and reads "Replaced by <id>".
|
|
8880
8629
|
const replaces = text(flags, "replaces");
|
|
8881
|
-
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8630
|
+
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8882
8631
|
return fail(write, json, "task add", "unknown-task", `no task \`${replaces}\` to replace`, EXIT.refused);
|
|
8883
|
-
}
|
|
8884
8632
|
noteActingPerson(flags, context);
|
|
8885
8633
|
// The existence check goes *inside* the replayed body. Outside it, a retry
|
|
8886
8634
|
// with the same key hits "already exists" and reports failure for a task the
|
|
@@ -8895,9 +8643,8 @@ async function addTask(positional, flags, context) {
|
|
|
8895
8643
|
// branch task.
|
|
8896
8644
|
return { ok: true, task: store.createTask({ id, title, ...(flags.has("report") ? { deliverable: "report" } : {}) }, now) };
|
|
8897
8645
|
}, result => result.ok);
|
|
8898
|
-
if (!outcome.ok)
|
|
8646
|
+
if (!outcome.ok)
|
|
8899
8647
|
return fail(write, json, "task add", "exists", `\`${id}\` already exists`, EXIT.refused);
|
|
8900
|
-
}
|
|
8901
8648
|
const filedId = outcome.task.id;
|
|
8902
8649
|
const remote = activeRemote();
|
|
8903
8650
|
if (remote !== null && filedId !== id) {
|
|
@@ -8914,9 +8661,8 @@ async function addTask(positional, flags, context) {
|
|
|
8914
8661
|
const placedIn = text(flags, "repo");
|
|
8915
8662
|
if (placedIn !== undefined) {
|
|
8916
8663
|
const placed = store.placeTask(store.refFor(BUILT_IN, filedId).id, canonicalProject(placedIn) ?? resolve(placedIn));
|
|
8917
|
-
if (typeof placed === "object" && !placed.ok)
|
|
8664
|
+
if (typeof placed === "object" && !placed.ok)
|
|
8918
8665
|
return fail(write, json, "task add", "scoped", "this task already has a scope — placement is immutable once somebody could have approved it", EXIT.refused);
|
|
8919
|
-
}
|
|
8920
8666
|
}
|
|
8921
8667
|
if (replaces !== undefined)
|
|
8922
8668
|
store.setTaskState(replaces, "cancelled", now, {}, undefined, outcome.task.id);
|
|
@@ -9017,17 +8763,14 @@ function requireTask(positional, flags, context) {
|
|
|
9017
8763
|
const { store, write, json, now } = context;
|
|
9018
8764
|
const id = positional[0];
|
|
9019
8765
|
const given = text(flags, "cap");
|
|
9020
|
-
if (id === undefined || given === undefined)
|
|
8766
|
+
if (id === undefined || given === undefined)
|
|
9021
8767
|
return fail(write, json, "task require", "usage", "`toolroll task require <id> --cap <kind:name>[,<kind:name>]` — or --cap none to clear", EXIT.usage);
|
|
9022
|
-
|
|
9023
|
-
if (store.getTask(id) === null) {
|
|
8768
|
+
if (store.getTask(id) === null)
|
|
9024
8769
|
return fail(write, json, "task require", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9025
|
-
}
|
|
9026
8770
|
const keys = given === "none" ? [] : given.split(",").map(one => one.trim()).filter(Boolean);
|
|
9027
8771
|
for (const key of keys) {
|
|
9028
|
-
if (parseCapabilityKey(key) === null)
|
|
8772
|
+
if (parseCapabilityKey(key) === null)
|
|
9029
8773
|
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);
|
|
9030
|
-
}
|
|
9031
8774
|
}
|
|
9032
8775
|
store.setRequirements(store.refFor(BUILT_IN, id).id, keys, mutationFrom(flags, now));
|
|
9033
8776
|
return succeed(write, json, "task require", { id, requirements: keys }, () => [
|
|
@@ -9050,21 +8793,18 @@ async function capCommand(positional, flags, context) {
|
|
|
9050
8793
|
const [action, name] = positional;
|
|
9051
8794
|
const repo = repoFrom(flags);
|
|
9052
8795
|
if (action === "add") {
|
|
9053
|
-
if (name === undefined)
|
|
8796
|
+
if (name === undefined)
|
|
9054
8797
|
return fail(write, json, "cap add", "usage", "`toolroll cap add <name> [--kind env|cli|mcp|ci|other] [--probe <cmd>] [--expires <iso>]`", EXIT.usage);
|
|
9055
|
-
}
|
|
9056
8798
|
const kind = (text(flags, "kind") ?? "env");
|
|
9057
|
-
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
8799
|
+
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
9058
8800
|
return fail(write, json, "cap add", "usage", "--kind takes env, cli, mcp, ci or other", EXIT.usage);
|
|
9059
|
-
}
|
|
9060
8801
|
// An env capability can have its probe synthesized from a fixed template
|
|
9061
8802
|
// — but only over a validated identifier, because the name lands inside
|
|
9062
8803
|
// a shell line. Anything else the operator writes explicitly.
|
|
9063
8804
|
let probe = text(flags, "probe") ?? null;
|
|
9064
8805
|
if (probe === null && kind === "env") {
|
|
9065
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
8806
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
9066
8807
|
return fail(write, json, "cap add", "usage", `\`${name}\` is not an environment variable name — give --probe explicitly`, EXIT.usage);
|
|
9067
|
-
}
|
|
9068
8808
|
probe = `test -n "$${name}"`;
|
|
9069
8809
|
}
|
|
9070
8810
|
const expires = text(flags, "expires") ?? null;
|
|
@@ -9149,16 +8889,14 @@ async function capCommand(positional, flags, context) {
|
|
|
9149
8889
|
if (action === "probe") {
|
|
9150
8890
|
const only = positional.slice(1);
|
|
9151
8891
|
for (const key of only) {
|
|
9152
|
-
if (parseCapabilityKey(key) === null)
|
|
8892
|
+
if (parseCapabilityKey(key) === null)
|
|
9153
8893
|
return fail(write, json, "cap probe", "usage", `\`${key}\` is not a capability key — say kind:name`, EXIT.usage);
|
|
9154
|
-
}
|
|
9155
8894
|
}
|
|
9156
8895
|
const outcomes = await probeRepo(store, repo, `operator@${hostname()}`, clock(), {
|
|
9157
8896
|
...(only.length === 0 ? {} : { only: new Set(only) }),
|
|
9158
8897
|
});
|
|
9159
|
-
if (outcomes.length === 0)
|
|
8898
|
+
if (outcomes.length === 0)
|
|
9160
8899
|
return fail(write, json, "cap probe", "empty", `nothing to probe for ${repo}`, EXIT.refused);
|
|
9161
|
-
}
|
|
9162
8900
|
const unverified = outcomes.filter(one => one.status !== "verified");
|
|
9163
8901
|
const lines = () => outcomes.map(one => ` ${`${one.kind}:${one.name}`.padEnd(32)} ${one.status}${one.detail === undefined ? "" : ` ${one.detail}`}`);
|
|
9164
8902
|
if (json) {
|
|
@@ -9179,9 +8917,8 @@ function listTasks(flags, context) {
|
|
|
9179
8917
|
const rawView = text(flags, 'view');
|
|
9180
8918
|
const rawLimit = text(flags, 'limit');
|
|
9181
8919
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
9182
|
-
if (wanted !== undefined && !STATES.includes(wanted))
|
|
8920
|
+
if (wanted !== undefined && !STATES.includes(wanted))
|
|
9183
8921
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
9184
|
-
}
|
|
9185
8922
|
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed'].includes(rawView) ||
|
|
9186
8923
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
9187
8924
|
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running or completed; --limit takes 1–100', EXIT.usage);
|
|
@@ -9432,19 +9169,14 @@ async function acceptTaskProof(positional, flags, context) {
|
|
|
9432
9169
|
if (task === null)
|
|
9433
9170
|
return fail(write, json, "task accept", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9434
9171
|
const acting = await askCredentials(flags, context);
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9439
|
-
if (!authenticated.ok) {
|
|
9440
|
-
return fail(write, json, "task accept", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
9441
|
-
}
|
|
9172
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9173
|
+
if (acting === null || !verified?.ok)
|
|
9174
|
+
return refuseApprover(context, "task accept", verified, "accepting incomplete proof is a person's act — it takes `--as <you> --token <t>`", id);
|
|
9442
9175
|
const ref = store.refFor(BUILT_IN, id);
|
|
9443
9176
|
// v40 fix: a reviewer run is never the attempt whose proof is accepted.
|
|
9444
9177
|
const latest = store.runsFor(ref.id).find(one => one.finishedAt !== null && (one.role === "builder" || one.role === "scout"));
|
|
9445
|
-
if (latest === undefined)
|
|
9178
|
+
if (latest === undefined)
|
|
9446
9179
|
return fail(write, json, "task accept", "no-run", `${id} has no finished attempt to accept`, EXIT.refused);
|
|
9447
|
-
}
|
|
9448
9180
|
const rawNote = text(flags, "note");
|
|
9449
9181
|
let note = null;
|
|
9450
9182
|
if (rawNote !== undefined && rawNote.trim() !== "") {
|
|
@@ -9469,9 +9201,8 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9469
9201
|
const { store, write, json, clock } = context;
|
|
9470
9202
|
const [runText] = positional;
|
|
9471
9203
|
const runId = Number(runText ?? "");
|
|
9472
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9204
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9473
9205
|
return fail(write, json, "task repair", "usage", "`toolroll task repair <run-id> [--yes] --as <you> --token <t>`", EXIT.usage);
|
|
9474
|
-
}
|
|
9475
9206
|
let chain = store.repairChainFor(runId);
|
|
9476
9207
|
if (chain === null && flags.get("yes") === true) {
|
|
9477
9208
|
const acting = await askCredentials(flags, context);
|
|
@@ -9488,12 +9219,10 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9488
9219
|
chain = store.repairChainFor(runId);
|
|
9489
9220
|
}
|
|
9490
9221
|
}
|
|
9491
|
-
if (chain === null)
|
|
9222
|
+
if (chain === null)
|
|
9492
9223
|
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);
|
|
9493
|
-
|
|
9494
|
-
if (chain.draftTask === null) {
|
|
9224
|
+
if (chain.draftTask === null)
|
|
9495
9225
|
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);
|
|
9496
|
-
}
|
|
9497
9226
|
const draftId = chain.draftTask;
|
|
9498
9227
|
const yes = flags.get("yes") === true;
|
|
9499
9228
|
if (!yes) {
|
|
@@ -9504,21 +9233,15 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9504
9233
|
]);
|
|
9505
9234
|
}
|
|
9506
9235
|
const acting = await askCredentials(flags, context);
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9511
|
-
if (!authenticated.ok) {
|
|
9512
|
-
return fail(write, json, "task repair", authenticated.reason, describeApproveFailure(authenticated.reason, draftId), EXIT.refused);
|
|
9513
|
-
}
|
|
9236
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9237
|
+
if (acting === null || !verified?.ok)
|
|
9238
|
+
return refuseApprover(context, "task repair", verified, "approving a repair draft is a person's act — it takes `--as <you> --token <t>`", draftId);
|
|
9514
9239
|
const draftScope = store.getScope(draftId);
|
|
9515
|
-
if (draftScope === null)
|
|
9240
|
+
if (draftScope === null)
|
|
9516
9241
|
return fail(write, json, "task repair", "no-scope", `${draftId} has no scope to approve`, EXIT.refused);
|
|
9517
|
-
}
|
|
9518
9242
|
const approved = approve(store, draftId, acting.name, clock(), draftScope.digest, acting.token);
|
|
9519
|
-
if (!approved.ok)
|
|
9243
|
+
if (!approved.ok)
|
|
9520
9244
|
return fail(write, json, "task repair", approved.reason, approved.reason === "policy" ? `${draftId}: ${approved.message}` : describeApproveFailure(approved.reason, draftId), EXIT.refused);
|
|
9521
|
-
}
|
|
9522
9245
|
return succeed(write, json, "task repair", { run: runId, draft: draftId, approvedBy: acting.name }, () => [
|
|
9523
9246
|
`Approved: ${draftId} (attempt ${chain.attempt}) will build on the next dispatch.`,
|
|
9524
9247
|
]);
|
|
@@ -9526,19 +9249,16 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9526
9249
|
async function stateTask(positional, flags, context) {
|
|
9527
9250
|
const { store, write, json, now } = context;
|
|
9528
9251
|
const [id, state] = positional;
|
|
9529
|
-
if (id === undefined || state === undefined)
|
|
9252
|
+
if (id === undefined || state === undefined)
|
|
9530
9253
|
return fail(write, json, "task state", "usage", "`toolroll task state <id> <state>`", EXIT.usage);
|
|
9531
|
-
|
|
9532
|
-
if (!STATES.includes(state)) {
|
|
9254
|
+
if (!STATES.includes(state))
|
|
9533
9255
|
return fail(write, json, "task state", "usage", `state is one of ${STATES.join(", ")}`, EXIT.usage);
|
|
9534
|
-
}
|
|
9535
9256
|
const backendName = text(flags, "backend") ?? BUILT_IN;
|
|
9536
9257
|
if (backendName !== BUILT_IN) {
|
|
9537
9258
|
const repo = repoFrom(flags);
|
|
9538
9259
|
const backend = openBackend(backendName, store, repo);
|
|
9539
|
-
if (backend === null)
|
|
9260
|
+
if (backend === null)
|
|
9540
9261
|
return fail(write, json, "task state", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
9541
|
-
}
|
|
9542
9262
|
const moved = await backend.setState(id, state);
|
|
9543
9263
|
if (!moved.ok) {
|
|
9544
9264
|
const code = moved.reason === "denied" || moved.reason === "unsupported" ? EXIT.refused : EXIT.failed;
|
|
@@ -9589,9 +9309,8 @@ function askTask(positional, flags, context) {
|
|
|
9589
9309
|
const { store, write, json, now } = context;
|
|
9590
9310
|
const [id, ...extra] = positional;
|
|
9591
9311
|
const person = text(flags, "person"), why = text(flags, "why")?.trim();
|
|
9592
|
-
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9312
|
+
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9593
9313
|
return fail(write, json, "task ask", "usage", "`toolroll task ask <id> --person <name> --why \"what they need to do\"`", EXIT.usage);
|
|
9594
|
-
}
|
|
9595
9314
|
if (why.length > 500 || /[\u0000-\u001f\u007f]/.test(why))
|
|
9596
9315
|
return fail(write, json, "task ask", "usage", "--why is at most 500 plain characters on one line", EXIT.usage);
|
|
9597
9316
|
noteActingPerson(flags, context);
|
|
@@ -9620,9 +9339,8 @@ async function leadCommand(positional, flags, context) {
|
|
|
9620
9339
|
return fail(write, json, command, "refused", "A lead cannot mint lead tokens. The person it acts for runs this with their password.", EXIT.refused);
|
|
9621
9340
|
const acting = await askCredentials(flags, context);
|
|
9622
9341
|
const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
9623
|
-
if (acting === null ||
|
|
9342
|
+
if (acting === null || !verified?.ok)
|
|
9624
9343
|
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);
|
|
9625
|
-
}
|
|
9626
9344
|
const now = context.clock();
|
|
9627
9345
|
if (flags.has("revoke")) {
|
|
9628
9346
|
const ended = store.revokeLeadCredentials(acting.name, acting.name, now);
|
|
@@ -9676,13 +9394,11 @@ function blockTask(positional, flags, context) {
|
|
|
9676
9394
|
const { store, write, json } = context;
|
|
9677
9395
|
const id = positional[0];
|
|
9678
9396
|
const on = text(flags, "on");
|
|
9679
|
-
if (id === undefined || on === undefined)
|
|
9397
|
+
if (id === undefined || on === undefined)
|
|
9680
9398
|
return fail(write, json, "task block", "usage", "`toolroll task block <id> --on <id>`", EXIT.usage);
|
|
9681
|
-
}
|
|
9682
9399
|
for (const each of [id, on]) {
|
|
9683
|
-
if (store.getTask(each) === null)
|
|
9400
|
+
if (store.getTask(each) === null)
|
|
9684
9401
|
return fail(write, json, "task block", "unknown-task", `no task \`${each}\``, EXIT.refused);
|
|
9685
|
-
}
|
|
9686
9402
|
}
|
|
9687
9403
|
const result = store.addEdge(id, on, mutationFrom(flags, context.now));
|
|
9688
9404
|
if (!result.ok)
|
|
@@ -9700,20 +9416,17 @@ async function steerTask(positional, flags, context) {
|
|
|
9700
9416
|
const { store, write, json } = context;
|
|
9701
9417
|
const id = positional[0];
|
|
9702
9418
|
const note = text(flags, "note");
|
|
9703
|
-
if (id === undefined || note === undefined)
|
|
9419
|
+
if (id === undefined || note === undefined)
|
|
9704
9420
|
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);
|
|
9705
|
-
}
|
|
9706
9421
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9707
9422
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
9708
9423
|
// but wrong is not-an-approver — the same taxonomy as every ceremony.
|
|
9709
9424
|
const acting = await askCredentials(flags, context);
|
|
9710
|
-
if (acting === null)
|
|
9425
|
+
if (acting === null)
|
|
9711
9426
|
return fail(write, json, "task steer", "usage", "steering takes `--as <you> --token <t>` — anonymous notes never reach an agent's brief", EXIT.usage);
|
|
9712
|
-
}
|
|
9713
9427
|
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9714
|
-
if (!authed.ok)
|
|
9428
|
+
if (!authed.ok)
|
|
9715
9429
|
return fail(write, json, "task steer", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9716
|
-
}
|
|
9717
9430
|
// --now: Pause and steer — the running Claude build's turn stops, then this note continues it.
|
|
9718
9431
|
if (flags.get("now") === true)
|
|
9719
9432
|
return pauseRunningBuild("task steer", id, note, verifiedAuthor(acting.name), flags, context);
|
|
@@ -9816,16 +9529,13 @@ function unblockTask(positional, flags, context) {
|
|
|
9816
9529
|
const { store, write, json } = context;
|
|
9817
9530
|
const id = positional[0];
|
|
9818
9531
|
const on = text(flags, "on");
|
|
9819
|
-
if (id === undefined || on === undefined)
|
|
9532
|
+
if (id === undefined || on === undefined)
|
|
9820
9533
|
return fail(write, json, "task unblock", "usage", "`toolroll task unblock <id> --on <id>`", EXIT.usage);
|
|
9821
|
-
|
|
9822
|
-
if (store.getTask(id) === null) {
|
|
9534
|
+
if (store.getTask(id) === null)
|
|
9823
9535
|
return fail(write, json, "task unblock", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9824
|
-
}
|
|
9825
9536
|
const result = store.removeEdge(id, on, mutationFrom(flags, context.now));
|
|
9826
|
-
if (!result.ok)
|
|
9537
|
+
if (!result.ok)
|
|
9827
9538
|
return fail(write, json, "task unblock", "not-waiting", `${id} was not waiting on ${on}`, EXIT.refused);
|
|
9828
|
-
}
|
|
9829
9539
|
return succeed(write, json, "task unblock", { blocked: id, blocker: on }, () => [
|
|
9830
9540
|
`${id} no longer waits for ${on}.`,
|
|
9831
9541
|
]);
|
|
@@ -9838,9 +9548,8 @@ function unblockTask(positional, flags, context) {
|
|
|
9838
9548
|
function nextTask(positional, flags, context) {
|
|
9839
9549
|
const { store, write, json } = context;
|
|
9840
9550
|
const id = positional[0];
|
|
9841
|
-
if (id === undefined)
|
|
9551
|
+
if (id === undefined)
|
|
9842
9552
|
return fail(write, json, "task next", "usage", "`toolroll task next <id> [--undo]`", EXIT.usage);
|
|
9843
|
-
}
|
|
9844
9553
|
if (flags.has("undo")) {
|
|
9845
9554
|
const cleared = store.clearTaskPriority(id, mutationFrom(flags, context.now));
|
|
9846
9555
|
if (!cleared.ok)
|
|
@@ -9874,9 +9583,8 @@ function assignTask(positional, flags, context) {
|
|
|
9874
9583
|
const id = positional[0];
|
|
9875
9584
|
const runner = text(flags, "runner");
|
|
9876
9585
|
const anyone = flags.has("anyone");
|
|
9877
|
-
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9586
|
+
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9878
9587
|
return fail(write, json, "task assign", "usage", "`toolroll task assign <id> --runner <name> | --anyone`", EXIT.usage);
|
|
9879
|
-
}
|
|
9880
9588
|
const moved = store.moveTask({ taskId: id, toRunner: runner ?? null, beforeTaskId: null }, context.clock(), mutationFrom(flags, context.now));
|
|
9881
9589
|
if (!moved.ok) {
|
|
9882
9590
|
const message = moved.reason === "unknown-task"
|
|
@@ -9906,13 +9614,11 @@ function assignTask(positional, flags, context) {
|
|
|
9906
9614
|
async function reopenTask(positional, flags, context) {
|
|
9907
9615
|
const { store, write, json } = context;
|
|
9908
9616
|
const id = positional[0];
|
|
9909
|
-
if (id === undefined)
|
|
9617
|
+
if (id === undefined)
|
|
9910
9618
|
return fail(write, json, "task reopen", "usage", "`toolroll task reopen <id> --as <you> --token <t>`", EXIT.usage);
|
|
9911
|
-
}
|
|
9912
9619
|
const acting = await askCredentials(flags, context);
|
|
9913
|
-
if (acting === null)
|
|
9620
|
+
if (acting === null)
|
|
9914
9621
|
return fail(write, json, "task reopen", "usage", "reopening takes `--as <you> --token <t>` — who resumed external work is recorded, not asserted", EXIT.usage);
|
|
9915
|
-
}
|
|
9916
9622
|
const reopened = store.reopenMirror(id, acting.name, context.clock());
|
|
9917
9623
|
if (!reopened.ok) {
|
|
9918
9624
|
const said = {
|
|
@@ -9945,9 +9651,8 @@ async function syncCommand(flags, context) {
|
|
|
9945
9651
|
.listGrants()
|
|
9946
9652
|
.filter(grant => grant.dispatch === true && grant.remoteRepo != null)
|
|
9947
9653
|
.filter(grant => only === undefined || grant.repo === resolve(only));
|
|
9948
|
-
if (grants.length === 0)
|
|
9654
|
+
if (grants.length === 0)
|
|
9949
9655
|
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);
|
|
9950
|
-
}
|
|
9951
9656
|
const adapter = ghDispatchAdapter();
|
|
9952
9657
|
const reports = [];
|
|
9953
9658
|
for (const grant of grants) {
|
|
@@ -9980,12 +9685,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
9980
9685
|
const { store, write, json, now } = context;
|
|
9981
9686
|
const id = positional[0];
|
|
9982
9687
|
const goal = text(flags, "goal");
|
|
9983
|
-
if (id === undefined || goal === undefined)
|
|
9688
|
+
if (id === undefined || goal === undefined)
|
|
9984
9689
|
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);
|
|
9985
|
-
|
|
9986
|
-
if (store.getTask(id) === null) {
|
|
9690
|
+
if (store.getTask(id) === null)
|
|
9987
9691
|
return fail(write, json, "task scope", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9988
|
-
}
|
|
9989
9692
|
// v39: this road bypasses proposeGuarded (a trusted local operator, not a
|
|
9990
9693
|
// web form re-authenticating staleness) but not the rubric requirement —
|
|
9991
9694
|
// every scope-producing road signs one, this one included.
|
|
@@ -10000,12 +9703,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10000
9703
|
// v69: a prepared commit. The machine checks it out as the attempt and
|
|
10001
9704
|
// runs the gate and the review; no agent is dispatched for it.
|
|
10002
9705
|
const candidateGiven = text(flags, "candidate");
|
|
10003
|
-
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
9706
|
+
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
10004
9707
|
return fail(write, json, "task scope", "usage", "--candidate is the full 40-character commit hash the machine will check out", EXIT.usage);
|
|
10005
|
-
|
|
10006
|
-
if (acceptanceParse.criteria.length === 0) {
|
|
9708
|
+
if (acceptanceParse.criteria.length === 0)
|
|
10007
9709
|
return fail(write, json, "task scope", "acceptance-required", "--acceptance named no valid criteria", EXIT.usage);
|
|
10008
|
-
}
|
|
10009
9710
|
const touches = (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(Boolean);
|
|
10010
9711
|
const badText = validateScopeText({ goal, outOfScope: text(flags, "not") ?? null, touches });
|
|
10011
9712
|
if (badText !== null)
|
|
@@ -10017,9 +9718,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10017
9718
|
: defaults?.buildPerRunMicrousd != null
|
|
10018
9719
|
? defaults.buildPerRunMicrousd / 1_000_000
|
|
10019
9720
|
: null;
|
|
10020
|
-
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
9721
|
+
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
10021
9722
|
return fail(write, json, "task scope", "bad-budget", "--budget-usd is a positive dollar amount", EXIT.usage);
|
|
10022
|
-
}
|
|
10023
9723
|
// v24 routing flags (foundations finding 5 — these used to be silently
|
|
10024
9724
|
// swallowed by the global parser): explicit flags resolve HERE, and an
|
|
10025
9725
|
// explicit ask that cannot resolve refuses rather than filing unresolved.
|
|
@@ -10028,9 +9728,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10028
9728
|
if (routingAsked) {
|
|
10029
9729
|
const scopeRef = store.lookupRef(id);
|
|
10030
9730
|
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") });
|
|
10031
|
-
if (!resolvedRouting.ok)
|
|
9731
|
+
if (!resolvedRouting.ok)
|
|
10032
9732
|
return fail(write, json, "task scope", resolvedRouting.reason, resolvedRouting.problem, EXIT.usage);
|
|
10033
|
-
}
|
|
10034
9733
|
explicitProfile = resolvedRouting.profile;
|
|
10035
9734
|
}
|
|
10036
9735
|
// C1/M3, the credentialed-CLI road: when --as/--token authenticate the
|
|
@@ -10106,9 +9805,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10106
9805
|
}
|
|
10107
9806
|
return { ok: true, scope: proposed, sealedUnderMode, modeRefusedCoordinator };
|
|
10108
9807
|
}));
|
|
10109
|
-
if (!filed.ok)
|
|
9808
|
+
if (!filed.ok)
|
|
10110
9809
|
return fail(write, json, "task scope", filed.reason, filed.message, EXIT.refused);
|
|
10111
|
-
}
|
|
10112
9810
|
const scope = filed.scope;
|
|
10113
9811
|
if (filed.modeRefusedCoordinator === true) {
|
|
10114
9812
|
return succeed(write, json, "task scope", { scope, approvedUnderMode: false, quarantined: true }, () => [
|
|
@@ -10158,12 +9856,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10158
9856
|
async function routeTaskCommand(positional, flags, context) {
|
|
10159
9857
|
const { store, write, json, clock } = context;
|
|
10160
9858
|
const id = positional[0];
|
|
10161
|
-
if (id === undefined)
|
|
9859
|
+
if (id === undefined)
|
|
10162
9860
|
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);
|
|
10163
|
-
|
|
10164
|
-
if (store.getTask(id) === null) {
|
|
9861
|
+
if (store.getTask(id) === null)
|
|
10165
9862
|
return fail(write, json, "task route", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10166
|
-
}
|
|
10167
9863
|
const ref = store.refFor(BUILT_IN, id);
|
|
10168
9864
|
const phaseGiven = text(flags, "phase");
|
|
10169
9865
|
const clearGiven = text(flags, "clear-phase");
|
|
@@ -10209,36 +9905,29 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10209
9905
|
if (!editing)
|
|
10210
9906
|
return show();
|
|
10211
9907
|
const acting = await askCredentials(flags, context);
|
|
10212
|
-
if (acting === null)
|
|
9908
|
+
if (acting === null)
|
|
10213
9909
|
return fail(write, json, "task route", "usage", "changing a route takes `--as <you> --token <t>` — it reroutes spend", EXIT.usage);
|
|
10214
|
-
|
|
10215
|
-
if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
|
|
9910
|
+
if (sizeGiven !== undefined && !isTaskSize(sizeGiven))
|
|
10216
9911
|
return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
|
|
10217
|
-
|
|
10218
|
-
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no") {
|
|
9912
|
+
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no")
|
|
10219
9913
|
return fail(write, json, "task route", "usage", "--risky is yes or no", EXIT.usage);
|
|
10220
|
-
}
|
|
10221
9914
|
// A person's size outranks the classifier's; an unnamed half keeps what the task has.
|
|
10222
9915
|
const sizeEdit = sizeGiven === undefined && riskyGiven === undefined ? undefined : {
|
|
10223
9916
|
size: sizeGiven ?? ref.sizing?.size ?? "medium",
|
|
10224
9917
|
risky: riskyGiven === undefined ? ref.sizing?.risky ?? false : riskyGiven === "yes",
|
|
10225
9918
|
};
|
|
10226
|
-
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
9919
|
+
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
10227
9920
|
return fail(write, json, "task route", "usage", "say --phase … --provider … --model … OR --clear-phase …, not both", EXIT.usage);
|
|
10228
|
-
}
|
|
10229
9921
|
const phase = phaseGiven ?? clearGiven;
|
|
10230
|
-
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
9922
|
+
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
10231
9923
|
return fail(write, json, "task route", "usage", `--${phaseGiven !== undefined ? "phase" : "clear-phase"} is one of ${ROUTE_PHASES.join(", ")}`, EXIT.usage);
|
|
10232
|
-
}
|
|
10233
9924
|
if (phaseGiven === "review")
|
|
10234
9925
|
return fail(write, json, "task route", "model-review-retired", "Separate model review has been removed; no reviewer route is needed.", EXIT.refused);
|
|
10235
9926
|
if (phaseGiven !== undefined) {
|
|
10236
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
9927
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
10237
9928
|
return fail(write, json, "task route", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
10238
|
-
|
|
10239
|
-
if (modelGiven === undefined || modelGiven === "") {
|
|
9929
|
+
if (modelGiven === undefined || modelGiven === "")
|
|
10240
9930
|
return fail(write, json, "task route", "usage", "an override names an exact --model — approvals bind exact routing", EXIT.usage);
|
|
10241
|
-
}
|
|
10242
9931
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
10243
9932
|
if (!valid.ok)
|
|
10244
9933
|
return fail(write, json, "task route", "invalid", valid.problem, EXIT.usage);
|
|
@@ -10256,9 +9945,8 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10256
9945
|
...(digestGiven === undefined ? {} : { expectDigest: digestGiven === "none" ? null : digestGiven }),
|
|
10257
9946
|
}, clock());
|
|
10258
9947
|
if (!edited.ok) {
|
|
10259
|
-
if (edited.reason === "unauthenticated")
|
|
9948
|
+
if (edited.reason === "unauthenticated")
|
|
10260
9949
|
return fail(write, json, "task route", edited.detail, describeApproveFailure(edited.detail, id), EXIT.refused);
|
|
10261
|
-
}
|
|
10262
9950
|
const code = { "no-task": "unknown-task", "live-claim": "claimed", changed: "changed", nothing: "usage", "not-configured": "not-configured" };
|
|
10263
9951
|
return fail(write, json, "task route", code[edited.reason], `${id}: ${edited.detail}`, edited.reason === "nothing" ? EXIT.usage : EXIT.refused);
|
|
10264
9952
|
}
|
|
@@ -10274,18 +9962,15 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10274
9962
|
async function approveTask(positional, flags, context) {
|
|
10275
9963
|
const { store, write, json, now } = context;
|
|
10276
9964
|
const id = positional[0];
|
|
10277
|
-
if (id === undefined)
|
|
9965
|
+
if (id === undefined)
|
|
10278
9966
|
return fail(write, json, "task approve", "usage", "which task?", EXIT.usage);
|
|
10279
|
-
}
|
|
10280
9967
|
const scope = store.getScope(id);
|
|
10281
|
-
if (scope === null)
|
|
9968
|
+
if (scope === null)
|
|
10282
9969
|
return fail(write, json, "task approve", "no-scope", `${id} has no scope to approve — write one first`, EXIT.refused);
|
|
10283
|
-
}
|
|
10284
9970
|
// Console parity (v47): a plan leg changed after a draft asks for a real
|
|
10285
9971
|
// re-plan — nothing approves the obsolete draft while the planner is owed.
|
|
10286
|
-
if (store.lookupRef(id)?.plan === "requested")
|
|
9972
|
+
if (store.lookupRef(id)?.plan === "requested")
|
|
10287
9973
|
return fail(write, json, "task approve", "planning", `${id} is waiting on a planner — approval is blocked until the drafted plan lands`, EXIT.refused);
|
|
10288
|
-
}
|
|
10289
9974
|
let saw = text(flags, "digest");
|
|
10290
9975
|
let { name: asWho, token } = credentialsFrom(flags, context);
|
|
10291
9976
|
let confirmedAloud = false;
|
|
@@ -10334,9 +10019,8 @@ async function approveTask(positional, flags, context) {
|
|
|
10334
10019
|
// Unconfirmed is "no, not yet" — exit 3 in both modes (round-4 finding 10).
|
|
10335
10020
|
return EXIT.refused;
|
|
10336
10021
|
}
|
|
10337
|
-
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10022
|
+
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10338
10023
|
return fail(write, json, "task approve", "usage", "approving non-interactively takes --yes --digest <d> --as <you> --token <t>", EXIT.usage);
|
|
10339
|
-
}
|
|
10340
10024
|
// The digest is named rather than assumed, so an operator who read one scope
|
|
10341
10025
|
// cannot approve a different one that replaced it while they were reading.
|
|
10342
10026
|
// The credential is required for a different reason: an agent that can run
|
|
@@ -10397,9 +10081,8 @@ export const PROPOSALS_ACTIONS = ["list", "confirm", "dismiss"];
|
|
|
10397
10081
|
async function proposalsCommand(positional, flags, context) {
|
|
10398
10082
|
const { store, write, json, now } = context;
|
|
10399
10083
|
const [action = "list", idGiven] = positional;
|
|
10400
|
-
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10084
|
+
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10401
10085
|
return fail(write, json, "proposals", "usage", `unknown \`proposals ${action}\` — try ${PROPOSALS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10402
|
-
}
|
|
10403
10086
|
store.sweepCoordinatorProposals(now);
|
|
10404
10087
|
let repos;
|
|
10405
10088
|
if (context.repoList !== undefined && context.repoList.length > 0) {
|
|
@@ -10476,9 +10159,8 @@ async function chatCommand(flags, context) {
|
|
|
10476
10159
|
if (flags.has("follow") && flags.has("no-follow"))
|
|
10477
10160
|
return fail(write, json, "chat", "usage", "Choose --follow or --no-follow.", EXIT.usage);
|
|
10478
10161
|
const credentials = await askCredentials(flags, context);
|
|
10479
|
-
if (credentials === null)
|
|
10162
|
+
if (credentials === null)
|
|
10480
10163
|
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);
|
|
10481
|
-
}
|
|
10482
10164
|
const ceilingGiven = text(flags, "ceiling-usd");
|
|
10483
10165
|
// The ceiling: the `--repo` list, or the ENROLLED registry beside the
|
|
10484
10166
|
// database (slice-2 review, finding 5) — never the opened-project history.
|
|
@@ -10634,9 +10316,8 @@ async function flowsCommand(positional, flags, context) {
|
|
|
10634
10316
|
async function approverCommand(positional, flags, context) {
|
|
10635
10317
|
const { store, write, json, now } = context;
|
|
10636
10318
|
const [action, name] = positional;
|
|
10637
|
-
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10319
|
+
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10638
10320
|
return fail(write, json, "approver", "usage", `unknown \`approver ${action}\` — try ${APPROVER_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10639
|
-
}
|
|
10640
10321
|
if (action === "list" || action === undefined) {
|
|
10641
10322
|
const approvers = store.listApprovers();
|
|
10642
10323
|
if (json) {
|
|
@@ -10653,9 +10334,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10653
10334
|
return EXIT.ok;
|
|
10654
10335
|
}
|
|
10655
10336
|
if (action === "add") {
|
|
10656
|
-
if (name === undefined)
|
|
10337
|
+
if (name === undefined)
|
|
10657
10338
|
return fail(write, json, "approver add", "usage", "an approver needs a name", EXIT.usage);
|
|
10658
|
-
}
|
|
10659
10339
|
let password = text(flags, "password");
|
|
10660
10340
|
const bootstrap = store.listApprovers().length === 0;
|
|
10661
10341
|
let by;
|
|
@@ -10668,9 +10348,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10668
10348
|
const chosen = await askHidden(`password for ${name} (enter to auto-generate): `);
|
|
10669
10349
|
if (chosen !== "") {
|
|
10670
10350
|
const again = await askHidden("again: ");
|
|
10671
|
-
if (again !== chosen)
|
|
10351
|
+
if (again !== chosen)
|
|
10672
10352
|
return fail(write, json, "approver add", "mismatch", "the two passwords did not match", EXIT.refused);
|
|
10673
|
-
}
|
|
10674
10353
|
password = chosen;
|
|
10675
10354
|
}
|
|
10676
10355
|
}
|
|
@@ -10707,17 +10386,14 @@ function holdTask(positional, flags, context) {
|
|
|
10707
10386
|
const { store, write, json, now } = context;
|
|
10708
10387
|
const id = positional[0];
|
|
10709
10388
|
const reason = text(flags, "reason");
|
|
10710
|
-
if (id === undefined || reason === undefined)
|
|
10389
|
+
if (id === undefined || reason === undefined)
|
|
10711
10390
|
return fail(write, json, "task hold", "usage", "`toolroll task hold <id> --reason <why>`", EXIT.usage);
|
|
10712
|
-
|
|
10713
|
-
if (store.getTask(id) === null) {
|
|
10391
|
+
if (store.getTask(id) === null)
|
|
10714
10392
|
return fail(write, json, "task hold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10715
|
-
}
|
|
10716
10393
|
const untilText = text(flags, "until");
|
|
10717
10394
|
const until = untilText === undefined ? null : new Date(untilText);
|
|
10718
|
-
if (until !== null && Number.isNaN(until.getTime()))
|
|
10395
|
+
if (until !== null && Number.isNaN(until.getTime()))
|
|
10719
10396
|
return fail(write, json, "task hold", "usage", "--until takes a date, e.g. 2026-08-12T09:00:00Z", EXIT.usage);
|
|
10720
|
-
}
|
|
10721
10397
|
store.hold(store.refFor(BUILT_IN, id).id, reason, until, now, mutationFrom(flags, now));
|
|
10722
10398
|
return succeed(write, json, "task hold", { id, reason, until: until?.toISOString() ?? null }, () => [
|
|
10723
10399
|
`${id} is on hold${until === null ? "" : ` until ${until.toISOString()}`}: ${reason}`,
|
|
@@ -10728,9 +10404,8 @@ function unholdTask(positional, flags, context) {
|
|
|
10728
10404
|
const id = positional[0];
|
|
10729
10405
|
if (id === undefined)
|
|
10730
10406
|
return fail(write, json, "task unhold", "usage", "which task?", EXIT.usage);
|
|
10731
|
-
if (store.getTask(id) === null)
|
|
10407
|
+
if (store.getTask(id) === null)
|
|
10732
10408
|
return fail(write, json, "task unhold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10733
|
-
}
|
|
10734
10409
|
const lifted = store.unhold(store.refFor(BUILT_IN, id).id, mutationFrom(flags, context.now));
|
|
10735
10410
|
if (!lifted)
|
|
10736
10411
|
return fail(write, json, "task unhold", "not-held", `${id} was not on hold`, EXIT.refused);
|
|
@@ -10750,19 +10425,14 @@ async function stopTaskCommand(positional, flags, context) {
|
|
|
10750
10425
|
const id = positional[0];
|
|
10751
10426
|
const runText = text(flags, "run");
|
|
10752
10427
|
const runId = Number(runText ?? "");
|
|
10753
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10428
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10754
10429
|
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);
|
|
10755
|
-
}
|
|
10756
10430
|
if (store.getTask(id) === null)
|
|
10757
10431
|
return fail(write, json, "task stop", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10758
10432
|
const acting = await askCredentials(flags, context);
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10763
|
-
if (!authenticated.ok) {
|
|
10764
|
-
return fail(write, json, "task stop", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10765
|
-
}
|
|
10433
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
10434
|
+
if (acting === null || !verified?.ok)
|
|
10435
|
+
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);
|
|
10766
10436
|
const asked = requestTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli" }, clock());
|
|
10767
10437
|
if (!asked.ok)
|
|
10768
10438
|
return fail(write, json, "task stop", asked.reason, asked.detail, EXIT.refused, { run: runId });
|
|
@@ -10790,26 +10460,20 @@ async function resumeTaskCommand(positional, flags, context) {
|
|
|
10790
10460
|
const id = positional[0];
|
|
10791
10461
|
const runText = text(flags, "run");
|
|
10792
10462
|
const runId = Number(runText ?? "");
|
|
10793
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10463
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10794
10464
|
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);
|
|
10795
|
-
}
|
|
10796
10465
|
if (store.getTask(id) === null)
|
|
10797
10466
|
return fail(write, json, "task resume", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10798
10467
|
const acting = await askCredentials(flags, context);
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10803
|
-
if (!authenticated.ok) {
|
|
10804
|
-
return fail(write, json, "task resume", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10805
|
-
}
|
|
10468
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
10469
|
+
if (acting === null || !verified?.ok)
|
|
10470
|
+
return refuseApprover(context, "task resume", verified, "resuming takes `--as <you> --token <t>` — it lets the next pass spend again", id);
|
|
10806
10471
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
10807
10472
|
const worktrees = new WorktreePool(store, { root: pool });
|
|
10808
10473
|
const resumed = resumeTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli", occupied: path => worktrees.inUse(path) }, clock());
|
|
10809
10474
|
if (!resumed.ok) {
|
|
10810
|
-
if (resumed.reason === "review")
|
|
10475
|
+
if (resumed.reason === "review")
|
|
10811
10476
|
return fail(write, json, "task resume", "review", `${resumed.detail} — separate model review has been removed; inspect the saved source result`, EXIT.refused, { run: runId });
|
|
10812
|
-
}
|
|
10813
10477
|
return fail(write, json, "task resume", resumed.reason, resumed.detail, EXIT.refused, { run: runId });
|
|
10814
10478
|
}
|
|
10815
10479
|
const control = taskControlOf(store, store.refFor(BUILT_IN, id).id, clock());
|
|
@@ -10838,21 +10502,18 @@ async function runCommand(positional, flags, context) {
|
|
|
10838
10502
|
const why = text(flags, "why")?.trim();
|
|
10839
10503
|
if (id === undefined || extra.length > 0 || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(runId))
|
|
10840
10504
|
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
10841
|
-
if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why))
|
|
10505
|
+
if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why))
|
|
10842
10506
|
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);
|
|
10843
|
-
}
|
|
10844
10507
|
const run = store.getRun(runId);
|
|
10845
10508
|
if (run === null)
|
|
10846
10509
|
return fail(write, json, command, "unknown-run", `no run #${runId}`, EXIT.refused);
|
|
10847
10510
|
const acting = await askCredentials(flags, context);
|
|
10848
|
-
if (acting === null)
|
|
10511
|
+
if (acting === null)
|
|
10849
10512
|
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);
|
|
10850
|
-
}
|
|
10851
10513
|
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10852
10514
|
const repo = store.refById(run.taskRef)?.repo ?? null;
|
|
10853
|
-
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10515
|
+
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10854
10516
|
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);
|
|
10855
|
-
}
|
|
10856
10517
|
const settled = store.settleRunWitnessesByApprover({ runId, by: acting.name, why }, clock());
|
|
10857
10518
|
if (!settled.ok)
|
|
10858
10519
|
return fail(write, json, command, settled.reason, settled.detail, EXIT.refused, { run: runId });
|
|
@@ -10879,9 +10540,7 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10879
10540
|
if (positional.length > 1 || repoFlag === undefined)
|
|
10880
10541
|
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);
|
|
10881
10542
|
// The projects Settings shows: those with work, and those registered (enrolled) with none yet.
|
|
10882
|
-
const
|
|
10883
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
10884
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag));
|
|
10543
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: false });
|
|
10885
10544
|
if (repo === undefined)
|
|
10886
10545
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10887
10546
|
const current = context.store.approvalRules(repo);
|
|
@@ -10901,9 +10560,8 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10901
10560
|
return fail(context.write, context.json, command, "usage", paths.problem, EXIT.usage);
|
|
10902
10561
|
const acting = await askCredentials(flags, context);
|
|
10903
10562
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
10904
|
-
if (acting === null ||
|
|
10563
|
+
if (acting === null || !verified?.ok || !context.store.isInstanceOperator(acting.name))
|
|
10905
10564
|
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);
|
|
10906
|
-
}
|
|
10907
10565
|
const next = {
|
|
10908
10566
|
notRequester: onOff === undefined ? current.notRequester : onOff === "on",
|
|
10909
10567
|
protectProject: protect === undefined ? (paths !== null && paths.ok ? false : current.protectProject) : protect === "project",
|
|
@@ -10926,12 +10584,9 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10926
10584
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
|
|
10927
10585
|
const repoFlag = text(flags, "repo");
|
|
10928
10586
|
const local = flags.get("local");
|
|
10929
|
-
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none"))
|
|
10587
|
+
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none"))
|
|
10930
10588
|
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);
|
|
10931
|
-
}
|
|
10932
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10933
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10934
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10589
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
10935
10590
|
if (repo === undefined)
|
|
10936
10591
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10937
10592
|
const current = context.store.projectNetwork(repo);
|
|
@@ -10961,24 +10616,55 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10961
10616
|
return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
|
|
10962
10617
|
const acting = await askCredentials(flags, context);
|
|
10963
10618
|
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10964
|
-
if (acting === null ||
|
|
10619
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
10965
10620
|
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);
|
|
10966
|
-
}
|
|
10967
10621
|
const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
|
|
10968
10622
|
return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
|
|
10969
10623
|
...words(changed.after),
|
|
10970
10624
|
"Scopes written from now on use this; scopes already approved keep the network they were approved with.",
|
|
10971
10625
|
]);
|
|
10972
10626
|
}
|
|
10627
|
+
/**
|
|
10628
|
+
* `project writes --repo <p> [workspace|anywhere]`: where the project's builds may write (writes-mode.ts). `workspace`:
|
|
10629
|
+
* only their checkout and its Git data, their caches, their temp folder and the provider state their CLI needs;
|
|
10630
|
+
* `anywhere`: anywhere outside Toolroll's fenced folders. New scopes seal it; approved ones keep theirs. Changing it is
|
|
10631
|
+
* an approver's act with a password typed now, kept in the ledger as before → after.
|
|
10632
|
+
*/
|
|
10633
|
+
async function projectWritesCommand(positional, flags, context) {
|
|
10634
|
+
const command = "project writes";
|
|
10635
|
+
const allowed = new Set(["repo", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
10636
|
+
for (const name of flags.keys())
|
|
10637
|
+
if (!allowed.has(name))
|
|
10638
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project writes option.`, EXIT.usage);
|
|
10639
|
+
const repoFlag = text(flags, "repo");
|
|
10640
|
+
const wanted = positional[1];
|
|
10641
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted)))
|
|
10642
|
+
return fail(context.write, context.json, command, "usage", "Use project writes --repo <project path> [workspace | anywhere].", EXIT.usage);
|
|
10643
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
10644
|
+
if (repo === undefined)
|
|
10645
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10646
|
+
const words = (one) => [`${repo}: builds write ${writesWords(one)}.`];
|
|
10647
|
+
const current = context.store.projectWrites(repo);
|
|
10648
|
+
if (wanted === undefined)
|
|
10649
|
+
return succeed(context.write, context.json, command, { repo, writes: current }, () => words(current));
|
|
10650
|
+
const acting = await askCredentials(flags, context);
|
|
10651
|
+
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10652
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
10653
|
+
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);
|
|
10654
|
+
const changed = context.store.setProjectWrites(repo, wanted, acting.name, context.clock());
|
|
10655
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, writes: changed.after }, () => [
|
|
10656
|
+
...words(changed.after),
|
|
10657
|
+
"Scopes written from now on use this; scopes already approved keep what they were approved with.",
|
|
10658
|
+
]);
|
|
10659
|
+
}
|
|
10973
10660
|
/** `config unconfined-agents [allow|refuse]`: whether a build may run on a machine that can't confine it at the OS
|
|
10974
10661
|
* (no macOS sandbox, no working bubblewrap). Refused unless an instance operator allows it, with a password. */
|
|
10975
10662
|
async function unconfinedAgentsCommand(positional, flags, context) {
|
|
10976
10663
|
const command = "config unconfined-agents";
|
|
10977
10664
|
const { store, write, json } = context;
|
|
10978
10665
|
const wanted = positional[1];
|
|
10979
|
-
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
10666
|
+
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
10980
10667
|
return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
|
|
10981
|
-
}
|
|
10982
10668
|
const words = (allowed) => [allowed
|
|
10983
10669
|
? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
|
|
10984
10670
|
: "Builds this machine can't confine at the OS don't start."];
|
|
@@ -10986,9 +10672,8 @@ async function unconfinedAgentsCommand(positional, flags, context) {
|
|
|
10986
10672
|
return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
|
|
10987
10673
|
const acting = await askCredentials(flags, context);
|
|
10988
10674
|
const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
|
|
10989
|
-
if (acting === null ||
|
|
10675
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
10990
10676
|
return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
|
|
10991
|
-
}
|
|
10992
10677
|
store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
|
|
10993
10678
|
return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
|
|
10994
10679
|
}
|
|
@@ -11002,9 +10687,7 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11002
10687
|
const repoFlag = text(flags, "repo");
|
|
11003
10688
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11004
10689
|
return fail(context.write, context.json, command, "usage", "Use project concurrency [<n>] --repo <project path>.", EXIT.usage);
|
|
11005
|
-
const
|
|
11006
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11007
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10690
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11008
10691
|
if (repo === undefined)
|
|
11009
10692
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11010
10693
|
const workers = context.store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(repo));
|
|
@@ -11020,9 +10703,8 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11020
10703
|
return fail(context.write, context.json, command, "usage", `Builds at once is a whole number from 1 to ${RUNNER_CAPACITY_MAX}.`, EXIT.usage);
|
|
11021
10704
|
const acting = await askCredentials(flags, context);
|
|
11022
10705
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11023
|
-
if (acting === null ||
|
|
10706
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11024
10707
|
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);
|
|
11025
|
-
}
|
|
11026
10708
|
const changed = saveProjectConcurrency(context.databaseFile, repo, n);
|
|
11027
10709
|
context.store.recordProjectConcurrency(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11028
10710
|
context.store.bumpWake();
|
|
@@ -11041,12 +10723,9 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11041
10723
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project checks option.`, EXIT.usage);
|
|
11042
10724
|
const repoFlag = text(flags, "repo");
|
|
11043
10725
|
const wanted = flags.get("batch");
|
|
11044
|
-
if (positional.length > 1 || repoFlag === undefined || (wanted !== undefined && wanted !== "on" && wanted !== "off"))
|
|
10726
|
+
if (positional.length > 1 || repoFlag === undefined || (wanted !== undefined && wanted !== "on" && wanted !== "off"))
|
|
11045
10727
|
return fail(context.write, context.json, command, "usage", "Use project checks [--batch on|off] --repo <project path>.", EXIT.usage);
|
|
11046
|
-
}
|
|
11047
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11048
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11049
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10728
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11050
10729
|
if (repo === undefined)
|
|
11051
10730
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11052
10731
|
const level = projectCheckLevel(context.store, repo).level;
|
|
@@ -11060,9 +10739,8 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11060
10739
|
}
|
|
11061
10740
|
const acting = await askCredentials(flags, context);
|
|
11062
10741
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token, repo);
|
|
11063
|
-
if (acting === null ||
|
|
10742
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11064
10743
|
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);
|
|
11065
|
-
}
|
|
11066
10744
|
const on = wanted === "on";
|
|
11067
10745
|
const changed = setProjectBatchChecks(context.store, repo, on, acting.name, context.clock());
|
|
11068
10746
|
return succeed(context.write, context.json, command, { repo, batch: on, before: changed.before, changed: changed.changed, windowMinutes: BATCH_WINDOW_MS / 60_000, level }, () => [
|
|
@@ -11081,9 +10759,7 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11081
10759
|
const repoFlag = text(flags, "repo");
|
|
11082
10760
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11083
10761
|
return fail(context.write, context.json, command, "usage", "Use project demo [<url>|off] --repo <project path>.", EXIT.usage);
|
|
11084
|
-
const
|
|
11085
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11086
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10762
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11087
10763
|
if (repo === undefined)
|
|
11088
10764
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11089
10765
|
const words = (url) => url === null ? `${repo}: no demo; scouts screenshot public pages only.` : `${repo}: scouts may also screenshot ${url}.`;
|
|
@@ -11097,9 +10773,8 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11097
10773
|
return fail(context.write, context.json, command, "usage", "The demo is an http or https address without a sign-in, or off.", EXIT.usage);
|
|
11098
10774
|
const acting = await askCredentials(flags, context);
|
|
11099
10775
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11100
|
-
if (acting === null ||
|
|
10776
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11101
10777
|
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);
|
|
11102
|
-
}
|
|
11103
10778
|
const changed = saveProjectDemo(context.databaseFile, repo, url);
|
|
11104
10779
|
context.store.recordProjectDemo(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11105
10780
|
return succeed(context.write, context.json, command, { repo, before: changed.before, demoUrl: changed.after }, () => [words(changed.after)]);
|
|
@@ -11139,9 +10814,8 @@ async function projectDeleteCommand(positional, flags, context) {
|
|
|
11139
10814
|
const store = context.store;
|
|
11140
10815
|
const acting = await askCredentials(flags, context);
|
|
11141
10816
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11142
|
-
if (acting === null ||
|
|
10817
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11143
10818
|
return fail(context.write, context.json, command, "refused", "An instance operator deletes projects: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11144
|
-
}
|
|
11145
10819
|
const holdings = projectHoldings(store, repo);
|
|
11146
10820
|
const running = projectRunning(store, repo, context.clock());
|
|
11147
10821
|
if (running.length > 0)
|
|
@@ -11244,9 +10918,8 @@ async function restoreCommand(file, positional, flags, write, json, now) {
|
|
|
11244
10918
|
catch (error) {
|
|
11245
10919
|
return fail(write, json, command, "failed", `The restore failed: ${describe(error)}. The database is unchanged.`, EXIT.failed);
|
|
11246
10920
|
}
|
|
11247
|
-
if (!report.ok)
|
|
10921
|
+
if (!report.ok)
|
|
11248
10922
|
return fail(write, json, command, report.refusals[0]?.reason ?? "refused", ["Nothing was restored.", ...report.refusals.map(one => ` ${one.words}`)].join("\n"), EXIT.refused, { report });
|
|
11249
|
-
}
|
|
11250
10923
|
const checked = `schema ${report.schemaVersion}, ledger chain verified (${report.ledger?.entries ?? 0} entries)`;
|
|
11251
10924
|
return succeed(write, json, command, { report }, () => dryRun
|
|
11252
10925
|
? [`Dry run: ${report.file} can be restored (${checked}). Nothing was changed.`]
|
|
@@ -11307,9 +10980,8 @@ async function budgetCommand(positional, flags, context) {
|
|
|
11307
10980
|
const target = chosen[0];
|
|
11308
10981
|
const acting = await askCredentials(flags, context);
|
|
11309
10982
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11310
|
-
if (acting === null ||
|
|
10983
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11311
10984
|
return fail(context.write, context.json, command, "refused", "An instance operator sets budgets: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11312
|
-
}
|
|
11313
10985
|
const known = target.scope === "installation" || (target.scope === "project" ? store.knownRepos().includes(target.key)
|
|
11314
10986
|
: target.scope === "person" ? store.accountFacts().some(one => one.name === target.key && one.revokedAt === null)
|
|
11315
10987
|
: store.handle.prepare("SELECT 1 FROM subagent WHERE id = ?").get(Number(target.key)) !== undefined);
|
|
@@ -11340,18 +11012,16 @@ async function notificationsCommand(positional, flags, context) {
|
|
|
11340
11012
|
return fail(context.write, context.json, command, "usage", `--${name} is not a notifications option.`, EXIT.usage);
|
|
11341
11013
|
const usage = "Use notifications quiet, notifications all, notifications digest <HH:MM>|off, notifications screenshots off|first|all, or notifications mute|unmute --repo <project>.";
|
|
11342
11014
|
const valued = action === "digest" || action === "screenshots";
|
|
11343
|
-
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest", "screenshots", "mute", "unmute"].includes(action)) || valued !== (value !== undefined) || (muting && (value !== undefined || text(flags, "repo") === undefined)))
|
|
11015
|
+
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest", "screenshots", "mute", "unmute"].includes(action)) || valued !== (value !== undefined) || (muting && (value !== undefined || text(flags, "repo") === undefined)))
|
|
11344
11016
|
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11345
|
-
}
|
|
11346
11017
|
if (action === "screenshots" && !RESULT_SCREENSHOTS.includes(value))
|
|
11347
11018
|
return fail(context.write, context.json, command, "usage", "Screenshots with results is off, first (the first one) or all (up to 4).", EXIT.usage);
|
|
11348
11019
|
if (action === "digest" && value !== "off" && !isDigestTime(value))
|
|
11349
11020
|
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);
|
|
11350
11021
|
const acting = await askCredentials(flags, context);
|
|
11351
11022
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11352
|
-
if (acting === null ||
|
|
11023
|
+
if (acting === null || !verified?.ok)
|
|
11353
11024
|
return fail(context.write, context.json, command, "refused", "Notifications are set per person: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11354
|
-
}
|
|
11355
11025
|
const now = context.clock();
|
|
11356
11026
|
if (muting) {
|
|
11357
11027
|
const typed = text(flags, "repo");
|
|
@@ -11409,17 +11079,15 @@ async function retentionCommand(positional, flags, context) {
|
|
|
11409
11079
|
if (action !== "set")
|
|
11410
11080
|
return fail(context.write, context.json, command, "usage", "Use retention show, retention preview or retention set <kind> <period>.", EXIT.usage);
|
|
11411
11081
|
const [kind, period, ...extra] = rest;
|
|
11412
|
-
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11082
|
+
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11413
11083
|
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);
|
|
11414
|
-
}
|
|
11415
11084
|
const days = parsePeriod(period);
|
|
11416
11085
|
if (days === undefined)
|
|
11417
11086
|
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);
|
|
11418
11087
|
const acting = await askCredentials(flags, context);
|
|
11419
11088
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11420
|
-
if (acting === null ||
|
|
11089
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11421
11090
|
return fail(context.write, context.json, command, "refused", "An instance operator sets retention: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11422
|
-
}
|
|
11423
11091
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
11424
11092
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
11425
11093
|
}
|
|
@@ -11708,9 +11376,8 @@ async function policyCommand(positional, flags, context) {
|
|
|
11708
11376
|
return fail(context.write, context.json, command, "usage", "Say what to set: --providers, --models, --tools or --ceiling.", EXIT.usage);
|
|
11709
11377
|
const acting = await askCredentials(flags, context);
|
|
11710
11378
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11711
|
-
if (acting === null ||
|
|
11379
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11712
11380
|
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);
|
|
11713
|
-
}
|
|
11714
11381
|
const current = store.orgPolicy();
|
|
11715
11382
|
const listFlag = (name, now) => {
|
|
11716
11383
|
if (!flags.has(name))
|
|
@@ -11748,9 +11415,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11748
11415
|
for (const name of flags.keys())
|
|
11749
11416
|
if (!allowed.has(name))
|
|
11750
11417
|
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
11751
|
-
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11418
|
+
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11752
11419
|
return fail(context.write, context.json, command, "usage", "Use ledger verify, ledger checkpoint or ledger export --from <day> --to <day>.", EXIT.usage);
|
|
11753
|
-
}
|
|
11754
11420
|
const store = context.store;
|
|
11755
11421
|
if (action === "verify") {
|
|
11756
11422
|
// The command walks the whole chain, every time.
|
|
@@ -11770,9 +11436,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11770
11436
|
if (action === "checkpoint") {
|
|
11771
11437
|
const acting = await askCredentials(flags, context);
|
|
11772
11438
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11773
|
-
if (acting === null ||
|
|
11439
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11774
11440
|
return fail(context.write, context.json, command, "refused", "An instance operator makes checkpoints: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11775
|
-
}
|
|
11776
11441
|
const made = store.ledgerCheckpoint(acting.name, context.clock());
|
|
11777
11442
|
if (made === null)
|
|
11778
11443
|
return fail(context.write, context.json, command, "empty", "The ledger has no entries yet.", EXIT.refused);
|
|
@@ -11876,3 +11541,29 @@ function databaseFailure(write, json, command, file, error) {
|
|
|
11876
11541
|
return fail(write, json, command, "database", describe(error), EXIT.failed);
|
|
11877
11542
|
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);
|
|
11878
11543
|
}
|
|
11544
|
+
/**
|
|
11545
|
+
* The refusal every password-checked CLI act shares: no name or password given is a usage error with the act's own
|
|
11546
|
+
* hint; a check that failed says why, about `subject`. The check itself stays at each call site.
|
|
11547
|
+
*/
|
|
11548
|
+
function refuseApprover(context, command, verified, usage, subject) {
|
|
11549
|
+
if (verified === null)
|
|
11550
|
+
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11551
|
+
const reason = verified.ok ? "" : verified.reason;
|
|
11552
|
+
return fail(context.write, context.json, command, reason, describeApproveFailure(reason, subject ?? ""), EXIT.refused);
|
|
11553
|
+
}
|
|
11554
|
+
/** The project `--repo` names among those Toolroll knows: the database's, the listed projects' (when `listed`) and the
|
|
11555
|
+
* registry's, matched as given, resolved, or (when `canonical`) canonical. */
|
|
11556
|
+
async function knownProject(context, repoFlag, match) {
|
|
11557
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11558
|
+
const known = [...new Set([...context.store.knownRepos(), ...(match.listed ? context.store.listProjects().map(one => one.path) : []), ...("error" in registered ? [] : registered.repos)])];
|
|
11559
|
+
return known.find(one => one === repoFlag || one === resolve(repoFlag) || (match.canonical && one === canonicalProject(repoFlag)));
|
|
11560
|
+
}
|
|
11561
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11562
|
+
/** The value flags a verb reads under a global switch's name, for parseOperateArgs. */
|
|
11563
|
+
export function ownValueFlags(command, argv) {
|
|
11564
|
+
if (command === "flows")
|
|
11565
|
+
return FLOWS_VALUE_FLAGS;
|
|
11566
|
+
if (command === "project" && argv.find(arg => !arg.startsWith("-")) === "network")
|
|
11567
|
+
return PROJECT_NETWORK_VALUE_FLAGS;
|
|
11568
|
+
return undefined;
|
|
11569
|
+
}
|