Rendered from specs/S15-cross-platform-tooling.md — edit it there, not here.
S15 — Cross-Platform Tooling (Development & Deployment)
Overview
This project targets contributors on Windows, macOS, and Linux alike, with no "works on my machine" assumption about the OS or shell. The shipped extension already has to hold to that bar — S08-SR-09 runs the E2E suite on both Linux and Windows CI runners, and specs/README.md's Scope section makes Python strictly optional (renderer fallback only), so nothing in the deployed extension requires a specific OS or an interpreter beyond Node/VS Code itself.
The development side had quietly drifted from that bar. Most of scripts/ is Node (.mjs), already portable, but a few scripts — most recently scripts/lint-workflows.sh, added to close the S09-SR-06 CI/local parity gap — are bash, and one contributor-facing script (scripts/bootstrap.sh, the documented first step for any checkout) is bash too. Both are wired into the mandatory local gate: bootstrap.sh is the project's npm ci replacement, and lint-workflows.sh runs inside npm run verify, which is the Husky pre-commit hook. Neither script runs without a bash interpreter, which is not part of a bare Windows + Node + git install.
This spec makes the existing product-level guarantee and the missing tooling-level guarantee both explicit in one place, so "OS-agnostic" covers development and deployment symmetrically, and so a future script doesn't reintroduce the gap the way lint-workflows.sh did.
Requirements
Mandatory Local Gate
- S15-SR-01 Every script invoked by
npm run verify(directly or via acheck:*/lint:*/test:*sub-script) MUST run on Windows, macOS, and Linux using only Node.js (already anengines.noderequirement) and git — no bash, POSIX shell, or Python assumed, and no reliance on Git for Windows bundling MSYS/Git Bash as an implicit shell. - S15-SR-02
scripts/bootstrap.shandscripts/lint-workflows.shMUST be rewritten as Node (.mjs) scripts, preserving their current behavior exactly:bootstrap.sh'snpm install --ignore-scripts --no-audit --no-fund+npx huskysequence, andlint-workflows.sh's PATH →go run→docker runfallback chain (in the same order, with the same pinnedACTIONLINT_VERSION) and its loud failure message when none of the three are available.package.json'slint:workflowsscript andAGENTS.md's bootstrap instructions MUST be updated to invoke the new Node entry points. - S15-SR-03 New scripts added under
scripts/in the future MUST default to Node (.mjs) unless the exemption in S15-SR-04 applies. A bash or Python script landing in the mandatory local gate (npm run verify,bootstrap, or any script a contributor must run to get a working checkout) is a spec violation to flag in review, not a style preference.
CI-Only Exemption
- S15-SR-04 A script that only ever runs on the pinned GitHub-hosted runner image — never on a contributor's machine — is exempt from S15-SR-01/03 (e.g.
scripts/ci-detect-source-changes.sh, bash). If such a script is ever wired intonpm run verifyor another contributor-facing command, it MUST first be ported per S15-SR-01/02's rules before that happens. This exemption is narrower than it once was:sync-readme- badges.pyno longer qualifies (S15-SR-06) — it's reached from an agent hook as well as CI, and a Python-only script is the wrong default for a project this size regardless of exemption technicalities (S15-SR-03).
Agent-Invoked Hooks
- S15-SR-05 Claude Code runs directly on a contributor's own machine as often as it runs in a managed container — nothing about
.claude/hooks/is inherently container-only — so every script Claude Code's harness invokes as a hook (.claude/settings.json'sSessionStart,PreToolUse,PostToolUse,UserPromptSubmitentries) MUST be Node (.mjs), invoked via an explicitnode <path>.mjscommand rather than relying on a shebang and the executable bit — the latter doesn't run on Windows without a POSIX layer, defeating the point.AGENTS.md's "guarantees live below the agent" rule still holds — none of these hooks becomes the sole enforcement of anything — but "convenience only" MUST NOT mean "broken on Windows"; a hook that silently never fires is a worse experience than not having it. A hook MAY still shell out to a script that keeps a documented exemption (S15-SR-04) — e.g.update-readme-badges.mjscallingnpm run badges:sync, which reachessync-readme-badges.py— as long as that failure path degrades gracefully (matches the pre-existing best-effort, never-blocks behavior) rather than breaking the hook itself.
Optional/Convenience Scripts
- S15-SR-06
scripts/sync-readme-badges.pyMUST be rewritten as Node (.mjs), preserving its exact behavior: readingpackage.jsonand (when present)coverage/coverage-summary.json, and rewriting the coverage/VS Code/Node/license shields.io badges inREADME.md, percent-encoding values identically to Python'surllib.parse.quote(s, safe='')(notencodeURIComponent, which leaves!*'()unescaped where Python doesn't).package.json'sbadges:syncscript and.claude/hooks/update-readme- badges.mjsMUST invoke it vianode;.github/workflows/maturity- refresh.ymlneeds no change since it already reaches the script throughnpm run badges:sync. Unlike S15-SR-01/02's mandatory-gate scripts, this one stays best-effort (never blocks a save or a build) — only the interpreter changes, not the exit-code/error-handling contract.
Non-Functional Requirements
- S15-NFR-01 (deployment, cross-reference — no new product requirement) The shipped extension's OS/interpreter agnosticism is already specified elsewhere and this spec does not duplicate it, only points to it: the E2E suite runs on Linux and Windows CI runners (
S08-SR-09), and Python is optional for every feature except the richer preview renderer, which falls back to a dependency-free built-in renderer without it (specs/README.mdScope,F01-FR-21/22).
Out of Scope
- Rewriting
scripts/ci-detect-source-changes.sh— covered by the S15-SR-04 exemption as long as it stays CI-only. - Any change to the shipped extension's runtime behavior — this spec is about contributor tooling only; the product-side guarantee already exists (S15-NFR-01's cross-references).
- Requiring a specific package manager or shell for optional/manual scripts outside the local gate (e.g.
npm run make-gifs, which already documents its own native-build prerequisites separately frombootstrap.sh).
Acceptance Criteria
npm run verifyand the bootstrap step succeed on a Windows runner (PowerShell or cmd, no WSL) with only Node ≥22 and git installed.scripts/lint-workflows.shandscripts/bootstrap.shno longer exist; their Node replacements produce equivalent output and exit codes for the same PATH/go/docker-availability scenarios the bash versions handled.package.jsonandAGENTS.mdreference the Node entry points, notbash scripts/*.sh.npm run check:traceabilitypasses with S15's requirement IDs present inspecs/traceability.json..claude/hooks/*.shno longer exist;.claude/settings.json's hookcommandentries invoke their.mjsreplacements explicitly vianode, and each hook's observable behavior (exit codes, stdout/stderr content, what triggers it) is unchanged from the bash version.scripts/sync-readme-badges.pyno longer exists; runningnpm run badges:sync(or theupdate-readme-badgeshook, ormaturity-refresh.yml) produces byte-identicalREADME.mdbadge output to what the Python version produced for the same inputs.
Relation to Existing Specs
- Cross-references
S08-SR-09(E2E suite on Linux + Windows) andspecs/README.md's Python-optional Scope note as the pre-existing deployment half of this guarantee — this spec is the single place that states "OS-agnostic, development and deployment both," rather than scattering the claim. - Complements
S09-SR-06, which addedlint-workflows.shto the local gate without noticing it introduced a new bash dependency; S15-SR-02 is the follow-up that closes that specific regression. - Follows
AGENTS.md's "Agnosticity & standardization" principle (AI-tool and model agnosticity) by extending the same reasoning to the OS/shell axis: durable guarantees should not depend on an environment detail a contributor might not have.
History
- 2026-07-24 — Initial spec, prompted by
lint-workflows.sh(S09-SR-06) landing as bash inside the mandatory local gate. - 2026-07-24 — Added S15-SR-05: extended scope to
.claude/hooks/*.sh. Initially left as bash on the reasoning that Claude Code sessions run in a managed Linux container; a contributor pointed out Claude Code also runs directly on a user's own machine (Windows included), where the same bash-shebang-doesn't-execute problem applies to hooks as much as tobootstrap.sh/lint-workflows.sh. - 2026-07-24 — Added S15-SR-06: extended scope to
scripts/sync-readme-badges.py, the project's only Python file. Narrowed the S15-SR-04 exemption accordingly — a script reached from both CI and an agent hook, and this simple to port, no longer earns the CI-only carve-out.
Requirement traceability
Status of each requirement in specs/traceability.json. Test coverage is auto-discovered from [ID] tags in test titles and is not listed here.
| Requirement | Status | Implementation | Note |
|---|---|---|---|
S15-NFR-01 | manual | .github/workflows/ci.yml | cross-reference only, satisfied by S08-SR-09's impl + specs/README.md Scope |
S15-SR-01 | manual | AGENTS.md, scripts/bootstrap.mjs, scripts/lint-workflows.mjs | the mandatory local gate is now Node-only (bootstrap.mjs, lint-workflows.mjs); policy stated in AGENTS.md's Rules section, enforced by review, no automated checker |
S15-SR-02 | manual | scripts/bootstrap.mjs, scripts/lint-workflows.mjs, package.json, scripts/check-consistency.mjs | bootstrap.sh and lint-workflows.sh rewritten as .mjs, same behavior (npm install --ignore-scripts + npx husky; actionlint PATH/go-run/docker fallback chain, same pinned version) |
S15-SR-03 | manual | AGENTS.md | policy documented in AGENTS.md's Rules section ("No bash/Python in the mandatory local gate"); enforced by review, no automated checker |
S15-SR-04 | manual | scripts/ci-detect-source-changes.sh | exemption narrowed: sync-readme-badges.py no longer qualifies (S15-SR-06); ci-detect-source-changes.sh remains CI-only, not part of npm run verify or bootstrap |
S15-SR-05 | manual | .claude/hooks/session-bootstrap.mjs, .claude/hooks/pre-commit-gate.mjs, .claude/hooks/check-coverage.mjs, .claude/hooks/spec-context.mjs, .claude/hooks/update-readme-badges.mjs, .claude/hooks/pr-branch-check.mjs, .claude/settings.json | all hooks are .mjs, invoked via explicit `node <path>.mjs` in .claude/settings.json instead of shebang + executable bit; agent-hook, no unit test. pr-branch-check.mjs (SessionStart) added: on a non-default branch, reminds the agent to keep an open PR rebased on the default branch and its checks green, using only git (no GitHub API/token needed) |
S15-SR-06 | manual | scripts/sync-readme-badges.mjs, package.json, .claude/hooks/update-readme-badges.mjs | Python rewritten as Node: same regex/JSON logic, custom enc() matching urllib.parse.quote(s, safe='') exactly (unlike encodeURIComponent); badges:sync and the agent hook both invoke it via node; agent-hook/CI script, no unit test |
Documented in
Documentation sections tagged spec:S15 (or one of its requirement ids) — 0 of ~492 expected words (0%). The expectation comes from this spec's evaluated complexity of 12.3 (7 documentable requirements — 6 SR, 1 NFR — weighted by kind and by each definition's length).
No documentation section is tagged for this spec yet — about 492 words are expected. Add a <!-- spec:S15 --> tag above the section that documents it.