@tangle-network/agent-app 0.60.214 → 0.60.216

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 (37) hide show
  1. package/dist/chunk-CD2LQXY6.js +306 -0
  2. package/dist/chunk-CD2LQXY6.js.map +1 -0
  3. package/dist/{chunk-KDPTXXOQ.js → chunk-J3N6RNMH.js} +456 -75
  4. package/dist/chunk-J3N6RNMH.js.map +1 -0
  5. package/dist/chunk-LRYY3OX7.js +105 -0
  6. package/dist/chunk-LRYY3OX7.js.map +1 -0
  7. package/dist/{chunk-LLO436QO.js → chunk-QM7RTTDY.js} +27 -86
  8. package/dist/chunk-QM7RTTDY.js.map +1 -0
  9. package/dist/{chunk-ZRLKB4KB.js → chunk-YWLZOPDS.js} +10 -11
  10. package/dist/{chunk-ZRLKB4KB.js.map → chunk-YWLZOPDS.js.map} +1 -1
  11. package/dist/launch-invariants/catalog.d.ts +16 -0
  12. package/dist/launch-invariants/cli.js +5 -2
  13. package/dist/launch-invariants/cli.js.map +1 -1
  14. package/dist/launch-invariants/index.d.ts +1 -1
  15. package/dist/launch-invariants/index.js +13 -5
  16. package/dist/launch-invariants/index.js.map +1 -1
  17. package/dist/launch-invariants/limit-alarms.d.ts +19 -1
  18. package/dist/launch-invariants/testing.d.ts +0 -3
  19. package/dist/launch-invariants/testing.js +5 -2
  20. package/dist/platform/delegated-keys.d.ts +3 -1
  21. package/dist/platform/index.js +10 -1
  22. package/dist/platform/index.js.map +1 -1
  23. package/dist/signoff/app-defaults.d.ts +65 -0
  24. package/dist/signoff/cli.js +71 -4
  25. package/dist/signoff/cli.js.map +1 -1
  26. package/dist/signoff/defaults-cli.d.ts +1 -0
  27. package/dist/signoff/index.js +2 -1
  28. package/dist/signoff/types.d.ts +6 -0
  29. package/dist/signoff/worker-defaults.d.ts +8 -0
  30. package/dist/turn-health/index.js +12 -291
  31. package/dist/turn-health/index.js.map +1 -1
  32. package/dist/turn-trace/index.js +20 -3
  33. package/dist/turn-trace/index.js.map +1 -1
  34. package/dist/turn-trace/otel.d.ts +11 -4
  35. package/package.json +1 -1
  36. package/dist/chunk-KDPTXXOQ.js.map +0 -1
  37. package/dist/chunk-LLO436QO.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,19 @@ 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 spawnSync4 } from "child_process";
565
+ import { existsSync as existsSync8 } from "fs";
211
566
  import { availableParallelism, homedir } from "os";
212
- import { basename as basename2, join as join7, resolve as resolve3 } from "path";
567
+ import { basename as basename2, join as join8, resolve as resolve3 } from "path";
213
568
 
214
569
  // src/signoff/node-version.ts
215
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
216
- import { join as join4 } from "path";
570
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
571
+ import { join as join5 } from "path";
217
572
 
218
573
  // 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");
