@tangle-network/agent-app 0.60.215 → 0.60.217

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 (42) hide show
  1. package/dist/chat-routes/index.js +1 -1
  2. package/dist/{chunk-QA2CBATZ.js → chunk-4QUOPWIG.js} +12 -9
  3. package/dist/chunk-4QUOPWIG.js.map +1 -0
  4. package/dist/chunk-CD2LQXY6.js +306 -0
  5. package/dist/chunk-CD2LQXY6.js.map +1 -0
  6. package/dist/chunk-LRYY3OX7.js +105 -0
  7. package/dist/chunk-LRYY3OX7.js.map +1 -0
  8. package/dist/{chunk-LLO436QO.js → chunk-QM7RTTDY.js} +27 -86
  9. package/dist/chunk-QM7RTTDY.js.map +1 -0
  10. package/dist/{chunk-KDPTXXOQ.js → chunk-WDY47GBR.js} +812 -115
  11. package/dist/chunk-WDY47GBR.js.map +1 -0
  12. package/dist/{chunk-ZRLKB4KB.js → chunk-YWLZOPDS.js} +10 -11
  13. package/dist/{chunk-ZRLKB4KB.js.map → chunk-YWLZOPDS.js.map} +1 -1
  14. package/dist/launch-invariants/catalog.d.ts +16 -0
  15. package/dist/launch-invariants/cli.js +5 -2
  16. package/dist/launch-invariants/cli.js.map +1 -1
  17. package/dist/launch-invariants/index.d.ts +1 -1
  18. package/dist/launch-invariants/index.js +13 -5
  19. package/dist/launch-invariants/index.js.map +1 -1
  20. package/dist/launch-invariants/limit-alarms.d.ts +19 -1
  21. package/dist/launch-invariants/testing.d.ts +0 -3
  22. package/dist/launch-invariants/testing.js +5 -2
  23. package/dist/sandbox/index.d.ts +3 -3
  24. package/dist/sandbox/index.js +1 -1
  25. package/dist/signoff/app-defaults.d.ts +65 -0
  26. package/dist/signoff/cache.d.ts +29 -0
  27. package/dist/signoff/cli.js +75 -4
  28. package/dist/signoff/cli.js.map +1 -1
  29. package/dist/signoff/defaults-cli.d.ts +1 -0
  30. package/dist/signoff/index.js +2 -1
  31. package/dist/signoff/store.d.ts +2 -0
  32. package/dist/signoff/types.d.ts +12 -0
  33. package/dist/signoff/worker-defaults.d.ts +8 -0
  34. package/dist/turn-health/index.js +12 -291
  35. package/dist/turn-health/index.js.map +1 -1
  36. package/dist/turn-trace/index.js +20 -3
  37. package/dist/turn-trace/index.js.map +1 -1
  38. package/dist/turn-trace/otel.d.ts +11 -4
  39. package/package.json +5 -2
  40. package/dist/chunk-KDPTXXOQ.js.map +0 -1
  41. package/dist/chunk-LLO436QO.js.map +0 -1
  42. package/dist/chunk-QA2CBATZ.js.map +0 -1
@@ -1,3 +1,7 @@
1
+ import {
2
+ LAUNCH_BUDGETS
3
+ } from "./chunk-LRYY3OX7.js";
4
+
1
5
  // src/signoff/worker-defaults.ts
2
6
  import { spawnSync } from "child_process";
3
7
  import { existsSync } from "fs";
@@ -6,10 +10,13 @@ import { basename, dirname, join } from "path";
6
10
  var WORKER_DEFAULTS_STEP = "worker defaults";
7
11
  var WORKER_COMPATIBILITY_DATE_FLOOR = "2026-09-01";
8
12
  var WRANGLER_CONFIG_NAME = /^wrangler[.-]?[\w.-]*\.(toml|json|jsonc)$/;
