Skip to content

Latest commit

 

History

History
3251 lines (2662 loc) · 183 KB

File metadata and controls

3251 lines (2662 loc) · 183 KB

Robot SF – Detailed Development Reference

This file preserves the previously published procedural reference for compatibility. Start at docs/dev_guide.md for the concise first-use path, or use docs/developer-guide.md to navigate the canonical topic guides. New procedures should be added to a task-focused owner and linked from the index rather than added here.

← Back to Documentation Index

Welcome to the Robot SF Development Guide! This document serves as the central reference for contributors working on the Robot SF codebase. It covers setup instructions, architectural overviews, coding standards, and best practices to ensure a smooth development experience.

Setup

Installation and setup

# Check host tools that live outside uv.
scripts/dev/check_runtime_requirements.sh

# Slim core setup (or use --extra viz / maps / benchmark / training / all)
uv sync --all-extras
source .venv/bin/activate
uv run pre-commit install

# Quick import check (works in core install without optional extras)
uv run python -c "from robot_sf.gym_env.environment_factory import make_robot_env; print('Import successful')"

Robot SF uses an aggressive extras split to keep the default core installation slim:

  • Core (uv sync): Gymnasium env factory, simulator basics, minimal SVG maps, random/social-force smoke
  • [viz] (uv sync --extra viz): PyGame, Matplotlib, MoviePy, Seaborn rendering and video tooling
  • [maps] (uv sync --extra maps): OSMnx, GeoPandas, PyProj geospatial map authoring
  • [benchmark] (uv sync --extra benchmark): Pandas, SciPy benchmark evaluation and reporting
  • [training] (uv sync --extra training): Stable-Baselines3, PyTorch, Optuna, W&B, TensorBoard
  • [all] (uv sync --extra all): all optional feature extras

Host tools and optional machine capabilities that are not installed by uv are tracked in docs/dev_runtime_requirements.md.

Instruction precedence and proportional readiness

Use the maintainer hierarchy and readiness matrix in AGENTS.md before older workflow prose or tool-specific compatibility pointers. In short: active maintainer direction wins over stale instructions, docs/maintainer_values.md defines the hard contracts, and Project #5 scores are advisory when fresh evidence or maintainer direction conflicts with them.

Routine workflow cleanup can proceed without extra confirmation when it is bounded and the PR or handoff clearly labels assumptions, uncertainty, evidence grade, and any deferred follow-up issue. Use a detached checkout at latest origin/main only for read-only discovery, duplicate checks, and issue creation or update work. Create or switch to a branch/worktree before editing docs or code, running validation for a PR, pushing, or publishing.

For long autonomous goals, delegated batches, and token-saving threads, seed the active prompt or resume summary with docs/templates/token_efficient_thread_profile.md. The profile keeps task_class, validation_tier, context budget, delegation artifact requirements, and output budget explicit without duplicating the maintainer hierarchy or readiness matrix.

Docs-only and instruction-only changes normally use the cheap validation path: inspect the diff, verify changed links or paths where practical, and run available lightweight checks. Skill or AI workflow edits should also run the relevant skill and sync checks, for example:

uv run python scripts/dev/check_skills.py --preflight <skill-name>
uv run python scripts/tools/sync_ai_config.py --check

Escalate to BASE_REF=origin/main scripts/dev/pr_ready_check.sh when the change touches scripts, schemas, generated indexes, routing behavior, automation, runtime behavior, benchmark/metric/schema semantics, model provenance, or paper-facing claims.

For benchmark scenario, metric, model-profile, and release-evidence changes, first apply the Benchmark Scenario And Model Governance review contract. It defines the versioning, comparability, reproduction, and deprecation details that PRs must make explicit before benchmark or paper-facing claims are treated as established.

Use BASE_REF=origin/main scripts/dev/pr_ready_check.sh for the dependency-minimal core readiness lane. If a change touches predictive or other optional-extra paths and you need the optional proof lane directly, run ROBOT_SF_TEST_LANE=optional scripts/dev/run_tests_parallel.sh --lane optional. This lane split was introduced for issue #3301 and PR #3314; the executable source of truth is scripts/dev/pr_ready_check.sh dispatching to scripts/dev/run_tests_parallel.sh. Before resolving workers or starting pytest, scripts/dev/run_tests_parallel.sh runs the same dependency-only preflight: the core lane checks core, while the optional and all lanes check all-extras. When readiness dispatches the optional lane, it first runs scripts/dev/check_worktree_optional_deps.py --profile all-extras --json. Missing extras are reported as structured setup evidence and stop that lane with an actionable uv sync --all-extras message; they are not reported as changed-code failures. The core lane remains dependency-minimal and excludes optional-only test paths listed in tests/support/optional_test_allowlist.txt. Probe failures, malformed JSON, unknown exit codes, and status/exit-code disagreements are preflight-tool failures and never receive the missing-extra install guidance.

Claim-map validation

The fast-results claim map is an executable issue queue, not only a context note. Before changing docs/context/issue_2943_fast_results_claim_map_v0.md, run:

uv run python scripts/dev/check_fast_results_claim_map.py --json

This check is also part of scripts/dev/pr_ready_check.sh. It verifies that each priority row has a status, p0 rows have exactly one owner issue and one next command or artifact, and completed rows point at durable evidence instead of worktree-local output/.

Fresh linked-worktree bootstrap

When creating a new linked worktree, prefer a sibling container next to the main checkout rather than a directory inside the repository. For this checkout, use ../robot_sf_ll7.worktrees/<branch-or-issue-slug> unless a user or native tool chooses another location. Keep issue work readable with names such as issue-123-short-description.

Create the worktree through the capacity-guarded helper from the main checkout:

MAIN_REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_PARENT="$(dirname "$MAIN_REPO_ROOT")/$(basename "$MAIN_REPO_ROOT").worktrees"
mkdir -p "$WORKTREE_PARENT"
git fetch origin main
scripts/dev/create_worktree.sh \
  --branch issue-123-short-description \
  --path "$WORKTREE_PARENT/issue-123-short-description" \
  --base origin/main
cd "$WORKTREE_PARENT/issue-123-short-description"

The helper checks the target filesystem before invoking Git. A low-space or non-writable target fails before checkout, so it cannot leave a partially populated worktree. The default threshold is 2 GiB and can be overridden for a deliberately bounded local run with ROBOT_SF_WORKTREE_MIN_FREE_BYTES or --minimum-free-bytes.

When the next action must run in the new worktree, bind it to the creation command with --exec; this is safer than assuming a shell changes directory after git worktree add:

scripts/dev/create_worktree.sh \
  --branch issue-123-short-description \
  --path "$WORKTREE_PARENT/issue-123-short-description" \
  --base origin/main \
  --exec git rev-parse --show-toplevel

The supplied command runs with the new worktree as its working directory. If it fails, the worktree remains available for diagnosis.

The helper creates new branches without automatic upstream tracking so concurrent linked-worktree creation does not contend on the shared Git configuration. Set a remote explicitly when publishing, such as git push -u origin <branch>.

Bootstrap the local machine context before using Python tools. You can detect a linked worktree because .git is a file that points into <main checkout>/.git/worktrees/<worktree-name>, and git rev-parse --git-common-dir resolves to the main checkout's .git directory instead of the worktree-local Git dir.

Stash safety in linked worktrees

All linked worktrees share one stash namespace: refs/stash lives in the common Git dir, so a bare git stash pop inside any worktree can apply another session's WIP into the wrong checkout. Never use a bare git stash pop in a linked worktree. Prefer temp commits (git commit -m "WIP <branch>") for long-lived lanes; when stashing is required, use git stash push -m "<branch> <purpose>" and restore with the fail-closed wrapper scripts/dev/safe_stash_pop.sh (pops only when the top stash entry names the current branch) or an explicit git stash pop stash@{n} after verifying the entry message (issue #7700).

Treat the worktree as fresh only if both local.machine.md and .venv are absent. If either already exists, assume the worktree has already been bootstrapped and reuse the existing setup.

A cheap fresh-worktree check is:

[ "$(git rev-parse --git-common-dir)" != "$(git rev-parse --git-dir)" ] \
  && [ ! -e local.machine.md ] \
  && [ ! -d .venv ]

Use the shared main-checkout environment by default for a fresh worktree:

scripts/dev/run_worktree_shared_venv.sh -- \
  python scripts/dev/check_worktree_optional_deps.py --profile all-extras

The shared-venv wrapper pins imports to the current worktree, sets UV_NO_SYNC=1, and checks scratch capacity before starting the command. This avoids materializing one full .venv per parallel worktree. Use bootstrap_worktree.sh only when a worktree-local environment is explicitly required; it creates and targets the local .venv, then adds UV_NO_SYNC=1 to its activation script. To intentionally resync a local environment, unset the guard for that command:

env -u UV_NO_SYNC UV_PROJECT_ENVIRONMENT="$PWD/.venv" uv sync --all-extras

The optional-dependency preflight uses import-spec probes without importing project code. A missing_optional result is setup evidence and should not be confused with a changed-code collection or runtime failure. The docs-proof wrapper checks the core profile before invoking uv run; the shared-venv wrapper checks that profile by default and accepts an explicit profile when the command needs optional packages:

scripts/dev/run_worktree_shared_venv.sh --profile all-extras -- pytest tests/benchmark -q

If a current-worktree .venv is missing or incomplete, the docs-proof and shared-venv entry points fail before starting uv and print the recovery command scripts/dev/bootstrap_worktree.sh. run_tests_parallel.sh likewise stops before worker resolution or pytest, identifies the selected profile, and prints uv sync --all-extras as the direct repair command. This prevents a lightweight Python-only environment from being reused as if it were a synchronized dependency profile. --standalone remains available only for commands whose no-project-import boundary is verified.

When the host is under pressure, inspect reclaim candidates without deleting anything:

scripts/dev/check_worktree_capacity.py --inventory --json

The inventory covers ignored generated output/, the uv cache, repository worktree containers, and recognizable agent worktrees under /dev/shm. It is a review aid only. Preserve durable evidence before pruning output/; remove only clean, pushed Git worktrees with git worktree remove; and remove only task-owned, no-longer-running /dev/shm scratch. No automated cleanup is performed. Each existing candidate is sized with a five-second per-path timeout by default. Override it with --size-timeout-seconds N for a deliberately bounded local diagnostic. A timeout or unavailable du result is reported as size_status with a machine-readable size_reason; it never becomes a zero-size or cleanup recommendation and does not change the separate capacity verdict.

Local CI scratch capacity

The local continuous-integration (CI) runner checks temporary-directory capacity before it starts dependency setup or a CI phase, so a nearly full temporary filesystem fails early with a usable remediation. The default guard requires 1 GiB free at ${TMPDIR:-/tmp}:

scripts/dev/run_ci_local.sh --no-setup lint test

When /tmp is a small or nearly full temporary filesystem, point the run at a writable, disk-backed directory instead:

scripts/dev/run_ci_local.sh --scratch-dir /path/on/disk
scripts/dev/run_worktree_shared_venv.sh --scratch-dir /path/on/disk -- \
  pytest tests/dev/test_ci_script_contract.py -q

--scratch-dir places temporary files plus the default uv, XDG, and Matplotlib caches below that directory. The wrappers retain an explicitly supplied ROBOT_SF_CI_MIN_FREE_BYTES override; use it only for a deliberately bounded run when the default 1 GiB guard is not appropriate. A capacity failure means no CI phase or wrapped command was started; it is not a test-suite result.

Notes:

  • The symlink target should point at the main checkout's local machine context, not a copied per-worktree file.
  • If the worktree path differs, derive the correct source from $MAIN_REPO_ROOT/local.machine.md.
  • Reuse the symlinked local.machine.md instead of copying it so machine-specific limits stay in sync across worktrees.
  • CARLA is intentionally not part of uv sync --all-extras. For a CARLA-capable worktree, add the host-side Python client explicitly with uv sync --all-extras --group carla, then run scripts/dev/check_carla_runtime.sh for preflight or scripts/dev/check_carla_runtime.sh --smoke for the bounded Docker connectivity proof.
  • If you are starting work on a feature branch, merge the latest origin/main into the current branch early so you inherit repository-wide fixes and workflow improvements before your local changes diverge. Typical command sequence:
git fetch origin main
git merge origin/main

Worktree teardown and preservation

Make worktree cleanup part of normal closeout after PR review, issue implementation, publishing, or abandoned exploration. If a worktree is no longer needed for active validation, CI follow-up, artifact recovery, or handoff, remove it safely or record why it is intentionally preserved.

Before deleting old worktrees, run git worktree list --porcelain from the main checkout and inspect each candidate with git -C <path> status --short --branch. If the worktree may contain generated evidence or local experiment outputs, also inspect relevant ignored paths, for example [ -d "<path>/output" ] && git -C <path> status --ignored --short -uall output. For a compact first pass, run uv run python scripts/dev/worktree_hygiene_snapshot.py --repo-status --retirement-plan --json. The retirement projection is a read-only review aid: it can classify rows as preserve, review, or removable, but it never deletes worktrees and does not replace human approval before any later git worktree remove command.

Only remove a worktree after preserving relevant tracked, untracked, and ignored-but-important changes through a commit, stash, patch, durable artifact promotion, or explicit handoff note. Do not delete dirty or unpushed worktrees unless the cleanup record states what was preserved or why nothing needed preservation. Classify large ignored directories such as output/ before removal as disposable, ignored cache, tracked manifest/evidence, durable-required, or handoff-needed; do not let worktree-local output/ become durable artifact storage. Use git worktree remove <path> for clean worktrees; reserve git worktree prune for stale administrative entries after local state is checked.

Targeted shared-venv worktree validation

For quick, targeted checks in a sibling worktree, prefer scripts/dev/run_worktree_shared_venv.sh -- <uv-run-command>: it uses an initialized current-worktree .venv when available, otherwise the main checkout .venv, while pinning imports to the current worktree:

scripts/dev/run_worktree_shared_venv.sh -- pytest tests/test_ci_script_contract.py -q
scripts/dev/run_worktree_shared_venv.sh --venv ../robot_sf_ll7/.venv -- ruff check scripts/dev
scripts/dev/run_worktree_shared_venv.sh --standalone -- \
  python scripts/dev/check_docs_evidence_integrity.py --files docs/dev_guide.md

The helper runs from git rev-parse --show-toplevel, sets UV_PROJECT_ENVIRONMENT to the selected .venv, and sets UV_NO_SYNC=1. By default it also prepends the worktree root to PYTHONPATH. This is intended for fast local feedback when dependencies are already current. It should fail if the selected .venv is missing instead of silently installing into the wrong checkout.

An explicit --venv also marks that exact environment for nested helpers that source scripts/dev/common_setup.sh; both VIRTUAL_ENV and UV_PROJECT_ENVIRONMENT remain bound to the selected path. An unrelated inherited VIRTUAL_ENV is not an override and does not suppress the repository-local activation policy.

Use --standalone for a dependency-light command whose tests verify that it does not import robot_sf or other project packages. This mode still reuses third-party dependencies from the shared environment, but it skips the project-source freshness check and does not add the worktree root to PYTHONPATH. For example, check_docs_evidence_integrity.py has a minimal-environment import guard in tests/tooling/test_docs_evidence_import_boundary.py, so it remains safe to run when an unrelated installed pysocialforce copy is stale. Do not use this mode for tests or commands that import project code; refresh the owning checkout environment for those commands instead.

If a fresh linked worktree fails to collect a focused test because an optional dependency such as torch is not installed in that worktree, rerun the same focused command through scripts/dev/run_worktree_shared_venv.sh -- uv run pytest <test-node>. A pass through the wrapper classifies the direct failure as setup or optional-dependency friction for that worktree, not as a code regression; record both commands in the PR or handoff.

Agent-run artifact paths in linked worktrees

In a linked worktree, .git is a file (not a directory), so writing to a literal .git/codex-agent-runs/active/... path fails. Use the shared helpers to resolve the correct absolute path via git rev-parse --git-common-dir.

Shell (for scripts that source scripts/dev/common_setup.sh):

source scripts/dev/common_setup.sh
artifact_dir="$(resolve_agent_artifact_dir my-subdir)"
mkdir -p "$artifact_dir"
echo "data" > "$artifact_dir/result.json"

Python (for scripts under scripts/dev/):

from scripts.dev.git_common import resolve_agent_artifact_dir

artifact_dir = resolve_agent_artifact_dir("my-subdir")
# artifact_dir is an absolute Path; mkdir is done automatically

One-liner (for ad-hoc shell use or agent instructions):

mkdir -p "$(git rev-parse --path-format=absolute --git-common-dir)/codex-agent-runs/active/my-subdir"

Never hard-code a literal .git/codex-agent-runs/... path in scripts, agent instructions, or task artifact wording. Always resolve through git rev-parse --git-common-dir or use the helpers above.

When validating the SNQI (Social Navigation Quality Index) contract or camera-ready exit handling, pass the relevant files explicitly. -k filters only after pytest has collected files, so starting from pytest tests -k ... can import unrelated optional stacks first. This command collects only the files that own the checks:

DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy scripts/dev/run_focused_tests.sh \
  tests/unit/benchmark/test_snqi_campaign_contract.py \
  tests/benchmark/test_camera_ready_campaign.py \
  tests/tools/test_run_camera_ready_benchmark.py \
  -k "snqi_contract or exit or camera_ready_summary" -q

If a change adds another focused contract test, append its file path to this command rather than falling back to the whole tests tree. This is a collection boundary, not a replacement for the optional readiness lane when the changed tests actually require optional dependencies.

Use a normal worktree-local uv sync --all-extras and PR_READY_MODE=final BASE_REF=origin/main scripts/dev/pr_ready_check.sh for final PR proof, dependency changes, generated lockfile validation, or any run where environment isolation matters.

Critical dependencies and setup: Fast-pysf integration

The fast-pysf/ directory contains the optimized SocialForce physics engine and is now integrated as a git subtree (previously a submodule). After cloning the repository, the fast-pysf code is automatically available—no additional initialization steps required.

Note: If you're working with an older branch that still uses submodules, see the Subtree Migration Guide for migration instructions and workflow differences.

Quick Start Commands

# source .venv
source .venv/bin/activate
# Lint+format
uv run ruff check --fix . && uv run ruff format .
# Tests
uv run pytest -n auto tests

Examples Quickstart Walkthrough

The examples/README.md file now captures a curated onboarding path. New contributors can get a full tour in roughly five minutes by running the quickstart trio in order:

uv run python examples/quickstart/01_basic_robot.py
uv run python examples/quickstart/02_trained_model.py
uv run python examples/quickstart/03_custom_map.py
  • 01_basic_robot.py introduces the environment factory pattern and headless rollouts.
  • 02_trained_model.py replays the bundled PPO baseline and writes JSONL metrics to output/results/episodes_demo_ppo.jsonl.
  • 03_custom_map.py shows how to load maps/svg_maps/debug_06.svg via RobotSimulationConfig.map_pool for custom layouts.

See examples/README.md for the decision tree, prerequisites, and links to additional tiers (advanced features, benchmarks, plotting, and archived scripts).

Advanced Feature Demos

Developers exploring specific capabilities should jump to the curated scripts in examples/advanced/. Each file follows the numbered naming scheme surfaced in examples/README.md and comes with a manifest-backed docstring describing how to run it. Highlights include:

  • Backends & factory ergonomics: 01_backend_selection.py and 02_factory_options.py demonstrate switching simulators and recording options via unified configs.
  • Observation & training workflows: 03_image_observations.py and 04_feature_extractors.py showcase image sensors and feature extractor presets (run with uv sync --extra training, or uv sync --all-extras for full local parity).
  • Pedestrian & policy scenarios: Scripts 0611 cover factory-based pedestrian environments, single/multi pedestrian setups, and PPO rollouts using the maintained checkpoints under model/.
  • Tooling, validation, and visualization: 12_social_force_planner_demo.py through 15_view_recording.py provide the Social Force planner showcase, SVG map validation helper, trajectory visualization, and recording playback flows.

Check the Advanced table in examples/README.md for prerequisites, tags, and whether a script is enabled for CI smoke execution.

Model registry