574
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3 } from "fs";
575
+ import { join as join4 } from "path";
576
+ var WORKFLOW_DIR = join4(".github", "workflows");
222
577
  function scalarValue(raw) {
223
578
  const withoutComment = raw.replace(/\s+#.*$/, "").trim();
224
579
  const quoted = /^(['"])(.*)\1$/.exec(withoutComment);
@@ -268,24 +623,24 @@ function pinsInWorkflow(repoRoot, file, source) {
268
623
  pins.push({ file, value, via: "node-version" });
269
624
  continue;
270
625
  }
271
- const target = join3(repoRoot, value);
272
- if (!existsSync3(target)) {
626
+ const target = join4(repoRoot, value);
627
+ if (!existsSync4(target)) {
273
628
  throw new Error(
274
629
  `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
630
  );
276
631
  }
277
- const declared = readFileSync2(target, "utf8").split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
632
+ const declared = readFileSync3(target, "utf8").split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
278
633
  if (declared !== void 0) pins.push({ file, value: declared, via: `node-version-file ${value}` });
279
634
  }
280
635
  return pins;
281
636
  }
282
637
  function scanMergeGateNodePins(repoRoot) {
283
- const dir = join3(repoRoot, WORKFLOW_DIR);
284
- if (!existsSync3(dir)) return [];
638
+ const dir = join4(repoRoot, WORKFLOW_DIR);
639
+ if (!existsSync4(dir)) return [];
285
640
  const pins = [];
286
641
  const files = readdirSync(dir).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
287
642
  for (const name of files) {
288
- const source = readFileSync2(join3(dir, name), "utf8");
643
+ const source = readFileSync3(join4(dir, name), "utf8");
289
644
  if (!triggersOnPullRequest(source)) continue;
290
645
  pins.push(...pinsInWorkflow(repoRoot, `${WORKFLOW_DIR}/${name}`, source));
291
646
  }
@@ -335,9 +690,9 @@ function resolveNodeRequirement(repoRoot, configured) {
335
690
  return { major, declared: configured.trim(), source: "signoff config `nodeVersion`" };
336
691
  }
337
692
  let fromNvmrc = null;
338
- const nvmrc = join4(repoRoot, ".nvmrc");
339
- if (existsSync4(nvmrc)) {
340
- const raw = readFileSync3(nvmrc, "utf8").trim();
693
+ const nvmrc = join5(repoRoot, ".nvmrc");
694
+ if (existsSync5(nvmrc)) {
695
+ const raw = readFileSync4(nvmrc, "utf8").trim();
341
696
  const major = majorOf(raw);
342
697
  if (major !== null) fromNvmrc = { major, declared: raw, source: ".nvmrc" };
343
698
  }
@@ -361,9 +716,9 @@ function assertNodeVersion(requirement, running = process.version) {
361
716
  }
362
717
  function assertNodeTypesVersion(repoRoot, requirement) {
363
718
  if (!requirement) return;
364
- const packagePath = join4(repoRoot, "package.json");
365
- if (!existsSync4(packagePath)) return;
366
- const manifest = JSON.parse(readFileSync3(packagePath, "utf8"));
719
+ const packagePath = join5(repoRoot, "package.json");
720
+ if (!existsSync5(packagePath)) return;
721
+ const manifest = JSON.parse(readFileSync4(packagePath, "utf8"));
367
722
  const declared = manifest.devDependencies?.["@types/node"] ?? manifest.dependencies?.["@types/node"] ?? manifest.optionalDependencies?.["@types/node"];
368
723
  if (!declared) return;
369
724
  const declaredMajor = /^[~^]?v?(\d+)(?:\.|$)/.exec(declared.trim())?.[1];
@@ -372,11 +727,11 @@ function assertNodeTypesVersion(repoRoot, requirement) {
372
727
  `signoff: this repo pins Node ${requirement.declared} but declares @types/node ${declared}. Use the latest @types/node ${requirement.major}.x release.`
373
728
  );
374
729
  }
375
- const installedPath = join4(repoRoot, "node_modules", "@types", "node", "package.json");
376
- if (!existsSync4(installedPath)) {
730
+ const installedPath = join5(repoRoot, "node_modules", "@types", "node", "package.json");
731
+ if (!existsSync5(installedPath)) {
377
732
  throw new Error(`signoff: @types/node is declared but missing after install in ${repoRoot}.`);
378
733
  }
379
- const installed = JSON.parse(readFileSync3(installedPath, "utf8"));
734
+ const installed = JSON.parse(readFileSync4(installedPath, "utf8"));
380
735
  const installedMajor = majorOf(installed.version ?? "");
381
736
  if (installedMajor !== requirement.major) {
382
737
  throw new Error(
@@ -660,8 +1015,8 @@ function planAttempts(step, seedBase, overrideRuns) {
660
1015
 
661
1016
  // src/signoff/store.ts
662
1017
  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";
1018
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2, utimesSync, writeFileSync } from "fs";
1019
+ import { join as join6, relative, sep } from "path";
665
1020
  var MANIFEST_FILES = [
666
1021
  "pnpm-lock.yaml",
667
1022
  "pnpm-workspace.yaml",
@@ -677,9 +1032,9 @@ function collectManifests(dir, root, out) {
677
1032
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
678
1033
  if (entry.isDirectory()) {
679
1034
  if (SKIP_DIRS.has(entry.name)) continue;
680
- collectManifests(join5(dir, entry.name), root, out);
1035
+ collectManifests(join6(dir, entry.name), root, out);
681
1036
  } else if (MANIFEST_FILES.includes(entry.name)) {
682
- out.push(relative(root, join5(dir, entry.name)).split(sep).join("/"));
1037
+ out.push(relative(root, join6(dir, entry.name)).split(sep).join("/"));
683
1038
  }
684
1039
  }
685
1040
  }
@@ -693,20 +1048,20 @@ function manifestCacheKey(treePath, files) {
693
1048
  for (const rel of files) {
694
1049
  hash.update(rel);
695
1050
  hash.update("\0");
696
- hash.update(createHash2("sha256").update(readFileSync4(join5(treePath, rel))).digest("hex"));
1051
+ hash.update(createHash2("sha256").update(readFileSync5(join6(treePath, rel))).digest("hex"));
697
1052
  hash.update("\n");
698
1053
  }
699
1054
  return hash.digest("hex");
700
1055
  }
701
1056
  function pruneStores(storesRoot, keep) {
702
- if (!existsSync5(storesRoot)) return [];
1057
+ if (!existsSync6(storesRoot)) return [];
703
1058
  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 };
1059
+ const full = join6(storesRoot, entry.name);
1060
+ return { full, mtimeMs: statSync2(full).mtimeMs };
706
1061
  }).sort((a, b) => b.mtimeMs - a.mtimeMs);
707
1062
  const pruned = [];
708
1063
  for (const stale of entries.slice(keep)) {
709
- rmSync(stale.full, { recursive: true, force: true });
1064
+ rmSync2(stale.full, { recursive: true, force: true });
710
1065
  pruned.push(stale.full);
711
1066
  }
712
1067
  return pruned;
@@ -720,10 +1075,10 @@ function resolveStore(options) {
720
1075
  );
721
1076
  }
722
1077
  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");
1078
+ const storesRoot = join6(cacheDir, "stores");
1079
+ const storeDir = join6(storesRoot, cacheKey);
1080
+ const marker = join6(storeDir, ".signoff-store.json");
1081
+ const hit = existsSync6(storeDir) && readdirSync2(storeDir).some((entry) => entry !== ".signoff-store.json");
727
1082
  mkdirSync(storeDir, { recursive: true });
728
1083
  writeFileSync(marker, `${JSON.stringify({ cacheKey, keyedOn: files, usedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
729
1084
  `);
@@ -733,12 +1088,12 @@ function resolveStore(options) {
733
1088
  }
734
1089
 
735
1090
  // src/signoff/workspace.ts
736
- import { spawnSync as spawnSync2 } from "child_process";
1091
+ import { spawnSync as spawnSync3 } from "child_process";
737
1092
  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";
1093
+ import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
1094
+ import { dirname as dirname2, isAbsolute, join as join7, resolve as resolve2 } from "path";
740
1095
  function git(args, cwd) {
741
- const result = spawnSync2("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
1096
+ const result = spawnSync3("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
742
1097
  if (result.error) throw new Error(`signoff: git ${args.join(" ")} failed to start: ${result.error.message}`);
743
1098
  if (result.status !== 0) {
744
1099
  throw new Error(`signoff: git ${args.join(" ")} exited ${result.status}
@@ -759,7 +1114,7 @@ function materializeCleanTree(options) {
759
1114
  const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], root).trim();
760
1115
  git(["worktree", "prune"], root);
761
1116
  mkdirSync2(dirname2(dest), { recursive: true });
762
- if (existsSync6(dest)) rmSync2(dest, { recursive: true, force: true });
1117
+ if (existsSync7(dest)) rmSync3(dest, { recursive: true, force: true });
763
1118
  git(["worktree", "add", "--detach", "--quiet", dest, head], root);
764
1119
  let diffSha256 = null;
765
1120
  let untrackedFiles = [];
@@ -767,28 +1122,28 @@ function materializeCleanTree(options) {
767
1122
  const patch = git(["diff", "HEAD", "--binary", "--no-color", "--no-ext-diff"], root);
768
1123
  if (patch.length > 0) {
769
1124
  diffSha256 = createHash3("sha256").update(patch).digest("hex");
770
- const patchFile = join6(dirname2(dest), `${dest.split("/").pop() ?? "tree"}.patch`);
1125
+ const patchFile = join7(dirname2(dest), `${dest.split("/").pop() ?? "tree"}.patch`);
771
1126
  writeFileSync2(patchFile, patch);
772
1127
  git(["apply", "--binary", "--whitespace=nowarn", patchFile], dest);
773
- rmSync2(patchFile, { force: true });
1128
+ rmSync3(patchFile, { force: true });
774
1129
  }
775
1130
  untrackedFiles = zsplit(git(["ls-files", "--others", "--exclude-standard", "-z"], root));
776
1131
  for (const rel of untrackedFiles) {
777
- const target = join6(dest, rel);
1132
+ const target = join7(dest, rel);
778
1133
  mkdirSync2(dirname2(target), { recursive: true });
779
- copyFileSync(join6(root, rel), target);
1134
+ copyFileSync(join7(root, rel), target);
780
1135
  }
781
1136
  }
782
1137
  const carried = [];
783
1138
  for (const rel of carryFiles) {
784
1139
  if (isAbsolute(rel)) throw new Error(`signoff: carryFiles must be repo-relative; got "${rel}"`);
785
1140
  const from = resolve2(root, rel);
786
- if (!existsSync6(from)) {
1141
+ if (!existsSync7(from)) {
787
1142
  throw new Error(
788
1143
  `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
1144
  );
790
1145
  }
791
- const target = join6(dest, rel);
1146
+ const target = join7(dest, rel);
792
1147
  mkdirSync2(dirname2(target), { recursive: true });
793
1148
  copyFileSync(from, target);
794
1149
  carried.push(rel);
@@ -810,9 +1165,9 @@ function removeCleanTree(tree) {
810
1165
  }
811
1166
 
812
1167
  // src/signoff/run.ts
813
- var DEFAULT_CACHE_DIR = join7(homedir(), ".cache", "agent-app-signoff");
1168
+ var DEFAULT_CACHE_DIR = join8(homedir(), ".cache", "agent-app-signoff");
814
1169
  function hostFacts(treePath, requirement) {
815
- const pm = spawnSync3("pnpm", ["--version"], { cwd: treePath, encoding: "utf8" });
1170
+ const pm = spawnSync4("pnpm", ["--version"], { cwd: treePath, encoding: "utf8" });
816
1171
  return {
817
1172
  node: process.version,
818
1173
  nodePinned: requirement?.declared ?? null,
@@ -854,7 +1209,7 @@ async function runSignoff(options = {}) {
854
1209
  const repoRoot = repoRootOf(repoDir);
855
1210
  const source = options.source ?? "working-tree";
856
1211
  let cacheDir = resolve3(options.cacheDir ?? DEFAULT_CACHE_DIR);
857
- const treePath = () => join7(cacheDir, "trees", `${basename2(repoRoot)}-${process.pid}`);
1212
+ const treePath = () => join8(cacheDir, "trees", `${basename2(repoRoot)}-${process.pid}`);
858
1213
  let tree = null;
859
1214
  try {
860
1215
  tree = materializeCleanTree({ repoDir: repoRoot, dest: treePath(), source });
@@ -874,7 +1229,7 @@ async function runSignoff(options = {}) {
874
1229
  const store = resolveStore({ treePath: tree.path, cacheDir, generations: config.storeGenerations });
875
1230
  options.onEvent?.({ kind: "store", storeDir: store.storeDir, cacheHit: store.hit, cacheKey: store.cacheKey });
876
1231
  const installSpec = config.install ?? {};
877
- const installCwd = join7(tree.path, installSpec.cwd ?? ".");
1232
+ const installCwd = join8(tree.path, installSpec.cwd ?? ".");
878
1233
  const installCommand = withStoreDir(
879
1234
  installSpec.run ?? "pnpm install --frozen-lockfile",
880
1235
  installSpec.storeDirFlag,
@@ -921,7 +1276,7 @@ async function runSignoff(options = {}) {
921
1276
  origin,
922
1277
  host,
923
1278
  install,
924
- steps: [...config.steps.map((step) => step.name), WORKER_DEFAULTS_STEP].map(
1279
+ steps: [...config.steps.map((step) => step.name), WORKER_DEFAULTS_STEP, APP_DEFAULTS_STEP].map(
925
1280
  (name) => ({
926
1281
  name,
927
1282
  status: "skipped",
@@ -951,7 +1306,7 @@ async function runSignoff(options = {}) {
951
1306
  options.onEvent?.({ kind: "step-start", name: step.name, command: plan.command, seed: plan.seed });
952
1307
  const result = await runCommand({
953
1308
  command: plan.command,
954
- cwd: join7(treeRoot, step.cwd ?? "."),
1309
+ cwd: join8(treeRoot, step.cwd ?? "."),
955
1310
  env: buildEnv(sharedEnv, [step.env]),
956
1311
  timeoutMs: step.timeoutMs,
957
1312
  signal
@@ -967,7 +1322,8 @@ async function runSignoff(options = {}) {
967
1322
  return { ok: true, value: attempts };
968
1323
  }
969
1324
  });
970
- const steps = [...outcomes.map(toStepResult), workerDefaults];
1325
+ const appDefaults = runAppDefaults(tree.path, config, options);
1326
+ const steps = [...outcomes.map(toStepResult), workerDefaults, appDefaults];
971
1327
  return finish({
972
1328
  ok: steps.every((step) => step.status === "passed"),
973
1329
  startedAt,
@@ -984,7 +1340,7 @@ async function runSignoff(options = {}) {
984
1340
  options
985
1341
  });
986
1342
  } finally {
987
- if (tree && !options.keepWorkspace && existsSync7(tree.path)) removeCleanTree(tree);
1343
+ if (tree && !options.keepWorkspace && existsSync8(tree.path)) removeCleanTree(tree);
988
1344
  }
989
1345
  }
990
1346
  function runWorkerDefaults(treeRoot, workerConfigs, options) {
@@ -1009,6 +1365,28 @@ function runWorkerDefaults(treeRoot, workerConfigs, options) {
1009
1365
  emitStepEnd(options, WORKER_DEFAULTS_STEP, ok ? "passed" : "failed", attempts);
1010
1366
  return { name: WORKER_DEFAULTS_STEP, status: ok ? "passed" : "failed", attempts, durationMs, startedAtMs: null, finishedAtMs: null };
1011
1367
  }
1368
+ function runAppDefaults(treeRoot, config, options) {
1369
+ const command = `agent-app-signoff ${APP_DEFAULTS_STEP}`;
1370
+ options.onEvent?.({ kind: "step-start", name: APP_DEFAULTS_STEP, command, seed: null });
1371
+ const started = Date.now();
1372
+ let output;
1373
+ let ok;
1374
+ try {
1375
+ const result = checkAppDefaults(treeRoot, { exemptions: config.agentApp?.exemptions });
1376
+ output = formatAppDefaults(result);
1377
+ ok = result.findings.length === 0;
1378
+ } catch (error) {
1379
+ output = `${error instanceof Error ? error.stack ?? error.message : String(error)}
1380
+ `;
1381
+ ok = false;
1382
+ }
1383
+ const durationMs = Date.now() - started;
1384
+ const attempts = [
1385
+ { command, seed: null, exitCode: ok ? 0 : 1, signal: null, durationMs, timedOut: false, output, outputTruncated: false }
1386
+ ];
1387
+ emitStepEnd(options, APP_DEFAULTS_STEP, ok ? "passed" : "failed", attempts);
1388
+ return { name: APP_DEFAULTS_STEP, status: ok ? "passed" : "failed", attempts, durationMs, startedAtMs: null, finishedAtMs: null };
1389
+ }
1012
1390
  function emitStepEnd(options, name, status, attempts) {
1013
1391
  options.onEvent?.({
1014
1392
  kind: "step-end",
@@ -1394,7 +1772,7 @@ function round(value) {
1394
1772
  }
1395
1773
 
1396
1774
  // src/signoff/shell-alignment-cli.ts
1397
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1775
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1398
1776
  import { dirname as dirname3, resolve as resolve4 } from "path";
1399
1777
  function shellAlignmentUsage() {
1400
1778
  return [
@@ -1448,7 +1826,7 @@ function parseShellAlignmentArgs(argv) {
1448
1826
  out.storageState = resolve4(take());
1449
1827
  break;
1450
1828
  case "--theme-script":
1451
- out.themeScript = readFileSync5(resolve4(take()), "utf8");
1829
+ out.themeScript = readFileSync6(resolve4(take()), "utf8");
1452
1830
  break;
1453
1831
  case "--ready":
1454
1832
  out.readySelector = take();
@@ -1787,7 +2165,7 @@ function formatSurfaceLadderReport(report) {
1787
2165
  }
1788
2166
 
1789
2167
  // src/signoff/surface-ladder-cli.ts
1790
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2168
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1791
2169
  import { dirname as dirname4, resolve as resolve5 } from "path";
1792
2170
  function surfaceLadderUsage() {
1793
2171
  return [
@@ -1849,7 +2227,7 @@ function parseSurfaceLadderArgs(argv) {
1849
2227
  out.storageState = resolve5(take());
1850
2228
  break;
1851
2229
  case "--theme-script":
1852
- out.themeScript = readFileSync6(resolve5(take()), "utf8");
2230
+ out.themeScript = readFileSync7(resolve5(take()), "utf8");
1853
2231
  break;
1854
2232
  case "--ready":
1855
2233
  out.readySelector = take();
@@ -1867,7 +2245,7 @@ function parseSurfaceLadderArgs(argv) {
1867
2245
  break;
1868
2246
  }
1869
2247
  case "--inject-css":
1870
- out.injectCss = readFileSync6(resolve5(take()), "utf8");
2248
+ out.injectCss = readFileSync7(resolve5(take()), "utf8");
1871
2249
  break;
1872
2250
  case "--screenshots":
1873
2251
  out.screenshotDir = resolve5(take());
@@ -2047,6 +2425,9 @@ export {
2047
2425
  WORKER_COMPATIBILITY_DATE_FLOOR,
2048
2426
  checkWorkerDefaults,
2049
2427
  formatWorkerDefaults,
2428
+ checkAppDefaults,
2429
+ formatAppDefaults,
2430
+ collectLastExports,
2050
2431
  loadSignoffConfig,
2051
2432
  runSignoff,
2052
2433
  formatSignoffReport,
@@ -2088,4 +2469,4 @@ export {
2088
2469
  formatWorkspaceSurfaceReport,
2089
2470
  runWorkspaceSurfaceCli
2090
2471
  };
2091
- //# sourceMappingURL=chunk-KDPTXXOQ.js.map
2472
+ //# sourceMappingURL=chunk-J3N6RNMH.js.map