toolroll 0.9.76 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +28 -8
  5. package/dist/assignment.js +3 -0
  6. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  7. package/dist/browser/workspace.css +2 -2
  8. package/dist/browser/workspace.js +31 -31
  9. package/dist/builder.d.ts +14 -0
  10. package/dist/builder.js +140 -41
  11. package/dist/chat-decide.js +4 -0
  12. package/dist/check-failure.d.ts +92 -0
  13. package/dist/check-failure.js +267 -0
  14. package/dist/check-progress.d.ts +28 -1
  15. package/dist/check-progress.js +105 -6
  16. package/dist/check-report.d.ts +52 -6
  17. package/dist/check-report.js +193 -8
  18. package/dist/checkout-cleanup.js +2 -2
  19. package/dist/cli.js +8 -5
  20. package/dist/coding-handoff.d.ts +1 -0
  21. package/dist/coding-handoff.js +20 -11
  22. package/dist/commit-extract.d.ts +6 -0
  23. package/dist/commit-extract.js +63 -8
  24. package/dist/console-chrome.d.ts +41 -0
  25. package/dist/console-chrome.js +166 -0
  26. package/dist/contracts/release-check.d.ts +12 -0
  27. package/dist/contracts/release-check.js +17 -0
  28. package/dist/contracts/verification-receipt.d.ts +20 -2
  29. package/dist/contracts/verification-receipt.js +15 -3
  30. package/dist/contracts/verification-view.d.ts +8 -0
  31. package/dist/controller-supervisor.d.ts +8 -0
  32. package/dist/controller-supervisor.js +33 -1
  33. package/dist/demo.js +3 -2
  34. package/dist/deploy-gate.js +5 -0
  35. package/dist/dispose.d.ts +3 -0
  36. package/dist/dispose.js +6 -3
  37. package/dist/doctor.d.ts +7 -0
  38. package/dist/doctor.js +10 -2
  39. package/dist/envelope.js +2 -1
  40. package/dist/evidence.js +4 -1
  41. package/dist/exec.d.ts +7 -0
  42. package/dist/exec.js +7 -1
  43. package/dist/git-floor.d.ts +16 -0
  44. package/dist/git-floor.js +36 -0
  45. package/dist/grant.js +5 -1
  46. package/dist/guides.js +4 -3
  47. package/dist/operate.d.ts +13 -0
  48. package/dist/operate.js +773 -319
  49. package/dist/prepared-evidence.d.ts +2 -1
  50. package/dist/prepared-evidence.js +8 -3
  51. package/dist/project-knowledge.js +6 -2
  52. package/dist/project-learning.js +2 -2
  53. package/dist/proof.js +6 -0
  54. package/dist/read-executor-worker.js +8 -4
  55. package/dist/read-executor.d.ts +18 -1
  56. package/dist/read-executor.js +8 -0
  57. package/dist/release-adapters.d.ts +48 -1
  58. package/dist/release-adapters.js +343 -10
  59. package/dist/release-authority.js +6 -2
  60. package/dist/release-auto.d.ts +38 -7
  61. package/dist/release-auto.js +131 -15
  62. package/dist/release-card.d.ts +15 -3
  63. package/dist/release-card.js +25 -9
  64. package/dist/release-ci-hold.d.ts +41 -0
  65. package/dist/release-ci-hold.js +53 -0
  66. package/dist/release-cli.js +21 -12
  67. package/dist/release-coverage.js +5 -2
  68. package/dist/release-recheck.d.ts +1 -1
  69. package/dist/release-recheck.js +6 -1
  70. package/dist/release.d.ts +108 -11
  71. package/dist/release.js +294 -39
  72. package/dist/review-context.js +8 -6
  73. package/dist/runner.js +2 -1
  74. package/dist/sandbox-runtime.d.ts +8 -0
  75. package/dist/sandbox-runtime.js +14 -2
  76. package/dist/scope.js +7 -1
  77. package/dist/secret-mint.d.ts +1 -0
  78. package/dist/secret-mint.js +14 -0
  79. package/dist/serve.js +101 -51
  80. package/dist/server/chat.js +5 -5
  81. package/dist/server/render-tasks.d.ts +2 -0
  82. package/dist/server/render-tasks.js +2 -1
  83. package/dist/server/request-context.d.ts +6 -0
  84. package/dist/server/runtime.d.ts +2 -0
  85. package/dist/server/task-views.js +7 -2
  86. package/dist/setup-guide.d.ts +2 -2
  87. package/dist/storage.d.ts +5 -0
  88. package/dist/storage.js +28 -1
  89. package/dist/store.d.ts +10 -0
  90. package/dist/store.js +25 -1
  91. package/dist/task-base.d.ts +4 -0
  92. package/dist/task-base.js +22 -2
  93. package/dist/teardown-git.d.ts +52 -3
  94. package/dist/teardown-git.js +143 -31
  95. package/dist/test-infra.js +1 -1
  96. package/dist/up-processes.d.ts +137 -0
  97. package/dist/up-processes.js +165 -0
  98. package/dist/up-role-child.d.ts +1 -0
  99. package/dist/up-role-child.js +77 -0
  100. package/dist/verification-evidence.d.ts +1 -0
  101. package/dist/verification-evidence.js +13 -1
  102. 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
