@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.
- package/dist/chat-routes/index.js +1 -1
- package/dist/{chunk-QA2CBATZ.js → chunk-4QUOPWIG.js} +12 -9
- package/dist/chunk-4QUOPWIG.js.map +1 -0
- package/dist/chunk-CD2LQXY6.js +306 -0
- package/dist/chunk-CD2LQXY6.js.map +1 -0
- package/dist/chunk-LRYY3OX7.js +105 -0
- package/dist/chunk-LRYY3OX7.js.map +1 -0
- package/dist/{chunk-LLO436QO.js → chunk-QM7RTTDY.js} +27 -86
- package/dist/chunk-QM7RTTDY.js.map +1 -0
- package/dist/{chunk-KDPTXXOQ.js → chunk-WDY47GBR.js} +812 -115
- package/dist/chunk-WDY47GBR.js.map +1 -0
- package/dist/{chunk-ZRLKB4KB.js → chunk-YWLZOPDS.js} +10 -11
- package/dist/{chunk-ZRLKB4KB.js.map → chunk-YWLZOPDS.js.map} +1 -1
- package/dist/launch-invariants/catalog.d.ts +16 -0
- package/dist/launch-invariants/cli.js +5 -2
- package/dist/launch-invariants/cli.js.map +1 -1
- package/dist/launch-invariants/index.d.ts +1 -1
- package/dist/launch-invariants/index.js +13 -5
- package/dist/launch-invariants/index.js.map +1 -1
- package/dist/launch-invariants/limit-alarms.d.ts +19 -1
- package/dist/launch-invariants/testing.d.ts +0 -3
- package/dist/launch-invariants/testing.js +5 -2
- package/dist/sandbox/index.d.ts +3 -3
- package/dist/sandbox/index.js +1 -1
- package/dist/signoff/app-defaults.d.ts +65 -0
- package/dist/signoff/cache.d.ts +29 -0
- package/dist/signoff/cli.js +75 -4
- package/dist/signoff/cli.js.map +1 -1
- package/dist/signoff/defaults-cli.d.ts +1 -0
- package/dist/signoff/index.js +2 -1
- package/dist/signoff/store.d.ts +2 -0
- package/dist/signoff/types.d.ts +12 -0
- package/dist/signoff/worker-defaults.d.ts +8 -0
- package/dist/turn-health/index.js +12 -291
- package/dist/turn-health/index.js.map +1 -1
- package/dist/turn-trace/index.js +20 -3
- package/dist/turn-trace/index.js.map +1 -1
- package/dist/turn-trace/otel.d.ts +11 -4
- package/package.json +5 -2
- package/dist/chunk-KDPTXXOQ.js.map +0 -1
- package/dist/chunk-LLO436QO.js.map +0 -1
- 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
|
|
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 !== ""
|
|
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
|
|
102
|
-
import { join as
|
|
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
|
-
|
|
147
|
-
|
|
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 (!
|
|
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 =
|
|
188
|
-
if (
|
|
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 =
|
|
191
|
-
if (!
|
|
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(
|
|
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
|
|
210
|
-
import {
|
|
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
|
|
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
|
|
216
|
-
import { join as
|
|
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
|
|
220
|
-
import { join as
|
|
221
|
-
var WORKFLOW_DIR =
|
|
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 =
|
|
272
|
-
if (!
|
|
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 =
|
|
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 =
|
|
284
|
-
if (!
|
|
922
|
+
const dir = join5(repoRoot, WORKFLOW_DIR);
|
|
923
|
+
if (!existsSync5(dir)) return [];
|
|
285
924
|
const pins = [];
|
|
286
|
-
const files =
|
|
925
|
+
const files = readdirSync2(dir).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
|
|
287
926
|
for (const name of files) {
|
|
288
|
-
const source =
|
|
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 =
|
|
339
|
-
if (
|
|
340
|
-
const raw =
|
|
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 =
|
|
365
|
-
if (!
|
|
366
|
-
const manifest = JSON.parse(
|
|
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 =
|
|
376
|
-
if (!
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
664
|
-
import { join as
|
|
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
|
|
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(
|
|
1319
|
+
collectManifests(join7(dir, entry.name), root, out);
|
|
681
1320
|
} else if (MANIFEST_FILES.includes(entry.name)) {
|
|
682
|
-
out.push(
|
|
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(
|
|
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 (!
|
|
703
|
-
const
|
|
704
|
-
const full =
|
|
705
|
-
return { 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
|
|
709
|
-
|
|
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 =
|
|
724
|
-
const storeDir =
|
|
725
|
-
const marker =
|
|
726
|
-
const hit =
|
|
727
|
-
|
|
728
|
-
|
|
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
|
|
1376
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
737
1377
|
import { createHash as createHash3 } from "crypto";
|
|
738
|
-
import { copyFileSync, existsSync as
|
|
739
|
-
import { dirname as dirname2, isAbsolute, join as
|
|
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 =
|
|
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
|
-
|
|
762
|
-
if (
|
|
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 =
|
|
771
|
-
|
|
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
|
-
|
|
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 =
|
|
778
|
-
|
|
779
|
-
copyFileSync(
|
|
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 =
|
|
786
|
-
if (!
|
|
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 =
|
|
792
|
-
|
|
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 =
|
|
1453
|
+
var DEFAULT_CACHE_DIR = join9(homedir(), ".cache", "agent-app-signoff");
|
|
814
1454
|
function hostFacts(treePath, requirement) {
|
|
815
|
-
const pm =
|
|
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 =
|
|
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 =
|
|
857
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
1398
|
-
import { dirname as dirname3, resolve as
|
|
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 =
|
|
2142
|
+
out.storageState = resolve5(take());
|
|
1449
2143
|
break;
|
|
1450
2144
|
case "--theme-script":
|
|
1451
|
-
out.themeScript =
|
|
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 =
|
|
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)
|
|
2187
|
+
if (options.screenshotDir) mkdirSync4(options.screenshotDir, { recursive: true });
|
|
1494
2188
|
const report = await checkShellAlignment(options);
|
|
1495
2189
|
if (options.jsonPath) {
|
|
1496
|
-
const abs =
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
|
1791
|
-
import { dirname as dirname4, resolve as
|
|
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 =
|
|
2543
|
+
out.storageState = resolve6(take());
|
|
1850
2544
|
break;
|
|
1851
2545
|
case "--theme-script":
|
|
1852
|
-
out.themeScript =
|
|
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 =
|
|
2564
|
+
out.injectCss = readFileSync8(resolve6(take()), "utf8");
|
|
1871
2565
|
break;
|
|
1872
2566
|
case "--screenshots":
|
|
1873
|
-
out.screenshotDir =
|
|
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)
|
|
2597
|
+
if (options.screenshotDir) mkdirSync5(options.screenshotDir, { recursive: true });
|
|
1904
2598
|
const report = await checkSurfaceLadder(options);
|
|
1905
2599
|
if (options.jsonPath) {
|
|
1906
|
-
const abs =
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
|
2002
|
-
import { dirname as dirname5, resolve as
|
|
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)
|
|
2725
|
+
if (options.screenshotDir) mkdirSync6(options.screenshotDir, { recursive: true });
|
|
2032
2726
|
const report = await checkWorkspaceSurface(options);
|
|
2033
2727
|
if (options.jsonPath) {
|
|
2034
|
-
const abs =
|
|
2035
|
-
|
|
2036
|
-
|
|
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-
|
|
2788
|
+
//# sourceMappingURL=chunk-WDY47GBR.js.map
|