The client for ultrapowers: /ultrapowers <plan-path> commits an approved plan and runs on an exe.dev fleet you provision — a disposable sandbox runs the plan…

_ _ _ ___ ____ ____ ___ ____ _ _ _ ____ ____ ____
| | | | |__/ |__| |__] | | | | | |___ |__/ [__
|__| |___ | | \ | | | |__| |_|_| |___ | \ ___]
Give your Superpowers Ultrapowers.
Ultrapowers writes the plan and runs it. It grew up alongside superpowers, the popular agent skill for a disciplined build workflow, and it now carries its own plan-authoring skill — so you can hand Claude Code a big, ambitious idea and get back finished, reviewed work, without paying frontier-model prices to build every line, and without reviewing code you'd rather not read.
Have you ever asked Claude to do ambitious work and blown through your five-hour token budget before it finished? (If you haven't, you're not trying hard enough;)
Frontier models invite bigger asks. "Product people" love this. You can finally think at the scale of the whole feature, the whole idea, the whole business, instead of the next function. But pointing a frontier model at a huge body of work and saying "build it" is the most expensive way to get it done. The frontier model writes every line, reviews itself, and drags one ever-growing transcript the whole way. You max out your subscription-subsidized tokens, and the work still isn't finished.
ultrapowers makes ambition affordable by using your frontier model only where it changes the outcome.
Honest caveat: the parallel-and-review machinery can cost more than a plain sequential run — it's running extra reviewers and isolated build environments. What it saves you from is what you'd reach for otherwise: asking one frontier model to build the whole thing directly. Against that, the savings are real. ultrapowers isn't "cheap" — it's disciplined. It refuses to waste your tokens.
1. Affordable ambition. You don't pay frontier prices to write boilerplate. Your frontier model plans and checks; cheaper models do the building in between.
2. Quality you don't have to babysit. Every task is checked by a separate reviewer before it counts — so you don't have to read the code yourself. You make one approval at the end, on the finished result, instead of signing off on code you can't (or don't want to) judge.
3. Faster finishes. (Sometimes.) When the plan allows, independent work runs at the same time instead of one piece after another, so big work lands sooner. That's the cherry, not the headline.
Superpowers is a popular Claude Code skill that gives Claude a disciplined way to build software: brainstorm the idea, write a plan, execute it, review it. It's excellent — but it builds one task at a time, and it keeps you in the loop the whole way.
Superpowers is an optional companion, not a requirement: brainstorming and its practice skills pair well with ultrapowers, but plan authoring is ultrapowers' own — the ultrawrite skill writes the plan. Bring a spec (however you arrived at it) and ultrapowers takes it from there. Same discipline, less babysitting.
Reach for ultrapowers on big, ambitious plans with independent pieces — the kind where parallel work and independent review pay off.
Skip it for small or tightly-connected plans, where the work has to happen in order anyway. There, a plain sequential run is the better tool — and ultrapowers will tell you so rather than spin up machinery you don't need.
In field use the human surface compresses to exactly the designed touchpoints: on a recent 7-task production run the operator's entire involvement was the planning decisions and one physical-world check no tooling could reach (confirming an email landed in a personal inbox) — everything between launch and the pre-merge gate ran autonomously, and the operator's unrelated work-in-progress was restored byte-for-byte.
ultrapowers runs on an exe.dev fleet you provision — the plugin is the client, and there is one engine, the Flock (factory/flock/engine.mjs). /ultrapowers <plan-path> publishes your approved plan and starts a run on a disposable sandbox of its own; every builder, every probe, every merge and every check execute there. Nothing builds, tests, or merges on your machine. When the run ends, the sandbox opens the pull request on the repository you ran in — ready if its own checks ended green, a draft otherwise — with the evidence linked in its body. If main moved while the run worked, the sandbox first joins the run's work onto the new main (factory/flock/catchup.mjs) and re-runs the plan's probes and checks there; only then does it merge its own pull request. A conflict or a red check leaves that pull request open and unmerged for you.
The clearest way to see what it does is to zoom in — the whole plan, then one task.
Here's a question that sounds simple and isn't: in any plan, which work has to happen in order — and which doesn't?
We do things one after another out of habit, and because a single worker can only hold one task at a time. But most of that sequence is an illusion. You can't paint a wall before it's built; that order is real, forced by the work itself. Two different walls, though? Two crews raise them at the same time, and nothing in the world objects. The only thing that genuinely makes one task wait for another is dependency — one task needing something another produces. Everything else only looks sequential because we're used to one pair of hands.
Take away the false order and you're left with the real skeleton of the job: a web of this must come before that. It's smaller than the to-do list makes it look. And it has an exact shape.
That shape has a name — a directed acyclic graph. Directed, because every arrow points from a task to the task that depends on it. Acyclic, because nothing can depend on itself, even the long way around a loop; if it could, the work could never start. Draw each "must-come-before" as an arrow and your plan stops being a list and becomes a map. Tasks with no path between them can run side by side. The depth of the graph is how long the work really takes; its width is how much can happen at once.
<img src="docs/assets/dag.gif" width="840" alt="An approved plan drawn as a directed acyclic graph. A single plan node fans out into independent tasks; dependency arrows cross between them, each task waiting only for what it needs; everything fans back into one merged pull request. Arrows point from a task to the work that depends on it.">
ultrapowers reads your approved plan and derives exactly this graph — from the files each task touches and the probes each task runs, not from a list you write — then runs it as a pool: every task whose dependencies have landed is started at once, and a task that needs another starts the moment that one lands. There is no round to wait for; the shape of the work is the only clock.
Pick any one of those circles. Up close, it isn't a dot — it's a claim on a shared board.
The run is a flock of builder agents with no leader. Each builder claims a task from the board and works on its own copy of the repository, only from the task's contract — what must be true afterwards, and the proof that decides it. Between tool calls, each copy takes in what its peers have published through a content-level merge, so a task that reads what another task wrote builds against it as it lands, not after a round. The pool grows and shrinks with the work that is claimable.
Work is measured, not reviewed: a task is done when its own Run: probes pass on the builder's copy, and every published snapshot is tested at the edge with the run-wide Check: lines. A judge reads only what a probe cannot see, and never blocks a task.
If main moved while the run worked, the sandbox catches the run up: it joins the run's work onto the new main through the same merge and re-runs the plan's setup, probes and checks there. A conflict or a red check leaves a draft pull request instead.
The engine that runs all of this is committed and frozen at the sha the launch names; it never writes a fresh version of itself at runtime. Same plan in, same structure out. And every run happens in a sandbox that exists only for that run — nothing it does can touch your checkout, and nothing it leaves behind survives it except the evidence it pushed.
None of this is magic, exactly. It's all premised on a handful of older, sturdy ideas — dependency graphs, clean clones, content merges, disposable sandboxes — each doing one small job well.
A TinyApp's screens come out in the look of 1st-Pouf, a small open-source component set by Mojtaba Beheshti, used here under its MIT licence and kept exactly as its author published it. Nobody writes page code for a screen: each one is a short description of which pieces it shows and which action each button runs. The run checks every screen itself and catches a button that is missing or wired to the wrong action before you see it.
While a TinyApp is being planned, you see its real screen in Claude's Browser pane, not a mock-up. Jev offers two or three arrangements of it, built only from what the app can actually do, and you pick the one you like. Then you pin notes on any part of it in Comment mode — click a button or drag a box around an area and say what should change — and watch each story play out on it. What you sign is the screen the fleet starts building from.
Four steps, once each. All of it happens inside Claude Code: the agent does the work and asks you to choose. The only things it cannot do for you are the consents that are yours to give in a browser.
From inside Claude Code:
/plugin marketplace add popmechanic/ultrapowers
/plugin install ultrapowers@ultrapowers
Superpowers is optional. ultrapowers authors and runs plans on its own. Install Superpowers alongside it if you want its brainstorming and practice skills as companions.
/ultrapowers setupRun /ultrapowers setup and answer what it asks. The agent runs the doctor, reads its rows — exe-dev, capacity, claude, accounts, github, integrations, evidence, verb-drift, kata, cloudflare — and fixes every red row with you, offering each choice as options rather than asking you to invent an answer. Setup is safe to re-run: the doctor (fleet/doctor.mjs) only reports, and setup only touches what is still red.
Your evidence repository. Every run's plan and record live in one private repository of your own, never in the repository being built — so you can run on someone else's repository too. The evidence row walks its one-time setup in three steps: name it in ~/.ultrapowers/fleet.json as "evidence": "<owner>/<repo>" (a launch can override it with --evidence-repo <owner>/<repo>), create it with gh repo create <owner>/<repo> --private, and give the fleet its integration with node fleet/target.mjs <owner>/<repo>. A run on <owner>/<repo> then leaves its record in that repository's runs/<owner>-<repo>/<N>/, at the tag <owner>-<repo>/run-<N>.
Three browser consents are yours to give, because only you can give them. You sign up at exe.dev and add your ssh key; you approve the GitHub app on your account; you approve ultrapowers on claude.ai, which hands back a single code, copied to your clipboard, where the agent picks it up. Everything else setup does itself.
What a run costs. A run bills your Claude Max subscription through an edge proxy, plus one exe.dev VM for the hours the run is up.
Authoring needs no fleet and no account. A session begins with one sentence — what you will be able to see or do after the run — and there are three places that sentence comes from:
ultrawrite skill and name the issue: that sentence becomes the plan's Claim, quoted, and you confirm it rather than write it again.ultrawrite and it asks you scenario questions — after this run, what can you see that you couldn't before? — with two or three concrete options to pick from. If you have Superpowers installed, its brainstorming skill is a good companion here; it ends by handing off to ultrawrite.ultrawrite takes unchanged.Whichever way you arrive, ultrawrite writes a claims-v1 plan — each task a contract and the proof that decides it, no steps to retype. You read that plan and approve it. That is the first of your two checkpoints.
In the repository you want built, run /ultrapowers <plan-path>. The plan rides to the sandbox on your evidence repository's live/<owner>-<repo>/run-<N> branch, and the run happens there: the builders, the probes, the merges, the checks. Watch it or walk away.
At the end you get the finished result: the sandbox opens the pull request on that repository — ultrapowers itself is just one such repository, and ultra/integration-run-<N> is the only branch a run ever pushes to it. Its body carries the gate receipt and links the run's record in your evidence repository. The pull request merges itself on the run's own evidence — once its own gate is green and main has not moved off the tip it caught up to. Launch with --hold and the pull request stays open instead — your second checkpoint, yours to merge or close. A run the gate parked leaves a draft pull request, and the sandbox merges nothing after a park: acknowledge it by hand — mark it ready, gh pr update-branch <N> if it is behind main, gh pr merge --squash <N>. Either way the run is over and the sandbox is gone.
--act-as-user is unavailable on team integrations. On an exe.dev team account that means every pull request a run opens is authored by exe-dev-github-integration[bot] rather than by you, and nothing downstream can undo it. Set the fleet up on your personal exe.dev account, and if setup offers you a choice of accounts, the GitHub integration has to stay personal.
Go deeper. The full mechanics — how plans become parallel work, how reviews are anchored, how the engine handles failure — live in skills/ultrapowers/SKILL.md and skills/ultrapowers/references/.
When Claude has a screen ready for you to look at, it opens the intent tray beside the chat (its tool for that is show_screen). The tray is where you tell Claude what you want from that screen: pick a version, add notes, and send them all back with one press.
A band above the prompt carries three buttons: Add note (n), Send (s) and Clear (c). Click a button, or press its letter once the band has focus (ctrl+x tab). The band shows only while the tray holds a screen, pick or note, and Send and Clear empty it. In the pane the note box takes typing, so click a version, Send or Clear there. /tray opens the tray yourself, and /note <element>: <note> adds a note about one element. Send hands everything — your pick first, then each note in the order you added it — to Claude as one message.
A preview page can leave picks and notes for the tray too: it writes them, one JSON object per line, to .ultrapowers/feedback.jsonl in the session's working directory, and the tray takes them in as if you had made them.
The tray needs Claude Code 2.1.287 or later.
hooks/register.js 337 lines1// Intent tray mod: Claude shows a screen through the show_screen tool, the
2// operator picks a version and adds notes in a pane beside the chat (or in
3// the preview page, which appends to .ultrapowers/feedback.jsonl), and Send
4// hands everything to Claude as one prompt holding a ```json block.
5
6const PANE = { id: "intent-tray", title: "Intent tray", rows: 10 };
7const FEEDBACK = ".ultrapowers/feedback.jsonl";
8const FENCE = "`".repeat(3);
9
10let screen = null; // { name, stage, versions, url }
11let pick = null;
12let notes = []; // { kind, target?, note }
13let offset = 0; // bytes of the feedback file already taken (always ends at a newline)
14let cwd = "";
15
16// One tray per working directory: sessions in other repos never see this one.
17function trayKey() {
18 return `tray:${cwd.replace(/\/+$/, "")}`;
19}
20
21async function save($) {
22 await $.store.set(trayKey(), { screen, pick, notes, offset });
23 $.ui.invalidate();
24}
25
26function empty() {
27 screen = null;
28 pick = null;
29 notes = [];
30}
31
32function hasContent() {
33 return Boolean(screen || pick || notes.length);
34}
35
36function feedbackPath() {
37 return cwd ? `${cwd.replace(/\/+$/, "")}/${FEEDBACK}` : FEEDBACK;
38}
39
40// "element: note" is an element note; text without a colon is a view note.
41function parseNote(text) {
42 const s = String(text || "").trim();
43 if (!s) return null;
44 const i = s.indexOf(":");
45 if (i < 0) return { kind: "view", note: s };
46 const target = s.slice(0, i).trim();
47 const note = s.slice(i + 1).trim();
48 if (!target) return note ? { kind: "view", note } : null;
49 if (!note) return null;
50 return { kind: "element", target, note };
51}
52
53function record(n) {
54 return n.kind === "view"
55 ? { kind: "view", note: n.note }
56 : { kind: n.kind, target: n.target, note: n.note };
57}
58
59async function addNote($, text) {
60 const n = parseNote(text);
61 if (!n) return;
62 notes.push(n);
63 await save($);
64}
65
66async function choose($, version) {
67 pick = version;
68 await save($);
69}
70
71async function send($) {
72 const recs = [];
73 if (pick != null) recs.push({ kind: "pick", screen: screen ? screen.name : "", chose: pick });
74 for (const n of notes) recs.push(record(n));
75 if (!recs.length) return;
76 const name = screen ? screen.name : "the screen";
77 const text =
78 `Feedback from the intent tray on ${name}:\n\n` +
79 `${FENCE}json\n${JSON.stringify(recs)}\n${FENCE}`;
80 let entered;
81 try {
82 entered = await $.prompt.submit({ text });
83 } catch {
84 return; // the tray keeps its notes
85 }
86 if (!entered || entered.drop != null) return;
87 empty();
88 await save($);
89}
90
91async function clear($) {
92 empty();
93 await save($);
94}
95
96async function openTray($) {
97 await $.ui.open({ ...PANE, focus: true });
98}
99
100// The file's bytes up to and including its last newline, or null when it cannot be read.
101async function wholeLines($) {
102 let text;
103 try {
104 text = await $.fs.read(feedbackPath());
105 } catch {
106 return null;
107 }
108 const bytes = new TextEncoder().encode(String(text));
109 return bytes.subarray(0, bytes.lastIndexOf(10) + 1);
110}
111
112async function readFeedback($) {
113 const held = await $.store.get(trayKey());
114 const stored = held && typeof held === "object" ? Number(held.offset) : 0;
115 if (stored > offset) offset = stored; // another session in this repo took them
116 let size;
117 try {
118 size = (await $.fs.stat(feedbackPath())).size;
119 } catch {
120 return;
121 }
122 if (size < offset) offset = 0; // the file was replaced
123 if (size === offset) return;
124 const bytes = await wholeLines($);
125 if (!bytes || bytes.length <= offset) return;
126 const lines = new TextDecoder().decode(bytes.subarray(offset)).split("\n").filter((l) => l.trim());
127 for (const line of lines) {
128 let r;
129 try {
130 r = JSON.parse(line);
131 } catch {
132 continue;
133 }
134 if (!r || typeof r !== "object") continue;
135 if (r.kind === "pick") {
136 if (r.chose != null) pick = String(r.chose);
137 } else if (r.note != null && String(r.note).trim()) {
138 const note = String(r.note);
139 if (r.target != null && String(r.target).trim()) {
140 const kind = r.kind === "region" || r.kind === "view" ? r.kind : "element";
141 notes.push(kind === "view" ? { kind, note } : { kind, target: String(r.target), note });
142 } else {
143 notes.push({ kind: "view", note });
144 }
145 }
146 }
147 offset = bytes.length;
148 await save($);
149}
150
151// Run one command to its end; true when it exits 0, false when it fails or cannot start.
152async function run($, argv) {
153 try {
154 const stream = $.process.spawn({ argv });
155 for await (const _ of stream);
156 const { code } = await stream.result;
157 return code === 0;
158 } catch {
159 return false;
160 }
161}
162
163// macOS has `open`; elsewhere (Debian's `open` is openvt) fall back to xdg-open.
164async function openScreen($) {
165 const url = screen && screen.url;
166 if (!url) return;
167 if (await run($, ["open", url])) return;
168 await run($, ["xdg-open", url]);
169}
170
171function noteLabel(n) {
172 return n.kind === "view" ? `[view] ${n.note}` : `[${n.kind}] ${n.target}: ${n.note}`;
173}
174
175function drawPane($, e) {
176 const { Box, Text, Button, Input } = $.ui.resolve(e);
177 const versions = (screen && screen.versions) || [];
178 const head = screen
179 ? `${screen.name}${screen.stage ? ` (${screen.stage})` : ""}`
180 : "No screen shown";
181 const top = [
182 Text({ key: "screen", children: [head + (pick != null ? ` picked: ${pick}` : "")] }),
183 ...versions.map((v, i) =>
184 Button({
185 key: `pick-${v}`,
186 hotkey: i < 9 ? String(i + 1) : undefined,
187 onPress: () => choose($, v),
188 children: [pick === v ? `[${v}]` : v],
189 })
190 ),
191 ...(screen && screen.url
192 ? [Button({ key: "open-screen", onPress: () => openScreen($), children: ["Open screen"] })]
193 : []),
194 ];
195 const rows = notes.map((n, i) =>
196 Box({
197 key: `note-${i}`,
198 flexDirection: "row",
199 children: [
200 Text({ key: `text-${i}`, children: [noteLabel(n)] }),
201 Button({
202 key: `drop-${i}`,
203 onPress: async () => {
204 notes.splice(i, 1);
205 await save($);
206 },
207 children: ["x"],
208 }),
209 ],
210 })
211 );
212 return Box({
213 key: "tray",
214 flexDirection: "column",
215 children: [
216 Box({ key: "top", flexDirection: "row", children: top }),
217 Input({
218 key: "view-note",
219 autoFocus: true,
220 placeholder: "element: note, or a note on the whole view",
221 onSubmit: (text) => addNote($, text),
222 }),
223 Box({
224 key: "actions",
225 flexDirection: "row",
226 children: [
227 Button({ key: "send", hotkey: "s", onPress: () => send($), children: ["Send"] }),
228 Button({ key: "clear", hotkey: "c", onPress: () => clear($), children: ["Clear"] }),
229 ],
230 }),
231 Box({
232 key: "notes",
233 flexDirection: "column",
234 children: rows.length ? rows : [Text({ key: "none", children: ["No notes yet"] })],
235 }),
236 ],
237 });
238}
239
240function drawBand($, e) {
241 const { Box, Text, Button } = $.ui.resolve(e);
242 const count = notes.length + (pick != null ? 1 : 0);
243 const label = `Intent tray: ${screen ? screen.name : "feedback"}, ${count} item${count === 1 ? "" : "s"}`;
244 return Box({
245 key: "band",
246 flexDirection: "row",
247 children: [
248 Text({ key: "band-label", children: [label] }),
249 Button({ key: "band-note", hotkey: "n", onPress: () => openTray($), children: ["Add note"] }),
250 Button({ key: "band-send", hotkey: "s", onPress: () => send($), children: ["Send"] }),
251 Button({ key: "band-clear", hotkey: "c", onPress: () => clear($), children: ["Clear"] }),
252 ...(screen && screen.url
253 ? [Button({ key: "band-open", onPress: () => openScreen($), children: ["Open screen"] })]
254 : []),
255 ],
256 });
257}
258
259export function register(on) {
260 on("session.start", async ($, e, next) => {
261 cwd = (e && e.cwd) || "";
262 const saved = await $.store.get(trayKey());
263 if (!saved || typeof saved !== "object") {
264 // Lines written before this repo's first session are history.
265 offset = (await wholeLines($))?.length ?? 0;
266 await save($);
267 } else {
268 screen = saved.screen || null;
269 pick = saved.pick != null ? saved.pick : null;
270 notes = Array.isArray(saved.notes) ? saved.notes : [];
271 offset = Number(saved.offset) || 0;
272 }
273 await $.command.register({ name: "tray", description: "Open the intent tray", immediate: true });
274 await $.command.register({
275 name: "note",
276 description: "Add a note to the intent tray: /note <element>: <note>, or /note <note>",
277 argumentHint: "[element: note]",
278 immediate: true,
279 });
280 await $.tool.register({
281 name: "show_screen",
282 description:
283 "Show the operator a screen in the intent tray so they can pick a version and add notes; then wait for their feedback.",
284 inputSchema: {
285 type: "object",
286 properties: {
287 name: { type: "string", description: "Name of the screen" },
288 stage: { type: "string", description: "What this round is about" },
289 versions: { type: "array", items: { type: "string" }, description: "Version labels to pick from" },
290 url: { type: "string", description: "Address of the preview page, opened by the tray's Open screen button" },
291 },
292 required: ["name"],
293 },
294 });
295 $.clock.every(1000, () => readFeedback($));
296 return next(e);
297 });
298
299 // A registered command is answered by a command.run hook; CommandSpec has no callback field.
300 on("command.run", { command: "tray" }, async ($) => {
301 await openTray($);
302 return {};
303 });
304
305 on("command.run", { command: "note" }, async ($, e) => {
306 const before = notes.length;
307 await addNote($, e.args);
308 return notes.length > before
309 ? { text: `Added to the intent tray (${notes.length} ${notes.length === 1 ? "note" : "notes"})` }
310 : { text: "Usage: /note <element>: <note>, or /note <note> for the whole view" };
311 });
312
313 on("tool.call", { tool: "mcp__ultrapowers__show_screen" }, async ($, e, next) => {
314 const input = (e && (e.input || e.args)) || e || {};
315 const name = String(input.name || e.name || "Screen");
316 const stage = input.stage != null ? String(input.stage) : e.stage != null ? String(e.stage) : "";
317 const raw = Array.isArray(input.versions) ? input.versions : Array.isArray(e.versions) ? e.versions : [];
318 const url = input.url != null ? String(input.url) : e.url != null ? String(e.url) : "";
319 empty();
320 screen = { name, stage, versions: raw.map(String), url };
321 await save($);
322 await $.ui.open(PANE);
323 return {
324 result:
325 `Showing "${name}" in the intent tray${screen.versions.length ? ` with versions ${screen.versions.join(", ")}` : ""}. ` +
326 "Wait for the operator's feedback: it arrives as their next message with a json block of picks and notes.",
327 };
328 });
329
330 on("ui.render", { component: "Pane", requestId: "intent-tray" }, async ($, e) => drawPane($, e));
331
332 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
333 if (!hasContent()) return next(e);
334 return drawBand($, e);
335 });
336}
337