+ };
@@ -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() }).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 {
@@ -8,6 +8,7 @@
8
8
  import { assignmentOf } from "./assignment.js";
9
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,6 +28,10 @@ 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.
package/dist/dispose.d.ts CHANGED
@@ -132,6 +132,9 @@ export declare function regateTask(store: Store, taskId: string, now: Date, appr
132
132
  kind: "mode";
133
133
  name: string;
134
134
  modeDigest: string;
135
+ } | {
136
+ kind: "release";
137
+ name: string;
135
138
  }): {
136
139
  ok: true;
137
140
  run: number;
package/dist/dispose.js CHANGED
@@ -455,7 +455,10 @@ export function regateTask(store, taskId, now, approval) {
455
455
  if (scope === null)
456
456
  return { ok: false, reason: "no-attempt", message: `${taskId} has no scope` };
457
457
  const head = last.headRevision;
458
- const actor = approval.kind === "operator" ? approval.name : `mode ${approval.name}`;
458
+ const actor = approval.kind === "operator" ? approval.name : approval.kind === "release" ? approval.name : `mode ${approval.name}`;
459
+ if (approval.kind === "release" && ((scope.candidate ?? null) === null || (scope.releaseTerms ?? null) === null)) {
460
+ return { ok: false, reason: "not-approved", message: `${taskId} isn't a release candidate; only a release reruns under its own approval` };
461
+ }
459
462
  // A prepared candidate (a release gate, the nightly check) already names the approved commit, and the builder loads
460
463
  // exactly its tree on every attempt. The rerun keeps that scope and its approval as they are: nothing is proposed or
461
464
  // sealed again, so a regate can never swap the approved commit, its release terms or the person's yes for others
@@ -481,7 +484,7 @@ export function regateTask(store, taskId, now, approval) {
481
484
  return { ok: false, reason: why[queued.reason] ?? "not-rejected", message: `${taskId} could not be queued again: ${queued.reason}` };
482
485
  }
483
486
  }
484
- store.addRunNote(last.id, "Toolroll", `The check will run again on the approved commit ${candidate.slice(0, 7)}, under the same approval, as a new attempt, ${approval.kind === "operator" ? `on ${approval.name}'s say-so` : "automatically once, because the failure was outside this change"}.`, now);
487
+ store.addRunNote(last.id, "Toolroll", `The check will run again on the approved commit ${candidate.slice(0, 7)}, under the same approval, as a new attempt, ${approval.kind === "operator" ? `on ${approval.name}'s say-so` : approval.kind === "release" ? "because Toolroll confirmed the failure was its own infrastructure, not the code" : "automatically once, because the failure was outside this change"}.`, now);
485
488
  return { ok: true, run: last.id, head: candidate };
486
489
  });
487
490
  }
@@ -494,7 +497,7 @@ export function regateTask(store, taskId, now, approval) {
494
497
  let sealed = false;
495
498
  if (approval.kind === "operator")
496
499
  sealed = approve(store, taskId, approval.name, now, proposed.digest, approval.token).ok;
497
- else
500
+ else if (approval.kind === "mode")
498
501
  sealed = store.sealScopeApproval(taskId, actor, now, {}, { kind: "mode", modeDigest: approval.modeDigest });
499
502
  if (!sealed)
500
503
  return { ok: false, reason: "not-approved", message: `${taskId}'s rerun scope could not be approved` };
package/dist/doctor.d.ts CHANGED
@@ -24,6 +24,11 @@ export type DoctorReport = {
24
24
  version: string;
25
25
  problem: string | null;
26
26
  };
27
+ /** The installed git against Toolroll's floor (git-floor.ts): null when it is new enough (or unreadable). */
28
+ git?: {
29
+ floor: string;
30
+ problem: string | null;
31
+ };
27
32
  };
28
33
  export type DoctorDeps = {
29
34
  platform?: NodeJS.Platform;
@@ -35,6 +40,8 @@ export type DoctorDeps = {
35
40
  procRoot?: string;
36
41
  /** The doctor's own environment: the terminal a `toolroll up` would start from. */
37
42
  env?: NodeJS.ProcessEnv;
43
+ /** The installed git's floor problem (a test's stand-in for git-floor.ts installedGitFloorProblem). */
44
+ gitFloorProblem?: string | null;
38
45
  };
39
46
  /** The names among `pairs` that hold a credential: a provider key, a credential-looking name, or a key-shaped value.
40
47
  * Values are only tested here and never leave. */