Files
windmill/AGENTS.md
02e4697157 feat: let operators compose full-code apps when the workspace grants it (#11229)
* feat: let operators compose full-code apps when the workspace grants it

Adds operator_settings.builder_apps, the second builder right, granted
independently of flows because the two are not equally verifiable: a composed
flow is checked in full server-side, while a full-code app is a browser-built
bundle no check can read. The app right rests instead on forced sandbox
isolation, a refusal of Viewer execution mode, and a deploy-time check of every
runnable the app can reach.

BuilderKind arrives with the second kind and is a mandatory argument, so no gate
can mean "either" by accident: one that did would authorize an app write on a
flows-only workspace. The only surfaces reading .any() are the ones that are
genuinely not per-kind, the seat a membership costs and listing drafts.

validate_operator_composed_app checks two surfaces rather than one, because they
are not the same list: the policy's triggerables and the runnableByPath entries
in the app value, which is what the deployed bundle actually resolves against.
Every colon in a triggerable key is a possible split, since execute_component
looks up "<component>:<path>" with an unrestricted component string.

create_app_raw_source stays closed: it compiles caller-supplied sources with a
bundler job on a worker, which is arbitrary code execution. Builders lose
nothing, since the browser and the CLI both bundle locally and deploy through
the multipart endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4

* fix: list a builder's app drafts and refuse inline code in them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 6d15ff957df4dcd5129e1ab217eab1a13af62fbb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check a builder app as it will be stored

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: show the home list edit button to builder operators

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: pause the raw app preview for an app an operator deployed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let app builders save a full-code app's deploy settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record the builder app settings path, public apps and the editor preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: cache the merged EE seat usage query

The EE query now reads audit_partitioned and skips service accounts, so the
cached entry for its builder_apps predicate no longer matched and offline EE
builds failed with no cached data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: pause the raw app preview for loaded drafts and unknown authors

A draft loaded from an operator ran in the viewer's preview because the pause
only checked the app's last deployer, and an author no longer in the workspace
read as a developer. The pause now checks a loaded draft's owner, records it
in the viewer's draft until the preview is run or deployed, and holds when the
author cannot be found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: name guest apps among the modes a builder may deploy

Guest runs as the publisher with no Viewer fallback, like Publisher and
Anonymous, and RestrictGuestAppDeployment refuses it per workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep a loaded draft's preview paused in AI sessions and chat edits

The session draft codec and the chat's draft save dropped loaded_from, so
opening a paused operator draft in an AI session, or editing it through the
chat, ran it with the viewer's session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: pin builder refusals for low-code apps and unreviewed previews

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the operator mark when loading a developer's copy of a draft

Loading another user's draft recorded only who saved it, so a developer's
copy of an operator draft they never previewed opened unpaused for the next
person who loaded it, running the operator's code with their session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let builders restore a full-code app from deployment history

Restore always sends allow_kind_change, which the builder check refused even
for a full-code version restored over a full-code app. Builders now drop the
flag, so the locked kind check still refuses an actual conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: rustfmt the builder checks in jobs.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record the builder kind-check race as an accepted risk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: never trust the owner name a loaded draft carries

A loaded draft's loaded_from is written by its owner, so an operator could
name a developer, or leave it empty, and the next person to load the draft
previewed it unpaused. A loaded draft that carries any mark now stays
paused as unattributable; an unmarked one is still checked against its
real owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record viewer-supplied resources in builder apps as an accepted risk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: say that the app right lets operators write frontend code

The settings section and the docs told admins builder operators "still
cannot write code", but the app right has them write the app's frontend
bundle. Only backend code stays out of reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: state the raw-source closure once and drop an unreachable preview case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf: look up only the author when deciding whether to pause the preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: pin that the app right alone bills operators as developers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: point ee-repo-ref at the EE seat test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: drop the editor preview pause now that sandboxed apps preview isolated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse a builder operator's app draft without the sandbox

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: turn the sandbox on when a builder operator opens the app editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse builder app drafts naming runnables they cannot read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note the editor preview asks before minting a builder app token

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: trim the builder app check comments to the constraints

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse an app runnable whose runType is not script, flow or hub

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check builder app draft policy keys like a deploy does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: count main's half-seat service accounts in the builder billing test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: name the builder policy check for drafts as well as deploys

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let a builder app draft autosave with a stale triggerables_v2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check the script version a builder app would run, not any version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse viewer mode in a builder app draft as on deploy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: hold builder app drafts and deploys to one check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: say a builder's script may not have finished deploying

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: cap builder app triggerable keys before reading every colon split

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse a builder app draft that runs as someone else

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: conform builder app drafts saved by the AI chat as the editor does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record that a deployed builder draft runs as its deployer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse builder app runnables not written as plain text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: a builder draft may carry its builder's own run identity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: offer builders Start with AI only when it opens a session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name full-code apps in the builder rights confirmation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name only the granted builder rights in the confirmation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note a wm_deployers builder may keep another run identity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: give builders a starter app that calls no missing runnables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: seed the builder starter under the picker for builders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: qualify the builder run identity comment for wm_deployers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: resolve builder app script hashes before opening the RLS transaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: run the builder app tests serially to share the rights cache safely

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: give builders the builder starter on a fresh editor load

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop a new-app load overtaken while waiting for the workspace list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep main's 401 for operators previewing without the app right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 5d7726c4979070ee8db4fb9e8014152872a641ba

This commit updates the EE repository reference after PR #816 was merged in windmill-ee-private.

Previous ee-repo-ref: 7151060bfc02908e97d9344b3820ee33a4951322

New ee-repo-ref: 5d7726c4979070ee8db4fb9e8014152872a641ba

Automated by sync-ee-ref workflow.

* refactor: let the app row's edit button trust its write flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: point ee-repo-ref at the merged EE companion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note the session raw-app editor does not conform builder drafts yet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-09 19:31:36 +02:00

224 lines
20 KiB
Markdown

# Windmill
Open-source platform for internal tools, workflows, API integrations, background jobs, and UIs. Rust backend + Svelte 5 frontend.
## Workflow
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context.
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages.
For a new user-facing feature, put the `feature_usage` telemetry in the plan as a proposed item
(see `docs/feature-telemetry.md`) so the user can keep or drop it — don't ask separately, and
don't instrument bugfixes or refactors.
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`, then
**exercise the change on the running instance**. Type-checks are not verification. Whatever the
change touches, get that path actually running, and stand up whatever that takes — this is
expected, not a last resort. A few examples, not a closed list: drive the UI with the Playwright
MCP, run a real job of the kind you touched, restart the backend with the cargo features the
path needs (`backend/AGENTS.md`), put a stub in front of an upstream, start MinIO for an S3
path, plant state with SQL, exercise it through the `wmill` CLI. If the path you need has no
obvious way in, invent one rather than skipping it; `docs/` carries recipes for several areas.
If it needs a credential or a third-party account, ask for one rather than skipping the test or
inventing a value. If you genuinely cannot exercise it, say which path went unexercised instead
of implying it was verified.
## Documentation
- **Validation**: `docs/validation.md` — what checks to run based on what you changed
- **Unreleased SDK changes**: `docs/wac-sdk-e2e.md` — exercising a client change on a real worker
- **Agent workers**: `docs/agent-worker-e2e.md` — building and running one locally. An agent
reaches the DB only through the API, so `Connection::Http` paths are never taken by a plain
`cargo run`; a normal build cannot start one at all.
- **External instance data tables**: `docs/external-instance-datatables.md` — the cluster Windmill
administers behind `external_instance` data tables and Ducklake catalogs: its invariants (one
lifecycle lock, managed-object markers, the setup gate, per-cluster roles, fork copy ownership)
and how to run one locally
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Operator write rights**: `docs/operator-write-rights.md` — which `operator_settings` flags are
enforced rather than cosmetic, and why a right that is granted-unless-withdrawn needs `Option`
fields and a jsonb merge rather than a serde default
- **Operator builder rights**: `docs/operator-builder-rights.md` — the workspace settings that let
operators compose flows and full-code apps, what the composition check must cover, which
raw-app endpoint stays closed on purpose, and why either right costs a full seat
- **Auth surface**: `docs/auth-surface.md` — credential precedence, session/cache invalidation
scope, which token labels email their owner at expiry, how OAuth login matches `login_type`, and
that every superadmin route refuses `$WM_TOKEN`. Read before designing anything that creates
users, tokens or sessions.
- **Product telemetry**: `docs/feature-telemetry.md` — when to instrument a new feature with
`feature_usage`, and the four-step recipe. An unregistered `(feature, kind)` pair is dropped
silently, so frontend-only instrumentation records nothing.
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
- **Frontend UUIDs**: do not call `crypto.randomUUID()` in frontend code. Import `randomUUID` from `$lib/utils/uuid` instead.
- **Code review**: review the current PR or branch against the shared review policy in `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test-coverage assessment). The skill at `.agents/skills/local-review/SKILL.md` orchestrates it. All three CLIs auto-discover the same SKILL — Claude reads `.claude/skills/` (symlinked to the canonical `.agents/skills/` file), Codex and Pi read `.agents/skills/` directly. Invoke with `/local-review` in Claude Code, `$local-review` (or `/skills` selector) in Codex, or `pi --skill local-review` / `/skill:local-review` in Pi. For a Codex-driven pass that mirrors the `codex-pr-review` GitHub action against your unpushed work (committed + uncommitted) before you push, use `/local-review-codex` (`.agents/skills/local-review-codex/`) — same `REVIEW.md` policy, `gpt-6.1-sol` model and `xhigh` reasoning as the action; requires the `codex` CLI >= 0.159.3.
- **Domain guides**: `.claude/skills/native-trigger/`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
- **Domain vocabulary**: `CONTEXT.md` — the words this codebase uses for its own concepts (step, step setting, trigger step, …). Name things the way it does.
- **CLI commands**: when adding/modifying/removing a command, subcommand, option, or description in `cli/src/commands/`, run `python system_prompts/generate.py` to refresh `system_prompts/auto-generated/` and `cli/src/guidance/skills.gen.ts`. The CLI docs the agents use to operate `wmill` are derived from the source — stale generated files give agents the wrong flags.
- **AI guidance** (chat prompts and CLI skills): what the AI knows about Windmill itself — how flows, scripts, apps, pipelines and resources work — is written once, in `system_prompts/base/` and `system_prompts/languages/`, and `system_prompts/generate.py` builds both the chat's `$system_prompts` exports and the CLI's skills from it. Add or change such guidance there, never inline in one consumer: a sentence only one side should see goes in a `<!-- cli-only -->` / `<!-- chat-only -->` block, and a new file reaches both through the `TOPICS` table. The chat prompt builders under `frontend/src/lib/components/copilot/chat/` add only tool plumbing and runtime values, and shared markdown names no chat tool, since it reaches every chat mode. Rerun `generate.py` after editing; `system_prompts/README.md` has the details.
- **Session recorder**: `frontend/src/lib/components/recording/` is also the recorder `wmill app dev --recording` serves, vendored into the CLI as `cli/src/commands/app/devRecorderBundle.gen.ts`. After changing `rawAppSnapshot.ts` or `rawAppRecording.svelte.ts`, run `bun run gen:dev-recorder` from `cli/` (`cli/test/dev_recorder_bundle_unit.test.ts` fails otherwise).
- **Raw-app policy**: `frontend/src/lib/components/raw_apps/rawAppPolicy.ts` also derives the policy the server's raw-app deploy stores, vendored into the bundle job as `backend/windmill-api/src/apps_raw_policy.gen.js`. After changing it or anything it imports, run `bun run gen:app-policy` from `cli/` (`cli/test/app_policy_bundle_unit.test.ts` fails otherwise). It rides in the job rather than being read from the CLI the job runs because the images install `windmill-cli` unpinned, so an image can carry one older than its server.
## Dev Environment
> **In a git worktree, the ports and database below are NOT the ones to use.** Each
> worktree gets its own backend port, frontend port and Postgres database, so the
> defaults in this section apply only to a plain single checkout. **Discover the real
> values before running anything** — see "Per-worktree ports and database" below.
**Check whether they are already running before starting anything.** In a managed worktree the
backend and frontend are already up in sibling panes — use those, don't spawn your own. A second
server started in your own shell fights the first one for the port.
**herdr** is the worktree manager; `$HERDR_ENV=1` marks one of its panes. `herdr pane list
--workspace "$HERDR_WORKSPACE_ID"` lists this worktree's panes — match on `cwd`, since the servers
run from `backend/` and `frontend/` — and `herdr pane read <pane_id> --source recent-unwrapped
--lines 50` reads one's log; `herdr --skill` prints the full reference for
inspecting and driving panes and agents. The plugins that provision these worktrees live in the
private `windmill-labs/windmill-herdr` — clone it and run `./setup.sh` to install them and the
keybindings they need.
A worktree from the older **webmux** setup sets `$WEBMUX_WORKTREE_PATH` instead and is driven
through tmux: `tmux list-panes -t "$(tmux display-message -p -t "$TMUX_PANE" '#{window_id}')" -F
'#{pane_index} #{pane_current_command}'` for what is running, `tmux capture-pane` for a pane's log.
See `backend/AGENTS.md` to restart the backend with different cargo features. The commands below
are for a plain checkout with nothing running.
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+)
- **DB**: `psql postgres://postgres:changeme@localhost:5432/windmill`
- **Login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings`
- **Migrations**: use `cargo sqlx migrate add -r <name>` from `backend/` to create new migrations (never generate timestamps manually)
- **Resource types ship through the hub, not migrations.** A new type, a provider's credential
included, is a `hub/<app>/<app>.resource_type.json` in `windmill-labs/windmill-integrations`
and reaches instances by hub sync; a product name its identifier misses (`typesafe` →
"TypeSafe") is that file's `display_name`, not a word in `resourceTypeDisplay.ts`'s map. Only a
type Windmill itself defines and reads as a product object (`ai_skill`, `ai_instruction`) is
also seeded by a migration, so the feature works before a sync. For local testing, insert the row.
### Per-worktree ports and database
**`.env.local` in the worktree root holds the real values** — `BACKEND_PORT`, `FRONTEND_PORT`,
`REMOTE`, `DATABASE_URL`, `CARGO_FEATURES`, `WM_DB_NAME`. Every manager writes those fields, so it
is correct whichever one you are in. Read it with `cat .env.local` from a shell: the file-read
tool denies every `.env.*` path, and `DATABASE_URL` is written with an `export` prefix that a
`^DATABASE_URL=` grep misses.
herdr keeps nothing besides that file; its hooks write it and read it back. A webmux worktree
additionally has `$(git rev-parse --git-dir)/webmux/runtime.env`, which every pane sources at
startup and which carries the extras `.env.local` lacks: `WEBMUX_*`, `WM_CLONE_DB`,
`USE_RUST_PLUGIN`.
In a plain checkout, fall back to `.env` / `.env.local` (repo root) and `backend/.env`.
Each worktree gets a **brand-new database**, created and migrated from scratch by the post-create
hook. It is not a copy of the main dev instance: you get the `admins` workspace, the
`admin@windmill.dev` superadmin, the license key copied from the base database, and whatever the
migrations seed — and none of your own workspaces, scripts, flows or apps. Create whatever a test
needs. Cloning the base `windmill` database instead is
opt-in via `WM_CLONE_DB` (the `windmill.worktree` plugin's `config.env` under herdr, or
`.webmux.yaml` under webmux); read the note in `.webmux.yaml` before turning it on.
The database is named after the **worktree directory, not the branch** (`scripts/worktree-common.sh`):
`windmill_` + the directory basename with `-` → `_`, which Postgres then truncates at 63
characters. herdr creates the directory under `~/.herdr/worktrees/<repo>/` from the branch name
with each `/` turned into `-`, so branch
`hugo/win-2544-add-options-field-to-the-postgresql-resource-type` gets the database
`windmill_hugo_win_2544_add_options_field_to_the_postgresql_reso` — the whole branch name, cut
mid-word at the limit. The two drift apart as soon as the branch is renamed, and webmux named its
directories after the ticket alone, so reconstructing the name from the branch you are on is
wrong in both. Take `WM_DB_NAME` from `.env.local` instead. Read that, or discover from what is
already running:
```bash
# match on the worktree directory, truncated the way Postgres truncates it (63 - len('windmill_')):
psql postgres://postgres:changeme@localhost:5432/postgres -tAc \
"select datname from pg_database where datname like 'windmill%'" \
| grep -F "$(basename "$(git rev-parse --show-toplevel)" | tr '/-' '_' | cut -c1-54)"
# the port the frontend actually proxies to (REMOTE of this worktree's vite):
for p in $(pgrep -f vite); do case "$(readlink /proc/$p/cwd)" in *"$(basename "$(git rev-parse --show-toplevel)")"*)
tr '\0' '\n' < /proc/$p/environ | grep -E '^REMOTE=|^PORT=';; esac; done
```
Getting these wrong is not a cheap mistake:
- **`DATABASE_URL` pointed at another worktree's database silently destroys the sqlx
cache.** `cargo run` and `cargo sqlx prepare` both compile `sqlx::query!` against the
**live** database, so the wrong one fails with `relation "<your_new_table>" does not
exist` — and `prepare` deletes the whole `.sqlx/` directory *before* it fails, leaving
it gutted. Always `cp -r backend/.sqlx <tmp>/sqlx_backup` first (see the `update-sqlx`
skill).
- **The frontend proxies to its own worktree's backend port, not 8000.** Starting a
backend on the wrong port leaves the UI up but every API call 502s, which reads like an
application bug rather than a misconfiguration.
- **Kill backends by pid scoped to this worktree's cwd** (`readlink /proc/<pid>/cwd`),
never `pkill -f target/debug/windmill` — that kills every sibling worktree's backend.
Beware that a `pgrep -f "<pattern>"` in a shell whose own command line contains
`<pattern>` matches the shell itself.
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**MUST use `outline` before `Read`** on unfamiliar files — a 500-line file costs ~500 lines of context, while `outline` costs ~20. Then **MUST use `body "X"`** instead of reading a full file to see one function/struct. Use `Read` with offset/limit only when you need surrounding context that `body` doesn't capture.
- `refs "X" --caller` instead of reading files to find which function contains each reference
- `callers "X"` / `callees "X"` for call-graph questions
EE files (`*_ee.rs`, `*_ee.ts`, `*_ee.svelte`) are indexed — you can `outline`, `def`, `body`, `refs` etc. on them just like regular files.
```bash
NAV="sh wm-ts-nav/nav"
# Use --root backend for Rust, --root frontend/src for TS/Svelte
$NAV --root backend outline backend/path/to/file.rs # file structure
$NAV --root backend def "ServiceName" # find definition
$NAV --root backend body "decrypt_oauth_data" # extract source code
$NAV --root backend search "%" --parent ServiceName # methods on a type
$NAV --root backend search "Trigger" --kind struct # find by kind
$NAV --root backend refs "X" --file handler.rs --caller # scoped refs with caller
$NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference. Use **Grep** instead when completeness matters (finding all usages, exhaustiveness checks):
- `refs`/`callers`/`callees` can't follow re-exports, glob imports, or different import paths to the same symbol
- Trait impls, macro-generated symbols (`sqlx::FromRow`), and namespace member access (`ns.X`) are invisible
- `callees` shows all identifiers in a function body, not just actual calls
## Core Principles
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- **Scratch stays outside the checkout.** Temp scripts, data dumps, cache backups and
screenshots go in the session scratch directory or `/tmp`, so nothing temporary can end up
committed. Write the paths in `rm`/`mv`/`cp` out literally: a PreToolUse hook proves each
operand, and auto-allows deletes, moves, copies and mode changes under `/tmp`, inside a git
checkout under `$HOME`, or in the Playwright MCP browser caches (`~/Library/Caches/ms-playwright`
and `ms-playwright-mcp`, `~/.cache/…` on Linux), as long as one operation stays within a single
root — a sibling checkout is a root of its own (`tar` and `unzip` stay `/tmp`-only). Chain
deletes freely, each proved on its own operands, but keep writes to one per line, name the
destination rather than a directory to drop it in, and put anything else on its own line: a
command the hook does not prove drops the whole line back to the normal permission flow. A
leading `~/` or `$HOME/` is expanded and proved; a quoted operand, any other `$VAR`, a redirect,
a `$(…)`, a relative `cd`, or a wrapper like `xargs rm` cannot be, and that deferral is what
turns a cleanup into a prompt.
- **Change files with Edit/Write, not the shell.** `sed -i`, `cat > file <<'EOF'` and inline
`python3 - <<'PY'` scripts put an edit through the PreToolUse guards and the permission
classifier, which match `Bash` and nothing else, so a routine edit arrives as a prompt. Bash
stays right for running things — tests, builds, git, one-off queries.
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked
- **A simpler design found late is still the design.** Work already spent is not an argument
for a shape, and neither is a clean review round, a passing suite, or a long PR thread. The
signal to stop and re-derive rather than patch again is a change that keeps growing to defend
its own structure: each review finding fixing an assumption the previous fix broke, the same
class of bug reappearing somewhere new, or most of the diff being consequences of one early
choice rather than the thing you set out to do. When that happens, say plainly what the
simpler design is and what switching costs — a migration, a review cycle restarted from zero,
work discarded — and let the user decide. Do not keep paying down the harder one because it
is nearly finished, and do not present the accumulated cost as a reason to continue.
- **Ship only the tests the PR needs.** A committed test must pin behavior a future change could plausibly break, and be the smallest setup that exercises the new logic. While developing, write as many exhaustive tests and do as much manual testing as you need to convince yourself the change works — then remove that scaffolding before marking the PR ready, keeping only the essential regression guard(s). A test that merely re-exercises pre-existing behavior, or needs elaborate fixtures to assert something trivial, is scaffolding: delete it. If nothing meaningful is left to guard, ship no test rather than a ceremonial one.
- **Comments record constraints, not narration.** Write a comment only for what the code can't show: why a non-obvious approach is required, what breaks if it's "simplified" away. State each invariant once, at the place where someone would break it, in ≤4 lines. Don't describe what the next line does, don't repeat the same rationale at multiple sites, and don't address the PR reviewer (justifying a change belongs in the PR description, not the code). Reference nothing ephemeral — no numbered steps from your dev flow, no "the poller / the test does X" scaffolding, no transient state that won't exist for the next reader; keep only the essential, durable rationale. Describe the code as it is, never its drafting history: "we no longer do X", "unchanged behavior", "instead of the previous approach" are meaningless to a reader who never saw the earlier iteration — before finishing, reread your comments as if the current state is the only state that ever existed.
- **Never attribute work to a specific customer, account, or "requested by a customer" in repo-tracked content** (PR descriptions, commit messages, code comments, docs). Describe changes by their technical motivation instead.