[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6QNEFS4B42P8E3YDKXZPFRN":3,"skill-same:01M6QNEFS4B42P8E3YDKXZPFRN":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":46,"repositoryHtmlUrl":47,"repositoryHomepageUrl":48,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":50,"updatedAt":51,"install":52,"resources":65},"01M6QNEFS4B42P8E3YDKXZPFRN","nodehammer",null,"Drive the nodehammer CLI and author detector-specific TOML\u002FLua scene configs for geometry selection, materials, tessellation, and export. Convert and inspect HEP geometry (DD4hep XML, ROOT TGeo, .nhb), validate configs, package .nhproj archives, and render the viewer. Use for nodehammer, its config format, nhb, nhr, .nhproj, nodehammer.toml, detector geometry conversion, wedge cuts, NH#### diagnostics, or `uvx nodehammer`.","paulgessinger\u002Fnodehammer","paulgessinger","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1058585?v=4","01M6QNE90XM7K4NR7JM199P4M0","Paul Gessinger","01M6QNE956VADZ9K4A2RPFMD39",2,"MIT","https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Fskills\u002Fnodehammer\u002FSKILL.md",[],[5,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"geometry","author","viewer","materials","for","detector","inspect","render","toml","cli","selection","conversion","use","format","convert","lua","package","config","validate","2026-11-10 10:12:30",false,"---\nname: nodehammer\ndescription: Drive the nodehammer CLI and author detector-specific TOML\u002FLua scene configs for geometry selection, materials, tessellation, and export. Convert and inspect HEP geometry (DD4hep XML, ROOT TGeo, .nhb), validate configs, package .nhproj archives, and render the viewer. Use for nodehammer, its config format, nhb, nhr, .nhproj, nodehammer.toml, detector geometry conversion, wedge cuts, NH#### diagnostics, or `uvx nodehammer`.\n---\n\n# nodehammer\n\nA pipeline that imports detector geometry, applies a config that selects and\nstyles parts of it, and writes it out — as a mesh (glTF\u002FOBJ), as an intermediate\nscene, as a JSON report, or into an interactive viewer.\n\nSame program either way: the compiled `nodehammer` executable and the\n`nodehammer` command from the Python wheel are one implementation (`cli::run`\ninside `libnodehammer`), so every command here behaves identically under\n`uvx nodehammer`. Differences that do exist are noted as such.\n\n## The output contract — read this before parsing anything\n\n**stdout is the answer. stderr is everything about producing it.**\n\nstdout carries the thing you would substitute into the next command: the\nflattened document, the JSON report, the path to the archive that was written,\nthe URL being served. stderr carries diagnostics, progress and summaries.\n\n```bash\narchive=$(nodehammer project pack -c scene.toml -i det.root -o det.nhproj)\n```\n\nThat yields the archive path, not a report about one. Two consequences:\n\n- **Never parse stderr.** It is prose for a human and is not a stable format.\n- **`-q` silences narration only.** Diagnostics, and the line a failing command\n  prints before it exits non-zero, are never affected. A flag that hid errors\n  would be a trap. `-v` restores narration; `-q`\u002F`-v` may be typed before or\n  after the subcommand.\n\nWhen calling `nodehammer` from a script, quiet is already the default for the\nlibrary entry point; the executable defaults to narrating because a person typed\nit.\n\n## Commands\n\nFive groups. Every one takes `-h`.\n\n| | |\n|---|---|\n| `convert` | import → config → write. The **output extension decides how far the pipeline runs** |\n| `inspect summary\\|tree\\|tags` | report on a geometry file; `--output-format json` |\n| `config validate\\|flatten` | check a config, or resolve it to one self-contained TOML |\n| `project pack\\|publish\\|info` | build, publish and read `.nhproj` archives |\n| `viewer open\\|serve\\|shot\\|bench` | the 3D viewer, in four postures |\n\nOption spellings are consistent across the tree: `-i\u002F--input`, `-c\u002F--config`,\n`-o\u002F--output`, `--input-format`, `--output-format`, `--color`, `--depth\u002F-d`,\n`--filter\u002F-f` mean the same thing everywhere they appear.\n\n### Commands that block — do not run these unprompted\n\n| Command | What it does | Use instead |\n|---|---|---|\n| `viewer open` | opens a **native window and blocks** until the user closes it | `viewer shot -o out.png` |\n| `viewer` (bare, with a path) | same — `open` is the default mode | `viewer shot` |\n| `viewer serve` | starts an HTTP server, **opens a browser, and blocks** | `viewer serve --no-browser` in a background job, or `project publish` for static output |\n| `viewer bench` | runs a GPU benchmark; needs a display | only when the user asked for numbers |\n\nTo *see* what a scene looks like, `nodehammer viewer shot -i det.nhb -c\nscene.toml -o shot.png --shot-width 1920` renders one PNG once the scene settles\nand quits. That is the agent-appropriate way to check a geometry visually. It\nstill needs a working GPU context, so it will fail on a headless machine with no\ndisplay.\n\n`shot` and `bench` render from **`-i` geometry plus an optional `-c` config**, not\nfrom a project: they reject a call without `--input` (`NH0900 … needs --input`)\neven when a `.nhproj` positional is given. To screenshot an archive, pass the\nconfig and geometry it was packed from.\n\nEverything else in the tree runs to completion and exits.\n\n### convert\n\n```bash\nnodehammer convert -i det.root -c scene.toml -o det.glb\nnodehammer convert -i det.root -o scene.nhb          # stop at the semantic scene\nnodehammer convert -i det.root -o a.glb -o b.obj     # several outputs, one import\n```\n\nThe output extension picks the writer *and* the depth of the pipeline:\n\n- `.nhb`, `.json` — the **semantic** scene. No tessellation. Fast.\n- `.glb`\u002F`.gltf`, `.obj`, `.nhr`, render-JSON — **tessellated** first.\n\n`--timing` prints per-step wall clock, `--size-report` a payload breakdown; both\nto stderr, and both survive `-q` because naming a report is asking for it.\n`--strict` turns warnings into errors. `--wedge-cut START END` performs a real\nManifold boolean, removing the azimuthal sector from START to END degrees\nmeasured from +x counter-clockwise — this is geometry, not a display effect.\n\n### inspect — the machine-readable one\n\n```bash\nnodehammer inspect --output-format json summary -i det.root\nnodehammer inspect --output-format json tree -i det.root --depth 3 --filter '*\u002FPixel*'\nnodehammer inspect --output-format json tags -i det.root\n```\n\n**Text and JSON are not the same content in two skins.** The text view draws\ntree lines, samples a tag key down to five values once it passes ten, and\nappends a \"(n shown, m filtered)\" footer. Parse the JSON, which carries the\nwhole set and no drawing. Every document is stamped `\"schema\": 1`.\n\n```jsonc\n\u002F\u002F summary\n{ \"schema\": 1, \"kind\": \"summary\", \"format\": \"tgeo\",\n  \"nodes\": 12043, \"shapes\": { \"Box\": 900, ... },\n  \"materials\": [\"Air\", ...],                       \u002F\u002F sorted\n  \"diagnostics\": { \"warnings\": 3, \"errors\": 0 } }\n\n\u002F\u002F tree\n{ \"schema\": 1, \"kind\": \"tree\", \"shown\": 40, \"filtered\": 12003,\n  \"maxDepth\": 3,        \u002F\u002F null when no limit applied — present, not absent\n  \"filter\": \"*\u002FPixel*\", \u002F\u002F null when none\n  \"nodes\": [ { \"path\": \"\u002FWorld\u002FDet\", \"name\": \"Det\", \"depth\": 1,\n               \"children\": 4, \"leaf\": false, \"tags\": {...} } ] }\n\n\u002F\u002F tags\n{ \"schema\": 1, \"kind\": \"tags\", \"nodeCount\": 12043, \"nodesWithTags\": 800,\n  \"keys\": { \"subdetector\": [\"pixel\", \"strip\"] } }   \u002F\u002F every value, not a sample\n```\n\n`maxDepth` and `filter` are **present-and-null** rather than absent, so read them\nunconditionally. `tags` is absent from a tree node that has none.\n\n### config\n\nConfigs come in two spellings, and the file decides which: `.toml` (with an\n`include` chain) or `.lua` (a script that is evaluated). Both commands take\neither — **do not look for a separate Lua command, there isn't one**.\n\n```bash\nnodehammer config validate -c scene.lua\nnodehammer config flatten  -c scene.toml -o flat.toml   # -o optional; stdout otherwise\n```\n\n`config flatten` is the tool to reach for whenever you need to know what a config\n*actually* says: it resolves the include tree (or runs the script) and emits one\nself-contained TOML that needs no companion files. Its output is round-trippable.\n\nFor **writing or editing a config**, read\n[the config format](references\u002Fconfig-format.md): TOML structure, predicates,\nselection, rule precedence, materials, tessellation, exports, and includes.\nFor Lua, also read [the Lua builder](references\u002Flua-config.md), including its\ncomposition rules and a runnable equivalent of the TOML example.\n\nFor **a config for a particular detector**, start with\n[the detector authoring workflow](references\u002Fdetector-config.md). Inspect the\nactual geometry before choosing paths or tags; a valid config can match nothing.\nUse TOML for direct declarations and Lua when repetition benefits from loops or\nhelpers, preserving an existing format or the user's preference.\nWhen adding internal detail or investigating flicker\u002Fz-fighting, follow that\nworkflow’s overlap checks: material-filled mothers and their daughters can\nrender competing surfaces even when conversion reports no diagnostics.\n\n`convert -c` takes either spelling too, so a Lua config needs no flattening first.\n\n### project — `.nhproj` archives\n\nA `.nhproj` is a ZIP holding a config, the geometry it names, and a manifest.\n\n```bash\nnodehammer project pack -c scene.toml -i det.root -o det.nhproj\nnodehammer project info det.nhproj\nnodehammer project publish det.nhproj -o site\u002F     # static site, no server\n```\n\n**`nodehammer.toml` inside an archive is a reserved key.** It is the manifest\nnaming the entry config and geometry. `project pack` refuses rather than\noverwriting when a config is actually named that, or an include resolves to it.\nDo not hand-write it, and do not unzip an archive, edit, and re-zip — repack from\nsources instead.\n\n`project publish` produces a directory you can serve with any static file server\n(GitHub Pages included). Prefer it over `viewer serve` for anything an agent\nproduces, because it terminates.\n\n## Inherent limitations — not bugs to work around\n\n**The importable set is smaller than the file types people assume.** Built in\nunconditionally: `.nhb` \u002F `.nhb.zst`, `.json` \u002F `.json.zst`, and the synthetic\ngenerator. Compile-time gated: `.root` (TGeo), DD4hep XML, and `.gdml` through\nGeant4 (`--input-format gdml`). GDML requires `NODEHAMMER_WITH_GEANT4=ON` and a\nGeant4 installation with GDML support. It uses `G4GDMLParser`, converts lengths\nto centimetres, preserves shared volumes and `copyNumber` tags, and keeps\nbooleans analytic. Other solids use Geant4 polyhedra when no direct mapping\nexists. Replicas and parameterisations are rejected explicitly. Schema validation\nis disabled so a remote GDML schema is not needed. Geant4 material\u002Felement tables\nremain process-owned; repeated imports can grow those tables.\n\n**Heavy importers are compile-time options, not runtime ones.** `NODEHAMMER_WITH_TGEO`,\n`NODEHAMMER_WITH_DD4HEP`, `NODEHAMMER_WITH_GEANT4`, `NODEHAMMER_WITH_GEOMODEL` are\n**off by default**, and the released Python wheel is built without them. So\n`cannot determine input format for 'det.root'` from a wheel install is the build\nconfiguration, not a broken file and not a bug — the fix is a build with the\nbackend enabled, never a different flag. Check with\n`nodehammer convert -i x.root -o \u002Fdev\u002Fnull` and read the code, not the extension.\n\n**Some DD4hep plugins require the global detector.** ALLEGRO's k4geo readout\nsegmentation is one example. In a DD4hep-enabled native build, opt in with:\n\n```bash\nnodehammer convert -i detector.xml --dd4hep-global -c scene.toml -o detector.glb\n```\n\nSource the experiment's plugin environment first. The flag loads one compact\ninto a fresh default detector; a populated detector or repeated global import\nis rejected. The Python console script and `runCli` expose the same flag.\n`readSemantic` accepts `.importerOptions = {{\"dd4hep.useGlobalDetector\", true}}`;\nthe default remains private. Python uses `read_semantic(path,\nimporter_options={\"dd4hep.useGlobalDetector\": True})`. Unknown keys, incorrect\nprimitive types, and options for a different backend raise `NH0105`.\nApplications with an existing\ndetector can use `fromDD4hep(detector)`. Only the native executable installs\nthe exit warning and redirects plugin stdout.\n\nIf a plugin calls `exit()` during XML loading, the native CLI prints `NH0302`.\nFor a private import it suggests `--dd4hep-global`. This warning uses `atexit`,\nso it does not cover `abort()`, `_Exit()`, or fatal signals. Successful imports\nand caught exceptions do not trigger it. No separate executable is needed.\n\n**DD4hep XML has no extension of its own.** It is recognised by sniffing for\n`\u003Clccdd` in the first 512 bytes, or by `--input-format dd4hep` when the sniff\nfails. A generic `.xml` that is not DD4hep will not resolve.\n\n**`viewer serve` needs a wasm runtime this build cannot produce.** It comes from\nthe separate `nodehammer-web` distribution, which the Python package depends on.\n`NH1000` means it was not found; the command prints the search ladder it walked.\n`--web-assets DIR` points at a locally built one.\n\n**A wasm build has no `project` or `viewer serve`.** There is no host to serve\nfrom — the wasm module *is* the viewer.\n\n## Diagnostics\n\nEvery failure carries a stable `NH####` code, greppable and safe to match on.\nRanges: `NH00xx` config · `NH01xx` import · `NH02xx` scene ops · `NH03xx` TGeo ·\n`NH04xx` selection · `NH05xx` tessellation · `NH06xx` export · `NH08xx` public\nAPI · `NH09xx` CLI usage · `NH10xx` web runtime · `NH11xx` project archives.\n\nThe prefix in the code's *name* says how far it goes: fatal (the call could not\ndeliver), error (the result exists and part of it is wrong), warning (complete,\nsomething was assumed).\n\n| You see | It means | Do |\n|---|---|---|\n| `NH0101 cannot determine input format for '…'` | extension unknown **or backend not compiled in** | check the limitations above before anything else |\n| `NH0900 positional path must be a .nhproj archive or a directory` | `viewer \u003Cpath>` got something else | pass a `.nhproj` or a directory |\n| ``NH0900 `viewer shot` needs --input`` | `shot`\u002F`bench` were given a project, or nothing | pass `-i` geometry (and `-c` config) |\n| `NH0901 path not found` \u002F `input file not found` | a typo, before any window opens | — |\n| `NH1000` | wasm runtime missing | `pip install nodehammer-web`, or `--web-assets DIR` |\n| `NH1001` | runtime and library disagree | rebuild both halves together |\n| `NH1100` | archive could not be assembled or read | `project info` on it; do not unzip by hand |\n| `NH0008 drop_coincident_faces … without merge_descendants` | the option is silently a no-op | enable `merge_descendants` in the same scope |\n\n## Recipes\n\n**\"What is in this file?\"** → `nodehammer inspect --output-format json summary -i FILE`\n\n**\"What does this config actually do?\"** → `nodehammer config flatten -c FILE`\n(resolves includes and Lua), then read the TOML.\n\n**\"Show me the geometry.\"** → `nodehammer viewer shot -i FILE [-c CONFIG] -o\n\u002Ftmp\u002Fshot.png --shot-width 1920`, then look at the PNG. Never `viewer open`.\n\n**\"Make something I can share.\"** → `project pack` then `project publish -o site\u002F`.\n\n**\"Is this config valid?\"** → `nodehammer config validate -c FILE`; exit code is\nthe verdict, diagnostics are on stderr, and the verdict text is on stdout.\n\n**\"Write a config for this detector.\"** → follow\n[detector config authoring](references\u002Fdetector-config.md), then validate,\nflatten, and check the selected geometry and rendered output.\n\n**\"Convert for a web page.\"** → `-o out.glb` (glTF binary), not `.obj`.\n\n## Do not\n\n- **Do not run `viewer open`, bare `viewer \u003Cpath>`, or `viewer serve` without\n  being asked.** They block until a human intervenes.\n- **Do not parse stderr**, or the text form of `inspect`. Use\n  `--output-format json`.\n- **Do not unzip, edit and re-zip a `.nhproj`.** Repack from sources.\n- **Do not hand-write `nodehammer.toml` inside an archive** — `project pack`\n  owns that key.\n- **Do not treat a missing importer as a broken file.** Check the compile-time\n  gates first.\n- **Do not add `-q` to make output parseable.** It only removes narration from\n  stderr; stdout was already clean.\n\n## Getting this skill\n\nIt ships inside the binary and is installed with:\n\n```bash\nnodehammer skills install            # into ~\u002F.agents\u002Fskills, linked from ~\u002F.claude\nnodehammer skills install --scope project\nnodehammer skills list\n```\n","# nodehammer\n\nA pipeline that imports detector geometry, applies a config that selects and\nstyles parts of it, and writes it out — as a mesh (glTF\u002FOBJ), as an intermediate\nscene, as a JSON report, or into an interactive viewer.\n\nSame program either way: the compiled `nodehammer` executable and the\n`nodehammer` command from the Python wheel are one implementation (`cli::run`\ninside `libnodehammer`), so every command here behaves identically under\n`uvx nodehammer`. Differences that do exist are noted as such.\n\n## The output contract — read this before parsing anything\n\n**stdout is the answer. stderr is everything about producing it.**\n\nstdout carries the thing you would substitute into the next command: the\nflattened document, the JSON report, the path to the archive that was written,\nthe URL being served. stderr carries diagnostics, progress and summaries.\n\n```bash\narchive=$(nodehammer project pack -c scene.toml -i det.root -o det.nhproj)\n```\n\nThat yields the archive path, not a report about one. Two consequences:\n\n- **Never parse stderr.** It is prose for a human and is not a stable format.\n- **`-q` silences narration only.** Diagnostics, and the line a failing command\n  prints before it exits non-zero, are never affected. A flag that hid errors\n  would be a trap. `-v` restores narration; `-q`\u002F`-v` may be typed before or\n  after the subcommand.\n\nWhen calling `nodehammer` from a script, quiet is already the default for the\nlibrary entry point; the executable defaults to narrating because a person typed\nit.\n\n## Commands\n\nFive groups. Every one takes `-h`.\n\n| | |\n|---|---|\n| `convert` | import → config → write. The **output extension decides how far the pipeline runs** |\n| `inspect summary\\|tree\\|tags` | report on a geometry file; `--output-format json` |\n| `config validate\\|flatten` | check a config, or resolve it to one self-contained TOML |\n| `project pack\\|publish\\|info` | build, publish and read `.nhproj` archives |\n| `viewer open\\|serve\\|shot\\|bench` | the 3D viewer, in four postures |\n\nOption spellings are consistent across the tree: `-i\u002F--input`, `-c\u002F--config`,\n`-o\u002F--output`, `--input-format`, `--output-format`, `--color`, `--depth\u002F-d`,\n`--filter\u002F-f` mean the same thing everywhere they appear.\n\n### Commands that block — do not run these unprompted\n\n| Command | What it does | Use instead |\n|---|---|---|\n| `viewer open` | opens a **native window and blocks** until the user closes it | `viewer shot -o out.png` |\n| `viewer` (bare, with a path) | same — `open` is the default mode | `viewer shot` |\n| `viewer serve` | starts an HTTP server, **opens a browser, and blocks** | `viewer serve --no-browser` in a background job, or `project publish` for static output |\n| `viewer bench` | runs a GPU benchmark; needs a display | only when the user asked for numbers |\n\nTo *see* what a scene looks like, `nodehammer viewer shot -i det.nhb -c\nscene.toml -o shot.png --shot-width 1920` renders one PNG once the scene settles\nand quits. That is the agent-appropriate way to check a geometry visually. It\nstill needs a working GPU context, so it will fail on a headless machine with no\ndisplay.\n\n`shot` and `bench` render from **`-i` geometry plus an optional `-c` config**, not\nfrom a project: they reject a call without `--input` (`NH0900 … needs --input`)\neven when a `.nhproj` positional is given. To screenshot an archive, pass the\nconfig and geometry it was packed from.\n\nEverything else in the tree runs to completion and exits.\n\n### convert\n\n```bash\nnodehammer convert -i det.root -c scene.toml -o det.glb\nnodehammer convert -i det.root -o scene.nhb          # stop at the semantic scene\nnodehammer convert -i det.root -o a.glb -o b.obj     # several outputs, one import\n```\n\nThe output extension picks the writer *and* the depth of the pipeline:\n\n- `.nhb`, `.json` — the **semantic** scene. No tessellation. Fast.\n- `.glb`\u002F`.gltf`, `.obj`, `.nhr`, render-JSON — **tessellated** first.\n\n`--timing` prints per-step wall clock, `--size-report` a payload breakdown; both\nto stderr, and both survive `-q` because naming a report is asking for it.\n`--strict` turns warnings into errors. `--wedge-cut START END` performs a real\nManifold boolean, removing the azimuthal sector from START to END degrees\nmeasured from +x counter-clockwise — this is geometry, not a display effect.\n\n### inspect — the machine-readable one\n\n```bash\nnodehammer inspect --output-format json summary -i det.root\nnodehammer inspect --output-format json tree -i det.root --depth 3 --filter '*\u002FPixel*'\nnodehammer inspect --output-format json tags -i det.root\n```\n\n**Text and JSON are not the same content in two skins.** The text view draws\ntree lines, samples a tag key down to five values once it passes ten, and\nappends a \"(n shown, m filtered)\" footer. Parse the JSON, which carries the\nwhole set and no drawing. Every document is stamped `\"schema\": 1`.\n\n```jsonc\n\u002F\u002F summary\n{ \"schema\": 1, \"kind\": \"summary\", \"format\": \"tgeo\",\n  \"nodes\": 12043, \"shapes\": { \"Box\": 900, ... },\n  \"materials\": [\"Air\", ...],                       \u002F\u002F sorted\n  \"diagnostics\": { \"warnings\": 3, \"errors\": 0 } }\n\n\u002F\u002F tree\n{ \"schema\": 1, \"kind\": \"tree\", \"shown\": 40, \"filtered\": 12003,\n  \"maxDepth\": 3,        \u002F\u002F null when no limit applied — present, not absent\n  \"filter\": \"*\u002FPixel*\", \u002F\u002F null when none\n  \"nodes\": [ { \"path\": \"\u002FWorld\u002FDet\", \"name\": \"Det\", \"depth\": 1,\n               \"children\": 4, \"leaf\": false, \"tags\": {...} } ] }\n\n\u002F\u002F tags\n{ \"schema\": 1, \"kind\": \"tags\", \"nodeCount\": 12043, \"nodesWithTags\": 800,\n  \"keys\": { \"subdetector\": [\"pixel\", \"strip\"] } }   \u002F\u002F every value, not a sample\n```\n\n`maxDepth` and `filter` are **present-and-null** rather than absent, so read them\nunconditionally. `tags` is absent from a tree node that has none.\n\n### config\n\nConfigs come in two spellings, and the file decides which: `.toml` (with an\n`include` chain) or `.lua` (a script that is evaluated). Both commands take\neither — **do not look for a separate Lua command, there isn't one**.\n\n```bash\nnodehammer config validate -c scene.lua\nnodehammer config flatten  -c scene.toml -o flat.toml   # -o optional; stdout otherwise\n```\n\n`config flatten` is the tool to reach for whenever you need to know what a config\n*actually* says: it resolves the include tree (or runs the script) and emits one\nself-contained TOML that needs no companion files. Its output is round-trippable.\n\nFor **writing or editing a config**, read\n[the config format](references\u002Fconfig-format.md): TOML structure, predicates,\nselection, rule precedence, materials, tessellation, exports, and includes.\nFor Lua, also read [the Lua builder](references\u002Flua-config.md), including its\ncomposition rules and a runnable equivalent of the TOML example.\n\nFor **a config for a particular detector**, start with\n[the detector authoring workflow](references\u002Fdetector-config.md). Inspect the\nactual geometry before choosing paths or tags; a valid config can match nothing.\nUse TOML for direct declarations and Lua when repetition benefits from loops or\nhelpers, preserving an existing format or the user's preference.\nWhen adding internal detail or investigating flicker\u002Fz-fighting, follow that\nworkflow’s overlap checks: material-filled mothers and their daughters can\nrender competing surfaces even when conversion reports no diagnostics.\n\n`convert -c` takes either spelling too, so a Lua config needs no flattening first.\n\n### project — `.nhproj` archives\n\nA `.nhproj` is a ZIP holding a config, the geometry it names, and a manifest.\n\n```bash\nnodehammer project pack -c scene.toml -i det.root -o det.nhproj\nnodehammer project info det.nhproj\nnodehammer project publish det.nhproj -o site\u002F     # static site, no server\n```\n\n**`nodehammer.toml` inside an archive is a reserved key.** It is the manifest\nnaming the entry config and geometry. `project pack` refuses rather than\noverwriting when a config is actually named that, or an include resolves to it.\nDo not hand-write it, and do not unzip an archive, edit, and re-zip — repack from\nsources instead.\n\n`project publish` produces a directory you can serve with any static file server\n(GitHub Pages included). Prefer it over `viewer serve` for anything an agent\nproduces, because it terminates.\n\n## Inherent limitations — not bugs to work around\n\n**The importable set is smaller than the file types people assume.** Built in\nunconditionally: `.nhb` \u002F `.nhb.zst`, `.json` \u002F `.json.zst`, and the synthetic\ngenerator. Compile-time gated: `.root` (TGeo), DD4hep XML, and `.gdml` through\nGeant4 (`--input-format gdml`). GDML requires `NODEHAMMER_WITH_GEANT4=ON` and a\nGeant4 installation with GDML support. It uses `G4GDMLParser`, converts lengths\nto centimetres, preserves shared volumes and `copyNumber` tags, and keeps\nbooleans analytic. Other solids use Geant4 polyhedra when no direct mapping\nexists. Replicas and parameterisations are rejected explicitly. Schema validation\nis disabled so a remote GDML schema is not needed. Geant4 material\u002Felement tables\nremain process-owned; repeated imports can grow those tables.\n\n**Heavy importers are compile-time options, not runtime ones.** `NODEHAMMER_WITH_TGEO`,\n`NODEHAMMER_WITH_DD4HEP`, `NODEHAMMER_WITH_GEANT4`, `NODEHAMMER_WITH_GEOMODEL` are\n**off by default**, and the released Python wheel is built without them. So\n`cannot determine input format for 'det.root'` from a wheel install is the build\nconfiguration, not a broken file and not a bug — the fix is a build with the\nbackend enabled, never a different flag. Check with\n`nodehammer convert -i x.root -o \u002Fdev\u002Fnull` and read the code, not the extension.\n\n**Some DD4hep plugins require the global detector.** ALLEGRO's k4geo readout\nsegmentation is one example. In a DD4hep-enabled native build, opt in with:\n\n```bash\nnodehammer convert -i detector.xml --dd4hep-global -c scene.toml -o detector.glb\n```\n\nSource the experiment's plugin environment first. The flag loads one compact\ninto a fresh default detector; a populated detector or repeated global import\nis rejected. The Python console script and `runCli` expose the same flag.\n`readSemantic` accepts `.importerOptions = {{\"dd4hep.useGlobalDetector\", true}}`;\nthe default remains private. Python uses `read_semantic(path,\nimporter_options={\"dd4hep.useGlobalDetector\": True})`. Unknown keys, incorrect\nprimitive types, and options for a different backend raise `NH0105`.\nApplications with an existing\ndetector can use `fromDD4hep(detector)`. Only the native executable installs\nthe exit warning and redirects plugin stdout.\n\nIf a plugin calls `exit()` during XML loading, the native CLI prints `NH0302`.\nFor a private import it suggests `--dd4hep-global`. This warning uses `atexit`,\nso it does not cover `abort()`, `_Exit()`, or fatal signals. Successful imports\nand caught exceptions do not trigger it. No separate executable is needed.\n\n**DD4hep XML has no extension of its own.** It is recognised by sniffing for\n`\u003Clccdd` in the first 512 bytes, or by `--input-format dd4hep` when the sniff\nfails. A generic `.xml` that is not DD4hep will not resolve.\n\n**`viewer serve` needs a wasm runtime this build cannot produce.** It comes from\nthe separate `nodehammer-web` distribution, which the Python package depends on.\n`NH1000` means it was not found; the command prints the search ladder it walked.\n`--web-assets DIR` points at a locally built one.\n\n**A wasm build has no `project` or `viewer serve`.** There is no host to serve\nfrom — the wasm module *is* the viewer.\n\n## Diagnostics\n\nEvery failure carries a stable `NH####` code, greppable and safe to match on.\nRanges: `NH00xx` config · `NH01xx` import · `NH02xx` scene ops · `NH03xx` TGeo ·\n`NH04xx` selection · `NH05xx` tessellation · `NH06xx` export · `NH08xx` public\nAPI · `NH09xx` CLI usage · `NH10xx` web runtime · `NH11xx` project archives.\n\nThe prefix in the code's *name* says how far it goes: fatal (the call could not\ndeliver), error (the result exists and part of it is wrong), warning (complete,\nsomething was assumed).\n\n| You see | It means | Do |\n|---|---|---|\n| `NH0101 cannot determine input format for '…'` | extension unknown **or backend not compiled in** | check the limitations above before anything else |\n| `NH0900 positional path must be a .nhproj archive or a directory` | `viewer \u003Cpath>` got something else | pass a `.nhproj` or a directory |\n| ``NH0900 `viewer shot` needs --input`` | `shot`\u002F`bench` were given a project, or nothing | pass `-i` geometry (and `-c` config) |\n| `NH0901 path not found` \u002F `input file not found` | a typo, before any window opens | — |\n| `NH1000` | wasm runtime missing | `pip install nodehammer-web`, or `--web-assets DIR` |\n| `NH1001` | runtime and library disagree | rebuild both halves together |\n| `NH1100` | archive could not be assembled or read | `project info` on it; do not unzip by hand |\n| `NH0008 drop_coincident_faces … without merge_descendants` | the option is silently a no-op | enable `merge_descendants` in the same scope |\n\n## Recipes\n\n**\"What is in this file?\"** → `nodehammer inspect --output-format json summary -i FILE`\n\n**\"What does this config actually do?\"** → `nodehammer config flatten -c FILE`\n(resolves includes and Lua), then read the TOML.\n\n**\"Show me the geometry.\"** → `nodehammer viewer shot -i FILE [-c CONFIG] -o\n\u002Ftmp\u002Fshot.png --shot-width 1920`, then look at the PNG. Never `viewer open`.\n\n**\"Make something I can share.\"** → `project pack` then `project publish -o site\u002F`.\n\n**\"Is this config valid?\"** → `nodehammer config validate -c FILE`; exit code is\nthe verdict, diagnostics are on stderr, and the verdict text is on stdout.\n\n**\"Write a config for this detector.\"** → follow\n[detector config authoring](references\u002Fdetector-config.md), then validate,\nflatten, and check the selected geometry and rendered output.\n\n**\"Convert for a web page.\"** → `-o out.glb` (glTF binary), not `.obj`.\n\n## Do not\n\n- **Do not run `viewer open`, bare `viewer \u003Cpath>`, or `viewer serve` without\n  being asked.** They block until a human intervenes.\n- **Do not parse stderr**, or the text form of `inspect`. Use\n  `--output-format json`.\n- **Do not unzip, edit and re-zip a `.nhproj`.** Repack from sources.\n- **Do not hand-write `nodehammer.toml` inside an archive** — `project pack`\n  owns that key.\n- **Do not treat a missing importer as a broken file.** Check the compile-time\n  gates first.\n- **Do not add `-q` to make output parseable.** It only removes narration from\n  stderr; stdout was already clean.\n\n## Getting this skill\n\nIt ships inside the binary and is installed with:\n\n```bash\nnodehammer skills install            # into ~\u002F.agents\u002Fskills, linked from ~\u002F.claude\nnodehammer skills install --scope project\nnodehammer skills list\n```\n",{"name":5,"description":7},"main","e3847b1faa7245df2ccbb8caa12e53cff20d2a55","skills\u002Fnodehammer",1,"https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer","https:\u002F\u002Fpaulgessinger.github.io\u002Fnodehammer\u002F","https:\u002F\u002Fgithub.com\u002Fpaulgessinger","Photography, Physics, Web Development","2026-11-08 05:43:23",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add paulgessinger\u002Fnodehammer --skill nodehammer",{"tool":58,"command":59},"bunx","bunx skills add paulgessinger\u002Fnodehammer --skill nodehammer",{"tool":61,"command":62},"pnpm","pnpm dlx skills add paulgessinger\u002Fnodehammer --skill nodehammer","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Fskills\u002Fnodehammer\u002FSKILL.md",true,[66,72,76],{"path":67,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":70,"sourceHtmlUrl":71},"references\u002Fconfig-format.md","REFERENCE","参考文档",23737,"https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Fskills\u002Fnodehammer\u002Freferences\u002Fconfig-format.md",{"path":73,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":74,"sourceHtmlUrl":75},"references\u002Fdetector-config.md",10978,"https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Fskills\u002Fnodehammer\u002Freferences\u002Fdetector-config.md",{"path":77,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":78,"sourceHtmlUrl":79},"references\u002Flua-config.md",5114,"https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Fskills\u002Fnodehammer\u002Freferences\u002Flua-config.md",[81,95],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":38},"01M6QNEZET5FB1Z0RXM97PEZQR","with-dash","A skill whose name is not a valid C++ identifier — the case that broke.","https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fwith-dash\u002FSKILL.md",[],[88,89,90,91,92,93,94],"with","dash","with dash","valid","c++","name","case",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":38},"01M6QNEMF3VJ3SJAPM3TFTCP3K","plain","A skill whose name is already a valid C++ identifier.","https:\u002F\u002Fgithub.com\u002Fpaulgessinger\u002Fnodehammer\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fplain\u002FSKILL.md",[],[97,91,92,93]]