toolroll 0.9.76 → 0.9.78

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +46 -10
  5. package/dist/assignment.js +3 -0
  6. package/dist/backup-ui.d.ts +9 -1
  7. package/dist/backup-ui.js +28 -3
  8. package/dist/backup.js +4 -3
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  10. package/dist/browser/workspace.css +2 -2
  11. package/dist/browser/workspace.js +31 -31
  12. package/dist/browser-workspace.d.ts +2 -0
  13. package/dist/builder.d.ts +18 -3
  14. package/dist/builder.js +152 -47
  15. package/dist/chat-decide.js +4 -0
  16. package/dist/check-failure.d.ts +92 -0
  17. package/dist/check-failure.js +295 -0
  18. package/dist/check-progress.d.ts +28 -1
  19. package/dist/check-progress.js +105 -6
  20. package/dist/check-report.d.ts +67 -6
  21. package/dist/check-report.js +254 -9
  22. package/dist/checkout-cleanup.js +2 -2
  23. package/dist/claim.d.ts +9 -0
  24. package/dist/claim.js +9 -1
  25. package/dist/cli.js +8 -5
  26. package/dist/coding-handoff.d.ts +1 -0
  27. package/dist/coding-handoff.js +20 -11
  28. package/dist/commit-extract.d.ts +6 -0
  29. package/dist/commit-extract.js +63 -8
  30. package/dist/console-chrome.d.ts +41 -0
  31. package/dist/console-chrome.js +166 -0
  32. package/dist/contracts/console-api.d.ts +1 -1
  33. package/dist/contracts/console-api.js +1 -1
  34. package/dist/contracts/release-check.d.ts +12 -0
  35. package/dist/contracts/release-check.js +17 -0
  36. package/dist/contracts/release-terms.d.ts +41 -0
  37. package/dist/contracts/release-terms.js +53 -0
  38. package/dist/contracts/verification-receipt.d.ts +20 -2
  39. package/dist/contracts/verification-receipt.js +15 -3
  40. package/dist/contracts/verification-view.d.ts +8 -0
  41. package/dist/controller-service.js +4 -1
  42. package/dist/controller-supervisor.d.ts +8 -0
  43. package/dist/controller-supervisor.js +33 -1
  44. package/dist/demo.js +3 -2
  45. package/dist/deploy-gate.js +5 -0
  46. package/dist/deploy-rescue.d.ts +41 -0
  47. package/dist/deploy-rescue.js +94 -0
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/dispose.d.ts +3 -0
  50. package/dist/dispose.js +9 -5
  51. package/dist/doctor.d.ts +7 -0
  52. package/dist/doctor.js +10 -2
  53. package/dist/envelope.js +2 -1
  54. package/dist/evidence.d.ts +9 -0
  55. package/dist/evidence.js +19 -4
  56. package/dist/exec.d.ts +9 -0
  57. package/dist/exec.js +22 -2
  58. package/dist/git-floor.d.ts +16 -0
  59. package/dist/git-floor.js +36 -0
  60. package/dist/grant.js +5 -1
  61. package/dist/guides.js +22 -11
  62. package/dist/latency-timeline.d.ts +46 -0
  63. package/dist/latency-timeline.js +120 -0
  64. package/dist/live-backup.d.ts +39 -0
  65. package/dist/live-backup.js +271 -0
  66. package/dist/operate.d.ts +13 -0
  67. package/dist/operate.js +969 -328
  68. package/dist/planner.js +2 -2
  69. package/dist/prepared-evidence.d.ts +2 -1
  70. package/dist/prepared-evidence.js +8 -3
  71. package/dist/project-knowledge.js +6 -2
  72. package/dist/project-learning.js +2 -2
  73. package/dist/proof.js +6 -0
  74. package/dist/read-executor-worker.js +8 -4
  75. package/dist/read-executor.d.ts +18 -1
  76. package/dist/read-executor.js +8 -0
  77. package/dist/release-adapters.d.ts +80 -2
  78. package/dist/release-adapters.js +441 -16
  79. package/dist/release-authority.js +6 -2
  80. package/dist/release-auto.d.ts +38 -7
  81. package/dist/release-auto.js +131 -15
  82. package/dist/release-card.d.ts +15 -3
  83. package/dist/release-card.js +25 -9
  84. package/dist/release-carry.d.ts +21 -1
  85. package/dist/release-carry.js +22 -0
  86. package/dist/release-ci-hold.d.ts +41 -0
  87. package/dist/release-ci-hold.js +53 -0
  88. package/dist/release-cli.d.ts +3 -0
  89. package/dist/release-cli.js +95 -17
  90. package/dist/release-coverage.js +5 -2
  91. package/dist/release-gate.d.ts +5 -0
  92. package/dist/release-gate.js +1 -1
  93. package/dist/release-recheck.d.ts +1 -1
  94. package/dist/release-recheck.js +8 -2
  95. package/dist/release.d.ts +144 -14
  96. package/dist/release.js +421 -81
  97. package/dist/review-context.js +8 -6
  98. package/dist/runner-process.d.ts +67 -0
  99. package/dist/runner-process.js +219 -0
  100. package/dist/runner.d.ts +48 -6
  101. package/dist/runner.js +73 -9
  102. package/dist/sandbox-runtime.d.ts +8 -0
  103. package/dist/sandbox-runtime.js +14 -2
  104. package/dist/scope.d.ts +19 -1
  105. package/dist/scope.js +26 -3
  106. package/dist/scout.js +2 -2
  107. package/dist/secret-mint.d.ts +1 -0
  108. package/dist/secret-mint.js +14 -0
  109. package/dist/serve.js +112 -51
  110. package/dist/server/chat.js +5 -5
  111. package/dist/server/http.d.ts +2 -0
  112. package/dist/server/pages.js +7 -3
  113. package/dist/server/remote-hooks.d.ts +1 -1
  114. package/dist/server/remote-hooks.js +5 -3
  115. package/dist/server/render-results.d.ts +2 -0
  116. package/dist/server/render-results.js +1 -0
  117. package/dist/server/render-tasks.d.ts +2 -0
  118. package/dist/server/render-tasks.js +2 -1
  119. package/dist/server/request-context.d.ts +8 -0
  120. package/dist/server/runtime.d.ts +2 -0
  121. package/dist/server/settings.js +22 -2
  122. package/dist/server/task-views.js +7 -2
  123. package/dist/server/tasks.js +4 -1
  124. package/dist/setup-guide.d.ts +2 -2
  125. package/dist/storage-sweep.js +1 -1
  126. package/dist/storage.d.ts +5 -0
  127. package/dist/storage.js +28 -1
  128. package/dist/store.d.ts +135 -2
  129. package/dist/store.js +275 -13
  130. package/dist/surface.js +3 -1
  131. package/dist/task-base.d.ts +4 -0
  132. package/dist/task-base.js +22 -2
  133. package/dist/teardown-git.d.ts +52 -3
  134. package/dist/teardown-git.js +145 -33
  135. package/dist/test-infra.js +1 -1
  136. package/dist/toolroll-update.js +2 -2
  137. package/dist/up-processes.d.ts +137 -0
  138. package/dist/up-processes.js +165 -0
  139. package/dist/up-role-child.d.ts +1 -0
  140. package/dist/up-role-child.js +81 -0
  141. package/dist/verification-evidence.d.ts +1 -0
  142. package/dist/verification-evidence.js +13 -1
  143. package/dist/workspace-revision.js +3 -1
  144. package/package.json +2 -1
