Contributing to pixtuoid
August 18, 2026 · View on GitHub
Thanks for your interest! PRs are welcome — especially new themes, sprite and
decoration polish, and Source adapters for agent CLIs we don't support yet
(the agent CLIs plus the OpenClaw gateway already wired up are listed in the README).
Before you start, read CLAUDE.md at the repo root (and the
nested crates/*/CLAUDE.md for the crate you touch). It holds the load-bearing
architecture invariants and conventions, and indexes each crate's "known sharp
edges" — full text in that crate's SHARP-EDGES.md. Many things that look like
bugs are documented, intentional design: read the entry, not just its index line.
Build & test
Requires a recent stable Rust toolchain and just
(brew install just). On Linux you also need lld (apt install lld). The
justfile is the single source of truth for every check — CI and the git hooks
call the same recipes.
just # list recipes
just preflight # full pre-push gate: lint → clippy → hack → test (the exact CI order;
# lint's `prose` arm fetches origin/main, so preflight needs network)
just fmt # auto-format
just test # the whole suite (cargo-nextest if installed, else cargo test)
cargo nextest run -p <crate> <filter> # fast loop while iterating on one crate
Don't chain
cargo clippy && cargo test— clippy and test use separate build caches, so chaining recompiles the whole workspace twice. Runjust preflight(the exact CI order), or one check at a time.
Activate the git hooks once per clone: git config core.hooksPath .githooks
(pre-commit = just fmt-check; pre-push = just preflight).
CI gates
just preflight is the local gate; these run only in CI, so a green preflight
does not mean a green PR:
- semver — a breaking change to
pixtuoid-core/pixtuoid-scenewithout a minor bump fails (the binary's lib target is not a semver surface). - api-surface — committed
cargo public-apigoldens atapi/<crate>.txt; regenerate withjust api-surface+ commit when the public surface moves. - docs —
cargo docwith-D warnings(broken/private intra-doc links deny) plus the doctests nextest skips. - coverage/smoke · gen-check · gen-readme-check · npm-check — committed media, README and npm manifest freshness.
- check-windows — msvc cross-lint on every PR.
- snapshots —
cargo insta; fails on a pending OR orphan.snap, the rot plaincargo testcan't see. - hygiene — the same
just lintrecipes preflight runs (its CI job exists so a skipped local preflight can't land a lint break), includingjust ci-observability(the yq + Conftest/OPA policy tests underpolicy/ci-observability/pinning cross-file workflow semantics actionlint can't express) andjust fixture-pii(gitleaks over the committed capture tree). The capture-tree RULES gate harder: they are Rust tests (tests/sources/captures.rs, entryjust fixture-metadata) and ridejust teston all three platforms. - zizmor — workflow/action security: symbolic-or-SHA pins,
credential-dropping checkouts, exact inline suppressions. Dependabot's
github-actionsentry lists/.github/actions/*beside/—directory: /alone leaves a composite's pin uncovered (policy-enforced). - The two automatic Claude reviewers ride
claude-readonly-review.yml: a read-only model job on the trusted default branch, the PR diff as inert data, a separate least-privilege publisher — and a third job that comments when the model job fails or declines, because absence otherwise renders as a pass (#809).claude.ymlrefuses fork PR heads. - CodeQL stays the advanced workflow (
codeql.yml): explicit languages, Rust'snonebuild mode fed the MSRV toolchain, a SARIF health gate.
Releasing
Versioning
Pre-1.0: patch (0.y.Z) = bug fixes and polish only — no new public API,
nothing breaks. minor (0.Y.z) = everything else: new user-facing features
AND any breaking change to the published crates' API. cargo semver-checks
machine-enforces only the "nothing breaks on a patch" half; "features also bump
minor" is convention, upheld in review. When a breaking change reddens
semver, bump the minor in the same PR — never weaken the lint.
Cutting the release
just setup-tools # once per clone
just bump 0.5.1 # rewrites EVERY version number (workspace + path-deps + lockfile),
# drafts release_notes(), runs preflight → branch release/v0.5.1
# curate the notes to ~6 highlights, then `just gen` (the HUD bakes
# CARGO_PKG_VERSION, so a bump drifts every committed still) and commit
# docs/images + site/public/demos — else smoke's gen-check reds the PR.
# PR → review → merge, then:
git tag v0.5.1 && git push origin v0.5.1 # fires release.yml → build + crates.io + npm
just bump stops before the tag — pushing the tag is the irreversible
publish, so a human owns it. The tag also publishes outside this repo:
homebrew-core's formula is autobump: true and builds from the tag tarball,
instantly, with DEFAULT features on macOS and Linux — the one configuration
our release never builds. Two consequences:
- A from-source build break lands in Homebrew's CI, not ours. Anything
adding a system-library dependency needs a matching
depends_onin the core formula, in the same bump PR. Outstanding now: the default-onaudiofeature needsdepends_on "alsa-lib"— #731. - Their
test doblock is a public contract — see the "homebrew-core contract" comments atcrates/pixtuoid/src/validate.rs,crates/pixtuoid/src/sources_cli.rs,crates/pixtuoid-core/src/source/claude_code.rs.
Preempt BrewTestBot: submit the bump PR yourself right after tagging. Publishing uses OIDC trusted publishing — CI carries no registry tokens; the per-crate/per-package Trusted Publishers must exist before the tag (#216).
The arc loop
Non-trivial work runs as an arc: design → build → gate → wrap.
- Pick — an issue (
gh issue list) or backlog item. - Grill the design — decide the open questions one at a time, each with a recommended answer, before writing code.
- Design gate (before build) — three lenses so slop dies in design: best-practice search (confirm the idiomatic way against real docs online, never memory) · adversarial design review (red-team the design before code exists) · deepening lens (would deleting this concentrate complexity or just move it? does the change deepen a module or add a shallow one?).
- Spec — synthesize into
docs/superpowers/specs/(LOCAL, git-ignored) and plan againstimpl-plan.prompt.md. - Mock gate (taste/visual work only) — ratify the AFTER visual before code
(
beautify-decorationskill). - Build — TDD: failing test → minimal impl → commit.
- Self-review — a standards+spec pass before pushing. Not the merge gate.
- Merge gate (non-negotiable) — the two-lens review (2+ differentiated
lenses on the diff) + green CI + every online-bot finding dispositioned,
judged under the
two-lens-reviewskill's convergence contract: churn budget before review, a two-fix-round hard cap, only a confirmed HIGH blocks, and a botFindings: 0is evidence, not the gate. (Bot errored or absent at HEAD → the skill's step 6 owns the fallback.) A human merges. - Wrap — retro; durable lessons go to the agent's own memory layer, not new repo docs.
Skills. Repo skills live in .claude/skills/
(committed; .agents/skills/ aliases them for Codex): two-lens-review,
beautify-decoration, add-source, add-theme, procedural-lofi.
On a fresh machine or a non-Claude tool, git clone gives you the repo skills
and every just gate; this section IS the loop for tools without skills. Do
not scaffold a CONTEXT.md/docs/adr/ convention here — the nested
CLAUDE.md + sharp-edges system is the (only) design record.
The running order
| when | run |
|---|---|
| before code, if non-trivial (new seam / ≥3 files) | plan against impl-plan.prompt.md |
touched the --json / SourceStatus / OutcomeRow shape | just gen-contract |
| before push | just preflight (never piped — a pipe eats the exit code) |
| before merge | the two-lens review |
| a source/lifecycle change | dogfood against live CC, or replay hermetically (tiers below) |
One change spanning the Rust lib + the site + the Raycast extension:
PARALLEL-DELIVERY.md.
The e2e tiers live under scripts/lib/; none runs in CI. Cheapest first:
just openclaw-e2e (hermetic envelopes, free) · just replay <fixture> (a
captured rollout through the full headless path) · just openclaw-multi-e2e
(N real gateways, free) · just openclaw-backend-e2e (one BILLED turn) ·
just live-sources [id ...] (one BILLED turn per installed CLI; the only tier
proving a real CLI's output becomes a sprite — sources with no invocation
entry are listed NOT COVERED, never skipped silently).
Advisory backstops that surface risk but never gate:
scripts/check_upstream_drift.py (wire-format drift) · just fixture-age
(which recorded fixtures a local CLI has moved past; LOCAL-only) ·
just bench / CodSpeed (local numbers authoritative; CI benches advisory) ·
just comment-lint's ast-grep arm. Which comment-lint arms BLOCK is stated
once, in gate_fails' docstring in scripts/comment-lint.py — not restated
here.
Conventions (the short version — see CLAUDE.md for the full set)
- TDD first — failing test → minimal impl. No code without a test.
- DRY, YAGNI — nothing beyond the current scope.
- No
unwrap()in non-test code;anyhow(app) /thiserror(core); the hook listener and JSONL watcher log-and-continue, never panic. - Comments explain WHY, not what.
- Keep docs current — structure/API/workflow changes update the relevant
CLAUDE.md/README.mdin the same commit. - macOS-first — BSD CLI;
shellcheckany.shyou touch. - Sprite changes need visual verification —
beautify-decorationskill; an intentional visual change commits thejust gen-regenerated references in the same change (CI pixel-diffs againstdocs/images/reference-*.png).
Architecture invariants (don't break these)
pixtuoid-coreandpixtuoid-scenehave no terminal or window dependencies (just arch+ the crate boundary enforce it); terminal/ window code lives in the binary'stui/andfloating/painters.- Events flow through one channel typed
mpsc::Sender<(Transport, AgentEvent)>; theTransporttag is load-bearing (hook-wins dedup). - The
Sourcetrait is the only seam for a transcript-bearing agent CLI (hook-only CLIs ship a hook decoder + an installTargetinstead). - Hook install writes through symlinks (
resolve_symlink). - The hook shim never blocks CC — always exit 0; the 200 ms send bound is watchdog-enforced on both platforms.
- Walkable mask = ground footprint only; sprites may be visually larger.
Pull requests
- Every PR is reviewed by 2+ agents with differentiated lenses before
merge — no exceptions. The mechanical teeth are the
claude-review+claude-security-reviewworkflows plus your local two-lens pass. - AI-authored PRs get the
needs-human-verifylabel and a human visual check. - Every reviewer/bot finding reaches exactly one terminal state in the PR
thread — FIXED · REFUTED-with-trace · RE-SCOPED · SURFACED, defined ONCE
in
pr-review.prompt.md. Agents never file issues, and "acknowledged, no action" is not a state.
Recurring pitfalls (this codebase's review history, distilled)
- Byte-vs-char slicing — user-visible text truncates on
char/grapheme boundaries, never bytes. - Parallel-implementation drift — a value in two places (platform arms, core+tui twins, manifest+enum) gets single-sourced or a bridge test; when your diff guards one path, grep for its siblings (#159→#172).
- Sanitize at the decode boundary — untrusted input is cleaned where it enters, not at each use site.
- Negative-branch test gaps — pin the REFUSAL path, both sides of any window/threshold, with offsets derived from the constant under test.
- Unwired additions — every new field/parameter/asset needs a consumer
wired in the same diff (
_xbindings andpubfields evade the lints; #61). - Denylist completeness — diff any strip-set against the platform's documented set; prefer an allowlist (#198/#201/#206).
Handy gh commands
gh pr checks --watch # live CI status
gh pr merge --auto --squash --delete-branch # auto-merge once checks pass
gh issue develop <number> --checkout # branch linked to an issue
gh run rerun --failed # rerun only failed CI jobs
Adding a new agent CLI
The registration steps (4–7, 9) are test-forced — skipping one fails
just test. Step 8 is forced only for hook-only sources; step 10 by the theme
guards; steps 1–3, 11 and 12 are on you.
- Verify the wire format against the CLI's actual source/releases first — transcript location, line shape, hooks, session identity; pin every fact to an upstream file/version. Audit its HOME RESOLVER per axis in the same pass — PROBE the installed artifact rather than trusting docs; an unmirrored axis is fail-silent: the watcher polls a directory the CLI never writes and the office stays empty (#880). Resolver axes are deliberately NOT drift-watched — re-run the probe matrix when the CLI majors.
- Write the source module —
crates/pixtuoid-core/src/source/<name>.rs:SOURCE_NAME, aLineDecoderfn (one JSONL line →Vec<AgentEvent>), a label deriver, unit tests per event mapping. Format knowledge lives HERE. - Implement the
Sourcetrait (an asyncrun(self, tx)watching + decoding until the session universe ends). Hook-only CLI? Skip the decoder, trait, and step 7:transcript: Nonein the registry row, format knowledge in ahook.customdecoder (it must claim EVERY event), and do step 8 instead. - Add ONE
SourceDescriptorrow insource/registry.rs— label prefix, decoder, hook keying,tool_id_key(verify against a CAPTURED tool call, not a neighbour — kimi'sToolCallcost a source its tool ids), truthful capability flags,verified_version+version_probe. Lifecycle policy derives from the flags; you do not edit the reducer. - The descriptor's
nameis the roster —registered_source_names()projectsREGISTRY, and the conformance suite then requires a fixture. - Drop a sanitized real-capture fixture under
tests/sources/fixtures/<name>/<scenario>/(see the fixtures README for provenance rules), thencargo insta review. The conformance harness asserts all of a session's events coalesce to ONEAgentId. Test-layout map:crates/pixtuoid-core/tests/CLAUDE.md. - Wire it into
runtime/driver.rs::run_async(the registry drives the guard test, not the spawning). - If the CLI has hooks, add an
install/target (aTargetrow +merge_install/merge_uninstall+ averify_schemafn mirroring the target's own config format + the registered-events↔decoder-arms guard). - Add a row to
site/src/sources.json(status,featured, per-OSplatforms), thenjust gen-readme. Pinned toregistered_source_names()bysupported_sources_manifest.rs. - Add the per-source badge hue — a
SourceColorsfield + value in EVERY theme file +badge_colorin the manifest row; the coverage, legibility and site-bridge tests fail until it exists. - Docs in the same PR: the nested
crates/pixtuoid-core/CLAUDE.mdentry, and acheck_upstream_drift.pyrow where one is owed — which surfaces owe one issource/drift.rs's header, read it there. A row is four steps: the const, theinsertin that crate'ssrc/drift_surface.rs,just gen-drift-surface(commit both fragments), and theSURFACE_ROWSrow plus its selftest case (the case census fails without it). - Three roster literals no failure message spells out: the row-by-row
byte pin in
corpus_check.rs;TOOL_ID_KEY_UNPROVENintests/sources/captures.rs; a case row +#[test]incrates/pixtuoid/tests/wire_to_pixels.rs.
License
By contributing, you agree your contributions are licensed under the same terms as the project.