MyGo 云音乐
Go와 MyGo로 만든 서드파티 네이버 클라우드 뮤직 데스크톱 클라이언트
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A fixed, canonical fictional tribunal — Case T-001: The Realm v. Jon Snow — argued and ruled on by seven independent AI agents: four representatives (two defense, two prosecution) and three judges, each modeled on a distinct real judicial reasoning method (Aharon Barak, Menachem Elon, Meir Shamgar).
The Tribunal decides one question — justified / not justified — and gives reasons. It does not impose a sentence, and the three judges' rulings are never combined into a single verdict; they are displayed independently, side by side.README 전문 보기 ↗Three judges, three separate rulings: the Elon judge found
the killing justified, and the Barak and Shamgar judges did not.
Grey Worm's argument was answered by
claude-haiku-4.5 rather than the default model: his first two attempts were
caught and discarded, and the escalation chain moved
him up a tier.
A fixed, canonical fictional tribunal — Case T-001: The Realm v. Jon Snow — argued and ruled on by seven independent AI agents: four representatives (two defence, two prosecution) and three judges, each modelled on a distinct real judicial reasoning method (Aharon Barak, Menachem Elon, Meir Shamgar).
The Tribunal decides one question — justified / not justified — and gives reasons. It does not impose a sentence, and the three judges' rulings are displayed independently, each on its own card.
Three-tier: browser (static HTML/CSS/vanilla JS, no build step or framework) → backend (Netlify Functions, TypeScript, bundled by esbuild) → database (Supabase/Postgres).
The backend holds the OpenRouter API key and orchestrates every model call. The database stores:
flowchart TB
B["Browser<br/>public/: HTML, CSS, vanilla JS"]
RT["netlify.toml<br/>the /api/* routes"]
subgraph FN["Netlify Functions — every secret stays here"]
READ["case.ts, trial.ts<br/>read the case and a trial"]
WRITE["trials.ts, abort.ts<br/>create, list and abort trials"]
AG["representative-background.ts<br/>judge-background.ts<br/>one agent per call"]
DBM["db.ts, chargeSheet.ts<br/>every read and write"]
GATE["siteGate.ts<br/>the site-gate header"]
PR["prompts.ts<br/>an agent's messages"]
OR["openrouter.ts<br/>the escalation chain"]
SB["supabase.ts<br/>the one client"]
end
DB[("Supabase / Postgres<br/>case_definitions<br/>trials<br/>representative_arguments<br/>judge_rulings<br/>api_call_logs<br/>agent_progress")]
OAPI(["OpenRouter"])
B -->|"fetch /api/*"| RT
RT --> READ
RT --> WRITE
RT -->|"per-IP limit"| AG
READ --> DBM
WRITE --> DBM
WRITE --> GATE
AG --> GATE
AG --> DBM
AG --> PR
AG --> OR
DBM --> SB
SB --> DB
OR --> OAPI
What the picture claims, each held to the code by
tests/docs.test.js:
app.js makes
goes to a route under /api/ on the site itself, served
through the redirects in netlify.toml, and neither
index.html nor styles.css loads
anything from another host. There is no line from the browser to OpenRouter or
to the database because there is no such call.openrouter.ts, and the Supabase URL
and key only in supabase.ts. Both come
from Netlify's environment variables and never reach the
browser. The site-gate token in app.js is the one value the
page sends for the backend to check, and it is no secret (see Anti-abuse and
cost controls).-background
names.Each judge reasons by its own method, visible in its opening lines: the Barak judge announces "a structured, rights-centered analysis" and defines "justified" and "necessary" before applying them; the Elon judge turns to a principle of Jewish legal tradition, the rodef or pursuer; the Shamgar judge sets out the governing standard and the institutional framework, then the chronology. Three independent rulings, side by side.
Representative and judge calls run as Netlify Background Functions rather than standard synchronous invocations — a real generation can take far longer than a synchronous function is allowed to run.
The browser triggers a call, gets an immediate
202, and
polls GET /api/trials/:id until the result lands.
Captured while the page polled: Jon Snow's argument had landed, Tyrion and Daenerys were still on their first attempts, and Grey Worm on his second - his first had repeated an 18-word sentence three times and was discarded. Each card's model line is the attempt that is starting, recorded as it begins. The trial went on to finish 7 of 7.
A 4-tier model escalation chain guards
against unusable output:
default model (2 attempts) → claude-haiku-4.5 (2 attempts) → a third model (2 attempts) → a last-resort model (1 attempt),
escalating once a tier has used up its attempts. The escalation signals are:
finish_reason === 'length' (hit the token cap);Three failures end the call at once instead, since no retry can fix them:
Transient failures are split by how long they took, because the right response differs:
Per-attempt timeouts scale with both prompt size and the token cap, so a judge's much larger prompt gets a larger ceiling.
Every tier in the chain is a paid model; none of them runs on a free tier. Tier 2 is significantly pricier than the default tier, which is why the chain is built to exhaust the cheapest option before reaching for a better one — not to avoid spending, but to spend proportionately.
Every discarded attempt — including a timeout — gets logged as its own real row (model, tokens, cost, duration, outcome, and the reply if there was one) the moment it's decided, not batched at the end, so the call log shows the whole path rather than only the final result.
A live-in-progress card shows the model and attempt actually running right now, updating as the chain escalates.
Abort stops server-side work, not just the UI. A Background Function can't be cancelled by the browser that started it, so the trial's abort is recorded in the database and each in-flight call checks for it before every attempt and as each attempt ends, and stops itself, rather than continuing to escalate through pricier models for a result nobody is waiting for.
A request already sent is left to finish: this app does not stream, and
OpenRouter's docs say a
non-streamed request is billed in full even if it is cancelled (as read on
2026-10-05). So the attempt that was running is logged as what it was, marked
aborted — a failed one is not retried, and a finished reply is
not saved.
Abort was clicked about four seconds in, while all four representatives' first attempts were generating. The four grey rows record the request to stop. Each attempt then ran to its end and is logged with its real tokens and cost - two repeated a sentence, two hit the token cap - and none was retried or saved. No judge ever started.
Three layers guard against abuse and runaway cost, since the deployed site runs on a paid model with no login:
Each route below is a rewrite in netlify.toml to one function
in netlify/functions/, named at the start of its
description. Everything this app's own code returns is JSON.
case.ts: The fixed case
record, the starting model for each role, and the shared
completion-token cap. Read once when the page loads.trials.ts: The 50 most recent trials, newest
first, for the run-history sidebar — each with its
status, how many of its 7 results were saved, whether it was aborted, and
whether any of its call-log rows is stored as failed (a
failed attempt, recovered or not, the row a call ends on when the server has
no OpenRouter key, or a row an abort wrote).trials.ts: Creates a trial and returns it
with the case record (201). Needs the site-gate
header. Makes no model call.trial.ts: Everything recorded for one trial:
the trial itself, the case record, its saved
and , (oldest first, without the stored reply text),
and . Polled
throughout a run, and read once to open a past trial. for an unknown id.They behave differently from the rest. They are the only ones that spend
OpenRouter quota, and the only Background
Functions: Netlify answers the POST with
202 as soon
as the call is accepted and runs the handler afterwards, so nothing the handler
returns ever reaches the browser, which learns the outcome by polling
GET /api/trials/:id.
The handler checks, in order, that the request came through its rate-limited
route on the site's main address, rather than at the function's own address or
another of the site's addresses, that it is a POST naming a trial and a role,
that the role is one it knows (jon_snow, tyrion_lannister,
daenerys_targaryen or grey_worm; barak, elon or shamgar), the
site-gate header, the site-wide call cap, that
the trial exists, and that the trial has not been aborted and the role has no
final outcome yet.
Because of the 202, a rejection at any of those steps never reaches the
browser: the site-gate and call-cap rejections are written to Netlify's function
logs, the others are not logged at all, and in the browser that role never
resolves — its card says so once polling
gives up, after about 12 minutes.
The last check turns away only requests the page never sends, since it triggers each role once per trial: a second call for a role, which would save over a result already kept, or a call for a trial the user aborted. A role with only discarded attempts logged has no outcome yet, so it is not turned away.
Netlify's per-IP rate
limit
(60 requests per 3
minutes
from one IP, counted across the two routes together, set on their redirects in
netlify.toml) is enforced by the platform ahead of the
handler, so a rejection there reaches the browser directly, as a 429. It is a
backstop far above normal use, since a full trial sends 7 requests.
Netlify also serves each function at its own
address, under
/.netlify/functions/, where no redirect applies, and the whole site at
main--tribunal-t001.netlify.app and at an address for each deploy, where it
counts the limit
separately.
The handler's first check refuses a request that arrives any of those ways,
comparing its address with the main one,
which Netlify gives functions as
URL.
So each request an agent function acts on has come through its route on the main
address and passed the one count.
Both were verified on the live site on 2026-10-06: the limit and the
functions' own addresses after one deploy, and
the site's other
addresses
after the next. Bursts from one IP were cut off with 429s after 62 requests
to one route, and after 63 split between the two (40 to one, 23 to the other).
Gated requests for a real trial sent straight to both agent functions' own
addresses, or through their routes to main--tribunal-t001.netlify.app, left
nothing behind, while the same request through its route on the main
address started its attempt within 5
seconds.
A judge's reply must contain a VERDICT: justified or VERDICT: not justified
line with its reasoning after it, and one without them is logged as a failure
rather than saved.
Six tables in Supabase/Postgres. supabase/schema.sql is
the authority on every column, type and constraint; what follows is a map of
what each table holds and the rules worth knowing, not a copy of that file.
| Table | One row per… |
|---|---|
case_definitions | case |
trials | run |
representative_arguments | representative whose argument was kept |
judge_rulings | judge whose ruling was kept |
api_call_logs | model-call attempt, kept or discarded |
agent_progress | trial and role |
case_definitions
and trials itself belongs to a single trial through trial_id,
and deleting a trial deletes its rows in all of them.Columns: case_code (the key), title, accused, deceased,
act_alleged, background, agreed_facts, question, scope_note
agreed_facts is a JSON
array of strings.schema.sql seeds the only row, T-001, and the app
reads the case from here at runtime
rather than from any copy in the code.Columns: id (a UUID, and the :id in every route above), case_code,
status, created_at, updated_at
status is created until every judge has a final outcome logged — a
ruling, a failure the chain gave up
on, or an abort — and then completed, set by the judge
endpoint that finds it so.created, however many
judges had already finished. It is completed only when the last judge had
finished before the abort was recorded.Columns: id, trial_id, role (one of the four
representatives), seat (defense or prosecution),
argument_text, model_used, created_at
judge_rulings
are what the sidebar's "N of 7" counts.api_call_logs.Columns: id, trial_id, role (one of the three
judges), verdict (justified or not justified),
reasoning_text, model_used, created_at
Columns: id, trial_id, agent_role, call_type (representative or
judge), model_used, prompt_tokens, completion_tokens, total_tokens,
cost (in US dollars), status (success or failed), error_message,
timestamp, duration_ms, response_text
agent_role, model_used, prompt_tokens, completion_tokens,
total_tokens, cost, status and timestamp.response_text is the model's reply word for word, stored for every attempt
that got one — discarded attempts included, so a truncated or degenerate reply
can be read in full afterwards — and empty for an attempt with no reply.
error_message starts with a marker saying what
happened to it, such as [transient-retried].duration_ms is empty on a row that timed no attempt:
aborted row when it stopped for an
abort, or a failed one when the server has no
OpenRouter key;failed row with the
model n/a for each pending role; that row is what running calls look for to
know they should stop.Columns: trial_id, role, model, tier_index, attempt_in_tier,
tier_max_attempts, updated_at
trial_id and role together are its key),
overwritten each time a new attempt starts.Every model call is shown, whether it was kept or thrown away, and every failed attempt carries a badge for the kind of failure it was; a call's final failure is spelled out in full on its agent's card.
A request an agent endpoint turns away at one of its checks makes no model call, so it has no row here: one that did not come through its route on the site's main address, is not a POST naming a trial and a role, or names an unknown role or trial; one the site gate or the call cap stops; and one for a role that already has an outcome, or for a trial that was aborted. These two tables are the full set of badges either view can produce.
One row per real model attempt, including attempts that were discarded in favour of a retry or an escalation.
A few rows have no attempt behind them:
failed row a call ends on when the server has no OpenRouter key.Rows for attempts that were retried or escalated are dimmed, since the call usually went on to succeed further down the table, and so are the abort endpoint's rows, which record the request to stop rather than an outcome. A call's last row is not dimmed, even when nothing was saved.
The same trial's ten attempts. Jon Snow's first
reply repeated a 26-word sentence and was retried on the same model; Grey Worm's
hit the 1,400-token
cap, then
closed on a near-copy of an earlier sentence, and was escalated to
claude-haiku-4.5, whose one call cost 0.41¢ of the trial's 0.56¢. The totals
count every attempt, discarded ones included.
| Badge | Colour | What triggers it |
|---|---|---|
success | green | The attempt returned usable content and was kept — for a judge, that includes a VERDICT line. This is the text shown on that agent's card. |
failed | red | The row that ended the call (the agent's card reads "call failed") — every tier or the time budget used up, a failure no retry can fix (not enough credit for the request, a rate limit that outlasts the time budget, or no OpenRouter key on the server, the one case with no attempt behind the row), or a judge reply with no VERDICT line and reasoning after it. |
| abort requested | grey | The row the abort endpoint writes, the moment Abort is clicked, for each role still pending. It records the request to stop, not a model call: the model reads n/a, with no tokens or duration, and it does not count against the site-wide call cap. Dimmed, like a discarded attempt. The running calls look for it and stop; the role's own aborted row, if its call was still running, follows. |
truncated | amber | finish_reason === 'length' on a reply with text — the model was still writing when it hit that tier's token cap. Retried or escalated; the caption says which. |
degenerated | amber | A detector fired on a reply that did not hit the token cap: , or repetition, verbatim or near-verbatim (, , any sentence 3+ times, a passage of 3+ sentences repeated word for word, the same clause twice in a row, a clause of 6+ words 3 times close together, , or a sentence in the last 10% that is 60%+ like one of the 4 before it (8+ words each)). Retried or escalated; the caption says which. |
truncated and degenerated are told apart by how an attempt ended, not by
everything that was wrong with its text:
truncated means the model was still writing when it hit the token cap;degenerated means it did not hit the cap and a
detector flagged the text (the detectors run on every
reply with text except a capped one or one an upstream error cut
short,
whatever else its finish_reason says).In practice a truncated reply is usually degenerate too — a repetition loop that ran until the cap stopped it. Measured on 2026-09-27, every capped reply from the default model whose text was stored was a loop, and every tier-1 reply kept as sound had finished well short of the 1,400 tokens the default model is given; a loop that stops on its own can come much closer.
One badge per trial, summarising the whole run.
| Badge | Colour | What triggers it |
|---|---|---|
completed | green | Finished, with all 7 of 7 results saved. |
| completed — missing N of 7 | amber | Finished, but fewer than 7 results were saved: some agent's call failed for good (every tier or the time budget used up, a failure no retry can fix, or a judge reply with no VERDICT line and reasoning after it), or a representative's call never ran (its trigger request rejected or never sent, or the call turned away by the site gate or the call cap). |
aborted | grey | Stopped by the user before the trial reached completion. |
| aborted (N of 7 completed) | grey | Stopped by the user, but the trial was marked complete too: the abort was recorded just as the last judge finished, or, in a trial run before the abort endpoint began refusing a completed trial, after the trial had already been marked complete. The count says how much survived. |
| in progress… | slate | Not finished, not aborted, and under 40 minutes old — presumably still running. |
interrupted | red | Not finished, not aborted, and over 40 minutes old, so it is treated as never going to finish — a dev-server restart mid-run, say, the page closed before the judges were started (the browser starts each phase), or a judge whose call never ran (its trigger request rejected, or the call turned away by , the call cap or, on a page opened at another of the site's addresses than , ), since a trial is finished once every judge has an outcome logged. The threshold is sized well above the genuine worst case: the page waiting on the representatives for as long as it polls, then every judge running out its full time budget. |
"Missing N" counts results that were actually saved, not whether any individual attempt failed along the way.
A failed attempt that a retry or escalation recovered from (a timeout, a rate limit, a truncated or degenerate reply) is logged, but it is not a flaw in the outcome; most complete trials have at least one, so labelling on that basis would falsely mark most runs as damaged. The call log still lists every attempt.
Every file tracked in the repository. Not tracked, and git-ignored:
node_modules/ and .netlify/, which are generated locally;.env, which holds your own keys (copied from
.env.example);.gitignore..
├── public/ static frontend, served as-is — no build step
│ ├── index.html
│ ├── app.js all client logic: running and aborting a trial, polling, rendering, run history
│ └── styles.css
├── netlify/functions/ backend — one file per Netlify Function
│ ├── case.ts GET /api/case
│ ├── trials.ts GET and POST /api/trials
│ ├── trial.ts GET /api/trials/:id
│ ├── representative-background.ts POST /api/trials/:id/representatives/:role
│ ├── judge-background.ts POST /api/trials/:id/judges/:role
│ ├── abort.ts POST /api/trials/:id/abort
│ └── lib/ code shared by the functions above
│ ├── openrouter.ts the escalation chain: tiers, timeouts, detectors
│ ├── models.ts the model for each role and tier, and the token cap
│ ├── pricing.ts per-token prices, for the cost column
│ ├── prompts.ts builds each agent's prompt; parses a judge's verdict
│ ├── representatives.ts the four representatives, in character
│ ├── judges.ts the three judges' reasoning methods
│ ├── chargeSheet.ts reads the case record from the database; formats it for prompts
│ ├── db.ts every other Supabase query, the call cap, the abort check, the refusal of calls the page never sends
│ ├── supabase.ts the Supabase client (service-role key)
│ ├── siteGate.ts the X-Site-Gate header check
│ ├── extractParams.ts reads :id and :role; checks the route and the main address
│ ├── safeHandler.ts turns an uncaught error into a JSON 500
│ ├── response.ts JSON response helper
│ └── types.ts types shared across the backend
├── supabase/schema.sql all six tables, the seeded case, RLS, grants
├── screenshots/ the images in this README, captured from the running app
│ ├── readme-1-hero-split-rulings.png
│ ├── readme-2-judges.png
│ ├── readme-3-call-log.png
│ ├── readme-4-aborted-trial.png
│ └── readme-5-in-progress.png
├── tests/ npm test (no network, no quota) and npm run check-render (GitHub's API)
│ ├── retry-logic.test.js the escalation chain, from the real TypeScript
│ ├── trial-status.test.js when a trial is completed; aborts; calls the page never sends; the call cap and the failure flag; replies kept for audit, off the page
│ ├── render-cards.test.js app.js: cards, the judges' banner, the call log, model ids, failed requests, the site-gate header, the card fades
│ ├── shared-constants.test.js values duplicated across files still agree; the page's timeouts outlast the server's; a card's text area is whole lines tall; call-log captions wrap evenly
│ ├── docs.test.js README, SPEC.md and CLAUDE.md agree with the code; README and SPEC.md link what they name
│ ├── markdown.test.js no Markdown file holds source GitHub is known to render wrongly; check-render's pieces of a large file join back into it
│ ├── check-render.js npm run check-render — renders each Markdown file through GitHub and compares the page with its source
│ └── support/ setup shared by the suites above
│ ├── compile-backend.js compiles the real backend TypeScript
│ ├── fake-supabase.js an in-memory Supabase, for running the backend
│ └── load-app.js runs the real app.js against a stub DOM
├── netlify.toml build and local-dev settings, the /api/* routes, and the agent routes' rate limit
├── package.json, package-lock.json
├── tsconfig.json type-checks netlify/functions (not app.js)
├── .env.example environment variables read by the backend and the render check
├── .gitignore
├── .vscode/settings.json turns format-on-save off for this workspace
├── SPEC.md the requirements, from the course's Case Design Dossier
├── README.md
└── CLAUDE.md the working brief and build/decision log
Prerequisites: Node.js, a Supabase project, an OpenRouter API key.
Apply supabase/schema.sql to the Supabase project first
— pasting it into the SQL Editor is enough. It enables the pgcrypto extension,
creates the six tables and their indexes, seeds the fixed case
record, enables row-level security, and issues the grants
the backend needs.
The grants at the bottom of that file matter on a project
created with "Automatically expose new tables" unchecked, as this one was:
there, without them, every backend call
fails with permission denied for table X
even though the key is correct.
npm install
cp .env.example .env # fill in OPENROUTER_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
npm run dev # netlify dev — serves the static frontend and functions locally
npm run typecheck # tsc --noEmit
npm run check-render # node tests/check-render.js — needs the network (see below)
npm test # six regression suites (see below)
npm test needs no network and spends no quota. The suites run the
real source rather than a copy of it — the backend compiled from its
TypeScript with the project's own tsc, and
app.js executed against a stub
DOM — except
tests/shared-constants.test.js and
tests/markdown.test.js, which read files as text:
tests/retry-logic.test.js — the escalation
chain, driven against a mocked fetch.tests/trial-status.test.js — against an
in-memory stand-in for Supabase, including
the real agent and trial endpoints end to end:
The other half of that
rule needs the
network, so it runs outside npm test:
npm run check-render
(tests/check-render.js) renders every Markdown
file through GitHub's own Markdown
API
and compares the page with its source:
That is what catches a defect nobody has thought of yet. Run it before every commit that touches a Markdown file; it uses one GitHub request per file, or per piece of a file over 200,000 characters: GitHub's API renders at most 400 KB, so such a file is sent in pieces cut before its headings.
GitHub allows 60 such requests an hour without a token, and 5,000 with
one.
The script sends one when GITHUB_TOKEN is set in the environment or in .env
(see .env.example); a fine-grained token with no permissions
is enough, since the Markdown
API
needs none when, as here, only public resources are requested.
netlify dev costs no Netlify credits —
it never touches the cloud build/deploy pipeline. It does reach the real
OpenRouter API for any representative/judge call, so local testing still spends
real quota.
This project was built with Claude Code. CLAUDE.md, tracked in
this repository, is the working brief and running status/decision
log used throughout — the case
content
and requirements it was built against, the architectural decisions and why they
were made, real bugs found and
fixed
(with root causes), and the reasoning behind UI/UX
choices made along the way.
Feature-complete and stable.
The full pipeline (four representatives in parallel, three judges after, independent rulings) has been verified across many real end-to-end trials against real models, both locally and against the live deployed site.
The reliability chain — tier escalation,
degenerate-output detection, and recovery from truncated or transient failures —
has been exercised repeatedly against the real OpenRouter API, including on the
deployed site: one production
trial caught a genuinely degenerate response,
retried it on the same model, hit the token cap, escalated a tier, and finished
cleanly, without anything being staged to provoke it. Six offline regression
suites (npm test) drive the real shipped source
for the same paths.
All three anti-abuse layers are in place:
netlify.toml and enforced by Netlify's own
platform,
as a backstop far above normal use, and the agent functions accept requests
only through those routes, on the site's main
address; both were verified on the live
site on 2026-10-06 (the limit and the
routes, the main
address),
at the limit of 60.The frontend (layout, live status display, call log transparency, a responsive card view for narrow screens, cross-browser scrollbar and interaction details) is complete.
openrouter.tsdb.ts
or chargeSheet.ts, the only
modules that import supabase.ts, where
the one client is made with the service-role key (see Database
for what the tables let other keys do).siteGate.ts;
case.ts and
trial.ts, which only read, do not import it.netlify.toml sets it on the two agent
routes only, and each agent function refuses a
request that did not come through its route on the site's main
address, through
extractParams.ts.extractParams.ts,
judges.ts,
models.ts,
pricing.ts,
representatives.ts,
response.ts,
safeHandler.ts and
types.ts, and the imports between the
modules of netlify/functions/lib/ other than those
into supabase.ts. Every arrow between
two modules is a real import, and every import from a function into a module
drawn has its arrow.404representative-background.ts:
Runs one representative through the escalation chain
and saves the argument, unless the trial has been
aborted meanwhile.judge-background.ts: Runs one judge
on the case record plus whichever representative
arguments were saved, and saves the
ruling, unless the trial has been aborted meanwhile. Marks
the trial completed once every judge has a final outcome.abort.ts: Takes { "roles": [...] } — the
roles still pending — records an abort for each one it
recognises, writing no second one for a role named twice or already
recorded, and replies with the roles it recorded. The running calls check
for it before every attempt and as each attempt ends, and stop. Needs the
site-gate header. 409 for a trial already
completed, which has nothing left running to stop.truncated | red | The same cap hit, with nothing left to fall back to: on the final tier, or with no time budget left for another. Nothing was saved. |
degenerated | red | The same detector hit, with nothing left to fall back to. Nothing was saved. |
escalated | amber | A plain HTTP failure from that tier's own model (e.g. a removed model id returning 404). Skips the tier's remaining attempts, since re-asking a model that just 404'd is pointless. On the last tier there is nowhere to escalate, so the same failure ends the call and shows as failed. |
| no response | amber | A transient failure — a timeout or network error, HTTP 408 or 429, a 5xx, a 200 carrying no text (including a reasoning model that spent its whole token cap reasoning), or a reply an upstream error cut short (finish_reason error), which is never kept. Retried on the same model, or escalated once the tier's attempts are used up; the caption says which. Coming back in under 10 seconds can make the retry free — not counted against the tier's attempts — but only for the first few at each tier, and only with enough time budget left to try again; past that a fast failure costs an attempt like any other. |
aborted | amber | The call's last row when the trial was aborted while it was running server-side, in one of three ways: it stopped before starting an attempt (no tokens, no duration); an attempt that failed as the abort landed is not retried; or a reply that finished after the abort is not saved. The last two keep that attempt's real tokens, cost and reply, since it ran and was paid for. |
truncated | amber | Legacy only: a second badge shown next to a green success on a row logged before truncation became a real failure, on 2026-08-29, whose completion is a whole multiple of the 1,400-token cap. New trials never produce it. |
tests/render-cards.test.js —
app.js's agent cards, the banner above the judges, the
call log, how it shortens model ids, how it reports a request that
fails outright, when a card's top and bottom fades show and that they stop
at its scrollbar, and that creating a trial, starting each agent and
aborting all carry the site-gate header.tests/shared-constants.test.js — values
that are deliberately duplicated across files still agree, the page's polling
and "interrupted" timeouts stay above the server's
time budget, a card's text area is a whole number of lines tall, so no
line is cut in half, and the captions in the call log wrap
evenly in the table view and as before in the card view. The duplicated
values: the markers, the fixed messages and the phrase that marks a
truncation, the roles with their names and
seats, the scrollbar's resting opacity, the
spinner's durations, the class that dims a call-log row, the
property that carries a card's scrollbar width to its fades, the classes
that show them, and the sidebar's width where the loading overlay restates
it.tests/docs.test.js — this README, SPEC.md
and the requirement parts of CLAUDE.md still say what the code
does.
schema.sql's order, with a section per table whose
Columns line names exactly its columns; and each badge label of more than
one word joined, or wrapped only where its table allows.CLAUDE.md to HARD RULE
4:
every link lands and holds no whitespace, every commit hash is linked at
each mention, and the first mention in each paragraph of a file,
npm test, a table, a route, or the thing a section's heading
names (e.g. "The escalation chain") is linked to
it.tests/markdown.test.js — no Markdown file,
CLAUDE.md included, holds source GitHub is known to render
wrongly: an HTML tag it would drop, a code span left open or holding an escape
it would print, a line of - or = that turns the text above into a heading,
a list, heading or quote starting mid-paragraph, a line starting with + or
* , a table missing its |---| row or with a row of the wrong width, an
unmatched **, a broken link, and the like.