@tangle-network/agent-app 0.45.24 → 0.45.26

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.
@@ -0,0 +1,1046 @@
1
+ // src/signoff/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join, resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { z } from "zod";
6
+ var SIGNOFF_CONFIG_FILES = ["signoff.config.mjs", "signoff.config.js"];
7
+ var shuffleSchema = z.object({
8
+ runs: z.number().int().positive().optional(),
9
+ seeds: z.array(z.number().int()).optional(),
10
+ args: z.array(z.string()).optional()
11
+ });
12
+ var stepSchema = z.object({
13
+ name: z.string().min(1),
14
+ run: z.string().min(1),
15
+ cwd: z.string().optional(),
16
+ env: z.record(z.string(), z.string()).optional(),
17
+ needs: z.array(z.string()).optional(),
18
+ timeoutMs: z.number().int().positive().optional(),
19
+ shuffle: z.union([z.boolean(), shuffleSchema]).optional()
20
+ });
21
+ var configSchema = z.object({
22
+ install: z.object({
23
+ run: z.string().min(1).optional(),
24
+ storeDirFlag: z.string().nullable().optional(),
25
+ storeEnv: z.string().nullable().optional(),
26
+ cwd: z.string().optional(),
27
+ timeoutMs: z.number().int().positive().optional(),
28
+ env: z.record(z.string(), z.string()).optional()
29
+ }).optional(),
30
+ steps: z.array(stepSchema).min(1),
31
+ maxParallel: z.number().int().positive().optional(),
32
+ env: z.record(z.string(), z.string()).optional(),
33
+ nodeVersion: z.string().min(1).optional(),
34
+ carryFiles: z.array(z.string()).optional(),
35
+ cacheDir: z.string().optional(),
36
+ storeGenerations: z.number().int().positive().optional()
37
+ });
38
+ function describeIssues(error, where) {
39
+ const lines = error.issues.map((issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
40
+ return `signoff: ${where} is not a valid config:
41
+ ${lines.join("\n")}`;
42
+ }
43
+ function parseSignoffConfig(value, where) {
44
+ const result = configSchema.safeParse(value);
45
+ if (!result.success) throw new Error(describeIssues(result.error, where));
46
+ return result.data;
47
+ }
48
+ var DERIVED_STEPS = [
49
+ { script: "peer-check", name: "peer floors" },
50
+ { script: "typecheck", name: "typecheck" },
51
+ { script: "test:gates", name: "incident-class gates" },
52
+ { script: "test", name: "unit tests", shuffle: true },
53
+ { script: "build", name: "build" },
54
+ { script: "build:check", name: "build + worker checks", supersedes: "build" },
55
+ { script: "test:generated", name: "generated projects", needsBuild: true },
56
+ { script: "knip", name: "dead-surface (knip)" }
57
+ ];
58
+ function deriveSignoffConfig(scripts) {
59
+ const present = DERIVED_STEPS.filter((candidate) => scripts[candidate.script] !== void 0);
60
+ const superseded = new Set(present.map((candidate) => candidate.supersedes).filter((name) => !!name));
61
+ const kept = present.filter((candidate) => !superseded.has(candidate.script));
62
+ const buildStep = kept.find((candidate) => candidate.script === "build" || candidate.script === "build:check");
63
+ const steps = kept.map((candidate) => ({
64
+ name: candidate.name,
65
+ run: `pnpm run ${candidate.script}`,
66
+ ...candidate.shuffle ? { shuffle: true } : {},
67
+ ...candidate.needsBuild && buildStep ? { needs: [buildStep.name] } : {}
68
+ }));
69
+ if (steps.length === 0) {
70
+ throw new Error(
71
+ `signoff: no config and no recognizable scripts. Add a \`signoff.config.mjs\` naming the steps this repo's CI runs, or a package.json "signoff" key. Recognized script names: ${DERIVED_STEPS.map((candidate) => candidate.script).join(", ")}.`
72
+ );
73
+ }
74
+ return { config: { steps }, used: kept.map((candidate) => candidate.script) };
75
+ }
76
+ async function loadSignoffConfig(options) {
77
+ const { repoRoot, configPath } = options;
78
+ if (configPath !== void 0) {
79
+ const abs = resolve(repoRoot, configPath);
80
+ if (!existsSync(abs)) throw new Error(`signoff: no config at ${abs}`);
81
+ return { config: await importConfig(abs), origin: { kind: "file", path: abs } };
82
+ }
83
+ for (const candidate of SIGNOFF_CONFIG_FILES) {
84
+ const abs = join(repoRoot, candidate);
85
+ if (existsSync(abs)) return { config: await importConfig(abs), origin: { kind: "file", path: abs } };
86
+ }
87
+ const pkgPath = join(repoRoot, "package.json");
88
+ if (!existsSync(pkgPath)) {
89
+ throw new Error(`signoff: ${repoRoot} has no package.json, no signoff.config.mjs, and nothing to derive from.`);
90
+ }
91
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
92
+ if (pkg.signoff !== void 0) {
93
+ return { config: parseSignoffConfig(pkg.signoff, `${pkgPath} "signoff"`), origin: { kind: "package-json", path: pkgPath } };
94
+ }
95
+ const derived = deriveSignoffConfig(pkg.scripts ?? {});
96
+ return { config: derived.config, origin: { kind: "derived", path: pkgPath, scripts: derived.used } };
97
+ }
98
+ async function importConfig(abs) {
99
+ const mod = await import(pathToFileURL(abs).href);
100
+ const value = mod.default;
101
+ if (value === void 0) throw new Error(`signoff: ${abs} must have a default export`);
102
+ return parseSignoffConfig(value, abs);
103
+ }
104
+
105
+ // src/signoff/workflow-pin.ts
106
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
107
+ import { join as join2 } from "path";
108
+ var WORKFLOW_DIR = join2(".github", "workflows");
109
+ function scalarValue(raw) {
110
+ const withoutComment = raw.replace(/\s+#.*$/, "").trim();
111
+ const quoted = /^(['"])(.*)\1$/.exec(withoutComment);
112
+ return (quoted?.[2] ?? withoutComment).trim();
113
+ }
114
+ function indentOf(line) {
115
+ return line.length - line.trimStart().length;
116
+ }
117
+ function isBlank(line) {
118
+ const trimmed = line.trim();
119
+ return trimmed.length === 0 || trimmed.startsWith("#");
120
+ }
121
+ function triggersOnPullRequest(source) {
122
+ const lines = source.split("\n");
123
+ for (let index = 0; index < lines.length; index += 1) {
124
+ const line = lines[index];
125
+ const header = /^(?:on|"on"|'on')\s*:(.*)$/.exec(line);
126
+ if (!header) continue;
127
+ const inline = scalarValue(header[1] ?? "");
128
+ if (inline.length > 0) {
129
+ return inline.replace(/^\[|\]$/g, "").split(",").map((token) => token.trim()).includes("pull_request");
130
+ }
131
+ let nesting = null;
132
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
133
+ const body = lines[cursor];
134
+ if (isBlank(body)) continue;
135
+ const bodyIndent = indentOf(body);
136
+ if (bodyIndent === 0) break;
137
+ if (nesting === null) nesting = bodyIndent;
138
+ if (bodyIndent !== nesting) continue;
139
+ const key = /^\s*(?:-\s*)?([A-Za-z_][\w-]*)\s*:?\s*$/.exec(body);
140
+ if (key?.[1] === "pull_request") return true;
141
+ }
142
+ return false;
143
+ }
144
+ return false;
145
+ }
146
+ function pinsInWorkflow(repoRoot, file, source) {
147
+ const pins = [];
148
+ for (const line of source.split("\n")) {
149
+ const match = /^\s*(node-version|node-version-file)\s*:\s*(\S.*)$/.exec(line);
150
+ if (!match) continue;
151
+ const key = match[1];
152
+ const value = scalarValue(match[2]);
153
+ if (value.includes("${{")) continue;
154
+ if (key === "node-version") {
155
+ pins.push({ file, value, via: "node-version" });
156
+ continue;
157
+ }
158
+ const target = join2(repoRoot, value);
159
+ if (!existsSync2(target)) {
160
+ throw new Error(
161
+ `signoff: ${file} reads its Node pin from "${value}" (node-version-file) and that file does not exist. The workflow this gate replaces cannot itself run, so there is nothing to verify against.`
162
+ );
163
+ }
164
+ const declared = readFileSync2(target, "utf8").split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
165
+ if (declared !== void 0) pins.push({ file, value: declared, via: `node-version-file ${value}` });
166
+ }
167
+ return pins;
168
+ }
169
+ function scanMergeGateNodePins(repoRoot) {
170
+ const dir = join2(repoRoot, WORKFLOW_DIR);
171
+ if (!existsSync2(dir)) return [];
172
+ const pins = [];
173
+ const files = readdirSync(dir).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
174
+ for (const name of files) {
175
+ const source = readFileSync2(join2(dir, name), "utf8");
176
+ if (!triggersOnPullRequest(source)) continue;
177
+ pins.push(...pinsInWorkflow(repoRoot, `${WORKFLOW_DIR}/${name}`, source));
178
+ }
179
+ return pins;
180
+ }
181
+ function resolveWorkflowNodePin(repoRoot, majorOf2) {
182
+ const pins = scanMergeGateNodePins(repoRoot);
183
+ if (pins.length === 0) return null;
184
+ const byMajor = /* @__PURE__ */ new Map();
185
+ for (const pin of pins) {
186
+ const major2 = majorOf2(pin.value);
187
+ if (major2 === null) continue;
188
+ const bucket = byMajor.get(major2);
189
+ if (bucket) bucket.push(pin);
190
+ else byMajor.set(major2, [pin]);
191
+ }
192
+ if (byMajor.size === 0) return null;
193
+ if (byMajor.size > 1) {
194
+ const detail = [...byMajor.values()].flat().map((pin) => ` ${pin.file} (${pin.via}): ${pin.value}`).join("\n");
195
+ throw new Error(
196
+ `signoff: the workflows that gate a merge here pin different Node majors, so there is no single runtime to verify:
197
+ ${detail}
198
+ Declare \`nodeVersion\` in the signoff config to say which one a sign-off means.`
199
+ );
200
+ }
201
+ const [entry] = [...byMajor.entries()];
202
+ if (entry === void 0) return null;
203
+ const [major, matched] = entry;
204
+ const first = matched[0];
205
+ const files = [...new Set(matched.map((pin) => pin.file))].join(", ");
206
+ return { major, declared: first.value, source: `${files} (${first.via})` };
207
+ }
208
+
209
+ // src/signoff/node-version.ts
210
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
211
+ import { join as join3 } from "path";
212
+ function majorOf(raw) {
213
+ const match = /^v?(\d+)(?:\.|$)/.exec(raw.trim());
214
+ return match?.[1] === void 0 ? null : Number.parseInt(match[1], 10);
215
+ }
216
+ function resolveNodeRequirement(repoRoot, configured) {
217
+ if (configured !== void 0) {
218
+ const major = majorOf(configured);
219
+ if (major === null) {
220
+ throw new Error(
221
+ `signoff: nodeVersion "${configured}" does not start with a major version. Declare a pin like "22" or "22.22.3".`
222
+ );
223
+ }
224
+ return { major, declared: configured.trim(), source: "signoff config `nodeVersion`" };
225
+ }
226
+ let fromNvmrc = null;
227
+ const nvmrc = join3(repoRoot, ".nvmrc");
228
+ if (existsSync3(nvmrc)) {
229
+ const raw = readFileSync3(nvmrc, "utf8").trim();
230
+ const major = majorOf(raw);
231
+ if (major !== null) fromNvmrc = { major, declared: raw, source: ".nvmrc" };
232
+ }
233
+ const fromWorkflow = resolveWorkflowNodePin(repoRoot, majorOf);
234
+ if (fromNvmrc && fromWorkflow && fromNvmrc.major !== fromWorkflow.major) {
235
+ throw new Error(
236
+ `signoff: .nvmrc pins Node ${fromNvmrc.declared} and ${fromWorkflow.source} pins ${fromWorkflow.declared}. A sign-off that replaces CI cannot verify two runtimes, and picking one silently would sign off a runtime the other half of the repo says is wrong. Make them agree, or declare \`nodeVersion\` in the signoff config.`
237
+ );
238
+ }
239
+ if (fromNvmrc) return fromNvmrc;
240
+ if (fromWorkflow) return { ...fromWorkflow, source: fromWorkflow.source };
241
+ return null;
242
+ }
243
+ function assertNodeVersion(requirement, running = process.version) {
244
+ if (!requirement) return;
245
+ const runningMajor = majorOf(running);
246
+ if (runningMajor === requirement.major) return;
247
+ throw new Error(
248
+ `signoff: this repo pins Node ${requirement.declared} (${requirement.source}) and you are running ${running}. A sign-off that replaces CI has to verify the runtime the product ships, so this refuses rather than reporting a pass it did not earn. Switch with \`nvm use ${requirement.major}\`, or change the pin if the product really has moved.`
249
+ );
250
+ }
251
+
252
+ // src/signoff/exec.ts
253
+ import { spawn } from "child_process";
254
+ var DEFAULT_MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
255
+ var DEFAULT_KILL_GRACE_MS = 5e3;
256
+ var HEAD_SHARE = 0.25;
257
+ var BoundedOutput = class {
258
+ constructor(budget) {
259
+ this.budget = budget;
260
+ this.headBudget = Math.floor(budget * HEAD_SHARE);
261
+ this.tailBudget = budget - this.headBudget;
262
+ }
263
+ budget;
264
+ head = "";
265
+ tail = "";
266
+ total = 0;
267
+ headBudget;
268
+ tailBudget;
269
+ push(chunk) {
270
+ this.total += chunk.length;
271
+ if (this.head.length < this.headBudget) {
272
+ const room = this.headBudget - this.head.length;
273
+ this.head += chunk.slice(0, room);
274
+ chunk = chunk.slice(room);
275
+ if (chunk.length === 0) return;
276
+ }
277
+ this.tail = (this.tail + chunk).slice(-this.tailBudget);
278
+ }
279
+ get truncated() {
280
+ return this.total > this.budget;
281
+ }
282
+ text() {
283
+ if (!this.truncated) return this.head + this.tail;
284
+ const elided = this.total - this.head.length - this.tail.length;
285
+ return `${this.head}
286
+
287
+ [signoff] ${elided} bytes elided (output exceeded ${this.budget} bytes)
288
+
289
+ ${this.tail}`;
290
+ }
291
+ };
292
+ function killGroup(pid, signal) {
293
+ try {
294
+ process.kill(-pid, signal);
295
+ } catch (err) {
296
+ if (err.code !== "ESRCH") throw err;
297
+ }
298
+ }
299
+ function runCommand(options) {
300
+ const {
301
+ command,
302
+ cwd,
303
+ env,
304
+ timeoutMs,
305
+ signal,
306
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
307
+ killGraceMs = DEFAULT_KILL_GRACE_MS,
308
+ onData
309
+ } = options;
310
+ return new Promise((resolve4, reject) => {
311
+ const startedAt = Date.now();
312
+ const buffer = new BoundedOutput(maxOutputBytes);
313
+ let timedOut = false;
314
+ let killTimer;
315
+ let graceTimer;
316
+ const child = spawn(command, {
317
+ cwd,
318
+ env,
319
+ shell: true,
320
+ // Group leader: lets one signal reach `sh` and everything it spawned.
321
+ detached: true,
322
+ stdio: ["ignore", "pipe", "pipe"]
323
+ });
324
+ const pid = child.pid;
325
+ const terminate = () => {
326
+ if (pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
327
+ killGroup(pid, "SIGTERM");
328
+ graceTimer = setTimeout(() => {
329
+ if (child.exitCode === null && child.signalCode === null) killGroup(pid, "SIGKILL");
330
+ }, killGraceMs);
331
+ graceTimer.unref();
332
+ };
333
+ const onAbort = () => terminate();
334
+ signal?.addEventListener("abort", onAbort, { once: true });
335
+ if (timeoutMs !== void 0) {
336
+ killTimer = setTimeout(() => {
337
+ timedOut = true;
338
+ terminate();
339
+ }, timeoutMs);
340
+ killTimer.unref();
341
+ }
342
+ const collect = (chunk) => {
343
+ const text = chunk.toString("utf8");
344
+ buffer.push(text);
345
+ onData?.(text);
346
+ };
347
+ child.stdout.on("data", collect);
348
+ child.stderr.on("data", collect);
349
+ const cleanup = () => {
350
+ if (killTimer) clearTimeout(killTimer);
351
+ if (graceTimer) clearTimeout(graceTimer);
352
+ signal?.removeEventListener("abort", onAbort);
353
+ };
354
+ child.on("error", (err) => {
355
+ cleanup();
356
+ reject(new Error(`signoff: could not start \`${command}\` in ${cwd}: ${err.message}`));
357
+ });
358
+ child.on("close", (code, sig) => {
359
+ cleanup();
360
+ resolve4({
361
+ command,
362
+ cwd,
363
+ // A signalled process reports code `null`; 128+n is the shell's own
364
+ // convention and keeps the field a number a caller can compare.
365
+ exitCode: code ?? (sig === "SIGKILL" ? 137 : 143),
366
+ signal: sig,
367
+ durationMs: Date.now() - startedAt,
368
+ output: buffer.text(),
369
+ truncated: buffer.truncated,
370
+ timedOut
371
+ });
372
+ });
373
+ });
374
+ }
375
+
376
+ // src/signoff/schedule.ts
377
+ function validateGraph(nodes) {
378
+ const seen = /* @__PURE__ */ new Set();
379
+ for (const node of nodes) {
380
+ if (seen.has(node.name)) throw new Error(`signoff: two steps are both named "${node.name}"; names must be unique`);
381
+ seen.add(node.name);
382
+ }
383
+ for (const node of nodes) {
384
+ for (const need of node.needs ?? []) {
385
+ if (!seen.has(need)) {
386
+ throw new Error(`signoff: step "${node.name}" needs "${need}", which is not a step in this config`);
387
+ }
388
+ }
389
+ }
390
+ const byName = new Map(nodes.map((node) => [node.name, node]));
391
+ const state = /* @__PURE__ */ new Map();
392
+ const walk = (name, path) => {
393
+ const status = state.get(name);
394
+ if (status === "done") return;
395
+ if (status === "visiting") {
396
+ const cycle = [...path.slice(path.indexOf(name)), name].join(" -> ");
397
+ throw new Error(`signoff: dependency cycle among steps: ${cycle}`);
398
+ }
399
+ state.set(name, "visiting");
400
+ for (const need of byName.get(name)?.needs ?? []) walk(need, [...path, name]);
401
+ state.set(name, "done");
402
+ };
403
+ for (const node of nodes) walk(node.name, []);
404
+ }
405
+ async function runGraph(options) {
406
+ const { nodes, maxParallel, keepGoing, run, now = () => Date.now() } = options;
407
+ validateGraph(nodes);
408
+ const origin = now();
409
+ const outcomes = /* @__PURE__ */ new Map();
410
+ const pending = new Map(nodes.map((node) => [node.name, node]));
411
+ const running = /* @__PURE__ */ new Map();
412
+ let aborted = false;
413
+ const failedNames = /* @__PURE__ */ new Set();
414
+ const passedNames = /* @__PURE__ */ new Set();
415
+ const blockedBy = (node) => (node.needs ?? []).some((need) => failedNames.has(need));
416
+ const ready = (node) => (node.needs ?? []).every((need) => passedNames.has(need));
417
+ const settle = (name, outcome) => {
418
+ outcomes.set(name, outcome);
419
+ if (outcome.status === "passed") passedNames.add(name);
420
+ else failedNames.add(name);
421
+ };
422
+ const start = (node) => {
423
+ pending.delete(node.name);
424
+ const controller = new AbortController();
425
+ const startedAtMs = now() - origin;
426
+ const promise = run(node, controller.signal).then((result) => {
427
+ const finishedAtMs = now() - origin;
428
+ const cancelled = controller.signal.aborted && !result.ok;
429
+ settle(node.name, {
430
+ name: node.name,
431
+ status: cancelled ? "cancelled" : result.ok ? "passed" : "failed",
432
+ value: result.value,
433
+ startedAtMs,
434
+ finishedAtMs
435
+ });
436
+ running.delete(node.name);
437
+ });
438
+ running.set(node.name, { promise, controller });
439
+ };
440
+ for (; ; ) {
441
+ if (!aborted) {
442
+ for (const node of [...pending.values()]) {
443
+ if (running.size >= maxParallel) break;
444
+ if (blockedBy(node)) {
445
+ pending.delete(node.name);
446
+ settle(node.name, { name: node.name, status: "blocked", value: null, startedAtMs: null, finishedAtMs: null });
447
+ continue;
448
+ }
449
+ if (ready(node)) start(node);
450
+ }
451
+ }
452
+ if (running.size === 0) {
453
+ if (pending.size === 0) break;
454
+ if (aborted) break;
455
+ const progressed = [...pending.values()].some((node) => ready(node) || blockedBy(node));
456
+ if (!progressed) break;
457
+ continue;
458
+ }
459
+ await Promise.race([...running.values()].map((entry) => entry.promise));
460
+ if (!keepGoing && failedNames.size > 0 && !aborted) {
461
+ aborted = true;
462
+ for (const entry of running.values()) entry.controller.abort();
463
+ }
464
+ }
465
+ for (const node of pending.values()) {
466
+ settle(node.name, {
467
+ name: node.name,
468
+ status: blockedBy(node) ? "blocked" : "skipped",
469
+ value: null,
470
+ startedAtMs: null,
471
+ finishedAtMs: null
472
+ });
473
+ }
474
+ return nodes.map((node) => {
475
+ const outcome = outcomes.get(node.name);
476
+ if (!outcome) throw new Error(`signoff: step "${node.name}" produced no outcome \u2014 scheduler bug`);
477
+ return outcome;
478
+ });
479
+ }
480
+
481
+ // src/signoff/seeds.ts
482
+ import { createHash, randomInt } from "crypto";
483
+ var DEFAULT_SHUFFLE_ARGS = [
484
+ "--sequence.shuffle.files=true",
485
+ "--sequence.seed={seed}"
486
+ ];
487
+ var DEFAULT_SHUFFLE_RUNS = 2;
488
+ function newSeedBase() {
489
+ return randomInt(0, 2 ** 31 - 1);
490
+ }
491
+ function deriveSeed(base, stepName, index) {
492
+ const digest = createHash("sha256").update(`${base}:${stepName}:${index}`).digest();
493
+ return digest.readUInt32BE(0) % 2 ** 31;
494
+ }
495
+ function assertShuffleArgsReachTheRunner(steps) {
496
+ for (const step of steps) {
497
+ if (!normalizeShuffle(step.shuffle)) continue;
498
+ const tokens = step.run.split(/\s+/).filter((token) => token.length > 0);
499
+ const pnpmAt = tokens.findIndex((token) => token === "pnpm" || token.endsWith("/pnpm"));
500
+ if (pnpmAt === -1) continue;
501
+ if (tokens.slice(pnpmAt + 1).some((token) => token === "run" || token === "exec" || token === "dlx")) continue;
502
+ throw new Error(
503
+ `signoff: step "${step.name}" runs \`${step.run}\` and is shuffled, but pnpm only forwards appended arguments to a script through \`run\`, \`exec\` or \`dlx\`. In the shorthand form pnpm 9 errors and pnpm 10 exits 0 having run nothing, which would report a passing suite that never executed. Write it as \`${step.run.replace(/\s(\S+)$/, " run $1")}\`.`
504
+ );
505
+ }
506
+ }
507
+ function normalizeShuffle(shuffle) {
508
+ if (shuffle === void 0 || shuffle === false) return null;
509
+ return shuffle === true ? {} : shuffle;
510
+ }
511
+ function planAttempts(step, seedBase, overrideRuns) {
512
+ const spec = normalizeShuffle(step.shuffle);
513
+ if (!spec) return [{ command: step.run, seed: null }];
514
+ const args = spec.args ?? DEFAULT_SHUFFLE_ARGS;
515
+ const seeds = spec.seeds && spec.seeds.length > 0 ? [...spec.seeds] : Array.from(
516
+ { length: overrideRuns ?? spec.runs ?? DEFAULT_SHUFFLE_RUNS },
517
+ (_unused, index) => deriveSeed(seedBase, step.name, index)
518
+ );
519
+ return seeds.map((seed) => ({
520
+ command: `${step.run} ${args.map((arg) => arg.replaceAll("{seed}", String(seed))).join(" ")}`,
521
+ seed
522
+ }));
523
+ }
524
+
525
+ // src/signoff/store.ts
526
+ import { createHash as createHash2 } from "crypto";
527
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync, statSync, utimesSync, writeFileSync } from "fs";
528
+ import { join as join4, relative, sep } from "path";
529
+ var MANIFEST_FILES = [
530
+ "pnpm-lock.yaml",
531
+ "pnpm-workspace.yaml",
532
+ "package.json",
533
+ ".npmrc",
534
+ ".nvmrc",
535
+ "package-lock.json",
536
+ "npm-shrinkwrap.json",
537
+ "yarn.lock"
538
+ ];
539
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".wrangler", ".react-router"]);
540
+ function collectManifests(dir, root, out) {
541
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
542
+ if (entry.isDirectory()) {
543
+ if (SKIP_DIRS.has(entry.name)) continue;
544
+ collectManifests(join4(dir, entry.name), root, out);
545
+ } else if (MANIFEST_FILES.includes(entry.name)) {
546
+ out.push(relative(root, join4(dir, entry.name)).split(sep).join("/"));
547
+ }
548
+ }
549
+ }
550
+ function manifestFiles(treePath) {
551
+ const found = [];
552
+ collectManifests(treePath, treePath, found);
553
+ return found.sort();
554
+ }
555
+ function manifestCacheKey(treePath, files) {
556
+ const hash = createHash2("sha256");
557
+ for (const rel of files) {
558
+ hash.update(rel);
559
+ hash.update("\0");
560
+ hash.update(createHash2("sha256").update(readFileSync4(join4(treePath, rel))).digest("hex"));
561
+ hash.update("\n");
562
+ }
563
+ return hash.digest("hex");
564
+ }
565
+ function pruneStores(storesRoot, keep) {
566
+ if (!existsSync4(storesRoot)) return [];
567
+ const entries = readdirSync2(storesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
568
+ const full = join4(storesRoot, entry.name);
569
+ return { full, mtimeMs: statSync(full).mtimeMs };
570
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs);
571
+ const pruned = [];
572
+ for (const stale of entries.slice(keep)) {
573
+ rmSync(stale.full, { recursive: true, force: true });
574
+ pruned.push(stale.full);
575
+ }
576
+ return pruned;
577
+ }
578
+ function resolveStore(options) {
579
+ const { treePath, cacheDir, generations = 4 } = options;
580
+ const files = manifestFiles(treePath);
581
+ if (files.length === 0) {
582
+ throw new Error(
583
+ `signoff: no package manifest under ${treePath}. A sign-off run installs from a lockfile; there is nothing here to install.`
584
+ );
585
+ }
586
+ const cacheKey = manifestCacheKey(treePath, files);
587
+ const storesRoot = join4(cacheDir, "stores");
588
+ const storeDir = join4(storesRoot, cacheKey);
589
+ const marker = join4(storeDir, ".signoff-store.json");
590
+ const hit = existsSync4(storeDir) && readdirSync2(storeDir).some((entry) => entry !== ".signoff-store.json");
591
+ mkdirSync(storeDir, { recursive: true });
592
+ writeFileSync(marker, `${JSON.stringify({ cacheKey, keyedOn: files, usedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
593
+ `);
594
+ const now = /* @__PURE__ */ new Date();
595
+ utimesSync(storeDir, now, now);
596
+ return { storeDir, cacheKey, hit, keyedOn: files, pruned: pruneStores(storesRoot, generations) };
597
+ }
598
+
599
+ // src/signoff/workspace.ts
600
+ import { spawnSync } from "child_process";
601
+ import { createHash as createHash3 } from "crypto";
602
+ import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
603
+ import { dirname, isAbsolute, join as join5, resolve as resolve2 } from "path";
604
+ function git(args, cwd) {
605
+ const result = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
606
+ if (result.error) throw new Error(`signoff: git ${args.join(" ")} failed to start: ${result.error.message}`);
607
+ if (result.status !== 0) {
608
+ throw new Error(`signoff: git ${args.join(" ")} exited ${result.status}
609
+ ${result.stderr.trim()}`);
610
+ }
611
+ return result.stdout;
612
+ }
613
+ function zsplit(out) {
614
+ return out.split("\0").filter((entry) => entry.length > 0);
615
+ }
616
+ function repoRootOf(dir) {
617
+ return git(["rev-parse", "--show-toplevel"], dir).trim();
618
+ }
619
+ function materializeCleanTree(options) {
620
+ const { repoDir, dest, source, carryFiles = [] } = options;
621
+ const root = repoRootOf(repoDir);
622
+ const head = git(["rev-parse", "HEAD"], root).trim();
623
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], root).trim();
624
+ git(["worktree", "prune"], root);
625
+ mkdirSync2(dirname(dest), { recursive: true });
626
+ if (existsSync5(dest)) rmSync2(dest, { recursive: true, force: true });
627
+ git(["worktree", "add", "--detach", "--quiet", dest, head], root);
628
+ let diffSha256 = null;
629
+ let untrackedFiles = [];
630
+ if (source === "working-tree") {
631
+ const patch = git(["diff", "HEAD", "--binary", "--no-color", "--no-ext-diff"], root);
632
+ if (patch.length > 0) {
633
+ diffSha256 = createHash3("sha256").update(patch).digest("hex");
634
+ const patchFile = join5(dirname(dest), `${dest.split("/").pop() ?? "tree"}.patch`);
635
+ writeFileSync2(patchFile, patch);
636
+ git(["apply", "--binary", "--whitespace=nowarn", patchFile], dest);
637
+ rmSync2(patchFile, { force: true });
638
+ }
639
+ untrackedFiles = zsplit(git(["ls-files", "--others", "--exclude-standard", "-z"], root));
640
+ for (const rel of untrackedFiles) {
641
+ const target = join5(dest, rel);
642
+ mkdirSync2(dirname(target), { recursive: true });
643
+ copyFileSync(join5(root, rel), target);
644
+ }
645
+ }
646
+ const carried = [];
647
+ for (const rel of carryFiles) {
648
+ if (isAbsolute(rel)) throw new Error(`signoff: carryFiles must be repo-relative; got "${rel}"`);
649
+ const from = resolve2(root, rel);
650
+ if (!existsSync5(from)) {
651
+ throw new Error(
652
+ `signoff: carryFiles names "${rel}", which does not exist at ${from}. Remove it from the config or create the file \u2014 installing without it would resolve against a different registry than the one you think you are verifying.`
653
+ );
654
+ }
655
+ const target = join5(dest, rel);
656
+ mkdirSync2(dirname(target), { recursive: true });
657
+ copyFileSync(from, target);
658
+ carried.push(rel);
659
+ }
660
+ return {
661
+ path: dest,
662
+ root,
663
+ head,
664
+ branch,
665
+ source,
666
+ dirty: diffSha256 !== null || untrackedFiles.length > 0,
667
+ diffSha256,
668
+ untrackedFiles,
669
+ carriedFiles: carried
670
+ };
671
+ }
672
+ function removeCleanTree(tree) {
673
+ git(["worktree", "remove", "--force", tree.path], tree.root);
674
+ }
675
+
676
+ // src/signoff/run.ts
677
+ import { spawnSync as spawnSync2 } from "child_process";
678
+ import { existsSync as existsSync6 } from "fs";
679
+ import { availableParallelism, homedir } from "os";
680
+ import { basename, join as join6, resolve as resolve3 } from "path";
681
+ var DEFAULT_CACHE_DIR = join6(homedir(), ".cache", "agent-app-signoff");
682
+ function hostFacts(treePath, requirement) {
683
+ const pm = spawnSync2("pnpm", ["--version"], { cwd: treePath, encoding: "utf8" });
684
+ return {
685
+ node: process.version,
686
+ nodePinned: requirement?.declared ?? null,
687
+ nodePinSource: requirement?.source ?? null,
688
+ packageManager: pm.status === 0 ? `pnpm ${pm.stdout.trim()}` : "pnpm (not resolvable)",
689
+ platform: process.platform,
690
+ arch: process.arch,
691
+ cpus: availableParallelism()
692
+ };
693
+ }
694
+ var SUCCESS_OUTPUT_TAIL = 4e3;
695
+ function toAttempt(result, seed, ok) {
696
+ return {
697
+ command: result.command,
698
+ seed,
699
+ exitCode: result.exitCode,
700
+ signal: result.signal,
701
+ durationMs: result.durationMs,
702
+ timedOut: result.timedOut,
703
+ output: ok ? result.output.slice(-SUCCESS_OUTPUT_TAIL) : result.output,
704
+ outputTruncated: result.truncated || ok && result.output.length > SUCCESS_OUTPUT_TAIL
705
+ };
706
+ }
707
+ function buildEnv(base, layers) {
708
+ const env = { ...base };
709
+ for (const layer of layers) {
710
+ if (layer) Object.assign(env, layer);
711
+ }
712
+ return env;
713
+ }
714
+ function withStoreDir(command, flag, storeDir) {
715
+ if (flag === null) return command;
716
+ return `${command} ${flag ?? "--store-dir"} ${JSON.stringify(storeDir)}`;
717
+ }
718
+ async function runSignoff(options = {}) {
719
+ const startedAt = /* @__PURE__ */ new Date();
720
+ const wallStart = Date.now();
721
+ const repoDir = resolve3(options.repoDir ?? process.cwd());
722
+ const repoRoot = repoRootOf(repoDir);
723
+ const { config, origin } = await loadSignoffConfig({ repoRoot, configPath: options.configPath });
724
+ validateGraph(config.steps.map((step) => ({ name: step.name, needs: step.needs })));
725
+ assertShuffleArgsReachTheRunner(config.steps);
726
+ const nodeRequirement = resolveNodeRequirement(repoRoot, config.nodeVersion);
727
+ assertNodeVersion(nodeRequirement);
728
+ const source = options.source ?? "working-tree";
729
+ const cacheDir = resolve3(options.cacheDir ?? config.cacheDir ?? DEFAULT_CACHE_DIR);
730
+ const treePath = join6(cacheDir, "trees", `${basename(repoRoot)}-${process.pid}`);
731
+ let tree = null;
732
+ try {
733
+ tree = materializeCleanTree({ repoDir: repoRoot, dest: treePath, source, carryFiles: config.carryFiles });
734
+ options.onEvent?.({ kind: "tree", path: tree.path, head: tree.head, dirty: tree.dirty });
735
+ const store = resolveStore({ treePath: tree.path, cacheDir, generations: config.storeGenerations });
736
+ options.onEvent?.({ kind: "store", storeDir: store.storeDir, cacheHit: store.hit, cacheKey: store.cacheKey });
737
+ const installSpec = config.install ?? {};
738
+ const installCwd = join6(tree.path, installSpec.cwd ?? ".");
739
+ const installCommand = withStoreDir(
740
+ installSpec.run ?? "pnpm install --frozen-lockfile",
741
+ installSpec.storeDirFlag,
742
+ store.storeDir
743
+ );
744
+ const storeEnvName = installSpec.storeEnv === null ? null : installSpec.storeEnv ?? "NPM_CONFIG_STORE_DIR";
745
+ const sharedEnv = buildEnv(process.env, [
746
+ // Parity with CI: a runner that behaves differently under `CI` (vitest's
747
+ // reporter, wrangler's prompts) must behave that way here too.
748
+ { CI: "true" },
749
+ config.env,
750
+ storeEnvName === null ? void 0 : { [storeEnvName]: store.storeDir }
751
+ ]);
752
+ options.onEvent?.({ kind: "install-start", command: installCommand });
753
+ const installResult = await runCommand({
754
+ command: installCommand,
755
+ cwd: installCwd,
756
+ env: buildEnv(sharedEnv, [installSpec.env]),
757
+ timeoutMs: installSpec.timeoutMs
758
+ });
759
+ options.onEvent?.({ kind: "install-end", exitCode: installResult.exitCode, durationMs: installResult.durationMs });
760
+ const install = {
761
+ command: installCommand,
762
+ storeDir: store.storeDir,
763
+ cacheKey: store.cacheKey,
764
+ cacheHit: store.hit,
765
+ keyedOn: store.keyedOn,
766
+ exitCode: installResult.exitCode,
767
+ durationMs: installResult.durationMs,
768
+ output: installResult.exitCode === 0 ? installResult.output.slice(-SUCCESS_OUTPUT_TAIL) : installResult.output,
769
+ outputTruncated: installResult.truncated
770
+ };
771
+ const host = hostFacts(tree.path, nodeRequirement);
772
+ const seedBase = options.seed ?? newSeedBase();
773
+ if (installResult.exitCode !== 0) {
774
+ return finish({
775
+ ok: false,
776
+ startedAt,
777
+ wallStart,
778
+ tree,
779
+ origin,
780
+ host,
781
+ install,
782
+ steps: config.steps.map(
783
+ (step) => ({
784
+ name: step.name,
785
+ status: "skipped",
786
+ attempts: [],
787
+ durationMs: 0,
788
+ startedAtMs: null,
789
+ finishedAtMs: null
790
+ })
791
+ ),
792
+ seedBase,
793
+ keepGoing: options.keepGoing ?? false,
794
+ workspaceRetained: options.keepWorkspace ?? false,
795
+ source,
796
+ options
797
+ });
798
+ }
799
+ const treeRoot = tree.path;
800
+ const outcomes = await runGraph({
801
+ nodes: config.steps,
802
+ maxParallel: options.maxParallel ?? config.maxParallel ?? availableParallelism(),
803
+ keepGoing: options.keepGoing ?? false,
804
+ run: async (step, signal) => {
805
+ const attempts = [];
806
+ for (const plan of planAttempts(step, seedBase, options.shuffleRuns)) {
807
+ options.onEvent?.({ kind: "step-start", name: step.name, command: plan.command, seed: plan.seed });
808
+ const result = await runCommand({
809
+ command: plan.command,
810
+ cwd: join6(treeRoot, step.cwd ?? "."),
811
+ env: buildEnv(sharedEnv, [step.env]),
812
+ timeoutMs: step.timeoutMs,
813
+ signal
814
+ });
815
+ const ok = result.exitCode === 0;
816
+ attempts.push(toAttempt(result, plan.seed, ok));
817
+ if (!ok) {
818
+ emitStepEnd(options, step.name, "failed", attempts);
819
+ return { ok: false, value: attempts };
820
+ }
821
+ }
822
+ emitStepEnd(options, step.name, "passed", attempts);
823
+ return { ok: true, value: attempts };
824
+ }
825
+ });
826
+ const steps = outcomes.map(toStepResult);
827
+ return finish({
828
+ ok: steps.every((step) => step.status === "passed"),
829
+ startedAt,
830
+ wallStart,
831
+ tree,
832
+ origin,
833
+ host,
834
+ install,
835
+ steps,
836
+ seedBase,
837
+ keepGoing: options.keepGoing ?? false,
838
+ workspaceRetained: options.keepWorkspace ?? false,
839
+ source,
840
+ options
841
+ });
842
+ } finally {
843
+ if (tree && !options.keepWorkspace && existsSync6(tree.path)) removeCleanTree(tree);
844
+ }
845
+ }
846
+ function emitStepEnd(options, name, status, attempts) {
847
+ options.onEvent?.({
848
+ kind: "step-end",
849
+ name,
850
+ status,
851
+ durationMs: attempts.reduce((total, attempt) => total + attempt.durationMs, 0)
852
+ });
853
+ }
854
+ function toStepResult(outcome) {
855
+ const attempts = outcome.value ?? [];
856
+ const durationMs = attempts.reduce((total, attempt) => total + attempt.durationMs, 0);
857
+ return {
858
+ name: outcome.name,
859
+ status: outcome.status,
860
+ attempts,
861
+ durationMs,
862
+ startedAtMs: outcome.startedAtMs,
863
+ finishedAtMs: outcome.finishedAtMs
864
+ };
865
+ }
866
+ function finish(input) {
867
+ const serialMs = input.install.durationMs + input.steps.reduce((total, step) => total + step.durationMs, 0);
868
+ const flags = [
869
+ `--source ${input.source}`,
870
+ `--seed ${input.seedBase}`,
871
+ ...input.keepGoing ? ["--keep-going"] : []
872
+ ];
873
+ return {
874
+ ok: input.ok,
875
+ startedAt: input.startedAt.toISOString(),
876
+ repo: {
877
+ root: input.tree.root,
878
+ head: input.tree.head,
879
+ branch: input.tree.branch,
880
+ source: input.source,
881
+ dirty: input.tree.dirty,
882
+ diffSha256: input.tree.diffSha256,
883
+ untrackedFiles: input.tree.untrackedFiles,
884
+ carriedFiles: input.tree.carriedFiles
885
+ },
886
+ configOrigin: input.origin,
887
+ workspace: input.tree.path,
888
+ workspaceRetained: input.workspaceRetained,
889
+ host: input.host,
890
+ install: input.install,
891
+ steps: input.steps,
892
+ seedBase: input.seedBase,
893
+ wallClockMs: Date.now() - input.wallStart,
894
+ serialMs,
895
+ keepGoing: input.keepGoing,
896
+ reproduce: `agent-app-signoff ${input.tree.root} ${flags.join(" ")}`
897
+ };
898
+ }
899
+
900
+ // src/signoff/report.ts
901
+ var BAR = "\u2500".repeat(72);
902
+ function ms(value) {
903
+ return value >= 1e4 ? `${(value / 1e3).toFixed(1)}s` : `${value}ms`;
904
+ }
905
+ function statusMark(status) {
906
+ switch (status) {
907
+ case "passed":
908
+ return "ok ";
909
+ case "failed":
910
+ return "FAIL";
911
+ case "cancelled":
912
+ return "kill";
913
+ case "blocked":
914
+ return "blkd";
915
+ case "skipped":
916
+ return "-- ";
917
+ }
918
+ }
919
+ function seedList(step) {
920
+ const seeds = step.attempts.map((attempt) => attempt.seed).filter((seed) => seed !== null);
921
+ return seeds.length === 0 ? "" : ` seeds ${seeds.join(", ")}`;
922
+ }
923
+ function peakConcurrency(steps) {
924
+ const events = [];
925
+ for (const step of steps) {
926
+ if (step.startedAtMs === null || step.finishedAtMs === null) continue;
927
+ events.push({ at: step.startedAtMs, delta: 1 }, { at: step.finishedAtMs, delta: -1 });
928
+ }
929
+ events.sort((a, b) => a.at - b.at || a.delta - b.delta);
930
+ let current = 0;
931
+ let peak = 0;
932
+ for (const event of events) {
933
+ current += event.delta;
934
+ peak = Math.max(peak, current);
935
+ }
936
+ return peak;
937
+ }
938
+ function formatSignoffReport(report) {
939
+ const lines = [];
940
+ const verdict = report.ok ? "SIGN-OFF PASSED" : "SIGN-OFF FAILED";
941
+ lines.push(BAR, `${verdict} \u2014 ${report.repo.branch} @ ${report.repo.head.slice(0, 12)}`, BAR, "");
942
+ lines.push("subject");
943
+ lines.push(` repo ${report.repo.root}`);
944
+ lines.push(` source ${report.repo.source}${report.repo.dirty ? " (working tree carries uncommitted work)" : ""}`);
945
+ if (report.repo.diffSha256) lines.push(` patch sha256:${report.repo.diffSha256.slice(0, 16)}`);
946
+ if (report.repo.untrackedFiles.length > 0) {
947
+ lines.push(` untracked ${report.repo.untrackedFiles.length} file(s) copied in`);
948
+ }
949
+ if (report.repo.carriedFiles.length > 0) lines.push(` carried ${report.repo.carriedFiles.join(", ")}`);
950
+ lines.push("");
951
+ lines.push("environment");
952
+ lines.push(` clean tree ${report.workspace}${report.workspaceRetained ? " (retained)" : " (removed)"}`);
953
+ lines.push(` install ${report.install.command}`);
954
+ lines.push(
955
+ ` store ${report.install.cacheHit ? "warm" : "cold"} \u2014 ${report.install.cacheKey.slice(0, 16)} (keyed on ${report.install.keyedOn.length} manifest file(s))`
956
+ );
957
+ lines.push(
958
+ ` host ${report.host.node} \xB7 ${report.host.packageManager} \xB7 ${report.host.cpus} cpus` + (report.host.nodePinned === null ? " \xB7 node UNPINNED by this repo" : ` \xB7 pinned ${report.host.nodePinned} (${report.host.nodePinSource})`)
959
+ );
960
+ lines.push(
961
+ ` config ${report.configOrigin.kind === "derived" ? `derived from scripts: ${report.configOrigin.scripts.join(", ")}` : report.configOrigin.path}`
962
+ );
963
+ lines.push("");
964
+ const width = Math.max(...report.steps.map((step) => step.name.length), "install".length);
965
+ lines.push("steps");
966
+ lines.push(
967
+ ` ${report.install.exitCode === 0 ? "ok " : "FAIL"} ${"install".padEnd(width)} ${ms(report.install.durationMs).padStart(8)}`
968
+ );
969
+ for (const step of report.steps) {
970
+ const window = step.startedAtMs === null || step.finishedAtMs === null ? "" : ` [${ms(step.startedAtMs)} \u2192 ${ms(step.finishedAtMs)}]`;
971
+ lines.push(
972
+ ` ${statusMark(step.status)} ${step.name.padEnd(width)} ${ms(step.durationMs).padStart(8)} ${step.attempts.length} run(s)${window}${seedList(step)}`
973
+ );
974
+ }
975
+ lines.push("");
976
+ const peak = peakConcurrency(report.steps);
977
+ const saved = report.serialMs - report.wallClockMs;
978
+ lines.push("timing");
979
+ lines.push(` wall clock ${ms(report.wallClockMs)}`);
980
+ lines.push(` serial sum ${ms(report.serialMs)} (install + every step, one after another)`);
981
+ lines.push(
982
+ ` parallel peak ${peak} step(s) at once \u2014 ` + (saved > 0 ? `${ms(saved)} saved, ${(report.serialMs / report.wallClockMs).toFixed(2)}x` : "no overlap available")
983
+ );
984
+ lines.push("");
985
+ const failures = report.steps.filter((step) => step.status === "failed" || step.status === "cancelled");
986
+ if (report.install.exitCode !== 0) {
987
+ lines.push(BAR, "install FAILED \u2014 no step could run", BAR, report.install.output.trimEnd(), "");
988
+ }
989
+ for (const step of failures) {
990
+ const last = step.attempts[step.attempts.length - 1];
991
+ lines.push(BAR);
992
+ lines.push(`${step.status === "cancelled" ? "CANCELLED" : "FAILED"}: ${step.name}`);
993
+ if (last) {
994
+ lines.push(` command ${last.command}`);
995
+ lines.push(` exit ${last.exitCode}${last.signal ? ` (${last.signal})` : ""}${last.timedOut ? " \u2014 TIMED OUT" : ""}`);
996
+ if (last.seed !== null) {
997
+ lines.push(` seed ${last.seed} \u2014 replay this order alone with the same seed`);
998
+ }
999
+ lines.push(BAR, last.output.trimEnd(), "");
1000
+ }
1001
+ }
1002
+ const blocked = report.steps.filter((step) => step.status === "blocked" || step.status === "skipped");
1003
+ if (blocked.length > 0) {
1004
+ lines.push(`not judged: ${blocked.map((step) => `${step.name} (${step.status})`).join(", ")}`);
1005
+ lines.push("");
1006
+ }
1007
+ lines.push(`reproduce: ${report.reproduce}`);
1008
+ return lines.join("\n");
1009
+ }
1010
+ function formatSignoffLine(report) {
1011
+ const passed = report.steps.filter((step) => step.status === "passed").length;
1012
+ return `${report.ok ? "signoff PASS" : "signoff FAIL"} ${report.repo.head.slice(0, 12)} \u2014 ${passed}/${report.steps.length} steps, ${ms(report.wallClockMs)} wall (${ms(report.serialMs)} serial), seed ${report.seedBase}, ${report.install.cacheHit ? "warm" : "cold"} store, clean install`;
1013
+ }
1014
+
1015
+ export {
1016
+ SIGNOFF_CONFIG_FILES,
1017
+ parseSignoffConfig,
1018
+ deriveSignoffConfig,
1019
+ loadSignoffConfig,
1020
+ triggersOnPullRequest,
1021
+ scanMergeGateNodePins,
1022
+ resolveWorkflowNodePin,
1023
+ resolveNodeRequirement,
1024
+ assertNodeVersion,
1025
+ runCommand,
1026
+ validateGraph,
1027
+ runGraph,
1028
+ DEFAULT_SHUFFLE_ARGS,
1029
+ DEFAULT_SHUFFLE_RUNS,
1030
+ newSeedBase,
1031
+ deriveSeed,
1032
+ assertShuffleArgsReachTheRunner,
1033
+ planAttempts,
1034
+ MANIFEST_FILES,
1035
+ manifestFiles,
1036
+ manifestCacheKey,
1037
+ resolveStore,
1038
+ repoRootOf,
1039
+ materializeCleanTree,
1040
+ removeCleanTree,
1041
+ runSignoff,
1042
+ peakConcurrency,
1043
+ formatSignoffReport,
1044
+ formatSignoffLine
1045
+ };
1046
+ //# sourceMappingURL=chunk-WD2B6HGY.js.map