fix(#3819): widen executor's pre-commit guard beyond worktree mode (#4343)

* fix(#3819): widen executor's pre-commit guard beyond worktree mode

The pre-commit protected-branch assertion in the executor agent (#2924)
only fired inside a Claude Code worktree and matched a hardcoded
five-name branch list. It never ran in an ordinary checkout and never
covered this repo's own default branch ("next"), so gsd-executor could
commit planning-repo documents directly onto a shared checkout's
default branch with no PR ever created.

Widen the guard to run in every isolation mode, and resolve the
protected branch via the repository's actual default branch (with the
existing five-name list retained as a fallback when the resolver
itself cannot be invoked) plus any configured git.protected_branches.
Add a git.allow_default_branch_commits escape hatch for projects that
intentionally execute on their default branch. Also point the
separate <final_commit> commit helper back at the same guard, so it
cannot be sidestepped by that path.

Emitted-Drift-Ack-Growth: gsd-executor.md — widened pre-commit protected-branch guard (#3819); tightened comments to stay under the size cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#3819): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-05 18:35:42 -04:00
committed by GitHub
parent 4e1c449281
commit c20675cc4d
8 changed files with 425 additions and 16 deletions

View File

@@ -1194,6 +1194,7 @@ All four fields are **optional and additive** — STATE.md files without them ke
| `git.branching_strategy` | enum | `none` | `none`, `phase`, or `milestone` |
| `git.base_branch` | string | `main` | The integration branch that phase/milestone branches are created from and merged back into. Override when your repo uses `master` or a release branch |
| `git.protected_branches` | array of non-empty strings | (none) | Optional additional shared branches that should trigger protected-branch warnings alongside the resolved base branch |
| `git.allow_default_branch_commits` | boolean | `false` | Escape hatch (#3819): when `true`, the executor's pre-commit guard no longer refuses to commit on the resolved default branch. Explicitly configured `git.protected_branches` names are still enforced. |
| `git.create_tag` | boolean | `true` | Create a git tag (`v[X.Y]`) on milestone completion. Set to `false` for projects with their own release flow |
| `git.phase_branch_template` | string | `gsd/phase-{phase}-{slug}` | Branch name template for phase strategy |
| `git.milestone_branch_template` | string | `gsd/{milestone}-{slug}` | Branch name template for milestone strategy |
@@ -1224,6 +1225,25 @@ still apply.
}
```
### Escape Hatch: Committing on the Default Branch
Some projects intentionally run GSD directly on their default branch (no branch-per-phase
workflow). For those, `git.allow_default_branch_commits: true` tells the executor's pre-commit
guard (see `agents/gsd-executor.md`) to stop refusing commits on the resolved default branch.
It narrows only the *automatic* default-branch protection — any branch name explicitly listed
in `git.protected_branches` stays protected regardless of this flag.
```json
{
"git": {
"allow_default_branch_commits": true
}
}
```
This does not change what gets committed, commit message format, or behavior on any
non-default branch — see issue #3819.
### Strategy Comparison
| Strategy | Creates Branch | Scope | Merge Point | Best For |
@@ -2213,6 +2233,7 @@ Two different rules apply, and the difference is deliberate ([#3532](https://git
global `effort` block keeps working in projects and does not trigger the warning.
- **The whole `git.*` namespace is project-scoped and never resolves from the global file**,
in either directory shape — not `git.base_branch`, not `git.protected_branches`, not
`git.allow_default_branch_commits`, not
`git.branching_strategy` or the branch templates. Branch policy is a property of the
repository, not of the machine, so it is read only from that project's
`.planning/config.json`. A `git` block in `~/.gsd/defaults.json` still seeds new projects