9
- function trackedWorkerConfigs(repoRoot) {
10
- const listed = spawnSync("git", ["ls-files", "-z"], { cwd: repoRoot, encoding: "utf8" });
13
+ function trackedFiles(repoRoot) {
14
+ const listed = spawnSync("git", ["ls-files", "-z"], { cwd: repoRoot, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
11
15
  if (listed.status !== 0) throw new Error(`signoff: git ls-files failed in ${repoRoot}: ${listed.stderr}`);
12
- return listed.stdout.split("\0").filter((path) => path !== "" && WRANGLER_CONFIG_NAME.test(basename(path))).sort();
16
+ return listed.stdout.split("\0").filter((path) => path !== "");
17
+ }
18
+ function trackedWorkerConfigs(repoRoot) {
19
+ return trackedFiles(repoRoot).filter((path) => WRANGLER_CONFIG_NAME.test(basename(path))).sort();
13
20
  }
14
21
  function problemsFor(config) {
15
22
  const problems = [];
@@ -98,10 +105,351 @@ function formatWorkerDefaults(result) {
98
105
  }
99
106
 
100
107
  // src/signoff/config.ts
101
- import { existsSync as existsSync2, readFileSync } from "fs";
102
- import { join as join2, resolve } from "path";
108
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
109
+ import { join as join3, resolve } from "path";
103
110
  import { pathToFileURL } from "url";
104
111
  import { z } from "zod";
112
+
113
+ // src/signoff/app-defaults.ts
114
+ import { spawnSync as spawnSync2 } from "child_process";
115
+ import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, statSync } from "fs";
116
+ import { tmpdir } from "os";
117
+ import { join as join2, posix } from "path";
118
+ var APP_DEFAULTS_STEP = "agent-app defaults";
119
+ var EXEMPTIBLE_DEFAULTS = ["turn-trace", "first-text", "prewarm", "placement", "limit-alarms"];
120
+ var AGENT_APP_OUTCOME_FLOORS = [
121
+ { id: "turn-trace", label: "turn trace", since: "0.60.199" },
122
+ { id: "first-text", label: "first text", since: "0.60.203" }
123
+ ];
124
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
125
+ var NOT_SOURCE = /(?:^|\/)(?:node_modules|dist|build|\.wrangler|tests?|__tests__|e2e|scripts|proof|journeys?|evals?|docs|examples|stories|\.storybook)\/|\.(?:test|spec|stories|e2e|bench)\.[cm]?[jt]sx?$|\.d\.ts$/;
126
+ var MAX_SOURCE_BYTES = 400 * 1024;
127
+ function readJson(path) {
128
+ try {
129
+ return JSON.parse(readFileSync(path, "utf8"));
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ function findAppPackages(repoRoot, tracked) {
135
+ const packages = [];
136
+ for (const path of tracked) {
137
+ if (posix.basename(path) !== "package.json" || /(?:^|\/)node_modules\//.test(path)) continue;
138
+ const pkg = readJson(join2(repoRoot, path));
139
+ if (!pkg || pkg.name === "@tangle-network/agent-app") continue;
140
+ const spec = pkg.dependencies?.["@tangle-network/agent-app"] ?? pkg.devDependencies?.["@tangle-network/agent-app"] ?? pkg.peerDependencies?.["@tangle-network/agent-app"];
141
+ if (spec === void 0) continue;
142
+ packages.push({ name: pkg.name ?? posix.dirname(path), dir: posix.dirname(path), spec });
143
+ }
144
+ return packages;
145
+ }
146
+ function underDir(path, dir) {
147
+ return dir === "." || path === dir || path.startsWith(`${dir}/`);
148
+ }
149
+ function withoutComments(text) {
150
+ return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(?<!:)\/\/.*$/gm, "");
151
+ }
152
+ function readSources(repoRoot, tracked, dir, others) {
153
+ const files = [];
154
+ for (const path of tracked) {
155
+ if (!underDir(path, dir) || !SOURCE_EXTENSIONS.test(path) || NOT_SOURCE.test(path)) continue;
156
+ if (others.some((other) => other !== dir && other !== "." && underDir(path, other) && underDir(other, dir))) continue;
157
+ const abs = join2(repoRoot, path);
158
+ try {
159
+ if (statSync(abs).size > MAX_SOURCE_BYTES) continue;
160
+ files.push({ path, text: withoutComments(readFileSync(abs, "utf8")) });
161
+ } catch {
162
+ }
163
+ }
164
+ return files;
165
+ }
166
+ function versionTuple(spec) {
167
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
168
+ if (!match || /^(?:workspace|file|link|portal|git|github|http|catalog):/.test(spec)) return null;
169
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
170
+ }
171
+ function atLeast(have, floor) {
172
+ const want = versionTuple(floor);
173
+ for (let index = 0; index < 3; index += 1) {
174
+ if (have[index] !== want[index]) return have[index] > want[index];
175
+ }
176
+ return true;
177
+ }
178
+ function filesMatching(sources, pattern) {
179
+ return sources.filter((file) => pattern.test(file.text));
180
+ }
181
+ function callArguments(text, openIndex) {
182
+ let depth = 0;
183
+ for (let index = openIndex; index < Math.min(text.length, openIndex + 1200); index += 1) {
184
+ const char = text[index];
185
+ if (char === "(") depth += 1;
186
+ else if (char === ")" && --depth === 0) return text.slice(openIndex, index + 1);
187
+ }
188
+ return text.slice(openIndex, openIndex + 1200);
189
+ }
190
+ function sinkCalls(sources) {
191
+ const calls = [];
192
+ for (const file of sources) {
193
+ for (const match of file.text.matchAll(/createIntelligenceTurnTraceSink\s*\(/g)) {
194
+ const args = callArguments(file.text, match.index + match[0].length - 1);
195
+ const direct = /\bproject\s*:\s*['"`]([^'"`]+)['"`]/.exec(args);
196
+ const named = /\bproject\s*:\s*([A-Za-z_$][\w$]*)\s*[,}\n]/.exec(args) ?? /\bproject\b\s*[,}]/.exec(args);
197
+ const identifier = named?.[1] ?? (named ? "project" : void 0);
198
+ const constant = identifier ? new RegExp(`(?:const|let)\\s+${identifier}\\s*(?::\\s*string\\s*)?=\\s*['"\`]([^'"\`]+)['"\`]`).exec(file.text) : null;
199
+ calls.push({ file: file.path, args, project: direct?.[1] ?? constant?.[1] ?? null });
200
+ }
201
+ }
202
+ return calls;
203
+ }
204
+ function pinRow(pin, spec) {
205
+ const tuple = pin === null ? null : versionTuple(pin);
206
+ if (pin === null || tuple === null) {
207
+ return { id: "pin", label: "agent-app pin", status: "unknown", detail: `"${spec}" is not a plain version; the floors cannot be compared` };
208
+ }
209
+ const below = AGENT_APP_OUTCOME_FLOORS.filter((floor) => !atLeast(tuple, floor.since));
210
+ const floors = AGENT_APP_OUTCOME_FLOORS.map((floor) => `${floor.label} ${floor.since}`).join(", ");
211
+ return below.length === 0 ? { id: "pin", label: "agent-app pin", status: "ok", detail: `${pin} (floors: ${floors})` } : { id: "pin", label: "agent-app pin", status: "gap", detail: `${pin} is below the ${below.map((floor) => `${floor.label} floor ${floor.since}`).join(" and the ")}` };
212
+ }
213
+ function withExemption(row, id, exemptions, optedOut) {
214
+ const reason = exemptions[id];
215
+ if (optedOut !== null) {
216
+ return reason ? { row: { ...row, status: "exempt", detail: `${optedOut}; exempt: ${reason}` }, finding: null } : { row: { ...row, status: "opted-out", detail: optedOut }, finding: `${row.label}: ${optedOut}, with no reason in signoff.config.mjs agentApp.exemptions.${id}` };
217
+ }
218
+ if (row.status !== "ok" && row.status !== "na" && reason) {
219
+ return { row: { ...row, status: "exempt", detail: `${row.detail}; exempt: ${reason}` }, finding: null };
220
+ }
221
+ return { row, finding: null };
222
+ }
223
+ function turnTraceRow(sources, calls, exports) {
224
+ const optOut = filesMatching(sources, /\bturnTrace\s*:\s*false\b/)[0];
225
+ if (optOut) return { row: { id: "turn-trace", label: "turn trace", status: "gap", detail: "" }, optedOut: `turnTrace: false in ${optOut.path}`, wired: false };
226
+ const customSink = filesMatching(sources, /\bturnTrace\s*:\s*\{/);
227
+ if (calls.length === 0 && customSink.length === 0) {
228
+ return { row: { id: "turn-trace", label: "turn trace", status: "gap", detail: "no turn trace sink; createChatTurnRoutes gets no turnTrace" }, optedOut: null, wired: false };
229
+ }
230
+ const project = calls.find((call) => call.project)?.project ?? null;
231
+ const last = project === null ? void 0 : exports?.get(project);
232
+ const where = calls[0]?.file ?? customSink[0]?.path ?? "";
233
+ const wired = `sink wired in ${where}${project ? ` (${project})` : ""}`;
234
+ if (last) {
235
+ return { row: { id: "turn-trace", label: "turn trace", status: "ok", detail: `${wired}; last export accepted ${new Date(last.at).toISOString().slice(0, 16)}Z` }, optedOut: null, wired: true };
236
+ }
237
+ if (last === null) {
238
+ return { row: { id: "turn-trace", label: "turn trace", status: "gap", detail: `${wired}; Intelligence holds no turn from the last 30 days for this key` }, optedOut: null, wired: true };
239
+ }
240
+ const routerKey = calls.find((call) => /\bapiKey\s*:[^,}]*\bTANGLE_API_KEY\b/.test(call.args));
241
+ if (routerKey) {
242
+ return { row: { id: "turn-trace", label: "turn trace", status: "unknown", detail: `${wired} with TANGLE_API_KEY; the ingest refused it for Legal (#468): run with --probe, or drop apiKey so the sink reads the Sandbox product key` }, optedOut: null, wired: true };
243
+ }
244
+ return { row: { id: "turn-trace", label: "turn trace", status: "ok", detail: `${wired}; last export not checked` }, optedOut: null, wired: true };
245
+ }
246
+ function firstTextRow(pin, sources, traceWired, calls, exports) {
247
+ const floor = AGENT_APP_OUTCOME_FLOORS.find((entry) => entry.id === "first-text")?.since ?? "";
248
+ const tuple = pin === null ? null : versionTuple(pin);
249
+ const marks = filesMatching(sources, /\.markFirstText\s*\(/).length > 0;
250
+ const routes = filesMatching(sources, /\bcreateChatTurnRoutes\s*[(<]/).length > 0;
251
+ if (!traceWired) return { id: "first-text", label: "first text", status: "gap", detail: "needs the turn trace" };
252
+ if (tuple !== null && !atLeast(tuple, floor)) return { id: "first-text", label: "first text", status: "gap", detail: `agent-app ${pin} does not mark first text; ${floor} does` };
253
+ if (!routes && !marks) return { id: "first-text", label: "first text", status: "gap", detail: "the turn owner does not call trace.markFirstText() and the app does not use createChatTurnRoutes" };
254
+ const project = calls.find((call) => call.project)?.project ?? null;
255
+ const last = project === null ? void 0 : exports?.get(project);
256
+ const observed = last?.firstTextMs === void 0 ? "" : `; last turn first_text_ms ${Math.round(last.firstTextMs)}`;
257
+ if (last && last.firstTextMs === void 0) return { id: "first-text", label: "first text", status: "gap", detail: "the last exported turn has no tangle.turn.first_text_ms" };
258
+ return { id: "first-text", label: "first text", status: "ok", detail: `${routes ? "createChatTurnRoutes marks it" : "the turn owner marks it"}${observed}` };
259
+ }
260
+ function prewarmRow(sources) {
261
+ const usesBox = filesMatching(sources, /@tangle-network\/agent-app\/sandbox|\bensureWorkspaceSandbox\b/).length > 0;
262
+ if (!usesBox) return { id: "prewarm", label: "prewarm route", status: "na", detail: "the app opens no workspace box" };
263
+ const shared = filesMatching(sources, /\bcreateWorkspacePrewarmRoute\b/)[0];
264
+ if (shared) return { id: "prewarm", label: "prewarm route", status: "ok", detail: `shared route in ${shared.path}` };
265
+ const local = sources.find((file) => /prewarm/i.test(file.path) || /['"`/]prewarm['"`/]/.test(file.text));
266
+ return local ? { id: "prewarm", label: "prewarm route", status: "gap", detail: `local copy in ${local.path}; mount createWorkspacePrewarmRoute` } : { id: "prewarm", label: "prewarm route", status: "gap", detail: "no prewarm route; the first send waits for the box" };
267
+ }
268
+ function placementRow(configs) {
269
+ const pinned = configs.find((file) => /^\s*\[placement\]\s*$[\s\S]{0,300}?^\s*(?:region|host|hostname)\s*=/m.test(file.text) || /"placement"\s*:\s*\{[^}]*"(?:region|host|hostname)"/.test(file.text));
270
+ if (pinned) return { id: "placement", label: "placement", status: "ok", detail: `pinned in ${pinned.path}` };
271
+ if (configs.length === 0) return { id: "placement", label: "placement", status: "na", detail: "no Wrangler config" };
272
+ const smart = configs.some((file) => /^\s*\[placement\]/m.test(file.text) || /"placement"/.test(file.text));
273
+ return {
274
+ id: "placement",
275
+ label: "placement",
276
+ status: "gap",
277
+ detail: smart ? "a [placement] without a region or host is smart placement, which does not pin; unmeasured against the D1 primary" : "no [placement]; unmeasured against the D1 primary. Pin it beside D1 or declare an exemption with the reason"
278
+ };
279
+ }
280
+ function limitAlarmsRow(sources) {
281
+ const wired = filesMatching(sources, /\bwithD1LimitAlarms\s*\(/);
282
+ const copies = filesMatching(sources, /\bcreateLimitAlarms\s*\(/);
283
+ const product = filesMatching(sources, /\bcreateProductLimitAlarms\s*\(/);
284
+ if (wired.length === 0) {
285
+ return {
286
+ id: "limit-alarms",
287
+ label: "limit alarms",
288
+ status: "gap",
289
+ detail: copies.length + product.length > 0 ? `alarms are built in ${(copies[0] ?? product[0])?.path} but nothing wraps the D1 binding with withD1LimitAlarms in the server` : "no limit alarms"
290
+ };
291
+ }
292
+ if (copies.length > 0) {
293
+ return { id: "limit-alarms", label: "limit alarms", status: "gap", detail: `wired in ${wired[0]?.path}; ${copies[0]?.path} keeps a local copy of the budgets: use createProductLimitAlarms with overrides` };
294
+ }
295
+ return { id: "limit-alarms", label: "limit alarms", status: "ok", detail: `wired in ${wired[0]?.path}` };
296
+ }
297
+ function parseWranglerUpload(output) {
298
+ const clean = output.replace(/\u001b\[[0-9;]*m/g, "");
299
+ const match = /Total Upload:\s*([\d.]+)\s*KiB\s*\/\s*gzip:\s*([\d.]+)\s*KiB/.exec(clean);
300
+ return match ? { rawKiB: Number(match[1]), gzipKiB: Number(match[2]) } : null;
301
+ }
302
+ function wranglerBinary(repoRoot, dir) {
303
+ for (const base of [join2(repoRoot, dir), repoRoot]) {
304
+ const bin = join2(base, "node_modules", ".bin", "wrangler");
305
+ if (existsSync2(bin)) return bin;
306
+ }
307
+ return null;
308
+ }
309
+ function bundleRow(repoRoot, dir, configs, measure) {
310
+ const label = "Worker upload";
311
+ if (configs.length === 0) return { id: "worker-bundle", label, status: "na", detail: "no Wrangler config" };
312
+ if (!measure) return { id: "worker-bundle", label, status: "unknown", detail: "not measured" };
313
+ const bin = wranglerBinary(repoRoot, dir);
314
+ if (!bin) return { id: "worker-bundle", label, status: "unknown", detail: "wrangler is not installed; install, build, then measure" };
315
+ const outDir = mkdtempSync(join2(tmpdir(), "agent-app-upload-"));
316
+ try {
317
+ const run = spawnSync2(bin, ["deploy", "--dry-run", "--env=", "--outdir", outDir], {
318
+ cwd: join2(repoRoot, dir),
319
+ encoding: "utf8",
320
+ timeout: 5 * 6e4,
321
+ maxBuffer: 20 * 1024 * 1024,
322
+ env: { ...process.env, CI: "1", WRANGLER_SEND_METRICS: "false", NO_COLOR: "1" }
323
+ });
324
+ const upload = parseWranglerUpload(`${run.stdout}
325
+ ${run.stderr}`);
326
+ if (!upload) {
327
+ const reason = `${run.stderr}${run.stdout}`.split("\n").map((line) => line.trim()).find((line) => /error|not found|no such/i.test(line)) ?? `wrangler exited ${run.status}`;
328
+ return { id: "worker-bundle", label, status: "unknown", detail: `unmeasured (build first): ${reason.slice(0, 160)}` };
329
+ }
330
+ const limit = LAUNCH_BUDGETS.workerUploadGzipKiB;
331
+ const share = upload.gzipKiB / limit;
332
+ return {
333
+ id: "worker-bundle",
334
+ label,
335
+ status: share >= LAUNCH_BUDGETS.alarmRatio ? "gap" : "ok",
336
+ detail: `${Math.round(upload.gzipKiB)} KiB gzip of ${limit} KiB (${Math.round(share * 100)}%); ${Math.round(upload.rawKiB)} KiB raw of ${LAUNCH_BUDGETS.workerUploadRawKiB} KiB`
337
+ };
338
+ } finally {
339
+ rmSync(outDir, { recursive: true, force: true });
340
+ }
341
+ }
342
+ function toolCountRow() {
343
+ return { id: "tool-count", label: "tool_count p50", status: "unknown", detail: "unmeasured; no ceiling until a measured link to first text exists" };
344
+ }
345
+ function checkAppDefaults(repoRoot, options = {}) {
346
+ const tracked = trackedFiles(repoRoot);
347
+ const packages = findAppPackages(repoRoot, tracked);
348
+ const owned = /* @__PURE__ */ new Map();
349
+ for (const path of trackedWorkerConfigs(repoRoot)) {
350
+ const owner = packages.filter((pkg) => underDir(path, pkg.dir)).sort((x, y) => y.dir.length - x.dir.length)[0];
351
+ if (owner) owned.set(owner.dir, [...owned.get(owner.dir) ?? [], path]);
352
+ }
353
+ const deployed = packages.filter((pkg) => owned.has(pkg.dir));
354
+ const apps = [];
355
+ const findings = [];
356
+ for (const pkg of deployed.length > 0 ? deployed : packages) {
357
+ const sources = readSources(repoRoot, tracked, pkg.dir, packages.map((other) => other.dir));
358
+ const configs = (owned.get(pkg.dir) ?? []).map((path) => ({ path, text: readFileSync(join2(repoRoot, path), "utf8") }));
359
+ const exemptions = options.exemptions ?? {};
360
+ const pin = versionTuple(pkg.spec) ? /(\d+\.\d+\.\d+)/.exec(pkg.spec)?.[1] ?? null : null;
361
+ const calls = sinkCalls(sources);
362
+ const trace = turnTraceRow(sources, calls, options.exports);
363
+ const traceWired = trace.wired;
364
+ const reports = [
365
+ { row: pinRow(pin, pkg.spec), id: null, optedOut: null },
366
+ { row: trace.row, id: "turn-trace", optedOut: trace.optedOut },
367
+ { row: firstTextRow(pin, sources, traceWired, calls, options.exports), id: "first-text", optedOut: null },
368
+ { row: prewarmRow(sources), id: "prewarm", optedOut: null },
369
+ { row: placementRow(configs), id: "placement", optedOut: null },
370
+ { row: toolCountRow(), id: null, optedOut: null },
371
+ { row: limitAlarmsRow(sources), id: "limit-alarms", optedOut: null },
372
+ { row: bundleRow(repoRoot, pkg.dir, configs, options.measureBundle ?? true), id: null, optedOut: null }
373
+ ];
374
+ const rows = [];
375
+ const appFindings = [];
376
+ for (const entry of reports) {
377
+ if (entry.id === null) {
378
+ rows.push(entry.row);
379
+ continue;
380
+ }
381
+ const resolved = withExemption(entry.row, entry.id, exemptions, entry.optedOut);
382
+ rows.push(resolved.row);
383
+ if (resolved.finding) appFindings.push(resolved.finding);
384
+ }
385
+ const traceResolved = rows.find((row) => row.id === "turn-trace");
386
+ if (trace.optedOut && traceResolved) {
387
+ const at = rows.findIndex((row) => row.id === "first-text");
388
+ rows[at] = { id: "first-text", label: "first text", status: traceResolved.status, detail: "the turn trace is turned off" };
389
+ }
390
+ apps.push({ app: pkg.name, dir: pkg.dir, pin, rows, findings: appFindings });
391
+ findings.push(...appFindings.map((finding) => `${pkg.name}: ${finding}`));
392
+ }
393
+ return { apps, findings };
394
+ }
395
+ var STATUS_MARK = {
396
+ ok: "ok ",
397
+ gap: "GAP ",
398
+ unknown: "? ",
399
+ exempt: "exempt ",
400
+ "opted-out": "FAIL ",
401
+ na: "n/a "
402
+ };
403
+ function formatAppDefaults(result) {
404
+ if (result.apps.length === 0) return "no agent app in this checkout; nothing to report\n";
405
+ const lines = [];
406
+ for (const app of result.apps) {
407
+ lines.push(`${app.app}${app.dir === "." ? "" : ` (${app.dir})`}`);
408
+ for (const row of app.rows) lines.push(` ${STATUS_MARK[row.status]}${row.label.padEnd(15)} ${row.detail}`);
409
+ }
410
+ for (const finding of result.findings) lines.push(`FAIL ${finding}`);
411
+ if (result.findings.length > 0) lines.push("Defaults and how to opt out with a reason: https://github.com/tangle-network/agent-app/blob/main/docs/app-defaults.md");
412
+ return `${lines.join("\n")}
413
+ `;
414
+ }
415
+ async function readLastTurnExport(project, options) {
416
+ const base = (options.baseUrl ?? "https://intelligence.tangle.tools").replace(/\/+$/, "");
417
+ const get = async (path) => {
418
+ const response = await (options.fetchImpl ?? fetch)(`${base}${path}`, {
419
+ headers: { authorization: `Bearer ${options.apiKey}` },
420
+ signal: AbortSignal.timeout(15e3)
421
+ });
422
+ if (!response.ok) throw new Error(`Intelligence answered HTTP ${response.status} for ${path.split("?")[0]}`);
423
+ return response.json();
424
+ };
425
+ const listing = await get(`/v1/traces?limit=1&q=agent_app.turn&attr=${encodeURIComponent(`tangle.project:${project}`)}`);
426
+ const newest = listing.items?.[0];
427
+ if (!newest) return null;
428
+ const spans = await get(`/v1/traces/${newest.traceId}/spans`);
429
+ const firstTextMs = Number(spans.items?.find((span) => span.name === "agent_app.turn")?.attributes?.["tangle.turn.first_text_ms"]);
430
+ return {
431
+ at: Number(BigInt(newest.startUnixNano) / 1000000n),
432
+ traceId: newest.traceId,
433
+ ...Number.isFinite(firstTextMs) ? { firstTextMs } : {}
434
+ };
435
+ }
436
+ async function collectLastExports(repoRoot, apiKey) {
437
+ const tracked = trackedFiles(repoRoot);
438
+ const projects = /* @__PURE__ */ new Set();
439
+ for (const pkg of findAppPackages(repoRoot, tracked)) {
440
+ for (const call of sinkCalls(readSources(repoRoot, tracked, pkg.dir, []))) if (call.project) projects.add(call.project);
441
+ }
442
+ const exports = /* @__PURE__ */ new Map();
443
+ for (const project of projects) {
444
+ try {
445
+ exports.set(project, await readLastTurnExport(project, { apiKey }));
446
+ } catch {
447
+ }
448
+ }
449
+ return exports;
450
+ }
451
+
452
+ // src/signoff/config.ts
105
453
  var SIGNOFF_CONFIG_FILES = ["signoff.config.mjs", "signoff.config.js"];
106
454
  var shuffleSchema = z.object({
107
455
  runs: z.number().int().positive().optional(),
@@ -133,7 +481,12 @@ var configSchema = z.object({
133
481
  carryFiles: z.array(z.string()).optional(),
134
482
  cacheDir: z.string().optional(),
135
483
  storeGenerations: z.number().int().positive().optional(),
136
- workerConfigs: z.array(z.string().min(1)).optional()
484
+ workerConfigs: z.array(z.string().min(1)).optional(),
485
+ agentApp: z.object({
486
+ exemptions: z.object(
487
+ Object.fromEntries(EXEMPTIBLE_DEFAULTS.map((id) => [id, z.string().trim().min(8, "say why in a sentence").optional()]))
488
+ ).strict().optional()
489
+ }).optional()
137
490
  });
138
491
  function describeIssues(error, where) {
139
492
  const lines = error.issues.map((issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
@@ -143,8 +496,10 @@ ${lines.join("\n")}`;
143
496
  function parseSignoffConfig(value, where) {
144
497
  const result = configSchema.safeParse(value);
145
498
  if (!result.success) throw new Error(describeIssues(result.error, where));
146
- if (result.data.steps.some((step) => step.name === WORKER_DEFAULTS_STEP)) {
147
- throw new Error(`signoff: ${where} declares a step named "${WORKER_DEFAULTS_STEP}", which the gate runs itself`);
499
+ for (const reserved of [WORKER_DEFAULTS_STEP, APP_DEFAULTS_STEP]) {
500
+ if (result.data.steps.some((step) => step.name === reserved)) {
501
+ throw new Error(`signoff: ${where} declares a step named "${reserved}", which the gate runs itself`);
502
+ }
148
503
  }
149
504
  return result.data;
150
505
  }
@@ -180,18 +535,18 @@ async function loadSignoffConfig(options) {
180
535
  const { repoRoot, configPath } = options;
181
536
  if (configPath !== void 0) {
182
537
  const abs = resolve(repoRoot, configPath);
183
- if (!existsSync2(abs)) throw new Error(`signoff: no config at ${abs}`);
538
+ if (!existsSync3(abs)) throw new Error(`signoff: no config at ${abs}`);
184
539
  return { config: await importConfig(abs), origin: { kind: "file", path: abs } };
185
540
  }
186
541
  for (const candidate of SIGNOFF_CONFIG_FILES) {
187
- const abs = join2(repoRoot, candidate);
188
- if (existsSync2(abs)) return { config: await importConfig(abs), origin: { kind: "file", path: abs } };
542
+ const abs = join3(repoRoot, candidate);
543
+ if (existsSync3(abs)) return { config: await importConfig(abs), origin: { kind: "file", path: abs } };
189
544
  }
190
- const pkgPath = join2(repoRoot, "package.json");
191
- if (!existsSync2(pkgPath)) {
545
+ const pkgPath = join3(repoRoot, "package.json");
546
+ if (!existsSync3(pkgPath)) {
192
547
  throw new Error(`signoff: ${repoRoot} has no package.json, no signoff.config.mjs, and nothing to derive from.`);
193
548
  }
194
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
549
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
195
550
  if (pkg.signoff !== void 0) {
196
551
  return { config: parseSignoffConfig(pkg.signoff, `${pkgPath} "signoff"`), origin: { kind: "package-json", path: pkgPath } };
197
552
  }
@@ -206,19 +561,303 @@ async function importConfig(abs) {
206
561
  }
207
562
 
208
563
  // src/signoff/run.ts
209
- import { spawnSync as spawnSync3 } from "child_process";
210
- import { existsSync as existsSync7 } from "fs";
564
+ import { spawnSync as spawnSync5 } from "child_process";
565
+ import { randomUUID as randomUUID2 } from "crypto";
566
+ import { existsSync as existsSync9 } from "fs";
211
567
  import { availableParallelism, homedir } from "os";
212
- import { basename as basename2, join as join7, resolve as resolve3 } from "path";
568
+ import { basename as basename4, join as join9, resolve as resolve4 } from "path";
569
+
570
+ // src/signoff/cache.ts
571
+ import { randomUUID } from "crypto";
572
+ import { spawnSync as spawnSync3 } from "child_process";
573
+ import {
574
+ existsSync as existsSync4,
575
+ lstatSync,
576
+ mkdirSync,
577
+ readFileSync as readFileSync3,
578
+ readdirSync,
579
+ renameSync,
580
+ rmSync as rmSync2,
581
+ readlinkSync,
582
+ realpathSync,
583
+ statSync as statSync2,
584
+ unlinkSync,
585
+ writeFileSync
586
+ } from "fs";
587
+ import { hostname } from "os";
588
+ import { basename as basename2, join as join4, relative, resolve as resolve2, sep } from "path";
589
+ import lockfile from "proper-lockfile";
590
+ var SIGNOFF_CACHE_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1e3;
591
+ var SIGNOFF_CACHE_MAX_BYTES = 4 * 1024 ** 3;
592
+ function processStart(pid) {
593
+ try {
594
+ const stat = readFileSync3(`/proc/${pid}/stat`, "utf8");
595
+ return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19] ?? null;
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+ function hasRunProcesses(run) {
601
+ if (process.platform !== "linux") return true;
602
+ try {
603
+ for (const entry of readdirSync("/proc")) {
604
+ if (!/^\d+$/.test(entry)) continue;
605
+ const proc = join4("/proc", entry);
606
+ try {
607
+ const start = processStart(Number(entry));
608
+ if (run.processStart && start && BigInt(start) < BigInt(run.processStart)) continue;
609
+ const cwd = readlinkSync(join4(proc, "cwd"));
610
+ if (cwd === run.tree || cwd.startsWith(run.tree + sep)) return true;
611
+ const env = readFileSync3(join4(proc, "environ"), "utf8").split("\0");
612
+ if (env.includes(`SIGNOFF_TREE=${run.tree}`)) return true;
613
+ } catch (error) {
614
+ if (["ENOENT", "ESRCH"].includes(error.code ?? "")) continue;
615
+ return true;
616
+ }
617
+ }
618
+ return false;
619
+ } catch {
620
+ return true;
621
+ }
622
+ }
623
+ function alive(run) {
624
+ if (run.host !== hostname()) return true;
625
+ try {
626
+ process.kill(run.pid, 0);
627
+ } catch (error) {
628
+ if (error.code === "ESRCH") return hasRunProcesses(run);
629
+ return true;
630
+ }
631
+ const currentStart = processStart(run.pid);
632
+ return !run.processStart || !currentStart || currentStart === run.processStart || hasRunProcesses(run);
633
+ }
634
+ function cacheRoot(cacheDir) {
635
+ mkdirSync(cacheDir, { recursive: true });
636
+ return realpathSync(cacheDir);
637
+ }
638
+ async function withCacheLock(cacheDir, action) {
639
+ const release = await lockfile.lock(cacheRoot(cacheDir), {
640
+ realpath: false,
641
+ stale: 3e5,
642
+ update: 3e4,
643
+ retries: { retries: 100, factor: 1, minTimeout: 100, maxTimeout: 100 }
644
+ });
645
+ try {
646
+ return await action();
647
+ } finally {
648
+ await release();
649
+ }
650
+ }
651
+ function markerDir(cacheDir) {
652
+ return join4(cacheDir, "active");
653
+ }
654
+ function writeMarker(path, run) {
655
+ const temp = `${path}.${randomUUID()}.tmp`;
656
+ writeFileSync(temp, `${JSON.stringify(run)}
657
+ `, { flag: "wx" });
658
+ renameSync(temp, path);
659
+ }
660
+ function protectedPaths(cacheDir) {
661
+ const paths = /* @__PURE__ */ new Set();
662
+ const root = markerDir(cacheDir);
663
+ if (!existsSync4(root)) return { paths, unknown: false };
664
+ let unknown = false;
665
+ for (const entry of readdirSync(root)) {
666
+ if (!entry.endsWith(".json")) continue;
667
+ const file = join4(root, entry);
668
+ let run;
669
+ try {
670
+ run = JSON.parse(readFileSync3(file, "utf8"));
671
+ if (!Number.isSafeInteger(run.pid) || run.pid <= 0 || typeof run.host !== "string" || typeof run.tree !== "string" || !run.tree.startsWith(resolve2(cacheDir) + sep) || run.processStart !== null && (typeof run.processStart !== "string" || !/^\d+$/.test(run.processStart)) || run.storeKey !== null && (typeof run.storeKey !== "string" || !/^[a-f0-9]{64}$/.test(run.storeKey))) {
672
+ unknown = true;
673
+ continue;
674
+ }
675
+ } catch {
676
+ unknown = true;
677
+ continue;
678
+ }
679
+ if (!alive(run)) {
680
+ unlinkSync(file);
681
+ continue;
682
+ }
683
+ const tree = resolve2(run.tree);
684
+ if (tree.startsWith(resolve2(cacheDir) + sep)) paths.add(tree);
685
+ if (run.storeKey && /^[a-f0-9]{64}$/.test(run.storeKey)) {
686
+ paths.add(join4(cacheDir, "stores", run.storeKey));
687
+ }
688
+ }
689
+ return { paths, unknown };
690
+ }
691
+ function protectLegacyRuns(cacheDir, active) {
692
+ const trees = join4(cacheDir, "trees");
693
+ if (!existsSync4(trees)) return;
694
+ for (const entry of readdirSync(trees, { withFileTypes: true })) {
695
+ if (!entry.isDirectory()) continue;
696
+ const tree = join4(trees, entry.name);
697
+ if (active.paths.has(tree) || !existsSync4(join4(tree, ".git"))) continue;
698
+ const match = /-(\d+)$/.exec(entry.name);
699
+ if (!match) continue;
700
+ const pid = Number(match[1]);
701
+ if (alive({ pid, host: hostname(), processStart: null, tree, storeKey: null })) {
702
+ active.paths.add(tree);
703
+ active.unknown = true;
704
+ }
705
+ }
706
+ }
707
+ async function openSignoffCacheSession(cacheDir, tree) {
708
+ const root = cacheRoot(cacheDir);
709
+ const treePath = resolve2(root, relative(resolve2(cacheDir), resolve2(tree)));
710
+ const file = join4(markerDir(root), `${process.pid}-${randomUUID()}.json`);
711
+ let run = {
712
+ pid: process.pid,
713
+ host: hostname(),
714
+ processStart: processStart(process.pid),
715
+ tree: treePath,
716
+ storeKey: null
717
+ };
718
+ await withCacheLock(root, () => {
719
+ mkdirSync(markerDir(root), { recursive: true });
720
+ writeMarker(file, run);
721
+ });
722
+ return {
723
+ cacheDir: root,
724
+ protectStore: async (cacheKey) => {
725
+ if (!/^[a-f0-9]{64}$/.test(cacheKey)) throw new Error("signoff: invalid store cache key");
726
+ await withCacheLock(root, () => {
727
+ run = { ...run, storeKey: cacheKey };
728
+ writeMarker(file, run);
729
+ });
730
+ },
731
+ close: async () => {
732
+ await withCacheLock(root, () => {
733
+ if (existsSync4(file)) unlinkSync(file);
734
+ });
735
+ }
736
+ };
737
+ }
738
+ function activeStoreKeys(cacheDir) {
739
+ cacheDir = cacheRoot(cacheDir);
740
+ const active = protectedPaths(cacheDir);
741
+ protectLegacyRuns(cacheDir, active);
742
+ const { paths, unknown } = active;
743
+ if (unknown && existsSync4(join4(cacheDir, "stores"))) return new Set(readdirSync(join4(cacheDir, "stores"), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name));
744
+ return new Set([...paths].filter((path) => path.startsWith(join4(cacheDir, "stores") + sep)).map((path) => basename2(path)));
745
+ }
746
+ function allocatedBytes(stat) {
747
+ return process.platform === "win32" ? stat.size : stat.blocks * 512;
748
+ }
749
+ function collectInodes(path, keys, sizes) {
750
+ let stat;
751
+ try {
752
+ stat = lstatSync(path);
753
+ } catch (error) {
754
+ if (error.code === "ENOENT") return;
755
+ throw error;
756
+ }
757
+ const key = `${stat.dev}:${stat.ino}`;
758
+ keys.add(key);
759
+ sizes.set(key, allocatedBytes(stat));
760
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
761
+ let children;
762
+ try {
763
+ children = readdirSync(path);
764
+ } catch (error) {
765
+ if (error.code === "ENOENT") return;
766
+ throw error;
767
+ }
768
+ for (const child of children) collectInodes(join4(path, child), keys, sizes);
769
+ }
770
+ function entries(cacheDir, protectedSet, unknown) {
771
+ const found = [];
772
+ for (const [kind, child] of [["tree", "trees"], ["store", "stores"]]) {
773
+ const root = join4(cacheDir, child);
774
+ if (!existsSync4(root)) continue;
775
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
776
+ if (!entry.isDirectory()) continue;
777
+ const path = join4(root, entry.name);
778
+ const recognized = kind === "store" ? /^[a-f0-9]{64}$/.test(entry.name) : existsSync4(join4(path, ".git"));
779
+ let mtimeMs;
780
+ try {
781
+ mtimeMs = statSync2(path).mtimeMs;
782
+ } catch (error) {
783
+ if (error.code === "ENOENT") continue;
784
+ throw error;
785
+ }
786
+ found.push({
787
+ path,
788
+ kind,
789
+ mtimeMs,
790
+ protected: unknown || protectedSet.has(path) || !recognized,
791
+ inodes: /* @__PURE__ */ new Set()
792
+ });
793
+ }
794
+ }
795
+ return found;
796
+ }
797
+ function removeEntry(entry) {
798
+ if (entry.kind === "store") {
799
+ rmSync2(entry.path, { recursive: true, force: true });
800
+ return;
801
+ }
802
+ const git2 = spawnSync3("git", ["-C", entry.path, "worktree", "remove", "--force", entry.path], { encoding: "utf8" });
803
+ if (git2.status !== 0) throw new Error(`signoff: cannot retire cached worktree ${entry.path}: ${git2.stderr.trim()}`);
804
+ }
805
+ async function pruneSignoffCache(cacheDir, options = {}) {
806
+ const root = cacheRoot(cacheDir);
807
+ const nowMs = options.nowMs ?? Date.now();
808
+ const maxAgeMs = options.maxAgeMs ?? SIGNOFF_CACHE_MAX_AGE_MS;
809
+ const maxBytes = options.maxBytes ?? SIGNOFF_CACHE_MAX_BYTES;
810
+ if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0 || !Number.isFinite(maxBytes) || maxBytes < 0) {
811
+ throw new Error("signoff: cache limits must be non-negative finite numbers");
812
+ }
813
+ return withCacheLock(root, () => {
814
+ const active = protectedPaths(root);
815
+ protectLegacyRuns(root, active);
816
+ for (const path of options.keepPaths ?? []) active.paths.add(resolve2(path));
817
+ const candidates = entries(root, active.paths, active.unknown);
818
+ const sizes = /* @__PURE__ */ new Map();
819
+ const references = /* @__PURE__ */ new Map();
820
+ for (const entry of candidates) {
821
+ collectInodes(entry.path, entry.inodes, sizes);
822
+ for (const key of entry.inodes) references.set(key, (references.get(key) ?? 0) + 1);
823
+ }
824
+ let bytes = [...sizes.values()].reduce((sum, size) => sum + size, 0);
825
+ const beforeBytes = bytes;
826
+ const selected = [];
827
+ const select = (entry) => {
828
+ selected.push(entry);
829
+ for (const key of entry.inodes) {
830
+ const remaining = (references.get(key) ?? 1) - 1;
831
+ references.set(key, remaining);
832
+ if (remaining === 0) bytes -= sizes.get(key) ?? 0;
833
+ }
834
+ };
835
+ const eligible = candidates.filter((entry) => !entry.protected).sort((a, b) => a.mtimeMs - b.mtimeMs);
836
+ for (const entry of eligible) {
837
+ if (nowMs - entry.mtimeMs >= maxAgeMs) select(entry);
838
+ }
839
+ for (const entry of eligible) {
840
+ if (bytes <= maxBytes) break;
841
+ if (!selected.includes(entry)) select(entry);
842
+ }
843
+ for (const entry of selected) removeEntry(entry);
844
+ return {
845
+ beforeBytes,
846
+ afterBytes: bytes,
847
+ removed: selected.map((entry) => entry.path),
848
+ overBudgetBytes: Math.max(0, bytes - maxBytes)
849
+ };
850
+ });
851
+ }
213
852
 
214
853
  // src/signoff/node-version.ts
215
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
216
- import { join as join4 } from "path";
854
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
855
+ import { join as join6 } from "path";
217
856
 
218
857
  // src/signoff/workflow-pin.ts
219
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
220
- import { join as join3 } from "path";
221
- var WORKFLOW_DIR = join3(".github", "workflows");
858
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
859
+ import { join as join5 } from "path";
860
+ var WORKFLOW_DIR = join5(".github", "workflows");
222
861
  function scalarValue(raw) {
223
862
  const withoutComment = raw.replace(/\s+#.*$/, "").trim();
224
863
  const quoted = /^(['"])(.*)\1$/.exec(withoutComment);
@@ -268,24 +907,24 @@ function pinsInWorkflow(repoRoot, file, source) {
268
907
  pins.push({ file, value, via: "node-version" });
269
908
  continue;
270
909
  }
271
- const target = join3(repoRoot, value);
272
- if (!existsSync3(target)) {
910
+ const target = join5(repoRoot, value);
911
+ if (!existsSync5(target)) {
273
912
  throw new Error(
274
913
  `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.`
275
914
  );
276
915
  }
277
- const declared = readFileSync2(target, "utf8").split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
916
+ const declared = readFileSync4(target, "utf8").split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
278
917
  if (declared !== void 0) pins.push({ file, value: declared, via: `node-version-file ${value}` });
279
918
  }
280
919
  return pins;
281
920
  }
282
921
  function scanMergeGateNodePins(repoRoot) {
283
- const dir = join3(repoRoot, WORKFLOW_DIR);
284
- if (!existsSync3(dir)) return [];
922
+ const dir = join5(repoRoot, WORKFLOW_DIR);
923
+ if (!existsSync5(dir)) return [];
285
924
  const pins = [];
286
- const files = readdirSync(dir).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
925
+ const files = readdirSync2(dir).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
287
926
  for (const name of files) {
288
- const source = readFileSync2(join3(dir, name), "utf8");
927
+ const source = readFileSync4(join5(dir, name), "utf8");
289
928
  if (!triggersOnPullRequest(source)) continue;
290
929
  pins.push(...pinsInWorkflow(repoRoot, `${WORKFLOW_DIR}/${name}`, source));
291
930
  }
@@ -335,9 +974,9 @@ function resolveNodeRequirement(repoRoot, configured) {
335
974
  return { major, declared: configured.trim(), source: "signoff config `nodeVersion`" };
336
975
  }
337
976
  let fromNvmrc = null;
338
- const nvmrc = join4(repoRoot, ".nvmrc");
339
- if (existsSync4(nvmrc)) {
340
- const raw = readFileSync3(nvmrc, "utf8").trim();
977
+ const nvmrc = join6(repoRoot, ".nvmrc");
978
+ if (existsSync6(nvmrc)) {
979
+ const raw = readFileSync5(nvmrc, "utf8").trim();
341
980
  const major = majorOf(raw);
342
981
  if (major !== null) fromNvmrc = { major, declared: raw, source: ".nvmrc" };
343
982
  }
@@ -361,9 +1000,9 @@ function assertNodeVersion(requirement, running = process.version) {
361
1000
  }
362
1001
  function assertNodeTypesVersion(repoRoot, requirement) {
363
1002
  if (!requirement) return;
364
- const packagePath = join4(repoRoot, "package.json");
365
- if (!existsSync4(packagePath)) return;
366
- const manifest = JSON.parse(readFileSync3(packagePath, "utf8"));
1003
+ const packagePath = join6(repoRoot, "package.json");
1004
+ if (!existsSync6(packagePath)) return;
1005
+ const manifest = JSON.parse(readFileSync5(packagePath, "utf8"));
367
1006
  const declared = manifest.devDependencies?.["@types/node"] ?? manifest.dependencies?.["@types/node"] ?? manifest.optionalDependencies?.["@types/node"];
368
1007
  if (!declared) return;
369
1008
  const declaredMajor = /^[~^]?v?(\d+)(?:\.|$)/.exec(declared.trim())?.[1];
@@ -372,11 +1011,11 @@ function assertNodeTypesVersion(repoRoot, requirement) {
372
1011
  `signoff: this repo pins Node ${requirement.declared} but declares @types/node ${declared}. Use the latest @types/node ${requirement.major}.x release.`
373
1012
  );
374
1013
  }
375
- const installedPath = join4(repoRoot, "node_modules", "@types", "node", "package.json");
376
- if (!existsSync4(installedPath)) {
1014
+ const installedPath = join6(repoRoot, "node_modules", "@types", "node", "package.json");
1015
+ if (!existsSync6(installedPath)) {
377
1016
  throw new Error(`signoff: @types/node is declared but missing after install in ${repoRoot}.`);
378
1017
  }
379
- const installed = JSON.parse(readFileSync3(installedPath, "utf8"));
1018
+ const installed = JSON.parse(readFileSync5(installedPath, "utf8"));
380
1019
  const installedMajor = majorOf(installed.version ?? "");
381
1020
  if (installedMajor !== requirement.major) {
382
1021
  throw new Error(
@@ -443,7 +1082,7 @@ function runCommand(options) {
443
1082
  killGraceMs = DEFAULT_KILL_GRACE_MS,
444
1083
  onData
445
1084
  } = options;
446
- return new Promise((resolve7, reject) => {
1085
+ return new Promise((resolve8, reject) => {
447
1086
  const startedAt = Date.now();
448
1087
  const buffer = new BoundedOutput(maxOutputBytes);
449
1088
  let timedOut = false;
@@ -493,7 +1132,7 @@ function runCommand(options) {
493
1132
  });
494
1133
  child.on("close", (code, sig) => {
495
1134
  cleanup();
496
- resolve7({
1135
+ resolve8({
497
1136
  command,
498
1137
  cwd,
499
1138
  // A signalled process reports code `null`; 128+n is the shell's own
@@ -660,8 +1299,8 @@ function planAttempts(step, seedBase, overrideRuns) {
660
1299
 
661
1300
  // src/signoff/store.ts
662
1301
  import { createHash as createHash2 } from "crypto";
663
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync, statSync, utimesSync, writeFileSync } from "fs";
664
- import { join as join5, relative, sep } from "path";
1302
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync3, utimesSync, writeFileSync as writeFileSync2 } from "fs";
1303
+ import { basename as basename3, join as join7, relative as relative2, sep as sep2 } from "path";
665
1304
  var MANIFEST_FILES = [
666
1305
  "pnpm-lock.yaml",
667
1306
  "pnpm-workspace.yaml",
@@ -674,12 +1313,12 @@ var MANIFEST_FILES = [
674
1313
  ];
675
1314
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".wrangler", ".react-router"]);
676
1315
  function collectManifests(dir, root, out) {
677
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1316
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
678
1317
  if (entry.isDirectory()) {
679
1318
  if (SKIP_DIRS.has(entry.name)) continue;
680
- collectManifests(join5(dir, entry.name), root, out);
1319
+ collectManifests(join7(dir, entry.name), root, out);
681
1320
  } else if (MANIFEST_FILES.includes(entry.name)) {
682
- out.push(relative(root, join5(dir, entry.name)).split(sep).join("/"));
1321
+ out.push(relative2(root, join7(dir, entry.name)).split(sep2).join("/"));
683
1322
  }
684
1323
  }
685
1324
  }
@@ -693,26 +1332,27 @@ function manifestCacheKey(treePath, files) {
693
1332
  for (const rel of files) {
694
1333
  hash.update(rel);
695
1334
  hash.update("\0");
696
- hash.update(createHash2("sha256").update(readFileSync4(join5(treePath, rel))).digest("hex"));
1335
+ hash.update(createHash2("sha256").update(readFileSync6(join7(treePath, rel))).digest("hex"));
697
1336
  hash.update("\n");
698
1337
  }
699
1338
  return hash.digest("hex");
700
1339
  }
701
- function pruneStores(storesRoot, keep) {
702
- if (!existsSync5(storesRoot)) return [];
703
- const entries = readdirSync2(storesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
704
- const full = join5(storesRoot, entry.name);
705
- return { full, mtimeMs: statSync(full).mtimeMs };
1340
+ function pruneStores(storesRoot, keep, protectedKeys) {
1341
+ if (!existsSync7(storesRoot)) return [];
1342
+ const entries2 = readdirSync3(storesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^[a-f0-9]{64}$/.test(entry.name)).map((entry) => {
1343
+ const full = join7(storesRoot, entry.name);
1344
+ return { full, mtimeMs: statSync3(full).mtimeMs };
706
1345
  }).sort((a, b) => b.mtimeMs - a.mtimeMs);
707
1346
  const pruned = [];
708
- for (const stale of entries.slice(keep)) {
709
- rmSync(stale.full, { recursive: true, force: true });
1347
+ for (const stale of entries2.slice(keep)) {
1348
+ if (protectedKeys.has(basename3(stale.full))) continue;
1349
+ rmSync3(stale.full, { recursive: true, force: true });
710
1350
  pruned.push(stale.full);
711
1351
  }
712
1352
  return pruned;
713
1353
  }
714
1354
  function resolveStore(options) {
715
- const { treePath, cacheDir, generations = 4 } = options;
1355
+ const { treePath, cacheDir, generations = 4, protectedStoreKeys = /* @__PURE__ */ new Set() } = options;
716
1356
  const files = manifestFiles(treePath);
717
1357
  if (files.length === 0) {
718
1358
  throw new Error(
@@ -720,25 +1360,25 @@ function resolveStore(options) {
720
1360
  );
721
1361
  }
722
1362
  const cacheKey = manifestCacheKey(treePath, files);
723
- const storesRoot = join5(cacheDir, "stores");
724
- const storeDir = join5(storesRoot, cacheKey);
725
- const marker = join5(storeDir, ".signoff-store.json");
726
- const hit = existsSync5(storeDir) && readdirSync2(storeDir).some((entry) => entry !== ".signoff-store.json");
727
- mkdirSync(storeDir, { recursive: true });
728
- writeFileSync(marker, `${JSON.stringify({ cacheKey, keyedOn: files, usedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
1363
+ const storesRoot = join7(cacheDir, "stores");
1364
+ const storeDir = join7(storesRoot, cacheKey);
1365
+ const marker = join7(storeDir, ".signoff-store.json");
1366
+ const hit = existsSync7(storeDir) && readdirSync3(storeDir).some((entry) => entry !== ".signoff-store.json");
1367
+ mkdirSync2(storeDir, { recursive: true });
1368
+ writeFileSync2(marker, `${JSON.stringify({ cacheKey, keyedOn: files, usedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
729
1369
  `);
730
1370
  const now = /* @__PURE__ */ new Date();
731
1371
  utimesSync(storeDir, now, now);
732
- return { storeDir, cacheKey, hit, keyedOn: files, pruned: pruneStores(storesRoot, generations) };
1372
+ return { storeDir, cacheKey, hit, keyedOn: files, pruned: pruneStores(storesRoot, generations, protectedStoreKeys) };
733
1373
  }
734
1374
 
735
1375
  // src/signoff/workspace.ts
736
- import { spawnSync as spawnSync2 } from "child_process";
1376
+ import { spawnSync as spawnSync4 } from "child_process";
737
1377
  import { createHash as createHash3 } from "crypto";
738
- import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
739
- import { dirname as dirname2, isAbsolute, join as join6, resolve as resolve2 } from "path";
1378
+ import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
1379
+ import { dirname as dirname2, isAbsolute, join as join8, resolve as resolve3 } from "path";
740
1380
  function git(args, cwd) {
741
- const result = spawnSync2("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
1381
+ const result = spawnSync4("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
742
1382
  if (result.error) throw new Error(`signoff: git ${args.join(" ")} failed to start: ${result.error.message}`);
743
1383
  if (result.status !== 0) {
744
1384
  throw new Error(`signoff: git ${args.join(" ")} exited ${result.status}
@@ -758,8 +1398,8 @@ function materializeCleanTree(options) {
758
1398
  const head = git(["rev-parse", "HEAD"], root).trim();
759
1399
  const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], root).trim();
760
1400
  git(["worktree", "prune"], root);
761
- mkdirSync2(dirname2(dest), { recursive: true });
762
- if (existsSync6(dest)) rmSync2(dest, { recursive: true, force: true });
1401
+ mkdirSync3(dirname2(dest), { recursive: true });
1402
+ if (existsSync8(dest)) rmSync4(dest, { recursive: true, force: true });
763
1403
  git(["worktree", "add", "--detach", "--quiet", dest, head], root);
764
1404
  let diffSha256 = null;
765
1405
  let untrackedFiles = [];
@@ -767,29 +1407,29 @@ function materializeCleanTree(options) {
767
1407
  const patch = git(["diff", "HEAD", "--binary", "--no-color", "--no-ext-diff"], root);
768
1408
  if (patch.length > 0) {
769
1409
  diffSha256 = createHash3("sha256").update(patch).digest("hex");
770
- const patchFile = join6(dirname2(dest), `${dest.split("/").pop() ?? "tree"}.patch`);
771
- writeFileSync2(patchFile, patch);
1410
+ const patchFile = join8(dirname2(dest), `${dest.split("/").pop() ?? "tree"}.patch`);
1411
+ writeFileSync3(patchFile, patch);
772
1412
  git(["apply", "--binary", "--whitespace=nowarn", patchFile], dest);
773
- rmSync2(patchFile, { force: true });
1413
+ rmSync4(patchFile, { force: true });
774
1414
  }
775
1415
  untrackedFiles = zsplit(git(["ls-files", "--others", "--exclude-standard", "-z"], root));
776
1416
  for (const rel of untrackedFiles) {
777
- const target = join6(dest, rel);
778
- mkdirSync2(dirname2(target), { recursive: true });
779
- copyFileSync(join6(root, rel), target);
1417
+ const target = join8(dest, rel);
1418
+ mkdirSync3(dirname2(target), { recursive: true });
1419
+ copyFileSync(join8(root, rel), target);
780
1420
  }
781
1421
  }
782
1422
  const carried = [];
783
1423
  for (const rel of carryFiles) {
784
1424
  if (isAbsolute(rel)) throw new Error(`signoff: carryFiles must be repo-relative; got "${rel}"`);
785
- const from = resolve2(root, rel);
786
- if (!existsSync6(from)) {
1425
+ const from = resolve3(root, rel);
1426
+ if (!existsSync8(from)) {
787
1427
  throw new Error(
788
1428
  `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.`
789
1429
  );
790
1430
  }
791
- const target = join6(dest, rel);
792
- mkdirSync2(dirname2(target), { recursive: true });
1431
+ const target = join8(dest, rel);
1432
+ mkdirSync3(dirname2(target), { recursive: true });
793
1433
  copyFileSync(from, target);
794
1434
  carried.push(rel);
795
1435
  }
@@ -810,9 +1450,9 @@ function removeCleanTree(tree) {
810
1450
  }
811
1451
 
812
1452
  // src/signoff/run.ts
813
- var DEFAULT_CACHE_DIR = join7(homedir(), ".cache", "agent-app-signoff");
1453
+ var DEFAULT_CACHE_DIR = join9(homedir(), ".cache", "agent-app-signoff");
814
1454
  function hostFacts(treePath, requirement) {
815
- const pm = spawnSync3("pnpm", ["--version"], { cwd: treePath, encoding: "utf8" });
1455
+ const pm = spawnSync5("pnpm", ["--version"], { cwd: treePath, encoding: "utf8" });
816
1456
  return {
817
1457
  node: process.version,
818
1458
  nodePinned: requirement?.declared ?? null,
@@ -850,17 +1490,35 @@ function withStoreDir(command, flag, storeDir) {
850
1490
  async function runSignoff(options = {}) {
851
1491
  const startedAt = /* @__PURE__ */ new Date();
852
1492
  const wallStart = Date.now();
853
- const repoDir = resolve3(options.repoDir ?? process.cwd());
1493
+ const repoDir = resolve4(options.repoDir ?? process.cwd());
854
1494
  const repoRoot = repoRootOf(repoDir);
855
1495
  const source = options.source ?? "working-tree";
856
- let cacheDir = resolve3(options.cacheDir ?? DEFAULT_CACHE_DIR);
857
- const treePath = () => join7(cacheDir, "trees", `${basename2(repoRoot)}-${process.pid}`);
1496
+ let cacheDir = resolve4(options.cacheDir ?? DEFAULT_CACHE_DIR);
1497
+ const treeName = `${basename4(repoRoot)}-${process.pid}-${randomUUID2()}`;
1498
+ const treePath = () => join9(cacheDir, "trees", treeName);
858
1499
  let tree = null;
1500
+ const sessions = [];
1501
+ let session = await openSignoffCacheSession(cacheDir, treePath());
1502
+ sessions.push(session);
1503
+ cacheDir = session.cacheDir;
1504
+ const prune = async (root, keepPaths = []) => {
1505
+ const result = await pruneSignoffCache(root, { keepPaths });
1506
+ if (result.removed.length || result.overBudgetBytes) {
1507
+ options.onEvent?.({ kind: "cache-prune", ...result });
1508
+ }
1509
+ };
859
1510
  try {
1511
+ await prune(cacheDir);
860
1512
  tree = materializeCleanTree({ repoDir: repoRoot, dest: treePath(), source });
861
1513
  let loaded = await loadSignoffConfig({ repoRoot: tree.path, configPath: options.configPath });
862
- cacheDir = resolve3(options.cacheDir ?? loaded.config.cacheDir ?? DEFAULT_CACHE_DIR);
1514
+ cacheDir = resolve4(options.cacheDir ?? loaded.config.cacheDir ?? DEFAULT_CACHE_DIR);
863
1515
  if (tree.path !== treePath() || (loaded.config.carryFiles?.length ?? 0) > 0) {
1516
+ if (session.cacheDir !== cacheDir) {
1517
+ session = await openSignoffCacheSession(cacheDir, treePath());
1518
+ sessions.push(session);
1519
+ cacheDir = session.cacheDir;
1520
+ await prune(cacheDir);
1521
+ }
864
1522
  removeCleanTree(tree);
865
1523
  tree = materializeCleanTree({ repoDir: repoRoot, dest: treePath(), source, carryFiles: loaded.config.carryFiles });
866
1524
  loaded = await loadSignoffConfig({ repoRoot: tree.path, configPath: options.configPath });
@@ -871,10 +1529,16 @@ async function runSignoff(options = {}) {
871
1529
  const nodeRequirement = resolveNodeRequirement(tree.path, config.nodeVersion);
872
1530
  assertNodeVersion(nodeRequirement);
873
1531
  options.onEvent?.({ kind: "tree", path: tree.path, head: tree.head, dirty: tree.dirty });
874
- const store = resolveStore({ treePath: tree.path, cacheDir, generations: config.storeGenerations });
1532
+ await session.protectStore(manifestCacheKey(tree.path, manifestFiles(tree.path)));
1533
+ const store = await withCacheLock(cacheDir, () => resolveStore({
1534
+ treePath: tree.path,
1535
+ cacheDir,
1536
+ generations: config.storeGenerations,
1537
+ protectedStoreKeys: activeStoreKeys(cacheDir)
1538
+ }));
875
1539
  options.onEvent?.({ kind: "store", storeDir: store.storeDir, cacheHit: store.hit, cacheKey: store.cacheKey });
876
1540
  const installSpec = config.install ?? {};
877
- const installCwd = join7(tree.path, installSpec.cwd ?? ".");
1541
+ const installCwd = join9(tree.path, installSpec.cwd ?? ".");
878
1542
  const installCommand = withStoreDir(
879
1543
  installSpec.run ?? "pnpm install --frozen-lockfile",
880
1544
  installSpec.storeDirFlag,
@@ -921,7 +1585,7 @@ async function runSignoff(options = {}) {
921
1585
  origin,
922
1586
  host,
923
1587
  install,
924
- steps: [...config.steps.map((step) => step.name), WORKER_DEFAULTS_STEP].map(
1588
+ steps: [...config.steps.map((step) => step.name), WORKER_DEFAULTS_STEP, APP_DEFAULTS_STEP].map(
925
1589
  (name) => ({
926
1590
  name,
927
1591
  status: "skipped",
@@ -951,7 +1615,7 @@ async function runSignoff(options = {}) {
951
1615
  options.onEvent?.({ kind: "step-start", name: step.name, command: plan.command, seed: plan.seed });
952
1616
  const result = await runCommand({
953
1617
  command: plan.command,
954
- cwd: join7(treeRoot, step.cwd ?? "."),
1618
+ cwd: join9(treeRoot, step.cwd ?? "."),
955
1619
  env: buildEnv(sharedEnv, [step.env]),
956
1620
  timeoutMs: step.timeoutMs,
957
1621
  signal
@@ -967,7 +1631,8 @@ async function runSignoff(options = {}) {
967
1631
  return { ok: true, value: attempts };
968
1632
  }
969
1633
  });
970
- const steps = [...outcomes.map(toStepResult), workerDefaults];
1634
+ const appDefaults = runAppDefaults(tree.path, config, options);
1635
+ const steps = [...outcomes.map(toStepResult), workerDefaults, appDefaults];
971
1636
  return finish({
972
1637
  ok: steps.every((step) => step.status === "passed"),
973
1638
  startedAt,
@@ -984,7 +1649,14 @@ async function runSignoff(options = {}) {
984
1649
  options
985
1650
  });
986
1651
  } finally {
987
- if (tree && !options.keepWorkspace && existsSync7(tree.path)) removeCleanTree(tree);
1652
+ try {
1653
+ if (tree && !options.keepWorkspace && existsSync9(tree.path)) removeCleanTree(tree);
1654
+ } finally {
1655
+ for (const active of sessions) {
1656
+ await active.close();
1657
+ await prune(active.cacheDir, options.keepWorkspace && tree ? [tree.path] : []);
1658
+ }
1659
+ }
988
1660
  }
989
1661
  }
990
1662
  function runWorkerDefaults(treeRoot, workerConfigs, options) {
@@ -1009,6 +1681,28 @@ function runWorkerDefaults(treeRoot, workerConfigs, options) {
1009
1681
  emitStepEnd(options, WORKER_DEFAULTS_STEP, ok ? "passed" : "failed", attempts);
1010
1682
  return { name: WORKER_DEFAULTS_STEP, status: ok ? "passed" : "failed", attempts, durationMs, startedAtMs: null, finishedAtMs: null };
1011
1683
  }
1684
+ function runAppDefaults(treeRoot, config, options) {
1685
+ const command = `agent-app-signoff ${APP_DEFAULTS_STEP}`;
1686
+ options.onEvent?.({ kind: "step-start", name: APP_DEFAULTS_STEP, command, seed: null });
1687
+ const started = Date.now();
1688
+ let output;
1689
+ let ok;
1690
+ try {
1691
+ const result = checkAppDefaults(treeRoot, { exemptions: config.agentApp?.exemptions });
1692
+ output = formatAppDefaults(result);
1693
+ ok = result.findings.length === 0;
1694
+ } catch (error) {
1695
+ output = `${error instanceof Error ? error.stack ?? error.message : String(error)}
1696
+ `;
1697
+ ok = false;
1698
+ }
1699
+ const durationMs = Date.now() - started;
1700
+ const attempts = [
1701
+ { command, seed: null, exitCode: ok ? 0 : 1, signal: null, durationMs, timedOut: false, output, outputTruncated: false }
1702
+ ];
1703
+ emitStepEnd(options, APP_DEFAULTS_STEP, ok ? "passed" : "failed", attempts);
1704
+ return { name: APP_DEFAULTS_STEP, status: ok ? "passed" : "failed", attempts, durationMs, startedAtMs: null, finishedAtMs: null };
1705
+ }
1012
1706
  function emitStepEnd(options, name, status, attempts) {
1013
1707
  options.onEvent?.({
1014
1708
  kind: "step-end",
@@ -1394,8 +2088,8 @@ function round(value) {
1394
2088
  }
1395
2089
 
1396
2090
  // src/signoff/shell-alignment-cli.ts
1397
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1398
- import { dirname as dirname3, resolve as resolve4 } from "path";
2091
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2092
+ import { dirname as dirname3, resolve as resolve5 } from "path";
1399
2093
  function shellAlignmentUsage() {
1400
2094
  return [
1401
2095
  "Usage: agent-app-signoff shell-alignment --base-url <url> --route <path> [--route <path> ...] [options]",
@@ -1445,10 +2139,10 @@ function parseShellAlignmentArgs(argv) {
1445
2139
  });
1446
2140
  break;
1447
2141
  case "--storage-state":
1448
- out.storageState = resolve4(take());
2142
+ out.storageState = resolve5(take());
1449
2143
  break;
1450
2144
  case "--theme-script":
1451
- out.themeScript = readFileSync5(resolve4(take()), "utf8");
2145
+ out.themeScript = readFileSync7(resolve5(take()), "utf8");
1452
2146
  break;
1453
2147
  case "--ready":
1454
2148
  out.readySelector = take();
@@ -1460,7 +2154,7 @@ function parseShellAlignmentArgs(argv) {
1460
2154
  break;
1461
2155
  }
1462
2156
  case "--screenshots":
1463
- out.screenshotDir = resolve4(take());
2157
+ out.screenshotDir = resolve5(take());
1464
2158
  break;
1465
2159
  case "--json":
1466
2160
  out.jsonPath = take();
@@ -1490,12 +2184,12 @@ ${shellAlignmentUsage()}
1490
2184
  `);
1491
2185
  return 2;
1492
2186
  }
1493
- if (options.screenshotDir) mkdirSync3(options.screenshotDir, { recursive: true });
2187
+ if (options.screenshotDir) mkdirSync4(options.screenshotDir, { recursive: true });
1494
2188
  const report = await checkShellAlignment(options);
1495
2189
  if (options.jsonPath) {
1496
- const abs = resolve4(options.jsonPath);
1497
- mkdirSync3(dirname3(abs), { recursive: true });
1498
- writeFileSync3(abs, `${JSON.stringify(report, null, 2)}
2190
+ const abs = resolve5(options.jsonPath);
2191
+ mkdirSync4(dirname3(abs), { recursive: true });
2192
+ writeFileSync4(abs, `${JSON.stringify(report, null, 2)}
1499
2193
  `);
1500
2194
  }
1501
2195
  process.stdout.write(`${formatShellAlignmentReport(report)}
@@ -1787,8 +2481,8 @@ function formatSurfaceLadderReport(report) {
1787
2481
  }
1788
2482
 
1789
2483
  // src/signoff/surface-ladder-cli.ts
1790
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1791
- import { dirname as dirname4, resolve as resolve5 } from "path";
2484
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2485
+ import { dirname as dirname4, resolve as resolve6 } from "path";
1792
2486
  function surfaceLadderUsage() {
1793
2487
  return [
1794
2488
  "Usage: agent-app-signoff surface-ladder --base-url <url> --route <path> [--route <path> ...] [options]",
@@ -1846,10 +2540,10 @@ function parseSurfaceLadderArgs(argv) {
1846
2540
  });
1847
2541
  break;
1848
2542
  case "--storage-state":
1849
- out.storageState = resolve5(take());
2543
+ out.storageState = resolve6(take());
1850
2544
  break;
1851
2545
  case "--theme-script":
1852
- out.themeScript = readFileSync6(resolve5(take()), "utf8");
2546
+ out.themeScript = readFileSync8(resolve6(take()), "utf8");
1853
2547
  break;
1854
2548
  case "--ready":
1855
2549
  out.readySelector = take();
@@ -1867,10 +2561,10 @@ function parseSurfaceLadderArgs(argv) {
1867
2561
  break;
1868
2562
  }
1869
2563
  case "--inject-css":
1870
- out.injectCss = readFileSync6(resolve5(take()), "utf8");
2564
+ out.injectCss = readFileSync8(resolve6(take()), "utf8");
1871
2565
  break;
1872
2566
  case "--screenshots":
1873
- out.screenshotDir = resolve5(take());
2567
+ out.screenshotDir = resolve6(take());
1874
2568
  break;
1875
2569
  case "--json":
1876
2570
  out.jsonPath = take();
@@ -1900,12 +2594,12 @@ ${surfaceLadderUsage()}
1900
2594
  `);
1901
2595
  return 2;
1902
2596
  }
1903
- if (options.screenshotDir) mkdirSync4(options.screenshotDir, { recursive: true });
2597
+ if (options.screenshotDir) mkdirSync5(options.screenshotDir, { recursive: true });
1904
2598
  const report = await checkSurfaceLadder(options);
1905
2599
  if (options.jsonPath) {
1906
- const abs = resolve5(options.jsonPath);
1907
- mkdirSync4(dirname4(abs), { recursive: true });
1908
- writeFileSync4(abs, `${JSON.stringify(report, null, 2)}
2600
+ const abs = resolve6(options.jsonPath);
2601
+ mkdirSync5(dirname4(abs), { recursive: true });
2602
+ writeFileSync5(abs, `${JSON.stringify(report, null, 2)}
1909
2603
  `);
1910
2604
  }
1911
2605
  process.stdout.write(`${formatSurfaceLadderReport(report)}
@@ -1998,8 +2692,8 @@ function formatWorkspaceSurfaceReport(report) {
1998
2692
  }
1999
2693
 
2000
2694
  // src/signoff/workspace-surface-cli.ts
2001
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2002
- import { dirname as dirname5, resolve as resolve6 } from "path";
2695
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2696
+ import { dirname as dirname5, resolve as resolve7 } from "path";
2003
2697
  var USAGE = [
2004
2698
  "Usage: agent-app-signoff workspace-surface --base-url <url> --route <path> [--route <path> ...] [options]",
2005
2699
  "",
@@ -2028,12 +2722,12 @@ ${USAGE}
2028
2722
  `);
2029
2723
  return 2;
2030
2724
  }
2031
- if (options.screenshotDir) mkdirSync5(options.screenshotDir, { recursive: true });
2725
+ if (options.screenshotDir) mkdirSync6(options.screenshotDir, { recursive: true });
2032
2726
  const report = await checkWorkspaceSurface(options);
2033
2727
  if (options.jsonPath) {
2034
- const abs = resolve6(options.jsonPath);
2035
- mkdirSync5(dirname5(abs), { recursive: true });
2036
- writeFileSync5(abs, `${JSON.stringify(report, null, 2)}
2728
+ const abs = resolve7(options.jsonPath);
2729
+ mkdirSync6(dirname5(abs), { recursive: true });
2730
+ writeFileSync6(abs, `${JSON.stringify(report, null, 2)}
2037
2731
  `);
2038
2732
  }
2039
2733
  process.stdout.write(`${formatWorkspaceSurfaceReport(report)}
@@ -2047,6 +2741,9 @@ export {
2047
2741
  WORKER_COMPATIBILITY_DATE_FLOOR,
2048
2742
  checkWorkerDefaults,
2049
2743
  formatWorkerDefaults,
2744
+ checkAppDefaults,
2745
+ formatAppDefaults,
2746
+ collectLastExports,
2050
2747
  loadSignoffConfig,
2051
2748
  runSignoff,
2052
2749
  formatSignoffReport,
@@ -2088,4 +2785,4 @@ export {
2088
2785
  formatWorkspaceSurfaceReport,
2089
2786
  runWorkspaceSurfaceCli
2090
2787
  };
2091
- //# sourceMappingURL=chunk-KDPTXXOQ.js.map
2788
+ //# sourceMappingURL=chunk-WDY47GBR.js.map