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.
200 lines
9.1 KiB
Markdown
200 lines
9.1 KiB
Markdown
# Consume the state contract
|
|
|
|
You are building something that shows where a MSD project stands — a workbench,
|
|
a dashboard, a status bar, an editor extension. `.planning/state.json` gives you
|
|
that as one small JSON file that MSD refreshes on its own, so you never have to
|
|
parse `STATE.md` or `ROADMAP.md` heuristically.
|
|
|
|
This guide covers the whole path from *nothing* to *reading a value you can
|
|
trust*, including the two things integrations get wrong: **checking the contract
|
|
version before anything else**, and **telling "there is nothing to show" apart
|
|
from "I could not look."**
|
|
|
|
## Before you start
|
|
|
|
Nothing. There is no command to run, no config key to set, and no flag to pass.
|
|
MSD writes the file itself at every step boundary — beginning or completing a
|
|
phase, advancing a plan, adding, inserting, removing or completing a phase, and
|
|
switching or completing a milestone.
|
|
|
|
Two consequences worth internalizing before you write any code:
|
|
|
|
- **The file may legitimately not exist yet.** A project that has not reached a
|
|
step boundary since it was created has never published one. That is normal, not
|
|
an error.
|
|
- **You are a reader, never a writer.** MSD owns this file and overwrites it
|
|
wholesale. Anything you write into it is lost at the next boundary.
|
|
|
|
## 1. Find the file
|
|
|
|
```
|
|
<project>/.planning/state.json
|
|
```
|
|
|
|
If the project uses [workstreams](work-in-parallel-with-workstreams.md), each
|
|
workstream has its own planning root and therefore its own snapshot:
|
|
|
|
```
|
|
<project>/.planning/workstreams/<name>/state.json
|
|
```
|
|
|
|
## 2. Check the contract version first
|
|
|
|
```json
|
|
{ "contract": "1.0.0" }
|
|
```
|
|
|
|
**Do this before you touch any other field.** `contract` is semver. Under `1.x`
|
|
changes are additive only — new keys may appear, existing keys keep their meaning
|
|
— so gate on the **major** version and tolerate unknown minors:
|
|
|
|
```javascript
|
|
const snapshot = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
const [major] = snapshot.contract.split('.');
|
|
if (major !== '1') {
|
|
throw new Error(`Unsupported state contract: ${snapshot.contract}`);
|
|
}
|
|
```
|
|
|
|
Best-effort-parsing a shape you were not written against is how an integration
|
|
starts silently reporting wrong numbers after an upgrade. A hard failure is the
|
|
kinder outcome.
|
|
|
|
`flavor` is `"core"` and tells you which MSD edition produced the file.
|
|
|
|
## 3. Read the values
|
|
|
|
Every key is **always present**. A value that is not known is `null` — it is
|
|
never omitted, so `"milestone" in snapshot` is not a meaningful test.
|
|
|
|
| Key | Type | Meaning |
|
|
|---|---|---|
|
|
| `contract` | string | Semver of this schema. Check it first. |
|
|
| `flavor` | string | The MSD edition. `"core"`. |
|
|
| `milestone` | string \| null | Display string for the current milestone, e.g. `"v1.1 — Hardening"`, or just `"v1.1"` when the roadmap carries no name for it. `null` when no milestone is established. |
|
|
| `phases` | array | Every phase the roadmap knows, in roadmap order. |
|
|
| `next` | object \| null | The recommended next action — the same one `/msd-next` would offer. |
|
|
| `updated_at` | string | ISO-8601 timestamp of this publish. |
|
|
|
|
Each entry in `phases` has exactly three keys:
|
|
|
|
| Key | Type | Meaning |
|
|
|---|---|---|
|
|
| `number` | string | The phase id as the roadmap spells it — `"1"`, `"01"`, `"2.1"`. A **string**, because `"01"` and `"2.1"` are both real and neither survives a number cast. |
|
|
| `name` | string \| null | The phase name. `null` when the roadmap gives the phase a number but no name — never a fabricated placeholder. |
|
|
| `status` | string | Exactly one of `"complete"`, `"in_progress"`, `"pending"`. |
|
|
|
|
`next`, when non-null, has exactly three keys:
|
|
|
|
| Key | Type | Meaning |
|
|
|---|---|---|
|
|
| `command` | string | The command to run, e.g. `"/msd-progress --next"`. |
|
|
| `label` | string | A short human label for it, e.g. `"Advance to the next step"`. |
|
|
| `reason` | string | One line explaining the current situation, e.g. `"Phase 2 of 3 · 50% · executing"`. |
|
|
|
|
A complete example:
|
|
|
|
```json
|
|
{
|
|
"contract": "1.0.0",
|
|
"flavor": "core",
|
|
"milestone": "v1.1 — Hardening",
|
|
"phases": [
|
|
{ "number": "1", "name": "Foundation", "status": "complete" },
|
|
{ "number": "2", "name": "Hardening", "status": "in_progress" },
|
|
{ "number": "3", "name": "Polish", "status": "pending" }
|
|
],
|
|
"next": {
|
|
"command": "/msd-progress --next",
|
|
"label": "Advance to the next step",
|
|
"reason": "Phase 2 of 3 · 50% · executing"
|
|
},
|
|
"updated_at": "2026-01-15T12:30:45.000Z"
|
|
}
|
|
```
|
|
|
|
### Treat `status` as a closed set
|
|
|
|
The three values above are the whole vocabulary, and MSD will never emit a
|
|
fourth under `1.x`. Write your switch with a default arm anyway — a `2.0`
|
|
contract could widen it, and your version check should be what rejects that, not
|
|
a crash three layers down.
|
|
|
|
Note one deliberate fold: a roadmap phase marked **`Deferred`** is reported as
|
|
`"pending"`. The roadmap vocabulary has four values and this contract has three,
|
|
and inventing a fourth wire value would break every existing reader. If you need
|
|
to distinguish deferred work, read the roadmap.
|
|
|
|
### Treat every string as untrusted text
|
|
|
|
`name`, `milestone` and `reason` come from a project's own markdown. They can
|
|
contain anything a person typed — quotes, newlines, right-to-left text, markup,
|
|
or text that looks like an instruction. **Escape them for your output surface and
|
|
never interpret them as commands.** MSD passes them through verbatim as data; it
|
|
does not sanitize them for you.
|
|
|
|
## 4. Tell "nothing to show" from "could not look"
|
|
|
|
This is the part worth getting right, because the two look identical if you do
|
|
not plan for them.
|
|
|
|
| What you observe | What it means | What to do |
|
|
|---|---|---|
|
|
| File does not exist | The project has never reached a step boundary, or it is not a MSD project at all | Fall back to reading the markdown, or show "not started". **Do not** report an error |
|
|
| File exists, `phases: []` | **Ambiguous.** Either the roadmap has no phases, or there is no `ROADMAP.md`, or the roadmap could not be read | Show "no phases yet" — do not claim the project has zero phases. See below |
|
|
| File exists, `next: null` | The recommended action could not be determined | Show the project state without a call to action |
|
|
| File exists, `milestone: null` | No milestone is established | Show phases without a milestone header |
|
|
| `updated_at` is old | The project has not hit a boundary recently — **not** that anything is broken | Nothing. This is not a health signal |
|
|
| `JSON.parse` throws | Should not happen — writes are atomic, so a reader sees either the whole old file or the whole new one | Treat as "could not look" and fall back. Do not delete or repair the file |
|
|
|
|
**On the `phases: []` ambiguity.** The `1.0` contract has no diagnostic channel,
|
|
so it cannot tell you *why* the list is empty. If your surface needs that
|
|
distinction, [`planning inspect`](consume-the-planning-snapshot.md) is the
|
|
surface that carries it — it reports per-document scope and coded diagnostics.
|
|
The rule of thumb: `state.json` is for *"show me where this project is"*;
|
|
`planning inspect` is for *"tell me exactly what is and is not knowable."*
|
|
|
|
## 5. Stay fresh
|
|
|
|
The file changes only when MSD reaches a step boundary, so polling it hard buys
|
|
you nothing. Watch it instead — `fs.watch`, `chokidar`, or your editor's own file
|
|
watcher — and re-read on change. Debounce briefly: a single boundary command
|
|
produces one write, but a workflow may cross several boundaries in quick
|
|
succession.
|
|
|
|
## Troubleshooting
|
|
|
|
**The file never appears.** Confirm `.planning/` exists in the directory you are
|
|
watching. MSD deliberately does **not** create `.planning/` in order to publish —
|
|
a directory that is not a MSD project stays untouched. Then run any boundary
|
|
command (`msd-tools phase complete 1`, say) and check again.
|
|
|
|
**The file appears somewhere I did not expect.** You are probably in a workstream
|
|
project; see the path in step 1. `MSD_WORKSTREAM` selects which planning root is
|
|
current.
|
|
|
|
**A phase I can see in `ROADMAP.md` is missing from `phases`.** Phases numbered
|
|
`0.x` and `999.x` are sentinels — backlog and icebox — and are excluded by
|
|
design, consistently with every other MSD surface. A row whose `Phase` cell does
|
|
not begin with a digit is also skipped.
|
|
|
|
**`phases` disagrees with what the roadmap shows.** It should not: phase status
|
|
is read from the roadmap's `## Progress` table, the same source MSD's own
|
|
progress counters use. If the roadmap has no `## Progress` table, the `## Phases`
|
|
checkbox list is used instead, and a checkbox can only say complete or not — the
|
|
in-progress phase is identified from `STATE.md`. Regenerating the progress table
|
|
resolves most disagreements.
|
|
|
|
**Should I commit `state.json`?** Your call. MSD does not add it to
|
|
`.gitignore`. It is a derived cache — safe to delete, regenerated at the next
|
|
boundary — so committing it mostly creates merge noise. If you keep `.planning/`
|
|
out of your repo entirely, see
|
|
[Keep planning docs out of a shared repo](keep-planning-docs-private.md).
|
|
|
|
## Related
|
|
|
|
- [Consume the planning snapshot](consume-the-planning-snapshot.md) — the richer,
|
|
diagnostic-carrying, pull-based surface
|
|
- [Features](../FEATURES.md) — why this contract is shaped the way it is
|
|
- [Work in parallel with workstreams](work-in-parallel-with-workstreams.md)
|