Trained policies are tracked in a local registry to make reuse and automation easier:

  • Human-readable notes: model/registry.md
  • Machine-readable registry: model/registry.yaml
  • Helper API: robot_sf.models.resolve_model_path(...) for on-demand loading (auto-downloads from W&B when metadata is present).

Use robot_sf.models.upsert_registry_entry(...) to auto-populate or update the registry from training pipelines. Benchmark-promoted learned checkpoints must also include benchmark_promotion observation-track metadata; see model/registry.md and docs/context/issue_1612_observation_track_architecture.md.

One‑liner quality gates (CLI):

uv run ruff check --fix . && uv run ruff format . && uvx ty@0.0.58 check . --exit-zero && uv run pytest -n auto tests

ty currently runs in advisory mode with --exit-zero: it reports findings, but the canonical typecheck phase is not a PR-readiness merge blocker by itself.

Acceptance tests (pytest-bdd pilot)

Acceptance scenarios live in tests/bdd/ as Gherkin .feature files with pytest-bdd step definitions. These tests describe deterministic, fixture-first repository workflows and must not require network, GUI, CARLA, GPU, or long benchmark execution. Run them with:

uv run pytest tests/bdd -q
uv run pytest --collect-only tests/bdd -q

The pilot covers episode schema validation: a valid record passes, a malformed record is rejected.