@@ -0,0 +1,41 @@
1
+ import { type BrowserProjectState } from "./browser-workspace.js";
2
+ import type { Store } from "./store.js";
3
+ import { type WorkIndexOptions } from "./work-index.js";
4
+ import type { WorkSummaryAccess } from "./work-summary.js";
5
+ import type { WorkspaceRevision } from "./workspace-revision.js";
6
+ export type SidebarProject = {
7
+ path: string;
8
+ name: string;
9
+ };
10
+ /** The sidebar's projects with their states (agent-state.ts) for one reader: the page's first read and the summary
11
+ * room's snapshots are this one function, so the two never disagree. Never marks anything seen. Work with no project
12
+ * yet is its own entry when this reader may see it and something counts there. */
13
+ export declare function projectSummaryOf(store: Store, now: Date, access: WorkSummaryAccess, actor: string, projects: readonly SidebarProject[], evidenceRoot: string | undefined): BrowserProjectState[];
14
+ /** One access, canonically and in full (every field it has, so a field added later keys too): the same reader and
15
+ * scope always spell the same key, whatever order its projects came in. */
16
+ export declare function accessKey(access: WorkSummaryAccess): string;
17
+ /**
18
+ * Values read from the database, each kept while the workspace revision (workspace-revision.ts: every meaningful
19
+ * commit, from any process, moves it) is the one it was read at, and never past the revision's own clock boundary
20
+ * (a lease or liveness window ending) or `maxAgeMs`, whichever is first — time alone (an agent going quiet) shows
21
+ * within that bound. A value is stored under the revision read before it was computed, so a commit that lands while it
22
+ * is computed only makes it miss sooner. Callers must not change a value they were handed.
23
+ */
24
+ export declare class RevisionCache {
25
+ #private;
26
+ constructor(revision: WorkspaceRevision, clock: () => Date, options?: {
27
+ maxEntries?: number;
28
+ maxAgeMs?: number;
29
+ });
30
+ /** The value for `key` at the current revision, or undefined. */
31
+ peek<T>(key: string): T | undefined;
32
+ /** The cached value, or `compute()` now (stored for the next reader). Errors are never cached. */
33
+ read<T>(key: string, compute: () => T): T;
34
+ /** The cached value, or one read asynchronously (a read worker) and stored. A failure answers undefined and leaves
35
+ * the key empty, so the request thread computes it as before. */
36
+ fill<T>(key: string, compute: () => Promise<T>): Promise<T | undefined>;
37
+ }
38
+ /** The keys the console's chrome reads are kept under (serve.ts and its read-worker prefetch spell them the same). */
39
+ export declare const countsKey: (access: WorkSummaryAccess) => string;
40
+ export declare const summaryKey: (actor: string, access: WorkSummaryAccess, projects: readonly SidebarProject[]) => string;
41
+ export declare const indexKey: (access: WorkSummaryAccess, options: WorkIndexOptions) => string;
@@ -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
+ };
@@ -98,7 +98,7 @@ export declare const CONSOLE_FORMS: {
98
98
  readonly storage: FormContract<"cleanup" | "password" | "path" | "preview">;
99
99
  readonly pullRequests: FormContract<"act" | "base" | "github" | "method" | "password" | "repo" | "when-green">;
100
100
  readonly checks: FormContract<"act" | "command" | "level" | "on" | "password" | "repo" | "timeout">;
101
- readonly backups: FormContract<"every" | "folder" | "keep" | "password">;
101
+ readonly backups: FormContract<"every" | "folder" | "keep" | "live_every" | "live_folder" | "password">;
102
102
  readonly data: FormContract<"password">;
103
103
  readonly flowsOn: FormContract<"repo" | "starter">;
104
104
  readonly integrationsTest: FormContract<"key">;
@@ -107,7 +107,7 @@ export const CONSOLE_FORMS = {
107
107
  storage: formContract("POST /settings/storage, /settings/storage/clean, /settings/storage/discard", ["password", "cleanup", "preview", "path"]),
108
108
  pullRequests: formContract("POST /settings/pull-requests", ["repo", "act", "password", "github", "base", "method", "when-green"]),
109
109
  checks: formContract("POST /settings/checks", ["repo", "password", "act", "on", "level", "command", "timeout"]),
110
- backups: formContract("POST /settings/backups", ["password", "every", "keep", "folder"]),
110
+ backups: formContract("POST /settings/backups", ["password", "every", "keep", "folder", "live_every", "live_folder"]),
111
111
  data: formContract("POST /settings/data", ["password"]),
112
112
  flowsOn: formContract("POST /settings/flows/on", ["repo", "starter"]),
113
113
  integrationsTest: formContract("POST /settings/integrations/test", ["key"]),
@@ -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, {
@@ -64,3 +64,44 @@ export declare function storedReleaseTerms(raw: unknown): {
64
64
  ok: false;
65
65
  problem: string;
66
66
  };
67
+ /**
68
+ * A release check's purpose (docs/plans/pipeline-speed.md, P0): what a person agrees to when they approve a release
69
+ * whose later commits can't carry (its checks run outside the sandbox, or carrying is off) — this exact candidate is a
70
+ * release's check. It never carries a later commit, never lets CI stand in for the check's units and never approves
71
+ * itself; it lets the plane hold the check to the release's strict report rules and rerun it, on this identical
72
+ * candidate under this same approval, after a failure the plane itself confirmed as infrastructure. Part of the scope
73
+ * digest when present; read strictly. A scope carries terms or this purpose, never both.
74
+ */
75
+ export declare const releaseCheckPurposeSchema: z.ZodObject<{
76
+ version: z.ZodLiteral<1>;
77
+ purpose: z.ZodLiteral<"release-check">;
78
+ repo: z.ZodString;
79
+ packageVersion: z.ZodString;
80
+ branch: z.ZodString;
81
+ pr: z.ZodInt;
82
+ base: z.ZodLiteral<"main">;
83
+ approvedSha: z.ZodString;
84
+ approvedTree: z.ZodString;
85
+ grant: z.ZodObject<{
86
+ id: z.ZodInt;
87
+ digest: z.ZodString;
88
+ }, z.core.$strict>;
89
+ access: z.ZodEnum<{
90
+ sandboxed: "sandboxed";
91
+ "sign-ins": "sign-ins";
92
+ unsandboxed: "unsandboxed";
93
+ }>;
94
+ }, z.core.$strict>;
95
+ export type ReleaseCheckPurpose = z.infer<typeof releaseCheckPurposeSchema>;
96
+ export declare function readReleaseCheckPurpose(input: unknown): ContractResult<ReleaseCheckPurpose>;
97
+ /** The digest a scope binds for its release check purpose, domain-separated from the terms' and every other input. */
98
+ export declare function releaseCheckPurposeDigest(purpose: ReleaseCheckPurpose): string;
99
+ /** Strict stored purpose: null column → null; anything that isn't exactly a purpose, as written → a problem. */
100
+ export declare function storedReleaseCheckPurpose(raw: unknown): {
101
+ ok: true;
102
+ purpose: ReleaseCheckPurpose | null;
103
+ } | {
104
+ ok: false;
105
+ problem: string;
106
+ };
107
+ export declare function serializeReleaseCheckPurpose(purpose: ReleaseCheckPurpose): string;
@@ -72,3 +72,56 @@ export function storedReleaseTerms(raw) {
72
72
  return { ok: false, problem: "the release terms are not stored exactly as written" };
73
73
  return { ok: true, terms: read.value };
74
74
  }
75
+ /**
76
+ * A release check's purpose (docs/plans/pipeline-speed.md, P0): what a person agrees to when they approve a release
77
+ * whose later commits can't carry (its checks run outside the sandbox, or carrying is off) — this exact candidate is a
78
+ * release's check. It never carries a later commit, never lets CI stand in for the check's units and never approves
79
+ * itself; it lets the plane hold the check to the release's strict report rules and rerun it, on this identical
80
+ * candidate under this same approval, after a failure the plane itself confirmed as infrastructure. Part of the scope
81
+ * digest when present; read strictly. A scope carries terms or this purpose, never both.
82
+ */
83
+ export const releaseCheckPurposeSchema = versioned(1, {
84
+ purpose: z.literal("release-check"),
85
+ repo: z.string().min(1).max(4096),
86
+ packageVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
87
+ branch: z.string().min(1).max(255).regex(/^[A-Za-z0-9._/-]+$/),
88
+ pr: z.int().min(1),
89
+ base: z.literal("main"),
90
+ approvedSha: sha,
91
+ approvedTree: sha,
92
+ grant: z.strictObject({ id: z.int(), digest: z.string().min(1).max(128) }),
93
+ access: z.enum(["sandboxed", "sign-ins", "unsandboxed"]),
94
+ });
95
+ export function readReleaseCheckPurpose(input) {
96
+ return readVersioned(releaseCheckPurposeSchema, input);
97
+ }
98
+ /** The digest a scope binds for its release check purpose, domain-separated from the terms' and every other input. */
99
+ export function releaseCheckPurposeDigest(purpose) {
100
+ return createHash("sha256").update(`release-check-purpose:${JSON.stringify(purpose)}`).digest("hex");
101
+ }
102
+ /** Strict stored purpose: null column → null; anything that isn't exactly a purpose, as written → a problem. */
103
+ export function storedReleaseCheckPurpose(raw) {
104
+ if (raw === null || raw === undefined)
105
+ return { ok: true, purpose: null };
106
+ if (typeof raw !== "string")
107
+ return { ok: false, problem: "the release check purpose is not stored as text" };
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(raw);
111
+ }
112
+ catch {
113
+ return { ok: false, problem: "the release check purpose can't be read" };
114
+ }
115
+ const read = readReleaseCheckPurpose(parsed);
116
+ if (!read.ok)
117
+ return { ok: false, problem: `the release check purpose doesn't match its contract (${read.issues[0]?.line ?? "invalid"})` };
118
+ if (JSON.stringify(read.value) !== raw)
119
+ return { ok: false, problem: "the release check purpose is not stored exactly as written" };
120
+ return { ok: true, purpose: read.value };
121
+ }
122
+ export function serializeReleaseCheckPurpose(purpose) {
123
+ const read = readReleaseCheckPurpose(purpose);
124
+ if (!read.ok)
125
+ throw new Error(`The release check purpose doesn't match its contract: ${read.issues[0]?.line ?? "invalid"}`);
126
+ return JSON.stringify(read.value);
127
+ }
@@ -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() }).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;
@@ -8,7 +8,10 @@ const stop = () => stopping.abort();
8
8
  process.on("SIGTERM", stop);
9
9
  process.on("SIGINT", stop);
10
10
  try {
11
- await superviseController({ file, argv, signal: stopping.signal, onState: state => {
11
+ // A stop is forwarded to the controller (SIGTERM) and waited for. Past 55 s its whole tree is stopped outright: before
12
+ // launchd's own ExitTimeOut (60 s) kills this process and could leave the controller running without it. `up` signs
13
+ // off within 50 s at its default grace and stops itself if this process dies first (operate.ts upCommand).
14
+ await superviseController({ file, argv, signal: stopping.signal, shutdownMs: 55_000, onState: state => {
12
15
  if (state.phase === "backoff")
13
16
  process.stderr.write(`Controller exited (${state.exit?.signal ?? state.exit?.code ?? "unknown"}); restart in ${state.retryMs} ms.\n`);
14
17
  } });
@@ -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>;