agent-lyceum 0.31.0 → 0.32.0
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/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/cli.js +3 -25
- package/dist/config.d.ts +11 -1
- package/dist/config.js +8 -1
- package/dist/doctor.js +8 -0
- package/dist/fs-util.d.ts +1 -1
- package/dist/process-runner.d.ts +5 -0
- package/dist/process-runner.js +4 -3
- package/dist/repeat-detect.d.ts +40 -0
- package/dist/repeat-detect.js +121 -0
- package/dist/run-outcome.d.ts +5 -1
- package/dist/run-outcome.js +9 -1
- package/dist/run-print.d.ts +3 -0
- package/dist/run-print.js +31 -0
- package/dist/run-session.d.ts +18 -0
- package/dist/run-session.js +109 -8
- package/dist/run-store.d.ts +29 -0
- package/dist/run-store.js +3 -0
- package/dist/schema.d.ts +34 -0
- package/dist/schema.js +24 -0
- package/dist/status.js +10 -1
- package/dist/summarize.d.ts +2 -0
- package/dist/summarize.js +85 -0
- package/dist/verify.d.ts +67 -0
- package/dist/verify.js +184 -0
- package/dist/worktree.d.ts +9 -0
- package/dist/worktree.js +80 -1
- package/package.json +1 -1
- package/web/dist/public/assets/index-CAW06fSt.css +2 -0
- package/web/dist/public/assets/index-CP0uF27G.js +9 -0
- package/web/dist/public/index.html +2 -2
- package/web/dist/server/brief.js +84 -0
- package/web/dist/server/hub.js +3 -0
- package/web/dist/server/summary.js +92 -0
- package/web/dist/start.js +10 -0
- package/web/dist/public/assets/index-B1Ld6Pl5.css +0 -2
- package/web/dist/public/assets/index-D2JeeQpl.js +0 -9
package/dist/verify.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWrite } from "./fs-util.js";
|
|
5
|
+
import { killTree, register, unregister } from "./process-runner.js";
|
|
6
|
+
import { FINGERPRINT_WINDOW_BYTES, failureFingerprint, fingerprintPaths } from "./repeat-detect.js";
|
|
7
|
+
/** Most output kept in a verify log; a longer output keeps its end and says so at the top. */
|
|
8
|
+
export const VERIFY_LOG_BYTES = 1024 * 1024;
|
|
9
|
+
/** Most output of one command put into the mail to the lead and into the report. */
|
|
10
|
+
export const VERIFY_TAIL_BYTES = 4096;
|
|
11
|
+
export const VERIFY_DIR = "verify";
|
|
12
|
+
/** The last `max` bytes of `buf` as text, never starting in the middle of a UTF-8 character. */
|
|
13
|
+
export function tailText(buf, max) {
|
|
14
|
+
if (buf.length <= max)
|
|
15
|
+
return buf.toString("utf8");
|
|
16
|
+
let start = buf.length - max;
|
|
17
|
+
while (start < buf.length && (buf[start] & 0xc0) === 0x80)
|
|
18
|
+
start++;
|
|
19
|
+
return buf.subarray(start).toString("utf8");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run one command with `sh -c` in its own process group: stdin closed, output (both streams, in arrival order) kept in memory up to
|
|
23
|
+
* 2 x VERIFY_LOG_BYTES. Timeout and cancel stop the whole tree (SIGTERM, SIGKILL after `killGraceMs`); this settles once, whatever
|
|
24
|
+
* still holds the pipes. The child is registered with the process runner, so `killAllRunning` takes it down when agent-lyceum exits.
|
|
25
|
+
*/
|
|
26
|
+
export function runCommand(run, opts) {
|
|
27
|
+
const grace = opts.killGraceMs ?? 5000;
|
|
28
|
+
const settleWait = opts.settleWaitMs ?? 1000;
|
|
29
|
+
if (opts.signal?.aborted)
|
|
30
|
+
return Promise.resolve({ cancelled: true });
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const started = Date.now();
|
|
33
|
+
let child;
|
|
34
|
+
try {
|
|
35
|
+
child = spawn("sh", ["-c", run], { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return resolve({ cancelled: false, exit: null, ms: 0, timedOut: false, error: e.message, output: Buffer.alloc(0) });
|
|
39
|
+
}
|
|
40
|
+
if (child.pid !== undefined)
|
|
41
|
+
register(child.pid);
|
|
42
|
+
let buf = Buffer.alloc(0);
|
|
43
|
+
let dropped = 0;
|
|
44
|
+
let exit = null;
|
|
45
|
+
let reason;
|
|
46
|
+
let spawnError;
|
|
47
|
+
let settled = false;
|
|
48
|
+
const timers = [];
|
|
49
|
+
const kill = (sig) => {
|
|
50
|
+
if (child.pid !== undefined)
|
|
51
|
+
killTree(child.pid, sig);
|
|
52
|
+
};
|
|
53
|
+
const finish = () => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
for (const t of timers)
|
|
58
|
+
clearTimeout(t);
|
|
59
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
60
|
+
kill("SIGKILL"); // whatever of the tree is still around
|
|
61
|
+
if (child.pid !== undefined)
|
|
62
|
+
unregister(child.pid);
|
|
63
|
+
child.stdout?.destroy();
|
|
64
|
+
child.stderr?.destroy();
|
|
65
|
+
// An abort that came after the process was gone but before the result was judged also withholds the verdict.
|
|
66
|
+
if (reason === "cancel" || opts.signal?.aborted)
|
|
67
|
+
return resolve({ cancelled: true });
|
|
68
|
+
const out = dropped ? Buffer.concat([Buffer.from(`[agent-lyceum: ${dropped} byte(s) of earlier output dropped]\n`), buf]) : buf;
|
|
69
|
+
resolve({ cancelled: false, exit, ms: Date.now() - started, timedOut: reason === "timeout", ...(spawnError ? { error: spawnError } : {}), output: out });
|
|
70
|
+
};
|
|
71
|
+
const stop = (why) => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
// Cancel outranks a timeout that is still cleaning up: the verdict is withheld either way, and the tree is already being stopped.
|
|
75
|
+
if (reason) {
|
|
76
|
+
if (why === "cancel")
|
|
77
|
+
reason = "cancel";
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
reason = why;
|
|
81
|
+
kill("SIGTERM");
|
|
82
|
+
timers.push(setTimeout(() => kill("SIGKILL"), grace));
|
|
83
|
+
timers.push(setTimeout(finish, grace + settleWait));
|
|
84
|
+
};
|
|
85
|
+
function onAbort() {
|
|
86
|
+
stop("cancel");
|
|
87
|
+
}
|
|
88
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
89
|
+
timers.push(setTimeout(() => stop("timeout"), opts.timeoutSec * 1000));
|
|
90
|
+
const onData = (chunk) => {
|
|
91
|
+
opts.onOutput?.(chunk);
|
|
92
|
+
buf = Buffer.concat([buf, chunk]);
|
|
93
|
+
if (buf.length > VERIFY_LOG_BYTES * 2) {
|
|
94
|
+
let start = buf.length - VERIFY_LOG_BYTES;
|
|
95
|
+
while (start < buf.length && (buf[start] & 0xc0) === 0x80)
|
|
96
|
+
start++;
|
|
97
|
+
dropped += start;
|
|
98
|
+
buf = buf.subarray(start);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
child.stdout?.on("data", onData);
|
|
102
|
+
child.stderr?.on("data", onData);
|
|
103
|
+
child.on("error", (e) => {
|
|
104
|
+
spawnError = e.code === "ENOENT" ? `${e.message} (sh or the working directory ${opts.cwd} not found)` : e.message;
|
|
105
|
+
finish();
|
|
106
|
+
});
|
|
107
|
+
child.on("exit", (code) => {
|
|
108
|
+
exit = code;
|
|
109
|
+
timers.push(setTimeout(finish, settleWait)); // a descendant may still hold the pipes
|
|
110
|
+
});
|
|
111
|
+
child.on("close", finish);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** The output as it goes into the log file: at most VERIFY_LOG_BYTES, with a first line saying so when the start was cut. */
|
|
115
|
+
export function logBody(output) {
|
|
116
|
+
if (output.length <= VERIFY_LOG_BYTES)
|
|
117
|
+
return output;
|
|
118
|
+
let start = output.length - VERIFY_LOG_BYTES;
|
|
119
|
+
while (start < output.length && (output[start] & 0xc0) === 0x80)
|
|
120
|
+
start++;
|
|
121
|
+
return Buffer.concat([Buffer.from(`[agent-lyceum: output truncated; the first ${start} byte(s) are not kept, the last ${output.length - start} follow]\n`), output.subarray(start)]);
|
|
122
|
+
}
|
|
123
|
+
/** The next free log number: above every record and every file already in `verify/`, so no attempt (also not an unrecorded one from a crash) is overwritten. */
|
|
124
|
+
export function nextLogNumber(runDir, records) {
|
|
125
|
+
let max = 0;
|
|
126
|
+
for (const r of records ?? [])
|
|
127
|
+
for (const c of r.commands)
|
|
128
|
+
max = Math.max(max, Number(/^verify\/(\d+)-/.exec(c.log)?.[1] ?? 0));
|
|
129
|
+
try {
|
|
130
|
+
for (const f of fs.readdirSync(path.join(runDir, VERIFY_DIR)))
|
|
131
|
+
max = Math.max(max, Number(/^(\d+)-/.exec(f)?.[1] ?? 0));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* no verify dir yet */
|
|
135
|
+
}
|
|
136
|
+
return max + 1;
|
|
137
|
+
}
|
|
138
|
+
/** Run the commands in order, stopping at the first failure (fail-fast). Cancelling gives no verdict. */
|
|
139
|
+
export async function runVerify(verify, runDir, records, opts, hooks = {}) {
|
|
140
|
+
const commands = [];
|
|
141
|
+
const tails = {};
|
|
142
|
+
let n = nextLogNumber(runDir, records);
|
|
143
|
+
for (const c of verify.commands) {
|
|
144
|
+
const rel = `${VERIFY_DIR}/${n++}-${c.name}.log`;
|
|
145
|
+
hooks.start?.(c.name);
|
|
146
|
+
const r = await runCommand(c.run, { cwd: opts.cwd, timeoutSec: c.timeout_sec, signal: opts.signal, killGraceMs: opts.killGraceMs, settleWaitMs: opts.settleWaitMs });
|
|
147
|
+
if (r.cancelled)
|
|
148
|
+
return { cancelled: true };
|
|
149
|
+
const body = logBody(r.output);
|
|
150
|
+
fs.mkdirSync(path.join(runDir, VERIFY_DIR), { recursive: true });
|
|
151
|
+
atomicWrite(path.join(runDir, rel), body);
|
|
152
|
+
const rec = { name: c.name, exit: r.exit, ms: r.ms, timed_out: r.timedOut, log: rel, ...(r.error ? { error: r.error } : {}) };
|
|
153
|
+
if (!commandPassed(rec))
|
|
154
|
+
rec.fingerprint = failureFingerprint(rec, tailText(r.output, FINGERPRINT_WINDOW_BYTES), fingerprintPaths(opts.cwd, runDir));
|
|
155
|
+
commands.push(rec);
|
|
156
|
+
tails[c.name] = tailText(r.output, VERIFY_TAIL_BYTES).replace(/\r\n/g, "\n");
|
|
157
|
+
hooks.end?.(rec);
|
|
158
|
+
if (!commandPassed(rec))
|
|
159
|
+
return { cancelled: false, passed: false, commands, tails, failed: rec };
|
|
160
|
+
}
|
|
161
|
+
if (opts.signal?.aborted)
|
|
162
|
+
return { cancelled: true };
|
|
163
|
+
return { cancelled: false, passed: true, commands, tails };
|
|
164
|
+
}
|
|
165
|
+
export const commandPassed = (c) => c.exit === 0 && !c.timed_out && !c.error;
|
|
166
|
+
/** How a failed command reads: "exit 1", "timed out after 600s", ... */
|
|
167
|
+
export function describeFailure(c, timeoutSec) {
|
|
168
|
+
if (c.error)
|
|
169
|
+
return `could not be run (${c.error})`;
|
|
170
|
+
if (c.timed_out)
|
|
171
|
+
return `timed out${timeoutSec ? ` after ${timeoutSec}s` : ""} and was killed`;
|
|
172
|
+
return c.exit === null ? "was killed by a signal" : `exited with code ${c.exit}`;
|
|
173
|
+
}
|
|
174
|
+
const secs = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
175
|
+
/** `## Dispatcher verification`: appended by the dispatcher after the lead's own report. */
|
|
176
|
+
export function verificationSection(rec, verify, cwd) {
|
|
177
|
+
const lines = rec.commands.map((c) => `- \`${c.name}\`: ${commandPassed(c) ? "passed" : `FAILED, ${describeFailure(c, verify.commands.find((x) => x.name === c.name)?.timeout_sec)}`} (${secs(c.ms)}, log ${c.log})`);
|
|
178
|
+
const skipped = verify.commands.slice(rec.commands.length).map((c) => `- \`${c.name}\`: not run (an earlier command failed)`);
|
|
179
|
+
return `## Dispatcher verification\n\nRun by the dispatcher in ${cwd}, not by an agent: ${rec.passed ? "all commands passed" : "NOT all commands passed"}.\n\n${[...lines, ...skipped].join("\n")}\n`;
|
|
180
|
+
}
|
|
181
|
+
/** The mail that sends a failed `done` back to the lead. */
|
|
182
|
+
export function rejectionMessage(failed, run, tail, timeoutSec, count, max) {
|
|
183
|
+
return `The run was not ended: before accepting your \`done\` (outcome completed) the dispatcher ran the project's verify commands, and one failed.\n\nCommand \`${failed.name}\` (\`${run}\`) ${describeFailure(failed, timeoutSec)}. Later commands were not run.\n\nLast output (at most ${VERIFY_TAIL_BYTES} bytes; the full log is ${failed.log} in the run directory):\n\n\`\`\`\n${tail.trimEnd()}\n\`\`\`\n\nFix the cause, then send \`done\` again. If the work cannot pass, report \`partial\` or \`blocked\` honestly (those are not verified). (Verification rejection ${count} of ${max}.)`;
|
|
184
|
+
}
|
package/dist/worktree.d.ts
CHANGED
|
@@ -81,6 +81,15 @@ export declare function snapshotHead(project: ResolvedProject, runId: string): s
|
|
|
81
81
|
* `from`: the checkout to snapshot — the lead's worktree in a concurrent run; by default your repo's working tree.
|
|
82
82
|
*/
|
|
83
83
|
export declare function snapshotBase(project: ResolvedProject, runId: string, n: number, from?: AgentWorkspace): string;
|
|
84
|
+
/**
|
|
85
|
+
* The git tree of the working tree as it is now (tracked changes and new, non-ignored files), through a throwaway index:
|
|
86
|
+
* nothing of the repo is touched. A worktree is read with the pinned git (no hooks, fsmonitor, filters from the worktree).
|
|
87
|
+
* Undefined when `dir` is not a git repository or the tree cannot be read (a nested repository in a worktree, ...).
|
|
88
|
+
*/
|
|
89
|
+
export declare function workingTreeHash(project: ResolvedProject, from?: AgentWorkspace, opts?: {
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
}): Promise<string | undefined>;
|
|
84
93
|
export declare const workspaceRoot: (project: ResolvedProject, runId: string, agent: string) => string;
|
|
85
94
|
/**
|
|
86
95
|
* The agent's own checkout of the snapshot `baseRef`, at `<project home>/worktrees/<run>/<agent>`.
|
package/dist/worktree.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { atomicWrite } from "./fs-util.js";
|
|
7
7
|
import { escapesParent } from "./paths.js";
|
|
8
|
+
import { killTree, register, unregister } from "./process-runner.js";
|
|
8
9
|
const IDENTITY = {
|
|
9
10
|
GIT_AUTHOR_NAME: "agent-lyceum",
|
|
10
11
|
GIT_AUTHOR_EMAIL: "agent-lyceum@localhost",
|
|
@@ -291,6 +292,84 @@ export function snapshotBase(project, runId, n, from) {
|
|
|
291
292
|
fs.rmSync(index, { force: true });
|
|
292
293
|
}
|
|
293
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The git tree of the working tree as it is now (tracked changes and new, non-ignored files), through a throwaway index:
|
|
297
|
+
* nothing of the repo is touched. A worktree is read with the pinned git (no hooks, fsmonitor, filters from the worktree).
|
|
298
|
+
* Undefined when `dir` is not a git repository or the tree cannot be read (a nested repository in a worktree, ...).
|
|
299
|
+
*/
|
|
300
|
+
export async function workingTreeHash(project, from, opts = {}) {
|
|
301
|
+
const index = path.join(os.tmpdir(), `agent-lyceum-index-${crypto.randomBytes(6).toString("hex")}`);
|
|
302
|
+
const deadline = Date.now() + (opts.timeoutMs ?? HASH_BUDGET_MS);
|
|
303
|
+
// No hooks (git runs post-index-change after writing an index, also a temporary one) and no fsmonitor, in both modes.
|
|
304
|
+
// Clean filters named in the repo's own config still run during `add -A` (as in snapshotBase); only the deadline bounds them.
|
|
305
|
+
const run = (cwd, args, env = {}) => boundedGit(cwd, from ? pinnedArgs(args) : ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", ...args], from ? pinnedEnv(from, env) : env, deadline, opts.signal);
|
|
306
|
+
try {
|
|
307
|
+
let cwd;
|
|
308
|
+
if (from) {
|
|
309
|
+
assertWorkspaceIntact(from);
|
|
310
|
+
assertNoNestedRepos(from); // sync, no hooks, quick
|
|
311
|
+
cwd = from.root;
|
|
312
|
+
}
|
|
313
|
+
else
|
|
314
|
+
cwd = await run(project.dir, ["rev-parse", "--show-toplevel"]);
|
|
315
|
+
const env = { GIT_INDEX_FILE: index };
|
|
316
|
+
const head = await run(cwd, ["rev-parse", "--verify", "HEAD"]).catch(() => undefined);
|
|
317
|
+
if (head)
|
|
318
|
+
await run(cwd, ["read-tree", head], env);
|
|
319
|
+
await run(cwd, ["add", "-A"], env);
|
|
320
|
+
return await run(cwd, ["write-tree"], env);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
fs.rmSync(index, { force: true });
|
|
327
|
+
fs.rmSync(`${index}.lock`, { force: true }); // a git killed during `add` leaves its lock behind
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** The most the tree hash may take in total (spec 27.7 record, spec 28 comparison); then it is absent and verification goes on. */
|
|
331
|
+
const HASH_BUDGET_MS = 30_000;
|
|
332
|
+
/** git in its own process group, killed with SIGKILL (group and descendants) at `deadline` or on `signal`; the event loop stays free meanwhile. */
|
|
333
|
+
function boundedGit(cwd, args, env, deadline, signal) {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
if (signal?.aborted)
|
|
336
|
+
return reject(new Error("aborted"));
|
|
337
|
+
const child = spawn("git", args, { cwd, env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
338
|
+
if (child.pid !== undefined)
|
|
339
|
+
register(child.pid);
|
|
340
|
+
let out = "";
|
|
341
|
+
let err = "";
|
|
342
|
+
let done = false;
|
|
343
|
+
const end = (e) => {
|
|
344
|
+
if (done)
|
|
345
|
+
return;
|
|
346
|
+
done = true;
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
signal?.removeEventListener("abort", onAbort);
|
|
349
|
+
if (child.pid !== undefined) {
|
|
350
|
+
killTree(child.pid, "SIGKILL"); // also what a filter left behind
|
|
351
|
+
unregister(child.pid);
|
|
352
|
+
}
|
|
353
|
+
child.stdout?.destroy();
|
|
354
|
+
child.stderr?.destroy();
|
|
355
|
+
if (e)
|
|
356
|
+
reject(e);
|
|
357
|
+
else
|
|
358
|
+
resolve(out.replace(/\n$/, ""));
|
|
359
|
+
};
|
|
360
|
+
const onAbort = () => end(new Error("aborted"));
|
|
361
|
+
const timer = setTimeout(() => end(new Error("timed out")), Math.max(0, deadline - Date.now()));
|
|
362
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
363
|
+
child.stdout?.on("data", (c) => {
|
|
364
|
+
out += c.toString("utf8");
|
|
365
|
+
});
|
|
366
|
+
child.stderr?.on("data", (c) => {
|
|
367
|
+
err += c.toString("utf8").slice(0, 4096);
|
|
368
|
+
});
|
|
369
|
+
child.on("error", (e) => end(e));
|
|
370
|
+
child.on("close", (code) => end(code === 0 ? undefined : new Error(`git ${args.join(" ")} failed: ${err.trim()}`)));
|
|
371
|
+
});
|
|
372
|
+
}
|
|
294
373
|
export const workspaceRoot = (project, runId, agent) => path.join(project.paths.root, "worktrees", runId, agent);
|
|
295
374
|
/** The user's repo's git common dir, as a real absolute path. Asked of the user's repo, never of a worktree. */
|
|
296
375
|
const commonDirOf = (top) => fs.realpathSync(git(top, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
|
package/package.json
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:Futura, "Century Gothic", "Avenir Next", "Trebuchet MS", sans-serif;--font-serif:"Hoefler Text", "Iowan Old Style", Baskerville, "Palatino Linotype", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-bold:700;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#1a2330;--color-paper:#f7f8f9;--color-wall:#e4e7ea;--color-signal:#0f6f6a;--color-working:#0f6f6a;--color-failed:#8e3434;--color-line:#c5cad1}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.top-0{top:0}.-left-8{left:calc(var(--spacing) * -8)}.left-1\/2{left:50%}.z-10{z-index:10}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-auto{margin-top:auto}.ml-3{margin-left:calc(var(--spacing) * 3)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-dvh{height:100dvh}.h-full{height:100%}.min-h-0{min-height:0}.w-2\/3{width:66.6667%}.w-20{width:calc(var(--spacing) * 20)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-36{min-width:calc(var(--spacing) * 36)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.list-decimal{list-style-type:decimal}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[7rem_minmax\(0\,1fr\)\]{grid-template-columns:7rem minmax(0,1fr)}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-y-1{row-gap:var(--spacing)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:2147483647px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-failed{border-color:var(--color-failed)}.border-ink{border-color:var(--color-ink)}.border-ink\/60{border-color:#1a233099}@supports (color:color-mix(in lab, red, red)){.border-ink\/60{border-color:color-mix(in oklab, var(--color-ink) 60%, transparent)}}.border-line{border-color:var(--color-line)}.border-signal{border-color:var(--color-signal)}.border-transparent{border-color:#0000}.border-l-ink{border-left-color:var(--color-ink)}.border-l-signal{border-left-color:var(--color-signal)}.bg-ink{background-color:var(--color-ink)}.bg-ink\/30{background-color:#1a23304d}@supports (color:color-mix(in lab, red, red)){.bg-ink\/30{background-color:color-mix(in oklab, var(--color-ink) 30%, transparent)}}.bg-line{background-color:var(--color-line)}.bg-paper{background-color:var(--color-paper)}.bg-signal{background-color:var(--color-signal)}.bg-wall{background-color:var(--color-wall)}.fill-failed{fill:var(--color-failed)}.fill-ink{fill:var(--color-ink)}.fill-none{fill:none}.stroke-line{stroke:var(--color-line)}.stroke-working{stroke:var(--color-working)}.p-0{padding:0}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pt-2{padding-top:calc(var(--spacing) * 2)}.inset-safe{padding-bottom:env(safe-area-inset-bottom);padding-left:env(safe-area-inset-left)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-left{text-align:left}.font-serif{font-family:var(--font-serif)}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-balance{text-wrap:balance}.text-pretty{text-wrap:pretty}.wrap-break-word{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-failed{color:var(--color-failed)}.text-ink{color:var(--color-ink)}.text-ink\/50{color:#1a233080}@supports (color:color-mix(in lab, red, red)){.text-ink\/50{color:color-mix(in oklab, var(--color-ink) 50%, transparent)}}.text-ink\/60{color:#1a233099}@supports (color:color-mix(in lab, red, red)){.text-ink\/60{color:color-mix(in oklab, var(--color-ink) 60%, transparent)}}.text-ink\/70{color:#1a2330b3}@supports (color:color-mix(in lab, red, red)){.text-ink\/70{color:color-mix(in oklab, var(--color-ink) 70%, transparent)}}.text-ink\/80{color:#1a2330cc}@supports (color:color-mix(in lab, red, red)){.text-ink\/80{color:color-mix(in oklab, var(--color-ink) 80%, transparent)}}.text-signal{color:var(--color-signal)}.text-working{color:var(--color-working)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.\!running{animation-play-state:running!important}.enter{--tw-ease:ease-out}.fade-in{--tw-enter-opacity:0}.running{animation-play-state:running}.group-data-\[open\]\:hidden:is(:where(.group)[data-open] *){display:none}.group-data-\[open\]\:inline:is(:where(.group)[data-open] *){display:inline}.first\:mt-0:first-child{margin-top:0}@media (hover:hover){.hover\:bg-paper\/60:hover{background-color:#f7f8f999}@supports (color:color-mix(in lab, red, red)){.hover\:bg-paper\/60:hover{background-color:color-mix(in oklab, var(--color-paper) 60%, transparent)}}}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-ink:focus-visible{outline-color:var(--color-ink)}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (width>=48rem){.md\:block{display:block}.md\:h-dvh{height:100dvh}.md\:min-w-0{min-width:0}.md\:grid-cols-\[15rem_minmax\(0\,1fr\)\]{grid-template-columns:15rem minmax(0,1fr)}.md\:flex-col{flex-direction:column}.md\:overflow-y-auto{overflow-y:auto}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (width>=64rem){.lg\:sticky{position:sticky}.lg\:top-20{top:calc(var(--spacing) * 20)}.lg\:max-h-\[calc\(100dvh-5rem\)\]{max-height:calc(100dvh - 5rem)}.lg\:grid-cols-\[minmax\(0\,1fr\)_18rem\]{grid-template-columns:minmax(0,1fr) 18rem}.lg\:overflow-y-auto{overflow-y:auto}.lg\:p-8{padding:calc(var(--spacing) * 8)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}body{font-family:var(--font-sans);color:var(--color-ink);background:var(--color-wall);margin:0}@media (prefers-reduced-motion:reduce){.enter{animation:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}
|