toolroll 0.9.71 → 0.9.73
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 +155 -0
- package/dist/agent-fence.d.ts +10 -2
- package/dist/agent-fence.js +47 -9
- 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/browser/workspace.js +28 -28
- package/dist/builder.js +48 -6
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +186 -5
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +205 -0
- package/dist/claim.js +1 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +111 -0
- package/dist/commit-extract.js +514 -0
- package/dist/contracts/console-api.d.ts +4 -4
- package/dist/contracts/console-api.js +5 -4
- 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 +66 -0
- package/dist/contracts/release-terms.js +74 -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/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-engine.js +5 -1
- 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-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/flows-ui.js +7 -1
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- 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/project-tools.js +3 -1
- package/dist/proof.js +2 -0
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -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 +16 -1
- package/dist/release-adapters.js +292 -29
- package/dist/release-authority.d.ts +118 -0
- package/dist/release-authority.js +135 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +236 -0
- package/dist/release-carry.js +664 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +158 -6
- package/dist/release-coverage.js +3 -0
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -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 +139 -3
- package/dist/release.js +641 -250
- 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 +122 -5
- package/dist/scope.js +236 -7
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- 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/settings.js +26 -5
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +230 -5
- package/dist/store.js +733 -37
- 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/task-outcome-cli.js +7 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- 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
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* command, the retained log and a reused gate's source — are checked in plain code after parsing.
|
|
6
6
|
*
|
|
7
7
|
* Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
|
|
8
|
-
* reused by an unchanged observation follow-up.
|
|
9
|
-
*
|
|
8
|
+
* reused by an unchanged observation follow-up. Version 3 (docs/plans/release-once.md) is a release check: it keeps
|
|
9
|
+
* the raw exit (`result`) apart from `coverage` — `full`, `composed` (a targeted run composed with the full check A),
|
|
10
|
+
* `partial` (never passes) or `identity-removed` (a required test went away; asks again) — names the sealed check
|
|
11
|
+
* artifacts, and for a composed check every link its reader proves again. A direct receipt is still sealed as version 1,
|
|
12
|
+
* so its bytes (and the digests bound to them) read the same in every Toolroll that reads receipts.
|
|
10
13
|
*/
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { type ContractResult } from "./contract.js";
|
|
13
|
-
export declare const VERIFICATION_FAILURES: readonly ["spawn-failed", "timed-out", "dependency-missing", "setup-stale", "setup-failed", "own-install-failed", "tracked-files-changed", "setup-changed-files", "checkout-moved", "cleanliness-unavailable", "dependency-still-missing", "retry-spawn-failed", "retry-timed-out", "custody-lost"];
|
|
16
|
+
export declare const VERIFICATION_FAILURES: readonly ["spawn-failed", "timed-out", "dependency-missing", "setup-stale", "setup-failed", "own-install-failed", "tracked-files-changed", "setup-changed-files", "checkout-moved", "cleanliness-unavailable", "dependency-still-missing", "retry-spawn-failed", "retry-timed-out", "custody-lost", "coverage-incomplete"];
|
|
14
17
|
/**
|
|
15
18
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
16
19
|
* it (`attemptFailed`, which downgrades to `short` rather than `refuted`: "we could not check" is not "the claim is
|
|
@@ -25,6 +28,7 @@ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<
|
|
|
25
28
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
26
29
|
"checkout-moved": "checkout-moved";
|
|
27
30
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
31
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
28
32
|
"custody-lost": "custody-lost";
|
|
29
33
|
"dependency-missing": "dependency-missing";
|
|
30
34
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -71,10 +75,12 @@ export declare const receiptLogSchema: z.ZodObject<{
|
|
|
71
75
|
ok: "ok";
|
|
72
76
|
}>>;
|
|
73
77
|
}, z.core.$strict>;
|
|
74
|
-
export declare const RECEIPT_VERSION =
|
|
78
|
+
export declare const RECEIPT_VERSION = 3;
|
|
79
|
+
export declare const RECEIPT_COVERAGES: readonly ["full", "composed", "partial", "identity-removed"];
|
|
80
|
+
export type ReceiptCoverage = typeof RECEIPT_COVERAGES[number];
|
|
75
81
|
/** The receipt as read: a reused gate names its source run and digest; a direct one names neither. */
|
|
76
82
|
export declare const verificationReceiptSchema: z.ZodObject<{
|
|
77
|
-
version: z.ZodLiteral<
|
|
83
|
+
version: z.ZodLiteral<3>;
|
|
78
84
|
run: z.ZodInt;
|
|
79
85
|
head: z.ZodString;
|
|
80
86
|
base: z.ZodNullable<z.ZodString>;
|
|
@@ -105,6 +111,7 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
105
111
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
106
112
|
"checkout-moved": "checkout-moved";
|
|
107
113
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
114
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
108
115
|
"custody-lost": "custody-lost";
|
|
109
116
|
"dependency-missing": "dependency-missing";
|
|
110
117
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -136,13 +143,40 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
136
143
|
ok: "ok";
|
|
137
144
|
}>>;
|
|
138
145
|
}, z.core.$strict>;
|
|
146
|
+
coverage: z.ZodOptional<z.ZodEnum<{
|
|
147
|
+
composed: "composed";
|
|
148
|
+
full: "full";
|
|
149
|
+
"identity-removed": "identity-removed";
|
|
150
|
+
partial: "partial";
|
|
151
|
+
}>>;
|
|
152
|
+
check: z.ZodOptional<z.ZodObject<{
|
|
153
|
+
attempt: z.ZodEnum<{
|
|
154
|
+
full: "full";
|
|
155
|
+
targeted: "targeted";
|
|
156
|
+
}>;
|
|
157
|
+
report: z.ZodNullable<z.ZodString>;
|
|
158
|
+
inventory: z.ZodNullable<z.ZodString>;
|
|
159
|
+
exitCode: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
|
|
160
|
+
}, z.core.$strict>>;
|
|
161
|
+
composed: z.ZodOptional<z.ZodObject<{
|
|
162
|
+
base: z.ZodInt;
|
|
163
|
+
baseReceipt: z.ZodString;
|
|
164
|
+
baseReport: z.ZodString;
|
|
165
|
+
authority: z.ZodInt;
|
|
166
|
+
carries: z.ZodArray<z.ZodInt>;
|
|
167
|
+
artifacts: z.ZodString;
|
|
168
|
+
plan: z.ZodString;
|
|
169
|
+
snapshot: z.ZodString;
|
|
170
|
+
report: z.ZodString;
|
|
171
|
+
}, z.core.$strict>>;
|
|
139
172
|
}, z.core.$strict>;
|
|
140
173
|
export type VerificationReceipt = z.infer<typeof verificationReceiptSchema>;
|
|
141
|
-
/** A version 1 receipt read as version
|
|
142
|
-
* is refused by its reader, which still knows the version
|
|
174
|
+
/** A version 1 or 2 receipt read as version 3: the same fields. Reuse is a version 2 field and coverage a version 3
|
|
175
|
+
* one; a receipt naming a field newer than it was sealed as is refused by its reader, which still knows the version. */
|
|
143
176
|
export declare function upgradeDirectReceipt(body: Record<string, unknown>): Record<string, unknown>;
|
|
144
177
|
export declare const RECEIPT_UPGRADES: {
|
|
145
178
|
readonly 1: typeof upgradeDirectReceipt;
|
|
179
|
+
readonly 2: typeof upgradeDirectReceipt;
|
|
146
180
|
};
|
|
147
|
-
/** Read a receipt: version
|
|
181
|
+
/** Read a receipt: version 3 as itself, 1 and 2 upgraded, a newer one refused plainly. */
|
|
148
182
|
export declare function readVerificationReceipt(input: unknown): ContractResult<VerificationReceipt>;
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* command, the retained log and a reused gate's source — are checked in plain code after parsing.
|
|
6
6
|
*
|
|
7
7
|
* Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
|
|
8
|
-
* reused by an unchanged observation follow-up.
|
|
9
|
-
*
|
|
8
|
+
* reused by an unchanged observation follow-up. Version 3 (docs/plans/release-once.md) is a release check: it keeps
|
|
9
|
+
* the raw exit (`result`) apart from `coverage` — `full`, `composed` (a targeted run composed with the full check A),
|
|
10
|
+
* `partial` (never passes) or `identity-removed` (a required test went away; asks again) — names the sealed check
|
|
11
|
+
* artifacts, and for a composed check every link its reader proves again. A direct receipt is still sealed as version 1,
|
|
12
|
+
* so its bytes (and the digests bound to them) read the same in every Toolroll that reads receipts.
|
|
10
13
|
*/
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { readVersioned, versioned } from "./contract.js";
|
|
@@ -25,6 +28,8 @@ export const VERIFICATION_FAILURES = [
|
|
|
25
28
|
"retry-spawn-failed",
|
|
26
29
|
"retry-timed-out",
|
|
27
30
|
"custody-lost",
|
|
31
|
+
// A release check that didn't cover what it owed, or lost a required test (release-once.md): never a pass.
|
|
32
|
+
"coverage-incomplete",
|
|
28
33
|
];
|
|
29
34
|
/**
|
|
30
35
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
@@ -59,7 +64,8 @@ export const receiptLogSchema = z.strictObject({
|
|
|
59
64
|
redacted: z.boolean(),
|
|
60
65
|
captureStatus: z.enum(["ok", "failed"]).nullable(),
|
|
61
66
|
});
|
|
62
|
-
export const RECEIPT_VERSION =
|
|
67
|
+
export const RECEIPT_VERSION = 3;
|
|
68
|
+
export const RECEIPT_COVERAGES = ["full", "composed", "partial", "identity-removed"];
|
|
63
69
|
const receiptFields = {
|
|
64
70
|
run: z.int(),
|
|
65
71
|
head: z.string(),
|
|
@@ -74,14 +80,25 @@ export const verificationReceiptSchema = versioned(RECEIPT_VERSION, {
|
|
|
74
80
|
command: receiptCommandSchema,
|
|
75
81
|
result: verifyCommandFactsSchema,
|
|
76
82
|
log: receiptLogSchema,
|
|
83
|
+
/** Version 3: what the check covered, apart from its exit. */
|
|
84
|
+
coverage: z.enum(RECEIPT_COVERAGES).optional(),
|
|
85
|
+
/** Version 3: the attempt sealed before the check ran, and its sealed report (null: it gave none). */
|
|
86
|
+
check: z.strictObject({ attempt: z.enum(["full", "targeted"]), report: z.string().nullable(), inventory: z.string().nullable(),
|
|
87
|
+
/** The command's own exit, kept here when `result` reads a failed attempt because the coverage fell short. */
|
|
88
|
+
exitCode: z.int().min(0).max(255).nullable().optional() }).optional(),
|
|
89
|
+
/** Version 3, composed: the full check A and every link the reader proves again. */
|
|
90
|
+
composed: z.strictObject({
|
|
91
|
+
base: z.int().min(1), baseReceipt: z.string(), baseReport: z.string(), authority: z.int().min(1), carries: z.array(z.int()),
|
|
92
|
+
artifacts: z.string(), plan: z.string(), snapshot: z.string(), report: z.string(),
|
|
93
|
+
}).optional(),
|
|
77
94
|
});
|
|
78
|
-
/** A version 1 receipt read as version
|
|
79
|
-
* is refused by its reader, which still knows the version
|
|
95
|
+
/** A version 1 or 2 receipt read as version 3: the same fields. Reuse is a version 2 field and coverage a version 3
|
|
96
|
+
* one; a receipt naming a field newer than it was sealed as is refused by its reader, which still knows the version. */
|
|
80
97
|
export function upgradeDirectReceipt(body) {
|
|
81
98
|
return { ...body, version: RECEIPT_VERSION };
|
|
82
99
|
}
|
|
83
|
-
export const RECEIPT_UPGRADES = { 1: upgradeDirectReceipt };
|
|
84
|
-
/** Read a receipt: version
|
|
100
|
+
export const RECEIPT_UPGRADES = { 1: upgradeDirectReceipt, 2: upgradeDirectReceipt };
|
|
101
|
+
/** Read a receipt: version 3 as itself, 1 and 2 upgraded, a newer one refused plainly. */
|
|
85
102
|
export function readVerificationReceipt(input) {
|
|
86
103
|
return readVersioned(verificationReceiptSchema, input, RECEIPT_UPGRADES);
|
|
87
104
|
}
|
|
@@ -39,6 +39,7 @@ export declare const legacyGateViewSchema: z.ZodObject<{
|
|
|
39
39
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
40
40
|
"checkout-moved": "checkout-moved";
|
|
41
41
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
42
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
42
43
|
"custody-lost": "custody-lost";
|
|
43
44
|
"dependency-missing": "dependency-missing";
|
|
44
45
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type DeploymentGate = {
|
|
3
|
+
ok: true;
|
|
4
|
+
taskId: string;
|
|
5
|
+
head: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
approvedBy: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
13
|
+
export declare function deploymentGate(store: Store, input: {
|
|
14
|
+
runId: number;
|
|
15
|
+
evidenceRoot: string;
|
|
16
|
+
now: Date;
|
|
17
|
+
}): DeploymentGate;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment gate, in the runtime that is releasing (docs/plans/approval-policy.md §1.7): whether a candidate run
|
|
3
|
+
* may be deployed at all — a person approved its scope, its exact result passed the approved check and was marked
|
|
4
|
+
* complete, and nothing still blocks it. `toolroll release` asks this BEFORE it starts anything from the candidate's
|
|
5
|
+
* checkout, so the candidate's own scripts never decide their own deployment. (scripts/deploy-candidate.mjs keeps the
|
|
6
|
+
* same checks for the deployment script, proven again there with the installed runtime's modules.)
|
|
7
|
+
*/
|
|
8
|
+
import { assignmentOf } from "./assignment.js";
|
|
9
|
+
import { liveReleaseOf, personApprovalOf } from "./scope.js";
|
|
10
|
+
import { verificationEvidence } from "./verification-evidence.js";
|
|
11
|
+
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
12
|
+
export function deploymentGate(store, input) {
|
|
13
|
+
const refuse = (message) => ({ ok: false, message });
|
|
14
|
+
const run = store.getRun(input.runId);
|
|
15
|
+
const ref = run === null ? null : store.refById(run.taskRef);
|
|
16
|
+
const scope = ref === null ? null : store.getScope(ref.externalId);
|
|
17
|
+
const head = run?.headRevision ?? null;
|
|
18
|
+
if (run === null || ref === null || head === null)
|
|
19
|
+
return refuse(`Run ${input.runId} is not a built candidate.`);
|
|
20
|
+
// `task regate` reruns the approved check on the last built commit and records no-change: a candidate only for that commit.
|
|
21
|
+
const regated = run.outcome === "no-change" && scope?.candidate === head &&
|
|
22
|
+
store.runsFor(run.taskRef).find(one => one.id < input.runId && one.role === "builder" && one.outcome === "built")?.headRevision === head;
|
|
23
|
+
if (run.role !== "builder" || !(run.outcome === "built" || regated))
|
|
24
|
+
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
25
|
+
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
|
26
|
+
return refuse("The run's signed scope is no longer the approved one.");
|
|
27
|
+
// One approval per release (release-once.md): a release's task deploys only while its authority stands for it — the
|
|
28
|
+
// live predicate proves the anchor's person approval (a carried commit's own seal is never a person's). Any other task
|
|
29
|
+
// needs a person's approval of its own.
|
|
30
|
+
const release = liveReleaseOf(store, ref.externalId, "deploy", input.now);
|
|
31
|
+
if (release.applies && !release.ok)
|
|
32
|
+
return refuse(`This release can't deploy: ${release.why}.`);
|
|
33
|
+
const anchorPerson = release.applies && release.ok ? personApprovalOf(store, release.authority.anchorTask) : null;
|
|
34
|
+
const person = anchorPerson ?? personApprovalOf(store, ref.externalId);
|
|
35
|
+
if (!person.ok)
|
|
36
|
+
return refuse(`A person has to approve this candidate's scope: ${person.why}.`);
|
|
37
|
+
const evidence = verificationEvidence(store, input.evidenceRoot, input.runId);
|
|
38
|
+
if (!evidence.ok || evidence.bytes === null)
|
|
39
|
+
return refuse(`Native check unavailable: ${evidence.ok ? "no receipt" : evidence.problem}.`);
|
|
40
|
+
let gate;
|
|
41
|
+
try {
|
|
42
|
+
gate = JSON.parse(evidence.bytes.toString());
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return refuse("The native check's receipt can't be read.");
|
|
46
|
+
}
|
|
47
|
+
const live = ref.repo === null ? null : store.liveVerifyCommand(ref.repo);
|
|
48
|
+
const result = gate["result"];
|
|
49
|
+
// A release check's coverage (receipt version 3): partial or identity-removed never deploys, whatever the exit.
|
|
50
|
+
if (gate["coverage"] !== undefined && gate["coverage"] !== "full" && gate["coverage"] !== "composed")
|
|
51
|
+
return refuse(`The release check didn't cover this candidate (${String(gate["coverage"])}).`);
|
|
52
|
+
const command = gate["command"];
|
|
53
|
+
if (gate["run"] !== input.runId || gate["head"] !== head || gate["scopeDigest"] !== scope.digest || result?.ran !== true || result.exitCode !== 0
|
|
54
|
+
|| live === null || command?.command !== live.command)
|
|
55
|
+
return refuse("The native check did not pass this candidate under the currently approved command.");
|
|
56
|
+
const assignment = assignmentOf(store, ref.externalId, input.now, { principal: "operator", repos: null, includeUnplaced: true }, input.evidenceRoot);
|
|
57
|
+
if (assignment?.state !== "complete" || assignment.receipt?.runId !== input.runId || assignment.receipt.head !== head
|
|
58
|
+
|| assignment.receipt.scopeDigest !== scope.digest || assignment.completion?.digest !== assignment.receipt.digest)
|
|
59
|
+
return refuse("The lead or user has not marked this exact result complete.");
|
|
60
|
+
const blockers = assignment.readiness?.blockers;
|
|
61
|
+
if (!Array.isArray(blockers))
|
|
62
|
+
return refuse("The result's readiness could not be read.");
|
|
63
|
+
if (blockers.length > 0)
|
|
64
|
+
return refuse(`This result isn't ready to deploy: ${blockers.map(one => one.message).join(" ")}`);
|
|
65
|
+
if (ref.repo === null)
|
|
66
|
+
return refuse("The candidate's task is in no project.");
|
|
67
|
+
return { ok: true, taskId: ref.externalId, head, repo: ref.repo, approvedBy: person.by };
|
|
68
|
+
}
|
package/dist/dispatch.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* fleet observations on top for people and agents reading the queue. A
|
|
7
7
|
* diagnosis can go stale; it can never grant a lease.
|
|
8
8
|
*/
|
|
9
|
+
import { authorityStandsSql } from "./approval-authority.js";
|
|
9
10
|
import { isAlive } from "./runner.js";
|
|
10
11
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
11
12
|
import { approvalOf } from "./scope.js";
|
|
@@ -15,8 +16,6 @@ import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js"
|
|
|
15
16
|
import { replacedWords } from "./task-status.js";
|
|
16
17
|
import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
|
|
17
18
|
import { buildReviewOf } from "./review-switch.js";
|
|
18
|
-
import { grantStandsSql } from "./flow-unattended.js";
|
|
19
|
-
import { accountReachesSql } from "./project-access.js";
|
|
20
19
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
21
20
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
22
21
|
/**
|
|
@@ -77,14 +76,8 @@ export function scopeApprovedForDispatch(store, taskRef, now) {
|
|
|
77
76
|
.prepare(`SELECT 1 AS hit FROM task_scope
|
|
78
77
|
JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
|
|
79
78
|
WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL
|
|
80
|
-
AND (
|
|
81
|
-
|
|
82
|
-
JOIN approver signer ON signer.name = om.signed_by
|
|
83
|
-
WHERE om.repo = task_ref.repo AND om.revoked_at IS NULL
|
|
84
|
-
AND om.absolute_expiry > ? AND om.digest = task_scope.mode_digest
|
|
85
|
-
AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", "task_ref.repo")}))
|
|
86
|
-
OR (task_scope.approval_basis = 'grant' AND ${grantStandsSql("task_scope.mode_digest", "?", "task_ref.repo")}))`)
|
|
87
|
-
.get(taskRef, now.toISOString(), now.toISOString());
|
|
79
|
+
AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
|
|
80
|
+
.get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
|
|
88
81
|
return row !== undefined;
|
|
89
82
|
}
|
|
90
83
|
/** The phase the ordinary dispatch loop would choose before it resolves an
|
package/dist/dispose.js
CHANGED
|
@@ -19,6 +19,7 @@ import { modeTermsFromJson } from "./modes.js";
|
|
|
19
19
|
import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
20
20
|
import { queueBuildReview } from "./build-review.js";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
22
23
|
import {} from "./proof.js";
|
|
23
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
24
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
@@ -162,7 +163,9 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
162
163
|
grant.publishOn !== "complete" &&
|
|
163
164
|
headSha !== null &&
|
|
164
165
|
headWithin(branch, grant.headPrefix) &&
|
|
165
|
-
(grant.selector === "all" || origin === "ours")
|
|
166
|
+
(grant.selector === "all" || origin === "ours") &&
|
|
167
|
+
// A nightly check or a release's task is never published (store.createPublicationIntent refuses both).
|
|
168
|
+
!nightlyTaskRef(store, taskRef) && !releaseTaskRef(store, taskRef)) {
|
|
166
169
|
const intentId = store.createPublicationIntent({
|
|
167
170
|
run: runId,
|
|
168
171
|
taskRef,
|
package/dist/evidence-pack.d.ts
CHANGED
package/dist/evidence-pack.js
CHANGED
|
@@ -15,6 +15,7 @@ import { rulesWords } from "./approval-policy.js";
|
|
|
15
15
|
import { assignmentOf } from "./assignment.js";
|
|
16
16
|
import { projectName } from "./project.js";
|
|
17
17
|
import { changedFilesOf } from "./result-completion.js";
|
|
18
|
+
import { approvalProofOf, personProof } from "./scope.js";
|
|
18
19
|
import { html, htmlString, joinHtml, styleElement } from "./html.js";
|
|
19
20
|
export const EVIDENCE_PACK_FORMAT = "standing-orders/evidence-pack/v1";
|
|
20
21
|
/** How to recheck one entry's seal, for whoever reads the JSON. */
|
|
@@ -55,7 +56,7 @@ export function evidencePack(store, taskId, access, who, now, evidenceRoot, chec
|
|
|
55
56
|
goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance.map(one => ({ id: one.id, statement: one.statement })),
|
|
56
57
|
budgetUsd: scope.budgetMicrousd === null ? null : scope.budgetMicrousd / 1_000_000, digest: scope.digest,
|
|
57
58
|
approved: scope.approvedBy === null || scope.approvedAt === null || scope.approvedDigest === null ? null
|
|
58
|
-
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: scope.approvalBasis
|
|
59
|
+
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: approvalBasisWord(store, version.id, scope.approvalBasis ?? null, scope.approvedBy, scope.approvedDigest), current: scope.approvedDigest === scope.digest },
|
|
59
60
|
writtenBy: history.authors,
|
|
60
61
|
approvals: history.votes,
|
|
61
62
|
},
|
|
@@ -206,7 +207,7 @@ export function evidencePackHtml(pack) {
|
|
|
206
207
|
...(scope.touches.length ? [["Touches", joinHtml(scope.touches, ", ")]] : []),
|
|
207
208
|
...(scope.acceptance.length ? [["Acceptance", html `<ol class="criteria">${scope.acceptance.map(one => html `<li>${one.statement}</li>`)}</ol>`]] : []),
|
|
208
209
|
...(scope.writtenBy.length ? [["Scope written by", joinHtml([...new Set(scope.writtenBy.map(one => one.author))], ", ")]] : []),
|
|
209
|
-
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : approved.basis === "flow" ? " (flow runs on its own)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
210
|
+
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : approved.basis === "flow" ? " (flow runs on its own)" : approved.basis === "lead" ? " (the lead, not a person)" : approved.basis === "schedule" ? " (standing order)" : approved.basis === "carried" ? " (carried from their release approval)" : approved.basis === "unknown" ? " (no person's proof on record)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
210
211
|
...(approvals.length > 1 ? [["Approvals", joinHtml(approvals.map(one => html `${one.approver} · ${when(one.at)}`), html `<br>`)]] : []),
|
|
211
212
|
["Scope digest", short(scope.digest)],
|
|
212
213
|
]),
|
|
@@ -252,3 +253,20 @@ export function standaloneEvidenceHtml(pack) {
|
|
|
252
253
|
return htmlString(html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Evidence pack · ${pack.task.id}</title>${styleElement(css)}</head><body>${evidencePackHtml(pack)}</body></html>
|
|
253
254
|
`);
|
|
254
255
|
}
|
|
256
|
+
/** Who approved, in the pack's words (approval policy §1.5): `person` only when a person's proof stands behind it. */
|
|
257
|
+
function approvalBasisWord(store, taskId, basis, by, digest) {
|
|
258
|
+
if (basis === "mode")
|
|
259
|
+
return "mode";
|
|
260
|
+
if (basis === "grant")
|
|
261
|
+
return "flow";
|
|
262
|
+
if (basis === "schedule")
|
|
263
|
+
return "schedule";
|
|
264
|
+
// A later release commit carried under a person's release approval (release-once.md): never a person's own yes.
|
|
265
|
+
if (basis === "carried")
|
|
266
|
+
return "carried";
|
|
267
|
+
if (basis === "lead")
|
|
268
|
+
return "lead";
|
|
269
|
+
if (basis === "password" || basis === "chat")
|
|
270
|
+
return personProof(approvalProofOf(store, taskId, by, digest)?.method) ? "person" : "unknown";
|
|
271
|
+
return "unknown";
|
|
272
|
+
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -201,6 +201,7 @@ export declare function storeEvidence(store: Store, root: string, runId: number,
|
|
|
201
201
|
redacted?: boolean;
|
|
202
202
|
captureStatus?: "ok" | "failed";
|
|
203
203
|
sourceBytesOriginal?: number;
|
|
204
|
+
cap?: number;
|
|
204
205
|
}): number;
|
|
205
206
|
/**
|
|
206
207
|
* Read an artifact's bytes back, believing nothing until it is proved on the
|
package/dist/evidence.js
CHANGED
|
@@ -397,7 +397,8 @@ export function imageDimensions(bytes, kind) {
|
|
|
397
397
|
* store the caller passed — inside whatever transaction the caller holds.
|
|
398
398
|
*/
|
|
399
399
|
export function storeEvidence(store, root, runId, kind, name, content, capture, now, options = {}) {
|
|
400
|
-
|
|
400
|
+
// `cap`: a sealed structured payload with its own documented bound (a release check's report: 256 KiB), never above 1 MiB.
|
|
401
|
+
const cap = options.cap === undefined ? EVIDENCE_CAPS[kind] : Math.min(options.cap, 1024 * 1024);
|
|
401
402
|
const stored = content.subarray(0, cap);
|
|
402
403
|
// Some callers that deliberately summarize/bound a source before storage
|
|
403
404
|
// may still report its true original size. This keeps `truncated` honest
|
package/dist/exec.d.ts
CHANGED
|
@@ -200,7 +200,7 @@ export declare function confined(file: string, args: readonly string[], bag: {
|
|
|
200
200
|
env: Record<string, string>;
|
|
201
201
|
};
|
|
202
202
|
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
203
|
-
export declare function withSandboxEnv<T extends Record<string, string | undefined>>(env: T | undefined, added: Record<string, string
|
|
203
|
+
export declare function withSandboxEnv<T extends Record<string, string | undefined>>(env: T | undefined, added: Record<string, string>, fenced?: boolean): T | Record<string, string | undefined> | undefined;
|
|
204
204
|
/** Run IDs are local to a database. Connections to the same file share
|
|
205
205
|
* custody; separate files and separate in-memory databases never do. */
|
|
206
206
|
export declare function runOwnerTag(store: Store, runId: number): string;
|
package/dist/exec.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
|
|
10
10
|
import { linuxFenceAvailable, macosFenceAvailable, sandboxedSpawn } from "./agent-fence.js";
|
|
11
|
+
import { withoutProtectedEnv } from "./protected-storage.js";
|
|
11
12
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
12
13
|
import { readlinkSync, realpathSync } from "node:fs";
|
|
13
14
|
import { observeProcessTree, stopProcessTree } from "./process-tree.js";
|
|
@@ -75,7 +76,10 @@ export function confined(file, args, bag) {
|
|
|
75
76
|
return { file, args: [...args], env: {} };
|
|
76
77
|
}
|
|
77
78
|
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
78
|
-
export function withSandboxEnv(env, added) {
|
|
79
|
+
export function withSandboxEnv(env, added, fenced = false) {
|
|
80
|
+
// A fenced launch never gets a chat or vault variable (protected-storage.ts), whoever made its environment.
|
|
81
|
+
if (fenced)
|
|
82
|
+
return withoutProtectedEnv({ ...(env ?? process.env), ...added });
|
|
79
83
|
return Object.keys(added).length === 0 ? env : { ...(env ?? process.env), ...added };
|
|
80
84
|
}
|
|
81
85
|
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
@@ -107,7 +111,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
107
111
|
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
108
112
|
const sandboxed = confined(file, args, { ...bag, cwd: spawnOptions.cwd, temp: spawnOptions.env?.["TMPDIR"] });
|
|
109
113
|
({ file, args } = sandboxed);
|
|
110
|
-
spawnOptions = { ...spawnOptions, env: withSandboxEnv(spawnOptions.env, sandboxed.env) };
|
|
114
|
+
spawnOptions = { ...spawnOptions, env: withSandboxEnv(spawnOptions.env, sandboxed.env, (bag.fence?.length ?? 0) > 0) };
|
|
111
115
|
// The policy refusal comes first: nothing is reserved, recorded or
|
|
112
116
|
// spawned for a spawn that cannot be contained as required.
|
|
113
117
|
const effective = contain ? currentContainment() : null;
|
|
@@ -583,7 +587,7 @@ export function run(file, args, options = {}) {
|
|
|
583
587
|
encoding: "utf8",
|
|
584
588
|
shell: false,
|
|
585
589
|
windowsHide: true,
|
|
586
|
-
...((env => env === undefined ? {} : { env }))(withSandboxEnv(childEnv, wrapped.env)),
|
|
590
|
+
...((env => env === undefined ? {} : { env }))(withSandboxEnv(childEnv, wrapped.env, (options.fence?.length ?? 0) > 0)),
|
|
587
591
|
}, (error, stdout, stderr) => {
|
|
588
592
|
if (error === null) {
|
|
589
593
|
resolve({ result: { code: 0, stdout, stderr, timedOut: false, notFound: false }, transient: false });
|
package/dist/flow-engine.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* waits for people. See flows.ts for what each zone does.
|
|
7
7
|
*/
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
|
+
import { currentActor } from "./actor.js";
|
|
10
|
+
import { activeRemote } from "./remote-run.js";
|
|
9
11
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
10
12
|
import { passOn, TEXT_LIMITS } from "./text-limits.js";
|
|
11
13
|
import { deciderOf, durationWords, fillFlowText, flowGoal, flowValueLabel, flowWorkTemplate, validateFlowDefinition, withinHours } from "./flows.js";
|
|
@@ -528,7 +530,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
528
530
|
ledger("approved");
|
|
529
531
|
return { ok: true, said: "Approved. The card is done." };
|
|
530
532
|
}
|
|
531
|
-
|
|
533
|
+
// Who decided, from trusted context (approval policy §1.7): only a person's approval lets a Pull request zone merge.
|
|
534
|
+
const authority = input.subagent !== undefined ? "subagent" : currentActor()?.lead === true ? "lead" : activeRemote() !== null ? "remote" : "person";
|
|
535
|
+
if (!store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry, authority }, now))
|
|
532
536
|
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
533
537
|
ledger("approved");
|
|
534
538
|
const title = definition.stages.find(one => one.id === stage.next)?.title ?? stage.next;
|
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
|
|
6
6
|
*/
|
|
7
7
|
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
|
-
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
8
|
+
import { BLANK, GALLERY, GALLERY_GROUPS, galleryAdmitsAnyone, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
9
9
|
import { choiceTargets } from "./flows.js";
|
|
10
10
|
import { html, joinHtml, postForm, reauth } from "./html.js";
|
|
11
11
|
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
@@ -107,7 +107,9 @@ export function galleryUseHtml(input) {
|
|
|
107
107
|
: html `<input type="hidden" name="repo" value="${input.repo}">`;
|
|
108
108
|
const preview = input.preview === null ? "" : html `<section class="gallery-preview" aria-labelledby="gallery-will"><h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="gallery-never">${input.preview.built.never}</p>${needing}${input.preview.startsFrom.length === 0 ? html `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => html `<p class="meta">Starts from: ${one}</p>`)}<details><summary>Every step</summary><ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol></details></section>`;
|
|
109
109
|
const sendResult = !sendsAlready(template) && html `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`;
|
|
110
|
-
|
|
110
|
+
// A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3).
|
|
111
|
+
const step = input.preview !== null && galleryAdmitsAnyone(template) ? reauth(html `<label>Your password <span class="meta">(it brings in items from anyone)</span><input type="password" name="password" autocomplete="current-password"></label>`) : "";
|
|
112
|
+
const actions = html `${step}<p class="gallery-actions">${input.preview !== null && html `<button type="submit" name="intent" value="create"${missing.length > 0 && html ` class="secondary" data-without-tools`}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>`;
|
|
111
113
|
const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(field)}<label>Name<input name="name" value="${input.name}" maxlength="80"></label>${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true }, hidden: { previewed: input.preview?.digest ?? "" } });
|
|
112
114
|
return html `<section class="gallery-use" data-gallery-template="${template.id}">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}${input.said ? html `<p role="status">${input.said}</p>` : ""}<p>${template.promise}</p>${input.diagram !== null && stepStrip(input.diagram)}${needsList(template)}${toolsBlock}${form}<p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
113
115
|
}
|
package/dist/flow-gallery.d.ts
CHANGED
|
@@ -136,9 +136,12 @@ export type GalleryUsed = {
|
|
|
136
136
|
said: string;
|
|
137
137
|
};
|
|
138
138
|
/** Use a template in a project: its scripts, its flow and its triggers, together or not at all. */
|
|
139
|
+
/** Whether a template's triggers bring in items from anyone: making it takes the person's password. */
|
|
140
|
+
export declare function galleryAdmitsAnyone(template: GalleryTemplate): boolean;
|
|
139
141
|
export declare function useGalleryTemplate(store: Store, template: GalleryTemplate, repo: string, given: GalleryAnswers, input: {
|
|
140
142
|
name?: string | null;
|
|
141
143
|
by: string;
|
|
142
144
|
now: Date;
|
|
143
145
|
dir: string | null;
|
|
146
|
+
widening?: "proved";
|
|
144
147
|
}): GalleryUsed;
|
package/dist/flow-gallery.js
CHANGED
|
@@ -20,7 +20,7 @@ import { scanForSecrets } from "./evidence.js";
|
|
|
20
20
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
21
21
|
import { saveScript, validateScript } from "./flow-scripts.js";
|
|
22
22
|
import { isToolrollRepo, planeReviewSteps } from "./flow-starters.js";
|
|
23
|
-
import { addFlowTriggerTo, describeTrigger, githubRepoOf, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
23
|
+
import { addFlowTriggerTo, describeTrigger, githubRepoOf, rawAdmitsAnyone, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
24
24
|
import { choiceTargets, clockTime, flowFromSteps, flowTerms, FLOW_TEMPLATES, ISSUE_LABEL } from "./flows.js";
|
|
25
25
|
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
26
26
|
import { publishingOf } from "./pull-request-flow.js";
|
|
@@ -640,6 +640,10 @@ export function galleryDigest(id, repo, answers) {
|
|
|
640
640
|
return createHash("sha256").update(JSON.stringify([id, repo, Object.entries(answers).sort(([a], [b]) => a.localeCompare(b))])).digest("hex").slice(0, 24);
|
|
641
641
|
}
|
|
642
642
|
/** Use a template in a project: its scripts, its flow and its triggers, together or not at all. */
|
|
643
|
+
/** Whether a template's triggers bring in items from anyone: making it takes the person's password. */
|
|
644
|
+
export function galleryAdmitsAnyone(template) {
|
|
645
|
+
return template.triggers.some(one => rawAdmitsAnyone(one));
|
|
646
|
+
}
|
|
643
647
|
export function useGalleryTemplate(store, template, repo, given, input) {
|
|
644
648
|
try {
|
|
645
649
|
const built = buildFromGallery(store, template, repo, given);
|
|
@@ -656,7 +660,7 @@ export function useGalleryTemplate(store, template, repo, given, input) {
|
|
|
656
660
|
const made = store.getFlow(id);
|
|
657
661
|
for (const trigger of built.triggers) {
|
|
658
662
|
// No one is shown the gallery's addresses: a webhook is made with none until its panel makes one (v126).
|
|
659
|
-
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false });
|
|
663
|
+
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false, ...(input.widening === undefined ? {} : { widening: input.widening }) });
|
|
660
664
|
if (!added.ok)
|
|
661
665
|
throw new Error(added.message);
|
|
662
666
|
if (added.reveal !== null)
|
|
@@ -5,7 +5,10 @@ export type PullRequestIo = {
|
|
|
5
5
|
gh: Runner;
|
|
6
6
|
evidenceRoot?: string;
|
|
7
7
|
};
|
|
8
|
-
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
9
|
-
|
|
8
|
+
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
9
|
+
* The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
|
|
10
|
+
* last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
|
|
11
|
+
* flow can't let an older person decision stand behind a newer machine one. */
|
|
12
|
+
export declare function approvedSinceBuild(store: Store, definition: FlowDefinition, card: FlowCardRow, builtAt?: string | null): string | null;
|
|
10
13
|
/** One pass of a Pull request zone for one card. Returns whether it changed anything. */
|
|
11
14
|
export declare function pullRequestStep(store: Store, flow: FlowRow, definition: FlowDefinition, stage: FlowStage, card: FlowCardRow, now: Date, io: PullRequestIo): Promise<boolean>;
|
|
@@ -19,15 +19,24 @@ import { adoptPublishedPullRequest } from "./publish.js";
|
|
|
19
19
|
import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
|
|
20
20
|
import { requestResultChanges } from "./result-actions.js";
|
|
21
21
|
import { revisionSourceOf } from "./result-review.js";
|
|
22
|
-
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
23
|
-
|
|
22
|
+
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
23
|
+
* The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
|
|
24
|
+
* last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
|
|
25
|
+
* flow can't let an older person decision stand behind a newer machine one. */
|
|
26
|
+
export function approvedSinceBuild(store, definition, card, builtAt = null) {
|
|
24
27
|
const kindOf = (id) => definition.stages.find(one => one.id === id)?.kind ?? null;
|
|
25
28
|
const events = store.flowEvents(card.id);
|
|
26
29
|
let built = -1;
|
|
27
30
|
events.forEach((event, index) => { if (kindOf(event.fromStage) === "task" || kindOf(event.toStage) === "task")
|
|
28
31
|
built = index; });
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// The latest DECISION of any kind governs: only decideFlowCard writes `approved` or `sent-back` (from a decision
|
|
33
|
+
// zone), whatever the zones are called now, so a send-back after a yes takes that yes back.
|
|
34
|
+
const latest = events.slice(built + 1).reverse().find(event => event.outcome === "approved" || event.outcome === "sent-back");
|
|
35
|
+
if (latest === undefined || latest.authority !== "person")
|
|
36
|
+
return null;
|
|
37
|
+
if (builtAt !== null && latest.at < builtAt)
|
|
38
|
+
return null;
|
|
39
|
+
return latest.actor;
|
|
31
40
|
}
|
|
32
41
|
/** One pass of a Pull request zone for one card. Returns whether it changed anything. */
|
|
33
42
|
export async function pullRequestStep(store, flow, definition, stage, card, now, io) {
|
|
@@ -134,7 +143,7 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
134
143
|
// Checks pass.
|
|
135
144
|
if (stage.merge === undefined)
|
|
136
145
|
return passed(`${pr} passed its checks.`, `Pull request: ${link} (checks passed)`);
|
|
137
|
-
const approver = approvedSinceBuild(store, definition, card);
|
|
146
|
+
const approver = approvedSinceBuild(store, definition, card, store.getRun(runId)?.finishedAt ?? null);
|
|
138
147
|
if (approver === null)
|
|
139
148
|
return waiting(`Checks passed on ${pr}, but no person approved this card since it was built, so it wasn't merged. Merge it from its task, or send it through a decision.`);
|
|
140
149
|
const merged = await mergePullRequest(store, { runId, by: `${approver} (approved in ${flow.name})`, exec: io.gh, method: stage.merge, clock: () => now });
|