From f6f7c330b034f3959ae8f8708bd580317fc90ff3 Mon Sep 17 00:00:00 2001 From: aamir Date: Mon, 31 Aug 2026 23:18:04 -0500 Subject: [PATCH 1/3] fix: remove private customer artifacts --- .github/workflows/ci.yml | 1 - AGENTS.md | 7 + apps/homescreen/app/cedar/cedar.css | 252 -- apps/homescreen/app/cedar/flows/flows.css | 459 ---- apps/homescreen/app/cedar/flows/lib.ts | 284 -- .../app/cedar/flows/orchard/LIVE.md | 35 - .../app/cedar/flows/orchard/engine.ts | 441 ---- .../app/cedar/flows/orchard/live.ts | 91 - .../app/cedar/flows/orchard/page.tsx | 1087 -------- .../app/cedar/flows/orchard/real.ts | 389 --- .../homescreen/app/cedar/flows/orchard/run.ts | 608 ----- .../app/components/OverviewCards.tsx | 4 +- apps/homescreen/app/moodboard/moodboard.css | 2336 ----------------- apps/homescreen/bun.lock | 9 - apps/homescreen/package.json | 3 - .../understudy-train/experiment-event.json | 516 ---- .../schemas/understudy-train/manifest.json | 28 - .../scripts/check-orchard-contract.mjs | 45 - apps/homescreen/scripts/orchard-contract.mjs | 20 - .../scripts/orchard-live-policy.mjs | 26 - .../scripts/orchard-workflow-proxy.mjs | 118 - design-qa.md | 1 - docs/prompt-evolution.md | 8 +- .../raw/ab-claude-sonnet-4-6.json | 12 +- .../raw/ab-gemma-4-31b-it.json | 8 +- .../raw/ab-gemma-4-e2b.json | 4 +- .../raw/ab-glm-5.2.json | 6 +- .../raw/ab-nemotron-3-nano.json | 6 +- scripts/package-smoke.mjs | 57 +- scripts/public-safety.mjs | 176 +- scripts/validate-public-skills.mjs | 39 +- .../examples/forced_likelihood.py | 2 +- src/partner-report.ts | 4 +- src/trace-foundry.ts | 8 +- tests/cli.test.mjs | 8 +- tests/exception-review.test.mjs | 14 +- tests/partner-report.test.mjs | 2 +- tests/prime-benchmark-import.test.mjs | 4 +- tests/public-safety.test.mjs | 138 + 39 files changed, 365 insertions(+), 6891 deletions(-) delete mode 100644 apps/homescreen/app/cedar/cedar.css delete mode 100644 apps/homescreen/app/cedar/flows/flows.css delete mode 100644 apps/homescreen/app/cedar/flows/lib.ts delete mode 100644 apps/homescreen/app/cedar/flows/orchard/LIVE.md delete mode 100644 apps/homescreen/app/cedar/flows/orchard/engine.ts delete mode 100644 apps/homescreen/app/cedar/flows/orchard/live.ts delete mode 100644 apps/homescreen/app/cedar/flows/orchard/page.tsx delete mode 100644 apps/homescreen/app/cedar/flows/orchard/real.ts delete mode 100644 apps/homescreen/app/cedar/flows/orchard/run.ts delete mode 100644 apps/homescreen/app/moodboard/moodboard.css delete mode 100644 apps/homescreen/schemas/understudy-train/experiment-event.json delete mode 100644 apps/homescreen/schemas/understudy-train/manifest.json delete mode 100644 apps/homescreen/scripts/check-orchard-contract.mjs delete mode 100644 apps/homescreen/scripts/orchard-contract.mjs delete mode 100644 apps/homescreen/scripts/orchard-live-policy.mjs delete mode 100644 apps/homescreen/scripts/orchard-workflow-proxy.mjs create mode 100644 tests/public-safety.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66a5da60..29e83cc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,6 @@ jobs: working-directory: apps/homescreen run: | bun install --frozen-lockfile - bun run orchard:contract-check bun run build rigor: diff --git a/AGENTS.md b/AGENTS.md index 51ebd251..6ad0f905 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,13 @@ OSS from the first commit. its data, spend, destination, or production-impact envelope. - No customer data, private traces, secrets, private repo names, or internal incident notes. +- Do not put customer or partner names, aliases, workload identifiers, metrics, + or derived artifacts in tracked paths or content, branch or commit names, pull + request metadata, or review comments. Use clearly synthetic placeholders. +- Before a public release, an authorized maintainer must provide the private, + newline-delimited `UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS` value while running + `npm run skills:validate` and `npm run package:smoke`. Untrusted pull-request + jobs use synthetic matcher tests and do not receive this private value. - Examples must use synthetic data or small public fixtures. - The desktop connects to the production remote-training control plane (`train.understudylabs.com`) as its default. This is no longer experimental: diff --git a/apps/homescreen/app/cedar/cedar.css b/apps/homescreen/app/cedar/cedar.css deleted file mode 100644 index b9703b6d..00000000 --- a/apps/homescreen/app/cedar/cedar.css +++ /dev/null @@ -1,252 +0,0 @@ -/* Cedar storyboard — its own surface, same language. - CAUTION: frames carry real Cedar Pilot aggregates (names, volumes, - costs). Local design exploration only — do not publish this branch. */ - -.cs { - --mb-ease: cubic-bezier(0.2, 0.7, 0.2, 1); - height: 100vh; - overflow-y: auto; - /* focusing a card mounts the panel below — reserve the scrollbar - gutter so the centered column never shifts sideways */ - scrollbar-gutter: stable; - background: #000; - color: #f2f2f0; - padding: 40px 32px 120px; -} -.cs-inner { - max-width: 1060px; - margin: 0 auto; -} -.cs-stage { - position: relative; - height: 480px; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 12px; - overflow: hidden; - background: radial-gradient( - 120% 90% at 50% 0%, - rgba(255, 255, 255, 0.03), - transparent 60% - ); -} -.cs-frame { - position: absolute; - inset: 0; - display: grid; - place-items: center; - animation: settle 300ms var(--mb-ease); -} - -/* frame 01: the org field */ -.cs-wl { - position: absolute; - display: grid; - justify-items: center; - gap: 8px; - transform: translate(-50%, -50%); -} -.cs-body { - position: relative; - display: grid; - place-items: center; -} -.cs-tiles { - display: grid; - gap: 5px; -} -.cs-tiles i { - width: 16px; - height: 16px; - border-radius: 4px; - background: color-mix(in srgb, var(--c) 16%, transparent); - border: 1px solid color-mix(in srgb, var(--c) 55%, transparent); - animation: mb-tile-breathe 6.4s var(--mb-ease) infinite; - animation-delay: calc(var(--i) * 300ms); -} -.cs-wl .mb-caption { - white-space: nowrap; -} -.cs-orbitpos { - position: absolute; - inset: -20px; - pointer-events: none; -} -.cs-orbitspin { - width: 100%; - height: 100%; - animation: mb-orbit 18s linear infinite; -} -.cs-orbit-ring { - position: absolute; - top: 0; - right: 12%; - width: 13px; - height: 13px; - border-radius: 999px; - border: 1.5px solid var(--mb-violet, #a78bfa); - background: transparent; -} - -/* charts */ -.cs-chart-line { - fill: none; - stroke: rgba(242, 242, 240, 0.9); - stroke-width: 1.6; -} -.cs-chart-area { - fill: rgba(255, 255, 255, 0.06); - stroke: none; -} -.cs-chart-grid { - stroke: rgba(255, 255, 255, 0.08); - stroke-width: 1; -} -.cs-milestone { - fill: none; - stroke: #f2b34c; - stroke-width: 1.4; - animation: mb-milestone-pulse 2.4s var(--mb-ease) infinite; -} -.cs-axis { - font-family: var(--font-mono); - font-size: 9px; - letter-spacing: 0.08em; - fill: rgba(242, 242, 240, 0.4); - text-transform: uppercase; -} - -/* frame 04: serving split */ -.cs-splitbar { - display: flex; - width: 720px; - height: 14px; - border-radius: 999px; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.14); -} -.cs-splitbar i { - height: 100%; -} -.cs-split-bodies { - position: relative; - width: 720px; - height: 120px; -} -.cs-split-body { - position: absolute; - bottom: 0; - transform: translateX(-50%); - display: grid; - justify-items: center; - gap: 8px; -} -.cs-ring { - border-radius: 999px; - border: 2px solid var(--c); - background: transparent; - animation: mb-breathe 5.2s var(--mb-ease) infinite; -} - -/* frame 05: the dial */ -.cs-dial { - display: grid; - gap: 18px; - justify-items: center; - width: 640px; -} -.cs-big { - font-family: var(--font-mono); - font-size: 40px; - font-weight: 300; - letter-spacing: 0.04em; -} -.cs-big b { - font-weight: 500; -} -.cs-costbar { - width: 100%; - height: 6px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - overflow: hidden; -} -.cs-costbar i { - display: block; - height: 100%; - background: linear-gradient(90deg, #6ee7a0, #f2b34c 70%, #d97757); - transition: width 300ms var(--mb-ease); -} - -/* proto: blocks rain into their stacks on mount (delay ∝ stack height) */ -@keyframes cp-drop { - from { - translate: 0 -360px; - opacity: 0; - } - to { - translate: 0 0; - opacity: 1; - } -} - -/* workload navigator: each card's mini-month breathes in its own hue */ -@keyframes cs-bar-breathe { - 0%, - 100% { - opacity: 0.5; - } - 50% { - opacity: 0.95; - } -} -.cs-wlcard { - display: grid; - gap: 8px; - text-align: left; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - padding: 12px 14px; - cursor: pointer; - font: inherit; - color: inherit; - transition: border-color 250ms var(--mb-ease), background 250ms var(--mb-ease), - opacity 250ms var(--mb-ease), transform 250ms var(--mb-ease); -} -.cs-wlcard:hover { - background: rgba(255, 255, 255, 0.03); - transform: translateY(-1px); -} -.cs-wlcard .bars { - display: flex; - align-items: flex-end; - gap: 2px; - height: 34px; -} -.cs-wlcard .bars i { - flex: 1; - border-radius: 1.5px; - animation: cs-bar-breathe 4.8s var(--mb-ease) infinite; -} - -/* capture rows are buttons — click opens the trace end to end */ -.cs-tracerow { - background: transparent; - border: none; - font: inherit; - color: inherit; - text-align: left; - cursor: pointer; - transition: background 200ms var(--mb-ease); -} -.cs-tracerow:hover { - background: rgba(255, 255, 255, 0.04); -} - -.cs-controls { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; - padding: 14px 4px; -} diff --git a/apps/homescreen/app/cedar/flows/flows.css b/apps/homescreen/app/cedar/flows/flows.css deleted file mode 100644 index bca8eb47..00000000 --- a/apps/homescreen/app/cedar/flows/flows.css +++ /dev/null @@ -1,459 +0,0 @@ -/* /cedar/flows — shared visual language for the eight explorations. - Black field · mono micro-labels · mint=incumbent · violet=candidate · - green/red=pass/fail · amber=train. One entrance animation (settle). */ - -.fx { - --inc: #9edbd3; - --cand: #a78bfa; - --good: #6ee7a0; - --bad: #e5534b; - --amber: #f2b34c; - --ink: #f2f2f0; - --dim: rgba(242, 242, 240, 0.45); - --dim2: rgba(242, 242, 240, 0.22); - --line: rgba(255, 255, 255, 0.1); - --card: rgba(255, 255, 255, 0.035); - --ease: cubic-bezier(0.2, 0.7, 0.2, 1); - font-family: var(--font-sans, system-ui); - color: var(--ink); -} -.fx * { box-sizing: border-box; } -.fx a { color: inherit; text-decoration: none; } - -@keyframes fx-settle { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } -@keyframes fx-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } -.fx-in { animation: fx-settle 0.5s var(--ease) both; } - -.fx-cap { - font-family: var(--font-mono, monospace); - font-size: 10px; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--dim); -} -.fx-big { font-family: var(--font-mono, monospace); font-size: 44px; font-weight: 500; letter-spacing: -0.02em; line-height: 1; } -.fx-dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; flex: none; } - -.fx-btn { - font-family: var(--font-mono, monospace); - font-size: 11px; - letter-spacing: 0.1em; - text-transform: uppercase; - padding: 9px 18px; - border-radius: 999px; - border: 1px solid var(--line); - background: var(--card); - color: var(--ink); - cursor: pointer; - transition: all 0.15s var(--ease); -} -.fx-btn:hover { border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.07); } -.fx-btn.primary { border-color: var(--cand); color: var(--cand); } -.fx-btn.primary:hover { background: rgba(167, 139, 250, 0.12); } -.fx-btn.amber { border-color: var(--amber); color: var(--amber); } -.fx-btn.amber:hover { background: rgba(242, 179, 76, 0.1); } -.fx-btn.ghost { border-color: transparent; color: var(--dim); } - -.fx-chip { - display: inline-flex; - align-items: center; - gap: 6px; - font-family: var(--font-mono, monospace); - font-size: 10px; - letter-spacing: 0.08em; - padding: 3px 9px; - border-radius: 999px; - border: 1px solid var(--line); - color: var(--dim); -} - -/* top-of-page breadcrumb shared by all directions */ -.fx-nav { display: flex; align-items: baseline; gap: 14px; margin-bottom: 34px; } -.fx-nav .spacer { flex: 1; } - -/* ---------- index (/cedar/flows) ---------- */ -.ix-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px; margin-top: 26px; } -.ix-tile { - display: block; - border: 1px solid var(--line); - border-radius: 12px; - background: var(--card); - padding: 16px; - transition: border-color 0.15s var(--ease), transform 0.15s var(--ease); -} -.ix-tile:hover { border-color: rgba(255, 255, 255, 0.28); transform: translateY(-2px); } -.ix-thumb { height: 96px; border-radius: 8px; background: rgba(0, 0, 0, 0.5); border: 1px solid var(--line); margin-bottom: 12px; display: grid; place-items: center; overflow: hidden; position: relative; } -.ix-name { font-family: var(--font-mono, monospace); font-size: 13px; letter-spacing: 0.06em; margin-bottom: 4px; } -.ix-sub { font-size: 12px; color: var(--dim); line-height: 1.45; } - -/* thumb sketches */ -.th-duel { display: flex; gap: 8px; } -.th-duel i { width: 44px; height: 56px; border-radius: 6px; border: 1px solid; } -.th-chat { display: flex; flex-direction: column; gap: 6px; width: 70%; } -.th-chat i { height: 7px; border-radius: 4px; background: var(--dim2); } -.th-waffle { display: grid; grid-template-columns: repeat(6, 13px); gap: 3px; } -.th-waffle i { width: 13px; height: 13px; border-radius: 2px; } -.th-race { display: flex; flex-direction: column; gap: 10px; width: 72%; } -.th-race i { height: 5px; border-radius: 3px; position: relative; background: var(--dim2); overflow: hidden; } -.th-race i::after { content: ""; position: absolute; inset: 0; width: var(--w, 60%); border-radius: 3px; background: var(--c); } -.th-rungs { display: flex; flex-direction: column; gap: 8px; align-items: center; } -.th-rungs i { width: 9px; height: 9px; border-radius: 999px; } -.th-dial { width: 64px; height: 64px; border-radius: 999px; background: conic-gradient(var(--cand) 0 24%, var(--inc) 24% 100%); position: relative; } -.th-dial::after { content: ""; position: absolute; inset: 12px; border-radius: 999px; background: #0a0a0a; } -.th-card { width: 78px; height: 56px; border: 1px solid var(--line); border-radius: 8px; display: grid; place-items: center; } -.th-card i { width: 54px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, var(--inc) 0 48%, var(--cand) 48% 100%); } -.th-swipe { position: relative; width: 60px; height: 60px; } -.th-swipe i { position: absolute; inset: 0; border: 1px solid var(--line); border-radius: 8px; background: #101013; } -.th-swipe i:nth-child(1) { transform: rotate(-8deg); } -.th-swipe i:nth-child(2) { transform: rotate(4deg); } -.th-swipe i:nth-child(3) { border-color: var(--cand); } - -/* ---------- duel ---------- */ -.dl-strip { display: flex; gap: 3px; flex-wrap: wrap; margin: 18px 0 30px; } -.dl-strip i { width: 10px; height: 10px; border-radius: 3px; background: var(--dim2); opacity: 0.5; transition: all 0.25s var(--ease); } -.dl-strip i.cur { outline: 1px solid var(--ink); outline-offset: 1px; opacity: 1; } -.dl-arena { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 22px; } -.dl-card { - border: 1px solid var(--line); - border-radius: 14px; - background: var(--card); - padding: 22px; - min-height: 210px; - cursor: pointer; - transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.15s var(--ease); - position: relative; -} -.dl-card:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.3); } -.dl-card.revealed { cursor: default; } -.dl-card.revealed:hover { transform: none; } -.dl-card.picked { box-shadow: 0 0 0 2px var(--rc, var(--ink)); } -.dl-ans { font-size: 20px; font-weight: 550; margin: 14px 0 10px; font-family: var(--font-mono, monospace); } -.dl-why { font-size: 13px; color: var(--dim); line-height: 1.55; } -.dl-name { position: absolute; top: 16px; right: 16px; } -.dl-verdict { text-align: center; margin-top: 26px; min-height: 44px; } -.dl-final { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.88); display: grid; place-items: center; z-index: 40; } -.dl-final > div { text-align: center; } - -/* ---------- chat ---------- */ -.ch-wrap { max-width: 620px; margin: 0 auto; display: flex; flex-direction: column; gap: 26px; padding-bottom: 40px; } -.ch-msg { animation: fx-settle 0.45s var(--ease) both; } -.ch-msg .fx-cap { margin-bottom: 7px; display: block; } -.ch-msg.you { text-align: right; } -.ch-msg.you .body { display: inline-block; background: rgba(255, 255, 255, 0.07); border-radius: 12px; padding: 10px 15px; } -.ch-body { font-size: 14.5px; line-height: 1.55; } -.ch-chips { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; } -.ch-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; } -.ch-pair > button { - text-align: left; - border: 1px solid var(--line); - border-radius: 10px; - background: var(--card); - color: var(--ink); - padding: 14px; - cursor: pointer; - font-size: 13px; - line-height: 1.5; - transition: border-color 0.15s var(--ease); -} -.ch-pair > button:hover { border-color: rgba(255, 255, 255, 0.32); } -.ch-pair > button:disabled { cursor: default; } -.ch-bar { height: 10px; border-radius: 5px; background: var(--dim2); overflow: hidden; display: flex; margin-top: 10px; } -.ch-bar i { height: 100%; transition: width 0.3s linear; } -.ch-dotload { display: inline-flex; gap: 4px; } -.ch-dotload i { width: 5px; height: 5px; border-radius: 999px; background: var(--dim); animation: fx-pulse 1.1s infinite; } -.ch-dotload i:nth-child(2) { animation-delay: 0.18s; } -.ch-dotload i:nth-child(3) { animation-delay: 0.36s; } - -/* ---------- waffle ---------- */ -.wf-head { display: flex; align-items: flex-end; gap: 34px; margin-bottom: 26px; } -.wf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(46px, 1fr)); gap: 7px; } -.wf-cell { - aspect-ratio: 1; - border-radius: 7px; - cursor: pointer; - border: 1px solid transparent; - transition: transform 0.15s var(--ease), border-color 0.15s var(--ease); - position: relative; -} -.wf-cell:hover { transform: scale(1.08); } -.wf-cell.sel { border-color: var(--ink); } -.wf-cell.hot::after { content: ""; position: absolute; inset: -3px; border-radius: 9px; border: 1px solid var(--amber); animation: fx-pulse 2.2s infinite; } -.wf-panel { border: 1px solid var(--line); border-radius: 14px; background: #0b0b0e; padding: 22px; margin-top: 24px; animation: fx-settle 0.35s var(--ease) both; } -.wf-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 14px; } -.wf-axes { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 14px; } -.wf-ax { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--dim); } - -/* ---------- race ---------- */ -.rc-lane { margin-bottom: 34px; } -.rc-track { display: flex; gap: 4px; margin-top: 10px; } -.rc-track i { flex: 1; height: 26px; border-radius: 4px; background: var(--dim2); opacity: 0.35; transition: all 0.2s var(--ease); } -.rc-track i.on { opacity: 1; } -.rc-head { display: flex; align-items: baseline; gap: 12px; } -.rc-pct { font-family: var(--font-mono, monospace); font-size: 26px; margin-left: auto; } -.rc-center { text-align: center; margin: 40px 0; min-height: 80px; } - -/* ---------- rungs ---------- */ -.rg-row { display: grid; grid-template-columns: 220px 1fr auto; gap: 22px; align-items: center; padding: 18px 0; border-bottom: 1px solid var(--line); } -.rg-rail { display: flex; align-items: center; gap: 0; } -.rg-node { width: 11px; height: 11px; border-radius: 999px; background: var(--dim2); flex: none; position: relative; } -.rg-node.done { background: var(--ink); } -.rg-node.cur { background: var(--cand); animation: fx-pulse 1.6s infinite; box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.15); } -.rg-node.ship { background: var(--good); } -.rg-node.train { background: var(--amber); } -.rg-link { height: 1px; flex: 1; background: var(--line); min-width: 26px; } -.rg-link.done { background: rgba(255, 255, 255, 0.4); } -.rg-legend { display: flex; gap: 26px; margin: 8px 0 22px; } -.rg-detail { grid-column: 1 / -1; padding: 6px 0 14px; animation: fx-settle 0.3s var(--ease) both; } -.rg-bar { height: 8px; border-radius: 4px; background: var(--dim2); overflow: hidden; margin-top: 5px; } -.rg-bar i { display: block; height: 100%; border-radius: 4px; } - -/* ---------- dial ---------- */ -.di-stage { display: grid; grid-template-columns: 320px 1fr; gap: 46px; align-items: center; margin-top: 30px; } -.di-donut { position: relative; width: 300px; height: 300px; } -.di-donut svg { transform: rotate(-90deg); } -.di-center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; } -.di-slider { width: 100%; accent-color: var(--cand); } -.di-mini { display: flex; gap: 3px; margin-top: 14px; } -.di-mini i { width: 9px; height: 18px; border-radius: 2px; } -.di-locked { opacity: 0.35; filter: saturate(0.2); pointer-events: none; } - -/* ---------- card ---------- */ -.cd-deck { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; margin-top: 26px; } -.cd-scene { perspective: 1200px; height: 210px; } -.cd-card { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.55s var(--ease); cursor: pointer; } -.cd-card.flipped { transform: rotateY(180deg); } -.cd-face { position: absolute; inset: 0; backface-visibility: hidden; border: 1px solid var(--line); border-radius: 14px; background: var(--card); padding: 18px; display: flex; flex-direction: column; } -.cd-face.back { transform: rotateY(180deg); background: #0c0c10; } -.cd-split { height: 14px; border-radius: 7px; overflow: hidden; display: flex; margin: 14px 0 8px; } -.cd-split i { height: 100%; } -.cd-corners { display: flex; justify-content: space-between; } -.cd-delta { font-family: var(--font-mono, monospace); font-size: 30px; margin-top: auto; } -.cd-task { font-size: 12px; color: var(--dim); display: flex; gap: 8px; align-items: center; padding: 3px 0; } - -/* ---------- swipe ---------- */ -.sw-stage { max-width: 520px; margin: 30px auto 0; position: relative; height: 400px; } -.sw-card { - position: absolute; inset: 0; - border: 1px solid var(--line); border-radius: 16px; background: #0d0d11; - padding: 26px; display: flex; flex-direction: column; gap: 14px; - transition: transform 0.4s var(--ease), opacity 0.4s var(--ease); -} -.sw-card.under1 { transform: scale(0.96) translateY(12px); opacity: 0.6; } -.sw-card.under2 { transform: scale(0.92) translateY(24px); opacity: 0.3; } -.sw-card.exit-l { transform: translateX(-120%) rotate(-6deg); opacity: 0; } -.sw-card.exit-r { transform: translateX(120%) rotate(6deg); opacity: 0; } -.sw-verdict { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono, monospace); font-size: 12px; letter-spacing: 0.08em; padding: 5px 12px; border-radius: 999px; width: fit-content; } -.sw-actions { display: flex; justify-content: center; gap: 14px; margin-top: 24px; } -.sw-tally { display: flex; justify-content: center; gap: 4px; margin-top: 18px; } -.sw-tally i { width: 12px; height: 6px; border-radius: 3px; background: var(--dim2); } - -@media (max-width: 760px) { - .dl-arena, .ch-pair, .wf-cols { grid-template-columns: 1fr; } - .di-stage { grid-template-columns: 1fr; } - .rg-row { grid-template-columns: 1fr; } -} - -/* ---------- gradient ---------- */ -.gr-row { display: grid; grid-template-columns: 150px 1fr 60px; gap: 16px; align-items: center; padding: 10px 0; } -.gr-div { position: relative; height: 22px; } -.gr-div::before { content: ""; position: absolute; left: 50%; top: -5px; bottom: -5px; width: 1px; background: rgba(255, 255, 255, 0.35); } -.gr-div i { position: absolute; top: 5px; height: 12px; border-radius: 2px; transition: width 0.5s var(--ease); } -.gr-delta { font-family: var(--font-mono, monospace); font-size: 13px; text-align: right; } -.gr-sect { margin-top: 38px; } -.gr-aim { display: inline-flex; align-items: center; gap: 8px; margin-top: 6px; } - -/* ---------- gepa ---------- */ -.gp-plot { position: relative; margin-top: 26px; } -.gp-note { font-family: var(--font-mono, monospace); font-size: 10.5px; color: var(--dim); letter-spacing: 0.04em; line-height: 1.5; } -.gp-gen { cursor: pointer; } -.gp-fail { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--dim); padding: 3px 0; font-family: var(--font-mono, monospace); } - -/* ---------- meter ---------- */ -.mt-bigs { display: flex; gap: 54px; margin: 26px 0 8px; flex-wrap: wrap; } -.mt-cells { display: flex; gap: 3px; flex-wrap: wrap; margin-top: 18px; } -.mt-cells i { width: 14px; height: 22px; border-radius: 3px; transition: all 0.3s var(--ease); } -.mt-panel { border: 1px solid var(--line); border-radius: 14px; background: var(--card); padding: 22px; margin-top: 26px; } -.mt-save { font-family: var(--font-mono, monospace); font-size: 38px; color: var(--good); } - -/* ---------- bench ---------- */ -.bn-rails { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 24px; align-items: start; } -.bn-rail > .fx-cap { display: block; margin-bottom: 12px; } -.bn-card { border: 1px solid var(--line); border-left: 3px solid var(--bc, var(--line)); border-radius: 10px; background: var(--card); padding: 14px; margin-bottom: 10px; animation: fx-settle 0.4s var(--ease) both; } -.bn-hypo { font-size: 13px; line-height: 1.5; } -.bn-meta { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; } -.bn-lineage { font-family: var(--font-mono, monospace); font-size: 10px; color: var(--dim2); letter-spacing: 0.06em; } -.bn-ghost { border-style: dashed; background: none; color: var(--dim2); text-align: center; padding: 18px; cursor: pointer; font-family: var(--font-mono, monospace); font-size: 12px; border-radius: 10px; border: 1px dashed var(--line); } -.bn-ghost:hover { color: var(--dim); border-color: rgba(255, 255, 255, 0.25); } -.bn-mini { height: 6px; border-radius: 3px; background: var(--dim2); overflow: hidden; margin-top: 8px; } -.bn-mini i { display: block; height: 100%; background: var(--cand); animation: fx-pulse 2s infinite; } - -/* ---------- tree ---------- */ -.tt-stage { position: relative; margin-top: 20px; } -.tt-node { position: absolute; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; gap: 7px; cursor: pointer; width: 120px; text-align: center; } -.tt-orb { width: 16px; height: 16px; border-radius: 999px; background: var(--dim2); transition: all 0.2s var(--ease); } -.tt-node.done .tt-orb { background: #f2f2f0; } -.tt-node.breakthrough .tt-orb { background: var(--cand); box-shadow: 0 0 18px rgba(167, 139, 250, 0.7); } -.tt-node.dead .tt-orb { background: transparent; border: 1.5px solid var(--bad); opacity: 0.65; } -.tt-node.next .tt-orb { background: var(--cand); animation: fx-pulse 1.6s infinite; box-shadow: 0 0 0 5px rgba(167, 139, 250, 0.15); } -.tt-node.locked { opacity: 0.35; } -.tt-node.sel .tt-orb { outline: 2px solid #f2f2f0; outline-offset: 3px; } -.tt-name { font-family: var(--font-mono, monospace); font-size: 10px; letter-spacing: 0.08em; color: var(--dim); text-transform: uppercase; } -.tt-gain { font-family: var(--font-mono, monospace); font-size: 10px; } -.tt-detail { border: 1px solid var(--line); border-radius: 12px; background: #0c0c10; padding: 18px 22px; margin-top: 18px; animation: fx-settle 0.3s var(--ease) both; display: flex; gap: 30px; align-items: baseline; flex-wrap: wrap; } - -/* index thumbs for 09-13 */ -.th-grad { display: flex; flex-direction: column; gap: 6px; width: 70%; } -.th-grad > i { position: relative; height: 8px; } -.th-grad > i::before { content: ""; position: absolute; left: 50%; top: -2px; bottom: -2px; width: 1px; background: rgba(255,255,255,.3); } -.th-grad > i::after { content: ""; position: absolute; top: 0; height: 8px; border-radius: 4px; left: 50%; width: var(--w); background: var(--c); } -.th-grad > i.neg::after { left: auto; right: 50%; } -.th-gepa { position: relative; width: 80%; height: 60%; } -.th-gepa i { position: absolute; width: 7px; height: 7px; border-radius: 999px; background: var(--cand); } -.th-meter { display: flex; align-items: flex-end; gap: 10px; height: 60%; } -.th-meter i { width: 20px; border-radius: 4px 4px 0 0; } -.th-bench { display: flex; gap: 6px; } -.th-bench i { width: 22px; height: 52px; border-radius: 4px; border: 1px solid var(--line); border-top: 3px solid var(--c); } -.th-tree { position: relative; width: 80%; height: 64%; } -.th-tree i { position: absolute; width: 8px; height: 8px; border-radius: 999px; } - -@media (max-width: 900px) { .bn-rails { grid-template-columns: 1fr; } } - -/* ---------- gepa v2 (gv-) — signature · reflection · frontier · merge ---------- */ -.gv-loop { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 22px 0 6px; } -.gv-stage { font-family: var(--font-mono, monospace); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line); color: var(--dim2); transition: all 0.25s var(--ease); } -.gv-stage.on { color: var(--cand); border-color: var(--cand); background: rgba(167, 139, 250, 0.08); } -.gv-arrow { color: var(--dim2); font-size: 11px; } -.gv-sig { border: 1px solid var(--line); border-radius: 12px; background: var(--card); padding: 16px 18px; margin-top: 20px; } -.gv-sig-text { font-family: var(--font-mono, monospace); font-size: 12px; line-height: 1.7; color: var(--ink); } -.gv-sig-text .add { color: var(--good); } -.gv-field { display: inline-flex; font-family: var(--font-mono, monospace); font-size: 10px; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--line); color: var(--dim); margin: 2px 4px 2px 0; } -.gv-grid { display: grid; grid-template-columns: 380px 1fr; gap: 26px; margin-top: 24px; align-items: start; } -.gv-tree { position: relative; } -.gv-edge { fill: none; stroke: rgba(255, 255, 255, 0.22); stroke-width: 1; transition: opacity 0.6s var(--ease); } -.gv-edge.merge { stroke: var(--amber); stroke-opacity: 0.6; stroke-dasharray: 3 3; } -.gv-node-dot { transition: all 0.4s var(--ease); } -.gv-node-score { font-family: var(--font-mono, monospace); font-size: 9.5px; fill: rgba(242, 242, 240, 0.5); text-anchor: middle; } -.gv-node-idx { font-family: var(--font-mono, monospace); font-size: 9px; fill: #0a0a0a; text-anchor: middle; font-weight: 600; } -.gv-detail { border: 1px solid var(--line); border-radius: 12px; background: #0c0c10; padding: 18px; min-height: 240px; animation: fx-settle 0.3s var(--ease) both; } -.gv-fb { border-left: 3px solid var(--bad); padding: 6px 12px; margin: 8px 0; font-size: 12px; color: var(--dim); line-height: 1.5; background: rgba(229, 83, 75, 0.05); border-radius: 0 6px 6px 0; } -.gv-front { display: flex; height: 16px; border-radius: 8px; overflow: hidden; margin-top: 10px; background: rgba(242, 242, 240, 0.08); } -.gv-front i { height: 100%; transition: width 0.5s var(--ease); } -.gv-ghost { opacity: 0.35; } -@media (max-width: 860px) { .gv-grid { grid-template-columns: 1fr; } } - -/* gepa v2 smoothing — render everything, reveal with transitions. - principle: nothing pops or reflows; opacity and size do the story. */ -.gv-reveal { opacity: 0; transition: opacity 0.9s var(--ease); } -.gv-reveal.on { opacity: 1; } -.gv-reveal.on.gv-ghost { opacity: 0.35; } -.gv-rule { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 0.7s var(--ease), opacity 0.7s var(--ease); } -.gv-rule.on { grid-template-rows: 1fr; opacity: 1; } -.gv-rule > span { overflow: hidden; min-height: 0; } -@keyframes gv-fade { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } } -.gv-detail-inner { animation: gv-fade 0.55s var(--ease) both; } - -/* ---------- generator (gn-) — traces → signature → scenarios ---------- */ -.gn-stage { opacity: 0; transform: translateY(6px); transition: opacity 0.9s var(--ease), transform 0.9s var(--ease); } -.gn-stage.on { opacity: 1; transform: none; } -.gn-traces { border: 1px solid var(--line); border-radius: 12px; background: var(--card); overflow: hidden; } -.gn-tracerow { display: flex; gap: 14px; align-items: baseline; padding: 9px 16px; border-bottom: 1px solid var(--line); font-family: var(--font-mono, monospace); font-size: 11.5px; color: var(--dim); opacity: 0; transition: opacity 0.7s var(--ease), background 0.3s var(--ease); } -.gn-tracerow.on { opacity: 1; } -.gn-tracerow.open { background: rgba(255, 255, 255, 0.04); color: var(--ink); } -.gn-tracerow:last-child { border-bottom: none; } -.gn-x { border: 1px solid var(--line); border-radius: 12px; background: #0b0b0e; padding: 18px; display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 0.8s var(--ease), opacity 0.8s var(--ease), margin 0.8s var(--ease); margin-top: 0; } -.gn-x.on { grid-template-rows: 1fr; opacity: 1; margin-top: 12px; } -.gn-x > div { overflow: hidden; min-height: 0; } -.gn-block { border-radius: 8px; padding: 10px 14px; font-family: var(--font-mono, monospace); font-size: 11.5px; line-height: 1.65; margin-bottom: 8px; transition: opacity 0.8s var(--ease), border-color 0.8s var(--ease); border: 1px solid transparent; } -.gn-block.const { color: var(--dim2); background: rgba(255, 255, 255, 0.025); } -.gn-block.vary { color: var(--ink); border-color: var(--cand); background: rgba(167, 139, 250, 0.05); } -.gn-block.gold { color: var(--ink); border-color: var(--inc); background: rgba(158, 219, 211, 0.05); } -.gn-sig-src span { transition: color 0.8s var(--ease); } -.gn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; } -.gn-card { border: 1px solid var(--line); border-radius: 12px; background: var(--card); padding: 14px; opacity: 0; transform: translateY(6px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); } -.gn-card.on { opacity: 1; transform: none; } -.gn-card.gen { border-style: dashed; border-color: rgba(167, 139, 250, 0.5); } -.gn-q { font-family: var(--font-mono, monospace); font-size: 12px; line-height: 1.55; min-height: 38px; } -.gn-a { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono, monospace); font-size: 10.5px; padding: 3px 10px; border-radius: 999px; margin-top: 10px; } - -/* ---------- atlas (at-) — clusters → understanding → mindmap ---------- */ -.at-row { display: flex; align-items: center; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--line); } -.at-name { width: 190px; font-family: var(--font-mono, monospace); font-size: 12px; } -.at-bubbles { display: flex; align-items: center; gap: 8px; flex: 1; } -.at-bub { border-radius: 999px; background: rgba(242, 242, 240, 0.18); cursor: pointer; transition: all 0.3s var(--ease); flex: none; } -.at-bub:hover { background: rgba(242, 242, 240, 0.35); } -.at-bub.sel { background: var(--cand); box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.15); } -.at-panel { border: 1px solid var(--line); border-radius: 14px; background: #0b0b0e; padding: 22px; margin-top: 22px; } -.at-stats { display: flex; gap: 40px; flex-wrap: wrap; margin-top: 14px; } -.at-stat b { display: block; font-family: var(--font-mono, monospace); font-size: 22px; font-weight: 500; } -.at-kw { display: inline-flex; font-family: var(--font-mono, monospace); font-size: 10.5px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--line); color: var(--dim); margin: 3px 6px 0 0; } -.at-map-node { opacity: 0; transition: opacity 0.8s var(--ease); } -.at-map-node.on { opacity: 1; } -.at-map-label { font-family: var(--font-mono, monospace); font-size: 9.5px; } - -/* ---------- scaffold (sc-) — the blank environment populates ---------- */ -.sc-env { border: 1px dashed rgba(255, 255, 255, 0.25); border-radius: 16px; padding: 20px; transition: border-color 0.9s var(--ease), box-shadow 0.9s var(--ease); } -.sc-env.complete { border-style: solid; border-color: var(--cand); box-shadow: 0 0 30px rgba(167, 139, 250, 0.12); } -.sc-grid { display: grid; grid-template-columns: 1fr 2fr; gap: 14px; } -.sc-stack { display: flex; flex-direction: column; gap: 14px; } -.sc-stack .sc-socket { min-height: 0; flex: 1; } -.sc-socket { border: 1px dashed var(--line); border-radius: 12px; padding: 14px; min-height: 250px; transition: border-color 0.8s var(--ease), background 0.8s var(--ease); display: flex; flex-direction: column; } -.sc-socket.filled { border-style: solid; background: rgba(255, 255, 255, 0.02); } -.sc-socket > .fx-cap { margin-bottom: 10px; } -.sc-tool { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--font-mono, monospace); font-size: 11px; padding: 5px 0; opacity: 0; transform: translateY(4px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); } -.sc-tool.on { opacity: 1; transform: none; } -.sc-tool b { font-weight: 400; color: #67e8f9; } -.sc-tool span { color: var(--dim2); } -.sc-job { font-size: 11.5px; line-height: 1.45; color: var(--dim); padding: 6px 0; border-bottom: 1px solid var(--line); opacity: 0; transform: translateY(4px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); } -.sc-job.on { opacity: 1; transform: none; } -.sc-job:last-of-type { border-bottom: none; } -.sc-rubrow { display: flex; align-items: center; gap: 8px; font-size: 11.5px; padding: 6px 0; opacity: 0; transition: opacity 0.6s var(--ease); } -.sc-rubrow.on { opacity: 1; } -.sc-rubrow .w { font-family: var(--font-mono, monospace); color: var(--dim2); width: 30px; } -.sc-vote { width: 22px; height: 22px; border-radius: 999px; border: 1px solid var(--line); background: none; color: var(--dim2); cursor: pointer; font-size: 11px; line-height: 1; transition: all 0.2s var(--ease); } -.sc-vote.yes { border-color: var(--good); color: var(--good); } -.sc-vote.no { border-color: var(--bad); color: var(--bad); } -.sc-replay { height: 10px; border-radius: 5px; background: rgba(242, 242, 240, 0.08); overflow: hidden; margin-top: 16px; } -.sc-replay i { display: block; height: 100%; background: var(--inc); width: 0%; transition: width 2.2s var(--ease); } -.sc-scan { font-family: var(--font-mono, monospace); font-size: 11px; color: var(--dim); } -@media (max-width: 820px) { .sc-grid { grid-template-columns: 1fr; } .sc-socket { min-height: 0; } } - -/* scaffold data socket: prompt families + jobs, fullscreen viewer */ -.sc-item { display: flex; gap: 8px; align-items: baseline; font-size: 11px; line-height: 1.45; color: var(--dim); padding: 5px 0; border-bottom: 1px solid var(--line); cursor: pointer; opacity: 0; transform: translateY(4px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease), color 0.2s var(--ease); } -.sc-item.on { opacity: 1; transform: none; } -.sc-item:hover { color: var(--ink); } -.sc-item:last-of-type { border-bottom: none; } -.sc-item .n { font-family: var(--font-mono, monospace); color: var(--dim2); flex: none; } -.sc-full { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.92); z-index: 60; display: flex; flex-direction: column; padding: 40px min(8vw, 90px); animation: gv-fade 0.35s var(--ease) both; } -.sc-full pre { flex: 1; overflow-y: auto; white-space: pre-wrap; font-family: var(--font-mono, monospace); font-size: 12px; line-height: 1.7; color: var(--ink); background: none; margin: 16px 0 0; } - -/* prompt→rubric analysis in the fullscreen viewer */ -.sc-full-body { flex: 1; display: grid; grid-template-columns: 1fr; gap: 26px; min-height: 0; margin-top: 16px; } -.sc-full-body.with-rules { grid-template-columns: 1fr 300px; } -.sc-full-body pre { margin: 0; } -.sc-rules { overflow-y: auto; border-left: 1px solid var(--line); padding-left: 20px; } -.sc-rule { display: flex; gap: 10px; align-items: baseline; padding: 9px 0; cursor: default; opacity: 0; animation: gv-fade 0.5s var(--ease) both; } -.sc-rule .w { font-family: var(--font-mono, monospace); font-size: 10px; color: var(--dim2); width: 26px; flex: none; } -.sc-rule .t { font-size: 12.5px; line-height: 1.5; } -.sc-mark { border-radius: 3px; padding: 0 2px; color: inherit; transition: background 0.25s var(--ease); } - -/* rubric adder + focus mode in the viewer */ -.sc-add { background: none; border: 1px dashed var(--line); border-radius: 8px; color: var(--dim); font-family: var(--font-mono, monospace); font-size: 11px; padding: 6px 10px; margin-top: 8px; outline: none; transition: border-color 0.2s var(--ease); } -.sc-add:focus { border-color: var(--cand); color: var(--ink); } -.sc-add::placeholder { color: var(--dim2); } -.sc-dim { opacity: 0.26; transition: opacity 0.6s var(--ease); } -.sc-gap { display: flex; align-items: center; gap: 14px; width: 100%; margin: 10px 0; background: none; border: none; cursor: pointer; color: var(--dim2); font-family: var(--font-mono, monospace); font-size: 10.5px; letter-spacing: 0.08em; animation: gv-fade 0.5s var(--ease) both; } -.sc-gap::before, .sc-gap::after { content: ""; flex: 1; height: 1px; background: rgba(255, 255, 255, 0.09); } -.sc-gap:hover { color: var(--ink); } -.sc-rule .x { background: none; border: none; color: var(--dim2); cursor: pointer; font-size: 11px; padding: 0 2px; flex: none; } -.sc-rule .x:hover { color: var(--bad); } - -/* wizard standard: the next-best-action lives bottom-right INSIDE the - flow's card, in normal flow so it never overlaps content */ -.fx-cta { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 16px; } - -/* tools below the fold: scroll region with an explicit notice */ -.sc-scrollwrap { position: relative; min-height: 0; } -.sc-scroll { max-height: 200px; overflow-y: auto; scrollbar-width: thin; } -.sc-scrollwrap::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 34px; background: linear-gradient(transparent, rgba(11, 11, 14, 0.95)); pointer-events: none; opacity: 1; transition: opacity 0.3s var(--ease); } -.sc-scrollwrap.at-end::after { opacity: 0; } -.sc-more { font-family: var(--font-mono, monospace); font-size: 10px; letter-spacing: 0.1em; color: var(--dim2); padding-top: 6px; text-transform: uppercase; } diff --git a/apps/homescreen/app/cedar/flows/lib.ts b/apps/homescreen/app/cedar/flows/lib.ts deleted file mode 100644 index 4e8266c2..00000000 --- a/apps/homescreen/app/cedar/flows/lib.ts +++ /dev/null @@ -1,284 +0,0 @@ -/* Shared substrate for the /cedar/flows explorations. - * - * Every direction renders the SAME real evidence — the archived 57-task - * domain-identification benchmark (incumbent gpt-4o vs candidate glm-5.2) - * — so the only variable between pages is the interaction metaphor. - * - * Color carries meaning everywhere: mint = incumbent, violet = candidate, - * green/red = pass/fail, amber = needs training. Words are a last resort. - */ - -import { useEffect, useState } from "react"; - -export const INC_C = "#9edbd3"; // incumbent · mint -export const CAND_C = "#a78bfa"; // candidate · violet -export const GOOD = "#6ee7a0"; -export const BAD = "#e5534b"; -export const AMBER = "#f2b34c"; -export const INK = "#f2f2f0"; -export const DIM = "rgba(242,242,240,0.45)"; - -// fallback when no archived run qualifies; useDuel prefers the LATEST -// big benchmark (>=50 tasks, >=2 models) from the index so new runs — -// including evolved-prompt 3-ways — become the default evidence -export const RUN_URL = - "/env-runs/domain-identification/history/20260703T043718+0000.json"; - -async function resolveRunUrl(wl: string): Promise { - try { - const idx = await fetch(`/env-runs/${wl}/index.json`).then((r) => - r.ok ? r.json() : [] - ); - const entry = (idx as { file: string; tasks: number; models: unknown[] }[]).find( - (e) => e.tasks >= 50 && e.models.length >= 2 - ); - if (entry) return `/env-runs/${wl}/${entry.file}`; - } catch { - /* fall back */ - } - return RUN_URL; -} - -export type Ex = { - exampleId: string; - reward: number; - metrics: Record; - finalAnswer: Record | null; - consistency?: number; - rewardSpread?: [number, number]; -}; - -export type RunModel = { - model: string; - wallSeconds?: number; - aggregate: Record; - examples: Ex[]; -}; - -export type Run = { - workload: string; - ranAt: string; - rubric: { name: string; weight: number }[]; - tasks: { - exampleId: string; - currentUser?: string; - goldDomainHint?: string; - crmDomains?: string[]; - incumbentModel?: string; - }[]; - models: RunModel[]; -}; - -export type Pair = { - i: number; - eid: string; - user: string; - gold: string; - crm: number; // how many CRM candidates matched (ambiguity) - inc: Ex; - cand: Ex; - flip: boolean; // meaningful reward gap - solveFlip: boolean; // one solved it, the other didn't -}; - -export type Duel = { - run: Run; - inc: RunModel; - cand: RunModel; - pairs: Pair[]; -}; - -export const solvedEx = (e?: Ex | null) => - ((e?.metrics ?? {})["task_solved"] ?? 0) >= 0.5; - -export const pct = (x: number | undefined | null) => - `${Math.round((x ?? 0) * 100)}%`; - -export function answerLabel(ex?: Ex | null): string { - const fa = (ex?.finalAnswer ?? {}) as Record; - if (fa.primaryDomain) return String(fa.primaryDomain); - if (fa.conversationId) return "existing thread"; - return "no match"; -} - -export function reasonText(ex?: Ex | null): string { - const fa = (ex?.finalAnswer ?? {}) as Record; - return typeof fa.reasoning === "string" ? fa.reasoning : ""; -} - -/** plain-english rubric axes — [label, why it matters] */ -export const AXIS_LABEL: Record = { - resolution_match: ["right answer", "routed exactly where production routed"], - domain_identified: ["right company", "found the correct company"], - final_json_valid: ["valid format", "answered in the shape the caller parses"], - lookup_discipline: ["honest tools", "no guessing, no carpet-bombing the CRM"], - task_solved: ["solved", "the whole task, strictly"], -}; -export const axisLabel = (n: string) => - AXIS_LABEL[n]?.[0] ?? n.replaceAll("_", " "); - -export function mulberry(seed: number) { - let a = seed >>> 0; - return () => { - a |= 0; - a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -export function shuffled(arr: T[], seed: number): T[] { - const r = mulberry(seed); - const out = [...arr]; - for (let i = out.length - 1; i > 0; i--) { - const j = Math.floor(r() * (i + 1)); - [out[i], out[j]] = [out[j], out[i]]; - } - return out; -} - -export function useDuel(): Duel | null { - const [duel, setDuel] = useState(null); - useEffect(() => { - let live = true; - resolveRunUrl("domain-identification") - .then((url) => fetch(url)) - .then((r) => r.json()) - .then((run: Run) => { - if (!live) return; - const incName = run.tasks[0]?.incumbentModel ?? "gpt-4o"; - const inc = - run.models.find((m) => m.model === incName) ?? run.models[0]; - const cand = run.models - .filter((m) => m !== inc) - .sort((a, b) => (b.aggregate.reward ?? 0) - (a.aggregate.reward ?? 0))[0]; - const byInc = new Map(inc.examples.map((e) => [e.exampleId, e])); - const byCand = new Map(cand.examples.map((e) => [e.exampleId, e])); - const pairs: Pair[] = run.tasks - .filter((t) => byInc.has(t.exampleId) && byCand.has(t.exampleId)) - .map((t, i) => { - const a = byInc.get(t.exampleId)!; - const b = byCand.get(t.exampleId)!; - return { - i, - eid: t.exampleId, - user: t.currentUser ?? "", - gold: t.goldDomainHint ?? "", - crm: (t.crmDomains ?? []).length, - inc: a, - cand: b, - flip: Math.abs(a.reward - b.reward) > 0.15, - solveFlip: solvedEx(a) !== solvedEx(b), - }; - }); - setDuel({ run, inc, cand, pairs }); - }) - .catch(() => {}); - return () => { - live = false; - }; - }, []); - return duel; -} - -/* ---- the generated rubric — what the user accepted in environment - * generation. race/gradient score with THIS, not the env defaults. ---- */ - -export type GenRule = { rule: string; quote?: string | null; weight: number }; -export type GenRubric = { rules: GenRule[]; version: number | null }; - -export function useGeneratedRubric(wl: string): GenRubric | null { - const [rubric, setRubric] = useState(null); - useEffect(() => { - let live = true; - fetch(`/env-analysis/${wl}-rubric.json`) - .then((r) => (r.ok ? r.json() : null)) - .then((d) => { - if (live && d && Array.isArray(d.rules) && d.rules.length) - setRubric({ rules: d.rules, version: d.version ?? null }); - }) - .catch(() => {}); - return () => { - live = false; - }; - }, [wl]); - return rubric; -} - -/** which measurable axis grades a rule — heuristic until each rule is - * individually instrumented in the env */ -export function ruleAxis(rule: string): string { - const t = rule.toLowerCase(); - if (/tool|lookup|call/.test(t)) return "lookup_discipline"; - if (/invent|guess|make up|fabricat/.test(t)) return "lookup_discipline"; - if (/format|json|structure/.test(t)) return "final_json_valid"; - return "resolution_match"; -} - -/** generated rules → normalized per-axis weights */ -export function axisWeightsFromRules(rules: GenRule[]): Record { - const w: Record = {}; - for (const r of rules) w[ruleAxis(r.rule)] = (w[ruleAxis(r.rule)] ?? 0) + r.weight; - const sum = Object.values(w).reduce((a, b) => a + b, 0) || 1; - for (const k of Object.keys(w)) w[k] /= sum; - return w; -} - -/** rescore an aggregate (or per-example metrics) with the user's rubric */ -export function rescore( - metrics: Record, - weights: Record -): number { - let n = 0; - for (const [ax, w] of Object.entries(weights)) n += (metrics[ax] ?? 0) * w; - return n; -} - -export const WORKLOADS = [ - { id: "domain-identification", tasks: 57 }, - { id: "zapier-automationbench", tasks: 25 }, - { id: "harvey-lab", tasks: 10 }, - { id: "analyzer", tasks: 18 }, - { id: "on-event-email-orchestrator", tasks: 16 }, - { id: "on-event-execution", tasks: 64 }, - { id: "on-event-meeting-orchestrator", tasks: 187 }, - { id: "automation", tasks: 52 }, - { id: "chat", tasks: 131 }, -]; - -export type WlRuns = { reward: Record; tasks: number } | null; - -/** most recent archived result per model, per workload (null = no runs yet) */ -export function useWorkloadRuns(): Record { - const [runs, setRuns] = useState>({}); - useEffect(() => { - let live = true; - Promise.all( - WORKLOADS.map(async (w) => { - try { - const idx = await fetch(`/env-runs/${w.id}/index.json`).then((r) => - r.ok ? r.json() : null - ); - if (!Array.isArray(idx) || !idx.length) return [w.id, null] as const; - const reward: Record = {}; - let tasks = 0; - for (const entry of idx) { - tasks = Math.max(tasks, entry.tasks ?? 0); - for (const m of entry.models ?? []) - if (!(m.model in reward)) reward[m.model] = m.reward; - } - return [w.id, { reward, tasks }] as const; - } catch { - return [w.id, null] as const; - } - }) - ).then((entries) => { - if (live) setRuns(Object.fromEntries(entries)); - }); - return () => { - live = false; - }; - }, []); - return runs; -} diff --git a/apps/homescreen/app/cedar/flows/orchard/LIVE.md b/apps/homescreen/app/cedar/flows/orchard/LIVE.md deleted file mode 100644 index 9a2abcd2..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/LIVE.md +++ /dev/null @@ -1,35 +0,0 @@ -# Orchard live Workflow view - -Orchard remains a static Tauri/Next export. A read-only sidecar keeps the Train -API key and run capability out of the browser and forwards only the canonical, -redacted `understudy.experiment-event.v1` stream. - -The sidecar validates every upstream event against the vendored canonical JSON -Schema and verifies its SHA-256 against -`schemas/understudy-train/manifest.json` before listening. Its `/healthz` -response exposes the contract bundle and event-schema hashes so a cutover -receipt can bind Orchard to the same platform contract consumed by executors. - -Run the sidecar on the same monitored host as Orchard: - -```bash -ORCHARD_TRAIN_API_URL=https://train.example \ -ORCHARD_EXPERIMENT_ID=exp-123 \ -ORCHARD_TRAIN_API_KEY=... \ -ORCHARD_EXPERIMENT_RUN_TOKEN=... \ -ORCHARD_VIEWER_TOKEN= \ -ORCHARD_ALLOWED_ORIGIN=https://spark-host.tailnet.ts.net \ -bun --cwd apps/homescreen run orchard:workflow-proxy -``` - -Set `NEXT_PUBLIC_ORCHARD_EVENT_PROXY_URL` at homescreen build time when the -sidecar is not available at `http://127.0.0.1:1431`. Set the matching -`NEXT_PUBLIC_ORCHARD_VIEWER_TOKEN`, an explicit -`NEXT_PUBLIC_ORCHARD_QUALITY_METRIC`, and -`NEXT_PUBLIC_ORCHARD_QUALITY_DIRECTION=higher`. The viewer token is a -short-lived, read-only capability scoped to this experiment; it is not a Train -API key and must be rotated with each published viewer build. Lower-is-better -metrics remain unscored until the renderer supports directional frontiers. -Missing origins and missing viewer authorization fail closed. The sidecar is not a -controller: it cannot submit, approve, cancel, or mutate experiments and stores -no state. Its cursor is supplied by the browser on each request. diff --git a/apps/homescreen/app/cedar/flows/orchard/engine.ts b/apps/homescreen/app/cedar/flows/orchard/engine.ts deleted file mode 100644 index 5e0e33b1..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/engine.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* 20 · orchard — the renderer, in the house WebGL2 dialect (waves/scale/ - * rlm): static vertex buffers uploaded once, every animation computed in - * the vertex shader from uniforms. Replay is uStep, semantic zoom is - * uZoom — the CPU never touches vertex data after mount. - * - * Per-vertex LOD: aBand fades a vertex in/out across zoom, aSpread - * collapses it onto its aggregate anchor as you pull back, so detail - * unfolds out of the dot it lives in instead of view-switching. - * Camera follows the scale.tsx idiom: drag pan, wheel zoom-to-cursor, - * glide toward a target until the user takes the wheel. - * - * uZoom is CSS px per world unit (dpr applied in-shader) so zoom feel - * and LOD thresholds match across displays. - */ - -import { STRIDE, type World } from "./run"; - -export type Camera = { x: number; y: number; z: number }; - -export type EngineOpts = { - onTap?: (wx: number, wy: number) => void; - onDouble?: (wx: number, wy: number) => void; - onHover?: (wx: number, wy: number, cssX: number, cssY: number) => void; - onHoverEnd?: () => void; - /* the user took the camera (drag or wheel) — cancel any scripted ride */ - onGrab?: () => void; - /* runs each frame before uniforms are read — drive the replay clock - * and DOM overlay from here */ - onFrame?: (dt: number, cam: Camera, now: number) => void; -}; - -export type Engine = ReturnType; - -const Z_MIN = 0.12; -const Z_MAX = 400; - -const VERT = `#version 300 es -precision highp float; -uniform vec2 uCam; -uniform float uZoom; // css px per world unit -uniform vec2 uRes; // backing px -uniform float uDpr; -uniform float uStep; -uniform float uFocus; -uniform float uFocusK; -layout(location=0) in vec2 aPos; -layout(location=1) in vec2 aAnchor; -layout(location=2) in vec2 aBirth; -layout(location=3) in float aStep; -layout(location=4) in float aSize; -layout(location=5) in float aMin; -layout(location=6) in float aMax; -layout(location=7) in vec3 aColor; -layout(location=8) in float aAlpha; -layout(location=9) in vec4 aBand; -layout(location=10) in vec2 aSpread; -layout(location=11) in float aEval; -out vec3 vColor; -out float vAlpha; -void main() { - float z = uZoom; - // replay: reveal at aStep, sprout from the parent position - float rev = smoothstep(aStep - 0.4, aStep, uStep); - float birth = clamp((uStep - aStep) * 1.6, 0.0, 1.0); - float eo = 1.0 - (1.0 - birth) * (1.0 - birth); - vec2 pos = mix(aBirth, aPos, eo); - // semantic zoom: collapse onto the aggregate anchor when pulled back - float spread = smoothstep(aSpread.x, aSpread.y, z); - pos = mix(aAnchor, pos, spread); - float band = smoothstep(aBand.x, aBand.y, z) * (1.0 - smoothstep(aBand.z, aBand.w, z)); - // newborn flare: cyan, slightly oversized, settling into state color - float nb = 1.0 - clamp((uStep - aStep) * 0.8, 0.0, 1.0); - float px = clamp(aSize * z, aMin, aMax) * (1.0 + nb * 0.9); - vColor = mix(aColor, vec3(0.404, 0.910, 0.976), nb * 0.85); - float focus = mix(1.0, abs(aEval - uFocus) < 0.5 ? 1.0 : 0.16, uFocusK); - vAlpha = aAlpha * rev * band * focus; - vec2 screen = (pos - uCam) * z * uDpr; - vec2 clip = screen / (uRes * 0.5); - gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); - gl_PointSize = px * uDpr; -}`; - -const FRAG_POINT = `#version 300 es -precision highp float; -in vec3 vColor; -in float vAlpha; -out vec4 outColor; -void main() { - vec2 c = gl_PointCoord - 0.5; - float a = (1.0 - smoothstep(0.38, 0.5, length(c))) * vAlpha; - if (a < 0.004) discard; - outColor = vec4(vColor, a); -}`; - -const FRAG_LINE = `#version 300 es -precision highp float; -in vec3 vColor; -in float vAlpha; -out vec4 outColor; -void main() { - if (vAlpha < 0.004) discard; - outColor = vec4(vColor, vAlpha); -}`; - -function compile(gl: WebGL2RenderingContext, type: number, src: string) { - const sh = gl.createShader(type)!; - gl.shaderSource(sh, src); - gl.compileShader(sh); - if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) - throw new Error(gl.getShaderInfoLog(sh) ?? "shader compile failed"); - return sh; -} - -function link(gl: WebGL2RenderingContext, vert: string, frag: string) { - const prog = gl.createProgram()!; - gl.attachShader(prog, compile(gl, gl.VERTEX_SHADER, vert)); - gl.attachShader(prog, compile(gl, gl.FRAGMENT_SHADER, frag)); - gl.linkProgram(prog); - if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) - throw new Error(gl.getProgramInfoLog(prog) ?? "program link failed"); - return prog; -} - -/* aPos(2) aAnchor(2) aBirth(2) aStep aSize aMin aMax aColor(3) aAlpha aBand(4) aSpread(2) aEval */ -const ATTRS: [number, number, number][] = [ - [0, 2, 0], - [1, 2, 2], - [2, 2, 4], - [3, 1, 6], - [4, 1, 7], - [5, 1, 8], - [6, 1, 9], - [7, 3, 10], - [8, 1, 13], - [9, 4, 14], - [10, 2, 18], - [11, 1, 20], -]; - -export function createEngine( - canvas: HTMLCanvasElement, - world: World, - opts: EngineOpts = {} -) { - const gl = canvas.getContext("webgl2", { antialias: false, alpha: true }); - if (!gl) return null; - const lose = gl.getExtension("WEBGL_lose_context"); - - const dpr = Math.min(window.devicePixelRatio || 1, 2); - - const makeVao = (data: Float32Array) => { - const vao = gl.createVertexArray()!; - gl.bindVertexArray(vao); - const buf = gl.createBuffer()!; - gl.bindBuffer(gl.ARRAY_BUFFER, buf); - gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); - for (const [loc, size, off] of ATTRS) { - gl.enableVertexAttribArray(loc); - gl.vertexAttribPointer(loc, size, gl.FLOAT, false, STRIDE * 4, off * 4); - } - gl.bindVertexArray(null); - return vao; - }; - const uni = (prog: WebGLProgram) => ({ - cam: gl.getUniformLocation(prog, "uCam"), - zoom: gl.getUniformLocation(prog, "uZoom"), - res: gl.getUniformLocation(prog, "uRes"), - dpr: gl.getUniformLocation(prog, "uDpr"), - step: gl.getUniformLocation(prog, "uStep"), - focus: gl.getUniformLocation(prog, "uFocus"), - focusK: gl.getUniformLocation(prog, "uFocusK"), - }); - - /* GL resources live behind `res` so a lost context (strict-mode - * remount after loseContext, soft navigation) can rebuild on - * "webglcontextrestored" — the loop skips drawing until then. */ - type GlRes = { - progPoint: WebGLProgram; - progLine: WebGLProgram; - vaoPoints: WebGLVertexArrayObject; - vaoLines: WebGLVertexArrayObject; - uPoint: ReturnType; - uLine: ReturnType; - }; - let res: GlRes | null = null; - const initGL = () => { - const progPoint = link(gl, VERT, FRAG_POINT); - const progLine = link(gl, VERT, FRAG_LINE); - res = { - progPoint, - progLine, - vaoPoints: makeVao(world.points), - vaoLines: makeVao(world.lines), - uPoint: uni(progPoint), - uLine: uni(progLine), - }; - gl.enable(gl.BLEND); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - gl.disable(gl.DEPTH_TEST); - gl.clearColor(0, 0, 0, 0); - }; - const onRestored = () => { - try { - initGL(); - } catch { - res = null; - } - }; - if (gl.isContextLost()) { - canvas.addEventListener("webglcontextrestored", onRestored, { once: true }); - lose?.restoreContext(); - } else initGL(); - - /* ---- state ---- */ - const cam: Camera = { x: 0, y: 0, z: 1 }; - let camTarget: Camera | null = null; - let step = 101; // fully revealed by default — render, then let ▶ re-tell it - let focusEval = -1; - let focusK = 0; - let focusOn = false; - - const fitCam = (pad = 0.86): Camera => { - const b = world.bounds; - const cw = Math.max(1, canvas.clientWidth); - const ch = Math.max(1, canvas.clientHeight); - const z = Math.min( - Z_MAX, - Math.max(Z_MIN, Math.min(cw / (b.x1 - b.x0), ch / (b.y1 - b.y0)) * pad) - ); - return { x: (b.x0 + b.x1) / 2, y: (b.y0 + b.y1) / 2, z }; - }; - - /* the initial fit waits until layout has given the canvas real size */ - let needFit = true; - const resize = () => { - const w = Math.max(1, Math.round(canvas.clientWidth * dpr)); - const h = Math.max(1, Math.round(canvas.clientHeight * dpr)); - if (canvas.width !== w || canvas.height !== h) { - canvas.width = w; - canvas.height = h; - } - if (needFit && canvas.clientWidth > 10 && canvas.clientHeight > 10) { - needFit = false; - Object.assign(cam, fitCam()); - } - }; - resize(); - const ro = new ResizeObserver(resize); - ro.observe(canvas); - - /* ---- camera controls (scale.tsx idiom) ---- */ - let dragging = false; - let moved = false; - let lastX = 0; - let lastY = 0; - - const cssPoint = (e: { clientX: number; clientY: number }) => { - const rect = canvas.getBoundingClientRect(); - return { - cx: e.clientX - rect.left, - cy: e.clientY - rect.top, - w: rect.width, - h: rect.height, - }; - }; - const toWorld = (cx: number, cy: number, w: number, h: number) => ({ - wx: cam.x + (cx - w / 2) / cam.z, - wy: cam.y + (cy - h / 2) / cam.z, - }); - - const onDown = (e: PointerEvent) => { - dragging = true; - moved = false; - camTarget = null; // user takes the wheel - opts.onGrab?.(); - lastX = e.clientX; - lastY = e.clientY; - try { canvas.setPointerCapture(e.pointerId); } catch { /* synthetic pointers */ } - canvas.style.cursor = "grabbing"; - }; - const onMove = (e: PointerEvent) => { - if (dragging) { - const dx = e.clientX - lastX; - const dy = e.clientY - lastY; - cam.x -= dx / cam.z; - cam.y -= dy / cam.z; - if (Math.hypot(dx, dy) > 2) moved = true; - lastX = e.clientX; - lastY = e.clientY; - } else { - const { cx, cy, w, h } = cssPoint(e); - const { wx, wy } = toWorld(cx, cy, w, h); - opts.onHover?.(wx, wy, cx, cy); - } - }; - const onUp = (e: PointerEvent) => { - if (!dragging) return; - dragging = false; - try { canvas.releasePointerCapture(e.pointerId); } catch { /* synthetic pointers */ } - canvas.style.cursor = "grab"; - if (!moved) { - const { cx, cy, w, h } = cssPoint(e); - const { wx, wy } = toWorld(cx, cy, w, h); - opts.onTap?.(wx, wy); - } - }; - const onDbl = (e: MouseEvent) => { - const { cx, cy, w, h } = cssPoint(e); - const { wx, wy } = toWorld(cx, cy, w, h); - opts.onDouble?.(wx, wy); - }; - const onWheel = (e: WheelEvent) => { - e.preventDefault(); - camTarget = null; - opts.onGrab?.(); - const { cx, cy, w, h } = cssPoint(e); - const sx = cx - w / 2; - const sy = cy - h / 2; - // world point under the cursor stays put while zoom changes - const wx = cam.x + sx / cam.z; - const wy = cam.y + sy / cam.z; - cam.z = Math.min(Z_MAX, Math.max(Z_MIN, cam.z * Math.exp(-e.deltaY * 0.0016))); - cam.x = wx - sx / cam.z; - cam.y = wy - sy / cam.z; - }; - const onLeave = () => opts.onHoverEnd?.(); - - canvas.addEventListener("pointerdown", onDown); - canvas.addEventListener("pointermove", onMove); - canvas.addEventListener("pointerup", onUp); - canvas.addEventListener("pointercancel", onUp); - canvas.addEventListener("pointerleave", onLeave); - canvas.addEventListener("dblclick", onDbl); - canvas.addEventListener("wheel", onWheel, { passive: false }); - canvas.style.cursor = "grab"; - canvas.style.touchAction = "none"; - - /* ---- frame loop ---- */ - let raf = 0; - let last = performance.now(); - const frame = (now: number) => { - const dt = Math.min((now - last) / 1000, 0.1); - last = now; - - // glide toward a target until the user takes over - if (camTarget) { - const t = camTarget; - const k = Math.min(1, dt * 4); - cam.x += (t.x - cam.x) * k; - cam.y += (t.y - cam.y) * k; - cam.z += (t.z - cam.z) * k; - if ( - Math.abs(t.x - cam.x) * cam.z < 0.5 && - Math.abs(t.y - cam.y) * cam.z < 0.5 && - Math.abs(t.z - cam.z) / t.z < 0.005 - ) { - Object.assign(cam, t); - camTarget = null; - } - } - // focus dim eases in and out - const fkT = focusOn ? 1 : 0; - focusK += (fkT - focusK) * Math.min(1, dt * 6); - - opts.onFrame?.(dt, cam, now); - - if (res) { - gl.viewport(0, 0, canvas.width, canvas.height); - gl.clear(gl.COLOR_BUFFER_BIT); - const draw = ( - prog: WebGLProgram, - u: ReturnType, - vao: WebGLVertexArrayObject, - mode: number, - count: number - ) => { - gl.useProgram(prog); - gl.uniform2f(u.cam, cam.x, cam.y); - gl.uniform1f(u.zoom, cam.z); - gl.uniform2f(u.res, canvas.width, canvas.height); - gl.uniform1f(u.dpr, dpr); - gl.uniform1f(u.step, step); - gl.uniform1f(u.focus, focusEval); - gl.uniform1f(u.focusK, focusK); - gl.bindVertexArray(vao); - gl.drawArrays(mode, 0, count); - }; - draw(res.progLine, res.uLine, res.vaoLines, gl.LINES, world.lineVertCount); - draw(res.progPoint, res.uPoint, res.vaoPoints, gl.POINTS, world.pointCount); - gl.bindVertexArray(null); - } - - raf = requestAnimationFrame(frame); - }; - raf = requestAnimationFrame(frame); - - return { - cam: (): Camera => ({ ...cam }), - setStep: (v: number) => { - step = v; - }, - getStep: () => step, - setFocus: (evalId: number) => { - if (evalId < 0) focusOn = false; - else { - focusEval = evalId; - focusOn = true; - } - }, - getFocus: () => (focusOn ? focusEval : -1), - flyTo: (t: Camera) => { - camTarget = { - x: t.x, - y: t.y, - z: Math.min(Z_MAX, Math.max(Z_MIN, t.z)), - }; - }, - fitAll: () => { - camTarget = fitCam(); - }, - worldToCss: (wx: number, wy: number): [number, number] => [ - (wx - cam.x) * cam.z + canvas.clientWidth / 2, - (wy - cam.y) * cam.z + canvas.clientHeight / 2, - ], - dispose: () => { - cancelAnimationFrame(raf); - ro.disconnect(); - canvas.removeEventListener("webglcontextrestored", onRestored); - canvas.removeEventListener("pointerdown", onDown); - canvas.removeEventListener("pointermove", onMove); - canvas.removeEventListener("pointerup", onUp); - canvas.removeEventListener("pointercancel", onUp); - canvas.removeEventListener("pointerleave", onLeave); - canvas.removeEventListener("dblclick", onDbl); - canvas.removeEventListener("wheel", onWheel); - // NOTE: no loseContext() here — see galaxy/engine.ts; a strict-mode - // remount shares this canvas's context and cannot restore a context - // that arrives already-lost. - }, - }; -} diff --git a/apps/homescreen/app/cedar/flows/orchard/live.ts b/apps/homescreen/app/cedar/flows/orchard/live.ts deleted file mode 100644 index 8e74027a..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/live.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { buildRealWorld, type OrchardRun, type OrchardRunSet } from "./real"; - -export type WorkflowEvent = { - schema_version: "understudy.experiment-event.v1"; - experiment_id: string; - sequence: number; - occurred_at: string; - type: string; - candidate_id?: string; - task_id?: string; - state?: string; - metrics?: Record; - frontier?: boolean; - phase?: string; - failure_class?: string; - code?: string; -}; - -type QualityPolicy = { metric: string; direction: "higher" | "lower" }; - -function scoreOf(metrics: Record | undefined, policy: QualityPolicy): number | null { - if (!metrics || policy.direction !== "higher") return null; - const value = metrics[policy.metric]; - return Number.isFinite(value) ? value : null; -} - -/** Convert the canonical redacted Workflow stream into Orchard's renderer model. */ -export function buildWorkflowWorld(events: WorkflowEvent[], quality: QualityPolicy) { - const sorted = [...events].sort((a, b) => a.sequence - b.sequence); - const experimentId = sorted[0]?.experiment_id ?? "waiting-for-workflow"; - const candidates = new Map(); - const ensure = (candidateId: string): OrchardRun => { - let run = candidates.get(candidateId); - if (!run) { - run = { - id: candidateId, - parent: null, - family: candidateId, - arch: "workflow candidate", - status: "queued", - at: null, - acc: null, - balanced: null, - bench: 1, - cost: null, - elapsed: null, - cases: [], - }; - candidates.set(candidateId, run); - } - return run; - }; - - for (const event of sorted) { - if (!event.candidate_id) continue; - const run = ensure(event.candidate_id); - run.at ??= event.occurred_at; - if (event.type === "candidate.state_changed" && event.state) run.status = event.state; - if (event.type === "score.snapshot") { - run.acc = scoreOf(event.metrics, quality); - run.balanced = run.acc; - } - if (event.type === "rollout.state_changed" && event.task_id && event.state) { - const prior = run.cases?.find((item) => item.id === event.task_id); - const state = event.state === "started" - ? "running" - : event.state as "succeeded" | "failed" | "timed_out"; - const next: NonNullable[number] = { - id: event.task_id, - ok: state === "succeeded", - state, - exp: null, - got: null, - text: `${event.task_id} · ${state}`, - }; - if (prior) Object.assign(prior, next); - else run.cases?.push(next); - } - } - - if (candidates.size === 0) ensure("waiting"); - const runs = [...candidates.values()]; - const data: OrchardRunSet = { - campaign: experimentId, - as_of: sorted.at(-1)?.occurred_at ?? new Date(0).toISOString(), - primary_bench: 1, - spend_usd: null, - runs, - }; - return buildRealWorld(data, { includeUnscored: true }); -} diff --git a/apps/homescreen/app/cedar/flows/orchard/page.tsx b/apps/homescreen/app/cedar/flows/orchard/page.tsx deleted file mode 100644 index 7161292c..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/page.tsx +++ /dev/null @@ -1,1087 +0,0 @@ -"use client"; - -/* 20 · orchard — every rewrite plants a grove. One continuous field - * holds the whole outer loop: records walk a spine, failed proposals - * orbit the record they tried to beat, each proposal unfolds into - * dozens of solution searches and each search into its tree of nodes. - * Drag to pan, wheel to dive from the whole climb to a single node — - * the climb chart below is the replay scrubber. WebGL draws the mass, - * the DOM draws the words. */ - -import Link from "next/link"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import "../../cedar.css"; -import "../../../moodboard/moodboard.css"; -import "../flows.css"; -import { AMBER, BAD, CAND_C, GOOD, INC_C } from "../lib"; -import { createEngine, type Camera, type Engine } from "./engine"; -import { buildWorkflowWorld, type WorkflowEvent } from "./live"; -import { generateRun, MAX_STEP, type World } from "./run"; - -type DataSrc = "synthetic" | "workflow"; - -const SPEED = 7; // outer steps per second during replay - -/* ---- the replay ride: most granular first ---- - * ▶ opens on ONE tree of aide_0 growing node by node, rings its winner, - * pulls back to the local grove, then back out to the whole field — and - * only then lets the climb run at full speed. Camera beats fire once at - * their timestamps; the step clock eases between keyframes so the world - * reveals in sync with where the camera is looking. Grabbing the camera - * (drag/wheel) or scrubbing the chart cancels the ride. */ -const RIDE_END = 10.2; -type RideKey = { t: number; step: number }; -const rideEase = (keys: RideKey[], t: number) => { - for (let i = 1; i < keys.length; i++) { - if (t <= keys[i].t) { - const a = keys[i - 1]; - const b = keys[i]; - const k = (t - a.t) / (b.t - a.t); - return a.step + (b.step - a.step) * (k * k * (3 - 2 * k)); - } - } - return keys[keys.length - 1].step; -}; - -type Sel = - | { kind: "eval"; id: number } - | { kind: "search"; id: number } - | { kind: "node"; id: number } - | null; - -/* ---- chart geometry — derived per world (real runs have their own - * step count and score range) ---- */ -const CW = 1020; -const CH = 168; -const PX = 46; -const PT = 16; -const PB = 26; - -type Geom = { - maxStep: number; - sx: (s: number) => number; - sy: (v: number) => number; - yTicks: number[]; - xTicks: number[]; -}; - -function makeGeom(world: World): Geom { - const maxStep = world.evals.length - 1; - let lo = Infinity; - let hi = -Infinity; - for (const ev of world.evals) { - lo = Math.min(lo, ev.perf); - hi = Math.max(hi, ev.perf); - } - const pad = Math.max(0.004, (hi - lo) * 0.07); - const yLo = lo - pad; - const yHi = hi + pad; - const sx = (s: number) => PX + (s / Math.max(1, maxStep)) * (CW - 2 * PX); - const sy = (v: number) => PT + (1 - (v - yLo) / (yHi - yLo)) * (CH - PT - PB); - const r2 = (v: number) => Math.round(v * 100) / 100; - return { - maxStep, - sx, - sy, - yTicks: [...new Set([r2(lo), r2((lo + hi) / 2), r2(hi)])], - xTicks: [...new Set([0, Math.round(maxStep / 4), Math.round(maxStep / 2), Math.round((3 * maxStep) / 4), maxStep])], - }; -} - -function hexPoints(x: number, y: number, r: number) { - return Array.from({ length: 6 }, (_, k) => { - const a = (Math.PI / 3) * k + Math.PI / 6; - return `${(x + r * Math.cos(a)).toFixed(1)},${(y + r * Math.sin(a)).toFixed(1)}`; - }).join(" "); -} - -const smooth = (a: number, b: number, v: number) => { - const t = Math.min(1, Math.max(0, (v - a) / (b - a))); - return t * t * (3 - 2 * t); -}; - -/* ---- picking: level follows the semantic zoom ---- */ -function pickAt(world: World, wx: number, wy: number, z: number): Sel { - if (z >= 22) { - // nodes — only searches whose eval is near the pointer - const tol = 14 / z; - let best = -1; - let bd = tol; - for (const ev of world.evals) { - if (Math.hypot(ev.x - wx, ev.y - wy) > 9 + 4 + tol) continue; - for (let gi = ev.n0; gi < ev.n1; gi++) { - const d = Math.hypot(world.nx[gi] - wx, world.ny[gi] - wy); - if (d < bd) { - bd = d; - best = gi; - } - } - } - if (best >= 0) return { kind: "node", id: best }; - } - if (z >= 3) { - const tol = 14 / z; - let best = -1; - let bd = Math.max(tol, 1.4); - for (const se of world.searches) { - const d = Math.hypot(se.x - wx, se.y - wy); - if (d < bd) { - bd = d; - best = se.id; - } - } - if (best >= 0) return { kind: "search", id: best }; - } - const tol = Math.max(16 / z, 3); - let best = -1; - let bd = tol; - for (const ev of world.evals) { - const d = Math.hypot(ev.x - wx, ev.y - wy); - if (d < bd) { - bd = d; - best = ev.id; - } - } - return best >= 0 ? { kind: "eval", id: best } : null; -} - -const evalOf = (world: World, s: Sel): number => - !s ? -1 : s.kind === "eval" ? s.id : s.kind === "search" ? world.searches[s.id].eval : world.searches[world.nsearch[s.id]].eval; - -const NODE_STATE = ["root", "explored", "best of its search", "dead end", "running"] as const; -const NODE_COLOR = ["#f2f2f0", INC_C, CAND_C, BAD, "#f2f2f0"] as const; - -export default function OrchardFlow() { - const [src, setSrc] = useState("synthetic"); - const [liveEvents, setLiveEvents] = useState([]); - const [liveStatus, setLiveStatus] = useState<"idle" | "connecting" | "live" | "unavailable">("idle"); - const world = useMemo( - () => src === "workflow" ? buildWorkflowWorld(liveEvents, { - metric: process.env.NEXT_PUBLIC_ORCHARD_QUALITY_METRIC ?? "quality", - direction: process.env.NEXT_PUBLIC_ORCHARD_QUALITY_DIRECTION === "lower" ? "lower" : "higher", - }) : generateRun(), - [src, liveEvents], - ); - const geom = useMemo(() => makeGeom(world), [world]); - const geomRef = useRef(geom); - geomRef.current = geom; - const nameOf = useCallback( - (id: number) => world.names?.[id] ?? `aide_${id}`, - [world] - ); - const canvasRef = useRef(null); - const engineRef = useRef(null); - const liveCamRef = useRef(null); - const [glFail, setGlFail] = useState(false); - const [sel, setSelState] = useState(null); - const [playing, setPlaying] = useState(false); - const [chartStep, setChartStep] = useState(MAX_STEP); - - const selRef = useRef(null); - const stepRef = useRef(MAX_STEP + 1.2); // start fully revealed - const playingRef = useRef(false); - const rideRef = useRef<{ t: number; fired: number } | null>(null); - const rideKeysRef = useRef([{ t: 0, step: 0 }]); - const chartIntRef = useRef(MAX_STEP); - const hoverRef = useRef<{ sel: Sel; wx: number; wy: number } | null>(null); - - const hudStepRef = useRef(null); - const hudPerfRef = useRef(null); - const zoomCapRef = useRef(null); - const cursorRef = useRef(null); - const trailRef = useRef(null); - const recordEls = useRef<(HTMLDivElement | null)[]>([]); - const hoverRingRef = useRef(null); - const hoverCapRef = useRef(null); - const selRingRef = useRef(null); - - const totals = useMemo( - () => ({ - searches: world.searches.length, - nodes: world.nodeCount, - survived: world.records.length - 1, - }), - [world, src] - ); - - useEffect(() => { - if (src !== "workflow") { - setLiveStatus("idle"); - return; - } - let stopped = false; - let timer: ReturnType | undefined; - let after = -1; - const seen = new Map(); - setLiveEvents([]); - setLiveStatus("connecting"); - const poll = async () => { - try { - const proxy = process.env.NEXT_PUBLIC_ORCHARD_EVENT_PROXY_URL ?? "http://127.0.0.1:1431"; - const viewerToken = process.env.NEXT_PUBLIC_ORCHARD_VIEWER_TOKEN; - if (!viewerToken) throw new Error("viewer capability unavailable"); - const response = await fetch(`${proxy.replace(/\/$/, "")}/events?after=${after}`, { - cache: "no-store", - headers: { authorization: `Bearer ${viewerToken}` }, - }); - if (!response.ok) throw new Error(`live source ${response.status}`); - const payload = await response.json() as { - events?: WorkflowEvent[]; - next_after?: number; - }; - for (const event of payload.events ?? []) seen.set(event.sequence, event); - after = payload.next_after ?? after; - if (!stopped) { - setLiveEvents([...seen.values()].sort((a, b) => a.sequence - b.sequence)); - setLiveStatus("live"); - } - } catch { - if (!stopped) setLiveStatus("unavailable"); - } finally { - if (!stopped) timer = setTimeout(poll, 1_000); - } - }; - void poll(); - return () => { - stopped = true; - if (timer) clearTimeout(timer); - }; - }, [src]); - - const select = useCallback( - (s: Sel) => { - selRef.current = s; - setSelState(s); - engineRef.current?.setFocus(evalOf(world, s)); - }, - [world] - ); - - const flyToEval = useCallback( - (id: number, z = 6) => { - const ev = world.evals[id]; - engineRef.current?.flyTo({ x: ev.x, y: ev.y, z }); - select({ kind: "eval", id }); - }, - [world, select] - ); - - /* ---- mount the engine ---- */ - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - /* a fresh world starts fully revealed and unselected */ - stepRef.current = world.evals.length - 1 + 1.2; - playingRef.current = false; - setPlaying(false); - rideRef.current = null; - selRef.current = null; - setSelState(null); - - const selPos = (s: Sel): [number, number] | null => { - if (!s) return null; - if (s.kind === "eval") return [world.evals[s.id].x, world.evals[s.id].y]; - if (s.kind === "search") return [world.searches[s.id].x, world.searches[s.id].y]; - return [world.nx[s.id], world.ny[s.id]]; - }; - - const hoverLabel = (s: Sel): string => { - if (!s) return ""; - if (s.kind === "eval") { - const ev = world.evals[s.id]; - return world.names - ? `${nameOf(ev.id)} · ${ev.perf.toFixed(4)}` - : `aide_${ev.id} · ${ev.perf.toFixed(4)} · ${ev.s1 - ev.s0} searches`; - } - if (s.kind === "search") { - const se = world.searches[s.id]; - return `s${se.id - world.evals[se.eval].s0} · best ${se.best.toFixed(4)} · ${se.n1 - se.n0} nodes`; - } - const st = world.nstate[s.id]; - if (world.ntrace?.[s.id]) { - const t = world.ntrace[s.id].replace(/\s+/g, " ").replace(/^Source: (.+?) Post: ?/, ""); - return `${st === 1 ? "✓" : "✕"} ${t.slice(0, 76)}…`; - } - return `${world.nscore[s.id].toFixed(4)} · ${NODE_STATE[st]}`; - }; - - /* the ride's camera beats. The protagonist is the first eval that - * has node-level data (synthetic: aide_0's trees; real: the first - * run with graded cases). Three variants of the same granular → - * group → field arc: tree worlds ring the winning node, case worlds - * dive into the failure-map disc, sparse worlds ride one level up. - * The step keys are protagonist-relative so the reveal reaches the - * protagonist while the camera is on it. */ - const evP = world.evals.find((e) => e.n1 > e.n0) ?? world.evals[0]; - const pStep = evP.step; - const rideKeys: RideKey[] = [ - { t: 0, step: 0 }, - { t: 3.6, step: pStep + 0.85 }, - { t: 5.2, step: pStep + 1.0 }, - { t: 7.6, step: pStep + 2.1 }, - { t: 10.2, step: Math.min(pStep + 6, world.evals.length - 1) }, - ]; - rideKeysRef.current = rideKeys; - let winSearch = evP.s0; - for (let i = evP.s0; i < evP.s1; i++) - if (world.searches[i].best > world.searches[winSearch].best) winSearch = i; - const winNode = world.searches[winSearch].bestNode; - const beats: { t: number; go: () => void }[] = - world.nodeCount > 0 && winNode >= 0 - ? [ - { - t: 0, - go: () => { - const se = world.searches[winSearch]; - engine!.flyTo({ x: se.x, y: se.y, z: 64 }); // one tree - }, - }, - { - t: 3.6, - go: () => { - select({ kind: "node", id: winNode }); // the winner, ringed - engine!.flyTo({ x: world.nx[winNode], y: world.ny[winNode], z: 100 }); - }, - }, - { - t: 5.2, - go: () => { - select({ kind: "search", id: winSearch }); // its place in the grove - engine!.flyTo({ x: evP.x, y: evP.y, z: 7 }); - }, - }, - { - t: 7.6, - go: () => { - select(null); // the whole field - engine!.fitAll(); - }, - }, - ] - : world.nodeCount > 0 - ? [ - { - t: 0, - go: () => { - select({ kind: "eval", id: evP.id }); // the graded run - engine!.flyTo({ x: evP.x, y: evP.y, z: 44 }); - }, - }, - { - t: 3.6, - go: () => { - engine!.flyTo({ x: evP.x, y: evP.y, z: 110 }); // into the failure map - }, - }, - { - t: 5.2, - go: () => { - engine!.flyTo({ x: evP.x, y: evP.y, z: 6 }); // its neighborhood - }, - }, - { - t: 7.6, - go: () => { - select(null); - engine!.fitAll(); - }, - }, - ] - : [ - { - t: 0, - go: () => { - select({ kind: "eval", id: evP.id }); // the first run - engine!.flyTo({ x: evP.x, y: evP.y, z: 24 }); - }, - }, - { - t: 3.6, - go: () => { - engine!.flyTo({ x: evP.x, y: evP.y, z: 6 }); // its neighborhood forms - }, - }, - { - t: 5.2, - go: () => { - engine!.flyTo({ x: evP.x, y: evP.y, z: 2.2 }); // the local cluster - }, - }, - { - t: 7.6, - go: () => { - select(null); - engine!.fitAll(); - }, - }, - ]; - - const engine = createEngine(canvas, world, { - onTap: (wx, wy) => { - const z = engine!.cam().z; - select(pickAt(world, wx, wy, z)); - }, - onGrab: () => { - rideRef.current = null; // the user takes the wheel — ride yields - }, - onDouble: (wx, wy) => { - const z = engine!.cam().z; - const p = pickAt(world, wx, wy, z); - if (!p) return; - select(p); - if (p.kind === "eval") { - const ev = world.evals[p.id]; - engine!.flyTo({ x: ev.x, y: ev.y, z: 6 }); - } else if (p.kind === "search") { - const se = world.searches[p.id]; - engine!.flyTo({ x: se.x, y: se.y, z: 60 }); - } else { - engine!.flyTo({ x: world.nx[p.id], y: world.ny[p.id], z: 140 }); - } - }, - onHover: (wx, wy) => { - const z = engine!.cam().z; - const p = pickAt(world, wx, wy, z); - hoverRef.current = p ? { sel: p, wx, wy } : null; - }, - onHoverEnd: () => { - hoverRef.current = null; - }, - onFrame: (dt, cam) => { - /* replay clock — scripted ride first, then full speed */ - if (playingRef.current) { - const ride = rideRef.current; - if (ride) { - ride.t += dt; - while (ride.fired < beats.length - 1 && ride.t >= beats[ride.fired + 1].t) { - ride.fired++; - beats[ride.fired].go(); - } - stepRef.current = rideEase(rideKeysRef.current, ride.t); - if (ride.t >= RIDE_END) rideRef.current = null; - } else { - stepRef.current = Math.min(stepRef.current + dt * SPEED, geomRef.current.maxStep + 1.2); - if (stepRef.current >= geomRef.current.maxStep + 1.2) { - playingRef.current = false; - setPlaying(false); - } - } - } - const st = stepRef.current; - engine!.setStep(st); - - /* HUD */ - const g = geomRef.current; - const si = Math.min(g.maxStep, Math.floor(st)); - if (hudStepRef.current) hudStepRef.current.textContent = `${si} / ${g.maxStep}`; - if (hudPerfRef.current) - hudPerfRef.current.textContent = world.frontierByStep[si].toFixed(4); - if (chartIntRef.current !== si) { - chartIntRef.current = si; - setChartStep(si); - } - - /* chart cursor + trailing frontier */ - const cx = g.sx(Math.min(st, g.maxStep)); - if (cursorRef.current) { - cursorRef.current.setAttribute("x1", `${cx}`); - cursorRef.current.setAttribute("x2", `${cx}`); - } - if (trailRef.current) { - let lastRec = 0; - for (const r of world.records) if (r <= si) lastRec = r; - const y = g.sy(world.frontierByStep[si]); - trailRef.current.setAttribute("x1", `${g.sx(lastRec)}`); - trailRef.current.setAttribute("x2", `${cx}`); - trailRef.current.setAttribute("y1", `${y}`); - trailRef.current.setAttribute("y2", `${y}`); - } - - /* semantic-zoom caption */ - const z = cam.z; - if (zoomCapRef.current) { - const unit = src === "workflow" - ? z < 2.8 - ? "one dot = a candidate" - : z < 13 - ? "candidates · spokes = lineage" - : "one dot = a task · white running · mint passed · red failed" - : world.names - ? z < 2.8 - ? "one dot = a modal run" - : z < 13 - ? "runs · spokes = real lineage" - : "one dot = one of derek's cases · mint right · red wrong" - : z < 2.8 - ? "one dot = a rewrite's evaluation" - : z < 13 - ? "one dot = a search" - : "one dot = a node"; - zoomCapRef.current.textContent = `${unit} · zoom ${z < 10 ? z.toFixed(1) : Math.round(z)}×`; - } - - /* record markers */ - const focus = engine!.getFocus(); - const lod = 1 - smooth(5, 8, z); - world.records.forEach((rs, i) => { - const el = recordEls.current[i]; - if (!el) return; - const ev = world.evals[rs]; - const [x, y] = engine!.worldToCss(ev.x, ev.y); - const dim = focus >= 0 && focus !== ev.id ? 0.25 : 1; - el.style.opacity = `${(st >= ev.step ? 1 : 0) * lod * dim}`; - el.style.transform = `translate3d(${x.toFixed(1)}px,${y.toFixed(1)}px,0)`; - }); - - /* hover ring + caption */ - const hv = hoverRef.current; - if (hoverRingRef.current && hoverCapRef.current) { - if (hv && hv.sel) { - const pos = selPos(hv.sel)!; - const [x, y] = engine!.worldToCss(pos[0], pos[1]); - const r = hv.sel.kind === "eval" ? 15 : hv.sel.kind === "search" ? 11 : 8; - const ring = hoverRingRef.current; - ring.style.opacity = "1"; - ring.style.width = `${r * 2}px`; - ring.style.height = `${r * 2}px`; - ring.style.transform = `translate3d(${(x - r).toFixed(1)}px,${(y - r).toFixed(1)}px,0)`; - const cap = hoverCapRef.current; - cap.style.opacity = "1"; - cap.style.transform = `translate3d(${(x + 14).toFixed(1)}px,${(y - 26).toFixed(1)}px,0)`; - cap.textContent = hoverLabel(hv.sel); - } else { - hoverRingRef.current.style.opacity = "0"; - hoverCapRef.current.style.opacity = "0"; - } - } - - /* selection ring */ - const s = selRef.current; - if (selRingRef.current) { - if (s) { - const pos = selPos(s)!; - const [x, y] = engine!.worldToCss(pos[0], pos[1]); - const r = s.kind === "eval" ? 17 : s.kind === "search" ? 12 : 9; - const ring = selRingRef.current; - ring.style.opacity = "1"; - ring.style.width = `${r * 2}px`; - ring.style.height = `${r * 2}px`; - ring.style.transform = `translate3d(${(x - r).toFixed(1)}px,${(y - r).toFixed(1)}px,0)`; - } else selRingRef.current.style.opacity = "0"; - } - }, - }); - - if (!engine) { - setGlFail(true); - return; - } - engineRef.current = engine; - if (src === "workflow" && liveCamRef.current) engine.flyTo(liveCamRef.current); - if (process.env.NODE_ENV === "development") - (window as unknown as { __orchard?: unknown }).__orchard = { engine, world }; - - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") select(null); - }; - window.addEventListener("keydown", onKey); - return () => { - window.removeEventListener("keydown", onKey); - if (src === "workflow") liveCamRef.current = engine.cam(); - engine.dispose(); - engineRef.current = null; - }; - // select is stable (useCallback on [world]); world is memoized once - }, [world, select, src]); - - /* ---- chart scrub ---- */ - const scrub = useCallback((e: React.PointerEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const x = ((e.clientX - rect.left) / rect.width) * CW; - const m = geomRef.current.maxStep; - const s = Math.min(m, Math.max(0, ((x - PX) / (CW - 2 * PX)) * m)); - stepRef.current = s; - rideRef.current = null; - playingRef.current = false; - setPlaying(false); - }, []); - - const replay = useCallback(() => { - if (playingRef.current) { - playingRef.current = false; - setPlaying(false); - return; - } - stepRef.current = 0; - rideRef.current = { t: 0, fired: -1 }; // the ride fires beat 0 on the next frame - playingRef.current = true; - setPlaying(true); - }, []); - - /* ---- selection panel content ---- */ - const panel = useMemo(() => { - if (!sel) - return src === "workflow" ? ( - <> - workflow event stream - - {world.evals.length} candidates · {world.nodeCount} tasks - - - {liveEvents.length} redacted events · {liveStatus} - - - white running · mint passed · red failed · cursor refresh 1s - - - ) : ( - <> - the run - - 99 rewrites proposed · {totals.survived} survived - - - {totals.searches.toLocaleString()} searches · {totals.nodes.toLocaleString()} nodes ·{" "} - 0.7032 → 0.7784 - - - drag to pan · wheel to dive · double-click to fly · chart scrubs the replay - - - ); - if (sel.kind === "eval") { - const ev = world.evals[sel.id]; - return ( - <> - - {nameOf(ev.id)} - - {ev.record ? ( - - ⬡ moved the record - - ) : ( - did not survive - )} - {ev.perf.toFixed(4)} - {ev.rule} - - {world.names - ? ev.parent >= 0 - ? `built on ${nameOf(ev.parent)}` - : "the first run" - : `${ev.s1 - ev.s0} searches · ${(ev.n1 - ev.n0).toLocaleString()} nodes` + - (ev.parent >= 0 ? ` · rewrote aide_${ev.parent}` : " · the seed")} - - - ); - } - if (sel.kind === "search") { - const se = world.searches[sel.id]; - const ev = world.evals[se.eval]; - return ( - <> - - {nameOf(ev.id)} · s{se.id - ev.s0} - - - {se.best.toFixed(4)} - - - best of {se.n1 - se.n0} nodes · depth {se.depth} - - - ); - } - const gi = sel.id; - const se = world.searches[world.nsearch[gi]]; - const ev = world.evals[se.eval]; - const st = world.nstate[gi]; - return ( - <> - - {nameOf(ev.id)} · s{se.id - ev.s0} · n{gi - se.n0} - - {world.nlabel ? ( - <> - - {world.nlabel[gi]} - - {world.ntrace?.[gi] && ( - - {world.ntrace[gi].replace(/\s+/g, " ").replace(/^Source: (.+?) Post: ?/, "$1 · ")} - - )} - - ) : ( - <> - - {world.nscore[gi].toFixed(4)} - - - {NODE_STATE[st]} - - - )} - - ); - }, [sel, world, totals, nameOf, src, liveEvents.length, liveStatus]); - - /* ---- breadcrumb ---- */ - const crumb = useMemo(() => { - const parts: { label: string; act: () => void }[] = [ - { - label: "run", - act: () => { - select(null); - engineRef.current?.fitAll(); - }, - }, - ]; - if (sel) { - const ei = evalOf(world, sel); - parts.push({ label: nameOf(ei), act: () => flyToEval(ei) }); - if (sel.kind !== "eval") { - const sid = sel.kind === "search" ? sel.id : world.nsearch[sel.id]; - const se = world.searches[sid]; - parts.push({ - label: `s${sid - world.evals[se.eval].s0}`, - act: () => { - select({ kind: "search", id: sid }); - engineRef.current?.flyTo({ x: se.x, y: se.y, z: 60 }); - }, - }); - if (sel.kind === "node") parts.push({ label: `n${sel.id - se.n0}`, act: () => {} }); - } - } - return parts; - }, [sel, world, select, flyToEval, nameOf]); - - /* ---- chart data ---- */ - const frontierSegs = useMemo(() => { - const segs: { x1: number; y1: number; x2: number; y2: number; at: number }[] = []; - for (let i = 1; i < world.records.length; i++) { - const a = world.evals[world.records[i - 1]]; - const b = world.evals[world.records[i]]; - segs.push({ x1: geom.sx(a.step), y1: geom.sy(a.frontier), x2: geom.sx(b.step), y2: geom.sy(a.frontier), at: b.step }); - segs.push({ x1: geom.sx(b.step), y1: geom.sy(a.frontier), x2: geom.sx(b.step), y2: geom.sy(b.frontier), at: b.step }); - } - return segs; - }, [world, geom]); - - return ( -
-
- - -

- every rewrite of the agent plants a grove — dozens of tree searches over the same - benchmark. records walk the green spine; everything else orbits the record it failed - to beat. one field, three scales: the climb, the grove, the node. -

- - {/* ---- stage ---- */} -
- {glFail ? ( -
- webgl2 unavailable — the orchard needs it -
- ) : ( - - )} - - {/* overlay: rings, labels, captions — words live in the DOM */} -
- {world.records.map((rs, i) => { - const ev = world.evals[rs]; - return ( -
{ - recordEls.current[i] = el; - }} - style={{ position: "absolute", left: 0, top: 0, willChange: "transform", opacity: 0 }} - > - - {rs === 0 ? ( - - ) : ( - - )} - - - {nameOf(ev.id)} {ev.perf.toFixed(4)} - -
- ); - })} -
-
-
-
- - {/* HUD */} -
-
-
step
-
-
-
-
performance
-
-
-
- -
- - -
- -
-
- - {/* ---- selection / summary strip ---- */} -
- {panel} -
- - {/* ---- the outer loop, climb idiom — doubles as the scrubber ---- */} -
-
- - outer loop · rewrites of the agent - - - performance ↑ · click to scrub -
- { - e.currentTarget.setPointerCapture(e.pointerId); - scrub(e); - }} - onPointerMove={(e) => { - if (e.buttons) scrub(e); - }} - > - {geom.yTicks.map((v) => ( - - - - {v.toFixed(2)} - - - ))} - {geom.xTicks.map((s) => ( - - {s} - - ))} - - {/* honest scatter */} - {world.evals.map((ev) => - ev.record ? null : ( - = ev.step ? 1 : 0, transition: "opacity 300ms var(--ease)" }} - /> - ) - )} - {/* frontier steps */} - {frontierSegs.map((s, i) => ( - = s.at ? 0.9 : 0, transition: "opacity 400ms var(--ease)" }} - /> - ))} - {/* trailing frontier to the cursor */} - - {/* records */} - {world.records.map((rs) => { - const ev = world.evals[rs]; - return ( - = ev.step ? 1 : 0, transition: "opacity 300ms var(--ease)", cursor: "pointer", pointerEvents: "auto" }} - onClick={(e) => { - e.stopPropagation(); - flyToEval(ev.id); - }} - /> - ); - })} - - -
- - {/* legend */} -
- {[ - [INC_C, "search node"], - [CAND_C, "best of its search"], - [BAD, "dead end"], - ["rgba(242,242,240,0.6)", "a search · a rewrite"], - [AMBER, "⬡ moved the record"], - [GOOD, "the frontier"], - ].map(([c, t]) => ( - - - {t} - - ))} -
-
-
- ); -} diff --git a/apps/homescreen/app/cedar/flows/orchard/real.ts b/apps/homescreen/app/cedar/flows/orchard/real.ts deleted file mode 100644 index 91bf7bb9..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/real.ts +++ /dev/null @@ -1,389 +0,0 @@ -/* 20 · orchard — canonical experiment records rendered as a field. - * Every scored run is a dot in launch order, records walk the spine when - * a run raises the best score on the frozen dev set, and failed proposals - * orbit the run they actually built on (parent_run_id — real lineage, not - * decoration). Runs on other benchmarks appear with their honest scores - * but never set records — different denominators do not compare. - * - * One search dot per run carries its architecture. Where a run's own - * artifact carries per-case grades (input_id/expected/predicted/correct - * — the fireworks holdout format), the cases become the node level: - * zoom into that run and see exactly which of Derek's rows it got - * right (mint) and wrong (red), same disc position for the same case - * across runs. No grades in the artifact → no nodes; nothing is - * inferred. - * - * Data is supplied by the redacted Workflow event projection in live.ts. - */ - -import { - AMBER, - BAND_EVAL, - BAND_NODE, - BAND_SEARCH, - BAND_SPINE, - BAND_SPOKE, - EVAL_R, - GOLDEN, - GREEN, - INK, - MINT, - mulberry32, - RED, - SPINE_GAP, - SPREAD_NODE, - SPREAD_NONE, - SPREAD_SEARCH, - STRIDE, - type Band, - type EvalMeta, - type RGB, - type SearchMeta, - type World, -} from "./run"; - -export type OrchardRun = { - id: string; - parent: string | null; - family: string; - arch: string; - status: string; - at: string | null; - acc: number | null; - balanced: number | null; - bench: number | null; - cost: number | null; - elapsed: number | null; - /* per-case grades where the run's artifact carries them, enriched - * with canonical label names and the raw post text */ - cases?: { - id: string; - ok: boolean; - state?: "not_started" | "running" | "succeeded" | "failed" | "timed_out"; - exp: string | number | null; - got: string | number | null; - expL?: string; - gotL?: string; - text?: string; - }[]; -}; - -export type OrchardRunSet = { - campaign: string; - as_of: string; - primary_bench: number; - spend_usd: number | null; - runs: OrchardRun[]; -}; - -export function buildRealWorld( - data: OrchardRunSet, - options: { includeUnscored?: boolean } = {}, -): World { - const rand = mulberry32(289); - const runs = data.runs.filter((r) => options.includeUnscored || r.acc !== null); - const primary = data.primary_bench; - - /* frontier + records over the primary (frozen dev) benchmark only */ - const records: number[] = []; - const frontierByStep = new Float32Array(runs.length); - let best = -Infinity; - runs.forEach((r, i) => { - const score = r.acc ?? (Number.isFinite(best) ? best : 0); - if (r.acc !== null && r.bench === primary && score > best) { - best = score; - records.push(i); - } - frontierByStep[i] = best; - }); - // steps before the first primary-bench run inherit its baseline - const firstRec = records.length ? frontierByStep[records[0]] : 0; - for (let i = 0; i < runs.length && frontierByStep[i] === -Infinity; i++) - frontierByStep[i] = firstRec; - - /* spine walk for records, same meander as the synthetic field */ - const recPos: { x: number; y: number }[] = []; - { - let hx = 0.35, - hy = 0.55, - px = 0, - py = 0; - recPos.push({ x: 0, y: 0 }); - for (let i = 1; i < records.length; i++) { - const turn = (rand() - 0.5) * 1.1; - const c = Math.cos(turn), - s = Math.sin(turn); - [hx, hy] = [hx * c - hy * s, hx * s + hy * c]; - const m = Math.hypot(hx, hy); - hx /= m; - hy /= m; - px += hx * SPINE_GAP; - py += hy * SPINE_GAP; - recPos.push({ x: px, y: py }); - } - } - - /* place evals: records on the spine; everything else orbits its REAL - * parent when we have one, else the latest record */ - const evals: EvalMeta[] = []; - const searches: SearchMeta[] = []; - const names: string[] = []; - const idToStep = new Map(runs.map((r, i) => [r.id, i])); - const recordSet = new Set(records); - const orbitCount = new Map(); - let latestRec = 0; - - /* node SoA — real graded cases, for the runs that have them. A case - * keeps the same disc position in every run so failure maps compare - * spatially across runs. */ - const totalCases = runs.reduce((a, r) => a + (r.cases?.length ?? 0), 0); - const nx = new Float32Array(totalCases); - const ny = new Float32Array(totalCases); - const nscore = new Float32Array(totalCases); - const nstate = new Uint8Array(totalCases); - const nsearch = new Int32Array(totalCases); - const nlabel: string[] = new Array(totalCases); - const ntrace: string[] = new Array(totalCases); - const caseSlot = new Map(); // case id → shared disc slot - let nodePtr = 0; - - runs.forEach((r, i) => { - const isRec = recordSet.has(i); - let cx: number, cy: number; - let anchor = -1; - if (isRec) { - const p = recPos[records.indexOf(i)]; - cx = p.x; - cy = p.y; - anchor = latestRec === i ? -1 : records[Math.max(0, records.indexOf(i) - 1)]; - } else { - const parentStep = r.parent != null ? idToStep.get(r.parent) : undefined; - anchor = parentStep !== undefined && parentStep < i ? parentStep : records.length ? latestRec : 0; - const a = evals[anchor] ?? { x: 0, y: 0 }; - const k = orbitCount.get(anchor) ?? 0; - orbitCount.set(anchor, k + 1); - const ang = k * GOLDEN + anchor * 1.7 + rand() * 0.35; - const rad = 30 + 13 * Math.sqrt(k + 1) + rand() * 6; - cx = a.x + Math.cos(ang) * rad; - cy = a.y + Math.sin(ang) * rad; - } - - const benchNote = - r.bench === primary ? `${r.bench}-row dev set` : `${r.bench}-row benchmark — off the record board`; - const rule = [r.arch || r.family, benchNote, r.cost != null ? `$${r.cost.toFixed(2)}` : null] - .filter(Boolean) - .join(" · "); - - /* graded cases → nodes on a disc around the run */ - const n0 = nodePtr; - if (r.cases) { - const nCases = r.cases.length; - for (const c of r.cases) { - let slot = caseSlot.get(c.id); - if (slot === undefined) { - slot = caseSlot.size; - caseSlot.set(c.id, slot); - } - const ang = slot * GOLDEN; - const rad = EVAL_R * 0.78 * Math.sqrt(((slot % nCases) + 0.5) / nCases); - const gi = nodePtr++; - nx[gi] = cx + Math.cos(ang) * rad; - ny[gi] = cy + Math.sin(ang) * rad; - nscore[gi] = typeof c.got === "number" ? c.got : -1; - nstate[gi] = c.state === "running" ? 4 : c.ok ? 1 : 3; - nsearch[gi] = i; - const expName = c.expL ?? `label ${c.exp ?? "?"}`; - const gotName = c.gotL ?? `label ${c.got ?? "?"}`; - nlabel[gi] = c.state === "running" - ? `running · ${c.id}` - : c.ok - ? `right · “${expName}”` - : `wrong · expected “${expName}” → said “${gotName}”`; - ntrace[gi] = c.text ?? ""; - } - } - - evals.push({ - id: i, - step: i, - x: cx, - y: cy, - perf: r.acc ?? (Number.isFinite(frontierByStep[i]) ? frontierByStep[i] : 0), - record: isRec, - parent: anchor, - rule, - frontier: frontierByStep[i], - s0: i, - s1: i + 1, - n0, - n1: nodePtr, - }); - names.push(r.family.toLowerCase()); - searches.push({ - id: i, - eval: i, - x: cx, - y: cy - EVAL_R * 0.35, - best: r.acc ?? (Number.isFinite(frontierByStep[i]) ? frontierByStep[i] : 0), - bestNode: -1, // cases have no "best" — the page's ride uses the disc itself - n0, - n1: nodePtr, - depth: 0, - }); - if (isRec) latestRec = i; - }); - - /* ---- pack vertex buffers (same layout as run.ts) ---- */ - const pointCount = evals.length + searches.length + totalCases; - const points = new Float32Array(pointCount * STRIDE); - const spokeEdges = evals.length - records.length; - const spineEdges = Math.max(0, records.length - 1); - const lineVertCount = (spokeEdges + spineEdges) * 2; - const lines = new Float32Array(lineVertCount * STRIDE); - - let p = 0; - const put = ( - buf: Float32Array, - at: number, - x: number, - y: number, - ax: number, - ay: number, - bx: number, - by: number, - step: number, - size: number, - min: number, - max: number, - rgb: RGB, - alpha: number, - band: Band, - spread: [number, number], - evalId: number - ) => { - const o = at * STRIDE; - buf[o] = x; - buf[o + 1] = y; - buf[o + 2] = ax; - buf[o + 3] = ay; - buf[o + 4] = bx; - buf[o + 5] = by; - buf[o + 6] = step; - buf[o + 7] = size; - buf[o + 8] = min; - buf[o + 9] = max; - buf[o + 10] = rgb[0]; - buf[o + 11] = rgb[1]; - buf[o + 12] = rgb[2]; - buf[o + 13] = alpha; - buf[o + 14] = band[0]; - buf[o + 15] = band[1]; - buf[o + 16] = band[2]; - buf[o + 17] = band[3]; - buf[o + 18] = spread[0]; - buf[o + 19] = spread[1]; - buf[o + 20] = evalId; - }; - - /* case nodes — a run's real failure map, sprouting from its dot */ - for (let gi = 0; gi < totalCases; gi++) { - const ev = evals[nsearch[gi]]; - const ok = nstate[gi] === 1; - const running = nstate[gi] === 4; - put( - points, - p++, - nx[gi], - ny[gi], - ev.x, - ev.y, - ev.x, - ev.y, - ev.step + 0.1 + ((gi - ev.n0) / Math.max(1, ev.n1 - ev.n0)) * 0.8, - 0.07, - 1.7, - 22, - running ? INK : ok ? MINT : RED, - running ? 0.95 : ok ? 0.8 : 0.85, - BAND_NODE, - SPREAD_NODE, - ev.id - ); - } - /* search dots — one per run, its architecture made visible */ - for (const se of searches) { - const ev = evals[se.eval]; - put(points, p++, se.x, se.y, ev.x, ev.y, ev.x, ev.y, ev.step + 0.05, 0.9, 2, 26, INK, 0.55, BAND_SEARCH, SPREAD_SEARCH, ev.id); - } - /* eval dots — sprout from their real parent */ - for (const ev of evals) { - const par = ev.parent >= 0 ? evals[ev.parent] : ev; - put( - points, - p++, - ev.x, - ev.y, - ev.x, - ev.y, - par.x, - par.y, - ev.step, - ev.record ? 4 : 2.2, - ev.record ? 9 : 5.5, - ev.record ? 15 : 9, - ev.record ? AMBER : INK, - ev.record ? 0.95 : 0.5, - BAND_EVAL, - SPREAD_NONE, - ev.id - ); - } - - let l = 0; - /* spokes: real lineage — a run points at what it built on */ - for (const ev of evals) { - if (ev.record || ev.parent < 0) continue; - const par = evals[ev.parent]; - put(lines, l++, par.x, par.y, par.x, par.y, par.x, par.y, ev.step, 0, 0, 0, INK, 0.1, BAND_SPOKE, SPREAD_NONE, ev.id); - put(lines, l++, ev.x, ev.y, ev.x, ev.y, par.x, par.y, ev.step, 0, 0, 0, INK, 0.1, BAND_SPOKE, SPREAD_NONE, ev.id); - } - /* spine: record → record */ - for (let i = 1; i < records.length; i++) { - const a = evals[records[i - 1]]; - const b = evals[records[i]]; - put(lines, l++, a.x, a.y, a.x, a.y, a.x, a.y, b.step, 0, 0, 0, GREEN, 0.55, BAND_SPINE, SPREAD_NONE, b.id); - put(lines, l++, b.x, b.y, b.x, b.y, a.x, a.y, b.step, 0, 0, 0, GREEN, 0.55, BAND_SPINE, SPREAD_NONE, b.id); - } - - let x0 = Infinity, - y0 = Infinity, - x1 = -Infinity, - y1 = -Infinity; - for (const ev of evals) { - x0 = Math.min(x0, ev.x - EVAL_R - 8); - y0 = Math.min(y0, ev.y - EVAL_R - 8); - x1 = Math.max(x1, ev.x + EVAL_R + 8); - y1 = Math.max(y1, ev.y + EVAL_R + 8); - } - - return { - evals, - searches, - records, - nx, - ny, - nscore, - nstate, - nsearch, - nodeCount: totalCases, - points, - lines, - pointCount, - lineVertCount: l, - frontierByStep, - bounds: { x0, y0, x1, y1 }, - names, - nlabel, - ntrace, - }; -} diff --git a/apps/homescreen/app/cedar/flows/orchard/run.ts b/apps/homescreen/app/cedar/flows/orchard/run.ts deleted file mode 100644 index 555d1deb..00000000 --- a/apps/homescreen/app/cedar/flows/orchard/run.ts +++ /dev/null @@ -1,608 +0,0 @@ -/* 20 · orchard — synthetic run generator + world layout. - * - * One seeded AIDE-style outer loop: aide_0 plus 99 rewrite proposals, - * seven of which move the record. Every proposal is an evaluation — a - * grove of dozens of solution searches, each search a tree of nodes. - * Everything is laid out once in a single continuous world so the - * renderer can morph between scales instead of switching views: - * - * meta records walk a meandering spine; failed proposals - * orbit the record they tried to beat - * grove searches sit on a phyllotaxis disc around the proposal - * tree tidy top-down layout inside each search's slot - * - * The generator is deterministic (mulberry32) so replay, layout and - * picking agree across mounts. A real-run adapter only needs to emit - * the same World shape. - */ - -export const RECORD_STEPS = [2, 6, 28, 39, 47, 63, 85]; -export const MAX_STEP = 99; -export const BASE_PERF = 0.7032; -export const FINAL_PERF = 0.7784; - -/* world-space constants shared with the engine's LOD bands */ -export const EVAL_R = 9; // grove disc radius -export const SPINE_GAP = 150; // distance between consecutive records - -export type NodeState = 0 | 1 | 2 | 3 | 4; // root · done · best · dead · running - -export type EvalMeta = { - id: number; - step: number; - x: number; - y: number; - perf: number; - record: boolean; - parent: number; // eval id of the record this proposal built on (-1 for aide_0) - rule: string; - frontier: number; // record value as of this step - s0: number; - s1: number; // search range [s0, s1) - n0: number; - n1: number; // node range [n0, n1) -}; - -export type SearchMeta = { - id: number; - eval: number; - x: number; - y: number; - best: number; // best node score in this search - bestNode: number; - n0: number; - n1: number; - depth: number; -}; - -export type World = { - evals: EvalMeta[]; - searches: SearchMeta[]; - records: number[]; // eval ids, chronological (starts with aide_0) - /* node SoA for picking + inspection */ - nx: Float32Array; - ny: Float32Array; - nscore: Float32Array; - nstate: Uint8Array; - nsearch: Int32Array; - nodeCount: number; - /* interleaved vertex data, STRIDE floats per vertex */ - points: Float32Array; - lines: Float32Array; - pointCount: number; - lineVertCount: number; - frontierByStep: Float32Array; // record value after each outer step - bounds: { x0: number; y0: number; x1: number; y1: number }; - /* real-data adapters may name evals (index = eval id); synthetic - * worlds leave this unset and the page falls back to aide_N */ - names?: string[]; - /* real-data adapters may caption nodes (index = global node id) — - * e.g. "case 2296 · label 31 → 104 · wrong" */ - nlabel?: string[]; - /* raw trace per node — the actual input the case graded (shown at - * case zoom on hover/select) */ - ntrace?: string[]; -}; - -/* vertex layout — keep in sync with engine.ts attribute table */ -export const STRIDE = 21; -// aPos(2) aAnchor(2) aBirth(2) aStep aSize aMin aMax aColor(3) aAlpha aBand(4) aSpread(2) aEval - -export const GOLDEN = Math.PI * (3 - Math.sqrt(5)); - -/* palette in linear-ish rgb (matches flows/lib hexes) */ -export const MINT: RGB = [0.62, 0.86, 0.83]; // #9edbd3 — a finished node -export const VIOLET: RGB = [0.655, 0.545, 0.98]; // #a78bfa — best of its search -export const INK: RGB = [0.949, 0.949, 0.941]; // #f2f2f0 -export const RED: RGB = [0.898, 0.325, 0.294]; // #e5534b — dead end -export const AMBER: RGB = [0.949, 0.702, 0.298]; // #f2b34c — record proposal -export const GREEN: RGB = [0.431, 0.906, 0.627]; // #6ee7a0 — the frontier -export type RGB = [number, number, number]; - -/* LOD bands (css-px per world unit): [fadeIn0, fadeIn1, fadeOut0, fadeOut1] */ -const ALWAYS_IN = -1; -const NEVER_OUT = 9e8; -export const BAND_EVAL: Band = [ALWAYS_IN, 0, 2.6, 4.8]; -export const BAND_SPOKE: Band = [ALWAYS_IN, 0, 2.8, 4.6]; -export const BAND_SPINE: Band = [ALWAYS_IN, 0, 4.5, 7.5]; -export const BAND_SEARCH: Band = [1.4, 2.8, 7, 13]; -export const BAND_NODE: Band = [3.5, 8, NEVER_OUT, NEVER_OUT + 1]; -const BAND_EDGE: Band = [7, 16, NEVER_OUT, NEVER_OUT + 1]; -export type Band = [number, number, number, number]; - -/* collapse thresholds: below spread0 the vertex sits on its anchor */ -export const SPREAD_NONE: [number, number] = [0, 0.0001]; -export const SPREAD_SEARCH: [number, number] = [1.1, 2.5]; -export const SPREAD_NODE: [number, number] = [2.2, 6.5]; - -const WIN_RULES = [ - "demands a root-cause diagnosis before any fix", - "caps retries at two — banks the failing diff instead", - "adds a plan critic that vetoes single-file rewrites", - "seeds each search from the last surviving patch", - "runs the evaluator twice and trusts only agreement", - "shrinks the edit window to the failing hunk", - "prefers reverts over repairs past depth four", -]; - -const FAIL_RULES = [ - "swaps the planner for freeform chain-of-thought", - "doubles the search width — drowns in duplicates", - "lets the agent edit its own harness mid-run", - "drops the diff summary from the context", - "raises temperature on retries", - "prunes any branch that regresses once", - "batches evaluations to save tokens", - "asks for three candidate fixes per node", - "trims the transcript to the last two turns", - "rewrites the whole file on every proposal", - "adds a self-score the search learns to game", - "caches evaluator verdicts across branches", -]; - -export function mulberry32(seed: number) { - let a = seed >>> 0; - return () => { - a |= 0; - a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -/* one tree: parents biased to chain (long droops) with occasional fans */ -function growTree(rand: () => number, n: number) { - const parent = new Int32Array(n).fill(-1); - for (let i = 1; i < n; i++) { - if (rand() < 0.55) parent[i] = i - 1; - else { - const a = Math.floor(rand() * i); - const b = Math.floor(rand() * i); - parent[i] = Math.min(a, b); // bias to earlier (shallower) nodes - } - } - return parent; -} - -/* tidy top-down layout into a unit box: x by leaf slots, y by depth */ -function layoutTree(parent: Int32Array) { - const n = parent.length; - const kids: number[][] = Array.from({ length: n }, () => []); - for (let i = 1; i < n; i++) kids[parent[i]].push(i); - const depth = new Float32Array(n); - for (let i = 1; i < n; i++) depth[i] = depth[parent[i]] + 1; - const x = new Float32Array(n); - let slot = 0; - const walk = (v: number) => { - if (kids[v].length === 0) { - x[v] = slot++; - return; - } - for (const k of kids[v]) walk(k); - x[v] = (x[kids[v][0]] + x[kids[v][kids[v].length - 1]]) / 2; - }; - walk(0); - const leaves = slot; // number of leaf slots - let maxD = 1; - for (let i = 0; i < n; i++) maxD = Math.max(maxD, depth[i]); - return { x, depth, leaves, maxD, kids }; -} - -export function generateRun(seed = 20260715): World { - const rand = mulberry32(seed); - const gauss = () => (rand() + rand() + rand()) / 1.5 - 1; // ~[-1,1] - - /* ---- outer loop: perf trajectory ---- */ - const recordSet = new Set(RECORD_STEPS); - const frontierByStep = new Float32Array(MAX_STEP + 1); - const perfByStep = new Float32Array(MAX_STEP + 1); - // diminishing record deltas that land exactly on FINAL_PERF - const weights = RECORD_STEPS.map((_, i) => 1 / (i + 1.35)); - const wSum = weights.reduce((a, b) => a + b, 0); - const gain = FINAL_PERF - BASE_PERF; - let frontier = BASE_PERF; - const recPerf = new Map(); - RECORD_STEPS.forEach((s, i) => { - frontier += (gain * weights[i]) / wSum; - recPerf.set(s, frontier); - }); - frontier = BASE_PERF; - for (let s = 0; s <= MAX_STEP; s++) { - if (recordSet.has(s)) frontier = recPerf.get(s)!; - frontierByStep[s] = frontier; - if (s === 0) perfByStep[s] = BASE_PERF; - else if (recordSet.has(s)) perfByStep[s] = frontier; - else { - // honest scatter under the line, a few catastrophes - const drop = - rand() < 0.12 ? 0.02 + rand() * 0.05 : 0.002 + Math.abs(gauss()) * 0.014; - perfByStep[s] = Math.max(0.688, frontierByStep[s] - drop); - } - } - - /* ---- meta layout: records walk a meandering spine ---- */ - const recPos: { x: number; y: number }[] = []; - let hx = 0.35, - hy = 0.55; // heading, roughly down-right - let px = 0, - py = 0; - recPos.push({ x: 0, y: 0 }); // aide_0 - for (let i = 0; i < RECORD_STEPS.length; i++) { - const turn = (rand() - 0.5) * 1.1; - const c = Math.cos(turn), - s = Math.sin(turn); - [hx, hy] = [hx * c - hy * s, hx * s + hy * c]; - const m = Math.hypot(hx, hy); - hx /= m; - hy /= m; - px += hx * SPINE_GAP; - py += hy * SPINE_GAP; - recPos.push({ x: px, y: py }); - } - - /* ---- evaluations ---- */ - const evals: EvalMeta[] = []; - const searches: SearchMeta[] = []; - const records: number[] = []; - const orbitCount = new Map(); // per parent record - let winRule = 0, - failRule = 0; - - // provisional counts to size arrays - const searchCountFor: number[] = []; - const nodeCountFor: number[][] = []; - let totalNodes = 0; - for (let s = 0; s <= MAX_STEP; s++) { - const nS = 18 + Math.floor(rand() * 25); // 18..42 searches - const perSearch: number[] = []; - for (let i = 0; i < nS; i++) { - const nN = 12 + Math.floor(rand() * 59); // 12..70 nodes - perSearch.push(nN); - totalNodes += nN; - } - searchCountFor.push(nS); - nodeCountFor.push(perSearch); - } - - const nx = new Float32Array(totalNodes); - const ny = new Float32Array(totalNodes); - const nscore = new Float32Array(totalNodes); - const nstate = new Uint8Array(totalNodes); - const nsearch = new Int32Array(totalNodes); - const nparent = new Int32Array(totalNodes); - const nstep = new Float32Array(totalNodes); - - let nodePtr = 0; - let recIdx = 0; // index into recPos of current record - let parentEval = -1; - - for (let s = 0; s <= MAX_STEP; s++) { - const isRec = s === 0 || recordSet.has(s); - let cx: number, cy: number; - if (isRec) { - const p = recPos[s === 0 ? 0 : RECORD_STEPS.indexOf(s) + 1]; - cx = p.x; - cy = p.y; - } else { - const anchor = recPos[recIdx]; - const k = orbitCount.get(recIdx) ?? 0; - orbitCount.set(recIdx, k + 1); - const ang = k * GOLDEN + recIdx * 1.7 + rand() * 0.35; - const rad = 30 + 13 * Math.sqrt(k + 1) + rand() * 6; - cx = anchor.x + Math.cos(ang) * rad; - cy = anchor.y + Math.sin(ang) * rad; - } - - const rule = - s === 0 - ? "the seed agent — hand-written harness" - : isRec - ? WIN_RULES[winRule++ % WIN_RULES.length] - : FAIL_RULES[failRule++ % FAIL_RULES.length]; - - const ev: EvalMeta = { - id: s, - step: s, - x: cx, - y: cy, - perf: perfByStep[s], - record: isRec, - parent: parentEval, - rule, - frontier: frontierByStep[s], - s0: searches.length, - s1: searches.length + searchCountFor[s], - n0: nodePtr, - n1: nodePtr, - }; - - /* grove: searches on a phyllotaxis disc */ - const nS = searchCountFor[s]; - for (let i = 0; i < nS; i++) { - const ang = i * GOLDEN + s * 0.9; - const rad = EVAL_R * Math.sqrt((i + 0.5) / nS); - const sx = cx + Math.cos(ang) * rad; - const sy = cy + Math.sin(ang) * rad; - const nN = nodeCountFor[s][i]; - const parent = growTree(rand, nN); - const lay = layoutTree(parent); - const boxW = Math.min(2.4, Math.max(0.8, lay.leaves * 0.11)); - const boxH = Math.min(2.6, Math.max(0.9, lay.maxD * 0.16)); - const n0 = nodePtr; - - /* scores: random walk from the era frontier */ - const base = frontierByStep[Math.max(0, s - 1)] - 0.004; - const xDenom = Math.max(1, lay.leaves - 1); - for (let j = 0; j < nN; j++) { - const gi = nodePtr; - nsearch[gi] = searches.length; - nparent[gi] = j === 0 ? -1 : n0 + parent[j]; - nscore[gi] = - j === 0 ? base : nscore[nparent[gi]] + gauss() * 0.006 + 0.0008; - const xn = lay.leaves > 1 ? lay.x[j] / xDenom : 0.5; - nx[gi] = sx + (xn - 0.5) * boxW + gauss() * 0.045; - ny[gi] = sy - boxH / 2 + (lay.depth[j] / lay.maxD) * boxH + gauss() * 0.045; - nodePtr++; - } - /* states: best of search, dead leaves */ - let bestNode = n0, - best = -Infinity; - for (let j = 0; j < nN; j++) { - const gi = n0 + j; - if (nscore[gi] > best) { - best = nscore[gi]; - bestNode = gi; - } - } - for (let j = 0; j < nN; j++) { - const gi = n0 + j; - const isLeaf = lay.kids[j].length === 0; - nstate[gi] = - j === 0 - ? 0 - : gi === bestNode - ? 2 - : isLeaf && nscore[gi] < nscore[nparent[gi]] - 0.004 - ? 3 - : 1; - } - searches.push({ - id: searches.length, - eval: s, - x: sx, - y: sy, - best, - bestNode, - n0, - n1: nodePtr, - depth: lay.maxD, - }); - } - ev.n1 = nodePtr; - - /* nudge scores so the eval's best equals its reported perf */ - let evBest = -Infinity; - for (let gi = ev.n0; gi < ev.n1; gi++) evBest = Math.max(evBest, nscore[gi]); - const off = ev.perf - evBest; - for (let gi = ev.n0; gi < ev.n1; gi++) nscore[gi] += off; - for (let si = ev.s0; si < ev.s1; si++) searches[si].best += off; - - /* reveal order: searches grow in parallel (round-robin by depth-in-tree) */ - const spans: [number, number][] = []; - for (let si = ev.s0; si < ev.s1; si++) spans.push([searches[si].n0, searches[si].n1]); - const order: number[] = []; - for (let j = 0; ; j++) { - let any = false; - for (const [a, b] of spans) - if (a + j < b) { - order.push(a + j); - any = true; - } - if (!any) break; - } - order.forEach((gi, k) => { - nstep[gi] = s + 0.05 + 0.85 * (k / order.length); - }); - - evals.push(ev); - if (isRec) { - records.push(s); - recIdx = records.length - 1; - parentEval = s; - } - } - - /* ---- pack vertex buffers ---- */ - const searchVerts = searches.length; - const evalVerts = evals.length; - const pointCount = totalNodes + searchVerts + evalVerts; - const points = new Float32Array(pointCount * STRIDE); - - let treeEdges = 0; - for (let gi = 0; gi < totalNodes; gi++) if (nparent[gi] >= 0) treeEdges++; - const spokeEdges = evals.length - records.length; - const spineEdges = records.length - 1; - const lineVertCount = (treeEdges + spokeEdges + spineEdges) * 2; - const lines = new Float32Array(lineVertCount * STRIDE); - - let p = 0; - const put = ( - buf: Float32Array, - at: number, - x: number, - y: number, - ax: number, - ay: number, - bx: number, - by: number, - step: number, - size: number, - min: number, - max: number, - rgb: RGB, - alpha: number, - band: Band, - spread: [number, number], - evalId: number - ) => { - const o = at * STRIDE; - buf[o] = x; - buf[o + 1] = y; - buf[o + 2] = ax; - buf[o + 3] = ay; - buf[o + 4] = bx; - buf[o + 5] = by; - buf[o + 6] = step; - buf[o + 7] = size; - buf[o + 8] = min; - buf[o + 9] = max; - buf[o + 10] = rgb[0]; - buf[o + 11] = rgb[1]; - buf[o + 12] = rgb[2]; - buf[o + 13] = alpha; - buf[o + 14] = band[0]; - buf[o + 15] = band[1]; - buf[o + 16] = band[2]; - buf[o + 17] = band[3]; - buf[o + 18] = spread[0]; - buf[o + 19] = spread[1]; - buf[o + 20] = evalId; - }; - - /* nodes */ - for (let gi = 0; gi < totalNodes; gi++) { - const se = searches[nsearch[gi]]; - const ev = evals[se.eval]; - const st = nstate[gi]; - const rgb = st === 0 ? INK : st === 2 ? VIOLET : st === 3 ? RED : MINT; - const alpha = st === 3 ? 0.38 : st === 2 ? 1 : 0.88; - const size = st === 2 ? 0.11 : st === 0 ? 0.085 : 0.062; - const bx = nparent[gi] >= 0 ? nx[nparent[gi]] : se.x; - const by = nparent[gi] >= 0 ? ny[nparent[gi]] : se.y; - put( - points, - p++, - nx[gi], - ny[gi], - se.x, - se.y, - bx, - by, - nstep[gi], - size, - 1.7, - st === 2 ? 30 : 22, - rgb, - alpha, - BAND_NODE, - SPREAD_NODE, - ev.id - ); - } - /* search dots */ - for (const se of searches) { - const ev = evals[se.eval]; - const n = se.n1 - se.n0; - put( - points, - p++, - se.x, - se.y, - ev.x, - ev.y, - ev.x, - ev.y, - ev.step + 0.05, - 0.16 * Math.sqrt(n), - 2, - 30, - INK, - 0.55, - BAND_SEARCH, - SPREAD_SEARCH, - ev.id - ); - } - /* eval dots */ - for (const ev of evals) { - const par = ev.parent >= 0 ? evals[ev.parent] : ev; - put( - points, - p++, - ev.x, - ev.y, - ev.x, - ev.y, - par.x, - par.y, - ev.step, - ev.record ? 4 : 2.2, - ev.record ? 9 : 5.5, - ev.record ? 15 : 9, - ev.record ? AMBER : INK, - ev.record ? 0.95 : 0.5, - BAND_EVAL, - SPREAD_NONE, - ev.id - ); - } - - let l = 0; - /* tree edges: parent vert then child vert (child sprouts from parent) */ - for (let gi = 0; gi < totalNodes; gi++) { - const pa = nparent[gi]; - if (pa < 0) continue; - const se = searches[nsearch[gi]]; - const evId = se.eval; - const dead = nstate[gi] === 3; - const rgb = dead ? RED : INK; - const alpha = dead ? 0.1 : 0.2; - put(lines, l++, nx[pa], ny[pa], se.x, se.y, nx[pa], ny[pa], nstep[gi], 0, 0, 0, rgb, alpha, BAND_EDGE, SPREAD_NODE, evId); - put(lines, l++, nx[gi], ny[gi], se.x, se.y, nx[pa], ny[pa], nstep[gi], 0, 0, 0, rgb, alpha, BAND_EDGE, SPREAD_NODE, evId); - } - /* spokes: record → failed proposal */ - for (const ev of evals) { - if (ev.record || ev.parent < 0) continue; - const par = evals[ev.parent]; - put(lines, l++, par.x, par.y, par.x, par.y, par.x, par.y, ev.step, 0, 0, 0, INK, 0.1, BAND_SPOKE, SPREAD_NONE, ev.id); - put(lines, l++, ev.x, ev.y, ev.x, ev.y, par.x, par.y, ev.step, 0, 0, 0, INK, 0.1, BAND_SPOKE, SPREAD_NONE, ev.id); - } - /* spine: record → record, frontier green */ - for (let i = 1; i < records.length; i++) { - const a = evals[records[i - 1]]; - const b = evals[records[i]]; - put(lines, l++, a.x, a.y, a.x, a.y, a.x, a.y, b.step, 0, 0, 0, GREEN, 0.55, BAND_SPINE, SPREAD_NONE, b.id); - put(lines, l++, b.x, b.y, b.x, b.y, a.x, a.y, b.step, 0, 0, 0, GREEN, 0.55, BAND_SPINE, SPREAD_NONE, b.id); - } - - /* bounds over eval centers + orbit reach */ - let x0 = Infinity, - y0 = Infinity, - x1 = -Infinity, - y1 = -Infinity; - for (const ev of evals) { - x0 = Math.min(x0, ev.x - EVAL_R - 8); - y0 = Math.min(y0, ev.y - EVAL_R - 8); - x1 = Math.max(x1, ev.x + EVAL_R + 8); - y1 = Math.max(y1, ev.y + EVAL_R + 8); - } - - return { - evals, - searches, - records, - nx, - ny, - nscore, - nstate, - nsearch, - nodeCount: totalNodes, - points, - lines, - pointCount, - lineVertCount, - frontierByStep, - bounds: { x0, y0, x1, y1 }, - }; -} diff --git a/apps/homescreen/app/components/OverviewCards.tsx b/apps/homescreen/app/components/OverviewCards.tsx index f55698bb..6d54efac 100644 --- a/apps/homescreen/app/components/OverviewCards.tsx +++ b/apps/homescreen/app/components/OverviewCards.tsx @@ -87,11 +87,11 @@ export function OverviewMetricTile({ // Model palette (design system v2.0), cycled deterministically over the // alphabetical rank of a series name within its key set. -export const CEDAR_PALETTE = ["#9edbd3", "#d97757", "#a78bfa", "#f2b34c", "#67e8f9"] as const; +export const SERIES_PALETTE = ["#9edbd3", "#d97757", "#a78bfa", "#f2b34c", "#67e8f9"] as const; export function overviewSeriesColors(keys: string[]): Map { const ranked = [...keys].sort((a, b) => a.localeCompare(b)); - return new Map(ranked.map((key, index) => [key, CEDAR_PALETTE[index % CEDAR_PALETTE.length]])); + return new Map(ranked.map((key, index) => [key, SERIES_PALETTE[index % SERIES_PALETTE.length]])); } /** Chip-row legend: colored sm-dot + series name, under the chart. */ diff --git a/apps/homescreen/app/moodboard/moodboard.css b/apps/homescreen/app/moodboard/moodboard.css deleted file mode 100644 index a5d19955..00000000 --- a/apps/homescreen/app/moodboard/moodboard.css +++ /dev/null @@ -1,2336 +0,0 @@ -/* Mood board — a visual language for models and what we do to them. - Everything on the black field, everything on the house easing. */ - -.mb { - --mb-ease: cubic-bezier(0.2, 0.7, 0.2, 1); - --mb-field: #000; - --mb-clay: #d97757; /* closed model A (Anthropic) */ - --mb-mint: #9edbd3; /* closed model B (OpenAI) */ - --mb-amber: #f2b34c; - --mb-violet: #a78bfa; - --mb-cyan: #67e8f9; - --mb-green: #6ee7a0; - /* the app shell locks body overflow (panes scroll internally), - so the board is its own scroll container */ - height: 100vh; - overflow-y: auto; - background: var(--mb-field); - color: #f2f2f0; - padding: 40px 32px 120px; -} -.mb-inner { - max-width: 1120px; - margin: 0 auto; -} -.mb h1 { - font-family: var(--font-mono); - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.22em; - color: rgba(242, 242, 240, 0.9); - margin: 0; -} -.mb-sub { - font-size: 13px; - color: rgba(242, 242, 240, 0.45); - margin: 6px 0 40px; - max-width: 620px; - line-height: 1.5; -} -.mb-section { - font-family: var(--font-mono); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.2em; - color: rgba(242, 242, 240, 0.55); - margin: 56px 0 6px; -} -.mb-section-note { - font-size: 12.5px; - color: rgba(242, 242, 240, 0.38); - margin: 0 0 18px; - max-width: 680px; - line-height: 1.5; -} -.mb-caption { - font-family: var(--font-mono); - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.16em; - color: rgba(242, 242, 240, 0.4); -} -.mb-caption b { - color: rgba(242, 242, 240, 0.85); - font-weight: 500; -} - -/* ---------- cards on the field ---------- */ -.mb-card { - position: relative; - background: radial-gradient( - 120% 90% at 50% 0%, - rgba(255, 255, 255, 0.035), - transparent 60% - ); - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 12px; - overflow: hidden; -} -.mb-card-label { - position: absolute; - top: 12px; - left: 14px; - right: 14px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - z-index: 3; - pointer-events: none; -} -/* labels never block the art, but controls that live in them must work */ -.mb-card-label .mb-seg, -.mb-card-label button { - pointer-events: auto; -} -.mb-grid { - display: grid; - gap: 14px; -} - -/* ---------- segmented state control ---------- */ -.mb-seg { - display: inline-flex; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 8px; - padding: 2px; - gap: 2px; - background: rgba(255, 255, 255, 0.03); -} -.mb-seg button { - background: transparent; - border: none; - color: rgba(242, 242, 240, 0.5); - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.06em; - padding: 4px 10px; - border-radius: 6px; - cursor: pointer; - transition: background 160ms var(--mb-ease), color 160ms var(--mb-ease); -} -.mb-seg button.active { - background: rgba(255, 255, 255, 0.12); - color: #f2f2f0; -} -.mb-chiprow { - display: flex; - gap: 8px; - align-items: center; -} -.mb-chip { - width: 16px; - height: 16px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.25); - cursor: pointer; - padding: 0; - transition: transform 160ms var(--mb-ease), box-shadow 160ms var(--mb-ease); -} -.mb-chip.active { - transform: scale(1.25); - box-shadow: 0 0 0 2px #000, 0 0 0 3.5px rgba(255, 255, 255, 0.6); -} -.mb-chip.square { - border-radius: 4px; -} - -/* ---------- baseline stage (the current app, faithfully) ---------- */ -.mb-stage { - position: relative; - display: grid; - place-items: center; - height: 420px; -} -.mb-stamp { - grid-area: 1 / 1; - width: 200px; - height: 200px; - object-fit: contain; - opacity: 0.05; - transform: scale(0.92); - filter: drop-shadow(0 0 18px rgba(255, 255, 255, 0.12)); -} -.mb-halo { - grid-area: 1 / 1; - width: 340px; - height: 340px; -} -.mb-orbit { - grid-area: 1 / 1; - width: 340px; - height: 340px; - position: relative; - pointer-events: none; - opacity: 0.62; - animation: mb-orbit 26s linear infinite; -} -.mb-orbit.active { - opacity: 1; - animation-duration: 16s; -} -.mb-orbit-body { - position: absolute; - top: 4%; - right: 6%; - width: 18%; - height: 18%; - min-width: 30px; - min-height: 30px; - filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.28)); - animation: mb-orbit-counter 26s linear infinite; -} -.mb-orbit.active .mb-orbit-body { - animation-duration: 16s; -} -@keyframes mb-orbit { - to { - transform: rotate(360deg); - } -} -@keyframes mb-orbit-counter { - to { - transform: rotate(-360deg); - } -} - -/* ---------- archetype grid ---------- */ -.mb-archetype { - display: grid; - place-items: center; - height: 220px; - padding-top: 14px; -} -.mb-archetype-halo { - width: 150px; - height: 150px; -} -.mb-archetype-foot { - position: absolute; - bottom: 12px; - left: 14px; - right: 14px; - display: flex; - justify-content: space-between; - align-items: center; - z-index: 3; -} - -/* ---------- closed-model languages: tiles on the field ---------- - Same breath, same orbit, same easing — but squares instead of circles. - Open weights are visible cores; closed models are sealed surfaces. */ -.mb-tilefield { - position: relative; - display: grid; - place-items: center; - height: 300px; -} -.mb-tiles { - display: grid; - grid-template-columns: repeat(3, 44px); - grid-template-rows: repeat(3, 44px); - gap: 10px; -} -.mb-tile { - border-radius: 10px; - background: color-mix(in srgb, var(--mb-tile-color) 16%, transparent); - border: 1px solid color-mix(in srgb, var(--mb-tile-color) 55%, transparent); - animation: mb-tile-breathe 5.2s var(--mb-ease) infinite; - animation-delay: calc(var(--i) * 260ms); -} -.mb-tilefield[data-state="thinking"] .mb-tile { - animation: mb-tile-ripple 1.6s var(--mb-ease) infinite; - animation-delay: calc(var(--ring) * 220ms); -} -.mb-tilefield[data-state="speaking"] .mb-tile { - animation: mb-tile-speak 1.1s var(--mb-ease) infinite; - animation-delay: calc(var(--col) * 140ms); -} -.mb-tilefield[data-state="asleep"] .mb-tile { - animation: none; - opacity: 0.28; -} -/* processing highlight: hops tile to tile while the colony works. - border-color and box-shadow only — channels the state keyframes - (which animate transform/background) never touch. */ -.mb-tile.hot { - border-color: color-mix(in srgb, var(--mb-tile-color) 75%, white); - box-shadow: - inset 0 0 0 1px color-mix(in srgb, var(--mb-tile-color) 60%, white), - 0 0 12px color-mix(in srgb, var(--mb-tile-color) 55%, transparent); - transition: border-color 140ms var(--mb-ease), box-shadow 140ms var(--mb-ease); -} -@keyframes mb-tile-breathe { - 0%, - 100% { - transform: scale(1); - opacity: 0.75; - } - 50% { - transform: scale(0.9); - opacity: 1; - } -} -@keyframes mb-tile-ripple { - 0%, - 100% { - transform: scale(1); - background: color-mix(in srgb, var(--mb-tile-color) 16%, transparent); - } - 35% { - transform: scale(1.14); - background: color-mix(in srgb, var(--mb-tile-color) 42%, transparent); - } -} -@keyframes mb-tile-speak { - 0%, - 100% { - transform: translateY(0); - } - 40% { - transform: translateY(-7px); - } -} - -/* closed model B: a sealed lattice that rotates as one body — the whole - surface moves, nothing inside is visible */ -.mb-lattice { - display: grid; - grid-template-columns: repeat(4, 30px); - grid-template-rows: repeat(4, 30px); - gap: 8px; - animation: mb-lattice-turn 32s linear infinite; -} -.mb-tilefield[data-state="thinking"] .mb-lattice { - animation-duration: 12s; -} -.mb-tilefield[data-state="asleep"] .mb-lattice { - animation-play-state: paused; - opacity: 0.28; -} -.mb-lattice .mb-tile { - border-radius: 4px; - animation-name: mb-tile-breathe; - animation-duration: 6.4s; -} -.mb-tilefield[data-state="speaking"] .mb-lattice .mb-tile { - animation-name: mb-tile-speak; - animation-duration: 1.1s; - animation-delay: calc(var(--col) * 140ms); -} -@keyframes mb-lattice-turn { - to { - transform: rotate(360deg); - } -} - -/* ---------- operations ---------- */ -.mb-op { - height: 280px; - display: grid; - place-items: center; -} - -/* A/B: two archetypes on one field, traffic split between them */ -.mb-ab { - position: relative; - width: 100%; - height: 100%; - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - justify-items: center; - padding: 0 26px; -} -.mb-ab-contender { - display: grid; - place-items: center; - gap: 6px; -} -.mb-ab-halo { - width: 110px; - height: 110px; -} -.mb-ab-splitter { - width: 7px; - height: 7px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.8); - box-shadow: 0 0 12px rgba(255, 255, 255, 0.5); - animation: mb-splitter-pulse 2.4s var(--mb-ease) infinite; -} -@keyframes mb-splitter-pulse { - 0%, - 100% { - box-shadow: 0 0 8px rgba(255, 255, 255, 0.35); - } - 50% { - box-shadow: 0 0 18px rgba(255, 255, 255, 0.7); - } -} -.mb-ab-packet { - position: absolute; - top: 50%; - left: 50%; - width: 4px; - height: 4px; - border-radius: 999px; - opacity: 0; -} -.mb-ab-packet.to-a { - background: var(--mb-a); - animation: mb-packet-left 2.4s var(--mb-ease) infinite; -} -.mb-ab-packet.to-b { - background: var(--mb-b); - animation: mb-packet-right 2.4s var(--mb-ease) infinite; -} -@keyframes mb-packet-left { - 0% { - transform: translate(0, -50%); - opacity: 0; - } - 12% { - opacity: 0.9; - } - 88% { - opacity: 0.9; - } - 100% { - transform: translate(calc(-1 * var(--dist)), -50%); - opacity: 0; - } -} -@keyframes mb-packet-right { - 0% { - transform: translate(0, -50%); - opacity: 0; - } - 12% { - opacity: 0.9; - } - 88% { - opacity: 0.9; - } - 100% { - transform: translate(var(--dist), -50%); - opacity: 0; - } -} -.mb-ab-score { - width: 110px; - height: 3px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - overflow: hidden; -} -.mb-ab-score i { - display: block; - height: 100%; - border-radius: 999px; - background: var(--mb-score-color); - animation: mb-score-fill 9s var(--mb-ease) infinite; -} -@keyframes mb-score-fill { - 0% { - width: 0; - } - 80%, - 100% { - width: var(--mb-score); - } -} - -/* RLM: one large mind vs the same compute fanned out horizontally */ -.mb-rlm { - display: grid; - grid-template-columns: 1fr 1fr; - width: 100%; - height: 100%; - align-items: center; - justify-items: center; -} -/* open models are empty circles — no gradient fills; the ring carries - the color, and rings mix by stacking (border + inset box-shadow) */ -.mb-rlm-one { - width: 74px; - height: 74px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; - animation: mb-breathe 5.2s var(--mb-ease) infinite; -} -@keyframes mb-breathe { - 0%, - 100% { - transform: scale(1); - } - 50% { - transform: scale(0.94); - } -} -.mb-rlm-fan { - position: relative; - width: 210px; - height: 170px; -} -.mb-rlm-root { - position: absolute; - left: 50%; - top: 8px; - transform: translateX(-50%); - width: 22px; - height: 22px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; - z-index: 2; -} -.mb-rlm-worker { - position: absolute; - left: 50%; - top: 12px; - width: 10px; - height: 10px; - border-radius: 999px; - border: 1.5px solid var(--mb-cyan); - background: transparent; - opacity: 0; - animation: mb-rlm-quest 6s var(--mb-ease) infinite; - animation-delay: calc(var(--i) * 90ms); -} -@keyframes mb-rlm-quest { - 0% { - transform: translate(-50%, 0) scale(0.4); - opacity: 0; - } - 16% { - transform: translate(calc(-50% + var(--fx)), var(--fy)) scale(1); - opacity: 0.95; - } - 58% { - transform: translate(calc(-50% + var(--fx)), var(--fy)) scale(1); - opacity: 0.95; - } - 62% { - border-color: var(--mb-green); - } - 84% { - transform: translate(-50%, 0) scale(0.5); - opacity: 0.8; - } - 100% { - transform: translate(-50%, 0) scale(0.4); - opacity: 0; - } -} - -/* GEPA: lineage buds, the frontier survives */ -.mb-gepa-node { - opacity: 0; - transition: opacity 700ms var(--mb-ease); -} -.mb-gepa-node.on { - opacity: 0.38; -} -.mb-gepa-node.on.frontier { - opacity: 1; - filter: drop-shadow(0 0 6px color-mix(in srgb, var(--mb-violet) 70%, transparent)); -} -.mb-gepa-edge { - stroke: rgba(255, 255, 255, 0.22); - stroke-width: 1; - opacity: 0; - transition: opacity 700ms var(--mb-ease); -} -.mb-gepa-edge.on { - opacity: 1; -} - -/* SFT: the stream becomes the model — examples flow in, the color takes */ -.mb-sft { - position: relative; - width: 100%; - height: 100%; - display: grid; - place-items: center; -} -.mb-sft-orb { - width: 84px; - height: 84px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.85); - background: transparent; - /* the ring itself takes the data's color — white to amber as rows land */ - animation: mb-sft-take 12s var(--mb-ease) infinite; -} -@keyframes mb-sft-take { - 0% { - border-color: rgba(255, 255, 255, 0.85); - border-width: 1.5px; - box-shadow: 0 0 0 rgba(0, 0, 0, 0); - } - 70%, - 92% { - border-color: var(--mb-sft-color); - border-width: 3px; - box-shadow: 0 0 16px color-mix(in srgb, var(--mb-sft-color) 30%, transparent); - } - 100% { - border-color: rgba(255, 255, 255, 0.85); - border-width: 1.5px; - box-shadow: 0 0 0 rgba(0, 0, 0, 0); - } -} -.mb-sft-dash { - position: absolute; - top: 50%; - left: 0; - width: 14px; - height: 2px; - border-radius: 2px; - background: var(--mb-sft-color); - opacity: 0; - animation: mb-sft-flow 2.6s var(--mb-ease) infinite; - animation-delay: calc(var(--i) * 420ms); -} -@keyframes mb-sft-flow { - 0% { - transform: translate(24px, calc(var(--jitter) * 1px)); - opacity: 0; - } - 14% { - opacity: 0.9; - } - 78% { - opacity: 0.9; - } - 100% { - transform: translate(130px, 0); - opacity: 0; - } -} - -/* RL: the two training loops, rings only, color = value. - GRPO — no critic: a group of rollouts is scored against its own mean; - above-mean rollouts push the policy green. - PPO — a critic scores each rollout; the update is clipped. */ -.mb-grpo, -.mb-ppo { - position: relative; - width: 264px; - height: 148px; -} -.mb-grpo-policy, -.mb-ppo-policy { - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - width: 44px; - height: 44px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.85); - background: transparent; -} -.mb-grpo-policy { - animation: mb-grpo-policy 7s var(--mb-ease) infinite; -} -@keyframes mb-grpo-policy { - 0%, - 76% { - border-color: rgba(255, 255, 255, 0.85); - box-shadow: none; - } - 84% { - border-color: var(--mb-green); - box-shadow: 0 0 16px color-mix(in srgb, var(--mb-green) 45%, transparent); - } - 94%, - 100% { - border-color: color-mix(in srgb, var(--mb-green) 55%, white); - box-shadow: none; - } -} -.mb-grpo-roll { - position: absolute; - left: 168px; - top: var(--y); - width: 22px; - height: 22px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.5); - background: transparent; - opacity: 0; -} -.mb-grpo-roll.win { - animation: mb-grpo-roll-win 7s var(--mb-ease) infinite; -} -.mb-grpo-roll.lose { - animation: mb-grpo-roll-lose 7s var(--mb-ease) infinite; -} -@keyframes mb-grpo-roll-win { - 0% { - transform: translateX(-120px) scale(0.4); - opacity: 0; - border-color: rgba(255, 255, 255, 0.5); - box-shadow: none; - } - 14% { - transform: translateX(0) scale(1); - opacity: 1; - } - 44% { - border-color: rgba(255, 255, 255, 0.5); - box-shadow: none; - } - 52%, - 68% { - border-color: var(--mb-green); - box-shadow: 0 0 10px color-mix(in srgb, var(--mb-green) 35%, transparent); - transform: translateX(0) scale(1); - opacity: 1; - } - 82% { - transform: translateX(-120px) scale(0.5); - opacity: 0.7; - } - 88%, - 100% { - transform: translateX(-120px) scale(0.4); - opacity: 0; - } -} -@keyframes mb-grpo-roll-lose { - 0% { - transform: translateX(-120px) scale(0.4); - opacity: 0; - border-color: rgba(255, 255, 255, 0.5); - } - 14% { - transform: translateX(0) scale(1); - opacity: 1; - } - 44% { - border-color: rgba(255, 255, 255, 0.5); - } - 52%, - 68% { - border-color: rgba(255, 255, 255, 0.15); - opacity: 0.6; - } - 80%, - 100% { - transform: translateX(0) scale(0.9); - opacity: 0; - } -} -.mb-grpo-mean { - position: absolute; - left: 152px; - width: 56px; - top: 50%; - border-top: 1px dashed rgba(255, 255, 255, 0.35); - opacity: 0; - animation: mb-grpo-mean 7s var(--mb-ease) infinite; -} -.mb-grpo-mean .mb-caption { - position: absolute; - left: 100%; - top: -7px; - padding-left: 5px; -} -@keyframes mb-grpo-mean { - 0%, - 40% { - opacity: 0; - } - 48%, - 70% { - opacity: 1; - } - 80%, - 100% { - opacity: 0; - } -} -.mb-ppo-policy { - animation: mb-ppo-policy 7s var(--mb-ease) infinite; -} -@keyframes mb-ppo-policy { - 0%, - 66% { - border-color: rgba(255, 255, 255, 0.85); - box-shadow: none; - } - /* the clip: a guard ring appears and the color moves only one - bounded step, however big the reward was */ - 74% { - border-color: color-mix(in srgb, var(--mb-green) 45%, white); - box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.14); - } - 84%, - 100% { - border-color: color-mix(in srgb, var(--mb-green) 35%, white); - box-shadow: none; - } -} -.mb-ppo-critic { - position: absolute; - left: 122px; - top: 16px; - width: 26px; - height: 26px; - border-radius: 999px; - border: 2px solid var(--mb-violet); - background: transparent; - animation: mb-ppo-critic 7s var(--mb-ease) infinite; -} -@keyframes mb-ppo-critic { - 0%, - 48% { - transform: scale(1); - box-shadow: none; - } - 56% { - transform: scale(1.25); - box-shadow: 0 0 12px color-mix(in srgb, var(--mb-violet) 45%, transparent); - } - 66%, - 100% { - transform: scale(1); - box-shadow: none; - } -} -.mb-ppo-env { - position: absolute; - right: 14px; - top: 50%; - transform: translateY(-50%); - width: 26px; - height: 26px; - border-radius: 6px; - border: 1px dashed rgba(255, 255, 255, 0.4); -} -.mb-ppo-dot { - position: absolute; - width: 6px; - height: 6px; - border-radius: 999px; - background: #fff; - opacity: 0; - animation: mb-ppo-dot 7s var(--mb-ease) infinite; -} -@keyframes mb-ppo-dot { - 0% { - left: 48px; - top: 68px; - background: #fff; - opacity: 0; - } - 8% { - opacity: 0.9; - } - 30% { - left: 216px; - top: 68px; - background: #fff; - } - 38% { - left: 216px; - top: 68px; - background: var(--mb-green); - } - 52% { - left: 132px; - top: 26px; - background: var(--mb-green); - } - 60% { - left: 132px; - top: 26px; - } - 72% { - left: 54px; - top: 60px; - opacity: 0.9; - } - 78%, - 100% { - left: 54px; - top: 60px; - opacity: 0; - } -} -.mb-rl-sublabel { - position: absolute; - text-align: center; -} - -/* ---------- the optimization ladder ---------- */ - -/* capture: traces accumulate, the slice freezes */ -.mb-capture { - display: grid; - grid-template-columns: repeat(6, 14px); - grid-auto-rows: 14px; - gap: 6px; - padding: 10px; - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 8px; - transition: border-color 500ms var(--mb-ease), box-shadow 500ms var(--mb-ease); -} -.mb-capture.frozen { - border-color: var(--mb-cyan); - box-shadow: 0 0 18px color-mix(in srgb, var(--mb-cyan) 22%, transparent); -} -.mb-cell { - border-radius: 3px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - transition: background 400ms var(--mb-ease), border-color 400ms var(--mb-ease); -} -.mb-cell.on { - background: color-mix(in srgb, var(--mb-cyan) 22%, transparent); - border-color: color-mix(in srgb, var(--mb-cyan) 60%, transparent); -} -.mb-capture.frozen .mb-cell.on { - background: transparent; /* crystallized: outline only */ -} - -/* climb: smallest rung first, first body over the bar wins */ -.mb-climb { - position: relative; - display: flex; - align-items: flex-end; - gap: 22px; - padding: 30px 12px 0; - height: 130px; -} -.mb-threshold { - position: absolute; - top: 20px; - left: -4px; - right: -4px; - border-top: 1px dashed rgba(255, 255, 255, 0.3); -} -.mb-threshold .mb-caption { - position: absolute; - right: 0; - top: -16px; -} -.mb-rung { - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.7); - background: transparent; - opacity: 0.5; - transition: all 500ms var(--mb-ease); -} -.mb-rung.trying { - opacity: 1; - transform: scale(1.12) translateY(-6px); - box-shadow: 0 0 16px rgba(255, 255, 255, 0.35); -} -.mb-rung.failed { - opacity: 0.15; -} -.mb-rung.passed { - opacity: 1; - /* green verdict stacks inside the white ring */ - box-shadow: - inset 0 0 0 3px color-mix(in srgb, var(--mb-green) 60%, transparent), - 0 0 18px color-mix(in srgb, var(--mb-green) 45%, transparent); -} - -/* hybrid: the local body triages, the sealed surface takes the hard tail */ -.mb-hybrid { - position: relative; - width: 260px; - height: 120px; -} -.mb-hybrid-local { - position: absolute; - left: 88px; - top: 50%; - transform: translateY(-50%); - width: 44px; - height: 44px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; -} -.mb-hybrid-remote { - position: absolute; - right: 6px; - top: 50%; - transform: translateY(-50%); - display: grid; - grid-template-columns: repeat(2, 16px); - grid-auto-rows: 16px; - gap: 5px; -} -.mb-hybrid-remote i, -.mb-distill-teacher i, -.mb-promote-body.incumbent i { - border-radius: 4px; - background: color-mix(in srgb, var(--mb-clay) 16%, transparent); - border: 1px solid color-mix(in srgb, var(--mb-clay) 55%, transparent); - animation: mb-tile-breathe 6.4s var(--mb-ease) infinite; -} -.mb-hybrid-packet { - position: absolute; - top: 50%; - left: 0; - width: 5px; - height: 5px; - border-radius: 999px; - opacity: 0; -} -.mb-hybrid-packet.easy { - background: #fff; - animation: mb-hyb-easy 3.6s var(--mb-ease) infinite; -} -.mb-hybrid-packet.hard { - background: var(--mb-clay); - animation: mb-hyb-hard 3.6s var(--mb-ease) infinite; - animation-delay: 450ms; -} -@keyframes mb-hyb-easy { - 0% { - transform: translate(0, -50%); - opacity: 0; - } - 12% { - opacity: 0.9; - } - 70% { - transform: translate(92px, -50%); - opacity: 0.9; - } - 82%, - 100% { - transform: translate(104px, -50%); - opacity: 0; - } -} -@keyframes mb-hyb-hard { - 0% { - transform: translate(0, -50%); - opacity: 0; - } - 12% { - opacity: 0.9; - } - 40% { - transform: translate(100px, calc(-50% - 34px)); - } - 80% { - transform: translate(206px, -50%); - opacity: 0.9; - } - 92%, - 100% { - transform: translate(214px, -50%); - opacity: 0; - } -} - -/* seeded env: a bounded world, the same run every time */ -.mb-env { - position: relative; - width: 170px; - height: 120px; - border: 1px dashed rgba(255, 255, 255, 0.3); - border-radius: 10px; -} -.mb-env-agent { - position: absolute; - width: 14px; - height: 14px; - border-radius: 999px; - background: radial-gradient( - circle at 38% 32%, - rgba(255, 255, 255, 0.95), - rgba(255, 255, 255, 0.3) - ); - animation: mb-env-run 7s var(--mb-ease) infinite; -} -@keyframes mb-env-run { - 0% { - left: 12px; - top: 92px; - opacity: 0; - } - 6% { - opacity: 1; - } - 25% { - left: 12px; - top: 16px; - } - 50% { - left: 76px; - top: 16px; - } - 75% { - left: 76px; - top: 78px; - } - 92% { - left: 140px; - top: 78px; - opacity: 1; - } - 100% { - left: 140px; - top: 78px; - opacity: 0; - } -} - -/* distill: sealed teacher streams, open student takes the color */ -.mb-distill { - position: relative; - width: 250px; - height: 120px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px; -} -.mb-distill-teacher { - display: grid; - grid-template-columns: repeat(2, 20px); - grid-auto-rows: 20px; - gap: 6px; -} -.mb-distill-dash { - position: absolute; - left: 64px; - top: 50%; - width: 14px; - height: 2px; - border-radius: 2px; - background: var(--mb-clay); - opacity: 0; - animation: mb-distill-flow 2.8s var(--mb-ease) infinite; - animation-delay: calc(var(--i) * 500ms); -} -@keyframes mb-distill-flow { - 0% { - transform: translate(0, calc(var(--jitter) * 1px)); - opacity: 0; - } - 14% { - opacity: 0.9; - } - 80% { - opacity: 0.9; - } - 100% { - transform: translate(118px, 0); - opacity: 0; - } -} -.mb-distill-student { - width: 44px; - height: 44px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; - /* the teacher's clay stacks in as an inner ring — the circle stays */ - animation: mb-distill-take 11s var(--mb-ease) infinite; -} -@keyframes mb-distill-take { - 0% { - box-shadow: inset 0 0 0 0 - color-mix(in srgb, var(--mb-clay) 85%, transparent); - } - 70%, - 92% { - box-shadow: inset 0 0 0 5px - color-mix(in srgb, var(--mb-clay) 85%, transparent); - } - 100% { - box-shadow: inset 0 0 0 0 - color-mix(in srgb, var(--mb-clay) 85%, transparent); - } -} - -/* compress / qat: same light, smaller body */ -.mb-compress { - width: 84px; - height: 84px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; - animation: mb-compress 8s var(--mb-ease) infinite; -} -@keyframes mb-compress { - 0%, - 15% { - transform: scale(1); - box-shadow: 0 0 22px rgba(255, 255, 255, 0.25); - } - 45%, - 80% { - transform: scale(0.6); - /* light stays constant while the body shrinks */ - box-shadow: 0 0 38px rgba(255, 255, 255, 0.42); - } - 100% { - transform: scale(1); - box-shadow: 0 0 22px rgba(255, 255, 255, 0.25); - } -} - -/* promote: traffic goes to the stage; the understudy takes it */ -.mb-promote { - position: relative; - width: 100%; - height: 100%; -} -.mb-promote-ring { - position: absolute; - left: 50%; - top: 50%; - width: 110px; - height: 110px; - margin: -55px 0 0 -55px; - border: 1px dashed rgba(255, 255, 255, 0.22); - border-radius: 999px; -} -.mb-promote-wing { - position: absolute; - right: 16%; - top: 12%; -} -.mb-promote-packet { - position: absolute; - left: 50%; - top: 50%; - width: 5px; - height: 5px; - border-radius: 999px; - background: #fff; - opacity: 0; - animation: mb-promote-packet 2.4s var(--mb-ease) infinite; -} -@keyframes mb-promote-packet { - 0% { - transform: translate(-320px, -50%); - opacity: 0; - } - 10% { - opacity: 0.85; - } - 85% { - opacity: 0.85; - } - 100% { - transform: translate(-64px, -50%); - opacity: 0; - } -} -.mb-promote-body { - position: absolute; - left: 50%; - top: 50%; -} -.mb-promote-body.incumbent { - display: grid; - grid-template-columns: repeat(2, 22px); - grid-auto-rows: 22px; - gap: 6px; - animation: mb-promote-a 16s var(--mb-ease) infinite; -} -.mb-promote-body.understudy { - width: 56px; - height: 56px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.85); - background: radial-gradient( - circle at 38% 32%, - rgba(255, 255, 255, 0.25), - rgba(255, 255, 255, 0.05) 65% - ); - animation: mb-promote-b 16s var(--mb-ease) infinite; -} -@keyframes mb-promote-a { - 0%, - 40% { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } - 56%, - 94% { - transform: translate(86px, -116px) scale(0.5); - opacity: 0.55; - } - 100% { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } -} -@keyframes mb-promote-b { - 0%, - 40% { - transform: translate(70px, -130px) scale(0.5); - opacity: 0.7; - } - 56%, - 94% { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } - 100% { - transform: translate(70px, -130px) scale(0.5); - opacity: 0.7; - } -} - -/* ---------- the record: hill climb & lineage ---------- */ -.mb-frontier-seg { - stroke: var(--mb-green); - stroke-width: 1.6; - opacity: 0; - transition: opacity 500ms var(--mb-ease); -} -.mb-frontier-seg.on { - opacity: 1; -} -.mb-run-dot { - opacity: 0; - transition: opacity 600ms var(--mb-ease); -} -.mb-run-dot.on { - opacity: 1; -} -.mb-milestone { - fill: none; - stroke: var(--mb-amber); - stroke-width: 1.4; - opacity: 0; - transition: opacity 500ms var(--mb-ease); -} -.mb-milestone.on { - opacity: 1; - animation: mb-milestone-pulse 2.4s var(--mb-ease) infinite; -} -@keyframes mb-milestone-pulse { - 0%, - 100% { - filter: drop-shadow(0 0 2px color-mix(in srgb, var(--mb-amber) 40%, transparent)); - } - 50% { - filter: drop-shadow(0 0 8px color-mix(in srgb, var(--mb-amber) 80%, transparent)); - } -} -.mb-milestone-label { - font-family: var(--font-mono); - font-size: 8.5px; - letter-spacing: 0.08em; - text-transform: uppercase; - fill: rgba(242, 242, 240, 0.55); - opacity: 0; - transition: opacity 500ms var(--mb-ease); -} -.mb-milestone-label.on { - opacity: 1; -} -.mb-lin-edge { - stroke: rgba(255, 255, 255, 0.16); - stroke-width: 1; - opacity: 0; - transition: opacity 600ms var(--mb-ease); -} -.mb-lin-edge.on { - opacity: 1; -} -.mb-lin-edge.frontier-edge { - stroke: color-mix(in srgb, var(--mb-green) 75%, transparent); - stroke-width: 1.6; -} -.mb-lin-node { - opacity: 0; - transition: opacity 600ms var(--mb-ease); -} -.mb-lin-node.on { - opacity: 0.4; -} -.mb-lin-node.on.frontier { - opacity: 1; - filter: drop-shadow(0 0 5px color-mix(in srgb, var(--mb-green) 60%, transparent)); -} -.mb-lin-node.on.invalid { - opacity: 0.8; -} - -/* ---------- global motion switch ---------- */ -.mb.mb-still * { - animation-play-state: paused !important; -} - -/* ---------- manual steppers on story sketches ---------- */ -.mb-stepper { - display: inline-flex; - gap: 2px; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 8px; - padding: 2px; - background: rgba(255, 255, 255, 0.03); -} -.mb-stepper button { - background: transparent; - border: none; - color: rgba(242, 242, 240, 0.6); - font-family: var(--font-mono); - font-size: 11px; - padding: 3px 9px; - border-radius: 6px; - cursor: pointer; - transition: background 160ms var(--mb-ease), color 160ms var(--mb-ease); -} -.mb-stepper button:hover { - background: rgba(255, 255, 255, 0.1); - color: #f2f2f0; -} - -/* ---------- the workload theater ---------- */ -.mb-theater { - display: flex; - flex-direction: column; -} -.mb-th-head { - display: flex; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; - padding: 12px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); -} -.mb-th-main { - display: grid; - grid-template-columns: 1fr 272px; - min-height: 420px; -} -.mb-th-stage { - position: relative; - overflow: hidden; -} -.mb-th-ring { - top: 46%; -} -.mb-th-body { - position: absolute; - display: grid; - place-items: center; - gap: 8px; - z-index: 2; - transform: translate(-50%, -50%); - transition: left 900ms var(--mb-ease), top 900ms var(--mb-ease), - transform 900ms var(--mb-ease), opacity 900ms var(--mb-ease); -} -.mb-th-body.at-stage { - left: 50%; - top: 46%; -} -.mb-th-body.at-wing { - left: 78%; - top: 20%; - transform: translate(-50%, -50%) scale(0.6); - opacity: 0.65; -} -.mb-th-tiles { - display: grid; - grid-template-columns: repeat(2, 24px); - grid-auto-rows: 24px; - gap: 6px; -} -.mb-th-tiles i { - border-radius: 5px; - background: color-mix(in srgb, var(--mb-clay) 16%, transparent); - border: 1px solid color-mix(in srgb, var(--mb-clay) 55%, transparent); - animation: mb-tile-breathe 6.4s var(--mb-ease) infinite; -} -.mb-th-orb { - width: 60px; - height: 60px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.85); - background: transparent; - /* family hue rides as a stacked inner ring, white stays the body */ - box-shadow: inset 0 0 0 4px - color-mix(in srgb, var(--mb-amber) 75%, transparent); - animation: mb-breathe 5.2s var(--mb-ease) infinite; -} -.mb-th-packet { - position: absolute; - width: 5px; - height: 5px; - border-radius: 999px; - opacity: 0; - z-index: 1; -} -.mb-th-packet.to-stage { - animation: mb-th-to-stage 4.4s var(--mb-ease) infinite; -} -.mb-th-packet.to-wing { - animation: mb-th-to-wing 4.4s var(--mb-ease) infinite; -} -@keyframes mb-th-to-stage { - 0% { - left: 2%; - top: 46%; - opacity: 0; - } - 12% { - opacity: 0.9; - } - /* absorbed at the body's edge — the dot never crosses the model */ - 76% { - left: 40%; - top: 46%; - opacity: 0.9; - } - 88%, - 100% { - left: 44%; - top: 46%; - opacity: 0; - } -} -/* processed output: a new, smaller square emerges on the far side */ -.mb-th-out { - position: absolute; - width: 6px; - height: 6px; - border-radius: 2px; - background: var(--mb-clay); - opacity: 0; - z-index: 1; - animation: mb-th-out 4.4s var(--mb-ease) infinite; -} -@keyframes mb-th-out { - 0%, - 46% { - left: 56%; - top: 46%; - transform: rotate(0deg); - opacity: 0; - } - 56% { - opacity: 0.85; - } - 92% { - left: 94%; - top: 46%; - transform: rotate(180deg); - opacity: 0.85; - } - 100% { - left: 95%; - top: 46%; - opacity: 0; - } -} -@keyframes mb-th-to-wing { - 0% { - left: 2%; - top: 46%; - opacity: 0; - } - 12% { - opacity: 0.9; - } - 55% { - left: 45%; - top: 32%; - } - 92% { - left: 77%; - top: 21%; - opacity: 0.9; - } - 100% { - left: 78%; - top: 20%; - opacity: 0; - } -} -.mb-th-overlay { - position: absolute; - left: 18px; - bottom: 14px; - z-index: 4; - background: rgba(0, 0, 0, 0.6); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - padding: 12px; - backdrop-filter: blur(4px); -} -.mb-th-overlay.wide { - right: 18px; - display: grid; - place-items: center; -} -.mb-th-overlay.scores { - display: grid; - gap: 8px; - min-width: 240px; -} -.mb-th-scorebox { - display: grid; - gap: 4px; -} -.mb-th-scorebox .mb-ab-score { - width: 100%; -} -.mb-th-scorebox .mb-ab-score i { - transition: width 400ms var(--mb-ease); -} -/* the understudy wrapper: the halo persona itself, expanded around the - sealed colony like the 01 baseline figure — traffic enters here, - capture happens here, tint = rows accumulated (white → cyan) */ -.mb-th-wraphost { - position: relative; - display: grid; - place-items: center; -} -.mb-th-wraphost .mb-tiles { - transition: transform 700ms var(--mb-ease); -} -.mb-th-wraphost.wrapped { - width: 235px; - height: 235px; -} -.mb-th-wraphost.wrapped .mb-tiles { - transform: scale(0.62); -} -.mb-th-wraphalo { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - pointer-events: none; -} -.mb-th-body > .mb-caption { - white-space: nowrap; -} -/* captured row: a cyan tick relayed down toward the eval tray */ -.mb-th-cap { - position: absolute; - width: 5px; - height: 5px; - border-radius: 1.5px; - background: var(--mb-cyan); - opacity: 0; - z-index: 1; - animation: mb-th-cap 4.4s var(--mb-ease) infinite; -} -@keyframes mb-th-cap { - 0%, - 50% { - left: 56%; - top: 48%; - opacity: 0; - } - 60% { - opacity: 0.9; - } - 92% { - left: 10%; - top: 80%; - opacity: 0.8; - } - 100% { - left: 9%; - top: 82%; - opacity: 0; - } -} -/* gateway switch bank */ -.mb-sw { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - padding: 9px 10px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - transition: opacity 200ms var(--mb-ease); -} -.mb-sw.disabled { - opacity: 0.35; -} -.mb-sw-knob { - position: relative; - flex: none; - width: 34px; - height: 18px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.3); - background: transparent; - cursor: pointer; - transition: border-color 200ms var(--mb-ease); -} -.mb-sw-knob::after { - content: ""; - position: absolute; - top: 2px; - left: 2px; - width: 12px; - height: 12px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.55); - transition: left 200ms var(--mb-ease), background 200ms var(--mb-ease); -} -.mb-sw-knob.on { - border-color: var(--mb-green); -} -.mb-sw-knob.on::after { - left: 18px; - background: var(--mb-green); -} -.mb-sw-knob:disabled { - cursor: default; -} -/* generation ≥ 1 incumbent: an open ring on the stage */ -.mb-th-inc-circle { - width: 74px; - height: 74px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.85); - background: transparent; - animation: mb-breathe 5.2s var(--mb-ease) infinite; - transition: border-color 600ms var(--mb-ease); -} -/* round responses out of a round incumbent */ -.mb-th-out.round { - border-radius: 999px; -} -/* shadow understudy orbit (outer div positions, inner div spins) */ -.mb-th-orbitpos { - position: absolute; - left: 50%; - top: 46%; - width: 330px; - height: 330px; - margin: -165px 0 0 -165px; - pointer-events: none; - z-index: 3; -} -.mb-th-orbitspin { - position: relative; - width: 100%; - height: 100%; - animation: mb-orbit 22s linear infinite; -} -.mb-th-trainee { - position: absolute; - top: 4%; - right: 8%; - width: 44px; - height: 44px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.6); - background: transparent; - transition: border-color 600ms var(--mb-ease); -} -/* the stream radiating out to the orbit */ -.mb-th-feed { - position: absolute; - left: 50%; - top: 46%; - width: 6px; - height: 6px; - border-radius: 999px; - background: var(--mb-cyan); - opacity: 0; - z-index: 2; - animation: mb-th-feed 3.4s var(--mb-ease) infinite; -} -@keyframes mb-th-feed { - 0% { - transform: translate(-50%, -50%) scale(0.5); - opacity: 0; - } - 14% { - opacity: 0.9; - } - 80% { - transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) - scale(1); - opacity: 0.9; - } - 100% { - transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) - scale(0.6); - opacity: 0; - } -} -/* the store */ -.mb-th-store { - position: absolute; - left: 18px; - bottom: 14px; - z-index: 4; - display: grid; - gap: 6px; - justify-items: start; -} -.mb-th-store .mb-capture { - grid-template-columns: repeat(8, 10px); - grid-auto-rows: 10px; - gap: 4px; - padding: 7px; -} -/* the wing: retired incumbents */ -.mb-th-retired { - position: absolute; - right: 16px; - bottom: 14px; - z-index: 4; - display: flex; - gap: 8px; - align-items: center; -} -.mb-th-retired i.sq { - width: 12px; - height: 12px; - border-radius: 3px; - background: color-mix(in srgb, var(--mb-clay) 25%, transparent); - border: 1px solid color-mix(in srgb, var(--mb-clay) 60%, transparent); -} -.mb-th-retired i.ring { - width: 12px; - height: 12px; - border-radius: 999px; - border: 1.5px solid rgba(255, 255, 255, 0.6); - opacity: 0.65; -} -.mb-th-rail { - border-left: 1px solid rgba(255, 255, 255, 0.08); - padding: 14px; - display: flex; - flex-direction: column; - gap: 6px; -} -.mb-th-job { - display: flex; - gap: 10px; - align-items: flex-start; - text-align: left; - background: transparent; - border: 1px solid transparent; - border-radius: 8px; - padding: 8px 10px; - cursor: pointer; - color: rgba(242, 242, 240, 0.75); - font: inherit; - transition: background 160ms var(--mb-ease), border-color 160ms var(--mb-ease); -} -.mb-th-job.active { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.14); - color: #f2f2f0; -} -.mb-th-job-n { - font-family: var(--font-mono); - font-size: 10px; - border: 1px solid rgba(255, 255, 255, 0.25); - border-radius: 999px; - width: 20px; - height: 20px; - display: grid; - place-items: center; - flex: none; -} -.mb-th-job.done .mb-th-job-n { - color: var(--mb-green); - border-color: color-mix(in srgb, var(--mb-green) 60%, transparent); -} -.mb-th-job-title { - display: block; - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; -} -.mb-th-job-sub { - display: block; - font-size: 11px; - color: rgba(242, 242, 240, 0.4); - margin-top: 2px; -} -.mb-th-controls { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; - padding: 12px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.08); - min-height: 56px; -} -.mb-th-btn { - font-family: var(--font-mono); - font-size: 11.5px; - letter-spacing: 0.06em; - background: rgba(255, 255, 255, 0.07); - color: #f2f2f0; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; - padding: 7px 14px; - cursor: pointer; - transition: background 160ms var(--mb-ease); -} -.mb-th-btn:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.14); -} -.mb-th-btn:disabled { - opacity: 0.35; - cursor: default; -} -.mb-th-btn.primary { - border-color: var(--mb-amber); - color: var(--mb-amber); -} -.mb-th-slider { - width: 240px; - accent-color: var(--mb-amber); -} -.mb-rung-btn { - cursor: pointer; - padding: 0; -} -.mb-rung-btn:disabled { - cursor: default; -} - -/* ---------- storyboard ---------- */ -.mb-sb-stage { - position: relative; - display: grid; - place-items: center; - min-height: 340px; -} -.mb-sb-frame { - position: relative; - width: 100%; - height: 340px; - display: grid; - place-items: center; - align-content: center; - gap: 18px; -} -.mb-sb-frame.split { - grid-template-columns: 1fr 1fr; - align-items: center; - justify-items: center; - gap: 0; -} -.mb-sb-persona { - position: relative; - display: grid; - place-items: center; -} -.mb-sb-stamp { - grid-area: 1 / 1; - width: 110px; - height: 110px; - object-fit: contain; - opacity: 0.05; -} -.mb-sb-halo { - grid-area: 1 / 1; - width: 190px; - height: 190px; -} -.mb-sb-chatbar { - display: flex; - justify-content: space-between; - align-items: center; - width: 340px; - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 12px; - padding: 10px 14px; - font-family: var(--font-mono); - font-size: 12px; - color: rgba(242, 242, 240, 0.4); -} -.mb-sb-traces { - display: grid; - gap: 8px; - justify-items: start; -} -.mb-sb-row { - display: flex; - align-items: center; - gap: 8px; - font-family: var(--font-mono); - font-size: 11.5px; - color: rgba(242, 242, 240, 0.6); - opacity: 0; - animation: mb-sb-enter 600ms var(--mb-ease) forwards; - animation-delay: calc(var(--i) * 180ms); -} -.mb-sb-row-dot { - width: 5px; - height: 5px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.5); - flex: none; -} -.mb-sb-row.chip { - margin-top: 8px; - border: 1px solid color-mix(in srgb, var(--mb-amber) 55%, transparent); - border-radius: 999px; - padding: 6px 12px; - color: var(--mb-amber); -} -.mb-sb-row.chip b { - color: var(--mb-amber); -} -@keyframes mb-sb-enter { - from { - opacity: 0; - transform: translateY(4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -/* storyboard frame 03 borrows the full tilefield; trim its canvas */ -.mb-sb-frame .mb-tilefield { - height: 220px; -} -.mb-sb-incumbent-label { - position: absolute; - top: 18px; - left: 0; - right: 0; - text-align: center; -} -.mb-sb-processing { - position: absolute; - bottom: 16px; - left: 0; - right: 0; - text-align: center; -} -/* requests: dots that fade away as they enter the model */ -.mb-sb-in { - position: absolute; - top: 50%; - width: 6px; - height: 6px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.9); - opacity: 0; - z-index: 2; - animation: mb-sb-in 3.6s var(--mb-ease) infinite; -} -@keyframes mb-sb-in { - 0% { - left: 6%; - transform: translateY(var(--jy)); - opacity: 0; - } - 12% { - opacity: 0.9; - } - 34% { - left: 37%; - transform: translateY(var(--jy)); - opacity: 0.9; - } - 46%, - 100% { - left: 42%; - transform: translateY(calc(var(--jy) / 2)); - opacity: 0; - } -} -/* processed: new smaller squares emerging on the other side */ -.mb-sb-out { - position: absolute; - top: 50%; - width: 6px; - height: 6px; - border-radius: 2px; - background: var(--mb-clay); - opacity: 0; - z-index: 2; - animation: mb-sb-out 3.6s var(--mb-ease) infinite; -} -@keyframes mb-sb-out { - 0%, - 48% { - left: 58%; - transform: translateY(calc(var(--jy) / 2)) rotate(0deg); - opacity: 0; - } - 58% { - opacity: 0.85; - } - 92% { - left: 92%; - transform: translateY(var(--jy)) rotate(180deg); - opacity: 0.85; - } - 100% { - left: 94%; - transform: translateY(var(--jy)) rotate(180deg); - opacity: 0; - } -} -/* theater incumbent: the full colony, compacted to stage scale */ -.mb-th-body .mb-tilefield { - height: auto; -} -.mb-th-body .mb-tiles { - grid-template-columns: repeat(3, 24px); - grid-template-rows: repeat(3, 24px); - gap: 7px; -} - -/* ---------- component ids, for storyboarding by reference ---------- */ -.mb-id { - font-family: var(--font-mono); - font-size: 9px; - letter-spacing: 0.1em; - color: rgba(242, 242, 240, 0.45); - border: 1px solid rgba(255, 255, 255, 0.16); - border-radius: 4px; - padding: 1px 5px; - margin-right: 8px; - vertical-align: 1px; -} - -/* ---------- first run: the fork ---------- */ -.mb-doors { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; - padding: 18px; -} -.mb-door { - display: grid; - gap: 10px; - justify-items: center; - padding: 26px 18px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 10px; - color: #f2f2f0; - font: inherit; - cursor: pointer; - transition: border-color 200ms var(--mb-ease), background 200ms var(--mb-ease); -} -.mb-door:hover { - background: rgba(255, 255, 255, 0.04); -} -.mb-door.chosen { - border-color: var(--mb-amber); -} -.mb-door-title { - font-family: var(--font-mono); - font-size: 12px; - letter-spacing: 0.12em; - text-transform: uppercase; -} -.mb-door-glyph { - display: flex; - gap: 4px; - align-items: flex-end; - height: 28px; -} -.mb-door-glyph i { - width: 5px; - border-radius: 2px; - background: rgba(255, 255, 255, 0.55); -} - -/* ---------- the codebase, symbolically ---------- */ -.mb-cb { - padding: 44px 18px 14px; - display: grid; - gap: 14px; -} -.mb-cb-crumb { - display: flex; - align-items: center; - gap: 8px; -} -.mb-cb-crumb-seg { - font-family: var(--font-mono); - font-size: 12px; - letter-spacing: 0.06em; - background: transparent; - border: none; - color: rgba(242, 242, 240, 0.5); - cursor: pointer; - padding: 2px 4px; -} -.mb-cb-crumb-seg.here { - color: #f2f2f0; -} -/* the tree: branches open in place, nothing is replaced */ -.mb-cb-tree { - display: grid; - gap: 6px; -} -.mb-cb-dirrow { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 9px 12px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 10px; - color: #f2f2f0; - font: inherit; - cursor: pointer; - text-align: left; - transition: border-color 200ms var(--mb-ease), background 200ms var(--mb-ease); -} -.mb-cb-dirrow:hover { - background: rgba(255, 255, 255, 0.04); - border-color: rgba(255, 255, 255, 0.28); -} -.mb-cb-dirrow .mb-cb-bars { - flex-direction: row; - align-items: center; -} -.mb-cb-caret { - display: inline-block; - font-size: 11px; - color: rgba(242, 242, 240, 0.5); - transition: transform 220ms var(--mb-ease); -} -.mb-cb-caret.open { - transform: rotate(90deg); -} -.mb-cb-kids { - margin: 6px 0 10px 14px; - padding-left: 14px; - border-left: 1px solid rgba(255, 255, 255, 0.12); - display: grid; - gap: 8px; - animation: settle 260ms var(--mb-ease); -} -.mb-cb-dirname, -.mb-cb-filename { - font-family: var(--font-mono); - font-size: 12px; - letter-spacing: 0.04em; -} -.mb-cb-bars { - display: flex; - flex-direction: column; - gap: 4px; - align-items: flex-start; -} -.mb-cb-bar { - display: block; - height: 4px; - border-radius: 2px; - background: rgba(255, 255, 255, 0.22); -} -.mb-cb-bar.llm { - background: color-mix(in srgb, var(--mb-clay) 75%, transparent); -} -.mb-cb-llmdot { - display: inline-block; - width: 6px; - height: 6px; - border-radius: 2px; - background: var(--mb-clay); - margin-left: 5px; - vertical-align: 0; -} -.mb-cb-files { - display: grid; - gap: 10px; -} -.mb-cb-file { - display: grid; - grid-template-columns: 130px 1fr auto; - gap: 16px; - align-items: center; - padding: 10px 12px; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 10px; -} -.mb-cb-file .mb-cb-bars { - flex-direction: row; - flex-wrap: wrap; - align-items: center; -} -.mb-cb-eps { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; -} -.mb-ep { - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.04em; - color: rgba(242, 242, 240, 0.75); - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 999px; - padding: 4px 10px; - cursor: pointer; - transition: border-color 180ms var(--mb-ease), color 180ms var(--mb-ease), - box-shadow 180ms var(--mb-ease); -} -.mb-ep b { - font-weight: 600; - color: rgba(242, 242, 240, 0.95); -} -.mb-ep:hover { - border-color: rgba(255, 255, 255, 0.45); -} -.mb-ep.iso { - border-color: var(--mb-amber); - color: var(--mb-amber); - box-shadow: 0 0 12px color-mix(in srgb, var(--mb-amber) 30%, transparent); -} -.mb-ep.cap { - border-color: var(--mb-cyan); - color: var(--mb-cyan); - box-shadow: 0 0 12px color-mix(in srgb, var(--mb-cyan) 25%, transparent); -} -.mb-cb-actions { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; - border-top: 1px solid rgba(255, 255, 255, 0.08); - padding-top: 12px; -} - -@media (prefers-reduced-motion: reduce) { - .mb *, - .mb *::before, - .mb *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/apps/homescreen/bun.lock b/apps/homescreen/bun.lock index 58c5ba3c..ddf5376e 100644 --- a/apps/homescreen/bun.lock +++ b/apps/homescreen/bun.lock @@ -18,7 +18,6 @@ "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-updater": "2.10.1", "ai": "^7.0.7", - "ajv": "8.17.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -511,8 +510,6 @@ "ai": ["ai@7.0.7", "", { "dependencies": { "@ai-sdk/gateway": "4.0.5", "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yoNPc4KIH7XKBm5syfZYOCpokc3AFkeBECgOryBbeYyYLbgC7AGGUXwexqYUVeUcmqL9pqej+cmtzOm+kWKmVw=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -689,10 +686,6 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], - "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -781,8 +774,6 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], diff --git a/apps/homescreen/package.json b/apps/homescreen/package.json index c327c323..2c070921 100644 --- a/apps/homescreen/package.json +++ b/apps/homescreen/package.json @@ -6,8 +6,6 @@ "dev": "next dev -p 1420", "build": "next build", "start": "next start -p 1420", - "orchard:contract-check": "node scripts/check-orchard-contract.mjs", - "orchard:workflow-proxy": "node scripts/orchard-workflow-proxy.mjs", "tauri": "tauri", "dev:explore": "next dev -p 14300", "tauri:dev:explore": "UNDERSTUDY_SERVER_PORT=17791 tauri dev --config src-tauri/tauri.dev-explore.conf.json" @@ -26,7 +24,6 @@ "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-updater": "2.10.1", "ai": "^7.0.7", - "ajv": "8.17.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/apps/homescreen/schemas/understudy-train/experiment-event.json b/apps/homescreen/schemas/understudy-train/experiment-event.json deleted file mode 100644 index b26409d6..00000000 --- a/apps/homescreen/schemas/understudy-train/experiment-event.json +++ /dev/null @@ -1,516 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "experiment.accepted" - }, - "budget_usd": { - "type": "number", - "minimum": 0 - }, - "holdout_sealed": { - "type": "boolean", - "const": true - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "budget_usd", - "holdout_sealed" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "experiment.phase_changed" - }, - "phase": { - "type": "string", - "enum": [ - "preflight", - "evaluating", - "optimizing", - "holdout_waiting", - "holdout_running", - "cleanup", - "terminal" - ] - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "phase" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "candidate.state_changed" - }, - "candidate_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "state": { - "type": "string", - "enum": [ - "queued", - "submitted", - "running", - "succeeded", - "failed", - "cancelled" - ] - }, - "job": { - "type": "object", - "properties": { - "executor": { - "type": "string", - "enum": [ - "modal", - "wafer", - "fireworks", - "spark", - "fixture" - ] - }, - "job_id": { - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "idempotency_key": { - "type": "string", - "minLength": 1, - "maxLength": 500 - }, - "submitted_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "required": [ - "executor", - "job_id", - "idempotency_key", - "submitted_at" - ], - "additionalProperties": false - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "candidate_id", - "state" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "rollout.state_changed" - }, - "candidate_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "task_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "state": { - "type": "string", - "enum": [ - "started", - "succeeded", - "failed", - "timed_out" - ] - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "candidate_id", - "task_id", - "state" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "score.snapshot" - }, - "candidate_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "metrics": { - "type": "object", - "propertyNames": { - "type": "string", - "minLength": 1, - "maxLength": 120 - }, - "additionalProperties": { - "type": "number" - } - }, - "frontier": { - "type": "boolean" - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "candidate_id", - "metrics", - "frontier" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "usage.snapshot" - }, - "candidate_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "usage": { - "type": "object", - "properties": { - "evidence_scope": { - "type": "string", - "enum": [ - "run_exclusive", - "account_window", - "unknown" - ] - }, - "requests": { - "anyOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] - }, - "input_tokens": { - "anyOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] - }, - "output_tokens": { - "anyOf": [ - { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] - }, - "actual_usd": { - "anyOf": [ - { - "type": "number", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "estimated_usd": { - "anyOf": [ - { - "type": "number", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "upper_bound_usd": { - "anyOf": [ - { - "type": "number", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "observed_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "required": [ - "evidence_scope", - "requests", - "input_tokens", - "output_tokens", - "actual_usd", - "estimated_usd", - "upper_bound_usd", - "observed_at" - ], - "additionalProperties": false - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "usage" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "schema_version": { - "type": "string", - "const": "understudy.experiment-event.v1" - }, - "experiment_id": { - "type": "string", - "minLength": 1, - "maxLength": 160, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "sequence": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - }, - "type": { - "type": "string", - "const": "experiment.error" - }, - "failure_class": { - "type": "string", - "enum": [ - "retryable", - "fatal", - "cancelled", - "budget_exhausted" - ] - }, - "code": { - "type": "string", - "minLength": 1, - "maxLength": 160 - }, - "retry_scheduled": { - "type": "boolean" - } - }, - "required": [ - "schema_version", - "experiment_id", - "sequence", - "occurred_at", - "type", - "failure_class", - "code", - "retry_scheduled" - ], - "additionalProperties": false - } - ], - "$id": "https://understudylabs.com/schemas/understudy-train-v1/experiment-event.json" -} diff --git a/apps/homescreen/schemas/understudy-train/manifest.json b/apps/homescreen/schemas/understudy-train/manifest.json deleted file mode 100644 index 2a0eda75..00000000 --- a/apps/homescreen/schemas/understudy-train/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "schema_version": "understudy.contract-bundle.v1", - "train_api_schema_version": "understudy-train-v1", - "bundle_sha256": "ca87c54f45ae89fe210ee2e48da2618cdf860f23dcccce163f004302bcdf7241", - "schemas": { - "experiment-event.json": "63f3b52377e13b59958b78f2ab050249cbd827d067756412d6bab3e7178b93b4", - "experiment-executor-cancellation-receipt.json": "21d5d8100017b3788a6b6907ca9089b32f95fce127659db59e4403771caac5a3", - "experiment-executor-job-ref.json": "56e791609f5038379bf72c62bee90e16296ae118307dec7ef9c7a9f21bf68a41", - "experiment-executor-job-status.json": "5bb05e0c9d03a95a530b4862fb204840e1897588bf3969eb19f1161901f968f6", - "experiment-executor-submit-request.json": "6ff8cfa383ff109d7dfe341e5ae2a4d330a8b298b0e8af51af0f1d3106f462c0", - "experiment-executor-usage-receipt.json": "ea81327efea7f2c7215aefa61d4b070e86316e4d20a02f91fa58a49d913b5d49", - "experiment-result.json": "b58840006bd6fee258639667f1fec83625a23c29e92f3f76d5a1e0de585d7f25", - "experiment-run-action-request.json": "bdc0242dd2e19b56f013608133658bcb0d1a32a88f13f73ed35c305b37d29027", - "experiment-run-create-response.json": "5dd86ea481a2597c81189113ff47a54d25b511b5e0f32da193f06db9857d2938", - "experiment-run-status-response.json": "b338626862f6a9c94109a73e2ae8b0fc6c366a486821a8d4f7a63e59d16d8a7a", - "experiment-spec.json": "b425faa979998896c6ad28df2d02abefdcef2c926ef71288cc05d3bed85a4ba7", - "training-capabilities-response.json": "2b21d26ad854d77d53b9cc455d57a2bf7cd8aec197e9c586009a867dac99b672", - "training-event.json": "c0cacfa957901121d8987b4b95ec275030cef6a4576945c6d1bdcebff750ea9f", - "training-run-action-request.json": "0820e8cd57a7d36c97af9168a219089902a7459c86a7a75f75094c79d7671ef8", - "training-run-create-request.json": "6667d1e51d02c769545743838dea755b1e9a2db90efc165151ad9d485ba433b1", - "training-run-create-response.json": "6e60daaaca16a4fe582bb0fd9a679b06b60e91275760bb45b9b46a8ba4afdce9", - "training-run-status-response.json": "8bf40472321d66b8bd5e4bebdaf10bcc22f98645941e3860d6e3961bf48991fe", - "training-run-validate-response.json": "5a989dabca149f6b87dcf99c32dc41bc49fe80e584c3f6bdbfc33a57f9c95a5a", - "training-upload-cleanup-request.json": "2cf5e7d03afae1858c5f6cce24a9aedfdfe771814a40d5f8bd1fb81bfae1f8ca", - "training-upload-intent-request.json": "f0947e33821b2dc33698196b28f66629f2ed3481d0c42db9b1d8dac69b519db6", - "training-upload-intent-response.json": "8ccff55d60a69590652b62ef02baf511125777885350814139b4b0797fc720de" - } -} diff --git a/apps/homescreen/scripts/check-orchard-contract.mjs b/apps/homescreen/scripts/check-orchard-contract.mjs deleted file mode 100644 index 0592cfbb..00000000 --- a/apps/homescreen/scripts/check-orchard-contract.mjs +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env node - -import { - contractBundleSha256, - eventSchemaSha256, - validateCanonicalEvent, -} from "./orchard-contract.mjs"; -import { validateEventPage, viewerAuthorized } from "./orchard-live-policy.mjs"; - -if (typeof validateCanonicalEvent !== "function") { - throw new Error("canonical event schema did not compile"); -} - -if (viewerAuthorized(undefined, "viewer") || viewerAuthorized("Bearer wrong", "viewer")) { - throw new Error("viewer capability check failed closed incorrectly"); -} -if (!viewerAuthorized("Bearer viewer", "viewer")) { - throw new Error("viewer capability check rejected an exact token"); -} - -const canonical = (sequence) => ({ sequence }); -const pageOptions = { - experimentId: "exp-test", - requestedAfter: 1, - isCanonicalEvent: (event) => Number.isInteger(event?.sequence), -}; -if (!validateEventPage({ - experiment_id: "exp-test", events: [canonical(2), canonical(3)], next_after: 3, has_more: false, -}, pageOptions)) throw new Error("valid event page was rejected"); -for (const invalid of [ - { experiment_id: "exp-test", events: [canonical(2), canonical(2)], next_after: 2, has_more: false }, - { experiment_id: "exp-test", events: [canonical(3), canonical(2)], next_after: 2, has_more: false }, - { experiment_id: "exp-test", events: [canonical(2)], next_after: 9, has_more: false }, - { experiment_id: "exp-test", events: [], next_after: 1, has_more: true }, -]) { - if (validateEventPage(invalid, pageOptions)) throw new Error("invalid event page was accepted"); -} - -process.stdout.write( - `${JSON.stringify({ - ok: true, - contract_bundle_sha256: contractBundleSha256, - event_schema_sha256: eventSchemaSha256, - })}\n`, -); diff --git a/apps/homescreen/scripts/orchard-contract.mjs b/apps/homescreen/scripts/orchard-contract.mjs deleted file mode 100644 index 40292b03..00000000 --- a/apps/homescreen/scripts/orchard-contract.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { createHash } from "node:crypto"; -import { readFileSync } from "node:fs"; -import Ajv2020 from "ajv/dist/2020.js"; - -const schemaUrl = new URL("../schemas/understudy-train/experiment-event.json", import.meta.url); -const manifestUrl = new URL("../schemas/understudy-train/manifest.json", import.meta.url); -const schemaBytes = readFileSync(schemaUrl); -const eventSchema = JSON.parse(schemaBytes); -const contractManifest = JSON.parse(readFileSync(manifestUrl, "utf8")); - -export const eventSchemaSha256 = createHash("sha256").update(schemaBytes).digest("hex"); -export const contractBundleSha256 = contractManifest.bundle_sha256; - -if (contractManifest.schemas?.["experiment-event.json"] !== eventSchemaSha256) { - throw new Error("vendored experiment event schema does not match the canonical manifest"); -} - -const ajv = new Ajv2020({ allErrors: true, strict: true }); -ajv.addFormat("date-time", true); -export const validateCanonicalEvent = ajv.compile(eventSchema); diff --git a/apps/homescreen/scripts/orchard-live-policy.mjs b/apps/homescreen/scripts/orchard-live-policy.mjs deleted file mode 100644 index 9b48f33e..00000000 --- a/apps/homescreen/scripts/orchard-live-policy.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { timingSafeEqual } from "node:crypto"; - -export const viewerAuthorized = (authorization, expectedToken) => { - if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) return false; - const supplied = Buffer.from(authorization.slice("Bearer ".length), "utf8"); - const expected = Buffer.from(expectedToken, "utf8"); - return supplied.length === expected.length && timingSafeEqual(supplied, expected); -}; - -export const validateEventPage = (body, { experimentId, requestedAfter, isCanonicalEvent }) => { - if ( - body?.experiment_id !== experimentId - || !Array.isArray(body.events) - || body.events.some((event) => !isCanonicalEvent(event)) - || !Number.isInteger(body.next_after) - || typeof body.has_more !== "boolean" - ) return false; - - const sequences = body.events.map((event) => event.sequence); - if (new Set(sequences).size !== sequences.length) return false; - if (sequences.some((sequence) => !Number.isInteger(sequence) || sequence <= requestedAfter)) return false; - if (sequences.some((sequence, index) => index > 0 && sequence <= sequences[index - 1])) return false; - if (body.next_after < requestedAfter) return false; - if (sequences.length === 0) return body.next_after === requestedAfter && body.has_more === false; - return body.next_after === sequences.at(-1); -}; diff --git a/apps/homescreen/scripts/orchard-workflow-proxy.mjs b/apps/homescreen/scripts/orchard-workflow-proxy.mjs deleted file mode 100644 index 702f8333..00000000 --- a/apps/homescreen/scripts/orchard-workflow-proxy.mjs +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env node -import { createServer } from "node:http"; -import { - contractBundleSha256, - eventSchemaSha256, - validateCanonicalEvent, -} from "./orchard-contract.mjs"; -import { validateEventPage, viewerAuthorized } from "./orchard-live-policy.mjs"; - -const required = (name) => { - const value = process.env[name]?.trim(); - if (!value) throw new Error(`${name} is required`); - return value; -}; - -const baseUrl = required("ORCHARD_TRAIN_API_URL").replace(/\/$/, ""); -const experimentId = required("ORCHARD_EXPERIMENT_ID"); -const apiKey = required("ORCHARD_TRAIN_API_KEY"); -const runToken = required("ORCHARD_EXPERIMENT_RUN_TOKEN"); -const viewerToken = required("ORCHARD_VIEWER_TOKEN"); -const allowedOrigin = process.env.ORCHARD_ALLOWED_ORIGIN?.trim() ?? "http://localhost:1420"; -const port = Number(process.env.ORCHARD_EVENT_PROXY_PORT ?? 1431); - -const safeEventKeys = new Set([ - "schema_version", "experiment_id", "sequence", "occurred_at", "type", - "budget_usd", "holdout_sealed", "phase", "candidate_id", "task_id", - "state", "metrics", "frontier", "usage", "failure_class", "code", - "retry_scheduled", -]); -const isCanonicalEvent = (event) => event !== null - && typeof event === "object" - && validateCanonicalEvent(event) - && event.experiment_id === experimentId - && Object.keys(event).every((key) => safeEventKeys.has(key) || key === "job"); - -const redact = (event) => Object.fromEntries( - Object.entries(event).filter(([key]) => safeEventKeys.has(key)), -); - -const headers = (origin) => ({ - ...(origin === allowedOrigin ? { "access-control-allow-origin": origin } : {}), - "access-control-allow-headers": "authorization, content-type", - "access-control-allow-methods": "GET, OPTIONS", - "cache-control": "no-store, max-age=0", - "content-type": "application/json; charset=utf-8", - "vary": "origin", -}); - -createServer(async (request, response) => { - const origin = request.headers.origin; - if (origin !== allowedOrigin) { - response.writeHead(403, headers(origin)); - response.end(JSON.stringify({ error: "origin not allowed" })); - return; - } - if (request.method === "OPTIONS") { - response.writeHead(204, headers(origin)); - response.end(); - return; - } - if (!viewerAuthorized(request.headers.authorization, viewerToken)) { - response.writeHead(401, headers(origin)); - response.end(JSON.stringify({ error: "viewer authorization required" })); - return; - } - const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`); - if (url.pathname === "/healthz") { - response.writeHead(200, headers(origin)); - response.end(JSON.stringify({ - ok: true, - experiment_id: experimentId, - contract_bundle_sha256: contractBundleSha256, - event_schema_sha256: eventSchemaSha256, - })); - return; - } - if (url.pathname !== "/events") { - response.writeHead(404, headers(origin)); - response.end(JSON.stringify({ error: "not found" })); - return; - } - const after = url.searchParams.get("after") ?? "-1"; - if (!/^-1$|^\d+$/.test(after)) { - response.writeHead(400, headers(origin)); - response.end(JSON.stringify({ error: "invalid cursor" })); - return; - } - const requestedAfter = Number(after); - try { - const upstream = await fetch( - `${baseUrl}/api/train/v1/experiments/${encodeURIComponent(experimentId)}/events?after=${after}`, - { - headers: { - authorization: `Bearer ${apiKey}`, - "x-understudy-train-run-token": runToken, - }, - signal: AbortSignal.timeout(8_000), - }, - ); - const body = await upstream.json(); - if (!upstream.ok) throw new Error(`upstream ${upstream.status}`); - if (!validateEventPage(body, { experimentId, requestedAfter, isCanonicalEvent })) { - throw new Error("upstream returned an invalid event stream"); - } - response.writeHead(200, headers(origin)); - response.end(JSON.stringify({ - experiment_id: body.experiment_id, - events: body.events.map(redact), - next_after: body.next_after, - has_more: body.has_more, - })); - } catch (error) { - response.writeHead(502, headers(origin)); - response.end(JSON.stringify({ error: error instanceof Error ? error.message : "upstream unavailable" })); - } -}).listen(port, "127.0.0.1", () => { - console.log(`orchard workflow proxy listening on http://127.0.0.1:${port}`); -}); diff --git a/design-qa.md b/design-qa.md index 3debc9e1..e7fb5373 100644 --- a/design-qa.md +++ b/design-qa.md @@ -70,7 +70,6 @@ final result: passed ## Local classifier training progress -- Source visual truth: Fable training flow at `http://localhost:1423/cedar/flows/training` - Reference capture: `outputs/training-design-qa/reference-training-1180x820.png` - Native capture: `eb95f01c-8835-49bd-b5d4-a53ed860e243-screenshot.png` - Combined comparison: `outputs/training-design-qa/comparison-reference-native.png` diff --git a/docs/prompt-evolution.md b/docs/prompt-evolution.md index 14c34658..1ba371c7 100644 --- a/docs/prompt-evolution.md +++ b/docs/prompt-evolution.md @@ -61,12 +61,10 @@ Runs are the expensive unit, not tokens. One invocation queues `1 (baseline) + generations + 1 (dev select) + 1 (holdout final)` runs; `--budget-runs` hard-caps this and the command stops cleanly when exhausted. -Per-generation cost on a real benchmark ≈ +Per-generation cost on a real benchmark is approximately `variants × train_tasks × rollouts_per_task` full agentic rollouts plus one -authoring call (cents). On a cedar-sized benchmark (~29 tasks/split), a -3-variant generation is ~90–120 rollouts (the bare arm reruns each -generation) — wall-clock is dominated by the executor, so budget generations, -not variants: 2 generations × 3 variants usually beats 6 × 1. +authoring call. Wall-clock is dominated by the executor, so use the measured +task count and `--budget-runs` to choose generations and variants. ## Future hook: rejection-guidance evolution diff --git a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-claude-sonnet-4-6.json b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-claude-sonnet-4-6.json index 62e50d28..bde8b09e 100644 --- a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-claude-sonnet-4-6.json +++ b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-claude-sonnet-4-6.json @@ -24008,7 +24008,7 @@ { "role": "tool", "tool_call_id": "chatcmpl-tool-bbd7f11e3fb1323c", - "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4170884212\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354982\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354986\", \"updated_at\": \"2026-07-19T10:37:25.354986\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"0649640032\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354994\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354994\", \"updated_at\": \"2026-07-19T10:37:25.354995\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3231869322\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354997\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354998\", \"updated_at\": \"2026-07-19T10:37:25.354998\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7828327069\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355000\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355000\", \"updated_at\": \"2026-07-19T10:37:25.355000\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7192170274\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355002\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355002\", \"updated_at\": \"2026-07-19T10:37:25.355003\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"2084319071\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355004\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355005\", \"updated_at\": \"2026-07-19T10:37:25.355005\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6371729386\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355007\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355007\", \"updated_at\": \"2026-07-19T10:37:25.355007\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"1354368537\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355009\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355010\", \"updated_at\": \"2026-07-19T10:37:25.355010\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4182278999\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355012\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355012\", \"updated_at\": \"2026-07-19T10:37:25.355013\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1110227642\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355014\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355015\", \"updated_at\": \"2026-07-19T10:37:25.355015\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"2413039997\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355017\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355017\", \"updated_at\": \"2026-07-19T10:37:25.355018\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8269757026\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355019\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355020\", \"updated_at\": \"2026-07-19T10:37:25.355020\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7019932395\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355022\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355022\", \"updated_at\": \"2026-07-19T10:37:25.355022\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5422548652\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355024\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355024\", \"updated_at\": \"2026-07-19T10:37:25.355025\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"5882943749\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355026\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355027\", \"updated_at\": \"2026-07-19T10:37:25.355027\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7669023214\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355029\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355029\", \"updated_at\": \"2026-07-19T10:37:25.355029\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"2684405319\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355031\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355031\", \"updated_at\": \"2026-07-19T10:37:25.355032\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8971833083\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355033\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355034\", \"updated_at\": \"2026-07-19T10:37:25.355034\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"4306137309\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355036\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355037\", \"updated_at\": \"2026-07-19T10:37:25.355037\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"7618859915\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355038\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355039\", \"updated_at\": \"2026-07-19T10:37:25.355039\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"1849737566\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355041\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355041\", \"updated_at\": \"2026-07-19T10:37:25.355042\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"5632534261\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355043\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355044\", \"updated_at\": \"2026-07-19T10:37:25.355044\"}], \"count\": 22}" + "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4170884212\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354982\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354986\", \"updated_at\": \"2026-07-19T10:37:25.354986\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"0649640032\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354994\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354994\", \"updated_at\": \"2026-07-19T10:37:25.354995\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3231869322\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.354997\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.354998\", \"updated_at\": \"2026-07-19T10:37:25.354998\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7828327069\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355000\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355000\", \"updated_at\": \"2026-07-19T10:37:25.355000\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7192170274\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355002\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355002\", \"updated_at\": \"2026-07-19T10:37:25.355003\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"2084319071\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355004\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355005\", \"updated_at\": \"2026-07-19T10:37:25.355005\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6371729386\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355007\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355007\", \"updated_at\": \"2026-07-19T10:37:25.355007\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"1354368537\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355009\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355010\", \"updated_at\": \"2026-07-19T10:37:25.355010\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4182278999\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355012\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355012\", \"updated_at\": \"2026-07-19T10:37:25.355013\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1110227642\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355014\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355015\", \"updated_at\": \"2026-07-19T10:37:25.355015\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"2413039997\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355017\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355017\", \"updated_at\": \"2026-07-19T10:37:25.355018\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8269757026\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355019\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355020\", \"updated_at\": \"2026-07-19T10:37:25.355020\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7019932395\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355022\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355022\", \"updated_at\": \"2026-07-19T10:37:25.355022\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5422548652\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355024\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355024\", \"updated_at\": \"2026-07-19T10:37:25.355025\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"5882943749\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355026\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355027\", \"updated_at\": \"2026-07-19T10:37:25.355027\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7669023214\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355029\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355029\", \"updated_at\": \"2026-07-19T10:37:25.355029\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"2684405319\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355031\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355031\", \"updated_at\": \"2026-07-19T10:37:25.355032\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8971833083\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355033\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355034\", \"updated_at\": \"2026-07-19T10:37:25.355034\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"4306137309\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355036\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355037\", \"updated_at\": \"2026-07-19T10:37:25.355037\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"7618859915\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355038\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355039\", \"updated_at\": \"2026-07-19T10:37:25.355039\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"1849737566\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355041\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355041\", \"updated_at\": \"2026-07-19T10:37:25.355042\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"5632534261\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:37:25.355043\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:37:25.355044\", \"updated_at\": \"2026-07-19T10:37:25.355044\"}], \"count\": 22}" }, { "role": "tool", @@ -24294,13 +24294,13 @@ "role": "assistant", "content": "All conversations have been tagged and annotated with Jira references. Now let me post the summary to the #engineering Slack channel.", "tool_calls": [ - "{\"id\": \"chatcmpl-tool-91ffdce43402c1af\", \"name\": \"api_fetch\", \"arguments\": \"{\\\"body\\\": \\\"{\\\\\\\"channel\\\\\\\": \\\\\\\"C_eng\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"*Bug Triage Summary \\u2014 February 07, 2026*\\\\\\\\n\\\\\\\\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\\\\\\\\n\\\\\\\\n*10 bug reports triaged to Jira (project SUP):*\\\\\\\\n\\\\\\\\n*Critical (Highest):*\\\\\\\\n1. SUP-4f211 \\u2014 Dashboard crashes when filtering by date \\u2014 Customer: user1@acme.com (hs_conv_201) \\u2014 keyword: crash\\\\\\\\n2. SUP-832ea \\u2014 500 error on API endpoint /v2/users \\u2014 Customer: dev@partner.io (hs_conv_203) \\u2014 keyword: 500\\\\\\\\n\\\\\\\\n*Major (High):*\\\\\\\\n3. SUP-882b3 \\u2014 Login fails with error message \\u2014 Customer: user3@techfirm.com (hs_conv_205) \\u2014 keyword: error\\\\\\\\n4. SUP-9d74b \\u2014 Export feature broken in new release \\u2014 Customer: user5@startup.io (hs_conv_207) \\u2014 keyword: broken\\\\\\\\n5. SUP-ac95c \\u2014 Calendar sync error after timezone change \\u2014 Customer: user11@enterprise.com (hs_conv_214) \\u2014 keyword: error \\u2014 Override: enterprise.com = Always Process (VIP customer)\\\\\\\\n6. SUP-5d466 \\u2014 Chart rendering broken on Firefox \\u2014 Customer: admin@blockedcorp.co (hs_conv_218) \\u2014 keyword: broken\\\\\\\\n7. SUP-aaaec \\u2014 Webhook delivery broken since yesterday \\u2014 Customer: ops@partner.io (hs_conv_219) \\u2014 keyword: broken\\\\\\\\n\\\\\\\\n*Minor (Medium):*\\\\\\\\n8. SUP-ab401 \\u2014 API rate limiting too aggressive \\u2014 Customer: user8@partner.io (hs_conv_210) \\u2014 default (*)\\\\\\\\n9. SUP-224b7 \\u2014 Notifications not sending \\u2014 Customer: user9@enterprise.com (hs_conv_211) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer)\\\\\\\\n10. SUP-e5f46 \\u2014 Widget rendering glitch on enterprise dashboard \\u2014 Customer: vip@enterprise.com (hs_conv_215) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\\\\\\\\n\\\\\\\\n*Excluded from triage:*\\\\\\\\n\\u2022 hs_conv_204 \\u2014 Billing page won't load (admin@blockedcorp.com) \\u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\\\\\\\\n\\u2022 hs_conv_213 \\u2014 Report generation crashes on large datasets (spam@spammer.net) \\u2014 excluded: customer_domain spammer.net (Known abuse)\\\\\\\\n\\u2022 hs_conv_216 \\u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \\u2014 excluded: Override rule internal.acme.com = Never Process\\\\\\\\n\\u2022 hs_conv_209 \\u2014 Search returns wrong results (user7@startup.io) \\u2014 not triaged: status is closed\\\\\\\\n\\\\\\\\n*Not bug reports (skipped):*\\\\\\\\n\\u2022 hs_conv_202 \\u2014 How to set up SSO with Okta (user2@acme.com) \\u2014 tag: question\\\\\\\\n\\u2022 hs_conv_206 \\u2014 Login failure on mobile app (user4@techfirm.com) \\u2014 tag: bug (not bug-report)\\\\\\\\n\\u2022 hs_conv_208 \\u2014 Add dark mode to settings page (user6@acme.com) \\u2014 tags: feature-request, bug-report \\u2014 excluded by feature-request tag; no override applies (domain acme.com)\\\\\\\\n\\u2022 hs_conv_217 \\u2014 Notification bell not clearing (user12@techfirm.com) \\u2014 tag: bug-reports (not bug-report)\\\\\\\\n\\u2022 hs_conv_noise_002 \\u2014 Import CSV failing silently (noise_user2@cedarpoint.example.com) \\u2014 tag: import\\\\\\\\n\\u2022 hs_conv_noise_003 \\u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \\u2014 tag: api\\\\\\\\n\\u2022 hs_conv_noise_010 \\u2014 Beacon widget not appearing on site (noise_user2@cedarpoint.example.com) \\u2014 tag: widget\\\\\\\\n\\\\\\\\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.\\\\\\\"}\\\", \\\"method\\\": \\\"POST\\\", \\\"params\\\": null, \\\"url\\\": \\\"https://slack.com/api/chat.postMessage\\\"}\"}" + "{\"id\": \"chatcmpl-tool-91ffdce43402c1af\", \"name\": \"api_fetch\", \"arguments\": \"{\\\"body\\\": \\\"{\\\\\\\"channel\\\\\\\": \\\\\\\"C_eng\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"*Bug Triage Summary \\u2014 February 07, 2026*\\\\\\\\n\\\\\\\\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\\\\\\\\n\\\\\\\\n*10 bug reports triaged to Jira (project SUP):*\\\\\\\\n\\\\\\\\n*Critical (Highest):*\\\\\\\\n1. SUP-4f211 \\u2014 Dashboard crashes when filtering by date \\u2014 Customer: user1@acme.com (hs_conv_201) \\u2014 keyword: crash\\\\\\\\n2. SUP-832ea \\u2014 500 error on API endpoint /v2/users \\u2014 Customer: dev@partner.io (hs_conv_203) \\u2014 keyword: 500\\\\\\\\n\\\\\\\\n*Major (High):*\\\\\\\\n3. SUP-882b3 \\u2014 Login fails with error message \\u2014 Customer: user3@techfirm.com (hs_conv_205) \\u2014 keyword: error\\\\\\\\n4. SUP-9d74b \\u2014 Export feature broken in new release \\u2014 Customer: user5@startup.io (hs_conv_207) \\u2014 keyword: broken\\\\\\\\n5. SUP-ac95c \\u2014 Calendar sync error after timezone change \\u2014 Customer: user11@enterprise.com (hs_conv_214) \\u2014 keyword: error \\u2014 Override: enterprise.com = Always Process (VIP customer)\\\\\\\\n6. SUP-5d466 \\u2014 Chart rendering broken on Firefox \\u2014 Customer: admin@blockedcorp.co (hs_conv_218) \\u2014 keyword: broken\\\\\\\\n7. SUP-aaaec \\u2014 Webhook delivery broken since yesterday \\u2014 Customer: ops@partner.io (hs_conv_219) \\u2014 keyword: broken\\\\\\\\n\\\\\\\\n*Minor (Medium):*\\\\\\\\n8. SUP-ab401 \\u2014 API rate limiting too aggressive \\u2014 Customer: user8@partner.io (hs_conv_210) \\u2014 default (*)\\\\\\\\n9. SUP-224b7 \\u2014 Notifications not sending \\u2014 Customer: user9@enterprise.com (hs_conv_211) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer)\\\\\\\\n10. SUP-e5f46 \\u2014 Widget rendering glitch on enterprise dashboard \\u2014 Customer: vip@enterprise.com (hs_conv_215) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\\\\\\\\n\\\\\\\\n*Excluded from triage:*\\\\\\\\n\\u2022 hs_conv_204 \\u2014 Billing page won't load (admin@blockedcorp.com) \\u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\\\\\\\\n\\u2022 hs_conv_213 \\u2014 Report generation crashes on large datasets (spam@spammer.net) \\u2014 excluded: customer_domain spammer.net (Known abuse)\\\\\\\\n\\u2022 hs_conv_216 \\u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \\u2014 excluded: Override rule internal.acme.com = Never Process\\\\\\\\n\\u2022 hs_conv_209 \\u2014 Search returns wrong results (user7@startup.io) \\u2014 not triaged: status is closed\\\\\\\\n\\\\\\\\n*Not bug reports (skipped):*\\\\\\\\n\\u2022 hs_conv_202 \\u2014 How to set up SSO with Okta (user2@acme.com) \\u2014 tag: question\\\\\\\\n\\u2022 hs_conv_206 \\u2014 Login failure on mobile app (user4@techfirm.com) \\u2014 tag: bug (not bug-report)\\\\\\\\n\\u2022 hs_conv_208 \\u2014 Add dark mode to settings page (user6@acme.com) \\u2014 tags: feature-request, bug-report \\u2014 excluded by feature-request tag; no override applies (domain acme.com)\\\\\\\\n\\u2022 hs_conv_217 \\u2014 Notification bell not clearing (user12@techfirm.com) \\u2014 tag: bug-reports (not bug-report)\\\\\\\\n\\u2022 hs_conv_noise_002 \\u2014 Import CSV failing silently (noise_user2@pinepoint.example.com) \\u2014 tag: import\\\\\\\\n\\u2022 hs_conv_noise_003 \\u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \\u2014 tag: api\\\\\\\\n\\u2022 hs_conv_noise_010 \\u2014 Beacon widget not appearing on site (noise_user2@pinepoint.example.com) \\u2014 tag: widget\\\\\\\\n\\\\\\\\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.\\\\\\\"}\\\", \\\"method\\\": \\\"POST\\\", \\\"params\\\": null, \\\"url\\\": \\\"https://slack.com/api/chat.postMessage\\\"}\"}" ] }, { "role": "tool", "tool_call_id": "chatcmpl-tool-91ffdce43402c1af", - "content": "{\"ok\": true, \"message\": {\"ts\": \"1784482802.148668\", \"channel\": \"C_eng\", \"user\": \"USLACKBOT\", \"text\": \"*Bug Triage Summary \\u2014 February 07, 2026*\\n\\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\\n\\n*10 bug reports triaged to Jira (project SUP):*\\n\\n*Critical (Highest):*\\n1. SUP-4f211 \\u2014 Dashboard crashes when filtering by date \\u2014 Customer: user1@acme.com (hs_conv_201) \\u2014 keyword: crash\\n2. SUP-832ea \\u2014 500 error on API endpoint /v2/users \\u2014 Customer: dev@partner.io (hs_conv_203) \\u2014 keyword: 500\\n\\n*Major (High):*\\n3. SUP-882b3 \\u2014 Login fails with error message \\u2014 Customer: user3@techfirm.com (hs_conv_205) \\u2014 keyword: error\\n4. SUP-9d74b \\u2014 Export feature broken in new release \\u2014 Customer: user5@startup.io (hs_conv_207) \\u2014 keyword: broken\\n5. SUP-ac95c \\u2014 Calendar sync error after timezone change \\u2014 Customer: user11@enterprise.com (hs_conv_214) \\u2014 keyword: error \\u2014 Override: enterprise.com = Always Process (VIP customer)\\n6. SUP-5d466 \\u2014 Chart rendering broken on Firefox \\u2014 Customer: admin@blockedcorp.co (hs_conv_218) \\u2014 keyword: broken\\n7. SUP-aaaec \\u2014 Webhook delivery broken since yesterday \\u2014 Customer: ops@partner.io (hs_conv_219) \\u2014 keyword: broken\\n\\n*Minor (Medium):*\\n8. SUP-ab401 \\u2014 API rate limiting too aggressive \\u2014 Customer: user8@partner.io (hs_conv_210) \\u2014 default (*)\\n9. SUP-224b7 \\u2014 Notifications not sending \\u2014 Customer: user9@enterprise.com (hs_conv_211) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer)\\n10. SUP-e5f46 \\u2014 Widget rendering glitch on enterprise dashboard \\u2014 Customer: vip@enterprise.com (hs_conv_215) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\\n\\n*Excluded from triage:*\\n\\u2022 hs_conv_204 \\u2014 Billing page won't load (admin@blockedcorp.com) \\u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\\n\\u2022 hs_conv_213 \\u2014 Report generation crashes on large datasets (spam@spammer.net) \\u2014 excluded: customer_domain spammer.net (Known abuse)\\n\\u2022 hs_conv_216 \\u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \\u2014 excluded: Override rule internal.acme.com = Never Process\\n\\u2022 hs_conv_209 \\u2014 Search returns wrong results (user7@startup.io) \\u2014 not triaged: status is closed\\n\\n*Not bug reports (skipped):*\\n\\u2022 hs_conv_202 \\u2014 How to set up SSO with Okta (user2@acme.com) \\u2014 tag: question\\n\\u2022 hs_conv_206 \\u2014 Login failure on mobile app (user4@techfirm.com) \\u2014 tag: bug (not bug-report)\\n\\u2022 hs_conv_208 \\u2014 Add dark mode to settings page (user6@acme.com) \\u2014 tags: feature-request, bug-report \\u2014 excluded by feature-request tag; no override applies (domain acme.com)\\n\\u2022 hs_conv_217 \\u2014 Notification bell not clearing (user12@techfirm.com) \\u2014 tag: bug-reports (not bug-report)\\n\\u2022 hs_conv_noise_002 \\u2014 Import CSV failing silently (noise_user2@cedarpoint.example.com) \\u2014 tag: import\\n\\u2022 hs_conv_noise_003 \\u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \\u2014 tag: api\\n\\u2022 hs_conv_noise_010 \\u2014 Beacon widget not appearing on site (noise_user2@cedarpoint.example.com) \\u2014 tag: widget\\n\\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.\", \"type\": \"message\", \"bot_id\": \"USLACKBOT\", \"username\": \"Zapier\"}, \"ts\": \"1784482802.148668\", \"channel\": \"C_eng\"}" + "content": "{\"ok\": true, \"message\": {\"ts\": \"1784482802.148668\", \"channel\": \"C_eng\", \"user\": \"USLACKBOT\", \"text\": \"*Bug Triage Summary \\u2014 February 07, 2026*\\n\\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\\n\\n*10 bug reports triaged to Jira (project SUP):*\\n\\n*Critical (Highest):*\\n1. SUP-4f211 \\u2014 Dashboard crashes when filtering by date \\u2014 Customer: user1@acme.com (hs_conv_201) \\u2014 keyword: crash\\n2. SUP-832ea \\u2014 500 error on API endpoint /v2/users \\u2014 Customer: dev@partner.io (hs_conv_203) \\u2014 keyword: 500\\n\\n*Major (High):*\\n3. SUP-882b3 \\u2014 Login fails with error message \\u2014 Customer: user3@techfirm.com (hs_conv_205) \\u2014 keyword: error\\n4. SUP-9d74b \\u2014 Export feature broken in new release \\u2014 Customer: user5@startup.io (hs_conv_207) \\u2014 keyword: broken\\n5. SUP-ac95c \\u2014 Calendar sync error after timezone change \\u2014 Customer: user11@enterprise.com (hs_conv_214) \\u2014 keyword: error \\u2014 Override: enterprise.com = Always Process (VIP customer)\\n6. SUP-5d466 \\u2014 Chart rendering broken on Firefox \\u2014 Customer: admin@blockedcorp.co (hs_conv_218) \\u2014 keyword: broken\\n7. SUP-aaaec \\u2014 Webhook delivery broken since yesterday \\u2014 Customer: ops@partner.io (hs_conv_219) \\u2014 keyword: broken\\n\\n*Minor (Medium):*\\n8. SUP-ab401 \\u2014 API rate limiting too aggressive \\u2014 Customer: user8@partner.io (hs_conv_210) \\u2014 default (*)\\n9. SUP-224b7 \\u2014 Notifications not sending \\u2014 Customer: user9@enterprise.com (hs_conv_211) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer)\\n10. SUP-e5f46 \\u2014 Widget rendering glitch on enterprise dashboard \\u2014 Customer: vip@enterprise.com (hs_conv_215) \\u2014 default (*) \\u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\\n\\n*Excluded from triage:*\\n\\u2022 hs_conv_204 \\u2014 Billing page won't load (admin@blockedcorp.com) \\u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\\n\\u2022 hs_conv_213 \\u2014 Report generation crashes on large datasets (spam@spammer.net) \\u2014 excluded: customer_domain spammer.net (Known abuse)\\n\\u2022 hs_conv_216 \\u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \\u2014 excluded: Override rule internal.acme.com = Never Process\\n\\u2022 hs_conv_209 \\u2014 Search returns wrong results (user7@startup.io) \\u2014 not triaged: status is closed\\n\\n*Not bug reports (skipped):*\\n\\u2022 hs_conv_202 \\u2014 How to set up SSO with Okta (user2@acme.com) \\u2014 tag: question\\n\\u2022 hs_conv_206 \\u2014 Login failure on mobile app (user4@techfirm.com) \\u2014 tag: bug (not bug-report)\\n\\u2022 hs_conv_208 \\u2014 Add dark mode to settings page (user6@acme.com) \\u2014 tags: feature-request, bug-report \\u2014 excluded by feature-request tag; no override applies (domain acme.com)\\n\\u2022 hs_conv_217 \\u2014 Notification bell not clearing (user12@techfirm.com) \\u2014 tag: bug-reports (not bug-report)\\n\\u2022 hs_conv_noise_002 \\u2014 Import CSV failing silently (noise_user2@pinepoint.example.com) \\u2014 tag: import\\n\\u2022 hs_conv_noise_003 \\u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \\u2014 tag: api\\n\\u2022 hs_conv_noise_010 \\u2014 Beacon widget not appearing on site (noise_user2@pinepoint.example.com) \\u2014 tag: widget\\n\\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.\", \"type\": \"message\", \"bot_id\": \"USLACKBOT\", \"username\": \"Zapier\"}, \"ts\": \"1784482802.148668\", \"channel\": \"C_eng\"}" }, { "role": "assistant", @@ -25021,7 +25021,7 @@ "ts": "1784482802.148668", "channel_id": "C_eng", "user_id": "USLACKBOT", - "text": "*Bug Triage Summary \u2014 February 07, 2026*\n\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\n\n*10 bug reports triaged to Jira (project SUP):*\n\n*Critical (Highest):*\n1. SUP-4f211 \u2014 Dashboard crashes when filtering by date \u2014 Customer: user1@acme.com (hs_conv_201) \u2014 keyword: crash\n2. SUP-832ea \u2014 500 error on API endpoint /v2/users \u2014 Customer: dev@partner.io (hs_conv_203) \u2014 keyword: 500\n\n*Major (High):*\n3. SUP-882b3 \u2014 Login fails with error message \u2014 Customer: user3@techfirm.com (hs_conv_205) \u2014 keyword: error\n4. SUP-9d74b \u2014 Export feature broken in new release \u2014 Customer: user5@startup.io (hs_conv_207) \u2014 keyword: broken\n5. SUP-ac95c \u2014 Calendar sync error after timezone change \u2014 Customer: user11@enterprise.com (hs_conv_214) \u2014 keyword: error \u2014 Override: enterprise.com = Always Process (VIP customer)\n6. SUP-5d466 \u2014 Chart rendering broken on Firefox \u2014 Customer: admin@blockedcorp.co (hs_conv_218) \u2014 keyword: broken\n7. SUP-aaaec \u2014 Webhook delivery broken since yesterday \u2014 Customer: ops@partner.io (hs_conv_219) \u2014 keyword: broken\n\n*Minor (Medium):*\n8. SUP-ab401 \u2014 API rate limiting too aggressive \u2014 Customer: user8@partner.io (hs_conv_210) \u2014 default (*)\n9. SUP-224b7 \u2014 Notifications not sending \u2014 Customer: user9@enterprise.com (hs_conv_211) \u2014 default (*) \u2014 Override: enterprise.com = Always Process (VIP customer)\n10. SUP-e5f46 \u2014 Widget rendering glitch on enterprise dashboard \u2014 Customer: vip@enterprise.com (hs_conv_215) \u2014 default (*) \u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\n\n*Excluded from triage:*\n\u2022 hs_conv_204 \u2014 Billing page won't load (admin@blockedcorp.com) \u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\n\u2022 hs_conv_213 \u2014 Report generation crashes on large datasets (spam@spammer.net) \u2014 excluded: customer_domain spammer.net (Known abuse)\n\u2022 hs_conv_216 \u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \u2014 excluded: Override rule internal.acme.com = Never Process\n\u2022 hs_conv_209 \u2014 Search returns wrong results (user7@startup.io) \u2014 not triaged: status is closed\n\n*Not bug reports (skipped):*\n\u2022 hs_conv_202 \u2014 How to set up SSO with Okta (user2@acme.com) \u2014 tag: question\n\u2022 hs_conv_206 \u2014 Login failure on mobile app (user4@techfirm.com) \u2014 tag: bug (not bug-report)\n\u2022 hs_conv_208 \u2014 Add dark mode to settings page (user6@acme.com) \u2014 tags: feature-request, bug-report \u2014 excluded by feature-request tag; no override applies (domain acme.com)\n\u2022 hs_conv_217 \u2014 Notification bell not clearing (user12@techfirm.com) \u2014 tag: bug-reports (not bug-report)\n\u2022 hs_conv_noise_002 \u2014 Import CSV failing silently (noise_user2@cedarpoint.example.com) \u2014 tag: import\n\u2022 hs_conv_noise_003 \u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \u2014 tag: api\n\u2022 hs_conv_noise_010 \u2014 Beacon widget not appearing on site (noise_user2@cedarpoint.example.com) \u2014 tag: widget\n\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.", + "text": "*Bug Triage Summary \u2014 February 07, 2026*\n\nTriage policy applied from spreadsheet ss_triage_policy (sheets: Severity Mapping, Exclusions, Override Rules).\n\n*10 bug reports triaged to Jira (project SUP):*\n\n*Critical (Highest):*\n1. SUP-4f211 \u2014 Dashboard crashes when filtering by date \u2014 Customer: user1@acme.com (hs_conv_201) \u2014 keyword: crash\n2. SUP-832ea \u2014 500 error on API endpoint /v2/users \u2014 Customer: dev@partner.io (hs_conv_203) \u2014 keyword: 500\n\n*Major (High):*\n3. SUP-882b3 \u2014 Login fails with error message \u2014 Customer: user3@techfirm.com (hs_conv_205) \u2014 keyword: error\n4. SUP-9d74b \u2014 Export feature broken in new release \u2014 Customer: user5@startup.io (hs_conv_207) \u2014 keyword: broken\n5. SUP-ac95c \u2014 Calendar sync error after timezone change \u2014 Customer: user11@enterprise.com (hs_conv_214) \u2014 keyword: error \u2014 Override: enterprise.com = Always Process (VIP customer)\n6. SUP-5d466 \u2014 Chart rendering broken on Firefox \u2014 Customer: admin@blockedcorp.co (hs_conv_218) \u2014 keyword: broken\n7. SUP-aaaec \u2014 Webhook delivery broken since yesterday \u2014 Customer: ops@partner.io (hs_conv_219) \u2014 keyword: broken\n\n*Minor (Medium):*\n8. SUP-ab401 \u2014 API rate limiting too aggressive \u2014 Customer: user8@partner.io (hs_conv_210) \u2014 default (*)\n9. SUP-224b7 \u2014 Notifications not sending \u2014 Customer: user9@enterprise.com (hs_conv_211) \u2014 default (*) \u2014 Override: enterprise.com = Always Process (VIP customer)\n10. SUP-e5f46 \u2014 Widget rendering glitch on enterprise dashboard \u2014 Customer: vip@enterprise.com (hs_conv_215) \u2014 default (*) \u2014 Override: enterprise.com = Always Process (VIP customer, overrides feature-request tag exclusion)\n\n*Excluded from triage:*\n\u2022 hs_conv_204 \u2014 Billing page won't load (admin@blockedcorp.com) \u2014 excluded: customer_domain blockedcorp.com (Contract terminated)\n\u2022 hs_conv_213 \u2014 Report generation crashes on large datasets (spam@spammer.net) \u2014 excluded: customer_domain spammer.net (Known abuse)\n\u2022 hs_conv_216 \u2014 Internal tool crashes on bulk import (dev@internal.acme.com) \u2014 excluded: Override rule internal.acme.com = Never Process\n\u2022 hs_conv_209 \u2014 Search returns wrong results (user7@startup.io) \u2014 not triaged: status is closed\n\n*Not bug reports (skipped):*\n\u2022 hs_conv_202 \u2014 How to set up SSO with Okta (user2@acme.com) \u2014 tag: question\n\u2022 hs_conv_206 \u2014 Login failure on mobile app (user4@techfirm.com) \u2014 tag: bug (not bug-report)\n\u2022 hs_conv_208 \u2014 Add dark mode to settings page (user6@acme.com) \u2014 tags: feature-request, bug-report \u2014 excluded by feature-request tag; no override applies (domain acme.com)\n\u2022 hs_conv_217 \u2014 Notification bell not clearing (user12@techfirm.com) \u2014 tag: bug-reports (not bug-report)\n\u2022 hs_conv_noise_002 \u2014 Import CSV failing silently (noise_user2@pinepoint.example.com) \u2014 tag: import\n\u2022 hs_conv_noise_003 \u2014 Webhook signatures not matching (noise_user3@tidewaterio.example.com) \u2014 tag: api\n\u2022 hs_conv_noise_010 \u2014 Beacon widget not appearing on site (noise_user2@pinepoint.example.com) \u2014 tag: widget\n\nAll processed conversations tagged with jira-triaged and the Jira issue key. Internal notes added with Jira references.", "thread_ts": null, "reply_count": 0, "is_bot": true, @@ -25642,7 +25642,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "import" @@ -25690,7 +25690,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "widget" diff --git a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-31b-it.json b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-31b-it.json index dbfc5f94..c8125e46 100644 --- a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-31b-it.json +++ b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-31b-it.json @@ -24882,7 +24882,7 @@ { "role": "tool", "tool_call_id": "chatcmpl-tool-b9ef16537e8c1879", - "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3301669496\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363494\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363499\", \"updated_at\": \"2026-07-19T10:38:29.363499\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"6788764549\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363506\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363506\", \"updated_at\": \"2026-07-19T10:38:29.363507\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0197678515\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363509\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363509\", \"updated_at\": \"2026-07-19T10:38:29.363509\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8791278381\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363511\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363512\", \"updated_at\": \"2026-07-19T10:38:29.363512\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4191898784\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363513\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363514\", \"updated_at\": \"2026-07-19T10:38:29.363514\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"9942763308\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363516\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363516\", \"updated_at\": \"2026-07-19T10:38:29.363516\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4349577752\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363518\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363519\", \"updated_at\": \"2026-07-19T10:38:29.363519\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"5561833260\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363521\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363521\", \"updated_at\": \"2026-07-19T10:38:29.363521\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2819479881\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363523\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363523\", \"updated_at\": \"2026-07-19T10:38:29.363524\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3163653031\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363525\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363526\", \"updated_at\": \"2026-07-19T10:38:29.363526\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"1098765712\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363528\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363528\", \"updated_at\": \"2026-07-19T10:38:29.363528\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4460814937\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363530\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363530\", \"updated_at\": \"2026-07-19T10:38:29.363530\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3778338416\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363532\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363532\", \"updated_at\": \"2026-07-19T10:38:29.363533\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0972007980\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363534\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363535\", \"updated_at\": \"2026-07-19T10:38:29.363535\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"2147812612\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363537\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363537\", \"updated_at\": \"2026-07-19T10:38:29.363538\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9413055255\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363539\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363540\", \"updated_at\": \"2026-07-19T10:38:29.363540\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"3704336337\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363542\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363542\", \"updated_at\": \"2026-07-19T10:38:29.363543\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2222145750\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363544\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363545\", \"updated_at\": \"2026-07-19T10:38:29.363545\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"8406199409\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363546\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363547\", \"updated_at\": \"2026-07-19T10:38:29.363547\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"9315776372\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363549\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363549\", \"updated_at\": \"2026-07-19T10:38:29.363549\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"3369408170\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363551\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363551\", \"updated_at\": \"2026-07-19T10:38:29.363552\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"3930297243\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363553\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363554\", \"updated_at\": \"2026-07-19T10:38:29.363554\"}], \"count\": 22}" + "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3301669496\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363494\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363499\", \"updated_at\": \"2026-07-19T10:38:29.363499\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"6788764549\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363506\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363506\", \"updated_at\": \"2026-07-19T10:38:29.363507\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0197678515\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363509\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363509\", \"updated_at\": \"2026-07-19T10:38:29.363509\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8791278381\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363511\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363512\", \"updated_at\": \"2026-07-19T10:38:29.363512\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4191898784\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363513\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363514\", \"updated_at\": \"2026-07-19T10:38:29.363514\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"9942763308\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363516\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363516\", \"updated_at\": \"2026-07-19T10:38:29.363516\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4349577752\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363518\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363519\", \"updated_at\": \"2026-07-19T10:38:29.363519\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"5561833260\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363521\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363521\", \"updated_at\": \"2026-07-19T10:38:29.363521\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2819479881\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363523\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363523\", \"updated_at\": \"2026-07-19T10:38:29.363524\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3163653031\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363525\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363526\", \"updated_at\": \"2026-07-19T10:38:29.363526\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"1098765712\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363528\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363528\", \"updated_at\": \"2026-07-19T10:38:29.363528\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"4460814937\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363530\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363530\", \"updated_at\": \"2026-07-19T10:38:29.363530\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3778338416\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363532\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363532\", \"updated_at\": \"2026-07-19T10:38:29.363533\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0972007980\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363534\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363535\", \"updated_at\": \"2026-07-19T10:38:29.363535\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"2147812612\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363537\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363537\", \"updated_at\": \"2026-07-19T10:38:29.363538\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9413055255\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363539\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363540\", \"updated_at\": \"2026-07-19T10:38:29.363540\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"3704336337\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363542\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363542\", \"updated_at\": \"2026-07-19T10:38:29.363543\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2222145750\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363544\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363545\", \"updated_at\": \"2026-07-19T10:38:29.363545\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"8406199409\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363546\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363547\", \"updated_at\": \"2026-07-19T10:38:29.363547\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"9315776372\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363549\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363549\", \"updated_at\": \"2026-07-19T10:38:29.363549\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"3369408170\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363551\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363551\", \"updated_at\": \"2026-07-19T10:38:29.363552\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"3930297243\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T10:38:29.363553\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T10:38:29.363554\", \"updated_at\": \"2026-07-19T10:38:29.363554\"}], \"count\": 22}" }, { "role": "tool", @@ -24944,7 +24944,7 @@ }, { "role": "assistant", - "content": "Now I have all the policy data. Let me analyze each conversation against the triage policy.\n\n**Severity Mapping Rules:**\n- Tag \"bug-report\", status \"active\", keyword \"crash\" \u2192 Priority \"Critical\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"500\" \u2192 Priority \"Critical\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"error\" \u2192 Priority \"Major\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"broken\" \u2192 Priority \"Major\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"*\" (catch-all) \u2192 Priority \"Minor\", Project \"SUP\"\n\n**Exclusions:**\n- Customer domain \"blockedcorp.com\" \u2192 Contract terminated\n- Customer domain \"spammer.net\" \u2192 Known abuse\n- Tag \"feature-request\" \u2192 Feature requests are not bugs\n\n**Override Rules:**\n- Customer domain \"enterprise.com\" \u2192 Always Process (VIP customer \u2014 process all reports regardless of tag exclusions)\n- Customer domain \"internal.acme.com\" \u2192 Never Process (Internal testing domain \u2014 do not create Jira issues)\n\nNow let me triage each conversation:\n\n| ID | Subject | Tags | Status | Domain | Decision |\n|---|---|---|---|---|---|\n| 201 | Dashboard crashes... | bug-report | active | acme.com | \u2705 Process \u2014 keyword \"crash\" \u2192 Critical |\n| 202 | SSO with Okta | question | active | acme.com | \u274c Not a bug (tag: question) |\n| 203 | 500 error on API... | bug-report | active | partner.io | \u2705 Process \u2014 keyword \"500\" \u2192 Critical |\n| 204 | Billing page won't load | bug-report | active | blockedcorp.com | \u274c Excluded \u2014 blockedcorp.com |\n| 205 | Login fails with error... | bug-report | active | techfirm.com | \u2705 Process \u2014 keyword \"error\" \u2192 Major |\n| 206 | Login failure on mobile | bug (not \"bug-report\") | active | techfirm.com | \u274c Not \"bug-report\" tag |\n| 207 | Export feature broken | bug-report | active | startup.io | \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| 208 | Add dark mode | feature-request, bug-report | active | acme.com | \u274c Excluded \u2014 feature-request tag |\n| 209 | Search returns wrong | bug-report | closed | startup.io | \u274c Not active |\n| 210 | API rate limiting | bug-report | active | partner.io | \u2705 Process \u2014 catch-all \u2192 Minor |\n| 211 | Notifications not sending | bug-report, urgent | active | enterprise.com | \u2705 Process \u2014 catch-all \u2192 Minor (VIP override) |\n| 212 | Password reset broken | bug-report | pending | acme.com | \u274c Not active |\n| 213 | Report generation crashes | bug-report | active | spammer.net | \u274c Excluded \u2014 spammer.net |\n| 214 | Calendar sync error | bug-report | active | enterprise.com | \u2705 Process \u2014 keyword \"error\" \u2192 Major (VIP) |\n| 215 | Widget rendering glitch | bug-report, feature-request | active | enterprise.com | \u2705 Process \u2014 VIP override overrides feature-request exclusion. Keyword \"broken\" \u2192 Major |\n| 216 | Internal tool crashes | bug-report | active | internal.acme.com | \u274c Override: Never Process |\n| 217 | Notification bell | bug-reports (not \"bug-report\") | active | techfirm.com | \u274c Tag is \"bug-reports\" not \"bug-report\" |\n| 218 | Chart rendering broken | bug-report | active | blockedcorp.co | \u274c Not blockedcorp.com \u2014 \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| 219 | Webhook delivery broken | bug-report, urgent | active | partner.io | \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| noise_002 | Import CSV failing | import | closed | cedarpoint.example.com | \u274c Not bug tag |\n| noise_003 | Webhook signatures | api | closed | tidewaterio.example.com | \u274c Not bug tag |\n| noise_010 | Beacon widget | widget | closed | cedarpoint.example.com | \u274c Not bug tag |\n\nLet me re-check conversation 215: \"Widget rendering glitch on enterprise dashboard\" \u2014 body says \"rendering incorrectly\", \"charts overlap\". The subject doesn't contain keywords but the body says \"rendering incorrectly\" \u2014 no \"crash\", \"500\", \"error\", \"broken\". Wait, actually no keyword match \u2192 catch-all \u2192 Minor. Let me re-check: subject is \"Widget rendering glitch on enterprise dashboard\" \u2014 no keyword. Body: \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\" \u2014 no \"crash\", \"500\", \"error\", \"broken\". So \u2192 catch-all \u2192 Minor.\n\nLet me also re-check conv 210: \"API rate limiting too aggressive\" \u2014 body: \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\" No keyword match \u2192 catch-all \u2192 Minor. \u2705\n\nConv 211: \"Notifications not sending\" \u2014 body: \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\" No keyword \u2192 catch-all \u2192 Minor.\n\nSo the conversations to process (create Jira issues for) are:\n1. **hs_conv_201** \u2014 \"Dashboard crashes when filtering by date\" \u2014 Critical (keyword: \"crash\")\n2. **hs_conv_203** \u2014 \"500 error on API endpoint /v2/users\" \u2014 Critical (keyword: \"500\")\n3. **hs_conv_205** \u2014 \"Login fails with error message\" \u2014 Major (keyword: \"error\")\n4. **hs_conv_207** \u2014 \"Export feature broken in new release\" \u2014 Major (keyword: \"broken\")\n5. **hs_conv_210** \u2014 \"API rate limiting too aggressive\" \u2014 Minor (catch-all)\n6. **hs_conv_211** \u2014 \"Notifications not sending\" \u2014 Minor (catch-all)\n7. **hs_conv_214** \u2014 \"Calendar sync error after timezone change\" \u2014 Major (keyword: \"error\")\n8. **hs_conv_215** \u2014 \"Widget rendering glitch on enterprise dashboard\" \u2014 Minor (catch-all, VIP override)\n9. **hs_conv_218** \u2014 \"Chart rendering broken on Firefox\" \u2014 Major (keyword: \"broken\")\n10. **hs_conv_219** \u2014 \"Webhook delivery broken since yesterday\" \u2014 Major (keyword: \"broken\")\n\nWait, let me reconsider conv 215 more carefully. It has both \"bug-report\" and \"feature-request\" tags. The exclusion says tag \"feature-request\" \u2192 exclude. But the override says enterprise.com \u2192 \"Always Process\" regardless of tag exclusions. So it should be processed. The keyword search: does the body contain \"broken\"? No. Subject: \"Widget rendering glitch on enterprise dashboard\" \u2014 no keyword. So catch-all \u2192 Minor.\n\nNow let me create all 10 Jira issues. I need to format the description in ADF format. Let me create all 10 in parallel.", + "content": "Now I have all the policy data. Let me analyze each conversation against the triage policy.\n\n**Severity Mapping Rules:**\n- Tag \"bug-report\", status \"active\", keyword \"crash\" \u2192 Priority \"Critical\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"500\" \u2192 Priority \"Critical\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"error\" \u2192 Priority \"Major\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"broken\" \u2192 Priority \"Major\", Project \"SUP\"\n- Tag \"bug-report\", status \"active\", keyword \"*\" (catch-all) \u2192 Priority \"Minor\", Project \"SUP\"\n\n**Exclusions:**\n- Customer domain \"blockedcorp.com\" \u2192 Contract terminated\n- Customer domain \"spammer.net\" \u2192 Known abuse\n- Tag \"feature-request\" \u2192 Feature requests are not bugs\n\n**Override Rules:**\n- Customer domain \"enterprise.com\" \u2192 Always Process (VIP customer \u2014 process all reports regardless of tag exclusions)\n- Customer domain \"internal.acme.com\" \u2192 Never Process (Internal testing domain \u2014 do not create Jira issues)\n\nNow let me triage each conversation:\n\n| ID | Subject | Tags | Status | Domain | Decision |\n|---|---|---|---|---|---|\n| 201 | Dashboard crashes... | bug-report | active | acme.com | \u2705 Process \u2014 keyword \"crash\" \u2192 Critical |\n| 202 | SSO with Okta | question | active | acme.com | \u274c Not a bug (tag: question) |\n| 203 | 500 error on API... | bug-report | active | partner.io | \u2705 Process \u2014 keyword \"500\" \u2192 Critical |\n| 204 | Billing page won't load | bug-report | active | blockedcorp.com | \u274c Excluded \u2014 blockedcorp.com |\n| 205 | Login fails with error... | bug-report | active | techfirm.com | \u2705 Process \u2014 keyword \"error\" \u2192 Major |\n| 206 | Login failure on mobile | bug (not \"bug-report\") | active | techfirm.com | \u274c Not \"bug-report\" tag |\n| 207 | Export feature broken | bug-report | active | startup.io | \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| 208 | Add dark mode | feature-request, bug-report | active | acme.com | \u274c Excluded \u2014 feature-request tag |\n| 209 | Search returns wrong | bug-report | closed | startup.io | \u274c Not active |\n| 210 | API rate limiting | bug-report | active | partner.io | \u2705 Process \u2014 catch-all \u2192 Minor |\n| 211 | Notifications not sending | bug-report, urgent | active | enterprise.com | \u2705 Process \u2014 catch-all \u2192 Minor (VIP override) |\n| 212 | Password reset broken | bug-report | pending | acme.com | \u274c Not active |\n| 213 | Report generation crashes | bug-report | active | spammer.net | \u274c Excluded \u2014 spammer.net |\n| 214 | Calendar sync error | bug-report | active | enterprise.com | \u2705 Process \u2014 keyword \"error\" \u2192 Major (VIP) |\n| 215 | Widget rendering glitch | bug-report, feature-request | active | enterprise.com | \u2705 Process \u2014 VIP override overrides feature-request exclusion. Keyword \"broken\" \u2192 Major |\n| 216 | Internal tool crashes | bug-report | active | internal.acme.com | \u274c Override: Never Process |\n| 217 | Notification bell | bug-reports (not \"bug-report\") | active | techfirm.com | \u274c Tag is \"bug-reports\" not \"bug-report\" |\n| 218 | Chart rendering broken | bug-report | active | blockedcorp.co | \u274c Not blockedcorp.com \u2014 \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| 219 | Webhook delivery broken | bug-report, urgent | active | partner.io | \u2705 Process \u2014 keyword \"broken\" \u2192 Major |\n| noise_002 | Import CSV failing | import | closed | pinepoint.example.com | \u274c Not bug tag |\n| noise_003 | Webhook signatures | api | closed | tidewaterio.example.com | \u274c Not bug tag |\n| noise_010 | Beacon widget | widget | closed | pinepoint.example.com | \u274c Not bug tag |\n\nLet me re-check conversation 215: \"Widget rendering glitch on enterprise dashboard\" \u2014 body says \"rendering incorrectly\", \"charts overlap\". The subject doesn't contain keywords but the body says \"rendering incorrectly\" \u2014 no \"crash\", \"500\", \"error\", \"broken\". Wait, actually no keyword match \u2192 catch-all \u2192 Minor. Let me re-check: subject is \"Widget rendering glitch on enterprise dashboard\" \u2014 no keyword. Body: \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\" \u2014 no \"crash\", \"500\", \"error\", \"broken\". So \u2192 catch-all \u2192 Minor.\n\nLet me also re-check conv 210: \"API rate limiting too aggressive\" \u2014 body: \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\" No keyword match \u2192 catch-all \u2192 Minor. \u2705\n\nConv 211: \"Notifications not sending\" \u2014 body: \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\" No keyword \u2192 catch-all \u2192 Minor.\n\nSo the conversations to process (create Jira issues for) are:\n1. **hs_conv_201** \u2014 \"Dashboard crashes when filtering by date\" \u2014 Critical (keyword: \"crash\")\n2. **hs_conv_203** \u2014 \"500 error on API endpoint /v2/users\" \u2014 Critical (keyword: \"500\")\n3. **hs_conv_205** \u2014 \"Login fails with error message\" \u2014 Major (keyword: \"error\")\n4. **hs_conv_207** \u2014 \"Export feature broken in new release\" \u2014 Major (keyword: \"broken\")\n5. **hs_conv_210** \u2014 \"API rate limiting too aggressive\" \u2014 Minor (catch-all)\n6. **hs_conv_211** \u2014 \"Notifications not sending\" \u2014 Minor (catch-all)\n7. **hs_conv_214** \u2014 \"Calendar sync error after timezone change\" \u2014 Major (keyword: \"error\")\n8. **hs_conv_215** \u2014 \"Widget rendering glitch on enterprise dashboard\" \u2014 Minor (catch-all, VIP override)\n9. **hs_conv_218** \u2014 \"Chart rendering broken on Firefox\" \u2014 Major (keyword: \"broken\")\n10. **hs_conv_219** \u2014 \"Webhook delivery broken since yesterday\" \u2014 Major (keyword: \"broken\")\n\nWait, let me reconsider conv 215 more carefully. It has both \"bug-report\" and \"feature-request\" tags. The exclusion says tag \"feature-request\" \u2192 exclude. But the override says enterprise.com \u2192 \"Always Process\" regardless of tag exclusions. So it should be processed. The keyword search: does the body contain \"broken\"? No. Subject: \"Widget rendering glitch on enterprise dashboard\" \u2014 no keyword. So catch-all \u2192 Minor.\n\nNow let me create all 10 Jira issues. I need to format the description in ADF format. Let me create all 10 in parallel.", "tool_calls": [ "{\"id\": \"chatcmpl-tool-84081835daced4ea\", \"name\": \"api_fetch\", \"arguments\": \"{\\\"body\\\": \\\"{\\\\\\\"fields\\\\\\\": {\\\\\\\"project\\\\\\\": {\\\\\\\"key\\\\\\\": \\\\\\\"SUP\\\\\\\"}, \\\\\\\"issuetype\\\\\\\": {\\\\\\\"name\\\\\\\": \\\\\\\"Bug\\\\\\\"}, \\\\\\\"summary\\\\\\\": \\\\\\\"Dashboard crashes when filtering by date\\\\\\\", \\\\\\\"priority\\\\\\\": {\\\\\\\"name\\\\\\\": \\\\\\\"Critical\\\\\\\"}, \\\\\\\"description\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"doc\\\\\\\", \\\\\\\"version\\\\\\\": 1, \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Bug report triaged from HelpScout conversation hs_conv_201.\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Customer: user1@acme.com\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Subject: Dashboard crashes when filtering by date\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Description: The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Priority: Critical (keyword: crash)\\\\\\\"}]}]}}}\\\", \\\"method\\\": \\\"POST\\\", \\\"params\\\": null, \\\"url\\\": \\\"https://api.atlassian.net/rest/api/3/issue\\\"}\"}", "{\"id\": \"chatcmpl-tool-8a21b4242c69c6b8\", \"name\": \"api_fetch\", \"arguments\": \"{\\\"body\\\": \\\"{\\\\\\\"fields\\\\\\\": {\\\\\\\"project\\\\\\\": {\\\\\\\"key\\\\\\\": \\\\\\\"SUP\\\\\\\"}, \\\\\\\"issuetype\\\\\\\": {\\\\\\\"name\\\\\\\": \\\\\\\"Bug\\\\\\\"}, \\\\\\\"summary\\\\\\\": \\\\\\\"500 error on API endpoint /v2/users\\\\\\\", \\\\\\\"priority\\\\\\\": {\\\\\\\"name\\\\\\\": \\\\\\\"Critical\\\\\\\"}, \\\\\\\"description\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"doc\\\\\\\", \\\\\\\"version\\\\\\\": 1, \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Bug report triaged from HelpScout conversation hs_conv_203.\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Customer: dev@partner.io\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Subject: 500 error on API endpoint /v2/users\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Description: We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\\\\\\\"}]}, {\\\\\\\"type\\\\\\\": \\\\\\\"paragraph\\\\\\\", \\\\\\\"content\\\\\\\": [{\\\\\\\"type\\\\\\\": \\\\\\\"text\\\\\\\", \\\\\\\"text\\\\\\\": \\\\\\\"Priority: Critical (keyword: 500)\\\\\\\"}]}]}}}\\\", \\\"method\\\": \\\"POST\\\", \\\"params\\\": null, \\\"url\\\": \\\"https://api.atlassian.net/rest/api/3/issue\\\"}\"}", @@ -26869,7 +26869,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "import" @@ -26917,7 +26917,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "widget" diff --git a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-e2b.json b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-e2b.json index bca63f8a..b30249bf 100644 --- a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-e2b.json +++ b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-gemma-4-e2b.json @@ -22341,7 +22341,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "import" @@ -22389,7 +22389,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "widget" diff --git a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-glm-5.2.json b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-glm-5.2.json index 58496ede..83fa6ded 100644 --- a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-glm-5.2.json +++ b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-glm-5.2.json @@ -24486,7 +24486,7 @@ { "role": "tool", "tool_call_id": "chatcmpl-tool-b6767d0ddf19c097", - "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7012509915\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291190\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291199\", \"updated_at\": \"2026-07-19T11:18:14.291200\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"1757909856\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291218\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291221\", \"updated_at\": \"2026-07-19T11:18:14.291221\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8779089304\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291228\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291230\", \"updated_at\": \"2026-07-19T11:18:14.291230\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3969165299\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291236\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291238\", \"updated_at\": \"2026-07-19T11:18:14.291238\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0000898010\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291244\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291246\", \"updated_at\": \"2026-07-19T11:18:14.291247\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"7638137638\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291253\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291254\", \"updated_at\": \"2026-07-19T11:18:14.291255\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6792638196\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291261\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291263\", \"updated_at\": \"2026-07-19T11:18:14.291263\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"4824689940\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291269\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291270\", \"updated_at\": \"2026-07-19T11:18:14.291271\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1240951631\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291278\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291280\", \"updated_at\": \"2026-07-19T11:18:14.291280\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5313607542\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291286\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291287\", \"updated_at\": \"2026-07-19T11:18:14.291288\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"3555665860\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291293\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291294\", \"updated_at\": \"2026-07-19T11:18:14.291295\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8792897960\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291301\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291302\", \"updated_at\": \"2026-07-19T11:18:14.291303\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8695669258\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291308\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291309\", \"updated_at\": \"2026-07-19T11:18:14.291310\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0102504299\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291315\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291317\", \"updated_at\": \"2026-07-19T11:18:14.291317\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"2949527637\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291322\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291324\", \"updated_at\": \"2026-07-19T11:18:14.291324\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8813228291\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291329\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291331\", \"updated_at\": \"2026-07-19T11:18:14.291331\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"6729313118\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291337\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291338\", \"updated_at\": \"2026-07-19T11:18:14.291338\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1972363657\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291344\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291345\", \"updated_at\": \"2026-07-19T11:18:14.291346\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"9237815982\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291351\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291352\", \"updated_at\": \"2026-07-19T11:18:14.291353\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"6113092068\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291358\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291360\", \"updated_at\": \"2026-07-19T11:18:14.291360\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"5320568515\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291366\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291367\", \"updated_at\": \"2026-07-19T11:18:14.291368\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"1991475275\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291374\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291375\", \"updated_at\": \"2026-07-19T11:18:14.291376\"}], \"count\": 22}" + "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7012509915\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291190\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291199\", \"updated_at\": \"2026-07-19T11:18:14.291200\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"1757909856\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291218\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291221\", \"updated_at\": \"2026-07-19T11:18:14.291221\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8779089304\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291228\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291230\", \"updated_at\": \"2026-07-19T11:18:14.291230\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"3969165299\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291236\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291238\", \"updated_at\": \"2026-07-19T11:18:14.291238\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0000898010\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291244\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291246\", \"updated_at\": \"2026-07-19T11:18:14.291247\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"7638137638\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291253\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291254\", \"updated_at\": \"2026-07-19T11:18:14.291255\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6792638196\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291261\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291263\", \"updated_at\": \"2026-07-19T11:18:14.291263\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"4824689940\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291269\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291270\", \"updated_at\": \"2026-07-19T11:18:14.291271\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1240951631\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291278\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291280\", \"updated_at\": \"2026-07-19T11:18:14.291280\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5313607542\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291286\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291287\", \"updated_at\": \"2026-07-19T11:18:14.291288\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"3555665860\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291293\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291294\", \"updated_at\": \"2026-07-19T11:18:14.291295\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8792897960\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291301\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291302\", \"updated_at\": \"2026-07-19T11:18:14.291303\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8695669258\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291308\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291309\", \"updated_at\": \"2026-07-19T11:18:14.291310\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0102504299\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291315\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291317\", \"updated_at\": \"2026-07-19T11:18:14.291317\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"2949527637\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291322\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291324\", \"updated_at\": \"2026-07-19T11:18:14.291324\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8813228291\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291329\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291331\", \"updated_at\": \"2026-07-19T11:18:14.291331\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"6729313118\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291337\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291338\", \"updated_at\": \"2026-07-19T11:18:14.291338\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1972363657\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291344\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291345\", \"updated_at\": \"2026-07-19T11:18:14.291346\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"9237815982\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291351\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291352\", \"updated_at\": \"2026-07-19T11:18:14.291353\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"6113092068\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291358\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291360\", \"updated_at\": \"2026-07-19T11:18:14.291360\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"5320568515\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291366\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291367\", \"updated_at\": \"2026-07-19T11:18:14.291368\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"1991475275\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T11:18:14.291374\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T11:18:14.291375\", \"updated_at\": \"2026-07-19T11:18:14.291376\"}], \"count\": 22}" }, { "role": "tool", @@ -26321,7 +26321,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "import" @@ -26369,7 +26369,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "widget" diff --git a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-nemotron-3-nano.json b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-nemotron-3-nano.json index 5a9972af..d0ca0165 100644 --- a/experiments/benchmark-hub-demo/automationbench-import/raw/ab-nemotron-3-nano.json +++ b/experiments/benchmark-hub-demo/automationbench-import/raw/ab-nemotron-3-nano.json @@ -24837,7 +24837,7 @@ { "role": "tool", "tool_call_id": "chatcmpl-tool-a1ad89cc5c304ab2", - "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1216216768\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392589\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392591\", \"updated_at\": \"2026-07-19T13:15:26.392591\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"4603054015\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392598\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392598\", \"updated_at\": \"2026-07-19T13:15:26.392599\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1029545064\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392601\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392601\", \"updated_at\": \"2026-07-19T13:15:26.392602\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1848671963\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392603\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392604\", \"updated_at\": \"2026-07-19T13:15:26.392604\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9018100378\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392606\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392606\", \"updated_at\": \"2026-07-19T13:15:26.392606\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"0929964779\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392608\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392609\", \"updated_at\": \"2026-07-19T13:15:26.392609\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8385851987\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392610\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392611\", \"updated_at\": \"2026-07-19T13:15:26.392611\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"7416401486\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392613\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392613\", \"updated_at\": \"2026-07-19T13:15:26.392613\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6803418119\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392615\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392615\", \"updated_at\": \"2026-07-19T13:15:26.392616\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9675694857\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392617\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392618\", \"updated_at\": \"2026-07-19T13:15:26.392618\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"1971252107\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392620\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392620\", \"updated_at\": \"2026-07-19T13:15:26.392620\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2991944111\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392622\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392622\", \"updated_at\": \"2026-07-19T13:15:26.392623\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0171989741\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392624\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392625\", \"updated_at\": \"2026-07-19T13:15:26.392625\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9604073490\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392627\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392628\", \"updated_at\": \"2026-07-19T13:15:26.392628\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"1491761809\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392630\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392630\", \"updated_at\": \"2026-07-19T13:15:26.392630\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5180769880\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392632\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392632\", \"updated_at\": \"2026-07-19T13:15:26.392632\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"4543910069\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392634\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392634\", \"updated_at\": \"2026-07-19T13:15:26.392635\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7531458180\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392636\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392637\", \"updated_at\": \"2026-07-19T13:15:26.392637\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"6718404161\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392638\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392639\", \"updated_at\": \"2026-07-19T13:15:26.392639\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"7384462134\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392641\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392641\", \"updated_at\": \"2026-07-19T13:15:26.392641\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"7493547877\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392643\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392644\", \"updated_at\": \"2026-07-19T13:15:26.392644\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@cedarpoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"5704015729\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392646\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392646\", \"updated_at\": \"2026-07-19T13:15:26.392646\"}], \"count\": 22}" + "content": "{\"conversations\": [{\"id\": \"hs_conv_201\", \"subject\": \"Dashboard crashes when filtering by date\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user1@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1216216768\", \"type\": \"customer\", \"body\": \"The dashboard crashes every time I try to filter by a custom date range. White screen and I have to reload.\", \"author_email\": \"user1@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392589\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392591\", \"updated_at\": \"2026-07-19T13:15:26.392591\"}, {\"id\": \"hs_conv_202\", \"subject\": \"How to set up SSO with Okta\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user2@acme.com\", \"assigned_to\": null, \"tags\": [\"question\"], \"threads\": [{\"id\": \"4603054015\", \"type\": \"customer\", \"body\": \"Can you walk me through the SSO setup process for our Okta integration?\", \"author_email\": \"user2@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392598\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392598\", \"updated_at\": \"2026-07-19T13:15:26.392599\"}, {\"id\": \"hs_conv_203\", \"subject\": \"500 error on API endpoint /v2/users\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1029545064\", \"type\": \"customer\", \"body\": \"We're getting consistent 500 errors when calling GET /v2/users with pagination. Started yesterday around 3pm EST.\", \"author_email\": \"dev@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392601\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392601\", \"updated_at\": \"2026-07-19T13:15:26.392602\"}, {\"id\": \"hs_conv_204\", \"subject\": \"Billing page won't load\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"1848671963\", \"type\": \"customer\", \"body\": \"Our billing page is completely broken. We can't view invoices or update payment info.\", \"author_email\": \"admin@blockedcorp.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392603\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392604\", \"updated_at\": \"2026-07-19T13:15:26.392604\"}, {\"id\": \"hs_conv_205\", \"subject\": \"Login fails with error message\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user3@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9018100378\", \"type\": \"customer\", \"body\": \"When I try to log in I get a generic error message and it kicks me back to the login screen. Happens every time.\", \"author_email\": \"user3@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392606\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392606\", \"updated_at\": \"2026-07-19T13:15:26.392606\"}, {\"id\": \"hs_conv_206\", \"subject\": \"Login failure on mobile app\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user4@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug\"], \"threads\": [{\"id\": \"0929964779\", \"type\": \"customer\", \"body\": \"The mobile app crashes when I try to log in. Android 14, latest app version.\", \"author_email\": \"user4@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392608\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392609\", \"updated_at\": \"2026-07-19T13:15:26.392609\"}, {\"id\": \"hs_conv_207\", \"subject\": \"Export feature broken in new release\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user5@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"8385851987\", \"type\": \"customer\", \"body\": \"The CSV export feature is broken since the v3.2 release. Getting empty files every time.\", \"author_email\": \"user5@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392610\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392611\", \"updated_at\": \"2026-07-19T13:15:26.392611\"}, {\"id\": \"hs_conv_208\", \"subject\": \"Add dark mode to settings page\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user6@acme.com\", \"assigned_to\": null, \"tags\": [\"feature-request\", \"bug-report\"], \"threads\": [{\"id\": \"7416401486\", \"type\": \"customer\", \"body\": \"Dark mode would be great. The current bright theme causes eye strain during late shifts.\", \"author_email\": \"user6@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392613\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392613\", \"updated_at\": \"2026-07-19T13:15:26.392613\"}, {\"id\": \"hs_conv_209\", \"subject\": \"Search returns wrong results\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user7@startup.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"6803418119\", \"type\": \"customer\", \"body\": \"The search function returns completely unrelated results when I search for contacts by name.\", \"author_email\": \"user7@startup.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392615\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392615\", \"updated_at\": \"2026-07-19T13:15:26.392616\"}, {\"id\": \"hs_conv_210\", \"subject\": \"API rate limiting too aggressive\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user8@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9675694857\", \"type\": \"customer\", \"body\": \"We're hitting rate limits after only 10 requests per minute. Our plan should allow 100.\", \"author_email\": \"user8@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392617\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392618\", \"updated_at\": \"2026-07-19T13:15:26.392618\"}, {\"id\": \"hs_conv_211\", \"subject\": \"Notifications not sending\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user9@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"1971252107\", \"type\": \"customer\", \"body\": \"We stopped receiving email notifications for new tickets about two days ago. Nothing in spam.\", \"author_email\": \"user9@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392620\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392620\", \"updated_at\": \"2026-07-19T13:15:26.392620\"}, {\"id\": \"hs_conv_212\", \"subject\": \"Password reset email broken\", \"status\": \"pending\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user10@acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"2991944111\", \"type\": \"customer\", \"body\": \"The password reset email never arrives. I've tried three times now.\", \"author_email\": \"user10@acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392622\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392622\", \"updated_at\": \"2026-07-19T13:15:26.392623\"}, {\"id\": \"hs_conv_213\", \"subject\": \"Report generation crashes on large datasets\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"spam@spammer.net\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"0171989741\", \"type\": \"customer\", \"body\": \"When I generate a report with more than 10k rows the whole thing crashes.\", \"author_email\": \"spam@spammer.net\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392624\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392625\", \"updated_at\": \"2026-07-19T13:15:26.392625\"}, {\"id\": \"hs_conv_214\", \"subject\": \"Calendar sync error after timezone change\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user11@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"9604073490\", \"type\": \"customer\", \"body\": \"After changing my timezone to PST, the calendar sync throws an error and all meetings show at the wrong time.\", \"author_email\": \"user11@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392627\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392628\", \"updated_at\": \"2026-07-19T13:15:26.392628\"}, {\"id\": \"hs_conv_215\", \"subject\": \"Widget rendering glitch on enterprise dashboard\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"vip@enterprise.com\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"feature-request\"], \"threads\": [{\"id\": \"1491761809\", \"type\": \"customer\", \"body\": \"The dashboard widgets are rendering incorrectly after the latest update. Charts overlap and some data is missing.\", \"author_email\": \"vip@enterprise.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392630\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392630\", \"updated_at\": \"2026-07-19T13:15:26.392630\"}, {\"id\": \"hs_conv_216\", \"subject\": \"Internal tool crashes on bulk import\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"dev@internal.acme.com\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"5180769880\", \"type\": \"customer\", \"body\": \"The bulk import tool crashes when processing more than 5000 records at once.\", \"author_email\": \"dev@internal.acme.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392632\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392632\", \"updated_at\": \"2026-07-19T13:15:26.392632\"}, {\"id\": \"hs_conv_217\", \"subject\": \"Notification bell not clearing\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"user12@techfirm.com\", \"assigned_to\": null, \"tags\": [\"bug-reports\"], \"threads\": [{\"id\": \"4543910069\", \"type\": \"customer\", \"body\": \"The notification bell icon still shows unread even after clicking on all notifications.\", \"author_email\": \"user12@techfirm.com\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392634\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392634\", \"updated_at\": \"2026-07-19T13:15:26.392635\"}, {\"id\": \"hs_conv_218\", \"subject\": \"Chart rendering broken on Firefox\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"admin@blockedcorp.co\", \"assigned_to\": null, \"tags\": [\"bug-report\"], \"threads\": [{\"id\": \"7531458180\", \"type\": \"customer\", \"body\": \"All charts are broken and show blank areas when using Firefox 120+.\", \"author_email\": \"admin@blockedcorp.co\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392636\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392637\", \"updated_at\": \"2026-07-19T13:15:26.392637\"}, {\"id\": \"hs_conv_219\", \"subject\": \"Webhook delivery broken since yesterday\", \"status\": \"active\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"ops@partner.io\", \"assigned_to\": null, \"tags\": [\"bug-report\", \"urgent\"], \"threads\": [{\"id\": \"6718404161\", \"type\": \"customer\", \"body\": \"Our webhook endpoint stopped receiving events since yesterday evening.\", \"author_email\": \"ops@partner.io\", \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392638\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392639\", \"updated_at\": \"2026-07-19T13:15:26.392639\"}, {\"id\": \"hs_conv_noise_002\", \"subject\": \"Import CSV failing silently\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"import\"], \"threads\": [{\"id\": \"7384462134\", \"type\": \"customer\", \"body\": \"Uploading a 10k-row CSV just spins and nothing imports.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392641\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392641\", \"updated_at\": \"2026-07-19T13:15:26.392641\"}, {\"id\": \"hs_conv_noise_003\", \"subject\": \"Webhook signatures not matching\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user3@tidewaterio.example.com\", \"assigned_to\": null, \"tags\": [\"api\"], \"threads\": [{\"id\": \"7493547877\", \"type\": \"customer\", \"body\": \"Our server can't verify the webhook HMAC signature.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392643\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392644\", \"updated_at\": \"2026-07-19T13:15:26.392644\"}, {\"id\": \"hs_conv_noise_010\", \"subject\": \"Beacon widget not appearing on site\", \"status\": \"closed\", \"mailbox_id\": null, \"customer_id\": null, \"customer_email\": \"noise_user2@pinepoint.example.com\", \"assigned_to\": null, \"tags\": [\"widget\"], \"threads\": [{\"id\": \"5704015729\", \"type\": \"customer\", \"body\": \"We deployed the Beacon code but nothing shows on the page.\", \"author_email\": null, \"author_name\": null, \"created_at\": \"2026-07-19T13:15:26.392646\"}], \"thread_count\": 1, \"created_at\": \"2026-07-19T13:15:26.392646\", \"updated_at\": \"2026-07-19T13:15:26.392646\"}], \"count\": 22}" }, { "role": "assistant", @@ -26411,7 +26411,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "import" @@ -26459,7 +26459,7 @@ "status": "closed", "mailbox_id": null, "customer_id": null, - "customer_email": "noise_user2@cedarpoint.example.com", + "customer_email": "noise_user2@pinepoint.example.com", "assigned_to": null, "tags": [ "widget" diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 1ddb4fcd..bfb3731f 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -1,15 +1,11 @@ #!/usr/bin/env node -import { existsSync, readFileSync } from "node:fs"; -import { extname } from "node:path"; import { spawnSync } from "node:child_process"; import { - allowedProductionUrls, - privateTerms, - productionUrlPatterns, - rawPayloadPatterns, - secretPatterns, - textExtensions, + configuredPrivateTerms, + redactPrivateTerms, + validatePublicPath, + validatePublicText, } from "./public-safety.mjs"; const forbiddenMemberParts = [ @@ -61,35 +57,8 @@ function npmPackFiles() { return payload[0].files; } -function textErrors(name, path) { - const extension = extname(path).toLowerCase(); - if (!existsSync(path) || (!textExtensions.has(extension) && extension !== ".map")) { - return []; - } - const text = readFileSync(path, "utf8"); - const errors = []; - for (const term of privateTerms) { - if (text.includes(term)) { - errors.push(`${name}: contains private release term ${JSON.stringify(term)}`); - } - } - for (const pattern of [...secretPatterns, ...rawPayloadPatterns]) { - if (pattern.test(text)) { - errors.push(`${name}: contains unsafe text matching ${pattern.source}`); - } - } - for (const pattern of productionUrlPatterns) { - for (const match of text.matchAll(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))) { - const url = match[0].replace(/\/+$/, ""); - if (!allowedProductionUrls.has(url)) { - errors.push(`${name}: contains unsafe text matching ${pattern.source}`); - } - } - } - return errors; -} - const errors = []; +const privateTerms = configuredPrivateTerms(); const packageFiles = npmPackFiles(); const packagePaths = new Set(packageFiles.map((entry) => entry.path)); for (const required of requiredPackageMembers) { @@ -109,11 +78,23 @@ for (const entry of packageFiles) { errors.push(`${packageName}: forbidden packaged path ${JSON.stringify(forbidden)}`); } } - errors.push(...textErrors(packageName, path)); + errors.push(...validatePublicPath(path, { + displayPath: packageName, + privateLabel: "private release term", + privateTerms, + })); + errors.push(...validatePublicText(path, { + displayPath: packageName, + privateLabel: "private release term", + privateTerms, + })); } for (const error of errors) { - console.log(error); + console.log(redactPrivateTerms(error, privateTerms)); +} +if (privateTerms.length === 0) { + console.log("note: private release terms are not configured; private-term checks were skipped"); } if (errors.length === 0) { console.log("ok npm package dry-run"); diff --git a/scripts/public-safety.mjs b/scripts/public-safety.mjs index 5ce50bfd..76b47e41 100644 --- a/scripts/public-safety.mjs +++ b/scripts/public-safety.mjs @@ -1,30 +1,9 @@ -import { existsSync, readFileSync } from "node:fs"; -import { extname } from "node:path"; - -export const privateTerms = [ - "/Users/luis/", - "/understudy-agent/", - "understudy-agent/", - "understudy-platform", - "understudy-knowledge", - "raw-notes", - "private/runbooks", - ".smithers", - "Fullcast", - "Cedar", - "Workgrounds", - "Forecast", - "Mercado", - "Meli", - "Super Admin", - "super-admin", - "D1 mutation", - "pool secret", - "R2 capture envelope", -]; +import { lstatSync, readFileSync, readlinkSync } from "node:fs"; + +export const privateTermsEnvironmentVariable = "UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS"; export const secretPatterns = [ - /sk-[A-Za-z0-9_-]{20,}/, + /(? allowedStart || end < allowedEnd; + if (isStrictSubstring && start >= allowedStart && end <= allowedEnd) { + return true; + } + allowedStart = value.indexOf(allowed, allowedStart + allowed.length); + } } - const text = readFileSync(path, "utf8"); - const errors = []; + return false; +} + +function privateTermRanges(value, term) { + const pattern = new RegExp(escapeRegExp(term), "giu"); + const ranges = []; + for (const match of value.matchAll(pattern)) { + const start = match.index; + const end = start + match[0].length; + if (!isAllowedPrivateTermCoincidence(value, start, end)) { + ranges.push([start, end]); + } + } + return ranges; +} + +function containsPrivateTerm(value, privateTerms) { + return privateTerms.some((term) => privateTermRanges(value, term).length > 0); +} + +export function redactPrivateTerms(value, privateTerms = configuredPrivateTerms()) { + let redacted = value; for (const term of privateTerms) { - if (text.includes(term)) { - errors.push(`${path}: contains ${privateLabel} ${JSON.stringify(term)}`); + const ranges = privateTermRanges(redacted, term); + for (let index = ranges.length - 1; index >= 0; index -= 1) { + const [start, end] = ranges[index]; + redacted = `${redacted.slice(0, start)}[private-term]${redacted.slice(end)}`; } } + return redacted; +} + +function readTrackedEntry(path) { + try { + const metadata = lstatSync(path); + if (metadata.isSymbolicLink()) { + return readlinkSync(path, "utf8"); + } + if (!metadata.isFile()) { + return null; + } + return readFileSync(path).toString("utf8"); + } catch (error) { + if (error?.code === "ENOENT") { + return null; + } + throw error; + } +} + +export function validatePublicPath( + path, + { + displayPath = path, + privateLabel = "private review term", + privateTerms = configuredPrivateTerms(), + } = {}, +) { + if (!containsPrivateTerm(path, privateTerms)) { + return []; + } + return [`${redactPrivateTerms(displayPath, privateTerms)}: path contains ${privateLabel}`]; +} + +export function validatePublicText( + path, + { + displayPath = path, + privateLabel = "private review term", + privateTerms = configuredPrivateTerms(), + } = {}, +) { + const text = readTrackedEntry(path); + if (text === null) { + return []; + } + const safePath = redactPrivateTerms(displayPath, privateTerms); + const errors = []; + if (containsPrivateTerm(text, privateTerms)) { + errors.push(`${safePath}: contains ${privateLabel}`); + } for (const pattern of secretPatterns) { if (pattern.test(text)) { - errors.push(`${path}: contains secret-shaped text matching ${pattern.source}`); + errors.push(`${safePath}: contains secret-shaped text matching ${pattern.source}`); } } for (const pattern of rawPayloadPatterns) { if (pattern.test(text)) { - errors.push(`${path}: contains raw payload marker matching ${pattern.source}`); + errors.push(`${safePath}: contains raw payload marker matching ${pattern.source}`); } } for (const pattern of productionUrlPatterns) { - for (const match of text.matchAll(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))) { + const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`; + for (const match of text.matchAll(new RegExp(pattern.source, flags))) { const url = match[0].replace(/\/+$/, ""); if (!allowedProductionUrls.has(url)) { - errors.push(`${path}: contains production/control-plane URL matching ${pattern.source}`); + errors.push(`${safePath}: contains production/control-plane URL matching ${pattern.source}`); } } } diff --git a/scripts/validate-public-skills.mjs b/scripts/validate-public-skills.mjs index 0cc91250..74664737 100644 --- a/scripts/validate-public-skills.mjs +++ b/scripts/validate-public-skills.mjs @@ -1,9 +1,14 @@ #!/usr/bin/env node -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; -import { validatePublicText } from "./public-safety.mjs"; +import { + configuredPrivateTerms, + redactPrivateTerms, + validatePublicPath, + validatePublicText, +} from "./public-safety.mjs"; const allowedTopLevel = new Set(["name", "description", "license", "allowed-tools", "metadata"]); const mvpPublicSkillNames = [ @@ -212,11 +217,11 @@ function validateSkill(path) { } function gitTrackedFiles() { - const result = spawnSync("git", ["ls-files"], { encoding: "utf8" }); + const result = spawnSync("git", ["ls-files", "-z"], { encoding: "utf8" }); if (result.status !== 0) { throw new Error(result.stderr || "git ls-files failed"); } - return result.stdout.split("\n").filter(Boolean); + return result.stdout.split("\0").filter(Boolean); } function isGitIgnored(path) { @@ -224,6 +229,18 @@ function isGitIgnored(path) { return result.status === 0; } +function trackedEntryExists(path) { + try { + lstatSync(path); + return true; + } catch (error) { + if (error?.code === "ENOENT") { + return false; + } + throw error; + } +} + function scanMarkdown(root) { if (!existsSync(root) || isGitIgnored(root)) { return []; @@ -244,6 +261,7 @@ function scanMarkdown(root) { function main() { const args = parseArgs(process.argv.slice(2)); + const privateTerms = configuredPrivateTerms(); const skillDirs = args.paths.flatMap(skillDirsForRoot); const errors = []; for (const root of args.paths) { @@ -258,16 +276,21 @@ function main() { errors.push(...scanMarkdown(docRoot)); } if (args.repo) { - const exclude = new Set(["scripts/package-smoke.mjs", "scripts/public-safety.mjs", "scripts/validate-public-skills.mjs"]); for (const path of gitTrackedFiles()) { - if (exclude.has(path) || isGitIgnored(path)) { + if (!trackedEntryExists(path) || isGitIgnored(path)) { continue; } - errors.push(...validatePublicText(path)); + errors.push(...validatePublicPath(path, { privateTerms })); + errors.push(...validatePublicText(path, { + privateTerms, + })); } } for (const error of errors) { - console.log(error); + console.log(redactPrivateTerms(error, privateTerms)); + } + if (args.repo && privateTerms.length === 0) { + console.log("note: private review terms are not configured; private-term checks were skipped"); } if (errors.length === 0) { console.log(`ok ${skillDirs.length} public skill(s)`); diff --git a/skills/local-distillation-lab/examples/forced_likelihood.py b/skills/local-distillation-lab/examples/forced_likelihood.py index 981f8773..091a277a 100644 --- a/skills/local-distillation-lab/examples/forced_likelihood.py +++ b/skills/local-distillation-lab/examples/forced_likelihood.py @@ -5,7 +5,7 @@ d_t = logπ(a_max) − logπ(τ_t), and the score_table "top-2 gap" (top1 − top2). Feeds a small standalone `G_spike` proxy for public smoke testing. -This is the missing executor behind `fullcast_pedagogical_forced_likelihood.py`'s +This is the missing executor behind a pedagogical forced-likelihood runner's `forced_token_logprobs_ref`. Pure-local, no provider calls. Validation (run as __main__): full-pass gather == incremental teacher-forced diff --git a/src/partner-report.ts b/src/partner-report.ts index 8c7c059e..79255eca 100644 --- a/src/partner-report.ts +++ b/src/partner-report.ts @@ -67,7 +67,7 @@ export type ScrubStats = { names: number; emails: number; urls: number; domains: /** * Scrub one text field: customer names → [partner], emails/URLs/domains → * bracketed placeholders. Name matching is case-insensitive and - * substring-based ON PURPOSE (e.g. "cedar" also catches "cedarcopilot"): + * substring-based ON PURPOSE (e.g. "sample" also catches "samplebot"): * over-scrubbing a shared report is cheap, leaking a customer name is not. */ export function scrubText(text: string, names: string[], stats?: ScrubStats): string { @@ -90,7 +90,7 @@ export function scrubText(text: string, names: string[], stats?: ScrubStats): st return out; } -/** Candidate customer-name tokens from a benchmark dir basename (e.g. "cedar-automation" → ["cedar"]). */ +/** Candidate customer-name tokens from a benchmark dir basename (e.g. "sample-automation" → ["sample"]). */ export function slugNameTokens(dirBasename: string): string[] { return dirBasename .toLowerCase() diff --git a/src/trace-foundry.ts b/src/trace-foundry.ts index 36364c86..6d97c0a9 100644 --- a/src/trace-foundry.ts +++ b/src/trace-foundry.ts @@ -1781,8 +1781,8 @@ export function printLeakageAudit(audit: LeakageAudit): void { * the first gold effect is legitimately the candidate's starting world; * every result at/after that point (the write's own echo, and any re-read of * the world the write changed) is expected post-state — serving it would let - * a candidate copy the answer instead of producing it (the 171-finding - * gold-leakage class found on the cedar-automation proof run). + * a candidate copy the answer instead of producing it (a gold-leakage class + * observed in multi-task proof runs). * * When the normalized captures are available (both build and regenerate paths * pass them) the boundary is computed over the full ordered call+result event @@ -1935,8 +1935,8 @@ export function writeVerifiersEnvironment( const capturesByKey = new Map(rows.map((row) => [String(row.capture_key), row])); // task_id on every fixture: fixtures are TASK-SCOPED. A later task's // pre-state read of the same underlying object legitimately contains an - // earlier task's gold writes (cross-task temporal leakage — 44 of cedar's - // findings); the world serves a rollout only its own task's fixtures (plus + // earlier task's gold writes (cross-task temporal leakage); the world serves + // a rollout only its own task's fixtures (plus // untagged old-layout ones), so that overlap is unreachable in-rollout. const toFixture = (taskId: string) => (result: Obj): Obj => ({ task_id: taskId, tool: result.tool, arguments: result.arguments ?? {}, status: result.status, content: result.content }); const observationSplit = tasks.map((task) => { const split = splitTaskObservations(task, capturesByKey); return { task_id: String(task.task_id), pre: split.pre.map(toFixture(String(task.task_id))), post: split.post.map(toFixture(String(task.task_id))) }; }); diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 1221df4d..1ee48630 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -4234,7 +4234,7 @@ class ScoreWithFeedback: const catalog = await runWithEnvAsync([ "--json", "evals", "catalog", "--project", "rehearsal", "--workload", "classify", "--from", "2026-06-01T00:00:00Z", "--to", "2026-06-08T00:00:00Z", - "--limit", "50", "--seed", "cedar-july", "--requires-tools", "--out", catalogPath, + "--limit", "50", "--seed", "fixture-july", "--requires-tools", "--out", catalogPath, ], env, repo); assert.equal(catalog.status, 0, catalog.stderr); assert.doesNotMatch(catalog.stdout, /SECRET_PROMPT|SECRET_COMPLETION/); @@ -4243,13 +4243,13 @@ class ScoreWithFeedback: const create = await runWithEnvAsync([ "--json", "evals", "cohort", "create", "--project", "rehearsal", "--workload", "classify", - "--from-catalog", catalogPath, "--name", "cedar-july", + "--from-catalog", catalogPath, "--name", "fixture-july", ], env, repo); assert.equal(create.status, 0, create.stderr); assert.equal(JSON.parse(create.stdout).cohort.id, "evc_123"); const createRequest = requests.find((entry) => entry.path.endsWith("/eval-cohorts") && entry.method === "POST"); assert.equal(createRequest.body.captures[0].request_id, "req_123"); - assert.equal(createRequest.body.selection.sampling_seed, "cedar-july"); + assert.equal(createRequest.body.selection.sampling_seed, "fixture-july"); const blocked = await runWithEnvAsync([ "evals", "cohort", "export", "evc_123", "--project", "rehearsal", "--workload", "classify", @@ -5083,7 +5083,7 @@ class ScoreWithFeedback: try { const source = join(root, "expenses.csv"); const labels = ["meals", "office_supplies", "travel"]; - const merchantGroups = ["alder", "birch", "cedar", "dogwood", "elm", "fir"]; + const merchantGroups = ["alder", "birch", "cypress", "dogwood", "elm", "fir"]; const rows = Array.from({ length: 90 }, (_, index) => { const label = labels[index % labels.length]; const merchant = merchantGroups[Math.floor(index / labels.length) % merchantGroups.length]; diff --git a/tests/exception-review.test.mjs b/tests/exception-review.test.mjs index 94ff3356..104d4239 100644 --- a/tests/exception-review.test.mjs +++ b/tests/exception-review.test.mjs @@ -343,27 +343,27 @@ describe("task feedback contract (understudy.task_feedback.v1)", () => { }); it("records the manifest benchmark_id (not the dir basename), falling back to the basename without benchmark.json", () => { - const dir = path.join(tmpDir(), "cedar-automation"); + const dir = path.join(tmpDir(), "sample-automation"); writeFoundryDir(dir, [task("t1")]); - const entry = loadProposedEntryFromDir(dir, "data-dir", "data--cedar-automation", false); + const entry = loadProposedEntryFromDir(dir, "data-dir", "data--sample-automation", false); const result = submitTaskFeedback(entry, { task_id: "t1", feedback: "wrong tool" }); assert.equal(result.ok, true); assert.equal(result.feedback.benchmark_id, "prop-bench", "the proposal-stamped benchmark_id wins over the dir basename"); // Fallback: no benchmark.json → dir basename (the legacy convention). - const bare = path.join(tmpDir(), "cedar-automation"); + const bare = path.join(tmpDir(), "sample-automation"); writeFoundryDir(bare, [task("t1")]); fs.rmSync(path.join(bare, "benchmark.json")); - const bareEntry = loadProposedEntryFromDir(bare, "data-dir", "data--cedar-automation", false); + const bareEntry = loadProposedEntryFromDir(bare, "data-dir", "data--sample-automation", false); const bareResult = submitTaskFeedback(bareEntry, { task_id: "t1", feedback: "wrong tool" }); assert.equal(bareResult.ok, true); - assert.equal(bareResult.feedback.benchmark_id, "cedar-automation"); + assert.equal(bareResult.feedback.benchmark_id, "sample-automation"); }); it("feedbackBelongsTo accepts BOTH the manifest id and the legacy dir-basename id", () => { - const legacy = makeTaskFeedback({ benchmark_id: "cedar-automation", task_id: "t1", feedback: "old line" }); + const legacy = makeTaskFeedback({ benchmark_id: "sample-automation", task_id: "t1", feedback: "old line" }); const modern = makeTaskFeedback({ benchmark_id: "trace-23a3902b7a7b6126", task_id: "t1", feedback: "new line" }); - const ids = { benchmarkId: "trace-23a3902b7a7b6126", dirBasename: "cedar-automation" }; + const ids = { benchmarkId: "trace-23a3902b7a7b6126", dirBasename: "sample-automation" }; assert.ok(feedbackBelongsTo(legacy, ids)); assert.ok(feedbackBelongsTo(modern, ids)); assert.ok(!feedbackBelongsTo(makeTaskFeedback({ benchmark_id: "other", task_id: "t1", feedback: "x" }), ids)); diff --git a/tests/partner-report.test.mjs b/tests/partner-report.test.mjs index 551ea446..3295f53d 100644 --- a/tests/partner-report.test.mjs +++ b/tests/partner-report.test.mjs @@ -134,7 +134,7 @@ describe("scrubbing", () => { }); it("derives customer-name tokens from the dir slug, skipping generic words", () => { - assert.deepEqual(slugNameTokens("cedar-automation"), ["cedar"]); + assert.deepEqual(slugNameTokens("sample-automation"), ["sample"]); assert.deepEqual(slugNameTokens("trace-benchmark-42"), []); }); }); diff --git a/tests/prime-benchmark-import.test.mjs b/tests/prime-benchmark-import.test.mjs index c1531b8a..e1d22123 100644 --- a/tests/prime-benchmark-import.test.mjs +++ b/tests/prime-benchmark-import.test.mjs @@ -284,11 +284,11 @@ test("a nonempty all-error traces.jsonl is rejected and remains resumable", () = source_dir: "source", rejected_dir: "rejected-runs", identity: { - benchmark_version: "cedar-v1", + benchmark_version: "sample-v1", environment_sha256: "abc123", verifier_version: "0.2.1", model: "gpt-5.5", - run_id: "gpt-5.5-cedar-v1", + run_id: "gpt-5.5-sample-v1", }, provider_policy: { provider: "openai", diff --git a/tests/public-safety.test.mjs b/tests/public-safety.test.mjs new file mode 100644 index 00000000..0dab98af --- /dev/null +++ b/tests/public-safety.test.mjs @@ -0,0 +1,138 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; +import { + configuredPrivateTerms, + parsePrivateTerms, + validatePublicPath, + validatePublicText, +} from "../scripts/public-safety.mjs"; + +function temporaryRoot(t) { + const root = mkdtempSync(join(tmpdir(), "understudy-public-safety-")); + t.after(() => rmSync(root, { force: true, recursive: true })); + return root; +} + +function writeFixture(t, relativePath, text) { + const path = join(temporaryRoot(t), relativePath); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, text); + return path; +} + +test("private terms are parsed from private, newline-delimited input", () => { + assert.deepEqual( + parsePrivateTerms(" ExampleTenant\nexampletenant\r\nExample Partner\n\n"), + ["ExampleTenant", "Example Partner"], + ); + assert.deepEqual( + configuredPrivateTerms({ UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: "One\nTwo" }), + ["One", "Two"], + ); + assert.deepEqual(configuredPrivateTerms({}), []); +}); + +test("mixed-case private terms are detected in CSS without leaking the term", (t) => { + const privateTerms = ["ExampleTenant"]; + const path = writeFixture(t, "theme.css", '.brand::after { content: "eXaMpLeTeNaNt"; }'); + const errors = validatePublicText(path, { privateTerms }); + + assert.equal(errors.length, 1); + assert.match(errors[0], /contains private review term/); + assert.doesNotMatch(errors[0], /exampletenant/i); +}); + +test("private terms are detected and redacted in tracked paths", () => { + const privateTerms = ["ExampleTenant"]; + const errors = validatePublicPath("apps/EXAMPLETENANT/theme.css", { privateTerms }); + + assert.deepEqual(errors, ["apps/[private-term]/theme.css: path contains private review term"]); +}); + +test("short private terms fail closed inside concatenated aliases", (t) => { + const privateTerms = ["Acme"]; + const path = writeFixture(t, "notes.txt", "Use acmebot and getAcmeData."); + + assert.equal(validatePublicText(path, { privateTerms }).length, 1); +}); + +test("single-token terms catch concatenated aliases at identifier edges", () => { + const privateTerms = ["ExampleTenant"]; + + assert.equal(validatePublicPath("apps/exampletenantbot/page.tsx", { privateTerms }).length, 1); + assert.equal(validatePublicPath("apps/myexampletenant/page.tsx", { privateTerms }).length, 1); + assert.equal(validatePublicPath("apps/myexampletenantbot/page.tsx", { privateTerms }).length, 1); + assert.equal(validatePublicPath("apps/getExampleTenantData.ts", { privateTerms }).length, 1); + assert.equal(validatePublicPath("apps/myexaMpletenantbot.ts", { privateTerms }).length, 1); + assert.equal(validatePublicPath("apps/myexampleTenantbot.ts", { privateTerms }).length, 1); +}); + +test("an explicit known-safe identifier can contain a private-term coincidence", () => { + assert.deepEqual(validatePublicPath("apps/producedArtifact.ts", { privateTerms: ["Darti"] }), []); + assert.equal( + validatePublicPath("apps/producedArtifact.ts", { privateTerms: ["producedArtifact"] }).length, + 1, + ); +}); + +test("phrases and path fragments are matched literally and case-insensitively", (t) => { + const privateTerms = ["Example Partner", "/private/example/"]; + const path = writeFixture(t, "notes.txt", "Notes for EXAMPLE PARTNER."); + + assert.equal(validatePublicText(path, { privateTerms }).length, 1); + assert.equal(validatePublicPath("docs/PRIVATE/EXAMPLE/run.md", { privateTerms }).length, 1); +}); + +test("symlink text is scanned without following the target", (t) => { + const privateTerms = ["ExampleTenant"]; + const root = temporaryRoot(t); + const path = join(root, "fixture-link"); + symlinkSync("../ExampleTenant/private", path); + + assert.equal(validatePublicText(path, { privateTerms }).length, 1); +}); + +test("repository scanning handles newline-delimited tracked paths", (t) => { + const root = temporaryRoot(t); + const privateTerm = "ExampleTenant"; + const path = join(root, `line\n${privateTerm}.css`); + writeFileSync(path, "synthetic fixture"); + assert.equal(spawnSync("git", ["init", "--quiet"], { cwd: root }).status, 0); + assert.equal(spawnSync("git", ["add", "--", path], { cwd: root }).status, 0); + + const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); + const result = spawnSync( + process.execPath, + [join(repositoryRoot, "scripts", "validate-public-skills.mjs"), "--repo", "--docs"], + { + cwd: root, + encoding: "utf8", + env: { ...process.env, UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: privateTerm }, + }, + ); + + assert.equal(result.status, 1, result.stderr); + assert.match(result.stdout, /path contains private review term/); + assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /exampletenant/i); +}); + +test("built-in secret, raw-payload, and production URL checks remain active", (t) => { + const marker = ["raw", "prompt"].join("_"); + const productionUrl = ["https://app", "understudylabs.com"].join("."); + const secret = `sk-${"A".repeat(24)}`; + const path = writeFixture(t, "unsafe.fixture", `${secret}\n${marker}\n${productionUrl}\n`); + const errors = validatePublicText(path, { privateTerms: [] }); + + assert.equal(errors.length, 3); + assert.ok(errors.some((error) => error.includes("secret-shaped"))); + assert.ok(errors.some((error) => error.includes("raw payload"))); + assert.ok(errors.some((error) => error.includes("production/control-plane URL"))); + + const readableSlug = writeFixture(t, "safe.fixture", "understudy-task-model-renamed-installs"); + assert.deepEqual(validatePublicText(readableSlug, { privateTerms: [] }), []); +}); From 98ece76af42d0f16620235e01b4a51107f1bd3f3 Mon Sep 17 00:00:00 2001 From: aamir Date: Mon, 31 Aug 2026 23:44:55 -0500 Subject: [PATCH 2/3] fix: enforce private release scanning (#479) - keep collision exemptions in the private maintainer policy\n- fail closed and rerun protected scans before release publication --- .github/workflows/desktop-release.yml | 8 ++ AGENTS.md | 6 +- docs/release-checklist.md | 9 +- scripts/package-smoke.mjs | 90 ++++++++------ scripts/public-safety.mjs | 118 +++++++++++------- scripts/validate-public-skills.mjs | 34 ++++-- tests/desktop-release-workflow.test.mjs | 13 +- tests/public-safety.test.mjs | 154 ++++++++++++++++++++++-- 8 files changed, 321 insertions(+), 111 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 90f71787..39b4d021 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -90,6 +90,14 @@ jobs: npm ci bun install --cwd apps/homescreen --frozen-lockfile + - name: Scan the public release contents + if: inputs.mode == 'release' + env: + UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: ${{ secrets.UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS }} + run: | + npm run skills:validate -- --release + npm run package:smoke -- --release + - name: Validate release credentials env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} diff --git a/AGENTS.md b/AGENTS.md index 6ad0f905..540ed0d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,8 +106,10 @@ OSS from the first commit. request metadata, or review comments. Use clearly synthetic placeholders. - Before a public release, an authorized maintainer must provide the private, newline-delimited `UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS` value while running - `npm run skills:validate` and `npm run package:smoke`. Untrusted pull-request - jobs use synthetic matcher tests and do not receive this private value. + `npm run skills:validate -- --release` and + `npm run package:smoke -- --release`. A term may be followed on the same line + by tab-separated exact safe enclosing tokens. Untrusted pull-request jobs use + synthetic matcher tests and do not receive this private value. - Examples must use synthetic data or small public fixtures. - The desktop connects to the production remote-training control plane (`train.understudylabs.com`) as its default. This is no longer experimental: diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 8c3891a9..14869f95 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -39,7 +39,7 @@ git ls-files Before publishing a package, run: ```sh -npm run package:smoke +npm run package:smoke -- --release ``` It runs `npm pack --dry-run --json` and inspects the included files for local @@ -66,8 +66,9 @@ dispatch again. The environment owns these secrets: `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_TEAM_ID`, and -`TAURI_SIGNING_PRIVATE_KEY`. Rotate them deliberately; never copy them into -workflow inputs, logs, repository variables, or committed files. +`TAURI_SIGNING_PRIVATE_KEY`, plus the private release policy +`UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS`. Rotate them deliberately; never copy +them into workflow inputs, logs, repository variables, or committed files. The commands below remain the local break-glass path. @@ -80,6 +81,8 @@ version advances without a newer CLI package. Fetch first, then run the source g ```sh git fetch origin npm run check +npm run skills:validate -- --release +npm run package:smoke -- --release npm run desktop:release-check -- --stage source ``` diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index bfb3731f..1bf02a02 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -2,7 +2,8 @@ import { spawnSync } from "node:child_process"; import { - configuredPrivateTerms, + configuredPrivateTermPolicy, + privateTermsEnvironmentVariable, redactPrivateTerms, validatePublicPath, validatePublicText, @@ -57,46 +58,57 @@ function npmPackFiles() { return payload[0].files; } -const errors = []; -const privateTerms = configuredPrivateTerms(); -const packageFiles = npmPackFiles(); -const packagePaths = new Set(packageFiles.map((entry) => entry.path)); -for (const required of requiredPackageMembers) { - if (!packagePaths.has(required)) { - errors.push(`package/${required}: required package file missing`); +function main() { + const release = process.argv.slice(2).includes("--release"); + const privateTermPolicy = configuredPrivateTermPolicy(); + if (release && privateTermPolicy.length === 0) { + console.error(`error: --release requires ${privateTermsEnvironmentVariable} to contain at least one private term`); + process.exitCode = 1; + return; } -} -const cliEntry = packageFiles.find((entry) => entry.path === "dist/bin.js"); -if (cliEntry && (!Number.isInteger(cliEntry.mode) || (cliEntry.mode & 0o111) === 0)) { - errors.push("package/dist/bin.js: CLI entry must be executable on Unix"); -} -for (const entry of packageFiles) { - const path = entry.path; - const packageName = `package/${path}`; - for (const forbidden of forbiddenMemberParts) { - if (path.includes(forbidden) || path.endsWith(forbidden)) { - errors.push(`${packageName}: forbidden packaged path ${JSON.stringify(forbidden)}`); + + const errors = []; + const packageFiles = npmPackFiles(); + const packagePaths = new Set(packageFiles.map((entry) => entry.path)); + for (const required of requiredPackageMembers) { + if (!packagePaths.has(required)) { + errors.push(`package/${required}: required package file missing`); } } - errors.push(...validatePublicPath(path, { - displayPath: packageName, - privateLabel: "private release term", - privateTerms, - })); - errors.push(...validatePublicText(path, { - displayPath: packageName, - privateLabel: "private release term", - privateTerms, - })); -} + const cliEntry = packageFiles.find((entry) => entry.path === "dist/bin.js"); + if (cliEntry && (!Number.isInteger(cliEntry.mode) || (cliEntry.mode & 0o111) === 0)) { + errors.push("package/dist/bin.js: CLI entry must be executable on Unix"); + } + for (const entry of packageFiles) { + const path = entry.path; + const packageName = `package/${path}`; + for (const forbidden of forbiddenMemberParts) { + if (path.includes(forbidden) || path.endsWith(forbidden)) { + errors.push(`${packageName}: forbidden packaged path ${JSON.stringify(forbidden)}`); + } + } + errors.push(...validatePublicPath(path, { + displayPath: packageName, + privateLabel: "private release term", + privateTermPolicy, + })); + errors.push(...validatePublicText(path, { + displayPath: packageName, + privateLabel: "private release term", + privateTermPolicy, + })); + } -for (const error of errors) { - console.log(redactPrivateTerms(error, privateTerms)); -} -if (privateTerms.length === 0) { - console.log("note: private release terms are not configured; private-term checks were skipped"); -} -if (errors.length === 0) { - console.log("ok npm package dry-run"); + for (const error of errors) { + console.log(redactPrivateTerms(error, privateTermPolicy)); + } + if (privateTermPolicy.length === 0) { + console.log("note: private release terms are not configured; private-term checks were skipped"); + } + if (errors.length === 0) { + console.log("ok npm package dry-run"); + } + process.exitCode = errors.length > 0 ? 1 : 0; } -process.exitCode = errors.length > 0 ? 1 : 0; + +main(); diff --git a/scripts/public-safety.mjs b/scripts/public-safety.mjs index 76b47e41..681e8c3c 100644 --- a/scripts/public-safety.mjs +++ b/scripts/public-safety.mjs @@ -28,74 +28,100 @@ export const allowedProductionUrls = new Set([ "https://api.understudylabs.com", ]); -const allowedPrivateTermCoincidences = new Set([ - "Timeline", - "consumeLine", - "decamelize", - "eemeli", - "producedArtifact", - "timeline", -]); - -export function parsePrivateTerms(value = "") { - const terms = []; - const seen = new Set(); +export function parsePrivateTermPolicy(value = "") { + const policy = []; + const entriesByTerm = new Map(); + const safeTokensByTerm = new Map(); for (const line of value.split(/\r?\n/)) { - const term = line.trim(); + const [termField, ...safeTokenFields] = line.split("\t"); + const term = termField.trim(); const key = term.toLowerCase(); - if (!term || seen.has(key)) { + if (!term) { continue; } - seen.add(key); - terms.push(term); + + let entry = entriesByTerm.get(key); + if (!entry) { + entry = { term, safeEnclosingTokens: [] }; + entriesByTerm.set(key, entry); + safeTokensByTerm.set(key, new Set()); + policy.push(entry); + } + + const seenSafeTokens = safeTokensByTerm.get(key); + for (const safeTokenField of safeTokenFields) { + const safeToken = safeTokenField.trim(); + const safeTokenKey = safeToken.toLowerCase(); + if (!safeToken || seenSafeTokens.has(safeTokenKey)) { + continue; + } + seenSafeTokens.add(safeTokenKey); + entry.safeEnclosingTokens.push(safeToken); + } } - return terms; + return policy; +} + +export function parsePrivateTerms(value = "") { + return parsePrivateTermPolicy(value).map(({ term }) => term); +} + +export function configuredPrivateTermPolicy(environment = process.env) { + return parsePrivateTermPolicy(environment[privateTermsEnvironmentVariable] ?? ""); } export function configuredPrivateTerms(environment = process.env) { - return parsePrivateTerms(environment[privateTermsEnvironmentVariable] ?? ""); + return configuredPrivateTermPolicy(environment).map(({ term }) => term); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -function isAllowedPrivateTermCoincidence(value, start, end) { - for (const allowed of allowedPrivateTermCoincidences) { - let allowedStart = value.indexOf(allowed); - while (allowedStart !== -1) { - const allowedEnd = allowedStart + allowed.length; +function literalRanges(value, literal) { + const pattern = new RegExp(escapeRegExp(literal), "giu"); + return [...value.matchAll(pattern)].map((match) => [match.index, match.index + match[0].length]); +} + +function isAllowedPrivateTermCoincidence(value, start, end, safeEnclosingTokens) { + for (const safeToken of safeEnclosingTokens) { + for (const [allowedStart, allowedEnd] of literalRanges(value, safeToken)) { const isStrictSubstring = start > allowedStart || end < allowedEnd; if (isStrictSubstring && start >= allowedStart && end <= allowedEnd) { return true; } - allowedStart = value.indexOf(allowed, allowedStart + allowed.length); } } return false; } -function privateTermRanges(value, term) { +function privateTermRanges(value, { term, safeEnclosingTokens }) { const pattern = new RegExp(escapeRegExp(term), "giu"); const ranges = []; for (const match of value.matchAll(pattern)) { const start = match.index; const end = start + match[0].length; - if (!isAllowedPrivateTermCoincidence(value, start, end)) { + if (!isAllowedPrivateTermCoincidence(value, start, end, safeEnclosingTokens)) { ranges.push([start, end]); } } return ranges; } -function containsPrivateTerm(value, privateTerms) { - return privateTerms.some((term) => privateTermRanges(value, term).length > 0); +function containsPrivateTerm(value, privateTermPolicy) { + return privateTermPolicy.some((entry) => privateTermRanges(value, entry).length > 0); +} + +function normalizePrivateTermPolicy(privateTermPolicy) { + return privateTermPolicy.map((entry) => ( + typeof entry === "string" ? { term: entry, safeEnclosingTokens: [] } : entry + )); } -export function redactPrivateTerms(value, privateTerms = configuredPrivateTerms()) { +export function redactPrivateTerms(value, privateTermPolicy = configuredPrivateTermPolicy()) { let redacted = value; - for (const term of privateTerms) { - const ranges = privateTermRanges(redacted, term); + for (const entry of normalizePrivateTermPolicy(privateTermPolicy)) { + const ranges = privateTermRanges(redacted, entry); for (let index = ranges.length - 1; index >= 0; index -= 1) { const [start, end] = ranges[index]; redacted = `${redacted.slice(0, start)}[private-term]${redacted.slice(end)}`; @@ -124,33 +150,39 @@ function readTrackedEntry(path) { export function validatePublicPath( path, - { + options = {}, +) { + const { displayPath = path, privateLabel = "private review term", - privateTerms = configuredPrivateTerms(), - } = {}, -) { - if (!containsPrivateTerm(path, privateTerms)) { + } = options; + const privateTermPolicy = normalizePrivateTermPolicy( + options.privateTermPolicy ?? options.privateTerms ?? configuredPrivateTermPolicy(), + ); + if (!containsPrivateTerm(path, privateTermPolicy)) { return []; } - return [`${redactPrivateTerms(displayPath, privateTerms)}: path contains ${privateLabel}`]; + return [`${redactPrivateTerms(displayPath, privateTermPolicy)}: path contains ${privateLabel}`]; } export function validatePublicText( path, - { + options = {}, +) { + const { displayPath = path, privateLabel = "private review term", - privateTerms = configuredPrivateTerms(), - } = {}, -) { + } = options; + const privateTermPolicy = normalizePrivateTermPolicy( + options.privateTermPolicy ?? options.privateTerms ?? configuredPrivateTermPolicy(), + ); const text = readTrackedEntry(path); if (text === null) { return []; } - const safePath = redactPrivateTerms(displayPath, privateTerms); + const safePath = redactPrivateTerms(displayPath, privateTermPolicy); const errors = []; - if (containsPrivateTerm(text, privateTerms)) { + if (containsPrivateTerm(text, privateTermPolicy)) { errors.push(`${safePath}: contains ${privateLabel}`); } for (const pattern of secretPatterns) { diff --git a/scripts/validate-public-skills.mjs b/scripts/validate-public-skills.mjs index 74664737..c9d5df67 100644 --- a/scripts/validate-public-skills.mjs +++ b/scripts/validate-public-skills.mjs @@ -4,7 +4,8 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { - configuredPrivateTerms, + configuredPrivateTermPolicy, + privateTermsEnvironmentVariable, redactPrivateTerms, validatePublicPath, validatePublicText, @@ -29,11 +30,13 @@ const mvpRouterTargets = [ const namePattern = /^[a-z0-9-]+$/; function parseArgs(argv) { - const args = { paths: [], docs: ["docs"], repo: false }; + const args = { paths: [], docs: ["docs"], release: false, repo: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--repo") { args.repo = true; + } else if (arg === "--release") { + args.release = true; } else if (arg === "--docs") { args.docs = []; while (argv[index + 1] && !argv[index + 1].startsWith("--")) { @@ -152,7 +155,7 @@ function hasUnsafeGepaHoldoutAccess(text) { return false; } -function validateSkill(path) { +function validateSkill(path, privateTermPolicy) { const skillMd = join(path, "SKILL.md"); if (!existsSync(skillMd)) { return [`${path}: missing SKILL.md`]; @@ -212,7 +215,7 @@ function validateSkill(path) { if (hasUnsafeGepaHoldoutAccess(text)) { errors.push(`${skillMd}: GEPA must not touch holdout data`); } - errors.push(...validatePublicText(skillMd)); + errors.push(...validatePublicText(skillMd, { privateTermPolicy })); return errors; } @@ -241,7 +244,7 @@ function trackedEntryExists(path) { } } -function scanMarkdown(root) { +function scanMarkdown(root, privateTermPolicy) { if (!existsSync(root) || isGitIgnored(root)) { return []; } @@ -253,7 +256,7 @@ function scanMarkdown(root) { } const path = join(entry.parentPath ?? root, entry.name); if (!isGitIgnored(path)) { - errors.push(...validatePublicText(path)); + errors.push(...validatePublicText(path, { privateTermPolicy })); } } return errors; @@ -261,7 +264,12 @@ function scanMarkdown(root) { function main() { const args = parseArgs(process.argv.slice(2)); - const privateTerms = configuredPrivateTerms(); + const privateTermPolicy = configuredPrivateTermPolicy(); + if (args.release && privateTermPolicy.length === 0) { + console.error(`error: --release requires ${privateTermsEnvironmentVariable} to contain at least one private term`); + process.exitCode = 1; + return; + } const skillDirs = args.paths.flatMap(skillDirsForRoot); const errors = []; for (const root of args.paths) { @@ -270,26 +278,26 @@ function main() { } } for (const skillDir of skillDirs) { - errors.push(...validateSkill(skillDir)); + errors.push(...validateSkill(skillDir, privateTermPolicy)); } for (const docRoot of args.docs) { - errors.push(...scanMarkdown(docRoot)); + errors.push(...scanMarkdown(docRoot, privateTermPolicy)); } if (args.repo) { for (const path of gitTrackedFiles()) { if (!trackedEntryExists(path) || isGitIgnored(path)) { continue; } - errors.push(...validatePublicPath(path, { privateTerms })); + errors.push(...validatePublicPath(path, { privateTermPolicy })); errors.push(...validatePublicText(path, { - privateTerms, + privateTermPolicy, })); } } for (const error of errors) { - console.log(redactPrivateTerms(error, privateTerms)); + console.log(redactPrivateTerms(error, privateTermPolicy)); } - if (args.repo && privateTerms.length === 0) { + if (args.repo && privateTermPolicy.length === 0) { console.log("note: private review terms are not configured; private-term checks were skipped"); } if (errors.length === 0) { diff --git a/tests/desktop-release-workflow.test.mjs b/tests/desktop-release-workflow.test.mjs index 30d825cf..79672e1b 100644 --- a/tests/desktop-release-workflow.test.mjs +++ b/tests/desktop-release-workflow.test.mjs @@ -42,6 +42,7 @@ test("Desktop release automation keeps every trust gate before publication", () "APPLE_SIGNING_IDENTITY", "APPLE_TEAM_ID", "TAURI_SIGNING_PRIVATE_KEY", + "UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS", ]) { assert.match(workflow, new RegExp(`secrets\\.${secret}`)); } @@ -56,7 +57,17 @@ test("Desktop release automation keeps every trust gate before publication", () assert.doesNotMatch(workflow, /cargo test/); assert.match(workflow, /notarytool history/); assert.match(workflow, /jq -e '\.history \| type == "array"'/); - assert.equal([...workflow.matchAll(/if: inputs\.mode == 'release'/g)].length, 10); + assert.equal([...workflow.matchAll(/if: inputs\.mode == 'release'/g)].length, 11); + assert.match(workflow, /npm run skills:validate -- --release/); + assert.match(workflow, /npm run package:smoke -- --release/); + assert.ok( + workflow.indexOf("npm ci") < + workflow.indexOf("npm run skills:validate -- --release"), + ); + assert.ok( + workflow.indexOf("npm run package:smoke -- --release") < + workflow.indexOf("gh release create"), + ); assert.match(workflow, /notarytool submit "\$app_zip"/); assert.match(workflow, /stapler staple "\$app"/); assert.match( diff --git a/tests/public-safety.test.mjs b/tests/public-safety.test.mjs index 0dab98af..0afcc154 100644 --- a/tests/public-safety.test.mjs +++ b/tests/public-safety.test.mjs @@ -1,12 +1,14 @@ import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { delimiter as pathDelimiter, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; import test from "node:test"; import { + configuredPrivateTermPolicy, configuredPrivateTerms, + parsePrivateTermPolicy, parsePrivateTerms, validatePublicPath, validatePublicText, @@ -25,15 +27,43 @@ function writeFixture(t, relativePath, text) { return path; } -test("private terms are parsed from private, newline-delimited input", () => { +function environmentWithoutPrivateTermPolicy() { + const environment = { ...process.env }; + delete environment.UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS; + return environment; +} + +test("private term policy is parsed from private, newline-delimited input", () => { + const configuredValue = [ + " ExamplePrivateTerm\tSyntheticExamplePrivateTermContainer\tSYNTHETICEXAMPLEPRIVATETERMCONTAINER", + "exampleprivateterm\tAnotherExamplePrivateTermContainer", + "PlainPrivateTerm", + "", + ].join("\r\n"); + + assert.deepEqual( + parsePrivateTermPolicy(configuredValue), + [ + { + term: "ExamplePrivateTerm", + safeEnclosingTokens: [ + "SyntheticExamplePrivateTermContainer", + "AnotherExamplePrivateTermContainer", + ], + }, + { term: "PlainPrivateTerm", safeEnclosingTokens: [] }, + ], + ); assert.deepEqual( - parsePrivateTerms(" ExampleTenant\nexampletenant\r\nExample Partner\n\n"), - ["ExampleTenant", "Example Partner"], + configuredPrivateTermPolicy({ UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: configuredValue }), + parsePrivateTermPolicy(configuredValue), ); + assert.deepEqual(parsePrivateTerms(configuredValue), ["ExamplePrivateTerm", "PlainPrivateTerm"]); assert.deepEqual( configuredPrivateTerms({ UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: "One\nTwo" }), ["One", "Two"], ); + assert.deepEqual(configuredPrivateTermPolicy({}), []); assert.deepEqual(configuredPrivateTerms({}), []); }); @@ -72,12 +102,31 @@ test("single-token terms catch concatenated aliases at identifier edges", () => assert.equal(validatePublicPath("apps/myexampleTenantbot.ts", { privateTerms }).length, 1); }); -test("an explicit known-safe identifier can contain a private-term coincidence", () => { - assert.deepEqual(validatePublicPath("apps/producedArtifact.ts", { privateTerms: ["Darti"] }), []); +test("configured safe enclosing tokens are literal, case-insensitive, strict substrings", () => { + const privateTermPolicy = parsePrivateTermPolicy( + "ExamplePrivateTerm\tSyntheticExamplePrivateTermContainer\tExamplePrivateTerm", + ); + + assert.deepEqual( + validatePublicPath("apps/SYNTHETICEXAMPLEPRIVATETERMCONTAINER.ts", { privateTermPolicy }), + [], + ); assert.equal( - validatePublicPath("apps/producedArtifact.ts", { privateTerms: ["producedArtifact"] }).length, + validatePublicPath("apps/ExamplePrivateTerm.ts", { privateTermPolicy }).length, 1, ); + assert.equal( + validatePublicPath("apps/UnlistedExamplePrivateTermContainer.ts", { + privateTermPolicy, + }).length, + 1, + ); + assert.deepEqual( + validatePublicPath("apps/PrefixSyntheticExamplePrivateTermContainerSuffix.ts", { + privateTermPolicy, + }), + [], + ); }); test("phrases and path fragments are matched literally and case-insensitively", (t) => { @@ -101,9 +150,12 @@ test("repository scanning handles newline-delimited tracked paths", (t) => { const root = temporaryRoot(t); const privateTerm = "ExampleTenant"; const path = join(root, `line\n${privateTerm}.css`); + const safeToken = `Synthetic${privateTerm}Container`; + const safePath = join(root, `${safeToken}.css`); writeFileSync(path, "synthetic fixture"); + writeFileSync(safePath, "synthetic fixture"); assert.equal(spawnSync("git", ["init", "--quiet"], { cwd: root }).status, 0); - assert.equal(spawnSync("git", ["add", "--", path], { cwd: root }).status, 0); + assert.equal(spawnSync("git", ["add", "--", path, safePath], { cwd: root }).status, 0); const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); const result = spawnSync( @@ -112,15 +164,97 @@ test("repository scanning handles newline-delimited tracked paths", (t) => { { cwd: root, encoding: "utf8", - env: { ...process.env, UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: privateTerm }, + env: { + ...process.env, + UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS: `${privateTerm}\t${safeToken}`, + }, }, ); assert.equal(result.status, 1, result.stderr); assert.match(result.stdout, /path contains private review term/); + assert.equal(result.stdout.match(/path contains private review term/g)?.length, 1); assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /exampletenant/i); }); +test("release-mode scanners fail closed without a private term policy", (t) => { + const root = temporaryRoot(t); + const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); + const environment = environmentWithoutPrivateTermPolicy(); + + for (const script of ["validate-public-skills.mjs", "package-smoke.mjs"]) { + const result = spawnSync( + process.execPath, + [join(repositoryRoot, "scripts", script), "--release"], + { cwd: root, encoding: "utf8", env: environment }, + ); + + assert.equal(result.status, 1, `${script}: ${result.stderr}`); + assert.match( + result.stderr, + /--release requires UNDERSTUDY_PUBLIC_SAFETY_PRIVATE_TERMS to contain at least one private term/, + ); + assert.equal(result.stdout, ""); + assert.doesNotMatch(result.stderr, /npm pack|not a git repository|ENOENT/i); + } +}); + +test("ordinary scanners keep the informational skip without a private term policy", (t) => { + const root = temporaryRoot(t); + const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); + const environment = environmentWithoutPrivateTermPolicy(); + assert.equal(spawnSync("git", ["init", "--quiet"], { cwd: root }).status, 0); + + const validator = spawnSync( + process.execPath, + [join(repositoryRoot, "scripts", "validate-public-skills.mjs"), "--repo", "--docs"], + { cwd: root, encoding: "utf8", env: environment }, + ); + assert.equal(validator.status, 0, validator.stderr); + assert.match(validator.stdout, /private-term checks were skipped/); + assert.match(validator.stdout, /ok 0 public skill\(s\)/); + + const fakeBin = join(root, "bin"); + mkdirSync(fakeBin); + const packageFiles = [ + { path: "dist/bin.js", mode: 0o755 }, + { path: ".agents/plugins/marketplace.json" }, + { path: ".codex-plugin/plugin.json" }, + { path: ".cursor-plugin/plugin.json" }, + { path: ".opencode/adapter.json" }, + { path: ".opencode/commands/understudy-onboard.md" }, + { path: ".hermes/adapter.json" }, + { path: "skills/install-agent-adapter/SKILL.md" }, + { path: "skills/install-agent-adapter/reference.md" }, + { path: "skills/local-distillation-lab/SKILL.md" }, + { path: "skills/local-distillation-lab/references/pedagogical-arm.md" }, + { path: "skills/recursive-language-model/SKILL.md" }, + { path: "skills/recursive-language-model/references/pedagogical-training.md" }, + ]; + const fakeNpm = join(fakeBin, "npm"); + writeFileSync( + fakeNpm, + `#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(JSON.stringify([{ files: packageFiles }]))});\n`, + ); + chmodSync(fakeNpm, 0o755); + + const packageSmoke = spawnSync( + process.execPath, + [join(repositoryRoot, "scripts", "package-smoke.mjs")], + { + cwd: root, + encoding: "utf8", + env: { + ...environment, + PATH: `${fakeBin}${pathDelimiter}${environment.PATH ?? ""}`, + }, + }, + ); + assert.equal(packageSmoke.status, 0, packageSmoke.stderr); + assert.match(packageSmoke.stdout, /private-term checks were skipped/); + assert.match(packageSmoke.stdout, /ok npm package dry-run/); +}); + test("built-in secret, raw-payload, and production URL checks remain active", (t) => { const marker = ["raw", "prompt"].join("_"); const productionUrl = ["https://app", "understudylabs.com"].join("."); From a97683d335e34e821559b2b0cf591f217c99db79 Mon Sep 17 00:00:00 2001 From: aamir Date: Mon, 31 Aug 2026 23:54:52 -0500 Subject: [PATCH 3/3] fix(ci): remove useless A2A URL format --- apps/homescreen/src-tauri/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/homescreen/src-tauri/src/server.rs b/apps/homescreen/src-tauri/src/server.rs index 71370c39..2b14d18c 100644 --- a/apps/homescreen/src-tauri/src/server.rs +++ b/apps/homescreen/src-tauri/src/server.rs @@ -2584,7 +2584,7 @@ async fn a2a_card( "name": "understudy-desktop", "description": "Local Understudy control plane: models, routes, traces, and runtime.", "version": env!("CARGO_PKG_VERSION"), - "url": format!("/a2a"), + "url": "/a2a", "capabilities": { "streaming": false, "tools": true }, "defaultInputModes": ["text"], "defaultOutputModes": ["text"],