Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
29a5ad9
feat(t27f): petal_glass -- trios glassmorphism, 27-block assembly and…
gHashTag Oct 19, 2026
f5360df
feat(t27os): native macOS glass window in t27, built by t27b (Refs #1…
gHashTag Oct 19, 2026
4e9f53d
feat(t27os): wide card grid -- the widget panel spans 880 x 440 of th…
gHashTag Oct 19, 2026
c8807a4
feat(t27os): top bar, trios sidebar mode, Dock icon drawn in t27
gHashTag Oct 19, 2026
f2ce0f6
feat(t27os): glass window behaves as trios -- title bar, crossfaded c…
gHashTag Oct 19, 2026
801400d
fix(t27os): title logo in petal_menu style at 14 pt, 4x frames, home …
gHashTag Oct 19, 2026
1fa6065
feat(t27os): fold into the trios compact panel (Refs #10782)
gHashTag Oct 19, 2026
5cb1047
feat(t27os): strict black full screen, point-for-point and hairline (…
gHashTag Oct 19, 2026
b8474e5
feat(t27os): brand-mark logo and a single TRINITY label (Refs #10782)
gHashTag Oct 19, 2026
1df0655
fix(t27os): logo without a drawn outline, the mark carries its own ga…
gHashTag Oct 19, 2026
182a6e0
feat(t27os): one title bar, honeycomb cards, two logo sizes
gHashTag Oct 19, 2026
b634159
feat(t27os): full-screen frost in black and white, layout helper spec
gHashTag Oct 19, 2026
e4dd890
fix(t27os): background is pure black #000000 in every mode
gHashTag Oct 19, 2026
ad0c842
fix(t27os): black frosted glass -- translucent #000000 in a window, o…
gHashTag Oct 19, 2026
686fc29
feat(t27os): agents in the honeycomb -- marks, names, session states,…
gHashTag Oct 19, 2026
d7ac85a
feat(t27os): centred title brand 1.2x, cell tap opens a task, no New …
gHashTag Oct 19, 2026
9c3e4e0
feat(t27os): hot reload -- the window swaps itself when ld writes a n…
gHashTag Oct 19, 2026
49b239d
feat(t27os): sidebar groups QUEEN / SPACES / AGENTS after herdr (Refs…
gHashTag Oct 19, 2026
c098030
feat(t27os): header and window title read TRINITY S3AI with a supersc…
gHashTag Oct 19, 2026
9c0fdbd
fix(t27os): every comb cell gets the same hairline outline (Refs #10782)
gHashTag Oct 19, 2026
6934b0f
feat(t27os): + opens a new chat, several sessions per agent, x closes…
gHashTag Oct 19, 2026
46f2d6c
feat(t27os): Queen panel -- context, event ring, confirm-gated sugges…
gHashTag Oct 19, 2026
872a700
feat(t27os): green edge glow is the GOD MODE signal, black edges othe…
gHashTag Oct 19, 2026
294c033
feat(t27os): SPACES "+" and row close, I LOVE OPEN SOURCE foot link
gHashTag Oct 19, 2026
cf25ede
feat(t27os): GOD MODE edge glow as a blue gas flame
gHashTag Oct 19, 2026
108162f
feat(t27os): repo SPACES + DOCTOR panel (Refs #10782)
gHashTag Oct 19, 2026
895b32a
feat(t27os): Claude Code terminal pane in the window (offline until p…
gHashTag Oct 19, 2026
1233daf
feat(t27os): sidebar foot reads OPEN SOURCE T27 (Refs #10782)
gHashTag Oct 19, 2026
3c5bc55
feat(t27os): pane_state, session_store, pty_proto and vt specs for th…
gHashTag Oct 19, 2026
702adef
feat(t27os): PTY server, client, sys imports and logic; panes survive…
gHashTag Oct 19, 2026
d6a1bf7
feat(t27os): Claude Code pane talks to the PTY server via pty_client …
gHashTag Oct 19, 2026
4dc650f
fix(t27os): keep the developer home path out of a session_store test …
gHashTag Oct 19, 2026
baf0e41
fix(t27os): keep the developer home path out of a pty_proto test (Ref…
gHashTag Oct 19, 2026
35e66dd
feat(t27os): terminal pane follows the window size (RESIZE via __ioct…
gHashTag Oct 19, 2026
163af1b
feat(t27os): frame gate, settled resize, min window size
gHashTag Oct 19, 2026
142d21b
feat(t27os): keep window state, pane models and the PTY server across…
gHashTag Oct 19, 2026
ff6133d
ledger(t27b): keep the t27os rows in path order after the rebase onto…
gHashTag Oct 19, 2026
4c451b9
feat(t27os): cache laid-out text lines and ellipsize long sidebar lab…
gHashTag Oct 19, 2026
5f9b003
feat(t27os): key manager, KEYS tab, per-pane profile, open-in-editor,…
gHashTag Oct 19, 2026
fad2058
ci: drop the extern t27os rows the reference blocks (Refs #10782)
gHashTag Oct 19, 2026
0099b52
ci: bless the duplicate-body ledger for the base's widget and petal_g…
gHashTag Oct 19, 2026
7016a31
ci: t27b ledger cap equals the non-pass rows (Refs #10782)
gHashTag Oct 19, 2026
e315fa3
ci: t27b ledger names the base's new widget specs, petal_glass and dr…
gHashTag Oct 19, 2026
de05151
t27os keys: Free Claude Code parity -- tier map, openrouter and ollam…
gHashTag Oct 20, 2026
b0d5f7b
T27 OS: matte black glass window, solid black in full screen, 27 edit…
gHashTag Oct 20, 2026
94c8018
T27 OS: the editor opens inside the window -- a T27 OS chip, the edit…
gHashTag Oct 20, 2026
3ad80b6
Merge master into claude/t27os-keys-v3: the T27 OS stack onto the mer…
claude Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"reference-bug": "the reference passes it for the wrong reason; t27b is right not to",
"n/a": "outside what t27b is for (say why in the PR that sets it)"
},
"max_not_pass": 132,
"max_not_pass": 133,
"entries": [
{"path": "specs/a/b_c.t27", "t27b": "pass"},
{"path": "specs/a_b/c.t27", "t27b": "pass"},
Expand Down Expand Up @@ -1561,6 +1561,7 @@
{"path": "specs/t27f/particle_engine.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"},
{"path": "specs/t27f/particle_field.t27", "t27b": "pass"},
{"path": "specs/t27f/pbr_sphere.t27", "t27b": "pass"},
{"path": "specs/t27f/petal_glass.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"},
{"path": "specs/t27f/petal_menu.t27", "t27b": "pass"},
{"path": "specs/t27f/quest.t27", "t27b": "pass"},
{"path": "specs/t27f/reel.t27", "t27b": "pass"},
Expand Down Expand Up @@ -1615,10 +1616,28 @@
{"path": "specs/t27mcp/telegram.t27", "t27b": "pass"},
{"path": "specs/t27mcp/tool_schema.t27", "t27b": "codegen", "blocker": "t27b: unsupported construct FnDecl(frame or branch range) at line 0 (fn the round trip fixtures: a tuple, arrays, a boolean, nested objects: conditional branch out of range (function too large))", "reason": "unimplemented"},
{"path": "specs/t27mcp/visual_output.t27", "t27b": "pass"},
{"path": "specs/t27os/agents.t27", "t27b": "pass"},
{"path": "specs/t27os/doctor.t27", "t27b": "pass"},
{"path": "specs/t27os/editors.t27", "t27b": "pass"},
{"path": "specs/t27os/frame_gate.t27", "t27b": "pass"},
{"path": "specs/t27os/hot_reload.t27", "t27b": "pass"},
{"path": "specs/t27os/install.t27", "t27b": "pass"},
{"path": "specs/t27os/keys.t27", "t27b": "pass"},
{"path": "specs/t27os/layout.t27", "t27b": "pass"},
{"path": "specs/t27os/pane_state.t27", "t27b": "pass"},
{"path": "specs/t27os/pty_client.t27", "t27b": "blocked", "blocker": "@extern import under the JIT", "reason": "n/a"},
{"path": "specs/t27os/pty_logic.t27", "t27b": "pass"},
{"path": "specs/t27os/pty_proto.t27", "t27b": "pass"},
{"path": "specs/t27os/pty_server.t27", "t27b": "blocked", "blocker": "@extern import under the JIT", "reason": "n/a"},
{"path": "specs/t27os/pty_sys.t27", "t27b": "blocked", "blocker": "@extern import under the JIT", "reason": "n/a"},
{"path": "specs/t27os/queen.t27", "t27b": "pass"},
{"path": "specs/t27os/router.t27", "t27b": "pass"},
{"path": "specs/t27os/session_store.t27", "t27b": "pass"},
{"path": "specs/t27os/term_pane.t27", "t27b": "pass"},
{"path": "specs/t27os/text_cache.t27", "t27b": "pass"},
{"path": "specs/t27os/vt.t27", "t27b": "pass"},
{"path": "specs/t27os/window_keep.t27", "t27b": "pass"},
{"path": "specs/t27os/windows.t27", "t27b": "pass"},
{"path": "specs/t27t/t27t.t27", "t27b": "pass"},
{"path": "specs/ternary/activation_quantizer.t27", "t27b": "pass"},
{"path": "specs/ternary/bitnet_layer.t27", "t27b": "pass"},
Expand Down
116 changes: 116 additions & 0 deletions docs/t27os/keys-orchestration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# T27 OS key orchestration, compared with Free Claude Code

Issue: gHashTag/t27#11224. Code: `specs/t27os/keys.t27` (decisions and data), the host side in
`keys_host.t27`, `pty_server.t27` and `glass_window.t27`. Stage 2 (a local router) is
`specs/t27os/router.t27`, decisions only. No key value appears in this file or in any spec; every test uses
fake values.

## Why

A pane that starts Claude Code inherited whatever provider the parent shell pointed at, and that provider
answered `429 Insufficient balance` or `Your organization has disabled Claude subscription access`. The pane
was dead until a person edited the environment by hand. The owner asked (2026-10-20) for key orchestration
like the tool Free Claude Code.

## The prior art, in our words

Free Claude Code (https://github.com/Alishahryar1/free-claude-code, page read 2026-10-20) is a local proxy
that makes Claude Code talk to many other providers. Its configuration, as the page describes it:

- one default model, plus an optional override for each tier (opus, sonnet, haiku, and a fourth tier); a tier
left empty falls back to the default;
- a model is written as `provider/model`;
- each provider has a list of numbered keys that can be reordered; key 1 is used first, and a fallback entry
may name a key number;
- an ordered fallback list: when a request still fails after its retries, the next entry is tried, so one
request may spend the quota of more than one provider;
- an optional bearer token that protects the local proxy;
- an admin page where keys, models and fallbacks are set.

## The constraint that shapes our version

T27 OS cannot run a local HTTP proxy yet: t27b has no listening INET socket and no streamed HTTPS client
(issue #10902), so `router.t27` stops at decisions. But several providers serve the Anthropic protocol
themselves. Claude Code can talk to them directly with `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` and the
tier model variables. So the orchestration is done per pane, not per request:

1. At pane spawn, choose a profile (a provider, its base URL, its key variable, its tier models) and set that
environment, and nothing else (`pick_profile`, `profile_var_*`, `claude_strips`).
2. When the pane shows a provider error, mark that provider bad for this pane and respawn the pane, which
resumes its Claude Code session, on the next profile or the next key (`classify_line`, `fo_action`).

No proxy is involved.

## Comparison

| Free Claude Code idea | In T27 OS | Spec function | What stays blocked |
|---|---|---|---|
| One default model | Yes, per profile | `MODELS_DEFAULT`, `tier_model(pr, TI_DEFAULT)`, sets `ANTHROPIC_MODEL` | Nothing |
| Per-tier overrides (opus, sonnet, haiku) | Yes, as data, one column per tier; an empty field falls back to the default; the small-fast variable follows haiku | `MODELS_OPUS/SONNET/HAIKU`, `tier_model`, `profile_var_name/text` | A fourth tier (the one the OpenRouter and Kimi pages call fable) is not mapped; changing a tier is a data edit, not a screen |
| `provider/model` syntax | A profile is a provider; its models sit in the tier table | `PROFILE_NAMES`, `PROFILE_PROVIDER` | The combined one-line syntax; it exists in Free Claude Code because its proxy picks the upstream per request |
| Many providers | Four profiles today: z.ai, Anthropic direct, OpenRouter, Ollama cloud, over the nine key rows | `PROFILE_PROVIDER`, `PROFILE_BASE_URLS`, `PROFILE_KEY_VARS` | A provider that does not speak the Anthropic protocol needs the local router and `specs/provider/transform.t27` (#10902). DeepSeek, Moonshot and MiniMax speak it but need key rows of their own (see below) |
| Numbered keys, key 1 first | Naming and choice are decided: key 1 keeps the existing Keychain item, keys 2 and 3 add `#2`, `#3`; least-recently-used rotation with a rest time | `keychain_label_n`, `MAX_KEYS`, `rotate_pick`, `rotate_pick_except` | The KEYS tab still shows one row per provider and the host reads only key 1; storing and listing keys 2 and 3 is host work |
| A fallback entry names a key number | Decided: a throttled key (429) with a spare key swaps the key and keeps the profile | `fo_action` = `FA_RESPAWN_KEY`, `fo_target` | The host does not pass the key number to the respawn yet |
| Ordered fallback list after retries | Yes, per pane: the profile list is walked forward only | `next_profile`, `fo_action` = `FA_RESPAWN_NEXT`, `MAX_FAILOVERS` | Per request, inside one conversation turn: needs the local router. A respawn does not replay the request that failed; the person continues in the resumed session |
| Retries before the fallback | Not ours: Claude Code retries by itself, and the failover starts when the error text reaches the screen | `classify_line` | Nothing to build |
| Reasoning effort per route | No | none | Out of scope; not set by any profile |
| Bearer token for the local proxy | Not needed, there is no proxy | none | Needed with the router (it binds 127.0.0.1, `ROUTER_BIND`) |
| Admin page for keys | Yes: the KEYS tab, Keychain storage, masked display, a check button, a circuit breaker | `ky_*`, `mask_key`, `status_after_probe`, `circuit_fails`, `bad_mask` | The signed-in account keys on app.t27.ai cannot be spent from a pane: no proxy route exists server-side (see the MAP block in `keys.t27`) |
| Local models (LM Studio, llama.cpp, Ollama) | Ollama cloud only | profile `ollama-cloud` | Local Ollama needs no key, so the stored-keys mask cannot say it is available; a host fact (is the server up) must join the mask first |
| OAuth sign-in for some providers | No | none | Out of scope |

## The pane failover, step by step

All functions are pure; the host reads the screen and respawns.

1. `classify_line(line)` sorts one screen line into a class by plain byte matching: `disabled Claude
subscription` is subscription disabled; `Insufficient balance` is out of balance (it wins over 429,
because a provider sends both for an empty account); a standalone `429` on a line that also says
"error" is rate limited; a standalone `401` or `403` there is login rejected; an `API Error` line that
matches nothing is other; anything else is no error. A number glued to other digits does not count, and
neither does "Edited 429 lines".
2. `fo_action(class, pr, stored, bad, done, alt)` answers keep, respawn on the next profile, respawn on the
next key of the same profile, or give up. `pr` is the profile in use, or none for a pane that started
without a key (where the "disabled Claude subscription access" line comes from). `alt` says another key of
the provider is usable (`rotate_pick_except`, the failed key skipped).
3. `fo_bad(class, pr, bad)` is the new bad mask for this pane; `fo_target(...)` is the profile to respawn on.
4. `fo_why(...)` writes the line for the KEYS tab: the profile, the failure and the move, never a key value.

It cannot loop. The chain is walked forward only (`next_profile` returns a profile after the one that failed),
so each profile is tried at most once per pane. A key swap is counted in `done`, and `MAX_FAILOVERS` ends the
count even if the host always reports a spare key. After every profile has failed once, the answer is give up
and the pane stays where it is. A line that is no recognised failure keeps the pane as it is.

## Providers read on 2026-10-20

Only providers whose own pages document an Anthropic-protocol base URL are used. Model names are copied from
the pages, not guessed.

| Provider | Base URL | Key variable | Tier models shown | Page | In T27 OS |
|---|---|---|---|---|---|
| z.ai | `https://api.z.ai/api/anthropic` | `ANTHROPIC_AUTH_TOKEN` | A flash model for every tier is what the stored key answers (owner, 2026-10-19); the page lists larger ones for opus and sonnet | https://docs.z.ai/devpack/tool/claude | Profile `zai`, first in the chain |
| Anthropic direct | none (Claude Code's own) | `ANTHROPIC_API_KEY` | none set | Claude Code itself | Profile `anthropic-api`, second |
| OpenRouter | `https://openrouter.ai/api` | `ANTHROPIC_AUTH_TOKEN`, with `ANTHROPIC_API_KEY` set empty | opus, sonnet, haiku: the `~anthropic/claude-*-latest` aliases (`[1m]` on opus and sonnet) | https://openrouter.ai/docs/cookbook/coding-agents/claude-code-integration.md | Profile `openrouter`, third. The page warns that Claude Code is only guaranteed with Anthropic models there |
| Ollama cloud | `https://ollama.com` | `ANTHROPIC_AUTH_TOKEN`, with `ANTHROPIC_API_KEY` set empty | one cloud model, named on the page, used for every tier | https://docs.ollama.com/integrations/claude-code | Profile `ollama-cloud`, fourth |
| DeepSeek | `https://api.deepseek.com/anthropic` | `ANTHROPIC_AUTH_TOKEN` | one flash model for the default, opus and sonnet (with `[1m]`), the same without it for haiku and subagents | https://api-docs.deepseek.com/guides/coding_agents | Documented, no profile yet |
| Moonshot (Kimi) | `https://api.moonshot.ai/anthropic` | `ANTHROPIC_AUTH_TOKEN` | one model for default, opus and sonnet (with `[1m]`), a code model for haiku | https://platform.kimi.ai/docs/guide/claude-code-kimi | Documented, no profile yet |
| MiniMax | `https://api.minimax.io/anthropic` | `ANTHROPIC_AUTH_TOKEN` | one flash model for every tier (with `[1m]`) | https://platform.minimax.io/docs/token-plan/claude-code.md | Documented, no profile yet |
| Ollama, local | `http://localhost:11434` | token set to a fixed word, no real key | a local model, named on the page | https://docs.ollama.com/integrations/claude-code | Left out, see the comparison |

The three "no profile yet" rows need a provider row of their own in `keys.t27` (nine rows today). The KEYS
window sizes its arrays by nine and the PTY server sizes its key table by nine, so the new rows are a
change in those files too; the profile data is then one more field in each table in `keys.t27`.

## Defaults chosen, and what is left

- Order: z.ai first, as before; the others follow in the order of the table. A profile is used only once its
provider's key is stored and has not failed its check. Nothing is enabled by this change on a Mac with no
extra key stored.
- A Mac that already holds an OpenRouter or Ollama key (stored for another agent) now also gives Claude panes
that profile after z.ai and the Anthropic API key.
- Host work, not decisions: call `classify_line` on the pane's error row only, never on the transcript body,
because a diff that shows these phrases would otherwise trigger a failover; keep the per-pane `bad` mask and
`done` count; pass the key number to the respawn; the window's start-up check loop covers the first two
profiles only and should run to `PROFILE_COUNT`.
- Blocked: the local router (per-request fallback, translation for non-Anthropic providers, a bearer token for
the proxy) until t27b has a listening socket and a streamed HTTPS client (#10902).
20 changes: 10 additions & 10 deletions specs/t27os/doctor.t27
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub const ST_STASH : str = "# stash ";
/// What the DOCTOR knows about a repository, one bit each.
pub const F_CHECKED : u32 = 1;
pub const F_WORKTREE : u32 = 2;
pub const F_AGENT : u32 = 4;
pub const DOC_F_AGENT : u32 = 4;
pub const F_DUP : u32 = 8;
pub const F_REPEATED : u32 = 16;
pub const F_HEAVY : u32 = 32;
Expand Down Expand Up @@ -267,7 +267,7 @@ pub fn keep_reason(flags: u32) -> u32 {
/// copy, or one of a folder made several times. A space bound in the sidebar is in use, not a leftover.
pub fn leftover(flags: u32) -> bool {
if (has(flags, F_BOUND)) { return false; }
return has(flags, F_DUP) or has(flags, F_AGENT) or has(flags, F_REPEATED);
return has(flags, F_DUP) or has(flags, DOC_F_AGENT) or has(flags, F_REPEATED);
}

/// Whether a row offers the remove button: a leftover that nothing keeps.
Expand All @@ -287,8 +287,8 @@ pub fn row_group(flags: u32) -> u32 {
/// worktree, else the first; every other clone is a duplicate. `keeper_rank` orders the candidates (lower wins,
/// ties go to the first discovered).
pub fn keeper_rank(flags: u32) -> u32 {
if (has(flags, F_AGENT) == false and has(flags, F_WORKTREE) == false) { return 0; }
if (has(flags, F_AGENT) == false) { return 1; }
if (has(flags, DOC_F_AGENT) == false and has(flags, F_WORKTREE) == false) { return 0; }
if (has(flags, DOC_F_AGENT) == false) { return 1; }
return 2;
}

Expand All @@ -302,7 +302,7 @@ pub fn dup_candidate(flags: u32) -> bool {
/// hidden ones never.
pub fn bind_rank(flags: u32) -> u32 {
if (has(flags, F_HIDDEN)) { return RANK_NEVER; }
if (has(flags, F_AGENT) or has(flags, F_WORKTREE)) { return RANK_PLUS; }
if (has(flags, DOC_F_AGENT) or has(flags, F_WORKTREE)) { return RANK_PLUS; }
return RANK_AUTO;
}

Expand Down Expand Up @@ -357,7 +357,7 @@ pub fn doc_scroll(off: i64, total: u32, rows: u32) -> u32 {
}

test a_leftover_with_work_in_it_is_never_offered {
const clean_agent : u32 = F_CHECKED + F_PUSH_CHECKED + F_AGENT;
const clean_agent : u32 = F_CHECKED + F_PUSH_CHECKED + DOC_F_AGENT;
assert removable(clean_agent);
assert removable(clean_agent + F_DIRTY) == false;
assert keep_reason(clean_agent + F_DIRTY) == KEEP_DIRTY;
Expand All @@ -368,8 +368,8 @@ test a_leftover_with_work_in_it_is_never_offered {
assert removable(clean_agent + F_NESTS) == false;
assert removable(clean_agent + F_NO_REMOTE) == false;
// not checked yet, or only half checked: kept
assert keep_reason(F_AGENT) == KEEP_UNCHECKED;
assert removable(F_CHECKED + F_AGENT) == false;
assert keep_reason(DOC_F_AGENT) == KEEP_UNCHECKED;
assert removable(F_CHECKED + DOC_F_AGENT) == false;
// a clean main clone with nothing left over is not a suggestion
assert removable(F_CHECKED + F_PUSH_CHECKED) == false;
// a space in use is not a leftover
Expand All @@ -386,10 +386,10 @@ test rows_are_grouped_removable_first {

test the_keeper_is_a_main_clone {
assert keeper_rank(0) < keeper_rank(F_WORKTREE);
assert keeper_rank(F_WORKTREE) < keeper_rank(F_AGENT);
assert keeper_rank(F_WORKTREE) < keeper_rank(DOC_F_AGENT);
assert dup_candidate(F_WORKTREE) == false;
assert dup_candidate(F_NO_REMOTE) == false;
assert dup_candidate(F_AGENT);
assert dup_candidate(DOC_F_AGENT);
}

test two_clicks_remove_one_row {
Expand Down
Loading
Loading