* 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>
4.7 KiB
Publish PRs without planning artifacts
You want GSD's .planning/ tree versioned in git on your own branch — so parallel executors can
read their plans and /gsd-undo has something to restore — while the pull request your team
reviews contains none of it.
This is the opposite trade to Keep planning docs out of a shared repo, which removes planning from git entirely and loses worktree isolation in the process. Use this guide when you want the history and want the remote clean.
Five steps, and the fourth is the one that decides whether the guarantee actually holds.
1. Keep planning artifacts committed
gsd-tools config-set planning.commit_docs true
This is already the default. Set it explicitly if someone previously turned it off, or if
.planning/ is listed in .gitignore — a gitignored .planning/ auto-resolves commit_docs to
false no matter what config.json says, and planning.pr_strict cannot filter what was never
committed.
If .planning/ is currently in .gitignore, remove that line before continuing.
2. Turn on strict PR filtering
gsd-tools config-set planning.pr_strict true
Confirm it took:
gsd-tools query config-get planning.pr_strict --raw
true means every .planning/ path will be filtered out of the generated PR branch —
STATE.md and ROADMAP.md included, not only the per-phase artifacts.
3. Do the work normally
Nothing about the phase loop changes. /gsd-plan-phase and /gsd-execute-phase commit planning
artifacts exactly as they always have, executor worktrees find their PLAN.md, and your working
branch carries the full planning history.
4. Generate the PR branch — and push that branch
/gsd-pr-branch
The command creates <your-branch>-pr from the target branch and rebuilds the history without any
.planning/ path. Push the generated -pr branch, never your working branch — the working
branch is where the planning history lives, and it is the one thing that must not reach the remote.
git push origin <your-branch>-pr
gh pr create --base main --head <your-branch>-pr
Requirements before it will run:
- A clean working tree. Uncommitted changes are rejected up front. Commit or stash first.
- Commits ahead of the target. With none, the command exits without creating a branch.
5. Read the verification summary
The run ends with a summary. Two lines carry the guarantee:
Mode: strict
Planning paths in diff: 0 (allowed 0, forbidden 0 — must be 0)
forbidden is the number that matters. In strict mode it counts every .planning/ path that
survived into the branch, and it must be 0. allowed is always 0 in strict mode — there is no
allowed population.
What each outcome means
| Summary line | Meaning | What to do |
|---|---|---|
Mode: strict … forbidden 0 |
The guarantee held. | Push the -pr branch. |
Mode: default |
planning.pr_strict did not resolve to true. |
Re-run step 2; the key is read from the project's .planning/config.json, so check you are in the right project root. |
forbidden greater than 0 |
The filter did not remove everything it promised. | Do not push. This is a bug — report it with the listed paths. |
Conflict outside the .planning/ filter |
A real content conflict between your commits and the target branch. | Resolve it on your working branch, then re-run /gsd-pr-branch. |
Working tree has uncommitted changes |
Step 4's precondition failed. | Commit or stash, then re-run. |
No commits ahead of <target> |
There is nothing to filter. | Not an error — you have not committed anything yet, or the target is wrong. |
An ℹ️ advisory listing .planning/ paths appears only in default mode, naming paths that are
neither transient nor structural (config.json, intel/, workstreams/) and therefore kept. Strict
mode removes those too, so the advisory never appears — if you see it, you are not in strict mode.
Verify it for yourself
Before trusting the setup on a real review, confirm the generated branch is clean:
git diff --name-only main..<your-branch>-pr | grep '^\.planning/'
No output is the expected result. Any output means step 2 did not take effect, or the run reported
a non-zero forbidden count you should not push past.
Related
- Keep planning docs out of a shared repo — the other posture: planning never enters git, at the cost of parallel executor worktrees
- Planning settings reference —
planning.pr_strict,planning.commit_docs, and the rest of theplanning.*keys /gsd-pr-branchreference