toolroll 0.9.67 → 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 +36 -0
- package/dist/acp-client.js +1 -3
- package/dist/agent-board.d.ts +0 -1
- package/dist/agent-board.js +0 -1
- package/dist/agent-fence.js +3 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +31 -29
- package/dist/builder.js +3 -26
- 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/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 +0 -3
- package/dist/invoke.js +5 -3
- package/dist/is-record.d.ts +2 -0
- package/dist/is-record.js +2 -0
- package/dist/lead-doors.d.ts +1 -5
- package/dist/lead-doors.js +0 -4
- package/dist/lead-tools.js +3 -6
- package/dist/live-detail.d.ts +0 -1
- package/dist/live-detail.js +0 -6
- package/dist/live-terminal.js +2 -1
- package/dist/live.d.ts +0 -1
- package/dist/live.js +1 -2
- package/dist/mcp-core.js +0 -2
- package/dist/memory-pass.js +1 -1
- package/dist/model-catalog.js +1 -1
- package/dist/modes.js +1 -12
- package/dist/operate.js +383 -750
- package/dist/password-bearer.d.ts +0 -2
- package/dist/password-bearer.js +0 -4
- package/dist/peek-cli.js +2 -1
- package/dist/peek.d.ts +2 -26
- package/dist/peek.js +2 -168
- package/dist/phase-routing.d.ts +0 -4
- package/dist/phase-routing.js +2 -21
- package/dist/planner-source.d.ts +2 -1
- package/dist/planner-source.js +2 -12
- package/dist/planner.js +2 -14
- package/dist/process-tree.d.ts +0 -2
- package/dist/process-tree.js +0 -10
- package/dist/project-delete.d.ts +0 -2
- package/dist/project-delete.js +1 -3
- package/dist/project-learning.d.ts +0 -14
- package/dist/project-skills.d.ts +0 -1
- package/dist/project-skills.js +0 -14
- package/dist/project-tools.js +2 -10
- package/dist/provider.d.ts +0 -17
- package/dist/provider.js +0 -26
- package/dist/pulls.d.ts +0 -2
- package/dist/pulls.js +0 -9
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +15 -5
- package/dist/remote-exec.d.ts +3 -2
- package/dist/remote-exec.js +3 -2
- package/dist/repos.d.ts +0 -5
- package/dist/repos.js +2 -10
- package/dist/repository-context.d.ts +0 -2
- package/dist/repository-context.js +0 -4
- package/dist/restart-certification.d.ts +0 -2
- package/dist/restart-certification.js +1 -5
- package/dist/result-follow-ups.d.ts +1 -4
- package/dist/result-follow-ups.js +1 -6
- 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.js +1 -13
- package/dist/scout-browser.js +2 -1
- package/dist/serve.js +2 -2
- package/dist/server/console.css +5 -133
- 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-results.d.ts +0 -6
- package/dist/server/render-results.js +3 -9
- package/dist/server/render-tasks.js +4 -5
- package/dist/server/route-table.d.ts +0 -4
- package/dist/server/route-table.js +0 -6
- package/dist/server/settings.js +23 -23
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +3 -3
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills.d.ts +0 -6
- package/dist/skills.js +0 -8
- package/dist/slack-chat.d.ts +1 -7
- package/dist/slack-chat.js +5 -18
- package/dist/slack-settings.d.ts +2 -6
- package/dist/slack-settings.js +10 -38
- package/dist/slack.js +12 -23
- package/dist/sleep.d.ts +2 -0
- package/dist/sleep.js +2 -0
- package/dist/store.d.ts +0 -73
- package/dist/store.js +3 -177
- 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/telegram-lead.js +2 -2
- package/dist/telegram.js +1 -1
- package/dist/token-notices.d.ts +0 -11
- package/dist/token-notices.js +1 -5
- package/dist/tool-secret-store.d.ts +0 -3
- package/dist/tool-secret-store.js +2 -11
- package/dist/toolroll-update.d.ts +0 -2
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +0 -1
- package/dist/verification-evidence.js +0 -1
- package/dist/webhooks.d.ts +0 -1
- package/dist/webhooks.js +0 -1
- package/dist/workspace-ui.d.ts +0 -8
- package/dist/workspace-ui.js +1 -6
- package/package.json +1 -1
- package/dist/contracts/native-census.d.ts +0 -42
- package/dist/contracts/native-census.js +0 -36
- package/dist/process-recovery-anchor.d.ts +0 -75
- package/dist/process-recovery-anchor.js +0 -188
- package/dist/process-recovery-coalition.d.ts +0 -25
- package/dist/process-recovery-coalition.js +0 -105
- package/dist/process-recovery-eligibility.d.ts +0 -179
- package/dist/process-recovery-eligibility.js +0 -174
- package/dist/process-recovery-native.d.ts +0 -161
- package/dist/process-recovery-native.js +0 -595
- package/dist/process-recovery-provenance.d.ts +0 -163
- package/dist/process-recovery-provenance.js +0 -292
- package/dist/process-recovery-services.d.ts +0 -56
- package/dist/process-recovery-services.js +0 -191
- package/dist/process-recovery-settlement.d.ts +0 -32
- package/dist/process-recovery-settlement.js +0 -133
- package/dist/process-recovery.d.ts +0 -41
- package/dist/process-recovery.js +0 -91
- package/dist/session-brief.d.ts +0 -8
- package/dist/session-brief.js +0 -24
- package/dist/session-cli.d.ts +0 -42
- package/dist/session-cli.js +0 -271
- package/dist/session-contract.d.ts +0 -183
- package/dist/session-contract.js +0 -122
- package/dist/session-remote.d.ts +0 -19
- package/dist/session-remote.js +0 -86
- package/dist/session-service.d.ts +0 -22
- package/dist/session-service.js +0 -157
- package/dist/team-cli.js +0 -692
package/dist/operate.js
CHANGED
|
@@ -9,7 +9,7 @@ import { checkPublicCommand } from "./public-check.js";
|
|
|
9
9
|
import { leadNameOf } from "./lead-identity.js";
|
|
10
10
|
import { maybeTriggerRepair } from "./dispose.js";
|
|
11
11
|
import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
|
|
12
|
-
import { fileAddTestsTask,
|
|
12
|
+
import { fileAddTestsTask, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
|
|
13
13
|
import { runBatchChecks } from "./batch-checks.js";
|
|
14
14
|
import { BATCH_HINT, BATCH_WINDOW_MS, projectBatchChecks, setProjectBatchChecks } from "./batch-policy.js";
|
|
15
15
|
import { buildReviewPass } from "./build-review.js";
|
|
@@ -97,13 +97,13 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
97
97
|
import { homedir, hostname } from "node:os";
|
|
98
98
|
import { basename, dirname, join, resolve } from "node:path";
|
|
99
99
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
100
|
-
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN,
|
|
100
|
+
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
|
|
101
101
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
102
102
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
103
103
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
104
104
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
105
105
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
106
|
-
import {
|
|
106
|
+
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
|
|
107
107
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
108
108
|
import { pushPass } from "./push.js";
|
|
109
109
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
|
|
@@ -120,7 +120,7 @@ import { spawn as spawnChild } from "node:child_process";
|
|
|
120
120
|
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
121
121
|
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
122
122
|
import { auditCommand } from "./audit-cli.js";
|
|
123
|
-
import { closedQuestionWords, hasDisguisedText,
|
|
123
|
+
import { closedQuestionWords, hasDisguisedText, validateNote } from "./decision.js";
|
|
124
124
|
import { readVerifiedArtifact, readVerifiedReport, storeEvidence } from "./evidence.js";
|
|
125
125
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges, encodePlannerSource, plannerSourceOf } from "./planner-source.js";
|
|
126
126
|
/**
|
|
@@ -162,7 +162,7 @@ function planContractLines(store, evidenceRoot, taskId) {
|
|
|
162
162
|
];
|
|
163
163
|
}
|
|
164
164
|
import { verdictWords as proofVerdictWords, matrixWords, semanticCoverage, coverageWords } from "./proof.js";
|
|
165
|
-
import { probeRepo
|
|
165
|
+
import { probeRepo } from "./probe.js";
|
|
166
166
|
import { diagnoseTaskDispatch, diagnosisIsDispatchable, scopeApprovedForDispatch, } from "./dispatch.js";
|
|
167
167
|
import { createDecisionServer } from "./serve.js";
|
|
168
168
|
import { readTailnetNames } from "./tailnet.js";
|
|
@@ -178,7 +178,7 @@ import { activeRemote, REMOTE_MESSAGES, REMOTE_REFUSED_FLAGS, REMOTE_SCOPES, Rem
|
|
|
178
178
|
import { projectAuthority } from "./project-access.js";
|
|
179
179
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
180
180
|
import { tally, spendLine } from "./summary.js";
|
|
181
|
-
import {
|
|
181
|
+
import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } from "./publish.js";
|
|
182
182
|
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
183
183
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
184
184
|
import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
|
|
@@ -189,7 +189,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
189
189
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
190
190
|
import { serveMcp } from "./mcp.js";
|
|
191
191
|
import { createInterface } from "node:readline";
|
|
192
|
-
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";
|
|
193
193
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
194
194
|
import { WorktreePool } from "./worktree.js";
|
|
195
195
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -217,7 +217,7 @@ import { plan as planTask } from "./planner.js";
|
|
|
217
217
|
import { attachTmux, elapsedWords, openInTmux, PEEK_TAIL_LINES, runPeek, snapshotLiveRuns } from "./peek-cli.js";
|
|
218
218
|
import { scout as scoutTask } from "./scout.js";
|
|
219
219
|
import { profileDigestOf } from "./scope.js";
|
|
220
|
-
import { PROVIDER_KEY_ENV,
|
|
220
|
+
import { PROVIDER_KEY_ENV, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
|
|
221
221
|
import { run, terminateLiveProviders, run as execRun } from "./exec.js";
|
|
222
222
|
import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
223
223
|
import { readPulls } from "./pulls.js";
|
|
@@ -1356,9 +1356,8 @@ async function readyCommand(flags, context) {
|
|
|
1356
1356
|
if (backendName !== BUILT_IN) {
|
|
1357
1357
|
const repo = repoFrom(flags);
|
|
1358
1358
|
const backend = openBackend(backendName, store, repo);
|
|
1359
|
-
if (backend === null)
|
|
1359
|
+
if (backend === null)
|
|
1360
1360
|
return fail(write, json, "ready", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
1361
|
-
}
|
|
1362
1361
|
const result = await backend.listReady();
|
|
1363
1362
|
if (!result.ok)
|
|
1364
1363
|
return fail(write, json, "ready", result.reason, result.message, EXIT.failed);
|
|
@@ -1443,17 +1442,14 @@ function claimCommand(positional, flags, context) {
|
|
|
1443
1442
|
// protects those leases would be protecting the wrong thing. This is what
|
|
1444
1443
|
// "auth from the first commit" is for — it is only cheap now.
|
|
1445
1444
|
const token = text(flags, "token");
|
|
1446
|
-
if (token === undefined)
|
|
1445
|
+
if (token === undefined)
|
|
1447
1446
|
return fail(write, json, "claim", "usage", "--token proves the runner is who it says", EXIT.usage);
|
|
1448
|
-
}
|
|
1449
1447
|
const auth = authenticate(store, runner, token);
|
|
1450
|
-
if (!auth.ok)
|
|
1448
|
+
if (!auth.ok)
|
|
1451
1449
|
return fail(write, json, "claim", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
1452
|
-
}
|
|
1453
1450
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
1454
|
-
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1451
|
+
if (backend === BUILT_IN && store.getTask(id) === null)
|
|
1455
1452
|
return fail(write, json, "claim", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
1456
|
-
}
|
|
1457
1453
|
const ttl = readTtl(flags);
|
|
1458
1454
|
if (ttl === null)
|
|
1459
1455
|
return fail(write, json, "claim", "usage", "--ttl takes whole seconds", EXIT.usage);
|
|
@@ -1475,9 +1471,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1475
1471
|
// tasks" while letting the asker declare which those are is not a rule.
|
|
1476
1472
|
origin: store.originOf(backend, id),
|
|
1477
1473
|
});
|
|
1478
|
-
if (!verdict.ok)
|
|
1474
|
+
if (!verdict.ok)
|
|
1479
1475
|
return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
|
|
1480
|
-
}
|
|
1481
1476
|
}
|
|
1482
1477
|
const ref = store.refFor(backend, id);
|
|
1483
1478
|
const result = acquire(store, ref.id, runner, {
|
|
@@ -1489,9 +1484,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1489
1484
|
if (!result.ok) {
|
|
1490
1485
|
// Two distinct refusals: somebody HOLDS it right now, or it is
|
|
1491
1486
|
// RESERVED for a specific worker whoever asks (queue columns, v19).
|
|
1492
|
-
if (result.reason === "reserved")
|
|
1487
|
+
if (result.reason === "reserved")
|
|
1493
1488
|
return fail(write, json, "claim", "reserved", `${id} is reserved for ${result.reservedFor} — only that worker takes it`, EXIT.refused, { reservedFor: result.reservedFor });
|
|
1494
|
-
}
|
|
1495
1489
|
if (result.reason === "external") {
|
|
1496
1490
|
const said = {
|
|
1497
1491
|
"stale-mirror": "this tracker item has not been seen recently — run `toolroll sync` first",
|
|
@@ -1503,24 +1497,18 @@ function claimCommand(positional, flags, context) {
|
|
|
1503
1497
|
detail: result.detail,
|
|
1504
1498
|
});
|
|
1505
1499
|
}
|
|
1506
|
-
if (result.reason === "mode-ended")
|
|
1500
|
+
if (result.reason === "mode-ended")
|
|
1507
1501
|
return fail(write, json, "claim", "mode-ended", result.message, EXIT.refused);
|
|
1508
|
-
|
|
1509
|
-
if (result.reason === "unauthenticated") {
|
|
1502
|
+
if (result.reason === "unauthenticated")
|
|
1510
1503
|
return fail(write, json, "claim", "unauthenticated", describeAuth(result.detail, runner), EXIT.refused);
|
|
1511
|
-
|
|
1512
|
-
if (result.reason === "unplaced") {
|
|
1504
|
+
if (result.reason === "unplaced")
|
|
1513
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);
|
|
1514
|
-
|
|
1515
|
-
if (result.reason === "unauthorized-repo") {
|
|
1506
|
+
if (result.reason === "unauthorized-repo")
|
|
1516
1507
|
return fail(write, json, "claim", "unauthorized-repo", `this runner is not bound to ${result.repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
1517
|
-
|
|
1518
|
-
if (result.reason === "coordinator-filed") {
|
|
1508
|
+
if (result.reason === "coordinator-filed")
|
|
1519
1509
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1520
|
-
|
|
1521
|
-
if (result.reason === "sweep-limit") {
|
|
1510
|
+
if (result.reason === "sweep-limit")
|
|
1522
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 });
|
|
1523
|
-
}
|
|
1524
1512
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1525
1513
|
}
|
|
1526
1514
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1640,9 +1628,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1640
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);
|
|
1641
1629
|
}
|
|
1642
1630
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1643
|
-
if (!authenticated.ok)
|
|
1631
|
+
if (!authenticated.ok)
|
|
1644
1632
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1645
|
-
}
|
|
1646
1633
|
const repos = context.repoList ?? [];
|
|
1647
1634
|
if (repos.length === 0) {
|
|
1648
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);
|
|
@@ -1652,9 +1639,8 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1652
1639
|
// a worker, no paste step. The file must not already exist: a second
|
|
1653
1640
|
// mint never silently overwrites a credential a running watch holds.
|
|
1654
1641
|
const tokenFile = text(flags, "token-file");
|
|
1655
|
-
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1642
|
+
if (tokenFile !== undefined && existsSync(tokenFile))
|
|
1656
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);
|
|
1657
|
-
}
|
|
1658
1644
|
const { runner, token, reclaimed } = register(store, {
|
|
1659
1645
|
name,
|
|
1660
1646
|
host: hostname(),
|
|
@@ -1694,13 +1680,11 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1694
1680
|
}
|
|
1695
1681
|
if (action === "heartbeat") {
|
|
1696
1682
|
const token = text(flags, "token");
|
|
1697
|
-
if (name === undefined || token === undefined)
|
|
1683
|
+
if (name === undefined || token === undefined)
|
|
1698
1684
|
return fail(write, json, "runner heartbeat", "usage", "`runner heartbeat <name> --token <token>`", EXIT.usage);
|
|
1699
|
-
}
|
|
1700
1685
|
const result = heartbeatRunner(store, name, token, now);
|
|
1701
|
-
if (!result.ok)
|
|
1686
|
+
if (!result.ok)
|
|
1702
1687
|
return fail(write, json, "runner heartbeat", result.reason, describeAuth(result.reason, name), EXIT.refused);
|
|
1703
|
-
}
|
|
1704
1688
|
return succeed(write, json, "runner heartbeat", { runner: result.runner }, () => [
|
|
1705
1689
|
`${name} checked in.`,
|
|
1706
1690
|
]);
|
|
@@ -1735,22 +1719,16 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1735
1719
|
return EXIT.ok;
|
|
1736
1720
|
}
|
|
1737
1721
|
if (action === "retire") {
|
|
1738
|
-
if (name === undefined)
|
|
1722
|
+
if (name === undefined)
|
|
1739
1723
|
return fail(write, json, "runner retire", "usage", "which runner?", EXIT.usage);
|
|
1740
|
-
}
|
|
1741
1724
|
// Retiring is revoking authority — the same operator act as minting it.
|
|
1742
1725
|
const acting = await askCredentials(flags, context);
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1747
|
-
if (!authenticated.ok) {
|
|
1748
|
-
return fail(write, json, "runner retire", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1749
|
-
}
|
|
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);
|
|
1750
1729
|
const retired = store.retireRunner(name, now, mutationFrom(flags, now));
|
|
1751
|
-
if (!retired)
|
|
1730
|
+
if (!retired)
|
|
1752
1731
|
return fail(write, json, "runner retire", "unknown", `no runner \`${name}\``, EXIT.refused);
|
|
1753
|
-
}
|
|
1754
1732
|
return succeed(write, json, "runner retire", { name }, () => [`${name} is retired.`]);
|
|
1755
1733
|
}
|
|
1756
1734
|
if (action === "bind") {
|
|
@@ -1766,17 +1744,15 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1766
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);
|
|
1767
1745
|
}
|
|
1768
1746
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1769
|
-
if (!authenticated.ok)
|
|
1747
|
+
if (!authenticated.ok)
|
|
1770
1748
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1771
|
-
}
|
|
1772
1749
|
const repos = context.repoList ?? [];
|
|
1773
1750
|
if (repos.length === 0) {
|
|
1774
1751
|
return fail(write, json, "runner bind", "usage", "--repo names at least one repository", EXIT.usage);
|
|
1775
1752
|
}
|
|
1776
1753
|
const bound = store.bindRunnerRepos(name, canonicalRepos(repos), now);
|
|
1777
|
-
if (!bound.ok)
|
|
1754
|
+
if (!bound.ok)
|
|
1778
1755
|
return fail(write, json, "runner bind", bound.reason, describeAuth(bound.reason, name), EXIT.refused);
|
|
1779
|
-
}
|
|
1780
1756
|
return succeed(write, json, "runner bind", { name, repos: bound.repos }, () => [
|
|
1781
1757
|
`${name} may build in:`,
|
|
1782
1758
|
...bound.repos.map(one => ` ${one}`),
|
|
@@ -1787,22 +1763,18 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1787
1763
|
// ledger as before → after. It never stops running work: claims already
|
|
1788
1764
|
// held keep going, and the new number applies at the next claim.
|
|
1789
1765
|
const given = positional[2];
|
|
1790
|
-
if (name === undefined || given === undefined || positional.length > 3)
|
|
1766
|
+
if (name === undefined || given === undefined || positional.length > 3)
|
|
1791
1767
|
return fail(write, json, "runner capacity", "usage", "`runner capacity <name> <n> --as <you> --token <t>`", EXIT.usage);
|
|
1792
|
-
}
|
|
1793
1768
|
const capacity = parseRunnerCapacity(given, { allowZero: true });
|
|
1794
|
-
if (capacity === null)
|
|
1769
|
+
if (capacity === null)
|
|
1795
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);
|
|
1796
|
-
}
|
|
1797
1771
|
const acting = await askCredentials(flags, context);
|
|
1798
1772
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
1799
|
-
if (acting === null ||
|
|
1773
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
1800
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);
|
|
1801
|
-
}
|
|
1802
1775
|
const changed = store.setRunnerCapacity(name, capacity, acting.name, now);
|
|
1803
|
-
if (!changed.ok)
|
|
1776
|
+
if (!changed.ok)
|
|
1804
1777
|
return fail(write, json, "runner capacity", changed.reason, changed.reason === "retired" ? `${name} is retired` : `no runner \`${name}\``, EXIT.refused);
|
|
1805
|
-
}
|
|
1806
1778
|
const running = store.liveClaimCount(name, now);
|
|
1807
1779
|
if (changed.after === 0) {
|
|
1808
1780
|
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
@@ -1852,9 +1824,8 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1852
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);
|
|
1853
1825
|
}
|
|
1854
1826
|
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1855
|
-
if (!authenticated.ok)
|
|
1827
|
+
if (!authenticated.ok)
|
|
1856
1828
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1857
|
-
}
|
|
1858
1829
|
const repos = context.repoList ?? [];
|
|
1859
1830
|
if (repos.length === 0) {
|
|
1860
1831
|
return fail(write, json, "coordinator mint", "usage", "--repo names at least one repository this coordinator may file into", EXIT.usage);
|
|
@@ -1892,17 +1863,12 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1892
1863
|
]);
|
|
1893
1864
|
}
|
|
1894
1865
|
if (action === "revoke") {
|
|
1895
|
-
if (name === undefined)
|
|
1866
|
+
if (name === undefined)
|
|
1896
1867
|
return fail(write, json, "coordinator revoke", "usage", "which coordinator? (`coordinator list` shows cids)", EXIT.usage);
|
|
1897
|
-
}
|
|
1898
1868
|
const acting = await askCredentials(flags, context);
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
1903
|
-
if (!authenticated.ok) {
|
|
1904
|
-
return fail(write, json, "coordinator revoke", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1905
|
-
}
|
|
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);
|
|
1906
1872
|
const revoked = revokeCoordinator(store, name, acting.name, now);
|
|
1907
1873
|
if (!revoked.ok) {
|
|
1908
1874
|
const said = revoked.reason === "unknown" ? `no coordinator \`${name}\` — \`coordinator list\` shows cids` : "already revoked";
|
|
@@ -2066,19 +2032,16 @@ async function buildCommand(positional, flags, context) {
|
|
|
2066
2032
|
const runner = text(flags, "runner");
|
|
2067
2033
|
const token = text(flags, "token");
|
|
2068
2034
|
const branch = text(flags, "branch");
|
|
2069
|
-
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2035
|
+
if (id === undefined || runner === undefined || token === undefined || branch === undefined)
|
|
2070
2036
|
return fail(write, json, "build", "usage", "`toolroll build <id> --runner <name> --token <t> --branch <b> --repo <path>`", EXIT.usage);
|
|
2071
|
-
}
|
|
2072
2037
|
const auth = authenticate(store, runner, token);
|
|
2073
|
-
if (!auth.ok)
|
|
2038
|
+
if (!auth.ok)
|
|
2074
2039
|
return fail(write, json, "build", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2075
|
-
}
|
|
2076
2040
|
const repo = repoFrom(flags);
|
|
2077
2041
|
// Standalone build proves membership BEFORE leasing anything (MCP spec
|
|
2078
2042
|
// v6, round-4 finding 2): the caller's --repo is a claim, not authority.
|
|
2079
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2043
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2080
2044
|
return fail(write, json, "build", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2081
|
-
}
|
|
2082
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).
|
|
2083
2046
|
if (store.getTask(id) === null)
|
|
2084
2047
|
return fail(write, json, "build", "unknown-task", `No task ${id}.`, EXIT.refused);
|
|
@@ -2093,9 +2056,8 @@ async function buildCommand(positional, flags, context) {
|
|
|
2093
2056
|
now,
|
|
2094
2057
|
...(text(flags, "base") === undefined ? {} : { base: text(flags, "base") }),
|
|
2095
2058
|
});
|
|
2096
|
-
if (!leased.ok)
|
|
2059
|
+
if (!leased.ok)
|
|
2097
2060
|
return fail(write, json, "build", leased.reason, leased.message, EXIT.refused);
|
|
2098
|
-
}
|
|
2099
2061
|
// The standalone road records its attempt and carries its exact lease the
|
|
2100
2062
|
// same as tick's — a park sealed here goes through the same fenced
|
|
2101
2063
|
// transaction, because a gate one road bypasses is a suggestion. No claim
|
|
@@ -2274,22 +2236,19 @@ async function tickCommand(flags, context) {
|
|
|
2274
2236
|
return demoFence;
|
|
2275
2237
|
const runner = text(flags, "runner");
|
|
2276
2238
|
const token = text(flags, "token");
|
|
2277
|
-
if (runner === undefined || token === undefined)
|
|
2239
|
+
if (runner === undefined || token === undefined)
|
|
2278
2240
|
return fail(write, json, "tick", "usage", "`toolroll tick --runner <name> --token <t> --repo <path> [--max <n>] [--base <ref>]`", EXIT.usage);
|
|
2279
|
-
}
|
|
2280
2241
|
// Heartbeat rather than bare auth: a pass that is about to hold leases for
|
|
2281
2242
|
// half an hour should also be on record as alive.
|
|
2282
2243
|
const auth = heartbeatRunner(store, runner, token, clock());
|
|
2283
|
-
if (!auth.ok)
|
|
2244
|
+
if (!auth.ok)
|
|
2284
2245
|
return fail(write, json, "tick", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
2285
|
-
}
|
|
2286
2246
|
if (updateAdmissionPaused(store.raw()))
|
|
2287
2247
|
return fail(write, json, "tick", "updating", UPDATE_PAUSED, EXIT.refused);
|
|
2288
2248
|
const maxGiven = text(flags, "max");
|
|
2289
2249
|
const maxAsked = maxGiven === undefined ? 1 : Number(maxGiven);
|
|
2290
|
-
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2250
|
+
if (!Number.isInteger(maxAsked) || maxAsked <= 0)
|
|
2291
2251
|
return fail(write, json, "tick", "usage", "--max takes a whole number of tasks", EXIT.usage);
|
|
2292
|
-
}
|
|
2293
2252
|
// The service's build lanes (project-concurrency.ts): a lane over its
|
|
2294
2253
|
// project's fair share still runs its housekeeping but starts no
|
|
2295
2254
|
// unattended build this pass; an extra lane only builds.
|
|
@@ -2297,17 +2256,15 @@ async function tickCommand(flags, context) {
|
|
|
2297
2256
|
const buildsOnly = flags.has("builds-only");
|
|
2298
2257
|
const syncAgeGiven = text(flags, "sync-max-age");
|
|
2299
2258
|
const syncMaxAgeMs = syncAgeGiven === undefined ? SYNC_MAX_AGE_MS : Number(syncAgeGiven) * 1000;
|
|
2300
|
-
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2259
|
+
if (!Number.isInteger(syncMaxAgeMs) || syncMaxAgeMs <= 0)
|
|
2301
2260
|
return fail(write, json, "tick", "usage", "--sync-max-age takes whole seconds", EXIT.usage);
|
|
2302
|
-
}
|
|
2303
2261
|
const repo = repoFrom(flags);
|
|
2304
2262
|
// THE PRE-I/O MEMBERSHIP CHECK (MCP spec v6, round-4 finding 1): the
|
|
2305
2263
|
// pass proves its canonical repo is in the runner's BOUND list before
|
|
2306
2264
|
// any git access, probe, or schedule fires. The --repo flag stops being
|
|
2307
2265
|
// authority on every runner road, not only at the claim.
|
|
2308
|
-
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2266
|
+
if (!auth.runner.repos.includes(canonicalProject(repo) ?? resolve(repo)))
|
|
2309
2267
|
return fail(write, json, "tick", "unauthorized-repo", `${runner} is not bound to ${repo} — \`runner bind\` adds it`, EXIT.refused);
|
|
2310
|
-
}
|
|
2311
2268
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
2312
2269
|
const model = text(flags, "model");
|
|
2313
2270
|
const repairModel = text(flags, "repair-model");
|
|
@@ -2342,9 +2299,8 @@ async function tickCommand(flags, context) {
|
|
|
2342
2299
|
let base = text(flags, "base");
|
|
2343
2300
|
if (base === undefined) {
|
|
2344
2301
|
const head = await git("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd: repo });
|
|
2345
|
-
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2302
|
+
if (head.code !== 0 || head.stdout.trim() === "")
|
|
2346
2303
|
return fail(write, json, "tick", "git", `${repo} has no branch checked out — say --base explicitly`, EXIT.refused);
|
|
2347
|
-
}
|
|
2348
2304
|
base = head.stdout.trim();
|
|
2349
2305
|
}
|
|
2350
2306
|
// Probes run at every checkpoint (§3): a key revoked overnight is caught
|
|
@@ -3669,9 +3625,8 @@ async function briefCommand(flags, context) {
|
|
|
3669
3625
|
// (§6): the last watch episode's window and runner, instead of "the last
|
|
3670
3626
|
// 24 hours" — which can mix two windows, or none.
|
|
3671
3627
|
const episode = flags.has("latest-watch") ? store.latestWatchEpisode(repo) : null;
|
|
3672
|
-
if (flags.has("latest-watch") && episode === null)
|
|
3628
|
+
if (flags.has("latest-watch") && episode === null)
|
|
3673
3629
|
return fail(write, json, "brief", "no-watch", "no watch episode recorded for this repo yet — run `toolroll watch` first", EXIT.refused);
|
|
3674
|
-
}
|
|
3675
3630
|
const since = episode?.startedAt ??
|
|
3676
3631
|
text(flags, "since") ??
|
|
3677
3632
|
new Date(clock().getTime() - 24 * 60 * 60_000).toISOString();
|
|
@@ -3841,13 +3796,11 @@ async function decideCommand(positional, flags, context) {
|
|
|
3841
3796
|
return EXIT.refused;
|
|
3842
3797
|
}
|
|
3843
3798
|
const id = Number(idText);
|
|
3844
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
3799
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
3845
3800
|
return fail(write, json, "decide", "usage", "`toolroll decide [<id>] [--choose <option>]`", EXIT.usage);
|
|
3846
|
-
}
|
|
3847
3801
|
const decision = store.getDecision(id);
|
|
3848
|
-
if (decision === null)
|
|
3802
|
+
if (decision === null)
|
|
3849
3803
|
return fail(write, json, "decide", "unknown-decision", `no decision ${id}`, EXIT.refused);
|
|
3850
|
-
}
|
|
3851
3804
|
const choice = text(flags, "choose");
|
|
3852
3805
|
if (choice === undefined) {
|
|
3853
3806
|
const evidence = store.evidenceFor(id);
|
|
@@ -3892,14 +3845,10 @@ async function decideCommand(positional, flags, context) {
|
|
|
3892
3845
|
return EXIT.ok;
|
|
3893
3846
|
}
|
|
3894
3847
|
const acting = await askCredentials(flags, context);
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
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));
|
|
3898
3851
|
const { name: asWho, token } = acting;
|
|
3899
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
3900
|
-
if (!authenticated.ok) {
|
|
3901
|
-
return fail(write, json, "decide", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
3902
|
-
}
|
|
3903
3852
|
const note = text(flags, "note");
|
|
3904
3853
|
const answered = store.answerDecision({ id, choice, by: asWho, via: "cli", ...(note === undefined ? {} : { note }) }, clock(), mutationFrom(flags, clock()));
|
|
3905
3854
|
if (!answered.ok) {
|
|
@@ -3999,9 +3948,8 @@ async function serveCommand(flags, context) {
|
|
|
3999
3948
|
const port = Number(portGiven ?? 4180);
|
|
4000
3949
|
// Validated like demo's --port — a NaN handed to listen() surfaced as a
|
|
4001
3950
|
// baffling bind failure instead of a usage answer (round-4 finding 11).
|
|
4002
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
3951
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
4003
3952
|
return fail(write, json, "serve", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
4004
|
-
}
|
|
4005
3953
|
const host = text(flags, "host") ?? "127.0.0.1";
|
|
4006
3954
|
const allow = text(flags, "allow-host");
|
|
4007
3955
|
// The authorization ceiling: --repo (comma-separable) names repos this
|
|
@@ -4020,12 +3968,10 @@ async function serveCommand(flags, context) {
|
|
|
4020
3968
|
// allow-list of one, and which is meaningless without the machine's
|
|
4021
3969
|
// runner assertion — links bind to runs this machine owns.
|
|
4022
3970
|
const editor = text(flags, "editor");
|
|
4023
|
-
if (editor !== undefined && editor !== "vscode")
|
|
3971
|
+
if (editor !== undefined && editor !== "vscode")
|
|
4024
3972
|
return fail(write, json, "serve", "usage", "--editor supports: vscode", EXIT.usage);
|
|
4025
|
-
|
|
4026
|
-
if (editor !== undefined && localRunner === undefined) {
|
|
3973
|
+
if (editor !== undefined && localRunner === undefined)
|
|
4027
3974
|
return fail(write, json, "serve", "usage", "--editor needs --runner <name> — links bind to the worktrees this machine's runner owns", EXIT.usage);
|
|
4028
|
-
}
|
|
4029
3975
|
// The first-account road (setup review): with no approver, the login
|
|
4030
3976
|
// page offers to create one, gated by this code — printed below, once.
|
|
4031
3977
|
const setupCode = store.listApprovers().length === 0 ? String(randomInt(100_000, 1_000_000)) : undefined;
|
|
@@ -4076,18 +4022,13 @@ async function serveCommand(flags, context) {
|
|
|
4076
4022
|
async function requeueTask(positional, flags, context) {
|
|
4077
4023
|
const { store, write, json, clock } = context;
|
|
4078
4024
|
const [id] = positional;
|
|
4079
|
-
if (id === undefined)
|
|
4025
|
+
if (id === undefined)
|
|
4080
4026
|
return fail(write, json, "task requeue", "usage", "`toolroll task requeue <id> --as <you> --token <t>`", EXIT.usage);
|
|
4081
|
-
}
|
|
4082
4027
|
const acting = await askCredentials(flags, context);
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
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);
|
|
4086
4031
|
const { name: asWho, token } = acting;
|
|
4087
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4088
|
-
if (!authenticated.ok) {
|
|
4089
|
-
return fail(write, json, "task requeue", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4090
|
-
}
|
|
4091
4032
|
const result = store.requeueTask(id, asWho, clock());
|
|
4092
4033
|
if (!result.ok) {
|
|
4093
4034
|
const why = {
|
|
@@ -4116,17 +4057,12 @@ async function requeueTask(positional, flags, context) {
|
|
|
4116
4057
|
async function regateTaskCommand(positional, flags, context) {
|
|
4117
4058
|
const { store, write, json, clock } = context;
|
|
4118
4059
|
const [id] = positional;
|
|
4119
|
-
if (id === undefined)
|
|
4060
|
+
if (id === undefined)
|
|
4120
4061
|
return fail(write, json, "task regate", "usage", "`toolroll task regate <id> --as <you> --token <t>`", EXIT.usage);
|
|
4121
|
-
}
|
|
4122
4062
|
const acting = await askCredentials(flags, context);
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
4127
|
-
if (!authenticated.ok) {
|
|
4128
|
-
return fail(write, json, "task regate", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4129
|
-
}
|
|
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);
|
|
4130
4066
|
const result = regateTask(store, id, clock(), { kind: "operator", name: acting.name, token: acting.token });
|
|
4131
4067
|
if (!result.ok)
|
|
4132
4068
|
return fail(write, json, "task regate", result.reason, result.message, EXIT.refused);
|
|
@@ -4143,49 +4079,39 @@ async function regateTaskCommand(positional, flags, context) {
|
|
|
4143
4079
|
async function planTaskCommand(positional, flags, context) {
|
|
4144
4080
|
const { store, write, json, clock } = context;
|
|
4145
4081
|
const [id] = positional;
|
|
4146
|
-
if (id === undefined)
|
|
4082
|
+
if (id === undefined)
|
|
4147
4083
|
return fail(write, json, "task plan", "usage", "`toolroll task plan <id> --as <you> --token <t>`", EXIT.usage);
|
|
4148
|
-
}
|
|
4149
4084
|
const acting = await askCredentials(flags, context);
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
if (!authenticated.ok) {
|
|
4155
|
-
return fail(write, json, "task plan", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
4156
|
-
}
|
|
4157
|
-
if (store.getTask(id) === null) {
|
|
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)
|
|
4158
4089
|
return fail(write, json, "task plan", "refused", `no task ${id}`, EXIT.refused);
|
|
4159
|
-
}
|
|
4160
4090
|
const ref = store.refFor(BUILT_IN, id);
|
|
4161
4091
|
// Plan pins (P2/C7): a named pair binds the PLAN phase only, refused
|
|
4162
4092
|
// while a live plan claim is spending on the previous answer.
|
|
4163
4093
|
const pinProvider = text(flags, "provider");
|
|
4164
4094
|
const pinModel = text(flags, "model");
|
|
4165
4095
|
if (pinProvider !== undefined) {
|
|
4166
|
-
if (!isProviderId(pinProvider))
|
|
4096
|
+
if (!isProviderId(pinProvider))
|
|
4167
4097
|
return fail(write, json, "task plan", "usage", `unknown provider \`${pinProvider}\``, EXIT.usage);
|
|
4168
|
-
}
|
|
4169
4098
|
// A plan pin is an exact pair (v47): the route freezes model ids, and
|
|
4170
4099
|
// a pin with no model would bind nothing exact.
|
|
4171
|
-
if (pinModel === undefined || pinModel === "")
|
|
4100
|
+
if (pinModel === undefined || pinModel === "")
|
|
4172
4101
|
return fail(write, json, "task plan", "usage", "a plan pin names an exact --model with --provider — approvals bind exact routing", EXIT.usage);
|
|
4173
|
-
}
|
|
4174
4102
|
const valid = validateSpec({ provider: pinProvider, model: pinModel });
|
|
4175
4103
|
if (!valid.ok)
|
|
4176
4104
|
return fail(write, json, "task plan", "invalid", valid.problem, EXIT.usage);
|
|
4177
4105
|
const pinned = store.setPlanPins(ref.id, pinProvider, pinModel, clock());
|
|
4178
|
-
if (!pinned.ok)
|
|
4106
|
+
if (!pinned.ok)
|
|
4179
4107
|
return fail(write, json, "task plan", "refused", "a planner holds this task right now — the pin would reroute a live spend", EXIT.refused);
|
|
4180
|
-
}
|
|
4181
4108
|
}
|
|
4182
4109
|
else if (pinModel !== undefined) {
|
|
4183
4110
|
return fail(write, json, "task plan", "usage", "--model rides --provider for a plan pin", EXIT.usage);
|
|
4184
4111
|
}
|
|
4185
4112
|
const result = store.requestPlan(ref.id, clock());
|
|
4186
|
-
if (!result.ok)
|
|
4113
|
+
if (!result.ok)
|
|
4187
4114
|
return fail(write, json, "task plan", "refused", result.reason, EXIT.refused);
|
|
4188
|
-
}
|
|
4189
4115
|
const autoPlan = authorizePlanUnderMode(store, id, acting.name, clock());
|
|
4190
4116
|
return succeed(write, json, "task plan", { id, autoPlan, ...(pinProvider === undefined ? {} : { planProvider: pinProvider, planModel: pinModel ?? null }) }, () => [
|
|
4191
4117
|
`${id} will be planned before it is built: the next pass dispatches a planner${pinProvider === undefined ? "" : ` on ${pinProvider}${pinModel === undefined ? "" : ` \u00b7 ${pinModel}`}`}.`,
|
|
@@ -4224,9 +4150,8 @@ async function reviewTaskCommand(positional, flags, context) {
|
|
|
4224
4150
|
return fail(write, json, "task review", "usage", "--run and --all require --brief.", EXIT.usage);
|
|
4225
4151
|
const [runText] = positional;
|
|
4226
4152
|
const runId = Number(runText ?? "");
|
|
4227
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4153
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
4228
4154
|
return fail(write, json, "task review", "usage", "`toolroll task review <run-id>`", EXIT.usage);
|
|
4229
|
-
}
|
|
4230
4155
|
// Read-only: the run's one automatic review, when it had one.
|
|
4231
4156
|
const review = buildReviewOf(store, runId);
|
|
4232
4157
|
if (review !== null) {
|
|
@@ -4261,26 +4186,19 @@ async function incidentCommand(positional, flags, context) {
|
|
|
4261
4186
|
write(" → toolroll incident resolve <id> --as <you> --token <t>");
|
|
4262
4187
|
return EXIT.refused;
|
|
4263
4188
|
}
|
|
4264
|
-
if (action !== "resolve")
|
|
4189
|
+
if (action !== "resolve")
|
|
4265
4190
|
return fail(write, json, "incident", "usage", "`toolroll incident [list|resolve <id>]`", EXIT.usage);
|
|
4266
|
-
}
|
|
4267
4191
|
const id = Number(idText);
|
|
4268
|
-
if (!Number.isInteger(id) || id <= 0)
|
|
4192
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
4269
4193
|
return fail(write, json, "incident resolve", "usage", "`toolroll incident resolve <id> --as <you> --token <t>`", EXIT.usage);
|
|
4270
|
-
}
|
|
4271
4194
|
const acting = await askCredentials(flags, context);
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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));
|
|
4275
4198
|
const { name: asWho, token } = acting;
|
|
4276
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
4277
|
-
if (!authenticated.ok) {
|
|
4278
|
-
return fail(write, json, "incident resolve", authenticated.reason, describeApproveFailure(authenticated.reason, String(id)), EXIT.refused);
|
|
4279
|
-
}
|
|
4280
4199
|
const resolved = store.resolveIncident(id, asWho, clock());
|
|
4281
|
-
if (!resolved)
|
|
4200
|
+
if (!resolved)
|
|
4282
4201
|
return fail(write, json, "incident resolve", "unknown-or-resolved", `no unresolved incident ${id}`, EXIT.refused);
|
|
4283
|
-
}
|
|
4284
4202
|
return succeed(write, json, "incident resolve", { id, by: asWho }, () => [
|
|
4285
4203
|
`Resolved incident ${id}, as ${asWho}. The task's hold is lifted; the next tick may take it.`,
|
|
4286
4204
|
]);
|
|
@@ -4314,21 +4232,18 @@ async function webhookCommand(positional, _flags, context) {
|
|
|
4314
4232
|
return EXIT.ok;
|
|
4315
4233
|
}
|
|
4316
4234
|
if (action === "primary") {
|
|
4317
|
-
if (which === undefined || !isMessagingChannel(which))
|
|
4235
|
+
if (which === undefined || !isMessagingChannel(which))
|
|
4318
4236
|
return fail(write, json, "webhook primary", "usage", "primary is one of telegram, slack, discord", EXIT.usage);
|
|
4319
|
-
}
|
|
4320
4237
|
const primary = effectivePrimary(process.env, dir, telegramConfigured);
|
|
4321
|
-
if (!primary.configured.includes(which))
|
|
4238
|
+
if (!primary.configured.includes(which))
|
|
4322
4239
|
return fail(write, json, "webhook primary", "unconfigured", `${which} is not configured — set it up first, then choose it`, EXIT.refused);
|
|
4323
|
-
}
|
|
4324
4240
|
savePrimary(dir, which);
|
|
4325
4241
|
return succeed(write, json, "webhook primary", { primary: which }, () => [
|
|
4326
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."}`,
|
|
4327
4243
|
]);
|
|
4328
4244
|
}
|
|
4329
|
-
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4245
|
+
if (action !== "set" || which !== "console-url" || value === undefined)
|
|
4330
4246
|
return fail(write, json, "webhook", "usage", "`toolroll webhook [status|primary <service>|set console-url <url>]`", EXIT.usage);
|
|
4331
|
-
}
|
|
4332
4247
|
const saved = saveConsoleUrl(dir, value);
|
|
4333
4248
|
if (!saved.ok)
|
|
4334
4249
|
return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
|
|
@@ -4344,9 +4259,8 @@ function resumeProviderCommand(positional, flags, context) {
|
|
|
4344
4259
|
const { store, write, json } = context;
|
|
4345
4260
|
const command = "providers resume";
|
|
4346
4261
|
const provider = positional[0];
|
|
4347
|
-
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4262
|
+
if (provider === undefined || !isProviderId(provider) || positional.length > 1)
|
|
4348
4263
|
return fail(write, json, command, "usage", `Use \`toolroll providers resume <${PROVIDER_IDS.join("|")}>\`.`, EXIT.usage);
|
|
4349
|
-
}
|
|
4350
4264
|
const who = text(flags, "as") ?? process.env["USER"] ?? "operator";
|
|
4351
4265
|
const lifted = liftAuthPause(store, provider, "person", who, context.clock());
|
|
4352
4266
|
if (lifted === null)
|
|
@@ -4440,14 +4354,12 @@ async function providersCommand(flags, context) {
|
|
|
4440
4354
|
if (flags.has("report")) {
|
|
4441
4355
|
const runnerName = text(flags, "runner");
|
|
4442
4356
|
const runnerToken = text(flags, "token");
|
|
4443
|
-
if (runnerName === undefined || runnerToken === undefined)
|
|
4357
|
+
if (runnerName === undefined || runnerToken === undefined)
|
|
4444
4358
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4445
|
-
}
|
|
4446
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));
|
|
4447
4360
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4448
|
-
if (!reported.ok)
|
|
4361
|
+
if (!reported.ok)
|
|
4449
4362
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
4450
|
-
}
|
|
4451
4363
|
recorded = observed;
|
|
4452
4364
|
}
|
|
4453
4365
|
if (json) {
|
|
@@ -4594,9 +4506,8 @@ async function agentsCommand(positional, flags, context) {
|
|
|
4594
4506
|
async function keysCommand(positional, flags, context) {
|
|
4595
4507
|
const { write, json } = context;
|
|
4596
4508
|
const [action, provider, ...rest] = positional;
|
|
4597
|
-
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4509
|
+
if (action === undefined || !KEYS_ACTIONS.includes(action))
|
|
4598
4510
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4599
|
-
}
|
|
4600
4511
|
if (action === "status") {
|
|
4601
4512
|
// ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
|
|
4602
4513
|
const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
|
|
@@ -4622,12 +4533,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4622
4533
|
write(" → `keys auth <provider> subscription|api-key` switches; stored keys are kept either way");
|
|
4623
4534
|
return EXIT.ok;
|
|
4624
4535
|
}
|
|
4625
|
-
if (provider === undefined || !isProviderId(provider))
|
|
4536
|
+
if (provider === undefined || !isProviderId(provider))
|
|
4626
4537
|
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
|
|
4627
|
-
|
|
4628
|
-
if (!isKeyedProvider(provider)) {
|
|
4538
|
+
if (!isKeyedProvider(provider))
|
|
4629
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);
|
|
4630
|
-
}
|
|
4631
4540
|
if (action === "clear") {
|
|
4632
4541
|
const cleared = clearProviderKey(provider);
|
|
4633
4542
|
const mode = readAuthMode(provider);
|
|
@@ -4641,21 +4550,18 @@ async function keysCommand(positional, flags, context) {
|
|
|
4641
4550
|
}
|
|
4642
4551
|
if (action === "verify") {
|
|
4643
4552
|
const stored = readProviderKey(provider);
|
|
4644
|
-
if (stored === null)
|
|
4553
|
+
if (stored === null)
|
|
4645
4554
|
return fail(write, json, "keys verify", "refused", `no stored ${provider} key to verify`, EXIT.refused);
|
|
4646
|
-
}
|
|
4647
4555
|
const verdict = await verifyProviderKey(provider, stored);
|
|
4648
4556
|
return succeed(write, json, "keys verify", { provider, verdict }, () => [verdictWords(provider, verdict)]);
|
|
4649
4557
|
}
|
|
4650
4558
|
if (action === "auth") {
|
|
4651
4559
|
const [wanted] = rest;
|
|
4652
|
-
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4560
|
+
if (wanted !== "subscription" && wanted !== "api-key")
|
|
4653
4561
|
return fail(write, json, "keys auth", "usage", `\`toolroll keys auth ${provider} subscription|api-key\``, EXIT.usage);
|
|
4654
|
-
}
|
|
4655
4562
|
const set = setAuthMode(provider, wanted);
|
|
4656
|
-
if (!set.ok)
|
|
4563
|
+
if (!set.ok)
|
|
4657
4564
|
return fail(write, json, "keys auth", "refused", `${provider} has no subscription login — it is api-key only`, EXIT.refused);
|
|
4658
|
-
}
|
|
4659
4565
|
return succeed(write, json, "keys auth", { provider, mode: wanted }, () => [
|
|
4660
4566
|
wanted === "subscription"
|
|
4661
4567
|
? `${provider} now uses its own login; its stored key is kept as the fallback you can switch to.`
|
|
@@ -4673,18 +4579,16 @@ async function keysCommand(positional, flags, context) {
|
|
|
4673
4579
|
}
|
|
4674
4580
|
}
|
|
4675
4581
|
else {
|
|
4676
|
-
if (process.stdin.isTTY)
|
|
4582
|
+
if (process.stdin.isTTY)
|
|
4677
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);
|
|
4678
|
-
}
|
|
4679
4584
|
const chunks = [];
|
|
4680
4585
|
for await (const chunk of process.stdin)
|
|
4681
4586
|
chunks.push(chunk);
|
|
4682
4587
|
value = Buffer.concat(chunks).toString("utf8");
|
|
4683
4588
|
}
|
|
4684
4589
|
const saved = saveProviderKey(provider, value);
|
|
4685
|
-
if (!saved.ok)
|
|
4590
|
+
if (!saved.ok)
|
|
4686
4591
|
return fail(write, json, "keys set", "implausible", "that does not look like an API key — check the paste", EXIT.refused);
|
|
4687
|
-
}
|
|
4688
4592
|
// Save is instant (shape-checked); verification is a live credential
|
|
4689
4593
|
// check that spends no tokens — skip it with --no-verify for an offline
|
|
4690
4594
|
// machine or a provider you cannot reach right now.
|
|
@@ -4704,9 +4608,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4704
4608
|
async function peopleCommand(positional, flags, context) {
|
|
4705
4609
|
const { store, write, json, clock } = context;
|
|
4706
4610
|
const [action, ...rest] = positional;
|
|
4707
|
-
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4611
|
+
if (action === undefined || !PEOPLE_ACTIONS.includes(action))
|
|
4708
4612
|
return fail(write, json, "people", "usage", `unknown \`people ${action ?? ""}\` — try ${PEOPLE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4709
|
-
}
|
|
4710
4613
|
if (action === "list") {
|
|
4711
4614
|
const accounts = store.accountFacts();
|
|
4712
4615
|
const invites = store.openInvites(clock());
|
|
@@ -4728,14 +4631,12 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4728
4631
|
return EXIT.ok;
|
|
4729
4632
|
}
|
|
4730
4633
|
const acting = await askCredentials(flags, context);
|
|
4731
|
-
if (acting === null)
|
|
4634
|
+
if (acting === null)
|
|
4732
4635
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4733
|
-
}
|
|
4734
4636
|
// Inviting and widening access are credential acts; revoking only narrows.
|
|
4735
4637
|
const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
4736
|
-
if (!authenticated.ok)
|
|
4638
|
+
if (!authenticated.ok)
|
|
4737
4639
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4738
|
-
}
|
|
4739
4640
|
const repoGiven = text(flags, "repo");
|
|
4740
4641
|
const allProjects = flags.has("all-projects");
|
|
4741
4642
|
const noProjects = flags.has("no-projects");
|
|
@@ -4756,9 +4657,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4756
4657
|
}
|
|
4757
4658
|
if (action === "invite") {
|
|
4758
4659
|
const roleFlag = text(flags, "role") ?? "viewer";
|
|
4759
|
-
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4660
|
+
if (roleFlag !== "viewer" && roleFlag !== "approver")
|
|
4760
4661
|
return fail(write, json, "people invite", "usage", "--role viewer|approver (viewer is the default)", EXIT.usage);
|
|
4761
|
-
}
|
|
4762
4662
|
if (noProjects)
|
|
4763
4663
|
return fail(write, json, "people invite", "usage", "An invitation needs --repo <path> or all-project access.", EXIT.usage);
|
|
4764
4664
|
const minted = store.mintInvite(roleFlag, acting.name, clock(), undefined, projects);
|
|
@@ -4769,9 +4669,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4769
4669
|
]);
|
|
4770
4670
|
}
|
|
4771
4671
|
const [name] = rest;
|
|
4772
|
-
if (name === undefined || name.trim() === "")
|
|
4672
|
+
if (name === undefined || name.trim() === "")
|
|
4773
4673
|
return fail(write, json, "people revoke", "usage", "`toolroll people revoke <name> --as <you> --token <t>`", EXIT.usage);
|
|
4774
|
-
}
|
|
4775
4674
|
const severed = store.revokeAccount(name.trim(), acting.name, clock());
|
|
4776
4675
|
if (!severed.ok) {
|
|
4777
4676
|
const words = severed.reason === "last-approver"
|
|
@@ -4836,16 +4735,14 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4836
4735
|
async function modeCommand(positional, flags, context) {
|
|
4837
4736
|
const { store, write, json, clock } = context;
|
|
4838
4737
|
const [action] = positional;
|
|
4839
|
-
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4738
|
+
if (action === undefined || !MODE_ACTIONS.includes(action))
|
|
4840
4739
|
return fail(write, json, "mode", "usage", `unknown \`mode ${action ?? ""}\` — try ${MODE_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4841
|
-
}
|
|
4842
4740
|
const repoGiven = text(flags, "repo");
|
|
4843
4741
|
// The same canonical string every other road stores for the repository
|
|
4844
4742
|
// (a symlinked path — /var vs /private/var — must name the same mode).
|
|
4845
4743
|
const repo = repoGiven === undefined ? undefined : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4846
|
-
if (repo === undefined)
|
|
4744
|
+
if (repo === undefined)
|
|
4847
4745
|
return fail(write, json, "mode", "usage", "`mode` needs --repo <canonical path> — a mode is per-repository", EXIT.usage);
|
|
4848
|
-
}
|
|
4849
4746
|
const now = clock();
|
|
4850
4747
|
if (action === "show") {
|
|
4851
4748
|
const mode = store.activeMode(repo, now);
|
|
@@ -4860,14 +4757,12 @@ async function modeCommand(positional, flags, context) {
|
|
|
4860
4757
|
]);
|
|
4861
4758
|
}
|
|
4862
4759
|
const acting = await askCredentials(flags, context);
|
|
4863
|
-
if (acting === null)
|
|
4760
|
+
if (acting === null)
|
|
4864
4761
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4865
|
-
}
|
|
4866
4762
|
// Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
|
|
4867
4763
|
const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4868
|
-
if (!authed.ok)
|
|
4764
|
+
if (!authed.ok)
|
|
4869
4765
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4870
|
-
}
|
|
4871
4766
|
if (action === "revoke") {
|
|
4872
4767
|
// Lowering authority is one click for ANY approver (v4 doctrine).
|
|
4873
4768
|
const revoked = store.revokeMode(repo, acting.name, "operator", now);
|
|
@@ -4877,13 +4772,11 @@ async function modeCommand(positional, flags, context) {
|
|
|
4877
4772
|
}
|
|
4878
4773
|
// set: RAISING authority — the password ceremony above already ran.
|
|
4879
4774
|
const nameGiven = text(flags, "name") ?? "standard";
|
|
4880
|
-
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4775
|
+
if (nameGiven !== "standard" && nameGiven !== "hands-off")
|
|
4881
4776
|
return fail(write, json, "mode set", "usage", "--name is standard or hands-off", EXIT.usage);
|
|
4882
|
-
}
|
|
4883
4777
|
const daysGiven = Number(text(flags, "days") ?? "1");
|
|
4884
|
-
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4778
|
+
if (!Number.isInteger(daysGiven) || daysGiven < 1 || daysGiven > MODE_MAX_DAYS)
|
|
4885
4779
|
return fail(write, json, "mode set", "usage", `--days is 1 to ${MODE_MAX_DAYS} (a mode always expires)`, EXIT.usage);
|
|
4886
|
-
}
|
|
4887
4780
|
const expiry = new Date(now.getTime() + daysGiven * 24 * 60 * 60_000).toISOString();
|
|
4888
4781
|
const terms = presetTerms(nameGiven, expiry);
|
|
4889
4782
|
// Explicit term overrides ride the same flags (all optional):
|
|
@@ -4895,12 +4788,10 @@ async function modeCommand(positional, flags, context) {
|
|
|
4895
4788
|
terms.autoApproveFiling = true;
|
|
4896
4789
|
if (flag(flags, "plan-auto"))
|
|
4897
4790
|
terms.planAuto = true;
|
|
4898
|
-
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)))
|
|
4899
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);
|
|
4900
|
-
|
|
4901
|
-
if (terms.planAuto && !terms.autoApproveFiling) {
|
|
4793
|
+
if (terms.planAuto && !terms.autoApproveFiling)
|
|
4902
4794
|
return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
|
|
4903
|
-
}
|
|
4904
4795
|
// Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
|
|
4905
4796
|
// The signature names every chat app it covers: a grant signed before that named Telegram alone, and stays so.
|
|
4906
4797
|
if (flag(flags, "chat-approve")) {
|
|
@@ -4908,9 +4799,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4908
4799
|
terms.chatApproveChats = CHAT_APPROVE_ALL;
|
|
4909
4800
|
}
|
|
4910
4801
|
// Automerge requires a live merge-capable grant on the repo (D1).
|
|
4911
|
-
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4802
|
+
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now))
|
|
4912
4803
|
return fail(write, json, "mode set", "no-grant", "automerge needs a merge-capable publication grant on this repo — file one first", EXIT.refused);
|
|
4913
|
-
}
|
|
4914
4804
|
const digest = modeDigestOf(terms);
|
|
4915
4805
|
const id = store.signMode({ repo, name: nameGiven, termsJson: modeTermsJson(terms), digest, signedBy: acting.name, absoluteExpiry: expiry, publication: terms.publication }, now);
|
|
4916
4806
|
return succeed(write, json, "mode set", { repo, id, digest, terms }, () => [
|
|
@@ -4921,9 +4811,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4921
4811
|
async function configCommand(positional, flags, context) {
|
|
4922
4812
|
const { store, write, json, clock } = context;
|
|
4923
4813
|
const [action, phase] = positional;
|
|
4924
|
-
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4814
|
+
if (action !== undefined && !CONFIG_ACTIONS.includes(action))
|
|
4925
4815
|
return fail(write, json, "config", "usage", `unknown \`config ${action}\` — try ${CONFIG_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4926
|
-
}
|
|
4927
4816
|
const repoGiven = text(flags, "repo");
|
|
4928
4817
|
const scope = repoGiven === undefined ? INSTALLATION_SCOPE : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
4929
4818
|
if (action === undefined || action === "show") {
|
|
@@ -4992,21 +4881,18 @@ async function configCommand(positional, flags, context) {
|
|
|
4992
4881
|
}
|
|
4993
4882
|
return EXIT.ok;
|
|
4994
4883
|
}
|
|
4995
|
-
if (action !== "set" && action !== "clear")
|
|
4884
|
+
if (action !== "set" && action !== "clear")
|
|
4996
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);
|
|
4997
|
-
}
|
|
4998
4886
|
// Global dollar thresholds (v15, operator request): defaults for filings
|
|
4999
4887
|
// and an installation-wide backstop. Authenticated like every spend
|
|
5000
4888
|
// routing act; the per-task digest stays the authority.
|
|
5001
4889
|
if (phase === "budgets") {
|
|
5002
4890
|
const acting = await askCredentials(flags, context);
|
|
5003
|
-
if (acting === null)
|
|
4891
|
+
if (acting === null)
|
|
5004
4892
|
return fail(write, json, `config ${action}`, "usage", "changing spend defaults takes `--as <you> --token <t>`", EXIT.usage);
|
|
5005
|
-
}
|
|
5006
4893
|
const authedBudget = authenticateApprover(store, acting.name, acting.token);
|
|
5007
|
-
if (!authedBudget.ok)
|
|
4894
|
+
if (!authedBudget.ok)
|
|
5008
4895
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
5009
|
-
}
|
|
5010
4896
|
if (action === "clear") {
|
|
5011
4897
|
store.setSpendDefaults({ buildPerRunMicrousd: null }, acting.name, clock());
|
|
5012
4898
|
return succeed(write, json, "config clear", { budgets: null }, () => ["Spend defaults cleared — filings state their own numbers again."]);
|
|
@@ -5019,9 +4905,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5019
4905
|
return Number.isFinite(value) && value > 0 ? Math.round(value * 1_000_000) : false;
|
|
5020
4906
|
};
|
|
5021
4907
|
const build = parseUsd("build-usd");
|
|
5022
|
-
if (build === false)
|
|
4908
|
+
if (build === false)
|
|
5023
4909
|
return fail(write, json, "config set", "usage", "--build-usd is a positive dollar amount", EXIT.usage);
|
|
5024
|
-
}
|
|
5025
4910
|
store.setSpendDefaults({ buildPerRunMicrousd: build }, acting.name, clock());
|
|
5026
4911
|
return succeed(write, json, "config set", { budgets: store.getSpendDefaults() }, () => [
|
|
5027
4912
|
"Spend defaults set. New filings pre-fill from these; every approval still restates its own numbers:",
|
|
@@ -5033,17 +4918,14 @@ async function configCommand(positional, flags, context) {
|
|
|
5033
4918
|
// Installation-scoped only — a per-repo chat would splinter the spend
|
|
5034
4919
|
// ceiling that makes the ledger a ceiling at all.
|
|
5035
4920
|
if (phase === "chat") {
|
|
5036
|
-
if (repoGiven !== undefined)
|
|
4921
|
+
if (repoGiven !== undefined)
|
|
5037
4922
|
return fail(write, json, `config ${action}`, "usage", "chat is installation-scoped — no --repo", EXIT.usage);
|
|
5038
|
-
}
|
|
5039
4923
|
const acting = await askCredentials(flags, context);
|
|
5040
|
-
if (acting === null)
|
|
4924
|
+
if (acting === null)
|
|
5041
4925
|
return fail(write, json, `config ${action}`, "usage", "changing chat spend takes `--as <you> --token <t>`", EXIT.usage);
|
|
5042
|
-
}
|
|
5043
4926
|
const authedChat = authenticateApprover(store, acting.name, acting.token);
|
|
5044
|
-
if (!authedChat.ok)
|
|
4927
|
+
if (!authedChat.ok)
|
|
5045
4928
|
return fail(write, json, `config ${action}`, "unauthenticated", "that is not an approver, or the token does not match", EXIT.refused);
|
|
5046
|
-
}
|
|
5047
4929
|
if (action === "clear") {
|
|
5048
4930
|
store.clearChatConfig();
|
|
5049
4931
|
return succeed(write, json, "config clear", { chat: null }, () => ["Chat is off — the config row is gone."]);
|
|
@@ -5052,31 +4934,25 @@ async function configCommand(positional, flags, context) {
|
|
|
5052
4934
|
const requestedModel = text(flags, "model");
|
|
5053
4935
|
const weeklyUsd = text(flags, "weekly-usd");
|
|
5054
4936
|
const dailyGiven = text(flags, "daily-turns");
|
|
5055
|
-
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")
|
|
5056
4938
|
return fail(write, json, "config set", "usage", "chat provider is one of claude-subscription, codex-subscription, anthropic-api, openrouter-api", EXIT.usage);
|
|
5057
|
-
}
|
|
5058
4939
|
const model = requestedModel ?? (isSubscriptionChatProvider(provider) ? "default" : "");
|
|
5059
|
-
if (!validModelId(model))
|
|
4940
|
+
if (!validModelId(model))
|
|
5060
4941
|
return fail(write, json, "config set", "usage", "--model must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes", EXIT.usage);
|
|
5061
|
-
}
|
|
5062
4942
|
const priced = isDirectChatProvider(provider) ? livePin(store, provider, model) ?? priceOf(model) : null;
|
|
5063
|
-
if (isDirectChatProvider(provider) && priced === null)
|
|
4943
|
+
if (isDirectChatProvider(provider) && priced === null)
|
|
5064
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);
|
|
5065
|
-
}
|
|
5066
4945
|
const weekly = Number(weeklyUsd);
|
|
5067
|
-
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
4946
|
+
if (isDirectChatProvider(provider) && (weeklyUsd === undefined || !Number.isFinite(weekly) || weekly <= 0))
|
|
5068
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);
|
|
5069
|
-
}
|
|
5070
4948
|
const daily = dailyGiven === undefined ? 50 : Number(dailyGiven);
|
|
5071
|
-
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
4949
|
+
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000)
|
|
5072
4950
|
return fail(write, json, "config set", "usage", "--daily-turns is a whole number between 1 and 1000", EXIT.usage);
|
|
5073
|
-
}
|
|
5074
4951
|
// The CLI pins from the compiled table (the console additionally offers
|
|
5075
4952
|
// OpenRouter's live catalog — priced by the party that bills it).
|
|
5076
4953
|
const pinned = isDirectChatProvider(provider) ? priced : { inMicrousd: 0, outMicrousd: 0 };
|
|
5077
|
-
if (pinned === null)
|
|
4954
|
+
if (pinned === null)
|
|
5078
4955
|
return fail(write, json, "config set", "unpriced-model", `no compiled price for ${model}`, EXIT.refused);
|
|
5079
|
-
}
|
|
5080
4956
|
store.setChatConfig({
|
|
5081
4957
|
provider,
|
|
5082
4958
|
model,
|
|
@@ -5097,42 +4973,33 @@ async function configCommand(positional, flags, context) {
|
|
|
5097
4973
|
: []),
|
|
5098
4974
|
]);
|
|
5099
4975
|
}
|
|
5100
|
-
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
4976
|
+
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5101
4977
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5102
|
-
}
|
|
5103
4978
|
const acting = await askCredentials(flags, context);
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5108
|
-
if (!authenticated.ok) {
|
|
5109
|
-
return fail(write, json, `config ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, phase), EXIT.refused);
|
|
5110
|
-
}
|
|
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);
|
|
5111
4982
|
// The STRONG tier (v47): a second, named row per phase the routing policy
|
|
5112
4983
|
// reaches for when size, quality, evidence, or publication demand it.
|
|
5113
4984
|
// Authenticated and audited exactly like the routine row; existing
|
|
5114
4985
|
// approvals are untouched — a sealed route never re-resolves.
|
|
5115
4986
|
// The LIGHT tier (v2): the fast agent a small change builds on.
|
|
5116
4987
|
const tierGiven = text(flags, "tier");
|
|
5117
|
-
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
4988
|
+
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light")
|
|
5118
4989
|
return fail(write, json, `config ${action}`, "usage", "--tier is `light` or `strong` — the routine tier is the plain phase row", EXIT.usage);
|
|
5119
|
-
}
|
|
5120
4990
|
// A light planner would never run (a small change makes no plan): refused rather than kept and ignored.
|
|
5121
4991
|
// Clearing one left from before stays possible.
|
|
5122
|
-
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")))
|
|
5123
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);
|
|
5124
|
-
}
|
|
5125
4994
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5126
4995
|
// has more room runs.
|
|
5127
4996
|
if (flag(flags, "also")) {
|
|
5128
|
-
if (phase !== "plan" && phase !== "build")
|
|
4997
|
+
if (phase !== "plan" && phase !== "build")
|
|
5129
4998
|
return fail(write, json, `config ${action}`, "usage", "--also is for plan or build — repairs follow the build's provider", EXIT.usage);
|
|
5130
|
-
}
|
|
5131
4999
|
const tier = (tierGiven ?? "routine");
|
|
5132
5000
|
const alsoProvider = text(flags, "provider");
|
|
5133
|
-
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5001
|
+
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5134
5002
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5135
|
-
}
|
|
5136
5003
|
if (action === "clear") {
|
|
5137
5004
|
const removed = store.clearPhaseTierAlternates(scope, phase, tier, alsoProvider ?? null);
|
|
5138
5005
|
return succeed(write, json, "config clear", { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
@@ -5140,9 +5007,8 @@ async function configCommand(positional, flags, context) {
|
|
|
5140
5007
|
]);
|
|
5141
5008
|
}
|
|
5142
5009
|
const alsoModel = text(flags, "model");
|
|
5143
|
-
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5010
|
+
if (alsoProvider === undefined || alsoModel === undefined)
|
|
5144
5011
|
return fail(write, json, "config set", "usage", "--also names an exact --provider and --model — approvals bind exact routing", EXIT.usage);
|
|
5145
|
-
}
|
|
5146
5012
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5147
5013
|
if (!alsoValid.ok)
|
|
5148
5014
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
@@ -5175,18 +5041,15 @@ async function configCommand(positional, flags, context) {
|
|
|
5175
5041
|
]);
|
|
5176
5042
|
}
|
|
5177
5043
|
const providerGiven = text(flags, "provider");
|
|
5178
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5044
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
5179
5045
|
return fail(write, json, "config set", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5180
|
-
}
|
|
5181
5046
|
const modelGiven = text(flags, "model") ?? null;
|
|
5182
5047
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
5183
|
-
if (!valid.ok)
|
|
5048
|
+
if (!valid.ok)
|
|
5184
5049
|
return fail(write, json, "config set", "invalid", valid.problem, EXIT.usage);
|
|
5185
|
-
}
|
|
5186
5050
|
if (tierGiven === "light") {
|
|
5187
|
-
if (modelGiven === null)
|
|
5051
|
+
if (modelGiven === null)
|
|
5188
5052
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5189
|
-
}
|
|
5190
5053
|
store.setPhaseTierConfig(scope, phase, "light", providerGiven, modelGiven, acting.name, clock());
|
|
5191
5054
|
return succeed(write, json, "config set", { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5192
5055
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
@@ -5194,12 +5057,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5194
5057
|
]);
|
|
5195
5058
|
}
|
|
5196
5059
|
if (tierGiven === "strong") {
|
|
5197
|
-
if (phase === "review" && providerGiven === "gemini")
|
|
5060
|
+
if (phase === "review" && providerGiven === "gemini")
|
|
5198
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);
|
|
5199
|
-
|
|
5200
|
-
if (modelGiven === null) {
|
|
5062
|
+
if (modelGiven === null)
|
|
5201
5063
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5202
|
-
}
|
|
5203
5064
|
store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
|
|
5204
5065
|
return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5205
5066
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
@@ -5238,9 +5099,8 @@ async function setupCommand(positional, flags, context) {
|
|
|
5238
5099
|
const action = positional[0] ?? "show";
|
|
5239
5100
|
const repo = text(flags, "repo");
|
|
5240
5101
|
if (action === "show") {
|
|
5241
|
-
if (repo === undefined)
|
|
5102
|
+
if (repo === undefined)
|
|
5242
5103
|
return fail(write, json, "setup show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5243
|
-
}
|
|
5244
5104
|
const live = store.liveWorktreeSetup(repo);
|
|
5245
5105
|
if (json) {
|
|
5246
5106
|
write(envelopeJson({ ok: true, command: "setup show", repo, setup: live }));
|
|
@@ -5251,20 +5111,14 @@ async function setupCommand(positional, flags, context) {
|
|
|
5251
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}`);
|
|
5252
5112
|
return EXIT.ok;
|
|
5253
5113
|
}
|
|
5254
|
-
if (action !== "set" && action !== "clear")
|
|
5114
|
+
if (action !== "set" && action !== "clear")
|
|
5255
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);
|
|
5256
|
-
|
|
5257
|
-
if (repo === undefined) {
|
|
5116
|
+
if (repo === undefined)
|
|
5258
5117
|
return fail(write, json, `setup ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5259
|
-
}
|
|
5260
5118
|
const acting = await askCredentials(flags, context);
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5265
|
-
if (!authenticated.ok) {
|
|
5266
|
-
return fail(write, json, `setup ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5267
|
-
}
|
|
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);
|
|
5268
5122
|
if (action === "clear") {
|
|
5269
5123
|
const cleared = store.clearWorktreeSetup(repo, acting.name, clock());
|
|
5270
5124
|
return succeed(write, json, "setup clear", { repo, cleared }, () => [
|
|
@@ -5272,25 +5126,21 @@ async function setupCommand(positional, flags, context) {
|
|
|
5272
5126
|
]);
|
|
5273
5127
|
}
|
|
5274
5128
|
const command = text(flags, "command");
|
|
5275
|
-
if (command === undefined || command.trim() === "")
|
|
5129
|
+
if (command === undefined || command.trim() === "")
|
|
5276
5130
|
return fail(write, json, "setup set", "usage", "--command <cmd> is what every fresh checkout will run", EXIT.usage);
|
|
5277
|
-
|
|
5278
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5131
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5279
5132
|
return fail(write, json, "setup set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5280
|
-
}
|
|
5281
5133
|
// Literal credentials never become standing rows (audit IV-5): a command
|
|
5282
5134
|
// that embeds a token is stored forever in plain text. Reference an
|
|
5283
5135
|
// environment variable the runner already exports instead. `$TOKEN` is
|
|
5284
5136
|
// a reference and passes; `=secret123` is a value and refuses.
|
|
5285
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) ||
|
|
5286
5138
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5287
|
-
if (credentialShaped)
|
|
5139
|
+
if (credentialShaped)
|
|
5288
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);
|
|
5289
|
-
}
|
|
5290
5141
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5291
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5142
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5292
5143
|
return fail(write, json, "setup set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5293
|
-
}
|
|
5294
5144
|
if (flags.get("yes") !== true) {
|
|
5295
5145
|
if (json) {
|
|
5296
5146
|
write(envelopeJson({ ok: false, command: "setup set", reason: "unconfirmed", repo, setupCommand: command, timeoutSeconds }));
|
|
@@ -5330,9 +5180,8 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5330
5180
|
const action = positional[0] ?? "show";
|
|
5331
5181
|
const repo = text(flags, "repo");
|
|
5332
5182
|
if (action === "show") {
|
|
5333
|
-
if (repo === undefined)
|
|
5183
|
+
if (repo === undefined)
|
|
5334
5184
|
return fail(write, json, "verify show", "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5335
|
-
}
|
|
5336
5185
|
const live = store.liveVerifyCommand(repo);
|
|
5337
5186
|
const quick = liveQuickCommand(store, repo);
|
|
5338
5187
|
const level = projectCheckLevel(store, repo);
|
|
@@ -5372,24 +5221,18 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5372
5221
|
...(wanted !== "full" ? ["The full check still runs when a pull request opens, and Merge waits for it unless a person merges anyway."] : []),
|
|
5373
5222
|
]);
|
|
5374
5223
|
}
|
|
5375
|
-
if (action !== "set" && action !== "clear")
|
|
5224
|
+
if (action !== "set" && action !== "clear")
|
|
5376
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);
|
|
5377
|
-
}
|
|
5378
5226
|
// The quick command is its own approved grant beside the full one.
|
|
5379
5227
|
const quickValue = text(flags, "quick");
|
|
5380
5228
|
const quickClear = action === "clear" && text(flags, "level") === "quick";
|
|
5381
5229
|
const grantKey = repo === undefined ? undefined : quickValue !== undefined || quickClear ? quickVerifyKey(repo) : repo;
|
|
5382
|
-
if (repo === undefined)
|
|
5230
|
+
if (repo === undefined)
|
|
5383
5231
|
return fail(write, json, `verify ${action}`, "usage", "which repo? --repo <path>", EXIT.usage);
|
|
5384
|
-
}
|
|
5385
5232
|
const acting = await askCredentials(flags, context);
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5390
|
-
if (!authenticated.ok) {
|
|
5391
|
-
return fail(write, json, `verify ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5392
|
-
}
|
|
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);
|
|
5393
5236
|
if (action === "clear") {
|
|
5394
5237
|
const cleared = store.clearVerifyCommand(grantKey, acting.name, clock());
|
|
5395
5238
|
if (quickClear)
|
|
@@ -5401,38 +5244,30 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5401
5244
|
]);
|
|
5402
5245
|
}
|
|
5403
5246
|
const command = quickValue ?? text(flags, "command");
|
|
5404
|
-
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5247
|
+
if (quickValue !== undefined && text(flags, "command") !== undefined)
|
|
5405
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);
|
|
5406
|
-
|
|
5407
|
-
if (quickValue !== undefined && flags.get("self-heal") === true) {
|
|
5249
|
+
if (quickValue !== undefined && flags.get("self-heal") === true)
|
|
5408
5250
|
return fail(write, json, "verify set", "usage", "--self-heal applies to the full command only", EXIT.usage);
|
|
5409
|
-
|
|
5410
|
-
if (command === undefined || command.trim() === "") {
|
|
5251
|
+
if (command === undefined || command.trim() === "")
|
|
5411
5252
|
return fail(write, json, "verify set", "usage", "--command <cmd> is what the plane re-runs after every commit", EXIT.usage);
|
|
5412
|
-
|
|
5413
|
-
if (command.length > 2000 || hasDisguisedText(command)) {
|
|
5253
|
+
if (command.length > 2000 || hasDisguisedText(command))
|
|
5414
5254
|
return fail(write, json, "verify set", "invalid", "the command must be under 2000 characters with no control or bidi characters", EXIT.usage);
|
|
5415
|
-
}
|
|
5416
5255
|
// Same rule as an approved setup command (audit IV-5): a command that
|
|
5417
5256
|
// embeds a literal credential is stored forever in plain text.
|
|
5418
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) ||
|
|
5419
5258
|
/\/\/[^\s/@]+:[^\s/@]+@/.test(command);
|
|
5420
|
-
if (credentialShaped)
|
|
5259
|
+
if (credentialShaped)
|
|
5421
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);
|
|
5422
|
-
}
|
|
5423
5261
|
const timeoutSeconds = Number(text(flags, "timeout-seconds") ?? "300");
|
|
5424
|
-
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5262
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)
|
|
5425
5263
|
return fail(write, json, "verify set", "invalid", "--timeout-seconds is 1..3600", EXIT.usage);
|
|
5426
|
-
}
|
|
5427
5264
|
const selfHeal = flags.get("self-heal") === true;
|
|
5428
5265
|
const expectedSetupDigest = text(flags, "setup-digest");
|
|
5429
|
-
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5266
|
+
if (!selfHeal && expectedSetupDigest !== undefined)
|
|
5430
5267
|
return fail(write, json, "verify set", "usage", "--setup-digest is only valid with --self-heal", EXIT.usage);
|
|
5431
|
-
}
|
|
5432
5268
|
const recoverySetup = selfHeal ? store.liveWorktreeSetup(repo) : null;
|
|
5433
|
-
if (selfHeal && recoverySetup === null)
|
|
5269
|
+
if (selfHeal && recoverySetup === null)
|
|
5434
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);
|
|
5435
|
-
}
|
|
5436
5271
|
if (flags.get("yes") !== true) {
|
|
5437
5272
|
if (json) {
|
|
5438
5273
|
write(envelopeJson({
|
|
@@ -5476,12 +5311,10 @@ async function verifyCommand(positional, flags, context) {
|
|
|
5476
5311
|
}
|
|
5477
5312
|
return EXIT.refused;
|
|
5478
5313
|
}
|
|
5479
|
-
if (selfHeal && expectedSetupDigest === undefined)
|
|
5314
|
+
if (selfHeal && expectedSetupDigest === undefined)
|
|
5480
5315
|
return fail(write, json, "verify set", "setup-digest-required", `confirm self-healing with --setup-digest ${recoverySetup?.digest} exactly as previewed`, EXIT.refused);
|
|
5481
|
-
|
|
5482
|
-
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest) {
|
|
5316
|
+
if (selfHeal && expectedSetupDigest !== recoverySetup?.digest)
|
|
5483
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);
|
|
5484
|
-
}
|
|
5485
5318
|
const saved = store.setVerifyCommand({
|
|
5486
5319
|
repo: grantKey,
|
|
5487
5320
|
command,
|
|
@@ -5547,13 +5380,9 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5547
5380
|
}
|
|
5548
5381
|
if (action === "grant" || action === "clear") {
|
|
5549
5382
|
const acting = await askCredentials(flags, context);
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
5554
|
-
if (!authenticated.ok) {
|
|
5555
|
-
return fail(write, json, `intake ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, repo), EXIT.refused);
|
|
5556
|
-
}
|
|
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);
|
|
5557
5386
|
if (action === "clear") {
|
|
5558
5387
|
const cleared = store.clearIntakeGrant(repo, acting.name, clock());
|
|
5559
5388
|
return succeed(write, json, "intake clear", { repo, cleared }, () => [
|
|
@@ -5562,12 +5391,10 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5562
5391
|
}
|
|
5563
5392
|
const github = text(flags, "github");
|
|
5564
5393
|
const label = text(flags, "label");
|
|
5565
|
-
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5394
|
+
if (github === undefined || !GITHUB_REPO_SHAPE.test(github))
|
|
5566
5395
|
return fail(write, json, "intake grant", "usage", "--github <owner/name> names the repository on GitHub", EXIT.usage);
|
|
5567
|
-
|
|
5568
|
-
if (label === undefined || !LABEL_SHAPE.test(label)) {
|
|
5396
|
+
if (label === undefined || !LABEL_SHAPE.test(label))
|
|
5569
5397
|
return fail(write, json, "intake grant", "usage", "--label <label> names the exact label that nominates an issue", EXIT.usage);
|
|
5570
|
-
}
|
|
5571
5398
|
const reviewersRaw = text(flags, "reviewers");
|
|
5572
5399
|
const reviewers = reviewersRaw === undefined
|
|
5573
5400
|
? null
|
|
@@ -5598,13 +5425,11 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5598
5425
|
`Granted: issues in ${github} labeled "${label}" become unapproved proposals for ${repo}.`,
|
|
5599
5426
|
]);
|
|
5600
5427
|
}
|
|
5601
|
-
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5428
|
+
if (action !== "preview" && action !== "run" && action !== "pr-comments")
|
|
5602
5429
|
return fail(write, json, "intake", "usage", "`toolroll intake [show|grant|clear|preview|run|pr-comments] --repo <path> …`", EXIT.usage);
|
|
5603
|
-
}
|
|
5604
5430
|
const grant = store.liveIntakeGrant(repo);
|
|
5605
|
-
if (grant === null)
|
|
5431
|
+
if (grant === null)
|
|
5606
5432
|
return fail(write, json, `intake ${action}`, "no-grant", `no intake grant for ${repo} — \`toolroll intake grant\` states the terms`, EXIT.refused);
|
|
5607
|
-
}
|
|
5608
5433
|
if (action === "pr-comments") {
|
|
5609
5434
|
// PR review comments become LOCAL diff comments (M8.17): own PRs only
|
|
5610
5435
|
// (the publication table IS the list of ours), named reviewers only,
|
|
@@ -5612,9 +5437,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5612
5437
|
// shared validator. Nothing here authorizes a spawn — ingested
|
|
5613
5438
|
// comments wait on the run page for the same one-tap seal and the
|
|
5614
5439
|
// same scope approval as comments typed in the console.
|
|
5615
|
-
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5440
|
+
if (grant.reviewers === null || grant.reviewers.length === 0)
|
|
5616
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);
|
|
5617
|
-
}
|
|
5618
5442
|
const publications = store
|
|
5619
5443
|
.openedPublications()
|
|
5620
5444
|
.filter(one => one.prNumber !== null &&
|
|
@@ -5695,9 +5519,8 @@ async function intakeCommand(positional, flags, context) {
|
|
|
5695
5519
|
}
|
|
5696
5520
|
const limit = Math.min(Math.max(Number(text(flags, "limit") ?? "50"), 1), 200);
|
|
5697
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 });
|
|
5698
|
-
if (asked.code !== 0)
|
|
5522
|
+
if (asked.code !== 0)
|
|
5699
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);
|
|
5700
|
-
}
|
|
5701
5524
|
let issues;
|
|
5702
5525
|
try {
|
|
5703
5526
|
issues = JSON.parse(asked.stdout);
|
|
@@ -5818,16 +5641,13 @@ function templateCommand(positional, flags, context) {
|
|
|
5818
5641
|
write("`toolroll template show <name>` · `template apply <name> --repo <path>`");
|
|
5819
5642
|
return EXIT.ok;
|
|
5820
5643
|
}
|
|
5821
|
-
if (action !== "show" && action !== "apply")
|
|
5644
|
+
if (action !== "show" && action !== "apply")
|
|
5822
5645
|
return fail(write, json, `template ${action}`, "usage", "`toolroll template list | show <name> | apply <name> --repo <path> [--file]`", EXIT.usage);
|
|
5823
|
-
|
|
5824
|
-
if (name === undefined) {
|
|
5646
|
+
if (name === undefined)
|
|
5825
5647
|
return fail(write, json, `template ${action}`, "usage", "which template? `toolroll template list` names them", EXIT.usage);
|
|
5826
|
-
}
|
|
5827
5648
|
const template = templateByName(name);
|
|
5828
|
-
if (template === null)
|
|
5649
|
+
if (template === null)
|
|
5829
5650
|
return fail(write, json, `template ${action}`, "unknown", `no template named ${name} — \`toolroll template list\``, EXIT.refused);
|
|
5830
|
-
}
|
|
5831
5651
|
if (action === "show") {
|
|
5832
5652
|
if (json) {
|
|
5833
5653
|
write(envelopeJson({ ok: true, command: "template show", template }));
|
|
@@ -5868,13 +5688,11 @@ function templateCommand(positional, flags, context) {
|
|
|
5868
5688
|
return EXIT.ok;
|
|
5869
5689
|
}
|
|
5870
5690
|
// apply
|
|
5871
|
-
if (template.kind === "recipe")
|
|
5691
|
+
if (template.kind === "recipe")
|
|
5872
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);
|
|
5873
|
-
}
|
|
5874
5693
|
const repoGiven = text(flags, "repo");
|
|
5875
|
-
if (repoGiven === undefined)
|
|
5694
|
+
if (repoGiven === undefined)
|
|
5876
5695
|
return fail(write, json, "template apply", "usage", "which repository? --repo <path> — a template never guesses where work lands", EXIT.usage);
|
|
5877
|
-
}
|
|
5878
5696
|
const goal = text(flags, "goal") ?? template.goal;
|
|
5879
5697
|
const outOfScope = text(flags, "not") ?? template.outOfScope;
|
|
5880
5698
|
const touches = text(flags, "touches") === undefined
|
|
@@ -6015,22 +5833,18 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6015
5833
|
const runnerName = text(flags, "runner");
|
|
6016
5834
|
const inlineToken = text(flags, "token");
|
|
6017
5835
|
const tokenPath = text(flags, "token-file");
|
|
6018
|
-
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
5836
|
+
if (inlineToken !== undefined && tokenPath !== undefined)
|
|
6019
5837
|
return fail(write, json, "daemon install", "usage", "choose one credential source: --token or --token-file", EXIT.usage);
|
|
6020
|
-
}
|
|
6021
5838
|
const token = inlineToken ?? readTokenFile(tokenPath);
|
|
6022
|
-
if (runnerName === undefined || token === undefined)
|
|
5839
|
+
if (runnerName === undefined || token === undefined)
|
|
6023
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);
|
|
6024
|
-
}
|
|
6025
5841
|
const auth = authenticate(store, runnerName, token);
|
|
6026
|
-
if (!auth.ok)
|
|
5842
|
+
if (!auth.ok)
|
|
6027
5843
|
return fail(write, json, "daemon install", auth.reason, describeAuth(auth.reason, runnerName), EXIT.refused);
|
|
6028
|
-
}
|
|
6029
5844
|
const heartbeatBefore = store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6030
5845
|
const located = await resolveBin();
|
|
6031
|
-
if (located === null)
|
|
5846
|
+
if (located === null)
|
|
6032
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);
|
|
6033
|
-
}
|
|
6034
5848
|
const watchFlags = [];
|
|
6035
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"]) {
|
|
6036
5850
|
const value = text(flags, name);
|
|
@@ -6067,15 +5881,13 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6067
5881
|
return EXIT.ok;
|
|
6068
5882
|
}
|
|
6069
5883
|
const installed = await installReplacingLegacy(plan, token, legacy, supervise);
|
|
6070
|
-
if (!installed.ok)
|
|
5884
|
+
if (!installed.ok)
|
|
6071
5885
|
return fail(write, json, "daemon install", "supervisor", installed.message, EXIT.failed);
|
|
6072
|
-
}
|
|
6073
5886
|
// The legacy watch beat for this runner until it stopped: only a beat after that proves the new one.
|
|
6074
5887
|
const readySince = legacy === null ? heartbeatBefore : store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6075
5888
|
const started = await daemonStatus(plan, supervise);
|
|
6076
|
-
if (started.state !== "running")
|
|
5889
|
+
if (started.state !== "running")
|
|
6077
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);
|
|
6078
|
-
}
|
|
6079
5891
|
// A supervisor PID is necessary but not sufficient: macOS can leave a
|
|
6080
5892
|
// process stuck behind a protected-folder access check before it ever
|
|
6081
5893
|
// opens the queue. The credentialed runner heartbeat is the end-to-end
|
|
@@ -6725,15 +6537,13 @@ async function watchCommand(flags, context) {
|
|
|
6725
6537
|
return demoFence;
|
|
6726
6538
|
const runner = text(flags, "runner");
|
|
6727
6539
|
const token = text(flags, "token") ?? readTokenFile(text(flags, "token-file"));
|
|
6728
|
-
if (runner === undefined || token === undefined)
|
|
6540
|
+
if (runner === undefined || token === undefined)
|
|
6729
6541
|
return fail(write, json, "watch", "usage", "`toolroll watch --runner <name> --token <t>|--token-file <path> --repo <path> [--for <ms>]`", EXIT.usage);
|
|
6730
|
-
}
|
|
6731
6542
|
// Passes built from these flags authenticate with the resolved token.
|
|
6732
6543
|
flags.set("token", token);
|
|
6733
6544
|
const auth = authenticate(store, runner, token);
|
|
6734
|
-
if (!auth.ok)
|
|
6545
|
+
if (!auth.ok)
|
|
6735
6546
|
return fail(write, json, "watch", auth.reason, describeAuth(auth.reason, runner), EXIT.refused);
|
|
6736
|
-
}
|
|
6737
6547
|
const repo = repoFrom(flags);
|
|
6738
6548
|
// The envelope contract holds for long commands too (Codex M5-M8 audit,
|
|
6739
6549
|
// C-1): in --json mode every progress line goes to stderr, and stdout
|
|
@@ -7028,9 +6838,8 @@ async function onboardCommand(flags, context) {
|
|
|
7028
6838
|
const seams = context.onboardSeams ?? {};
|
|
7029
6839
|
const portGiven = text(flags, "port");
|
|
7030
6840
|
const port = Number(portGiven ?? 4180);
|
|
7031
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6841
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7032
6842
|
return fail(write, json, "onboard", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7033
|
-
}
|
|
7034
6843
|
// The console where `up` last served, unless --port says otherwise.
|
|
7035
6844
|
const served = portGiven === undefined ? readUpConsoleUrl(dirname(context.databaseFile)) : null;
|
|
7036
6845
|
const home = seams.home ?? homedir();
|
|
@@ -7123,32 +6932,26 @@ async function upCommand(flags, context) {
|
|
|
7123
6932
|
// 1. Validation, before anything at all.
|
|
7124
6933
|
const portGiven = text(flags, "port");
|
|
7125
6934
|
const port = Number(portGiven ?? 4180);
|
|
7126
|
-
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
6935
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536))
|
|
7127
6936
|
return fail(write, json, "up", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7128
|
-
}
|
|
7129
6937
|
const editorGiven = text(flags, "editor");
|
|
7130
|
-
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
6938
|
+
if (editorGiven !== undefined && editorGiven !== "vscode")
|
|
7131
6939
|
return fail(write, json, "up", "usage", "--editor supports: vscode", EXIT.usage);
|
|
7132
|
-
}
|
|
7133
6940
|
const forGiven = text(flags, "for");
|
|
7134
|
-
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
6941
|
+
if (forGiven !== undefined && (!Number.isInteger(Number(forGiven)) || Number(forGiven) <= 0))
|
|
7135
6942
|
return fail(write, json, "up", "usage", "--for takes a positive whole number of milliseconds", EXIT.usage);
|
|
7136
|
-
}
|
|
7137
6943
|
const capacityGiven = text(flags, "capacity");
|
|
7138
6944
|
const capacityFlag = capacityGiven === undefined ? null : parseRunnerCapacity(capacityGiven);
|
|
7139
|
-
if (capacityGiven !== undefined && capacityFlag === null)
|
|
6945
|
+
if (capacityGiven !== undefined && capacityFlag === null)
|
|
7140
6946
|
return fail(write, json, "up", "usage", `--capacity is a whole number of tasks, 1 to ${RUNNER_CAPACITY_MAX}`, EXIT.usage);
|
|
7141
|
-
}
|
|
7142
6947
|
const runnerFlag = text(flags, "runner");
|
|
7143
|
-
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
6948
|
+
if (runnerFlag !== undefined && !validRunnerName(runnerFlag))
|
|
7144
6949
|
return fail(write, json, "up", "usage", `a worker name is 1–${RUNNER_NAME_MAX} characters with no control characters`, EXIT.usage);
|
|
7145
|
-
}
|
|
7146
6950
|
// --host / --allow-host (setup review): `up` reaches a phone over a
|
|
7147
6951
|
// tailnet exactly as `serve` does, so one process is the whole install.
|
|
7148
6952
|
const hostFlag = text(flags, "host") ?? "127.0.0.1";
|
|
7149
|
-
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
6953
|
+
if (!/^[A-Za-z0-9.:-]{1,253}$/.test(hostFlag))
|
|
7150
6954
|
return fail(write, json, "up", "usage", "--host is an address or a name", EXIT.usage);
|
|
7151
|
-
}
|
|
7152
6955
|
const allowFlag = text(flags, "allow-host");
|
|
7153
6956
|
// --public-url: `up` serves a real domain exactly as `serve` does (one process is the whole team server).
|
|
7154
6957
|
const publicUrlFlag = text(flags, "public-url");
|
|
@@ -7166,9 +6969,8 @@ async function upCommand(flags, context) {
|
|
|
7166
6969
|
// person must repeat on every start.
|
|
7167
6970
|
const registryPath = registryPathOf(context);
|
|
7168
6971
|
const loadedRegistry = await loadProjectRegistry(registryPath);
|
|
7169
|
-
if ("error" in loadedRegistry)
|
|
6972
|
+
if ("error" in loadedRegistry)
|
|
7170
6973
|
return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
|
|
7171
|
-
}
|
|
7172
6974
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7173
6975
|
const rootInputs = (text(flags, "project-root") ?? "")
|
|
7174
6976
|
.split(",")
|
|
@@ -7177,9 +6979,8 @@ async function upCommand(flags, context) {
|
|
|
7177
6979
|
const explicitRoots = [];
|
|
7178
6980
|
for (const input of rootInputs) {
|
|
7179
6981
|
const canonical = canonicalProject(input);
|
|
7180
|
-
if (canonical === null)
|
|
6982
|
+
if (canonical === null)
|
|
7181
6983
|
return fail(write, json, "up", "project-root", `${input} is not a directory this machine can use`, EXIT.refused);
|
|
7182
|
-
}
|
|
7183
6984
|
if (!explicitRoots.includes(canonical))
|
|
7184
6985
|
explicitRoots.push(canonical);
|
|
7185
6986
|
}
|
|
@@ -7216,9 +7017,8 @@ async function upCommand(flags, context) {
|
|
|
7216
7017
|
if (explicitRepos.length > 0) {
|
|
7217
7018
|
for (const input of explicitRepos) {
|
|
7218
7019
|
const root = await proveRepo(input);
|
|
7219
|
-
if (root === null)
|
|
7020
|
+
if (root === null)
|
|
7220
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);
|
|
7221
|
-
}
|
|
7222
7022
|
if (!repos.includes(root))
|
|
7223
7023
|
repos.push(root);
|
|
7224
7024
|
pinnedRepos.add(root);
|
|
@@ -7231,9 +7031,8 @@ async function upCommand(flags, context) {
|
|
|
7231
7031
|
if (cwdRepo !== null)
|
|
7232
7032
|
pinnedRepos.add(cwdRepo);
|
|
7233
7033
|
}
|
|
7234
|
-
if (repos.length === 0 && projectRoots.length === 0)
|
|
7034
|
+
if (repos.length === 0 && projectRoots.length === 0)
|
|
7235
7035
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
7236
|
-
}
|
|
7237
7036
|
// A person at a terminal reads three lines (onboarding); the folders are for --verbose, a service log, or an agent.
|
|
7238
7037
|
const quietStart = !json && !flags.has("verbose") && (context.upSeams?.terminal ?? process.stdout.isTTY === true) && !underAgent(context.upSeams?.env ?? process.env);
|
|
7239
7038
|
if (!quietStart) {
|
|
@@ -7249,9 +7048,8 @@ async function upCommand(flags, context) {
|
|
|
7249
7048
|
probe.once("error", () => resolveProbe(false));
|
|
7250
7049
|
probe.listen(port, "127.0.0.1", () => resolveProbe(true));
|
|
7251
7050
|
});
|
|
7252
|
-
if (!reserved)
|
|
7051
|
+
if (!reserved)
|
|
7253
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);
|
|
7254
|
-
}
|
|
7255
7053
|
// Under a coding agent nothing is asked and no secret is printed, even in a real terminal: what is shown lands in
|
|
7256
7054
|
// its transcript.
|
|
7257
7055
|
const agentRunning = underAgent(context.upSeams?.env ?? process.env);
|
|
@@ -7262,9 +7060,8 @@ async function upCommand(flags, context) {
|
|
|
7262
7060
|
try {
|
|
7263
7061
|
// 4. The approver (findings 2/8/17/21/27/29/32).
|
|
7264
7062
|
const approverPlan = await resolveUpApprover(context, flags, canPrompt);
|
|
7265
|
-
if ("refusal" in approverPlan)
|
|
7063
|
+
if ("refusal" in approverPlan)
|
|
7266
7064
|
return fail(write, json, "up", "login", approverPlan.refusal, EXIT.refused);
|
|
7267
|
-
}
|
|
7268
7065
|
approver = approverPlan;
|
|
7269
7066
|
for (const note of approverPlan.notes)
|
|
7270
7067
|
progress(note);
|
|
@@ -7272,9 +7069,8 @@ async function upCommand(flags, context) {
|
|
|
7272
7069
|
// recovering runner authority on an UNVERIFIED operator identity was a
|
|
7273
7070
|
// registration road around the ceremony. No verified password, no
|
|
7274
7071
|
// runner — the login file or another approver restores the road.
|
|
7275
|
-
if (!approverPlan.verified)
|
|
7072
|
+
if (!approverPlan.verified)
|
|
7276
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);
|
|
7277
|
-
}
|
|
7278
7074
|
// 5. The runner, through the atomic door (findings 1/16/26/31), with
|
|
7279
7075
|
// the suffix budget (finding 24) for generated names only.
|
|
7280
7076
|
const base = runnerFlag ?? normalizeRunnerName(hostname());
|
|
@@ -7759,9 +7555,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7759
7555
|
if (demoFence !== null)
|
|
7760
7556
|
return demoFence;
|
|
7761
7557
|
const [channel, action] = positional;
|
|
7762
|
-
if (channel !== "telegram")
|
|
7558
|
+
if (channel !== "telegram")
|
|
7763
7559
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status|digest|retry]`", EXIT.usage);
|
|
7764
|
-
}
|
|
7765
7560
|
if (action === "retry") {
|
|
7766
7561
|
// Replies already written and fenced on the pairing like every send: this only moves their next attempt to now.
|
|
7767
7562
|
const retried = store.retryTelegramReplies(null, clock());
|
|
@@ -7777,9 +7572,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7777
7572
|
removed ? "Token file removed." : "There was no token file to remove.",
|
|
7778
7573
|
]);
|
|
7779
7574
|
}
|
|
7780
|
-
if (value === undefined)
|
|
7575
|
+
if (value === undefined)
|
|
7781
7576
|
return fail(write, json, "bridge token", "usage", "`toolroll bridge telegram token <bot-token>` (from @BotFather), or --clear", EXIT.usage);
|
|
7782
|
-
}
|
|
7783
7577
|
const saved = saveBotToken(context.telegramTokenFile, value);
|
|
7784
7578
|
if (!saved.ok)
|
|
7785
7579
|
return fail(write, json, "bridge token", "bad-token", saved.message, EXIT.refused);
|
|
@@ -7806,13 +7600,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7806
7600
|
]);
|
|
7807
7601
|
}
|
|
7808
7602
|
const parsed = /^([1-9][0-9]{0,3})(m|h)$/.exec(every);
|
|
7809
|
-
if (parsed === null)
|
|
7603
|
+
if (parsed === null)
|
|
7810
7604
|
return fail(write, json, "bridge digest", "usage", "--every takes minutes or hours: 30m, 2h, 24h — or --off", EXIT.usage);
|
|
7811
|
-
}
|
|
7812
7605
|
const everyMs = Number(parsed[1]) * (parsed[2] === "h" ? 3_600_000 : 60_000);
|
|
7813
|
-
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7606
|
+
if (everyMs < 5 * 60_000 || everyMs > 7 * 24 * 3_600_000)
|
|
7814
7607
|
return fail(write, json, "bridge digest", "usage", "the digest cadence is between 5 minutes and 7 days", EXIT.usage);
|
|
7815
|
-
}
|
|
7816
7608
|
store.setTelegramDigest(everyMs, "cli", clock());
|
|
7817
7609
|
return succeed(write, json, "bridge digest", { everyMs }, () => [
|
|
7818
7610
|
`Digest every ${digestWords(everyMs)}. Routine facts are held and sent together; decisions and anything that needs you now still page at once.`,
|
|
@@ -7847,18 +7639,15 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7847
7639
|
}
|
|
7848
7640
|
if (action === "pair" || action === "unpair") {
|
|
7849
7641
|
const acting = await askCredentials(flags, context);
|
|
7850
|
-
if (acting === null)
|
|
7642
|
+
if (acting === null)
|
|
7851
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);
|
|
7852
|
-
}
|
|
7853
7644
|
const { name: asWho, token } = acting;
|
|
7854
7645
|
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7855
|
-
if (!authenticated.ok)
|
|
7646
|
+
if (!authenticated.ok)
|
|
7856
7647
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7857
|
-
}
|
|
7858
7648
|
if (action === "unpair") {
|
|
7859
|
-
if (source === null)
|
|
7649
|
+
if (source === null)
|
|
7860
7650
|
return fail(write, json, "bridge unpair", "no-token", "no bot token, so no bot to unpair", EXIT.refused);
|
|
7861
|
-
}
|
|
7862
7651
|
const revoked = store.unpairTelegram(source.botId, asWho, clock());
|
|
7863
7652
|
return succeed(write, json, "bridge unpair", { revoked }, () => [
|
|
7864
7653
|
revoked
|
|
@@ -7877,13 +7666,11 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7877
7666
|
"chat only, and the chat will answer as you — treat it accordingly.",
|
|
7878
7667
|
]);
|
|
7879
7668
|
}
|
|
7880
|
-
if (action !== undefined)
|
|
7669
|
+
if (action !== undefined)
|
|
7881
7670
|
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status]`", EXIT.usage);
|
|
7882
|
-
}
|
|
7883
7671
|
// The pass.
|
|
7884
|
-
if (source === null)
|
|
7672
|
+
if (source === null)
|
|
7885
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);
|
|
7886
|
-
}
|
|
7887
7674
|
const fenced = networkFence(context.telegramEnv ?? (context.telegramTransport === undefined ? process.env : {}));
|
|
7888
7675
|
if (fenced !== null)
|
|
7889
7676
|
return fail(write, json, "bridge", "network-fenced", networkFenceLine(fenced), EXIT.refused);
|
|
@@ -7948,9 +7735,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7948
7735
|
clock,
|
|
7949
7736
|
...(flags.has("inbound-only") ? { deliver: false } : {}),
|
|
7950
7737
|
});
|
|
7951
|
-
if (!passed.ok)
|
|
7738
|
+
if (!passed.ok)
|
|
7952
7739
|
return fail(write, json, "bridge", passed.reason, passed.message, EXIT.refused);
|
|
7953
|
-
}
|
|
7954
7740
|
const { report } = passed;
|
|
7955
7741
|
const broke = report.problems.length > 0;
|
|
7956
7742
|
const idle = report.sent === 0 && report.answered === 0 && report.paired === 0 && (report.statusReplies ?? 0) === 0
|
|
@@ -7960,12 +7746,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7960
7746
|
...(report.backlog ? ["Telegram still holds more updates than one pass's budget — run it again."] : []),
|
|
7961
7747
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
7962
7748
|
];
|
|
7963
|
-
if (broke)
|
|
7749
|
+
if (broke)
|
|
7964
7750
|
return fail(write, json, "bridge", "telegram-transport", lines().join("\n"), EXIT.failed, { report });
|
|
7965
|
-
|
|
7966
|
-
if (idle) {
|
|
7751
|
+
if (idle)
|
|
7967
7752
|
return fail(write, json, "bridge", "idle", "nothing to send, nothing arrived", EXIT.refused, { report });
|
|
7968
|
-
}
|
|
7969
7753
|
return succeed(write, json, "bridge", { report }, lines);
|
|
7970
7754
|
}
|
|
7971
7755
|
// ---- publication -----------------------------------------------------------
|
|
@@ -7993,14 +7777,12 @@ async function publishCommand(positional, flags, context) {
|
|
|
7993
7777
|
const [action] = positional;
|
|
7994
7778
|
// Bare `publish` IS an action: the publication pass (push branches,
|
|
7995
7779
|
// open PRs, sweep merges) — only a NAMED unknown action refuses.
|
|
7996
|
-
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7780
|
+
if (action !== undefined && !PUBLISH_ACTIONS.includes(action))
|
|
7997
7781
|
return fail(write, json, "publish", "usage", `unknown \`publish ${action}\` — try ${PUBLISH_ACTIONS.join(", ")}, or bare \`publish\` for the publication pass`, EXIT.usage);
|
|
7998
|
-
}
|
|
7999
7782
|
if (action === "setup") {
|
|
8000
7783
|
const mergeMethod = text(flags, "merge-method") ?? "squash";
|
|
8001
|
-
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
7784
|
+
if (!["squash", "merge", "rebase"].includes(mergeMethod))
|
|
8002
7785
|
return fail(write, json, "publish setup", "usage", "--merge-method is squash, merge or rebase", EXIT.usage);
|
|
8003
|
-
}
|
|
8004
7786
|
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
8005
7787
|
if (!checked.ok)
|
|
8006
7788
|
return fail(write, json, "publish setup", checked.reason, checked.message, EXIT.refused);
|
|
@@ -8022,26 +7804,20 @@ async function publishCommand(positional, flags, context) {
|
|
|
8022
7804
|
return EXIT.refused;
|
|
8023
7805
|
}
|
|
8024
7806
|
const acting = await askCredentials(flags, context);
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token, repo);
|
|
8029
|
-
if (!authenticated.ok) {
|
|
8030
|
-
return fail(write, json, "publish setup", authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8031
|
-
}
|
|
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);
|
|
8032
7810
|
savePublishing(store, plan, acting.name, settings, clock());
|
|
8033
7811
|
return succeed(write, json, "publish setup", { publishing: publishingOf(store, repo) }, () => ["Pull requests are on.", ...terms]);
|
|
8034
7812
|
}
|
|
8035
7813
|
if (action === "grant") {
|
|
8036
7814
|
const github = text(flags, "github");
|
|
8037
|
-
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
7815
|
+
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github))
|
|
8038
7816
|
return fail(write, json, "publish grant", "usage", "`--github <owner/name>` is required, exactly", EXIT.usage);
|
|
8039
|
-
}
|
|
8040
7817
|
const wantsMerge = flags.has("allow-merge");
|
|
8041
7818
|
const mergeMethod = text(flags, "merge-method");
|
|
8042
|
-
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
7819
|
+
if (wantsMerge && (mergeMethod === undefined || !["squash", "merge", "rebase"].includes(mergeMethod)))
|
|
8043
7820
|
return fail(write, json, "publish grant", "usage", "--allow-merge names its method: --merge-method squash|merge|rebase", EXIT.usage);
|
|
8044
|
-
}
|
|
8045
7821
|
const spec = {
|
|
8046
7822
|
repo,
|
|
8047
7823
|
githubRepo: github,
|
|
@@ -8082,14 +7858,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8082
7858
|
return EXIT.refused;
|
|
8083
7859
|
}
|
|
8084
7860
|
const acting = await askCredentials(flags, context);
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
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);
|
|
8088
7864
|
const { name: asWho, token } = acting;
|
|
8089
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8090
|
-
if (!authenticated.ok) {
|
|
8091
|
-
return fail(write, json, "publish grant", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8092
|
-
}
|
|
8093
7865
|
store.savePublicationGrant({ ...spec, grantedBy: asWho }, clock());
|
|
8094
7866
|
return succeed(write, json, "publish grant", { granted: true, grant: spec }, () => [
|
|
8095
7867
|
`Granted by ${asWho}:`,
|
|
@@ -8099,14 +7871,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8099
7871
|
}
|
|
8100
7872
|
if (action === "revoke") {
|
|
8101
7873
|
const acting = await askCredentials(flags, context);
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
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);
|
|
8105
7877
|
const { name: asWho, token } = acting;
|
|
8106
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
8107
|
-
if (!authenticated.ok) {
|
|
8108
|
-
return fail(write, json, "publish revoke", authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
8109
|
-
}
|
|
8110
7878
|
const revoked = store.revokePublicationGrant(repo, asWho, clock());
|
|
8111
7879
|
return succeed(write, json, "publish revoke", { revoked }, () => [
|
|
8112
7880
|
revoked
|
|
@@ -8131,25 +7899,18 @@ async function publishCommand(positional, flags, context) {
|
|
|
8131
7899
|
if (action === "merge" || action === "refire") {
|
|
8132
7900
|
const prGiven = positional[1];
|
|
8133
7901
|
const pr = Number(prGiven);
|
|
8134
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7902
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8135
7903
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8136
|
-
}
|
|
8137
7904
|
const acting = await askCredentials(flags, context);
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8142
|
-
if (!authenticated.ok) {
|
|
8143
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8144
|
-
}
|
|
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);
|
|
8145
7908
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8146
|
-
if (publication === undefined)
|
|
7909
|
+
if (publication === undefined)
|
|
8147
7910
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8148
|
-
}
|
|
8149
7911
|
const intent = store.mergeIntentFor(publication.id);
|
|
8150
|
-
if (intent === null)
|
|
7912
|
+
if (intent === null)
|
|
8151
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);
|
|
8152
|
-
}
|
|
8153
7914
|
if (action === "merge") {
|
|
8154
7915
|
// The EXACT-INTENT ceremony (E1): the yes covers this head and no
|
|
8155
7916
|
// other — a moved head refuses rather than authorizing the unseen.
|
|
@@ -8178,21 +7939,15 @@ async function publishCommand(positional, flags, context) {
|
|
|
8178
7939
|
if (action === "unblock" || action === "rearm") {
|
|
8179
7940
|
const prGiven = positional[1];
|
|
8180
7941
|
const pr = Number(prGiven);
|
|
8181
|
-
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
7942
|
+
if (prGiven === undefined || !Number.isInteger(pr) || pr <= 0)
|
|
8182
7943
|
return fail(write, json, `publish ${action}`, "usage", `\`toolroll publish ${action} <pr> --as <you> --token <t>\``, EXIT.usage);
|
|
8183
|
-
}
|
|
8184
7944
|
const acting = await askCredentials(flags, context);
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
8189
|
-
if (!authenticated.ok) {
|
|
8190
|
-
return fail(write, json, `publish ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8191
|
-
}
|
|
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);
|
|
8192
7948
|
const publication = store.openedPublications().find(one => one.prNumber === pr);
|
|
8193
|
-
if (publication === undefined)
|
|
7949
|
+
if (publication === undefined)
|
|
8194
7950
|
return fail(write, json, `publish ${action}`, "unknown", `no open publication holds PR #${pr}`, EXIT.refused);
|
|
8195
|
-
}
|
|
8196
7951
|
if (action === "unblock") {
|
|
8197
7952
|
const lifted = store.liftMergeBlocker(publication.id, acting.name, context.clock());
|
|
8198
7953
|
store.resolveEpisodes(`merge-attn:${publication.id}`, context.clock());
|
|
@@ -8210,9 +7965,8 @@ async function publishCommand(positional, flags, context) {
|
|
|
8210
7965
|
: `PR #${pr} had no refused merge to re-arm.`,
|
|
8211
7966
|
]);
|
|
8212
7967
|
}
|
|
8213
|
-
if (action !== undefined)
|
|
7968
|
+
if (action !== undefined)
|
|
8214
7969
|
return fail(write, json, "publish", "usage", "`toolroll publish [grant|revoke|status|unblock|rearm]`", EXIT.usage);
|
|
8215
|
-
}
|
|
8216
7970
|
// The pass.
|
|
8217
7971
|
const report = await publishPass(store, {
|
|
8218
7972
|
repo,
|
|
@@ -8234,12 +7988,10 @@ async function publishCommand(positional, flags, context) {
|
|
|
8234
7988
|
...report.problems.map(problem => ` problem: ${problem}`),
|
|
8235
7989
|
...merges.problems.map(problem => ` merge: ${problem}`),
|
|
8236
7990
|
];
|
|
8237
|
-
if (report.problems.length > 0)
|
|
7991
|
+
if (report.problems.length > 0)
|
|
8238
7992
|
return fail(write, json, "publish", "publish-problems", lines().join("\n"), EXIT.failed, { report });
|
|
8239
|
-
|
|
8240
|
-
if (idle) {
|
|
7993
|
+
if (idle)
|
|
8241
7994
|
return fail(write, json, "publish", "idle", "nothing owed — no pending publications", EXIT.refused, { report });
|
|
8242
|
-
}
|
|
8243
7995
|
return succeed(write, json, "publish", { report }, lines);
|
|
8244
7996
|
}
|
|
8245
7997
|
// ---- the outbox -----------------------------------------------------------
|
|
@@ -8263,9 +8015,8 @@ async function peekCommand(positional, flags, context) {
|
|
|
8263
8015
|
}
|
|
8264
8016
|
const linesGiven = text(flags, "lines");
|
|
8265
8017
|
const lines = linesGiven === undefined ? 12 : Number(linesGiven);
|
|
8266
|
-
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8018
|
+
if (!Number.isInteger(lines) || lines < 1 || lines > PEEK_TAIL_LINES)
|
|
8267
8019
|
return fail(write, json, "peek", "usage", `--lines is a whole number from 1 to ${PEEK_TAIL_LINES}`, EXIT.usage);
|
|
8268
|
-
}
|
|
8269
8020
|
const interactive = !json && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
8270
8021
|
if (flags.has("tmux")) {
|
|
8271
8022
|
if (!interactive)
|
|
@@ -8322,9 +8073,8 @@ async function outboxCommand(positional, flags, context) {
|
|
|
8322
8073
|
if (demoFence !== null)
|
|
8323
8074
|
return demoFence;
|
|
8324
8075
|
const command = text(flags, "cmd");
|
|
8325
|
-
if (command === undefined)
|
|
8076
|
+
if (command === undefined)
|
|
8326
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);
|
|
8327
|
-
}
|
|
8328
8078
|
// The exact command owns a leased destination receipt, including across concurrent CLI invocations.
|
|
8329
8079
|
const owner = `outbox-${randomUUID()}`;
|
|
8330
8080
|
// Push delivery has independent receipts and must keep working if this command fails.
|
|
@@ -8397,28 +8147,23 @@ async function enrollCommand(positional, flags, context) {
|
|
|
8397
8147
|
const repo = positional[0] === undefined ? process.cwd() : resolve(positional[0]);
|
|
8398
8148
|
const backend = text(flags, "backend") ?? BUILT_IN;
|
|
8399
8149
|
const mutations = readMutations(flags);
|
|
8400
|
-
if (mutations === null)
|
|
8150
|
+
if (mutations === null)
|
|
8401
8151
|
return fail(write, json, "enroll", "usage", `--allow takes ${MUTATION_CLASSES.join(", ")}`, EXIT.usage);
|
|
8402
|
-
}
|
|
8403
8152
|
const selectorFlag = text(flags, "selector") ?? "ours";
|
|
8404
|
-
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8153
|
+
if (selectorFlag !== "ours" && selectorFlag !== "all")
|
|
8405
8154
|
return fail(write, json, "enroll", "usage", "--selector is `ours` or `all`", EXIT.usage);
|
|
8406
|
-
}
|
|
8407
8155
|
const paths = readPaths(flags, backend);
|
|
8408
|
-
if (paths.length === 0)
|
|
8156
|
+
if (paths.length === 0)
|
|
8409
8157
|
return fail(write, json, "enroll", "usage", `--paths says what may be written for backend \`${backend}\``, EXIT.usage);
|
|
8410
|
-
}
|
|
8411
8158
|
// External dispatch (v20): its own explicit yes, never in any default.
|
|
8412
8159
|
// A dispatch grant binds EXACTLY ONE remote repository and mints this
|
|
8413
8160
|
// plane's marker identity.
|
|
8414
8161
|
const wantsDispatch = flags.has("allow-dispatch");
|
|
8415
8162
|
const dispatchRepo = text(flags, "github");
|
|
8416
|
-
if (wantsDispatch && backend !== "github-issues")
|
|
8163
|
+
if (wantsDispatch && backend !== "github-issues")
|
|
8417
8164
|
return fail(write, json, "enroll", "usage", "--allow-dispatch is a github-issues authority in this release", EXIT.usage);
|
|
8418
|
-
|
|
8419
|
-
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo))) {
|
|
8165
|
+
if (wantsDispatch && (dispatchRepo === undefined || !GITHUB_REPO_SHAPE.test(dispatchRepo)))
|
|
8420
8166
|
return fail(write, json, "enroll", "usage", "--allow-dispatch binds exactly one tracker: name it with `--github <owner/name>`", EXIT.usage);
|
|
8421
|
-
}
|
|
8422
8167
|
const grant = await proposeGrant({
|
|
8423
8168
|
repo,
|
|
8424
8169
|
backend,
|
|
@@ -8509,17 +8254,14 @@ function grantsCommand(context) {
|
|
|
8509
8254
|
return EXIT.ok;
|
|
8510
8255
|
}
|
|
8511
8256
|
function checkProgressCommand(positional, context) {
|
|
8512
|
-
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] ?? ""))
|
|
8513
8258
|
return fail(context.write, context.json, "check-progress", "usage", "use check-progress <run>", EXIT.usage);
|
|
8514
|
-
}
|
|
8515
8259
|
const run = Number(positional[0]);
|
|
8516
|
-
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8260
|
+
if (!Number.isSafeInteger(run) || context.store.getRun(run) === null)
|
|
8517
8261
|
return fail(context.write, context.json, "check-progress", "unknown-run", "no such run", EXIT.refused);
|
|
8518
|
-
}
|
|
8519
8262
|
const progress = context.store.checkProgress(run);
|
|
8520
|
-
if (progress === null)
|
|
8263
|
+
if (progress === null)
|
|
8521
8264
|
return fail(context.write, context.json, "check-progress", "no-progress", "this run has no saved check progress", EXIT.refused);
|
|
8522
|
-
}
|
|
8523
8265
|
return succeed(context.write, context.json, "check-progress", { run, progress }, () => [progress.line]);
|
|
8524
8266
|
}
|
|
8525
8267
|
function revokeCommand(positional, flags, context) {
|
|
@@ -8535,9 +8277,8 @@ function revokeCommand(positional, flags, context) {
|
|
|
8535
8277
|
if (revoked && outgoing?.dispatch === true && outgoing.remoteRepo != null) {
|
|
8536
8278
|
void ghDispatchAdapter().deleteMarker(outgoing.remoteRepo);
|
|
8537
8279
|
}
|
|
8538
|
-
if (!revoked)
|
|
8280
|
+
if (!revoked)
|
|
8539
8281
|
return fail(write, json, "revoke", "no-grant", `${repo} was not enrolled for ${backend}`, EXIT.refused);
|
|
8540
|
-
}
|
|
8541
8282
|
return succeed(write, json, "revoke", { repo, backend }, () => [
|
|
8542
8283
|
`Revoked. ${backend} in ${repo} is read-only again.`,
|
|
8543
8284
|
]);
|
|
@@ -8675,14 +8416,12 @@ async function waitTask(positional, flags, context) {
|
|
|
8675
8416
|
if (!allowed.has(name))
|
|
8676
8417
|
return fail(context.write, context.json, command, "usage", `--${name} is not a task wait option.`, EXIT.usage);
|
|
8677
8418
|
}
|
|
8678
|
-
if (positional.length !== 1)
|
|
8419
|
+
if (positional.length !== 1)
|
|
8679
8420
|
return fail(context.write, context.json, command, "usage", "Use `toolroll task wait <id> [--timeout <seconds>]`.", EXIT.usage);
|
|
8680
|
-
}
|
|
8681
8421
|
const givenTimeout = text(flags, "timeout");
|
|
8682
8422
|
const timeoutSeconds = givenTimeout === undefined ? null : Number(givenTimeout);
|
|
8683
|
-
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8423
|
+
if (timeoutSeconds !== null && (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0))
|
|
8684
8424
|
return fail(context.write, context.json, command, "usage", "--timeout must be zero or a positive number of seconds.", EXIT.usage);
|
|
8685
|
-
}
|
|
8686
8425
|
const task = positional[0];
|
|
8687
8426
|
const startedAt = Date.now();
|
|
8688
8427
|
const timeoutMs = timeoutSeconds === null ? null : timeoutSeconds * 1_000;
|
|
@@ -8836,15 +8575,13 @@ async function addTask(positional, flags, context) {
|
|
|
8836
8575
|
// A scout task lives in the built-in backend (v4 review, finding 2):
|
|
8837
8576
|
// the deliverable is a column on OUR task_ref, stamped in the same
|
|
8838
8577
|
// transaction as the filing — a tracker's item has no such promise.
|
|
8839
|
-
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8578
|
+
if (flags.has("report") && backendName !== BUILT_IN)
|
|
8840
8579
|
return fail(write, json, "task add", "usage", "--report files a scout task in the built-in backend only — drop --backend", EXIT.usage);
|
|
8841
|
-
}
|
|
8842
8580
|
if (backendName !== BUILT_IN) {
|
|
8843
8581
|
const repo = repoFrom(flags);
|
|
8844
8582
|
const backend = openBackend(backendName, store, repo);
|
|
8845
|
-
if (backend === null)
|
|
8583
|
+
if (backend === null)
|
|
8846
8584
|
return fail(write, json, "task add", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
8847
|
-
}
|
|
8848
8585
|
const created = await backend.create({ title });
|
|
8849
8586
|
if (!created.ok) {
|
|
8850
8587
|
// A denial is a refusal, not a breakage: the tool worked exactly as
|
|
@@ -8890,9 +8627,8 @@ async function addTask(positional, flags, context) {
|
|
|
8890
8627
|
const id = text(flags, "id") ?? (activeRemote() === null ? slug(title, now) : `task-${randomBytes(16).toString("hex")}`);
|
|
8891
8628
|
// Re-filing: the new task replaces an earlier one, which is cancelled and reads "Replaced by <id>".
|
|
8892
8629
|
const replaces = text(flags, "replaces");
|
|
8893
|
-
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8630
|
+
if (replaces !== undefined && (replaces === id || store.getTask(replaces) === null))
|
|
8894
8631
|
return fail(write, json, "task add", "unknown-task", `no task \`${replaces}\` to replace`, EXIT.refused);
|
|
8895
|
-
}
|
|
8896
8632
|
noteActingPerson(flags, context);
|
|
8897
8633
|
// The existence check goes *inside* the replayed body. Outside it, a retry
|
|
8898
8634
|
// with the same key hits "already exists" and reports failure for a task the
|
|
@@ -8907,9 +8643,8 @@ async function addTask(positional, flags, context) {
|
|
|
8907
8643
|
// branch task.
|
|
8908
8644
|
return { ok: true, task: store.createTask({ id, title, ...(flags.has("report") ? { deliverable: "report" } : {}) }, now) };
|
|
8909
8645
|
}, result => result.ok);
|
|
8910
|
-
if (!outcome.ok)
|
|
8646
|
+
if (!outcome.ok)
|
|
8911
8647
|
return fail(write, json, "task add", "exists", `\`${id}\` already exists`, EXIT.refused);
|
|
8912
|
-
}
|
|
8913
8648
|
const filedId = outcome.task.id;
|
|
8914
8649
|
const remote = activeRemote();
|
|
8915
8650
|
if (remote !== null && filedId !== id) {
|
|
@@ -8926,9 +8661,8 @@ async function addTask(positional, flags, context) {
|
|
|
8926
8661
|
const placedIn = text(flags, "repo");
|
|
8927
8662
|
if (placedIn !== undefined) {
|
|
8928
8663
|
const placed = store.placeTask(store.refFor(BUILT_IN, filedId).id, canonicalProject(placedIn) ?? resolve(placedIn));
|
|
8929
|
-
if (typeof placed === "object" && !placed.ok)
|
|
8664
|
+
if (typeof placed === "object" && !placed.ok)
|
|
8930
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);
|
|
8931
|
-
}
|
|
8932
8666
|
}
|
|
8933
8667
|
if (replaces !== undefined)
|
|
8934
8668
|
store.setTaskState(replaces, "cancelled", now, {}, undefined, outcome.task.id);
|
|
@@ -9029,17 +8763,14 @@ function requireTask(positional, flags, context) {
|
|
|
9029
8763
|
const { store, write, json, now } = context;
|
|
9030
8764
|
const id = positional[0];
|
|
9031
8765
|
const given = text(flags, "cap");
|
|
9032
|
-
if (id === undefined || given === undefined)
|
|
8766
|
+
if (id === undefined || given === undefined)
|
|
9033
8767
|
return fail(write, json, "task require", "usage", "`toolroll task require <id> --cap <kind:name>[,<kind:name>]` — or --cap none to clear", EXIT.usage);
|
|
9034
|
-
|
|
9035
|
-
if (store.getTask(id) === null) {
|
|
8768
|
+
if (store.getTask(id) === null)
|
|
9036
8769
|
return fail(write, json, "task require", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9037
|
-
}
|
|
9038
8770
|
const keys = given === "none" ? [] : given.split(",").map(one => one.trim()).filter(Boolean);
|
|
9039
8771
|
for (const key of keys) {
|
|
9040
|
-
if (parseCapabilityKey(key) === null)
|
|
8772
|
+
if (parseCapabilityKey(key) === null)
|
|
9041
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);
|
|
9042
|
-
}
|
|
9043
8774
|
}
|
|
9044
8775
|
store.setRequirements(store.refFor(BUILT_IN, id).id, keys, mutationFrom(flags, now));
|
|
9045
8776
|
return succeed(write, json, "task require", { id, requirements: keys }, () => [
|
|
@@ -9062,21 +8793,18 @@ async function capCommand(positional, flags, context) {
|
|
|
9062
8793
|
const [action, name] = positional;
|
|
9063
8794
|
const repo = repoFrom(flags);
|
|
9064
8795
|
if (action === "add") {
|
|
9065
|
-
if (name === undefined)
|
|
8796
|
+
if (name === undefined)
|
|
9066
8797
|
return fail(write, json, "cap add", "usage", "`toolroll cap add <name> [--kind env|cli|mcp|ci|other] [--probe <cmd>] [--expires <iso>]`", EXIT.usage);
|
|
9067
|
-
}
|
|
9068
8798
|
const kind = (text(flags, "kind") ?? "env");
|
|
9069
|
-
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
8799
|
+
if (!["env", "cli", "mcp", "ci", "other"].includes(kind))
|
|
9070
8800
|
return fail(write, json, "cap add", "usage", "--kind takes env, cli, mcp, ci or other", EXIT.usage);
|
|
9071
|
-
}
|
|
9072
8801
|
// An env capability can have its probe synthesized from a fixed template
|
|
9073
8802
|
// — but only over a validated identifier, because the name lands inside
|
|
9074
8803
|
// a shell line. Anything else the operator writes explicitly.
|
|
9075
8804
|
let probe = text(flags, "probe") ?? null;
|
|
9076
8805
|
if (probe === null && kind === "env") {
|
|
9077
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
8806
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
9078
8807
|
return fail(write, json, "cap add", "usage", `\`${name}\` is not an environment variable name — give --probe explicitly`, EXIT.usage);
|
|
9079
|
-
}
|
|
9080
8808
|
probe = `test -n "$${name}"`;
|
|
9081
8809
|
}
|
|
9082
8810
|
const expires = text(flags, "expires") ?? null;
|
|
@@ -9161,16 +8889,14 @@ async function capCommand(positional, flags, context) {
|
|
|
9161
8889
|
if (action === "probe") {
|
|
9162
8890
|
const only = positional.slice(1);
|
|
9163
8891
|
for (const key of only) {
|
|
9164
|
-
if (parseCapabilityKey(key) === null)
|
|
8892
|
+
if (parseCapabilityKey(key) === null)
|
|
9165
8893
|
return fail(write, json, "cap probe", "usage", `\`${key}\` is not a capability key — say kind:name`, EXIT.usage);
|
|
9166
|
-
}
|
|
9167
8894
|
}
|
|
9168
8895
|
const outcomes = await probeRepo(store, repo, `operator@${hostname()}`, clock(), {
|
|
9169
8896
|
...(only.length === 0 ? {} : { only: new Set(only) }),
|
|
9170
8897
|
});
|
|
9171
|
-
if (outcomes.length === 0)
|
|
8898
|
+
if (outcomes.length === 0)
|
|
9172
8899
|
return fail(write, json, "cap probe", "empty", `nothing to probe for ${repo}`, EXIT.refused);
|
|
9173
|
-
}
|
|
9174
8900
|
const unverified = outcomes.filter(one => one.status !== "verified");
|
|
9175
8901
|
const lines = () => outcomes.map(one => ` ${`${one.kind}:${one.name}`.padEnd(32)} ${one.status}${one.detail === undefined ? "" : ` ${one.detail}`}`);
|
|
9176
8902
|
if (json) {
|
|
@@ -9191,9 +8917,8 @@ function listTasks(flags, context) {
|
|
|
9191
8917
|
const rawView = text(flags, 'view');
|
|
9192
8918
|
const rawLimit = text(flags, 'limit');
|
|
9193
8919
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
9194
|
-
if (wanted !== undefined && !STATES.includes(wanted))
|
|
8920
|
+
if (wanted !== undefined && !STATES.includes(wanted))
|
|
9195
8921
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
9196
|
-
}
|
|
9197
8922
|
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed'].includes(rawView) ||
|
|
9198
8923
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
9199
8924
|
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running or completed; --limit takes 1–100', EXIT.usage);
|
|
@@ -9444,19 +9169,14 @@ async function acceptTaskProof(positional, flags, context) {
|
|
|
9444
9169
|
if (task === null)
|
|
9445
9170
|
return fail(write, json, "task accept", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9446
9171
|
const acting = await askCredentials(flags, context);
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9451
|
-
if (!authenticated.ok) {
|
|
9452
|
-
return fail(write, json, "task accept", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
9453
|
-
}
|
|
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);
|
|
9454
9175
|
const ref = store.refFor(BUILT_IN, id);
|
|
9455
9176
|
// v40 fix: a reviewer run is never the attempt whose proof is accepted.
|
|
9456
9177
|
const latest = store.runsFor(ref.id).find(one => one.finishedAt !== null && (one.role === "builder" || one.role === "scout"));
|
|
9457
|
-
if (latest === undefined)
|
|
9178
|
+
if (latest === undefined)
|
|
9458
9179
|
return fail(write, json, "task accept", "no-run", `${id} has no finished attempt to accept`, EXIT.refused);
|
|
9459
|
-
}
|
|
9460
9180
|
const rawNote = text(flags, "note");
|
|
9461
9181
|
let note = null;
|
|
9462
9182
|
if (rawNote !== undefined && rawNote.trim() !== "") {
|
|
@@ -9481,9 +9201,8 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9481
9201
|
const { store, write, json, clock } = context;
|
|
9482
9202
|
const [runText] = positional;
|
|
9483
9203
|
const runId = Number(runText ?? "");
|
|
9484
|
-
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9204
|
+
if (runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
9485
9205
|
return fail(write, json, "task repair", "usage", "`toolroll task repair <run-id> [--yes] --as <you> --token <t>`", EXIT.usage);
|
|
9486
|
-
}
|
|
9487
9206
|
let chain = store.repairChainFor(runId);
|
|
9488
9207
|
if (chain === null && flags.get("yes") === true) {
|
|
9489
9208
|
const acting = await askCredentials(flags, context);
|
|
@@ -9500,12 +9219,10 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9500
9219
|
chain = store.repairChainFor(runId);
|
|
9501
9220
|
}
|
|
9502
9221
|
}
|
|
9503
|
-
if (chain === null)
|
|
9222
|
+
if (chain === null)
|
|
9504
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);
|
|
9505
|
-
|
|
9506
|
-
if (chain.draftTask === null) {
|
|
9224
|
+
if (chain.draftTask === null)
|
|
9507
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);
|
|
9508
|
-
}
|
|
9509
9226
|
const draftId = chain.draftTask;
|
|
9510
9227
|
const yes = flags.get("yes") === true;
|
|
9511
9228
|
if (!yes) {
|
|
@@ -9516,21 +9233,15 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9516
9233
|
]);
|
|
9517
9234
|
}
|
|
9518
9235
|
const acting = await askCredentials(flags, context);
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
9523
|
-
if (!authenticated.ok) {
|
|
9524
|
-
return fail(write, json, "task repair", authenticated.reason, describeApproveFailure(authenticated.reason, draftId), EXIT.refused);
|
|
9525
|
-
}
|
|
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);
|
|
9526
9239
|
const draftScope = store.getScope(draftId);
|
|
9527
|
-
if (draftScope === null)
|
|
9240
|
+
if (draftScope === null)
|
|
9528
9241
|
return fail(write, json, "task repair", "no-scope", `${draftId} has no scope to approve`, EXIT.refused);
|
|
9529
|
-
}
|
|
9530
9242
|
const approved = approve(store, draftId, acting.name, clock(), draftScope.digest, acting.token);
|
|
9531
|
-
if (!approved.ok)
|
|
9243
|
+
if (!approved.ok)
|
|
9532
9244
|
return fail(write, json, "task repair", approved.reason, approved.reason === "policy" ? `${draftId}: ${approved.message}` : describeApproveFailure(approved.reason, draftId), EXIT.refused);
|
|
9533
|
-
}
|
|
9534
9245
|
return succeed(write, json, "task repair", { run: runId, draft: draftId, approvedBy: acting.name }, () => [
|
|
9535
9246
|
`Approved: ${draftId} (attempt ${chain.attempt}) will build on the next dispatch.`,
|
|
9536
9247
|
]);
|
|
@@ -9538,19 +9249,16 @@ async function repairTaskCommand(positional, flags, context) {
|
|
|
9538
9249
|
async function stateTask(positional, flags, context) {
|
|
9539
9250
|
const { store, write, json, now } = context;
|
|
9540
9251
|
const [id, state] = positional;
|
|
9541
|
-
if (id === undefined || state === undefined)
|
|
9252
|
+
if (id === undefined || state === undefined)
|
|
9542
9253
|
return fail(write, json, "task state", "usage", "`toolroll task state <id> <state>`", EXIT.usage);
|
|
9543
|
-
|
|
9544
|
-
if (!STATES.includes(state)) {
|
|
9254
|
+
if (!STATES.includes(state))
|
|
9545
9255
|
return fail(write, json, "task state", "usage", `state is one of ${STATES.join(", ")}`, EXIT.usage);
|
|
9546
|
-
}
|
|
9547
9256
|
const backendName = text(flags, "backend") ?? BUILT_IN;
|
|
9548
9257
|
if (backendName !== BUILT_IN) {
|
|
9549
9258
|
const repo = repoFrom(flags);
|
|
9550
9259
|
const backend = openBackend(backendName, store, repo);
|
|
9551
|
-
if (backend === null)
|
|
9260
|
+
if (backend === null)
|
|
9552
9261
|
return fail(write, json, "task state", "usage", `no backend \`${backendName}\``, EXIT.usage);
|
|
9553
|
-
}
|
|
9554
9262
|
const moved = await backend.setState(id, state);
|
|
9555
9263
|
if (!moved.ok) {
|
|
9556
9264
|
const code = moved.reason === "denied" || moved.reason === "unsupported" ? EXIT.refused : EXIT.failed;
|
|
@@ -9601,9 +9309,8 @@ function askTask(positional, flags, context) {
|
|
|
9601
9309
|
const { store, write, json, now } = context;
|
|
9602
9310
|
const [id, ...extra] = positional;
|
|
9603
9311
|
const person = text(flags, "person"), why = text(flags, "why")?.trim();
|
|
9604
|
-
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9312
|
+
if (id === undefined || extra.length > 0 || person === undefined || why === undefined || why === "")
|
|
9605
9313
|
return fail(write, json, "task ask", "usage", "`toolroll task ask <id> --person <name> --why \"what they need to do\"`", EXIT.usage);
|
|
9606
|
-
}
|
|
9607
9314
|
if (why.length > 500 || /[\u0000-\u001f\u007f]/.test(why))
|
|
9608
9315
|
return fail(write, json, "task ask", "usage", "--why is at most 500 plain characters on one line", EXIT.usage);
|
|
9609
9316
|
noteActingPerson(flags, context);
|
|
@@ -9632,9 +9339,8 @@ async function leadCommand(positional, flags, context) {
|
|
|
9632
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);
|
|
9633
9340
|
const acting = await askCredentials(flags, context);
|
|
9634
9341
|
const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
9635
|
-
if (acting === null ||
|
|
9342
|
+
if (acting === null || !verified?.ok)
|
|
9636
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);
|
|
9637
|
-
}
|
|
9638
9344
|
const now = context.clock();
|
|
9639
9345
|
if (flags.has("revoke")) {
|
|
9640
9346
|
const ended = store.revokeLeadCredentials(acting.name, acting.name, now);
|
|
@@ -9688,13 +9394,11 @@ function blockTask(positional, flags, context) {
|
|
|
9688
9394
|
const { store, write, json } = context;
|
|
9689
9395
|
const id = positional[0];
|
|
9690
9396
|
const on = text(flags, "on");
|
|
9691
|
-
if (id === undefined || on === undefined)
|
|
9397
|
+
if (id === undefined || on === undefined)
|
|
9692
9398
|
return fail(write, json, "task block", "usage", "`toolroll task block <id> --on <id>`", EXIT.usage);
|
|
9693
|
-
}
|
|
9694
9399
|
for (const each of [id, on]) {
|
|
9695
|
-
if (store.getTask(each) === null)
|
|
9400
|
+
if (store.getTask(each) === null)
|
|
9696
9401
|
return fail(write, json, "task block", "unknown-task", `no task \`${each}\``, EXIT.refused);
|
|
9697
|
-
}
|
|
9698
9402
|
}
|
|
9699
9403
|
const result = store.addEdge(id, on, mutationFrom(flags, context.now));
|
|
9700
9404
|
if (!result.ok)
|
|
@@ -9712,20 +9416,17 @@ async function steerTask(positional, flags, context) {
|
|
|
9712
9416
|
const { store, write, json } = context;
|
|
9713
9417
|
const id = positional[0];
|
|
9714
9418
|
const note = text(flags, "note");
|
|
9715
|
-
if (id === undefined || note === undefined)
|
|
9419
|
+
if (id === undefined || note === undefined)
|
|
9716
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);
|
|
9717
|
-
}
|
|
9718
9421
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9719
9422
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
9720
9423
|
// but wrong is not-an-approver — the same taxonomy as every ceremony.
|
|
9721
9424
|
const acting = await askCredentials(flags, context);
|
|
9722
|
-
if (acting === null)
|
|
9425
|
+
if (acting === null)
|
|
9723
9426
|
return fail(write, json, "task steer", "usage", "steering takes `--as <you> --token <t>` — anonymous notes never reach an agent's brief", EXIT.usage);
|
|
9724
|
-
}
|
|
9725
9427
|
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9726
|
-
if (!authed.ok)
|
|
9428
|
+
if (!authed.ok)
|
|
9727
9429
|
return fail(write, json, "task steer", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9728
|
-
}
|
|
9729
9430
|
// --now: Pause and steer — the running Claude build's turn stops, then this note continues it.
|
|
9730
9431
|
if (flags.get("now") === true)
|
|
9731
9432
|
return pauseRunningBuild("task steer", id, note, verifiedAuthor(acting.name), flags, context);
|
|
@@ -9828,16 +9529,13 @@ function unblockTask(positional, flags, context) {
|
|
|
9828
9529
|
const { store, write, json } = context;
|
|
9829
9530
|
const id = positional[0];
|
|
9830
9531
|
const on = text(flags, "on");
|
|
9831
|
-
if (id === undefined || on === undefined)
|
|
9532
|
+
if (id === undefined || on === undefined)
|
|
9832
9533
|
return fail(write, json, "task unblock", "usage", "`toolroll task unblock <id> --on <id>`", EXIT.usage);
|
|
9833
|
-
|
|
9834
|
-
if (store.getTask(id) === null) {
|
|
9534
|
+
if (store.getTask(id) === null)
|
|
9835
9535
|
return fail(write, json, "task unblock", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9836
|
-
}
|
|
9837
9536
|
const result = store.removeEdge(id, on, mutationFrom(flags, context.now));
|
|
9838
|
-
if (!result.ok)
|
|
9537
|
+
if (!result.ok)
|
|
9839
9538
|
return fail(write, json, "task unblock", "not-waiting", `${id} was not waiting on ${on}`, EXIT.refused);
|
|
9840
|
-
}
|
|
9841
9539
|
return succeed(write, json, "task unblock", { blocked: id, blocker: on }, () => [
|
|
9842
9540
|
`${id} no longer waits for ${on}.`,
|
|
9843
9541
|
]);
|
|
@@ -9850,9 +9548,8 @@ function unblockTask(positional, flags, context) {
|
|
|
9850
9548
|
function nextTask(positional, flags, context) {
|
|
9851
9549
|
const { store, write, json } = context;
|
|
9852
9550
|
const id = positional[0];
|
|
9853
|
-
if (id === undefined)
|
|
9551
|
+
if (id === undefined)
|
|
9854
9552
|
return fail(write, json, "task next", "usage", "`toolroll task next <id> [--undo]`", EXIT.usage);
|
|
9855
|
-
}
|
|
9856
9553
|
if (flags.has("undo")) {
|
|
9857
9554
|
const cleared = store.clearTaskPriority(id, mutationFrom(flags, context.now));
|
|
9858
9555
|
if (!cleared.ok)
|
|
@@ -9886,9 +9583,8 @@ function assignTask(positional, flags, context) {
|
|
|
9886
9583
|
const id = positional[0];
|
|
9887
9584
|
const runner = text(flags, "runner");
|
|
9888
9585
|
const anyone = flags.has("anyone");
|
|
9889
|
-
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9586
|
+
if (id === undefined || (runner === undefined && !anyone) || (runner !== undefined && anyone))
|
|
9890
9587
|
return fail(write, json, "task assign", "usage", "`toolroll task assign <id> --runner <name> | --anyone`", EXIT.usage);
|
|
9891
|
-
}
|
|
9892
9588
|
const moved = store.moveTask({ taskId: id, toRunner: runner ?? null, beforeTaskId: null }, context.clock(), mutationFrom(flags, context.now));
|
|
9893
9589
|
if (!moved.ok) {
|
|
9894
9590
|
const message = moved.reason === "unknown-task"
|
|
@@ -9918,13 +9614,11 @@ function assignTask(positional, flags, context) {
|
|
|
9918
9614
|
async function reopenTask(positional, flags, context) {
|
|
9919
9615
|
const { store, write, json } = context;
|
|
9920
9616
|
const id = positional[0];
|
|
9921
|
-
if (id === undefined)
|
|
9617
|
+
if (id === undefined)
|
|
9922
9618
|
return fail(write, json, "task reopen", "usage", "`toolroll task reopen <id> --as <you> --token <t>`", EXIT.usage);
|
|
9923
|
-
}
|
|
9924
9619
|
const acting = await askCredentials(flags, context);
|
|
9925
|
-
if (acting === null)
|
|
9620
|
+
if (acting === null)
|
|
9926
9621
|
return fail(write, json, "task reopen", "usage", "reopening takes `--as <you> --token <t>` — who resumed external work is recorded, not asserted", EXIT.usage);
|
|
9927
|
-
}
|
|
9928
9622
|
const reopened = store.reopenMirror(id, acting.name, context.clock());
|
|
9929
9623
|
if (!reopened.ok) {
|
|
9930
9624
|
const said = {
|
|
@@ -9957,9 +9651,8 @@ async function syncCommand(flags, context) {
|
|
|
9957
9651
|
.listGrants()
|
|
9958
9652
|
.filter(grant => grant.dispatch === true && grant.remoteRepo != null)
|
|
9959
9653
|
.filter(grant => only === undefined || grant.repo === resolve(only));
|
|
9960
|
-
if (grants.length === 0)
|
|
9654
|
+
if (grants.length === 0)
|
|
9961
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);
|
|
9962
|
-
}
|
|
9963
9656
|
const adapter = ghDispatchAdapter();
|
|
9964
9657
|
const reports = [];
|
|
9965
9658
|
for (const grant of grants) {
|
|
@@ -9992,12 +9685,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
9992
9685
|
const { store, write, json, now } = context;
|
|
9993
9686
|
const id = positional[0];
|
|
9994
9687
|
const goal = text(flags, "goal");
|
|
9995
|
-
if (id === undefined || goal === undefined)
|
|
9688
|
+
if (id === undefined || goal === undefined)
|
|
9996
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);
|
|
9997
|
-
|
|
9998
|
-
if (store.getTask(id) === null) {
|
|
9690
|
+
if (store.getTask(id) === null)
|
|
9999
9691
|
return fail(write, json, "task scope", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10000
|
-
}
|
|
10001
9692
|
// v39: this road bypasses proposeGuarded (a trusted local operator, not a
|
|
10002
9693
|
// web form re-authenticating staleness) but not the rubric requirement —
|
|
10003
9694
|
// every scope-producing road signs one, this one included.
|
|
@@ -10012,12 +9703,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10012
9703
|
// v69: a prepared commit. The machine checks it out as the attempt and
|
|
10013
9704
|
// runs the gate and the review; no agent is dispatched for it.
|
|
10014
9705
|
const candidateGiven = text(flags, "candidate");
|
|
10015
|
-
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
9706
|
+
if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
|
|
10016
9707
|
return fail(write, json, "task scope", "usage", "--candidate is the full 40-character commit hash the machine will check out", EXIT.usage);
|
|
10017
|
-
|
|
10018
|
-
if (acceptanceParse.criteria.length === 0) {
|
|
9708
|
+
if (acceptanceParse.criteria.length === 0)
|
|
10019
9709
|
return fail(write, json, "task scope", "acceptance-required", "--acceptance named no valid criteria", EXIT.usage);
|
|
10020
|
-
}
|
|
10021
9710
|
const touches = (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(Boolean);
|
|
10022
9711
|
const badText = validateScopeText({ goal, outOfScope: text(flags, "not") ?? null, touches });
|
|
10023
9712
|
if (badText !== null)
|
|
@@ -10029,9 +9718,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10029
9718
|
: defaults?.buildPerRunMicrousd != null
|
|
10030
9719
|
? defaults.buildPerRunMicrousd / 1_000_000
|
|
10031
9720
|
: null;
|
|
10032
|
-
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
9721
|
+
if (budgetGiven !== undefined && (!Number.isFinite(Number(budgetGiven)) || Number(budgetGiven) <= 0))
|
|
10033
9722
|
return fail(write, json, "task scope", "bad-budget", "--budget-usd is a positive dollar amount", EXIT.usage);
|
|
10034
|
-
}
|
|
10035
9723
|
// v24 routing flags (foundations finding 5 — these used to be silently
|
|
10036
9724
|
// swallowed by the global parser): explicit flags resolve HERE, and an
|
|
10037
9725
|
// explicit ask that cannot resolve refuses rather than filing unresolved.
|
|
@@ -10040,9 +9728,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10040
9728
|
if (routingAsked) {
|
|
10041
9729
|
const scopeRef = store.lookupRef(id);
|
|
10042
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") });
|
|
10043
|
-
if (!resolvedRouting.ok)
|
|
9731
|
+
if (!resolvedRouting.ok)
|
|
10044
9732
|
return fail(write, json, "task scope", resolvedRouting.reason, resolvedRouting.problem, EXIT.usage);
|
|
10045
|
-
}
|
|
10046
9733
|
explicitProfile = resolvedRouting.profile;
|
|
10047
9734
|
}
|
|
10048
9735
|
// C1/M3, the credentialed-CLI road: when --as/--token authenticate the
|
|
@@ -10118,9 +9805,8 @@ async function scopeTask(positional, flags, context) {
|
|
|
10118
9805
|
}
|
|
10119
9806
|
return { ok: true, scope: proposed, sealedUnderMode, modeRefusedCoordinator };
|
|
10120
9807
|
}));
|
|
10121
|
-
if (!filed.ok)
|
|
9808
|
+
if (!filed.ok)
|
|
10122
9809
|
return fail(write, json, "task scope", filed.reason, filed.message, EXIT.refused);
|
|
10123
|
-
}
|
|
10124
9810
|
const scope = filed.scope;
|
|
10125
9811
|
if (filed.modeRefusedCoordinator === true) {
|
|
10126
9812
|
return succeed(write, json, "task scope", { scope, approvedUnderMode: false, quarantined: true }, () => [
|
|
@@ -10170,12 +9856,10 @@ async function scopeTask(positional, flags, context) {
|
|
|
10170
9856
|
async function routeTaskCommand(positional, flags, context) {
|
|
10171
9857
|
const { store, write, json, clock } = context;
|
|
10172
9858
|
const id = positional[0];
|
|
10173
|
-
if (id === undefined)
|
|
9859
|
+
if (id === undefined)
|
|
10174
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);
|
|
10175
|
-
|
|
10176
|
-
if (store.getTask(id) === null) {
|
|
9861
|
+
if (store.getTask(id) === null)
|
|
10177
9862
|
return fail(write, json, "task route", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10178
|
-
}
|
|
10179
9863
|
const ref = store.refFor(BUILT_IN, id);
|
|
10180
9864
|
const phaseGiven = text(flags, "phase");
|
|
10181
9865
|
const clearGiven = text(flags, "clear-phase");
|
|
@@ -10221,36 +9905,29 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10221
9905
|
if (!editing)
|
|
10222
9906
|
return show();
|
|
10223
9907
|
const acting = await askCredentials(flags, context);
|
|
10224
|
-
if (acting === null)
|
|
9908
|
+
if (acting === null)
|
|
10225
9909
|
return fail(write, json, "task route", "usage", "changing a route takes `--as <you> --token <t>` — it reroutes spend", EXIT.usage);
|
|
10226
|
-
|
|
10227
|
-
if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
|
|
9910
|
+
if (sizeGiven !== undefined && !isTaskSize(sizeGiven))
|
|
10228
9911
|
return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
|
|
10229
|
-
|
|
10230
|
-
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no") {
|
|
9912
|
+
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no")
|
|
10231
9913
|
return fail(write, json, "task route", "usage", "--risky is yes or no", EXIT.usage);
|
|
10232
|
-
}
|
|
10233
9914
|
// A person's size outranks the classifier's; an unnamed half keeps what the task has.
|
|
10234
9915
|
const sizeEdit = sizeGiven === undefined && riskyGiven === undefined ? undefined : {
|
|
10235
9916
|
size: sizeGiven ?? ref.sizing?.size ?? "medium",
|
|
10236
9917
|
risky: riskyGiven === undefined ? ref.sizing?.risky ?? false : riskyGiven === "yes",
|
|
10237
9918
|
};
|
|
10238
|
-
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
9919
|
+
if (phaseGiven !== undefined && clearGiven !== undefined)
|
|
10239
9920
|
return fail(write, json, "task route", "usage", "say --phase … --provider … --model … OR --clear-phase …, not both", EXIT.usage);
|
|
10240
|
-
}
|
|
10241
9921
|
const phase = phaseGiven ?? clearGiven;
|
|
10242
|
-
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
9922
|
+
if (phase !== undefined && !ROUTE_PHASES.includes(phase))
|
|
10243
9923
|
return fail(write, json, "task route", "usage", `--${phaseGiven !== undefined ? "phase" : "clear-phase"} is one of ${ROUTE_PHASES.join(", ")}`, EXIT.usage);
|
|
10244
|
-
}
|
|
10245
9924
|
if (phaseGiven === "review")
|
|
10246
9925
|
return fail(write, json, "task route", "model-review-retired", "Separate model review has been removed; no reviewer route is needed.", EXIT.refused);
|
|
10247
9926
|
if (phaseGiven !== undefined) {
|
|
10248
|
-
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
9927
|
+
if (providerGiven === undefined || !isProviderId(providerGiven))
|
|
10249
9928
|
return fail(write, json, "task route", "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
10250
|
-
|
|
10251
|
-
if (modelGiven === undefined || modelGiven === "") {
|
|
9929
|
+
if (modelGiven === undefined || modelGiven === "")
|
|
10252
9930
|
return fail(write, json, "task route", "usage", "an override names an exact --model — approvals bind exact routing", EXIT.usage);
|
|
10253
|
-
}
|
|
10254
9931
|
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
10255
9932
|
if (!valid.ok)
|
|
10256
9933
|
return fail(write, json, "task route", "invalid", valid.problem, EXIT.usage);
|
|
@@ -10268,9 +9945,8 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10268
9945
|
...(digestGiven === undefined ? {} : { expectDigest: digestGiven === "none" ? null : digestGiven }),
|
|
10269
9946
|
}, clock());
|
|
10270
9947
|
if (!edited.ok) {
|
|
10271
|
-
if (edited.reason === "unauthenticated")
|
|
9948
|
+
if (edited.reason === "unauthenticated")
|
|
10272
9949
|
return fail(write, json, "task route", edited.detail, describeApproveFailure(edited.detail, id), EXIT.refused);
|
|
10273
|
-
}
|
|
10274
9950
|
const code = { "no-task": "unknown-task", "live-claim": "claimed", changed: "changed", nothing: "usage", "not-configured": "not-configured" };
|
|
10275
9951
|
return fail(write, json, "task route", code[edited.reason], `${id}: ${edited.detail}`, edited.reason === "nothing" ? EXIT.usage : EXIT.refused);
|
|
10276
9952
|
}
|
|
@@ -10286,18 +9962,15 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
10286
9962
|
async function approveTask(positional, flags, context) {
|
|
10287
9963
|
const { store, write, json, now } = context;
|
|
10288
9964
|
const id = positional[0];
|
|
10289
|
-
if (id === undefined)
|
|
9965
|
+
if (id === undefined)
|
|
10290
9966
|
return fail(write, json, "task approve", "usage", "which task?", EXIT.usage);
|
|
10291
|
-
}
|
|
10292
9967
|
const scope = store.getScope(id);
|
|
10293
|
-
if (scope === null)
|
|
9968
|
+
if (scope === null)
|
|
10294
9969
|
return fail(write, json, "task approve", "no-scope", `${id} has no scope to approve — write one first`, EXIT.refused);
|
|
10295
|
-
}
|
|
10296
9970
|
// Console parity (v47): a plan leg changed after a draft asks for a real
|
|
10297
9971
|
// re-plan — nothing approves the obsolete draft while the planner is owed.
|
|
10298
|
-
if (store.lookupRef(id)?.plan === "requested")
|
|
9972
|
+
if (store.lookupRef(id)?.plan === "requested")
|
|
10299
9973
|
return fail(write, json, "task approve", "planning", `${id} is waiting on a planner — approval is blocked until the drafted plan lands`, EXIT.refused);
|
|
10300
|
-
}
|
|
10301
9974
|
let saw = text(flags, "digest");
|
|
10302
9975
|
let { name: asWho, token } = credentialsFrom(flags, context);
|
|
10303
9976
|
let confirmedAloud = false;
|
|
@@ -10346,9 +10019,8 @@ async function approveTask(positional, flags, context) {
|
|
|
10346
10019
|
// Unconfirmed is "no, not yet" — exit 3 in both modes (round-4 finding 10).
|
|
10347
10020
|
return EXIT.refused;
|
|
10348
10021
|
}
|
|
10349
|
-
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10022
|
+
if (saw === undefined || asWho === undefined || token === undefined)
|
|
10350
10023
|
return fail(write, json, "task approve", "usage", "approving non-interactively takes --yes --digest <d> --as <you> --token <t>", EXIT.usage);
|
|
10351
|
-
}
|
|
10352
10024
|
// The digest is named rather than assumed, so an operator who read one scope
|
|
10353
10025
|
// cannot approve a different one that replaced it while they were reading.
|
|
10354
10026
|
// The credential is required for a different reason: an agent that can run
|
|
@@ -10409,9 +10081,8 @@ export const PROPOSALS_ACTIONS = ["list", "confirm", "dismiss"];
|
|
|
10409
10081
|
async function proposalsCommand(positional, flags, context) {
|
|
10410
10082
|
const { store, write, json, now } = context;
|
|
10411
10083
|
const [action = "list", idGiven] = positional;
|
|
10412
|
-
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10084
|
+
if (!PROPOSALS_ACTIONS.includes(action))
|
|
10413
10085
|
return fail(write, json, "proposals", "usage", `unknown \`proposals ${action}\` — try ${PROPOSALS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10414
|
-
}
|
|
10415
10086
|
store.sweepCoordinatorProposals(now);
|
|
10416
10087
|
let repos;
|
|
10417
10088
|
if (context.repoList !== undefined && context.repoList.length > 0) {
|
|
@@ -10488,9 +10159,8 @@ async function chatCommand(flags, context) {
|
|
|
10488
10159
|
if (flags.has("follow") && flags.has("no-follow"))
|
|
10489
10160
|
return fail(write, json, "chat", "usage", "Choose --follow or --no-follow.", EXIT.usage);
|
|
10490
10161
|
const credentials = await askCredentials(flags, context);
|
|
10491
|
-
if (credentials === null)
|
|
10162
|
+
if (credentials === null)
|
|
10492
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);
|
|
10493
|
-
}
|
|
10494
10164
|
const ceilingGiven = text(flags, "ceiling-usd");
|
|
10495
10165
|
// The ceiling: the `--repo` list, or the ENROLLED registry beside the
|
|
10496
10166
|
// database (slice-2 review, finding 5) — never the opened-project history.
|
|
@@ -10646,9 +10316,8 @@ async function flowsCommand(positional, flags, context) {
|
|
|
10646
10316
|
async function approverCommand(positional, flags, context) {
|
|
10647
10317
|
const { store, write, json, now } = context;
|
|
10648
10318
|
const [action, name] = positional;
|
|
10649
|
-
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10319
|
+
if (action !== undefined && !APPROVER_ACTIONS.includes(action))
|
|
10650
10320
|
return fail(write, json, "approver", "usage", `unknown \`approver ${action}\` — try ${APPROVER_ACTIONS.join(", ")}`, EXIT.usage);
|
|
10651
|
-
}
|
|
10652
10321
|
if (action === "list" || action === undefined) {
|
|
10653
10322
|
const approvers = store.listApprovers();
|
|
10654
10323
|
if (json) {
|
|
@@ -10665,9 +10334,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10665
10334
|
return EXIT.ok;
|
|
10666
10335
|
}
|
|
10667
10336
|
if (action === "add") {
|
|
10668
|
-
if (name === undefined)
|
|
10337
|
+
if (name === undefined)
|
|
10669
10338
|
return fail(write, json, "approver add", "usage", "an approver needs a name", EXIT.usage);
|
|
10670
|
-
}
|
|
10671
10339
|
let password = text(flags, "password");
|
|
10672
10340
|
const bootstrap = store.listApprovers().length === 0;
|
|
10673
10341
|
let by;
|
|
@@ -10680,9 +10348,8 @@ async function approverCommand(positional, flags, context) {
|
|
|
10680
10348
|
const chosen = await askHidden(`password for ${name} (enter to auto-generate): `);
|
|
10681
10349
|
if (chosen !== "") {
|
|
10682
10350
|
const again = await askHidden("again: ");
|
|
10683
|
-
if (again !== chosen)
|
|
10351
|
+
if (again !== chosen)
|
|
10684
10352
|
return fail(write, json, "approver add", "mismatch", "the two passwords did not match", EXIT.refused);
|
|
10685
|
-
}
|
|
10686
10353
|
password = chosen;
|
|
10687
10354
|
}
|
|
10688
10355
|
}
|
|
@@ -10719,17 +10386,14 @@ function holdTask(positional, flags, context) {
|
|
|
10719
10386
|
const { store, write, json, now } = context;
|
|
10720
10387
|
const id = positional[0];
|
|
10721
10388
|
const reason = text(flags, "reason");
|
|
10722
|
-
if (id === undefined || reason === undefined)
|
|
10389
|
+
if (id === undefined || reason === undefined)
|
|
10723
10390
|
return fail(write, json, "task hold", "usage", "`toolroll task hold <id> --reason <why>`", EXIT.usage);
|
|
10724
|
-
|
|
10725
|
-
if (store.getTask(id) === null) {
|
|
10391
|
+
if (store.getTask(id) === null)
|
|
10726
10392
|
return fail(write, json, "task hold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10727
|
-
}
|
|
10728
10393
|
const untilText = text(flags, "until");
|
|
10729
10394
|
const until = untilText === undefined ? null : new Date(untilText);
|
|
10730
|
-
if (until !== null && Number.isNaN(until.getTime()))
|
|
10395
|
+
if (until !== null && Number.isNaN(until.getTime()))
|
|
10731
10396
|
return fail(write, json, "task hold", "usage", "--until takes a date, e.g. 2026-08-12T09:00:00Z", EXIT.usage);
|
|
10732
|
-
}
|
|
10733
10397
|
store.hold(store.refFor(BUILT_IN, id).id, reason, until, now, mutationFrom(flags, now));
|
|
10734
10398
|
return succeed(write, json, "task hold", { id, reason, until: until?.toISOString() ?? null }, () => [
|
|
10735
10399
|
`${id} is on hold${until === null ? "" : ` until ${until.toISOString()}`}: ${reason}`,
|
|
@@ -10740,9 +10404,8 @@ function unholdTask(positional, flags, context) {
|
|
|
10740
10404
|
const id = positional[0];
|
|
10741
10405
|
if (id === undefined)
|
|
10742
10406
|
return fail(write, json, "task unhold", "usage", "which task?", EXIT.usage);
|
|
10743
|
-
if (store.getTask(id) === null)
|
|
10407
|
+
if (store.getTask(id) === null)
|
|
10744
10408
|
return fail(write, json, "task unhold", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10745
|
-
}
|
|
10746
10409
|
const lifted = store.unhold(store.refFor(BUILT_IN, id).id, mutationFrom(flags, context.now));
|
|
10747
10410
|
if (!lifted)
|
|
10748
10411
|
return fail(write, json, "task unhold", "not-held", `${id} was not on hold`, EXIT.refused);
|
|
@@ -10762,19 +10425,14 @@ async function stopTaskCommand(positional, flags, context) {
|
|
|
10762
10425
|
const id = positional[0];
|
|
10763
10426
|
const runText = text(flags, "run");
|
|
10764
10427
|
const runId = Number(runText ?? "");
|
|
10765
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10428
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10766
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);
|
|
10767
|
-
}
|
|
10768
10430
|
if (store.getTask(id) === null)
|
|
10769
10431
|
return fail(write, json, "task stop", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10770
10432
|
const acting = await askCredentials(flags, context);
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10775
|
-
if (!authenticated.ok) {
|
|
10776
|
-
return fail(write, json, "task stop", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10777
|
-
}
|
|
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);
|
|
10778
10436
|
const asked = requestTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli" }, clock());
|
|
10779
10437
|
if (!asked.ok)
|
|
10780
10438
|
return fail(write, json, "task stop", asked.reason, asked.detail, EXIT.refused, { run: runId });
|
|
@@ -10802,26 +10460,20 @@ async function resumeTaskCommand(positional, flags, context) {
|
|
|
10802
10460
|
const id = positional[0];
|
|
10803
10461
|
const runText = text(flags, "run");
|
|
10804
10462
|
const runId = Number(runText ?? "");
|
|
10805
|
-
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10463
|
+
if (id === undefined || runText === undefined || !Number.isInteger(runId) || runId < 1)
|
|
10806
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);
|
|
10807
|
-
}
|
|
10808
10465
|
if (store.getTask(id) === null)
|
|
10809
10466
|
return fail(write, json, "task resume", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
10810
10467
|
const acting = await askCredentials(flags, context);
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10815
|
-
if (!authenticated.ok) {
|
|
10816
|
-
return fail(write, json, "task resume", authenticated.reason, describeApproveFailure(authenticated.reason, id), EXIT.refused);
|
|
10817
|
-
}
|
|
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);
|
|
10818
10471
|
const pool = text(flags, "pool") ?? join(dirname(databasePath(process.env, homedir())), "worktrees");
|
|
10819
10472
|
const worktrees = new WorktreePool(store, { root: pool });
|
|
10820
10473
|
const resumed = resumeTaskStop(store, { taskId: id, runId, by: acting.name, via: "cli", occupied: path => worktrees.inUse(path) }, clock());
|
|
10821
10474
|
if (!resumed.ok) {
|
|
10822
|
-
if (resumed.reason === "review")
|
|
10475
|
+
if (resumed.reason === "review")
|
|
10823
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 });
|
|
10824
|
-
}
|
|
10825
10477
|
return fail(write, json, "task resume", resumed.reason, resumed.detail, EXIT.refused, { run: runId });
|
|
10826
10478
|
}
|
|
10827
10479
|
const control = taskControlOf(store, store.refFor(BUILT_IN, id).id, clock());
|
|
@@ -10850,21 +10502,18 @@ async function runCommand(positional, flags, context) {
|
|
|
10850
10502
|
const why = text(flags, "why")?.trim();
|
|
10851
10503
|
if (id === undefined || extra.length > 0 || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(runId))
|
|
10852
10504
|
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
10853
|
-
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))
|
|
10854
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);
|
|
10855
|
-
}
|
|
10856
10507
|
const run = store.getRun(runId);
|
|
10857
10508
|
if (run === null)
|
|
10858
10509
|
return fail(write, json, command, "unknown-run", `no run #${runId}`, EXIT.refused);
|
|
10859
10510
|
const acting = await askCredentials(flags, context);
|
|
10860
|
-
if (acting === null)
|
|
10511
|
+
if (acting === null)
|
|
10861
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);
|
|
10862
|
-
}
|
|
10863
10513
|
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
10864
10514
|
const repo = store.refById(run.taskRef)?.repo ?? null;
|
|
10865
|
-
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10515
|
+
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
|
|
10866
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);
|
|
10867
|
-
}
|
|
10868
10517
|
const settled = store.settleRunWitnessesByApprover({ runId, by: acting.name, why }, clock());
|
|
10869
10518
|
if (!settled.ok)
|
|
10870
10519
|
return fail(write, json, command, settled.reason, settled.detail, EXIT.refused, { run: runId });
|
|
@@ -10891,9 +10540,7 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10891
10540
|
if (positional.length > 1 || repoFlag === undefined)
|
|
10892
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);
|
|
10893
10542
|
// The projects Settings shows: those with work, and those registered (enrolled) with none yet.
|
|
10894
|
-
const
|
|
10895
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
10896
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag));
|
|
10543
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: false });
|
|
10897
10544
|
if (repo === undefined)
|
|
10898
10545
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10899
10546
|
const current = context.store.approvalRules(repo);
|
|
@@ -10913,9 +10560,8 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10913
10560
|
return fail(context.write, context.json, command, "usage", paths.problem, EXIT.usage);
|
|
10914
10561
|
const acting = await askCredentials(flags, context);
|
|
10915
10562
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
10916
|
-
if (acting === null ||
|
|
10563
|
+
if (acting === null || !verified?.ok || !context.store.isInstanceOperator(acting.name))
|
|
10917
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);
|
|
10918
|
-
}
|
|
10919
10565
|
const next = {
|
|
10920
10566
|
notRequester: onOff === undefined ? current.notRequester : onOff === "on",
|
|
10921
10567
|
protectProject: protect === undefined ? (paths !== null && paths.ok ? false : current.protectProject) : protect === "project",
|
|
@@ -10938,12 +10584,9 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10938
10584
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
|
|
10939
10585
|
const repoFlag = text(flags, "repo");
|
|
10940
10586
|
const local = flags.get("local");
|
|
10941
|
-
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"))
|
|
10942
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);
|
|
10943
|
-
}
|
|
10944
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10945
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10946
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10589
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
10947
10590
|
if (repo === undefined)
|
|
10948
10591
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10949
10592
|
const current = context.store.projectNetwork(repo);
|
|
@@ -10973,9 +10616,8 @@ async function projectNetworkCommand(positional, flags, context) {
|
|
|
10973
10616
|
return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
|
|
10974
10617
|
const acting = await askCredentials(flags, context);
|
|
10975
10618
|
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10976
|
-
if (acting === null ||
|
|
10619
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
10977
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);
|
|
10978
|
-
}
|
|
10979
10621
|
const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
|
|
10980
10622
|
return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
|
|
10981
10623
|
...words(changed.after),
|
|
@@ -10996,12 +10638,9 @@ async function projectWritesCommand(positional, flags, context) {
|
|
|
10996
10638
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project writes option.`, EXIT.usage);
|
|
10997
10639
|
const repoFlag = text(flags, "repo");
|
|
10998
10640
|
const wanted = positional[1];
|
|
10999
|
-
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted)))
|
|
10641
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted)))
|
|
11000
10642
|
return fail(context.write, context.json, command, "usage", "Use project writes --repo <project path> [workspace | anywhere].", EXIT.usage);
|
|
11001
|
-
}
|
|
11002
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11003
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11004
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10643
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11005
10644
|
if (repo === undefined)
|
|
11006
10645
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11007
10646
|
const words = (one) => [`${repo}: builds write ${writesWords(one)}.`];
|
|
@@ -11010,9 +10649,8 @@ async function projectWritesCommand(positional, flags, context) {
|
|
|
11010
10649
|
return succeed(context.write, context.json, command, { repo, writes: current }, () => words(current));
|
|
11011
10650
|
const acting = await askCredentials(flags, context);
|
|
11012
10651
|
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
11013
|
-
if (acting === null ||
|
|
10652
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11014
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);
|
|
11015
|
-
}
|
|
11016
10654
|
const changed = context.store.setProjectWrites(repo, wanted, acting.name, context.clock());
|
|
11017
10655
|
return succeed(context.write, context.json, command, { repo, before: changed.before, writes: changed.after }, () => [
|
|
11018
10656
|
...words(changed.after),
|
|
@@ -11025,9 +10663,8 @@ async function unconfinedAgentsCommand(positional, flags, context) {
|
|
|
11025
10663
|
const command = "config unconfined-agents";
|
|
11026
10664
|
const { store, write, json } = context;
|
|
11027
10665
|
const wanted = positional[1];
|
|
11028
|
-
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
10666
|
+
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse"))
|
|
11029
10667
|
return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
|
|
11030
|
-
}
|
|
11031
10668
|
const words = (allowed) => [allowed
|
|
11032
10669
|
? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
|
|
11033
10670
|
: "Builds this machine can't confine at the OS don't start."];
|
|
@@ -11035,9 +10672,8 @@ async function unconfinedAgentsCommand(positional, flags, context) {
|
|
|
11035
10672
|
return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
|
|
11036
10673
|
const acting = await askCredentials(flags, context);
|
|
11037
10674
|
const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
|
|
11038
|
-
if (acting === null ||
|
|
10675
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11039
10676
|
return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
|
|
11040
|
-
}
|
|
11041
10677
|
store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
|
|
11042
10678
|
return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
|
|
11043
10679
|
}
|
|
@@ -11051,9 +10687,7 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11051
10687
|
const repoFlag = text(flags, "repo");
|
|
11052
10688
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11053
10689
|
return fail(context.write, context.json, command, "usage", "Use project concurrency [<n>] --repo <project path>.", EXIT.usage);
|
|
11054
|
-
const
|
|
11055
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11056
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10690
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11057
10691
|
if (repo === undefined)
|
|
11058
10692
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11059
10693
|
const workers = context.store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(repo));
|
|
@@ -11069,9 +10703,8 @@ async function projectConcurrencyCommand(positional, flags, context) {
|
|
|
11069
10703
|
return fail(context.write, context.json, command, "usage", `Builds at once is a whole number from 1 to ${RUNNER_CAPACITY_MAX}.`, EXIT.usage);
|
|
11070
10704
|
const acting = await askCredentials(flags, context);
|
|
11071
10705
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11072
|
-
if (acting === null ||
|
|
10706
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11073
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);
|
|
11074
|
-
}
|
|
11075
10708
|
const changed = saveProjectConcurrency(context.databaseFile, repo, n);
|
|
11076
10709
|
context.store.recordProjectConcurrency(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11077
10710
|
context.store.bumpWake();
|
|
@@ -11090,12 +10723,9 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11090
10723
|
return fail(context.write, context.json, command, "usage", `--${name} is not a project checks option.`, EXIT.usage);
|
|
11091
10724
|
const repoFlag = text(flags, "repo");
|
|
11092
10725
|
const wanted = flags.get("batch");
|
|
11093
|
-
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"))
|
|
11094
10727
|
return fail(context.write, context.json, command, "usage", "Use project checks [--batch on|off] --repo <project path>.", EXIT.usage);
|
|
11095
|
-
}
|
|
11096
|
-
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11097
|
-
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11098
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10728
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11099
10729
|
if (repo === undefined)
|
|
11100
10730
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11101
10731
|
const level = projectCheckLevel(context.store, repo).level;
|
|
@@ -11109,9 +10739,8 @@ async function projectChecksCommand(positional, flags, context) {
|
|
|
11109
10739
|
}
|
|
11110
10740
|
const acting = await askCredentials(flags, context);
|
|
11111
10741
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token, repo);
|
|
11112
|
-
if (acting === null ||
|
|
10742
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11113
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);
|
|
11114
|
-
}
|
|
11115
10744
|
const on = wanted === "on";
|
|
11116
10745
|
const changed = setProjectBatchChecks(context.store, repo, on, acting.name, context.clock());
|
|
11117
10746
|
return succeed(context.write, context.json, command, { repo, batch: on, before: changed.before, changed: changed.changed, windowMinutes: BATCH_WINDOW_MS / 60_000, level }, () => [
|
|
@@ -11130,9 +10759,7 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11130
10759
|
const repoFlag = text(flags, "repo");
|
|
11131
10760
|
if (positional.length > 2 || repoFlag === undefined)
|
|
11132
10761
|
return fail(context.write, context.json, command, "usage", "Use project demo [<url>|off] --repo <project path>.", EXIT.usage);
|
|
11133
|
-
const
|
|
11134
|
-
const known = [...new Set([...context.store.knownRepos(), ...("error" in registered ? [] : registered.repos)])];
|
|
11135
|
-
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10762
|
+
const repo = await knownProject(context, repoFlag, { listed: false, canonical: true });
|
|
11136
10763
|
if (repo === undefined)
|
|
11137
10764
|
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11138
10765
|
const words = (url) => url === null ? `${repo}: no demo; scouts screenshot public pages only.` : `${repo}: scouts may also screenshot ${url}.`;
|
|
@@ -11146,9 +10773,8 @@ async function projectDemoCommand(positional, flags, context) {
|
|
|
11146
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);
|
|
11147
10774
|
const acting = await askCredentials(flags, context);
|
|
11148
10775
|
const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
|
|
11149
|
-
if (acting === null ||
|
|
10776
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11150
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);
|
|
11151
|
-
}
|
|
11152
10778
|
const changed = saveProjectDemo(context.databaseFile, repo, url);
|
|
11153
10779
|
context.store.recordProjectDemo(acting.name, repo, changed.before, changed.after, context.clock());
|
|
11154
10780
|
return succeed(context.write, context.json, command, { repo, before: changed.before, demoUrl: changed.after }, () => [words(changed.after)]);
|
|
@@ -11188,9 +10814,8 @@ async function projectDeleteCommand(positional, flags, context) {
|
|
|
11188
10814
|
const store = context.store;
|
|
11189
10815
|
const acting = await askCredentials(flags, context);
|
|
11190
10816
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11191
|
-
if (acting === null ||
|
|
10817
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11192
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);
|
|
11193
|
-
}
|
|
11194
10819
|
const holdings = projectHoldings(store, repo);
|
|
11195
10820
|
const running = projectRunning(store, repo, context.clock());
|
|
11196
10821
|
if (running.length > 0)
|
|
@@ -11293,9 +10918,8 @@ async function restoreCommand(file, positional, flags, write, json, now) {
|
|
|
11293
10918
|
catch (error) {
|
|
11294
10919
|
return fail(write, json, command, "failed", `The restore failed: ${describe(error)}. The database is unchanged.`, EXIT.failed);
|
|
11295
10920
|
}
|
|
11296
|
-
if (!report.ok)
|
|
10921
|
+
if (!report.ok)
|
|
11297
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 });
|
|
11298
|
-
}
|
|
11299
10923
|
const checked = `schema ${report.schemaVersion}, ledger chain verified (${report.ledger?.entries ?? 0} entries)`;
|
|
11300
10924
|
return succeed(write, json, command, { report }, () => dryRun
|
|
11301
10925
|
? [`Dry run: ${report.file} can be restored (${checked}). Nothing was changed.`]
|
|
@@ -11356,9 +10980,8 @@ async function budgetCommand(positional, flags, context) {
|
|
|
11356
10980
|
const target = chosen[0];
|
|
11357
10981
|
const acting = await askCredentials(flags, context);
|
|
11358
10982
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11359
|
-
if (acting === null ||
|
|
10983
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11360
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);
|
|
11361
|
-
}
|
|
11362
10985
|
const known = target.scope === "installation" || (target.scope === "project" ? store.knownRepos().includes(target.key)
|
|
11363
10986
|
: target.scope === "person" ? store.accountFacts().some(one => one.name === target.key && one.revokedAt === null)
|
|
11364
10987
|
: store.handle.prepare("SELECT 1 FROM subagent WHERE id = ?").get(Number(target.key)) !== undefined);
|
|
@@ -11389,18 +11012,16 @@ async function notificationsCommand(positional, flags, context) {
|
|
|
11389
11012
|
return fail(context.write, context.json, command, "usage", `--${name} is not a notifications option.`, EXIT.usage);
|
|
11390
11013
|
const usage = "Use notifications quiet, notifications all, notifications digest <HH:MM>|off, notifications screenshots off|first|all, or notifications mute|unmute --repo <project>.";
|
|
11391
11014
|
const valued = action === "digest" || action === "screenshots";
|
|
11392
|
-
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest", "screenshots", "mute", "unmute"].includes(action)) || valued !== (value !== undefined) || (muting && (value !== undefined || text(flags, "repo") === undefined)))
|
|
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)))
|
|
11393
11016
|
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11394
|
-
}
|
|
11395
11017
|
if (action === "screenshots" && !RESULT_SCREENSHOTS.includes(value))
|
|
11396
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);
|
|
11397
11019
|
if (action === "digest" && value !== "off" && !isDigestTime(value))
|
|
11398
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);
|
|
11399
11021
|
const acting = await askCredentials(flags, context);
|
|
11400
11022
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11401
|
-
if (acting === null ||
|
|
11023
|
+
if (acting === null || !verified?.ok)
|
|
11402
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);
|
|
11403
|
-
}
|
|
11404
11025
|
const now = context.clock();
|
|
11405
11026
|
if (muting) {
|
|
11406
11027
|
const typed = text(flags, "repo");
|
|
@@ -11458,17 +11079,15 @@ async function retentionCommand(positional, flags, context) {
|
|
|
11458
11079
|
if (action !== "set")
|
|
11459
11080
|
return fail(context.write, context.json, command, "usage", "Use retention show, retention preview or retention set <kind> <period>.", EXIT.usage);
|
|
11460
11081
|
const [kind, period, ...extra] = rest;
|
|
11461
|
-
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11082
|
+
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0)
|
|
11462
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);
|
|
11463
|
-
}
|
|
11464
11084
|
const days = parsePeriod(period);
|
|
11465
11085
|
if (days === undefined)
|
|
11466
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);
|
|
11467
11087
|
const acting = await askCredentials(flags, context);
|
|
11468
11088
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11469
|
-
if (acting === null ||
|
|
11089
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11470
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);
|
|
11471
|
-
}
|
|
11472
11091
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
11473
11092
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
11474
11093
|
}
|
|
@@ -11757,9 +11376,8 @@ async function policyCommand(positional, flags, context) {
|
|
|
11757
11376
|
return fail(context.write, context.json, command, "usage", "Say what to set: --providers, --models, --tools or --ceiling.", EXIT.usage);
|
|
11758
11377
|
const acting = await askCredentials(flags, context);
|
|
11759
11378
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11760
|
-
if (acting === null ||
|
|
11379
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11761
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);
|
|
11762
|
-
}
|
|
11763
11381
|
const current = store.orgPolicy();
|
|
11764
11382
|
const listFlag = (name, now) => {
|
|
11765
11383
|
if (!flags.has(name))
|
|
@@ -11797,9 +11415,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11797
11415
|
for (const name of flags.keys())
|
|
11798
11416
|
if (!allowed.has(name))
|
|
11799
11417
|
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
11800
|
-
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11418
|
+
if (extra.length > 0 || (action !== "verify" && action !== "checkpoint" && action !== "export"))
|
|
11801
11419
|
return fail(context.write, context.json, command, "usage", "Use ledger verify, ledger checkpoint or ledger export --from <day> --to <day>.", EXIT.usage);
|
|
11802
|
-
}
|
|
11803
11420
|
const store = context.store;
|
|
11804
11421
|
if (action === "verify") {
|
|
11805
11422
|
// The command walks the whole chain, every time.
|
|
@@ -11819,9 +11436,8 @@ async function ledgerCommand(positional, flags, context) {
|
|
|
11819
11436
|
if (action === "checkpoint") {
|
|
11820
11437
|
const acting = await askCredentials(flags, context);
|
|
11821
11438
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11822
|
-
if (acting === null ||
|
|
11439
|
+
if (acting === null || !verified?.ok || !store.isInstanceOperator(acting.name))
|
|
11823
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);
|
|
11824
|
-
}
|
|
11825
11441
|
const made = store.ledgerCheckpoint(acting.name, context.clock());
|
|
11826
11442
|
if (made === null)
|
|
11827
11443
|
return fail(context.write, context.json, command, "empty", "The ledger has no entries yet.", EXIT.refused);
|
|
@@ -11925,6 +11541,23 @@ function databaseFailure(write, json, command, file, error) {
|
|
|
11925
11541
|
return fail(write, json, command, "database", describe(error), EXIT.failed);
|
|
11926
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);
|
|
11927
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
|
+
}
|
|
11928
11561
|
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11929
11562
|
/** The value flags a verb reads under a global switch's name, for parseOperateArgs. */
|
|
11930
11563
|
export function ownValueFlags(command, argv) {
|