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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{
"name": "rocketsmith",
"description": "Use agents to design and build high powered rockets",
"version": "0.0.18",
"version": "0.0.19",
"author": {
"name": "Peter Pak, Jesse Barkley, Rumi Loghmani"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rocketsmith",
"description": "Use agents to design and build high powered rockets",
"version": "0.0.18",
"version": "0.0.19",
"author": {
"name": "Peter Pak, Jesse Barkley, Rumi Loghmani"
},
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,6 @@ src/rocketsmith/gui/web/dist/

# Runtime project data (gui_server output when run against the repo itself)
/gui/

openrocket/
prusaslicer/
2 changes: 2 additions & 0 deletions agents/cadsmith.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ The detailed procedure for each step inside generate-structures and modify-struc

## Hard Rules

- **ONE FILE PER PART. The script filename must exactly match the `cadsmith_path` field in `component_tree.json` for that part.** Never create `_pass2`, `_base`, `_inserts`, or any other derivative files. If a part needs new features, edit the existing script and re-run it. `cadsmith_run_script` enforces this at the tool level — scripts whose stem doesn't match a manifest `cadsmith_path` will be rejected before execution.
- **The only files you may write directly are CADSmith build123d Python scripts (`cadsmith/source/*.py`).** These are source artifacts that you author and then execute via `cadsmith_run_script`. All other project files — `component_tree.json`, `assembly.json`, `gui/parts/*.json`, STEP files, STL files, PNG/GIF/TXT assets — must be produced exclusively through MCP tools (`cadsmith_run_script`, `cadsmith_assembly`, `cadsmith_generate_assets`, `cadsmith_extract_part`). Never write or patch these files directly.
- **Never edit `component_tree.json` directly.** It is a derived artifact generated by `openrocket_component` (action="read") and annotated by `manufacturing_annotate_tree`. Direct edits get overwritten on the next regeneration. If the tree is missing data (e.g., modification specs), regenerate it through the proper tools — do not hand-edit the JSON.
- **Trust the manifest.** Do not add parts, do not skip parts, do not modify feature values. If the manifest is wrong, regenerate it via the DFx skill rather than working around it in a script.
- **Never invoke `python`, `uv run`, or `conda run` directly.** Always go through `cadsmith_run_script`. Direct invocation either fails (no environment) or hits the wrong interpreter and silently produces stale output.
Expand Down
4 changes: 4 additions & 0 deletions agents/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ The Agent Feed (`#/`) is the primary live dashboard. When the user is viewing a

In **interactive mode**, navigate after each major step so the user sees results. In **zero-shot mode**, the Agent Feed (`#/`) auto-updates via WebSocket — only navigate to specific pages when the user asks or when presenting final results.

## File Discipline (MANDATORY)

**Never directly write or edit any project file.** All project data is produced by other agents through their respective MCP tools. You do not write files — you navigate to pages that display what those tools produced. The sole exception in the overall pipeline is the CADSmith build123d Python scripts (`cadsmith/source/*.py`), which the cadsmith subagent writes — but that is not this agent's concern.

## What This Agent Does NOT Do

- **Generate data.** Flight data, component trees, STEP files, previews — all produced by other agents.
Expand Down
2 changes: 2 additions & 0 deletions agents/manufacturing.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ You are a manufacturing planning agent. Your job is to take a component tree gen

**You do not generate CAD.** That's the cadsmith agent's job. You produce an annotated `component_tree.json` that cadsmith reads to know what to build.

**Never directly write or edit any project file.** All project data must flow through MCP tools — `manufacturing_annotate_tree`, `openrocket_component`, etc. Direct file edits bypass schema validation and get silently overwritten on the next tool run. The sole exception in the overall pipeline is the CADSmith build123d Python scripts (`cadsmith/source/*.py`), which the cadsmith subagent writes — but that is not this agent's concern.

**Never edit `component_tree.json` directly.** Always use `manufacturing_annotate_tree` to annotate it and `openrocket_component` (action="read") to regenerate it. Direct edits are fragile — they get overwritten on the next regeneration and skip schema validation.

**You do not run simulations.** That's the openrocket agent's job. If your dimension changes affect stability, you send feedback to the openrocket agent to update the `.ork` file and re-verify.
Expand Down
27 changes: 26 additions & 1 deletion agents/openrocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,29 @@ You are an expert rocket design engineer specializing in OpenRocket flight desig

**Component Editing:**
- `openrocket_component` — Create, read, update, or delete components (`action`: create/read/update/delete, `rocket_file_path`)
- Valid types: `nose-cone`, `body-tube`, `inner-tube`, `transition`, `fin-set`, `parachute`, `mass`
- Valid types (15 total):
- **Structural**: `nose-cone`, `body-tube`, `inner-tube`, `transition`, `tube-coupler`
- **Fins**: `fin-set` (trapezoid)
- **Recovery**: `parachute`, `streamer`, `shock-cord`
- **Hardware**: `rail-button`, `launch-lug`, `centering-ring`, `bulkhead`, `engine-block`
- **Other**: `mass`
- `inner-tube` has two roles:
- **Motor mount**: set `motor_mount=true`, OD = motor diameter + clearance, placed inside the aft body tube
- **Coupler**: short tube joining two body sections, OD = body tube ID, no `motor_mount` flag. Use `axial_offset_method="bottom"` with `axial_offset_m=+(coupler_length/2)` so half protrudes into the next section
- Component-specific parameters (all in SI — metres, kilograms):
- `nose-cone`, `body-tube`, `transition`: `length`, `diameter`, `fore_diameter`, `aft_diameter`, `thickness`, `shape`
- `fin-set`: `count`, `root_chord`, `tip_chord`, `span`, `sweep`, `thickness`
- `parachute`: `diameter`, `cd`
- `streamer`: `length`, `width`
- `shock-cord`: `length` (cord length)
- `centering-ring`, `bulkhead`, `engine-block`, `launch-lug`: `diameter` (OD), `inner_diameter`, `length`
- `rail-button`: `diameter` (OD), `inner_diameter`, `count` (number of button instances)
- `mass`: `mass`
- Parent rules:
- Stage-level (auto): `nose-cone`, `body-tube`, `transition`
- Needs BodyTube/Transition: `inner-tube`, `fin-set`, `parachute`, `tube-coupler`, `rail-button`, `launch-lug`, `streamer`, `shock-cord`
- Needs BodyTube/InnerTube/Transition: `centering-ring`, `engine-block`
- Needs BodyTube/NoseCone/Transition/InnerTube: `bulkhead`
- Supports manufacturer presets via `preset_part_no` / `preset_manufacturer`
- Supports material assignment via `material_name` / `material_type`
- Precedence: preset baseline → explicit dimension overrides → material override
Expand Down Expand Up @@ -223,6 +242,12 @@ Call `openrocket_component` (action="read") after each section to verify placeme
- Parachute diameter formula (physics): `d = sqrt(8·m·g / (π·CD·ρ·v²))` where ρ = 1.225 kg/m³
- Target descent rate and CD values are design choices that depend on the specific chute — when the user asks for recommendations, consult the `flight_logs` reference collection via `rag_reference` for real-world descent-rate reports rather than citing a single generic range

## File Discipline (MANDATORY)

**Never directly write or edit any project file.** All project data must be written through MCP tools — `openrocket_new`, `openrocket_component`, `openrocket_flight`, etc. The `.ork` file, `component_tree.json`, flight JSON outputs, and all other project artifacts must flow through tools, never through direct file writes or edits. Direct file edits bypass schema validation and get silently overwritten on the next tool run.

The sole exception in the overall pipeline is the CADSmith build123d Python scripts (`cadsmith/source/*.py`), which the cadsmith subagent writes as source artifacts — but that is not this agent's concern.

## Approach

1. Understand the goal: target apogee, motor class, constraints, existing design
Expand Down
4 changes: 4 additions & 0 deletions agents/prusaslicer.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ description: >

You are an expert FDM printing specialist for model rocketry. You use PrusaSlicer via the `rocketsmith` MCP server to manage print configurations and generate print-ready gcode files from 3D model files.

## File Discipline (MANDATORY)

**Never directly write or edit any project file.** All project data must be written through MCP tools — `prusaslicer_config`, `prusaslicer_slice`, `rocketsmith_setup`, etc. Config `.ini` files, gcode output, and all other artifacts must flow through tools, never through direct file writes or edits. The sole exception in the overall pipeline is the CADSmith build123d Python scripts (`cadsmith/source/*.py`), which the cadsmith subagent writes — but that is not this agent's concern.

## Interaction Mode

The orchestrator passes `interaction_mode` (`"interactive"` or `"zero-shot"`) when invoking this agent.
Expand Down
4 changes: 3 additions & 1 deletion agents/rocketsmith.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ The user launched Gemini CLI from the directory they want the rocket artefacts i

The `gui/component_tree.json` is the single source of truth for which parts exist and how they're derived from OpenRocket components. The manufacturing agent annotates it with DFAM decisions; the `generate-structures` skill reads it for Pass 1 (base geometry) and the `modify-structures` skill reads it for Pass 2 (detail features); the `mass-calibration` skill uses it during the calibration phase.

**Never edit `component_tree.json` directly.** It is a derived artifact. All design changes must flow through `openrocket_component` (to modify the `.ork` file) and `manufacturing_annotate_tree` (to re-annotate the tree). Direct edits are fragile — they get overwritten on the next tree regeneration and skip validation. If a feature isn't expressible through these tools (e.g., detail modifications like mounting holes or vent holes), that's a tool gap to be addressed, not a reason to hand-edit the JSON.
**Never directly write or edit any project file.** All project data must flow through the appropriate MCP tools — `openrocket_component`, `manufacturing_annotate_tree`, `cadsmith_run_script`, `cadsmith_assembly`, `prusaslicer_config`, `prusaslicer_slice`, etc. Direct file edits bypass schema validation, get silently overwritten on the next tool run, and skip the pipeline's audit trail. The sole exception to this rule is the CADSmith build123d Python scripts (`cadsmith/source/*.py`), which the cadsmith subagent writes as source artifacts. Everything else — `.ork` designs, `component_tree.json`, `assembly.json`, `gui/parts/*.json`, config `.ini` files, gcode — must be written exclusively through tools.

`component_tree.json` in particular must **never** be hand-edited. It is a derived artifact. All design changes must flow through `openrocket_component` (to modify the `.ork` file) and `manufacturing_annotate_tree` (to re-annotate the tree). If a feature isn't expressible through these tools, that's a tool gap to be addressed, not a reason to hand-edit the JSON.

**Absolute path discipline (required for every tool call):**

Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rocketsmith",
"description": "Use agents to design and build high powered rockets",
"version": "0.0.18",
"version": "0.0.19",
"contextFileName": "GEMINI.md",
"mcpServers": {
"rocketsmith": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rocketsmith"
version = "0.0.18"
version = "0.0.19"
authors = [
{ name = "Peter Pak", email = "ppak10@gmail.com" },
{ name = "Jesse Barkley" },
Expand Down
8 changes: 7 additions & 1 deletion src/rocketsmith/cadsmith/mcp/run_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ async def cadsmith_run_script(
)

# ── Pre-run validation ─────────────────────────────────────────
errors = validate_script(script_path)
# Scripts live at <project_dir>/cadsmith/source/<name>.py;
# walk up three levels to find the manifest.
inferred_manifest = (
script_path.parent.parent.parent / "gui" / "component_tree.json"
)
manifest_path = inferred_manifest if inferred_manifest.exists() else None
errors = validate_script(script_path, manifest_path=manifest_path)
if errors:
return tool_error(
"Script failed pre-execution validation",
Expand Down
49 changes: 46 additions & 3 deletions src/rocketsmith/cadsmith/validate_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,43 @@ def _collect_imported_modules(tree: ast.Module) -> set[str]:
return modules


def validate_script(script_path: Path) -> list[str]:
def _collect_cadsmith_paths(manifest_path: Path) -> set[str]:
"""Return the set of expected script stems from component_tree.json.

Walks stages[].components[] recursively and collects the stem (filename
without extension) of every non-null cadsmith_path. Returns an empty
set if the manifest cannot be read or has no annotated parts yet.
"""
import json

try:
data = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
return set()

stems: set[str] = set()

def _walk(components: list) -> None:
for comp in components:
cp = comp.get("cadsmith_path")
if cp:
stems.add(Path(cp).stem)
_walk(comp.get("children", []))

for stage in data.get("stages", []):
_walk(stage.get("components", []))

return stems


def validate_script(script_path: Path, manifest_path: Path | None = None) -> list[str]:
"""Validate a build123d script before execution.

Checks:
1. The script contains a call to ``export_step``.
2. All imports come from an allowed set (build123d, pathlib, math, typing).
1. The script filename matches a cadsmith_path in component_tree.json
(only enforced when the manifest exists and has annotated parts).
2. The script contains a call to ``export_step``.
3. All imports come from an allowed set (build123d, pathlib, math, typing).

Returns:
A list of human-readable error strings. An empty list means the
Expand All @@ -71,6 +102,18 @@ def validate_script(script_path: Path) -> list[str]:

errors: list[str] = []

# ── Manifest part-name check ───────────────────────────────────────
if manifest_path is not None and manifest_path.exists():
valid_stems = _collect_cadsmith_paths(manifest_path)
if valid_stems and script_path.stem not in valid_stems:
errors.append(
f"Script '{script_path.name}' does not match any part in "
f"component_tree.json. Valid names: "
f"{', '.join(sorted(s + '.py' for s in valid_stems))}. "
"Edit the existing part script instead of creating a new one, "
"or add the part to the manifest first via manufacturing_annotate_tree."
)

# ── Export checks ──────────────────────────────────────────────────
call_names = _collect_call_names(tree)
missing_exports = REQUIRED_EXPORTS - call_names
Expand Down
443 changes: 229 additions & 214 deletions src/rocketsmith/data/gui/main.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/rocketsmith/gui/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/rocketsmith/gui/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rocketsmith-gui",
"private": true,
"version": "0.0.18",
"version": "0.0.19",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
76 changes: 70 additions & 6 deletions src/rocketsmith/gui/web/src/components/ComponentTreeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo, useEffect, useState } from "react";
import { Network, CircleDot, CirclePlus } from "lucide-react";
import { Network, CircleDot, CirclePlus, Box, Umbrella, Wrench, Flame, Cpu } from "lucide-react";
import { fetchJson, getOfflineFilesTree } from "@/lib/server";
import { Badge } from "@/components/ui/badge";
import {
Expand Down Expand Up @@ -27,13 +27,70 @@ const FATE_VARIANT: Record<string, "default" | "neutral"> = {
skip: "neutral",
};

// Category → icon + color
const CATEGORY_ICON: Record<string, React.ReactNode> = {
structural: <Box className="size-3 shrink-0 text-blue-400" />,
recovery: <Umbrella className="size-3 shrink-0 text-green-400" />,
hardware: <Wrench className="size-3 shrink-0 text-orange-400" />,
propulsion: <Flame className="size-3 shrink-0 text-red-400" />,
electronics: <Cpu className="size-3 shrink-0 text-purple-400" />,
};

function fmtMass(q: Qty | null): string {
if (!q) return "";
const [val, unit] = q;
if (unit.includes("kilogram")) return `${(val * 1000).toFixed(1)} g`;
return `${val.toFixed(1)} g`;
}

/** Extract the most useful dimension summary for a component type. */
function fmtDims(type: string, dims: Record<string, unknown>): string {
function mm(key: string): number {
const v = dims[key];
if (Array.isArray(v)) return v[0] as number;
if (typeof v === "number") return v;
return 0;
}
function str(n: number): string { return n > 0 ? n.toFixed(0) : "?"; }

switch (type) {
case "RailButton": {
const od = mm("outer_diameter"); const n = (dims.instance_count as number) ?? 1;
return `×${n} ⌀${str(od)} mm`;
}
case "LaunchLug": {
const od = mm("outer_diameter"); const len = mm("length");
return `⌀${str(od)} × ${str(len)} mm`;
}
case "CenteringRing":
case "BulkHead":
case "EngineBlock": {
const od = mm("od"); const t = mm("thickness");
return `⌀${str(od)} t${str(t)} mm`;
}
case "Parachute": {
const d = mm("diameter");
return d > 0 ? `⌀${str(d)} mm` : "";
}
case "Streamer": {
const len = mm("length"); const w = mm("width");
return len > 0 ? `${str(len)} × ${str(w)} mm` : "";
}
case "ShockCord": {
const len = mm("length");
return len > 0 ? `${str(len)} mm` : "";
}
case "TrapezoidFinSet":
case "EllipticalFinSet":
case "FreeformFinSet": {
const n = (dims.count as number) ?? 0; const span = mm("span");
return n > 0 ? `×${n} ${str(span)} mm span` : "";
}
default:
return "";
}
}

function countComponents(nodes: ComponentNode[]): number {
let count = 0;
for (const n of nodes) {
Expand Down Expand Up @@ -87,6 +144,7 @@ function ComponentRow({
{isLast ? "└─" : "├─"}
</span>
)}
{CATEGORY_ICON[node.category] ?? null}
<Badge
variant={FATE_VARIANT[fate] ?? "neutral"}
className="text-[9px] px-1.5 py-0 uppercase shrink-0"
Expand All @@ -95,11 +153,17 @@ function ComponentRow({
</Badge>
<span className="font-heading truncate">{node.name}</span>
<span className="text-xs text-foreground/40 shrink-0">{node.type}</span>
{node.mass && (
<span className="ml-auto text-xs text-foreground/50 shrink-0">
{fmtMass(node.mass)}
</span>
)}
<span className="ml-auto flex items-center gap-2 shrink-0">
{(() => {
const dimStr = fmtDims(node.type, node.dimensions as Record<string, unknown>);
return dimStr ? (
<span className="text-xs text-foreground/35 font-mono">{dimStr}</span>
) : null;
})()}
{node.mass && (
<span className="text-xs text-foreground/50">{fmtMass(node.mass)}</span>
)}
</span>
</li>
{node.children.map((child, i) => (
<ComponentRow
Expand Down
Loading
Loading