parallax

Drive Parallax — a simulation runtime whose results carry how much of them was real. Point it at a context (a directory, an agent workspace, a table list), it proposes an ontology built from what is a

SKILL.md

GitHub

parallax

Parallax is a simulation runtime built so that it cannot lie about being a simulation. It is not trying to be a simulator that is right; it is trying to be one whose reproducibility you can check in five seconds.

It is the bstack's ontology simulation layer: where a proposed change is modelled and accepted before it is applied anywhere else.

Repository: https://github.com/broomva/skills, at skills/simulation/parallax/ · hub: https://parallax-hub.onrender.com

Install

There is one copy of this skill and it is this one, so there is one address:

npx skills add broomva/skills --skill parallax -g -a claude-code

The runtime comes with it, at runtime/ inside the installed skill -- installing the skill installs the layer. Building it is what puts parallax on PATH, and it needs Bun.

The path below is where -g -a claude-code puts it. This block names it in full rather than opening with a bare cd runtime, because a reader copies these lines from wherever they happen to be standing, and a relative cd that silently depends on an unstated cwd is the same defect this skill's own tests/test_skill_md_claims.py exists to catch:

cd ~/.claude/skills/parallax/runtime
bun install
bun link            # puts `parallax` on PATH
parallax help

Installed somewhere else -- a different agent target, a project-scoped install, or a clone of the monorepo -- substitute that location; the three commands are the same, and runtime/ is always directly inside the skill.

Or drive it as tools inside an agent, with no dependency on ai or zod:

import { generateText } from "ai";
import { parallaxTools } from "./runtime/src/tools";

await generateText({ model, tools: parallaxTools(), prompt });

The flow

Nine commands, one for each tool. The order a thread uses them:

Step Command What it is
1 parallax status where this workspace stands, read from disk
2 parallax propose an ontology built from what is actually in the directory
3 parallax render re-send the proposal byte-for-byte
4 parallax parse-reply --text <msg> classify a human's reply; records nothing
5 parallax answer --answer <n>=<value> record answers without accepting
6 parallax accept --by <who> the gate: nothing simulates before this
7 parallax run --horizon N --seed N roll forward, write a receipt
8 parallax receipt --run <id> locate or export the receipt
— parallax reject --reason <why> archive a refusal; refusals are kept

Add --json to any of them for the {ok, value} envelope. Same values, different framing.

The four rules that are not guessable

1. Read the state from disk, never from the conversation. A Parallax session is a new OS process every turn, and an ActiveOntology is branded with a module-private symbol that does not survive a JSON round-trip — trust cannot be serialised. So "we already accepted that" is not a thing you can remember; it is a thing you look up. Call parallax status first on any turn that mentions accepting, running, answering, or a previous proposal.

2. Relay text verbatim. Never restate a number. propose and run return a rendered text. Send it as-is. A restated number is an invented number the moment it is wrong, and a paraphrased proposal that a human then accepts is an acceptance of the paraphrase — which is why render exists and re-summarising does not.

3. n indexes the STORED proposal. The numbering the human was shown is the numbering that binds them. Never renumber the questions yourself.

4. A refusal is a code, not a sentence. Every surface returns {ok:false,error:{code,reason,detail?}}. reason is for the human; code is what you branch on. Exit 2 is a typed refusal — a gate doing its job. Exit 1 is a defect.

The deterministic core

Two lookups, because reasoning about them is how they go wrong:

Both run from the workspace Parallax is reading, so the script is addressed by its own location and never relatively. parallax status reports on the directory you are standing in; cd-ing into the skill to reach the script would make the skill directory the workspace, which fails silently by succeeding on the wrong context.

SKILL="$HOME/.claude/skills/parallax"   # a checkout instead: <repo>/skills/simulation/parallax

# what to do next, from the state on disk
parallax status --json | python3 "$SKILL/scripts/parallax_next.py" --status -
#   next: parallax answer --proposal 94c50e77db74 --answer 1=<value>
#   why:  1 blocking question(s) open, no answers recorded yet. ...

# what a refusal actually means
parallax run --json 2>err.json || python3 "$SKILL/scripts/parallax_next.py" --error err.json
#   NO_ACCEPTED_ONTOLOGY
#     Nothing has been accepted in this workspace. Accept a proposal first.

next comes back null when no Parallax command applies -- an unreadable workspace is the case -- rather than naming the command that produced the result.

--json on either for a machine-readable answer. Exit 0 determined, 3 the document parsed but names no state or code on file, 2 the input is not that document.

scripts/parallax_next.py carries a remedy for all 52 error codes, and the set is pinned against references/error-codes.txt — captured from runtime/src/tools/errors.ts at a named commit. A code added in the runtime with no remedy here goes red rather than being answered with an invented one.

The two refusals that look like bugs and are not

RECONCILIATION_UNACKNOWLEDGED on accept. The executable domain ignores fields the human was shown as read from their own context. Tell them first — relay detail.unmappedFromContext — and only then retry with --acknowledge-unmapped. Setting the flag without telling them defeats the gate, which is the product.

An empty workspace that should not be empty. Inside a sandboxed session a derived path is denied, and a denied read comes back as an empty directory rather than an error. So a wrong path does not fail; it reads as "your workspace has nothing in it". Do not retry with a path you computed. There is no path argument on the tool surface for exactly this reason — --root is CLI-only, where the person typing the path is the confinement.

What Parallax will not tell you

Nothing here is calibrated against a real business. There are no transcripts. The runtime README and the landing page both say so, and it should stay said. If you need an accuracy number, Parallax deliberately does not publish one — determinism is what it offers instead.

An MCP server is not built. The seam is cut and reads finished — toolSpecs() hands out every tool as data, and a test asserts every tool name is legal as an MCP tool name — but only the transport is missing. Do not tell anyone to connect over MCP.

Contributing to Parallax itself

Read runtime/AGENTS.md. The short version: three gates, all run from runtime/, in order — bun test, bun run typecheck (bun test does not typecheck), bun run mutants (a passing suite is not a testing suite).

Tests

Run from the skill's own directory. The path below is relative on purpose, because this skill has two homes and only a relative path is correct in both: skills/simulation/parallax/ in broomva/skills (canonical), and ~/.claude/skills/parallax/ once installed.

python3 -m pytest tests/ -q          # 44 passed (37 hermetic + 7 live-CLI)

The 7 live-CLI tests skip unless parallax is on PATH (bun link in this skill's runtime/). A run reporting 37 passed, 7 skipped is the expected result without it; 44 passed means the remedy table was checked against the real binary.

tests/test_skill_md_claims.py asserts the path and the count above are true, because this section was wrong when it shipped: it named skills/tooling/parallax/tests/, which does not resolve from an install, and said 29 when there were 36. A documented command a reader copies and cannot run teaches them the skill is broken -- and a skill with more than one home is exactly where an absolute path rots first.