The garage and reception
Where an idea enters, how it gets an immutable name, and what it costs — sketched, not built.
The flow
flowchart LR
U([a person has an idea]) -->|one append| G[floor-garage<br/>garage.jsonl]
G -->|pulled| R[floor-reception<br/>its own mailroom]
R -->|routed| F[floor-<role><br/>floor-mailroom.jsonl]
F -.->|run observed, later| L[floor-garage<br/>ledger.jsonl<br/><i>not built</i>]
L -.->|derived| G
Four logs. All append-only. All joined by one key.
One key: the prompt is its own name
id = sha256(prompt)[0:16]
The id is the content. That gets three properties for free rather than by discipline:
floor-garage — the only new shape
floor-garage/garage.jsonl, one row per idea. Minimal on purpose: entering an idea should
cost one append and no decisions.
{"id":"3205b4b7e770ea84","ts":"2026-09-01T21:03:11.402Z","kind":"request","state":"parked","prompt":"Adding an idea should cost one command...","prompt_tokens_est":36,"agent_tokens":null}
| Field | Grade | What |
|---|---|---|
id |
measured | sha256(prompt)[0:16] |
ts |
measured | when it was parked |
kind |
declared | idea, issue, or request |
state |
declared | parked until reception pulls it |
prompt |
— | the text, verbatim |
prompt_tokens_est |
inferred | ceil(chars / 4). A heuristic, named as one |
agent_tokens |
— | null, never 0 |
floor-reception — no new shape at all
Reception needs no new file. Its mailroom already is the routing log, and the event vocabulary already covers it:
| Event | Means |
|---|---|
delivered |
an idea arrived from the garage |
dispatched |
it was routed to a role floor |
The body line carries the id and the destination. This is the cheapest correct answer:
reception is a queue, a queue is an append-only log, and every floor already has one.
What it costs — the part you left open
You cannot know an idea's agent cost while writing the prompt. You can know it afterwards, and the mechanism already exists.
Claude Code writes one JSONL transcript per session under
~/.claude/projects/<slug>/<session-id>.jsonl. Every assistant row carries
message.usage. Measured 2026-09-01: 249 of 249 assistant rows in one session, with
input_tokens, output_tokens, cache_read_input_tokens,
cache_creation_input_tokens and server_tool_use.
So cost is not a field somebody fills in. It is a third log, harvested:
{"id":"3205b4b7e770ea84","ts":"...","floor":"floor-data","session":"df316340","input_tokens":18402,"output_tokens":2911,"cache_read_input_tokens":911204,"tool_calls":37}
An idea's cost then becomes derived — the mean or the worst case over its ledger rows — and it improves as runs accumulate. Zero rows means unknown, which is honest and is the same rule the rest of this repo follows: never write a number you did not measure.
"JSONL, or something more performant?"
Keep JSONL. The performance tier already exists and is a different layer.
This repo's standing rule is that the JSONL is the record and every database is a
rebuildable cache of it. src/load/pglite.ts and src/load/ioredis.ts already do that
for the manifest and the mailrooms; the garage joins them as two more tables and one more
key prefix. If reading the garage ever becomes slow, that is an argument for loading it,
not for changing the file.
A content-addressed id makes both loaders trivial: primary key in Postgres, hash field in Redis, no surrogate key and no sequence.
What did not change
Adding two floors required no code change at all. npm run gen:mailroom gave both a
mailroom, npm run gen:manifest picked them up, and every gate passed. The 20 role floors,
the generators, the loaders, the Ink view and the hooks are untouched.
That is the abstraction working: a floor is a directory with a mailroom, and the manifest is a cache of whatever directories exist.
The one place it strains
The manifest now says this, and it is wrong in a way worth keeping visible:
{"slug":"floor-garage","role":"garage","file_count":1,"populated":true,"mailroom_rows":1}
garage is not a role, and populated: true here means "holds a backlog", not "carries
plugin content" — so the 11-populated / 9-shell split that the split exists to express is
now diluted. The fix is a kind column on ManifestRow, role or infrastructure.
It is filed in the garage rather than applied, because it changes src/types.ts, the
generated zod, both loaders, the Ink view and the tests — which is more than a sketch should
spend.
The backlog is in the garage
Six rows, and they are the open questions about the garage itself:
| Kind | In one line |
|---|---|
| issue | harvest agent cost from transcripts into ledger.jsonl |
| request | ManifestRow needs a kind column |
| request | garage.jsonl has no validator and nothing grades it |
| request | npm run garage -- add "<prompt>" — one command, no hand-written JSON |
| request | reception should record why an idea was routed |
| issue | nothing expires an idea |
python3 -c "import json;[print(r['kind'], r['id'], r['prompt'][:60]) for r in map(json.loads, open('floor-garage/garage.jsonl'))]"