Skip to content

fix(runtime): stpa actually runs on Node — and CI can now tell - #12

Merged
MyAlterLego merged 1 commit into
mainfrom
fix/node-runtime
Aug 19, 2026
Merged

MyAlterLego merged 1 commit into
mainfrom
fix/node-runtime

Conversation

@MyAlterLego

Copy link
Copy Markdown
Contributor

Closes #11.

The bug

Every stpa subcommand that spawns a tool died on Node with SyntaxError: Cannot use import statement outside a module. Only stpa --help worked, because it never spawns anything. PR #9's headline claim — "Bun is no longer an install prerequisite" — was false for four weeks.

The root cause is not the obvious one

Issue #11 said "the repo has no package.json, so Node resolves .ts as CommonJS." That is wrong, and I only found out by trying to make the new CI gate fail.

In a clean directory, Node's module-syntax detection reads these tools as ESM perfectly well. The bug does not reproduce there. It needs an ancestor directory whose package.json declares {"type":"commonjs"} — an explicit ancestor declaration beats detection.

That is not a corner case. It is the documented install path:

~/.claude/package.json          →  {"type":"commonjs"}
~/.claude/skills/triarch-stpa/  →  every import in Tools/ is a SyntaxError

So the README told people to clone into exactly the location that broke it, and a clean-checkout CI job would have passed the entire time.

The fix

A dependency-free package.json declaring {"type":"module"}. It shadows any ancestor and makes resolution explicit rather than a property of where the repo happens to sit.

That declaration makes two files genuinely ESM, so they had to follow:

  • stpa — require() → import, __dirname → fileURLToPath(import.meta.url). Used fileURLToPath rather than import.meta.dirname so the launcher still covers the whole Node range its own RUNTIME block claims to support.
  • Tools/UcaGrid.ts — had two inline require("node:fs") calls and no imports at all. It was implicitly CommonJS, which is why it was the one tool that already worked under Node. Now it imports at the top like every sibling.

The CI lane that should have existed

Node 22 and 24, running the pipeline end-to-end through the CLI — then doing it again with the repo staged under a {"type":"commonjs"} ancestor. That second step is the one that matters; without it the job is green theatre.

And because a gate that cannot fail is decoration, a final step deletes package.json and asserts the break returns. I wrote that step after the first version of this gate passed with the fix removed — which is how the real root cause surfaced.

Verification

Node 25.9.0 Bun 1.3.11
stpa --help pass pass
All 13 tools resolve pass pass
stpa run end-to-end pass — both HTML artifacts pass — both HTML artifacts
Under commonjs ancestor pass pass
Negative control (no package.json, commonjs ancestor) fails as intended n/a

Bun regression-checked: full pipeline, 32 cells, 8 findings, 100% coverage, REPORT.html + SUMMARY.html.

Also

28 bun <Tool>.ts usage strings across 13 tools became stpa <verb>. They hardcoded a runtime the toolkit no longer requires and pointed people at files instead of the command.

Still open, not in this PR: Tools/DiscoveryGate.ts:275 writes its template without mkdir and dies on a raw ENOENT; the committed Examples/ledgerline/REPORT.html is 5 lines of CSS stale against its own renderer.

🤖 Generated with Claude Code

PR #9 shipped "runs on Node or Bun" green and broken. Every subcommand that
spawned a tool died on Node with `SyntaxError: Cannot use import statement
outside a module`; only `stpa --help` worked, because it never spawns anything.

The root cause is not the one it looks like. Node's module-syntax detection
reads these .ts tools as ESM perfectly well in a clean directory — the bug does
not reproduce there. It needs an ANCESTOR whose package.json declares
{"type":"commonjs"}, because an explicit ancestor declaration beats detection.
That is not a corner case: `~/.claude/package.json` declares commonjs and skills
install into `~/.claude/skills/`, so the documented install path was precisely
the configuration that broke. Anyone who followed the README got a toolkit where
nothing but --help ran.

The fix is a dependency-free package.json declaring {"type":"module"}, which
shadows any ancestor and makes resolution explicit instead of a property of where
the repo happens to sit. That declaration then makes the launcher and one tool
genuinely ESM, so:

- `stpa` moves from require() to import, and from __dirname to
  fileURLToPath(import.meta.url). fileURLToPath rather than import.meta.dirname
  so the launcher still covers the whole Node range its own RUNTIME block claims.
- `Tools/UcaGrid.ts` had two inline require("node:fs") calls and no imports at
  all — it was implicitly CommonJS, which is why it alone survived. Now it
  imports at the top like every sibling.

CI grew a Node lane on 22 and 24 that runs the pipeline end-to-end through the
CLI, then does it again with the repo staged under a commonjs ancestor. That
second step is the one that matters: a Node job in a clean checkout would have
passed for the whole four weeks the toolkit was broken. And because a gate that
cannot fail is decoration, a final step deletes package.json and asserts the
break comes back.

Also swept the usage strings: 28 `bun <Tool>.ts` lines across 13 tools became
`stpa <verb>`. They hardcoded a runtime the toolkit no longer requires and
pointed people at files instead of the command.

Closes #11

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MyAlterLego
MyAlterLego merged commit 574effb into main Aug 19, 2026
6 checks passed
@MyAlterLego
MyAlterLego deleted the fix/node-runtime branch August 19, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stpa is broken under Node — every subcommand that spawns a tool dies

1 participant