toolroll 0.9.71 → 0.9.72
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 +79 -0
- package/dist/agent-fence.d.ts +5 -2
- package/dist/agent-fence.js +5 -2
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/builder.js +48 -6
- package/dist/chat-decide.js +17 -2
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +204 -0
- package/dist/claim.js +1 -1
- package/dist/commit-extract.d.ts +94 -0
- package/dist/commit-extract.js +436 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +60 -0
- package/dist/contracts/release-terms.js +70 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/flow-engine.js +5 -1
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flows-ui.js +7 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/project-delete-ui.js +1 -1
- package/dist/proof.js +2 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +8 -0
- package/dist/release-adapters.js +212 -15
- package/dist/release-authority.d.ts +112 -0
- package/dist/release-authority.js +126 -0
- package/dist/release-carry.d.ts +233 -0
- package/dist/release-carry.js +645 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +124 -5
- package/dist/release-coverage.js +3 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +131 -1
- package/dist/release.js +562 -244
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +16 -5
- package/dist/sandbox-runtime.js +19 -6
- package/dist/scope.d.ts +115 -5
- package/dist/scope.js +219 -7
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/store.d.ts +227 -5
- package/dist/store.js +727 -37
- package/dist/task-outcome-cli.js +7 -3
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.72 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
- **One approval per release.** A person approves a release once. Later
|
|
6
|
+
commits on the same pull request carry that approval only when every change
|
|
7
|
+
since is unit tests, journey scripts, Markdown under docs/, the README or
|
|
8
|
+
the changelog, within 400 changed lines, 20 files and 800 lines of churn,
|
|
9
|
+
and the production build is byte-for-byte the approved one. Anything else
|
|
10
|
+
asks again, and so does any change after the deploy has started. The
|
|
11
|
+
approval sheet states the release and what asks again; the release summary
|
|
12
|
+
and the squash message list every commit that landed after the approval.
|
|
13
|
+
**Stop carrying later commits** on the task page (or `toolroll release
|
|
14
|
+
revoke`) ends it, and `toolroll release ask-every-commit on` makes a
|
|
15
|
+
project's releases ask for every commit. A release approval expires after
|
|
16
|
+
3 days.
|
|
17
|
+
- **Re-checks of a release re-run only what failed.** On the second and later
|
|
18
|
+
release checks, only the failed unit files, journey groups and upgrade
|
|
19
|
+
versions run again, with every test changed since and a short sanity pass.
|
|
20
|
+
A check that can't prove it covered everything counts as failed, and a
|
|
21
|
+
test that disappears or is newly skipped stops carrying.
|
|
22
|
+
- **A nightly full check of main.** `toolroll release nightly --repo <path>`
|
|
23
|
+
sets up a daily full check of main (03:00 local by default). A failure
|
|
24
|
+
becomes an attention notice naming what failed, holds re-checks that run
|
|
25
|
+
only part of the suite until a full check passes it again, and a nightly
|
|
26
|
+
that hasn't run for 26 hours says so.
|
|
27
|
+
- **Sandboxed checks can start Chromium on macOS.** Playwright's default
|
|
28
|
+
browser (its Chromium headless shell) now starts inside the check sandbox:
|
|
29
|
+
the sandbox may register and look up Chromium's port rendezvous service,
|
|
30
|
+
`org.chromium.Chromium.MachPortRendezvousServer.<pid>`, and nothing else.
|
|
31
|
+
Builds, flows and tool servers don't get this; SECURITY.md says what a
|
|
32
|
+
hostile check could do with it. A project set to **Not sandboxed** only for
|
|
33
|
+
a headless browser check can go back to **Sandboxed** in Settings → Project
|
|
34
|
+
→ Check access. Chrome for Testing (`channel: "chromium"`, headed runs)
|
|
35
|
+
still needs **Not sandboxed**.
|
|
36
|
+
- **Checks with sign-ins no longer reach your keychain.** Toolroll reads
|
|
37
|
+
Claude Code's and gh's sign-ins from the keychain itself, outside the
|
|
38
|
+
sandbox, and hands the check just those tokens in its environment. A check
|
|
39
|
+
gets Claude's current access token and can't renew it.
|
|
40
|
+
- **A wrong password on an approval says "That password didn't match."**
|
|
41
|
+
instead of "not approved: not-an-approver". Someone who isn't an approver
|
|
42
|
+
gets the same answer.
|
|
43
|
+
- **Closing a live terminal signals only its own processes.** It stops them
|
|
44
|
+
before it kills them, so a child one starts at that moment can't slip out
|
|
45
|
+
from under the close; each signal goes only to a process still the one it
|
|
46
|
+
saw (same pid, same start); and after the program ends, its terminal's
|
|
47
|
+
name no longer counts as proof, since the device may already be someone
|
|
48
|
+
else's. A process it can't prove is its own is never signalled, and the
|
|
49
|
+
close says it's uncertain (the worktree stays).
|
|
50
|
+
- **Only a person approves work.** The lead's token used to approve a task's
|
|
51
|
+
scope and be recorded as its owner's password approval, including on a
|
|
52
|
+
release. Now `task approve` (and `task repair --yes`) with a lead token, or
|
|
53
|
+
any approval made with a remote API token, is refused: "Only a person can
|
|
54
|
+
approve this." Approve on the task page or from your paired chat
|
|
55
|
+
(docs/plans/approval-policy.md, phase 1; the setting that lets the lead
|
|
56
|
+
approve, recorded as the lead's, comes next).
|
|
57
|
+
- **Only a person merges by hand.** `task merge` (including `--anyway`) and
|
|
58
|
+
the merge button refuse a lead or API token. A flow's Pull request zone
|
|
59
|
+
merges only after a person's recorded decision; a subagent's approval, or
|
|
60
|
+
a decision from before decisions recorded who made them, waits.
|
|
61
|
+
- **Protected work counts only people.** A vote a machine credential left, or
|
|
62
|
+
one with no proof on record, doesn't count as one of the two people, and a
|
|
63
|
+
lead or API token's completion is never the second person. A person can
|
|
64
|
+
mark an already-complete result complete again to release a held push.
|
|
65
|
+
- **Releases and deployments need a person's approval with its proof on
|
|
66
|
+
record.** The release gate and `deploy-browser.mjs` (every phase, resumed
|
|
67
|
+
ones included) refuse an approval a lead token made, a mode, a flow, a
|
|
68
|
+
standing order, or an approval with no recorded proof. `toolroll release` proves the
|
|
69
|
+
candidate in its own runtime, and that the checkout is exactly that commit,
|
|
70
|
+
before it runs anything from the candidate's checkout. Paired-chat
|
|
71
|
+
approvals now record their own proof, so they still count. Update the
|
|
72
|
+
plane before running `toolroll release`: an older plane can't say who
|
|
73
|
+
approved, and the release waits.
|
|
74
|
+
- Approvals a lead token made before this change are read for what they were:
|
|
75
|
+
they no longer dispatch, and the task asks for a person's approval. Seals
|
|
76
|
+
with no recorded authority stand on nothing. A standing order's firing is
|
|
77
|
+
approved by its own authority (`schedule`), which stands only while its
|
|
78
|
+
approver can still approve in the project.
|
|
79
|
+
- Schema v134 and v135 only add columns and tables; `toolroll update` runs
|
|
80
|
+
them in place.
|
|
81
|
+
|
|
3
82
|
## 0.9.71 — 2026-10-25
|
|
4
83
|
|
|
5
84
|
- **Sandboxed launches can open a terminal of their own on macOS.** A check
|
package/dist/agent-fence.d.ts
CHANGED
|
@@ -93,9 +93,12 @@ export type FenceGrants = {
|
|
|
93
93
|
/** macOS: the project's sealed mach services beyond the runtime's baseline (its `allowMachLookup`). */
|
|
94
94
|
machLookup?: string[];
|
|
95
95
|
/** macOS: the keychain's services (securityd), denied to every launch without it (sandbox-runtime.ts
|
|
96
|
-
* SEATBELT_NO_KEYCHAIN). Only fenceFor gives it, to a build's agent
|
|
97
|
-
*
|
|
96
|
+
* SEATBELT_NO_KEYCHAIN). Only fenceFor gives it, to a build's agent. Never a check's (one with sign-ins gets those in
|
|
97
|
+
* its environment, invoke.ts keychainSignIns), never a tool server's (serverGrantsOf). */
|
|
98
98
|
keychain?: true;
|
|
99
|
+
/** macOS: a project check's launch, whose Chromium may start (sandbox-runtime.ts SEATBELT_CHROMIUM). Only fenceFor
|
|
100
|
+
* sets it, only for kind "check". */
|
|
101
|
+
browser?: true;
|
|
99
102
|
/** A `sign-ins` check's exact sign-in files, read again inside the home denies (check-access.ts, signInGrants). */
|
|
100
103
|
signIns?: SignInGrants;
|
|
101
104
|
/** Never written, wherever the launch writes (homeWriteDenies): what runs for the operator next or signs them in. */
|
package/dist/agent-fence.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* checkout writable; other tasks' checkouts read, never written; the project's read-deny list; under policy v1 (an
|
|
13
13
|
* approval sealed before 0.9.70) the home configuration denied by name; a sign-ins check's exact files read again; the
|
|
14
14
|
* run's proxy as its only way out when its network is confined. On macOS every launch also gets a pseudo-terminal of its
|
|
15
|
-
* own and no other terminal,
|
|
15
|
+
* own and no other terminal, a project check's launch Chromium's own service, and only a build's agent the keychain
|
|
16
16
|
* (sandbox-runtime.ts seatbeltRulesFor, static rules).
|
|
17
17
|
*
|
|
18
18
|
* Codex (every platform) fences its own agent's commands with its own sandbox: a permission profile with the same
|
|
@@ -985,6 +985,7 @@ export function srtTermsOf(fence, grants = NO_GRANTS) {
|
|
|
985
985
|
readConfig,
|
|
986
986
|
writeConfig: { allowOnly: [...new Set(allowOnly)].sort(), denyWithinAllow: [...denyWrite].sort() },
|
|
987
987
|
allowMachLookup: [...grants.machLookup ?? [], ...grants.signIns?.mach ?? []],
|
|
988
|
+
...(grants.browser === true ? { browser: true } : {}),
|
|
988
989
|
...(grants.keychain === true ? { keychain: true } : {}),
|
|
989
990
|
network: network === undefined || network.mode === "open" ? { kind: "open" } : {
|
|
990
991
|
kind: "proxy", httpPort: network.proxyPort, ...(network.brokerPorts[0] === undefined ? {} : { socksPort: network.brokerPorts[0] }),
|
|
@@ -1088,6 +1089,8 @@ export function fenceFor(kind, ctx) {
|
|
|
1088
1089
|
...(written === null ? {} : { writes: written.writes, readOnly: [...new Set([...made.readOnly, ...written.readOnly])].sort() }),
|
|
1089
1090
|
...(ctx.network === undefined ? {} : { network: ctx.network }),
|
|
1090
1091
|
...(ctx.machLookup === undefined || ctx.machLookup.length === 0 ? {} : { machLookup: [...ctx.machLookup] }),
|
|
1092
|
+
// A project's checks may drive a browser (Playwright's Chromium); builds, flows and tool servers don't get it.
|
|
1093
|
+
...(kind === "check" ? { browser: true } : {}),
|
|
1091
1094
|
// A build's agent signs in from the keychain (Claude Code does); nothing else it launches reaches it.
|
|
1092
1095
|
...(kind === "build" ? { keychain: true } : {}),
|
|
1093
1096
|
// Under a write allowlist these are out of reach already; where the launch writes anywhere, they are denied by name.
|
|
@@ -1232,7 +1235,7 @@ export function homeWriteDenies(home, binding, provider, platform = process.plat
|
|
|
1232
1235
|
* where it always has), and never the build's own checkout or launch folder read back (nothing a build wrote loads in a
|
|
1233
1236
|
* process that holds a tool's secrets). */
|
|
1234
1237
|
export function serverGrantsOf(grants) {
|
|
1235
|
-
const { network: _agentOnly, writes: _agentWrites, keychain: _agentKeychain, ...rest } = grants;
|
|
1238
|
+
const { network: _agentOnly, writes: _agentWrites, keychain: _agentKeychain, browser: _checkOnly, ...rest } = grants;
|
|
1236
1239
|
return {
|
|
1237
1240
|
...rest,
|
|
1238
1241
|
...(rest.pool === undefined ? {} : { pool: { ...rest.pool, own: null } }),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
2
|
+
* expression, used exactly three times: the mode and grant clauses, and the release's expiry). It never asks whether the row is approved.
|
|
3
|
+
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
4
|
+
export declare const authorityStandsSql: (scope: string, repo: string, now: string) => string;
|
|
5
|
+
/** SQL: a `carried` seal stands only on its own carry row — this task, this approved digest, this candidate, and the
|
|
6
|
+
* reference `<authority>:<carry>` the carry transaction wrote (release-once.md). */
|
|
7
|
+
export declare const carrySealSql: (scope: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* SQL: a task that belongs to a release authority (its anchor, or a carried commit) dispatches only as the current
|
|
10
|
+
* generation's tip while the release is open and unexpired, with the anchor's approver still an active approver who
|
|
11
|
+
* can reach the project and the anchor's approval on its exact sealed digest. A task that belongs to no release passes.
|
|
12
|
+
* The claim and build belts read the whole predicate (scope.ts liveReleaseOf) besides.
|
|
13
|
+
*/
|
|
14
|
+
export declare const releaseStandsSql: (scope: string, now: string) => string;
|
|
15
|
+
/** SQL: a machine made this seal — any proof at the seal's instant that isn't a person's (whatever its detail says,
|
|
16
|
+
* parsed or not: fail closed), or the lead's own approval act then. */
|
|
17
|
+
export declare const machineSealSql: (scope: string) => string;
|
|
18
|
+
/** SQL: a person's proof at the seal's instant for these exact bytes, its detail parsed (the versioned JSON record,
|
|
19
|
+
* whitespace and all, or the legacy text). */
|
|
20
|
+
export declare const personSealSql: (scope: string) => string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which approval authorities stand, as ONE SQL predicate every dispatch belt shares (docs/plans/approval-policy.md
|
|
3
|
+
* §1.5): the queue's dispatch query, the work index, and the claim and build belts (`Store.modeApprovalLive`).
|
|
4
|
+
*
|
|
5
|
+
* An allowlist, never a deny-list: a person's yes (`password`, `chat`) stands unless a machine made that seal; a mode
|
|
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. Everything else — the lead (`lead`, which
|
|
8
|
+
* phase 1 never lets stand), a null basis, a word this code doesn't write — stands on nothing.
|
|
9
|
+
*
|
|
10
|
+
* The seal's origin is read from the acts recorded AT the seal's own instant, exactly as scope.ts sealOriginOf reads
|
|
11
|
+
* it: an older proof for the same bytes never vouches for a later seal.
|
|
12
|
+
*/
|
|
13
|
+
import { grantStandsSql } from "./flow-unattended.js";
|
|
14
|
+
import { accountReachesSql } from "./project-access.js";
|
|
15
|
+
/** The proofs a person gives (src/person-proof.ts), as an SQL list. */
|
|
16
|
+
const PERSON_PROOFS = "('password-now', 'sso-now', 'fresh-password', 'fresh-sso', 'session', 'cli-login', 'chat', 'passkey')";
|
|
17
|
+
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
18
|
+
* expression, used exactly three times: the mode and grant clauses, and the release's expiry). It never asks whether the row is approved.
|
|
19
|
+
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
20
|
+
export const authorityStandsSql = (scope, repo, now) => `COALESCE((((COALESCE(${scope}.approval_basis, '') IN ('password', 'chat') AND NOT ${machineSealSql(scope)}
|
|
21
|
+
AND (${personSealSql(scope)} OR NOT ${automationSql(scope)} OR ${approverStandsSql(scope, repo)}))
|
|
22
|
+
OR (${scope}.approval_basis = 'carried' AND ${carrySealSql(scope)})
|
|
23
|
+
OR (${scope}.approval_basis = 'mode' AND EXISTS (SELECT 1 FROM operating_mode om
|
|
24
|
+
JOIN approver signer ON signer.name = om.signed_by
|
|
25
|
+
WHERE om.repo = ${repo} AND om.revoked_at IS NULL AND om.absolute_expiry > ${now} AND om.digest = ${scope}.mode_digest
|
|
26
|
+
AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", repo)}))
|
|
27
|
+
OR (${scope}.approval_basis = 'grant' AND ${grantStandsSql(`${scope}.mode_digest`, now, repo)})
|
|
28
|
+
OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)}))
|
|
29
|
+
AND ${releaseStandsSql(scope, now)}), 0)`;
|
|
30
|
+
/** SQL: a `carried` seal stands only on its own carry row — this task, this approved digest, this candidate, and the
|
|
31
|
+
* reference `<authority>:<carry>` the carry transaction wrote (release-once.md). */
|
|
32
|
+
export const carrySealSql = (scope) => `EXISTS (SELECT 1 FROM release_carry rc2 JOIN release_authority ra2 ON ra2.id = rc2.authority
|
|
33
|
+
WHERE rc2.task_id = ${scope}.task_id AND rc2.scope_digest = ${scope}.approved_digest AND rc2.to_sha = ${scope}.candidate
|
|
34
|
+
AND ${scope}.mode_digest = ra2.id || ':' || rc2.id)`;
|
|
35
|
+
/**
|
|
36
|
+
* SQL: a task that belongs to a release authority (its anchor, or a carried commit) dispatches only as the current
|
|
37
|
+
* generation's tip while the release is open and unexpired, with the anchor's approver still an active approver who
|
|
38
|
+
* can reach the project and the anchor's approval on its exact sealed digest. A task that belongs to no release passes.
|
|
39
|
+
* The claim and build belts read the whole predicate (scope.ts liveReleaseOf) besides.
|
|
40
|
+
*/
|
|
41
|
+
export const releaseStandsSql = (scope, now) => `((NOT EXISTS (SELECT 1 FROM release_authority rx WHERE rx.anchor_task = ${scope}.task_id)
|
|
42
|
+
AND NOT EXISTS (SELECT 1 FROM release_carry cx WHERE cx.task_id = ${scope}.task_id))
|
|
43
|
+
OR EXISTS (SELECT 1 FROM release_authority ra WHERE ra.tip_task = ${scope}.task_id AND ra.state = 'open' AND ra.expires_at > ${now}
|
|
44
|
+
AND (ra.anchor_task = ${scope}.task_id OR EXISTS (SELECT 1 FROM release_carry rc WHERE rc.authority = ra.id AND rc.task_id = ${scope}.task_id))
|
|
45
|
+
AND NOT EXISTS (SELECT 1 FROM release_authority rn WHERE rn.repo = ra.repo AND rn.version = ra.version AND rn.pr = ra.pr AND rn.generation > ra.generation)
|
|
46
|
+
AND EXISTS (SELECT 1 FROM task_scope an JOIN approver ap ON ap.name = an.approved_by
|
|
47
|
+
WHERE an.task_id = ra.anchor_task AND an.approved_digest = ra.anchor_digest AND an.digest = ra.anchor_digest
|
|
48
|
+
AND ap.revoked_at IS NULL AND ap.role = 'approver' AND ${accountReachesSql("ap", "ra.repo")})))`;
|
|
49
|
+
/** SQL: a machine made this seal — any proof at the seal's instant that isn't a person's (whatever its detail says,
|
|
50
|
+
* parsed or not: fail closed), or the lead's own approval act then. */
|
|
51
|
+
export const machineSealSql = (scope) => `(EXISTS (SELECT 1 FROM action_ledger mp WHERE mp.task_id = ${scope}.task_id AND mp.action = 'approval proved'
|
|
52
|
+
AND mp.actor = ${scope}.approved_by AND mp.at = ${scope}.approved_at AND mp.outcome NOT IN ${PERSON_PROOFS})
|
|
53
|
+
OR EXISTS (SELECT 1 FROM task_act la JOIN task_ref lr ON lr.id = la.task_ref AND lr.backend = 'built-in'
|
|
54
|
+
WHERE lr.external_id = ${scope}.task_id AND la.act = 'approved' AND la.lead = 1
|
|
55
|
+
AND la.account = ${scope}.approved_by AND la.at = ${scope}.approved_at))`;
|
|
56
|
+
/** SQL: a person's proof at the seal's instant for these exact bytes, its detail parsed (the versioned JSON record,
|
|
57
|
+
* whitespace and all, or the legacy text). */
|
|
58
|
+
export const personSealSql = (scope) => `EXISTS (SELECT 1 FROM action_ledger pp WHERE pp.task_id = ${scope}.task_id AND pp.action = 'approval proved'
|
|
59
|
+
AND pp.actor = ${scope}.approved_by AND pp.at = ${scope}.approved_at AND pp.outcome IN ${PERSON_PROOFS}
|
|
60
|
+
AND CASE WHEN json_valid(pp.detail) THEN json_extract(pp.detail, '$.v') = 1 AND json_type(pp.detail, '$.digest') = 'text'
|
|
61
|
+
AND json_extract(pp.detail, '$.digest') = ${scope}.approved_digest
|
|
62
|
+
ELSE pp.detail LIKE '% · generation % · digest ' || ${scope}.approved_digest END)`;
|
|
63
|
+
/** SQL: the task a standing order's firing filed (its own record of the firing). */
|
|
64
|
+
const automationSql = (scope) => `EXISTS (SELECT 1 FROM flow_trigger_event fe WHERE fe.task = ${scope}.task_id)`;
|
|
65
|
+
/** SQL: the seal's approver can still approve in the project. */
|
|
66
|
+
const approverStandsSql = (scope, repo) => `EXISTS (SELECT 1 FROM approver sched WHERE sched.name = ${scope}.approved_by AND sched.revoked_at IS NULL AND sched.role = 'approver'
|
|
67
|
+
AND ${accountReachesSql("sched", repo)})`;
|
package/dist/assignment.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { currentActor, withActor } from "./actor.js";
|
|
6
6
|
import { replacedWords } from "./task-status.js";
|
|
7
|
-
import { COMPLETER_ACTOR, COMPLETION_ACTION, familyChangedFiles, landedReasonOf } from "./result-completion.js";
|
|
7
|
+
import { COMPLETER_ACTOR, COMPLETION_ACTION, COMPLETION_BY_MACHINE, COMPLETION_BY_PERSON, familyChangedFiles, landedReasonOf, personCompletionOwed } from "./result-completion.js";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { readSchemaVersion, stopFactOf } from "./store.js";
|
|
10
|
-
import { approvalOf } from "./scope.js";
|
|
10
|
+
import { approvalOf, liveReleaseOf, machineActing } from "./scope.js";
|
|
11
11
|
import { openWorkDecisionOf, taskWorkSummaryOf, workDecisionAction } from "./work-summary.js";
|
|
12
12
|
import { evidenceRoot, readVerifiedArtifact, readVerifiedReport, verifyArtifact } from "./evidence.js";
|
|
13
13
|
import { sweepIfMoved } from "./sweep-gate.js";
|
|
@@ -119,6 +119,10 @@ function buildChecks(store, root, runId) {
|
|
|
119
119
|
if (receipt === null)
|
|
120
120
|
return { ...unavailable(level === "off" ? "Checks were off for this build." : "No machine check is recorded."), status: "not-run" };
|
|
121
121
|
const ran = receipt.result.ran === true, exitCode = ran ? receipt.result.exitCode : null;
|
|
122
|
+
// A release check whose coverage fell short (release-once.md) failed, whatever its command exited with.
|
|
123
|
+
if (receipt.result.failure === "coverage-incomplete")
|
|
124
|
+
return { status: "failed", exitCode: null, command: receipt.command.command, logArtifactId: receipt.log.artifactId, level,
|
|
125
|
+
detail: "Checks didn't cover everything this release owed, or a required test went missing." };
|
|
122
126
|
// The sealed receipt names the grant that ran: the quick one, or the full one.
|
|
123
127
|
const sealedLevel = typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:") ? "quick" : "full";
|
|
124
128
|
const words = sealedLevel === "quick" ? "Quick checks" : "Checks";
|
|
@@ -450,8 +454,19 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
450
454
|
if (!/^[a-f0-9]{64}$/.test(receiptDigest) || current.receipt?.digest !== receiptDigest)
|
|
451
455
|
return { ok: false, reason: "stale", message: "The result changed. Read the current assignment before checking it." };
|
|
452
456
|
const receipt = current.receipt;
|
|
457
|
+
// The nightly check of main is a check only (release-once.md, part 3): nobody completes it.
|
|
458
|
+
if (store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(current.receipt.taskId) !== undefined)
|
|
459
|
+
return { ok: false, reason: "check-only", message: "The nightly check of main is only a check; it isn't completed." };
|
|
453
460
|
if (current.state !== "ready-to-check" && current.state !== "complete")
|
|
454
461
|
return { ok: false, reason: "not-ready", message: "This assignment still has unresolved execution, scope or decisions." };
|
|
462
|
+
// A release task completes only while its release approval stands for it (release-once.md, liveReleaseOf).
|
|
463
|
+
const release = liveReleaseOf(store, current.receipt.taskId, "complete", now);
|
|
464
|
+
if (release.applies && !release.ok)
|
|
465
|
+
return { ok: false, reason: "release", message: `This release can't be completed: ${release.why}.` };
|
|
466
|
+
// Who is completing, from trusted context (approval policy §1.7a): a person, or a machine credential (the lead's
|
|
467
|
+
// token, a remote API run) that may complete ordinary work but is never the second person on protected work.
|
|
468
|
+
const person = actor.startsWith("operator:") && !machineActing() ? actor.slice("operator:".length) : null;
|
|
469
|
+
const marked = actor.startsWith("operator:") ? (person === null ? COMPLETION_BY_MACHINE : COMPLETION_BY_PERSON) : null;
|
|
455
470
|
if (current.state !== "complete") {
|
|
456
471
|
// The one readiness guard: a failed or missing required check, an unresolved requirement or HIGH finding refuses.
|
|
457
472
|
const blockers = current.readiness?.blockers ?? [];
|
|
@@ -459,11 +474,11 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
459
474
|
return { ok: false, reason: "not-ready", message: blockers.map(one => one.message).join(" ") };
|
|
460
475
|
// v102: declared paths are only a promise. A result whose actual diff reaches protected files, on a
|
|
461
476
|
// scope one person approved, completes only by someone else — two people have then seen the work.
|
|
462
|
-
const problem = store.protectedResultProblem(receipt.taskId, familyChangedFiles(store, receipt.taskId, receipt.runId, root),
|
|
477
|
+
const problem = store.protectedResultProblem(receipt.taskId, familyChangedFiles(store, receipt.taskId, receipt.runId, root), person);
|
|
463
478
|
if (problem !== null)
|
|
464
479
|
return { ok: false, reason: "approval-rules", message: problem };
|
|
465
480
|
store.recordAction({ at: now.toISOString(), actor, repo: current.repo,
|
|
466
|
-
taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work" });
|
|
481
|
+
taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work", detail: marked });
|
|
467
482
|
const ref = store.lookupRef(current.rootId);
|
|
468
483
|
// Whose act this is, even outside a command that named its person: the operator who marked it.
|
|
469
484
|
const by = currentActor() ?? (actor.startsWith("operator:") ? { account: actor.slice("operator:".length), lead: false } : null);
|
|
@@ -471,6 +486,13 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
471
486
|
store.noteTaskAct(ref.id, "completed", now, undefined, by);
|
|
472
487
|
store.bumpWake();
|
|
473
488
|
}
|
|
489
|
+
else if (person !== null && personCompletionOwed(store, receipt.taskId, receipt.runId, receiptDigest, person, root)) {
|
|
490
|
+
// Already complete, but no person's completion is on record that lets protected work publish (a machine's, or one
|
|
491
|
+
// from before completions said who made them): this person's completion is recorded now (approval policy §1.7a).
|
|
492
|
+
store.recordAction({ at: now.toISOString(), actor, repo: current.repo,
|
|
493
|
+
taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work", detail: COMPLETION_BY_PERSON });
|
|
494
|
+
store.bumpWake();
|
|
495
|
+
}
|
|
474
496
|
const assignment = assignmentOf(store, current.rootId, now, access, root);
|
|
475
497
|
noteAssignmentStatus(store, assignment, now);
|
|
476
498
|
return { ok: true, assignment };
|