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.
7.9 KiB
Consume the planning snapshot
You are building something that needs to know where a MSD project stands —
a dashboard, a status page, a harness UI, a bot that comments on a pull request.
planning inspect gives you that as one JSON document, so you never have to
parse ROADMAP.md, REQUIREMENTS.md, *-PLAN.md, or *-SUMMARY.md yourself.
This guide covers the whole path from off to reading a value you can trust, including the part most integrations get wrong: telling "nothing to report" apart from "could not look."
Before you start
You need msd-tools on the machine, and a project directory containing
.planning/. Nothing has to be enabled or configured — the command is read-only
and always available.
1. Get a snapshot
msd-tools query planning inspect
The dotted form is identical, if that reads better in your code:
msd-tools query planning.inspect
The command takes no arguments. If you pass one, it fails loudly rather than ignoring it — see Troubleshooting.
To inspect a project other than your current directory, use the global --cwd
flag, which every msd-tools command accepts:
msd-tools query planning inspect --cwd /path/to/project
2. Check the schema version first
{ "schema_version": 1 }
Reject any version you were not written against. Do this before you touch any other field:
const snapshot = JSON.parse(stdout);
if (snapshot.schema_version !== 1) {
throw new Error(`Unsupported planning snapshot schema: ${snapshot.schema_version}`);
}
Best-effort-parsing an unknown shape is how an integration starts silently reporting wrong numbers after an upgrade. A hard failure is the kinder outcome.
3. Read a value — and check its scope
Most answers arrive alongside a scope. It tells you whether the value is a
real answer or a placeholder for one that could not be produced:
scope |
Meaning | What to render |
|---|---|---|
complete |
The read succeeded. The value is real — including when it is 0 or [] |
The value |
truncated |
Part of the source could not be read | The value, marked partial |
unscoped |
The source exists but nothing could be located in it | "Unknown" |
unreadable |
The source could not be read at all | "Unknown" |
The distinction that matters: complete with an empty value is a real answer.
A milestone with zero phases genuinely has zero phases. unreadable with an
empty value means nobody looked. Rendering both as "0 phases" is the bug this
field exists to prevent.
const phases = snapshot.progress.accepted_phases;
if (phases.scope !== 'complete') {
render('Progress unavailable'); // could not look
} else {
render(`${phases.completed} / ${phases.total}`); // real, even if 0 / 0
}
4. Handle a withheld percentage
progress.accepted_phases and progress.completed_plans each carry
{completed, total, percent, scope}. percent is null whenever scope is
not complete.
That is deliberate. A percentage computed from a partial phase set is a
confidently wrong number, and a consumer cannot tell it apart from a real one.
Do not substitute 0, and do not compute your own from completed / total —
those counts are partial too.
const { percent } = snapshot.progress.completed_plans;
render(percent === null ? '—' : `${percent}%`);
percent: 0 under a complete scope is a real 0 and should be rendered.
5. Read the three kinds of phase evidence separately
Each entry in phases[] reports three independent signals. They are not
combined into one verdict, and you should not combine them either — they
answer different questions and can legitimately disagree.
| Field | Question it answers |
|---|---|
verification |
Did the verifier pass this phase? This is what complete is derived from |
roadmap_acceptance |
Is the ROADMAP checkbox ticked? |
uat |
Are there unresolved user-acceptance items? |
roadmap_acceptance carries authoritative: false, and it means it. A ticked
checkbox is a human annotation with no machine authority — completion comes from
disk state. If you show the checkbox, label it as an annotation, not as status.
A phase can be complete: true with open UAT items. That is a real state, not a
contradiction.
6. Handle unknown rather than guessing
Where evidence is absent or two sources disagree, the value is null or
"unknown" and diagnostics[] says why. Nothing is inferred.
The case you will hit most often is task-scoped file provenance:
provenance |
What it means | What to show |
|---|---|---|
task_scoped |
The summary attributed files to this exact task | The file list |
plan_scoped |
A summary exists, but only lists files for the whole plan | "Not attributed" — not the plan's list |
absent |
No summary yet | "Not started" |
plan_scoped is the common case and is not an error. Attributing a plan's file
list to one of its tasks would be a guess, so the snapshot declines to make it.
The plan-level list is still available at plans[].changed_files, where it is
accurate.
When a task's planned and changed files both exist and disagree, agreement is
"conflicting" and both lists are present, unreconciled. Show both; do not pick.
7. Read the diagnostics
Every non-answer above has a matching entry in diagnostics[]:
{ "code": "requirement_unmapped", "subject": "AUTH-03", "detail": "..." }
code is a stable identifier from a frozen vocabulary — match on it, never on
detail, whose wording may change. Common codes:
| Code | Meaning |
|---|---|
planning_root_absent |
No .planning/ directory — every section below is a non-answer |
roadmap_unscoped |
No milestone version could be resolved; none was invented |
requirements_absent |
No REQUIREMENTS.md |
requirement_unmapped |
A requirement no Traceability row maps to a phase |
requirement_phase_unknown |
A requirement mapped to a phase that is not on disk |
orphan_phase_dir |
A phase directory the current milestone does not declare |
task_changed_files_plan_scoped |
Task-level file attribution unavailable (see step 6) |
task_changed_files_conflicting |
Planned and changed files disagree |
percent_withheld |
A percentage was suppressed because its scope was not complete |
An empty diagnostics[] means every value in the snapshot is a real answer.
8. Handle a large payload
On a big project the JSON can exceed the ~50 KB console limit. msd-tools
handles this for you: it writes to a temp file and resolves the reference before
writing to stdout, so you always receive JSON. If you are invoking msd-tools
through a shell wrapper that captures stdout directly, no special handling is
needed.
Troubleshooting
Unknown planning subcommand. Available: inspect
You typed a subcommand that does not exist. inspect is the only one.
planning inspect takes no arguments; got flag: --phase
v1 always returns the whole project. It refuses scoping arguments rather than
ignoring them — silently returning an unscoped snapshot to a caller who asked
for a scoped one would be worse. Filter the phases[] array on your side.
Everything is unknown and diagnostics[0].code is planning_root_absent
You are not in a MSD project directory. Use --cwd, or cd first.
A phase you expect is missing from phases[]
Check orphan_phase_dirs[]. phases[] is scoped to the phases the current
milestone's ROADMAP window declares; a directory on disk that the roadmap never
mentions is reported there instead, so that a genuinely orphaned directory
cannot masquerade as a planned phase.
Related
planning inspectreference — every field, with exact semantics- Resolve unreachable-guard findings — the same "nothing to report vs. could not look" distinction, one layer down