toolroll 0.9.72 → 0.9.74
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 +108 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-actions.js +13 -8
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/chat-quiet.js +3 -3
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +20 -19
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +84 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +11 -2
- package/dist/release.js +82 -6
- package/dist/repository-context-ui.js +2 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +34 -23
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/flows.js +40 -10
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +29 -8
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +34 -1
- package/dist/store.js +81 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/subscription-chat.js +5 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
package/dist/release-cli.js
CHANGED
|
@@ -16,13 +16,16 @@ import { RELEASE_LIMITS, runRelease } from "./release.js";
|
|
|
16
16
|
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
|
-
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit } from "./release-carry.js";
|
|
19
|
+
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
|
|
20
20
|
import { authenticateApprover, machineActing, personProof } from "./scope.js";
|
|
21
21
|
import { createNightlyCheckFlow } from "./nightly-check.js";
|
|
22
22
|
import { parseSchedule } from "./flow-schedule.js";
|
|
23
23
|
export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itself, from a pushed branch with an open pull request
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
First looks at the pull request's checks: a required check that failed stops before anything is filed or sent for
|
|
26
|
+
approval, and checks not started or still running are named and waited for (stopping if they don't finish within
|
|
27
|
+
the gate's limit). Then requires an active approver's scope approval (the only human step) and a passing Full check
|
|
28
|
+
on the exact commit.
|
|
26
29
|
Completes that result, drains, backs up, deploys and relinks both CLI names to that commit. After PR checks pass,
|
|
27
30
|
squash-merges, deletes the matching remote branch, verifies the merged tree and tags. Both macOS legs gate npm
|
|
28
31
|
publication; Homebrew waits for its checks to register and all pass before merging.
|
|
@@ -49,6 +52,11 @@ toolroll release nightly --repo <path> --as <you> --token <password> [--schedule
|
|
|
49
52
|
toolroll release ask-every-commit on|off --repo <path> [--as <you> --token <password>] — this project's releases ask
|
|
50
53
|
for every later commit (on), or carry test-only commits again (off: your password, never the lead's token)
|
|
51
54
|
|
|
55
|
+
toolroll release unsandboxed-carry on|off --repo <path> [--as <you> --token <password>] — carry release approvals
|
|
56
|
+
when this project's checks aren't sandboxed (on: your password, never the lead's token). With it on, later
|
|
57
|
+
test-only commits of a release run outside the sandbox without being shown to you first. Off by default; it counts
|
|
58
|
+
only for releases approved while it is on.
|
|
59
|
+
|
|
52
60
|
Options
|
|
53
61
|
--no-carry every later commit of this release asks for approval again
|
|
54
62
|
--to <commit> with carry: the later commit
|
|
@@ -126,7 +134,7 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
126
134
|
return refuse("usage", "--yes goes with --release-gate <id>; a release needs no confirmation beyond the owner's approval.", 2);
|
|
127
135
|
if (positional[0] === "nightly" && positional.length === 1)
|
|
128
136
|
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: args.includes("--start") }, write, json, deps);
|
|
129
|
-
if (["carry", "revoke", "ask-every-commit"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
137
|
+
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
130
138
|
return runAuthorityCommand(positional[0], positional[1], { to, request, as, token, repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo) }, write, json, deps);
|
|
131
139
|
}
|
|
132
140
|
if (positional.length !== 1)
|
|
@@ -255,6 +263,31 @@ async function runAuthorityCommand(command, target, flags, write, json, deps) {
|
|
|
255
263
|
const words = target === "on" ? "Every later release commit in this project asks for approval." : "Later test-only release commits ship under the release approval.";
|
|
256
264
|
return answer(true, { repo: flags.repo, ask: target === "on" }, words, 0);
|
|
257
265
|
}
|
|
266
|
+
if (command === "unsandboxed-carry") {
|
|
267
|
+
if (target !== "on" && target !== "off")
|
|
268
|
+
return answer(false, { reason: "usage", message: "unsandboxed-carry takes on or off." }, "unsandboxed-carry takes on or off.", 2);
|
|
269
|
+
if (flags.repo === undefined)
|
|
270
|
+
return answer(false, { reason: "usage", message: "Name the project with --repo <path>." }, "Name the project with --repo <path>.", 2);
|
|
271
|
+
// Turning it on relaxes a release's rules: a person's own ceremony, as an approval needs, never a lead or remote
|
|
272
|
+
// token. Turning it off is only stricter.
|
|
273
|
+
let actor = "cli";
|
|
274
|
+
if (target === "on") {
|
|
275
|
+
const refused = (message) => answer(false, { reason: "person-only", message }, message, 1);
|
|
276
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
|
|
277
|
+
return refused("Only a person turns this on, never the lead or a remote token.");
|
|
278
|
+
if (flags.as === undefined || flags.token === undefined)
|
|
279
|
+
return refused("Turning this on needs your password: add --as <you> --token <password>.");
|
|
280
|
+
const proved = authenticateApprover(store, flags.as, flags.token, flags.repo);
|
|
281
|
+
if (!proved.ok || !personProof(proved.method))
|
|
282
|
+
return refused("Only an approver of this project, proved as a person, turns this on.");
|
|
283
|
+
actor = flags.as;
|
|
284
|
+
}
|
|
285
|
+
setUnsandboxedCarry(store, flags.repo, target === "on", actor, now);
|
|
286
|
+
const words = target === "on"
|
|
287
|
+
? "Release approvals approved from now on carry when this project's checks aren't sandboxed: later test-only commits will run outside the sandbox without being shown to you first."
|
|
288
|
+
: "Release approvals carry only when this project's checks are sandboxed.";
|
|
289
|
+
return answer(true, { repo: flags.repo, unsandboxedCarry: target === "on" }, words, 0);
|
|
290
|
+
}
|
|
258
291
|
const id = Number(target);
|
|
259
292
|
if (!Number.isSafeInteger(id) || id < 1)
|
|
260
293
|
return answer(false, { reason: "usage", message: `${target} isn't a release id.` }, `${target} isn't a release id.`, 2);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release's repository as Toolroll records it (docs/plans/chat-management.md §0a.2), never as the gate checkout
|
|
3
|
+
* says: the checkout is one of the plane's recorded projects (by real path), its Git directory is that project's own
|
|
4
|
+
* plain `.git` folder, and its GitHub `owner/name` and the URL fetched from come from the recorded remote
|
|
5
|
+
* (`project.remote_url`), not the checkout's `origin`. The checkout is where the release check ran the build's own code,
|
|
6
|
+
* so its `.git/config` is treated as build-written.
|
|
7
|
+
*/
|
|
8
|
+
export type ReleaseRepo = {
|
|
9
|
+
checkout: string;
|
|
10
|
+
gitDir: string;
|
|
11
|
+
remote: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
ssh: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** The programs the coordinator runs, by absolute path, found once when it starts. */
|
|
16
|
+
export type ReleaseTools = {
|
|
17
|
+
git: string;
|
|
18
|
+
gh: string;
|
|
19
|
+
ssh: string;
|
|
20
|
+
};
|
|
21
|
+
/** owner/name and transport of a recorded GitHub remote, read strictly (https or ssh to github.com, nothing else). */
|
|
22
|
+
export declare function releaseRemote(url: string): {
|
|
23
|
+
slug: string;
|
|
24
|
+
ssh: boolean;
|
|
25
|
+
} | null;
|
|
26
|
+
/** Fixed system folders: the only PATH the coordinator's Git and gh run with. */
|
|
27
|
+
export declare const RELEASE_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
28
|
+
/** Git, gh and ssh by absolute path: the system's Git (or the first in the fixed system folders), the installed gh as
|
|
29
|
+
* this process's PATH finds it now, and the system ssh. Resolved once; never looked up again. */
|
|
30
|
+
export declare function releaseTools(path?: string): ReleaseTools;
|
|
31
|
+
/** The coordinator's Git and gh environment: an allowlist, a fixed PATH, no system or global Git config, no prompts. */
|
|
32
|
+
export declare function releaseGitEnv(base?: Readonly<NodeJS.ProcessEnv>): NodeJS.ProcessEnv;
|
|
33
|
+
/** Every coordinator Git command's fixed settings, ahead of its own arguments: the recorded Git directory and checkout,
|
|
34
|
+
* no hooks, fsmonitor, replacement objects, pager or submodule recursion, only https (and ssh for an ssh remote), the
|
|
35
|
+
* system ssh, and gh as the only credential helper. */
|
|
36
|
+
export declare function releaseGitArgs(repo: ReleaseRepo, tools: ReleaseTools): string[];
|
|
37
|
+
/** The named remote the coordinator fetches, looks and pushes through: defined only on its own command line (`-c`), so
|
|
38
|
+
* no checkout setting names or rewrites it. */
|
|
39
|
+
export declare const RELEASE_REMOTE = "toolroll-release";
|
|
40
|
+
/** The first local or worktree setting a gate checkout's config holds that a release won't run with, or null: one
|
|
41
|
+
* outside the allowlist (an include, a command, a proxy, TLS or header setting, a credential helper, a URL rewrite, a
|
|
42
|
+
* hooks folder), a remote named like a URL or after the coordinator's own, or a remote over a transport helper. */
|
|
43
|
+
export declare function releaseConfigProblem(listing: string): string | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release coordinator's Git, as one hardened runner's pieces (docs/plans/chat-management.md §0a.2): the recorded
|
|
3
|
+
* repository's shape, the programs by absolute path, the allowlisted environment, the fixed settings and the checkout
|
|
4
|
+
* config allowlist. Kept apart from release-adapters.ts so the release card (release-card.ts) reads Git the same way
|
|
5
|
+
* without loading the coordinator.
|
|
6
|
+
*/
|
|
7
|
+
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
8
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
9
|
+
import { HARDENED_GIT_ARGS } from "./agent-fence.js";
|
|
10
|
+
import { TEARDOWN_FLAGS } from "./teardown-git.js";
|
|
11
|
+
/** owner/name and transport of a recorded GitHub remote, read strictly (https or ssh to github.com, nothing else). */
|
|
12
|
+
export function releaseRemote(url) {
|
|
13
|
+
const part = "([A-Za-z0-9_.-]+)";
|
|
14
|
+
const https = new RegExp(`^https://github\\.com/${part}/${part}?(?:\\.git)?/?$`).exec(url);
|
|
15
|
+
const ssh = new RegExp(`^(?:ssh://)?git@github\\.com[:/]${part}/${part}?(?:\\.git)?/?$`).exec(url);
|
|
16
|
+
const match = https ?? ssh;
|
|
17
|
+
if (match === null || match[2] === undefined || match[1].startsWith(".") || match[2].startsWith("."))
|
|
18
|
+
return null;
|
|
19
|
+
return { slug: `${match[1]}/${match[2].replace(/\.git$/, "")}`, ssh: https === null };
|
|
20
|
+
}
|
|
21
|
+
const executable = (path) => { try {
|
|
22
|
+
accessSync(path, fsConstants.X_OK);
|
|
23
|
+
return statSync(path).isFile();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
} };
|
|
28
|
+
/** Fixed system folders: the only PATH the coordinator's Git and gh run with. */
|
|
29
|
+
export const RELEASE_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
30
|
+
/** Git, gh and ssh by absolute path: the system's Git (or the first in the fixed system folders), the installed gh as
|
|
31
|
+
* this process's PATH finds it now, and the system ssh. Resolved once; never looked up again. */
|
|
32
|
+
export function releaseTools(path = process.env["PATH"] ?? "") {
|
|
33
|
+
const find = (name, dirs) => {
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
if (!isAbsolute(dir))
|
|
36
|
+
continue;
|
|
37
|
+
const candidate = join(dir, name);
|
|
38
|
+
if (executable(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
const system = ["/usr/bin", "/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
44
|
+
return {
|
|
45
|
+
git: find("git", system) ?? "/usr/bin/git",
|
|
46
|
+
gh: find("gh", [...path.split(delimiter), ...system]) ?? "/usr/bin/gh",
|
|
47
|
+
ssh: find("ssh", ["/usr/bin", "/bin"]) ?? "/usr/bin/ssh",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The coordinator's Git and gh environment: an allowlist, a fixed PATH, no system or global Git config, no prompts. */
|
|
51
|
+
export function releaseGitEnv(base = process.env) {
|
|
52
|
+
const kept = ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "USER", "LOGNAME", "SSH_AUTH_SOCK", "GH_CONFIG_DIR", "XDG_CONFIG_HOME", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST"];
|
|
53
|
+
return {
|
|
54
|
+
...Object.fromEntries(kept.flatMap(name => base[name] === undefined ? [] : [[name, base[name]]])),
|
|
55
|
+
PATH: RELEASE_PATH, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0", GH_PROMPT_DISABLED: "1", GIT_ASKPASS: "", SSH_ASKPASS: "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Every coordinator Git command's fixed settings, ahead of its own arguments: the recorded Git directory and checkout,
|
|
59
|
+
* no hooks, fsmonitor, replacement objects, pager or submodule recursion, only https (and ssh for an ssh remote), the
|
|
60
|
+
* system ssh, and gh as the only credential helper. */
|
|
61
|
+
export function releaseGitArgs(repo, tools) {
|
|
62
|
+
return [`--git-dir=${repo.gitDir}`, `--work-tree=${repo.checkout}`, "--no-replace-objects", "--no-pager", ...HARDENED_GIT_ARGS, ...TEARDOWN_FLAGS,
|
|
63
|
+
"-c", "protocol.allow=never", "-c", "protocol.https.allow=always", ...(repo.ssh ? ["-c", "protocol.ssh.allow=always"] : []),
|
|
64
|
+
"-c", `core.sshCommand=${tools.ssh}`, "-c", "core.askPass=", "-c", "fetch.recurseSubmodules=false", "-c", "core.pager=cat",
|
|
65
|
+
// Where it connects and what it sends: verified TLS, no proxy, no extra header or cookie file. (An empty CA file
|
|
66
|
+
// or path can't be given here, Git then trusts nothing; a checkout setting either stops the release instead.)
|
|
67
|
+
"-c", "http.sslVerify=true", "-c", "http.proxy=", "-c", "http.extraHeader=", "-c", "http.cookieFile=",
|
|
68
|
+
// The recorded URL, as a remote that exists only here.
|
|
69
|
+
"-c", `remote.${RELEASE_REMOTE}.url=${repo.remote}`, "-c", `remote.${RELEASE_REMOTE}.pushurl=${repo.remote}`, "-c", `remote.${RELEASE_REMOTE}.proxy=`,
|
|
70
|
+
...(repo.ssh ? [] : ["-c", `credential.helper=!'${tools.gh.replace(/'/g, "'\\''")}' auth git-credential`])];
|
|
71
|
+
}
|
|
72
|
+
/** The named remote the coordinator fetches, looks and pushes through: defined only on its own command line (`-c`), so
|
|
73
|
+
* no checkout setting names or rewrites it. */
|
|
74
|
+
export const RELEASE_REMOTE = "toolroll-release";
|
|
75
|
+
/** The local and worktree settings a gate checkout may hold for a release (an allowlist): plain values Git writes
|
|
76
|
+
* itself and that never decide where it connects, what it sends or what it runs. Anything else stops the release. */
|
|
77
|
+
const RELEASE_KEYS = [
|
|
78
|
+
/^core\.(repositoryformatversion|filemode|bare|logallrefupdates|ignorecase|precomposeunicode|symlinks|autocrlf|eol|safecrlf|quotepath|longpaths|checkstat|trustctime)$/,
|
|
79
|
+
/^extensions\.(worktreeconfig|objectformat|refstorage)$/,
|
|
80
|
+
/^remote\.[^/:\\]+\.(url|fetch|pushurl|tagopt|prune)$/,
|
|
81
|
+
/^branch\..+\.(remote|merge|rebase|pushremote|description|vscode-merge-base)$/,
|
|
82
|
+
/^(user\.(name|email)|pull\.(rebase|ff)|init\.defaultbranch|lfs\.repositoryformatversion)$/,
|
|
83
|
+
];
|
|
84
|
+
/** The first local or worktree setting a gate checkout's config holds that a release won't run with, or null: one
|
|
85
|
+
* outside the allowlist (an include, a command, a proxy, TLS or header setting, a credential helper, a URL rewrite, a
|
|
86
|
+
* hooks folder), a remote named like a URL or after the coordinator's own, or a remote over a transport helper. */
|
|
87
|
+
export function releaseConfigProblem(listing) {
|
|
88
|
+
const fields = listing.split("\0");
|
|
89
|
+
for (let at = 0; at + 1 < fields.length; at += 2) {
|
|
90
|
+
if (fields[at] !== "local" && fields[at] !== "worktree")
|
|
91
|
+
continue;
|
|
92
|
+
const entry = fields[at + 1];
|
|
93
|
+
const newline = entry.indexOf("\n");
|
|
94
|
+
const key = (newline < 0 ? entry : entry.slice(0, newline)).toLowerCase();
|
|
95
|
+
const value = (newline < 0 ? "" : entry.slice(newline + 1)).trim();
|
|
96
|
+
if (key.includes(RELEASE_REMOTE))
|
|
97
|
+
return key;
|
|
98
|
+
if (key === "core.fsmonitor" && /^(false|no|off|0)$/i.test(value))
|
|
99
|
+
continue;
|
|
100
|
+
if (!RELEASE_KEYS.some(pattern => pattern.test(key)))
|
|
101
|
+
return key;
|
|
102
|
+
if (/^remote\./.test(key) && /^[A-Za-z][A-Za-z0-9+.-]*::/.test(value))
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* gate → approval → check → complete → quiet → deploy → link → ci → merge → tree → tag → publish → homebrew → done
|
|
7
7
|
*
|
|
8
|
-
* gate
|
|
8
|
+
* gate, before filing a task for that commit, looks at the pull request's required checks on it: one that failed stops
|
|
9
|
+
* the release before anything is filed or sent for approval; it waits, naming them, for checks not started or still
|
|
10
|
+
* running, and stops if they don't finish within its limit (a task already filed is not looked at again; ci checks
|
|
11
|
+
* once more before merging). It then files a prepared-candidate task pinned to the branch's exact commit, with the full
|
|
12
|
+
* release check. approval
|
|
9
13
|
* waits for an active approver's yes to that exact scope (never a mode's, an AI's or the lead's). check requires a Full
|
|
10
14
|
* result of that commit: a failed check stops the release and stays visible, nothing reruns it. complete marks that
|
|
11
15
|
* exact result Complete through the task's own authorization. quiet waits until nothing builds. deploy runs the
|
|
@@ -35,7 +39,7 @@ export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "
|
|
|
35
39
|
export declare const RELEASE_TAG_CHECKS: readonly ["node 22 · macos-latest", "node 24 · macos-latest", "publish"];
|
|
36
40
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
37
41
|
export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
38
|
-
type WaitingStep = "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
42
|
+
type WaitingStep = "gate" | "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
39
43
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
40
44
|
export declare const RELEASE_LIMITS: Readonly<Record<WaitingStep, number>>;
|
|
41
45
|
/** One check attempt; `app` is the GitHub App that made it (github-actions for a workflow), when known. */
|
|
@@ -86,6 +90,9 @@ export type GateState = {
|
|
|
86
90
|
} | null;
|
|
87
91
|
/** A finished build that left no result to complete (it failed before the check), in words. */
|
|
88
92
|
failed: string | null;
|
|
93
|
+
/** A newer attempt is queued or running (`task regate` after a failed check): the result shown is the one before it.
|
|
94
|
+
* Absent from a plane too old to say. */
|
|
95
|
+
pending?: boolean;
|
|
89
96
|
completion: {
|
|
90
97
|
actor: string;
|
|
91
98
|
at: string;
|
|
@@ -467,4 +474,6 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
|
|
|
467
474
|
};
|
|
468
475
|
/** Run the release from wherever its journal stopped. */
|
|
469
476
|
export declare function runRelease(options: ReleaseOptions, adapters: ReleaseAdapters): Promise<ReleaseOutcome>;
|
|
477
|
+
/** A probe's failure that is only the network to GitHub being slow, never a decision (a Stop) or another error. */
|
|
478
|
+
export declare function transientNetworkError(error: unknown): boolean;
|
|
470
479
|
export {};
|
package/dist/release.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* gate → approval → check → complete → quiet → deploy → link → ci → merge → tree → tag → publish → homebrew → done
|
|
7
7
|
*
|
|
8
|
-
* gate
|
|
8
|
+
* gate, before filing a task for that commit, looks at the pull request's required checks on it: one that failed stops
|
|
9
|
+
* the release before anything is filed or sent for approval; it waits, naming them, for checks not started or still
|
|
10
|
+
* running, and stops if they don't finish within its limit (a task already filed is not looked at again; ci checks
|
|
11
|
+
* once more before merging). It then files a prepared-candidate task pinned to the branch's exact commit, with the full
|
|
12
|
+
* release check. approval
|
|
9
13
|
* waits for an active approver's yes to that exact scope (never a mode's, an AI's or the lead's). check requires a Full
|
|
10
14
|
* result of that commit: a failed check stops the release and stays visible, nothing reruns it. complete marks that
|
|
11
15
|
* exact result Complete through the task's own authorization. quiet waits until nothing builds. deploy runs the
|
|
@@ -45,7 +49,7 @@ export const REQUIRED_PR_CHECKS = [
|
|
|
45
49
|
export const RELEASE_TAG_CHECKS = ["node 22 · macos-latest", "node 24 · macos-latest", "publish"];
|
|
46
50
|
export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
47
51
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
48
|
-
export const RELEASE_LIMITS = { approval: 24 * 60, check: 120, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
52
|
+
export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 120, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
49
53
|
const POLL_MS = 15_000;
|
|
50
54
|
/** A stop the release reports: the step it stopped at and why, in words. */
|
|
51
55
|
class Stop extends Error {
|
|
@@ -177,16 +181,29 @@ export async function runRelease(options, adapters) {
|
|
|
177
181
|
delete j.intents.deploy;
|
|
178
182
|
save();
|
|
179
183
|
};
|
|
180
|
-
/** Ask `probe` until it answers, within this step's limit.
|
|
184
|
+
/** Ask `probe` until it answers, within this step's limit. A short network failure on the way to GitHub (git or gh
|
|
185
|
+
* timing out) is no answer yet, never the release's end: the next poll asks again, until the step's own limit. */
|
|
181
186
|
const waitFor = async (at, what, probe) => {
|
|
182
187
|
const deadline = adapters.now().getTime() + limits[at] * 60_000;
|
|
183
|
-
let said = false;
|
|
188
|
+
let said = false, unreachable = null;
|
|
184
189
|
for (;;) {
|
|
185
|
-
|
|
190
|
+
let answer;
|
|
191
|
+
try {
|
|
192
|
+
answer = await probe();
|
|
193
|
+
unreachable = null;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (!transientNetworkError(error))
|
|
197
|
+
throw error;
|
|
198
|
+
if (unreachable === null)
|
|
199
|
+
say(` GitHub didn't answer in time (${networkWords(error)}); asking again`);
|
|
200
|
+
unreachable = networkWords(error);
|
|
201
|
+
answer = null;
|
|
202
|
+
}
|
|
186
203
|
if (answer !== null)
|
|
187
204
|
return answer;
|
|
188
205
|
if (adapters.now().getTime() >= deadline)
|
|
189
|
-
throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes.`);
|
|
206
|
+
throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes${unreachable === null ? "" : ` (GitHub last didn't answer: ${unreachable})`}.`);
|
|
190
207
|
if (!said) {
|
|
191
208
|
say(` waiting for ${what} (up to ${limits[at]} min)`);
|
|
192
209
|
said = true;
|
|
@@ -464,11 +481,13 @@ export async function runRelease(options, adapters) {
|
|
|
464
481
|
if (existing.exists && j.freshTask === true && j.intents.gate === undefined)
|
|
465
482
|
throw new Stop("approval", `Task ${j.task.id} appeared before this release filed it; rerun with --new.`);
|
|
466
483
|
if (existing.exists) {
|
|
484
|
+
// Filed already (a resume after a lost answer): the task stands, and the ci step judges its checks.
|
|
467
485
|
if (existing.candidate !== j.candidate.sha)
|
|
468
486
|
throw new Stop("conflict", `Task ${j.task.id} exists for a different commit (${existing.candidate?.slice(0, 7) ?? "none"}).`);
|
|
469
487
|
j.task.scopeDigest = existing.scopeDigest;
|
|
470
488
|
}
|
|
471
489
|
else {
|
|
490
|
+
await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe));
|
|
472
491
|
intend("gate");
|
|
473
492
|
await adapters.repo.sync(j.branch);
|
|
474
493
|
const filed = await adapters.toolroll.fileGate({
|
|
@@ -516,6 +535,9 @@ export async function runRelease(options, adapters) {
|
|
|
516
535
|
await watch("dispatch");
|
|
517
536
|
const gate = await adapters.toolroll.gate(j.task.id);
|
|
518
537
|
await approved(gate);
|
|
538
|
+
// A rerun of the same approved commit is on its way: the failure before it is not this release's answer.
|
|
539
|
+
if (gate.pending === true && gate.result?.check !== "passed")
|
|
540
|
+
return null;
|
|
519
541
|
if (gate.failed !== null)
|
|
520
542
|
throw new Stop("check-failed", `The release build of ${j.task.id} failed: ${gate.failed}. Fix it and release the new commit with --new.`);
|
|
521
543
|
if (gate.result === null || gate.result.check === "running" || gate.result.check === "pending")
|
|
@@ -886,3 +908,57 @@ async function deployed(adapters, candidate) {
|
|
|
886
908
|
const [runtime] = runtimes;
|
|
887
909
|
return runtime !== undefined && runtime !== "" && await adapters.toolroll.runtimeCommit(runtime) === candidate ? runtime : null;
|
|
888
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* The gate's look at the pull request's required checks on the exact commit, before anything is filed or anyone is
|
|
913
|
+
* asked to approve (#237): a required check that failed or was cancelled stops here; checks not started or still
|
|
914
|
+
* running are named, and the gate waits for them; ones that never finish within the gate's limit stop here too. GitHub
|
|
915
|
+
* not answering is waited out the same way, never taken for a pass. The ci step still checks again before merging.
|
|
916
|
+
*/
|
|
917
|
+
async function earlyChecks(adapters, j, wait) {
|
|
918
|
+
const sha = j.candidate.sha.slice(0, 7);
|
|
919
|
+
const required = REQUIRED_PR_CHECKS;
|
|
920
|
+
let named = "", unread = false;
|
|
921
|
+
try {
|
|
922
|
+
await wait(`the pull request checks on ${sha}`, async () => {
|
|
923
|
+
let runs;
|
|
924
|
+
try {
|
|
925
|
+
runs = await adapters.github.checks(j.github, j.candidate.sha);
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
if (!unread)
|
|
929
|
+
adapters.say(` Couldn't read the pull request checks on ${sha} (${error?.message ?? error}); trying again.`);
|
|
930
|
+
unread = true;
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
const verdict = checksVerdict(runs.filter(run => required.includes(run.name)), required);
|
|
934
|
+
if (verdict.state === "failed") {
|
|
935
|
+
throw new Stop("ci-failed", `Pull request checks already failed on ${sha}: ${verdict.failed.join(", ")}. Nothing was filed or sent for approval. Fix the branch, then release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\` (or rerun the failed job on GitHub and run this command again).`);
|
|
936
|
+
}
|
|
937
|
+
if (verdict.state === "passed")
|
|
938
|
+
return true;
|
|
939
|
+
const now = verdict.waiting.join(", ");
|
|
940
|
+
if (now !== named) {
|
|
941
|
+
adapters.say(` Pull request checks on ${sha} not finished yet: ${now}.`);
|
|
942
|
+
named = now;
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
if (!(error instanceof Stop) || error.reason !== "timeout")
|
|
949
|
+
throw error;
|
|
950
|
+
throw new Stop("ci-missing", `Pull request checks on ${sha} didn't all finish in time${named === "" ? (unread ? " (GitHub didn't answer)" : "") : `: ${named}`}. Nothing was filed or sent for approval. Start or rerun them on GitHub, then run this command again.`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** The network failures a release's waits ride out: git or gh timing out (or cut off) on the way to GitHub. */
|
|
954
|
+
const TRANSIENT_NETWORK = /Recv failure: (?:Operation timed out|Connection reset by peer)|TLS handshake timeout|Operation timed out|Connection timed out|i\/o timeout|Connection reset by peer|Failed to connect to [^\s]*github\.com port \d+/i;
|
|
955
|
+
/** A probe's failure that is only the network to GitHub being slow, never a decision (a Stop) or another error. */
|
|
956
|
+
export function transientNetworkError(error) {
|
|
957
|
+
if (error instanceof Stop || error instanceof Restart)
|
|
958
|
+
return false;
|
|
959
|
+
return TRANSIENT_NETWORK.test(String(error?.message ?? error));
|
|
960
|
+
}
|
|
961
|
+
/** The part of a network failure worth saying: its matching phrase. */
|
|
962
|
+
function networkWords(error) {
|
|
963
|
+
return TRANSIENT_NETWORK.exec(String(error?.message ?? error))?.[0] ?? "a network failure";
|
|
964
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, postForm } from "./html.js";
|
|
2
|
+
import { framedSection } from './knowledge-ui.js';
|
|
2
3
|
export function repositoryContextHtml(repo, query, result, csrf) {
|
|
3
4
|
const root = `/settings/knowledge?repo=${encodeURIComponent(repo)}`;
|
|
4
|
-
return html `<
|
|
5
|
+
return framedSection('Relevant code', html `<form class="field-row" method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label>Find code<input name="q" value="${query}" maxlength="1000" placeholder="A feature, file or symbol"></label><button type="submit">Search</button></form>${result === null ? '' : html `<p class="meta">${result.index.engine === 'typescript-ast' ? 'Code relationships' : 'Source search'}${result.mode === 'impact' ? ' · possible change impact' : ''}</p>${result.excerpts.length ? result.excerpts.map(one => html `<details><summary>${one.file}:${one.line}${one.symbol ? ` · ${one.symbol}` : ''}</summary><p class="meta">${one.reason}</p><pre>${one.text}</pre><a href="${root}&mode=impact&q=${encodeURIComponent(one.file)}">What this may affect</a></details>`) : html `<p>No matching source was found in this bounded search.</p>`}${result.relationships.length ? html `<details><summary>Related files</summary><ul>${result.relationships.map(one => html `<li>${one.from} → ${one.to} · ${one.kind}</li>`)}</ul></details>` : ''}<details><summary>Context details</summary><p>${result.index.status}${result.checkout ? ` · checkout ${result.checkout.head.slice(0, 8)}` : ''}</p>${result.warnings.map(one => html `<p>${one}</p>`)}</details>`}${csrf ? html `<details><summary>Refresh code index</summary>${postForm("/settings/knowledge/refresh", html `<p>Builds a local map of TypeScript and JavaScript imports. Uses no model.</p><button type="submit">Refresh index</button>`, { hidden: { repo } })}</details>` : ''}`, 'repository-context');
|
|
5
6
|
}
|
package/dist/run-efficiency.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* kept 24 hours) are read once, bounded, through the descriptor-safe live reader, for step counts and observed step
|
|
9
9
|
* times. No command text is ever stored here: repeats are counted, never quoted.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { type Run, type Store } from "./store.js";
|
|
12
12
|
import type { LiveActivityKind, LiveRecord } from "./live-records.js";
|
|
13
13
|
export declare const RUN_EFFICIENCY_VERSION = 3;
|
|
14
14
|
/** Budgets for one derivation: never more than the live file's own cap, and a bounded record count. */
|
|
@@ -62,10 +62,10 @@ export declare function usageOf(run: Pick<Run, "provider" | "model" | "tokensIn"
|
|
|
62
62
|
* others their times are partly made up (Codex reports a step when it completes, ACP agents may report late), so no
|
|
63
63
|
* duration is claimed for them. */
|
|
64
64
|
export declare function stepsOf(records: Iterable<LiveRecord>, truncated: string | null, measured?: boolean): RunEfficiency["steps"];
|
|
65
|
-
/** Derive a finished run's How it ran from its records. */
|
|
65
|
+
/** Derive a finished run's How it ran from its records, read under the run's own captured "Show commands" answer. */
|
|
66
66
|
export declare function deriveRunEfficiency(run: Run & {
|
|
67
67
|
usageJson?: string | null;
|
|
68
|
-
}, evidenceRoot: string, now: Date): RunEfficiency;
|
|
68
|
+
}, evidenceRoot: string, now: Date, commands?: boolean): RunEfficiency;
|
|
69
69
|
/**
|
|
70
70
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
71
71
|
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved.
|
package/dist/run-efficiency.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run went (docs/plans/run-efficiency.md): derived from the run's own records, display only. It never changes a
|
|
3
|
+
* completion, an approval, a charge or a check verdict, and it claims nothing its sources don't hold — an unreported
|
|
4
|
+
* number is unknown (null, shown as a dash), never zero.
|
|
5
|
+
*
|
|
6
|
+
* Sources, and what each can say: the run row (wall times, tokens, the provider's raw usage with its cache counts, the
|
|
7
|
+
* agent's turns where reported) is kept with the run; the live records (`<evidence>/live/<run>.jsonl`, display-only,
|
|
8
|
+
* kept 24 hours) are read once, bounded, through the descriptor-safe live reader, for step counts and observed step
|
|
9
|
+
* times. No command text is ever stored here: repeats are counted, never quoted.
|
|
10
|
+
*/
|
|
11
|
+
import { runLiveCommands } from "./store.js";
|
|
1
12
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
13
|
import { join } from "node:path";
|
|
3
14
|
import { LIVE_RETAIN_MS, legacyLiveLogPath, liveLogPath, liveWindowRecords } from "./live.js";
|
|
@@ -95,7 +106,7 @@ const noSteps = (state, reason) => ({ state, reason, total: null, failed: null,
|
|
|
95
106
|
* directory), measured once, and never more than the budget read whatever the file does meanwhile; then parsed line by
|
|
96
107
|
* line. A file over the budget, in the older text format, with lines that don't read, or cut or capped while the agent
|
|
97
108
|
* ran, says its steps are incomplete. Null when there is no step file at all. */
|
|
98
|
-
function readRecords(evidenceRoot, runId) {
|
|
109
|
+
function readRecords(evidenceRoot, runId, commands) {
|
|
99
110
|
try {
|
|
100
111
|
const dir = lstatSync(join(evidenceRoot, "live"));
|
|
101
112
|
if (!dir.isDirectory() || dir.isSymbolicLink())
|
|
@@ -154,10 +165,10 @@ function readRecords(evidenceRoot, runId) {
|
|
|
154
165
|
whole.length = EFFICIENCY_RECORDS;
|
|
155
166
|
const keys = { inKey: false };
|
|
156
167
|
// One record per line: a whole line that doesn't read is a record lost.
|
|
157
|
-
const read = liveWindowRecords({ text: whole.length === 0 ? "" : `${whole.join("\n")}\n`, format: "records" }, keys);
|
|
168
|
+
const read = liveWindowRecords({ text: whole.length === 0 ? "" : `${whole.join("\n")}\n`, format: "records" }, commands, keys);
|
|
158
169
|
const lost = whole.length - read.length;
|
|
159
170
|
// A finished run's last line without its newline is read like any other; one that doesn't read is a record lost.
|
|
160
|
-
const tail = unfinished.trim() === "" ? [] : liveWindowRecords({ text: `${unfinished}\n`, format: "records" }, keys);
|
|
171
|
+
const tail = unfinished.trim() === "" ? [] : liveWindowRecords({ text: `${unfinished}\n`, format: "records" }, commands, keys);
|
|
161
172
|
const records = [...read, ...tail];
|
|
162
173
|
const lostAll = lost + (unfinished.trim() !== "" && tail.length === 0 ? 1 : 0);
|
|
163
174
|
if (records.length === 0 && text.trim() !== "")
|
|
@@ -170,10 +181,10 @@ function readRecords(evidenceRoot, runId) {
|
|
|
170
181
|
: null;
|
|
171
182
|
return { records, truncated };
|
|
172
183
|
}
|
|
173
|
-
/** Derive a finished run's How it ran from its records. */
|
|
174
|
-
export function deriveRunEfficiency(run, evidenceRoot, now) {
|
|
184
|
+
/** Derive a finished run's How it ran from its records, read under the run's own captured "Show commands" answer. */
|
|
185
|
+
export function deriveRunEfficiency(run, evidenceRoot, now, commands = false) {
|
|
175
186
|
const finished = run.finishedAt;
|
|
176
|
-
const read = readRecords(evidenceRoot, run.id);
|
|
187
|
+
const read = readRecords(evidenceRoot, run.id, commands);
|
|
177
188
|
const steps = read !== null && "unreadable" in read ? noSteps("truncated", read.unreadable)
|
|
178
189
|
: read !== null ? stepsOf(read.records, read.truncated, run.provider === "claude")
|
|
179
190
|
: finished !== null && now.getTime() - Date.parse(finished) > LIVE_RETAIN_MS ? noSteps("expired", "Step details are kept for a day after a run; this one's have expired.")
|
|
@@ -196,8 +207,9 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
196
207
|
return null;
|
|
197
208
|
const usageJson = store.handle.prepare("SELECT usage_json FROM run WHERE id = ?").get(runId)?.["usage_json"];
|
|
198
209
|
const full = { ...run, usageJson: typeof usageJson === "string" ? usageJson : null };
|
|
210
|
+
const commands = runLiveCommands(store, runId);
|
|
199
211
|
if (run.finishedAt === null)
|
|
200
|
-
return deriveRunEfficiency(full, evidenceRoot, now);
|
|
212
|
+
return deriveRunEfficiency(full, evidenceRoot, now, commands);
|
|
201
213
|
let saved;
|
|
202
214
|
try {
|
|
203
215
|
saved = store.handle.prepare("SELECT parser_version, payload_json FROM run_efficiency WHERE run = ?").get(runId);
|
|
@@ -211,7 +223,7 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
211
223
|
}
|
|
212
224
|
catch { /* derived again below */ }
|
|
213
225
|
}
|
|
214
|
-
const derived = deriveRunEfficiency(full, evidenceRoot, now);
|
|
226
|
+
const derived = deriveRunEfficiency(full, evidenceRoot, now, commands);
|
|
215
227
|
const payload = JSON.stringify(derived);
|
|
216
228
|
// A row too large for its column (it can't be: the counts are bounded) is shown, never saved.
|
|
217
229
|
// Saving is a convenience: a store that can't take the row (read-only, busy) still shows what was derived.
|
package/dist/scope.d.ts
CHANGED
|
@@ -945,3 +945,10 @@ export declare function liveReleaseOf(store: Store, taskId: string, action: Rele
|
|
|
945
945
|
* check was composed from a targeted run: a full pass is needed first. Null when nothing blocks.
|
|
946
946
|
*/
|
|
947
947
|
export declare function mainDebtProblem(store: Store, repo: string, taskId: string): string | null;
|
|
948
|
+
/**
|
|
949
|
+
* A deploy gate (docs/plans/chat-management.md §0a.5): a scope whose signed terms name a prepared candidate or carry
|
|
950
|
+
* release terms. Both are in the scope digest and written at filing, before any release authority exists, so this is
|
|
951
|
+
* read from the scope alone: a first approval and a fallback gate filed without --release are deploy gates too. From
|
|
952
|
+
* chat it is approved only through the release card.
|
|
953
|
+
*/
|
|
954
|
+
export declare function deployGateScope(scope: Pick<Scope, "candidate" | "releaseTerms">): boolean;
|
package/dist/scope.js
CHANGED
|
@@ -1327,7 +1327,8 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1327
1327
|
export function releaseTermsSentence(terms) {
|
|
1328
1328
|
if (!terms.policy.carry)
|
|
1329
1329
|
return `Release ${terms.packageVersion} from pull request #${terms.pr} at ${terms.approvedSha.slice(0, 7)}. Every later commit asks for your approval again.`;
|
|
1330
|
-
|
|
1330
|
+
const outside = terms.access === "sandboxed" ? "" : " This project's checks don't run in the sandbox: those later test-only commits will run outside the sandbox without being shown to you first.";
|
|
1331
|
+
return `Release ${terms.packageVersion} from pull request #${terms.pr} at ${terms.approvedSha.slice(0, 7)}. Later commits on this pull request that only change tests, docs or the changelog ship without asking you again, until it merges or for 3 days. You'll see each one in the release summary.${outside}`;
|
|
1331
1332
|
}
|
|
1332
1333
|
/** What asks again, in full: the terms behind the sentence. */
|
|
1333
1334
|
export function releaseAsksAgainWords(terms) {
|
|
@@ -1338,7 +1339,7 @@ export function releaseAsksAgainWords(terms) {
|
|
|
1338
1339
|
`when it deletes, renames or changes the mode of a file, adds a binary, a symlink or a submodule; when it changes the production build;`,
|
|
1339
1340
|
`when more than ${p.limits.lines} lines or ${p.limits.files} files change after this approval, or ${p.limits.churn} lines of churn;`,
|
|
1340
1341
|
...(p.protectProject ? ["when it changes anything: this project is protected;"] : p.protectedPaths.length === 0 ? [] : [`when it changes a protected path: ${p.protectedPaths.join(", ")};`]),
|
|
1341
|
-
`and always once the deploy starts. Repository ${terms.repo}, branch ${terms.branch} into ${terms.base}, check grant ${terms.grant.id}, checks in the sandbox, until ${terms.expiresAt}.`,
|
|
1342
|
+
`and always once the deploy starts. Repository ${terms.repo}, branch ${terms.branch} into ${terms.base}, check grant ${terms.grant.id}, ${terms.access === "sandboxed" ? "checks in the sandbox" : `checks outside the sandbox (${terms.access}), carried because the project allows it`}, until ${terms.expiresAt}.`,
|
|
1342
1343
|
];
|
|
1343
1344
|
}
|
|
1344
1345
|
function releaseTermsWords(terms) {
|
|
@@ -1358,7 +1359,7 @@ function describeApproval(approval) {
|
|
|
1358
1359
|
export { personProof } from "./person-proof.js";
|
|
1359
1360
|
import { personProof } from "./person-proof.js";
|
|
1360
1361
|
import { releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1361
|
-
import { newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
|
|
1362
|
+
import { CHECK_ACCESS_WIDTH, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
|
|
1362
1363
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
1363
1364
|
export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
|
|
1364
1365
|
/** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
|
|
@@ -1494,8 +1495,15 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1494
1495
|
const grant = store.liveVerifyCommand(a.repo);
|
|
1495
1496
|
if (grant === null || grant.id !== a.verifyCommand || grant.digest !== a.verifyDigest)
|
|
1496
1497
|
return refuse("the release check command changed since the approval");
|
|
1497
|
-
|
|
1498
|
-
|
|
1498
|
+
// The checks reach no further than at the approval. Wider than the sandbox, a carried commit stands only while both
|
|
1499
|
+
// the sealed and the current "Carry release approvals when checks aren't sandboxed" allow it (the stricter of the two).
|
|
1500
|
+
const access = store.projectCheckAccess(a.repo);
|
|
1501
|
+
const sealedWidth = CHECK_ACCESS_WIDTH[a.accessDigest], nowWidth = CHECK_ACCESS_WIDTH[access];
|
|
1502
|
+
if (sealedWidth === undefined || nowWidth === undefined || nowWidth > sealedWidth)
|
|
1503
|
+
return refuse(a.accessDigest === "sandboxed" ? "the project's checks no longer run in the sandbox" : "the project's checks reach further than when the release was approved");
|
|
1504
|
+
if (carry !== null && (a.accessDigest !== "sandboxed" || access !== "sandboxed") && (a.policy?.unsandboxedCarry !== true || !carriesUnsandboxed(store, a.repo))) {
|
|
1505
|
+
return refuse("carrying release approvals when checks aren't sandboxed is off");
|
|
1506
|
+
}
|
|
1499
1507
|
if (a.policy === null)
|
|
1500
1508
|
return refuse("this Toolroll doesn't know the release's carry rules; update Toolroll first");
|
|
1501
1509
|
if (action === "deploy" || action === "merge") {
|
|
@@ -1522,3 +1530,12 @@ export function mainDebtProblem(store, repo, taskId) {
|
|
|
1522
1530
|
return null;
|
|
1523
1531
|
return `the nightly full check on main has ${open} open failure${open === 1 ? "" : "s"}, so a check made from a re-run of what failed can't deploy; a full check has to pass first`;
|
|
1524
1532
|
}
|
|
1533
|
+
/**
|
|
1534
|
+
* A deploy gate (docs/plans/chat-management.md §0a.5): a scope whose signed terms name a prepared candidate or carry
|
|
1535
|
+
* release terms. Both are in the scope digest and written at filing, before any release authority exists, so this is
|
|
1536
|
+
* read from the scope alone: a first approval and a fallback gate filed without --release are deploy gates too. From
|
|
1537
|
+
* chat it is approved only through the release card.
|
|
1538
|
+
*/
|
|
1539
|
+
export function deployGateScope(scope) {
|
|
1540
|
+
return (scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null;
|
|
1541
|
+
}
|
package/dist/scout.d.ts
CHANGED
|
@@ -131,6 +131,9 @@ export declare function readReportImage(folder: string, file: string): {
|
|
|
131
131
|
};
|
|
132
132
|
/** The shorten turn's ask: the fields over their limits, and the same report again within them. */
|
|
133
133
|
export declare function shortenReportBrief(over: readonly ReportProblem[], reportFile: string, structured: boolean): string;
|
|
134
|
+
/** Whether a stream event shows the session writing its plan-mode plan
|
|
135
|
+
* file (~/.claude/plans/…) or handing a plan to ExitPlanMode. */
|
|
136
|
+
export declare function touchesPlanFile(event: Record<string, unknown>): boolean;
|
|
134
137
|
/** A cited link without its secrets: credential-shaped query values and path parts become REDACTED, a fragment
|
|
135
138
|
* that carries one is dropped, and a link still showing one keeps only its origin. Unchanged when it has none. */
|
|
136
139
|
export declare function scrubUrl(text: string): {
|