* 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:
@@ -902,6 +902,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
||||
base_branch: get('base_branch', { section: 'git', field: 'base_branch' }),
|
||||
protected_branches: getNested('git', 'protected_branches'),
|
||||
allow_default_branch_commits: getNested('git', 'allow_default_branch_commits'),
|
||||
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
||||
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
||||
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
|
||||
|
||||
@@ -77,6 +77,15 @@ interface EffectiveGitConfig {
|
||||
protectedBranches: string[];
|
||||
/** Rendered form of every entry rejected as unusable, for the caller to report. */
|
||||
rejectedProtectedBranches: string[];
|
||||
/**
|
||||
* `git.allow_default_branch_commits` escape hatch (#3819). When `true`, the
|
||||
* resolved base branch is no longer auto-added to `protectedBranches` — for
|
||||
* a project that legitimately runs GSD directly on its default branch.
|
||||
* Explicitly configured `protected_branches` entries are unaffected: this
|
||||
* flag narrows only the automatic base-branch protection, never a name the
|
||||
* project named on purpose.
|
||||
*/
|
||||
allowDefaultBranchCommits: boolean;
|
||||
}
|
||||
|
||||
/** Render a rejected config value for a diagnostic without throwing on exotic input. */
|
||||
@@ -150,6 +159,7 @@ function readEffectiveGitConfig(
|
||||
config = {
|
||||
base_branch: _readGitKey(normalized, 'base_branch'),
|
||||
protected_branches: _readGitNested(normalized, 'protected_branches'),
|
||||
allow_default_branch_commits: _readGitNested(normalized, 'allow_default_branch_commits'),
|
||||
};
|
||||
}
|
||||
} catch { /* malformed direct edit contributes no policy values */ }
|
||||
@@ -186,7 +196,11 @@ function readEffectiveGitConfig(
|
||||
rejectedProtectedBranches.push(renderRejected(rawProtectedBranches));
|
||||
}
|
||||
|
||||
return { baseBranch, protectedBranches, rejectedProtectedBranches };
|
||||
// #3819: nested-only read (mirrors protected_branches — new key, no legacy
|
||||
// flat form, so a top-level spelling must not become an undocumented alias).
|
||||
const allowDefaultBranchCommits = config.allow_default_branch_commits === true;
|
||||
|
||||
return { baseBranch, protectedBranches, rejectedProtectedBranches, allowDefaultBranchCommits };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,6 +388,8 @@ export interface ProtectedBranchStatus {
|
||||
rejectedProtectedBranches: string[];
|
||||
isProtected: boolean;
|
||||
verified: boolean;
|
||||
/** Mirrors the `git.allow_default_branch_commits` config value (#3819). */
|
||||
allowDefaultBranchCommits: boolean;
|
||||
}
|
||||
|
||||
/** Resolve the base branch plus configured protected-branch extensions. */
|
||||
@@ -388,13 +404,17 @@ export function resolveProtectedBranchStatus(
|
||||
effectiveConfig.baseBranch,
|
||||
deps,
|
||||
);
|
||||
const protectedBranches = [...new Set([baseBranch, ...effectiveConfig.protectedBranches])];
|
||||
const protectedBranches = [...new Set([
|
||||
...(effectiveConfig.allowDefaultBranchCommits ? [] : [baseBranch]),
|
||||
...effectiveConfig.protectedBranches,
|
||||
])];
|
||||
return {
|
||||
baseBranch,
|
||||
protectedBranches,
|
||||
rejectedProtectedBranches: effectiveConfig.rejectedProtectedBranches,
|
||||
isProtected: protectedBranches.includes(currentBranch),
|
||||
verified,
|
||||
allowDefaultBranchCommits: effectiveConfig.allowDefaultBranchCommits,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user