loki-mode 10.9.1 → 10.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.github/actions/issue-to-pr/action.yml +2 -2
  2. package/README.md +9 -3
  3. package/SKILL.md +12 -2
  4. package/VERSION +1 -1
  5. package/autonomy/lib/proof-generator.py +1 -0
  6. package/autonomy/loki +39 -9
  7. package/autonomy/receipt_jwt.py +46 -1
  8. package/autonomy/run.sh +1 -1
  9. package/autonomy/telemetry.sh +1 -1
  10. package/autonomy/trigger-server.py +1 -0
  11. package/bin/loki +12 -1
  12. package/bin/loki-postinstall.js +12 -0
  13. package/dashboard/__init__.py +1 -1
  14. package/docs/CLI-REFERENCE.md +139 -0
  15. package/docs/INSTALLATION.md +1 -1
  16. package/docs/LOKI-10-BUILD-PROMPT.md +2 -2
  17. package/docs/PLANS-INDEX.md +2 -3
  18. package/docs/SLO.md +3 -3
  19. package/docs/alternative-installations.md +1 -1
  20. package/docs/bug-fixes/agent-04-memory-fixes.md +1 -1
  21. package/docs/dev/release-checklist.md +1 -1
  22. package/{.github/workflows/loki-issue-to-pr.yml → docs/examples/loki-issue-to-pr.yml.example} +2 -0
  23. package/docs/{ONE-RUN-AUDIT.md → history/one-run-audit.md} +6 -0
  24. package/docs/{BUG-AUDIT-v6.61.0.md → history/v6.61.0-audit.md} +6 -0
  25. package/docs/{V8-AGENT-SDK-PLAN.md → history/v8-agent-sdk-plan.md} +6 -0
  26. package/docs/{CONFIG-FILE-PLAN.md → history/v8-config-plan.md} +6 -0
  27. package/docs/test-scenarios/enterprise-scenarios.md +1 -1
  28. package/docs/v10/BOARD.md +3 -0
  29. package/docs/v10/CLI-MODERN.md +190 -0
  30. package/docs/v10/DEPS.md +148 -169
  31. package/docs/v10/FAILURE-CLASSES.md +23 -2
  32. package/docs/v10/GUARDS.md +18 -0
  33. package/docs/v10/LEGACY-REMOVAL.md +334 -25
  34. package/docs/v10/PROGRESS.md +15 -0
  35. package/loki-ts/dist/loki.js +1574 -1794
  36. package/mcp/__init__.py +1 -1
  37. package/package.json +3 -2
  38. package/packages/control-plane/dist/server.js +733 -397
  39. package/packages/control-plane/src/db/fixture-cleanup.ts +47 -0
  40. package/packages/control-plane/src/server/app.ts +2 -0
  41. package/packages/control-plane/src/server/integrity.ts +48 -9
  42. package/packages/control-plane/src/server/routes/stats.ts +16 -5
  43. package/packages/control-plane/src/server/runs.ts +16 -4
  44. package/packages/control-plane/ui/dist/assets/index-BinL6h3i.css +1 -0
  45. package/packages/control-plane/ui/dist/assets/index-BrA22FUs.js +152 -0
  46. package/packages/control-plane/ui/dist/index.html +2 -2
  47. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  48. package/schemas/status-result.schema.json +13 -22
  49. package/skills/release-cadence.md +5 -5
  50. package/web-app/dist/assets/{AdminPage-DqMnfZrH.js → AdminPage-BvbxMtZO.js} +1 -1
  51. package/web-app/dist/assets/{Avatar-BEof29du.js → Avatar-Dc7Scg02.js} +1 -1
  52. package/web-app/dist/assets/{Badge-WD53OL5m.js → Badge-BmVuOqbM.js} +1 -1
  53. package/web-app/dist/assets/{Button-Rzk5flhU.js → Button-DvzqhxRU.js} +1 -1
  54. package/web-app/dist/assets/{CockpitPage-Bqz3PCMh.js → CockpitPage-D8AZo7Ha.js} +1 -1
  55. package/web-app/dist/assets/{ComparePage-Byo2NzAZ.js → ComparePage-NmqaTXDz.js} +1 -1
  56. package/web-app/dist/assets/{ErrorBoundary-BuE0kH3W.js → ErrorBoundary-PGl8Kzoq.js} +1 -1
  57. package/web-app/dist/assets/{EvidenceReceiptPanel-CKFHOPBx.js → EvidenceReceiptPanel-2NY4n9gv.js} +1 -1
  58. package/web-app/dist/assets/{GitHubIssuesPanel-Df_kTMqI.js → GitHubIssuesPanel-DBO_LHHM.js} +1 -1
  59. package/web-app/dist/assets/{GitHubPRsPanel-CouvcKrZ.js → GitHubPRsPanel-K7m9uJ1N.js} +1 -1
  60. package/web-app/dist/assets/{HomePage-dEpLzj0x.js → HomePage-qNQzAJxM.js} +2 -2
  61. package/web-app/dist/assets/{LoginPage-6DVjUND5.js → LoginPage-esu3OK65.js} +1 -1
  62. package/web-app/dist/assets/{MagicPage-BDQ9MOfY.js → MagicPage-CCdVvXrA.js} +1 -1
  63. package/web-app/dist/assets/{MetricsPage-CXiJ04G_.js → MetricsPage-bEvugU2t.js} +1 -1
  64. package/web-app/dist/assets/{NotFoundPage-BaXhZvyj.js → NotFoundPage-BlEjQ1wX.js} +1 -1
  65. package/web-app/dist/assets/{ProjectPage-BzE4V7Ti.js → ProjectPage-Bnug_Yzv.js} +3 -3
  66. package/web-app/dist/assets/{ProjectsPage-ipf6TLMX.js → ProjectsPage-M8r1vfpn.js} +1 -1
  67. package/web-app/dist/assets/{SettingsPage-BDxkUKN6.js → SettingsPage-NqAJpB48.js} +1 -1
  68. package/web-app/dist/assets/{ShowcasePage-YQXD72SM.js → ShowcasePage-8FggxL1P.js} +1 -1
  69. package/web-app/dist/assets/{SystemSettingsPage-Dou7zHS0.js → SystemSettingsPage-3Sz3kFOO.js} +1 -1
  70. package/web-app/dist/assets/{TeamsPage-CQarfcYr.js → TeamsPage-nV-6vdWM.js} +1 -1
  71. package/web-app/dist/assets/{TemplatesPage-CPi1R8Xj.js → TemplatesPage-Brbc_VR9.js} +1 -1
  72. package/web-app/dist/assets/{TerminalOutput-WiGhWdYD.js → TerminalOutput-D1yta5ac.js} +1 -1
  73. package/web-app/dist/assets/{activity-DOa7ejQA.js → activity-oEeKuqFe.js} +1 -1
  74. package/web-app/dist/assets/{bell-CX-VAlYN.js → bell-3NJtqBRR.js} +1 -1
  75. package/web-app/dist/assets/{bot-EDXaZmHc.js → bot-CuvJZ6x1.js} +1 -1
  76. package/web-app/dist/assets/{check-CnBdKk3L.js → check-n4289IZw.js} +1 -1
  77. package/web-app/dist/assets/{chevron-left-BJ_ydRQW.js → chevron-left-CmQeio7h.js} +1 -1
  78. package/web-app/dist/assets/{circle-alert-tz8ufhc_.js → circle-alert-CmmVW-Pd.js} +1 -1
  79. package/web-app/dist/assets/{clock-DFBZPSsH.js → clock-QFqk7GlA.js} +1 -1
  80. package/web-app/dist/assets/{cloud-uy3p_5Q7.js → cloud-BNkSokmq.js} +1 -1
  81. package/web-app/dist/assets/{code-xml-3CbYD9YL.js → code-xml-nWeketqj.js} +1 -1
  82. package/web-app/dist/assets/{database-YiNBXc9c.js → database-C83MXTmJ.js} +1 -1
  83. package/web-app/dist/assets/{dollar-sign-D0wBa4Cv.js → dollar-sign-Blg6TO4y.js} +1 -1
  84. package/web-app/dist/assets/{file-code-corner-DDdZVZ5v.js → file-code-corner-Tk11oUzm.js} +1 -1
  85. package/web-app/dist/assets/{file-plus-D09LTBqt.js → file-plus-DF_eyA6I.js} +1 -1
  86. package/web-app/dist/assets/{globe-BirTPwff.js → globe-D5hDqqmn.js} +1 -1
  87. package/web-app/dist/assets/{hammer-DaE5qgnd.js → hammer-C2hm9lzl.js} +1 -1
  88. package/web-app/dist/assets/{index-D8t7KnQf.js → index-pMxOIwXq.js} +3 -3
  89. package/web-app/dist/assets/{layers-DVVrckRY.js → layers-XWQKGXg0.js} +1 -1
  90. package/web-app/dist/assets/{loader-circle-2-nMGmcb.js → loader-circle-BRuvorm2.js} +1 -1
  91. package/web-app/dist/assets/{lock-D0Xj43pC.js → lock-BSGvQf5v.js} +1 -1
  92. package/web-app/dist/assets/{package-BB7S3egm.js → package-3FJYUYgJ.js} +1 -1
  93. package/web-app/dist/assets/{plus-BLhoMrn3.js → plus-CeIx6g9P.js} +1 -1
  94. package/web-app/dist/assets/{refresh-cw-DMc2uPLw.js → refresh-cw-ChutW42g.js} +1 -1
  95. package/web-app/dist/assets/{rotate-ccw-DM3IP9x7.js → rotate-ccw-BdSjUAk-.js} +1 -1
  96. package/web-app/dist/assets/{scroll-text-lH2uW7rB.js → scroll-text-BjFqxpw3.js} +1 -1
  97. package/web-app/dist/assets/{server-BkerZcdR.js → server-C7UEKdol.js} +1 -1
  98. package/web-app/dist/assets/{shield-alert-B8ohcUfu.js → shield-alert-DY8fpEs7.js} +1 -1
  99. package/web-app/dist/assets/{thumbs-up-CTIxLzPD.js → thumbs-up-Cz1san1g.js} +1 -1
  100. package/web-app/dist/assets/{trash-2-C3zIpsH5.js → trash-2-D2MvGYeP.js} +1 -1
  101. package/web-app/dist/assets/{trending-down-BTT7wV4Y.js → trending-down-CxO3OYOB.js} +1 -1
  102. package/web-app/dist/assets/{trending-up-BzHdCDvF.js → trending-up-DCHKPJLe.js} +1 -1
  103. package/web-app/dist/assets/{upload-HxR34-A8.js → upload-CdPJpSHC.js} +1 -1
  104. package/web-app/dist/assets/{usePolling-DDj6Fupy.js → usePolling-DAH6K3ST.js} +1 -1
  105. package/web-app/dist/assets/{user-qH5y9yNy.js → user-Byyt01W4.js} +1 -1
  106. package/web-app/dist/index.html +1 -1
  107. package/docs/BRANCH-LIFECYCLE-PLAN.md +0 -354
  108. package/docs/EXECUTION-COCKPIT-PLAN.md +0 -289
  109. package/packages/control-plane/ui/dist/assets/index-BQbFv65M.css +0 -1
  110. package/packages/control-plane/ui/dist/assets/index-Dbhu244h.js +0 -155
