Skip to content

Latest commit

 

History

History
356 lines (286 loc) · 21.7 KB

AGENTS.md

File metadata and controls

356 lines (286 loc) · 21.7 KB

Isaac Capture — agent notes

Product naming

Isaac Capture names the overall library and repository. Isaac Teleop remains its teleoperation component; preserve that brand and existing technical identifiers.

CRITICAL — mandatory preflight (do not skip)

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:

  1. List the directories you expect to edit or create files under (e.g. src/core/live_trackers/cpp/, src/core/deviceio_base/cpp/inc/…).
  2. For each such directory, read AGENTS.md in that directory if it exists.
  3. Walk up toward the IsaacCapture repo root and read AGENTS.md in 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/*' | sort

If 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/).

Product naming

  • Spell the product name “Isaac Capture” in prose; avoid the ambiguous shorthand “Capture”.

Component licensing

  • Keep licensing summaries brief in README and CONTRIBUTING; automate routine checks and updates rather than documenting manual header maintenance.

  • Preserve root LICENSE.md verbatim 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.

CMake & include structure

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.

Shell scripts

  • In bash scripts (#!/bin/bash, #!/usr/bin/env bash, or files sourced 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.

Logging — one tree, one place to configure it

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 means logging.getLogger(__name__) inside src/python/isaaccapture/ (where __name__ already starts with isaaccapture.) and an explicit logging.getLogger("isaaccapture.<area>.<module>") anywhere else, including examples/. In C++ it means isaaccapture::Logger::get("isaaccapture.<module>.<ClassName>"). Dotted names are the hierarchy: a bare name like "robot_viz" is a sibling of isaaccapture, 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 the isaaccapture logger or to the Python root logger. basicConfig() installs a handler on the root logger, and nothing here sets propagate = False, so from that call onward every isaaccapture record is emitted twice — once in the shared line format and once in yours. The one exception is a program that imports no isaaccapture at all, and is meant to keep it that way: there are no handlers of ours for it to duplicate, and reaching for logging_config would 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, so import isaaccapture would fail outright. Keep the logger names under isaaccapture. 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 --verbose flag should call set_console_level("debug"), not build a handler.
  • print() / std::cout are 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 in src/plugins/*/main.cpp, cloudxr/oob_teleop_*.py and 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::cout shows the operator half of it, and under ISAACCAPTURE_LOG_SOCKET — where local_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 -rF the phrase over .github/, docs/, scripts/ and tests/. .github/workflows/build-ubuntu.yml alone 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's warn/err spellings are deliberately not accepted, because Python has no entry for them) and ISAACCAPTURE_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=off switches off capture_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), and ISAACCAPTURE_NATIVE_CAPTURE_FILE is 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=off switches 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-time dup2(); 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 and docstrings — say it once, briefly

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 log is 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.

Commits — DCO sign-off for AI-drafted commits

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

Remote pushes

  • 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.

Pre-commit — match CI before you stop

  • 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.yaml runs pre-commit with SKIP=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-conflict can 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-files means all tracked files. pre-commit enumerates through git ls-files, so a file you have created but not yet git added is skipped and the run passes for the wrong reason — REUSE and ruff format alike. Stage first (git add -A), then run the hooks.

  • Format touched Python source with ruff format before staging. If the pre-commit hook rewrites a file, restage it and rerun the hooks.

  • REUSE: files covered by the REUSE hook need SPDX-FileCopyrightText and SPDX-License-Identifier in the form the repo already uses (for example the HTML comment block at the top of README.md also applies to AGENTS.md and similar docs).

  • Keep local dependency or toolchain trees outside the repository or ignored: the REUSE --fix hook enumerates non-ignored files and can rewrite them.

  • C++ formatting is enforced by CI, not pre-commit. The hook set runs ruff for Python but does not run clang-format; CI (build-ubuntu.yml) installs clang-format-14 and rejects unformatted C++ as -Wclang-format-violations. Before pushing, format touched C++ with the system clang-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.md or adjacent // comments so the next run does not repeat it—unless it is already documented.

The wheel SBOM

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.

A rule changed in one place is changed in none

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.

Mandatory learning loop (AGENTS.md and comments)

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:

  1. The user is dissatisfied or corrects your approach (wrong layer, scope, style, or a fix that does not stick).
  2. Pre-commit or CI fails on something you or another agent could hit again (linters, REUSE/SPDX, formatting, policy hooks, etc.).
  3. You repeat the same category of error after a correction.

You must:

  1. Distill what went wrong into a short, reusable pattern (a rule or boundary, not a chat transcript).
  2. Update the right artifact in the same working pass as the fix: prefer the most local AGENTS.md for package- or subtree-level rules; use the repo root AGENTS.md only for expectations that span the whole tree; put volatile or line-specific detail in // comments next to the code.
  3. Respect scope vs main (below): only add AGENTS.md bullets for this branch’s delta to main (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 main just 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 an AGENTS.md introduced 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.md edits tied to this branch; they add churn and read like stale noise after merge.