toolroll 0.9.74 → 0.9.76
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 +80 -0
- package/dist/agent-fence.js +8 -0
- 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.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/batch-checks.js +3 -1
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +1 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +23 -9
- package/dist/contracts/console-api.d.ts +6 -1
- package/dist/contracts/console-api.js +11 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +3 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/discover.js +6 -1
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +42 -7
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- 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 +3 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +9 -1
- package/dist/planner.js +5 -1
- package/dist/project-knowledge.js +1 -1
- package/dist/project-learning.js +65 -7
- package/dist/project-tools.js +19 -1
- package/dist/pull-request-flow.js +4 -0
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.d.ts +24 -2
- package/dist/release-adapters.js +136 -10
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +64 -0
- package/dist/release-auto.d.ts +153 -0
- package/dist/release-auto.js +496 -0
- package/dist/release-cli.js +56 -2
- package/dist/release-coverage.js +1 -1
- package/dist/release.d.ts +105 -1
- package/dist/release.js +229 -9
- package/dist/repository-context.js +5 -2
- package/dist/result-follow-ups.js +23 -2
- package/dist/result-review.js +5 -1
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +44 -3
- package/dist/scout.js +5 -1
- package/dist/serve.js +162 -6
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-room.js +4 -1
- package/dist/server/chat.js +31 -23
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/pages.js +18 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +28 -24
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +30 -17
- package/dist/server/route-table.js +17 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +111 -31
- package/dist/server/task-views.js +8 -1
- package/dist/server/tasks.js +11 -2
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.js +52 -13
- package/dist/subagent-work.js +3 -0
- package/dist/task-base.js +1 -1
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +59 -5
- package/dist/teardown-git.js +1022 -90
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.76 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- **Releases can approve themselves when they're green.** A new per-project
|
|
6
|
+
setting, **Release approval: Ask me / Approve automatically when checks
|
|
7
|
+
pass**, is off by default. Turning it on takes your typed password;
|
|
8
|
+
turning it off takes one click and withdraws every automatic approval not
|
|
9
|
+
yet merged. When it's on, a release task first reads "Checking
|
|
10
|
+
automatically" (`auto-check`), which lets its release check run and
|
|
11
|
+
nothing else. Once GitHub CI and the release check both pass on the exact
|
|
12
|
+
candidate, it approves itself (`auto-green`) and the release continues as
|
|
13
|
+
after your yes. You're told, not asked.
|
|
14
|
+
- **What an automatic approval must prove.** Up to two failed attempts per
|
|
15
|
+
CI job, and two failed release-check builds on the same commit, are
|
|
16
|
+
tolerated as flakes. Past that it asks you. The change must touch only
|
|
17
|
+
allowed paths: product code and its tests, test files under `test/`,
|
|
18
|
+
docs, the changelog and a version-only `package.json` bump. Scripts, CI,
|
|
19
|
+
configuration, dependencies, policy files and test helpers (found from the
|
|
20
|
+
tests' own imports) need a person. CI is read again before the deploy, the
|
|
21
|
+
CLI switch, the merge and the tag. Anything not green stops the release,
|
|
22
|
+
withdraws the approval and tells you.
|
|
23
|
+
- **A red CI after the merge holds publishing.** No tag, npm publish or
|
|
24
|
+
Homebrew update happens until a person runs
|
|
25
|
+
`toolroll release publish-ok <task>` with their password.
|
|
26
|
+
- **Stricter CI verdicts on every release, yours too.** Only checks on the
|
|
27
|
+
exact head count. Required checks must come from GitHub Actions, so
|
|
28
|
+
another app's same-name success can't hide a failure. A newer run of the
|
|
29
|
+
same workflow supersedes its older runs on the commit.
|
|
30
|
+
- **Toolroll's own git on agent checkouts runs against a frozen view.**
|
|
31
|
+
Every git command the plane runs on a checkout an agent wrote (staging,
|
|
32
|
+
committing, cleanup, pool refresh, scouts, planners, coding sessions, page
|
|
33
|
+
reads) runs against a private copy of the repository's settings taken
|
|
34
|
+
once. Changes the agent makes to them afterwards are never read.
|
|
35
|
+
- **Git settings are denied by default.** Only audited config sections and
|
|
36
|
+
variables pass. Aliases, URL rewrites that reach remote helpers, empty
|
|
37
|
+
filter names and `hook.*` are refused. Writes allow no network transport.
|
|
38
|
+
Local git-lfs installs still work.
|
|
39
|
+
- **Writes are bound to the project's recorded repository.** A checkout
|
|
40
|
+
whose `.git` points anywhere else is refused. Before git writes through a
|
|
41
|
+
view, every path it could write is walked for links and hard links. Plane
|
|
42
|
+
git writes on one checkout run one at a time. On git before 2.48, a
|
|
43
|
+
checkout added through the view is pointed at its repository, not at the
|
|
44
|
+
view.
|
|
45
|
+
- **A git incident is recorded and you're notified.** A swap seen after git
|
|
46
|
+
ran is logged and raised as an attention notification. The notification
|
|
47
|
+
names the project by Toolroll's own record, never by a path the agent
|
|
48
|
+
chose.
|
|
49
|
+
- **`discover --dirty` reads repositories through the same hardened git.**
|
|
50
|
+
- Database: schema v136 (unchanged).
|
|
51
|
+
|
|
52
|
+
## 0.9.75 — 2026-10-27
|
|
53
|
+
|
|
54
|
+
- **Opening a page only shows it.** Pages no longer run checks, tidy up or
|
|
55
|
+
call out as you open them. Sign-ins, integrations, update checks, storage
|
|
56
|
+
and Codex's tools are checked in the background, and Models, Updates,
|
|
57
|
+
Storage, Pull requests, Tools, Project setup and the GitHub listing show
|
|
58
|
+
their last answer and when it was checked, with a **Check now** button to
|
|
59
|
+
ask again. On a coding session, **Review for shipping** and the changes
|
|
60
|
+
view are buttons, so a link preview or prefetch can't run them. Reading a
|
|
61
|
+
checkout an agent wrote never runs that checkout's own git settings.
|
|
62
|
+
- **Reading an agent's checkout can't be raced into running a filter.** Git
|
|
63
|
+
reads the checkout through a private copy of its settings taken once, so a
|
|
64
|
+
filter the agent adds to its git config and `.git/info/attributes` after
|
|
65
|
+
that copy never runs.
|
|
66
|
+
- **The task card never contradicts itself.** One answer to "did the
|
|
67
|
+
approved check pass" across the task page, the road, the Tasks list, the
|
|
68
|
+
result page and chat. A check whose saved log can't be read is never
|
|
69
|
+
shown as passed, and a check whose command was approved again since it ran
|
|
70
|
+
says so: "The check command changed since this check; run checks again."
|
|
71
|
+
- **Lead-filed runs nest under the lead on Agents → Live.** Runs your own
|
|
72
|
+
lead filed sit under its tile. Once its turn has gone, they stay grouped
|
|
73
|
+
under a quiet "Filed by lead chat" header. The lead's full screen lists
|
|
74
|
+
every task it filed. Only you see what your lead filed.
|
|
75
|
+
- **The lead passes asks to a helper instead of answering for them.** "Ask
|
|
76
|
+
Maya to …" now reaches Maya even when she seems to lack a tool or the
|
|
77
|
+
knowledge for it; she says so herself or asks you first.
|
|
78
|
+
- **A helper's "can't" reaches whoever asked.** When a helper can't do
|
|
79
|
+
something asked at its desk, its note (or its reason) goes back to the
|
|
80
|
+
person or agent who asked, instead of waiting in an inbox.
|
|
81
|
+
- Database: schema v136 (unchanged).
|
|
82
|
+
|
|
3
83
|
## 0.9.74 — 2026-10-27
|
|
4
84
|
|
|
5
85
|
- **Agents → Live: every agent and subagent at a glance.** A read-only grid
|
package/dist/agent-fence.js
CHANGED
|
@@ -32,6 +32,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
32
32
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
33
33
|
import { readMonitoring } from "./monitoring-settings.js";
|
|
34
34
|
import { existingFolders } from "./names.js";
|
|
35
|
+
import { ensureGitViewsRoot, gitViewsRoot } from "./git-views.js";
|
|
35
36
|
import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
|
|
36
37
|
import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
|
|
37
38
|
import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
|
|
@@ -175,6 +176,13 @@ export function agentFence(options) {
|
|
|
175
176
|
fence.add(join(state, entry));
|
|
176
177
|
}
|
|
177
178
|
}
|
|
179
|
+
// The plane's private views of repositories for its own git (git-views.ts): made first, so no build is the one to
|
|
180
|
+
// create them, and denied by name whether or not that worked (a sandbox that can't cover the path refuses to launch).
|
|
181
|
+
try {
|
|
182
|
+
ensureGitViewsRoot(home);
|
|
183
|
+
}
|
|
184
|
+
catch { /* fenced by name below either way */ }
|
|
185
|
+
fence.add(gitViewsRoot(home));
|
|
178
186
|
for (const secrets of existingFolders(home, { dot: true }))
|
|
179
187
|
fence.add(secrets);
|
|
180
188
|
// v104: the folder the audit stream writes to holds the whole instance's history.
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* An allowlist, never a deny-list: a person's yes (`password`, `chat`) stands unless a machine made that seal; a mode
|
|
6
6
|
* or a flow grant stands while its signature does; a standing order's firing (`schedule`, or an old firing stored as
|
|
7
|
-
* `password`) stands while its approver can still approve in the project
|
|
7
|
+
* `password`) stands while its approver can still approve in the project; an automatic release seal (`auto-check`,
|
|
8
|
+
* `auto-green`: docs/plans/auto-approve-green.md) stands on its own evidence row while the project's setting is on and
|
|
9
|
+
* the person who turned it on can still approve there. Everything else — the lead (`lead`, which
|
|
8
10
|
* phase 1 never lets stand), a null basis, a word this code doesn't write — stands on nothing.
|
|
9
11
|
*
|
|
10
12
|
* The seal's origin is read from the acts recorded AT the seal's own instant, exactly as scope.ts sealOriginOf reads
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
*/
|
|
13
15
|
import { grantStandsSql } from "./flow-unattended.js";
|
|
14
16
|
import { accountReachesSql } from "./project-access.js";
|
|
17
|
+
import { autoSealSql } from "./release-authority.js";
|
|
15
18
|
/** The proofs a person gives (src/person-proof.ts), as an SQL list. */
|
|
16
19
|
const PERSON_PROOFS = "('password-now', 'sso-now', 'fresh-password', 'fresh-sso', 'session', 'cli-login', 'chat', 'passkey')";
|
|
17
20
|
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
@@ -25,7 +28,8 @@ export const authorityStandsSql = (scope, repo, now) => `COALESCE((((COALESCE(${
|
|
|
25
28
|
WHERE om.repo = ${repo} AND om.revoked_at IS NULL AND om.absolute_expiry > ${now} AND om.digest = ${scope}.mode_digest
|
|
26
29
|
AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", repo)}))
|
|
27
30
|
OR (${scope}.approval_basis = 'grant' AND ${grantStandsSql(`${scope}.mode_digest`, now, repo)})
|
|
28
|
-
OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)})
|
|
31
|
+
OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)})
|
|
32
|
+
OR (${scope}.approval_basis IN ('auto-check', 'auto-green') AND ${autoSealSql(scope, repo)} AND ${approverStandsSql(scope, repo)}))
|
|
29
33
|
AND ${releaseStandsSql(scope, now)}), 0)`;
|
|
30
34
|
/** SQL: a `carried` seal stands only on its own carry row — this task, this approved digest, this candidate, and the
|
|
31
35
|
* reference `<authority>:<carry>` the carry transaction wrote (release-once.md). */
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type Html } from "./html.js";
|
|
8
8
|
import type { ApprovalRules } from "./approval-policy.js";
|
|
9
|
+
/** The rules' style, and the same for the Releases section under its own class (so a page holds one `.approval-rules`
|
|
10
|
+
* form, not two). */
|
|
9
11
|
export declare const APPROVAL_RULES_CSS: string;
|
|
10
12
|
export type ApprovalRulesView = {
|
|
11
13
|
repo: string;
|
|
@@ -20,6 +22,15 @@ export type ApprovalRulesView = {
|
|
|
20
22
|
};
|
|
21
23
|
/** The rules in plain words, for anyone who can see the project. */
|
|
22
24
|
export declare function rulesSummary(rules: ApprovalRules): string;
|
|
25
|
+
/** Settings → Approval rules → Releases (docs/plans/auto-approve-green.md): who approves this project's releases. */
|
|
26
|
+
export type ReleaseApprovalView = {
|
|
27
|
+
repo: string;
|
|
28
|
+
auto: boolean;
|
|
29
|
+
setBy: string | null;
|
|
30
|
+
at: string | null;
|
|
31
|
+
canChange: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function releaseApprovalHtml(view: ReleaseApprovalView): Html;
|
|
23
34
|
export declare function approvalRulesHtml(view: ApprovalRulesView, notice: {
|
|
24
35
|
said?: string | null;
|
|
25
36
|
problem?: string | null;
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* ledger, before → after.
|
|
6
6
|
*/
|
|
7
7
|
import { html, postForm, reauth } from "./html.js";
|
|
8
|
-
|
|
8
|
+
const RULES_CSS = `.approval-rules{max-width:720px;min-width:0}.approval-rules fieldset{border:0;padding:0;margin:18px 0 0;min-width:0}` +
|
|
9
9
|
`.approval-rules legend{font-weight:600;font-size:.875rem;margin-bottom:8px}.approval-rules .choice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;align-items:start;margin:0 0 10px;font-weight:400}` +
|
|
10
10
|
`.approval-rules .choice input{margin:.2rem 0 0}.approval-rules .choice strong{display:block;font-size:.875rem;font-weight:500}.approval-rules .choice small{display:block;color:var(--muted-foreground);font-size:.8125rem;margin-top:2px}` +
|
|
11
11
|
`.approval-rules textarea{min-height:88px;font-family:var(--font-mono);font-size:.8125rem}.approval-rules .paths{margin:4px 0 0 26px}` +
|
|
12
12
|
`.approval-rules .current{margin:0 0 4px}.approval-rules .step-up{margin-top:16px}.approval-rules button[type=submit]{margin-top:14px}`;
|
|
13
|
+
/** The rules' style, and the same for the Releases section under its own class (so a page holds one `.approval-rules`
|
|
14
|
+
* form, not two). */
|
|
15
|
+
export const APPROVAL_RULES_CSS = RULES_CSS + RULES_CSS.replaceAll(".approval-rules", ".release-approval");
|
|
13
16
|
/** The rules in plain words, for anyone who can see the project. */
|
|
14
17
|
export function rulesSummary(rules) {
|
|
15
18
|
const parts = [];
|
|
@@ -21,6 +24,15 @@ export function rulesSummary(rules) {
|
|
|
21
24
|
parts.push(`Work touching ${rules.protectedPaths.join(", ")} is protected: two people approve it, never a subagent or an operating mode.`);
|
|
22
25
|
return parts.length === 0 ? "No approval rules: anyone who can approve on this project can approve its work, including their own." : parts.join(" ");
|
|
23
26
|
}
|
|
27
|
+
export function releaseApprovalHtml(view) {
|
|
28
|
+
const since = view.setBy === null ? "" : html ` <span class="meta">Changed by ${view.setBy} on ${(view.at ?? "").slice(0, 10)}.</span>`;
|
|
29
|
+
const current = html `<p class="current">${view.auto ? "Releases approve themselves once the pull request's CI and the release check pass on the exact commit. Anything red still asks you." : "Every release waits for your approval."}${since}</p>`;
|
|
30
|
+
const heading = html `<h2>Release approval</h2>`;
|
|
31
|
+
if (!view.canChange)
|
|
32
|
+
return html `<section class="release-approval">${heading}${current}<p class="meta">An approver of this project sets this.</p></section>`;
|
|
33
|
+
const choice = (value, title, hint) => html `<label class="choice"><input type="radio" name="action" value="${value}"${(view.auto ? "auto" : "ask") === value ? html ` checked` : ""}><span><strong>${title}</strong><small>${hint}</small></span></label>`;
|
|
34
|
+
return html `<section class="release-approval">${heading}${current}${postForm("/settings/release-approval", html `<fieldset aria-label="Release approval">${choice("ask", "Ask me", "A release deploys only after you approve it.")}${choice("auto", "Approve automatically when checks pass", "You're told when a release approves itself. A failed check, red CI or a CI failure after the deploy still stops and asks you.")}</fieldset><div class="step-up"><label>Your Toolroll password (to approve automatically)<input type="password" name="password" autocomplete="current-password"></label></div><button type="submit">Save</button>`, { hidden: { repo: view.repo } })}</section>`;
|
|
35
|
+
}
|
|
24
36
|
export function approvalRulesHtml(view, notice) {
|
|
25
37
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
26
38
|
const changed = view.rules.updatedBy === null ? "" : html ` <span class="meta">Changed by ${view.rules.updatedBy} on ${(view.rules.updatedAt ?? "").slice(0, 10)}.</span>`;
|
package/dist/assignment.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type AssignmentChecks = {
|
|
|
28
28
|
batch?: ChecksBatch | null;
|
|
29
29
|
/** Checks Off: the passing full release check that contains this commit (release-coverage.ts), read now, never sealed. */
|
|
30
30
|
release?: ReleaseCoverage | null;
|
|
31
|
+
/** Why a saved check can't be read, when it is the project's check command approved again since it ran (its files
|
|
32
|
+
* are intact; running checks again settles it). Presentation only, never sealed. */
|
|
33
|
+
cause?: "command-changed" | null;
|
|
31
34
|
};
|
|
32
35
|
export type AssignmentReceipt = {
|
|
33
36
|
digest: string;
|
package/dist/assignment.js
CHANGED
|
@@ -21,7 +21,7 @@ import { requiredCheckCommandFor, runCheckLevel } from "./check-levels.js";
|
|
|
21
21
|
import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
|
|
22
22
|
import { buildReviewOf, findingWords } from "./review-switch.js";
|
|
23
23
|
import { NEEDS, WAITS, processNeedOf, resultHoldUpSentence } from "./needs-you.js";
|
|
24
|
-
import { assignmentStageOf, checkBackingOf, completionBlockersOf, requirementWordOf } from "./task-status.js";
|
|
24
|
+
import { assignmentStageOf, checkBackingOf, completionBlockersOf, receiptChecksOf, requirementWordOf } from "./task-status.js";
|
|
25
25
|
import { releaseCoverageOf } from "./release-coverage.js";
|
|
26
26
|
import { leadClaimOf } from "./lead-voice.js";
|
|
27
27
|
/** Status-first handoff for routine reads. Fetch get_assignment only when
|
|
@@ -53,7 +53,7 @@ function olderStoreSafe(read, absent) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
/** A check as the receipt digest seals it: presentation-only fields (check-levels.ts) left out. */
|
|
56
|
-
const sealedChecks = ({ level: _level, running: _running, batch: _batch, release: _release, ...proof }) => proof;
|
|
56
|
+
const sealedChecks = ({ level: _level, running: _running, batch: _batch, release: _release, cause: _cause, ...proof }) => proof;
|
|
57
57
|
const OWNER_ACTION = "assignment claimed";
|
|
58
58
|
const CHECK_ACTION = COMPLETION_ACTION;
|
|
59
59
|
const actorOf = (owner) => `${owner.kind}:${owner.id}`;
|
|
@@ -87,7 +87,7 @@ function ownerOf(store, rootId, repo) {
|
|
|
87
87
|
// Report the recorded check outcome, never turn a model verdict into a check.
|
|
88
88
|
// Follow-up checks on the same commit (Run checks) upgrade a pass and show a failure.
|
|
89
89
|
export function assignmentChecksForRun(store, root, runId, now) {
|
|
90
|
-
const own = buildChecks(store, root, runId);
|
|
90
|
+
const own = recordedFailureStands(store, runId, buildChecks(store, root, runId));
|
|
91
91
|
const run = store.getRun(runId);
|
|
92
92
|
const followUps = followUpChecksOf(store, runId, now, root);
|
|
93
93
|
if (followUps.length === 0)
|
|
@@ -106,6 +106,18 @@ export function assignmentChecksForRun(store, root, runId, now) {
|
|
|
106
106
|
...(changed ? { status: read.status, level: read.level, exitCode: read.exitCode, logArtifactId: latest?.logArtifactId ?? own.logArtifactId,
|
|
107
107
|
detail: read.status === "passed" ? `${words} passed.` : `${words} failed (exit ${read.exitCode}).` } : {}) };
|
|
108
108
|
}
|
|
109
|
+
/** A failure the builder recorded (run_check) stands when its saved receipt can't be read now or says nothing ran:
|
|
110
|
+
* fail closed, every surface reads the same known failure and its exit code (task-status.ts approvedCheckOf). */
|
|
111
|
+
function recordedFailureStands(store, runId, own) {
|
|
112
|
+
if (own.status !== "unavailable" && own.status !== "not-run")
|
|
113
|
+
return own;
|
|
114
|
+
const recorded = olderStoreSafe(() => store.runCheckFor(runId), null);
|
|
115
|
+
if (recorded?.status !== "failed")
|
|
116
|
+
return own;
|
|
117
|
+
return { ...own, status: "failed", exitCode: recorded.exitCode,
|
|
118
|
+
detail: `${own.level === "quick" ? "Quick checks" : "Checks"} failed${recorded.exitCode === null ? "" : ` (exit ${recorded.exitCode})`}. ${own.detail}` };
|
|
119
|
+
}
|
|
120
|
+
const COMMAND_MISSING_OR_CHANGED = "The original approved verification command is missing or changed.";
|
|
109
121
|
function buildChecks(store, root, runId) {
|
|
110
122
|
const level = runCheckLevel(store, runId);
|
|
111
123
|
const unavailable = (detail) => ({ status: "unavailable", exitCode: null, command: null, logArtifactId: null, detail, level });
|
|
@@ -113,8 +125,13 @@ function buildChecks(store, root, runId) {
|
|
|
113
125
|
return unavailable("Saved checks are unavailable.");
|
|
114
126
|
try {
|
|
115
127
|
const gate = verificationEvidence(store, root, runId);
|
|
116
|
-
if (!gate.ok)
|
|
117
|
-
|
|
128
|
+
if (!gate.ok) {
|
|
129
|
+
// The approved command changed since this check ran: nothing is missing, the check just no longer binds to it.
|
|
130
|
+
const run = store.getRun(runId), repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
131
|
+
const live = repo === null ? null : store.liveVerifyCommand(repo);
|
|
132
|
+
const changed = gate.problem === COMMAND_MISSING_OR_CHANGED && live !== null && run !== null && live.approvedAt > run.startedAt;
|
|
133
|
+
return changed ? { ...unavailable(gate.problem), cause: "command-changed" } : unavailable(gate.problem);
|
|
134
|
+
}
|
|
118
135
|
const receipt = gate.bytes === null ? null : JSON.parse(gate.bytes);
|
|
119
136
|
if (receipt === null)
|
|
120
137
|
return { ...unavailable(level === "off" ? "Checks were off for this build." : "No machine check is recorded."), status: "not-run" };
|
|
@@ -390,10 +407,12 @@ function readinessOf(store, receipt, repo, review) {
|
|
|
390
407
|
const high = review === null || review.sentBackAs !== null || review.state !== "reviewed" ? 0 : review.high.length;
|
|
391
408
|
const checkRequired = olderStoreSafe(() => requiredCheckCommandFor(store, repo, receipt.taskId, receipt.checks.level), null) !== null;
|
|
392
409
|
const matrix = receipt.proof?.matrix ?? [];
|
|
393
|
-
|
|
394
|
-
const
|
|
410
|
+
// The approved check as every surface reads it: a recorded pass the saved receipt can't show doesn't count.
|
|
411
|
+
const checks = receiptChecksOf(receipt);
|
|
412
|
+
const backing = checkBackingOf(checks);
|
|
413
|
+
const verdict = shownVerdictOf(receipt.proof?.verdict ?? null, { checkFailed: !report && checks?.status === "failed", highFindings: high,
|
|
395
414
|
reviewPending: review?.state === "pending", criteriaPassed: matrix.every(row => requirementWordOf(row, null, backing) === "Met") });
|
|
396
|
-
const blockers = completionBlockersOf({ report, checks
|
|
415
|
+
const blockers = completionBlockersOf({ report, checks, checkRequired, matrix, verdict: receipt.proof?.verdict ?? null,
|
|
397
416
|
accepted: receipt.proofAcceptance !== null, high });
|
|
398
417
|
return { verdict, checksOff: !report && receipt.checks.level === "off", blockers };
|
|
399
418
|
}
|
package/dist/batch-checks.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { run as execRun } from "./exec.js";
|
|
6
|
+
import { hardenedGit } from "./teardown-git.js";
|
|
6
7
|
import { approvedCommandShell, SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
7
8
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
8
9
|
import { boundStreamHeadTail, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
@@ -86,7 +87,8 @@ function finishOne(store, one, repo, task, status, detail, at) {
|
|
|
86
87
|
async function checkCohort(store, root, repo, cohort, deps) {
|
|
87
88
|
const now = deps.now ?? (() => new Date());
|
|
88
89
|
const runner = deps.runner ?? execRun;
|
|
89
|
-
|
|
90
|
+
// The project's repository and checkouts of builds' commits, both agent-writable: the hardened git (teardown-git.ts).
|
|
91
|
+
const git = deps.git ?? hardenedGit(execRun);
|
|
90
92
|
const command = store.liveVerifyCommand(repo);
|
|
91
93
|
if (command === null)
|
|
92
94
|
return 0;
|