Skip to content
Closed
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 .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:

- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v3
with:
install_components: beta

- name: Submit integration tests to Cloud Build
run: |
Expand All @@ -54,7 +56,7 @@ jobs:
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
publish_base="true"
fi
gcloud builds submit \
gcloud beta builds submit \
--project="${{ vars.GCP_PROJECT_ID }}" \
--region="${{ vars.GCP_REGION }}" \
--gcs-source-staging-dir="${{ vars.GCP_CLOUD_BUILD_SOURCE_STAGING_DIR }}" \
Expand Down
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
86 changes: 0 additions & 86 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,86 +0,0 @@
# CLAUDE.md

Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.

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

## 1. Think Before Coding

**Don't assume. Don't hide confusion. Surface tradeoffs.**

Before implementing:

- 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]
```

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:

- Avoid scattering multiple try/catch blocks where one is enough.
- Catch errors at the level where you can actually do something about them.

---

**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.
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
15 changes: 6 additions & 9 deletions config/settings.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,9 @@
# 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
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
29 changes: 29 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"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": [
"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"
]
}
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.

28 changes: 25 additions & 3 deletions ros2_ws/src/brain/brain_client/brain_client/agents/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
to keep the main brain_client_node.py clean and focused.
"""

from typing import Any

from brain_client.agents.loader import AgentLoader
from brain_client.agents.types import Agent
from brain_client.common.script_paths import (
ensure_user_directories,
get_agent_directories,
get_workspace_dir,
)
from brain_client.skills.physical_refs import render_refs, write_refs

# roster types that are physical skills (data, no code class) — these are what
# the generated physical_skills package covers
_PHYSICAL_TYPES = frozenset({"learned", "replay", "eval", "physical"})


def initialize_agents(logger, skills_dict: dict[str, Any] | None = None) -> tuple[dict[str, Any], Any | None]:
def initialize_agents(logger, skills_dict: dict[str, dict] | None = None) -> tuple[dict[str, Agent], Agent | None]:
"""
Initialize all agents using dynamic loading.

Expand All @@ -36,6 +41,13 @@ def initialize_agents(logger, skills_dict: dict[str, Any] | None = None) -> tupl
# and, if present, ~/agents (in place — never moved).
ensure_user_directories()

# Agent files may `from physical_skills import X`, so make sure the
# generated package matches this roster before importing them. The skills
# server writes it too (on every publish); write_refs content-compares, so
# whichever runs second is a no-op. Doing it here as well means agent
# loading never depends on the two processes' ordering.
_regenerate_physical_refs(logger, skills_dict)

agents_directories = [str(p) for p in get_agent_directories()]

# Load all agents dynamically from all directories
Expand Down Expand Up @@ -64,3 +76,13 @@ def initialize_agents(logger, skills_dict: dict[str, Any] | None = None) -> tupl
logger.error("No agents loaded! This will cause issues.")

return agents, default_agent


def _regenerate_physical_refs(logger, skills_dict: dict[str, dict] | None) -> None:
"""Write workspace/physical_skills/ from the roster metadata. Skipped when
no roster is available (nothing to generate from — an existing package is
left alone rather than emptied)."""
if not skills_dict:
return
entries = [meta for meta in skills_dict.values() if meta.get("type") in _PHYSICAL_TYPES]
write_refs(get_workspace_dir() / "physical_skills", render_refs(entries), logger)
Loading
Loading