Merge-race prevention (ADR — issues #5389 and #6272)

Problem. Three main-red incidents in 36 hours (2026-07-11/12) had the same shape: two PRs, each green on its own merge-ref, broke main when both landed in a 3-second merge race. The red-main merge hold (#5385) stops breakage stacking once main is red, but nothing prevented the race itself: a PR's CI ran against a main that moved before the merge landed.

Decision: risk-tiered gate-side integration. Exact-head CI and review evidence remain mandatory, but unrelated movement on main does not force a full branch refresh for every ordinary PR. The repository uses the explicit base_sensitive marker-file selector from issue #5559:

  • Selector: scripts/dev/base_sensitive_selector.py and scripts/dev/check_base_sensitive_gates.py --pr <pr-number> --json classify a complete changed-file inventory as base_sensitive when it intersects a test file declaring the base_sensitive marker; a missing inventory is unknown and fails closed.

  • Changed-file provenance: the gate tries gh pr diff <pr-number> --name-only first. If GitHub rejects the unified diff because it exceeds its line limit, it falls back to the paginated REST pulls/<number>/files endpoint with strict page and filename validation. A failed, malformed, empty, pagination-exhausted, or 3,000-file-capped response remains unknown; no partial file list is used for admission.

  • Base-sensitive path: a base_sensitive PR must pass the existing workflow-run/base freshness check and the focused base_sensitive subset against the current base before merge-ready admission.

  • Ordinary path: a trusted exact-head review may record base-policy: ordinary-cas @ <head-sha> for a complete non-intersecting inventory. The guarded receipt owner then records every filename, status, and prior rename path, derives the complete Python-test candidate set from those records, and classifies each candidate's marker content at immutable base/head/current-main refs (including both paths of renamed candidates), verifies the current-main commit before treating a missing path as absent, preserves the trusted full-40-hex policy carrier, and records the result of scripts/dev/check_pr_current_base_cas.py immediately before its expected-head merge. This proof qualifies only stale_merge_base; it does not waive exact-head CI, review, metadata, thread, branch protection, or any second gate failure.

  • Unknown path: missing selector, current-main, head, review-thread, or CAS provenance fails closed.

  • Script: scripts/dev/check_pr_merge_staleness.py <pr-number>.

  • Integration: the gh-pr-merger skill runs this check for the base-sensitive path and runs check_pr_current_base_cas.py as the immediate final preflight for every guarded merge.

  • Behavior: the precise base-sensitive path reads the completed workflow run's recorded pull_requests[].base.sha; when that provenance is unavailable, the checker fails closed rather than inferring freshness. The ordinary path relies on the explicit selector plus the immediate CAS check and the GitHub head compare-and-swap guard. The author must update the branch and re-run CI before a base-sensitive PR becomes mergeable again. Because the installed gh version does not provide gh pr update-branch, use the guarded repository helper after recording the current head SHA. The drop-in scripts/dev/update_pr_branch_safely.sh <number> --expected-head-sha <sha> tries gh/gh api update-branch first and falls back to a lease-protected local rebase/push when that path is unavailable (issue #5775). If the PR source branch was deleted on the remote, the helper detects the missing refs/heads/<head-ref> after the expected-head guard passes and restores it with a plain (non-force) push of the immutable PR head SHA, which it already verified equal to --expected-head-sha; the restore is reported in the JSON result (source_ref_restored, additive source_ref_restore_failed / source_ref_restore values) and the normal update path runs afterwards. Cross-fork PRs with a deleted head branch and unreachable immutable head SHAs fail closed with a machine-readable error instead of attempting a restore (issue #6689). The older REST-only scripts/dev/update_pr_branch.py is kept for environments where the REST update-branch endpoint works.

Stacked PR orchestration (issue #7345)

Use scripts/dev/stacked_prs.py for a stack ordered from root to tip. The helper is a guarded coordinator for branch/base-ref mechanics; it does not replace the exact-head review, metadata, thread, CI, or branch-protection gates described above. All mutating operations are dry-run by default, and --apply requires an PR=SHA guard for every PR in the supplied stack.

Inspect the live stack before changing it:

uv run python scripts/dev/stacked_prs.py status \
  --prs <root-pr> <child-pr> <tip-pr> --json

The status record reports each head/base ref and SHA, current check-run conclusions (older superseded runs are excluded), review digest, requested reviewers, review-thread resolution, exact-head verdict, final PR metadata digest, and whether the current stack alignment is merge-ready. Review, review-comment, conversation-comment, and check-run collections are read through bounded REST pagination and include page/row provenance in the pagination field. A full page at the configured budget is reported as possibly truncated and fails closed; malformed pages also fail closed. Unknown review-thread state is never treated as green.

To align a stack, preview the desired root -> main and child -> parent-source-branch changes, then apply them only with the exact heads captured from the same snapshot:

uv run python scripts/dev/stacked_prs.py retarget \
  --prs <root-pr> <child-pr> --json
uv run python scripts/dev/stacked_prs.py retarget --apply \
  --prs <root-pr> <child-pr> \
  --expected-head <root-pr>=<root-sha> \
  --expected-head <child-pr>=<child-sha> --json

For local branch synchronization, use a clean linked worktree. The command fetches the base and stack branches, merges the preceding remote branch into each branch in order, and pushes ordinary (non-force) updates. It restores the worktree's original branch after an applied run:

uv run python scripts/dev/stacked_prs.py sync \
  --worktree /path/to/linked/worktree \
  --branches <root-branch> <child-branch> <tip-branch> --json
uv run python scripts/dev/stacked_prs.py sync --apply \
  --worktree /path/to/linked/worktree \
  --branches <root-branch> <child-branch> <tip-branch> --json

merge-cascade squash-merges only the current green root with GitHub's exact-head merge guard. After the merge it verifies whether GitHub automatically retargeted the next PR to main. If not, it explicitly retargets that PR, verifies the result, and stops until the base change has fresh CI and exact-head review evidence. Re-run the command for the next PR; it never blindly merges a child against a stale pre-merge base:

uv run python scripts/dev/stacked_prs.py merge-cascade --apply \
  --prs <root-pr> <child-pr> <tip-pr> \
  --expected-head <root-pr>=<root-sha> \
  --expected-head <child-pr>=<child-sha> \
  --expected-head <tip-pr>=<tip-sha> --json

Do not run sync --apply, retarget --apply, or merge-cascade --apply concurrently against the same branch/PR. Refresh the status snapshot and expected heads after any external push, retarget, merge, or review change. The helper does not force-push, resolve conflicts, bypass requested reviewers, or delete branches.

Why not GitHub merge queue yet? The native merge queue is the ideal solution — it re-validates each PR against the up-to-date prospective main before merging automatically. The repository has selected the bounded current-base subset plus CAS policy until the queue is configured because:

  1. It works immediately without enabling a repository-level feature that requires maintainer approval to toggle branch-protection settings.
  2. The explicit selector preserves the stronger current-base proof for the known snapshot, fixture-hash, tuple-shape, and count-ratchet surfaces.
  3. It is easy to roll back to universal refresh if an attributable stale-base incident is observed; see docs/context/issue_6272_risk_tiered_stale_base_policy.md.

When to revisit. If the native merge queue becomes available and is enabled, the ordinary CAS path can be replaced by the queue's built-in re-validation, which is strictly stronger. The base-sensitive gate remains useful as a safety net for non-GitHub CI providers.

Pre-publication state refresh (issue #6916)

The local readiness stamp proves a branch and HEAD were validated, but it cannot tell whether the claimed issue was closed by a concurrent PR or whether a remote branch tip changed while readiness was running. Use scripts/dev/check_prepublication_state.py around expensive publication work:

  1. capture the issue, base, remote branch, and local HEAD SHAs before readiness.
  2. Run check immediately before opening or updating the PR.
  3. Treat superseded and blocked as fail-closed stops. Treat refresh-required as stale evidence; run sync --integrate only from a clean worktree, resolve conflicts if needed, then rerun readiness and capture a new baseline.

The gate records the exact before/after SHAs, any newly opened covering PR, and any merged PR that explicitly closes the issue. An open PR is matched only when its title or body contains an explicit same-repository Closes, Fixes, or Resolves reference; ordinary mentions and other repositories do not supersede the route. Its integration path uses ordinary Git merges and never resets or deletes local worktrees. The capture command accepts either a bare base branch such as main or the equivalent remote-qualified form such as origin/main when --remote origin is used. The required --repo value accepts OWNER/REPO (for example, ll7/robot_sf_ll7) or a local checkout path; a local path is resolved through the named Git remote and the normalized repository slug is stored in the snapshot. A checkout without a usable GitHub remote fails before remote-state collection, so pass the explicit repository slug in that case. For repositories whose merged-PR history exceeds the default REST page budget, pass --max-pr-pages <positive-integer> to capture; a later check or sync reuses that recorded budget unless it receives an explicit override. Truncated inventories still block publication.

When the authenticated GraphQL quota is exhausted, the gate falls back independently for issue state, open-covering-PR, and merged-closing-PR discovery to the bounded REST endpoints already used by the issue closure audit. The snapshot records remote_state_sources and any remote_state_fallbacks, so a REST-backed decision is auditable rather than silently presented as a native read. Authentication, authorization, repository-resolution, malformed-response, and truncated-inventory failures remain blocked; do not replace this gate with an ad-hoc manual state check. The shared REST PR inventory defaults to 50 pages of 100 rows (5,000 rows); the closure audit can raise that bounded cap with --max-pr-pages when a repository outgrows it, while the prepublication gate still blocks rather than treating a capped inventory as complete.

Rollback path. Remove step 7 from .agents/skills/gh-pr-merger/SKILL.md and .opencode/skills/gh-pr-merger/SKILL.md. The script scripts/dev/check_pr_merge_staleness.py and its tests can be deleted at that point.

Observation follow-up (issue #7261). The selected policy must be measured from a named, SHA-pinned normal-throughput window after rollout; a live queue snapshot is not a latency or causality measurement. Use scripts/dev/measure_stale_base_policy.py with an explicit stale_base_observation_window.v1 input. The helper keeps ordinary compare-and-swap waits and base-sensitive refresh waits separate, requires exact-head/base evidence for stale-base attribution, and reports missing source data as not_available. Source kinds, input SHA-256, the deterministic record audit, red-main coverage, and independent pre-rollout baseline evidence are preserved in the report; fixture sources cannot be promoted by editing the top-level evidence status. Its output is workflow evidence only and does not authorize a policy change, merge, campaign, or publication.

Merge queue gate (issue #6274)

Problem. An external or parallel auto-merge path merged several PRs without the merge-ready label and without a current exact-head gate-verdict: accepted trailer (issue #6274). The in-repo gh-pr-merger contract is fail-closed, but it only governs merges it performs itself; any dispatcher that routes through the GitHub native merge queue (or auto-merge) bypassed those gates, so review notes could not fail closed.

Scope: a native-queue gate, not a complete #6274 closure. This workflow can protect only native merge_group events after a maintainer activates it as a required check. It does not locate, change, or prove coverage of the direct/parallel merge dispatcher observed in #6274. Keep #6274 open until that dispatcher and the active main protection configuration have both been verified.

Decision: required merge-queue status-check gate. We add a dedicated status-check workflow that runs inside the native merge queue and enforces the same fail-closed preflight as gh-pr-merger before the queue auto-merges a PR:

  • Workflow: .github/workflows/merge-queue-gate.yml (checks out the gate implementation from the trusted base revision rather than the evaluated PR or synthetic merge-group tree, with checkout credentials disabled; enforces the required check fail-closed at queue-time on merge_group, and publishes non-blocking source-PR and workflow_dispatch audits). Source-PR audits refresh when merge-ready is added or removed and when a labeled source head changes. They preserve a truthful failed passed value and reasons in their audit while exiting zero, so admission readiness is not misreported as failing implementation CI. If a source-head base predates this gate file, the run records a notice and skips evaluation so the bootstrap PR can merge; a queue-time run fails closed when the trusted implementation is unavailable. The queue-time invocation independently validates the synthetic merge group.
  • Script: scripts/dev/merge_queue_gate.py (pure gate logic + live CLI).
  • Checks enforced: non-draft state, current merge-ready label, a current exact-head gate-verdict: accepted @ <head_sha> trailer, and a current pr-metadata: reconciled @ <digest> trailer binding the exact final PR title/body pair (the gate reuses scripts/dev/pr_loop_policy.has_current_accepted_gate_verdict) authored by a repository owner, member, or collaborator; verdict-like text from an untrusted contributor is ignored. The metadata digest is computed from the live title/body through the REST-backed snapshot and stale or missing metadata evidence fails closed. The gate also requires no unresolved actionable review threads and no outstanding explicitly requested reviewers. The current source-head CI rollup must also remain green; superseded check runs are discarded with the same helper used by the guarded merger preflight. Under the REST quota fallback, check runs are enriched with their authoritative Actions workflow identity before that classifier runs; missing identity remains independently fail-closed. The gate excludes its own in-progress source-head check to avoid waiting on itself. The exact-head trailer binds that CI and review evidence to the source head, while the merge queue independently runs its required checks on the synthetic queue head. The live queue must use GitHub's ALLGREEN strategy ("Only merge non-failing pull requests"), so every earlier entry represented by a grouped synthetic head must pass its own gate; HEADGREEN fails closed because it can merge a failing earlier entry with a passing tail entry. Staleness is fresh by construction inside the queue (the queue base SHA equals current main).
  • Audit record: the job emits a merge_queue_gate.v1 audit with the evaluated head SHA, the source-head SHA encoded in the queue ref and its binding verdict, queue merging strategy, base SHA, label set, metadata digest and metadata-verdict status, gate-verdict status, staleness verdict, CI conclusion, reviewer-thread resolution plus requested-reviewer status, and the current closing-discipline status/blockers from PR commit and issue metadata, so every merge decision is inspectable and reproducible.
  • Self-test: uv run python scripts/dev/merge_queue_gate.py --self-test exercises the fail-closed contract deterministically (the issue #6274 validation scenarios).

Required maintainer toggle (cannot be done from a worktree). The gate fails closed only after a maintainer adds the status check Merge Queue Gate / merge-queue-gate to the merge queue's required status checks, enables Only merge non-failing pull requests (ALLGREEN), and enables Require conversation resolution before merging in the branch-protection rules for main (Settings → Branches → main → merge queue). The workflow verifies ALLGREEN at runtime and fails closed if the queue is configured as HEADGREEN. GitHub does not reliably create a fresh source-head Actions check when a reviewer resolves or reopens a thread; requiring conversation resolution therefore makes the gate's no-unresolved-threads condition binding at merge time. The source-PR audit is observational and is deliberately not a required status check; exact-head review evidence and all other admission conditions are re-evaluated by the fail-closed merge_group run. Until these toggles are applied, the workflow does not provide the queue-side contract; the in-repo gh-pr-merger preflight remains binding for guarded merges. Enabling GitHub's native merge queue itself also requires maintainer approval to toggle branch-protection settings, consistent with the gate-side rationale above.

Single-account merge receipt (issue #7669)

Plain-language contract. A merge may proceed with one account only when a versioned receipt proves that every ordinary implementation-integrity condition was observed on the exact PR head. The receipt does not turn a missing approval into a general bypass: domain, scientific/evidence, legal/release, security, dependency, draft, thread, requested-reviewer, metadata, and hosted-check conditions remain independent fail-closed holds.

The canonical owner is scripts/dev/single_account_merge_receipt.py, with the contract recorded in scripts/dev/single_account_merge_receipt.v1.schema.json. A receipt binds the repository and PR, head/base/current-main SHAs, final PR metadata digest, terminal exact-head required checks, independent implementation-review carrier and evidence digest, review-thread disposition, requested reviewers and teams, all separate hold dimensions, optional waiver actor/reason/time, the expected-head compare-and-swap (CAS) request, and an ordinary_cas proof when the recorded PR base predates current main. That proof binds the complete normalized changed-file record inventory (filename, status, and prior rename path), derives the exact test candidate set, and binds immutable base/head/current-main content refs for changed Python test marker candidates (including both paths of renamed candidates), a verified current-main commit ref, the full trusted exact-head policy carrier, and immediate current-main/head CAS; a 3,000-file-capped inventory fails closed, and the proof can qualify only a lone stale_merge_base gate reason. New receipts carry a required closing_discipline audit field. A passing result is bound to the live PR head and body digest and records that paginated PR commit metadata and current issue metadata were checked; blocked or unavailable results remain fail-closed. The receipt digest covers the pre-merge observation and is preserved when GitHub returns the merge commit SHA.

The compatibility helper scripts/dev/gh_pr_merge.sh is a delegating caller, not a second merge authority. It validates the full expected head and repository identity, asks the receipt owner to write a report, and then asks that same owner to apply it. Native CLI or direct REST merge writers are not available from the shell path, so transport failures fail closed. Receipts produced by the pre-follow-up v1 implementation remain structurally readable when closing_discipline is absent, but validation and apply still block with missing closing evidence; callers must generate a current receipt before merging.

This compatibility helper intentionally does not delete the source branch. Source-branch cleanup is a separate guarded post-merge action under the gh-pr-merger deletion boundary and requires verification that no unique, unpreserved work remains. Calling the helper alone does not authorize branch cleanup.

When the GraphQL-backed PR snapshot is rate-limited, --mode report-only and --mode validate reuse the bounded REST snapshot path for ordinary PR, label, comment, review, requested-reviewer, and hosted-check facts. Each live receipt records evidence_provenance with the route used for those facts, plus the REST sources for the exact base and changed-coverage checks. Review-thread resolution remains GraphQL-only: if that read is unavailable, the receipt records thread_resolution.status: unavailable and stays blocked. REST fallback is evidence recovery, not merge authority; it never authorizes a merge or bypasses the thread gate. The fallback route is only used for recognized GraphQL quota exhaustion, paginates and validates the ordinary REST collections (including legacy commit statuses), and still fails closed when any gate-critical field is unavailable, malformed, or incomplete.

Use the three explicit modes as follows:

  • --mode report-only reads the canonical merge-gate snapshot and writes an inspectable receipt; it performs no remote mutation.
  • --mode validate --receipt-file <path> rereads live state and compares it with the immutable receipt; a changed head, base, metadata, check, review, thread, requested reviewer, hold, or ordinary-CAS proof blocks.
  • --mode apply --receipt-file <path> repeats validation, rereads the live PR body/head and rechecks paginated commit metadata plus current issue metadata immediately before letting the receipt owner issue exactly one expected-head squash merge, then rereads the closed/merged PR and records the returned SHA. scripts/dev/stacked_prs.py merge-cascade --apply is the stack coordinator and delegates its root merge to the same owner; its stack receipt must carry the same explicit closing-discipline result.

The only permitted waiver is the bounded absence of a distinct human implementation reviewer, and it requires an actor, reason, and timestamp. It cannot waive a required hosted check, scientific or evidence review, domain approval, legal/release or security gate, dependency hold, draft state, unresolved thread, requested reviewer, unqualified stale base, or metadata mismatch. If the post-merge readback is invalid, preserve the receipt and route an incident; do not reconstruct the pre-merge evidence or reuse the waiver. The repository policy fixture scripts/dev/single_account_merge_authority_fixture.v1.json enumerates the callers and prevents new direct merge endpoints from bypassing this contract.

Changed-line coverage admission (issue #7293). The authoritative proof for merge admission is the changed-coverage-gate check run on the exact source PR head SHA. CI enables coverage on the fast-feedback shards for pull requests, checks out the immutable PR head, combines those shards, and runs scripts/coverage/check_changed_files_coverage.py with explicit --base-sha and --head-sha values. Because this lane performs the complete exact-head checkout and shared all-extras setup before combining shards, its hosted job has a bounded 30-minute timeout. The same checker used by local readiness emits a changed-coverage.v1 artifact containing the base/head binding, event, coverage-artifact SHA-256, thresholds, selected and skipped paths, changed executable/covered/missing lines, declaration-only proofs, a passed or not_required verdict, and no_merge: true; missing coverage data, below-minimum coverage, a changed-head mismatch, malformed diff/artifact evidence, or an incomplete check-run query is a blocker. A pure-deletion file with a valid coverage row and no new-file line numbers is reported as 100.0 with scope changed executable lines 0/0; there are no new executable lines to cover. A not_required verdict is only for a head with no executable Python changes in the configured coverage scope, and remains observable in the artifact rather than being inferred from a skipped job. Hosted fast feedback runs the complete non-slow all lane, so an optional-extra change cannot be proven by a core-only shard.

The local pr_ready_check.sh coverage lane remains useful for fast feedback, but its disposable output is not merge authority. The hosted changed-coverage-gate is the merge-admission proof; scripts/dev/merge_queue_gate.py queries check runs on the exact live PR head and rejects a missing, pending, failed, malformed, or stale result for source-changing PRs. The CI workflow intentionally skips a PR whose complete changed-file set matches **/*.md or docs/**; in that case the gate fetches and validates the complete GitHub changed-file set and records changed_coverage_status: not_required. An API failure, incomplete file listing, or any mixed/non- ignored path remains a blocker. The existing coverage-gate absolute-floor and baseline checks continue to run on main/manual/merge-group full-suite events; they do not substitute for the changed-line proof. Direct merge dispatchers must consume the same exact-head check before their CAS step (tracked separately by #7407).

Relationship to the gate-side staleness check. The staleness preflight (step 7 of gh-pr-merger) remains as a safety net for guarded merges performed by gh-pr-merger and for non-queue CI providers. Inside the native merge queue, staleness is inherently fresh, so the merge-queue gate records the staleness verdict for audit purposes but does not block on it.

Final PR title/body reconciliation for squash merges

Squash merging makes the PR title and body the durable human-facing summary of the delivered change. After any revision or fix push, the review loop rebuilds the final body from the current diff, validation, claims, and follow-ups, then reconciles it with the final title through the REST-only helper:

source .venv/bin/activate

uv run python scripts/dev/gh_pr_body_rest.py <number> --reconcile \
  --title "<final title>" --repo ll7/robot_sf_ll7 --body-file <final-body.md>

The command reads the live pair first, returns an explicit no-op when unchanged, and otherwise patches title and body together before verifying both. Helper writers serialize per-PR through a host-local advisory lock held from the read through a final post-update read; if an external writer changes the pair during that window, reconciliation fails closed with a conflict. The title changes only when scope, intent, type, or issue linkage changed; the body is always regenerated against the final state. The resulting exact SHA-256 metadata digest is recorded in trusted review evidence as pr-metadata: reconciled @ <digest>, alongside the exact-head gate-verdict trailer. A missing or stale trailer blocks both gh-pr-merger and native merge-queue admission. Metadata-only reconciliation does not rerun source CI, but it invalidates prior final-state review evidence until the new digest is reviewed. The legacy body-only REST mode remains available for compatibility; new final-state handoffs use reconciliation.

If the final verification read observes a concurrent external write, the helper returns status: conflict with the previous, desired, and observed metadata digests plus any available head SHAs. It also returns the stable next_action refresh_live_metadata_and_exact_head_review, policy_state: pending_pr_metadata, and policy_action: refresh_snapshot. Treat prior_review_reuse: forbidden as binding: refresh the live PR metadata, rebuild the final review evidence, and obtain a new exact-head review before retrying. Do not overwrite the newer metadata automatically or treat this result as an ordinary successful reconciliation.

Exact-head stability snapshot (issue #7523)

Final exact-head handoffs need repeated manual refreshes whenever main moves during local proof or GitHub reports completed-success workflow jobs while check-runs remain pending. Run the deterministic stability snapshot once after local proof and again immediately before any handoff step; it is route-evidence-only, never retries automatically, and never authorizes a merge:

uv run python scripts/dev/check_pr_ci_status.py <pr-number> --stability-snapshot --json \
  --expected-head-sha <head-sha> --expected-main-sha <current-main-sha> \
  --expected-metadata-digest <64-hex> --repo ll7/robot_sf_ll7

The pr_stability_snapshot.v1 result reports the observed PR head SHA, the current main SHA, the base ref/SHA, the exact title/body SHA-256 metadata digest (the same metadata_digest as the pr-metadata: reconciled @ <digest> trailer), the CI rollup, and REST/GraphQL quota state. status is one of stable, changed, failure, pending, status_propagation_lag, quota_blocked, or error:

  • changed (with invalidated_reasons and the observed values) when the head, current main, or metadata digest moved since the snapshot. The smallest safe resume command re-runs the snapshot against the observed values; nothing is retried automatically and no merge is authorized.
  • status_propagation_lag is distinct from ordinary pending work and from terminal failure; the checks payload carries the check_run_stale_job_success diagnostic and the resume command is the bounded CI monitor (exit code 2 until the lag resolves).
  • quota_blocked surfaces the gh api rate_limit core/GraphQL reset (or a Retry-After value) as resume.min_delay_seconds and resume.resume_epoch_seconds; re-run the snapshot only after that delay, never in a spin loop.
  • Exit codes: 0 stable, 1 changed/failure/error, 2 inconclusive (pending, status-propagation-lag, or quota-blocked; resume later).

Optional --metadata-title + --metadata-body-file compare the desired final title/body pair; metadata drift then resumes with uv run python scripts/dev/gh_pr_body_rest.py <pr> --reconcile before re-snapshotting. The snapshot does not apply merge-ready, bypass reviews, or relax any fail-closed gate; scripts/dev/check_pr_current_base_cas.py and the monitor's exact-head guard remain the binding final preflights.

Reusable dev scripts

Prefer calling shared scripts from scripts/dev/ so VS Code tasks, local shells, and Codex skills use the same commands:

scripts/dev/ruff_fix_format.sh
scripts/dev/run_tests_parallel.sh
scripts/dev/run_worktree_shared_venv.sh -- pytest tests/test_ci_script_contract.py -q
scripts/dev/run_ci_local.sh
scripts/dev/local_signoff.sh --no-setup lint test
scripts/dev/check_docs_proof_consistency_diff.sh
scripts/dev/sbatch_use_max_time.sh --partition <partition> --qos <qos> --sbatch-arg --partition=<partition> --sbatch-arg --qos=<qos> SLURM/templates/gpu_training.sl
uv run python scripts/dev/update_pr_branch.py <pr-number> --expected-head-sha <head-sha>
scripts/dev/update_pr_branch_safely.sh <pr-number> --expected-head-sha <head-sha>
BASE_REF=origin/main scripts/dev/pr_ready_check.sh
PR_READY_MODE=final BASE_REF=origin/main scripts/dev/pr_ready_check.sh
uv run python scripts/dev/complexity_runtime_baseline.py --top 10 robot_sf scripts tests
uv run python scripts/dev/ci_timing_summary.py --run-id <github-actions-run-id> --top 10
scripts/dev/gh_comment.sh pr --current <<'EOF'
Summary line
- bullet 1
- bullet 2
EOF

scripts/dev/run_ci_local.sh is the local CI-equivalent entrypoint for the shared validation phases. By default it runs uv sync --all-extras --frozen, migrates legacy artifacts, then delegates to scripts/dev/ci_driver.sh so local runs and .github/workflows/ci.yml share the same phase definitions (lint, typecheck, test, examples-smoke, smoke, and artifact-policy). Pass explicit phases to scope a run, for example scripts/dev/run_ci_local.sh lint test. After dependencies are already current, use scripts/dev/run_ci_local.sh --no-setup lint test for faster repeat local feedback.

scripts/dev/local_signoff.sh is the optional local-CI attestation wrapper. It runs selected run_ci_local.sh phases, auto-installs the basecamp/gh-signoff GitHub CLI extension if missing, and posts advisory signoff/local-* statuses only after the worktree is clean and HEAD is already pushed to its push remote. It never calls gh signoff install and never changes branch protection. Use scripts/dev/local_signoff.sh --no-setup lint test for fast repeat local proof, or scripts/dev/local_signoff.sh --full before a higher-confidence handoff.

Before opening a PR, fetch the latest origin/main, integrate it into the feature branch with either merge or rebase, and only then run PR_READY_MODE=final BASE_REF=origin/main scripts/dev/pr_ready_check.sh. Final mode refuses to write readiness evidence unless the non-ignored worktree is clean, so the stamp represents committed HEAD rather than an interim dirty-tree check. Plain BASE_REF=origin/main scripts/dev/pr_ready_check.sh remains useful for local feedback while edits are in progress; if it records a dirty-tree stamp, treat that stamp as interim and rerun final mode after committing. The BASE_REF value tells the readiness gate what to compare against; it does not update the feature branch by itself, so validation from before the latest-main sync is stale for PR creation. Do not wait until PR creation to pick up main branch improvements on long-lived feature branches; merge latest origin/main into the current branch when active work starts, then sync again before opening the PR.

Use uv run python scripts/dev/complexity_runtime_baseline.py --top 10 robot_sf scripts tests before/after substantial refactor PRs when you need a quick, repeatable snapshot of largest modules, longest functions, and optional pytest duration rows from a captured --pytest-log. Use uv run python scripts/dev/ci_timing_summary.py --run-id <github-actions-run-id> --top 10 when GitHub CI wall time drifts from local readiness and you need queue, job, and slowest-step timings from gh run view data.

For routine autopilot CI waits, prefer the compact monitor helper instead of leaving the parent thread idle on raw GitHub output. In a fresh linked worktree, run it through the shared-venv wrapper so uv reuses the owning checkout's environment and does not create or prompt for a local .venv:

scripts/dev/run_worktree_shared_venv.sh -- python scripts/dev/check_pr_ci_status.py \
  <pr-number> \
  --expected-head-sha <head-sha> \
  --poll-attempts 40 \
  --poll-interval 30 \
  --max-wall-seconds 1200 \
  --json

The wrapper sets UV_PROJECT_ENVIRONMENT to the owning checkout's .venv and UV_NO_SYNC=1, so the command works from a worktree that has not run uv sync. The --help output of scripts/dev/check_pr_ci_status.py also prints this invocation for quick agent copy/paste. Use --max-wall-seconds to give long-running monitors a clean local stop path before patching or pushing a branch. The cap applies to nested gh reads as well as inter-poll sleeps; a read that reaches the cap emits one machine-readable fail-closed error and stops. On POSIX hosts, a timed-out gh process group and its local descendants are terminated together. Exit code 2 means checks were still pending when the local cap expired between reads, not that remote GitHub checks were cancelled or failed; a nested-read timeout is an error (exit code 1) and is never merge authorization. If the parent workflow is already completed/success and every recorded job step ends with a successful Complete job step while GitHub still reports a pending check/job lifecycle (either a job that remains in_progress or a stale check-run over a completed/success job), the JSON payload marks the bounded blocker as checks.pending_reason: "status_propagation_lag" and includes the parent-run/job IDs. It also emits checks.diagnostic: "check_run_stale_job_success" (and copies that code into monitor.diagnostic) so consumers can distinguish this check-run reconciliation condition from ordinary pending work. This remains fail-closed pending evidence; it is not merge authorization. When current Actions checks remain queued beyond the monitor's five-minute default threshold, the payload instead records checks.pending_reason: "runner_queue_starvation", the oldest queued age, queued check names, and their actionable run URLs. Use --queue-starvation-seconds to tune that diagnostic threshold for a known environment. This is an external queue blocker only: checks.overall remains pending, and neither the monitor nor merge admission treats it as success. For Actions lifecycle age warnings, queued and setup phases prefer the current job's created_at, then fall back to the workflow timestamp; this avoids aging a newly queued job from an older parent workflow. The human actions_gate_age summary counts the corresponding age_warnings entries and names them, while missing timestamps remain unaged and fail-closed. The workflow also runs a separate reproducibility-check-reconciliation job after the diagnostic. That job invokes scripts/dev/reconcile_reproducibility_check_run.py, which identifies the exact Actions job by workflow run, attempt, and head SHA. It patches a check-run only when that exact job is completed successfully and the check-run is still pending, then reads the check-run back to verify completed/success. Identity mismatches, terminal failures, missing jobs, and API errors remain fail-closed with a JSON report; the reconciliation job is diagnostic-only and outside the ci aggregate.

For a pending Actions check with a job URL, the monitor performs bounded REST enrichment of the workflow run and job records to report the current phase separately from test conclusions. The default stale warning threshold is 900 seconds; set it explicitly when a different operational window is appropriate:

scripts/dev/run_worktree_shared_venv.sh -- python scripts/dev/check_pr_ci_status.py \
  <pr-number> \
  --expected-head-sha <head-sha> \
  --actions-stale-after-seconds 900 \
  --poll-attempts 40 --poll-interval 30 --max-wall-seconds 1200 --json

checks.actions_lifecycle reports queued, setup, and in_progress items with phase age, timestamp source, run/job IDs, and exact-head matching. checks.age_warnings marks gates that exceed the configured threshold without changing the fail-closed checks.overall: "pending" result. checks.superseded_runs names an older exact-head run and its newer same-workflow replacement rather than hiding the replacement relationship behind a count. When a stale run has an independently matching head SHA, checks.recovery prints inspect, cancel, rerun, and bounded monitor commands. These are explicit suggestions only: the tool does not cancel or rerun Actions, and it never authorizes a merge. Missing REST metadata or a mismatching run head suppresses mutation commands and leaves the route evidence incomplete.

Each JSON payload includes monitor metadata for the active delegation ledger: expected head SHA, SHA-match result, poll attempt, wait budget, optional wall-clock cap, deadline, and route_evidence_only: true. When the local wall cap expires while checks are still pending, the payload also includes monitor.local_stop_reason: "max_wall_seconds". Monitor success is route evidence only; reassess the current PR head SHA and normal readiness proof before labeling or merging.

When CI polling repeats similar JSON in the parent thread, switch to status-change summaries and write the full monitor payload to the active ledger or a common-Git-dir artifact. For in-progress run debugging, inspect job metadata first; fetch direct job-log excerpts only for the failing or suspect step, and avoid gh run view --log dumps until the run is complete enough for that command to return useful output.

When a completed job's normal log is absent (for example, a runner infrastructure failure omitted the job from the log archive), recover its retained check-run annotations with:

uv run python scripts/dev/diagnose_actions_job.py <job-id>

The helper prints normal logs when they are available and otherwise prints the annotations linked from the job metadata. It exits nonzero if neither source provides diagnostics.

For routine goal-autopilot orientation, prefer the compact state snapshot helper before broad parent thread reads:

uv run python -m scripts.dev.autopilot_state_snapshot \
  --include-worktrees \
  --claim-issue <issue-number> \
  --issue-search "is:issue is:open <queue-filter>" \
  --pr <pr-number>

The JSON output includes source commands, branch/head SHA, origin/main SHA, linked worktrees, claim refs, issue queue rows, explicit PR headline state, compact tracked status, generated-path presence, a controller_checkpoint, and freshness metadata. Use the checkpoint as the first resume artifact after compaction or automatic continuation: it should name the active branch/PR, known generated paths, stale claims, check state, and next action without reopening raw logs, issue queues, worktree inventories, or skill files. Compact status omits generated untracked trees such as .venv, .opencode, node_modules, and output, reporting only the generated roots that are present. Check summaries reconcile duplicate timestamped runs from the same workflow/job and expose the discarded count as superseded; an older cancelled run is not treated as current failure when a newer replacement is present. Run fresh focused gh/git checks before claim, push, PR, label, merge, or publication decisions. Raw logs and broad CLI output are appropriate when the snapshot reports ok: false, stale claims, missing state, or insufficient fields.

Stale issue-claim reconciliation (issue #7025)

Claims are cross-machine leases, not disposable branches. Inspect the bounded, read-only claim audit before considering cleanup:

uv run python scripts/dev/issue_claim.py reconcile --limit 100

The report joins each agent-claims/issue-<number> ref with the live issue state and explicit PR references. It marks a claim releasable only when the issue is closed, or when only terminal covering PRs remain, and no open covering PR is observed. Missing or contradictory issue state, active coverage, malformed responses, and capped PR inventories remain preserved with a reason; ok: false is an intentional fail-closed result, not permission to delete anything.

An explicit cleanup pass requires a terminal reason and re-reads the claim SHA, issue state, and coverage immediately before using Git's compare-and-delete lease:

uv run python scripts/dev/issue_claim.py reconcile \
  --release-stale --reason closed --limit 100

Do not run the cleanup form as a substitute for reviewing the report, and never delete historical claim refs by branch age or by name alone. An SHA race, state change, active PR, or incomplete snapshot retains the claim.

Worktree rows are capped by default; use worktree_count and worktrees_truncated to decide whether a larger --worktree-limit is worth the parent-thread context cost. For remote cleanup and branch-drift triage, use the read-only hygiene snapshot before broad git worktree output or stale-worktree cleanup:

uv run python scripts/dev/worktree_hygiene_snapshot.py --repo-status --retirement-plan --json

The payload reports total and included worktree counts, dirty worktrees, missing upstreams, ahead/behind drift, detached heads, truncation status, and optional preservation-aware retirement reasons. The retirement projection fails closed to preserve or review for dirty tracked or untracked content, unpushed commits, detached or missing-upstream rows, active or unavailable claim state, unavailable merge state, and ignored output/ content that looks durable or needs handoff. Use --filter <branch-or-path-substring> or --worktree-limit <n> when remote hosts have many linked worktrees.

For a read-only preservation-aware retirement projection, use the bounded report explicitly:

uv run python scripts/dev/worktree_hygiene_snapshot.py \
  --retirement-plan --include-all-worktrees \
  --worktree-budget 256 --time-budget-seconds 60 --json

The retirement projection classifies each row as preserve, review, or removeable. It joins bounded PR coverage and remote issue-claim state, reports dirty/ahead/detached/missing-upstream reasons, and classifies ignored roots as cache, documented disposable output, durable-required, or handoff-needed. --worktree-budget and --time-budget-seconds bound the all-worktree scan itself, including local inventory construction. Rows that do not fit are retained as review-only, and the JSON progress.terminal_status is incomplete; needs_review also reports unavailable evidence. Treat any non-zero exit from an incomplete or needs-review report as a stop signal. Unknown PR, claim, status, or artifact evidence is a blocker. The command never removes worktrees; any later removal still requires human approval and the preservation procedure above.

For delegation routing and PR-review polling, treat snapshot_pr_queue as the entry point:

  • Preflight lanes with --expected-head-sha <sha> before dispatch.
  • Reuse preflight.status (healthy | stale | blocked) and next_action to avoid stale or noisy routes.
  • Explicit stop-state labels (blocked, state:blocked, state:blocked-external-input, evidence:blocked, state:hold, or decision-required) are preserved in preflight.blocked_state. They take precedence over review or merge hints and emit await_blocker_owner_or_approval with the relevant next_owner_or_gate; a green check result does not clear an explicit policy, evidence, or approval blocker.

For example, a green, mergeable PR carrying state:blocked remains owner-gated:

{
  "preflight": {
    "status": "blocked",
    "blocked_state": {
      "status": "blocked",
      "labels": ["state:blocked"],
      "reasons": ["explicit_blocked:state:blocked"],
      "next_owner_or_gate": "blocker_owner_or_maintainer"
    }
  },
  "next_action": "await_blocker_owner_or_approval"
}
  • Invalidate stale-lane routes (refresh snapshot) before reassigning or reviewing.
  • Schema pr_queue_snapshot.v2 adds base_freshness to each PR row, with base_sha, current_main_sha, a bounded verdict (fresh, stale, missing-base, or unavailable-current-main), and the required action. Stale bases are stale; missing or unavailable provenance is blocked, so those rows cannot route to merge readiness from the compact snapshot alone.
  • If GraphQL quota is exhausted during --active discovery, the snapshot uses a bounded REST open-PR list plus paginated per-PR REST enrichment. The active list uses up to 100 rows per page and marks truncated: true only when the requested cap may have discarded rows; a short final page proves completion. Per-PR reviews, conversation comments, and head-bound check runs use bounded 100-row pages and fail closed after the page budget or on malformed payloads, with endpoint status recorded in rest_enrichment. Such snapshots carry data_source: rest_fallback_graphql_quota and route_evidence_only: true; GraphQL-only review threads are unknown_graphql_quota, so every row remains blocked from merge-ready admission until a fresh thread-capable snapshot is available. A REST-list failure emits one compact error row and never fabricates PR entries.
  • Start review loops from compact review_snapshot, comment_snapshot, and checks output, not raw full-comment payloads.

Integration admission report (issue #7647)

scripts/dev/integration_admission_report.py classifies one PR and a bounded queue snapshot as report-only routing evidence. It consumes an existing pr_queue_snapshot.v2 JSON payload, reuses the PR loop policy and trusted metadata/claim readers, and never calls GitHub or authorizes an external action. Missing baseline data is unavailable; malformed input is invalid; blocker and invalidation codes remain explicit. Use a fixed --as-of instant when age/freshness is required:

uv run python scripts/dev/integration_admission_report.py \
  --snapshot output/pr_queue_snapshot.json --pr 2677 --max-queue-items 20 \
  --as-of 2026-08-20T12:00:00Z --json

The versioned output contract is integration_admission_report.v1.schema.json. The report uses the frozen policy dimensions docs | test_only | tooling | runtime | benchmark | evidence | release, isolated | component_shared | repository_control_plane, low | standard | optional_matrix | full, ordinary | independent_exact_head | domain | author, none | network | artifact | compute | release, and ordinary | current_base_required. Unknown and unavailable inputs remain explicit. Queue output includes state counts plus separate CI, review, and external lane demand; it is an estimate, not a dispatch command.

The #7520 pilot is report-only for 14 days and at least 20 terminal PR dispositions, extending to 28 days if the sample is smaller. Retain the policy only if useful terminal throughput is not materially reduced and CI spent on superseded/unadmitted heads, invalidated exact-head reviews, duplicate or competing PRs, stale prepared candidates, post-merge repairs, and maintainer/domain decision latency do not worsen. Record a retain, revise, or roll_back disposition with those measures; raw open-PR count is diagnostic only. This report remains implementation/workflow evidence, not merge authority or scientific evidence.

uv run python -m scripts.dev.snapshot_pr_queue --prs 2677 --json \
  --expected-head-sha "$PR_HEAD_SHA"

The resulting JSON keeps review/comment/CI payloads compact; review noise is reduced to counts, latest author-attributed samples, and bounded body excerpts.

When GraphQL quota is exhausted, --active uses a bounded REST open-pull-request list and the paginated per-PR REST enrichment instead of returning an error-only queue. Such snapshots mark data_source: rest_fallback_graphql_quota and each row carries review_threads_admission: fail_closed_unknown, because REST cannot refresh GraphQL-only review threads. REST enrichment status is exposed under rest_enrichment; an endpoint failure or page budget exhaustion is recorded and blocks the row's preflight. The PR loop policy classifies a merge-ready row in that state as unknown_review_threads and routes it to await_review_threads; the fallback is queue orientation only and never establishes merge readiness.

Use BASE_REF=origin/main scripts/dev/check_docs_proof_consistency_diff.sh before PR handoff when a branch adds or edits context notes, evidence bundles, or other proof-heavy docs surfaces. The checker is intentionally conservative: it only flags high-confidence issues such as missing docs/context/README.md links for new top-level context notes, tracked evidence files that still contain absolute local paths, and tracked evidence that links to ignored output/ artifacts. It also validates the curated machine-readable context catalog at docs/context/catalog.yaml so indexed context entry points keep explicit status and freshness metadata. Run uv run python scripts/validation/check_docs_proof_consistency.py --check-evidence-catalog for the explicit full evidence-catalog hygiene pass; it scans tracked docs/context/evidence/ bundles and reports bundles that have no catalog entry. When issue or PR text needs to classify proof strength, use the artifact evidence vocabulary so local output/ paths are not promoted into durable benchmark or paper-facing claims.

Issue-reading with comments (REST-backed)

gh issue view <number> --comments fails on GitHub CLI 2.45.x (Ubuntu noble) with a repository.issue.projectCards GraphQL deprecation error (issue #5729), and the native-first read also fails with a GraphQL: API rate limit already exceeded error when the GraphQL quota is exhausted (issue #5896). The thread command falls back to paginated REST reads for all GraphQL-path failures (deprecated field, quota exhaustion, secondary rate limit, generic GraphQL error) while keeping authentication, authorization, and repository-resolution failures fail-closed. Use the REST-backed helpers for all issue-with-comments reads:

# Preferred: complete thread read with native-first fallback
uv run python scripts/dev/gh_issue_rest.py thread <number> --repo ll7/robot_sf_ll7

# Shell wrapper (same logic, concise invocation):
bash scripts/dev/gh_issue_view.sh <number> --repo ll7/robot_sf_ll7

# Explicit REST read with normalized fields (stable JSON output shape):
uv run python scripts/dev/gh_issue_rest.py view <number> --repo ll7/robot_sf_ll7 --comments
uv run python scripts/dev/gh_issue_rest.py view <number> --json number title state url labels comments

All issue-delivery skills (goal-issue-implementation, gh-issue-clarifier, and its selected-issue compatibility aliases) use gh_issue_rest.py thread as the primary path; see docs/context/issue_713_batch_first_issue_workflow.md for the full command reference.

Blocked-queue re-surfacing (issue #7070)

Use the blocked-queue watcher to report blocked issues whose explicit blocked-triage-v1 issue/PR dependency is closed or merged:

uv run python scripts/dev/blocked_queue_watcher.py \
  --repo ll7/robot_sf_ll7 --json

The default is report-only. The tier-1 issue-graph evaluator resolves all referenced issues and pull requests in one GraphQL request, and classifies path, external, in-repository, malformed, or otherwise unsupported conditions as unevaluatable. API failures are errors, never clean/not-fired results.

The shared issue-audit writer applies an additional fail-closed guard before adding state:blocked or state:blocked-external-input: the complete issue thread must already contain a blocked-triage-v1 reason block or a Blocked-by: #<number> reference. A prose-only blocker is reported but does not receive a dispatch-suppressing label; the writer adds needs-triage when that existing label is available. The audit plan's blocked_label_report records the evidence and the applied or declined decision, and the REST apply path rejects a blocked-label mutation missing its reason evidence.

After reviewing the report, an explicitly authorized routing pass may add only needs-triage to fired issues:

uv run python scripts/dev/blocked_queue_watcher.py \
  --repo ll7/robot_sf_ll7 --apply --json

The watcher never writes state:ready; human review remains required before a blocked issue becomes dispatchable. The path/external/in-repository tiers need separately reviewed adapters and are not inferred from issue prose.

Issue #7074 adds opt-in blocked-triage-v1 adapter mappings. The mapping is the executable contract; prose in unblock_condition and watcher remains descriptive only. All adapters require version: 1, reject unknown fields, and produce fired, not-fired, unevaluatable, or error with proof provenance. A malformed mapping is unevaluatable, and an adapter/API failure is error, so --apply will not route either case.

Supported mappings are deliberately small and bounded:

adapter:
  version: 1
  kind: path_presence
  name: path_exists
  path: configs/benchmark/risk_layer_ablation.yaml
  path_type: file
adapter:
  version: 1
  kind: external_probe
  name: github_graphql_quota
  minimum_remaining: 100
adapter:
  version: 1
  kind: repo_predicate
  name: text_present
  path: robot_sf/adversarial
  text: adversarial_independent_outcomes

Path checks stay below the repository root. Repository predicates may only scan configs/, docs/, robot_sf/, scripts/, or tests/, and are limited to 256 files and 8 MiB. The external probe is a fixed GitHub CLI quota lookup; arbitrary shell commands, URLs, credentials, issue prose, and state:ready writes are not supported.

For GitHub issue batches and Project #5 updates, follow the batch-first workflow note:

  • docs/context/issue_713_batch_first_issue_workflow.md
  • Use REST-backed gh api repos/... calls for ordinary issue, label, PR, branch, commit, and workflow-run operations when possible.
  • Reserve GraphQL for Projects v2 operations, review-thread operations, and nested reads that are genuinely cheaper.
  • Use local git for branch, diff, merge-base, and commit state instead of asking GitHub.
  • Prefer GitHub MCP / GitHub app tools for interactive issue, PR, and project work when available, but switch to REST for issue cleanup when GraphQL quota is low.
  • Keep gh for scripted batch operations, derived score sync, auth debugging, REST fallback, and one-off deterministic commands.
  • Clean up issues first, then route Project #5 metadata, then run derived score sync once at the end.
  • Cache project and field IDs once per shell session instead of rediscovering them for every issue; for long-running or multi-agent work, use a local gitignored .github/cache/project5.json cache following docs/templates/github.project5-cache.example.json.
  • Check gh api rate_limit before large batches and leave Project #5 writes pending when GraphQL is exhausted instead of retry-looping.
  • For low-GraphQL or long autonomous publication runs, keep a REST-first command ledger covering PR creation, commit check-run polling, issue comments, labels, merge, and branch cleanup. Treat remote branch deletion reporting a missing ref after merge as a cleanup caveat to record, not as evidence that the merge failed; verify the merged PR or base branch SHA instead.

PR labels, conversation comments, and publication (REST-backed)

On affected GitHub CLI versions, gh pr edit <number> --add-label <label> and gh pr view <number> --comments fail inside the GraphQL client with the retired Projects Classic field (repository.pullRequest.projectCards) and can even exit 0 while emitting the error and no usable content (issue #6496, observed while reviewing #6454). Neither operation needs Projects Classic data, so perform them through the REST-only helpers instead of the broad gh pr commands:

# read the current issue/PR label inventory (paginated REST read)
uv run python scripts/dev/gh_pr_label_rest.py list <number> \
  --repo ll7/robot_sf_ll7

# add/remove a PR label (verify-on-write, pure REST issues-labels endpoint)
uv run python scripts/dev/gh_pr_label_rest.py add <number> \
  --label merge-ready --repo ll7/robot_sf_ll7
uv run python scripts/dev/gh_pr_label_rest.py remove <number> \
    --label merge-ready --repo ll7/robot_sf_ll7

# PR conversation comments, drop-in for `gh pr view <number> --comments`
# (pure REST repos/{repo}/issues/{n}/comments; no projectCards field queried)
uv run python scripts/dev/gh_pr_comments_rest.py <number> --repo ll7/robot_sf_ll7
uv run python scripts/dev/gh_pr_comments_rest.py <number> --repo ll7/robot_sf_ll7 --plain

# publish a PR conversation comment without the GraphQL `gh pr comment` path
scripts/dev/gh_comment.sh pr <number> --repo ll7/robot_sf_ll7 --body-file <path>
scripts/dev/gh_comment.sh pr --current --repo ll7/robot_sf_ll7 --body-file <path>

The label helper covers paginated reads plus merge-ready add/remove (and any PR or issue label) through repos/{repo}/issues/{number}/labels, which GitHub treats as the PR label endpoint. The comment helper reads the conversation thread through repos/{repo}/issues/{number}/comments (GitHub treats PR numbers as issue numbers), returning the PR header plus the same conversation-level comments gh pr view --comments would show. Inline review comments (pulls/{number}/comments) are intentionally out of scope. The existing gh_comment.sh pr wrapper resolves an explicit or current PR through REST and posts the body file to issues/{number}/comments, so it does not require a GraphQL PR-comment lookup. Read-only PR header fields still use gh pr view <number> --json ...; only the label-edit and --comments paths hit the deprecated field. The PR-review and guarded-merger skills (goal-pr-review, gh-pr-merger) reference these helpers for label, read, and publication operations, and the REST reads fail closed on auth, malformed, or truncated payloads. Focused offline tests live in tests/dev/test_gh_pr_label_rest.py and tests/dev/test_gh_pr_comments_rest.py; the production comment wrapper is covered by tests/test_ci_script_contract.py.

REST-first publication snippets for low-GraphQL autopilot

For token-efficient autopilot runs, collect compact local snapshots before broad GitHub or repository reads:

uv run python -m scripts.dev.snapshot_issue_batch 2665 2675 --json
uv run python -m scripts.dev.snapshot_issue_batch 2665 2675 --json \
  --capsule-dir "$(git rev-parse --path-format=absolute --git-common-dir)/codex-agent-runs/active"
uv run python -m scripts.dev.snapshot_issue_batch --claimable --json
uv run python -m scripts.dev.snapshot_issue_batch --blocked-external-report \
  --report-path "$(git rev-parse --path-format=absolute --git-common-dir)/codex-agent-runs/active/blocked-external-assets.md"
uv run python -m scripts.dev.snapshot_issue_batch --active-portfolio \
  --report-path "$(git rev-parse --path-format=absolute --git-common-dir)/codex-agent-runs/active/active-issue-portfolio.md" \
  --json
uv run python -m scripts.dev.snapshot_pr_queue --prs 2677 2678 2679 --json \
  --expected-head-sha "$PR_HEAD_SHA"
uv run python scripts/dev/pr_babysitter_snapshot.py 2679 --expected-head-sha "$SHA" --json
uv run python scripts/dev/watch_pr_ci_status.py 2679 --expected-head-sha "$SHA" --json --once

The --claimable compatibility command emits a candidate_queue: label-filtered rows are candidate evidence, while only rows in its claimable_issues list passed the live admission check. It omits issues classified as blocked on external data, assets, licenses, or human staging input. Use --include-blocked-external only when deliberately auditing that parked queue, or --blocked-external-report to generate a compact human-action report with monthly review dates. Use --active-portfolio for a compact non-mutating open-issue portfolio that classifies executable, human-decision, blocked-external, diagnostic-only, stale synthesis, and paper-critical rows with owner types and label-change recommendations. Issues carrying routing:needs-compute remain visible for audit but classify as needs_compute and are excluded from implementation dispatch until compute or private execution authorization is established. Issues carrying needs-triage likewise remain visible for audit but are excluded from autonomous implementation dispatch until human routing review clears the label. Any explicit blocked:* label is likewise retained for audit but classifies as blocked, including the exact blocker label in the row's non-claimable evidence; the row remains excluded from autonomous implementation dispatch. Explicit state:review rows are retained for audit and classify as review; they remain outside autonomous implementation dispatch until the review gate is cleared. The queue also emits an admission_reason_histogram so not_admitted counts remain actionable. Before any autonomous claim write, route the candidate through scripts/dev/goal_issue_admission.py, which performs the live source-ref, issue, dependency, and claim preflight before calling the atomic claim writer. Snapshot consumers should use the canonical admission projection; direct issue_claim.py acquire calls are reserved for an explicit maintainer override with an actor, reason, and declaration that no scientific claim is being made.

Use the snapshot JSON to seed worker prompts and active ledgers. Redirect broad search output or raw GitHub bodies to private agent-run artifacts; return only the compact snapshot, context capsule path, validation command, exit status, and short evidence excerpt to the parent Codex thread.

For routine repository discovery, exclude the dense tracked evidence archive unless the archive is the explicit search target. Start with a focused command such as:

rg -n "<concept>" AGENTS.md docs scripts tests robot_sf .agents \
  --glob '!docs/context/evidence/**' --glob '!output/**'

Omit the evidence exclusion only when the task is to inspect or validate evidence artifacts. This keeps ordinary code and workflow matches visible without treating the archive as unimportant.

For implementation-thread validation, prefer scripts/dev/run_focused_tests.sh for focused pytest targets. It stores the full pytest log under the common Git-dir agent-run artifacts and prints only a bounded pass/fail summary by default. Use FOCUSED_TEST_FULL_OUTPUT=1 only when the raw pytest stream itself is the thing being debugged. For non-pytest gates or commands that may produce large failure logs, use uv run python scripts/dev/run_compact_validation.py -- <command>. It stores the full log and summary JSON under the common Git-dir agent-run artifacts and prints only the command, exit code, elapsed time, artifact paths, failing pytest node ids when present, and a bounded failure excerpt.

After delegated worker runs, summarize route efficiency from one or more scripts/dev/routed_worker_manifest.py outputs without reading raw worker logs:

uv run python scripts/dev/route_efficiency_report.py output/issue-2764/worker/routing_manifest.json \
  --format markdown

The report counts delegated attempts, complete artifact sets, reroutes, validation presence, and optional final acceptance metadata. It also emits a routing_recommendations array with deterministic classes (prefer_provider, avoid_provider, investigate_failure_class, reroute_threshold_met, no_recommendation). Each entry includes class, action, evidence, and caveat keys, and the markdown report includes a compact "Routing recommendations" section. Route success and complete artifact presence are route evidence only; they are not task acceptance. The orchestrator must still inspect the diff and run the required local validation.

PR-loop dry-run policy can consume the same routed-worker manifests directly:

uv run python -m scripts.dev.pr_loop_policy --snapshot output/pr_queue.json \
  --manifest 1234=output/issue-2764/worker/routing_manifest.json --json

Manifest-driven decisions remain dry-run and mutation-free. Complete artifacts can unblock ready_to_merge classification only when the compact PR snapshot is otherwise ready; missing artifacts, failed validation text, stale expected heads, risky manifest paths, and draft PRs stay reroute/stop signals instead of task acceptance.

For phase-end token audits, record one compact route-efficiency row before starting another delegated batch:

  • latest Codex usage snapshot and whether it is close to the user-defined stop guard;
  • largest parent-thread outputs since the previous audit, such as broad multi-directory rg results, full skill rereads, raw validation output, raw GitHub JSON, or verbose delegate final messages;
  • failed command patterns, confusing helper contracts, repeated monitor noise, and unclear instructions that caused retries or semantic drift;
  • cache hits and misses for loaded skills, issue or PR snapshots, route quota facts, CI monitor commands, and validation artifacts;
  • accepted, rejected, rerouted, and skipped delegates, with the reason the route was or was not cheaper than direct Codex work;
  • next route change, such as using rg --files | rg <pattern> before content search, requiring smaller app-agent final messages, or reusing a recorded quota reset instead of retrying the same blocked route.

Keep the row in the active ledger or a common-Git-dir self-review note. Promote it into durable docs only when the same leak repeats, the leak was expensive, or the user explicitly asks for workflow improvements.

For implementation-thread reviews over a recent time window, do not reopen the full transcript in the parent thread. Generate a bounded session-summary artifact that records the time window, record counts, largest output records, broad commands, failed commands, and unclear-instruction themes; use that artifact plus existing self-review notes as the evidence base for any workflow patch.

For historical route audits, add --dashboard and pass multiple routed-worker manifest files. Dashboard mode emits route_efficiency_dashboard.v1 JSON or Markdown with overall metrics, per-manifest breakdowns, provider trends, incomplete-provider and failure-class totals, common missing artifact counts, and the same route-evidence-only recommendations and warning.

Version 2 routed-worker manifests also expose the additive aggregation contract. A route is confirmed only when its terminal state is successful and non-empty result, narrative, and validation artifacts are present. Empty worker output, an explicit no-findings signal, or a permission-denied/headless failure remains inconclusive; readers must downgrade a contradictory reported confirmed value. This is still route evidence only, not task acceptance or research evidence.

They also expose additive delegation records per attempt and a bounded delegation_recovery.v1 recovery object. A pre-start startup_backend_404 is distinct from a worker_task_failure and may recommend one next startup attempt, but the manifest does not sleep or spawn a worker. Once the hard retry budget is exhausted, recovery.fallback requires manual or local review and keeps independent_review_authorized false; a successful prior worker suppresses later retries to avoid duplicate work.

PR Review: Route Efficiency

When reviewing PRs with route-efficiency changes, ensure:

  • Route completeness vs task success: Complete artifacts or a zero exit code are not evidence that the task was accepted, correct, or merged.
  • Validation presence vs validation success: Check validation_presence.present separately from validation_presence.success_inferable; a validation artifact can exist while the validation result failed or stayed ambiguous.
  • Reroute count interpretation: Treat high reroute counts (2 or more) and reroute_threshold_met warnings as routing-friction evidence, not as proof that the final diff is wrong.
  • Raw-log avoidance: Start from scripts/dev/route_efficiency_report.py outputs and compact worker artifacts; read raw worker logs only when compact evidence is missing, inconsistent, failed, or suspicious.
  • Visible evidence warning: Confirm the report still displays the route-evidence-only warning and does not let route metrics replace manual diff inspection or local validation.
  • Startup recovery boundary: Confirm a pre-start backend 404 is not relabeled as a worker task failure, retry recommendations are bounded, and a failed delegation cannot supply accepted review evidence or merge-ready authorization.

Shared model routing

Use the shared model-routing pointer for delegated model and provider selection. It owns the current native tiers, evidenced escalation rule, and external-provider budget alternatives. Do not copy a volatile model inventory or maintain a local sidecar route table in this repository. Route resolution is dispatch context, not validation or acceptance proof.

Assume OWNER, REPO, ISSUE, BRANCH, and BASE are set:

gh api repos/$OWNER/$REPO/pulls -X POST \
  -f title="Issue #$ISSUE: short summary" \
  -f head=$BRANCH -f base=$BASE \
  -f body="Automated publication update from Issue #$ISSUE"
PR=$(gh api repos/$OWNER/$REPO/pulls --method GET \
  -f state=open -f head="$OWNER:$BRANCH" \
  --jq '.[0].number')
uv run python scripts/dev/gh_pr_body_rest.py "$PR" --repo "$OWNER/$REPO" \
  --reconcile --title "<final title>" --body-file /path/to/updated-pr-body.md
RUN_ID=$(gh api repos/$OWNER/$REPO/actions/runs \
  --method GET \
  -f branch=$BRANCH \
  -q '.workflow_runs | sort_by(.created_at) | reverse | .[0].id')
while :; do
  STATUS=$(gh api repos/$OWNER/$REPO/actions/runs/$RUN_ID --jq '.status')
  echo "run=$RUN_ID status=$STATUS"
  [ "$STATUS" = "completed" ] && break
  sleep 20
done
CONCLUSION=$(gh api repos/$OWNER/$REPO/actions/runs/$RUN_ID --jq '.conclusion')
PR_SHA=$(gh api repos/$OWNER/$REPO/pulls/$PR --jq '.head.sha')
gh api repos/$OWNER/$REPO/commits/$PR_SHA/check-runs \
  --jq '.check_runs[] | [.name, .status, .conclusion] | @tsv'
gh api repos/$OWNER/$REPO/issues/$ISSUE/comments -f body="CI=$CONCLUSION (run=$RUN_ID)"
gh api repos/$OWNER/$REPO/issues/$ISSUE/labels -X POST -f labels[]="autopilot-reviewed"
gh api repos/$OWNER/$REPO/issues/$ISSUE/labels/needs-publication -X DELETE --silent
RECEIPT_FILE=/tmp/pr-$PR-single-account-receipt.json
uv run python scripts/dev/single_account_merge_receipt.py \
  --repo "$OWNER/$REPO" --pr "$PR" --mode report-only --output "$RECEIPT_FILE"
uv run python scripts/dev/single_account_merge_receipt.py \
  --repo "$OWNER/$REPO" --pr "$PR" --mode apply --receipt-file "$RECEIPT_FILE"
gh api repos/$OWNER/$REPO/git/refs/heads/$BRANCH -X DELETE \
  || gh api repos/$OWNER/$REPO/git/refs/heads/$BRANCH --silent >/dev/null \
  || echo "branch ref already absent; confirm merge by checking issue/merge commit before closing out"
  • GraphQL is still appropriate for: Projects v2 objects, review-thread resolution, and nested permission-dependent reads where REST needs multiple expanded requests and GraphQL is materially cheaper.

Context note workflow

For non-trivial work, persist reusable insights, decisions, reasoning, validation notes, and handoff context in Markdown instead of leaving them trapped in chat or PR history.

  • Use docs/context/README.md as the canonical workflow and naming guide.
  • Prefer updating an existing canonical note before creating a new one.
  • If a touched note is outdated or superseded, update it, remove it, or mark it clearly with a pointer to the current source.
  • Link notes to the related issue/PR, canonical docs, validation commands, and replacement notes.
  • Use .agents/skills/context-note-maintainer/SKILL.md when the task includes creating or refreshing context notes.
  • For docs/context-only changes, use the documented default gate: inspect the diff, verify changed README/INDEX/catalog links, and run BASE_REF=origin/main scripts/dev/check_docs_proof_consistency_diff.sh. The wrapper auto-detects context-only PRs and includes README/INDEX/catalog in that gate. State explicitly when benchmark, simulator, or full PR readiness gates were not run because the branch only changes discoverability or workflow text.

Canonical context-note integrity checks

Use the following sequence when adding or changing a context note. Run it from the repository root so the commands resolve the active worktree and its origin/main base:

# Changed-document proof, including README/INDEX anchors for context-only diffs.
BASE_REF=origin/main scripts/dev/check_docs_proof_consistency_diff.sh

# Repository-wide Markdown link integrity (the full-link pass).
uv run python scripts/dev/check_docs_evidence_integrity.py --full

# Context-note index/catalog coverage and freshness.
uv run python scripts/tools/check_context_note_freshness.py \
  --index docs/context/INDEX.md \
  --context-dir docs/context \
  --catalog docs/context/catalog.yaml

The changed-document wrapper is the normal PR gate. To include freshness checks for only notes changed on the branch, set DOCS_PROOF_CHECK_FRESHNESS=1; use DOCS_PROOF_FRESHNESS_STRICT=1 only when the pre-existing warning backlog is intentionally part of the review. The default freshness run is non-strict: repository-wide orphan-note findings are warning-only and may remain as an existing backlog, while malformed or superseded notes without a valid replacement remain blocking. This boundary keeps a new note fail-closed without making an unrelated historical orphan warning look like a failure in the changed-document proof.

Agent memory conventions

The repository now keeps a repo-local Markdown memory layer under memory/ for stable cross-session agent context.

  • Start with memory/MEMORY.md, which acts as the concise index.
  • Store reusable memory in typed subdirectories such as memory/architecture/, memory/decisions/, memory/experiments/, memory/failures/, and memory/benchmarks/.
  • Use the experiment naming pattern memory/experiments/YYYY-MM-DD_<topic>.md.
  • Keep memory/MEMORY.md short and push detail into linked topic files so it stays compatible with startup loading in agent runtimes that read project files.
  • Use docs/context/ for issue execution history and validation detail; use memory/ only for knowledge worth reusing across future sessions.
  • Optional MCP integration should expose the Markdown files directly; do not add a retrieval database or vector store unless the repository's retrieval-deferral policy changes.

Question-first experiment registry

Use experiments/registry.yaml for planned or active exploratory ML/search/manual-control runs that need a reviewable question, hypothesis, command, artifact expectation, evidence grade, and paper relevance before execution. This registry complements GitHub issues, W&B artifacts, local telemetry under output/run-tracker/, and publication bundles; it does not make local output/ files durable.

Validate the registry with:

uv run python scripts/tools/validate_experiment_registry.py experiments/registry.yaml

On macOS, scripts/dev/run_tests_parallel.sh uses a bounded fixed xdist worker count by default instead of -n auto, because the unbounded auto worker selection can leave local validation wrappers hanging after child processes should have exited. Override with PYTEST_NUM_WORKERS=<int> or PYTEST_NUM_WORKERS=auto when needed.

scripts/dev/run_tests_parallel.sh also accepts PYTEST_XDIST_DIST=<mode> to select the pytest-xdist scheduler. The default remains load for compatibility. Use alternate schedulers such as PYTEST_XDIST_DIST=worksteal only for targeted local experiments until hosted evidence shows they improve CI timing without exposing new order dependencies.

For new SLURM batch jobs, prefer scripts/dev/sbatch_use_max_time.sh so the submitted wall time tracks the live partition and QoS maximum instead of an outdated hardcoded #SBATCH --time value. See docs/dev/slurm_submission.md for the workflow.

After a job reaches a terminal state, use scripts/tools/slurm_job_finalize.py to record the observed state, required local artifacts, checksums, and issue-update summary. See the SLURM post-run closeout guide; the finalizer is metadata-only and does not turn local files into durable benchmark evidence.

For paper-facing benchmark release runs, use the dedicated release wrapper:

uv run python scripts/tools/run_benchmark_release.py \
  --manifest configs/benchmarks/releases/paper_experiment_matrix_v1_release_v0_1.yaml

Release-process references:

  • docs/benchmark_release_protocol.md
  • docs/benchmark_release_reproducibility.md

Benchmark fallback policy

Benchmark work is fail-closed by default. Use the canonical policy notes:

  • docs/context/issue_691_benchmark_fallback_policy.md
  • docs/context/issue_1436_reproducibility_flaky_acceptance.md

Fallback execution can still be useful for diagnostics and reproduction probes, but it must not be reported as a successful benchmark outcome. Reruns are allowed only for environment-class failures, not for benchmark contract failures, fallback/degraded execution, or unfavorable statistical outcomes.

Environment factory pattern (CRITICAL)

Always use factory functions — never instantiate gymnasium environments directly:

from robot_sf.gym_env.environment_factory import (
    make_robot_env,
    make_image_robot_env,
    make_pedestrian_env,
)

# Basic robot navigation
env = make_robot_env(debug=True)

# With image observations
env = make_image_robot_env(debug=True)

# Pedestrian environment (requires trained robot model)
env = make_pedestrian_env(robot_model=model, debug=True)

The compact reviewer contract for environment creation, rollout ownership, reward ownership, benchmark verifier boundaries, and PPO run-record provenance is docs/training/environment_contract.md.

Key architectural layers

  • robot_sf/gym_env/: Gymnasium environment implementations with factory pattern
  • robot_sf/baselines/: Baseline navigation algorithms (e.g., SocialForce) for benchmarking
  • robot_sf/benchmark/: Benchmark runner, CLI, metrics collection, and schema validation
  • robot_sf/sim/: Core simulation components (FastPysfWrapper for pedestrian physics)
  • fast-pysf/: Git subtree providing optimized SocialForce pedestrian simulation
  • docs/: Documentation, design notes, and development guides

Schema Management

Canonical schema location: robot_sf/benchmark/schemas/

  • Episode schemas: episode.schema.v1.json (single source of truth)
  • Runtime resolution: Use robot_sf.benchmark.schema_loader.load_schema() for schema loading
  • Schema validation: Automatic validation against JSON Schema draft 2020-12
  • Version management: Semantic versioning with breaking change detection
  • Git hooks: Prevent duplicate schema files from being committed

Data flow and integration

  • Training loop: scripts/training/train_ppo.py → factory functions → vectorized environments → StableBaselines3 (uv sync --extra training)
  • RLlib workflow: scripts/training/train_dreamerv3_rllib.py → factory functions → RLlib env registration → DreamerV3 (uv sync --extra rllib --extra training)
  • Benchmarking: robot_sf/benchmark/cli.py → baseline algorithms → episode runs → JSON/JSONL output → analysis
  • Pedestrian simulation: Robot environments → FastPysfWrapper → fast-pysf subtree → NumPy/Numba physics

Configuration hierarchy

For complete documentation, see Configuration Architecture (precedence rules, migration guide, module structure).

Config-first workflow (default)

Prefer committed YAML configs under configs/ as the default way to run training and sweeps. This keeps runs reproducible, reviewable, and easy to replay on another machine.

  • Commit stable run definitions (scenario, seeds, metrics, cadence) in config files.
  • Document a canonical uv run ... --config <path> command in docs/PR text.
  • Reserve direct CLI tuning flags for temporary local overrides.

Use unified config classes from robot_sf.gym_env.unified_config:

from robot_sf.gym_env.unified_config import RobotSimulationConfig, ImageRobotConfig

config = RobotSimulationConfig()
config.peds_have_static_obstacle_forces = True  # Enable pedestrian-obstacle forces
config.peds_have_robot_repulsion = True  # Enable pedestrian-robot repulsion
env = make_robot_env(config=config)

Backend selection (simulator swap)

The simulation backend can be selected via configuration without modifying environment code. Available backends are registered in robot_sf.sim.registry:

from robot_sf.gym_env.environment_factory import make_robot_env
from robot_sf.gym_env.unified_config import RobotSimulationConfig

# Use fast-pysf backend (default)
config = RobotSimulationConfig()
config.backend = "fast-pysf"  # Default; can be omitted
env = make_robot_env(config=config)

# Use dummy backend (for testing)
config = RobotSimulationConfig()
config.backend = "dummy"
env = make_robot_env(config=config)

Available backends:

  • "fast-pysf" (default): SocialForce pedestrian simulation via fast-pysf subtree
  • "dummy": Minimal test simulator with constant positions (for smoke tests)

Backend registration: Custom backends can be registered via robot_sf.sim.registry.register_backend(). See robot_sf/sim/backends/ for implementation examples.

Error handling: Unknown backend names fall back to legacy init_simulators() with a warning. For strict validation, use robot_sf.gym_env.config_validation.validate_config() before environment creation.

Planner selection (visibility vs classic grid)

Global planning can be toggled via RobotSimulationConfig:

from robot_sf.gym_env.environment_factory import make_robot_env
from robot_sf.gym_env.unified_config import RobotSimulationConfig
from robot_sf.planner.classic_global_planner import ClassicPlannerConfig

config = RobotSimulationConfig(
    use_planner=True,
    planner_backend="classic",  # or "visibility"
    planner_classic_config=ClassicPlannerConfig(cells_per_meter=1.0, inflate_radius_cells=2),
)
env = make_robot_env(config=config)
  • "classic" uses the grid-based planner (Theta*/A* family) and is the default.
  • "visibility" uses the visibility-graph planner.

Utility Modules

All shared utility functions and type definitions live in robot_sf/common/:

  • robot_sf/common/types - Type aliases (Vec2D, Line2D, RobotPose, Circle2D, etc.)
  • robot_sf/common/errors - Error handling utilities (raise_fatal_with_remedy, warn_soft_degrade)
  • robot_sf/common/seed - Random seed management for reproducibility (set_global_seed, SeedReport)
  • robot_sf/common/compat - Compatibility helpers (validate_compatibility)

Example imports:

from robot_sf.common.types import Vec2D, RobotPose, Line2D
from robot_sf.common.errors import raise_fatal_with_remedy
from robot_sf.common.seed import set_global_seed

# Convenience imports also available:
from robot_sf.common import Vec2D, RobotPose, set_global_seed

Troubleshooting:

  • If IDE autocomplete doesn't work after importing from robot_sf.common, restart your IDE's language server:
    • VS Code: Command Palette → "Python: Restart Language Server"
    • PyCharm: File → Invalidate Caches / Restart

Design and development workflow recommendations

  • Consider using https://github.com/github/spec-kit for complex, multi-contract specifications and design docs. Do not use it as the default governance layer for ordinary work.
    • Examples can be found in the specs directory.
    • Prompts are unique to the llm provider used. Adjust accordingly.
    • Canonical AI assistant content lives in .agents/:
      • Canonical skills live in .agents/skills/.
      • .agents/skills/ is mirrored at .codex/skills/ and .opencode/skills/.
      • .agents/prompts/codex/ is mirrored at .codex/prompts/.
      • .agents/prompts/github/ is mirrored at .github/prompts/.
      • .agents/agents/github/ is mirrored at .github/agents/.
      • .agents/commands/gemini/ is mirrored at .gemini/commands/.
    • Validate or repair supported mirrors with uv run python scripts/tools/sync_ai_config.py --check or uv run python scripts/tools/sync_ai_config.py --fix.
    • LLM Constitution and guides can be found here:
      • docs/maintainer_values.md
      • .specify/memory/constitution.md
      • AGENTS.md
    • For the repository's cross-agent compatibility stance and the retrieval → planning → execution → verification discipline mapped to repo-local skills, see docs/context/issue_728_coding_agents_compatibility.md.
    • For autonomous goal-loop skills around issue discovery, issue implementation, PR review, and user-in-the-loop issue audit, see docs/context/goal_driven_agent_loops_2026-05-13.md.
  • Clarify exact requirements before starting implementation.
  • If necessary, ask clarifying questions (with options) to confirm scope, interfaces, data handling, UX, and performance.
    • Discuss possible options and trade-offs.
    • Give arguments to the options for easy decision-making.
    • Provide options to quickly converge on a decision.
  • For complex tasks:
    • Create a design doc (see template below) for non-trivial changes.
    • Create a file based TODO list (see example below).
    • Break task down into smaller subtasks and tackle them iteratively.
  • Prioritize must-haves over nice-to-haves
  • Document assumptions and trade-offs.
  • Ensure that the documentation, docstrings, and comments are updated to reflect code changes.
  • Docstring style is specified in pyproject.toml -> [tool.ruff.lint.pydocstyle] -> convention
  • Progress cadence: always keep tests and documentation up-to-date. As long as you document your chain of thought and what ran, you can report outcomes after finishing the work.
  • Prefer programmatic use and factory functions over CLI; the CLI is not important.
  • Working mode: prioritize a thin, end-to-end slice that runs. Optimize and polish after a green smoke test (env reset→step loop or demo run).
  • Whenever possible, add a demo or example to illustrate new functionality.
  • Avoid disabling linters, type checks, or tests unless absolutely necessary.
    • Whenever you have the chance, refactor to fix issues rather than suppressing them. Especially # noqa: C901 (complexity) and # type: ignore (type hints).
  • Prefer refactoring over adding # noqa suppressions; only use # noqa as a short-lived exception with a clear plan to remove it.
  • Always document the purpose of documents at the top of the file. (e.g., Python files, README.md, design docs, issue folders)
  • Use American English.

One-liner architecture summary

  • Architecture in one line: Gymnasium envs → factory functions → FastPysfWrapper → fast-pysf physics; training/eval via StableBaselines3; baselines/benchmarks under robot_sf/baselines and robot_sf/benchmark.
  • Environments: always create via factories (make_robot_env, make_image_robot_env, make_pedestrian_env). Configure via robot_sf.gym_env.unified_config only; toggle flags before passing to the factory.
  • Simulation glue: interact with pedestrian physics through robot_sf/sim/fast_pysf_wrapper.py. Don’t import from fast-pysf directly inside envs.
  • Baselines/benchmarks: get planners with robot_sf.baselines.get_baseline(...). Prefer programmatic runners; CLI exists at robot_sf/benchmark/cli.py for convenience.
  • Local planner adapters: start from docs/dev/planner_adapter_template.md and the diagnostic reference_adapter path before adding a new map-runner planner key.
  • Demos/trainings: keep runnable examples in examples/ and scripts in scripts/. Place models in model/, maps in maps/svg_maps/, and write outputs under output/.
  • Tests: core in tests/; GUI in tests/pygame/ (headless: DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy). Physics-specific tests live in fast-pysf/tests/.
  • Quality gates (local): Install Dependencies → Ruff: Format and Fix → Check Code Quality (Ruff + advisory ty) → Type Check (advisory) → Run Tests (see VS Code Tasks).

Test style conventions

  • Default to pytest-style tests in tests/ (functions + pytest fixtures).
  • Using unittest.mock for mocks/stubs is fine, but avoid adding new unittest.TestCase suites.
  • Legacy fast-pysf/tests/unittest remains unittest-based for upstream compatibility; new fast-pysf tests should still prefer pytest.

Docstring-on-touch

  • If you change a function/class body or signature, replace any TODO docstring placeholder in that scope with a real docstring.
  • Keep it brief and accurate (one or two sentences is enough); focus on intent and non-obvious behavior.
  • Purely mechanical edits (formatting, imports, lint fixes) do not require docstring updates.
  • Avoid mass docstring sweeps; improve documentation incrementally as code changes.
  • Use uv run python scripts/validation/check_docstring_todos.py --mode report to inspect the current placeholder backlog by top-level area and file.
  • scripts/validation/docstring_todo_baseline.json is an increase-only ratchet. Update it with --mode write-baseline only after an intentional cleanup or maintainer-approved backlog change.
  • Use uv run python scripts/validation/check_active_doc_examples.py to report stale active-doc command and artifact examples. Add --fail-on-diagnostic when a PR or CI lane should fail on new hits, and use an inline active-docs-check: allow marker only for intentional examples.

Map Bounds Format

  • MapDefinition.bounds accepts either flat tuples (x_start, x_end, y_start, y_end) or pair-of-points ((x1, y1), (x2, y2)).
  • At runtime, bounds are normalized to the flat tuple format because the fast-pysf backend and legacy utilities expect it.

Artifact policy & tooling

  • Canonical outputs live under output/ with stable subdirectories: output/coverage/, output/benchmarks/, output/recordings/, output/wandb/, and output/tmp/.
  • Run uv run python scripts/tools/migrate_artifacts.py (or the console entry point uv run robot-sf-migrate-artifacts) after pulling to consolidate any legacy results/, recordings/, htmlcov/, or coverage.json paths.
  • Enforce the policy locally and in CI with uv run python scripts/tools/check_artifact_root.py; the guard fails fast when new top-level artifacts appear.
  • Override the artifact destination by exporting ROBOT_SF_ARTIFACT_ROOT=/path/to/custom/output before invoking scripts; the helpers and guard honor the override consistently.
  • Canonical helpers in robot_sf.common.artifact_paths (e.g., ensure_canonical_tree) create the required layout for tests and tooling—prefer them over hard-coded paths.
  • Need a guided walkthrough? Follow the artifact policy quickstart for migration, guard usage, and override examples end to end.

Testing strategy (UNIFIED test suite)

For the canonical contributor QA runbook, 12-class test taxonomy, command matrix, failure classification, and explicit CI rerun rules, see Contributor QA Runbook and Test Taxonomy. For the risk-based map of critical shared contracts to their direct tests and validation lanes, see Test Traceability Matrix.

The project now uses a unified test suite running both robot_sf and fast-pysf tests via a single command.

Unified Test Suite

# Run ALL tests (robot_sf + fast-pysf) - RECOMMENDED
uv run pytest -n auto  # Number of test is steadily increasing, ca. 1200

# Run fast unit tests only (excludes slow/integration)
uv run pytest -m "not slow" tests
  # Note: integration/perf-heavy directories are auto-marked as slow in tests/conftest.py.

# Run only robot_sf tests
uv run pytest tests

# Run only fast-pysf tests
uv run pytest fast-pysf/tests  # → 12 tests

# Run with parallel execution (faster)
uv run pytest -n auto

Legacy / Specialized Test Suites

# 1. Main unit/integration tests (2-3 min) - NOW PART OF UNIFIED SUITE
uv run pytest -n auto tests  # → 881 tests

# Fast unit test pass (skip slow/integration)
uv run pytest -m "not slow" tests

# 2. GUI/display-dependent tests (headless mode)
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy uv run pytest tests/pygame

# 3. fast-pysf subtree tests - NOW PART OF UNIFIED SUITE
uv run pytest fast-pysf/tests  # → 12 tests (all passing with map fixtures)

Note: The unified test command (uv run pytest) automatically discovers and runs tests from both tests/ and fast-pysf/tests/ directories. Test count increased from ~43 (legacy documentation) to 893 tests after fast-pysf integration.

Test Significance Verification

Before fixing or investigating test failures, verify the test's value and necessity. Not all tests provide equal value; some may be outdated, overly brittle, or testing non-critical behavior.

Evaluation Questions (ask before investing fix effort):

  1. Core Feature Coverage: Does this test verify a public contract?

    • Factory behavior, schema compliance, metric correctness, deterministic reproducibility
    • If YES → High priority, fix immediately
  2. User Impact: Would failure in production affect users?

    • Incorrect metrics, broken benchmarks, environment crashes
    • If YES → High priority, fix immediately
  3. Regression Prevention: Does it catch known past bugs?

    • Validates recent fixes, prevents known failure modes
    • If YES → Medium priority, fix within sprint
  4. Edge Case vs. Common Path: Does it test rare scenarios?

    • Low real-world occurrence, no documented incidents
    • If YES and no incidents → Low priority, consider archiving
  5. Brittleness: Does it fail frequently without indicating real bugs?

    • Timing issues, display dependencies, environmental flakiness
    • If YES → Candidate for refactoring or removal
  6. Redundancy: Is the same behavior tested elsewhere?

    • Identical logic covered by unit and integration tests
    • If YES → Consider consolidation

Decision Actions:

  • High Priority (Core/User Impact): Fix immediately, these protect critical invariants
  • Medium Priority (Regression/Important Edges): Fix or update within sprint; document if deferred
  • Low Priority (Rare edges, redundant): Consider archiving; reassess value before fix effort
  • Flaky/Brittle: Stabilize with retries/mocks if valuable; otherwise remove with documented rationale

Maintenance Discipline:

  • Removing tests requires documented reason in commit message
  • Deferred low-priority failures need tracking issue with "test-debt" label
  • New tests MUST include docstring stating: (1) what contract/behavior is verified, (2) why it matters
  • Quarterly audit: review tests by runtime and failure frequency; challenge bottom 10% on value

Example: If a test for an obscure edge case in trajectory smoothing fails but:

  • No user has ever reported this scenario
  • The edge case requires artificial setup unlikely in real usage
  • Core smoothing is covered by other tests

→ Consider documenting the edge case in code comments and archiving the test, rather than spending hours debugging environmental setup issues.

Coverage workflow (explicit opt-in)

Coverage collection is no longer enabled by default. Run tests normally for fast execution, and enable coverage explicitly when needed.

The test harness sets the ROBOT_SF_ARTIFACT_ROOT environment variable so that example scripts and helpers write into a temporary directory instead of the repository tree. This keeps the canonical output/ hierarchy clean while preserving normal example behavior.

Try to increase the test coverage over time by adding tests when touching code. See the must-have checklist below for guidance.

Quick start

# Run tests (no coverage by default)
uv run pytest tests

# Run tests with coverage (CI and explicit opt-in local runs)
ROBOT_SF_PYTEST_COVERAGE=1 scripts/dev/run_tests_parallel.sh tests

# Run a focused local check and discard generated coverage output after success
scripts/dev/run_focused_tests.sh tests/test_force_flags.py -q

# View HTML report (preferred helper)
uv run python scripts/coverage/open_coverage_report.py

# Manual fallback
open output/coverage/htmlcov/index.html

# Or use VS Code task: "Run Tests with Coverage" → "Open Coverage Report"

What gets measured

  • Included: The canonical wrapper and non-PR CI measure only the robot_sf/ package. Their --cov=robot_sf argument overrides the broader pyproject.toml coverage.py source list.
  • Not measured by this command: fast-pysf/pysocialforce. It remains in the coverage.py configuration but is not included in wrapper reports or the non-PR CI coverage gate.
  • Excluded when the configured source list is used: Test files (*/tests/*, */test_*, fast-pysf/tests/*), examples (examples/*, fast-pysf/examples/*), scripts (scripts/*), tests/pygame/*, */conftest.py, */__pycache__/*
  • Output formats:
    • Terminal summary (printed after test run)
    • HTML report (output/coverage/htmlcov/index.html - interactive, detailed)
    • JSON data (output/coverage/coverage.json - for tooling)

Understanding coverage output

Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
robot_sf/gym_env/environment_factory.py   150     15  90.00%  42-45, 89-92
robot_sf/sim/simulator.py                 200     50  75.00%  10-20, 150-180
---------------------------------------------------------------------
TOTAL                                   10605    876  91.73%
  • Stmts: Total executable lines
  • Miss: Uncovered lines
  • Cover: Percentage covered
  • Missing: Line numbers not executed by tests

Coverage configuration

Configured in pyproject.toml:

  • [tool.coverage.run] — collection settings (source, omit patterns, parallel support)
  • [tool.coverage.report] — report formatting (precision, exclusions)
  • scripts/dev/run_tests_parallel.sh — explicit pytest coverage opt-in for local wrapper and CI runs

No changes needed for normal development — default pytest runs skip coverage output for faster feedback, while CI and explicit wrapper opt-in still generate reports.

Advanced usage

# Run with parallel workers (faster local feedback)
uv run pytest tests -n auto

# Run with parallel workers and explicit coverage collection
ROBOT_SF_PYTEST_COVERAGE=1 scripts/dev/run_tests_parallel.sh tests

# Run specific test file with coverage
ROBOT_SF_PYTEST_COVERAGE=1 scripts/dev/run_tests_parallel.sh tests/test_gymnasium_env_contracts.py -v
# Run specific test file without coverage
uv run pytest tests/test_gymnasium_env_contracts.py -v

# View coverage data programmatically
python -c "import json; print(json.load(open('output/coverage/coverage.json'))['totals'])"

Known limitation: focused --cov with Torch

Passing --cov directly to uv run pytest on a focused test file can trigger a RuntimeError: function '_has_torch_function' already has a docstring from torch/overrides.py when pytest-cov's trace hook causes a Torch C-extension to be partially re-initialised. This was fixed in robot_sf/telemetry/tensorboard_adapter.py (#5101) but the underlying pytest-cov+torch incompatibility persists on CPython 3.13 + torch ≥ 2.10.

Use the canonical wrapper for any coverage run that imports Torch:

# Correct: wrapper sets up coverage in a Torch-safe import order
ROBOT_SF_PYTEST_COVERAGE=1 scripts/dev/run_tests_parallel.sh tests/test_batched_lidar_kernel.py -v

# Incorrect: direct --cov flag may crash conftest collection
# uv run pytest tests/test_batched_lidar_kernel.py --cov=robot_sf.sensor.range_sensor

If you need focused changed-line coverage during development, run the test without --cov first to confirm it passes, then use the wrapper for the coverage snapshot.

For complete coverage collection details, baseline tracking, absolute floor enforcement, and CI integration, see Coverage Guide (Note: gap analysis and trend tracking were descoped under issue #3349).

For the reproducible, commit-stamped aggregate of all quality signals (test results, coverage, mutation, duration, flakiness, contract/scenario coverage, reproducibility, performance regression, and escaped defects) — which preserves the diagnostic-vs-gate distinction and never collapses signals into a single score — see Quality Report Guide (issue #6213).

Must-have checklist

  • Use factory env creators; do not instantiate env classes directly.
  • Set config via robot_sf.gym_env.unified_config before env creation; avoid ad‑hoc kwargs.
  • Keep lib code print-free; use logging from loguru for info and warnings.
  • Run VS Code Tasks: Install Dependencies, Ruff: Format and Fix, Check Code Quality (Ruff + advisory ty), Type Check (advisory), Run Tests.
  • Add a test or smoke (e.g., env reset/step) when you change public behavior.
  • For GUI-dependent tests, set headless env vars; avoid flaky display usage in CI.
  • Treat fast-pysf/ as part of the repository, changes can be made.
  • Put new demos under examples/ and new runners under scripts/.
  • Whenever a demo is possible, add one.

Optional backlog (track but don’t block)

  • Tighten type hints for new public APIs; migrate call sites gradually.
  • Add programmatic benchmark examples and extend baseline coverage.
  • Update or add docs under docs/ for new components; include diagrams when useful.
  • Add performance smoke (steps/sec) when touching hot paths.
  • Add proper docstrings to comply with pydoclint and pydocstyle

Quick links

  • Environment overview: docs/ENVIRONMENT.md
  • Simulation view: docs/SIM_VIEW.md
  • Refactoring and architecture notes: docs/refactoring/
  • SNQI tools and metrics: docs/snqi-weight-tools/README.md
  • Data analysis helpers: docs/DATA_ANALYSIS.md
  • Contributor onboarding / repo structure: AGENTS.md

Executive summary

  • Architecture: Social navigation RL framework with Gymnasium environments, SocialForce pedestrian simulation via fast-pysf subtree, StableBaselines3 training pipeline, and optional RLlib DreamerV3 workflow
  • Core pattern: Factory-based environment creation (make_robot_env() etc.) — never instantiate environments directly
  • Dependencies: fast-pysf git subtree for pedestrian physics (automatically included after clone, see Subtree Migration Guide)
  • Toolchain: uv + Ruff + ty + pytest with VS Code tasks; run quality gates before pushing
  • Testing: Unit tests in tests/, GUI-dependent tests in tests/pygame/ (with headless env vars), integration tests for smoke/performance validation
  • Documentation: Comprehensive docs under docs/ with design principles, architecture, usage, and migration notes
    • Development notes: docs/dev/*

Logging & Observability (Principle XII)

The canonical logging facade is Loguru. Library code (anything under robot_sf/ or wrappers over fast-pysf) must not use bare print() for informational or warning messages. The Ruff T201 rule is enabled to enforce this; every surviving print() in robot_sf/ is an intentional exception that must be locally justified.

Acceptable print() exception categories (issue #6478, maintainer option B):

  1. Human-facing CLI stdout — CLI entry points where stdout is the UX (e.g. robot_sf/benchmark/cli.py, robot_sf/examples_cli.py, robot_sf/benchmark/snqi/cli.py). Suppressed via a pyproject.toml [tool.ruff.lint.per-file-ignores] entry.
  2. Machine-readable stdout — JSON/Markdown report emission meant to be piped or redirected (e.g. forecast_lane_inventory.py, issue_5578_speed_tier_synthesis.py). Suppressed per-file or with an inline # noqa: T201 - CLI output.
  3. Subprocess JSON IPC — a worker subprocess writing a JSON result to stdout for its parent process (e.g. camera_ready/resource_lifecycle.py). Inline # noqa: T201 - subprocess JSON IPC to parent process.
  4. Loguru-unavailable fallback — a fallback logger body that runs only when Loguru cannot be imported, where using Loguru would be circular (e.g. episode_replay_figure.py::_LoggerFallback). Inline # noqa: T201 - loguru unavailable in fallback logger.
  5. Early bootstrap failures before logging configuration, and tests explicitly asserting stdout content.

Never reroute categories 1–3 to Loguru: that moves machine-readable or human-facing stdout off stdout and breaks piping, redirection, and subprocess consumers. Migration of any stray (un-justified) print to from loguru import logger with logger.info|warning|error is maintenance (PATCH) unless it changes user-visible contract output.

Guidelines:

  • Prefer structured context (e.g., logger.info("Reset complete seed={seed} scenario={sid}")).
  • Avoid inside per‑timestep loops; aggregate and log at episode boundaries to protect performance budgets.
  • Use WARNING for degraded but continuing states (e.g., zero frames when recording requested), ERROR for aborting conditions, CRITICAL for irreversible state corruption.
  • Tests may temporarily raise log level to DEBUG for diagnosing flakes but should reset after.
  • Provide a toggle (env var or parameter) when adding verbose debug logging to hot paths.

Rationale: Centralized logging enables deterministic capture/suppression in benchmarks, simplifies CI noise control, and aligns with Constitution Principle XII (Preferred Logging & Observability).

Code quality standards

  • Clear, intent‑revealing names; small, cohesive functions; robust error handling.
  • Follow existing style; document non‑obvious choices with comments/docstrings.
  • Add helpful comments to quickly understand the code’s purpose and logic.
  • Avoid duplication; prefer composition and reuse.
  • Keep public behavior backward‑compatible unless explicitly stated.
  • Write comprehensive unit tests for new features and bug fixes (GUI tests in tests/pygame/).
  • Verify test value before investing fix effort (see Test Significance Verification in Testing Strategy section).
  • Math vs numpy: use math for scalar ops/constants, numpy for vectorized/array ops, and avoid mixing within a single expression.

Design decisions

  • Favor readability and maintainability over micro‑optimizations.
  • Use type hints for all public functions and methods; prefer typing over Any.
  • Use exceptions for error handling; avoid silent failures.

CLI vs programmatic use

  • This project prioritizes traceability and reproducibility of benchmarks. Prefer generating script- and config-driven workflows over ad-hoc command lines or inline parameter tweaks.

  • Do not focus on the cli directly; prefer programmatic use and factory functions.

  • The CLI is not important; prefer programmatic use and factory functions.

  • Use logging for non‑error informational messages; avoid print statements except in CLI entry points.

  • Configs: configs//.yaml (single source of truth for all hyperparameters, seeds, envs).

  • Scripts: scripts/_.py (read config path, set up run dirs, log metadata, call library code).

  • Runs/outputs/benchmarks: output/benchmarks/_/ (store config.yaml, git_meta.json, logs, metrics, artifacts).

  • Deterministic seed in both config and code

Code reviews

  • All changes must be reviewed by at least one other team member.
  • Reviewers should check for correctness, style, test coverage, and documentation.
  • Use GitHub’s review tools to leave comments and approve changes.

One-real-path-test rule

For serialization, subprocess, GPU-isolation, artifact-promotion, and CLI-handoff code, keep at least one test on the same serialization and invocation path production uses. Do not manually pre-transform a fixture before the boundary: it can bypass the exact conversion or dispatch defect the test is meant to catch. The subprocess-isolation regression test at tests/benchmark/test_camera_ready_subprocess_isolation.py is the reference pattern.

For a shared-helper migration, record a per-call-site contract table in the PR and test every applicable row: return type/top-level schema, missing and malformed input behavior (including caller exit code), minimal import footprint, eager versus streaming reads, path:line context, and output ordering. Mark genuinely inapplicable rows explicitly.

Docstrings

  • Every module, function, class, and method should have a docstring.
  • Docstrings should use triple double quotes (""").
  • The first line should be a short summary of the object’s purpose, starting with a capital letter and ending with a period.
  • If more detail is needed, leave a blank line after the summary, then continue with a longer description.
  • For functions/methods: document parameters, return values, exceptions raised, and side effects.
  • Private/internal code should also have docstrings explaining their purpose for easier maintainability.
  • Follow the pydocstyle convention specified in pyproject.toml.

Clarify questions (with options)

  • In case of ambiguity or uncertainty about requirements, always ask clarifying questions before starting implementation. Provide multiple-choice options to facilitate quick decision-making. Group questions by scope, interfaces, data handling, UX, and performance.
  • Before implementing, confirm requirements with targeted questions.
  • Prefer multiple‑choice options to speed decisions; group by scope, interfaces, data, UX, performance.
  • Add arguments to the options for easy decision-making.
  • If answers are unknown, propose sensible defaults and proceed (don't block on non‑essentials).

Examples (copy‑ready):

  • Scope: Is the metric per episode or a per‑timestep aggregate?
  • Interfaces: Return shape dict[str, float] or a dataclass?
  • Data: How to handle NaN/missing — drop, impute, or error?
  • UX: Any hotkey conflicts with existing controls; prefer , and .?
  • Performance: Target budget for feature X (ms/frame)?

Problem‑solving approach

  • Break problems into smaller tasks; research prior art and patterns.
  • Clearly prioritize must‑haves vs nice‑to‑haves.
  • Consider system‑wide impact, edge cases, error handling, and failure modes.
  • Document architectural decisions and trade‑offs.

Tooling and tasks (uv, Ruff, pytest, ty, VS Code)

  • Dependencies/runtime: uv
    • Install/resolve: VS Code task “Install Dependencies” (uv sync)
    • Run: uv run <cmd> for any Python command
    • Add deps: uv add <package> (or edit pyproject.toml and sync)
  • Lint/format: Ruff
    • VS Code task “Ruff: Format and Fix” (keeps repo ruff‑clean with the expanded rule set; document exceptions with comments)
  • Type checking: ty
    • VS Code task "Type Check (advisory)" (uvx ty@0.0.58 check . --exit-zero; reports findings while exiting zero for current compatibility)
    • Type findings are useful quality signals and should be fixed when practical, especially in substantially touched files or stable contracts such as public interfaces, benchmark schemas, planner contracts, config parsing, map definitions, artifact metadata, and CLI boundaries.
    • PRs are not blocked solely because the advisory ty phase reports findings. Reviewers may still request typing fixes when findings affect changed code or stable contracts.
    • A fail-closed typecheck gate, changed-files ratchet, or baseline-reduction workflow must be proposed separately before becoming a merge requirement.
  • Tests: pytest
    • VS Code task “Run Tests” (default suite)
    • “Run Tests (Show All Warnings)” for diagnostics
    • “Run Tests (GUI)” for display‑dependent tests (headless via environment vars)
    • VS Code task “PR Ready Check” runs Ruff fix/format, full tests (incl. slow), changed‑files coverage gate, diff‑only TODO docstring warnings, and the TODO-docstring backlog ratchet
  • Code quality checks: VS Code task “Check Code Quality (Ruff + advisory ty)”
  • Diagrams: VS Code task “Generate UML”

Quality gates to run locally before pushing:

  1. Install Dependencies → 2) Ruff: Format and Fix → 3) Check Code Quality (Ruff + advisory ty) → 4) Type Check (advisory) → 5) Run Tests

Shortcuts (optional shell):

  • Break down complex problems into smaller, manageable tasks
  • Research existing solutions and patterns before implementing new approaches
  • Use existing libraries and frameworks when possible to avoid reinventing the wheel
  • Consider the impact of changes on the entire system, not just the immediate problem
  • Document architectural decisions and trade-offs made during implementation
  • Think about edge cases, error handling, and potential failure modes

Documentation Standards

Technical Documentation

  • Create comprehensive documentation for all significant changes and new features
  • Save documentation files in the docs/ directory using a clear folder structure
  • Each major feature or issue should have its own subfolder named in kebab-case
    • Format: docs/dev/issues/42-fix-button-alignment/ or docs/dev/issuesfeature-name/
  • Use descriptive README.md files as the main documentation entry point for each folder

Docs Folder Structure

Here’s a concise map of the docs folder to help you find the right guidance quickly. Each folder should include a README.md for context, links, and references.

Top-level guides (entry points)

  • README.md — Main docs landing page.
  • dev_guide.md — Primary development reference (setup, workflow, testing, CI).
  • qa_test_strategy.md — Canonical contributor QA runbook and test taxonomy.
  • ENVIRONMENT.md — Environment overview and usage.
  • SIM_VIEW.md — Simulation view/UI notes.
  • UV_MIGRATION.md — Migration notes to uv.
  • Topic-specific guides:
    • DATA_ANALYSIS.md, trajectory_visualization.md, SVG_MAP_EDITOR.md, fast_pysf_wrapper.md, pyreverse.md, curvature_metric.md, snqi_weight_cli_updates.md.

Focused subfolders

  • 2x-speed-vissimstate-fix/
    • README.md — Notes and outcome for the VissimState 2x speed fix.
  • baselines/
    • social_force.md — Baseline Social Force documentation.
  • docs/dev/ — In-progress/engineering docs and design notes
  • extract-pedestrian-action-helper/
    • README.md — Helper tool documentation.
  • img/ — Images used across docs
  • ped_metrics/ Pedestrian metrics documentation and analysis notes.
  • refactoring/ Migration/architecture reports and plans
  • snqi-weight-tools/ — SNQI weight tooling user docs and schema
  • templates/ Template for new design docs.
  • video/ Demo animations for docs.

Documentation Content Requirements

Documentation should include:

  • Problem Statement: Clear description of the issue being addressed
  • Solution Overview: High-level approach and architectural decisions
  • Implementation Details: Code examples, API changes, and technical specifics
  • Impact Analysis: What systems/users are affected and how
  • Testing Strategy: How the changes were validated
  • Future Considerations: Potential improvements or known limitations
  • Related Links: References to GitHub issues, pull requests, or external resources

Documentation Best Practices

  • Use proper markdown formatting with clear headings and structure
  • Include code examples with syntax highlighting
  • Add diagrams or screenshots when they improve understanding
    • Mermaid diagrams are welcome and encouraged for visualizing workflows, architecture, and relationships
  • Write for future developers who may be unfamiliar with the context
  • Keep documentation up-to-date as code evolves
  • Use consistent formatting and follow markdown linting standards
  • Prefer GitHub-flavored Markdown (GFM) conventions so docs render correctly on GitHub
  • Write issue references as Issue #123 in prose, lists, and tables instead of starting a Markdown line with a bare #123 token.
  • Avoid duplications. Link to existing documentation when relevant.
  • Always provide README.md files in new documentation folders for overview and reference.
  • When the document is longer than 50 lines, create a table of contents at the top for easy navigation. Ideally, use markdown.extension.toc.create to Markdown All in One: Create Table of Contents.

Visualizations and Reports

  • Use visualizations to illustrate complex concepts or data flows
  • Include performance reports or benchmarks when relevant
  • Ensure all visual assets are stored in the docs/img/, docs/figures/ or docs/video/ directories for easy access and consistency
  • Generate figures using code when possible to ensure reproducibility
  • Figures should be exported in high-quality vector formats (e.g., SVG, PDF) for clarity

Figure and Visualization Guidelines

All figures must be reproducible from code and directly integratable into LaTeX documents:

  • Output format
    • Always export vector PDFs (.pdf) for inclusion in LaTeX.
    • Optionally export .png (300 dpi) for slides/presentations.
  • Reproducibility
    • Each figure = one tracked script or CLI command in robot_sf/benchmark/figures/, scripts/generate_figures.py, or the robot_sf_bench CLI.
    • The generator must read data, generate the plot, and save into docs/figures/.
    • No manual edits in Illustrator, Inkscape, etc.
    • Clear and unique output filenames: fig-<short-description>.pdf.
  • Version control
    • Scripts and generated figures go into version control.
    • Data files (if any) go into output/figures/ (respecting the canonical artifact root).
  • Consistent style
    • Use Matplotlib with predefined rcParams:
      • savefig.bbox = "tight"
      • pdf.fonttype = 42
      • font sizes: 9 pt labels, 8 pt ticks/legend
      • line width ~1.2–1.6 pt
    • Axis labels and math should use LaTeX syntax: r"$\sin(x)$".
  • Figure sizing
    • Provide helper function for resizing
    • Default: single-column width (fraction=1.0).
  • File locations
    • Figures go into docs/figures/ (tracked).
    • Data exports (if used) into output/figures/.

CI/CD expectations

  • Tests: uv run pytest tests
  • Lint: uv run ruff check . and uv run ruff format --check .
  • The pipeline mirrors the local quality gates. Ensure green locally first.
  • After merging fresh origin/main, or after CI reports a formatting failure outside the files you intentionally touched, run the repo-wide lightweight lint/format gate that CI uses. A changed-file format check can be stale when the shared baseline moved.

CI mapping to local tasks and CLI:

  • fast-feedback matrix → four scripts/dev/ci_driver.sh test shards on every event; shard 1 also runs lint and advisory type checking. Pull requests exclude slow tests and upload one trace-based coverage database per shard for exact-head changed coverage, while non-PR events run the complete suite and upload one coverage database per shard using the faster sysmon backend.
  • coverage-gate job → combines all four non-PR coverage databases, enforces the 85.0% absolute coverage floor, and updates the advisory main baseline.
  • smoke-artifacts job → scripts/dev/ci_driver.sh smoke artifact-policy
  • aggregate ci job → requires the coverage gate on non-PR events and all other split jobs while keeping the existing required-check name stable
  • local full equivalent → scripts/dev/run_ci_local.sh

Workflow location: .github/workflows/ci.yml.

Main CI signal and staleness-aware merge policy

A red main signal is advisory, not a blanket merge hold. Prioritize PRs that explicitly repair the breakage (fix(ci): unbreak main or unbreak-main). For other PRs, inspect whether their changed files overlap the suspected breakage; hold only an overlapping PR. A clean, up-to-date PR with its own green checks may merge while an unrelated main failure is being repaired.

The deterministic main signal is:

uv run python scripts/dev/main_ci_is_green.py   # exit 0 green, 1 not-green

It decides from the most recent completed CI run on main; an in-progress, cancelled, or timed-out run is stale, not red. Reviewing a PR while main is red is fine, and only the suspected-file overlap or per-PR staleness check can hold an otherwise green PR.

For automated gates, emit the machine-readable signal instead of parsing the human line (issue #5571). The --json flag prints the main_ci_is_green.v1 schema and still exits 0 (green) / 1 (not green), so the gate contract is satisfied without text scraping:

uv run python scripts/dev/main_ci_is_green.py --json
# -> {"schema_version": "main_ci_is_green.v1", "is_green": true, "status": "green",
#     "repo": "ll7/robot_sf_ll7", "workflow": "CI",
#     "deciding_run": {"databaseId": ..., "conclusion": "success", "status": "completed",
#                      "headSha": "...", "createdAt": "..."}}

status is one of green / red / stale. A stale verdict (no decisive completed run in the window) still fails closed to not green, but is reported distinctly so the gate can hold for a fresh run rather than treat it as a main regression. The --quiet flag suppresses the human line; the existing exit-code contract is unchanged.

Scheduled main-CI incident reconciliation

Open issues carrying the canonical ll7-main-red-incident:v1 body marker (or the compatibility label of the same name) are reconciled by .github/workflows/main-ci-incident-reconcile.yml. The body marker is the inventory identity because older incident creators may omit the label. The workflow uses the existing main_ci_incident_reconcile.py signal and requires two newer consecutive decisive green runs before it posts an evidence comment and closes an incident as completed. Active, pending, malformed, or concurrent-change cases remain open. Cancelled or superseded runs are neutral: they count as neither green nor red and cannot satisfy either slot in the two-green streak.

To preserve that evidence boundary, pull requests must use Refs #N for these incidents instead of GitHub's semantic closing keywords (Closes, Fixes, or Resolves). The blocking PR Contract Check rejects semantic closure for either the canonical body marker or its compatibility label, leaving the scheduled reconciler as the sole closer after the two-green criterion is met.

The Actions run evidence window is paginated. The reconciler reads full workflow-run pages and stops only after two decisive completed green/red runs are visible, so a cancellation-saturated newest page cannot hide the decisive history. The default page budget is ten; --max-run-pages N changes it, and the legacy --run-limit N option is retained as an alias for that page budget. If the budget is exhausted before two decisive runs are found, the helper fails closed instead of classifying an incomplete window.

The helper is report-only unless --apply is supplied, so an offline or local inspection can use:

uv run python scripts/dev/reconcile_main_ci_incidents.py --json

The scheduled apply lane uses GitHub's Representational State Transfer (REST) API with read-before-write and readback checks. It writes a report under output/main-ci-incidents/; the directory is workflow-local evidence and is not a durable benchmark artifact.

CI Performance Monitoring

The CI pipeline separates fast feedback from the heavier smoke/artifact tail:

  • fast-feedback distributes pytest over four runners; pull requests use the fast-only marker and trace-based per-shard coverage for exact-head changed coverage, while main, manual, and merge-queue events run the complete suite with per-shard coverage data. Merge-queue coverage also uses the trace backend because it feeds the exact-head changed-coverage gate.
  • coverage-gate combines the complete non-PR coverage data before enforcing the 85.0% absolute floor and advisory baseline comparison.
  • smoke-artifacts runs validation smoke checks, uploads benchmark/recording artifacts, and enforces the artifact-root policy.
  • Both jobs call the canonical scripts/dev/ci_driver.sh phases instead of duplicating validation semantics in workflow YAML.
  • System packages are installed through the supported apt-get path in one update/install step per job; the workflow does not download apt-fast at runtime.

The smoke lane still includes performance monitoring and regression checks through current, committed entry points:

Workflow Integration:

  • Fast lint/typecheck/test feedback is reported before smoke/artifact completion.
  • smoke-artifacts uploads map verification, benchmark, recording, and cold/warm performance artifacts from the canonical output/ tree.
  • Cold/warm regression smoke is driven by uv run python -m robot_sf.benchmark.perf_cold_warm through scripts/dev/ci_driver.sh smoke. Pull requests run the check in advisory mode; main and workflow_dispatch runs use the stricter regression gate.
  • Startup/reset performance is measured by scripts/validation/performance_smoke_test.py during strict smoke runs, and telemetry smoke/perf coverage is exercised by scripts/validation/run_examples_smoke.py --perf-tests-only.

Local Testing:

  • Use scripts/dev/run_ci_local.sh for the canonical local CI-equivalent path.
  • Use scripts/dev/run_ci_local.sh --no-setup <phase> ... for repeat local phase runs after the worktree has already been synced.
  • Use scripts/dev/ci_driver.sh <phase> for narrower local phases such as lint, typecheck, test, smoke, or artifact-policy.
  • Use uv run python scripts/dev/ci_timing_summary.py --run-id <github-actions-run-id> --top 10 to inspect GitHub-hosted CI queue time, job duration, and slowest-step timing from a completed run.
  • Use uv run python scripts/dev/complexity_runtime_baseline.py --top 10 robot_sf scripts tests when a refactor needs a local snapshot of large modules, long functions, or captured pytest duration rows.
  • Optional local GitHub Actions execution with act is not currently a supported repository workflow. Issue #1308 evaluated this path on 2026-05-18 and did not adopt it because Docker was available but act was not installed, so no workflow job could be proven locally. See the evaluation note.
  • gh act is now installed on one local machine and Issue #1342 proved non-interactive --dryrun workflow graph validation, but not a real local workflow execution. Keep using scripts/dev/run_ci_local.sh and BASE_REF=origin/main scripts/dev/pr_ready_check.sh as the supported local proof paths until a real narrow gh act target is recorded.

Performance Breach Handling:

  • Cold/warm PR smoke uses advisory thresholds by default; main and workflow_dispatch runs enforce the stricter regression gate.
  • Startup/reset smoke supports soft and hard thresholds through ROBOT_SF_PERF_CREATION_SOFT, ROBOT_SF_PERF_CREATION_HARD, ROBOT_SF_PERF_RESET_SOFT, ROBOT_SF_PERF_RESET_HARD, and ROBOT_SF_PERF_ENFORCE=1.

Validation scenarios and performance

Validation scenarios (run after changes)

./scripts/validation/test_basic_environment.sh
./scripts/validation/test_model_prediction.sh
./scripts/validation/test_complete_simulation.sh
uv run python scripts/validation/run_examples_smoke.py --dry-run
uv run python scripts/validation/run_examples_smoke.py --perf-tests-only
uv run python scripts/validation/run_examples_smoke.py
uv run python scripts/validation/svg_inspect.py maps/svg_maps --pattern "classic_*.svg" --strict warning
uv run python scripts/tools/check_artifact_root.py

# Performance baseline validation
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy \
  uv run python scripts/validation/performance_smoke_test.py

Success criteria:

  • Basic environment: exits 0; no exceptions.

  • Model prediction: exits 0; logs model load and inference without errors.

  • Complete simulation: exits 0; simulation runs to completion without errors.

  • Example smoke harness: exits 0; all ci_enabled examples pass and archived entries are reported as skipped via manifest metadata.

  • SVG inspection: exits 0 in strict mode only if no warning/error findings are detected at the selected threshold.

  • Artifact guard: exits 0; repository root remains clean with all artifacts under output/ (mirror of the CI enforcement step).

  • Performance smoke test: exits 0; meets baseline performance targets (see docs/performance_notes.md).

    • Threshold logic now includes soft vs hard tiers with environment overrides. Soft breaches on CI default to WARN (exit 0) unless ROBOT_SF_PERF_ENFORCE=1.
      • Environment variables:
        • ROBOT_SF_PERF_CREATION_SOFT (default 3.0)
        • ROBOT_SF_PERF_CREATION_HARD (default 8.0)
        • ROBOT_SF_PERF_RESET_SOFT (default 0.50 resets/sec)
        • ROBOT_SF_PERF_RESET_HARD (default 0.20 resets/sec)
    • ROBOT_SF_PERF_ENFORCE=1 to fail on soft (and hard) breaches (use locally for strict tuning).
    • (Advanced) ROBOT_SF_PERF_SOFT / ROBOT_SF_PERF_HARD may be set to numeric seconds to temporarily override thresholds (intended only for internal testing of enforcement logic; not part of the stable public interface).
      • Hard threshold breaches always FAIL.

    Example maintenance workflow

    1. Validate cataloguv run python scripts/validation/validate_examples_manifest.py ensures the manifest enumerates every script and that docstrings stay aligned with summaries.
    2. Review planned changesuv run python scripts/validation/run_examples_smoke.py --dry-run prints the ci_enabled set before executing pytest, making it easy to confirm archive decisions.
    3. Run tracker/perf gateuv run python scripts/validation/run_examples_smoke.py --perf-tests-only --perf-num-resets 2 exercises the imitation pipeline tracker smoke plus telemetry perf wrapper without re-running the entire pytest suite.
    4. Execute smoke harnessuv run python scripts/validation/run_examples_smoke.py runs all active examples headlessly; pytest fixtures already configure pygame for a dummy display.
    5. Archive responsibly – whenever a script moves into examples/_archived/, update examples/_archived/README.md, set ci_enabled: false with a ci_reason, and point the module docstring at the maintained replacement.

Run tracker & history CLI

  • Enable the tracker with --enable-tracker on examples/advanced/16_imitation_learning_pipeline.py. A background guard now snapshots manifests roughly every five seconds and traps SIGINT/SIGTERM, so failed or cancelled runs emit a failed manifest entry automatically.
  • Inspect live progress with status or watch:
    uv run python scripts/tools/run_tracker_cli.py status <run_id>
    uv run python scripts/tools/run_tracker_cli.py watch <run_id> --interval 1.0
    Both commands read the latest manifest snapshot and show current step, elapsed time, ETA, and the last completed step.
  • Use list to review prior runs (defaults to the most recent 20). Helpful filters:
    • --status pending|running|completed|failed|cancelled
    • --since 2025-01-15T00:00:00+00:00 (UTC ISO timestamps)
    • --format table|json for human vs machine-readable output
  • summary (aliased as show) prints per-run breakdowns, with --format text|json|markdown. Markdown output intentionally mirrors the exported summaries so docs/changelogs can embed them verbatim.
  • export writes Markdown or JSON summaries directly to disk:
    uv run python scripts/tools/run_tracker_cli.py export <run_id> \
      --format markdown \
      --output output/run-tracker/summaries/<run_id>.md
    Exports include per-step durations, artifact paths, and any failure context produced by the guard.
  • Mirror telemetry to TensorBoard when you need dashboards:
    uv run python scripts/tools/run_tracker_cli.py enable-tensorboard <run_id> --logdir output/run-tracker/tb/<run_id>
    uv run tensorboard --logdir output/run-tracker/tb
    The CLI replays telemetry.jsonl into SummaryWriter so you can inspect CPU/GPU trends without touching the canonical JSON artifacts.
  • Run the performance smoke wrapper straight from the CLI instead of calling scripts manually:
    uv run python scripts/tools/run_tracker_cli.py perf-tests \
      --scenario configs/validation/minimal.yaml \
      --output output/run-tracker/perf-tests/latest \
      --num-resets 5
    Results are persisted in perf_test_results.json with pass/soft-breach/fail classification plus any recommendations triggered by the telemetry rules.
  • Because the guard writes manifests on a timer and on signals, partial runs survive restarts—list/show will always have at most a five-second gap between what ran and what was recorded.

Performance benchmarking (optional)

# Run maintained performance smoke when performance impact is suspected
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy \
uv run python scripts/validation/performance_smoke_test.py

Performance expectations

  • Environment creation: < 1 second
  • Model loading: 1–5 seconds
  • Simulation performance: ~22 steps/second (~45ms/step)
  • Build time: 2–3 minutes (first time)
  • Test suite: 2–3 minutes (≈170 tests)

Benchmark runner: parallel workers and resume

The benchmark runner supports process-based parallel execution and safe resume.

  • Parallelism: Use multiple workers to run independent episodes concurrently.
  • Resume: Skips episodes that are already present in the output JSONL.

Key points

  • Parent-only writes: only the parent process writes JSONL lines to avoid corruption.
  • Episode identity: jobs are identified deterministically from scenario params and seed; existing episodes are skipped when resume is enabled.
  • macOS: workers > 1 uses the spawn start method; ensure worker code is importable/picklable and defined at module top level (no lambdas/closures).

CLI usage

  • Run a batch with parallel workers and default resume behavior:
    • robot_sf_bench run --matrix configs/baselines/example_matrix.yaml --out output/benchmarks/episodes.jsonl --workers 4
  • Force recomputation (disable resume):
    • robot_sf_bench run --matrix configs/baselines/example_matrix.yaml --out output/benchmarks/episodes.jsonl --workers 4 --no-resume
  • Opt in to schema-backed pedestrian-impact reductions:
    • robot_sf_bench run --matrix configs/scenarios/planner_sanity_matrix_v1.yaml --out output/benchmarks/ped_impact/episodes.jsonl --experimental-ped-impact
  • Baseline computation also accepts the same flags:
    • robot_sf_bench baseline --episodes output/benchmarks/episodes.jsonl --output output/benchmarks/baseline.jsonl --workers 4

Programmatic usage

  • Prefer factory functions and programmatic APIs in library code:
    • from robot_sf.benchmark.runner import run_batch
    • from robot_sf.benchmark import baseline_stats
    • run_batch(scenarios, out_path=..., schema_path=..., workers=4, resume=True)
    • baseline_stats.run_and_compute_baseline(episodes_path=..., out_path=..., workers=4, resume=True)

Notes

  • Default behavior is resume=True for programmatic APIs and CLI (omit --no-resume to keep it enabled).
  • When resuming, open files in append mode if you want to keep existing lines; the runner will not duplicate episodes.
  • On macOS spawn, module-level top-level functions are required for worker processes to import successfully.
  • Resume accelerator: The runner writes a small sidecar manifest (episodes.jsonl.manifest.json) caching episode ids and file stat. On subsequent runs, resume uses this manifest when valid and transparently falls back to scanning the JSONL if the sidecar is stale or missing. No user action required.

Aggregation and Confidence Intervals

Once you have a JSONL of episodes, you can aggregate metrics by group and optionally attach bootstrap confidence intervals.

CLI usage

  • Aggregate without CIs (default):
    • robot_sf_bench aggregate --in output/benchmarks/episodes.jsonl --out output/benchmarks/summary.json
  • Aggregate with CIs (enable with >0 samples):
    • robot_sf_bench aggregate --in output/benchmarks/episodes.jsonl --out output/benchmarks/summary_ci.json --bootstrap-samples 1000 --bootstrap-confidence 0.95 --bootstrap-seed 123

Options

  • --group-by: Dotted path for grouping (default: scenario_params.algo)
  • --fallback-group-by: Used when group-by is missing (default: scenario_id)
  • --bootstrap-samples: Number of bootstrap resamples; 0 disables CI keys
  • --bootstrap-confidence: Confidence level, e.g., 0.90, 0.95
  • --bootstrap-seed: Optional deterministic seed for CIs
  • --snqi-weights/--snqi-baseline: Recompute metrics.snqi during aggregation
  • Pedestrian-impact records: Aggregation flattens metrics.pedestrian_impact.canonical_reductions into ped_impact_* reduction columns, then applies the same mean/median/p95 summaries.

Output format

  • For each group and metric, the aggregator returns mean, median, p95.
  • When CIs are enabled, additional keys are included: mean_ci, median_ci, p95_ci as [low, high].
  • When CIs are enabled and at least two groups share (scenario_id, seed) episode identities (seed_index is accepted as a fallback), an additive top-level pairwise_contrasts block reports paired bootstrap mean deltas, confidence intervals, two-sided bootstrap sign p-values, Holm-adjusted p-values, and paired Cohen's dz effect sizes. Deltas are right_minus_left for comparison keys like A__vs__B.
  • Holm correction is applied within the current aggregate family (family="all") separately for each metric. For scenario-family-specific correction, filter or split the input records by family before aggregation.

Planner Inclusion Gate

Run the planner inclusion check when a planner is being considered for promotion from experimental/testing-only status into a promoted benchmark set:

uv run robot_sf_bench planner-inclusion-check \
  --algo orca \
  --matrix configs/scenarios/planner_sanity_matrix_v1.yaml \
  --output-dir output/planner_inclusion/orca

The command writes <algo>_episodes.jsonl plus <algo>_inclusion_report.json. The report is a review artifact, not an automatic status update. It fails closed with explicit reasons for runner or schema failures, NaN/infinite aggregates, slow runtime, too few episodes, low success rate, or excess collision rate.

Programmatic usage

from robot_sf.benchmark.aggregate import read_jsonl, compute_aggregates_with_ci

records = read_jsonl("output/benchmarks/episodes.jsonl")
summary = compute_aggregates_with_ci(
    records,
    group_by="scenario_params.algo",
    fallback_group_by="scenario_id",
    bootstrap_samples=1000,
    bootstrap_confidence=0.95,
    bootstrap_seed=123,
)

Training and examples

Available demos

uv run python examples/quickstart/01_basic_robot.py
uv run python examples/quickstart/02_trained_model.py
uv run python examples/quickstart/03_custom_map.py
uv run python examples/advanced/06_pedestrian_env_factory.py

Training scripts

uv run python scripts/training/train_ppo.py --config configs/training/ppo/expert_ppo_issue_576_br06_v3_15m_all_maps_randomized.yaml
uv run python scripts/training/launch_optuna_expert_ppo.py --config configs/training/ppo_imitation/optuna_expert_ppo.yaml
uv run python scripts/evaluate.py

Imitation Learning Pipeline (PPO Pre-training)

The project supports accelerating PPO training via behavioral cloning pre-training from expert trajectories. This enables sample-efficient training by warm-starting agents with expert demonstrations.

Install the optional imitation stack before running BC pre-training:

uv sync --group imitation

Use uv run --group imitation ... for BC pre-training commands.

Quick Overview: Expert PPO Training → Trajectory Collection → BC Pre-training → PPO Fine-tuning → Comparison Analysis

For complete documentation, see Imitation Learning Pipeline Guide which includes:

  • Detailed step-by-step workflow
  • Configuration file examples
  • Validation and debugging tools
  • Artifact locations and manifest tracking
  • Sample-efficiency metrics (target: ≤70% of baseline timesteps)
  • Troubleshooting and best practices

Quick Start:

# End-to-end wrapper (recommended for new users)
uv run python examples/advanced/16_imitation_learning_pipeline.py

# Or run individual steps manually:
uv run python scripts/training/train_ppo.py --config configs/training/ppo/expert_ppo_issue_576_br06_v3_15m_all_maps_randomized.yaml
uv run python scripts/training/collect_expert_trajectories.py --dataset-id expert_v1 --policy-id ppo_expert_v1 --episodes 200
uv run --group imitation python scripts/training/pretrain_from_expert.py --config configs/training/ppo_imitation/bc_pretrain.yaml
uv run python scripts/training/train_ppo_with_pretrained_policy.py --config configs/training/ppo_imitation/ppo_finetune.yaml

Set --log-level DEBUG if you need the full resolved-config dumps from the factory helpers (default is INFO to keep console noise down). Use --backend <name> to override the auto-selected simulator backend (defaults to the fastest available choice via select_best_backend). The end-to-end example auto-generates BC/ppo fine-tuning configs under output/tmp/imitation_pipeline/, so you only need to edit the YAML files when running the scripts manually.

BC pre-training configs default to device: auto, which lets Stable-Baselines3 and imitation use available accelerators. Set device: cpu in the BC YAML when you need a deterministic or resource-constrained CPU-only run.

Also see:

  • End-to-end example: examples/advanced/16_imitation_learning_pipeline.py
  • Detailed workflows: specs/001-ppo-imitation-pretrain/quickstart.md

RLlib DreamerV3 (drive_state + rays)

RLlib DreamerV3 can be trained on the default non-image observation contract.

# Install RLlib optional dependency
uv sync --extra rllib

# Validate config
uv run --extra rllib python scripts/training/train_dreamerv3_rllib.py \
  --config configs/training/rllib_dreamerv3/drive_state_rays.yaml \
  --dry-run

# Run training
uv run --extra rllib python scripts/training/train_dreamerv3_rllib.py \
  --config configs/training/rllib_dreamerv3/drive_state_rays.yaml

The workflow uses deterministic flattening order (drive_state, rays) and can normalize actions to [-1,1] for DreamerV3. The launcher also pins Ray workers to the active interpreter and disables uv run runtime-env propagation to avoid worker-side environment rebuilds. See docs/training/dreamerv3_rllib_drive_state_rays.md for the Auxme launch/monitor/recovery runbook.

Docker training (advanced)

# Build and run GPU training (requires NVIDIA Docker)
# NOTE: May fail in CI environments due to network restrictions

Common issues and solutions

Build issues

  • uv not found → curl -LsSf https://astral.sh/uv/install.sh | sh (or use the package manager path in docs/ENVIRONMENT.md)
  • ffmpeg missing → sudo apt-get install -y ffmpeg

Runtime issues

  • Import errors → ensure venv is activated: source .venv/bin/activate
  • Display errors → run headless: DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy
  • Model loading warnings → StableBaselines3 warnings about legacy Gym-trained models are expected (re-save models to clear them)
  • Model compatibility → use newest models for best compatibility (e.g., ppo_model_retrained_10m_2025-02-01.zip)

Migration notes

  • The project uses uv for env/runner and a factory pattern for environment creation.
  • See docs/UV_MIGRATION.md and docs/refactoring/ for details.

Helpful definitions and repository structure

Helpful definitions

  • uv: Fast Python package/dependency manager and runner (uv sync, uv run).
  • Ruff: Python linter and formatter (run via "Ruff: Format and Fix" task).
  • pytest: Testing framework (run via "Run Tests" tasks).
  • VS Code tasks: Standardized workflows (install, lint, test, diagram).
  • Quality gates: Minimal checks before pushing (install → lint/format → quality check → tests).

Repository structure (key dirs)

  • robot_sf/ (source), examples/, tests/, tests/pygame/, fast-pysf/ (subtree), scripts/, model/, docs/

Definition of Done (DoD)

  • Requirements clarified (with options/assumptions recorded).
  • Design doc added/updated and linked (if non‑trivial).
  • Code implemented with tests (unit/integration; GUI when needed).
  • For research/benchmark/metric/paper-facing analysis-tool PRs: include one representative use on durable/versioned input (tracked config, model checkpoint, committed fixture, or versioned W&B artifact), or link a concrete follow-up issue that names the decision, claim boundary, or synthesis surface the tool will update. Local-only output/ files are not durable proof unless promoted or represented by a tracked manifest. Small support helpers (formatters, CLI wrappers, quick diagnostics) that make no research/benchmark/metric/paper claim should state NA - support helper with the reason. Trace-panel generators, topology-score instrumentation, seed-sufficiency analysis, and why-report generation are research-facing examples that need first use or a concrete follow-up.
  • Ruff clean and “Check Code Quality (Ruff + advisory ty)” reviewed locally.
  • Advisory typecheck reviewed. Fix practical findings in touched files and stable contracts, and document any meaningful remaining findings in the PR when they affect the change.
  • Docs updated (README in feature folder, diagrams if changed).
  • Validation matched to risk per maintainer_values.md: runtime, benchmark, metric, schema, model-provenance, and paper-facing changes need executable proof; low-risk docs/instruction changes use diff review, referenced path/link checks, and lightweight automated checks when available. State explicitly in the PR which heavier gates were skipped and why.
  • Feature branch synced with latest origin/main before PR creation, then required validation scripts run and pass for the selected risk level.
  • CI green (lint + tests) and PR opened with appropriate links.

Templates

Use the following templates for specific tasks.

Every issue template collects the canonical archetype and evidence_tier metadata defined in the issue #1512 convention. Markdown templates provide the metadata block near the top of the issue body; YAML issue forms expose both fields as required dropdowns. Use exactly one value from each enum and add repository-relative paths under linked_policy when a policy governs the issue. This keeps newly filed issues machine-checkable without rewriting existing issue bodies or changing labels and project fields.

Security & network policy

  • No secrets in code, configs, or commit messages.
  • Avoid network access in tests; prefer local fixtures. If unavoidable, document and gate behind flags.
  • Don't exfiltrate data; handle PII safely (none expected in this repo).

Large files & artifacts policy

  • Don't commit large binaries to the repo; prefer Git LFS for models/datasets when needed.
  • Use the model/ directory conventions; document artifact sources and versions.

Quick reference and TL;DR checklist

Quick reference commands

# Setup after installation
uv sync && source .venv/bin/activate

# Validate changes
uv run ruff check . && uv run ruff format . && uvx ty check . --exit-zero && uv run pytest tests

# Functional smoke (headless)
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy \
uv run python -c "from robot_sf.gym_env.environment_factory import make_robot_env; env = make_robot_env(); env.reset(); print('OK')"

# Optional perf smoke
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy \
uv run python scripts/validation/performance_smoke_test.py

# If running commands outside of `uv run`, activate the virtual environment:
source .venv/bin/activate

The uvx ty check . --exit-zero step is advisory: it should report findings without failing the command. Treat findings in touched code and stable contracts as reviewer-actionable even though the phase exits zero.

Proportional validation

Validation depth follows docs/maintainer_values.md: apply proof in proportion to risk. Do not treat the heaviest path as the default for every change.

  • Low-risk docs/instruction changes use the cheap path by default: inspect the diff, verify changed links or referenced paths, and run lightweight automated checks when they exist (for example, BASE_REF=origin/main scripts/dev/check_docs_proof_consistency_diff.sh for context-only or proof-heavy docs surfaces).
  • Runtime, benchmark, metric, schema, model-provenance, and paper-facing changes need executable proof appropriate to the claim (tests, benchmark runs, schema checks, etc.).
  • Strong claims escalate the bar even when the touched file is documentation. A docs-only change that makes a benchmark, metric, schema, model-provenance, or paper-facing claim still needs the corresponding strength of evidence.

If this section conflicts with current maintainer direction or maintainer_values.md, follow the higher-precedence source and make the smallest doc update needed to remove the drift.

TL;DR workflow checklist

  1. Clarify requirements and pick the validation path by change type (see Proportional validation above; docs/maintainer_values.md is the higher-precedence source).
  2. For non-trivial runtime/benchmark/metric/schema/paper-facing changes, draft a design doc under docs/ and link the issue; for low-risk docs/instruction changes, skip the design doc unless it clarifies scope.
  3. Implement with small, reviewed commits.
  4. Add/extend tests in tests/ or tests/pygame/ when touching runtime behavior.
  5. Run the gates that match the risk: diff/link/path checks and lightweight automated checks for docs/instruction changes; Install Dependencies → Ruff: Format and Fix → Check Code Quality (Ruff + advisory ty) → Type Check (advisory) → Run Tests for runtime and claim-heavy changes.
  6. Update docs/diagrams; run “Generate UML” if classes changed.
  7. Open PR with summary, risks, validation evidence, and links to docs/tests. Explicitly state which gates were skipped when using the cheap docs/instruction path.

Agent run manifest for PRs

For nontrivial agent-assisted runs (Codex, Claude Code, Copilot, or other), attach or link an agent_run_manifest.yaml so the run is auditable. See Agent Run Manifest for when this is required versus optional, where to store it, and trace/log hygiene. Start from docs/templates/agent_run_manifest.yaml. Do not block all PRs on this yet; it is required for major agent-assisted work that creates or changes durable evidence, benchmark/reporting gates, generated artifacts, CI/release policy, or substantial code paths, and recommended for multi-agent or multi-run tasks.

  • If this PR used a nontrivial agent run, attach or link an agent_run_manifest.yaml and confirm trace/log redaction was checked.

Issue #5303 checker authority

The only current entry point for the promotion-capable search contract is the powered, side-effect-free v2 checker: uv run python scripts/tools/check_issue_5303_search_promotion_contract_v2.py (and its pure --identities mode). The historical three-seed v1 contract and timing-control paths remain available only to reproduce their pinned diagnostic artifacts; they cannot authorize promotion, execution, or transfer work. Current operational code and documentation must not invoke either v1 checker path.

Final-readiness checklist for scripted tooling work

  • Run uv run ruff check <touched_files> and uv run ruff format <touched_files> before finalizing.
  • Run focused tests that cover modified paths (for example uv run pytest tests/dev/test_snapshot_pr_queue.py).
  • Run changed-file coverage if practical: uv run python scripts/coverage/check_changed_files_coverage.py --base $BASE_REF --include "scripts/dev/*" --include "tests/dev/*".
  • Ensure the worktree is clean for final PR-ready evidence: git status --short should be empty, then rerun final proof with PR_READY_MODE=final. Before running a broad git status --short --untracked-files=normal, use the compact state snapshot helper to see tracked changes and generated roots without dumping generated trees.

Per-Test Performance Budget

To prevent regression of integration test runtime, a performance budget policy is enforced for all tests:

Policy defaults (feature 124):

  • Soft threshold: < 20s (advisory – prints guidance when exceeded)
  • Hard timeout: 60s (enforced via @pytest.mark.timeout(60) or signal alarms inside long-running integration tests)
  • Report count: Top 10 slowest tests printed at session end
  • Relax mode: Set ROBOT_SF_PERF_RELAX=1 to suppress soft breach warnings (use sparingly; still prints report)
  • Enforce mode: Set ROBOT_SF_PERF_ENFORCE=1 to escalate any soft or hard breach to a test session failure

Implementation components (all under tests/perf_utils/):

  • policy.pyPerformanceBudgetPolicy dataclass providing classify(duration)-> none|soft|hard
  • reporting.py – aggregation and formatted slow test report
  • guidance.py – deterministic heuristic suggestions (reduce episodes, horizon, matrix size, etc.)
  • minimal_matrix.py – single-source helper for minimal benchmark scenario matrix (used by resume & reproducibility tests)

Collector flow:

  1. Each test call duration captured via a timing hook in tests/conftest.py.
  2. At terminal summary the top-N slow tests are ranked and printed with breach classification & guidance lines.
  3. If ROBOT_SF_PERF_ENFORCE=1 (and relax not set) any soft or hard breach converts the run to a failure (exit code changed). Optional internal overrides: set ROBOT_SF_PERF_SOFT / ROBOT_SF_PERF_HARD for targeted enforcement tests.

Guidance examples:

  • Soft breach near 25s: "Reduce episode count / seeds", "Use minimal scenario matrix helper"
  • Very long (>40s) test: horizon + matrix recommendations prioritized

Authoring guidance for new tests:

  • Keep semantic assertions; minimize episodes (max_episodes=2), horizon, seed list
  • Reuse write_minimal_matrix instead of duplicating inline YAML
  • Assert absence of heavy artifacts (videos) where not required

Performance troubleshooting checklist:

  1. Confirm smoke=True or minimal workload flags applied
  2. Reduce max_episodes, initial_episodes, batch_size
  3. Disable bootstrap sampling (bootstrap_samples=0)
  4. Lower horizon_override
  5. Ensure workers=1 for deterministic ordering in timing-sensitive tests

When relaxing: Use ROBOT_SF_PERF_RELAX=1 temporarily only for known CI variance; file a follow-up issue if sustained.

Hard timeout breaches should be rare; investigate infinite loops or large scenario expansions if encountered.

Multi-robot LiDAR and sprite rendering

LiDAR robot detection toggle

  • LidarScannerSettings.detect_other_robots controls whether LiDAR rays include other robots.
  • Default is True.
  • This modifies ray distances only and keeps observation keys unchanged (drive_state, rays).

SimulationView representation modes

  • SimulationView supports per-entity render mode fields:
  • robot_render_mode, ped_render_mode, ego_ped_render_mode with values circle or sprite.
  • Optional sprite paths:
  • robot_sprite_path, ped_sprite_path, ego_ped_sprite_path.
  • If sprite loading fails, rendering falls back to circles and emits a warning.

Example

from robot_sf.render.sim_view import SimulationView
from robot_sf.sensor.range_sensor import LidarScannerSettings

lidar_cfg = LidarScannerSettings(detect_other_robots=True)

view = SimulationView(
    robot_render_mode="sprite",
    ped_render_mode="sprite",
    ego_ped_render_mode="sprite",
)