The canonical instruction file for any agent (human or AI) working in this repo.
CLAUDE.md / AGENTS.md are thin pointers here — edit this file.
protoAgent is a LangGraph-based agent runtime with a FastAPI server, a React
console (apps/web), a plugin system, and an A2A surface. Python is the core;
TypeScript is the console.
- Server:
protoagent serve— orpython -m server, the module form the frozen sidecar uses (neverpython server.py; single-file launch was retired in ADR 0023 and CI fails on it). Theprotoagentcommand (ADR 0075) is the discoverable front door:protoagent --helplists the management subcommands (plugin/workspace/fleet/skills/config) plus lifecycle (up/down/status/serve/setup);protoagent upruns the instance detached andprotoagent statusreports it. Both front doors route through the same dispatcher (server/cli.py::dispatch), sopython -m server <sub>keeps working. Console is served fromapps/web/dist;/healthzis the readiness probe. - Isolated dev instance (don't stomp prod data):
scripts/dev.shruns a sandboxed instance viaPROTOAGENT_INSTANCE=dev(ADR 0065 two-tier paths) on:7871— its whole root is~/.protoagent/dev/(config + every store under it), and it inherits the machine-wide box layer (~/.protoagent/host-config.yaml, gateway/model defaults) so it boots configured with fresh, separate chat/tasks/knowledge. The default instance is~/.protoagent/default/on:7870, untouched.scripts/dev-reset.shwipes just the sandbox. Use this for feature testing instead of the default instance. - Spinning up a throwaway test server while the user's real instance(s) run
(e.g. an agent booting a PR build for review): FULLY isolate it — own box root
too, not just an instance id. Plain
dev.shshares the box root (~/.protoagent), which is data-safe but trips the desktop's co-residence warning (#1552) and can collide on box-level resources (mDNS advertise, scheduler owner-lock). Instead:PROTOAGENT_BOX_ROOT=/tmp/pa-<name> PROTOAGENT_INSTANCE=<name> python -m server --port <free>— nothing under~/.protoagentis shared or touched. Tradeoff: a fresh box root does not inherit box config (host-config.yamlgateway/model defaults), so seed a gateway in that instance if the test needs model-backed features; pure-console/UI review works as-is. (Serving a worktree's ownapps/web/dist:cd <worktree> && … python -m server—_bundle_root()anchors to the loadedserver/package, so it serves that checkout's build.) - Factory-reset the default instance:
scripts/reset.shwipes the prod instance back to a clean slate (next boot runs the setup wizard) — for testing the fresh-user flow via CLI (there is no in-app reset). Always--dry-runfirst to read the plan. It's safe on a multi-instance machine: every other instance (~/.protoagent/<name>, the dev sandbox, fleet members) and the machine-wide box layer (host-config.yaml,commons/) are preserved. Flags:--yes,--backup,--keep-secrets(keep gateway creds),--include-dev,--force(stop a bound server first). (Reset-script rewrite for the ADR-0065 single-subtree layout is a follow-up; see the env-vars gotcha.) - See where state lives:
protoagent config explain(orpython -m server config explain, orGET /api/config/explain) prints this instance's id, both roots (box + instance), every resolved path, and the per-field settings cascade with provenance (secrets redacted) — the way to answer "where is my config / where did my key go". - Python deps: managed with
uv(pyproject.toml [project.dependencies]is the source of truth;uv.lockis tracked).uv syncto install. - Console deps:
npm ciat the repo root (npm workspaces; the web app is@protoagent/web). Changing/bumping a dependency requires npm ≥ 11 (npm install -g npm@11) — see the npm-10 no-op gotcha below. - Console dev loop (frontend):
npm run dev(HMR) /npm run preview(built dist) serve the console on:5173and proxy all backend calls (/api,/a2a, events,/agents,/plugins,/_ds) toPROTOAGENT_API_BASE, defaulthttp://127.0.0.1:7871— the ISOLATED dev instance fromscripts/dev.sh, not the default/prod:7870the desktop app runs. So the correct loop isscripts/dev.sh(backend, :7871) +npm run dev(frontend) — both isolated, so dev testing never touches your~/.protoagentdata. Vite prints a loud red guard if you ever pointPROTOAGENT_API_BASEat:7870. (Historically it defaulted to:7870, which silently crossed dev traffic into the prod/desktop instance.)
Run the same commands CI runs (.github/workflows/checks.yml) — locally,
before the PR, not after. CI is the merge gate; a red PR is wasted cycles.
| Gate | Command |
|---|---|
| Lint | ruff check . (pinned ruff==0.15.10) |
| Import contracts | lint-imports (pinned import-linter==2.11) |
| Attribution in sync | python scripts/gen_attribution.py --check (regenerate with uv sync && uv run python scripts/gen_attribution.py after a dep bump) |
| Python tests | python -m pytest tests/ -q |
| Lean-image smoke | python scripts/live_smoke.py |
| Web unit | npm run test:unit --workspace @protoagent/web |
| Web e2e | npm run test:e2e --workspace @protoagent/web (Playwright/chromium) |
If a change is genuinely test-free (docs, config, pure refactor), say so explicitly in the PR description — but that is the exception, not the default.
Issues are gated too — but only flagged, never blocked. The silent
issue-gate workflow (.github/workflows/issue-gate.yml) labels any issue
missing the required structure with needs-info (no comment) and removes it
once you edit the issue to conform. Use the Bug / Enhancement issue forms
— their required fields match the gate; a free-form issue needs at least a
Problem / What's-wrong section, plus repro + evidence (bugs) or a
proposed-direction / acceptance (enhancements). Intentional free-form → add the
gate-exempt label. Full checklist: CONTRIBUTING.md.
These are the failures that actually recur — read them before you edit.
-
Instance paths are two-tier (box / instance) — one rule, resolve once (ADR 0065). Every on-disk location comes from
infra.paths.instance_paths()(a frozenInstancePaths): the box tier (box_root=~/.protoagentor/sandbox) holds machine-shared state (host-config.yaml,commons/, heartbeats); the instance tier (instance_root) holds this agent's config + every store.instance_root = PROTOAGENT_HOME | box_root/PROTOAGENT_INSTANCE | box_root/default. Don't compute store paths by hand or reach for the deletedscope_leaf/PROTOAGENT_CONFIG_DIR(both retired — desktop/Docker/fleet now setPROTOAGENT_HOME); add a per-store accessor or useinstance_paths().store("<name>"). Identity comes from env only — never config-file content.config explainprints the resolved layout. -
npm 10 silently no-ops workspace dependency bumps — use npm ≥ 11. With a dep resolved under
apps/web/node_modules/(e.g.@protolabsai/ui, nested because its pinned@protolabsai/designconflicts with the hoisted one), npm 10's arborist keeps the old version through every supported command — rootnpm installafter a range bump,npm install <pkg>@<v> -w @protoagent/web,npm update <pkg> -w— even when the locked version no longer satisfies the manifest range. No error, nothing changes (repro'd three ways, 2026-07-12). npm 11 (npm install -g npm@11) resolves the same bump correctly with a plain rootnpm install. The two arborists also disagree about peer-stub reachability, so npm 10'scirejects an npm-11-generated lockfile ("Missing: @types/react@… from lock file") — which is why every CI job touching the root lockfile pinsnpm install -g npm@11beforenpm ci(the Dockerfile web-builder stage too — node:20-slim ships npm 10; missing it broke every GHCR publish + the v0.101.0 release build until fixed) and the checks/desktop-build/marketing-deploy/docs workflows). Keep new workflows consistent. After any dep bump, regenerateTHIRD_PARTY_LICENSES.md(uv run python scripts/gen_attribution.py) or the attribution gate fails. -
No unused variables. ruff selects
F(pyflakes);F841(assigned-but- unused) fails CI andruff check --fixdoes not auto-fix it. Don't leave dead locals in code or tests. (Style rulesE402/E501/E702/E731/E741are intentionally ignored — lazy/late imports and 120-col comment lines are idiomatic here. Config:pyproject.toml [tool.ruff].) -
Config dataclass ↔ golden field map. Adding or removing a field on the graph config dataclass (
graph/config.py) requires updating the golden field map intests/test_config_roundtrip.py, or the test fails with "golden field map is out of sync with the dataclass fields." Wire the field in all three places: the dataclass default, thefrom_dictparser, and the golden test. -
Import layering (enforced by
lint-imports).graph/and the infra packages (a2a_impl/ observability/ security/ infra/ tools/ knowledge/ events/ scheduler/ runtime/ ops/) must never importserver/oroperator_api/;operator_api/must never importserver/. (ops/is the ADR 0075 D2 shared-operation layer — one op wrapping a core, called by the CLI, REST, and MCP adapters; being neutral is what lets all three import it.) Theignore_importslists inpyproject.toml [tool.importlinter]are a burndown list of grandfathered violations — remove entries, never add to them. import-linter sees function-level (lazy) imports too, so you can't hide one inside a function. -
Module names. It's
a2a_impl/(NOTa2a/— that shadows the A2A SDK). Metrics live inobservability/→from observability import metrics. Security helpers insecurity/, box/runtime infra ininfra/. (Root-module reorg: ADR around #896.) -
Tool / state injection.
current_session_id()is empty inside tool bodies (only middleware sees it). Read per-turn state viaInjectedState(ProtoAgentState) — don't monkeypatch the resolver in tests (false confidence). -
CSS comments. Never put
*/inside a CSS comment — it breaks the minifier and silently corrupts the build. Guarded byapps/web/scripts/check-css-comments.mjs(prebuild gate). -
DS AppShell width is controlled. Store rail widths verbatim; never re-clamp them (re-clamping breaks drag-to-collapse).
- Match the surrounding code — naming, comment density, and idioms. New code should read like the file it lives in.
- Tests go in
tests/(pytest +pytest-asyncio); the console's inapps/web/src/**/*.test.ts(x)(vitest) andapps/web/e2e(Playwright). - Architecture decisions are MADR ADRs in
docs/adr/NNNN-*.md; dev notes indocs/dev/. Check the relevant ADR before changing a subsystem's contract. - Don't commit secrets. A gitleaks gate runs in CI (
secret-scan.yml). - Don't re-commit local churn —
config/plugins/*installs andplugins.lockworking-tree changes are expected dev-local state.