Files
msd-core/docs/how-to/diagnose-a-foreign-gsd-tools.md
Tom Boucher 63abcface9 feat(#3146): resolve gsd_run so workflows cannot reach a foreign gsd-tools (#3831)
* feat(#3146): resolve gsd_run so workflows cannot reach a foreign gsd-tools

The predecessor package get-shit-done-cc publishes a colliding gsd-tools bin whose phases.clear DELETES where this package's ARCHIVES, and both print success-shaped output against a gitignored .planning/ -- which is how #3129 cost a user 43 phase directories with no error and nothing recoverable from git.

The launcher's PATH branch now resolves gsd_run, published only by this package and self-locating via its own symlink chain to the sibling shim, instead of the colliding gsd-tools. A foreign handler becomes unreachable from PATH, and when no gsd_run is reachable the resolver fails closed rather than falling back -- that fallback was the vulnerability. This is smaller than the branch it replaces, which matters: the preamble is inlined into 113 shipped files and agents/gsd-verifier.md sits 2 bytes under a red-line size cap.

unset -f gsd_run leads the preamble so a re-source is idempotent. Without it, command -v finds the shell function, returns a bare name, and the resolver falls through to an exit 1 that kills a sourced caller's shell.

Adds gsd-tools runtime-identity, a manual diagnostic reporting this runtime's package coordinates over the baked package-identity (#498) and readHostVersion, with a strict total classifier: only a JSON object with an exact packageName verifies, since JSON.parse admits 0/"str"/[]/null/true.

An inlined identity assertion was built and reviewed first, then withdrawn -- it breaks five frozen size ceilings and no assertion fits in 2 bytes.

Closes #3146

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3146): stop sync:launcher relocating a deliberate preamble placement

Pre-existing defect, surfaced by this PR because sync is a no-op unless the snippet content actually changes. transformFile inserts the preamble into the first block that CALLS gsd_run, but gsd-core/workflows/explore.md deliberately places it in a bootstrap-only block that DEFINES gsd_run without calling it -- its own comment explains why: declining the research offer must not leave Step 5's commit call unbootstrapped. Stripping empties that block of calls, so the preamble migrated forward and broke the define-before-use invariant tests/explore-command.test.cjs pins.

Reproduced on a pristine origin/next checkout with the base snippet and base file, so this was not introduced here. The insertion target now honours a block that already carried the preamble, falling back to the first calling block for files that have none yet. Adds a behavioral regression test over a two-block fixture.

Also updates three runtime-launcher-parity tests that pinned the removed PATH fallback to gsd-tools. Their intent is preserved -- the PATH stub is renamed gsd_run so it is reachable by the new resolver, and the RUNTIME_DIR-wins test still asserts the stub is never invoked. Fixture shebangs move to an absolute /bin/sh, because the fixture PATH is deliberately restricted and #!/usr/bin/env sh could not resolve.

Refs #3146

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3146): backfill changeset PR number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#3146): document the FEATURES.md section-numbering practice

The monotonically increasing section number in docs/FEATURES.md is the most frequent merge-conflict source in this repo, and it has TWO conflict cells, not one: the ### N. heading and the hand-maintained table of contents. Two PRs adding differently numbered features still collide on the TOC, so renumbering alone does not make a branch safe. This branch alone was renumbered 165 -> 166 -> 167 -> 168 across successive rebases.

Adds a CONTRIBUTING section stating the practice: allocate the number last, never pre-emptively renumber, take max+1 after a rebase and update the TOC in the same commit, and never renumber someone else's section. Fork contributors are told explicitly they may leave the number to a maintainer at merge rather than chasing the counter. Agents are told to lease the allocation and to include the file in their published touched set.

Records the durable fix as planned rather than pretending it exists: FEATURES.md should be generated from per-feature fragments the way CHANGELOG.md is generated from .changeset/, and the way tests/emitted-drift-acks/ works (#2914).

Also renumbers this branch's own section to 168, leaving 167 to the PR already in flight.

Refs #3146

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:57:16 -04:00

4.5 KiB

How to diagnose which gsd-tools is running

Two packages publish a binary called gsd-tools: this one, and the predecessor get-shit-done-cc. They answer some of the same verb names with different behavior. This guide tells you which one you have and how to fix a bad resolution.

Use it when:

  • a workflow behaves unlike its documentation,
  • a workflow stops with gsd-tools.cjs not found … and gsd_run is not on PATH,
  • or you simply want to confirm which tool a project is running against.

Why this matters

#3129 is the worked example. phases.clear archives your phase directories under this package and deletes them under the predecessor. Both print success-shaped output, and .planning/ is gitignored by default, so the difference is invisible until the directories are gone.

Shipped workflows no longer resolve gsd-tools from PATH at all — they resolve gsd_run, which only this package publishes. That closes the path that caused #3129. The steps below are for confirming your setup and for fixing the cases the resolver now refuses.

Ask the tool what it is

gsd-tools runtime-identity

A healthy GSD runtime prints:

{
  "packageName": "@opengsd/gsd-core",
  "version": "1.12.0"
}

Three other outcomes are possible, and they mean different things:

What you see What it means
The JSON above This is our tool. Nothing to do.
A usage screen mentioning gsd-sdk, exit 1 This is the predecessor's binary. See A different package owns it.
Error: Unknown command: runtime-identity This is our tool, but older than the verb. See It is an old gsd-core.
Nothing runs at all Nothing named gsd-tools is on your PATH. That is fine — workflows do not need it.

Then find out which package owns the file:

readlink -f "$(command -v gsd-tools)"

A different package owns it

The resolved path contains get-shit-done-cc, or the identity probe printed a gsd-sdk usage screen.

Workflows will not reach it — they resolve gsd_run, which that package does not publish — so this is no longer dangerous. It is still worth resolving, because you invoking gsd-tools by hand will reach the wrong tool.

If you no longer use the predecessor:

npm uninstall -g get-shit-done-cc

If you need both installed, put this package's bin directory first on PATH:

export PATH="$(npm prefix -g)/bin:$PATH"

Re-run gsd-tools runtime-identity and confirm it reports @opengsd/gsd-core.

It is an old gsd-core

The resolved path is inside @opengsd/gsd-core, but runtime-identity is not a known command. That version predates the verb. Nothing is wrong beyond being out of date:

npm install -g @opengsd/gsd-core@latest

A workflow says gsd_run is not on PATH

The resolver looked for gsd_run and found nothing, and none of the path-based locations matched either. It stops rather than guessing — falling back to an arbitrary gsd-tools is exactly the behavior that caused #3129.

This is expected in one specific case: an installation old enough to predate the gsd_run binary (#381). Upgrade:

npm install -g @opengsd/gsd-core@latest

Confirm the binary is now present:

command -v gsd_run

If you deliberately pin an old version, invoke workflows from a project or config directory where the path-based resolution branches apply — a local install under gsd-core/bin/gsd-tools.cjs, or your runtime's config directory — rather than relying on PATH.

Tell "not installed" apart from "wrong one installed"

These two look similar and have opposite fixes:

command -v gsd_run   || echo "gsd_run:   NOT FOUND"
command -v gsd-tools || echo "gsd-tools: NOT FOUND"
  • gsd_run found — workflows resolve correctly, whatever gsd-tools says.
  • gsd_run missing, gsd-tools present — the likely collision case. Run readlink -f "$(command -v gsd-tools)" and follow the matching section above.
  • Both missing — nothing is installed globally; workflows will use a local or config-directory install if one exists.