From 1e723e94c1b79634a4334174eac1803d9399d198 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasilev Date: Wed, 16 Sep 2026 00:45:19 +0700 Subject: [PATCH 1/2] fix(website): the unbound-tools group is stated, not implied by absence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit groups.triByAgent got its '-' bucket only when some tri command had no agent, so the day every command gained one the key vanished and qa/tools-spec-contract read undefined where it asks for a list -- the same shape the contract's own message promises, "unbound under '-'". This is the second time today the same class of bug surfaced in this file: a structure that held only while the data had a particular property, and broke when the property went away. "Nothing is unbound" is a fact worth stating, so the bucket is always there and empty when it is empty. Группа '-' создавалась, только если существовали команды без агента. Как только агент появился у всех, ключ исчез, и контракт получил undefined там, где просил список. Теперь группа есть всегда и пуста, когда пуста. Co-Authored-By: Claude Opus 5 --- apps/website/scripts/agents-from-specs.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/website/scripts/agents-from-specs.mjs b/apps/website/scripts/agents-from-specs.mjs index ef956c8589..f4dd9d4bb8 100644 --- a/apps/website/scripts/agents-from-specs.mjs +++ b/apps/website/scripts/agents-from-specs.mjs @@ -870,7 +870,12 @@ export function buildSpecCatalogs({ skillSpecs, cronSpecs, agentSpecs = [], func collisions, // Grouping the navigator shows: tri commands by owning letter (unbound under '-'), MCP servers by repo. groups: { - triByAgent: Object.fromEntries([...new Set(triTools.flatMap((x) => (x.agents.length ? x.agents.map((a) => a.letter) : ['-'])))].sort().map((l) => [l, triTools.filter((x) => (l === '-' ? x.agents.length === 0 : x.agents.some((a) => a.letter === l))).map((x) => x.id)])), + // '-' is always present, empty when nothing is unbound. It used to appear + // only while some command had no agent, so the day every command gained + // one the bucket vanished and the contract read undefined where it had + // asked for a list. "Nothing is unbound" is a fact worth stating, not an + // absence to infer. + triByAgent: Object.fromEntries([...new Set(['-', ...triTools.flatMap((x) => x.agents.map((a) => a.letter))])].sort().map((l) => [l, triTools.filter((x) => (l === '-' ? x.agents.length === 0 : x.agents.some((a) => a.letter === l))).map((x) => x.id)])), mcpByRepo: Object.fromEntries(['gHashTag/t27', 'gHashTag/trinity'].map((r) => [r, mcpTools.filter((x) => x.repo === r).map((x) => x.id)])), triByRepo: Object.fromEntries(TOOL_REPOS.map((r) => [r, triTools.filter((x) => x.repo === r).map((x) => x.id)])), }, From f8974945fabcb71b7b6e279c41f28335d87a0f4d Mon Sep 17 00:00:00 2001 From: gHashTag Date: Thu, 17 Sep 2026 13:33:43 +0700 Subject: [PATCH 2/2] LANES: the swarm's width, and whether the width is working MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On 2026-09-17 the swarm reached 100% utilisation for the first time and produced nothing. Ten lanes were busy; every turn ended refused in zero seconds, because all five provider keys answered HTTP 429. A lane held by a refusal looks exactly like a lane doing work, so the headline number lied. The view prints utilisation beside the evidence for whether it is work — a turn refused in under a second never reached the provider — and states the lane law: capacity is live keys times lanes per key, and a worker ceiling cannot conjure a lane a key does not pay for. It also states how a player widens the swarm and how XP is earned for it: per turn that reached a branch, never per dispatch, because paying per dispatch would pay best when the swarm is most broken. No field accepts a provider key and none ever will; this is a public static page. The contributor board, per-key health and the XP ledger have no endpoint yet. Per the contract in queenHud.ts they are named as missing, with the shape the endpoint would carry, rather than invented. Co-Authored-By: Claude Code --- apps/website/qa/agents-spec-contract.mjs | 7 +- apps/website/src/components/QueenLanes.css | 155 +++++++++++++ apps/website/src/components/QueenLanes.tsx | 243 +++++++++++++++++++++ apps/website/src/components/queenHud.ts | 7 +- apps/website/src/lib/queenModules.ts | 15 ++ apps/website/src/pages/Queen.tsx | 26 +++ 6 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 apps/website/src/components/QueenLanes.css create mode 100644 apps/website/src/components/QueenLanes.tsx diff --git a/apps/website/qa/agents-spec-contract.mjs b/apps/website/qa/agents-spec-contract.mjs index d9eef813e5..2d7aea60d7 100644 --- a/apps/website/qa/agents-spec-contract.mjs +++ b/apps/website/qa/agents-spec-contract.mjs @@ -360,7 +360,12 @@ assert.ok(triModule && HUD_VIEWS.includes('tri'), 'TRI (the app inside the game) assert.equal(triModule.key, 'r', 'TRI opens on r: digits spent, t is TOOLS, p is PROJECT') assert.ok(triModule.en.hint.includes('(key r)') && triModule.ru.hint.includes('(клавиша r)'), 'TRI names its letter key in both hints') for (const lang of ['en', 'ru']) assert.ok(triModule[lang].name && triModule[lang].body.length > 40, `tri: ${lang} copy missing`) -assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tpr', 'the rail keys are 1-9, 0, t, p, r in that order') +const lanesModule = MODULES.find((m) => m.tab === 'lanes') +assert.ok(lanesModule && HUD_VIEWS.includes('lanes'), 'LANES (the swarm\'s width) is a module and a view') +assert.equal(lanesModule.key, 'l', 'LANES opens on l: digits spent, t is TOOLS, p is PROJECT, r is TRI') +assert.ok(lanesModule.en.hint.includes('(key l)') && lanesModule.ru.hint.includes('(клавиша l)'), 'LANES names its letter key in both hints') +for (const lang of ['en', 'ru']) assert.ok(lanesModule[lang].name && lanesModule[lang].body.length > 40, `lanes: ${lang} copy missing`) +assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprl', 'the rail keys are 1-9, 0, t, p, r, l in that order') // 6. Translations are connected through .t27 contract specs, never hardcoded. // Every specs/i18n/*.t27 the corpus carries is in both catalogs' i18n lists diff --git a/apps/website/src/components/QueenLanes.css b/apps/website/src/components/QueenLanes.css new file mode 100644 index 0000000000..a406688d61 --- /dev/null +++ b/apps/website/src/components/QueenLanes.css @@ -0,0 +1,155 @@ +/* QueenLanes.css - LANES, the view that says how many bees can work at once and + refuses to let utilisation stand in for work. Same panel skin as QueenIntel: + colours come from the --hud-* variables Queen.css defines, with the live + palette as fallback, and the body is the only scroll owner. */ + +.queen27-lanes { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-height: 0; + height: 100%; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: var(--hud-panel, rgba(2, 8, 6, 0.86)); + clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); +} + +.queen27-lanes-head { + padding: 12px 16px; + border-bottom: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); +} + +.queen27-lanes-head h2 { + margin: 0; + font-size: 0.82rem; + letter-spacing: 0.18em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-head p { + margin: 6px 0 0; + font-size: 0.78rem; + line-height: 1.5; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-body { + min-height: 0; + overflow-y: auto; + padding: 14px 16px 20px; + display: grid; + gap: 12px; + align-content: start; +} + +/* ---- the four figures ------------------------------------------------- */ + +.queen27-lanes-figures { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); + gap: 10px; +} + +.queen27-lanes-figure { + margin: 0; + padding: 10px 12px; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: rgba(0, 255, 136, 0.04); + display: grid; + gap: 2px; +} + +.queen27-lanes-figure figcaption { + font-size: 0.64rem; + letter-spacing: 0.16em; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-figure b { + font-size: 1.6rem; + font-weight: 600; + line-height: 1.1; + color: var(--hud-accent, #00ff88); + font-variant-numeric: tabular-nums; +} + +.queen27-lanes-figure small { + font-size: 0.66rem; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-figure-wide { + grid-column: span 2; +} + +/* ---- cards ------------------------------------------------------------ */ + +.queen27-lanes-card { + padding: 12px 14px; + border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22)); + background: rgba(2, 8, 6, 0.5); +} + +.queen27-lanes-card h3 { + margin: 0 0 8px; + font-size: 0.72rem; + letter-spacing: 0.16em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-card h4 { + margin: 12px 0 4px; + font-size: 0.68rem; + letter-spacing: 0.14em; + color: var(--hud-accent, #00ff88); +} + +.queen27-lanes-card p { + margin: 0 0 6px; + font-size: 0.78rem; + line-height: 1.55; + color: var(--hud-text, rgba(233, 255, 244, 0.9)); +} + +/* The alarm skin is earned, not decorative: it appears only when the last + dispatch was refused in under a second, which is a spent quota. */ +.queen27-lanes-card-alarm { + border-color: rgba(255, 77, 94, 0.55); + background: rgba(255, 77, 94, 0.07); +} + +.queen27-lanes-alarm { + color: #ff4d5e; + font-weight: 600; +} + +.queen27-lanes-warn { + color: #ffc24d; +} + +.queen27-lanes-note, +.queen27-lanes-meta { + font-size: 0.72rem; + color: var(--hud-dim, rgba(214, 255, 235, 0.68)); +} + +.queen27-lanes-meta { + font-variant-numeric: tabular-nums; +} + +.queen27-lanes-formula { + display: block; + margin: 6px 0; + padding: 8px 10px; + font-size: 0.72rem; + line-height: 1.5; + overflow-x: auto; + border: 1px dashed var(--hud-line, rgba(0, 255, 136, 0.22)); + color: var(--hud-accent, #00ff88); + background: rgba(0, 0, 0, 0.35); +} + +.queen27-lanes-error { + margin: 0; + font-size: 0.74rem; + color: #ff4d5e; +} diff --git a/apps/website/src/components/QueenLanes.tsx b/apps/website/src/components/QueenLanes.tsx new file mode 100644 index 0000000000..f357f71cb9 --- /dev/null +++ b/apps/website/src/components/QueenLanes.tsx @@ -0,0 +1,243 @@ +// The Queen's LANES view: how many bees can work at once, why that number is +// what it is, and how a player raises it. +// +// The view exists because on 2026-09-17 the swarm reached 100% utilisation for +// the first time and produced nothing at all. Ten lanes were busy; every turn +// ended `refused` in zero seconds, because all five provider keys answered +// HTTP 429 (z.ai code 1302, "Rate limit reached"). A lane occupied by a refusal +// looks exactly like a lane doing work, so the headline number lied. +// +// Hence the shape of this panel: utilisation is shown, and immediately beside it +// the evidence for whether it is *work*. The two are never printed as one. +// +// Following the contract in queenHud.ts: every number here is a field the public +// /queen/status endpoint already sends. The ones the swarm knows but does not +// publish — how many keys are live, who contributed them, what each has spent — +// are named as missing, with the endpoint that would carry them. A panel that +// invents them would be the same lie in a different font. + +import { useI18n } from '../i18n/context' +import './QueenLanes.css' + +export interface LanesWorkers { + capacity: number + active: number + idle: number + utilization: number +} + +export interface LanesDispatch { + issue: number + dispatchedAt: string + finishedAt: string | null + outcome: string | null +} + +export interface LanesStatus { + workers?: LanesWorkers | null + dispatches?: { + total: number + finished: number + running: number + unreviewed?: number + latest?: LanesDispatch | null + } | null +} + +export interface LanesCopy { + directive: string + directiveBody: string +} + +const COPY = { + en: { + capacity: 'LANES', + capacityHint: 'bees that can work at once', + active: 'BUSY', + idle: 'FREE', + util: 'UTILISATION', + lawTitle: 'THE LANE LAW', + law: 'A lane is not a setting. Capacity is the number of provider keys that answer, times the lanes each key is allowed to open. The worker ceiling is only a ceiling: it cannot conjure a lane a key does not pay for.', + lawFormula: 'capacity = live keys × lanes per key', + lawUnknown: 'How many keys are live is not on the public endpoint. This panel will not guess it.', + workTitle: 'IS IT WORK?', + workLead: 'Utilisation counts occupied lanes. A refused turn occupies one exactly like a working turn, so the two must be read together, never as one number.', + lastDispatch: 'last dispatch', + instantRefusal: 'Zero-second refusal — the lane was occupied and nothing was attempted. This is what a spent quota looks like from outside.', + refusalHint: 'Refusals are cheap and fast, so a starved swarm reports its best utilisation ever.', + healthy: 'The last dispatch ran for a measurable time. That is necessary for real work, and not sufficient: only a branch proves it.', + pending: 'The last dispatch is still running.', + noDispatch: 'No dispatch recorded yet.', + unreviewed: 'finished, awaiting review', + donateTitle: 'DONATE A LANE', + donateLead: 'The currency is quota, not lanes. A key that answers adds both; a lane without quota multiplies zero. This is the one contribution that raises the ceiling for everybody at once.', + xpTitle: 'HOW XP IS EARNED', + xpRule: 'XP accrues to the owner of the key a turn ran on — but only for a turn that reached a branch. Never per dispatch: a refused turn occupies a lane exactly like a working one, and paying for it would pay best when the swarm is most broken.', + neverTitle: 'THIS PAGE NEVER TAKES A KEY', + never: 'No field here accepts a provider key, and none ever will. This is a public static page; a secret typed into it is a secret published. Keys are handed over out of band and held by the server alone.', + missingTitle: 'NOT WIRED YET', + missing: 'The contributor board, the per-key health and the XP ledger need an endpoint that does not exist yet:', + missingEndpoint: 'GET /queen/public-lanes → { keys: [{ owner, state: "live" | "cooldown" | "spent", lanes, turnsToBranch }] }', + missingWhy: 'Until it answers, this view shows the swarm-wide numbers above and says nothing about who paid for them.', + branchTitle: 'WHERE THE WORK GOES', + branchLead: 'A finished turn writes a branch inside the container, which deliberately holds no push credential — so nothing reaches GitHub until something outside pushes it. That publishing step is where a day of bee work can sit unseen.', + branchGit: 'GitButler is the intended safe path for it: virtual branches let the queue be published and reviewed in slices instead of as one irreversible flood.', + }, + ru: { + capacity: 'ПОЛОСЫ', + capacityHint: 'пчёл могут работать одновременно', + active: 'ЗАНЯТО', + idle: 'СВОБОДНО', + util: 'ЗАГРУЗКА', + lawTitle: 'ЗАКОН ПОЛОСЫ', + law: 'Полоса — не настройка. Ёмкость это число отвечающих ключей провайдера, умноженное на число полос, разрешённых одному ключу. Потолок воркеров — только потолок: он не создаст полосу, за которую не платит ключ.', + lawFormula: 'ёмкость = живые ключи × полос на ключ', + lawUnknown: 'Сколько ключей живо — публичный эндпоинт не сообщает. Эта панель не станет угадывать.', + workTitle: 'А ЭТО РАБОТА?', + workLead: 'Загрузка считает занятые полосы. Отказной ход занимает полосу ровно так же, как рабочий, поэтому два числа читаются вместе и никогда не сливаются в одно.', + lastDispatch: 'последняя выдача', + instantRefusal: 'Отказ за ноль секунд — полоса была занята, а попытки не было. Так снаружи выглядит исчерпанная квота.', + refusalHint: 'Отказы дёшевы и мгновенны, поэтому голодающий рой показывает лучшую загрузку в своей истории.', + healthy: 'Последняя выдача длилась измеримое время. Это необходимо для настоящей работы и недостаточно: доказывает её только ветка.', + pending: 'Последняя выдача ещё выполняется.', + noDispatch: 'Выдач пока не записано.', + unreviewed: 'завершено, ждёт ревью', + donateTitle: 'ОТДАТЬ ПОЛОСУ', + donateLead: 'Валюта — квота, а не полосы. Отвечающий ключ добавляет и то и другое; полоса без квоты умножает ноль. Это единственный вклад, который поднимает потолок сразу для всех.', + xpTitle: 'КАК НАЧИСЛЯЕТСЯ XP', + xpRule: 'XP идёт владельцу ключа, на котором прошёл ход, — но только за ход, дошедший до ветки. Никогда за диспатч: отказной ход занимает полосу так же, как рабочий, и плата за него платила бы лучше всего тогда, когда рой сломан сильнее всего.', + neverTitle: 'ЭТА СТРАНИЦА НИКОГДА НЕ ПРИНИМАЕТ КЛЮЧ', + never: 'Здесь нет поля для ключа провайдера и не будет. Это публичная статическая страница; секрет, введённый в неё, — секрет опубликованный. Ключи передаются вне игры и хранятся только на сервере.', + missingTitle: 'ЕЩЁ НЕ ПОДКЛЮЧЕНО', + missing: 'Доска вкладчиков, здоровье по каждому ключу и журнал XP требуют эндпоинта, которого пока нет:', + missingEndpoint: 'GET /queen/public-lanes → { keys: [{ owner, state: "live" | "cooldown" | "spent", lanes, turnsToBranch }] }', + missingWhy: 'Пока он не отвечает, вкладка показывает общие числа роя выше и молчит о том, кто за них заплатил.', + branchTitle: 'КУДА УХОДИТ РАБОТА', + branchLead: 'Завершённый ход пишет ветку внутри контейнера, который намеренно не держит push-креденшел, — поэтому до GitHub ничего не доходит, пока её не выложит что-то снаружи. Именно на этом шаге день работы пчёл может простоять невидимым.', + branchGit: 'GitButler — предполагаемый безопасный путь для этого: виртуальные ветки позволяют публиковать и ревьюить очередь порциями, а не одним необратимым потоком.', + }, +} as const + +/** Milliseconds a dispatch lasted, or null when it has not finished. */ +const durationMs = (d: LanesDispatch): number | null => { + if (!d.finishedAt) return null + const ms = Date.parse(d.finishedAt) - Date.parse(d.dispatchedAt) + return Number.isFinite(ms) ? ms : null +} + +export function QueenLanes({ + status, + error, + c, +}: { + status: LanesStatus | null + error: string | null + c: LanesCopy +}) { + const { lang } = useI18n() + const t = lang === 'ru' ? COPY.ru : COPY.en + + const workers = status?.workers ?? null + const dispatches = status?.dispatches ?? null + const latest = dispatches?.latest ?? null + const ran = latest ? durationMs(latest) : null + // A refusal that took no measurable time never reached the provider. That is + // the signature of a spent quota, and the only way to tell it apart from a + // busy swarm without reading the container's own logs. + const instant = latest?.outcome === 'refused' && ran !== null && ran < 1000 + + return ( +
+
+

{c.directive}

+

{c.directiveBody}

+
+ +
+
+
+
{t.capacity}
+ {workers ? workers.capacity : '—'} + {t.capacityHint} +
+
+
{t.active}
+ {workers ? workers.active : '—'} +
+
+
{t.idle}
+ {workers ? workers.idle : '—'} +
+
+
{t.util}
+ {workers ? `${workers.utilization}%` : '—'} +
+
+ + {error ?

{error}

: null} + +
+

{t.workTitle}

+

{t.workLead}

+ {latest ? ( + <> +

+ {t.lastDispatch}: #{latest.issue} + {latest.outcome ? ` · ${latest.outcome}` : ''} + {ran !== null ? ` · ${(ran / 1000).toFixed(1)}s` : ''} +

+ {instant ? ( + <> +

{t.instantRefusal}

+

{t.refusalHint}

+ + ) : ran === null ? ( +

{t.pending}

+ ) : ( +

{t.healthy}

+ )} + + ) : ( +

{t.noDispatch}

+ )} + {dispatches?.unreviewed ? ( +

+ {dispatches.unreviewed} {t.unreviewed} +

+ ) : null} +
+ +
+

{t.lawTitle}

+

{t.law}

+ {t.lawFormula} +

{t.lawUnknown}

+
+ +
+

{t.donateTitle}

+

{t.donateLead}

+

{t.xpTitle}

+

{t.xpRule}

+

{t.neverTitle}

+

{t.never}

+
+ +
+

{t.missingTitle}

+

{t.missing}

+ {t.missingEndpoint} +

{t.missingWhy}

+
+ +
+

{t.branchTitle}

+

{t.branchLead}

+

{t.branchGit}

+
+
+
+ ) +} + +export default QueenLanes diff --git a/apps/website/src/components/queenHud.ts b/apps/website/src/components/queenHud.ts index f9a549abd3..12b3218231 100644 --- a/apps/website/src/components/queenHud.ts +++ b/apps/website/src/components/queenHud.ts @@ -7,7 +7,7 @@ // derivable from these types, the number does not exist yet and the panel // must say so rather than invent it. -export type HudView = "comb" | "specs" | "kanban" | "map" | "factory" | "research" | "skills" | "crons" | "agents" | "functions" | "tools" | "project" | "tri"; +export type HudView = "comb" | "specs" | "kanban" | "map" | "factory" | "research" | "skills" | "crons" | "agents" | "functions" | "tools" | "project" | "tri" | "lanes"; // In command-panel order: the key that opens a view is HUD_KEYS at the same // position, and `?tab=` accepts exactly these names. Kept identical to // lib/queenModules (qa/agents-spec-contract.mjs checks the two lists agree), so @@ -30,12 +30,15 @@ export const HUD_VIEWS: readonly HudView[] = [ // Thirteenth, on the letter r: TRI, the app at app.t27.ai inside the game (the // digits are spent, t is TOOLS, p is PROJECT). "tri", + // Fourteenth, on the letter l: LANES -- how many bees can work at once, and + // why utilisation is not the same question as whether they are working. + "lanes", ] as const; // The keyboard shortcut per view, by position: the digits 1-9, then 0, then // letters once the digits are spent. The rail prints HUD_KEYS[i] on button i and // the shell binds exactly these keys; a tenth or eleventh view takes the next // entry here and nothing else changes. -export const HUD_KEYS: readonly string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "t", "p", "r"] as const; +export const HUD_KEYS: readonly string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "t", "p", "r", "l"] as const; export const hudKeyOf = (view: HudView): string => HUD_KEYS[HUD_VIEWS.indexOf(view)] ?? ""; export type Territory = "held" | "neutral" | "fog"; diff --git a/apps/website/src/lib/queenModules.ts b/apps/website/src/lib/queenModules.ts index 991da22f83..a321d7cc8a 100644 --- a/apps/website/src/lib/queenModules.ts +++ b/apps/website/src/lib/queenModules.ts @@ -210,6 +210,21 @@ export const MODULES = [ body: 'Приложение app.t27.ai внутри игры. Каждый экран — настоящая страница приложения во фрейме: лента, агент, ИИ-конвейер от сценария через голос, фото, липсинк и видео к редактору, профиль и CRM владельца. Экран записан в адресе (?tab=tri&screen=chat, а для одного профиля ещё path=), поэтому ссылка его открывает, а перезагрузка сохраняет. На t27.ai Telegram не даёт своему входу загрузиться внутри чужого сайта, поэтому экраны, которым нужен человек, говорят об этом и ведут в само приложение; лента работает для всех. Пока приложение не разрешит t27.ai показывать себя во фрейме, экран не отвечает и говорит об этом. Открывается буквой r.', }, }, + { + tab: 'lanes', + key: 'l', + glyph: '≡', + en: { + name: 'LANES', + hint: 'How many bees can work at once, and whether they are working (key l)', + body: 'The swarm\'s width, and the one question the width cannot answer. Capacity is the number of provider keys that answer times the lanes each key may open — a worker ceiling cannot conjure a lane a key does not pay for. Beside the utilisation figure the view prints the evidence for whether it is work at all: a turn refused in under a second never reached the provider, and a starved swarm reports its best utilisation ever, because a refusal occupies a lane exactly like a real turn. It also states how a player widens the swarm — a donated key adds quota, which is the actual currency — and how XP is earned for it: per turn that reached a branch, never per dispatch. No field on this page accepts a provider key and none ever will; it is a public static page, and a secret typed into it is a secret published. The digits are spent, so this view opens on the letter l.', + }, + ru: { + name: 'ПОЛОСЫ', + hint: 'Сколько пчёл работают одновременно и работают ли вообще (клавиша l)', + body: 'Ширина роя и единственный вопрос, на который ширина не отвечает. Ёмкость — это число отвечающих ключей провайдера, умноженное на число полос, разрешённых одному ключу: потолок воркеров не создаст полосу, за которую не платит ключ. Рядом с цифрой загрузки вид печатает свидетельство того, работа ли это вообще: ход, отказанный быстрее секунды, до провайдера не дошёл, а голодающий рой показывает лучшую загрузку в своей истории, потому что отказ занимает полосу так же, как настоящий ход. Здесь же сказано, чем игрок расширяет рой — отданный ключ добавляет квоту, а квота и есть настоящая валюта — и как за это начисляется XP: за ход, дошедший до ветки, и никогда за диспатч. Ни одно поле этой страницы не принимает ключ провайдера и не будет: это публичная статическая страница, а секрет, введённый в неё, — секрет опубликованный. Цифры заняты, поэтому вид открывается буквой l.', + }, + }, ] as const export type QueenModule = (typeof MODULES)[number]; diff --git a/apps/website/src/pages/Queen.tsx b/apps/website/src/pages/Queen.tsx index 57562336ff..16ea1f2929 100644 --- a/apps/website/src/pages/Queen.tsx +++ b/apps/website/src/pages/Queen.tsx @@ -73,6 +73,7 @@ const ENGINE_FLAG = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("engine") : null; import { useI18n } from "../i18n/context"; import { QueenTri } from "../components/QueenTri"; +import { QueenLanes } from "../components/QueenLanes"; import { QueenIdentity } from "../components/QueenIdentity"; import { hashParamsOf, tabAddress } from "../lib/triScreens"; import { @@ -157,6 +158,14 @@ interface ResearchGraph { interface QueenStatus { status: "ok"; + /** Lane occupancy as the swarm reports it. Occupancy, not throughput: a + refused turn holds a lane exactly like a working one (see QueenLanes). */ + workers?: { + capacity: number; + active: number; + idle: number; + utilization: number; + } | null; /** The swarm's own word for its state on the wire (working, idle, …). */ swarmState?: string | null; scheduler: { @@ -339,6 +348,11 @@ const COPY = { projectSources: "sources pinned", triView: "TRI", triHint: "The app inside the game: feed, agent, AI generation, profile and CRM (key r)", + lanesView: "LANES", + lanesHint: "How many bees can work at once, and whether they are working (key l)", + lanesDirective: "LANES", + lanesDirectiveBody: + "Capacity is live provider keys times the lanes each may open. Utilisation counts occupied lanes — a refused turn occupies one too, so it is printed beside the evidence, never instead of it.", triScreens: "App screens", triFeed: "Feed", triAgent: "Agent", @@ -663,6 +677,11 @@ const COPY = { projectSources: "источников закреплено", triView: "TRI", triHint: "Приложение внутри игры: лента, агент, ИИ-генерация, профиль и CRM (клавиша r)", + lanesView: "ПОЛОСЫ", + lanesHint: "Сколько пчёл работают одновременно и работают ли вообще (клавиша l)", + lanesDirective: "ПОЛОСЫ", + lanesDirectiveBody: + "Ёмкость — живые ключи провайдера, умноженные на полосы каждого. Загрузка считает занятые полосы, а отказной ход занимает полосу тоже, поэтому она печатается рядом со свидетельством, а не вместо него.", triScreens: "Экраны приложения", triFeed: "Лента", triAgent: "Агент", @@ -2405,6 +2424,7 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) // Thirteenth, on the letter r (HUD_KEYS[12]; digits spent, t is TOOLS, p is // PROJECT): TRI, the app at app.t27.ai inside the game, one screen per address. { view: "tri" as const, glyph: "△", label: c.triView, hint: c.triHint }, + { view: "lanes" as const, glyph: "≡", label: c.lanesView, hint: c.lanesHint }, ]; const viewLabel = commandItems.find((item) => item.view === view)?.label ?? c.combView; @@ -2830,6 +2850,12 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) projectSources: c.projectSources, }} /> + ) : boardView === "lanes" ? ( + ) : boardView === "tri" ? (