refactor: hard-fork GSD -> MSD (Make Software Done)

Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
Jakub Zych
2026-10-06 01:47:40 +02:00
parent fe069b2a56
commit a9a7a328e6
2763 changed files with 78465 additions and 78434 deletions

View File

@@ -1,11 +1,11 @@
# Contributing to GSD Core
# Contributing to MSD Core
## Getting Started
```bash
# Clone the repo
git clone https://github.com/open-gsd/gsd-core.git
cd gsd-core
git clone https://github.com/golem15com/msd-core.git
cd msd-core
# Activate the pinned Node version from .nvmrc
nvm use
@@ -31,7 +31,7 @@ environment validator, daily commands, and troubleshooting.
## Types of Contributions
GSD accepts three types of contributions. Each type has a different process and a different bar for acceptance. **Read this section before opening anything.**
MSD accepts three types of contributions. Each type has a different process and a different bar for acceptance. **Read this section before opening anything.**
### 🐛 Fix (Bug Report)
@@ -72,11 +72,11 @@ A feature adds something new — a new command, a new workflow, a new concept, a
**Process:**
1. **Discuss first** — check [Discussions](https://github.com/open-gsd/gsd-core/discussions) to see if the idea has been raised. If it has and was declined, don't open a new issue.
2. Open a [Feature Request issue](https://github.com/open-gsd/gsd-core/issues/new?template=feature_request.yml) with the complete spec. The template requires: the solo-developer problem being solved, what is being added, full scope of affected files and systems, user stories, acceptance criteria, and assessment of maintenance burden.
3. **Wait for maintainer approval.** A maintainer must label the issue `approved-feature` before you write a single line of code. Approval is not guaranteed — GSD is intentionally lean and many valid ideas are declined because they conflict with the project's design philosophy.
3. **Wait for maintainer approval.** A maintainer must label the issue `approved-feature` before you write a single line of code. Approval is not guaranteed — MSD is intentionally lean and many valid ideas are declined because they conflict with the project's design philosophy.
4. Write the code. Implement exactly the approved spec. Changes to scope require re-approval.
5. Open a PR using the [Feature PR template](.github/PULL_REQUEST_TEMPLATE/feature.md) — link the approved issue.
**Rejection reasons:** Issue not labeled `approved-feature`, spec is incomplete, scope exceeds what was approved, feature conflicts with GSD's solo-developer focus, maintenance burden too high.
**Rejection reasons:** Issue not labeled `approved-feature`, spec is incomplete, scope exceeds what was approved, feature conflicts with MSD's solo-developer focus, maintenance burden too high.
---
@@ -120,7 +120,7 @@ PRs that arrive without a properly-labeled linked issue are closed automatically
## Where Do I Open My PR? (Branching Model)
GSD uses two long-lived branches: `main` (production, what's on npm `@latest`)
MSD uses two long-lived branches: `main` (production, what's on npm `@latest`)
and `next` (integration for the upcoming release). **Almost every PR targets
`next`.** Full guide: [`docs/branching.md`](docs/branching.md).
@@ -142,7 +142,7 @@ git checkout next
git pull --ff-only origin next
git checkout -b fix/3187-config-corruption
# ... commit, push
gh pr create --base next --repo open-gsd/gsd-core
gh pr create --base next --repo golem15com/msd-core
```
If you target the wrong branch by accident, the `PR Target Validator`
@@ -155,7 +155,7 @@ another PR to `next` lands — so in practice you rebase much less.
**But `next` does still require "up-to-date before merging".** Branch
protection has `required_status_checks.strict = true`; check it yourself with
`gh api repos/open-gsd/gsd-core/branches/next/protection --jq '.required_status_checks.strict'`.
`gh api repos/golem15com/msd-core/branches/next/protection --jq '.required_status_checks.strict'`.
If another PR lands while yours is open, yours goes `BEHIND` and must be
rebased before it can merge.
@@ -192,7 +192,7 @@ Contributor requirements (summary):
- **Use the correct PR template** — there are separate templates for [Fix](.github/PULL_REQUEST_TEMPLATE/fix.md), [Enhancement](.github/PULL_REQUEST_TEMPLATE/enhancement.md), and [Feature](.github/PULL_REQUEST_TEMPLATE/feature.md). Using the wrong template or using the default template for a feature is a rejection reason.
- **Link with a closing keyword** — use `Closes #123`, `Fixes #123`, or `Resolves #123` in the PR body. The CI check will fail and the PR will be auto-closed if no valid issue reference is found.
- **Test-only and docs-only follow-up PRs may reference without closing.** If your PR is documentation or regression coverage only — say, a repo-wide guard for a fix that already shipped — and there is no open issue for it to close, use a non-closing reference instead: `Refs #123`. `Ref`, `Refs`, `References`, `Relates to`, `Related to`, and `Follow-up to` are all accepted in that position. Do **not** write a closing keyword against an already-closed issue to satisfy the check; on merge it closes nothing, and it trains readers to treat closing keywords as decorative.
- **Qualifying diff shape:** every changed file must be under `tests/`, under `docs/`, or a root-level `*.md` (`README.md`, `CONTRIBUTING.md`, …). This mirrors the doc-only classification the push gate already uses, and it is deliberately root-only — markdown under a subdirectory (`gsd-core/workflows/*.md`, `agents/*.md`, `commands/**/*.md`) is runtime-loaded text, not documentation, so it still requires a closing keyword. `CHANGELOG.md` is excluded too: edit it through a `.changeset/` fragment, never directly.
- **Qualifying diff shape:** every changed file must be under `tests/`, under `docs/`, or a root-level `*.md` (`README.md`, `CONTRIBUTING.md`, …). This mirrors the doc-only classification the push gate already uses, and it is deliberately root-only — markdown under a subdirectory (`msd-core/workflows/*.md`, `agents/*.md`, `commands/**/*.md`) is runtime-loaded text, not documentation, so it still requires a closing keyword. `CHANGELOG.md` is excluded too: edit it through a `.changeset/` fragment, never directly.
- This weaker form is accepted **only** for that diff shape. A PR touching anything else still needs a closing keyword, and a PR with no issue reference at all still fails. On a very large PR (more than 100 changed files) the check cannot confirm the diff shape and falls back to requiring a closing keyword.
- **One concern per PR** — bug fixes, enhancements, and features must be separate PRs
- **No drive-by formatting** — don't reformat code unrelated to your change
@@ -206,14 +206,14 @@ Contributor requirements (summary):
```bash
npm run changeset -- --type Fixed --pr <YOUR_PR_NUMBER> \
--body "**\`/gsd-foo\` no longer drops trailing slashes** — explain the user-visible change."
--body "**\`/msd-foo\` no longer drops trailing slashes** — explain the user-visible change."
```
This writes `.changeset/<adjective>-<noun>-<noun>.md`. Three random words → concurrent PRs never collide. Allowed `type:` values follow [Keep a Changelog](https://keepachangelog.com/): `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
Fragments are consolidated into `CHANGELOG.md` at release time by the release workflow. See [`.changeset/README.md`](.changeset/README.md) for the format spec and [#2975](https://github.com/open-gsd/gsd-core/issues/2975) for the rationale.
**CI enforcement:** the `Changeset Required` workflow (`scripts/changeset/lint.cjs`) fails any PR that touches `bin/`, `gsd-core/`, `src/`, `agents/`, `commands/`, `hooks/`, or `sdk/src/` without a `.changeset/*.md` fragment. (`src/` is the TypeScript source of truth compiled into `gsd-core/bin/lib/*.cjs`, so editing it is a user-facing change even though the generated `.cjs` is gitignored and never appears in the diff.)
**CI enforcement:** the `Changeset Required` workflow (`scripts/changeset/lint.cjs`) fails any PR that touches `bin/`, `msd-core/`, `src/`, `agents/`, `commands/`, `hooks/`, or `sdk/src/` without a `.changeset/*.md` fragment. (`src/` is the TypeScript source of truth compiled into `msd-core/bin/lib/*.cjs`, so editing it is a user-facing change even though the generated `.cjs` is gitignored and never appears in the diff.)
> **Running it locally.** The lint derives its changed-file set from `GITHUB_BASE_REF`, which only CI sets. `node scripts/changeset/lint.cjs` on a developer machine therefore does **not** evaluate your branch and can report success on a PR that CI will fail. Pass the base explicitly to reproduce the CI result:
>
@@ -238,7 +238,7 @@ To re-format an existing release by hand (e.g. backfilling an older release):
```bash
node scripts/release-notes/format-github-release-notes.cjs \
--tag vX.Y.Z --repo open-gsd/gsd-core --apply
--tag vX.Y.Z --repo golem15com/msd-core --apply
```
Omit `--apply` to print the reformatted body to stdout for review without
@@ -451,16 +451,16 @@ test('does the thing', () => {
Import helpers from `tests/helpers.cjs` instead of inlining temp directory creation:
```javascript
const { createTempProject, createTempGitProject, createTempDir, cleanup, runGsdTools } = require('./helpers.cjs');
const { createTempProject, createTempGitProject, createTempDir, cleanup, runMsdTools } = require('./helpers.cjs');
```
| Helper | Creates | Use When |
|--------|---------|----------|
| `createTempProject(prefix?)` | tmpDir with `.planning/phases/` | Testing GSD tools that need planning structure |
| `createTempProject(prefix?)` | tmpDir with `.planning/phases/` | Testing MSD tools that need planning structure |
| `createTempGitProject(prefix?)` | Same + git init + initial commit | Testing git-dependent features |
| `createTempDir(prefix?)` | Bare temp directory | Testing features that don't need `.planning/` |
| `cleanup(tmpDir)` | Removes directory recursively | Always use in `afterEach` |
| `runGsdTools(args, cwd, env?)` | Executes gsd-tools.cjs | Testing CLI commands |
| `runMsdTools(args, cwd, env?)` | Executes msd-tools.cjs | Testing CLI commands |
### Spawning a subprocess: use the process seam
@@ -710,7 +710,7 @@ You do not need all twelve cases for every PR. You do need to cover the cases th
#### CLI and command routing
Changes to CLI parsing, command dispatch, query dispatch, command routers, `gsd-tools`, or `gsd-sdk` must include a negative input matrix for the affected command family.
Changes to CLI parsing, command dispatch, query dispatch, command routers, `msd-tools`, or `msd-sdk` must include a negative input matrix for the affected command family.
Required cases where relevant:
@@ -763,7 +763,7 @@ Property-style parser tests are encouraged for high-risk parsers. They must be d
This is stricter than the adversarial-input rule above and exists because of it: `tests/fixtures/adversarial/` covers hostile input, but a fixture written by the parser's own author — even a deliberately "realistic" one — is still drawn from the author's mental model of the format. It can only ever confirm what the author already believed, never surface what they didn't anticipate. A property-test generator has the same failure mode one level up: seeding the generator from the writer/render function that produces the same format makes the document shape a constant, so the property can never explore a document the writer wouldn't produce (see the document-shaped vs. writer-seeded property tests in `tests/api-coverage.test.cjs` for a worked example — the writer-seeded one cannot fail against a decoy table; the document-shaped one can).
For a gate whose fixtures come from real user reports, put them under `tests/fixtures/representative/<gate>/` with a `MANIFEST.json` labeling each fixture's source issue and expected gate verdict, and drive them through the gate's real CLI entrypoint (gate-verdict altitude), not the parser function in isolation — see `tests/fixtures/representative/README.md` and `tests/representative-corpus.test.cjs`. If the gate is not yet fixed, do not mark the assertion `{ todo: true }` and do not skip it: this repo's test-runner (`gsd-test` / `gsd-test-runner`) has no concept of node:test's `todo` option — its JSONL result parser only recognizes `kind: "pass" | "fail"`, so a thrown todo-marked test is still counted as a real failure and blocks the push gate. Instead record BOTH the correct target verdict (`expected*`) and the exact current observed verdict (`currentBuggyOutput`) in the manifest, and assert against `currentBuggyOutput` — an honest, non-vacuous characterization of today's known-broken behavior that passes today and breaks loudly the moment the real fix changes the observed output, forcing the assertion to be flipped to `expected*`.
For a gate whose fixtures come from real user reports, put them under `tests/fixtures/representative/<gate>/` with a `MANIFEST.json` labeling each fixture's source issue and expected gate verdict, and drive them through the gate's real CLI entrypoint (gate-verdict altitude), not the parser function in isolation — see `tests/fixtures/representative/README.md` and `tests/representative-corpus.test.cjs`. If the gate is not yet fixed, do not mark the assertion `{ todo: true }` and do not skip it: this repo's test-runner (`msd-test` / `msd-test-runner`) has no concept of node:test's `todo` option — its JSONL result parser only recognizes `kind: "pass" | "fail"`, so a thrown todo-marked test is still counted as a real failure and blocks the push gate. Instead record BOTH the correct target verdict (`expected*`) and the exact current observed verdict (`currentBuggyOutput`) in the manifest, and assert against `currentBuggyOutput` — an honest, non-vacuous characterization of today's known-broken behavior that passes today and breaks loudly the moment the real fix changes the observed output, forcing the assertion to be flipped to `expected*`.
#### Filesystem writes and installers
@@ -825,7 +825,7 @@ Generator tests should run in temp fixtures and assert atomic output behavior. D
```javascript
// BAD — source-grep theater
const configSrc = fs.readFileSync(
path.join(GSD_ROOT, 'gsd-core', 'bin', 'lib', 'config-schema.cjs'), 'utf-8'
path.join(MSD_ROOT, 'msd-core', 'bin', 'lib', 'config-schema.cjs'), 'utf-8'
);
assert.ok(
configSrc.includes("'workflow.plan_bounce'"),
@@ -843,7 +843,7 @@ test('config-set accepts workflow.plan_bounce', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools('config-set workflow.plan_bounce true', tmpDir);
const result = runMsdTools('config-set workflow.plan_bounce true', tmpDir);
assert.ok(result.success, `config-set should accept workflow.plan_bounce: ${result.error}`);
const configPath = path.join(tmpDir, '.planning', 'config.json');
@@ -869,7 +869,7 @@ Some tests legitimately read source files. There are six recognized categories:
| `structural-regression-guard` | A specific code pattern must (or must not) exist to prevent a class of bug (e.g., regex global-state misuse). Behavioral tests cannot distinguish which pattern was used. |
| `docs-parity` | A reference doc must stay in sync with source-defined constants (e.g., `CONFIG_DEFAULTS`). The source is the canonical list; there is no runtime API to enumerate it. |
| `integration-test-input` | A source file is used as a real fixture input to a transformation function under test — the file is not inspected for strings but passed as data. |
| `structural-implementation-guard` | A feature's interception or wiring point is not reachable end-to-end via `runGsdTools`. Used temporarily until a behavioral path exists. |
| `structural-implementation-guard` | A feature's interception or wiring point is not reachable end-to-end via `runMsdTools`. Used temporarily until a behavioral path exists. |
| `pending-migration-to-typed-ir` | **Tracked for correction, not exempted.** Test was identified by the lint as carrying a raw-text-matching pattern that contradicts the rule above. Each annotated file MUST cite the open migration issue (e.g. `// allow-test-rule: pending-migration-to-typed-ir [#NNNN]`) so the tracking is auditable. New tests cannot use this category — they must refactor production to expose typed IR. The annotation is removed when the test is corrected. |
**Suppression is site-scoped, not file-wide.** A marker suppresses only the violation it sits next
@@ -924,7 +924,7 @@ The following are all violations of the same rule:
```javascript
// BAD — substring match on text written by the code under test
const cmdContent = fs.readFileSync(path.join(tmpDir, 'gsd-sdk.cmd'), 'utf8');
const cmdContent = fs.readFileSync(path.join(tmpDir, 'msd-sdk.cmd'), 'utf8');
assert.ok(cmdContent.includes(`@node ${jsonQuoted} %*`), '.cmd embeds shim path');
// BAD — regex match on a child process's human-readable stdout formatter
@@ -959,7 +959,7 @@ Concretely: for any system-under-test that produces text output (a file renderer
#### Concrete example from this repo
`gsd-core/bin/verify-reapply-patches.cjs` exposes a frozen `REASON` enum and emits it through `--json`. Tests assert `report.results[0].reason === REASON.FAIL_USER_LINES_MISSING` rather than regex-matching the human-readable prose. The human formatter exists for operator console output only — tests must not depend on it. Adding a new reason code requires updating the `REASON` enum, the `--json` output, AND the test that locks `Object.keys(REASON).sort()` — three coordinated changes that keep the code surface from drifting from the test surface. A pure builder that returns the IR (no I/O) and a writer that consumes it — `fs.statSync(target).size === Buffer.byteLength(render())` to prove the writer writes what the renderer produces, **without comparing content** — is the same pattern applied to rendered files.
`msd-core/bin/verify-reapply-patches.cjs` exposes a frozen `REASON` enum and emits it through `--json`. Tests assert `report.results[0].reason === REASON.FAIL_USER_LINES_MISSING` rather than regex-matching the human-readable prose. The human formatter exists for operator console output only — tests must not depend on it. Adding a new reason code requires updating the `REASON` enum, the `--json` output, AND the test that locks `Object.keys(REASON).sort()` — three coordinated changes that keep the code surface from drifting from the test surface. A pure builder that returns the IR (no I/O) and a writer that consumes it — `fs.statSync(target).size === Buffer.byteLength(render())` to prove the writer writes what the renderer produces, **without comparing content** — is the same pattern applied to rendered files.
#### Hiding grep behind a function is still grep
@@ -1031,7 +1031,7 @@ For examples of required negative matrices, parser fixtures, filesystem fault in
When you can, run the local test bench harness before opening a PR — especially for Windows-sensitive changes.
- Setup guide: [gsd-test-runner getting started](https://github.com/open-gsd/gsd-test-runner/blob/main/docs/getting-started.md)
- Setup guide: [msd-test-runner getting started](https://github.com/open-gsd/gsd-test-runner/blob/main/docs/getting-started.md)
- Preferred PR evidence: include the bench results summary (or artifact link) in your PR body.
This gives maintainers a faster, higher-confidence signal than CI-only validation.
@@ -1045,15 +1045,15 @@ sources it feeds, run:
npm run check:alias-drift
```
This verifies the built alias artifacts under `gsd-core/bin/lib/` agree with their
This verifies the built alias artifacts under `msd-core/bin/lib/` agree with their
source of truth — each family's `*_SUBCOMMANDS` list must match the `subcommand`
values derived from its `*_COMMAND_ALIASES` table, in order, and each router must
reference its own list. The surface is enumerated once in
`scripts/lib/alias-drift-families.cjs`.
### Editing shipped content (gsd-core/workflows, references, templates, contexts, agents/, commands/gsd/)
### Editing shipped content (msd-core/workflows, references, templates, contexts, agents/, commands/msd/)
Editing the content of a copied shipped file — a `gsd-core/workflows/*.md`, an agent, a
Editing the content of a copied shipped file — a `msd-core/workflows/*.md`, an agent, a
command definition — requires **zero manual fixture regeneration**. There is no
committed path→hash manifest or per-file size baseline to update by hand; the
differential attribution check (`tests/emitted-attribution.test.cjs`, ADR-2719) computes
@@ -1065,12 +1065,12 @@ a converter change, for example — go through a **commit trailer on one of your
commits** (ADR-3942; name the key, say why):
```
Emitted-Drift-Ack-Hash: skills/gsd-add-tests/SKILL.md — the converter rewrote every skill header
Emitted-Drift-Ack-Hash: skills/msd-add-tests/SKILL.md — the converter rewrote every skill header
Emitted-Drift-Ack-Growth: explore.md — new dispatch section, reasoning ships with the block
```
See `CONTEXT.md`'s `### Emitted Artifact Provenance` entry for the full model. Growth in a
`gsd-core/workflows/*.md` or `agents/gsd-*.md` file is reported with its exact byte delta
`msd-core/workflows/*.md` or `agents/msd-*.md` file is reported with its exact byte delta
and needs the same acknowledgment; the outer tier hard caps in
`tests/workflow-size-budget.test.cjs` / `tests/agent-size-budget.test.cjs` are unaffected
and still apply.
@@ -1096,8 +1096,8 @@ You do not need to memorize any of this. **The failure output names its own reme
tells you which key to add and prints a minimal trailer line you can paste onto one of your
commits. Note the two key spaces, because the message says which one applies: an
unattributable **hash** ripple is keyed on the emitted path
(`skills/gsd-add-tests/SKILL.md`), while **growth** is keyed on the bare filename as it
appears under `gsd-core/workflows/` or `agents/` (`explore.md`). The two spaces are
(`skills/msd-add-tests/SKILL.md`), while **growth** is keyed on the bare filename as it
appears under `msd-core/workflows/` or `agents/` (`explore.md`). The two spaces are
structurally distinct — a `Growth` trailer never excuses a `Hash` ripple, even when the key
text happens to match.
@@ -1119,7 +1119,7 @@ reports an ordinary (non-protected) line that moved from a spine into its own de
without a declaration, add:
```
Boundary-Move-Declared: gsd-core/workflows/plan-phase.md — condensed the filesystem-fallback banner into one summary paragraph
Boundary-Move-Declared: msd-core/workflows/plan-phase.md — condensed the filesystem-fallback banner into one summary paragraph
```
Same range (`git log $(git merge-base <base> HEAD)..HEAD`), same fail-closed behavior on an
@@ -1174,7 +1174,7 @@ hooks does not enable this check:
```bash
# set locally in your shell profile (example)
export GSD_BLOCKED_AUTHOR_REGEX='@example-corp\.com$'
export MSD_BLOCKED_AUTHOR_REGEX='@example-corp\.com$'
```
With that exported, a push carrying a commit whose author email matches is blocked,
@@ -1182,8 +1182,8 @@ and the hook names the offending commits. Unset the variable to disable it.
### Every `commit` invocation in shipped content must declare `--files`
`tests/commit-files-pathspec.test.cjs` scans every `.md` under `gsd-core/workflows/`,
`gsd-core/references/`, `agents/`, `commands/`, `skills/` and `docs/` for invocations of
`tests/commit-files-pathspec.test.cjs` scans every `.md` under `msd-core/workflows/`,
`msd-core/references/`, `agents/`, `commands/`, `skills/` and `docs/` for invocations of
the `commit` seam, and fails if any of them reaches the runtime without a `--files`
scope. An unscoped invocation lands on the blanket-stage default and sweeps the whole
`.planning/` index into a commit whose message names one artifact — that is [#2269](https://github.com/open-gsd/gsd-core/issues/2269),
@@ -1197,7 +1197,7 @@ would blind the guard to every site the issue was filed about. Two consequences
hit while editing shipped content, and the failure output names both:
**A prose mention that runs into its sentence is flagged.** Nothing distinguishes
`gsd_run query commit` followed by ordinary words from an invocation with arguments
`msd_run query commit` followed by ordinary words from an invocation with arguments
without guessing at English, so the scan does not try. Write the command reference in
backticks — the repo's own convention — and it is correctly read as a mention.
@@ -1207,7 +1207,7 @@ stand in for your intent. Declare it on the invocation's own line, in shell-comm
position:
```
gsd_run query commit "docs: message" # gsd-scan-ignore: #2269 counter-example for the docs
msd_run query commit "docs: message" # msd-scan-ignore: #2269 counter-example for the docs
```
That block is a live example of itself: the invocation above really is unscoped, and it
@@ -1223,8 +1223,8 @@ means the runtime executed the line.
### Every `git add` in shipped content that can reach `.planning/` must sit inside an *executable* `commit_docs` check
`tests/commit-docs-bypass.test.cjs` scans every `.md` under `gsd-core/workflows/`,
`gsd-core/references/`, `agents/`, `commands/` and `skills/` and fails if a `git add` that could
`tests/commit-docs-bypass.test.cjs` scans every `.md` under `msd-core/workflows/`,
`msd-core/references/`, `agents/`, `commands/` and `skills/` and fails if a `git add` that could
stage `.planning/` is not enclosed by a `commit_docs` check that actually runs.
This is the sibling of the `--files` guard above, and it exists for the complementary hole.
@@ -1244,24 +1244,24 @@ git add "${EVAL_REVIEW_FILE}" ← runs unconditionally; the bold line i
````
The bash block executes whatever the sentence above it says. Write the check in shell, which is
the form `gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md` already uses:
the form `msd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md` already uses:
```bash
COMMIT_DOCS=$(gsd_run query config-get commit_docs 2>/dev/null || echo "true")
COMMIT_DOCS=$(msd_run query config-get commit_docs 2>/dev/null || echo "true")
if [ "$COMMIT_DOCS" != "false" ]; then
git add "${ARTIFACT}"
fi
```
Both polarities are accepted (`!= "false"` and `= "true"`). The `|| echo "true"` fallback is
deliberate: a tooling failure must fail *open*, or a broken `gsd-tools` silently stops committing
deliberate: a tooling failure must fail *open*, or a broken `msd-tools` silently stops committing
planning docs for someone who wants them.
Three consequences worth knowing before you edit shipped content:
**Guard state does not cross a fenced block.** Each fenced block is its own shell, so an `if`
opened in one block does not protect a `git add` in the next — the same reason
`new-milestone.md` warns that a `GSD_WS` guard set in an earlier step reads as unset later. Put
`new-milestone.md` warns that a `MSD_WS` guard set in an earlier step reads as unset later. Put
the check and the `git add` in the same block.
**An unresolvable path is treated as reaching `.planning/`.** `git add "${ARTIFACT}"` is flagged,
@@ -1272,7 +1272,7 @@ trigger it. If your `git add` genuinely cannot touch `.planning/`, name the path
**Only fenced lines are scanned.** Inline-backtick prose — including the anti-pattern
documentation that tells you never to run `git add -A` — is not executable and is not flagged.
The same `# gsd-scan-ignore: #NNN` declaration as the `--files` guard exempts a deliberate
The same `# msd-scan-ignore: #NNN` declaration as the `--files` guard exempts a deliberate
counter-example, on the invocation's own line, in shell-comment position, with a reason naming a
tracking issue or URL. Both guards share one tokenizer and one marker implementation
(`tests/helpers/shipped-command-scan.cjs`) so the two conventions can never drift into two rules
@@ -1281,7 +1281,7 @@ wearing one name.
**Known limits.** The scan (`tests/helpers/planning-add-guard.cjs`) is a token-oriented text scan,
not a shell interpreter, and it targets accidental reintroduction of an unguarded stage by a
contributor editing shipped content — not a determined bypass. Four shapes are confirmed (#3585)
to stage `.planning/` at runtime while scoring zero offenders, and none is a shape GSD content
to stage `.planning/` at runtime while scoring zero offenders, and none is a shape MSD content
actually uses: `eval "git add -A"`, `find .planning -type f | xargs git add`, a one-line shell
function body (`f() { git add -A; }`), and a backslash line-continuation split across two physical
lines. The scan also only models `git add` and `git commit -a`/`--all` as staging commands — it
@@ -1327,7 +1327,7 @@ The following checks run on every PR in addition to the test suite:
| Job | What it checks | How to pass |
|-----|----------------|-------------|
| `Lint — ESLint` | No source-grep tests (see above), via the `local/no-source-grep` rule | Replace with `runGsdTools()` behavioral tests, or add `// allow-test-rule: <reason>` |
| `Lint — ESLint` | No source-grep tests (see above), via the `local/no-source-grep` rule | Replace with `runMsdTools()` behavioral tests, or add `// allow-test-rule: <reason>` |
| `Lint — cross-platform portability` | Windows-portability defects in tests, via `local/no-path-literal-in-assert` (more rules land per [ADR-1703](docs/adr/1703-portability-enforcement-architecture.md)) — e.g. a path-returning call asserted against a hardcoded `/`-literal | Normalize the actual: `String(pathFn(...)).replace(/\\/g, '/')`, or structure platform-specific code behind a `process.platform !== 'win32'` guard. **No `eslint-disable`** — see [cross-platform-portability-rules.md](docs/contributing/cross-platform-portability-rules.md) |
| `lint-docs-guard-registration.cjs` (via `npm run lint:ci`) | A test that reads shipped `docs/` content must be registered so it runs on the PR that changes those docs — otherwise it can only fail after merge | Register it in `scripts/docs-guard-registry.cjs`, mapping the test to the docs paths it reads, or mark it `// docs-guard-exempt: <reason>` and list it in `scripts/lint-docs-guard-registration.exempt-baseline.cjs` — see [docs-guard-registration.md](docs/contributing/docs-guard-registration.md) |
| `lint-response-language-coverage.cjs` (via `npm run lint:ci`) | Every workflow file instructs the model to honour `response_language` in user-facing prose, and the directive names inter-tool narration rather than questions alone — a directive that omits the narration class leaves running commentary in English beside translated answers (#2529) | Give the file one of the four coverage forms: the eager `@`-reference, its own inline directive, the pinned line, or proven inheritance from the parent that dispatches it — see [response-language-coverage.md](docs/contributing/response-language-coverage.md) |
@@ -1374,14 +1374,14 @@ Defensive normalization at trust boundaries must validate both the value's type
## Code Style
- **CommonJS** (`.cjs`) — the project uses `require()`, not ESM `import`
- **No external dependencies in core** — `gsd-tools.cjs` and all lib files use only Node.js built-ins
- **Conventional commits** — `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `ci:`. The full grammar is `<type>(<scope>): <subject>` (enforced by `hooks/gsd-validate-commit.sh`; subject ≤72 chars, lowercase, imperative mood, no trailing period). When the work resolves a tracked issue, put the issue number in the scope: `fix(#1520): randomize mktemp temp paths on BSD/macOS`. The same convention applies to PR titles — release notes are grouped by the title's type prefix (`feat` → Feature, `fix` → Fix, non-user-facing types omitted, everything else → Enhancement).
- **No external dependencies in core** — `msd-tools.cjs` and all lib files use only Node.js built-ins
- **Conventional commits** — `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `ci:`. The full grammar is `<type>(<scope>): <subject>` (enforced by `hooks/msd-validate-commit.sh`; subject ≤72 chars, lowercase, imperative mood, no trailing period). When the work resolves a tracked issue, put the issue number in the scope: `fix(#1520): randomize mktemp temp paths on BSD/macOS`. The same convention applies to PR titles — release notes are grouped by the title's type prefix (`feat` → Feature, `fix` → Fix, non-user-facing types omitted, everything else → Enhancement).
## File Structure
```
bin/install.js — Installer (multi-runtime)
gsd-core/
msd-core/
bin/lib/ — Core library modules (.cjs)
workflows/ — Workflow definitions (.md)
Large workflows split per progressive-disclosure
@@ -1397,14 +1397,14 @@ gsd-core/
requires an Emitted-Drift-Ack-Growth commit trailer
(ADR-3942), no committed snapshot to regenerate. Loose tier
hard caps remain in tests/workflow-size-budget.test.cjs.
The same applies to agent files (agents/gsd-*.md,
The same applies to agent files (agents/msd-*.md,
tests/agent-size-budget.test.cjs). Full how-to +
reference in docs/TESTING-SUITES.md (Workflow &
agent size budget); see issue #1074.
references/ — Reference documentation (.md)
templates/ — File templates
agents/ — Agent definitions (.md) — CANONICAL SOURCE
commands/gsd/ — Slash command definitions (.md)
commands/msd/ — Slash command definitions (.md)
tests/ — Test files (.test.cjs)
helpers.cjs — Shared test utilities
docs/ — User-facing documentation
@@ -1412,13 +1412,13 @@ docs/ — User-facing documentation
### Source of truth for agents
Only `agents/` at the repo root is tracked by git. The following directories may exist on a developer machine with GSD installed and **must not be edited** — they are install-sync outputs and will be overwritten:
Only `agents/` at the repo root is tracked by git. The following directories may exist on a developer machine with MSD installed and **must not be edited** — they are install-sync outputs and will be overwritten:
| Path | Gitignored | What it is |
|------|-----------|------------|
| `.claude/agents/` | Yes (`.gitignore:9`) | Local Claude Code runtime sync |
| `.cursor/agents/` | Yes (`.gitignore:12`) | Local Cursor IDE bundle |
| `.github/agents/gsd-*` | Yes (`.gitignore:37`) | Local CI-surface bundle |
| `.github/agents/msd-*` | Yes (`.gitignore:37`) | Local CI-surface bundle |
If you find that `.claude/agents/` has drifted from `agents/` (e.g., after a branch change), re-run `bin/install.js` to re-sync from the canonical source. Always edit `agents/` — never the derivative directories.