Imported from schneik80/PowerTools (
AGENTS.md). Install upstream withnpx skills add schneik80/PowerTools. Copyright stays with the author.
AGENTS.md — start here
Entry point for AI coding agents (Claude Code loads it through CLAUDE.md).
It is deliberately short: the rules below are the ones this repo has already
paid for once, and every line links to the long form. Read the matching
.claude/rules/*.md before editing the paths it covers, even if your tool
does not load them automatically.
What this is
- PowerTools is a single Autodesk Fusion add-in (Python) consolidating
55 commands (
_cmd(entries incommand_registry.py— count it, do not quote this number) behind one entry point (PowerTools.py), one registry (command_registry.py), one settings store (settings_store.py) and one shared helper package (lib/ptAddInUtils, imported asptutil). - It runs inside Fusion's bundled Python 3.14, has no runtime
dependencies, and
import adskresolves only inside Fusion. Tests stubadskand cover pure logic only. - Fusion exists on macOS and Windows only. A Linux checkout can lint, test, build the release zip and the PDF, but can never run the add-in. Say "not yet exercised in Fusion" when that is true of your change.
Environment and commands
# one-time (no venv is committed; the pin must match .github/workflows/ci.yml)
python3 -m venv .venv && .venv/bin/pip install "ruff==0.15.20" "pytest>=8.0"
# the four CI gates -- run them all before every commit
ruff format . # (CI runs `ruff format --check .`)
ruff check .
.venv/bin/python -m pytest -q # -> "782 passed, 2 skipped" (count grows)
python3 tools/pandoc/build_readme_pdf.py --check # README.pdf built from this README.md?
python3 tools/release/build_release.py --version v0.0.0-test # dry run -> dist/ (git add first)
python3 tools/pandoc/build_readme_pdf.py # after any README.md edit
python3 commands/<cmd>/resources/generate_icons.py # icon sets
Development happens on three devices, so check where you are (hostname)
before trusting any environment note, and name the device in anything you write
down -- "on this machine" is ambiguous and therefore useless:
| Device | Fusion | Role |
|---|---|---|
ADSKMVG91G2F5W -- MacBook Air M4, macOS 26.5.1, arm64 |
Yes -- both channels | macOS dev box; everything below was verified here |
g16win.local -- Windows 11, x86_64 |
Yes -- both channels | Catches the Windows-only path bugs (25d5f48, 93c6b36) |
ryzen-nobara.local -- Nobara 44 (Fedora base), x86_64 |
No -- no native Linux client exists | Lint / test / zip / PDF only -- always "not yet exercised in Fusion" |
Both Fusion devices run production and pre-production, so a device name alone does not identify the build. For anything build-sensitive, **say device
- channel** -- a bug on one and not the other is as often a channel difference as a platform one.
Roster detail and per-device paths:
.agent/environment.md.
On ADSKMVG91G2F5W use the invocations above verbatim: python -m pytest
and python3 -m pytest both fail there with No module named pytest because
pytest exists only in .venv, which in turn has no ruff. Bare ruff is
0.15.20 and matches the CI pin; check ruff --version against ci.yml before
trusting a --check result, and fall back to uvx ruff@<pin> on a mismatch.
The other two devices' toolchains have not been characterised -- verify
rather than assume, and record what you find.
.debug(git-ignored marker in the repo root) turns onptutil.logand the debugpy server. Without it logging is a no-op..claude/settings.jsonis the shared permission allowlist; put personal overrides in.claude/settings.local.json(git-ignored).- Skills:
build-readme-pdf,generate-icons(.claude/skills/). - The
fusionMCP server (localhost:27182) introspects a running Fusion;ConnectionRefusedmeans Fusion is not running, not that the tool is missing.autodesk-product-helpsearches the official API reference — use it instead of recalling API names.
Non-negotiables
Each one has cost a fix already; hashes are git show-able and the long form is
docs/dev/lessons.md.
executenever fires with no document open. A command with noCommandInputsdoes its work incommandCreated(f18b911,11cfc51).- Never loop
adsk.doEvents()inside a handler; nevertime.sleep()on the UI thread. Useptutil.pump_events_for()(ce4e768,f0ff1af). - Re-acquire document/design handles after any pumped wait and check
isValidbefore closing -- stale handles fault natively, not with an exception (a1d22e1,11cfc51). - Custom graphics only in
executePreview; read Custom graphics that stay painted first (b3bed5f). - Read selections with
ptutil.capture_selections()ininputChanged, never fromexecute(a91da41). - Never close a document inside a command event (
11cfc51). - Off the main thread, call only
app.fireCustomEvent; ignore its return value; starting a command from a palette event needsthreading.Timer-> custom event (c440ad3,266e2c2). app.data.activeProjectraises when no project is in context -- usecache_utils.get_active_project(); exceptions inincomingFromHTMLare swallowed silently (7535954).- Fusion IDs use
_, never-. Renaming aCMD_IDorphans user QAT pins; renaming a registry key needssettings_store.RENAMED_COMMANDS(6789216,7dee722). - Never delete a built-in tab/panel; make
start()/stop()idempotent; one tab per control (3b92f3f). - Never hardcode Autodesk paths or unpublished workspace IDs -- probe
candidates and log what resolved (
af05499,fusion_recents.py). lib/ptAddInUtils/__init__.pyimport order is load-bearing, andconfig.pyimports ptutil before definingDEBUG-- no import-time reads of config flags (0b179cd,018f0c7).- Pure logic lives in an
adsk-free module with tests (logic.py,pathgraph.py,catalog.py). A plausible wrong number is worse than an error (c8c0382,b3bed5f). - Cross-platform Python:
.casefold()explicitly (normcaseis Windows-only); OR permission bits;os.accesslies on Windows shares;except Exception:never bare (4cb4901,19ac0f7,14f42ca). - Every command change keeps the contract: registry entry with the exact
doc filename,
CMD_Description(exact casing, ASCII, text from the docs),docs/<Doc>.md+docs/arch/<Doc>.md+docs/arch/index.md+ README row, generated icons pinned intests/test_command_icons.py(aa6802e,e263d4e). ruff format .before every commit -- formatting is a hard CI gate; pure reformat commits go in.git-blame-ignore-revs(ef424c6,ef14b11).README.pdfis rebuilt in the same commit asREADME.md-- CI and a pytest case check a SHA stamp in the PDF against the Markdown (48722db,28188f7).- The release zip is
git ls-filesminus explicit exclusions intools/release/build_release.py; anything newly tracked ships unless excluded there, andtests/test_release_build.pymoves with it (1cb6d3e). - Stdlib only -- in the add-in and in
tools/(Fusion's Python has no pip, no Pillow; CI installs nothing but ruff and pytest). - Never call
args.command.doExecute()insidecommandCreated-- it re-enters the command manager on a half-built command and segfaults Fusion. Bail out withcommands/_command_abort.abort_before_dialog(),consume_abort()inexecute,clear_abort()indestroy; an AST test enforces it (14871d7,a90be46,5bae0e3). The ban is that one callback only --doExecuteis still the right mechanism frominputChangedand from deferred custom events, and three call sites are deliberate. Read the doExecute rule before removing any of them.
Where to look
Full index: docs/dev/codebase-map.md.
| Task | Open |
|---|---|
| Diagnose a symptom ("graphics flash", "nothing happens", segfault) | .agent/symptom-index.md -- start here, it routes |
| Which command to run; Fusion paths; crash dumps; MCP | .agent/environment.md |
| Add / rename a command | command_registry.py, settings_store.py, .claude/rules/commands-registry.md |
| How a command is wired | commands/closealldocuments/ (simple), commands/measurepath/ (dialog + graphics + pathgraph.py), commands/assemblypalette/ (palette) |
| Shared helpers before writing new ones | lib/ptAddInUtils/ index in the codebase map |
| QAT File-menu placement, retries | commands/preferences/entry.py, commands/openrecent/entry.py |
| Timer -> custom event deferral, future polling | commands/assemblypalette/entry.py, commands/teamaddins/entry.py |
| Palette page <-> Python RPC | commands/preferences/entry.py + resources/html/app.js |
| Long save/close runs, crash mitigations | commands/bottomupupdate/entry.py |
| Bailing out of a command before its dialog | commands/_command_abort.py, commands/changecyclecolor/entry.py |
Heavy geometry in an adsk-free solver |
commands/flattensurface/flatten.py, docs/dev/Flatten Surface solver.md |
| Test harness | tests/conftest.py, .claude/rules/tests-ci.md |
| Release, PDF, icons | tools/, .claude/rules/docs-release.md, the two skills |
| Fusion quirks, by symptom | docs/dev/lessons.md, .claude/rules/fusion-api.md |
Working conventions
- This repo does not use pull requests. Never offer or open one. Branch,
commit, merge to
main(--ff-onlywhen it applies), push, then delete the branch locally and onorigin. Do not commit or push unless asked -- but once asked, finish that whole sequence without confirming each step. "commit and sync" means commit andgit pushtomain. Long form:.agent/workflow.md. - Commits:
Area: imperative summary(e.g.Preferences: open the palette from commandCreated, not execute) or a plain imperative line; a prose body that explains why, cites the commits it corrects or builds on, and ends withCloses #Nwhen applicable. ACo-Authored-By:trailer is customary for agent-written commits. - Verify API names against the official Fusion reference, not memory; the commit says which properties were verified.
- Read the DEBUG log / crash report before theorising. Two identical-looking "Preferences needs a document" bugs had different root causes.
- Docs are part of the change, not a follow-up. Descriptions and user text
come from
docs/, not invention. - When the user corrects you on something durable, add it to
docs/dev/lessons.md(and to a rule file if it is path-specific) in the same change.
Documentation map
| Doc | Read it for |
|---|---|
.agent/README.md |
How this guidance is layered, and where a new learning goes |
.agent/symptom-index.md |
Symptom -> cause -> the rule and commit that explain it |
.agent/environment.md |
Verified commands, Fusion paths, API stubs, crash dumps, MCP |
.agent/workflow.md |
Branch/commit/merge/issue conventions (no PRs) |
docs/dev/index.md |
Setup, layout, tooling, .debug, doc map |
docs/dev/codebase-map.md |
Where everything is; command table; ptutil index; stale items |
docs/dev/lessons.md |
The mistakes ledger (long form of the rules above) |
docs/arch/architecture.md |
Lifecycle, shared access points, execution model, config.py |
docs/arch/<Command>.md |
Per-command architecture notes |
docs/dev/debugging.md |
Attaching VS Code / Zed to Fusion (macOS-centric) |
docs/dev/release.md |
What ships, what is stripped, how a release is cut |
docs/dev/Custom graphics that stay painted.md |
Drawing in the viewport |
docs/dev/Insert and position a component from a palette.md |
Starting Fusion commands from a palette |
docs/dev/Flatten Surface solver.md |
The worked example of a pure-Python geometry solver behind a command |
.claude/rules/*.md |
Path-scoped checklists: fusion-api, commands-registry, tests-ci, palettes-html, docs-release |