toolroll 0.9.75 → 0.9.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +241 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +36 -8
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.js +3 -0
- package/dist/batch-checks.js +3 -1
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/builder.d.ts +14 -0
- package/dist/builder.js +141 -42
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +267 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +52 -6
- package/dist/check-report.js +193 -8
- package/dist/checkout-cleanup.js +2 -2
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/coding-workspace.js +7 -5
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +8 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discover.js +6 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +6 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.js +4 -1
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +7 -1
- package/dist/first-run.js +3 -1
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +7 -1
- package/dist/guides.js +4 -3
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +779 -319
- package/dist/planner.js +5 -1
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +23 -3
- package/dist/proof.js +6 -0
- package/dist/pull-request-flow.js +4 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +71 -2
- package/dist/release-adapters.js +471 -12
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +68 -0
- package/dist/release-auto.d.ts +184 -0
- package/dist/release-auto.js +612 -0
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.js +77 -14
- package/dist/release-coverage.js +5 -2
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +6 -1
- package/dist/release.d.ts +207 -6
- package/dist/release.js +495 -20
- package/dist/repository-context.js +4 -6
- package/dist/result-follow-ups.js +3 -1
- package/dist/review-context.js +8 -6
- package/dist/runner.js +2 -1
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +51 -4
- package/dist/scout.js +5 -1
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +101 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +25 -14
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.js +3 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +29 -2
- package/dist/server/task-views.js +15 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +11 -1
- package/dist/store.js +44 -5
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +90 -13
- package/dist/teardown-git.js +1027 -129
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +77 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +2 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console's shared chrome (the sidebar's project states, the Tasks count, the crew list): computed at most once
|
|
3
|
+
* per workspace revision for one reader's exact access, and readable on a read worker (read-executor.ts) as well as on
|
|
4
|
+
* the request thread. Cache metadata only: nothing here grants access. Every entry's key carries every authorization
|
|
5
|
+
* input its computation reads (the reader, their admitted projects, whether they see unplaced work, the projects their
|
|
6
|
+
* sidebar may list), so a value read for one scope is never served to another.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { NO_COUNTS, projectStates, STATE_WORDS } from "./agent-state.js";
|
|
10
|
+
import { boundProjectSummary } from "./browser-workspace.js";
|
|
11
|
+
import { WORK_INDEX_MAX_LIMIT, workIndexPage } from "./work-index.js";
|
|
12
|
+
/** Tasks the rail lists under a project; the rest are a count that links to the project's tasks. */
|
|
13
|
+
const RAIL_TASKS = 6;
|
|
14
|
+
/** Asks a project names for the chime (its count is complete beyond them). */
|
|
15
|
+
const ASKS_PER_PROJECT = 100;
|
|
16
|
+
/** The sidebar's projects with their states (agent-state.ts) for one reader: the page's first read and the summary
|
|
17
|
+
* room's snapshots are this one function, so the two never disagree. Never marks anything seen. Work with no project
|
|
18
|
+
* yet is its own entry when this reader may see it and something counts there. */
|
|
19
|
+
export function projectSummaryOf(store, now, access, actor, projects, evidenceRoot) {
|
|
20
|
+
const states = new Map(projectStates(store, access, actor, now, evidenceRoot, projects.map(one => one.path)).map(one => [one.repo, one]));
|
|
21
|
+
// A row that needs a person or failed says what the Tasks list says it needs (its own next step); a row at work says
|
|
22
|
+
// what its agent did last; a finished one says Done.
|
|
23
|
+
const asking = [...states.values()].flatMap(one => one.families.slice(0, RAIL_TASKS)).filter(one => one.state === "needs-you" || one.state === "failed").map(one => one.rootId);
|
|
24
|
+
const needWords = new Map();
|
|
25
|
+
if (asking.length > 0) {
|
|
26
|
+
try {
|
|
27
|
+
// Every row the rail may list, in pages the index allows.
|
|
28
|
+
for (let at = 0; at < asking.length; at += WORK_INDEX_MAX_LIMIT)
|
|
29
|
+
for (const item of workIndexPage(store, now, access, { roots: asking.slice(at, at + WORK_INDEX_MAX_LIMIT), limit: WORK_INDEX_MAX_LIMIT, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) }).items)
|
|
30
|
+
needWords.set(item.rootId, item.status.detail !== "" ? item.status.detail : item.status.label);
|
|
31
|
+
}
|
|
32
|
+
catch { /* the rows keep their state's words */ }
|
|
33
|
+
}
|
|
34
|
+
// The chime's subjects: an opaque key per obligation and version, scoped to this reader (no words, no ids in it).
|
|
35
|
+
const keyOf = (...parts) => createHash("sha256").update([actor, ...parts].join("\u0000")).digest("base64url").slice(0, 22);
|
|
36
|
+
// Newest first, so a new ask always has a key however many older ones wait (those were heard when they were new).
|
|
37
|
+
const asksOf = (state) => [
|
|
38
|
+
...(state?.families ?? []).flatMap(one => one.ask === undefined ? [] : [{ at: one.ask.at, ask: { key: keyOf("task", one.rootId, one.ask.version), task: one.rootId, run: one.ask.run, at: one.ask.at } }]),
|
|
39
|
+
...(state?.flowAsks ?? []).map(one => ({ at: one.at, ask: { key: keyOf("flow", one.card, one.stage, one.entry), task: null, run: null, at: one.at } })),
|
|
40
|
+
].sort((a, b) => b.at.localeCompare(a.at)).slice(0, ASKS_PER_PROJECT).map(one => one.ask);
|
|
41
|
+
const railOf = (state) => {
|
|
42
|
+
const families = state?.families ?? [];
|
|
43
|
+
const tasks = families.slice(0, RAIL_TASKS).flatMap((one) => {
|
|
44
|
+
const task = store.getTask(one.rootId);
|
|
45
|
+
const line = one.state === "needs-you" || one.state === "failed" ? needWords.get(one.rootId) ?? null : one.line;
|
|
46
|
+
return task === null ? [] : [{ id: one.rootId, title: task.title, href: `/t/${encodeURIComponent(one.rootId)}`, state: one.state, line: line ?? STATE_WORDS[one.state] }];
|
|
47
|
+
});
|
|
48
|
+
return { tasks, more: Math.max(0, families.length - RAIL_TASKS), asks: asksOf(state) };
|
|
49
|
+
};
|
|
50
|
+
const rows = projects.map(one => {
|
|
51
|
+
const state = states.get(one.path);
|
|
52
|
+
return { path: one.path, name: one.name, href: `/work?project=${encodeURIComponent(one.path)}`, state: state?.state ?? "idle", counts: state?.counts ?? { ...NO_COUNTS }, ...railOf(state) };
|
|
53
|
+
});
|
|
54
|
+
const unplaced = states.get(null);
|
|
55
|
+
if (unplaced !== undefined && unplaced.state !== "idle")
|
|
56
|
+
rows.push({ path: null, name: "No project", href: "/work", state: unplaced.state, counts: unplaced.counts, ...railOf(unplaced) });
|
|
57
|
+
return boundProjectSummary(rows);
|
|
58
|
+
}
|
|
59
|
+
/** One access, canonically and in full (every field it has, so a field added later keys too): the same reader and
|
|
60
|
+
* scope always spell the same key, whatever order its projects came in. */
|
|
61
|
+
export function accessKey(access) {
|
|
62
|
+
const record = access;
|
|
63
|
+
return JSON.stringify(Object.keys(record).sort().map(name => {
|
|
64
|
+
const value = record[name];
|
|
65
|
+
return [name, Array.isArray(value) ? [...new Set(value.map(String))].sort() : value === undefined ? null : value];
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Values read from the database, each kept while the workspace revision (workspace-revision.ts: every meaningful
|
|
70
|
+
* commit, from any process, moves it) is the one it was read at, and never past the revision's own clock boundary
|
|
71
|
+
* (a lease or liveness window ending) or `maxAgeMs`, whichever is first — time alone (an agent going quiet) shows
|
|
72
|
+
* within that bound. A value is stored under the revision read before it was computed, so a commit that lands while it
|
|
73
|
+
* is computed only makes it miss sooner. Callers must not change a value they were handed.
|
|
74
|
+
*/
|
|
75
|
+
export class RevisionCache {
|
|
76
|
+
#entries = new Map();
|
|
77
|
+
#revision;
|
|
78
|
+
#clock;
|
|
79
|
+
#maxEntries;
|
|
80
|
+
#maxAgeMs;
|
|
81
|
+
constructor(revision, clock, options = {}) {
|
|
82
|
+
this.#revision = revision;
|
|
83
|
+
this.#clock = clock;
|
|
84
|
+
this.#maxEntries = options.maxEntries ?? 128;
|
|
85
|
+
this.#maxAgeMs = options.maxAgeMs ?? 30_000;
|
|
86
|
+
}
|
|
87
|
+
/** The value for `key` at the current revision, or undefined. */
|
|
88
|
+
peek(key) {
|
|
89
|
+
const entry = this.#entries.get(key);
|
|
90
|
+
if (entry === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
let revision;
|
|
93
|
+
try {
|
|
94
|
+
revision = this.#revision.current();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (entry.revision !== revision || this.#clock().getTime() >= entry.expiresAt) {
|
|
100
|
+
this.#entries.delete(key);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return entry.value;
|
|
104
|
+
}
|
|
105
|
+
/** The cached value, or `compute()` now (stored for the next reader). Errors are never cached. */
|
|
106
|
+
read(key, compute) {
|
|
107
|
+
const hit = this.peek(key);
|
|
108
|
+
if (hit !== undefined)
|
|
109
|
+
return hit;
|
|
110
|
+
const started = this.#started();
|
|
111
|
+
const value = compute();
|
|
112
|
+
if (started !== null)
|
|
113
|
+
this.#store(key, started, value);
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
/** The cached value, or one read asynchronously (a read worker) and stored. A failure answers undefined and leaves
|
|
117
|
+
* the key empty, so the request thread computes it as before. */
|
|
118
|
+
async fill(key, compute) {
|
|
119
|
+
const hit = this.peek(key);
|
|
120
|
+
if (hit !== undefined)
|
|
121
|
+
return hit;
|
|
122
|
+
const started = this.#started();
|
|
123
|
+
if (started === null)
|
|
124
|
+
return undefined;
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = await compute();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
// A worker can finish after an approval changed or a lease expired. Its caller must not pin that old answer
|
|
133
|
+
// into a request even though the shared cache would reject it on the next lookup.
|
|
134
|
+
try {
|
|
135
|
+
if (this.#revision.current() !== started.revision || this.#clock().getTime() >= started.expiresAt)
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
this.#store(key, started, value);
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
#started() {
|
|
145
|
+
try {
|
|
146
|
+
const now = this.#clock();
|
|
147
|
+
return { revision: this.#revision.current(), expiresAt: Math.min(this.#revision.expiresAt(now), now.getTime() + this.#maxAgeMs) };
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
#store(key, started, value) {
|
|
154
|
+
this.#entries.delete(key);
|
|
155
|
+
if (this.#entries.size >= this.#maxEntries)
|
|
156
|
+
this.#entries.delete(this.#entries.keys().next().value);
|
|
157
|
+
this.#entries.set(key, { ...started, value });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** The keys the console's chrome reads are kept under (serve.ts and its read-worker prefetch spell them the same). */
|
|
161
|
+
export const countsKey = (access) => `counts\u0000${accessKey(access)}`;
|
|
162
|
+
export const summaryKey = (actor, access, projects) => `summary\u0000${actor}\u0000${accessKey(access)}\u0000${JSON.stringify(projects.map(one => [one.path, one.name]))}`;
|
|
163
|
+
export const indexKey = (access, options) => {
|
|
164
|
+
const record = options;
|
|
165
|
+
return `index\u0000${accessKey(access)}\u0000${JSON.stringify(Object.keys(record).sort().map(name => [name, record[name] ?? null]))}`;
|
|
166
|
+
};
|
|
@@ -72,6 +72,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
72
72
|
readonly code: FormContract<"acceptance" | "answers" | "base" | "candidate" | "decision" | "goal" | "model" | "password" | "prompt" | "repo" | "requestId" | "title" | "visual" | `question:${string}`>;
|
|
73
73
|
readonly sweep: FormContract<"confirm" | "snapshot">;
|
|
74
74
|
readonly passwordless: FormContract<"action" | "password">;
|
|
75
|
+
readonly releaseApproval: FormContract<"action" | "password" | "repo">;
|
|
75
76
|
readonly term: FormContract<"password" | "program" | "repo">;
|
|
76
77
|
readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
|
|
77
78
|
readonly skillsChange: FormContract<"action" | "content" | "files" | "identity" | "method" | "nonce" | "repo" | "restore" | "revision" | "sample" | "sha" | "url">;
|
|
@@ -81,6 +81,7 @@ export const CONSOLE_FORMS = {
|
|
|
81
81
|
code: formContract("POST /code, /code/start, /code/<session>/<act>, /code/<anything else>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
|
|
82
82
|
sweep: formContract("POST /sweeps/<sweep>/cancel-rest", ["confirm", "snapshot"]),
|
|
83
83
|
passwordless: formContract("POST /settings/passwordless", ["action", "password"]),
|
|
84
|
+
releaseApproval: formContract("POST /settings/release-approval", ["repo", "action", "password"]),
|
|
84
85
|
term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
|
|
85
86
|
skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
|
|
86
87
|
skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
|
|
@@ -43,6 +43,10 @@ export declare const checkReportSchema: z.ZodObject<{
|
|
|
43
43
|
ran: z.ZodBoolean;
|
|
44
44
|
code: z.ZodNullable<z.ZodInt>;
|
|
45
45
|
timedOut: z.ZodBoolean;
|
|
46
|
+
startedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
47
|
+
signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
48
|
+
stopped: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
spawnFailed: z.ZodOptional<z.ZodBoolean>;
|
|
46
50
|
}, z.core.$strict>>;
|
|
47
51
|
units: z.ZodNullable<z.ZodObject<{
|
|
48
52
|
reusedFromCI: z.ZodBoolean;
|
|
@@ -81,6 +85,7 @@ export declare const checkReportSchema: z.ZodObject<{
|
|
|
81
85
|
scripted: "scripted";
|
|
82
86
|
}>;
|
|
83
87
|
finished: z.ZodBoolean;
|
|
88
|
+
inventoried: z.ZodOptional<z.ZodBoolean>;
|
|
84
89
|
groups: z.ZodArray<z.ZodObject<{
|
|
85
90
|
group: z.ZodString;
|
|
86
91
|
state: z.ZodEnum<{
|
|
@@ -102,6 +107,7 @@ export declare const checkReportSchema: z.ZodObject<{
|
|
|
102
107
|
}, z.core.$strict>>;
|
|
103
108
|
upgrades: z.ZodNullable<z.ZodObject<{
|
|
104
109
|
finished: z.ZodBoolean;
|
|
110
|
+
inventoried: z.ZodOptional<z.ZodBoolean>;
|
|
105
111
|
versions: z.ZodArray<z.ZodObject<{
|
|
106
112
|
version: z.ZodString;
|
|
107
113
|
state: z.ZodEnum<{
|
|
@@ -130,6 +136,8 @@ export declare const checkInventorySchema: z.ZodObject<{
|
|
|
130
136
|
unit: "unit";
|
|
131
137
|
upgrade: "upgrade";
|
|
132
138
|
}>>;
|
|
139
|
+
ciSubstitute: z.ZodOptional<z.ZodBoolean>;
|
|
140
|
+
release: z.ZodOptional<z.ZodBoolean>;
|
|
133
141
|
}, z.core.$strict>;
|
|
134
142
|
export type CheckInventory = z.infer<typeof checkInventorySchema>;
|
|
135
143
|
/** The sealed inventory artifact: what was sealed before the run, and what the run declared. */
|
|
@@ -148,6 +156,8 @@ export declare const sealedInventorySchema: z.ZodObject<{
|
|
|
148
156
|
unit: "unit";
|
|
149
157
|
upgrade: "upgrade";
|
|
150
158
|
}>>;
|
|
159
|
+
ciSubstitute: z.ZodOptional<z.ZodBoolean>;
|
|
160
|
+
release: z.ZodOptional<z.ZodBoolean>;
|
|
151
161
|
}, z.core.$strict>;
|
|
152
162
|
declared: z.ZodObject<{
|
|
153
163
|
groups: z.ZodArray<z.ZodObject<{
|
|
@@ -229,6 +239,8 @@ export type CheckHandshake = {
|
|
|
229
239
|
mode: "full" | "targeted";
|
|
230
240
|
plan: TargetedPlan | null;
|
|
231
241
|
watch: string[];
|
|
242
|
+
owed?: CheckPart[];
|
|
243
|
+
ciSubstitute?: boolean;
|
|
232
244
|
};
|
|
233
245
|
/** A unit test: its file and full name. */
|
|
234
246
|
export declare const unitIdentity: (file: string, testName: string) => string;
|
|
@@ -38,6 +38,12 @@ export const checkReportSchema = versioned(1, {
|
|
|
38
38
|
ran: z.boolean(),
|
|
39
39
|
code: z.int().min(0).max(255).nullable(),
|
|
40
40
|
timedOut: z.boolean(),
|
|
41
|
+
/** When it started, the signal that ended it, whether the check stopped it (another part's failure or the check's
|
|
42
|
+
* own limit) and whether it could start at all — each apart from its code. Absent from an older orchestrator. */
|
|
43
|
+
startedAt: z.string().max(40).nullable().optional(),
|
|
44
|
+
signal: z.string().max(20).nullable().optional(),
|
|
45
|
+
stopped: z.boolean().optional(),
|
|
46
|
+
spawnFailed: z.boolean().optional(),
|
|
41
47
|
})).max(CHECK_PARTS.length),
|
|
42
48
|
/** The unit suite, from the Vitest main process's fd-3 lines: every file's outcome, and the identities that failed,
|
|
43
49
|
* were skipped, passed on a retry or were asked about. Null when the unit part didn't run. */
|
|
@@ -65,6 +71,9 @@ export const checkReportSchema = versioned(1, {
|
|
|
65
71
|
script: z.enum(JOURNEY_SCRIPTS),
|
|
66
72
|
kind: z.enum(["scripted", "real"]),
|
|
67
73
|
finished: z.boolean(),
|
|
74
|
+
/** The runner listed its groups before any started (an empty list is an explicit "none of this kind"); absent or
|
|
75
|
+
* false, the run has no inventory and nothing it lacks can be told from nothing it owed. */
|
|
76
|
+
inventoried: z.boolean().optional(),
|
|
68
77
|
groups: z.array(z.strictObject({
|
|
69
78
|
group: groupName,
|
|
70
79
|
state: z.enum(["passed", "failed", "not-run"]),
|
|
@@ -77,6 +86,8 @@ export const checkReportSchema = versioned(1, {
|
|
|
77
86
|
})).max(4),
|
|
78
87
|
upgrades: z.strictObject({
|
|
79
88
|
finished: z.boolean(),
|
|
89
|
+
/** The upgrade path listed its versions before any ran. */
|
|
90
|
+
inventoried: z.boolean().optional(),
|
|
80
91
|
versions: z.array(z.strictObject({ version: versionName, state: z.enum(["passed", "failed", "not-run"]) })).max(500),
|
|
81
92
|
}).nullable(),
|
|
82
93
|
});
|
|
@@ -88,7 +99,13 @@ export function readCheckReport(input) {
|
|
|
88
99
|
* sealed beside these after it ran. */
|
|
89
100
|
export const checkInventorySchema = versioned(1, {
|
|
90
101
|
units: z.array(repoPath).max(20_000),
|
|
102
|
+
/** The parts the check owes. For a full check the plane decides them before it runs (check-report.ts
|
|
103
|
+
* fullObligationsOf) and they ride on the handshake; every one needs an outcome. */
|
|
91
104
|
parts: z.array(z.enum(CHECK_PARTS)).max(CHECK_PARTS.length),
|
|
105
|
+
/** Whether CI's run of the exact commit may stand in for the local unit suite. Absent is no. */
|
|
106
|
+
ciSubstitute: z.boolean().optional(),
|
|
107
|
+
/** Whether the plane prepared it as a release's check (sealed with the attempt; absent on older inventories). */
|
|
108
|
+
release: z.boolean().optional(),
|
|
92
109
|
});
|
|
93
110
|
/** The sealed inventory artifact: what was sealed before the run, and what the run declared. */
|
|
94
111
|
export const sealedInventorySchema = versioned(1, {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { type ContractResult } from "./contract.js";
|
|
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"];
|
|
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", "no-result", "stopped-by-toolroll", "baseline-unverifiable"];
|
|
17
17
|
/**
|
|
18
18
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
19
19
|
* it (`attemptFailed`, which downgrades to `short` rather than `refuted`: "we could not check" is not "the claim is
|
|
@@ -26,12 +26,14 @@ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<
|
|
|
26
26
|
ran: z.ZodLiteral<false>;
|
|
27
27
|
attemptFailed: z.ZodLiteral<true>;
|
|
28
28
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
"baseline-unverifiable": "baseline-unverifiable";
|
|
29
30
|
"checkout-moved": "checkout-moved";
|
|
30
31
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
31
32
|
"coverage-incomplete": "coverage-incomplete";
|
|
32
33
|
"custody-lost": "custody-lost";
|
|
33
34
|
"dependency-missing": "dependency-missing";
|
|
34
35
|
"dependency-still-missing": "dependency-still-missing";
|
|
36
|
+
"no-result": "no-result";
|
|
35
37
|
"own-install-failed": "own-install-failed";
|
|
36
38
|
"retry-spawn-failed": "retry-spawn-failed";
|
|
37
39
|
"retry-timed-out": "retry-timed-out";
|
|
@@ -39,6 +41,7 @@ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<
|
|
|
39
41
|
"setup-failed": "setup-failed";
|
|
40
42
|
"setup-stale": "setup-stale";
|
|
41
43
|
"spawn-failed": "spawn-failed";
|
|
44
|
+
"stopped-by-toolroll": "stopped-by-toolroll";
|
|
42
45
|
"timed-out": "timed-out";
|
|
43
46
|
"tracked-files-changed": "tracked-files-changed";
|
|
44
47
|
}>>;
|
|
@@ -47,6 +50,11 @@ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<
|
|
|
47
50
|
ran: z.ZodLiteral<true>;
|
|
48
51
|
exitCode: z.ZodInt;
|
|
49
52
|
setupReplayed: z.ZodOptional<z.ZodLiteral<true>>;
|
|
53
|
+
firstAttempt: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
class: z.ZodLiteral<"launch">;
|
|
55
|
+
program: z.ZodString;
|
|
56
|
+
exitCode: z.ZodInt;
|
|
57
|
+
}, z.core.$strict>>;
|
|
50
58
|
}, z.core.$strict>]>;
|
|
51
59
|
export type VerifyCommandFacts = z.infer<typeof verifyCommandFactsSchema>;
|
|
52
60
|
/** The approved verification grant the gate ran, exactly as the store held it. */
|
|
@@ -76,7 +84,7 @@ export declare const receiptLogSchema: z.ZodObject<{
|
|
|
76
84
|
}>>;
|
|
77
85
|
}, z.core.$strict>;
|
|
78
86
|
export declare const RECEIPT_VERSION = 3;
|
|
79
|
-
export declare const RECEIPT_COVERAGES: readonly ["full", "composed", "partial", "identity-removed"];
|
|
87
|
+
export declare const RECEIPT_COVERAGES: readonly ["full", "composed", "partial", "identity-removed", "no-result"];
|
|
80
88
|
export type ReceiptCoverage = typeof RECEIPT_COVERAGES[number];
|
|
81
89
|
/** The receipt as read: a reused gate names its source run and digest; a direct one names neither. */
|
|
82
90
|
export declare const verificationReceiptSchema: z.ZodObject<{
|
|
@@ -109,12 +117,14 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
109
117
|
ran: z.ZodLiteral<false>;
|
|
110
118
|
attemptFailed: z.ZodLiteral<true>;
|
|
111
119
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
120
|
+
"baseline-unverifiable": "baseline-unverifiable";
|
|
112
121
|
"checkout-moved": "checkout-moved";
|
|
113
122
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
114
123
|
"coverage-incomplete": "coverage-incomplete";
|
|
115
124
|
"custody-lost": "custody-lost";
|
|
116
125
|
"dependency-missing": "dependency-missing";
|
|
117
126
|
"dependency-still-missing": "dependency-still-missing";
|
|
127
|
+
"no-result": "no-result";
|
|
118
128
|
"own-install-failed": "own-install-failed";
|
|
119
129
|
"retry-spawn-failed": "retry-spawn-failed";
|
|
120
130
|
"retry-timed-out": "retry-timed-out";
|
|
@@ -122,6 +132,7 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
122
132
|
"setup-failed": "setup-failed";
|
|
123
133
|
"setup-stale": "setup-stale";
|
|
124
134
|
"spawn-failed": "spawn-failed";
|
|
135
|
+
"stopped-by-toolroll": "stopped-by-toolroll";
|
|
125
136
|
"timed-out": "timed-out";
|
|
126
137
|
"tracked-files-changed": "tracked-files-changed";
|
|
127
138
|
}>>;
|
|
@@ -130,6 +141,11 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
130
141
|
ran: z.ZodLiteral<true>;
|
|
131
142
|
exitCode: z.ZodInt;
|
|
132
143
|
setupReplayed: z.ZodOptional<z.ZodLiteral<true>>;
|
|
144
|
+
firstAttempt: z.ZodOptional<z.ZodObject<{
|
|
145
|
+
class: z.ZodLiteral<"launch">;
|
|
146
|
+
program: z.ZodString;
|
|
147
|
+
exitCode: z.ZodInt;
|
|
148
|
+
}, z.core.$strict>>;
|
|
133
149
|
}, z.core.$strict>]>;
|
|
134
150
|
log: z.ZodObject<{
|
|
135
151
|
artifactId: z.ZodInt;
|
|
@@ -147,6 +163,7 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
147
163
|
composed: "composed";
|
|
148
164
|
full: "full";
|
|
149
165
|
"identity-removed": "identity-removed";
|
|
166
|
+
"no-result": "no-result";
|
|
150
167
|
partial: "partial";
|
|
151
168
|
}>>;
|
|
152
169
|
check: z.ZodOptional<z.ZodObject<{
|
|
@@ -157,6 +174,7 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
157
174
|
report: z.ZodNullable<z.ZodString>;
|
|
158
175
|
inventory: z.ZodNullable<z.ZodString>;
|
|
159
176
|
exitCode: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
|
|
177
|
+
retried: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
160
178
|
}, z.core.$strict>>;
|
|
161
179
|
composed: z.ZodOptional<z.ZodObject<{
|
|
162
180
|
base: z.ZodInt;
|
|
@@ -30,6 +30,12 @@ export const VERIFICATION_FAILURES = [
|
|
|
30
30
|
"custody-lost",
|
|
31
31
|
// A release check that didn't cover what it owed, or lost a required test (release-once.md): never a pass.
|
|
32
32
|
"coverage-incomplete",
|
|
33
|
+
// A Full release check with no complete, valid result (check-report.ts fullCheckProblem): an error, never a pass.
|
|
34
|
+
"no-result",
|
|
35
|
+
// Toolroll's own shutdown stopped the check (its hard stop): it never finished.
|
|
36
|
+
"stopped-by-toolroll",
|
|
37
|
+
// A Full check whose recorded last release couldn't be verified: what it owed is unknown, so it never ran.
|
|
38
|
+
"baseline-unverifiable",
|
|
33
39
|
];
|
|
34
40
|
/**
|
|
35
41
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
@@ -39,7 +45,10 @@ export const VERIFICATION_FAILURES = [
|
|
|
39
45
|
export const verifyCommandFactsSchema = z.union([
|
|
40
46
|
z.strictObject({ configured: z.literal(false) }),
|
|
41
47
|
z.strictObject({ configured: z.literal(true), ran: z.literal(false), attemptFailed: z.literal(true), failure: z.enum(VERIFICATION_FAILURES).optional() }),
|
|
42
|
-
z.strictObject({ configured: z.literal(true), ran: z.literal(true), exitCode: z.int().min(0).max(255), setupReplayed: z.literal(true).optional()
|
|
48
|
+
z.strictObject({ configured: z.literal(true), ran: z.literal(true), exitCode: z.int().min(0).max(255), setupReplayed: z.literal(true).optional(),
|
|
49
|
+
// The first attempt of a check rerun after the approved setup, classified on its own (eval-harness review 2,
|
|
50
|
+
// finding 5): only a launch failure — its program missing — is ever replayed.
|
|
51
|
+
firstAttempt: z.strictObject({ class: z.literal("launch"), program: z.string().min(1).max(200), exitCode: z.int().min(0).max(255) }).optional() }),
|
|
43
52
|
]);
|
|
44
53
|
/** The approved verification grant the gate ran, exactly as the store held it. */
|
|
45
54
|
export const receiptCommandSchema = z.strictObject({
|
|
@@ -65,7 +74,7 @@ export const receiptLogSchema = z.strictObject({
|
|
|
65
74
|
captureStatus: z.enum(["ok", "failed"]).nullable(),
|
|
66
75
|
});
|
|
67
76
|
export const RECEIPT_VERSION = 3;
|
|
68
|
-
export const RECEIPT_COVERAGES = ["full", "composed", "partial", "identity-removed"];
|
|
77
|
+
export const RECEIPT_COVERAGES = ["full", "composed", "partial", "identity-removed", "no-result"];
|
|
69
78
|
const receiptFields = {
|
|
70
79
|
run: z.int(),
|
|
71
80
|
head: z.string(),
|
|
@@ -85,7 +94,10 @@ export const verificationReceiptSchema = versioned(RECEIPT_VERSION, {
|
|
|
85
94
|
/** Version 3: the attempt sealed before the check ran, and its sealed report (null: it gave none). */
|
|
86
95
|
check: z.strictObject({ attempt: z.enum(["full", "targeted"]), report: z.string().nullable(), inventory: z.string().nullable(),
|
|
87
96
|
/** 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()
|
|
97
|
+
exitCode: z.int().min(0).max(255).nullable().optional(),
|
|
98
|
+
/** What passed only on a retry inside the check (check-failure.ts retriedInside), sealed here from its report: null
|
|
99
|
+
* when it gave no valid report, so nobody can tell (unknown, never "no retries"). Absent on older receipts. */
|
|
100
|
+
retried: z.array(z.string().min(1).max(800)).max(6000).nullable().optional() }).optional(),
|
|
89
101
|
/** Version 3, composed: the full check A and every link the reader proves again. */
|
|
90
102
|
composed: z.strictObject({
|
|
91
103
|
base: z.int().min(1), baseReceipt: z.string(), baseReport: z.string(), authority: z.int().min(1), carries: z.array(z.int()),
|
|
@@ -37,12 +37,14 @@ export declare const legacyGateViewSchema: z.ZodObject<{
|
|
|
37
37
|
ran: z.ZodLiteral<false>;
|
|
38
38
|
attemptFailed: z.ZodLiteral<true>;
|
|
39
39
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
40
|
+
"baseline-unverifiable": "baseline-unverifiable";
|
|
40
41
|
"checkout-moved": "checkout-moved";
|
|
41
42
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
42
43
|
"coverage-incomplete": "coverage-incomplete";
|
|
43
44
|
"custody-lost": "custody-lost";
|
|
44
45
|
"dependency-missing": "dependency-missing";
|
|
45
46
|
"dependency-still-missing": "dependency-still-missing";
|
|
47
|
+
"no-result": "no-result";
|
|
46
48
|
"own-install-failed": "own-install-failed";
|
|
47
49
|
"retry-spawn-failed": "retry-spawn-failed";
|
|
48
50
|
"retry-timed-out": "retry-timed-out";
|
|
@@ -50,6 +52,7 @@ export declare const legacyGateViewSchema: z.ZodObject<{
|
|
|
50
52
|
"setup-failed": "setup-failed";
|
|
51
53
|
"setup-stale": "setup-stale";
|
|
52
54
|
"spawn-failed": "spawn-failed";
|
|
55
|
+
"stopped-by-toolroll": "stopped-by-toolroll";
|
|
53
56
|
"timed-out": "timed-out";
|
|
54
57
|
"tracked-files-changed": "tracked-files-changed";
|
|
55
58
|
}>>;
|
|
@@ -58,6 +61,11 @@ export declare const legacyGateViewSchema: z.ZodObject<{
|
|
|
58
61
|
ran: z.ZodLiteral<true>;
|
|
59
62
|
exitCode: z.ZodInt;
|
|
60
63
|
setupReplayed: z.ZodOptional<z.ZodLiteral<true>>;
|
|
64
|
+
firstAttempt: z.ZodOptional<z.ZodObject<{
|
|
65
|
+
class: z.ZodLiteral<"launch">;
|
|
66
|
+
program: z.ZodString;
|
|
67
|
+
exitCode: z.ZodInt;
|
|
68
|
+
}, z.core.$strict>>;
|
|
61
69
|
}, z.core.$strict>]>;
|
|
62
70
|
log: z.ZodObject<{
|
|
63
71
|
artifactId: z.ZodInt;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
1
2
|
export type ControllerState = {
|
|
2
3
|
phase: "starting" | "running" | "backoff" | "stopped";
|
|
3
4
|
generation: number;
|
|
@@ -27,4 +28,11 @@ export declare function superviseController(args: {
|
|
|
27
28
|
maxRetryMs?: number;
|
|
28
29
|
stableMs?: number;
|
|
29
30
|
shutdownMs?: number;
|
|
31
|
+
/** An IPC channel to each controller (fd 3), for a parent that speaks to it (`toolroll up`'s console and worker). */
|
|
32
|
+
ipc?: boolean;
|
|
33
|
+
/** Prefix every line the controller writes, sent to this process's stderr: two controllers' logs stay apart. An
|
|
34
|
+
* empty label sends its lines there unprefixed (a JSON log keeps one object a line). */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Each spawned controller's handle, before it runs: the caller listens to its messages and may signal it. */
|
|
37
|
+
onSpawn?: (child: ChildProcess, generation: number) => void;
|
|
30
38
|
}): Promise<void>;
|
|
@@ -56,13 +56,23 @@ export async function superviseController(args) {
|
|
|
56
56
|
if (args.signal.aborted)
|
|
57
57
|
break;
|
|
58
58
|
const exited = await new Promise(done => {
|
|
59
|
+
const output = args.label === undefined ? "inherit" : "pipe";
|
|
59
60
|
const owned = spawn(args.file, [...args.argv], {
|
|
60
61
|
...(args.cwd === undefined ? {} : { cwd: args.cwd }),
|
|
61
62
|
...(args.env === undefined ? {} : { env: args.env }),
|
|
62
|
-
stdio: "inherit",
|
|
63
|
+
stdio: args.ipc === true ? ["ignore", output, output, "ipc"] : args.label === undefined ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
63
64
|
windowsHide: true,
|
|
64
65
|
});
|
|
65
66
|
child = owned;
|
|
67
|
+
if (args.label !== undefined)
|
|
68
|
+
for (const stream of [owned.stdout, owned.stderr])
|
|
69
|
+
labelLines(stream, args.label);
|
|
70
|
+
try {
|
|
71
|
+
args.onSpawn?.(owned, generation);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
process.stderr.write("Controller supervisor could not attach to its controller.\n");
|
|
75
|
+
}
|
|
66
76
|
owned.once("spawn", () => {
|
|
67
77
|
emit({ phase: "running", generation, controllerPid: owned.pid ?? null });
|
|
68
78
|
if (args.signal.aborted)
|
|
@@ -102,3 +112,25 @@ export async function superviseController(args) {
|
|
|
102
112
|
emit({ phase: "stopped", generation, controllerPid: null });
|
|
103
113
|
}
|
|
104
114
|
}
|
|
115
|
+
/** Each whole line of a controller's output, prefixed with its label, on this process's stderr. */
|
|
116
|
+
function labelLines(stream, label) {
|
|
117
|
+
if (stream === null)
|
|
118
|
+
return;
|
|
119
|
+
const prefix = label === "" ? "" : `${label} `;
|
|
120
|
+
let pending = "";
|
|
121
|
+
stream.setEncoding("utf8");
|
|
122
|
+
stream.on("data", (chunk) => {
|
|
123
|
+
const lines = (pending + chunk).split("\n");
|
|
124
|
+
pending = lines.pop() ?? "";
|
|
125
|
+
// Bounded: a controller that never ends a line still reaches the log.
|
|
126
|
+
if (pending.length > 64 * 1024) {
|
|
127
|
+
lines.push(pending);
|
|
128
|
+
pending = "";
|
|
129
|
+
}
|
|
130
|
+
for (const line of lines)
|
|
131
|
+
process.stderr.write(`${prefix}${line}\n`);
|
|
132
|
+
});
|
|
133
|
+
stream.on("end", () => { if (pending !== "")
|
|
134
|
+
process.stderr.write(`${prefix}${pending}\n`); pending = ""; });
|
|
135
|
+
stream.on("error", () => { });
|
|
136
|
+
}
|
package/dist/demo.js
CHANGED
|
@@ -989,11 +989,12 @@ export function makeDemoRepos(root) {
|
|
|
989
989
|
writeFileSync(join(dir, file), content);
|
|
990
990
|
}
|
|
991
991
|
try {
|
|
992
|
-
execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir, stdio: "ignore" });
|
|
993
|
-
execFileSync("git", ["add", "-A"], { cwd: dir, stdio: "ignore" });
|
|
992
|
+
execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir, stdio: "ignore", timeout: 30_000 });
|
|
993
|
+
execFileSync("git", ["add", "-A"], { cwd: dir, stdio: "ignore", timeout: 30_000 });
|
|
994
994
|
execFileSync("git", ["-c", "user.email=demo@localhost", "-c", "user.name=demo", "commit", "-q", "-m", "seed"], {
|
|
995
995
|
cwd: dir,
|
|
996
996
|
stdio: "ignore",
|
|
997
|
+
timeout: 30_000,
|
|
997
998
|
});
|
|
998
999
|
}
|
|
999
1000
|
catch {
|
package/dist/deploy-gate.d.ts
CHANGED
|
@@ -14,4 +14,7 @@ export declare function deploymentGate(store: Store, input: {
|
|
|
14
14
|
runId: number;
|
|
15
15
|
evidenceRoot: string;
|
|
16
16
|
now: Date;
|
|
17
|
+
/** Inside `toolroll release` (its own reserved deploy): a release's own automatic approval also stands
|
|
18
|
+
* (docs/plans/auto-approve-green.md). Anywhere else only a person's approval deploys. */
|
|
19
|
+
releaseFlow?: boolean;
|
|
17
20
|
}): DeploymentGate;
|
package/dist/deploy-gate.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* same checks for the deployment script, proven again there with the installed runtime's modules.)
|
|
7
7
|
*/
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
|
-
import { liveReleaseOf, personApprovalOf } from "./scope.js";
|
|
9
|
+
import { liveReleaseOf, personApprovalOf, releaseApprovalOf } from "./scope.js";
|
|
10
10
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
11
|
+
import { releaseCiRunningOf } from "./release-ci-hold.js";
|
|
11
12
|
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
12
13
|
export function deploymentGate(store, input) {
|
|
13
14
|
const refuse = (message) => ({ ok: false, message });
|
|
@@ -27,13 +28,18 @@ export function deploymentGate(store, input) {
|
|
|
27
28
|
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
28
29
|
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
|
29
30
|
return refuse("The run's signed scope is no longer the approved one.");
|
|
31
|
+
// A person may approve during CI, including a release without carry terms. Every deployment road must wait
|
|
32
|
+
// for the plane's exact-head observation, not only the release coordinator's polling loop.
|
|
33
|
+
if (releaseCiRunningOf(store, ref.externalId) !== null)
|
|
34
|
+
return refuse("This release still waits for GitHub CI. Resume it with `toolroll release`.");
|
|
30
35
|
// One approval per release (release-once.md): a release's task deploys only while its authority stands for it — the
|
|
31
36
|
// live predicate proves the anchor's person approval (a carried commit's own seal is never a person's). Any other task
|
|
32
37
|
// needs a person's approval of its own.
|
|
33
38
|
const release = liveReleaseOf(store, ref.externalId, "deploy", input.now);
|
|
34
39
|
if (release.applies && !release.ok)
|
|
35
40
|
return refuse(`This release can't deploy: ${release.why}.`);
|
|
36
|
-
const anchorPerson = release.applies && release.ok
|
|
41
|
+
const anchorPerson = release.applies && release.ok
|
|
42
|
+
? (input.releaseFlow === true ? releaseApprovalOf(store, release.authority.anchorTask, input.now) : personApprovalOf(store, release.authority.anchorTask)) : null;
|
|
37
43
|
const person = anchorPerson ?? personApprovalOf(store, ref.externalId);
|
|
38
44
|
if (!person.ok)
|
|
39
45
|
return refuse(`A person has to approve this candidate's scope: ${person.why}.`);
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
|
|
|
97
97
|
export declare function lockHeld(file: string): boolean;
|
|
98
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
99
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
100
|
-
version?: never;
|
|
101
100
|
active: boolean;
|
|
102
101
|
phase: string;
|
|
103
102
|
detail: string;
|
|
@@ -111,6 +110,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
111
110
|
wasRunning?: never;
|
|
112
111
|
backupPath?: never;
|
|
113
112
|
updateId?: never;
|
|
113
|
+
version?: never;
|
|
114
114
|
buildId?: never;
|
|
115
115
|
workDir?: never;
|
|
116
116
|
} | {
|
package/dist/discover.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { basename } from "node:path";
|
|
21
21
|
import { run } from "./exec.js";
|
|
22
|
+
import { checkoutGit } from "./teardown-git.js";
|
|
22
23
|
import { parseWorktrees, parseBranches } from "./git.js";
|
|
23
24
|
import { findRepos } from "./scan.js";
|
|
24
25
|
/** Enough to keep git busy without turning a laptop fan on. */
|
|
@@ -43,7 +44,11 @@ const BRANCH_FORMAT = "--format=%(refname:short)%09%(upstream:track)%09%(committ
|
|
|
43
44
|
const BRANCH_FORMAT_CHEAP = "--format=%(refname:short)%09%09%(committerdate:iso8601-strict)";
|
|
44
45
|
export async function inspectRepo(path, options = {}) {
|
|
45
46
|
const { runner = run, dirty = false } = options;
|
|
46
|
-
|
|
47
|
+
// Scanned and enrolled projects share their repository with every build, which can write its config, info and hooks
|
|
48
|
+
// (`--dirty`'s status would run a planted fsmonitor or filter): the hardened git, against a frozen view of the
|
|
49
|
+
// repository (teardown-git.ts checkoutGit). An injected runner (tests) is used as given.
|
|
50
|
+
const hardened = runner === run ? checkoutGit(run, "build") : runner;
|
|
51
|
+
const git = (args, timeoutMs = REF_TIMEOUT_MS) => hardened(GIT, [...READ_ONLY, ...args], { cwd: path, timeoutMs });
|
|
47
52
|
const [worktrees, branchRead, head, remote, status] = await Promise.all([
|
|
48
53
|
git(["worktree", "list", "--porcelain"]),
|
|
49
54
|
readBranches(git),
|
package/dist/dispose.d.ts
CHANGED