Repository navigation
feat(dist): npm-free MCP spec, squad-init action, and container image (#1593) - #1610
Conversation
…#1593) Second slice of the standalone distribution work. Builds on the bundle mechanism from the previous commit and closes the gap that made it incomplete. The gap: installing Squad from a bundle removed npm from *installation*, but `squad init` still wrote an npx-based `squad_state` MCP entry into `.mcp.json`, and probed registry.npmjs.org to choose the version. On a firewalled machine the CLI installed fine and then the MCP server silently failed to launch on first use — the exact failure mode this work exists to prevent. What changed: - mcp-spec.ts gains a tier-0 ahead of the two npx tiers. When SQUAD_STANDALONE_HOME is set (exported by the bundle launchers) the spec points at that bundle's launcher by absolute path with args ['state-mcp']. It short-circuits before the registry probe, so a machine with no route to npm makes no network call here at all. Absolute because Copilot spawns the MCP server in its own environment, where neither PATH nor that variable is guaranteed. - describeMcpSpec() previously read args[1] to name the package; standalone specs have no package there and would have printed '<unknown>'. It now reports the launcher path. - build-standalone.mjs launchers (sh, cmd, ps1) export SQUAD_STANDALONE_HOME. - .github/actions/squad-init — composite action that installs from a release bundle and runs squad init. This is the direct replacement for the `npx @bradygaster/squad-cli init` step in the gh-aw activation job (#1587). It also fails the build if the resulting .mcp.json still mentions npx. - Dockerfile + .dockerignore — image built from a bundle instead of `npm install -g`, honoring the existing container contract in docs/reference/container-image.md (WORKDIR /app, non-root uid 1001, SIGTERM, `squad watch --execute`). Sets SQUAD_STANDALONE_HOME=/opt/squad. Built with --include-optional because the contract documents OTLP export, which lives in the optional OpenTelemetry packages. - .gitattributes pins LF for Dockerfile and .dockerignore. CRLF breaks RUN line continuations when the image builds on Linux. Verified: - 13 new tests in mcp-spec-init.test.ts, including an explicit assertion that the registry probe never fires in standalone mode. 23/23 pass. - Built a bundle and ran `squad init` from it on win32-x64: the generated .mcp.json contains command=<bundle>/squad.cmd, args=['state-mcp'], no npx. - Runtime stage of the Dockerfile built and exercised on linux/amd64 with a host-built bundle: `squad --version` returns 0.11.0 as the non-root squad user, `which squad` resolves to /opt/squad/squad, `squad init` writes command=/opt/squad/squad, and `squad state-mcp` answers a JSON-RPC initialize handshake with serverInfo.name=squad-state. - lint, docs-build, mcp-root-write, state-mcp, cspell, markdownlint all clean. Not verified: the Dockerfile builder stage. This machine intercepts TLS, so every container gets ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE reaching registry.npmjs.org — reproducible with a bare `docker run node:22-slim npm view lodash`. Not a Dockerfile defect, but the full image has never been built end to end and CI must prove it. Likewise the squad-init action cannot run until a release with bundle assets exists. Refs #1593, #1587 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
🏗️ Architectural Review
Automated architectural review — informational only. |
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 3 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 0 active Copilot thread(s) resolved (2 outdated skipped) |
| ✅ | CI passing | All checks passing |
Files Changed (12 files, +675 −23)
| File | +/− |
|---|---|
.changeset/standalone-cross-build-fix.md |
+5 −0 |
.changeset/standalone-mcp-spec-1593.md |
+5 −0 |
.dockerignore |
+15 −0 |
.gitattributes |
+5 −0 |
.github/actions/squad-init/action.yml |
+189 −0 |
Dockerfile |
+71 −0 |
docs/src/content/docs/features/standalone-install.md |
+69 −3 |
packages/squad-cli/src/cli/core/mcp-spec.ts |
+62 −6 |
packages/squad-cli/src/cli/core/upgrade.ts |
+6 −1 |
scripts/build-standalone.mjs |
+33 −11 |
test/mcp-spec-init.test.ts |
+106 −2 |
test/standalone-build.test.ts |
+109 −0 |
Total: +675 −23
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
🟠 Impact Analysis — PR #1610Risk tier: 🟠 HIGH 📊 Summary
🎯 Risk Factors
📦 Modules Affectedci-workflows (1 file)
docs (1 file)
root (5 files)
scripts (1 file)
squad-cli (2 files)
tests (2 files)
This report is generated automatically for every PR. See #733 for details. |
There was a problem hiding this comment.
Pull request overview
Adds an npm-free path for the squad_state MCP server spec when running Squad from a standalone bundle, plus distribution helpers (composite action + container image) to consume those bundles in CI and containers.
Changes:
- Add a new “standalone bundle” tier in the MCP spec resolver (driven by
SQUAD_STANDALONE_HOME) and fixdescribeMcpSpecfor standalone specs. - Add a
squad-initcomposite action that installs from release bundles and runssquad init. - Add a root
Dockerfile+ docs updates for building/running an image from a bundle (no npm registry dependency at runtime).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/mcp-spec-init.test.ts | Adds tests for standalone-tier resolution, standalone launcher detection, and describeMcpSpec behavior. |
| scripts/build-standalone.mjs | Exports SQUAD_STANDALONE_HOME from bundle launchers (cmd/ps1/sh). |
| packages/squad-cli/src/cli/core/mcp-spec.ts | Adds standalone tier + launcher detection and exposes STANDALONE_HOME_ENV. |
| packages/squad-cli/src/cli/core/upgrade.ts | Fixes describeMcpSpec to correctly describe standalone launcher specs. |
| docs/src/content/docs/features/standalone-install.md | Documents squad-init, container usage, and the npx-free .mcp.json behavior. |
| Dockerfile | Builds a container image from a standalone bundle (multi-stage). |
| .github/actions/squad-init/action.yml | Composite action to install from bundle + run squad init, and summarize results. |
| .gitattributes | Forces LF endings for Docker-related files to prevent build issues. |
| .dockerignore | Reduces build context / prevents host artifacts from leaking into the image build. |
| .changeset/standalone-mcp-spec-1593.md | Changeset for the squad-cli patch release. |
Addresses both Copilot review comments on #1610. 1. The action fetched scripts/install.sh from the repository HEAD over the network. That is unpinned remote code execution, and the installer could drift from the release tag being installed. It now runs the install.sh that ships alongside the action, resolved from github.action_path. Consumers pin the action to a SHA, so the installer is pinned with it. 2. The npx check only wrote a warning to the step summary and exited 0, which did not match what the PR claimed. An npx-based squad_state entry is unrunnable on exactly the firewalled runners this action targets, and it fails later at Copilot start rather than here, so it now fails the step. Verified the action_path walk resolves correctly on Linux (alpine container: /repo/.github/actions/squad-init -> /repo/scripts/install.sh) rather than assuming the relative depth. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
…tests Found while running the installer end to end for the first time: building a linux or darwin bundle from a Windows host failed. node-v22.20.0-linux-x64/bin/npx: Can't create '...': Invalid argument tar: Error exit delayed from previous errors. The builder extracted the whole Node.js archive, and the posix archives carry symlinks (bin/npm, bin/npx, bin/corepack) that Windows cannot create. Only the node binary is ever shipped, so extraction is now scoped to that single member. Extraction failures also report the underlying tar error instead of a bare non-zero exit. This would have bitten anyone building non-linux targets from a Windows runner, and the release workflow's structural checks would not have caught it. Also adds test/standalone-build.test.ts (11 cases) covering the behaviors that break silently: selective extraction, SQUAD_STANDALONE_HOME in all three launchers, symlink resolution in the posix launcher, optional-dep omission, checksum verification, VERSION/PREFIX/REPO overrides, and an assertion that install.sh never reaches the npm registry. Verified the full install path in a container with no Node and no npm: served a packaged bundle over HTTP, ran the real install.sh against it, and got squad --version -> 0.11.0 through the PATH symlink. Corrupting the asset made the installer abort with a checksum mismatch, as intended. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
|
Pushed two follow-ups: the review fixes (b121030) and a real bug I found while testing (4756266). Both review comments addressedThe unpinned
|
…) (#1611) * feat(dist): npm-free MCP spec, squad-init action, and container image (#1593) Second slice of the standalone distribution work. Builds on the bundle mechanism from the previous commit and closes the gap that made it incomplete. The gap: installing Squad from a bundle removed npm from *installation*, but `squad init` still wrote an npx-based `squad_state` MCP entry into `.mcp.json`, and probed registry.npmjs.org to choose the version. On a firewalled machine the CLI installed fine and then the MCP server silently failed to launch on first use — the exact failure mode this work exists to prevent. What changed: - mcp-spec.ts gains a tier-0 ahead of the two npx tiers. When SQUAD_STANDALONE_HOME is set (exported by the bundle launchers) the spec points at that bundle's launcher by absolute path with args ['state-mcp']. It short-circuits before the registry probe, so a machine with no route to npm makes no network call here at all. Absolute because Copilot spawns the MCP server in its own environment, where neither PATH nor that variable is guaranteed. - describeMcpSpec() previously read args[1] to name the package; standalone specs have no package there and would have printed '<unknown>'. It now reports the launcher path. - build-standalone.mjs launchers (sh, cmd, ps1) export SQUAD_STANDALONE_HOME. - .github/actions/squad-init — composite action that installs from a release bundle and runs squad init. This is the direct replacement for the `npx @bradygaster/squad-cli init` step in the gh-aw activation job (#1587). It also fails the build if the resulting .mcp.json still mentions npx. - Dockerfile + .dockerignore — image built from a bundle instead of `npm install -g`, honoring the existing container contract in docs/reference/container-image.md (WORKDIR /app, non-root uid 1001, SIGTERM, `squad watch --execute`). Sets SQUAD_STANDALONE_HOME=/opt/squad. Built with --include-optional because the contract documents OTLP export, which lives in the optional OpenTelemetry packages. - .gitattributes pins LF for Dockerfile and .dockerignore. CRLF breaks RUN line continuations when the image builds on Linux. Verified: - 13 new tests in mcp-spec-init.test.ts, including an explicit assertion that the registry probe never fires in standalone mode. 23/23 pass. - Built a bundle and ran `squad init` from it on win32-x64: the generated .mcp.json contains command=<bundle>/squad.cmd, args=['state-mcp'], no npx. - Runtime stage of the Dockerfile built and exercised on linux/amd64 with a host-built bundle: `squad --version` returns 0.11.0 as the non-root squad user, `which squad` resolves to /opt/squad/squad, `squad init` writes command=/opt/squad/squad, and `squad state-mcp` answers a JSON-RPC initialize handshake with serverInfo.name=squad-state. - lint, docs-build, mcp-root-write, state-mcp, cspell, markdownlint all clean. Not verified: the Dockerfile builder stage. This machine intercepts TLS, so every container gets ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE reaching registry.npmjs.org — reproducible with a bare `docker run node:22-slim npm view lodash`. Not a Dockerfile defect, but the full image has never been built end to end and CI must prove it. Likewise the squad-init action cannot run until a release with bundle assets exists. Refs #1593, #1587 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf * fix(action): run the bundled installer and fail on npx MCP specs Addresses both Copilot review comments on #1610. 1. The action fetched scripts/install.sh from the repository HEAD over the network. That is unpinned remote code execution, and the installer could drift from the release tag being installed. It now runs the install.sh that ships alongside the action, resolved from github.action_path. Consumers pin the action to a SHA, so the installer is pinned with it. 2. The npx check only wrote a warning to the step summary and exited 0, which did not match what the PR claimed. An npx-based squad_state entry is unrunnable on exactly the firewalled runners this action targets, and it fails later at Copilot start rather than here, so it now fails the step. Verified the action_path walk resolves correctly on Linux (alpine container: /repo/.github/actions/squad-init -> /repo/scripts/install.sh) rather than assuming the relative depth. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf * fix(dist): cross-build posix bundles from Windows, add build/install tests Found while running the installer end to end for the first time: building a linux or darwin bundle from a Windows host failed. node-v22.20.0-linux-x64/bin/npx: Can't create '...': Invalid argument tar: Error exit delayed from previous errors. The builder extracted the whole Node.js archive, and the posix archives carry symlinks (bin/npm, bin/npx, bin/corepack) that Windows cannot create. Only the node binary is ever shipped, so extraction is now scoped to that single member. Extraction failures also report the underlying tar error instead of a bare non-zero exit. This would have bitten anyone building non-linux targets from a Windows runner, and the release workflow's structural checks would not have caught it. Also adds test/standalone-build.test.ts (11 cases) covering the behaviors that break silently: selective extraction, SQUAD_STANDALONE_HOME in all three launchers, symlink resolution in the posix launcher, optional-dep omission, checksum verification, VERSION/PREFIX/REPO overrides, and an assertion that install.sh never reaches the npm registry. Verified the full install path in a container with no Node and no npm: served a packaged bundle over HTTP, ran the real install.sh against it, and got squad --version -> 0.11.0 through the PATH symlink. Corrupting the asset made the installer abort with a checksum mismatch, as intended. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf * feat(dist): Homebrew and winget packaging, plus a real squad.exe (#1593) Third slice of the distribution work. Adds the two package managers people actually expect, and the Windows executable that makes one of them possible. The blocker I found first: winget's portable installer only creates command aliases for .exe targets — .cmd and .bat are explicitly unsupported. Our Windows bundle's entry point was squad.cmd, so a winget manifest would have installed something that could not be invoked. Homebrew has no such restriction and works with the existing shell launcher. So Windows bundles now ship a real squad.exe, built with Node's single executable application support. It is the vendored runtime with a small launcher embedded, so it REPLACES runtime/node.exe rather than adding to the bundle — a Windows bundle is still 114 MB. The launcher resolves the bundle from its own location and follows a symlink if it finds one, because winget installs portables by symlinking the exe into a links directory. Also here: - scripts/generate-packaging.mjs renders the Homebrew cask and the three winget manifests from a release's SHA256SUMS.txt. Both ecosystems embed a version and a per-artifact SHA-256, so hand-maintaining them guarantees they go stale — this is exactly the standing cost flagged in #1593. - The release workflow generates them after publishing and attaches them as an artifact for a maintainer to submit. - mcp-spec now prefers squad.exe over squad.cmd on Windows. Since the fix for CVE-2024-27980 Node refuses to spawn a .cmd without shell:true, so an MCP client spawning the command directly would fail to start squad_state. Two real bugs found while testing, both of which CI would have hit: 1. GNU tar cannot read zip archives; bsdtar (Windows, macOS) can. The release workflow builds every target on ubuntu-latest, so every win32 bundle would have failed to unpack its Node runtime. Now uses unzip on posix hosts, and the workflow installs it for win32 targets. 2. execFileSync with shell:true splits an unquoted command path on spaces, so invoking process.execPath from "C:\Program Files\nodejs" failed. The run() helper now quotes. Verified: - squad.exe, squad.cmd and squad.ps1 all return 0.11.0 from a real bundle - squad init from squad.exe scaffolds 5 agents and writes an npx-free .mcp.json pointing at the bundle launcher - SEA blob is version-locked: a blob built by Node 23.5 injected into the vendored 22.20.0 produced an exe that died with an access violation (0xC0000005). The builder now downloads a matching host Node to generate it. - Cross-build works: a Linux container unzipped the Windows runtime, generated the blob with Linux Node 22.20.0, injected it with postject, and the resulting PE ran correctly on Windows. That is the production path, since the workflow builds all six targets on ubuntu-latest. - Generator output parses as YAML, all three winget manifests agree on identifier/version/schema, and both installers point at .exe - 67 tests pass (18 new for the generator, 8 new for the exe and extraction) Dropped the shebang from generate-packaging.mjs so vitest can import it on Windows; scripts/pr-readiness.mjs, the repo's other importable script, has no shebang either. Worth noting separately: the same shebang interaction is why test/promote-insider-tag.test.ts is one of the pre-existing Windows failures. Left alone as out of scope. Not verified: no winget or Homebrew submission has been made, because neither can be tested without a published release carrying bundle assets. Code signing and notarization are still absent, and injecting the SEA blob invalidates the Node runtime's Authenticode signature, so SmartScreen will warn on Windows. That is the main gap before either package should be recommended widely. Refs #1593 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf * fix(ci): suppress shellcheck SC2016 on Actions expression in release workflow The single-quoted backticks are intentional markdown formatting in the step summary, not shell expressions. Disable SC2016 for those lines. Closes #1611 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Tamir Dresher <342800+tamirdresher@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Brady Gaster <41929050+bradygaster@users.noreply.github.com> Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
Distribution surface for Squad's agentic workflows: - workflows/squad.md: unified /squad slash command (stub) - workflows/shared/squad.md: bootstrap component (installs Squad, bundles state) Uses squad-init composite action (from #1610) instead of npx. Frontmatter is complete; implementation bodies are stubs pending #1613-#1617. Closes #1620 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
Second slice of the standalone distribution work, following #1595 (now merged). Adds the pieces that turn the bundle mechanism into something you can actually consume: a composite action, a container image, and a fix for a gap that made the first slice incomplete.
Thanks for merging #1595, @bradygaster. This is rebased onto current
dev, so it targetsdevdirectly rather than stacking.Why
While verifying what
squad initactually produces, I found the first slice only solved half the problem.Installing from a bundle removed npm from installation. But
squad initstill wrote this into.mcp.json:{ "command": "npx", "args": ["-y", "@bradygaster/squad-cli@0.11.0", "state-mcp"] }…and called
registry.npmjs.orgto decide which version to pin. So on a firewalled machine the CLI would install cleanly and then thesquad_stateMCP server would silently fail to launch on first use. That's exactly the failure this work exists to prevent, and the npm-free install would have shipped looking complete while being broken.How
Tier-0 in the MCP resolver.
resolveSquadStateMcpSpecgains a tier ahead of the two npx ones. WhenSQUAD_STANDALONE_HOMEis set — exported by the bundle launchers — it emits the bundle's own launcher:{ "command": "/opt/squad/squad", "args": ["state-mcp"] }Two deliberate details:
PATHnor that env var is guaranteed.describeMcpSpecbug caught on the way. It readargs[1]to name the package. Standalone specs have nothing there, so the success message would have printed<unknown>. Now reports the launcher path.squad-initcomposite action. Installs from a release bundle and runs init — the direct replacement fornpx @bradygaster/squad-cli initin the gh-aw activation job. It also fails the step if the resulting.mcp.jsonstill mentions npx, so this can't silently regress.Dockerfile. Built from a bundle rather than
npm install -g, following the contract already documented inreference/container-image.md(WORKDIR/app, non-root uid 1001, SIGTERM,squad watch --execute). Uses--include-optionalbecause that contract documents OTLP export, which lives in the optional OpenTelemetry packages.Verification
Container, on linux/amd64:
squad --versionas non-rootsquaduser0.11.0which squad/opt/squad/squadsquad init→.mcp.jsoncommand/opt/squad/squad— no npxsquad state-mcpJSON-RPCinitialize{"serverInfo":{"name":"squad-state",...}}That last one is the one I cared about: the MCP server completes a real handshake inside a container that cannot reach npm.
Also: 13 new tests (23/23 in
mcp-spec-init.test.ts) · bundle on win32-x64 writescommand=<bundle>\squad.cmd·npm run lint✅ ·docs-build,mcp-root-write,state-mcp✅ · cspell + markdownlint ✅What is NOT verified
The Dockerfile builder stage has never run. My machine intercepts TLS, so every container fails to reach
registry.npmjs.org:That's environmental, not a Dockerfile defect — and there's some irony in being blocked by the exact firewall condition this PR addresses. But it means the full image has never been built end to end, and CI needs to prove it. I verified the runtime stage by feeding it a host-built bundle, which covers every line where I made a decision, but not the
npm ci+ build layer.The
squad-initaction has never run either — it downloads a release asset, and no release with bundle assets exists yet. Same chicken-and-egg as #1595.Changeset
Included (
patchforsquad-cli) — unlike #1595, this slice does touchpackages/squad-cli/src/..changeset/standalone-mcp-spec-1593.md)PR Readiness Checklist
Branch & Commit
devdev(rebased onto543d0eac)Build & Test
npm run buildpassesnpm test— 41 test files fail identically on cleandevon Windows (pre-existing path-separator issues, measured in feat(dist): standalone npm-free bundles for squad-cli (#1593) #1595). This branch adds none. Deferring to CI.npm run lintpassesnpm run lint:eslintpasses (0 errors)Docs
Exports
Breaking Changes
None. The npx tiers are untouched when
SQUAD_STANDALONE_HOMEis unset, which is every existing npm install.Waivers
None requested. Flagging rather than waiving: the Dockerfile builder stage and the composite action are both unproven end to end, for the environmental reasons above. I would rather that be stated plainly than have CI be the first place anyone finds out.