Files
msd-core/docs/how-to/diagnose-a-foreign-gsd-tools.md
Tom Boucher 36375513b9 feat(#3840): generate docs/FEATURES.md from per-feature fragments (#3845)
* feat(#3840): generate docs/FEATURES.md from per-feature fragments

docs/FEATURES.md was hand-maintained, and every feature PR wrote into two
shared mutable cells: the '### N.' heading whose integer was hand-allocated at
authoring time, and the hand-maintained table of contents. Concurrent PRs all
picked the same next integer, and two PRs adding differently numbered features
still collided on the TOC. #3831 was renumbered 165 -> 166 -> 167 -> 168 across
successive rebases, each collision also costing a full matrix verification run
because the sha-keyed pass marker dies with the rebase.

Mechanism: one fragment per feature at docs/features/<slug>.md carrying
id/title/group (and an optional order) in frontmatter, consolidated by
scripts/gen-features.cjs --write|--check into a marker-delimited region of
docs/FEATURES.md that holds BOTH the TOC and every section body. Group headings
and their order are derived too - a group sorts by its lowest-ordered member -
so there is no shared registry to edit either; optional per-group prose lives in
docs/features/_groups/<slug>.md. A contributor adds exactly one new file.
Wired into regen:derived and lint:generated-sync alongside the eight existing
generators, matching gen-adr-index.cjs's CLI shape and typed-REASON reporting.

Migration froze all 168 existing numbers verbatim: identical section set,
identical order, identical bodies. Two defects found in the tree are fixed
inline rather than carried forward - the '## Related' block had been spliced
into the middle of the document, orphaning §142's Reference line, and four
inbound anchors were already broken on next (FEATURES.md#runtime-identity in
two files, and #143-spec-phase-edge-completeness-probe off by one). Since the
repo has no link checker, --check now validates every inbound
FEATURES.md#anchor by resolved target, so that class cannot ship silently
again; locale FEATURES.md files resolve elsewhere and stay out of scope.

Refs #3840

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

* fix(#3840): carry upstream §69 delta into its fragment and harden the generator

Review found section 69 missing '[--strict]' and REQ-STATE-05/06 versus
origin/next. Root cause was a stale base, not extraction loss: those lines
landed in 394bf384b (#3844) AFTER this branch forked at 63abcface, and
'git diff 63abcface origin/next -- docs/FEATURES.md' is exactly that hunk.
Merging origin/next auto-applied the hunk into the GENERATED region, which
--check immediately reported as stale; the delta is now carried in
docs/features/statemd-consistency-gates.md and regenerated from there.

--write is now fail-closed. It previously rendered the region even with
violations outstanding, warning only on stderr and exiting 0, so a
'--write && git commit' chain could commit a FEATURES.md carrying two
colliding sections. It now refuses and exits 1; --force is the explicit
override and says so in the report. The test that pinned the old behavior now
pins the refusal, plus the --force override and its scoping.

Marker forgery is rejected at two layers. A fragment body containing
'<!-- FEATURES:START' or '<!-- FEATURES:END' is a typed
body_forges_region_marker violation (fragments and group notes alike), and
spliceIntoFeatures anchors the end boundary with lastIndexOf instead of
indexOf, so a marker that reaches the document by any other route can only
make the generated region grow, never shrink. Matching is on marker PREFIXES,
so a decorated variant comment cannot slip past.

Symlinked corpus entries are refused with a typed dirent_not_regular_file
rather than read. A fork PR could otherwise commit docs/features/evil.md as a
symlink to any readable path and have the generator inline those bytes into
the committed docs/FEATURES.md on the next regen.

Equivalence re-verified with a method that cannot cancel out. The first
check extracted both operands with the same body-normalising helper, so
anything that helper dropped was dropped on both sides. The replacement runs
two independent passes: a global content-line multiset diff with no
per-section logic at all (0 gained, 19 lost, all 19 the stale hand-written
mini-TOC links this change deliberately deletes), and a per-section
byte-exact body diff carrying a coverage assertion that fails loudly per file
when the extractor accounts for fewer lines than the file contains. That
assertion caught two blind spots in the checker itself. 168/168 sections
present, order identical, one intended body difference (§142 regains the
Reference line orphaned by the misplaced '## Related' block).

Refs #3840

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

* chore(#3840): backfill changeset PR number

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 22:49:00 -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.