enhance(#3587): add a per-phase commit_docs override (#3601)

* feat(#3587): add a per-phase commit_docs override

Delivers epic #2292's second user story: commit an architecture phase's
artifacts while execution phases stay local. commit_docs was project-wide and
binary, so the only choices were all phases or none.

Shape is a config dynamic key phase_commit_docs.<phase-id>, following the 14
existing dynamicKeyPatterns precedents rather than inventing a PLAN.md
frontmatter spec -- which #2292 itself flags as becoming its own maintenance
surface.

Tier 1 resolves in cmdCommit, NOT in loadConfig: loadConfig has no phase
context and is called by nearly every command, so threading one through it to
serve a single caller would be a far larger blast radius for no gain. The phase
comes from detectPhaseNumberFromFiles, which cmdCommit already computes for
branch naming and which is already hardened against the #2539 project-code bug.

Suppression by the per-phase tier returns its own reason rather than reusing
skipped_commit_docs_false -- telling a user their project setting is false when
it is true would be actively misleading. Additive; the two existing reason
strings that agents/gsd-executor.md matches on are unchanged.

The manifest's phase-id pattern is a hand-copy of PHASE_NUMBER_TOKEN_SOURCE
because the manifest is hand-maintained JSON, so a behavioral parity test
asserts both surfaces accept and reject the same token shapes.

* fix(#3587): fold tests, close review findings, update reference docs

Fold: the new tests were added as their own file, which required loosening a
grandfathered lint-test-file-count bucket 5-to-6. A ratchet exists to go down
only. commit-docs-bypass.test.cjs is the established commit_docs test home and
already hosts two folded suites, so the tests fold there as a third block and
the allowlist is reverted untouched.

Standards review: CONTEXT.md and the test header both cited a
phase-commit-docs-manifest-parity.test.cjs that never existed; a repo-wide
sweep found a fourth stale cite in the schema manifest description. All four
now name the real location.

Spec review: the issue's Scope of changes named planning-config.md and
git-planning-commit.md and neither was touched. Both now document the four-tier
precedence and the new skip reason.

Security review, minor and unproven: detectPhaseNumberFromFiles returns the
FIRST matching path's phase, so a --files list spanning two phases resolves the
override against whichever comes first. That helper is hardened and widely used,
so it is not changed; the behavior is pinned by a named test and disclosed in
the design and user docs. A pinned behavior is not a bug; an unpinned surprise
is.

* chore(#3587): backfill changeset pr number to 3601

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-17 21:59:40 -04:00
committed by GitHub
parent f56ffa86ab
commit debeabd524
11 changed files with 718 additions and 17 deletions

View File

@@ -530,6 +530,55 @@ Note: a file deliberately force-added under an otherwise-ignored `.planning/` (`
.planning/keep.md`) triggers this same warning — there is no reliable way to distinguish an
intentional force-add from the accidental case above, so `W029` is expected in that situation too.
### Per-Phase Override (`phase_commit_docs`)
`commit_docs` is a single project-wide switch by default, but a tech lead may want to commit one
phase's artifacts (e.g. an architecture or ADR phase) while keeping execution phases local. Set a
dynamic key of the form `phase_commit_docs.<phase-id>` to override `commit_docs` for that phase only:
```bash
gsd-tools config-set phase_commit_docs.03 true
gsd-tools config-set phase_commit_docs.07 false
```
```json
{
"commit_docs": false,
"phase_commit_docs": {
"03": true,
"07": false
}
}
```
The `<phase-id>` segment accepts the same phase-number shapes GSD uses elsewhere (`3`, `03`,
`12A`, `3.2` — a project-code prefix like `PROJ-03` is normalized to the bare phase number before
lookup), so `phase_commit_docs.3` and `phase_commit_docs.03` refer to the same entry.
**Resolution order** (highest wins) when `gsd-tools commit` / `query commit` resolves the phase from
the committed `--files` paths:
1. `phase_commit_docs.<phase-id>` for the phase being committed
2. explicit `commit_docs` / `planning.commit_docs` in config.json
3. `.gitignore` auto-detect (see [Auto-Detection](#auto-detection) above)
4. the manifest default (`true`)
A per-phase value must be a real boolean — `"true"` (string), `1`, or `null` are never coerced and
fall through to the next tier. A value set for a different phase than the one being committed never
applies (no cross-phase leak). A commit that names no phase-scoped file (e.g. a project-wide
`ROADMAP.md`-only commit) has no phase to look up, so tier 1 is inapplicable and resolution starts
at tier 2 — unchanged from pre-#3587 behavior.
When tier 1 suppresses a commit, the skip envelope's `reason` is
`skipped_commit_docs_phase_false` — distinct from the project-wide `skipped_commit_docs_false` —
so a caller is never told "commit_docs is false" when the project setting is actually `true`.
A commit spanning multiple phases resolves the override against the first phase in the `--files`
list, so scope `--files` to one phase when using the override.
See [Keep planning docs out of a shared repo](how-to/keep-planning-docs-private.md#per-phase-override)
for a worked example.
---
## Hook Settings

View File

@@ -87,10 +87,55 @@ it — removing files from the index is destructive and the timing is yours.
- **Teammates who have already pulled the tracked files** will see them deleted by your step-3
commit. That is the intended effect — the files leave the repo, not their working copies of your
branch — but say so in the commit message or PR description so it is not a surprise.
- **Per-phase control** (committing docs for an architecture phase while keeping execution phases
local) is tracked separately; `commit_docs` is currently project-wide.
## Per-phase override
You just made the whole project local-only in step 1. If instead you want ONE phase's artifacts —
say, an architecture or ADR phase whose PLAN.md and REQUIREMENTS.md are worth sharing with the
team — committed while every other phase stays local, set a `phase_commit_docs` entry for that
phase's number instead of (or on top of) the project-wide switch:
```bash
gsd-tools config-set planning.commit_docs false
gsd-tools config-set phase_commit_docs.03 true
```
```json
{
"planning": { "commit_docs": false },
"phase_commit_docs": { "03": true }
}
```
Now `gsd-tools query commit` commits phase 03's artifacts normally, and skips (with the
`skipped_commit_docs_phase_false`-or-`skipped_commit_docs_false` reason depending on which tier
decided) every other phase's — the project-wide setting from step 1 still governs everything
`phase_commit_docs` does not name.
A few things worth knowing before you rely on this:
- **The phase-id form doesn't matter.** `phase_commit_docs.3`, `phase_commit_docs.03`, and (if your
project uses a `project_code`) `phase_commit_docs.PROJ-03` all resolve to the same entry — GSD
normalizes the phase number before lookup, the same way it does everywhere else phase numbers are
compared.
- **It only applies to a commit that names a phase-scoped file.** `gsd-tools query commit` resolves
the phase from the `--files` paths you pass it (via the `.planning/phases/<phase-dir>/…`
segment). A commit that names no phase file — e.g. a bare `ROADMAP.md` update — has no phase to
look up, so `phase_commit_docs` never applies to it and the project-wide setting governs, same as
before this feature existed.
- **It's per phase, not per artifact.** You cannot commit a phase's ADR but suppress its SUMMARY
within the same commit; the override applies to the whole phase.
- **Reverse direction works too.** With the project-wide default (`commit_docs: true`), set
`phase_commit_docs.<phase-id> false` to suppress just one noisy execution phase while everything
else commits normally.
Full precedence order (highest wins): `phase_commit_docs.<phase-id>` → explicit `commit_docs` /
`planning.commit_docs` → `.gitignore` auto-detect → the manifest default. See
[Configuration reference — per-phase override](../CONFIGURATION.md#per-phase-override-phase_commit_docs)
for the complete rules, including how a non-boolean value is handled.
## Related
- [Configuration reference — `planning.commit_docs`](../CONFIGURATION.md#planning-settings)
- [Configuration reference — auto-detection and the tracked-files caveat](../CONFIGURATION.md#auto-detection)
- [Configuration reference — per-phase override](../CONFIGURATION.md#per-phase-override-phase_commit_docs)