Throwaway probe for check 40: what a mod can do with no screen.

<!-- README.md -->
Direct AI agents building your product, in plain words, with checks they can't skip.
Wheelhouse is for a product owner who isn't a developer. You say what you want in plain words. A front desk asks you short questions about what is yours to decide, each with buttons to click. Behind it, agents build the work and other agents review it, and nothing reaches the main version of your product until it has passed checks the agents can't skip. You make the product decisions; the agents do the building.
v0.1.1 is an early release for trying Wheelhouse out. It hasn't yet been used from start to finish on a real product.
A card looks like this (an illustration):
Approve · Add the booking page to your product? It lets customers pick a time from your calendar. The checks passed and a reviewer has looked. [ Approve ] [ Change it ]
And a briefing opens like this:
Since you were last here at 09:10: the booking page is ready for you to look at. One thing needs you. Nothing is broken.
What it costs. Wheelhouse is free and open source (Apache 2.0). The agents run on your own Claude plan, so their work comes out of your plan's usage, and Wheelhouse's board shows this week's usage, including the share of your weekly allowance.
What it protects. Agents never hold your GitHub or GitLab keys, your clicks are signed so an agent can't approve for you, and on a Mac the agents' commands run fenced off from the rest of your computer. What it doesn't protect you from is in SECURITY.md.
How to take it out again. Type /wheelhouse:uninstall in a conversation on your product. What it removes, and what it keeps, is in docs/data.md.
You need Node.js (a free program; download the version marked LTS), Claude Desktop, signed in, and about 15 minutes. A GitHub or GitLab account helps; setup shows you where to make one if you have neither.
Projects in your home folder, named after the product; open the session on it. For a product you already have, open the session on its folder. For a product you set up with Wheelhouse on another computer, make an empty folder here too: setup brings the product into it. Please run these two commands in this folder, one after the other, and tell me when both have finished:
claude plugin marketplace add "where-is-brett/wheelhouse#v0.1.1" --scope project
claude plugin install wheelhouse@wheelhouse --scope project
Claude asks before it runs each one: allow it. The first adds Wheelhouse's own catalogue, at release v0.1.1; the second installs Wheelhouse from that catalogue, for this project only. Both name Wheelhouse's catalogue, so nothing else with a similar name can be installed by mistake. (A later release, from Wheelhouse's releases page, goes in place of v0.1.1.) If you prefer a terminal, the same two commands work there, run in the product's folder.
where-is-brett/wheelhouse, choose Wheelhouse from it, choose this project when asked where, then start a new session on the folder.Setup goes one step at a time. Each step shows you a card listing exactly what it will change, and nothing changes until you click Go ahead. You can stop at any card and carry on later: setup picks up where it left off. To take Wheelhouse out of a project again, type /wheelhouse:uninstall in a session on it.
Where your conversations, transcripts and records go, and what uninstall removes and keeps, is in docs/data.md.
What Wheelhouse protects you from, what it doesn't, and how to report a problem are in SECURITY.md. Setup switches these protections on by itself; on a Mac you can also choose Make it stricter when it asks how to connect to GitHub or GitLab.
Wheelhouse also runs in VS Code with GitHub Copilot, with nothing else installed. This is its lean tier ("Lean"): you need VS Code with Copilot, and git. You don't need Node.js, a code host, administrator rights or anything else, and Wheelhouse installs nothing. Everything stays on your computer, and Copilot builds in your own chat, one feature at a time.
Status: v0.1.1 is an early release for trying Wheelhouse out. Where this page says v0.1.1, use the latest release's name.
Open your product's folder in VS Code, open Copilot's chat in agent mode, and paste this line:
Set up Wheelhouse from https://github.com/where-is-brett/wheelhouse at release v0.1.1
Copilot asks you before it runs each command, as it always does. What the line does, one step at a time:
~/.copilot/wheelhouse/<tag>/, copies Wheelhouse's skills, agents and instructions into ~/.copilot/, and checks every copied file against the fingerprint the release lists for it. A file that doesn't match stops setup..vscode/settings.json (your own file is only changed after you click Approve), makes the record folder .wheelhouse/record/ and the board wheelhouse/board.md, and makes one commit of the files it added.Then the chat follows Wheelhouse's own playbook for Lean: the record, the board, cards, building, merging, undo and the copy, all with git. The board's first lines say plainly what is and isn't enforced on your computer. At Lean nothing enforces STOP, an approval is a click in the chat that the chat records itself, and the VS Code settings are conveniences, not controls. Lean is not for projects with real users.
For the chat reading this: check git --version first; then git clone --depth 1 --branch <tag> https://github.com/where-is-brett/wheelhouse.git "$HOME/.copilot/wheelhouse/<tag>", and follow adapters/copilot/skills/setup-lean/SKILL.md in that clone, step by step.
Status: v0.1.1 is an early release for trying Wheelhouse out, with its tests passing. The design is in docs/specs/2026-10-17-wheelhouse-design.md, and where the work stands is in HANDOVER.md.
hooks/register.ts 50 lines1// probes/40-mod-headless/hooks/register.ts
2// Throwaway probe mod for check 40: what a mod can do with no screen. It registers a command
3// `digest` answering { text: 'probe digest' }; at the session's start it starts
4// $.clock.every(5000, ...), logging each fire and, on the third, submitting
5// 'probe: timer prompt'; and it imports part.ts, which holds the prompt.submit hook. Two
6// ui.render hooks that only pass on are here so the validator shows how it spells their
7// matchers. Everything is logged to probe-40-mod.jsonl in the session's working folder.
8import type { Register, EngineInterface } from 'claude-code'
9import { registerPart } from './part'
10
11const LOG = 'probe-40-mod.jsonl'
12let queue: Promise<void> = Promise.resolve()
13
14function append($: EngineInterface, record: object): Promise<void> {
15 queue = queue.then(async () => {
16 let before = ''
17 try { before = await $.fs.read(LOG) } catch { before = '' }
18 await $.fs.write(LOG, before + JSON.stringify({ at: new Date().toISOString(), ...record }) + '\n')
19 }).catch(() => {})
20 return queue
21}
22
23export const register: Register = on => {
24 registerPart(on)
25
26 on('session.start', async ($, e, next) => {
27 await append($, { event: 'session.start' })
28 await $.command.register({ name: 'digest', description: 'Probe 40: answers a line of text.' })
29 let fires = 0
30 $.clock.every(5000, async () => {
31 fires += 1
32 await append($, { event: 'clock.every', fire: fires })
33 if (fires === 3) {
34 await append($, { event: 'prompt.submit called' })
35 const result = await $.prompt.submit({ text: 'probe: timer prompt' })
36 await append($, { event: 'prompt.submit resolved', result: 'drop' in result && result.drop ? { drop: result.drop } : { text: result.text } })
37 }
38 })
39 return next(e)
40 })
41
42 on('command.run', { command: 'digest' }, async $ => {
43 await append($, { event: 'command.run', command: 'digest' })
44 return { text: 'probe digest' }
45 })
46
47 on('ui.render', { component: 'Pane', requestId: 'probe40' }, async ($, e, next) => next(e))
48 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => next(e))
49}
50hooks/part.ts 38 lines1// probes/40-mod-headless/hooks/part.ts
2// Throwaway probe mod for check 40, the part register.ts imports: one prompt.submit hook that
3// logs every prompt it sees, with its origin, to probe-40-part.jsonl in the session's working
4// folder, and answers { drop: 'probe' } for a text that begins "probe-drop:".
5import type { Register, EngineInterface } from 'claude-code'
6
7const LOG = 'probe-40-part.jsonl'
8let queue: Promise<void> = Promise.resolve()
9
10// One line at a time, in the order they were made.
11function append($: EngineInterface, record: object): Promise<void> {
12 queue = queue.then(async () => {
13 let before = ''
14 try { before = await $.fs.read(LOG) } catch { before = '' }
15 await $.fs.write(LOG, before + JSON.stringify({ at: new Date().toISOString(), ...record }) + '\n')
16 }).catch(() => {})
17 return queue
18}
19
20export const registerPart: Register = on => {
21 on('prompt.submit', async ($, e, next) => {
22 const drops = e.text.startsWith('probe-drop:')
23 await append($, {
24 event: 'prompt.submit',
25 text: e.text.slice(0, 80),
26 origin: e.origin,
27 now: await $.clock.now(),
28 isMeta: e.isMeta === true,
29 turnId: e.turnId ?? null,
30 wait: e.wait,
31 command: e.command ?? null,
32 answer: drops ? 'drop' : 'next',
33 })
34 if (drops) return { drop: 'probe' }
35 return next(e)
36 })
37}
38