Isaac Capture names the overall library and repository. Isaac Teleop remains its teleoperation component; preserve that brand and existing technical identifiers.
Hard requirement: Do not edit or add code under IsaacCapture/ until you have read (e.g. with the Read tool) every relevant AGENTS.md file as defined below. This is not optional, not "only for large tasks," and not satisfiable by inferring from prior conversation. If you have not read those files in this session for this task, stop and read them first.
You must:
- List the directories you expect to edit or create files under (e.g.
src/core/live_trackers/cpp/,src/core/deviceio_base/cpp/inc/…). - For each such directory, read
AGENTS.mdin that directory if it exists. - Walk up toward the IsaacCapture repo root and read
AGENTS.mdin every ancestor directory that has one (e.g.live_trackers/→src/core/→IsaacCapture/AGENTS.md(this file)).
You must not assume that reading the single "closest" AGENTS.md is enough. Multiple AGENTS.md files can apply to one change (e.g. both live_trackers and deviceio_base).
Listing every AGENTS.md in this repo (no curated table here—add new files under the tree without editing this document):
# Run from the IsaacCapture repository root (the directory that contains this file):
find . -name AGENTS.md ! -path '*/.git/*' | sortIf you cannot run a shell, use your search/glob tools on the pattern **/AGENTS.md from the same root. That inventory is for orientation; you must still read every file that applies to the directories you will touch (steps 1–3 above).
Optional context index: src/core/AGENTS.md (also on the ancestor walk—read it when working under src/core/).
- Spell the product name “Isaac Capture” in prose; avoid the ambiguous shorthand “Capture”.
-
Keep licensing summaries brief in README and CONTRIBUTING; automate routine checks and updates rather than documenting manual header maintenance.
-
Preserve root
LICENSE.mdverbatim as Apache-2.0; explain component exceptions in README/CONTRIBUTING and local license files, so GitHub can recognize the default. -
EgoPost uses
LicenseRef-OpenMDW-1.1; preserve upstream exceptions and notices. Keep license-boundary changes separate from copyright-template and year-policy changes. -
Keep canonical texts in
LICENSES/unmodified, including template placeholders; do not prepend project copyright or SPDX headers or include them in header repair. -
Source hosting does not authorize wheel inclusion. Keep package discovery explicit and verify license metadata/notices in each artifact before shipping a new component.
When you create or edit a CMakeLists.txt, add or move a target, place a header,
write #include directives, or restructure directories, follow the repo's
canonical CMake/include rules in cmake/cmake-structure.md.
Read that file before touching build files, header placement, or include paths.
Per-tool shims auto-load the same doc for Claude Code, Cursor, Copilot, and
CodeRabbit (.claude/skills/cmake-structure/, .cursor/rules/cmake-structure.mdc,
.github/instructions/cmake-structure.instructions.md, .coderabbit.yaml); they
only point here — edit the rules in the doc, not the shims.
- In bash scripts (
#!/bin/bash,#!/usr/bin/env bash, or filessourced only by bash), use[[ ... ]]for conditional tests, never the POSIX single-bracket[ ... ].[[is safer (no word-splitting or glob expansion on unquoted operands) and more feature-rich; SonarQube/ShellCheck (SC2292) flag the single-bracket form. - Caveat:
[[is a bash builtin and is not POSIX. Before converting, confirm the script's interpreter is bash — a genuine#!/bin/sh(dash) script must keep[ ... ]. Check the shebang (and, for sourced files, who sources them) first.
Every process in this repo, Python and C++, feeds a single logger tree rooted
at isaaccapture. These rules apply wherever you emit output, not only inside
the logging packages.
- Name every logger under
isaaccapture.In Python that meanslogging.getLogger(__name__)insidesrc/python/isaaccapture/(where__name__already starts withisaaccapture.) and an explicitlogging.getLogger("isaaccapture.<area>.<module>")anywhere else, includingexamples/. In C++ it meansisaaccapture::Logger::get("isaaccapture.<module>.<ClassName>"). Dotted names are the hierarchy: a bare name like"robot_viz"is a sibling ofisaaccapture, not a descendant, so the console and file handlers attached to the root of this tree can never see its records. - Never call
logging.basicConfig(), and never attach your own handler to theisaaccapturelogger or to the Python root logger.basicConfig()installs a handler on the root logger, and nothing here setspropagate = False, so from that call onward everyisaaccapturerecord is emitted twice — once in the shared line format and once in yours. The one exception is a program that imports noisaaccaptureat all, and is meant to keep it that way: there are no handlers of ours for it to duplicate, and reaching forlogging_configwould add the dependency the program exists without. The camera_viz streamer,examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_streamer.py, is the case — a sender-only camera box runs it with no CUDA/Vulkan/OpenXR runtime installed, soimport isaaccapturewould fail outright. Keep the logger names underisaaccapture.anyway; the naming rule above is a convention, not a runtime coupling. - Configure through the public API instead:
logging_config.set_console_level(),set_console_logger_name_filter(),set_console_content_filter(),set_console_format(),set_logger_colors(). A--verboseflag should callset_console_level("debug"), not build a handler. print()/std::coutare for deliberate terminal UX only — CLI usage text, interactive prompts, operator banners, progress lines a log file would ruin. Diagnostics go to a logger. Existing raw-output sites that survive insrc/plugins/*/main.cpp,cloudxr/oob_teleop_*.pyand similar are that deliberate kind; do not "migrate" them, and do not add new ones for diagnostics.- One operator message, one channel. The line rejecting an argument and
the usage block printed straight after it are one message; splitting them
between a logger and
std::coutshows the operator half of it, and underISAACCAPTURE_LOG_SOCKET— wherelocal_sinks()gives a forwarding sink and no console sink — none of the logged half. Judge such a site by the message it belongs to, not by the call on its own line. - A log message some machine greps for is an interface. Before rewording
or re-homing one,
grep -rFthe phrase over.github/,docs/,scripts/andtests/..github/workflows/build-ubuntu.ymlalone waits on three literals and fails fast on eight more, and a miss there does not look like a broken marker — the job waits out its timeout and blames the wrong thing. Note what moving a class name from the message into the logger name costs: the two halves are then separated by the level and pid columns, so no fixed string spans them any more. - Evaluate timed operator reminders on every status snapshot. Gate transition messages on state entry, but do not gate elapsed-time reminders on a transition; repeated snapshots are what advance those timers.
- Six environment variables are the whole external contract. The first
four are read identically by both halves — change one and change both, or
the two stop agreeing:
ISAACCAPTURE_LOGGING(see the next rule),ISAACCAPTURE_LOG_DIR(where log files land),ISAACCAPTURE_LOG_LEVEL(the console threshold every process starts at, the Python leader's included; the six level names and the numeric form, nothing else — spdlog'swarn/errspellings are deliberately not accepted, because Python has no entry for them) andISAACCAPTURE_LOG_SOCKET(set by the session leader; its presence is what makes a process forward instead of owning handlers). The last two belong to the Python half alone:ISAACCAPTURE_NATIVE_CAPTURE=offswitches offcapture_native_output()'s rebinding of the host's fd 1 and fd 2 (a process isaaccapture launches still gets the capture file — its descriptors are not the host's), andISAACCAPTURE_NATIVE_CAPTURE_FILEis written by the leader and read by processes with no interpreter so they can point their own stdio at the same file. Do not invent a seventh. ISAACCAPTURE_LOGGING=offswitches this whole system off; unset or any other value leaves it on. Off means none of it runs — no handler, log file, socket, native capture or thread of its own — and C++ loggers write every level to stderr. A call site whose output would otherwise vanish keeps its pre-system code (print(),basicConfig(), its own handler) behind the check; only that off-path code is exempt from the rules above, since nothing of ours is attached for it to duplicate.- Never rebind the host process's fd 1 or fd 2 outside a scope. This is a
library its host imports; its descriptors are not ours. Output that no logger
can reach — vendor code that formats its own lines onto a descriptor — is
captured either inside
logging_config.capture_native_output(), which restores what it found, or in a process this library launched, whose descriptors are ours to set. There is deliberately no process-wide mode and no import-timedup2(); do not add either back.
Subsystem-internal rules live with the code:
src/python/isaaccapture/logging_config/AGENTS.md
and src/core/log_bridge/AGENTS.md. Read the
relevant one before changing either half.
Comments earn their place by recording what the code cannot: a constraint, a measurement, a trap someone already paid for. They are not a place to narrate.
-
Default to a few lines. A block comment over ~8 lines, or a docstring over ~6, needs a reason. If the explanation is genuinely long, it belongs in a
README.md/ design doc that a reader can skip, not inline where they cannot. -
State the fact, drop the argument. "MuJoCo rewrites meshes into their inertial frame, so STL axes need
mesh_pos/mesh_quat" beats three paragraphs re-deriving why. Keep measured numbers and file/line references; cut the prose around them. -
No counts that drift. A comment must not state how many files, rows, components or call sites something currently has. The number is true the day it is written and wrong after the next build or refactor, and nothing fails when it goes stale. State the property instead — "these SDKs share build-ids", not "they share five". A measured number earns its place only when it is a fixed fact about the world (a protocol limit, a benchmark with its date).
-
Do not narrate history. "An earlier revision did X and it was wrong" is what
git logis for. If the wrong approach is tempting enough to warn about, write the warning as a rule — "do not derive this from the mesh" — not as a changelog. -
No emphasis-by-shouting. Occasional caps for a single load-bearing word is fine; whole sentences in caps are noise once every third comment has them.
-
One statement of a fact, in one place. If a constraint is already in the code, the test name, or an adjacent doc, do not restate it.
This applies to #/// comments, docstrings, and comment blocks in
CMakeLists.txt, pyproject.toml, YAML data files and scene XML.
Any commit whose message is drafted or edited by an AI agent must include
a Signed-off-by: line (Developer Certificate of Origin). Pass -s /
--signoff to git commit, or add the line manually:
Signed-off-by: Your Name <your@email.com>
The name and e-mail must match the committer's git identity
(git config user.name / git config user.email).
A commit-msg pre-commit hook enforces this. Install it once per clone
(in addition to the usual pre-commit install):
pre-commit install --hook-type commit-msg- When moving a focused fix from a release branch onto
main, identify its original base and replay only the fix commits. Do not carry release-only CI changes or duplicate upstream features into the new branch. - Before pushing, compare every URL from
git remote get-url --push --all <remote>with the user-specified owner and repository. Do not infer the destination from the account, remote alias, or branch name. - Before updating an existing pull request, verify its exact head owner, repository, and ref. A similarly named branch does not update that pull request.
-
From the IsaacCapture repo root (this directory), run pre-commit and fix all failures before you treat a change as finished (do not only rely on “should pass” reasoning).
-
Use the same hook set as GitHub Actions:
.github/workflows/pre-commit.yamlruns pre-commit withSKIP=check-copyright-year. Mirror that locally:SKIP=check-copyright-year pre-commit run --all-files
-
Finish a merge or rebase before the full hook run. While merge or rebase metadata exists,
check-merge-conflictcan mistake an RST heading underline (=======) for an unresolved conflict. Before bypassing that false positive for a merge commit, verify there are no unmerged paths or actual conflict markers. Rerun the full hook set after the merge commit and before pushing. -
--all-filesmeans all tracked files. pre-commit enumerates throughgit ls-files, so a file you have created but not yetgit added is skipped and the run passes for the wrong reason — REUSE andruff formatalike. Stage first (git add -A), then run the hooks. -
Format touched Python source with
ruff formatbefore staging. If the pre-commit hook rewrites a file, restage it and rerun the hooks. -
REUSE: files covered by the REUSE hook need
SPDX-FileCopyrightTextandSPDX-License-Identifierin the form the repo already uses (for example the HTML comment block at the top ofREADME.mdalso applies toAGENTS.mdand similar docs). -
Keep local dependency or toolchain trees outside the repository or ignored: the REUSE
--fixhook enumerates non-ignored files and can rewrite them. -
C++ formatting is enforced by CI, not pre-commit. The hook set runs
rufffor Python but does not runclang-format; CI (build-ubuntu.yml) installsclang-format-14and rejects unformatted C++ as-Wclang-format-violations. Before pushing, format touched C++ with the systemclang-format(match CI's version 14) and verify:clang-format -i $(git diff --name-only main -- '*.cpp' '*.hpp' '*.h' '*.cc') clang-format --dry-run --Werror $(git diff --name-only main -- '*.cpp' '*.hpp' '*.h' '*.cc')
-
If a hook failure shows missing or non-obvious repo policy (not a one-off typo), you must add a short reminder under Mandatory learning loop rules to the right
AGENTS.mdor adjacent//comments so the next run does not repeat it—unless it is already documented.
Published wheels carry a contents SBOM and license evidence derived from the build itself. There is no checked-in dependency registry and adding one would be a second source of truth — declare a dependency the way you already would and it appears in the next wheel's inventory.
When the SBOM step stops the Release build, fix the cause; never exclude a file or
record an assumed license. Remedies: scripts/sbom/README.md ('What stops the Release
build').
The collector is a project of its own: uv run --project scripts/sbom isaaccapture-sbom .... Its dependency pins live in scripts/sbom/pyproject.toml
and nowhere else — never restate them as --with flags in a workflow or a doc.
Its tests run the same way, through uv; do not register them with the product's
CMake or CTest.
Read scripts/sbom/README.md first if you are
changing anything under scripts/sbom/, changing how a dependency reaches a
wheel, or touching the wheel-packaging steps in build-ubuntu.yml.
When a fact is stated in more than one place — a predicate in several modules, a claim in both the code and the docs, a heading over the rows it describes — fixing one instance and leaving the rest is the most common way a correction fails to stick here. It has happened repeatedly: a licence rule enforced in four modules, a design doc contradicting its own collector, a per-row reason corrected under a heading that still said the opposite.
- Grep for the claim, not the line you edited. Search the phrase, the
predicate, the constant — across
docs/,scripts/,tests/and the code — and fix every hit in the same pass. - Prefer one authority to a swept duplicate. If the same rule has to hold in several places, give it one definition and call it; the sweep is what you do when you cannot.
- Verify against output, not against the test suite. Several of these passed every test while producing wrong artifacts. Regenerate and look.
Hard requirement: When any of the following happens, you must complete steps 1–3 before you end the session or move on as if the work were complete:
- The user is dissatisfied or corrects your approach (wrong layer, scope, style, or a fix that does not stick).
- Pre-commit or CI fails on something you or another agent could hit again (linters, REUSE/SPDX, formatting, policy hooks, etc.).
- You repeat the same category of error after a correction.
You must:
- Distill what went wrong into a short, reusable pattern (a rule or boundary, not a chat transcript).
- Update the right artifact in the same working pass as the fix: prefer the most local
AGENTS.mdfor package- or subtree-level rules; use the repo rootAGENTS.mdonly for expectations that span the whole tree; put volatile or line-specific detail in//comments next to the code. - Respect scope vs
main(below): only addAGENTS.mdbullets for this branch’s delta tomain(or the agreed base), not for behavior that already exists on the base branch.
You must not skip documentation updates because the user did not say “update AGENTS.md”—failed checks and repeated mistakes trigger this loop automatically.
What belongs in AGENTS.md
- High-level expectations: boundaries between layers, what to avoid, naming or structural conventions, CMake/include policy at a glance, “always / never” rules that stay true across refactors.
- Style of work: how minimal to keep diffs, when to ask for clarification, how this subsystem should relate to OpenXR/schema/deviceio, etc.
- For build-only requests, use the documented CMake flags and environment first (for example
-DISAAC_TELEOP_PYTHON_VERSION=...) rather than patching build files to select options.
What does not belong in AGENTS.md
- Low-level or volatile detail: exact call sequences, field-by-field semantics, long checklists tied to one function, anything that will go stale the next time the code moves.
- Put that in comments in the source files where the behavior lives (short
//notes: intent, invariants, or “do not X because …”).
Scope — document what this branch changes, not main
- New or updated bullets should capture learnings from the delta between this branch (or commit) and
main(or whatever long-lived base you are targeting—release branch, parent MR, etc.). - Do not restate facts that already hold on
mainjust because this branch touches nearby files. If behavior landed in the base history, it belongs in code comments next to that code or in main’s docs—not as “new” guidance in anAGENTS.mdintroduced only to support a smaller follow-up change. - Example: schema- or API-level choices that merged before this branch are out of scope for
AGENTS.mdedits tied to this branch; they add churn and read like stale noise after merge.