Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ workspace/.training_pending/
# Per-skill persistent key-value stores (Skill.storage, skills/types.py).
workspace/skill_storage/

# Generated on-robot by the skill catalog (typed physical-skill refs).
workspace/physical_skills/

# recordings folder
recordings/

Expand All @@ -116,7 +119,6 @@ data/.last_mode
arm_wave/

.vscode
ros2_ws/src/brain/brain_client/innate/skills.pyi
workspace/innate_skills/**/*.engine

node_modules/
Expand Down
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,52 @@ Run `innate` with no arguments to print the current system status (version, mode
| `innate volume` | Get or set speaker volume |
| `innate --help` | Show all commands |

## Writing Skills

Skills live in `workspace/` (see [workspace/README.md](workspace/README.md)). A skill is a
`Skill` subclass; everything it consumes is declared with a type annotation.

### Never `time.sleep` — always `self.sleep`

**In skill code, use `self.sleep(seconds)`. Never `time.sleep(seconds)`.**

`self.sleep` wakes and raises `SkillCancelled` the moment a Stop lands; `time.sleep` blocks
to completion, so a skill that uses it keeps running (and keeps the robot moving) after the
user pressed Stop. Sleeping is the only cancel point a loop needs — write the loop as if
cancel didn't exist and let the framework halt the base and report `CANCELLED`.

```python
while traveled < target:
self.mobility.send_cmd_vel(linear_x=velocity, duration=0.5)
self.sleep(0.1) # ✅ cancellable
# time.sleep(0.1) # ❌ Stop is ignored until the sleep finishes
```

`time` itself is fine for *measuring* — `time.time()` / `time.monotonic()` for deadlines and
elapsed checks. The rule is only about blocking.

Related cancel-aware helpers, all of which raise `SkillCancelled` too:

| Call | Use for |
|---|---|
| `self.sleep(seconds)` | Any pause in skill code |
| `self.wait_for(read, timeout)` | Block until a reader returns non-`None` |
| `self.check_cancelled()` | A checkpoint with no sleep (e.g. before an irreversible commit) |
| `self.cancelled` | Read the latch without raising |

Cleanup belongs in `try/finally` inside `execute()`. `self.on_cancel(hook)` is only for
forwarding a cancel to an external action goal — braking the base is automatic.

### The one exception: committed, non-cancellable sections

Teardown and already-committed physical actions must **not** be cancellable, so they use
`time.sleep` on purpose. Once `pick_any_object` closes the gripper, a cancel must not unwind
mid-grip and drop the object over the floor, so `_close_twist_lift` sleeps with `time.sleep`
and the run finishes carrying the object home.

If you write such a section, say so in a comment — otherwise the next reader "fixes" it back
to `self.sleep` and reintroduces the bug. Everywhere else, `self.sleep`.

## Key ROS Packages

| Package | Role |
Expand Down
97 changes: 23 additions & 74 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,35 @@
# CLAUDE.md

Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Project instructions for Claude. See [AGENTS.md](AGENTS.md) for the system overview, the
`innate` CLI, and the ROS package map.

**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## Writing skills

## 1. Think Before Coding
### Never `time.sleep` — always `self.sleep`

**Don't assume. Don't hide confusion. Surface tradeoffs.**
**In skill code, use `self.sleep(seconds)`. Never `time.sleep(seconds)`.**

Before implementing:
`self.sleep` wakes and raises `SkillCancelled` the moment a Stop lands; `time.sleep` blocks
to completion, so a skill that uses it keeps running (and keeps the robot moving) after the
user pressed Stop. Sleeping is the only cancel point a loop needs — write the loop as if
cancel didn't exist and let the framework halt the base and report `CANCELLED`.

- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.

## 2. Simplicity First

**Minimum code that solves the problem. Nothing speculative.**

- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.

Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

## 3. Surgical Changes

**Touch only what you must. Clean up only your own mess.**

When editing existing code:

- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.

When your changes create orphans:

- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.

The test: Every changed line should trace directly to the user's request.

## 4. Goal-Driven Execution

**Define success criteria. Loop until verified.**

Transform tasks into verifiable goals:

- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"

For multi-step tasks, state a brief plan:

```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```python
while traveled < target:
self.mobility.send_cmd_vel(linear_x=velocity, duration=0.5)
self.sleep(0.1) # ✅ cancellable
# time.sleep(0.1) # ❌ Stop is ignored until the sleep finishes
```

Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

## 5. Clean, Readable Code

**You are a senior engineer. Write clean, maintainable code.**

Clean code is understood without reading the comments:

- Use clear, descriptive variable and function names that state intent.
- Keep complexity low. Avoid deep, multiple-levels-of-nesting indentation.
- Use early returns to flatten control flow instead of nesting.
- Keep things simple, always.

When handling errors, don't overdo it:
`time` itself is fine for *measuring* — `time.time()` / `time.monotonic()` for deadlines and
elapsed checks. The rule is only about blocking.

- Avoid scattering multiple try/catch blocks where one is enough.
- Catch errors at the level where you can actually do something about them.
`self.wait_for(read, timeout)` and `self.check_cancelled()` are cancel-aware too; cleanup
belongs in `try/finally`.

---
**The one exception:** teardown and already-committed physical actions must *not* be
cancellable, so they use `time.sleep` deliberately — e.g. once `pick_any_object` closes the
gripper, a cancel must not unwind mid-grip and drop the object. If you write such a section,
comment it, or the next reader will "fix" it back to `self.sleep` and reintroduce the bug.

**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
See [AGENTS.md](AGENTS.md#writing-skills) for the full cancellation contract.
54 changes: 16 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ You will find skills in two different directories:

- **Built-in skills** — Located in `workspace/innate_skills/`.
- **Your custom skills** — Stored in `workspace/custom_skills/`. Gitignored and yours to play with.
- **Skill packs** — Any other folder dropped into `workspace/` loads as its own package (ids `<folder>/<name>`). A pack that lives elsewhere on disk is symlinked in (`ln -s /opt/team/skills workspace/team_skills`) and works the same, hot reload included.

Helpers work like normal Python: any `.py` in your skills folder that doesn't define a `Skill` is just a module — `import` it, use relative imports inside subfolders, share across packages by bare name (`from innate_skills import arm_utils`). Device helpers are methods on the interfaces (`self.manipulation.go(...)`, `self.mobility.rotate_by(...)`); camera math and Gemini live under `innate` (`from innate import geometry, vision, gemini`).

### Skill definition

Expand Down Expand Up @@ -164,52 +167,27 @@ You will find skills in two different directories:
<td width="50%" valign="top">
<strong>Code skill</strong> — call the mobility interface to move forward.<br>
Saved as <code>workspace/custom_skills/move_forward.py</code>:
<pre lang="python">from brain_client.skills.types import Interface, InterfaceType, Skill, SkillResult
import time
<pre lang="python">from innate import Mobility, Skill, SkillReturn


class MoveForward(Skill):
"""Move the robot forward by a given distance."""

mobility = Interface(InterfaceType.MOBILITY)

def __init__(self, logger):
super().__init__(logger)
self._cancelled = False
"""Move the robot forward by a given distance in meters."""

@property
def name(self):
return "move_forward"

def guidelines(self):
return "Move the robot forward by a given distance in meters."

def execute(self, distance_m: float = 0.5):
self._cancelled = False

if self.mobility is None:
return "Mobility interface not available", SkillResult.FAILURE
mobility: Mobility # declare what you use; the runtime injects it

def execute(self, distance_m: float = 0.5) -> SkillReturn:
speed = 0.2 # m/s
duration = distance_m / speed
self.mobility.send_cmd_vel(linear_x=speed, duration=duration)

stop_at = time.time() + duration
while True:
remaining = stop_at - time.time()
if not remaining > 0:
break
if self._cancelled:
self.mobility.send_cmd_vel(linear_x=0.0)
return "Move cancelled", SkillResult.CANCELLED
time.sleep(min(0.05, remaining))

return f"Moved forward {distance_m} m", SkillResult.SUCCESS

def cancel(self):
self._cancelled = True
return "Move cancelled"
self.sleep(duration) # like time.sleep, but a Stop unwinds it
return f"Moved forward {distance_m} m"
</pre>
The return value is the run's result message; call
<code>self.fail(message)</code> to end the run as a failure.
Cancellation is the framework's job: <code>self.sleep</code> (and every
blocking framework call) raises the moment a Stop lands, the base is
braked automatically, and the run reports CANCELLED — skills carry no
cancel code.
</td>
</tr>
</table>
Expand Down Expand Up @@ -336,7 +314,7 @@ Input devices live in [`workspace/inputs/`](workspace/inputs/) and are pure Pyth
import threading
import time

from brain_client.input_types import InputDevice
from brain_client.inputs.types import InputDevice


def read_thermometer_celsius() -> float:
Expand Down
1 change: 0 additions & 1 deletion ci/run_integration_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ fi
echo "=== unit tests (fast, no ROS) ==="
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -q \
src/brain/brain_client/test/test_fake_cloud_selftest.py \
src/brain/brain_client/test/test_backwards_compat.py \
src/brain/manipulation/test/test_config_validation.py

echo "=== unit tests: webapp front door (aiohttp, no ROS) ==="
Expand Down
19 changes: 10 additions & 9 deletions config/settings.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,13 @@
# cmd_publish_hz: 50.0 # cmd_vel republish rate during a move
# poll_period_sec: 0.02 # action-loop poll interval

# ── Extra agent / skill directories ───────────────────────────────────
# Scan agents/skills from extra locations, on top of the built-in workspace/ dirs.
# Absolute paths anywhere on the machine, scanned in place (never created); in a Docker/sim
# setup, host paths must also be mounted into the container. (Read directly by the agent /
# skill loaders — not a ROS node, so leave the section name as `script_paths`.)
# script_paths:
# ros__parameters:
# extra_agent_dirs: ["/home/me/my-agents", "/opt/team/agents"]
# extra_skill_dirs: ["/home/me/my-skills"]
# Extra agent/skill directories (script_paths.extra_agent_dirs / extra_skill_dirs)
# were removed in 0.7: agents and skills load only from workspace/. To add a
# skill pack, drop its directory into workspace/ — it is picked up (and
# hot-reloaded) without a restart. A pack that must stay elsewhere on disk is
# symlinked in instead, and behaves the same:
# ln -s /opt/team/skills ~/innate-os/workspace/team_skills
# Dirs a 0.6.x settings.yaml configured are symlinked into workspace/
# automatically on update (scripts/update/migrate_user_data.sh): skill dirs as
# custom_skills/ subpackages (keeping their local/<name> skill ids), agent-dir
# entries file-by-file into custom_agents/.
8 changes: 4 additions & 4 deletions docs/INPUT_DEVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HelloWorld(Directive):
```python
# workspace/inputs/my_sensor_input.py

from brain_client.input_types import InputDevice
from brain_client.inputs.types import InputDevice
import threading
import time

Expand Down Expand Up @@ -215,9 +215,9 @@ innate-os/
│ └── hello_world_directive.py # Uses get_inputs()
└── ros2_ws/src/brain/brain_client/
└── brain_client/
├── input_types.py # Base class
├── input_loader.py # Auto-discovery
└── input_manager_node.py # ROS bridge
├── inputs/types.py # Base class
├── inputs/loader.py # Auto-discovery
└── nodes/input_manager.py # ROS bridge
```

## Summary
Expand Down
1 change: 0 additions & 1 deletion docs/PARAMETERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ To tune something, **uncomment a whole stanza** (the `node:`, `ros__parameters:`
| `navigation_grid_localizer` | `max_score_threshold`, `max_range`, `auto_localize_timeout` | `0.3`, `12.0`, `30.0` |
| `brain_client_node` | `cartesia_voice_id` (TTS voice), `vertical_fov`, `pose_image_interval`, `scan_stale_after_sec`, `send_depth`, `send_arm_camera_image`, `log_everything`, STT/transcribe models | see template |
| `uninavid_node` (VLN) | `forward_speed`, `turn_speed`, `cmd_duration_sec`, `image_send_hz`, `consecutive_stops_to_complete`, `cmd_publish_hz`, `poll_period_sec` | `0.3` / `0.8`, rest see template |
| `script_paths` | `extra_agent_dirs`, `extra_skill_dirs` (extra dirs scanned on top of `workspace/`) | `[]` |

> **Driving caps vs the safety clamp.** `motion_control` is the *driving feel* cap: the
> joystick, keyboard, and app drive joystick all ship the same `0.4` m/s / `1.0` rad/s
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ignore = [
]

[tool.ruff.lint.isort]
known-first-party = ["mars_nav", "mars_control", "mars_cam", "mars_arm", "brain_client", "manipulation"]
known-first-party = ["mars_nav", "mars_control", "mars_cam", "mars_arm", "brain_client", "manipulation", "workspace", "innate", "innate_proxy"]

[tool.ruff.format]
quote-style = "double"
Expand Down
30 changes: 30 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"pythonVersion": "3.10",
"pythonPlatform": "Linux",
// Scoped to the tree that is actually clean under standard mode: the skills
// API and the skills written against it. Unscoped, pyright checks all 291
// files in the repo and reports ~230 real diagnostics from code that was
// never typed — burying the ones a skill author needs to see. Add paths
// here as they get cleaned (test/ and the rest of ros2_ws are not yet).
"include": [
"ros2_ws/src/brain/brain_client/brain_client/skills",
"ros2_ws/src/brain/brain_client/innate",
"workspace/innate_skills"
],
"extraPaths": [
"workspace",
"ros2_ws/src/brain/brain_client",
"ros2_ws/src/cloud/clients/proxy-client",
"ros2_ws/install/brain_client/local/lib/python3.10/dist-packages",
"ros2_ws/install/brain_messages/local/lib/python3.10/dist-packages",
"ros2_ws/install/innate_cloud_msgs/local/lib/python3.10/dist-packages",
"ros2_ws/install/mars_msgs/local/lib/python3.10/dist-packages",
"/opt/ros/humble/lib/python3.10/site-packages",
"/opt/ros/humble/local/lib/python3.10/dist-packages",
// System site-packages (pydantic, etc.). Needed when the language server
// has no usable interpreter and would otherwise only see the paths above.
"/usr/local/lib/python3.10/dist-packages",
"/usr/lib/python3/dist-packages",
"/usr/lib/python3.10/dist-packages"
]
}
1 change: 0 additions & 1 deletion ros2_ws/pip-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ dynamixel-sdk
pyserial
trimesh
smbus2
sounddevice
bluezero
python-chess

Expand Down
2 changes: 1 addition & 1 deletion ros2_ws/src/brain/brain_client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ament_python_install_package(${PROJECT_NAME}
PACKAGE_DIR brain_client
)

# Public authoring namespace for skill files (`from innate.skills import ...`)
# Public authoring namespace for skill files (`from innate import Skill, Mobility, ...`)
ament_python_install_package(innate
PACKAGE_DIR innate
)
Expand Down
9 changes: 0 additions & 9 deletions ros2_ws/src/brain/brain_client/brain_client/agent_types.py

This file was deleted.

Loading
Loading