This file preserves the previously published procedural reference for compatibility. Start at
docs/dev_guide.mdfor the concise first-use path, or usedocs/developer-guide.mdto navigate the canonical topic guides. New procedures should be added to a task-focused owner and linked from the index rather than added here.
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.
# 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.
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 --checkEscalate 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.
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 --jsonThis 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/.
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-toplevelThe 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.
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-extrasThe 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-extrasThe 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 -qIf 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 --jsonThe 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.
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 testWhen /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.mdinstead 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 withuv sync --all-extras --group carla, then runscripts/dev/check_carla_runtime.shfor preflight orscripts/dev/check_carla_runtime.sh --smokefor the bounded Docker connectivity proof. - If you are starting work on a feature branch, merge the latest
origin/maininto 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/mainMake 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.
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.mdThe 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.
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 automaticallyOne-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" -qIf 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.
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.
# source .venv
source .venv/bin/activate
# Lint+format
uv run ruff check --fix . && uv run ruff format .
# Tests
uv run pytest -n auto testsThe 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.py01_basic_robot.pyintroduces the environment factory pattern and headless rollouts.02_trained_model.pyreplays the bundled PPO baseline and writes JSONL metrics tooutput/results/episodes_demo_ppo.jsonl.03_custom_map.pyshows how to loadmaps/svg_maps/debug_06.svgviaRobotSimulationConfig.map_poolfor custom layouts.
See examples/README.md for the decision tree, prerequisites, and links to additional
tiers (advanced features, benchmarks, plotting, and archived scripts).
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.pyand02_factory_options.pydemonstrate switching simulators and recording options via unified configs. - Observation & training workflows:
03_image_observations.pyand04_feature_extractors.pyshowcase image sensors and feature extractor presets (run withuv sync --extra training, oruv sync --all-extrasfor full local parity). - Pedestrian & policy scenarios: Scripts
06–11cover factory-based pedestrian environments, single/multi pedestrian setups, and PPO rollouts using the maintained checkpoints undermodel/. - Tooling, validation, and visualization:
12_social_force_planner_demo.pythrough15_view_recording.pyprovide 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.
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.
uv run ruff check --fix . && uv run ruff format . && uvx ty@0.0.58 check . --exit-zero && uv run pytest -n auto teststy 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 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 -qThe pilot covers episode schema validation: a valid record passes, a malformed record is rejected.
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.pyandscripts/dev/check_base_sensitive_gates.py --pr <pr-number> --jsonclassify a complete changed-file inventory asbase_sensitivewhen it intersects a test file declaring thebase_sensitivemarker; a missing inventory isunknownand fails closed. -
Changed-file provenance: the gate tries
gh pr diff <pr-number> --name-onlyfirst. If GitHub rejects the unified diff because it exceeds its line limit, it falls back to the paginated RESTpulls/<number>/filesendpoint with strict page and filename validation. A failed, malformed, empty, pagination-exhausted, or 3,000-file-capped response remainsunknown; no partial file list is used for admission. -
Base-sensitive path: a
base_sensitivePR must pass the existing workflow-run/base freshness check and the focusedbase_sensitivesubset 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 ofscripts/dev/check_pr_current_base_cas.pyimmediately before its expected-head merge. This proof qualifies onlystale_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-mergerskill runs this check for the base-sensitive path and runscheck_pr_current_base_cas.pyas 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 installedghversion does not providegh pr update-branch, use the guarded repository helper after recording the current head SHA. The drop-inscripts/dev/update_pr_branch_safely.sh <number> --expected-head-sha <sha>triesgh/gh apiupdate-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 missingrefs/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, additivesource_ref_restore_failed/source_ref_restorevalues) 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-onlyscripts/dev/update_pr_branch.pyis kept for environments where the RESTupdate-branchendpoint works.
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> --jsonThe 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> --jsonFor 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> --jsonmerge-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> --jsonDo 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:
- It works immediately without enabling a repository-level feature that requires maintainer approval to toggle branch-protection settings.
- The explicit selector preserves the stronger current-base proof for the known snapshot, fixture-hash, tuple-shape, and count-ratchet surfaces.
- 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.
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:
capturethe issue, base, remote branch, and local HEAD SHAs before readiness.- Run
checkimmediately before opening or updating the PR. - Treat
supersededandblockedas fail-closed stops. Treatrefresh-requiredas stale evidence; runsync --integrateonly 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.
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 onmerge_group, and publishes non-blocking source-PR andworkflow_dispatchaudits). Source-PR audits refresh whenmerge-readyis added or removed and when a labeled source head changes. They preserve a truthful failedpassedvalue 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-readylabel, a current exact-headgate-verdict: accepted @ <head_sha>trailer, and a currentpr-metadata: reconciled @ <digest>trailer binding the exact final PR title/body pair (the gate reusesscripts/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'sALLGREENstrategy ("Only merge non-failing pull requests"), so every earlier entry represented by a grouped synthetic head must pass its own gate;HEADGREENfails 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 currentmain). - Audit record: the job emits a
merge_queue_gate.v1audit 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-testexercises 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.
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-onlyreads 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 --applyis 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.
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.
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_ll7The 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(withinvalidated_reasonsand the observed values) when the head, currentmain, 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_lagis distinct from ordinarypendingwork and from terminalfailure; the checks payload carries thecheck_run_stale_job_successdiagnostic and the resume command is the bounded CI monitor (exit code 2 until the lag resolves).quota_blockedsurfaces thegh api rate_limitcore/GraphQL reset (or aRetry-Aftervalue) asresume.min_delay_secondsandresume.resume_epoch_seconds; re-run the snapshot only after that delay, never in a spin loop.- Exit codes:
0stable,1changed/failure/error,2inconclusive (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.
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
EOFscripts/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 \
--jsonThe 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 --jsonchecks.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.
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 100The 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 100Do 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 --jsonThe 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 --jsonThe 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) andnext_actionto avoid stale or noisy routes. - Explicit stop-state labels (
blocked,state:blocked,state:blocked-external-input,evidence:blocked,state:hold, ordecision-required) are preserved inpreflight.blocked_state. They take precedence over review or merge hints and emitawait_blocker_owner_or_approvalwith the relevantnext_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.v2addsbase_freshnessto each PR row, withbase_sha,current_main_sha, a bounded verdict (fresh,stale,missing-base, orunavailable-current-main), and the required action. Stale bases arestale; missing or unavailable provenance isblocked, so those rows cannot route to merge readiness from the compact snapshot alone. - If GraphQL quota is exhausted during
--activediscovery, 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 markstruncated: trueonly 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 inrest_enrichment. Such snapshots carrydata_source: rest_fallback_graphql_quotaandroute_evidence_only: true; GraphQL-only review threads areunknown_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, andchecksoutput, not raw full-comment payloads.
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 --jsonThe 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.
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 commentsAll 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.
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 --jsonThe 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 --jsonThe 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: fileadapter:
version: 1
kind: external_probe
name: github_graphql_quota
minimum_remaining: 100adapter:
version: 1
kind: repo_predicate
name: text_present
path: robot_sf/adversarial
text: adversarial_independent_outcomesPath 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
gitfor 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
ghfor 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.jsoncache followingdocs/templates/github.project5-cache.example.json. - Check
gh api rate_limitbefore 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.
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.
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 --onceThe --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 markdownThe 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 --jsonManifest-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
rgresults, 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.
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.presentseparately fromvalidation_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_metwarnings as routing-friction evidence, not as proof that the final diff is wrong. - Raw-log avoidance: Start from
scripts/dev/route_efficiency_report.pyoutputs 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-readyauthorization.
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.mdRUN_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 --silentRECEIPT_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.
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.mdas 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.mdwhen 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.
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.yamlThe 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.
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/, andmemory/benchmarks/. - Use the experiment naming pattern
memory/experiments/YYYY-MM-DD_<topic>.md. - Keep
memory/MEMORY.mdshort 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; usememory/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.
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.yamlOn 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.yamlRelease-process references:
docs/benchmark_release_protocol.mddocs/benchmark_release_reproducibility.md
Benchmark work is fail-closed by default. Use the canonical policy notes:
docs/context/issue_691_benchmark_fallback_policy.mddocs/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.
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.
robot_sf/gym_env/: Gymnasium environment implementations with factory patternrobot_sf/baselines/: Baseline navigation algorithms (e.g., SocialForce) for benchmarkingrobot_sf/benchmark/: Benchmark runner, CLI, metrics collection, and schema validationrobot_sf/sim/: Core simulation components (FastPysfWrapper for pedestrian physics)fast-pysf/: Git subtree providing optimized SocialForce pedestrian simulationdocs/: Documentation, design notes, and development guides
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
- 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-pysfsubtree → NumPy/Numba physics
For complete documentation, see Configuration Architecture (precedence rules, migration guide, module structure).
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)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.
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.
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_seedTroubleshooting:
- 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
- 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
specsdirectory. - 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/.
- Canonical skills live in
- Validate or repair supported mirrors with
uv run python scripts/tools/sync_ai_config.py --checkoruv run python scripts/tools/sync_ai_config.py --fix. - LLM Constitution and guides can be found here:
docs/maintainer_values.md.specify/memory/constitution.mdAGENTS.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.
- Examples can be found in the
- 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).
- Whenever you have the chance, refactor to fix issues rather than suppressing them. Especially
- Prefer refactoring over adding
# noqasuppressions; only use# noqaas 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.
- Architecture in one line: Gymnasium envs → factory functions → FastPysfWrapper → fast-pysf physics; training/eval via StableBaselines3; baselines/benchmarks under
robot_sf/baselinesandrobot_sf/benchmark. - Environments: always create via factories (
make_robot_env,make_image_robot_env,make_pedestrian_env). Configure viarobot_sf.gym_env.unified_configonly; toggle flags before passing to the factory. - Simulation glue: interact with pedestrian physics through
robot_sf/sim/fast_pysf_wrapper.py. Don’t import fromfast-pysfdirectly inside envs. - Baselines/benchmarks: get planners with
robot_sf.baselines.get_baseline(...). Prefer programmatic runners; CLI exists atrobot_sf/benchmark/cli.pyfor convenience. - Local planner adapters: start from
docs/dev/planner_adapter_template.mdand the diagnosticreference_adapterpath before adding a new map-runner planner key. - Demos/trainings: keep runnable examples in
examples/and scripts inscripts/. Place models inmodel/, maps inmaps/svg_maps/, and write outputs underoutput/. - Tests: core in
tests/; GUI intests/pygame/(headless:DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy). Physics-specific tests live infast-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).
- Default to pytest-style tests in
tests/(functions + pytest fixtures). - Using
unittest.mockfor mocks/stubs is fine, but avoid adding newunittest.TestCasesuites. - Legacy
fast-pysf/tests/unittestremains unittest-based for upstream compatibility; new fast-pysf tests should still prefer pytest.
- If you change a function/class body or signature, replace any
TODO docstringplaceholder 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 reportto inspect the current placeholder backlog by top-level area and file. scripts/validation/docstring_todo_baseline.jsonis an increase-only ratchet. Update it with--mode write-baselineonly after an intentional cleanup or maintainer-approved backlog change.- Use
uv run python scripts/validation/check_active_doc_examples.pyto report stale active-doc command and artifact examples. Add--fail-on-diagnosticwhen a PR or CI lane should fail on new hits, and use an inlineactive-docs-check: allowmarker only for intentional examples.
MapDefinition.boundsaccepts 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.
- Canonical outputs live under
output/with stable subdirectories:output/coverage/,output/benchmarks/,output/recordings/,output/wandb/, andoutput/tmp/. - Run
uv run python scripts/tools/migrate_artifacts.py(or the console entry pointuv run robot-sf-migrate-artifacts) after pulling to consolidate any legacyresults/,recordings/,htmlcov/, orcoverage.jsonpaths. - 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/outputbefore 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.
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.
# 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# 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.
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):
-
Core Feature Coverage: Does this test verify a public contract?
- Factory behavior, schema compliance, metric correctness, deterministic reproducibility
- If YES → High priority, fix immediately
-
User Impact: Would failure in production affect users?
- Incorrect metrics, broken benchmarks, environment crashes
- If YES → High priority, fix immediately
-
Regression Prevention: Does it catch known past bugs?
- Validates recent fixes, prevents known failure modes
- If YES → Medium priority, fix within sprint
-
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
-
Brittleness: Does it fail frequently without indicating real bugs?
- Timing issues, display dependencies, environmental flakiness
- If YES → Candidate for refactoring or removal
-
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 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.
# 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"- Included: The canonical wrapper and non-PR CI measure only the
robot_sf/package. Their--cov=robot_sfargument overrides the broaderpyproject.tomlcoverage.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)
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
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.
# 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'])"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_sensorIf 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).
- Use factory env creators; do not instantiate env classes directly.
- Set config via
robot_sf.gym_env.unified_configbefore 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 underscripts/. - Whenever a demo is possible, add one.
- 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
- 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
- Architecture: Social navigation RL framework with Gymnasium environments, SocialForce pedestrian simulation via
fast-pysfsubtree, StableBaselines3 training pipeline, and optional RLlib DreamerV3 workflow - Core pattern: Factory-based environment creation (
make_robot_env()etc.) — never instantiate environments directly - Dependencies:
fast-pysfgit 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 intests/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/*
- Development notes:
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):
- 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 apyproject.toml[tool.ruff.lint.per-file-ignores]entry. - 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. - 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. - 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. - 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).
- 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
mathfor scalar ops/constants,numpyfor vectorized/array ops, and avoid mixing within a single expression.
- Favor readability and maintainability over micro‑optimizations.
- Use type hints for all public functions and methods; prefer
typingoverAny. - Use exceptions for error handling; avoid silent failures.
-
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
- 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.
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.
- 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.
- 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)?
- 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.
- 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 editpyproject.tomland 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
typhase 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.
- VS Code task "Type Check (advisory)" (
- 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:
- 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
- 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/ordocs/dev/issuesfeature-name/
- Format:
- Use descriptive README.md files as the main documentation entry point for each folder
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.
- 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.
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 notesextract-pedestrian-action-helper/- README.md — Helper tool documentation.
img/— Images used across docsped_metrics/Pedestrian metrics documentation and analysis notes.refactoring/Migration/architecture reports and planssnqi-weight-tools/— SNQI weight tooling user docs and schematemplates/Template for new design docs.video/Demo animations for docs.
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
- 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 #123in prose, lists, and tables instead of starting a Markdown line with a bare#123token. - 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.createto Markdown All in One: Create Table of Contents.
- 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/ordocs/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
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.
- Always export vector PDFs (
- Reproducibility
- Each figure = one tracked script or CLI command in
robot_sf/benchmark/figures/,scripts/generate_figures.py, or therobot_sf_benchCLI. - 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.
- Each figure = one tracked script or CLI command in
- 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)$".
- Use Matplotlib with predefined
- 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/.
- Figures go into
- Tests:
uv run pytest tests - Lint:
uv run ruff check .anduv 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-feedbackmatrix → fourscripts/dev/ci_driver.sh testshards 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-gatejob → combines all four non-PR coverage databases, enforces the 85.0% absolute coverage floor, and updates the advisory main baseline.smoke-artifactsjob →scripts/dev/ci_driver.sh smoke artifact-policy- aggregate
cijob → 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.
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-greenIt 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.
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 --jsonThe 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.
The CI pipeline separates fast feedback from the heavier smoke/artifact tail:
fast-feedbackdistributes 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-gatecombines the complete non-PR coverage data before enforcing the 85.0% absolute floor and advisory baseline comparison.smoke-artifactsruns validation smoke checks, uploads benchmark/recording artifacts, and enforces the artifact-root policy.- Both jobs call the canonical
scripts/dev/ci_driver.shphases instead of duplicating validation semantics in workflow YAML. - System packages are installed through the supported
apt-getpath in one update/install step per job; the workflow does not downloadapt-fastat 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-artifactsuploads map verification, benchmark, recording, and cold/warm performance artifacts from the canonicaloutput/tree.- Cold/warm regression smoke is driven by
uv run python -m robot_sf.benchmark.perf_cold_warmthroughscripts/dev/ci_driver.sh smoke. Pull requests run the check in advisory mode;mainandworkflow_dispatchruns use the stricter regression gate. - Startup/reset performance is measured by
scripts/validation/performance_smoke_test.pyduring strict smoke runs, and telemetry smoke/perf coverage is exercised byscripts/validation/run_examples_smoke.py --perf-tests-only.
Local Testing:
- Use
scripts/dev/run_ci_local.shfor 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 aslint,typecheck,test,smoke, orartifact-policy. - Use
uv run python scripts/dev/ci_timing_summary.py --run-id <github-actions-run-id> --top 10to 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 testswhen a refactor needs a local snapshot of large modules, long functions, or captured pytest duration rows. - Optional local GitHub Actions execution with
actis not currently a supported repository workflow. Issue #1308 evaluated this path on 2026-05-18 and did not adopt it because Docker was available butactwas not installed, so no workflow job could be proven locally. See the evaluation note. gh actis now installed on one local machine and Issue #1342 proved non-interactive--dryrunworkflow graph validation, but not a real local workflow execution. Keep usingscripts/dev/run_ci_local.shandBASE_REF=origin/main scripts/dev/pr_ready_check.shas the supported local proof paths until a real narrowgh acttarget is recorded.
Performance Breach Handling:
- Cold/warm PR smoke uses advisory thresholds by default;
mainandworkflow_dispatchruns 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, andROBOT_SF_PERF_ENFORCE=1.
./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.pySuccess 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_enabledexamples 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)
- Environment variables:
ROBOT_SF_PERF_ENFORCE=1to fail on soft (and hard) breaches (use locally for strict tuning).- (Advanced)
ROBOT_SF_PERF_SOFT/ROBOT_SF_PERF_HARDmay 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.
- Validate catalog –
uv run python scripts/validation/validate_examples_manifest.pyensures the manifest enumerates every script and that docstrings stay aligned with summaries. - Review planned changes –
uv run python scripts/validation/run_examples_smoke.py --dry-runprints theci_enabledset before executing pytest, making it easy to confirm archive decisions. - Run tracker/perf gate –
uv run python scripts/validation/run_examples_smoke.py --perf-tests-only --perf-num-resets 2exercises the imitation pipeline tracker smoke plus telemetry perf wrapper without re-running the entire pytest suite. - Execute smoke harness –
uv run python scripts/validation/run_examples_smoke.pyruns all active examples headlessly; pytest fixtures already configure pygame for a dummy display. - Archive responsibly – whenever a script moves into
examples/_archived/, updateexamples/_archived/README.md, setci_enabled: falsewith aci_reason, and point the module docstring at the maintained replacement.
- Threshold logic now includes soft vs hard tiers with environment overrides. Soft breaches on CI default to WARN (exit 0) unless
- Enable the tracker with
--enable-trackeronexamples/advanced/16_imitation_learning_pipeline.py. A background guard now snapshots manifests roughly every five seconds and trapsSIGINT/SIGTERM, so failed or cancelled runs emit afailedmanifest entry automatically. - Inspect live progress with
statusorwatch:Both commands read the latest manifest snapshot and show current step, elapsed time, ETA, and the last completed step.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
- Use
listto 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|jsonfor human vs machine-readable output
summary(aliased asshow) prints per-run breakdowns, with--format text|json|markdown. Markdown output intentionally mirrors the exported summaries so docs/changelogs can embed them verbatim.exportwrites Markdown or JSON summaries directly to disk:Exports include per-step durations, artifact paths, and any failure context produced by the guard.uv run python scripts/tools/run_tracker_cli.py export <run_id> \ --format markdown \ --output output/run-tracker/summaries/<run_id>.md
- Mirror telemetry to TensorBoard when you need dashboards:
The CLI replays
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
telemetry.jsonlinto 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:
Results are persisted in
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
perf_test_results.jsonwith 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/showwill always have at most a five-second gap between what ran and what was recorded.
# Run maintained performance smoke when performance impact is suspected
DISPLAY= MPLBACKEND=Agg SDL_VIDEODRIVER=dummy \
uv run python scripts/validation/performance_smoke_test.py- 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)
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.
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_reductionsintoped_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_indexis accepted as a fallback), an additive top-levelpairwise_contrastsblock 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 areright_minus_leftfor comparison keys likeA__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.
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/orcaThe 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,
)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.pyuv 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.pyThe 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 imitationUse 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.yamlSet --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 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.yamlThe 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.
# Build and run GPU training (requires NVIDIA Docker)
# NOTE: May fail in CI environments due to network restrictions- uv not found →
curl -LsSf https://astral.sh/uv/install.sh | sh(or use the package manager path indocs/ENVIRONMENT.md) - ffmpeg missing →
sudo apt-get install -y ffmpeg
- 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)
- The project uses uv for env/runner and a factory pattern for environment creation.
- See
docs/UV_MIGRATION.mdanddocs/refactoring/for details.
- 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).
robot_sf/(source),examples/,tests/,tests/pygame/,fast-pysf/(subtree),scripts/,model/,docs/
- 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 stateNA - support helperwith 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/mainbefore PR creation, then required validation scripts run and pass for the selected risk level. - CI green (lint + tests) and PR opened with appropriate links.
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.
- issue template - Agent-ready fallback for small executable tasks
- YAML issue forms for common backlog lanes: research validation, test debt, blocked external artifact/runtime, execution run, and epic. These supplement the Markdown templates; they do not replace the fallback template.
- issue creator skill - Turn vague prompts into structured issues
- issue template auditor skill - Review and repair underspecified issues
- priority assessor skill - Review Project #5 priority inputs against the rubric and explain plausibility
- PR opener skill - Open ready PRs by default with the repository template, issue-scope verification, and conservative readiness freshness checks
- design doc template
- external data audit template - Record source, license, redistribution, checksum, and Robot SF use decisions before staging external assets
- PR template
- 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).
- 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.
# 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/activateThe 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.
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.shfor 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.
- Clarify requirements and pick the validation path by change type (see
Proportional validation above;
docs/maintainer_values.mdis the higher-precedence source). - 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. - Implement with small, reviewed commits.
- Add/extend tests in
tests/ortests/pygame/when touching runtime behavior. - 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.
- Update docs/diagrams; run “Generate UML” if classes changed.
- 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.
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.
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.
- Run
uv run ruff check <touched_files>anduv 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 --shortshould be empty, then rerun final proof withPR_READY_MODE=final. Before running a broadgit status --short --untracked-files=normal, use the compact state snapshot helper to see tracked changes and generated roots without dumping generated trees.
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=1to suppress soft breach warnings (use sparingly; still prints report) - Enforce mode: Set
ROBOT_SF_PERF_ENFORCE=1to escalate any soft or hard breach to a test session failure
Implementation components (all under tests/perf_utils/):
policy.py–PerformanceBudgetPolicydataclass providingclassify(duration)-> none|soft|hardreporting.py– aggregation and formatted slow test reportguidance.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:
- Each test call duration captured via a timing hook in
tests/conftest.py. - At terminal summary the top-N slow tests are ranked and printed with breach classification & guidance lines.
- 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: setROBOT_SF_PERF_SOFT/ROBOT_SF_PERF_HARDfor 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_matrixinstead of duplicating inline YAML - Assert absence of heavy artifacts (videos) where not required
Performance troubleshooting checklist:
- Confirm
smoke=Trueor minimal workload flags applied - Reduce
max_episodes,initial_episodes,batch_size - Disable bootstrap sampling (
bootstrap_samples=0) - Lower
horizon_override - Ensure
workers=1for 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.
LidarScannerSettings.detect_other_robotscontrols whether LiDAR rays include other robots.- Default is
True. - This modifies ray distances only and keeps observation keys unchanged (
drive_state,rays).
SimulationViewsupports per-entity render mode fields:robot_render_mode,ped_render_mode,ego_ped_render_modewith valuescircleorsprite.- 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.
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",
)