@@ -0,0 +1,47 @@
1
+ // One-time cleanup of fixture runs that earlier test suites leaked into real control DBs (FC-07b).
2
+ // Runs at CP start. A marker audit row makes it run once per DB; every removal is audited first, in the same transaction.
3
+ import { Database } from "bun:sqlite";
4
+ import { realpathSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { sep } from "node:path";
7
+ import { audit } from "./prune.ts";
8
+
9
+ export const FIXTURE_REPO = "acme/widget";
10
+ const DONE = "fixture.cleanup.done";
11
+
12
+ const tempRoots = (): string[] => {
13
+ const roots = new Set<string>(["/tmp", "/private/tmp", "/var/tmp", tmpdir()]);
14
+ for (const r of [...roots]) { try { roots.add(realpathSync(r)); } catch { /* absent */ } }
15
+ return [...roots].map((r) => r.replace(/[\\/]+$/, "")).filter((r) => r.length > 1);
16
+ };
17
+
18
+ const underTemp = (p: string, roots: string[]): boolean => roots.some((r) => p === r || p.startsWith(r + sep) || p.startsWith(r + "/"));
19
+
20
+ /** Removes leaked fixture runs once per DB. Returns the number of runs removed (0 on every later start). */
21
+ export function cleanupLeakedFixtures(sqlite: Database): number {
22
+ let removed = 0;
23
+ sqlite.transaction(() => {
24
+ if (sqlite.query("select 1 x from audit where action = ?").get(DONE)) return;
25
+ const roots = tempRoots();
26
+ const tempSources = new Set(
27
+ (sqlite.query("select source_id, realpath from local_repos").all() as { source_id: string; realpath: string }[])
28
+ .filter((r) => underTemp(r.realpath, roots)).map((r) => r.source_id),
29
+ );
30
+ const all = sqlite.query("select source_id, run_id, origin_repo from runs").all() as { source_id: string; run_id: string; origin_repo: string | null }[];
31
+ const doomed = all.filter((r) => r.origin_repo === FIXTURE_REPO || tempSources.has(r.source_id));
32
+ const touched = new Set<string>();
33
+ for (const k of doomed) {
34
+ const ev = (sqlite.query("select count(*) n from events where source_id = ? and run_id = ?").get(k.source_id, k.run_id) as { n: number }).n;
35
+ audit(sqlite, "fixture.cleanup", "startup", { source_id: k.source_id, run_id: k.run_id, origin_repo: k.origin_repo, events: ev });
36
+ sqlite.query("delete from events where source_id = ? and run_id = ?").run(k.source_id, k.run_id);
37
+ sqlite.query("delete from runs where source_id = ? and run_id = ?").run(k.source_id, k.run_id);
38
+ touched.add(k.source_id);
39
+ removed++;
40
+ }
41
+ for (const id of touched) {
42
+ sqlite.query("delete from sources where id = ? and not exists (select 1 from runs where source_id = ?) and not exists (select 1 from events where source_id = ?)").run(id, id, id);
43
+ }
44
+ audit(sqlite, DONE, "startup", { removed });
45
+ }).immediate();
46
+ return removed;
47
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join, resolve, sep } from "node:path";
3
3
  import { Hono, type Context } from "hono";
4
+ import { cleanupLeakedFixtures } from "../db/fixture-cleanup.ts";
4
5
  import { openDb } from "../db/migrate.ts";
5
6
  import { ingest } from "./ingest.ts";
6
7
  import { defaultAnswerDir, writeAnswer } from "./answer.ts";
@@ -22,6 +23,7 @@ const defaultUiDir = () => [join(import.meta.dir, "../../ui/dist"), join(import.
22
23
  export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: string; token?: string; loopbackOnly?: boolean; repoDir?: string; startBin?: string; spawnImpl?: typeof spawnStart }) {
23
24
  const uiDir = opts.uiDir ?? defaultUiDir();
24
25
  const { db, sqlite } = openDb(opts.dbPath);
26
+ cleanupLeakedFixtures(sqlite);
25
27
  recomputeLegacy(db);
26
28
  let ready = true;
27
29
  const answerDir = opts.answerDir ?? defaultAnswerDir();
@@ -5,6 +5,8 @@
5
5
  // TAMPERED means positive evidence of forgery. A log the CP cannot check (redacted before ingest) is UNVERIFIED, never TAMPERED and never VERIFIED.
6
6
  import { createHash, createPublicKey, verify, type KeyObject } from "node:crypto";
7
7
  import { readFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
8
10
  import type { EventEnvelope } from "../../../../loki-ts/src/engine10/types.ts";
9
11
 
10
12
  export interface RunIntegrity {
@@ -16,10 +18,10 @@ export interface RunIntegrity {
16
18
  sig_checked: boolean;
17
19
  reasons: string[];
18
20
  }
19
- export type KeyResolver = ((kid: string) => KeyObject | undefined) & { configured?: boolean; fingerprint?: string };
21
+ export type KeyResolver = ((kid: string) => KeyObject | undefined) & { configured?: boolean; strict?: boolean; reason?: string; fingerprint?: string };
20
22
  export interface IntegrityOpts {
21
23
  pubkeyFor?: KeyResolver;
22
- /** At least one key is configured: an unknown kid is then TAMPERED, as in the engine's checkLogSeal. Defaults to pubkeyFor.configured. */
24
+ /** Strict key set (LOKI_CP_RECEIPT_PUBKEYS declared): an unknown kid is TAMPERED, as in the engine's checkLogSeal. Defaults to pubkeyFor.strict, else pubkeyFor.configured. */
23
25
  keysConfigured?: boolean;
24
26
  }
25
27
 
@@ -36,14 +38,51 @@ const normVerdict = (v: string | null | undefined): string | null => (typeof v =
36
38
  /** Same derivation as loki-ts/src/engine10/stages/seal.ts kidOf (parity-tested). */
37
39
  export const kidOf = (pub: KeyObject): string => createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${pub.export({ format: "jwk" }).x}"}`).digest("base64url");
38
40
 
39
- /** Public keys from LOKI_CP_RECEIPT_PUBKEYS (colon-separated PEM files, like `loki verify` retired keys), by kid. Unreadable files are skipped. */
40
- export function pubkeysFromEnv(env: Record<string, string | undefined> = process.env): KeyResolver {
41
- const keys = (env["LOKI_CP_RECEIPT_PUBKEYS"] ?? "").split(":").map((f) => f.trim()).filter(Boolean).flatMap((f) => {
42
- try { return [createPublicKey(readFileSync(f))]; } catch { return []; }
43
- });
41
+ type Env = Record<string, string | undefined>;
42
+ const isEd25519 = (k: KeyObject): boolean => k.asymmetricKeyType === "ed25519";
43
+
44
+ /** The local signer's PUBLIC half, found in the order seal.ts loadSigningKey uses: inline LOKI_RECEIPT_SIGNING_KEY, then LOKI_RECEIPT_SIGNING_KEY_FILE,
45
+ * then $HOME/.loki/keys/receipt-ed25519.pem. There is no separate public file on disk, so the public KeyObject is derived in memory (createPublicKey)
46
+ * and only that derived object is kept. Unlike loadSigningKey this never chmods, creates or generates anything, and never retains, logs or returns
47
+ * private material. An absent default-path key means no key and no reason; one that is named but unreadable or not Ed25519 is reported in `reason`. */
48
+ export function localKeyInfo(env: Env = process.env): { key?: KeyObject; reason?: string } {
49
+ const inline = env["LOKI_RECEIPT_SIGNING_KEY"]?.trim();
50
+ const given = env["LOKI_RECEIPT_SIGNING_KEY_FILE"]?.trim();
51
+ try {
52
+ let pub: KeyObject;
53
+ if (inline) pub = createPublicKey(inline);
54
+ else {
55
+ const file = given || join(env["HOME"] || homedir(), ".loki", "keys", "receipt-ed25519.pem");
56
+ try { pub = createPublicKey(readFileSync(file)); } catch (e) {
57
+ if (!given && (e as NodeJS.ErrnoException).code === "ENOENT") return {};
58
+ throw e;
59
+ }
60
+ }
61
+ return isEd25519(pub) ? { key: pub } : { reason: "local signing key is not an Ed25519 key" };
62
+ } catch (e) {
63
+ const code = (e as NodeJS.ErrnoException).code;
64
+ return { reason: `local signing key ${inline ? "(LOKI_RECEIPT_SIGNING_KEY)" : given ? "file" : "default file"} could not be used: ${code ?? "malformed PEM"}` };
65
+ }
66
+ }
67
+ export const localPublicKey = (env: Env = process.env): KeyObject | undefined => localKeyInfo(env).key;
68
+
69
+ const pemList = (raw: string | undefined): KeyObject[] => (raw ?? "").split(":").map((f) => f.trim()).filter(Boolean).flatMap((f) => {
70
+ try { const k = createPublicKey(readFileSync(f)); return isEd25519(k) ? [k] : []; } catch { return []; }
71
+ });
72
+
73
+ /** Verification keys by kid: the local signer's public half (automatic), LOKI_RECEIPT_RETIRED_PUBKEYS (trusted, as `loki verify` honours them) and
74
+ * LOKI_CP_RECEIPT_PUBKEYS (colon-separated Ed25519 public PEM files for remote/team keys). Unreadable or non-Ed25519 files are skipped.
75
+ * `strict` is true only when LOKI_CP_RECEIPT_PUBKEYS names at least one usable key: then an unknown kid is TAMPERED (the operator declared the key set).
76
+ * With only implicit keys an unknown kid (CI, another machine, a rotation) is NOT CHECKED, as `loki verify` treats it. */
77
+ export function pubkeysFromEnv(env: Env = process.env): KeyResolver {
78
+ const info = localKeyInfo(env);
79
+ const explicit = pemList(env["LOKI_CP_RECEIPT_PUBKEYS"]);
80
+ const keys = [...(info.key ? [info.key] : []), ...pemList(env["LOKI_RECEIPT_RETIRED_PUBKEYS"]), ...explicit];
44
81
  const fn: KeyResolver = (kid) => keys.find((k) => kidOf(k) === kid);
45
82
  fn.configured = keys.length > 0;
46
- fn.fingerprint = createHash("sha256").update(keys.map(kidOf).sort().join(",")).digest("hex").slice(0, 16); // identifies the key set a row was judged under
83
+ fn.strict = explicit.length > 0;
84
+ if (info.reason) fn.reason = info.reason;
85
+ fn.fingerprint = createHash("sha256").update(keys.map(kidOf).sort().join(",") + (fn.strict ? "|strict" : "")).digest("hex").slice(0, 16); // key set and mode a row was judged under
47
86
  return fn;
48
87
  }
49
88
 
@@ -63,7 +102,7 @@ export function verifyRunIntegrity(evs: readonly EventEnvelope[], opts: Integrit
63
102
  const sealedAt = evs.map((e, i) => (e.type === "receipt.sealed" ? i : -1)).filter((i) => i >= 0);
64
103
  const finalised = doneAt >= 0 || sealedAt.length > 0;
65
104
  const lines = evs.map(line);
66
- const keysConfigured = opts.keysConfigured ?? opts.pubkeyFor?.configured ?? false;
105
+ const keysConfigured = opts.keysConfigured ?? opts.pubkeyFor?.strict ?? opts.pubkeyFor?.configured ?? false;
67
106
  let sigChecked = false;
68
107
 
69
108
  if (finalised) { // a live run may still be missing a batch; once it claims an end, every seq must be present
@@ -13,12 +13,16 @@ export interface Stats {
13
13
  runs_running: number;
14
14
  by_verdict: Record<string, number>;
15
15
  blocked_waiting: number;
16
- /** Plain VERIFIED (attested, signature-checked) over finished runs that carry a verdict; null when there are none. */
16
+ /** Plain VERIFIED (attested, signature-checked) over the VERIFIABLE finished outcomes (verified, signature-unchecked verified, failed); ALREADY_SATISFIED and other outcomes are not in the denominator. Null when there are none. */
17
17
  verified_rate: number | null;
18
+ /** Finished runs by outcome class: verified (checked), failed (FAILED, PARTIAL, TAMPERED), already_satisfied (either signature state), other (SPEC_CONFLICT, UNVERIFIED, unattested, anything else). Unchecked verified runs are in verified_unchecked. */
19
+ breakdown: { verified: number; failed: number; already_satisfied: number; other: number };
18
20
  /** Finished successes whose signature could not be checked (VERIFIED (signature not checked)); a rate of 0 with these is "not measured", not "failing". */
19
21
  verified_unchecked: number;
20
- /** At least one receipt verification key is configured (LOKI_CP_RECEIPT_PUBKEYS). */
22
+ /** At least one receipt verification key is configured (the local signer key or LOKI_CP_RECEIPT_PUBKEYS). */
21
23
  keys_configured: boolean;
24
+ /** Present when a named local signing key could not be used (unreadable, malformed, not Ed25519); keys_configured then reflects the remaining keys. */
25
+ keys_reason?: string;
22
26
  cost: {
23
27
  /** Sum over runs whose cost is fully measured; null when no run in the window is. */
24
28
  measured_usd: number | null;
@@ -34,7 +38,7 @@ export interface Stats {
34
38
  export function computeStats(db: Db, since: string | null): Stats {
35
39
  const all = db.select().from(runs).all().filter((r) => !since || (r.startedAt !== null && r.startedAt >= since));
36
40
  const by: Record<string, number> = {};
37
- let finished = 0, running = 0, blocked = 0, withVerdict = 0, verified = 0, unchecked = 0;
41
+ let finished = 0, running = 0, blocked = 0, verified = 0, unchecked = 0, failed = 0, satisfied = 0, other = 0;
38
42
  let mUsd = 0, mRuns = 0, pUsd = 0, pRuns = 0;
39
43
  for (const r of all) {
40
44
  if (r.endedAt) finished++; else running++;
@@ -42,7 +46,13 @@ export function computeStats(db: Db, since: string | null): Stats {
42
46
  const verdict = r.verdict ? effectiveVerdict({ verdict: r.verdict, tampered: r.tampered === 1, attested: r.attested === 1, sig_checked: r.sigChecked === 1 }) : null;
43
47
  if (verdict) {
44
48
  by[verdict] = (by[verdict] ?? 0) + 1;
45
- if (r.endedAt) { withVerdict++; if (verdict === "VERIFIED") verified++; else if (verdict === "VERIFIED (signature not checked)") unchecked++; }
49
+ if (r.endedAt) {
50
+ if (verdict === "VERIFIED") verified++;
51
+ else if (verdict === "VERIFIED (signature not checked)") unchecked++;
52
+ else if (verdict === "ALREADY_SATISFIED" || verdict === "ALREADY_SATISFIED (signature not checked)") satisfied++;
53
+ else if (verdict === "FAILED" || verdict === "PARTIAL" || verdict === "TAMPERED") failed++;
54
+ else other++;
55
+ }
46
56
  }
47
57
  if (r.verdict === "SPEC_CONFLICT") blocked++;
48
58
  if (r.costUsd !== null) { mUsd += r.costUsd; mRuns++; }
@@ -58,7 +68,8 @@ export function computeStats(db: Db, since: string | null): Stats {
58
68
  const r6 = (n: number): number => Math.round(n * 1e6) / 1e6;
59
69
  return {
60
70
  since, runs_total: all.length, runs_finished: finished, runs_running: running, by_verdict: by, blocked_waiting: blocked,
61
- verified_rate: withVerdict ? verified / withVerdict : null, verified_unchecked: unchecked, keys_configured: pubkeysFromEnv().configured === true,
71
+ verified_rate: verified + unchecked + failed ? verified / (verified + unchecked + failed) : null,
72
+ breakdown: { verified, failed, already_satisfied: satisfied, other }, verified_unchecked: unchecked, keys_configured: pubkeysFromEnv().configured === true, ...(pubkeysFromEnv().reason ? { keys_reason: pubkeysFromEnv().reason } : {}),
62
73
  cost: {
63
74
  measured_usd: mRuns ? r6(mUsd) : null, measured_runs: mRuns, partial_usd: pRuns ? r6(pUsd) : null, partial_runs: pRuns,
64
75
  label: pRuns ? "partial" : mRuns ? "measured" : "not measured",
@@ -55,9 +55,16 @@ const parseRun = (r: typeof runs.$inferSelect) => ({
55
55
  effective_verdict: effectiveVerdict({ verdict: r.verdict, tampered: r.tampered === 1, attested: r.attested === 1, sig_checked: r.sigChecked === 1 }),
56
56
  status: r.endedAt ? "completed" : "running", elapsed_s: elapsedS(r),
57
57
  });
58
+ /** The task title the intake stage recorded (its `title`, else the first line of `task`), for sidebar rows of runs that have no issue ref. Null when intake has not completed. */
59
+ export function taskTitle(db: Db, sourceId: string, runId: string): string | null {
60
+ const d = db.select({ data: events.data }).from(events).where(and(eq(events.sourceId, sourceId), eq(events.runId, runId), eq(events.type, "stage.completed"), eq(events.stage, "intake"))).orderBy(asc(events.seq)).limit(1).get()?.data as Record<string, unknown> | undefined;
61
+ const raw = str(d?.["title"]) ?? str(d?.["task"]);
62
+ const t = raw?.split("\n")[0]?.trim();
63
+ return t ? (t.length > 140 ? `${t.slice(0, 139)}...` : t) : null;
64
+ }
58
65
  // A run with no run.completed yet is running: add its live fields (stage, files) from the stored events.
59
66
  const withLive = (db: Db, r: typeof runs.$inferSelect) => {
60
- const p = parseRun(r);
67
+ const p = { ...parseRun(r), title: taskTitle(db, r.sourceId, r.runId) };
61
68
  return r.endedAt ? { ...p, current_stage: null, files_touched: [] as string[] } : { ...p, ...liveInfo(loadEvents(db, r.sourceId, r.runId)) };
62
69
  };
63
70
 
@@ -139,13 +146,18 @@ export function runDetail(db: Db, sourceId: string, runId: string) {
139
146
  const r = db.select().from(runs).where(and(eq(runs.sourceId, sourceId), eq(runs.runId, runId))).get();
140
147
  if (!r) return null;
141
148
  const evs = loadEvents(db, sourceId, runId);
142
- const stages: { stage: string; started_at: string | null; ended_at: string | null; status: string }[] = [];
149
+ const stages: { stage: string; started_at: string | null; ended_at: string | null; status: string; reason: string | null }[] = [];
143
150
  for (const e of evs) {
144
151
  if (e.stage === null || !e.type.startsWith("stage.")) continue;
145
152
  let s = stages.find((x) => x.stage === e.stage);
146
- if (!s) stages.push((s = { stage: e.stage, started_at: null, ended_at: null, status: "started" }));
153
+ if (!s) stages.push((s = { stage: e.stage, started_at: null, ended_at: null, status: "started", reason: null }));
147
154
  if (e.type === "stage.started") s.started_at = e.ts;
148
- else { s.ended_at = e.ts; s.status = e.type.slice("stage.".length); }
155
+ else {
156
+ s.ended_at = e.ts; s.status = e.type.slice("stage.".length);
157
+ // The engine records why a stage was skipped or failed (stage.skipped data.reason); nothing is invented when it did not.
158
+ const why = (e.data as Record<string, unknown> | null)?.reason;
159
+ s.reason = typeof why === "string" && why ? why : null;
160
+ }
149
161
  }
150
162
  const f = fold(evs);
151
163
  const done = f.run.completed?.data;
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap";:root,[data-theme=light]{--cp-bg: #F2EEE7;--cp-bg-2: #EBE6DC;--cp-bg-3: #E4DFD4;--cp-card: #FFFEFB;--cp-hover: #EBE6DC;--cp-glass: rgba(255,254,251,.82);--cp-glass-border: rgba(32,21,21,.08);--cp-glass-shadow: 0 4px 24px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);--cp-text: #201515;--cp-text-2: #36342E;--cp-text-muted: #6B675C;--cp-text-inverse: #ffffff;--cp-border: #E2DDD2;--cp-border-light: #ECEAE3;--cp-accent: #553DE9;--cp-accent-hover: #4432c4;--cp-accent-active: #3828a0;--cp-accent-glow: rgba(85,61,233,.15);--cp-accent-muted: rgba(85,61,233,.1);--cp-success: #1f8a52;--cp-warning: #9a6a12;--cp-error: #b23a3a;--cp-info: #2F71E3;--cp-success-fill: #1FC5A8;--cp-success-muted: rgba(31,197,168,.12);--cp-warning-fill: #D4A03C;--cp-warning-muted: rgba(212,160,60,.12);--cp-error-fill: #C45B5B;--cp-error-muted: rgba(196,91,91,.12);--cp-info-muted: rgba(47,113,227,.12);--cp-accent-solid: #553DE9;--cp-accent-ink: #553DE9;--cp-error-ink: #b23a3a;--cp-success-ink: #1a7546;--cp-warning-ink: #895e10;--cp-info-ink: #2963c8;--cp-text-subtle: #5f5b52;--cp-opus: #d97706;--cp-sonnet: #553DE9;--cp-haiku: #1FC5A8;--cp-shadow-sm: 0 1px 2px rgba(32,21,21,.04);--cp-shadow-md: 0 4px 6px rgba(32,21,21,.06);--cp-shadow-lg: 0 10px 15px rgba(32,21,21,.08);--cp-focus: 0 0 0 3px rgba(85,61,233,.25)}[data-theme=dark]{--cp-bg: #0F0B16;--cp-bg-2: #17121F;--cp-bg-3: #211A2C;--cp-card: #17121F;--cp-hover: #211A2C;--cp-glass: rgba(15,11,22,.82);--cp-glass-border: rgba(255,255,255,.08);--cp-glass-shadow: 0 4px 24px rgba(0,0,0,.25), 0 1px 2px rgba(0,0,0,.12);--cp-text: #F6F2EA;--cp-text-2: #CFC9BD;--cp-text-muted: #968F84;--cp-text-inverse: #0F0B16;--cp-border: rgba(255,255,255,.08);--cp-border-light: rgba(255,255,255,.04);--cp-accent: #8b7bf5;--cp-accent-hover: #9c8ff7;--cp-accent-active: #6258D0;--cp-accent-glow: rgba(139,123,245,.2);--cp-accent-muted: rgba(123,107,240,.18);--cp-success: #2ED8B6;--cp-warning: #E8B84A;--cp-error: #E07070;--cp-info: #5A9CF5;--cp-success-fill: #2ED8B6;--cp-success-muted: rgba(46,216,182,.18);--cp-warning-fill: #E8B84A;--cp-warning-muted: rgba(232,184,74,.18);--cp-error-fill: #E07070;--cp-error-muted: rgba(224,112,112,.18);--cp-info-muted: rgba(90,156,245,.18);--cp-success-ink: #2ED8B6;--cp-warning-ink: #E8B84A;--cp-info-ink: #6DA8F6;--cp-accent-solid: #6a55f2;--cp-accent-ink: #9c8ef6;--cp-error-ink: #E58989;--cp-text-subtle: #968F84;--cp-opus: #f59e0b;--cp-sonnet: #8b7bf5;--cp-haiku: #2ED8B6;--cp-shadow-sm: 0 1px 2px rgba(0,0,0,.4);--cp-shadow-md: 0 4px 12px rgba(0,0,0,.5);--cp-shadow-lg: 0 10px 25px rgba(0,0,0,.6);--cp-focus: 0 0 0 3px rgba(123,107,240,.3)}@media(prefers-color-scheme:dark){:root:not([data-theme=light]){--cp-bg: #0F0B16;--cp-bg-2: #17121F;--cp-bg-3: #211A2C;--cp-card: #17121F;--cp-hover: #211A2C;--cp-glass: rgba(15,11,22,.82);--cp-glass-border: rgba(255,255,255,.08);--cp-glass-shadow: 0 4px 24px rgba(0,0,0,.25), 0 1px 2px rgba(0,0,0,.12);--cp-text: #F6F2EA;--cp-text-2: #CFC9BD;--cp-text-muted: #968F84;--cp-text-inverse: #0F0B16;--cp-border: rgba(255,255,255,.08);--cp-border-light: rgba(255,255,255,.04);--cp-accent: #8b7bf5;--cp-accent-hover: #9c8ff7;--cp-accent-active: #6258D0;--cp-accent-glow: rgba(139,123,245,.2);--cp-accent-muted: rgba(123,107,240,.18);--cp-success: #2ED8B6;--cp-warning: #E8B84A;--cp-error: #E07070;--cp-info: #5A9CF5;--cp-success-fill: #2ED8B6;--cp-success-muted: rgba(46,216,182,.18);--cp-warning-fill: #E8B84A;--cp-warning-muted: rgba(232,184,74,.18);--cp-error-fill: #E07070;--cp-error-muted: rgba(224,112,112,.18);--cp-info-muted: rgba(90,156,245,.18);--cp-success-ink: #2ED8B6;--cp-warning-ink: #E8B84A;--cp-info-ink: #6DA8F6;--cp-accent-solid: #6a55f2;--cp-accent-ink: #9c8ef6;--cp-error-ink: #E58989;--cp-text-subtle: #968F84;--cp-opus: #f59e0b;--cp-sonnet: #8b7bf5;--cp-haiku: #2ED8B6;--cp-shadow-sm: 0 1px 2px rgba(0,0,0,.4);--cp-shadow-md: 0 4px 12px rgba(0,0,0,.5);--cp-shadow-lg: 0 10px 25px rgba(0,0,0,.6);--cp-focus: 0 0 0 3px rgba(123,107,240,.3)}}:root{--cp-font-serif: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;--cp-font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;--cp-font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Menlo, monospace;--cp-text-xs: 10px;--cp-text-sm: 11px;--cp-text-base: 12px;--cp-text-md: 13px;--cp-text-lg: 14px;--cp-text-xl: 16px;--cp-text-2xl: 18px;--cp-text-3xl: 24px;--cp-space-xs: 4px;--cp-space-sm: 8px;--cp-space-md: 12px;--cp-space-lg: 16px;--cp-space-xl: 24px;--cp-space-2xl: 32px;--cp-space-3xl: 48px;--cp-radius-sm: 6px;--cp-radius-md: 12px;--cp-radius-lg: 16px;--cp-radius-nav: 9999px;--cp-radius-full: 9999px;--cp-ease: cubic-bezier(.4,0,.2,1);--cp-dur-fast: .1s;--cp-dur: .2s;--cp-dur-slow: .3s;--cp-sidebar-w: 264px;--cp-detail-w: 288px;--cp-rail-w: 48px;--cp-main-pad: 28px 32px;--cp-thread-max: 820px;--cp-term-bg: #0B0B0B;--cp-term-bar: #111111;--cp-term-side: #0D0D0D;--cp-term-line: #1D1D1D;--cp-term-card: #101010;--cp-term-text: #E8E4DA;--cp-term-strong: #F6F2EA;--cp-term-muted: #968F84;--cp-term-dim: #7A716A;--cp-term-purple: #7E74EE;--cp-term-lilac: #C8BEFF;--cp-term-green: #26C39B;--cp-term-amber: #F0B84A;--cp-term-red: #F07A7A;--cp-z-dropdown: 100;--cp-z-sticky: 200;--cp-z-modal: 300;--cp-z-popover: 400;--cp-z-toast: 600}.cp-root{background:var(--cp-bg);color:var(--cp-text);font-family:var(--cp-font-sans);font-size:var(--cp-text-md);line-height:1.5;-webkit-font-smoothing:antialiased;font-feature-settings:"cv02","cv03","cv04","cv11"}@keyframes cp-pulse{0%,to{opacity:1}50%{opacity:.4}}@keyframes cp-spin{to{transform:rotate(360deg)}}@keyframes cp-fade-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.cp-fade-in{animation:cp-fade-in var(--cp-dur) var(--cp-ease)}.cp-pulse{animation:cp-pulse 2s var(--cp-ease) infinite}.cp-spinner{animation:cp-spin .8s linear infinite}@media(prefers-reduced-motion:reduce){.cp-fade-in,.cp-pulse,.cp-spinner{animation:none}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--cp-font-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--cp-font-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.min-h-11{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-full{width:100%}.min-w-0{min-width:0px}.flex-1{flex:1 1 0%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-red-500\/50{border-color:#ef444480}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.bg-amber-500\/15{background-color:#f59e0b26}.bg-emerald-500\/15{background-color:#10b98126}.bg-red-500\/15{background-color:#ef444426}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-600\/20{background-color:#dc262633}.bg-sky-500\/15{background-color:#0ea5e926}.bg-sky-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-sky-700{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-500\/15{background-color:#64748b26}.bg-violet-500\/15{background-color:#8b5cf626}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-5{padding-left:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:var(--cp-font-mono)}.font-sans{font-family:var(--cp-font-sans)}.font-serif{font-family:var(--cp-font-serif)}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-inverse{color:var(--cp-text-inverse)}.text-muted{color:var(--cp-text-muted)}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-violet-700{--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.accent-hover{accent-color:var(--cp-hover)}.accent-muted{accent-color:var(--cp-text-muted)}.shadow-lg{--tw-shadow: var(--cp-shadow-lg);--tw-shadow-colored: var(--cp-shadow-lg);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: var(--cp-shadow-md);--tw-shadow-colored: var(--cp-shadow-md);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: var(--cp-shadow-sm);--tw-shadow-colored: var(--cp-shadow-sm);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-500\/30{--tw-ring-color: rgb(245 158 11 / .3)}.ring-emerald-500\/30{--tw-ring-color: rgb(16 185 129 / .3)}.ring-red-500\/30{--tw-ring-color: rgb(239 68 68 / .3)}.ring-red-600\/50{--tw-ring-color: rgb(220 38 38 / .5)}.ring-sky-500\/30{--tw-ring-color: rgb(14 165 233 / .3)}.ring-slate-500\/30{--tw-ring-color: rgb(100 116 139 / .3)}.ring-violet-500\/30{--tw-ring-color: rgb(139 92 246 / .3)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{margin:0;background:var(--cp-bg);color:var(--cp-text);font-family:var(--cp-font-sans)}.cp-term{background:var(--cp-term-bg);color:var(--cp-term-text);border:1px solid #1A1A1A;border-radius:20px;overflow:hidden;box-shadow:0 40px 80px -40px #0a0a0a80}.cp-term-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 20px;background:var(--cp-term-bar);border-bottom:1px solid var(--cp-term-line);font-family:var(--cp-font-mono);font-size:12px;color:var(--cp-term-muted)}.cp-term-body{display:grid;grid-template-columns:1fr;min-height:320px}@media(min-width:768px){.cp-term-body{grid-template-columns:220px minmax(0,1fr)}}.cp-term-side{padding:16px;background:var(--cp-term-side);border-bottom:1px solid var(--cp-term-line);font-family:var(--cp-font-mono);font-size:12px}@media(min-width:768px){.cp-term-side{border-bottom:0;border-right:1px solid var(--cp-term-line)}}.cp-term-main{padding:20px 24px;min-width:0}.cp-term-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 24px;border-top:1px solid var(--cp-term-line);font-family:var(--cp-font-mono);font-size:11px;color:var(--cp-term-muted)}.cp-tl-row{display:grid;grid-template-columns:64px 96px minmax(0,1fr);gap:4px 12px;padding:8px 0;border-bottom:1px solid var(--cp-term-line);font-size:12px;line-height:1.55}.cp-tl-meta{grid-column:3;display:flex;flex-wrap:wrap;gap:4px 14px;font-family:var(--cp-font-mono);font-size:11px;color:var(--cp-term-muted)}.cp-trunc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.cp-eyebrow{font-family:var(--cp-font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--cp-text-muted)}.cp-display{font-family:var(--cp-font-sans);font-weight:650;letter-spacing:-.025em;line-height:1.12}.cp-pill-btn{border-radius:9999px}.hover\:underline:hover{text-decoration-line:underline}.disabled\:opacity-50:disabled{opacity:.5}.dark\:border-slate-700:is([data-theme=dark] *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-800:is([data-theme=dark] *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:bg-slate-800:is([data-theme=dark] *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-900:is([data-theme=dark] *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:text-amber-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:text-sky-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-violet-300:is([data-theme=dark] *){--tw-text-opacity: 1;color:rgb(196 181 253 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:flex{display:flex}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-end{align-items:flex-end}}@media(min-width:768px){.md\:flex{display:flex}.md\:table-cell{display:table-cell}.md\:hidden{display:none}.md\:min-h-0{min-height:0px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:p-6{padding:1.5rem}.md\:px-3{padding-left:.75rem;padding-right:.75rem}.md\:py-0{padding-top:0;padding-bottom:0}.md\:py-1{padding-top:.25rem;padding-bottom:.25rem}}