AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证BeTenshi Console — monitor and test AI services (vLLM, anyonebuild)
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
One console for the AI services running on your own machines. It starts and
stops them, keeps them inside a memory budget, routes work to whichever model
should handle it, tells you what the box can and cannot run, and shows every
call that went through it.
Hangar is the product; Console is the operator surface. The canonical public
entry point is hangar.humanquest.net, with console.humanquest.net as a second
entry point. The private source repository is alibad/hangar; BeTenshi itself
is only one machine profile.查看完整 README ↗Your projects, live work, models and machines in one workspace. Explore projects as buildings on floating islands, continue Claude and Codex work, review idle sessions, and use models on a connected machine through SSH. Hangar also starts and stops local AI services, keeps them inside a memory budget, and shows the requests that pass through its gateway.
Start with the connected workspace setup, the
second-machine handoff, or the
visual identity. The project engine and 3D world are bundled in
this repository. No Atlas checkout, account access or separate Atlas service is
required. Install Node 22+, Python 3.10+ and Git; GitHub CLI authentication enables
remote repository discovery. Machine configuration and session records stay local
under ignored var/.
Hangar is the product; Console is the operator surface. The canonical public
entry point is hangar.humanquest.net, with console.humanquest.net as a second
entry point. The source repository is alibad/hangar; BeTenshi itself
is only one machine profile.
The full console is a Next.js app you run on the machine itself. A hosted copy can reach explicitly configured remote services, but any surface that requires local drives, logs, developer history, model files, or persistent local output shows a Local machine required placeholder instead of fabricated empty data. See Hosted mode.
Every screenshot in this README was captured by driving the real console with a real browser — see How these screenshots were made. Nothing here is a mockup.
Running local AI is not one program, it is a small fleet: an inference server, a diffusion server, speech in and speech out, a gateway in front of them, a metrics stack behind them. Each is a separate process with its own port, its own memory appetite and its own idea of how to be started.
The failure modes are boring and constant:
This console is the single place that answers those. It is the machine's front desk, not a chat UI that happens to have settings.
The console runs on more than one machine, and those machines have nothing in
common — different operating systems, different accelerators, and, critically,
different memory models. Rather than branching on process.platform all over
the code, everything a machine differs by lives in one JSON file.
config/hosts/
b5.json a MacBook Pro — Apple silicon, unified memory, Ollama + LocalAI
betenshi.json a Windows box — discrete NVIDIA GPU, vLLM, tunnelled services
example.json an annotated template — start here
A profile declares the machine's services, how its memory is budgeted, what kind of GPU it has, and — importantly — which workstreams it can actually back.
Here is the same page on both machines. The screenshots predate B5's LocalAI adapter, so they show the intentionally smaller initial Mac profile. The current profile also exposes Draw Things image/video, Qwen speech, and embeddings through one loopback-only adapter; genuinely absent CUDA-only tools such as SAM3/SAM3D still say Not on this machine rather than offering a button into a dead tab:
| BeTenshi — RTX 5090, 15 services | B5 — Apple M5 Max, 2 services |
|---|---|
Nothing in the page is conditional on a hostname. Both are the same component reading a different JSON file.
Availability is always shown, never used to hide. A console that dropped half its navigation on a smaller box would read as a different, smaller product — and it would conceal exactly the thing you came to find out.
The memory model is the field that matters most:
memory.kind | Means | The console then |
|---|---|---|
discrete | A card and a host with separate budgets | Shows two bars, and refuses a model that will not fit the card |
unified | Apple silicon — one pool shared by CPU and GPU | Shows a single Memory bar and treats a model's footprint as one number |
Getting it wrong does not throw — it silently budgets against the wrong ceiling.
Adding a machine is adding a JSON file. There is no code to write. Copy
config/hosts/example.json, which documents every
field inline and is validated by the same test suite as the real profiles, so it
cannot silently rot.
A host profile says what a machine has. It cannot say whether any of it works, and for a long time nothing here could.
On 21 September 2026 an audit of this console found that every image it had produced on the Mac for four days was high-frequency colour static. Nothing noticed, because nothing looked:
| Layer | What it checked | What it said |
|---|---|---|
| health probe | is the port answering | up |
| the adapter | did the command exit 0 | yes |
file | is it a valid PNG | yes — signature, CRC, IEND all correct |
| the gallery | did a file arrive | shown, with a latency badge |
| Requests | did the HTTP call return | 200 |
The cause was environmental: Draw Things' standalone CLI ships Metal 4 cooperative-tensor shaders that macOS 27's Metal compiler rejects. The older build fails silently, returning a structurally perfect PNG of noise. The same weights render correctly in the Draw Things GUI app, so it was the binary, not the machine, and not the model.
The engine was the accident. The lesson is that a 200 is not evidence, so
profiles now declare a runtimes block — which engine drives each capability,
and when its output was last inspected:
"runtimes": {
"image": {
"driver": "mflux",
"model": "flux2-klein-4b",
"serviceId": "qwen",
"verifiedAt": "2026-09-21T12:16:20.000Z",
"verifiedNote": "512×512 in 32.9s, 78% smooth"
}
}
verifiedAt is written by exactly one thing:
node scripts/doctor.mjs # run everything, record nothing
node scripts/doctor.mjs --write # record only what genuinely passed
The doctor runs a real job per capability and inspects the result — it does not ask whether a service replied:
| Capability | The job | What is checked |
|---|---|---|
| text | a question with one correct answer | the answer, and that it is not an empty string |
| vision | a red square | that it says red |
| image | 512², 4 steps | pixel statistics: a picture has flat regions, static has none |
| tts | one spoken sentence | that the audio is long enough and not silent |
| stt | that same audio | that the words come back |
| embedding | three strings | that related text really does score closer than unrelated |
| video | a few frames | the container, and a decoded frame through the image test |
A failure clears a previous pass. A stale claim that something works is the exact lie the mechanism exists to prevent — which is what an OS upgrade produced here, silently, overnight.
The same check runs inline: every image is measured before it is saved, so a
backend that starts emitting noise returns a 502 naming the driver instead of
filling a gallery with plausible-looking rows. The measurement and the numbers
behind the threshold are in src/lib/image-quality.ts.
Engines are drivers, not assumptions. The same capability has different right answers per machine, so the profile picks one:
| Capability | Apple silicon | NVIDIA | CPU-only |
|---|---|---|---|
| text, vision, embedding | Ollama | Ollama or vLLM | Ollama |
| image | mflux (MLX) | ComfyUI or Qwen-Image | not yet |
| speech in / out | mlx-audio | Whisper / Kokoro | not yet |
Requires Node 22 or newer (/api/scout imports node:sqlite, a Node 22
builtin; there is an .nvmrc).
git clone https://github.com/alibad/hangar.git
cd hangar
npm install
cp .env.example .env.local # optional — every variable in it is optional
npm run dev # http://localhost:8003
On first run the console resolves a host profile automatically: HOST_ID if
set, else a hostname matching a file in config/hosts/, else a Mac is b5,
else the default. It will come up against whatever is actually listening on
this machine and report everything else as on-demand or unavailable.
Hangar decides what it can do by reading one JSON file per machine — and until that file exists for your computer, it falls back to one of the example profiles that ship with it. So a fresh clone shows a working-looking console describing somebody else's machine. It says so, in place, rather than letting you debug services you never installed:
Two ways past it. Ask an agent — open this repo in Claude Code or Codex and
run /hangar-setup. Codex can use .agents/skills/hangar-setup/SKILL.md, and
Claude Code can use .claude/skills/hangar-setup/SKILL.md. The skill probes what is actually listening here, writes the
profile and its start commands, registers it, and refuses to call itself done
while that banner is still up. Or do it by hand from
config/hosts/example.json, which documents every
field inline.
If you only want the measurement:
node scripts/detect-host.mjs
That prints the hostname, platform, accelerator, memory model and every known port that answers in the shape that service answers in. It leaves out two kinds of port, for two different reasons:
:4000 ai-router HTTP 200 on a Mac with no router installed, because another
project's dev server held the port. A wrong identification is worse than a
missing one, because the console then reports that service as DOWN forever
while you debug software you never installed.The service manager is the other half. Start/Stop buttons all go through a
supervisor on 127.0.0.1:8099:
node scripts/manager.cjs
Without it the console renders but reports an empty machine — no capacity, no ready counts, every workstream stuck on "On demand". That does not look like a missing supervisor; it looks like an empty box. Start it first.
npm test # 126 tests, run by node --test, no bundler needed
Then prove the machine can do what its profile claims. This is a separate question from whether the services are up, and the answer is frequently different:
node scripts/doctor.mjs
Every service the host profile declares, whether or not it is up, with its live
footprint and the controls to change that. Processes started outside the console
are detected and adopted rather than duplicated — the Hand over button on
Ollama below exists because the Ollama menu-bar app started it, not the manager.
Below the cards, Agent access hands the same stack to a coding agent over a
single MCP server: start and stop services, read logs and the GPU, generate an
image, transcribe audio. The same server carries the project world, so an agent
reads what you read: hangar_pending (what is waiting on you, repo by repo),
hangar_brief (a project's purpose, folder state, commits, app and recent
sessions), hangar_sessions, hangar_jobs and hangar_job, plus focus / park /
hide, a note, open-in-agent and rescan. See Connected workspace.
Local weights and cloud APIs in one list, each sized against this card and this drive. Not a catalogue: a verdict. The header reads 146 of 219 open-weights models on llm-stats run on this one, and an Also yours row carries the same count for every other machine you have registered — here, BeTenshi · 31.8 GB VRAM · 63.3 GB RAM · 112 run there, explicitly labelled declared, not measured. So "will this run, and where?" is answered before anything is downloaded.
A playground against whichever text model the host is serving, showing latency
and token count for every exchange. The answer below came from qwen3.8:27b-mlx
running locally: 4968 ms, 215 tokens.
Text to image against a local diffusion model, with the run setup beside it: which checkpoint is loaded, which service serves it, and what it costs. The lighthouse below was generated during the walk that produced this README — Qwen-Image, 1024×1024, 24 steps, on the RTX 5090.
It refuses work that will not fit. The first attempt at that image was rejected, correctly, with a 409:
VRAM needs 20.2 GB more plus 0 GB reserved and 0.02 GB safety,
but only 2.76 GB is currently free
Whisper, Kokoro, Chatterbox and Qwen-Image had been started alongside the running vLLM, and together they over-subscribe a 31.8 GB card. The console names the consumers rather than letting the GPU thrash — this is the admission control the resource policy exists for, and it is the single most distinctive thing here.
Whisper for speech to text, Kokoro and Chatterbox for text to speech, with voice cloning, in one pane per direction. Each engine is a service the console starts and stops like any other.
A live feed of traffic across the console and every local service, with
health-check noise filtered out by default. The rows in this capture are the
walkthrough that produced this README hitting /api/chat — it is a photograph
of its own run.
The console is meant to be there without anyone starting it.
macOS — two launchd agents, RunAtLoad + KeepAlive:
npm run build # the agent refuses to start without a .next build
./scripts/install-agents.sh
The plists are committed as .template files carrying a __CONSOLE_DIR__
placeholder. launchd requires absolute paths and will not expand ~ or read an
environment variable, so the path has to be substituted at install time — which
is what that script does. (They used to hardcode one person's home directory in
eight places.)
launchctl list | grep hangar # status
launchctl kickstart -k gui/$UID/com.hangar.console # restart after a rebuild
tail -f var/console.log # logs
./scripts/install-agents.sh --uninstall # remove
Windows — scripts/start-console.ps1, driven by
a Scheduled Task at logon.
Everything is optional. A console with an empty .env.local runs fine against
local services. See .env.example, which documents each variable
and — more usefully — what breaks without it.
| Variable | For |
|---|---|
HOST_ID | Force a host profile instead of detecting one |
PUBLIC_DOMAIN | The domain a tunnelled host publishes its services under |
CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET | Cloudflare Access service token, for reaching authRequired services from a deployed console |
HANGAR_DB_PATH / HANGAR_STORAGE_DB | Move the local databases off their default path |
Host profiles write public URLs as https://llm.${PUBLIC_DOMAIN} rather than
literal hostnames. This is deliberate and it is about disclosure, not
configurability: the full list of a machine's public hostnames is a map of what
that machine exposes and what software is behind each entry. Unset,
PUBLIC_DOMAIN resolves to example.com, which fails obviously rather than
plausibly. Local use never touches it — getServiceUrl() returns the localhost
URL whenever the console and its services are on the same box.
src/app/ one page (hash-routed tabs) + 54 API routes
src/components/ one component per surface
src/lib/ host.ts resolves the profile; everything else hangs off it
config/hosts/ the machines — add one by adding a file
config/ router aliases, resource policy, model metadata
scripts/ the service manager, MCP server, doctor, launchd templates, tests
.agents/skills/ /hangar-setup — Codex setup skill for a new machine
.claude/skills/ /hangar-setup — Claude Code setup skill for a new machine
docs/ design notes and model experiments
Two things are worth knowing before reading the code:
src/lib/host.ts is the spine. The profile is resolved once, in
next.config.ts, and published as NEXT_PUBLIC_HOST_ID. Nothing below it
calls os, because two client components import the service registry and
os does not bundle for the browser.scripts/manager.cjs is
CommonJS and has to read the same file the app does. That retired a
hand-copied second service list which had already drifted.This is a personal tool that has been running daily on two machines. It is shared because the host-profile approach is reusable, not because it is a product. Concretely:
package.json claims ISC but there is no LICENSE file,
so default copyright currently applies. Treat it as source-available until
that is settled.local-chatterbox returns HTTP 500 for its own built-in voice, so the
text-to-speech pane fails on the default engine. Kokoro synthesizes fine at
the same endpoint, so this is the engine, not the route.main.scripts/doctor.mjs video fails and the Video tab
refuses to submit. Left declared rather than deleted: the machine really
does have the weights, and hiding that would misreport what is here.runtimes block is
written but carries no verifiedAt, because the doctor has not been run on
that machine. The console says "never checked" rather than guessing either
way.Fixed in this repository, found the same way:
crypto.randomUUID() exists
only in a secure context — HTTPS or localhost — and a LAN address is
neither. Generate died before sending anything and the UI said nothing. A
headless box you open from a laptop is exactly the normal setup; this only
shows up if you drive it over the network./api/metrics hardcoded the vllm service id and returned 502 on every
page load on any host without one.They were not taken by hand. A walk script drives both real consoles in a real browser, performs the interactions, and captures what happened:
node scripts/walk-console.mjs # every reachable host
node scripts/walk-console.mjs --host betenshi
node scripts/walk-console.mjs --host b5 --features chat,models
Two hosts, because one host cannot show this product. The same walk runs against both and the captures sit side by side. B5 now backs text, vision, embeddings, speech, image, and video with its existing local installations; the captured September 19 walk is a historical snapshot from before that adapter was added and should not be read as the current capability list.
It uses the capture layer from Walkthrough Studio, which enforces — by measuring the output, not by trusting the flag it passed — that every capture is Retina, that the viewport is really the size it asked for, that animations are frozen so frames are deterministic, that no capture is a near-empty frame, and that no two captures in a walk are too similar to be distinct states.
That last one earned its keep twice. The chat walk originally captured a third state — the prompt typed but not sent — and the layer rejected it: 0.9% of the frame changed. The bytes differed, so a checksum would have passed it, but a reader would have seen the same screen twice. It later rejected a placeholder capture taken after image generation silently failed, which is how that failure was noticed at all.
What it found that reading the source did not: the secure-context crash that
breaks Image Studio for anyone opening the console from another machine, a
/api/metrics route that 502'd on every page load on one of the two hosts, a
Start button offered for a service that host does not have, and a TTS engine
returning 500 for its own default voice.
What it could not do here. The capture layer has no file-upload primitive,
so speech-to-text — which works by dropping an audio file — could not be driven;
the Speech Lab capture shows the surface with Whisper running, and the
transcription itself is not claimed. The hub half of the skill is also unused:
it wants to own catalog and run files inside a Walkthrough Studio checkout, and
this project is not registered in one, so the run record lives in
docs/walkthrough/ instead.
Captured 2026-09-19 across three runs — see
docs/walkthrough/2026-09-19-notes.md
for what each produced, which services were started to make the walk possible,
and the verification that every one of them was put back.
The two machines in these images:
| BeTenshi | B5 | |
|---|---|---|
| Hardware | Windows, RTX 5090 (31.8 GB) | MacBook Pro, Apple M5 Max (128 GB) |
| Memory model | discrete — two budgets | unified — one pool |
| Services declared | 15 | 2 |
| Up during the walk | 10 | 2 |
Nothing was started specially for the photographs except the services named in the notes, and those were stopped again.
Workstreams now live in a persistent sidebar: Audio, Visual, Chat & Code, and Decisions & Processes. Existing hash routes and tools remain available. Audio includes Effects (#lab-audio), Voices & Speech (#voices), Speech Recognition (#speech-recognition) and the existing Music Lab (#lab-music). The legacy #speech link still opens the shared speech workspace.
Effects uses bounded, portable layer recipes and CPU-only browser rendering. The tank pilot includes licensed CC0 stems for main-gun, impact, destruction and engine sounds. Source provenance is in public/audio-lab/pilot/project.json; the preparation script takes an external sound-stack root and the RTS AI web checkout. Imports, rendered WAVs, sidecars and reviews live in gitignored var/audio-lab/ (override with AUDIO_LAB_DIR). Each render has a new ID; writes publish metadata atomically. Reviews do not replace game files. The existing sound bake-off stays accessible from the Library.
The tank crew director calls only host-declared local neural speech services, keeps the selected voice explicit, and saves raw and processed takes separately. General speech synthesis, engine-managed cloning, microphone/file recognition, dictation and Music Lab retain their existing implementations.
For a local Faction Viewer audition, run npm run preview:viewer in the sibling RTS AI web checkout. Its default loopback port is 3463 (RTSAI_VIEWER_PORT overrides it). Set that address in Audio Lab, render or load a revision, then choose Audition in Faction Viewer. The bridge verifies the actor, faction, revision and local asset URL; the Viewer can switch between candidate and current game audio or clear the override. Engine movement playback remains a manual audition, not a native game binding. Native installation and release approval remain separate steps.
Hosted Hangar preserves the public setup-guide/private-console split. New disk writes, neural pilot generation and the Viewer bridge require the local runtime. User recordings and generated revisions are not deployed. The walkthrough builder (scripts/make-audio-walkthrough.py) uses local Kokoro am_michael and verified screenshots; its output stays gitignored under public/walkthroughs/.