toolroll 0.9.75 → 0.9.77
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 +241 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +36 -8
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.js +3 -0
- package/dist/batch-checks.js +3 -1
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/builder.d.ts +14 -0
- package/dist/builder.js +141 -42
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +267 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +52 -6
- package/dist/check-report.js +193 -8
- package/dist/checkout-cleanup.js +2 -2
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/coding-workspace.js +7 -5
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +8 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discover.js +6 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +6 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.js +4 -1
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +7 -1
- package/dist/first-run.js +3 -1
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +7 -1
- package/dist/guides.js +4 -3
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +779 -319
- package/dist/planner.js +5 -1
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +23 -3
- package/dist/proof.js +6 -0
- package/dist/pull-request-flow.js +4 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +71 -2
- package/dist/release-adapters.js +471 -12
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +68 -0
- package/dist/release-auto.d.ts +184 -0
- package/dist/release-auto.js +612 -0
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.js +77 -14
- package/dist/release-coverage.js +5 -2
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +6 -1
- package/dist/release.d.ts +207 -6
- package/dist/release.js +495 -20
- package/dist/repository-context.js +4 -6
- package/dist/result-follow-ups.js +3 -1
- package/dist/review-context.js +8 -6
- package/dist/runner.js +2 -1
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +51 -4
- package/dist/scout.js +5 -1
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +101 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +25 -14
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.js +3 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +29 -2
- package/dist/server/task-views.js +15 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +11 -1
- package/dist/store.js +44 -5
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +90 -13
- package/dist/teardown-git.js +1027 -129
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +77 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +2 -1
package/dist/release-card.js
CHANGED
|
@@ -25,6 +25,7 @@ import { verifiedRange } from "./commit-extract.js";
|
|
|
25
25
|
import { releaseTermsSchema } from "./contracts/release-terms.js";
|
|
26
26
|
import { routeFromJson } from "./phase-routing.js";
|
|
27
27
|
import { projectName } from "./project.js";
|
|
28
|
+
import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
28
29
|
import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
|
|
29
30
|
const NONE = "none";
|
|
30
31
|
/** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
|
|
@@ -152,13 +153,21 @@ export function approvalTermsOf(store, scope, words) {
|
|
|
152
153
|
export const termsText = (lines) => lines.map(line => `${line.label}: ${line.value}`).join("\n");
|
|
153
154
|
const SHA = /^[0-9a-f]{40}$/;
|
|
154
155
|
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
155
|
-
/**
|
|
156
|
-
*
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
/**
|
|
157
|
+
* The plane's own record of the last verified release of `repo`: the merge commit GitHub answered with, recorded once
|
|
158
|
+
* the release verified the merged tree (the newest merged authority whose merge operation succeeded). Never a tag.
|
|
159
|
+
* Whether the plane recorded one, and its merge commit (eval-harness review 2, finding 4):
|
|
160
|
+
* `recorded: false` only when no merged release is on the record at all; a recorded one whose commit isn't a full id is
|
|
161
|
+
* `recorded: true` with `sha: null` — something to refuse on, never "no previous release".
|
|
162
|
+
*/
|
|
163
|
+
export function releaseBaselineRecord(store, repo) {
|
|
164
|
+
const row = store.handle.prepare(`SELECT o.external_ref FROM release_authority a LEFT JOIN release_operation o
|
|
165
|
+
ON o.authority = a.id AND o.kind = 'merge' AND o.state = 'succeeded'
|
|
166
|
+
WHERE a.repo = ? AND a.state = 'merged' ORDER BY a.id DESC, o.finished_at DESC LIMIT 1`).get(repo);
|
|
167
|
+
if (row === undefined)
|
|
168
|
+
return { recorded: false };
|
|
169
|
+
const ref = row["external_ref"];
|
|
170
|
+
return { recorded: true, sha: typeof ref === "string" && SHA.test(ref) ? ref : null };
|
|
162
171
|
}
|
|
163
172
|
/** The commits after `from` up to `to`, as verified Git objects only, from the project's recorded Git directory: the
|
|
164
173
|
* release coordinator's checkout config allowlist first, then commit-extract's verified range (every commit hashed here,
|
|
@@ -198,7 +207,12 @@ function pullRequestCommits(slug, pr) {
|
|
|
198
207
|
}
|
|
199
208
|
/** What changed since the last verified release, for a release's terms, from Toolroll's records only. */
|
|
200
209
|
export function releaseChangeList(store, terms) {
|
|
201
|
-
const
|
|
210
|
+
const record = releaseBaselineRecord(store, terms.repo);
|
|
211
|
+
// A release recorded with a merge commit that can't be read: the list couldn't be read (eval-harness review 2,
|
|
212
|
+
// finding 4) — never the pull request's commits, as if there were no previous release.
|
|
213
|
+
if (record.recorded && record.sha === null)
|
|
214
|
+
return { baseline: null, commits: null, source: "unread", baselineUnreadable: true, digest: sha256(`release-changes:${JSON.stringify({ baseline: null, source: "unread", commits: null, recorded: true })}`) };
|
|
215
|
+
const baseline = record.recorded ? record.sha : null;
|
|
202
216
|
let commits;
|
|
203
217
|
let source;
|
|
204
218
|
if (baseline !== null) {
|
|
@@ -221,7 +235,7 @@ function changeWords(changes, terms, cap) {
|
|
|
221
235
|
const head = changes.baseline === null ? `No earlier verified release is recorded. ${changes.source === "pull-request" ? `Pull request #${terms?.pr ?? "?"}'s commits:` : ""}`.trim()
|
|
222
236
|
: `Changes since the last verified release (${changes.baseline.slice(0, 7)}):`;
|
|
223
237
|
if (changes.commits === null)
|
|
224
|
-
return [head, changes.baseline === null ? `The pull request's commits couldn't be read; see #${terms?.pr ?? "?"} on GitHub.` : "The change list couldn't be read."];
|
|
238
|
+
return [head, changes.baseline === null && changes.baselineUnreadable !== true ? `The pull request's commits couldn't be read; see #${terms?.pr ?? "?"} on GitHub.` : "The change list couldn't be read."];
|
|
225
239
|
if (changes.commits.length === 0)
|
|
226
240
|
return [head, "No commits."];
|
|
227
241
|
return [head, ...changes.commits.slice(0, cap).map(one => `• ${one.sha.slice(0, 7)} ${one.subject.slice(0, 100)}`), ...(changes.commits.length > cap ? [`• and ${changes.commits.length - cap} more`] : [])];
|
|
@@ -242,6 +256,8 @@ export function releaseCardOf(store, scope, formDigest, words) {
|
|
|
242
256
|
terms === null ? `Checks: ${value("checks")} · check access: ${value("checkAccess")}` : `Carry later commits: ${terms.policy.carry ? "yes" : "no"} · checks: ${value("checks")} · expires ${terms.expiresAt}`,
|
|
243
257
|
// Option A's disclosure, on the card as on the approval sheet: never approved from chat without it.
|
|
244
258
|
...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
|
|
259
|
+
// Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
|
|
260
|
+
...(releaseCiRunningOf(store, scope.taskId) === null ? [] : [RELEASE_CI_RUNNING_WORDS]),
|
|
245
261
|
"",
|
|
246
262
|
...changeWords(changes, terms, 5),
|
|
247
263
|
"",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A release's approval held for its pull request's CI (Codex review round 1, finding 4). A leaf module: the seal
|
|
2
|
+
* primitive (store.ts), the person's ceremony (scope.ts), chat and release-auto.ts all read the same row. */
|
|
3
|
+
import type { Store } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* A release filed while its pull request's CI was still running (pipeline-speed.md D2) waits for CI before anyone but
|
|
6
|
+
* the automatic road may approve it (Codex review round 1, finding 4): `release-auto.ts holdApprovalForCi` records the
|
|
7
|
+
* hold before the task is filed, and only the plane's own reading of exact-head CI passing lifts it
|
|
8
|
+
* (`liftCiHold`). The newest row for the task wins; the hold names the candidate commit it waits on.
|
|
9
|
+
* Since Alex's decision of 2026-10-29 (a person may approve during CI) the coordinator records a "CI still running"
|
|
10
|
+
* note instead (below); a hold an older coordinator placed still holds until lifted.
|
|
11
|
+
*/
|
|
12
|
+
export declare const RELEASE_CI_HELD_ACTION = "release approval waits for CI";
|
|
13
|
+
export declare const RELEASE_CI_PASSED_ACTION = "release approval CI passed";
|
|
14
|
+
/** The candidate whose CI a release task's approval waits for, or null when nothing holds it. Fail closed: a hold row
|
|
15
|
+
* whose candidate can't be read still holds. */
|
|
16
|
+
export declare function releaseCiHoldOf(store: Store, taskId: string): {
|
|
17
|
+
candidate: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
/** Why a person can't approve a release waiting for CI yet, in words. */
|
|
20
|
+
export declare function releaseCiHoldWords(hold: {
|
|
21
|
+
candidate: string | null;
|
|
22
|
+
}): string;
|
|
23
|
+
/**
|
|
24
|
+
* A person's release asks for approval while its pull request's CI still runs (Alex, 2026-10-29; pipeline-speed.md,
|
|
25
|
+
* "Decision: a person's release asks during CI"). This row never holds approval: it lets the approval card say so, until
|
|
26
|
+
* the plane reads exact-head CI passing (`liftCiHold` records RELEASE_CI_PASSED_ACTION, which ends it as it ends a
|
|
27
|
+
* hold). Deployment gates and operation issuance also refuse while it remains, so a direct deploy cannot bypass
|
|
28
|
+
* the coordinator's CI wait. Its absence is not a CI pass: the coordinator still reads exact-head checks itself.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RELEASE_CI_RUNNING_ACTION = "release asked while CI runs";
|
|
31
|
+
export declare const RELEASE_CI_RUNNING_WORDS = "GitHub's checks are still running; nothing deploys until they pass.";
|
|
32
|
+
/** The candidate whose CI was still running when the release task was filed, or null once the plane read it passing. */
|
|
33
|
+
export declare function releaseCiRunningOf(store: Store, taskId: string): {
|
|
34
|
+
candidate: string | null;
|
|
35
|
+
} | null;
|
|
36
|
+
/** Record that the release task is filed while CI runs on `candidate` (before the task exists). Idempotent. */
|
|
37
|
+
export declare function noteCiRunning(store: Store, input: {
|
|
38
|
+
taskId: string;
|
|
39
|
+
candidate: string;
|
|
40
|
+
now: Date;
|
|
41
|
+
}): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A release filed while its pull request's CI was still running (pipeline-speed.md D2) waits for CI before anyone but
|
|
3
|
+
* the automatic road may approve it (Codex review round 1, finding 4): `release-auto.ts holdApprovalForCi` records the
|
|
4
|
+
* hold before the task is filed, and only the plane's own reading of exact-head CI passing lifts it
|
|
5
|
+
* (`liftCiHold`). The newest row for the task wins; the hold names the candidate commit it waits on.
|
|
6
|
+
* Since Alex's decision of 2026-10-29 (a person may approve during CI) the coordinator records a "CI still running"
|
|
7
|
+
* note instead (below); a hold an older coordinator placed still holds until lifted.
|
|
8
|
+
*/
|
|
9
|
+
export const RELEASE_CI_HELD_ACTION = "release approval waits for CI";
|
|
10
|
+
export const RELEASE_CI_PASSED_ACTION = "release approval CI passed";
|
|
11
|
+
/** The candidate whose CI a release task's approval waits for, or null when nothing holds it. Fail closed: a hold row
|
|
12
|
+
* whose candidate can't be read still holds. */
|
|
13
|
+
export function releaseCiHoldOf(store, taskId) {
|
|
14
|
+
const row = store.handle.prepare("SELECT action, detail FROM action_ledger WHERE task_id = ? AND action IN (?, ?) ORDER BY id DESC LIMIT 1")
|
|
15
|
+
.get(taskId, RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION);
|
|
16
|
+
if (row === undefined || row["action"] !== RELEASE_CI_HELD_ACTION)
|
|
17
|
+
return null;
|
|
18
|
+
const detail = typeof row["detail"] === "string" ? row["detail"] : "";
|
|
19
|
+
return { candidate: /^[0-9a-f]{40}$/.test(detail) ? detail : null };
|
|
20
|
+
}
|
|
21
|
+
/** Why a person can't approve a release waiting for CI yet, in words. */
|
|
22
|
+
export function releaseCiHoldWords(hold) {
|
|
23
|
+
const commit = hold.candidate === null ? "this release's commit" : hold.candidate.slice(0, 7);
|
|
24
|
+
return `GitHub CI is still running on ${commit}. This release can be approved once every pull request check passes; \`toolroll release\` asks you then.`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A person's release asks for approval while its pull request's CI still runs (Alex, 2026-10-29; pipeline-speed.md,
|
|
28
|
+
* "Decision: a person's release asks during CI"). This row never holds approval: it lets the approval card say so, until
|
|
29
|
+
* the plane reads exact-head CI passing (`liftCiHold` records RELEASE_CI_PASSED_ACTION, which ends it as it ends a
|
|
30
|
+
* hold). Deployment gates and operation issuance also refuse while it remains, so a direct deploy cannot bypass
|
|
31
|
+
* the coordinator's CI wait. Its absence is not a CI pass: the coordinator still reads exact-head checks itself.
|
|
32
|
+
*/
|
|
33
|
+
export const RELEASE_CI_RUNNING_ACTION = "release asked while CI runs";
|
|
34
|
+
export const RELEASE_CI_RUNNING_WORDS = "GitHub's checks are still running; nothing deploys until they pass.";
|
|
35
|
+
/** The candidate whose CI was still running when the release task was filed, or null once the plane read it passing. */
|
|
36
|
+
export function releaseCiRunningOf(store, taskId) {
|
|
37
|
+
const row = store.handle.prepare("SELECT action, detail FROM action_ledger WHERE task_id = ? AND action IN (?, ?) ORDER BY id DESC LIMIT 1")
|
|
38
|
+
.get(taskId, RELEASE_CI_RUNNING_ACTION, RELEASE_CI_PASSED_ACTION);
|
|
39
|
+
if (row === undefined || row["action"] !== RELEASE_CI_RUNNING_ACTION)
|
|
40
|
+
return null;
|
|
41
|
+
const detail = typeof row["detail"] === "string" ? row["detail"] : "";
|
|
42
|
+
return { candidate: /^[0-9a-f]{40}$/.test(detail) ? detail : null };
|
|
43
|
+
}
|
|
44
|
+
/** Record that the release task is filed while CI runs on `candidate` (before the task exists). Idempotent. */
|
|
45
|
+
export function noteCiRunning(store, input) {
|
|
46
|
+
if (!/^[0-9a-f]{40}$/.test(input.candidate))
|
|
47
|
+
throw new Error("a CI note needs the exact candidate commit");
|
|
48
|
+
const running = releaseCiRunningOf(store, input.taskId);
|
|
49
|
+
if (running !== null && running.candidate === input.candidate)
|
|
50
|
+
return;
|
|
51
|
+
store.recordAction({ at: input.now.toISOString(), actor: "toolroll", repo: store.lookupRef(input.taskId)?.repo ?? null, taskId: input.taskId, runId: null,
|
|
52
|
+
action: RELEASE_CI_RUNNING_ACTION, outcome: "running", source: "work", detail: input.candidate });
|
|
53
|
+
}
|
package/dist/release-cli.js
CHANGED
|
@@ -17,7 +17,8 @@ import { processes, releaseAdapters } from "./release-adapters.js";
|
|
|
17
17
|
import { releaseGate } from "./release-gate.js";
|
|
18
18
|
import { databasePath, openStore, revokeRelease } from "./store.js";
|
|
19
19
|
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
|
|
20
|
-
import { authenticateApprover, machineActing, personProof } from "./scope.js";
|
|
20
|
+
import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
|
|
21
|
+
import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
|
|
21
22
|
import { createNightlyCheckFlow } from "./nightly-check.js";
|
|
22
23
|
import { parseSchedule } from "./flow-schedule.js";
|
|
23
24
|
export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itself, from a pushed branch with an open pull request
|
|
@@ -52,6 +53,13 @@ toolroll release nightly --repo <path> --as <you> --token <password> [--schedule
|
|
|
52
53
|
toolroll release ask-every-commit on|off --repo <path> [--as <you> --token <password>] — this project's releases ask
|
|
53
54
|
for every later commit (on), or carry test-only commits again (off: your password, never the lead's token)
|
|
54
55
|
|
|
56
|
+
toolroll release auto-approve on|off --repo <path> [--as <you> --token <password>] — this project's release approval:
|
|
57
|
+
on approves a release automatically once its pull request's CI and the release check pass on the exact commit
|
|
58
|
+
(your password typed now, never the lead's token or a remembered login); off asks you again (no password).
|
|
59
|
+
|
|
60
|
+
toolroll release publish-ok <task> --as <you> --token <password> — let an automatically approved release that was held
|
|
61
|
+
after its merge (its checks stopped passing) go on to its tag, publication and Homebrew (a person only)
|
|
62
|
+
|
|
55
63
|
toolroll release unsandboxed-carry on|off --repo <path> [--as <you> --token <password>] — carry release approvals
|
|
56
64
|
when this project's checks aren't sandboxed (on: your password, never the lead's token). With it on, later
|
|
57
65
|
test-only commits of a release run outside the sandbox without being shown to you first. Off by default; it counts
|
|
@@ -76,7 +84,10 @@ export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll wo
|
|
|
76
84
|
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule"]);
|
|
77
85
|
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start"]);
|
|
78
86
|
export async function runReleaseCommand(args, write, deps = {}) {
|
|
79
|
-
|
|
87
|
+
// The flag, not a secret's value that happens to read "--json".
|
|
88
|
+
// A secret is never exactly one of these names (the loop below refuses that as a missing value).
|
|
89
|
+
const bare = new Set(args.filter(one => BARE_FLAGS.has(one)));
|
|
90
|
+
const json = bare.has("--json");
|
|
80
91
|
const refuse = (reason, message, code) => {
|
|
81
92
|
write(json ? checkedEnvelopeJson({ ok: false, command: "release", reason, message }) : message);
|
|
82
93
|
return code;
|
|
@@ -85,14 +96,20 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
85
96
|
const limits = {};
|
|
86
97
|
let repo, tap, gate, to, request, as, token, scheduleFlag;
|
|
87
98
|
for (let i = 0; i < args.length; i += 1) {
|
|
88
|
-
const
|
|
89
|
-
if (BARE_FLAGS.has(
|
|
99
|
+
const raw = args[i];
|
|
100
|
+
if (BARE_FLAGS.has(raw))
|
|
90
101
|
continue;
|
|
102
|
+
// `--flag=value` for every value flag; a secret (--token) is also the next argument even when it starts with "--".
|
|
103
|
+
const equals = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
104
|
+
const arg = equals === -1 ? raw : raw.slice(0, equals);
|
|
91
105
|
if (VALUE_FLAGS.has(arg)) {
|
|
92
|
-
const value = args[i + 1];
|
|
93
|
-
|
|
106
|
+
const value = equals === -1 ? args[i + 1] : raw.slice(equals + 1);
|
|
107
|
+
// A secret may start with "--"; it is the value unless it is exactly one of this command's flags (never swallowed).
|
|
108
|
+
const namesFlag = value !== undefined && (BARE_FLAGS.has(value) || VALUE_FLAGS.has(value.split("=")[0]));
|
|
109
|
+
if (value === undefined || (equals === -1 && value.startsWith("--") && (arg !== "--token" || namesFlag)))
|
|
94
110
|
return refuse("usage", `${arg} takes a value.`, 2);
|
|
95
|
-
|
|
111
|
+
if (equals === -1)
|
|
112
|
+
i += 1;
|
|
96
113
|
if (arg === "--repo")
|
|
97
114
|
repo = value;
|
|
98
115
|
else if (arg === "--to")
|
|
@@ -117,24 +134,24 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
117
134
|
}
|
|
118
135
|
continue;
|
|
119
136
|
}
|
|
120
|
-
if (
|
|
137
|
+
if (raw.startsWith("--"))
|
|
121
138
|
return refuse("usage", `Unknown option ${arg}.\n${RELEASE_HELP}`, 2);
|
|
122
139
|
positional.push(arg);
|
|
123
140
|
}
|
|
124
|
-
if (
|
|
141
|
+
if (bare.has("--help")) {
|
|
125
142
|
write(RELEASE_HELP);
|
|
126
143
|
return 0;
|
|
127
144
|
}
|
|
128
145
|
if (gate !== undefined) {
|
|
129
146
|
if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
|
|
130
147
|
return refuse("usage", "--release-gate <id> takes no branch or release options.", 2);
|
|
131
|
-
return runGateRelease(gate,
|
|
148
|
+
return runGateRelease(gate, bare.has("--yes"), write, json, deps.gateWorld ?? installedGateWorld());
|
|
132
149
|
}
|
|
133
|
-
if (
|
|
150
|
+
if (bare.has("--yes"))
|
|
134
151
|
return refuse("usage", "--yes goes with --release-gate <id>; a release needs no confirmation beyond the owner's approval.", 2);
|
|
135
152
|
if (positional[0] === "nightly" && positional.length === 1)
|
|
136
|
-
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start:
|
|
137
|
-
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
153
|
+
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
|
|
154
|
+
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
138
155
|
return runAuthorityCommand(positional[0], positional[1], { to, request, as, token, repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo) }, write, json, deps);
|
|
139
156
|
}
|
|
140
157
|
if (positional.length !== 1)
|
|
@@ -161,7 +178,7 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
161
178
|
const adapters = deps.adapters === undefined
|
|
162
179
|
? releaseAdapters({ checkout, say, ...(tap === undefined ? {} : { tap }), ...(limits["deploy"] === undefined ? {} : { deployMinutes: limits["deploy"] }) })
|
|
163
180
|
: { ...deps.adapters, say };
|
|
164
|
-
const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh:
|
|
181
|
+
const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh: bare.has("--new"), noCarry: bare.has("--no-carry") }, adapters);
|
|
165
182
|
return report(outcome, write, json, lines);
|
|
166
183
|
}
|
|
167
184
|
function report(outcome, write, json, lines) {
|
|
@@ -263,6 +280,52 @@ async function runAuthorityCommand(command, target, flags, write, json, deps) {
|
|
|
263
280
|
const words = target === "on" ? "Every later release commit in this project asks for approval." : "Later test-only release commits ship under the release approval.";
|
|
264
281
|
return answer(true, { repo: flags.repo, ask: target === "on" }, words, 0);
|
|
265
282
|
}
|
|
283
|
+
if (command === "publish-ok") {
|
|
284
|
+
// A person's own decision to go on after an automatic release's checks stopped passing past its merge: never the
|
|
285
|
+
// lead, a remote token or a mode.
|
|
286
|
+
const refused = (message) => answer(false, { reason: "person-only", message }, message, 1);
|
|
287
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
|
|
288
|
+
return refused("Only a person lets a held release go on, never the lead or a remote token.");
|
|
289
|
+
if (flags.as === undefined || flags.token === undefined || flags.token === "")
|
|
290
|
+
return refused("Letting a held release go on needs your password: add --as <you> --token <password>.");
|
|
291
|
+
const repo = store.lookupRef(target)?.repo ?? null;
|
|
292
|
+
if (repo === null)
|
|
293
|
+
return answer(false, { reason: "not-found", message: `No release task ${target}.` }, `No release task ${target}.`, 1);
|
|
294
|
+
const proved = authenticateApprover(store, flags.as, flags.token, repo);
|
|
295
|
+
if (!proved.ok || !personProof(proved.method))
|
|
296
|
+
return refused("Only an approver of this project, proved as a person, lets a held release go on.");
|
|
297
|
+
const allowed = allowAutoPublish(store, { taskId: target, by: flags.as, now });
|
|
298
|
+
return allowed.ok ? answer(true, { task: target }, `Release task ${target} may be tagged and published: rerun \`toolroll release\` to go on.`, 0) : answer(false, { reason: "refused", message: allowed.why }, allowed.why, 1);
|
|
299
|
+
}
|
|
300
|
+
if (command === "auto-approve") {
|
|
301
|
+
if (target !== "on" && target !== "off")
|
|
302
|
+
return answer(false, { reason: "usage", message: "auto-approve takes on or off." }, "auto-approve takes on or off.", 2);
|
|
303
|
+
if (flags.repo === undefined)
|
|
304
|
+
return answer(false, { reason: "usage", message: "Name the project with --repo <path>." }, "Name the project with --repo <path>.", 2);
|
|
305
|
+
const repo = store.listProjects().map(one => one.path).find(path => path === flags.repo) ?? null;
|
|
306
|
+
if (repo === null)
|
|
307
|
+
return answer(false, { reason: "not-found", message: "That isn't a project Toolroll knows." }, "That isn't a project Toolroll knows.", 1);
|
|
308
|
+
// Turning it on lets releases approve themselves: a credential act (docs/plans/passwordless.md), only a password
|
|
309
|
+
// typed now by an approver of this project — never the lead's token, an API token, a remembered login or a session.
|
|
310
|
+
// Turning it off only asks a person again, so it needs no password.
|
|
311
|
+
let actor = flags.as ?? "cli";
|
|
312
|
+
if (target === "on") {
|
|
313
|
+
const refused = (message) => answer(false, { reason: "person-only", message }, message, 1);
|
|
314
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
|
|
315
|
+
return refused("Only a person turns on automatic release approval, never the lead or a remote token.");
|
|
316
|
+
if (flags.as === undefined || flags.token === undefined || flags.token === "")
|
|
317
|
+
return refused("Turning on automatic release approval needs your password: add --as <you> --token <password>.");
|
|
318
|
+
const proved = proveCredential(store, flags.as, flags.token, repo);
|
|
319
|
+
if (!proved.ok)
|
|
320
|
+
return refused("Only an approver of this project, with their password typed now, turns on automatic release approval.");
|
|
321
|
+
actor = flags.as;
|
|
322
|
+
}
|
|
323
|
+
const changed = setReleaseApproval(store, repo, target === "on", actor, now);
|
|
324
|
+
const words = target === "on"
|
|
325
|
+
? "Releases in this project approve themselves once the pull request's CI and the release check pass on the exact commit. You're told when one does; anything red still asks you."
|
|
326
|
+
: `Releases in this project ask you again.${changed.withdrawn.length === 0 ? "" : ` ${changed.withdrawn.length} automatic approval${changed.withdrawn.length === 1 ? " was" : "s were"} taken back.`}`;
|
|
327
|
+
return answer(true, { repo, autoApprove: target === "on", changed: changed.changed, withdrawn: changed.withdrawn }, words, 0);
|
|
328
|
+
}
|
|
266
329
|
if (command === "unsandboxed-carry") {
|
|
267
330
|
if (target !== "on" && target !== "off")
|
|
268
331
|
return answer(false, { reason: "usage", message: "unsandboxed-carry takes on or off." }, "unsandboxed-carry takes on or off.", 2);
|
package/dist/release-coverage.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* existing records and Git; nothing is written back, so no proof, receipt,
|
|
5
5
|
* scope digest or schema changes. Fails closed: a receipt that can't be read,
|
|
6
6
|
* a missing commit or any Git error leaves the result uncovered. */
|
|
7
|
-
import {
|
|
7
|
+
import { viewedGitSync } from "./teardown-git.js";
|
|
8
8
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
9
9
|
import { runCheckLevel } from "./check-levels.js";
|
|
10
10
|
/** How many of the newest passing release checks one read considers. */
|
|
@@ -25,7 +25,10 @@ export const gitIsAncestor = (repo, ancestor, descendant) => {
|
|
|
25
25
|
return known;
|
|
26
26
|
let answer;
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
// Every git version through a frozen read view, bounded (eval-harness review 1, finding 7).
|
|
29
|
+
const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000 }).status;
|
|
30
|
+
if (status !== 0)
|
|
31
|
+
throw Object.assign(new Error("git merge-base"), { status });
|
|
29
32
|
answer = true;
|
|
30
33
|
}
|
|
31
34
|
catch (error) {
|
|
@@ -72,7 +72,7 @@ export declare function planTargetedRecheck(store: Store, root: string, input: {
|
|
|
72
72
|
now: Date;
|
|
73
73
|
}): RecheckDecision;
|
|
74
74
|
/** Coverage of a check: full, composed from A, partial (never passes), or identity-removed (asks again). */
|
|
75
|
-
export type Coverage = "full" | "composed" | "partial" | "identity-removed";
|
|
75
|
+
export type Coverage = "full" | "composed" | "partial" | "identity-removed" | "no-result";
|
|
76
76
|
/**
|
|
77
77
|
* Whether a targeted run B, with A's sealed evidence, its plan and snapshot, covers every item of B's inventory
|
|
78
78
|
* (coverage conservation), and every required identity passed — re-derived from sealed values only.
|
package/dist/release-recheck.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Coverage trusts test code (Alex, 2026-10-25): these results protect against accidental damage, not a hostile test.
|
|
21
21
|
* Authority never reads them; the release's own records do.
|
|
22
22
|
*/
|
|
23
|
-
import { CHECK_CONTEXT_CAPTURE, CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, OBLIGATION_SNAPSHOT_CAPTURE, TARGETED_PLAN_CAPTURE, checkContextOf, isUnitFile, payloadDigest, projectGitDir, readSealedJson, treeFactsOf } from "./check-report.js";
|
|
23
|
+
import { CHECK_CONTEXT_CAPTURE, CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, OBLIGATION_SNAPSHOT_CAPTURE, TARGETED_PLAN_CAPTURE, checkContextOf, fullCheckProblem, isUnitFile, payloadDigest, projectGitDir, readSealedJson, treeFactsOf } from "./check-report.js";
|
|
24
24
|
import { readVerifiedCommit, verifiedTreeDiff } from "./commit-extract.js";
|
|
25
25
|
import { checkContextSchema, fileIdentity, groupIdentity, journeyIdentity, obligationSnapshotSchema, readCheckReport, sealedInventorySchema, targetedPlanSchema, unitIdentity, versionIdentity, } from "./contracts/release-check.js";
|
|
26
26
|
import { parseContract } from "./contracts/contract.js";
|
|
@@ -483,6 +483,11 @@ export function settleCoverage(store, root, input, gateOf) {
|
|
|
483
483
|
if (removedProtected(b.report, b.inventory, protectedIdentities(store, root, b.attempt.authority, input.runId)))
|
|
484
484
|
return removed();
|
|
485
485
|
}
|
|
486
|
+
// A release's Full check (its task carries signed release terms, before or after its authority opens — an automatic
|
|
487
|
+
// approval's check runs before) counts only with a complete, valid result (check-report.ts): anything less is no result.
|
|
488
|
+
const release = b.attempt.authority !== null || (store.getScope(input.taskId)?.releaseTerms ?? null) !== null;
|
|
489
|
+
if (release && fullCheckProblem({ report: b.report, inventory: b.inventory, exitCode: input.exitCode }) !== null)
|
|
490
|
+
return { coverage: "no-result", composed: null };
|
|
486
491
|
return { coverage: "full", composed: null };
|
|
487
492
|
}
|
|
488
493
|
if (b.plan === null || b.snapshot === null || b.attempt.baseRun === null || b.attempt.authority === null)
|