Vibecode Skribbl
track this build4 phases, 12 steps, beginner friendly0%The canvas is the easy half. React Flow plus xterm.js plus node-pty gets you panning, zooming and half a dozen live agent terminals in a weekend, and it is a genuinely nice way to watch a fleet work. The gaps are all in the boring layer underneath: knowing when an agent is actually blocked on you rather than just quiet, keeping sessions alive across a restart or a crash, and counting tokens accurately enough that the number can be trusted. Each of those is days of work on its own, and the weekend build reads as finished until the first agent silently dies behind a tab you were not looking at.
You are building a lean indie version of Skribbl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Skribbl · indie build A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Terminals | A real shell per node through a pseudo-terminal, rendered by a terminal emulator | anything less than a real pty breaks the first full screen program | | Canvas | A pan and zoom node graph with terminals as nodes and links as recorded relationships | watching a fleet side by side is the whole point | | Durability | Each node runs inside a terminal multiplexer session named by node id | a crashed window reattaches instead of killing the agent | | Status | Hook and event channels the agent itself exposes, never scraped output | a guess is worse than showing nothing | | Metering | Four token counters from the agent's own accounting, priced from a dated table | cache reads and input tokens are not the same money | | Runtime | A desktop application with native pseudo-terminal bindings | a browser cannot own a process | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the desktop application and its build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A native build toolchain for the pty bindings** · free - Why: The pseudo-terminal bindings compile against your desktop runtime's version, and this is the first failure on every fresh machine. - Get it: Your platform's compiler and build tools, then rebuild the bindings for the runtime version you ship. - Verify: A shell spawns in a node after a clean install - [ ] **A terminal multiplexer** · free - Why: Session survival across crashes and restarts comes entirely from it, and without it every relaunch kills every agent. - Get it: Install it from your package manager and confirm a session survives closing the terminal that started it. - Verify: A detached session is still there after its terminal closes - [ ] **A coding agent installed and authenticated, with its hooks reference read** · the agent's own subscription or per-token billing - Why: Status and usage come from the agent's own channels, and each agent exposes them differently or not at all, which decides what the canvas can honestly show. - Get it: Install the agent, authenticate it, and read its hooks and status line documentation before writing phase three. - Verify: A hook fires and writes an event on a test prompt - [ ] **Your price table, dated** · free - Why: The four counters are priced differently and the rates change, so the table needs a checked-on date shown beside every total. - Get it: Copy the current per-token rates for each model you run, including the cache read and cache write multipliers, and record the date. - Verify: The table has four rates per model and a date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. - hook-driven agent status, so a blocked agent looks the same as an idle one - session survival across restarts and crashes - an accurate token meter, cache reads price nothing like input tokens - a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else If one of those is essential to you, that is the reason to keep paying for Skribbl, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Skribbl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a canvas for running several coding agents side by side that never guesses what an agent is doing to replace Skribbl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two figures on this canvas are easy to fake and both must be real or absent. Status. From the outside, an agent quietly running a long build and an agent that stopped to ask you a question look identical: a terminal that has not printed for a while. Inferring 'blocked' from silence gets it wrong in both directions, and each wrong direction costs something, an hour of an agent waiting on you or an interruption of one that was fine. So status comes only from a channel the agent itself provides, such as its hooks, and where no such channel exists the node says unknown rather than guessing. Cost. A token count is worth showing only if it comes from the agent's own accounting, and only as four counters that are never summed: uncached input, cache writes, cache reads and output. They are priced differently. At the time of checking, a cache read costs a tenth of the input price, a five minute cache write one and a quarter times it and an hour long write twice, with per-model exceptions, so a single 'tokens' figure is wrong in both directions and a spend figure built from it cannot be acted on. ### Stack (fixed, do not substitute) - Terminals: A real shell per node through a pseudo-terminal, rendered by a terminal emulator · anything less than a real pty breaks the first full screen program - Canvas: A pan and zoom node graph with terminals as nodes and links as recorded relationships · watching a fleet side by side is the whole point - Durability: Each node runs inside a terminal multiplexer session named by node id · a crashed window reattaches instead of killing the agent - Status: Hook and event channels the agent itself exposes, never scraped output · a guess is worse than showing nothing - Metering: Four token counters from the agent's own accounting, priced from a dated table · cache reads and input tokens are not the same money - Runtime: A desktop application with native pseudo-terminal bindings · a browser cannot own a process ### Data model (create this before Phase 1) - nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something. ### Phase 1 · A real shell in a node Build: Spawn a shell per node through a pseudo-terminal in a chosen working directory and render it in a terminal emulator on a pan and zoom canvas. Propagate node resizes to the pty. Track process groups so closing a node ends everything it started. Bound scrollback. Trap: Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. Done when: A full screen program in a node redraws correctly after the node is resized. Closing a node leaves no child process, checked by listing processes afterwards. Six nodes typing simultaneously show no input lag, measured. Scrollback is bounded and the bound is stated. Do not build yet: No persistence, multiplexer, status or metering. ### Phase 2 · Sessions that outlive the window Build: Run each node's shell inside a multiplexer session named by node id, attaching if it exists and creating it if not. Persist node layout in the application's data directory with atomic writes. On launch, reattach, restore scrollback, and mark a node whose session has gone as dead rather than quietly spawning a fresh shell. Trap: Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. Done when: Killing the application and relaunching reattaches to every running agent with its scrollback, tested by killing the process mid-task. A node whose session died shows as dead with the exit reason where available. Layout writes are atomic, tested by killing during a save. A machine without the multiplexer refuses at startup with the install instruction. Do not build yet: No status or metering yet. ### Phase 3 · Status from the agent, or unknown Build: Install the agent's own hooks per node. For Claude Code that is a Notification hook, whose types include permission_prompt, idle_prompt and agent_needs_input, and a Stop hook that fires once per turn; read the current hooks reference before wiring them. Each hook writes an event carrying its session id to a socket the canvas reads. Map events to nodes by a per-node identifier. Any agent without such a channel shows unknown. Trap: Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. Done when: A permission prompt in a node changes its state within a second of the hook firing, and a completed turn changes it back, tested against a real agent. A node running a plain shell with no hooks shows unknown rather than idle. Hook events are matched to nodes by identifier rather than by which node printed last. No status is derived from terminal output anywhere, verified by review. Do not build yet: No metering or links yet. ### Phase 4 · Counting tokens without lying Build: Read usage from the agent's own accounting rather than the screen. For Claude Code, a status line command receives JSON on standard input carrying the usage broken out into input, cache creation and cache read tokens, plus an estimated cost at list price, debounced at three hundred milliseconds; check the current reference. Write it per session to a file the canvas reads. Keep the four counters apart, price each from a table with a checked-on date, and label the total as an estimate at list price. Trap: Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. Done when: Each node shows four counters that match the agent's own figures for the session, compared by hand. The canvas total is the sum of priced counters and shows the price table date. A model missing from the price table shows counters with no money figure rather than a wrong one. An agent with no accounting channel shows no counters rather than zero. Do not build yet: No links or export yet. ### Phase 5 · Twelve terminals without falling over Build: Draw links between nodes with a direction and store them as relationships only. Isolate the renderer from the pty host so a canvas crash never kills an agent. Bound memory per node and in total, throttle rendering for nodes outside the viewport, and measure frame time with twelve nodes streaming. Export a session as the layout, each node's transcript location and its usage. Trap: Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. Done when: Twelve nodes streaming output keep typing latency in the focused node under a stated bound, measured. A renderer crash leaves every agent running and reattaches on recovery, tested by killing the renderer. Links persist with direction and deleting a node removes its links by a stated rule. Export contains the layout, transcript locations and usage in a readable format. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer and cloud sync, which turn a process supervisor into a distributed system. - Code signing and notarisation, which is a distribution project of its own, and without it the platform's gatekeeper blocks anyone you send a build to. - Message passing between agents through links, which is an orchestration protocol worth designing on its own rather than bolting onto a drawing. - Any status inferred from terminal output, because a guess is worse than showing nothing. ### README must contain - That the pty bindings must be rebuilt against your desktop runtime's version, which is the one thing that breaks on a fresh machine - That the multiplexer is required and sessions are named by node id, so a relaunch reattaches - Which hooks are installed per node, what they write and where - That spend is four counters priced from a dated table at list price, and what the date means - That unknown means no channel has reported, and is never replaced with a guess ===== AGENTS.md ===== # Agent instructions · Skribbl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A real shell per node through a pseudo-terminal, rendered by a terminal emulator, A pan and zoom node graph with terminals as nodes and links as recorded relationships, Each node runs inside a terminal multiplexer session named by node id, Hook and event channels the agent itself exposes, never scraped output, Four token counters from the agent's own accounting, priced from a dated table, A desktop application with native pseudo-terminal bindings. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ===== BUILD_PLAN.md ===== # Build plan · Skribbl A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A real shell in a node Each node is a real process that resizes, scrolls and dies cleanly. ### Steps 1. Spawn through a pty and propagate resizes Full screen programs draw for the size the pty reports, not the size you see. Files: `src/main/pty.ts` 2. Kill the process group on close An invisible agent is still a billed agent. Files: `src/main/pty.ts`, `tests/no-orphans.test.ts` 3. Bound scrollback per node Twelve unbounded buffers is how the canvas dies at four in the afternoon. Files: `src/renderer/terminal.tsx` ### Done when - [ ] Full screen programs redraw correctly after a resize - [ ] Closing a node leaves no child process - [ ] Six nodes typing show no input lag - [ ] Scrollback is bounded with the bound stated ### Watch out - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. ## Phase 2 · Sessions that outlive the window A crash or a restart reattaches to running agents rather than killing them. ### Steps 1. Attach or create by node id The session name is the node id, so a relaunch finds it without a lookup table. Files: `src/main/mux.ts` 2. Mark a missing session dead, never respawn silently A fresh shell in an old node's place is a lie with a cursor. Files: `src/main/mux.ts`, `tests/dead-session.test.ts` 3. Write the layout atomically A crash during a save should cost nothing. Files: `src/main/layout.ts` ### Done when - [ ] Relaunching reattaches to running agents with scrollback - [ ] A dead session is shown as dead - [ ] Layout writes survive a kill mid-save - [ ] A missing multiplexer refuses at startup ### Watch out - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. ## Phase 3 · Status from the agent, or unknown A node shows waiting, working, finished or unknown, and never a guess. ### Steps 1. Install per-node hooks that write to a socket The agent knows when it is waiting. Nothing else does. Files: `src/main/hooks.ts`, `config/hooks.json` 2. Match events to nodes by identifier Not by which terminal printed last, which is wrong as soon as two are busy. Files: `src/main/status.ts` 3. Render unknown where nothing has fired Unknown is a true statement. Idle is a guess. Files: `src/renderer/status-badge.tsx` ### Done when - [ ] A permission prompt changes state within a second - [ ] A hookless shell shows unknown - [ ] Events match nodes by identifier - [ ] No status derives from terminal output ### Watch out - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. ## Phase 4 · Counting tokens without lying Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table. ### Steps 1. Read usage from the agent's status channel Its own accounting, written to a file the canvas tails. Files: `src/main/usage.ts`, `config/statusline.sh` 2. Price from a dated table, never a sum Four rates, one date, and the date shown beside the total. Files: `src/main/pricing.ts`, `config/prices.json` 3. Show no figure rather than a wrong one A missing model or a missing channel produces a blank, not a zero. Files: `src/renderer/meter.tsx`, `tests/pricing.test.ts` ### Done when - [ ] Four counters match the agent's own figures - [ ] The total shows the price table date - [ ] An unknown model shows counters without money - [ ] A channel-less agent shows no counters ### Watch out - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ## Not in this build - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. ## After v1, if you want it - A per-node spend ceiling that pauses the agent through its own channel, once metering is trusted - Git worktree creation per node, so parallel agents stop fighting over one checkout - A queue node that hands the next task to whichever agent finished, once hooks are reliable ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The multiplexer binary. Sessions are named by node id. MUX_BIN=tmux # Required. Where installed hooks write events. The canvas reads, never guesses. HOOK_SOCKET_PATH=~/.local/share/canvas/hooks.sock # Required. Per-session usage written by the agent's status line command. STATUS_FILE_DIR=~/.local/share/canvas/status # Required. Four rates per model and a checked-on date, shown beside totals. PRICE_TABLE_PATH=./config/prices.json # Required. Node positions and links, written atomically. LAYOUT_PATH=~/.local/share/canvas/layout.json # Required. Per node. Twelve unbounded buffers is how the canvas dies. SCROLLBACK_LINES=5000 # Required. Above this the canvas refuses with the reason rather than degrading quietly. MAX_NODES=16 # Required. Present as a constant. A guess is worse than showing nothing. INFER_STATUS=never
You are building a lean indie version of Skribbl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Skribbl · indie build A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Terminals | A real shell per node through a pseudo-terminal, rendered by a terminal emulator | anything less than a real pty breaks the first full screen program | | Canvas | A pan and zoom node graph with terminals as nodes and links as recorded relationships | watching a fleet side by side is the whole point | | Durability | Each node runs inside a terminal multiplexer session named by node id | a crashed window reattaches instead of killing the agent | | Status | Hook and event channels the agent itself exposes, never scraped output | a guess is worse than showing nothing | | Metering | Four token counters from the agent's own accounting, priced from a dated table | cache reads and input tokens are not the same money | | Runtime | A desktop application with native pseudo-terminal bindings | a browser cannot own a process | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the desktop application and its build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A native build toolchain for the pty bindings** · free - Why: The pseudo-terminal bindings compile against your desktop runtime's version, and this is the first failure on every fresh machine. - Get it: Your platform's compiler and build tools, then rebuild the bindings for the runtime version you ship. - Verify: A shell spawns in a node after a clean install - [ ] **A terminal multiplexer** · free - Why: Session survival across crashes and restarts comes entirely from it, and without it every relaunch kills every agent. - Get it: Install it from your package manager and confirm a session survives closing the terminal that started it. - Verify: A detached session is still there after its terminal closes - [ ] **A coding agent installed and authenticated, with its hooks reference read** · the agent's own subscription or per-token billing - Why: Status and usage come from the agent's own channels, and each agent exposes them differently or not at all, which decides what the canvas can honestly show. - Get it: Install the agent, authenticate it, and read its hooks and status line documentation before writing phase three. - Verify: A hook fires and writes an event on a test prompt - [ ] **Your price table, dated** · free - Why: The four counters are priced differently and the rates change, so the table needs a checked-on date shown beside every total. - Get it: Copy the current per-token rates for each model you run, including the cache read and cache write multipliers, and record the date. - Verify: The table has four rates per model and a date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. - hook-driven agent status, so a blocked agent looks the same as an idle one - session survival across restarts and crashes - an accurate token meter, cache reads price nothing like input tokens - a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else If one of those is essential to you, that is the reason to keep paying for Skribbl, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Skribbl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a canvas for running several coding agents side by side that never guesses what an agent is doing to replace Skribbl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two figures on this canvas are easy to fake and both must be real or absent. Status. From the outside, an agent quietly running a long build and an agent that stopped to ask you a question look identical: a terminal that has not printed for a while. Inferring 'blocked' from silence gets it wrong in both directions, and each wrong direction costs something, an hour of an agent waiting on you or an interruption of one that was fine. So status comes only from a channel the agent itself provides, such as its hooks, and where no such channel exists the node says unknown rather than guessing. Cost. A token count is worth showing only if it comes from the agent's own accounting, and only as four counters that are never summed: uncached input, cache writes, cache reads and output. They are priced differently. At the time of checking, a cache read costs a tenth of the input price, a five minute cache write one and a quarter times it and an hour long write twice, with per-model exceptions, so a single 'tokens' figure is wrong in both directions and a spend figure built from it cannot be acted on. ### Stack (fixed, do not substitute) - Terminals: A real shell per node through a pseudo-terminal, rendered by a terminal emulator · anything less than a real pty breaks the first full screen program - Canvas: A pan and zoom node graph with terminals as nodes and links as recorded relationships · watching a fleet side by side is the whole point - Durability: Each node runs inside a terminal multiplexer session named by node id · a crashed window reattaches instead of killing the agent - Status: Hook and event channels the agent itself exposes, never scraped output · a guess is worse than showing nothing - Metering: Four token counters from the agent's own accounting, priced from a dated table · cache reads and input tokens are not the same money - Runtime: A desktop application with native pseudo-terminal bindings · a browser cannot own a process ### Data model (create this before Phase 1) - nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something. ### Phase 1 · A real shell in a node Build: Spawn a shell per node through a pseudo-terminal in a chosen working directory and render it in a terminal emulator on a pan and zoom canvas. Propagate node resizes to the pty. Track process groups so closing a node ends everything it started. Bound scrollback. Trap: Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. Done when: A full screen program in a node redraws correctly after the node is resized. Closing a node leaves no child process, checked by listing processes afterwards. Six nodes typing simultaneously show no input lag, measured. Scrollback is bounded and the bound is stated. Do not build yet: No persistence, multiplexer, status or metering. ### Phase 2 · Sessions that outlive the window Build: Run each node's shell inside a multiplexer session named by node id, attaching if it exists and creating it if not. Persist node layout in the application's data directory with atomic writes. On launch, reattach, restore scrollback, and mark a node whose session has gone as dead rather than quietly spawning a fresh shell. Trap: Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. Done when: Killing the application and relaunching reattaches to every running agent with its scrollback, tested by killing the process mid-task. A node whose session died shows as dead with the exit reason where available. Layout writes are atomic, tested by killing during a save. A machine without the multiplexer refuses at startup with the install instruction. Do not build yet: No status or metering yet. ### Phase 3 · Status from the agent, or unknown Build: Install the agent's own hooks per node. For Claude Code that is a Notification hook, whose types include permission_prompt, idle_prompt and agent_needs_input, and a Stop hook that fires once per turn; read the current hooks reference before wiring them. Each hook writes an event carrying its session id to a socket the canvas reads. Map events to nodes by a per-node identifier. Any agent without such a channel shows unknown. Trap: Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. Done when: A permission prompt in a node changes its state within a second of the hook firing, and a completed turn changes it back, tested against a real agent. A node running a plain shell with no hooks shows unknown rather than idle. Hook events are matched to nodes by identifier rather than by which node printed last. No status is derived from terminal output anywhere, verified by review. Do not build yet: No metering or links yet. ### Phase 4 · Counting tokens without lying Build: Read usage from the agent's own accounting rather than the screen. For Claude Code, a status line command receives JSON on standard input carrying the usage broken out into input, cache creation and cache read tokens, plus an estimated cost at list price, debounced at three hundred milliseconds; check the current reference. Write it per session to a file the canvas reads. Keep the four counters apart, price each from a table with a checked-on date, and label the total as an estimate at list price. Trap: Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. Done when: Each node shows four counters that match the agent's own figures for the session, compared by hand. The canvas total is the sum of priced counters and shows the price table date. A model missing from the price table shows counters with no money figure rather than a wrong one. An agent with no accounting channel shows no counters rather than zero. Do not build yet: No links or export yet. ### Phase 5 · Twelve terminals without falling over Build: Draw links between nodes with a direction and store them as relationships only. Isolate the renderer from the pty host so a canvas crash never kills an agent. Bound memory per node and in total, throttle rendering for nodes outside the viewport, and measure frame time with twelve nodes streaming. Export a session as the layout, each node's transcript location and its usage. Trap: Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. Done when: Twelve nodes streaming output keep typing latency in the focused node under a stated bound, measured. A renderer crash leaves every agent running and reattaches on recovery, tested by killing the renderer. Links persist with direction and deleting a node removes its links by a stated rule. Export contains the layout, transcript locations and usage in a readable format. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer and cloud sync, which turn a process supervisor into a distributed system. - Code signing and notarisation, which is a distribution project of its own, and without it the platform's gatekeeper blocks anyone you send a build to. - Message passing between agents through links, which is an orchestration protocol worth designing on its own rather than bolting onto a drawing. - Any status inferred from terminal output, because a guess is worse than showing nothing. ### README must contain - That the pty bindings must be rebuilt against your desktop runtime's version, which is the one thing that breaks on a fresh machine - That the multiplexer is required and sessions are named by node id, so a relaunch reattaches - Which hooks are installed per node, what they write and where - That spend is four counters priced from a dated table at list price, and what the date means - That unknown means no channel has reported, and is never replaced with a guess ===== AGENTS.md ===== # Agent instructions · Skribbl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A real shell per node through a pseudo-terminal, rendered by a terminal emulator, A pan and zoom node graph with terminals as nodes and links as recorded relationships, Each node runs inside a terminal multiplexer session named by node id, Hook and event channels the agent itself exposes, never scraped output, Four token counters from the agent's own accounting, priced from a dated table, A desktop application with native pseudo-terminal bindings. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ===== BUILD_PLAN.md ===== # Build plan · Skribbl A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A real shell in a node Each node is a real process that resizes, scrolls and dies cleanly. ### Steps 1. Spawn through a pty and propagate resizes Full screen programs draw for the size the pty reports, not the size you see. Files: `src/main/pty.ts` 2. Kill the process group on close An invisible agent is still a billed agent. Files: `src/main/pty.ts`, `tests/no-orphans.test.ts` 3. Bound scrollback per node Twelve unbounded buffers is how the canvas dies at four in the afternoon. Files: `src/renderer/terminal.tsx` ### Done when - [ ] Full screen programs redraw correctly after a resize - [ ] Closing a node leaves no child process - [ ] Six nodes typing show no input lag - [ ] Scrollback is bounded with the bound stated ### Watch out - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. ## Phase 2 · Sessions that outlive the window A crash or a restart reattaches to running agents rather than killing them. ### Steps 1. Attach or create by node id The session name is the node id, so a relaunch finds it without a lookup table. Files: `src/main/mux.ts` 2. Mark a missing session dead, never respawn silently A fresh shell in an old node's place is a lie with a cursor. Files: `src/main/mux.ts`, `tests/dead-session.test.ts` 3. Write the layout atomically A crash during a save should cost nothing. Files: `src/main/layout.ts` ### Done when - [ ] Relaunching reattaches to running agents with scrollback - [ ] A dead session is shown as dead - [ ] Layout writes survive a kill mid-save - [ ] A missing multiplexer refuses at startup ### Watch out - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. ## Phase 3 · Status from the agent, or unknown A node shows waiting, working, finished or unknown, and never a guess. ### Steps 1. Install per-node hooks that write to a socket The agent knows when it is waiting. Nothing else does. Files: `src/main/hooks.ts`, `config/hooks.json` 2. Match events to nodes by identifier Not by which terminal printed last, which is wrong as soon as two are busy. Files: `src/main/status.ts` 3. Render unknown where nothing has fired Unknown is a true statement. Idle is a guess. Files: `src/renderer/status-badge.tsx` ### Done when - [ ] A permission prompt changes state within a second - [ ] A hookless shell shows unknown - [ ] Events match nodes by identifier - [ ] No status derives from terminal output ### Watch out - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. ## Phase 4 · Counting tokens without lying Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table. ### Steps 1. Read usage from the agent's status channel Its own accounting, written to a file the canvas tails. Files: `src/main/usage.ts`, `config/statusline.sh` 2. Price from a dated table, never a sum Four rates, one date, and the date shown beside the total. Files: `src/main/pricing.ts`, `config/prices.json` 3. Show no figure rather than a wrong one A missing model or a missing channel produces a blank, not a zero. Files: `src/renderer/meter.tsx`, `tests/pricing.test.ts` ### Done when - [ ] Four counters match the agent's own figures - [ ] The total shows the price table date - [ ] An unknown model shows counters without money - [ ] A channel-less agent shows no counters ### Watch out - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ## Not in this build - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. ## After v1, if you want it - A per-node spend ceiling that pauses the agent through its own channel, once metering is trusted - Git worktree creation per node, so parallel agents stop fighting over one checkout - A queue node that hands the next task to whichever agent finished, once hooks are reliable ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The multiplexer binary. Sessions are named by node id. MUX_BIN=tmux # Required. Where installed hooks write events. The canvas reads, never guesses. HOOK_SOCKET_PATH=~/.local/share/canvas/hooks.sock # Required. Per-session usage written by the agent's status line command. STATUS_FILE_DIR=~/.local/share/canvas/status # Required. Four rates per model and a checked-on date, shown beside totals. PRICE_TABLE_PATH=./config/prices.json # Required. Node positions and links, written atomically. LAYOUT_PATH=~/.local/share/canvas/layout.json # Required. Per node. Twelve unbounded buffers is how the canvas dies. SCROLLBACK_LINES=5000 # Required. Above this the canvas refuses with the reason rather than degrading quietly. MAX_NODES=16 # Required. Present as a constant. A guess is worse than showing nothing. INFER_STATUS=never
You are building a production product version of Skribbl. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Skribbl · product brief ## Problem The canvas is the easy half. React Flow plus xterm.js plus node-pty gets you panning, zooming and half a dozen live agent terminals in a weekend, and it is a genuinely nice way to watch a fleet work. The gaps are all in the boring layer underneath: knowing when an agent is actually blocked on you rather than just quiet, keeping sessions alive across a restart or a crash, and counting tokens accurately enough that the number can be trusted. Each of those is days of work on its own, and the weekend build reads as finished until the first agent silently dies behind a tab you were not looking at. ## Product outcome A canvas where a dozen agents run for a day, survive the window crashing, show when they are genuinely waiting for you, and display a spend figure you can act on before the bill. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20 and native build tools for node-pty - A coding agent CLI already installed and authenticated - tmux, if you want sessions to survive a restart ## Explicit non-goals for v1 - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. - hook-driven agent status, so a blocked agent looks the same as an idle one - session survival across restarts and crashes - an accurate token meter, cache reads price nothing like input tokens - a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else ## Success criteria - Closing a node is confirmed to leave no child process - Relaunching is confirmed to reattach with scrollback - A hookless shell is confirmed to show unknown - Four counters are confirmed to match the agent's own figures - Twelve streaming nodes are confirmed under the latency bound ===== BRIEF.md ===== # Build brief · Skribbl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a canvas for running several coding agents side by side that never guesses what an agent is doing to replace Skribbl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two figures on this canvas are easy to fake and both must be real or absent. Status. From the outside, an agent quietly running a long build and an agent that stopped to ask you a question look identical: a terminal that has not printed for a while. Inferring 'blocked' from silence gets it wrong in both directions, and each wrong direction costs something, an hour of an agent waiting on you or an interruption of one that was fine. So status comes only from a channel the agent itself provides, such as its hooks, and where no such channel exists the node says unknown rather than guessing. Cost. A token count is worth showing only if it comes from the agent's own accounting, and only as four counters that are never summed: uncached input, cache writes, cache reads and output. They are priced differently. At the time of checking, a cache read costs a tenth of the input price, a five minute cache write one and a quarter times it and an hour long write twice, with per-model exceptions, so a single 'tokens' figure is wrong in both directions and a spend figure built from it cannot be acted on. ### Stack (fixed, do not substitute) - Terminals: A real shell per node through a pseudo-terminal, rendered by a terminal emulator · anything less than a real pty breaks the first full screen program - Canvas: A pan and zoom node graph with terminals as nodes and links as recorded relationships · watching a fleet side by side is the whole point - Durability: Each node runs inside a terminal multiplexer session named by node id · a crashed window reattaches instead of killing the agent - Status: Hook and event channels the agent itself exposes, never scraped output · a guess is worse than showing nothing - Metering: Four token counters from the agent's own accounting, priced from a dated table · cache reads and input tokens are not the same money - Runtime: A desktop application with native pseudo-terminal bindings · a browser cannot own a process ### Data model (create this before Phase 1) - nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something. ### Phase 1 · A real shell in a node Build: Spawn a shell per node through a pseudo-terminal in a chosen working directory and render it in a terminal emulator on a pan and zoom canvas. Propagate node resizes to the pty. Track process groups so closing a node ends everything it started. Bound scrollback. Trap: Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. Done when: A full screen program in a node redraws correctly after the node is resized. Closing a node leaves no child process, checked by listing processes afterwards. Six nodes typing simultaneously show no input lag, measured. Scrollback is bounded and the bound is stated. Do not build yet: No persistence, multiplexer, status or metering. ### Phase 2 · Sessions that outlive the window Build: Run each node's shell inside a multiplexer session named by node id, attaching if it exists and creating it if not. Persist node layout in the application's data directory with atomic writes. On launch, reattach, restore scrollback, and mark a node whose session has gone as dead rather than quietly spawning a fresh shell. Trap: Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. Done when: Killing the application and relaunching reattaches to every running agent with its scrollback, tested by killing the process mid-task. A node whose session died shows as dead with the exit reason where available. Layout writes are atomic, tested by killing during a save. A machine without the multiplexer refuses at startup with the install instruction. Do not build yet: No status or metering yet. ### Phase 3 · Status from the agent, or unknown Build: Install the agent's own hooks per node. For Claude Code that is a Notification hook, whose types include permission_prompt, idle_prompt and agent_needs_input, and a Stop hook that fires once per turn; read the current hooks reference before wiring them. Each hook writes an event carrying its session id to a socket the canvas reads. Map events to nodes by a per-node identifier. Any agent without such a channel shows unknown. Trap: Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. Done when: A permission prompt in a node changes its state within a second of the hook firing, and a completed turn changes it back, tested against a real agent. A node running a plain shell with no hooks shows unknown rather than idle. Hook events are matched to nodes by identifier rather than by which node printed last. No status is derived from terminal output anywhere, verified by review. Do not build yet: No metering or links yet. ### Phase 4 · Counting tokens without lying Build: Read usage from the agent's own accounting rather than the screen. For Claude Code, a status line command receives JSON on standard input carrying the usage broken out into input, cache creation and cache read tokens, plus an estimated cost at list price, debounced at three hundred milliseconds; check the current reference. Write it per session to a file the canvas reads. Keep the four counters apart, price each from a table with a checked-on date, and label the total as an estimate at list price. Trap: Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. Done when: Each node shows four counters that match the agent's own figures for the session, compared by hand. The canvas total is the sum of priced counters and shows the price table date. A model missing from the price table shows counters with no money figure rather than a wrong one. An agent with no accounting channel shows no counters rather than zero. Do not build yet: No links or export yet. ### Phase 5 · Twelve terminals without falling over Build: Draw links between nodes with a direction and store them as relationships only. Isolate the renderer from the pty host so a canvas crash never kills an agent. Bound memory per node and in total, throttle rendering for nodes outside the viewport, and measure frame time with twelve nodes streaming. Export a session as the layout, each node's transcript location and its usage. Trap: Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. Done when: Twelve nodes streaming output keep typing latency in the focused node under a stated bound, measured. A renderer crash leaves every agent running and reattaches on recovery, tested by killing the renderer. Links persist with direction and deleting a node removes its links by a stated rule. Export contains the layout, transcript locations and usage in a readable format. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer and cloud sync, which turn a process supervisor into a distributed system. - Code signing and notarisation, which is a distribution project of its own, and without it the platform's gatekeeper blocks anyone you send a build to. - Message passing between agents through links, which is an orchestration protocol worth designing on its own rather than bolting onto a drawing. - Any status inferred from terminal output, because a guess is worse than showing nothing. ### README must contain - That the pty bindings must be rebuilt against your desktop runtime's version, which is the one thing that breaks on a fresh machine - That the multiplexer is required and sessions are named by node id, so a relaunch reattaches - Which hooks are installed per node, what they write and where - That spend is four counters priced from a dated table at list price, and what the date means - That unknown means no channel has reported, and is never replaced with a guess ===== ARCHITECTURE.md ===== # Architecture · Skribbl ## Stack | Part | Choice | Why | | --- | --- | --- | | Terminals | A real shell per node through a pseudo-terminal, rendered by a terminal emulator | anything less than a real pty breaks the first full screen program | | Canvas | A pan and zoom node graph with terminals as nodes and links as recorded relationships | watching a fleet side by side is the whole point | | Durability | Each node runs inside a terminal multiplexer session named by node id | a crashed window reattaches instead of killing the agent | | Status | Hook and event channels the agent itself exposes, never scraped output | a guess is worse than showing nothing | | Metering | Four token counters from the agent's own accounting, priced from a dated table | cache reads and input tokens are not the same money | | Runtime | A desktop application with native pseudo-terminal bindings | a browser cannot own a process | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Terminals | Real ptys with resize propagation and group kill. | Any emulator, provided the pty is real and closing kills the group. | | Durability | Named multiplexer sessions and atomic layout. | Any multiplexer, provided a missing session is shown as dead. | | Status | Hook events mapped by identifier. | Any agent channel. Not swappable for output scraping. | | Metering | Four counters priced from a dated table. | Any accounting source, provided the counters are never summed. | | Canvas | Nodes, links and throttled rendering. | Any graph view, provided off-screen nodes are throttled. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MUX_BIN` · required · The multiplexer binary. Sessions are named by node id. - `HOOK_SOCKET_PATH` · required · Where installed hooks write events. The canvas reads, never guesses. - `STATUS_FILE_DIR` · required · Per-session usage written by the agent's status line command. - `PRICE_TABLE_PATH` · required · Four rates per model and a checked-on date, shown beside totals. - `LAYOUT_PATH` · required · Node positions and links, written atomically. - `SCROLLBACK_LINES` · required · Per node. Twelve unbounded buffers is how the canvas dies. - `MAX_NODES` · required · Above this the canvas refuses with the reason rather than degrading quietly. - `INFER_STATUS` · required · Present as a constant. A guess is worse than showing nothing. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Skribbl product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A real shell per node through a pseudo-terminal, rendered by a terminal emulator, A pan and zoom node graph with terminals as nodes and links as recorded relationships, Each node runs inside a terminal multiplexer session named by node id, Hook and event channels the agent itself exposes, never scraped output, Four token counters from the agent's own accounting, priced from a dated table, A desktop application with native pseudo-terminal bindings. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. - Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. ===== MILESTONES.md ===== # Delivery milestones · Skribbl Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A real shell in a node Each node is a real process that resizes, scrolls and dies cleanly. ### Steps 1. Spawn through a pty and propagate resizes Full screen programs draw for the size the pty reports, not the size you see. Files: `src/main/pty.ts` 2. Kill the process group on close An invisible agent is still a billed agent. Files: `src/main/pty.ts`, `tests/no-orphans.test.ts` 3. Bound scrollback per node Twelve unbounded buffers is how the canvas dies at four in the afternoon. Files: `src/renderer/terminal.tsx` ### Done when - [ ] Full screen programs redraw correctly after a resize - [ ] Closing a node leaves no child process - [ ] Six nodes typing show no input lag - [ ] Scrollback is bounded with the bound stated ### Watch out - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. ## M2 · Sessions that outlive the window A crash or a restart reattaches to running agents rather than killing them. ### Steps 1. Attach or create by node id The session name is the node id, so a relaunch finds it without a lookup table. Files: `src/main/mux.ts` 2. Mark a missing session dead, never respawn silently A fresh shell in an old node's place is a lie with a cursor. Files: `src/main/mux.ts`, `tests/dead-session.test.ts` 3. Write the layout atomically A crash during a save should cost nothing. Files: `src/main/layout.ts` ### Done when - [ ] Relaunching reattaches to running agents with scrollback - [ ] A dead session is shown as dead - [ ] Layout writes survive a kill mid-save - [ ] A missing multiplexer refuses at startup ### Watch out - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. ## M3 · Status from the agent, or unknown A node shows waiting, working, finished or unknown, and never a guess. ### Steps 1. Install per-node hooks that write to a socket The agent knows when it is waiting. Nothing else does. Files: `src/main/hooks.ts`, `config/hooks.json` 2. Match events to nodes by identifier Not by which terminal printed last, which is wrong as soon as two are busy. Files: `src/main/status.ts` 3. Render unknown where nothing has fired Unknown is a true statement. Idle is a guess. Files: `src/renderer/status-badge.tsx` ### Done when - [ ] A permission prompt changes state within a second - [ ] A hookless shell shows unknown - [ ] Events match nodes by identifier - [ ] No status derives from terminal output ### Watch out - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. ## M4 · Counting tokens without lying Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table. ### Steps 1. Read usage from the agent's status channel Its own accounting, written to a file the canvas tails. Files: `src/main/usage.ts`, `config/statusline.sh` 2. Price from a dated table, never a sum Four rates, one date, and the date shown beside the total. Files: `src/main/pricing.ts`, `config/prices.json` 3. Show no figure rather than a wrong one A missing model or a missing channel produces a blank, not a zero. Files: `src/renderer/meter.tsx`, `tests/pricing.test.ts` ### Done when - [ ] Four counters match the agent's own figures - [ ] The total shows the price table date - [ ] An unknown model shows counters without money - [ ] A channel-less agent shows no counters ### Watch out - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ## M5 · Twelve terminals without falling over (production only) A dozen live agents run for a day without the canvas becoming the bottleneck. ### Steps 1. Isolate the renderer from the pty host A canvas crash must cost a window, never an agent. Files: `src/main/host.ts`, `tests/renderer-crash.test.ts` 2. Throttle off-screen nodes and measure latency With twelve streaming. One node proves nothing. Files: `src/renderer/scheduler.ts` 3. Export layout, transcripts and usage So a day's work can be read somewhere other than this canvas. Files: `src/main/export.ts` ### Done when - [ ] Typing latency stays under the bound with twelve streaming - [ ] A renderer crash leaves agents running - [ ] Links persist with direction and delete by rule - [ ] Export is readable outside the canvas ### Watch out - Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. ===== OPERATIONS.md ===== # Operations · Skribbl ## Backup The layout file and the price table are the only state the canvas owns, and both are small. The agents' transcripts live with the agents and are theirs to back up. ## Restore Restore the layout and relaunch, and confirm that every node either reattached to a live session or shows as dead. A restore that quietly spawned fresh shells has lost the work the sessions were protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Input latency in the focused node with everything streaming, orphaned processes after closes, and hook events per node. A node with no events for a day almost always means its hooks are not installed rather than that it never waited. ## Incident checklist An agent lost mid-task is the incident, and it means the multiplexer was missing or a node respawned instead of reattaching. A spend figure that disagrees with the bill means the counters were summed or the table is stale, and the date beside the total says which. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Closing a node is confirmed to leave no child process - [ ] Relaunching is confirmed to reattach with scrollback - [ ] A hookless shell is confirmed to show unknown - [ ] Four counters are confirmed to match the agent's own figures - [ ] Twelve streaming nodes are confirmed under the latency bound ## Launch constraint Do not market omitted Skribbl capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The multiplexer binary. Sessions are named by node id. MUX_BIN=tmux # Required. Where installed hooks write events. The canvas reads, never guesses. HOOK_SOCKET_PATH=~/.local/share/canvas/hooks.sock # Required. Per-session usage written by the agent's status line command. STATUS_FILE_DIR=~/.local/share/canvas/status # Required. Four rates per model and a checked-on date, shown beside totals. PRICE_TABLE_PATH=./config/prices.json # Required. Node positions and links, written atomically. LAYOUT_PATH=~/.local/share/canvas/layout.json # Required. Per node. Twelve unbounded buffers is how the canvas dies. SCROLLBACK_LINES=5000 # Required. Above this the canvas refuses with the reason rather than degrading quietly. MAX_NODES=16 # Required. Present as a constant. A guess is worse than showing nothing. INFER_STATUS=never
# Skribbl · indie build A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Terminals | A real shell per node through a pseudo-terminal, rendered by a terminal emulator | anything less than a real pty breaks the first full screen program | | Canvas | A pan and zoom node graph with terminals as nodes and links as recorded relationships | watching a fleet side by side is the whole point | | Durability | Each node runs inside a terminal multiplexer session named by node id | a crashed window reattaches instead of killing the agent | | Status | Hook and event channels the agent itself exposes, never scraped output | a guess is worse than showing nothing | | Metering | Four token counters from the agent's own accounting, priced from a dated table | cache reads and input tokens are not the same money | | Runtime | A desktop application with native pseudo-terminal bindings | a browser cannot own a process | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the desktop application and its build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A native build toolchain for the pty bindings** · free - Why: The pseudo-terminal bindings compile against your desktop runtime's version, and this is the first failure on every fresh machine. - Get it: Your platform's compiler and build tools, then rebuild the bindings for the runtime version you ship. - Verify: A shell spawns in a node after a clean install - [ ] **A terminal multiplexer** · free - Why: Session survival across crashes and restarts comes entirely from it, and without it every relaunch kills every agent. - Get it: Install it from your package manager and confirm a session survives closing the terminal that started it. - Verify: A detached session is still there after its terminal closes - [ ] **A coding agent installed and authenticated, with its hooks reference read** · the agent's own subscription or per-token billing - Why: Status and usage come from the agent's own channels, and each agent exposes them differently or not at all, which decides what the canvas can honestly show. - Get it: Install the agent, authenticate it, and read its hooks and status line documentation before writing phase three. - Verify: A hook fires and writes an event on a test prompt - [ ] **Your price table, dated** · free - Why: The four counters are priced differently and the rates change, so the table needs a checked-on date shown beside every total. - Get it: Copy the current per-token rates for each model you run, including the cache read and cache write multipliers, and record the date. - Verify: The table has four rates per model and a date ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. - hook-driven agent status, so a blocked agent looks the same as an idle one - session survival across restarts and crashes - an accurate token meter, cache reads price nothing like input tokens - a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else If one of those is essential to you, that is the reason to keep paying for Skribbl, and the README should say so rather than pretend.
# Build brief · Skribbl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a canvas for running several coding agents side by side that never guesses what an agent is doing to replace Skribbl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two figures on this canvas are easy to fake and both must be real or absent. Status. From the outside, an agent quietly running a long build and an agent that stopped to ask you a question look identical: a terminal that has not printed for a while. Inferring 'blocked' from silence gets it wrong in both directions, and each wrong direction costs something, an hour of an agent waiting on you or an interruption of one that was fine. So status comes only from a channel the agent itself provides, such as its hooks, and where no such channel exists the node says unknown rather than guessing. Cost. A token count is worth showing only if it comes from the agent's own accounting, and only as four counters that are never summed: uncached input, cache writes, cache reads and output. They are priced differently. At the time of checking, a cache read costs a tenth of the input price, a five minute cache write one and a quarter times it and an hour long write twice, with per-model exceptions, so a single 'tokens' figure is wrong in both directions and a spend figure built from it cannot be acted on. ### Stack (fixed, do not substitute) - Terminals: A real shell per node through a pseudo-terminal, rendered by a terminal emulator · anything less than a real pty breaks the first full screen program - Canvas: A pan and zoom node graph with terminals as nodes and links as recorded relationships · watching a fleet side by side is the whole point - Durability: Each node runs inside a terminal multiplexer session named by node id · a crashed window reattaches instead of killing the agent - Status: Hook and event channels the agent itself exposes, never scraped output · a guess is worse than showing nothing - Metering: Four token counters from the agent's own accounting, priced from a dated table · cache reads and input tokens are not the same money - Runtime: A desktop application with native pseudo-terminal bindings · a browser cannot own a process ### Data model (create this before Phase 1) - nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something. ### Phase 1 · A real shell in a node Build: Spawn a shell per node through a pseudo-terminal in a chosen working directory and render it in a terminal emulator on a pan and zoom canvas. Propagate node resizes to the pty. Track process groups so closing a node ends everything it started. Bound scrollback. Trap: Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. Done when: A full screen program in a node redraws correctly after the node is resized. Closing a node leaves no child process, checked by listing processes afterwards. Six nodes typing simultaneously show no input lag, measured. Scrollback is bounded and the bound is stated. Do not build yet: No persistence, multiplexer, status or metering. ### Phase 2 · Sessions that outlive the window Build: Run each node's shell inside a multiplexer session named by node id, attaching if it exists and creating it if not. Persist node layout in the application's data directory with atomic writes. On launch, reattach, restore scrollback, and mark a node whose session has gone as dead rather than quietly spawning a fresh shell. Trap: Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. Done when: Killing the application and relaunching reattaches to every running agent with its scrollback, tested by killing the process mid-task. A node whose session died shows as dead with the exit reason where available. Layout writes are atomic, tested by killing during a save. A machine without the multiplexer refuses at startup with the install instruction. Do not build yet: No status or metering yet. ### Phase 3 · Status from the agent, or unknown Build: Install the agent's own hooks per node. For Claude Code that is a Notification hook, whose types include permission_prompt, idle_prompt and agent_needs_input, and a Stop hook that fires once per turn; read the current hooks reference before wiring them. Each hook writes an event carrying its session id to a socket the canvas reads. Map events to nodes by a per-node identifier. Any agent without such a channel shows unknown. Trap: Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. Done when: A permission prompt in a node changes its state within a second of the hook firing, and a completed turn changes it back, tested against a real agent. A node running a plain shell with no hooks shows unknown rather than idle. Hook events are matched to nodes by identifier rather than by which node printed last. No status is derived from terminal output anywhere, verified by review. Do not build yet: No metering or links yet. ### Phase 4 · Counting tokens without lying Build: Read usage from the agent's own accounting rather than the screen. For Claude Code, a status line command receives JSON on standard input carrying the usage broken out into input, cache creation and cache read tokens, plus an estimated cost at list price, debounced at three hundred milliseconds; check the current reference. Write it per session to a file the canvas reads. Keep the four counters apart, price each from a table with a checked-on date, and label the total as an estimate at list price. Trap: Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. Done when: Each node shows four counters that match the agent's own figures for the session, compared by hand. The canvas total is the sum of priced counters and shows the price table date. A model missing from the price table shows counters with no money figure rather than a wrong one. An agent with no accounting channel shows no counters rather than zero. Do not build yet: No links or export yet. ### Phase 5 · Twelve terminals without falling over Build: Draw links between nodes with a direction and store them as relationships only. Isolate the renderer from the pty host so a canvas crash never kills an agent. Bound memory per node and in total, throttle rendering for nodes outside the viewport, and measure frame time with twelve nodes streaming. Export a session as the layout, each node's transcript location and its usage. Trap: Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. Done when: Twelve nodes streaming output keep typing latency in the focused node under a stated bound, measured. A renderer crash leaves every agent running and reattaches on recovery, tested by killing the renderer. Links persist with direction and deleting a node removes its links by a stated rule. Export contains the layout, transcript locations and usage in a readable format. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer and cloud sync, which turn a process supervisor into a distributed system. - Code signing and notarisation, which is a distribution project of its own, and without it the platform's gatekeeper blocks anyone you send a build to. - Message passing between agents through links, which is an orchestration protocol worth designing on its own rather than bolting onto a drawing. - Any status inferred from terminal output, because a guess is worse than showing nothing. ### README must contain - That the pty bindings must be rebuilt against your desktop runtime's version, which is the one thing that breaks on a fresh machine - That the multiplexer is required and sessions are named by node id, so a relaunch reattaches - Which hooks are installed per node, what they write and where - That spend is four counters priced from a dated table at list price, and what the date means - That unknown means no channel has reported, and is never replaced with a guess
# Agent instructions · Skribbl indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A real shell per node through a pseudo-terminal, rendered by a terminal emulator, A pan and zoom node graph with terminals as nodes and links as recorded relationships, Each node runs inside a terminal multiplexer session named by node id, Hook and event channels the agent itself exposes, never scraped output, Four token counters from the agent's own accounting, priced from a dated table, A desktop application with native pseudo-terminal bindings. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total.
# Build plan · Skribbl A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A real shell in a node Each node is a real process that resizes, scrolls and dies cleanly. ### Steps 1. Spawn through a pty and propagate resizes Full screen programs draw for the size the pty reports, not the size you see. Files: `src/main/pty.ts` 2. Kill the process group on close An invisible agent is still a billed agent. Files: `src/main/pty.ts`, `tests/no-orphans.test.ts` 3. Bound scrollback per node Twelve unbounded buffers is how the canvas dies at four in the afternoon. Files: `src/renderer/terminal.tsx` ### Done when - [ ] Full screen programs redraw correctly after a resize - [ ] Closing a node leaves no child process - [ ] Six nodes typing show no input lag - [ ] Scrollback is bounded with the bound stated ### Watch out - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. ## Phase 2 · Sessions that outlive the window A crash or a restart reattaches to running agents rather than killing them. ### Steps 1. Attach or create by node id The session name is the node id, so a relaunch finds it without a lookup table. Files: `src/main/mux.ts` 2. Mark a missing session dead, never respawn silently A fresh shell in an old node's place is a lie with a cursor. Files: `src/main/mux.ts`, `tests/dead-session.test.ts` 3. Write the layout atomically A crash during a save should cost nothing. Files: `src/main/layout.ts` ### Done when - [ ] Relaunching reattaches to running agents with scrollback - [ ] A dead session is shown as dead - [ ] Layout writes survive a kill mid-save - [ ] A missing multiplexer refuses at startup ### Watch out - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. ## Phase 3 · Status from the agent, or unknown A node shows waiting, working, finished or unknown, and never a guess. ### Steps 1. Install per-node hooks that write to a socket The agent knows when it is waiting. Nothing else does. Files: `src/main/hooks.ts`, `config/hooks.json` 2. Match events to nodes by identifier Not by which terminal printed last, which is wrong as soon as two are busy. Files: `src/main/status.ts` 3. Render unknown where nothing has fired Unknown is a true statement. Idle is a guess. Files: `src/renderer/status-badge.tsx` ### Done when - [ ] A permission prompt changes state within a second - [ ] A hookless shell shows unknown - [ ] Events match nodes by identifier - [ ] No status derives from terminal output ### Watch out - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. ## Phase 4 · Counting tokens without lying Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table. ### Steps 1. Read usage from the agent's status channel Its own accounting, written to a file the canvas tails. Files: `src/main/usage.ts`, `config/statusline.sh` 2. Price from a dated table, never a sum Four rates, one date, and the date shown beside the total. Files: `src/main/pricing.ts`, `config/prices.json` 3. Show no figure rather than a wrong one A missing model or a missing channel produces a blank, not a zero. Files: `src/renderer/meter.tsx`, `tests/pricing.test.ts` ### Done when - [ ] Four counters match the agent's own figures - [ ] The total shows the price table date - [ ] An unknown model shows counters without money - [ ] A channel-less agent shows no counters ### Watch out - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ## Not in this build - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. ## After v1, if you want it - A per-node spend ceiling that pauses the agent through its own channel, once metering is trusted - Git worktree creation per node, so parallel agents stop fighting over one checkout - A queue node that hands the next task to whichever agent finished, once hooks are reliable
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The multiplexer binary. Sessions are named by node id. MUX_BIN=tmux # Required. Where installed hooks write events. The canvas reads, never guesses. HOOK_SOCKET_PATH=~/.local/share/canvas/hooks.sock # Required. Per-session usage written by the agent's status line command. STATUS_FILE_DIR=~/.local/share/canvas/status # Required. Four rates per model and a checked-on date, shown beside totals. PRICE_TABLE_PATH=./config/prices.json # Required. Node positions and links, written atomically. LAYOUT_PATH=~/.local/share/canvas/layout.json # Required. Per node. Twelve unbounded buffers is how the canvas dies. SCROLLBACK_LINES=5000 # Required. Above this the canvas refuses with the reason rather than degrading quietly. MAX_NODES=16 # Required. Present as a constant. A guess is worse than showing nothing. INFER_STATUS=never
# Skribbl · product brief ## Problem The canvas is the easy half. React Flow plus xterm.js plus node-pty gets you panning, zooming and half a dozen live agent terminals in a weekend, and it is a genuinely nice way to watch a fleet work. The gaps are all in the boring layer underneath: knowing when an agent is actually blocked on you rather than just quiet, keeping sessions alive across a restart or a crash, and counting tokens accurately enough that the number can be trusted. Each of those is days of work on its own, and the weekend build reads as finished until the first agent silently dies behind a tab you were not looking at. ## Product outcome A canvas where a dozen agents run for a day, survive the window crashing, show when they are genuinely waiting for you, and display a spend figure you can act on before the bill. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20 and native build tools for node-pty - A coding agent CLI already installed and authenticated - tmux, if you want sessions to survive a restart ## Explicit non-goals for v1 - Multiplayer and cloud sync. - Code signing and notarisation. - Message passing between agents. - Status inferred from terminal output. - hook-driven agent status, so a blocked agent looks the same as an idle one - session survival across restarts and crashes - an accurate token meter, cache reads price nothing like input tokens - a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else ## Success criteria - Closing a node is confirmed to leave no child process - Relaunching is confirmed to reattach with scrollback - A hookless shell is confirmed to show unknown - Four counters are confirmed to match the agent's own figures - Twelve streaming nodes are confirmed under the latency bound
# Build brief · Skribbl The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a canvas for running several coding agents side by side that never guesses what an agent is doing to replace Skribbl. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two figures on this canvas are easy to fake and both must be real or absent. Status. From the outside, an agent quietly running a long build and an agent that stopped to ask you a question look identical: a terminal that has not printed for a while. Inferring 'blocked' from silence gets it wrong in both directions, and each wrong direction costs something, an hour of an agent waiting on you or an interruption of one that was fine. So status comes only from a channel the agent itself provides, such as its hooks, and where no such channel exists the node says unknown rather than guessing. Cost. A token count is worth showing only if it comes from the agent's own accounting, and only as four counters that are never summed: uncached input, cache writes, cache reads and output. They are priced differently. At the time of checking, a cache read costs a tenth of the input price, a five minute cache write one and a quarter times it and an hour long write twice, with per-model exceptions, so a single 'tokens' figure is wrong in both directions and a spend figure built from it cannot be acted on. ### Stack (fixed, do not substitute) - Terminals: A real shell per node through a pseudo-terminal, rendered by a terminal emulator · anything less than a real pty breaks the first full screen program - Canvas: A pan and zoom node graph with terminals as nodes and links as recorded relationships · watching a fleet side by side is the whole point - Durability: Each node runs inside a terminal multiplexer session named by node id · a crashed window reattaches instead of killing the agent - Status: Hook and event channels the agent itself exposes, never scraped output · a guess is worse than showing nothing - Metering: Four token counters from the agent's own accounting, priced from a dated table · cache reads and input tokens are not the same money - Runtime: A desktop application with native pseudo-terminal bindings · a browser cannot own a process ### Data model (create this before Phase 1) - nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something. ### Phase 1 · A real shell in a node Build: Spawn a shell per node through a pseudo-terminal in a chosen working directory and render it in a terminal emulator on a pan and zoom canvas. Propagate node resizes to the pty. Track process groups so closing a node ends everything it started. Bound scrollback. Trap: Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. Done when: A full screen program in a node redraws correctly after the node is resized. Closing a node leaves no child process, checked by listing processes afterwards. Six nodes typing simultaneously show no input lag, measured. Scrollback is bounded and the bound is stated. Do not build yet: No persistence, multiplexer, status or metering. ### Phase 2 · Sessions that outlive the window Build: Run each node's shell inside a multiplexer session named by node id, attaching if it exists and creating it if not. Persist node layout in the application's data directory with atomic writes. On launch, reattach, restore scrollback, and mark a node whose session has gone as dead rather than quietly spawning a fresh shell. Trap: Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. Done when: Killing the application and relaunching reattaches to every running agent with its scrollback, tested by killing the process mid-task. A node whose session died shows as dead with the exit reason where available. Layout writes are atomic, tested by killing during a save. A machine without the multiplexer refuses at startup with the install instruction. Do not build yet: No status or metering yet. ### Phase 3 · Status from the agent, or unknown Build: Install the agent's own hooks per node. For Claude Code that is a Notification hook, whose types include permission_prompt, idle_prompt and agent_needs_input, and a Stop hook that fires once per turn; read the current hooks reference before wiring them. Each hook writes an event carrying its session id to a socket the canvas reads. Map events to nodes by a per-node identifier. Any agent without such a channel shows unknown. Trap: Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. Done when: A permission prompt in a node changes its state within a second of the hook firing, and a completed turn changes it back, tested against a real agent. A node running a plain shell with no hooks shows unknown rather than idle. Hook events are matched to nodes by identifier rather than by which node printed last. No status is derived from terminal output anywhere, verified by review. Do not build yet: No metering or links yet. ### Phase 4 · Counting tokens without lying Build: Read usage from the agent's own accounting rather than the screen. For Claude Code, a status line command receives JSON on standard input carrying the usage broken out into input, cache creation and cache read tokens, plus an estimated cost at list price, debounced at three hundred milliseconds; check the current reference. Write it per session to a file the canvas reads. Keep the four counters apart, price each from a table with a checked-on date, and label the total as an estimate at list price. Trap: Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. Done when: Each node shows four counters that match the agent's own figures for the session, compared by hand. The canvas total is the sum of priced counters and shows the price table date. A model missing from the price table shows counters with no money figure rather than a wrong one. An agent with no accounting channel shows no counters rather than zero. Do not build yet: No links or export yet. ### Phase 5 · Twelve terminals without falling over Build: Draw links between nodes with a direction and store them as relationships only. Isolate the renderer from the pty host so a canvas crash never kills an agent. Bound memory per node and in total, throttle rendering for nodes outside the viewport, and measure frame time with twelve nodes streaming. Export a session as the layout, each node's transcript location and its usage. Trap: Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one. Done when: Twelve nodes streaming output keep typing latency in the focused node under a stated bound, measured. A renderer crash leaves every agent running and reattaches on recovery, tested by killing the renderer. Links persist with direction and deleting a node removes its links by a stated rule. Export contains the layout, transcript locations and usage in a readable format. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer and cloud sync, which turn a process supervisor into a distributed system. - Code signing and notarisation, which is a distribution project of its own, and without it the platform's gatekeeper blocks anyone you send a build to. - Message passing between agents through links, which is an orchestration protocol worth designing on its own rather than bolting onto a drawing. - Any status inferred from terminal output, because a guess is worse than showing nothing. ### README must contain - That the pty bindings must be rebuilt against your desktop runtime's version, which is the one thing that breaks on a fresh machine - That the multiplexer is required and sessions are named by node id, so a relaunch reattaches - Which hooks are installed per node, what they write and where - That spend is four counters priced from a dated table at list price, and what the date means - That unknown means no channel has reported, and is never replaced with a guess
# Architecture · Skribbl ## Stack | Part | Choice | Why | | --- | --- | --- | | Terminals | A real shell per node through a pseudo-terminal, rendered by a terminal emulator | anything less than a real pty breaks the first full screen program | | Canvas | A pan and zoom node graph with terminals as nodes and links as recorded relationships | watching a fleet side by side is the whole point | | Durability | Each node runs inside a terminal multiplexer session named by node id | a crashed window reattaches instead of killing the agent | | Status | Hook and event channels the agent itself exposes, never scraped output | a guess is worse than showing nothing | | Metering | Four token counters from the agent's own accounting, priced from a dated table | cache reads and input tokens are not the same money | | Runtime | A desktop application with native pseudo-terminal bindings | a browser cannot own a process | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Terminals | Real ptys with resize propagation and group kill. | Any emulator, provided the pty is real and closing kills the group. | | Durability | Named multiplexer sessions and atomic layout. | Any multiplexer, provided a missing session is shown as dead. | | Status | Hook events mapped by identifier. | Any agent channel. Not swappable for output scraping. | | Metering | Four counters priced from a dated table. | Any accounting source, provided the counters are never summed. | | Canvas | Nodes, links and throttled rendering. | Any graph view, provided off-screen nodes are throttled. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MUX_BIN` · required · The multiplexer binary. Sessions are named by node id. - `HOOK_SOCKET_PATH` · required · Where installed hooks write events. The canvas reads, never guesses. - `STATUS_FILE_DIR` · required · Per-session usage written by the agent's status line command. - `PRICE_TABLE_PATH` · required · Four rates per model and a checked-on date, shown beside totals. - `LAYOUT_PATH` · required · Node positions and links, written atomically. - `SCROLLBACK_LINES` · required · Per node. Twelve unbounded buffers is how the canvas dies. - `MAX_NODES` · required · Above this the canvas refuses with the reason rather than degrading quietly. - `INFER_STATUS` · required · Present as a constant. A guess is worse than showing nothing. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Skribbl product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A real shell per node through a pseudo-terminal, rendered by a terminal emulator, A pan and zoom node graph with terminals as nodes and links as recorded relationships, Each node runs inside a terminal multiplexer session named by node id, Hook and event channels the agent itself exposes, never scraped output, Four token counters from the agent's own accounting, priced from a dated table, A desktop application with native pseudo-terminal bindings. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. - Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one.
# Delivery milestones · Skribbl Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A real shell in a node Each node is a real process that resizes, scrolls and dies cleanly. ### Steps 1. Spawn through a pty and propagate resizes Full screen programs draw for the size the pty reports, not the size you see. Files: `src/main/pty.ts` 2. Kill the process group on close An invisible agent is still a billed agent. Files: `src/main/pty.ts`, `tests/no-orphans.test.ts` 3. Bound scrollback per node Twelve unbounded buffers is how the canvas dies at four in the afternoon. Files: `src/renderer/terminal.tsx` ### Done when - [ ] Full screen programs redraw correctly after a resize - [ ] Closing a node leaves no child process - [ ] Six nodes typing show no input lag - [ ] Scrollback is bounded with the bound stated ### Watch out - Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell. ## M2 · Sessions that outlive the window A crash or a restart reattaches to running agents rather than killing them. ### Steps 1. Attach or create by node id The session name is the node id, so a relaunch finds it without a lookup table. Files: `src/main/mux.ts` 2. Mark a missing session dead, never respawn silently A fresh shell in an old node's place is a lie with a cursor. Files: `src/main/mux.ts`, `tests/dead-session.test.ts` 3. Write the layout atomically A crash during a save should cost nothing. Files: `src/main/layout.ts` ### Done when - [ ] Relaunching reattaches to running agents with scrollback - [ ] A dead session is shown as dead - [ ] Layout writes survive a kill mid-save - [ ] A missing multiplexer refuses at startup ### Watch out - Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node. ## M3 · Status from the agent, or unknown A node shows waiting, working, finished or unknown, and never a guess. ### Steps 1. Install per-node hooks that write to a socket The agent knows when it is waiting. Nothing else does. Files: `src/main/hooks.ts`, `config/hooks.json` 2. Match events to nodes by identifier Not by which terminal printed last, which is wrong as soon as two are busy. Files: `src/main/status.ts` 3. Render unknown where nothing has fired Unknown is a true statement. Idle is a guess. Files: `src/renderer/status-badge.tsx` ### Done when - [ ] A permission prompt changes state within a second - [ ] A hookless shell shows unknown - [ ] Events match nodes by identifier - [ ] No status derives from terminal output ### Watch out - Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node. ## M4 · Counting tokens without lying Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table. ### Steps 1. Read usage from the agent's status channel Its own accounting, written to a file the canvas tails. Files: `src/main/usage.ts`, `config/statusline.sh` 2. Price from a dated table, never a sum Four rates, one date, and the date shown beside the total. Files: `src/main/pricing.ts`, `config/prices.json` 3. Show no figure rather than a wrong one A missing model or a missing channel produces a blank, not a zero. Files: `src/renderer/meter.tsx`, `tests/pricing.test.ts` ### Done when - [ ] Four counters match the agent's own figures - [ ] The total shows the price table date - [ ] An unknown model shows counters without money - [ ] A channel-less agent shows no counters ### Watch out - Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total. ## M5 · Twelve terminals without falling over (production only) A dozen live agents run for a day without the canvas becoming the bottleneck. ### Steps 1. Isolate the renderer from the pty host A canvas crash must cost a window, never an agent. Files: `src/main/host.ts`, `tests/renderer-crash.test.ts` 2. Throttle off-screen nodes and measure latency With twelve streaming. One node proves nothing. Files: `src/renderer/scheduler.ts` 3. Export layout, transcripts and usage So a day's work can be read somewhere other than this canvas. Files: `src/main/export.ts` ### Done when - [ ] Typing latency stays under the bound with twelve streaming - [ ] A renderer crash leaves agents running - [ ] Links persist with direction and delete by rule - [ ] Export is readable outside the canvas ### Watch out - Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one.
# Operations · Skribbl ## Backup The layout file and the price table are the only state the canvas owns, and both are small. The agents' transcripts live with the agents and are theirs to back up. ## Restore Restore the layout and relaunch, and confirm that every node either reattached to a live session or shows as dead. A restore that quietly spawned fresh shells has lost the work the sessions were protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Input latency in the focused node with everything streaming, orphaned processes after closes, and hook events per node. A node with no events for a day almost always means its hooks are not installed rather than that it never waited. ## Incident checklist An agent lost mid-task is the incident, and it means the multiplexer was missing or a node respawned instead of reattaching. A spend figure that disagrees with the bill means the counters were summed or the table is stale, and the date beside the total says which. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Closing a node is confirmed to leave no child process - [ ] Relaunching is confirmed to reattach with scrollback - [ ] A hookless shell is confirmed to show unknown - [ ] Four counters are confirmed to match the agent's own figures - [ ] Twelve streaming nodes are confirmed under the latency bound ## Launch constraint Do not market omitted Skribbl capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The multiplexer binary. Sessions are named by node id. MUX_BIN=tmux # Required. Where installed hooks write events. The canvas reads, never guesses. HOOK_SOCKET_PATH=~/.local/share/canvas/hooks.sock # Required. Per-session usage written by the agent's status line command. STATUS_FILE_DIR=~/.local/share/canvas/status # Required. Four rates per model and a checked-on date, shown beside totals. PRICE_TABLE_PATH=./config/prices.json # Required. Node positions and links, written atomically. LAYOUT_PATH=~/.local/share/canvas/layout.json # Required. Per node. Twelve unbounded buffers is how the canvas dies. SCROLLBACK_LINES=5000 # Required. Above this the canvas refuses with the reason rather than degrading quietly. MAX_NODES=16 # Required. Present as a constant. A guess is worse than showing nothing. INFER_STATUS=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
Not for the canvas. For the layer that keeps twelve child processes honest: status that comes from the agent itself instead of scraped output, sessions that come back after a reboot, and a spend number accurate enough to act on before the bill arrives.
xhook-driven agent status, so a blocked agent looks the same as an idle one
xsession survival across restarts and crashes
xan accurate token meter, cache reads price nothing like input tokens
xa signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else
Skribbl pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| founding infinity | $29/user | $8.25/user | 3 concurrent agents; founding-plan access while licenses remain available. |
free tierno free tier; 14-day trial only
billingmonthly or annual
hidden costsThe plan does not include Claude Code, model API or other agent-provider subscriptions; the trial requires a card from day 2 and charges on day 15 unless cancelled.
verified 2026-08-14 · source ↗
Vibecode Skribbl
Kinda. The core of Skribbl is buildable in a weekend with the prompt on this page, but there are real gaps: hook-driven agent status, so a blocked agent looks the same as an idle one, session survival across restarts and crashes. Read the honest list above before committing.
How much does Skribbl cost?
Skribbl costs about $8.25/month (Pro, checked 2026-08-03), which is $99 per year.
What do I lose by replacing Skribbl?
Honestly: hook-driven agent status, so a blocked agent looks the same as an idle one; session survival across restarts and crashes; an accurate token meter, cache reads price nothing like input tokens; a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Skribbl?
Yes: claude-squad (open-source terminal manager for multiple agents in tmux and git worktrees), Crystal (open-source desktop app running parallel agent sessions in worktrees), vibe-kanban (open-source board for queueing and reviewing coding agent tasks). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.