Before committing or opening a PR, run these in order and fix all issues:
cargo fmt
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace -qNever skip a step. Never commit code that fails any of these.
Integration tests for ff-rdp-cli are organized into a few consolidated
targets rather than one binary per file — every extra top-level tests/*.rs
file is a separate test binary that cargo test must compile, link, and run,
which dominates the iteration loop's wall-clock cost.
- Live-Firefox tests (anything gated behind
FF_RDP_LIVE_TESTS=1) go incrates/ff-rdp-cli/tests/live/<slug>.rsplus amodline incrates/ff-rdp-cli/tests/live/main.rs. They compile into the singlelivetest target. A new top-levelcrates/ff-rdp-cli/tests/live_*.rsfile is a review defect — it re-introduces the ~45-binary linking cost iter-100b removed. Thecheck-live-test-layoutxtask gate (wired intocheck-iteration-readyand the CIdisciplinejob) fails the build if one reappears. - Every
#[test]undertests/live/must carry#[ignore](iter-113 Theme B). A plaincargo testmust stay Firefox-free and fast; a bare (ungated) live test hangs a Firefox-less CI job for the whole job budget before the job timeout fires — exactly the iter-112 failure. Thecheck-live-test-layoutgate now also scanstests/live/and fails on any#[test]that is neither#[ignore]-gated nor annotated. Convention:#[test]immediately followed by#[ignore = "requires a live Firefox instance — set FF_RDP_LIVE_TESTS=1"](an intervening#[cfg(unix)]between the two is fine). For the rare runtime-gated fast probe that must run by default — e.g. acheck-pre-fix-reprotarget (run viacargo test --exactwithout--include-ignored) or a Firefox-free mock probe — add an// allow-ungated-live: <reason>comment in the attribute block above the#[test]instead. - Launch waits are bounded and env-overridable (iter-113 Theme A). The
live launchers wait for Firefox's remote-debugging port via a bound that
defaults to 30 s and is overridable with
FF_RDP_LIVE_LAUNCH_TIMEOUT_SECS(whole seconds).common::wait_for_debugger_port_withinpanics with a message naming the launcher binary and port when the port never opens, so a wedged or absent Firefox fails fast and self-describingly instead of hanging. - Shared live-test helpers live in
crates/ff-rdp-cli/tests/common/mod.rs, declared once fromtests/live/main.rsvia#[path = "../common/mod.rs"] mod common;; suites refer to them asuse crate::common::…(e.g.live_tests_enabled,live_network_tests_enabled). - Mock-server e2e tests go under
tests/e2e/as modules oftests/e2e/main.rs(thee2etarget) — see iter-46. - Run one migrated live suite:
FF_RDP_LIVE_TESTS=1 cargo test -p ff-rdp-cli --test live <module> -- --include-ignored. Enumerate every live test name (no Firefox needed):cargo test -p ff-rdp-cli --test live -- --list.
Every new pub item introduced in a PR must have at least one non-test consumer in the
same PR. The check-dead-primitives command enforces this:
cargo run -p xtask -- check-dead-primitives --since origin/mainThis diffs against origin/main (fallback: main), finds new pub fn/struct/enum/trait/mod
declarations, and uses ripgrep to confirm at least one non-test caller exists. Exit 1 if any
new pub items are unwired.
Ripgrep (rg) must be on your PATH. Install via your package manager:
- macOS:
brew install ripgrep - Ubuntu:
sudo apt-get install ripgrep - Windows:
winget install BurntSushi.ripgrep.MSVC
Every TODO, FIXME, or XXX comment in new code must be accompanied by either:
- A GitHub issue link:
https://github.com/ractive/ff-rdp/issues/N - A Jira-style ticket:
WORD-123 - An explicit allow annotation:
// allow-todo: <reason>
cargo run -p xtask -- check-todo-annotations --since origin/mainExit 1 if any unannotated TODOs are found in the diff.
cargo run -p xtask -- check-iteration-plan kb/iterations/iteration-NN-slug.mdThis validates:
statusis one of:planned,in-progress,in-review,done- If the plan body mentions
pub fn/struct/enum/trait/mod,first_call_sitesmust be non-empty withprimitiveandsitekeys per entry - A
dogfood_pathfrontmatter key or a## Dogfood pathbody section is present
If a plan has a firefox_refs: frontmatter key, validate that the cited line ranges
exist in the local Firefox checkout:
FF_RDP_FIREFOX_PATH=/Users/james/devel/firefox \
cargo run -p xtask -- check-firefox-refs kb/iterations/iteration-NN-slug.mdSet FF_RDP_FIREFOX_PATH to your Firefox source tree. The default is /Users/james/devel/firefox.
Plans with no firefox_refs: key are accepted silently. Added in iter-73.
If any crates/ff-rdp-core/src/actors/<X>.rs was changed, the corresponding
kb/rdp/actors/<X>.md must also be updated (or a // allow-actor-kb-skip: <reason>
annotation added to the first 20 lines of the actor file):
cargo run -p xtask -- check-actor-kb-sync --since origin/mainAdded in iter-73. See the ACTOR_KB_MAP constant in crates/xtask/src/check_actor_kb_sync.rs
for the full actor → kb path mapping.
Iteration plans may include a dogfood_script key in their YAML frontmatter pointing to
a sibling shell script:
dogfood_script: iteration-85-dogfood-57-carryovers-and-runnable-dogfood-path.dogfood.shThe script lives next to the .md plan file and is executed by:
cargo run -p xtask -- check-dogfood-script kb/iterations/iteration-NN-slug.mdRequirements:
- The script must write the sentinel file
/tmp/ff-rdp-iter-<N>-dogfood-okbefore exiting 0 (whereNis the iteration number extracted from the plan filename). - The gate is silently skipped if
FF_RDP_LIVE_TESTSis not set to"1". - Plans with no
dogfood_scriptfield are also skipped (pass) — existing iterations without the field continue to work. dogfood_pathanddogfood_scriptmay coexist; a warning is emitted but it is not a hard failure.
check-dogfood-script is the 7th sub-check run by check-iteration-ready. It is also
a required CI step on iter-* branches in the Live Tests workflow (live.yml) when
FF_RDP_LIVE_TESTS=1.
Windows: the bash invocation is skipped on non-unix platforms (CI runs on ubuntu-latest).
A rdp-spec-reviewer subagent is installed at ~/.claude/agents/rdp-spec-reviewer.md
(mirrored from tools/agents/rdp-spec-reviewer.md). When a PR touches actor files, the
/create-pr skill invokes it and appends a ## Spec drift section to the PR body.
To invoke manually:
claude --agent rdp-spec-reviewer --input tools/agents/fixtures/synthetic-watcher-diff.patchThe agent mirror follows the same pattern as the ralph-loop scripts mirror: edit both
~/.claude/agents/rdp-spec-reviewer.md and tools/agents/rdp-spec-reviewer.md in sync.
A pre-commit hook that enforces the TODO annotation rules is included in .githooks/.
To install it:
git config core.hooksPath .githooksThe hook scans the staged diff for unannotated TODO/FIXME/XXX and exits non-zero
with the offending file:line if any are found.
Bypass (emergencies only):
git commit --no-verifyNote: the CI discipline job is the load-bearing gate. The pre-commit hook is a
developer convenience — bypassing it locally doesn't skip CI.
New iteration plans live in kb/iterations/. Use the template:
cp kb/iterations/_template.md kb/iterations/iteration-NN-slug.mdThen edit the frontmatter:
title:"Iteration NN: Short title"date: today's datebranch:iter-NN/short-descriptionfirst_call_sites: list any newpubitems with their first call sitedogfood_path: describe how to manually exercise the iteration's output
The plan linter (cargo xtask check-iteration-plan) enforces these fields.
Before calling /create-pr on any iter-* branch, run the aggregator that
wraps every discipline gate into a single command:
# Resolve the plan automatically from the current branch:
BRANCH=$(git branch --show-current)
PLAN=$(cargo run -q -p xtask -- find-iteration-plan --branch "$BRANCH" 2>/dev/null || true)
if [ -n "$PLAN" ]; then
cargo run -p xtask -- check-iteration-ready --plan "$PLAN" --base origin/main
ficheck-iteration-ready runs:
check-dead-primitives --since <base>— no unwired new pub itemscheck-todo-annotations --since <base>— no bare TODO/FIXME/XXXcheck-actor-kb-sync --since <base>— actor.rschanges paired with kb updatescheck-firefox-refs <plan>—firefox_refs:line ranges validcheck-discipline-regression— mirror sync + replay baselinesac-fidelity-check.sh— ticked ACs backed by diff evidencecheck-live-test-layout— no stray top-leveltests/live_*.rsbinaries (iter-100b) and every#[test]undertests/live/is#[ignore]-gated or// allow-ungated-live:-annotated (iter-113 Theme B)
Fix every reported failure before pushing. The /create-pr skill runs this
automatically on iter-* branches.
Note: CI still runs each gate individually as separate required checks for clearer per-check GitHub status attribution.
- One iteration = one branch = one PR
- Branch naming:
iter-N/short-description - Self-review the diff before requesting review — catch fmt, clippy, dead code yourself
- The
disciplineCI job runscheck-dead-primitivesandcheck-todo-annotationson every PR
cargo audit (RustSec advisory DB) and cargo deny check (advisories +
licences + bans + sources) run on every PR via the supply-chain job in
.github/workflows/ci.yml. They are required checks.
When a new advisory lands and breaks CI, choose one path:
- Yank-and-upgrade (preferred). Run
cargo update -p <crate>to a patched version, regenerateCargo.lock, commit. - Pin a working version. If the maintainer hasn't released a fix yet
but a known-good prior version exists, pin it with
<crate> = "=X.Y.Z"inCargo.tomland link the upstream issue. - Ignore with reason. If the advisory does not apply to our use of
the crate (e.g. a
dev-dependency, or a code path we never invoke), add the advisory ID to[advisories].ignoreindeny.tomlwith a# advisory ID — short justification, link to upstream issuecomment. Never ignore without a written reason.
License or ban regressions follow the same rule of thumb: prefer removing the offending dep; only widen the allow-list if the licence is genuinely compatible.
Parser-surface fuzz harnesses live in fuzz/ (transport_recv_from,
parse_page_map_str, parse_script_file). They run for 60 s each on
every PR via the fuzz job.
Local setup (nightly only):
rustup install nightly
cargo install cargo-fuzz
cd fuzz
cargo +nightly fuzz run transport_recv_from seeds/transport_recv_from -- -max_total_time=60When CI reports a fuzz crash:
- Download the minimised input from the failed job's artifacts.
- Reproduce locally with
cargo +nightly fuzz run <target> <input>. - Open a GitHub issue tagged
fuzz-findingwith the minimised input attached. - Fix the parser, then check the input into
fuzz/seeds/<target>/as a permanent regression seed.
See fuzz/README.md for the full target list.
When running iterations via the ralph-loop skill, each agent also runs the xtask discipline
checks before invoking /create-pr. See the ralph-loop SKILL.md for details.
The live-tests GitHub Actions job must be a required status check on main so that
a red live-tests run blocks merging (iter-87).
Verify current state:
bash tools/branch-protection.sh ractive/ff-rdpApply the protection rule (requires admin access):
⚠️ The--field required_status_checks=...PUT replaces the entirecontextsarray. Before running, GET the current protection and include every existing required check in the new array, otherwise you will drop them.gh api repos/ractive/ff-rdp/branches/main/protection \ --jq '.required_status_checks.contexts'
# Example only — replace the contexts array with the merged set from the GET above.
gh api repos/ractive/ff-rdp/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":false,"contexts":["live-tests","fmt","clippy"]}' \
--field enforce_admins=false \
--field required_pull_request_reviews=null \
--field restrictions=nullAfter applying, re-run bash tools/branch-protection.sh to confirm it exits 0.