* 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>
This commit is contained in:
@@ -2121,6 +2121,46 @@ node gsd-tools.cjs state complete-phase --phase 3
|
||||
|
||||
---
|
||||
|
||||
### `runtime-identity`
|
||||
|
||||
Report the package coordinates of the `gsd-tools` that is executing. Used by the runtime
|
||||
launcher preamble to confirm a shipped workflow reached this package's tool rather than a
|
||||
different package that also provides a `gsd-tools` binary.
|
||||
|
||||
**Prerequisites:** none — it reads no project state and needs no resolvable project root
|
||||
**Produces:** a JSON identity payload on stdout
|
||||
|
||||
```bash
|
||||
node gsd-tools.cjs runtime-identity
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"packageName": "@opengsd/gsd-core",
|
||||
"version": "1.12.0"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Value |
|
||||
|---|---|---|
|
||||
| `packageName` | string | Always `@opengsd/gsd-core`. Baked at build time from `package.json`, so it survives an installed tree that carries no real `package.json`. |
|
||||
| `version` | string | The installed host version. Falls back to `0.0.0` when neither `gsd-core/VERSION` nor a runtime-root `package.json` is readable. |
|
||||
|
||||
`--raw` emits the same payload on a single line.
|
||||
|
||||
The payload is additive-only: consumers must ignore unrecognized keys. `version` is
|
||||
reported but is **not** asserted by the launcher check — identity alone determines whether
|
||||
the check passes, so a `0.0.0` development tree still verifies.
|
||||
|
||||
This is a **manual diagnostic**. Nothing invokes it automatically; the launcher prevents the
|
||||
package-collision failure structurally instead, by resolving `gsd_run` rather than `gsd-tools`
|
||||
on `PATH`.
|
||||
|
||||
See [Diagnose which gsd-tools is running](how-to/diagnose-a-foreign-gsd-tools.md) for using it,
|
||||
and [Runtime identity](FEATURES.md#runtime-identity) for the rationale.
|
||||
|
||||
---
|
||||
|
||||
## Community Commands
|
||||
|
||||
### Community Hooks
|
||||
|
||||
@@ -3606,3 +3606,20 @@ See [Resolve verify-command path findings](how-to/resolve-verify-command-path-fi
|
||||
- The adjacent **vacuous pass** — a command that runs successfully and asserts nothing, such as a test-name filter matching zero tests and exiting 0 — is a distinct problem and is explicitly out of scope.
|
||||
|
||||
See [State a failing direction](how-to/state-a-failing-direction.md) and [`gsd-tools check verify-failure-directions`](COMMANDS.md#gsd-tools-check-verify-failure-directions).
|
||||
---
|
||||
|
||||
### 168. Runtime Identity
|
||||
|
||||
**Purpose:** The predecessor package `get-shit-done-cc` publishes a binary named `gsd-tools`, and so does this one. They answer some of the same verb names with **different semantics**. [#3129](https://github.com/open-gsd/gsd-core/issues/3129) is the worked example: `phases.clear` **archives** here and **deletes** there. Both print success-shaped output, and `.planning/` is gitignored by default, so a user lost 43 phase directories with no error, no warning, and nothing recoverable from git. The failure was silent in both directions — the workflow could not tell it had reached the wrong handler, and the handler could not tell it had been called by a workflow written for a different contract (#3146).
|
||||
|
||||
**Behavior:** the launcher's `PATH` resolution branch now looks for **`gsd_run`** instead of `gsd-tools`. Only this package publishes `gsd_run`; the predecessor publishes `gsd-tools` and `gsd-sdk`. Our `gsd_run` follows its own symlink chain and executes the `gsd-tools.cjs` sitting **beside it**, so resolving it cannot land on a foreign handler. Separately, `gsd-tools runtime-identity` reports this runtime's package coordinates, so a human or a support thread can settle "which tool am I actually running?" in one command.
|
||||
|
||||
**This eliminates the failure rather than reporting it.** An earlier iteration of this work asserted identity inside the shared launcher preamble and warned when it could not be confirmed. That approach was abandoned for two reasons. First, a warning only helps a reader who acts on it. Second, and decisively, the preamble is inlined into 113 shipped files, several of which sit within **single-digit bytes** of frozen size ceilings — `agents/gsd-verifier.md` had 2 bytes of headroom — and those caps are red lines, not budgets. The resolver change is **smaller than what it replaces**, so every one of those files got slightly *further* from its ceiling.
|
||||
|
||||
**It fails closed.** If no `gsd_run` is reachable, the resolver falls through its remaining path-based branches and finally errors with an install command. It does not fall back to executing whatever `gsd-tools` happens to be on `PATH` — that fallback *was* the vulnerability.
|
||||
|
||||
**A doubly-sourced preamble cannot build a recursive launcher.** `command -v gsd_run` finds the shell *function* on a second source and would return the bare string `gsd_run`; an executability guard rejects a non-path result, so the function can never be defined in terms of itself.
|
||||
|
||||
**Known limits:** an installation of this package old enough to predate `bin/gsd_run` ([#381](https://github.com/open-gsd/gsd-core/issues/381)) is no longer reachable through the `PATH` branch and must be upgraded or invoked through one of the path-based branches. The `runtime-identity` verb is a manual diagnostic, not an automatic gate — nothing currently asserts identity on every invocation, which remains open for a future release now that the byte budget is understood. Path-based resolution branches are unchanged and still trust their configured location.
|
||||
|
||||
**Reference:** [`runtime-identity`](COMMANDS.md#runtime-identity) · [Diagnose which gsd-tools is running](how-to/diagnose-a-foreign-gsd-tools.md)
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
"runtime-config-adapter-registry.cjs",
|
||||
"runtime-homes.cjs",
|
||||
"runtime-hooks-surface.cjs",
|
||||
"runtime-identity.cjs",
|
||||
"runtime-name-policy.cjs",
|
||||
"runtime-slash.cjs",
|
||||
"schema-detect.cjs",
|
||||
|
||||
@@ -623,6 +623,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `runtime-hooks-surface.cjs` | Runtime hooks surface module — standalone hook-surface writer functions extracted from bin/install.js (ADR-857 phase 5f-1); owns Cline/Cursor/Copilot/Codex hook artifact generation and reconciliation. |
|
||||
| `runtime-name-policy.cjs` | Runtime name normalization policy — canonical token sanitization for runtime identifiers used in path construction and display |
|
||||
| `runtime-homes.cjs` | Canonical runtime → global config/skills directory mapping; first-class support for all 15 runtimes including Hermes nested layout and Cline rules-based exclusion (#3126) |
|
||||
| `runtime-identity.cjs` | Runtime identity surface (#3146) — exports `classifyIdentityProbe` (pure, total; `(stdout, exitCode, spawnFailed, timedOut)` → `ok`/`identity_mismatch`/`no_identity_verb`/`unparseable`/`probe_failed`), `buildIdentityPayload`, and `explainVerdict`; backs the `runtime-identity` verb, a manual diagnostic for telling this package's `gsd-tools` apart from the predecessor's colliding binary. The collision itself is prevented structurally by the launcher resolving `gsd_run` rather than `gsd-tools` on `PATH` |
|
||||
| `runtime-slash.cjs` | Runtime-aware slash-command formatter — single source of truth for emitting `/gsd-<cmd>` (skills-based runtimes) and `$gsd-<cmd>` (codex) in user-facing output and persisted artifacts (#3584) |
|
||||
| `schema-detect.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, Supabase, TypeORM, Payload); exports `detectSchemaFiles`, `detectSchemaOrm`, `checkSchemaDrift`, `SCHEMA_PATTERNS`, `ORM_INFO` |
|
||||
| `secrets.cjs` | Secret-config masking convention (`****<last-4>`) for integration keys; exports `SECRET_CONFIG_KEYS`, `isSecretKey`, `maskSecret`, `maskIfSecret` |
|
||||
|
||||
@@ -29,6 +29,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [State a failing direction](how-to/state-a-failing-direction.md) — say what output constitutes failure for an `<automated>` verify command, and migrate a phase planned before the rule
|
||||
- [Resolve a contract-drift finding](how-to/resolve-contract-drift-findings.md) — bring an agent's completion contract, read-tag gate, or deleted-file test reference back into agreement with the registry
|
||||
- [Resolve unreachable-guard findings](how-to/resolve-unreachable-guard-findings.md) — fix shell guards whose fallback arm cannot run, and tell "nothing to report" apart from "could not look"
|
||||
- [Diagnose which gsd-tools is running](how-to/diagnose-a-foreign-gsd-tools.md) — tell this package's tool apart from the predecessor's colliding binary and from a gsd-core too old to identify itself
|
||||
- [Resolve an ESLint glob-coverage finding](how-to/resolve-eslint-coverage-findings.md) — bring a source file that matches no lint rule under coverage, or record a reasoned exemption
|
||||
- [Read the statusline freshness marker](how-to/read-the-statusline-freshness-marker.md) — turn on `state ~N commits back`, and tell "STATE.md is fresh" apart from "freshness could not be established"
|
||||
- [Consume the planning snapshot](how-to/consume-the-planning-snapshot.md) — read `planning inspect` from a dashboard or harness, and tell "nothing to report" apart from "could not look"
|
||||
|
||||
130
docs/how-to/diagnose-a-foreign-gsd-tools.md
Normal file
130
docs/how-to/diagnose-a-foreign-gsd-tools.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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](https://github.com/open-gsd/gsd-core/issues/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
|
||||
|
||||
```bash
|
||||
gsd-tools runtime-identity
|
||||
```
|
||||
|
||||
A healthy GSD runtime prints:
|
||||
|
||||
```json
|
||||
{
|
||||
"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](#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](#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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
npm uninstall -g get-shit-done-cc
|
||||
```
|
||||
|
||||
If you need both installed, put this package's bin directory first on `PATH`:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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](https://github.com/open-gsd/gsd-core/issues/381)). Upgrade:
|
||||
|
||||
```bash
|
||||
npm install -g @opengsd/gsd-core@latest
|
||||
```
|
||||
|
||||
Confirm the binary is now present:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
## Related
|
||||
|
||||
- [Runtime identity](../FEATURES.md#runtime-identity) — why the launcher resolves `gsd_run`
|
||||
rather than asserting identity at every call
|
||||
- [`runtime-identity`](../COMMANDS.md#runtime-identity) — the verb's exact output
|
||||
- [#3129](https://github.com/open-gsd/gsd-core/issues/3129) — the incident this prevents
|
||||
Reference in New Issue
Block a user