* test(#2971): failing-first suite for the pr-branch planning-path filter Binds the not-yet-built planning.pr_strict mode and the corrected filter recipe for /gsd-pr-branch across six layers: pure classification and forbidden-path predicates, real-git fixtures that run the cherry-pick filter loop end to end, config-key registration through the real CLI and both manifests, the executed worktree-materialization claim the issue's triage asked to establish, fast-check properties over arbitrary path sets, and a drift guard over the shipped workflow. Two live defects in today's shipped recipe are pinned as regressions, both reproduced empirically first: `git rm -r --cached` stages a deletion of any .planning/ path the target branch already tracks, so the generated PR removes the base branch's planning files; and the same command leaves the cherry-picked file untracked on disk, so a second commit touching that path aborts the pick with "untracked working tree files would be overwritten" and every remaining commit is silently dropped. The test helper parses the canonical path lists out of gsd-core/workflows/pr-branch.md rather than restating them, so the workflow stays the single source of truth and the suite cannot drift from what ships. Refs #2971 * feat(#2971): strict planning filter mode for /gsd-pr-branch Adds planning.pr_strict — a boolean, default false, that selects what /gsd-pr-branch means by "filtered". Default mode is unchanged: structural planning state survives into the PR branch and the nine transient subdirectories do not. Strict mode drops every .planning/ path, structural files included, and carries a commit over only when it touches at least one file outside .planning/. Strict mode is what makes planning.commit_docs: true safe for a project that versions its planning tree locally but publishes none of it. The alternative posture, commit_docs: false, silently costs parallel executor isolation — a worktree is checked out from a commit, so an untracked or ignored .planning/ is simply absent inside it and the executor has no PLAN.md to read. That claim is now established by an executed fixture rather than inherited. The two path lists are declared once and both projections derived from them, so create_pr_branch and verify can no longer disagree about what the filter promised. verify previously counted every .planning/ path against a documented success criterion of zero while create_pr_branch was specified to preserve five structural files, so a correct run reported itself as failed on every phase that touched STATE.md — which is every phase. It now asserts against the active mode, and names the .planning/ paths default mode deliberately keeps rather than trading a wrong signal for silence. Two verified defects in the same recipe are fixed alongside, because strict mode would have amplified both. `git rm -r --cached` staged a deletion for any .planning/ path the target branch already tracked, so the generated PR removed the base branch's planning files — under strict mode that would have been the entire tree. The same command left the picked file untracked on disk, so a second commit touching that path aborted the cherry-pick with "untracked working tree files would be overwritten" and every remaining commit was silently dropped. Both were reproduced against real git before being fixed. The filter now forces excluded paths back to what the PR branch's HEAD carries, in the index and the working tree; a conflict outside the filter halts instead of being improvised past; a commit left empty by filtering is skipped rather than failing. A clean-working-tree precondition makes the worktree half safe. Closes #2971 * fix(#2971): unwind the checkout on a conflict halt, and test the real recipe Two review findings, both fixed in place. The isolated adversarial pass found that the conflict-outside-the-filter branch exited while leaving the user checked out on the half-built PR branch with cherry-pick state still live — this loop runs in the user's own working directory, so stranding them there is a real cost even though it is not a vulnerability. The branch now aborts the pick, returns to the original branch, removes the partial PR branch, and says so before exiting. The standards pass found the L2 fixtures executed a hand-written mirror of the cherry-pick filter recipe rather than the recipe itself, so a reordering in the workflow would not have been caught — and the order is load-bearing, since restoring a path from HEAD before removing it inverts the filter. The helper now extracts the canonical loop from the shipped workflow and the fixtures execute that verbatim, which also gives the conflict-halt unwind above real coverage. The drift guard additionally pins the two commands' relative order and asserts the workflow carries exactly one canonical loop. Also records the publication gate in the CONTEXT.md glossary next to the commit gate it is distinct from. Refs #2971 * fix(#2971): make the conflict-halt unwind actually unwind, and use the colon slash form The remote matrix caught two defects in the previous commit. The halt path claimed to restore the original branch but did not. `git cherry-pick --abort` does not apply to a single `--no-commit` pick with no sequencer file, and the fallback left the unmerged index in place, which makes `git checkout` refuse — a failure the `2>/dev/null || true` then swallowed, so the user was told they had been restored while still sitting on the half-built PR branch. The unwind now drops sequencer state, hard-resets the disposable PR branch to clear the unmerged index, and only claims a restore when the checkout actually succeeded; when it does not, it says where the user is and gives them the two commands to finish it by hand. Verified against real git: exit 1, the conflict named, HEAD back on the original branch, the partial branch gone, a clean tree and no CHERRY_PICK_HEAD. Two runtime-loaded source artifacts used the retired `/gsd-<cmd>` hyphen form, which names a command no runtime registers. The canonical authoring token for workflows and references is `/gsd:<cmd>`; docs keep the hyphen form, so the documentation added in this branch is unaffected. The comment in src/config.cts moves to the colon form too, since it propagates into the generated lib. Refs #2971 * docs(#2971): backfill PR number into the changeset fragments (#3720) --------- Co-authored-by: sim <sim@local>
202 lines
9.1 KiB
Markdown
202 lines
9.1 KiB
Markdown
# Keep planning docs out of a shared repo
|
|
|
|
You are working in a team repo and want GSD's `.planning/` artifacts — PLAN.md, SUMMARY.md,
|
|
ROADMAP.md, STATE.md — to stay on your machine instead of appearing in commits your teammates read.
|
|
|
|
This takes four steps, and the third is the one people miss.
|
|
|
|
## 1. Turn off doc commits
|
|
|
|
```bash
|
|
gsd-tools config-set planning.commit_docs false
|
|
```
|
|
|
|
`gsd-tools query commit` now returns a `skipped` envelope instead of committing, and every workflow
|
|
that writes a planning artifact honors it.
|
|
|
|
## 2. Ignore the directory
|
|
|
|
Add to `.gitignore`:
|
|
|
|
```
|
|
.planning/
|
|
```
|
|
|
|
This is also enough on its own: when `.planning/` is gitignored and `config.json` sets no explicit
|
|
value, GSD auto-resolves `commit_docs` to `false`. Setting it explicitly in step 1 is clearer, and
|
|
it survives someone later editing `.gitignore`.
|
|
|
|
## 3. Untrack what git is already tracking
|
|
|
|
**This is the step that catches people out.** `.gitignore` only stops git picking up *new* files.
|
|
It has no effect on files already committed — git keeps tracking those, so `git add -A` keeps
|
|
staging them even though steps 1 and 2 are both done.
|
|
|
|
Because GSD's default is `commit_docs: true`, most existing projects already have `.planning/`
|
|
in history, which makes this the common case rather than an edge case.
|
|
|
|
```bash
|
|
git rm -r --cached .planning/
|
|
git commit -m "chore: stop tracking planning docs"
|
|
```
|
|
|
|
`--cached` removes the files from the index only — your files on disk are untouched.
|
|
|
|
To check whether this applies to you before running it:
|
|
|
|
```bash
|
|
git ls-files .planning
|
|
```
|
|
|
|
Any output means git is still tracking those paths.
|
|
|
|
## 4. Keep search working
|
|
|
|
With `.planning/` ignored, tools that respect `.gitignore` stop searching it — including GSD's own
|
|
broad searches, which is rarely what you want, since the planning docs are exactly what you want an
|
|
agent to read.
|
|
|
|
```bash
|
|
gsd-tools config-set planning.search_gitignored true
|
|
```
|
|
|
|
This adds `--no-ignore` to broad searches so `.planning/` is still found locally.
|
|
|
|
## Verify
|
|
|
|
```bash
|
|
gsd-tools validate health
|
|
```
|
|
|
|
A clean result means you are done. If you skipped step 3, you will see:
|
|
|
|
```
|
|
W029 .planning/ is gitignored but N file(s) are still tracked by git
|
|
Fix: git rm -r --cached .planning/ && git commit -m "chore: stop tracking planning docs"
|
|
```
|
|
|
|
`W029` is advisory. GSD will not untrack files for you, and `--repair` deliberately does not act on
|
|
it — removing files from the index is destructive and the timing is yours.
|
|
|
|
## Notes
|
|
|
|
- **A deliberate force-add also raises `W029`.** If you intentionally keep one file tracked under an
|
|
otherwise-ignored `.planning/` (`git add -f .planning/decisions.md`), the warning still appears.
|
|
There is no reliable way to tell an intentional force-add from the accidental case, so the warning
|
|
is expected there too.
|
|
- **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 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.
|
|
|
|
## Pre-commit guard hook (optional)
|
|
|
|
Steps 1-4 stop **GSD's own** commit path from writing `.planning/`. They do not stop a plain
|
|
`git add -A` + `git commit` — run by hand, by a teammate, or by a script outside GSD's own
|
|
tooling — from staging and committing `.planning/` anyway. `gsd-tools commit-docs-guard enable`
|
|
closes that specific gap by installing a `.git/hooks/pre-commit` hook that refuses any commit
|
|
staging `.planning/` files while `commit_docs` resolves to `false`. Resolution honors the full
|
|
precedence chain above — including a `phase_commit_docs.<phase-id>` override for the phase the
|
|
staged `.planning/` files belong to — the same resolution `gsd-tools commit`/`query commit` uses,
|
|
so the hook never contradicts them.
|
|
|
|
This is opt-in only — no GSD install path wires it in for you:
|
|
|
|
```bash
|
|
gsd-tools commit-docs-guard enable
|
|
```
|
|
|
|
If a commit would violate `commit_docs`, the hook blocks it and names the staged files and the
|
|
`git reset` command to unstage them, matching `gsd-tools check-commit`'s own message. Remove it
|
|
with:
|
|
|
|
```bash
|
|
gsd-tools commit-docs-guard disable
|
|
```
|
|
|
|
**Enable refuses rather than guesses** in three situations, each reported with a reason:
|
|
|
|
- **An existing `pre-commit` hook you didn't get from GSD.** The file is left byte-for-byte
|
|
unchanged; wire the guard into it by hand (`gsd-tools check-commit --raw` is the check to add).
|
|
- **`core.hooksPath` is already configured.** A hook written to `.git/hooks/pre-commit` would
|
|
never run in that case, so nothing is written; add the same check to whatever hook lives at the
|
|
configured path instead.
|
|
- **The current directory is not a git repository.**
|
|
|
|
`enable`/`disable` are idempotent and safe to script: a second `enable` is a no-op that reports
|
|
success rather than duplicating content, and `disable` on a repo with no hook installed succeeds
|
|
rather than erroring. The hook is identified by a stable `# gsd-core:commit-docs-guard` marker
|
|
line inside the file, checked by presence rather than exact content — appending your own line to
|
|
the installed hook afterward does not make GSD stop recognizing it as its own. In a linked
|
|
worktree or submodule (where `.git` is a file, not a directory), `enable` resolves the real,
|
|
shared hooks directory via git itself rather than assuming a literal `.git/hooks` path.
|
|
|
|
Windows note: the hook runs under Git Bash, same as any other git hook. GSD's own remote test
|
|
matrix is Linux-only, so this specific behavior is verified on Linux/macOS plus code review, not
|
|
by an automated Windows run.
|
|
|
|
## If you need parallel executors, use the other posture instead
|
|
|
|
Everything above keeps `.planning/` out of git, and that has one consequence worth knowing before
|
|
you commit to it: **parallel executor worktrees stop working.** A worktree is checked out from a
|
|
commit, so an untracked or ignored `.planning/` does not exist inside it and the executor has no
|
|
`PLAN.md` to read. Untracked planning also has no git history, so `/gsd-undo` and revert paths have
|
|
nothing to restore.
|
|
|
|
If what you actually want is "planning is versioned locally, but never reaches the remote", leave
|
|
`commit_docs` on and set `planning.pr_strict: true` instead — see
|
|
[Publish PRs without planning artifacts](publish-prs-without-planning-artifacts.md).
|
|
|
|
## Related
|
|
|
|
- [Publish PRs without planning artifacts](publish-prs-without-planning-artifacts.md)
|
|
- [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)
|
|
- [Configuration reference — the pre-commit guard hook](../CONFIGURATION.md#commit_docs-pre-commit-guard-opt-in)
|