diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs
index 6b66d2668..5aecaac42 100644
--- a/get-shit-done/bin/lib/config.cjs
+++ b/get-shit-done/bin/lib/config.cjs
@@ -23,7 +23,7 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.skip_discuss',
'workflow._auto_chain_active',
'workflow.use_worktrees',
- 'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
+ 'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored',
'hooks.context_warnings',
'project_code', 'phase_naming',
diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md
index 20ee56f37..225852006 100644
--- a/get-shit-done/references/planning-config.md
+++ b/get-shit-done/references/planning-config.md
@@ -10,6 +10,7 @@ Configuration options for `.planning/` directory behavior.
},
"git": {
"branching_strategy": "none",
+ "base_branch": null,
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
@@ -21,6 +22,7 @@ Configuration options for `.planning/` directory behavior.
| `commit_docs` | `true` | Whether to commit planning artifacts to git |
| `search_gitignored` | `false` | Add `--no-ignore` to broad rg searches |
| `git.branching_strategy` | `"none"` | Git branching approach: `"none"`, `"phase"`, or `"milestone"` |
+| `git.base_branch` | `null` (auto-detect) | Target branch for PRs and merges (e.g. `"master"`, `"develop"`). When `null`, auto-detects from `git symbolic-ref refs/remotes/origin/HEAD`, falling back to `"main"`. |
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy |
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy |
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md
index e95e7cb61..302d26494 100644
--- a/get-shit-done/workflows/complete-milestone.md
+++ b/get-shit-done/workflows/complete-milestone.md
@@ -537,6 +537,15 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
Extract `branching_strategy`, `phase_branch_template`, `milestone_branch_template`, and `commit_docs` from init JSON.
+Detect base branch:
+```bash
+BASE_BRANCH=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get git.base_branch 2>/dev/null || echo "")
+if [ -z "$BASE_BRANCH" ] || [ "$BASE_BRANCH" = "null" ]; then
+ BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')
+ BASE_BRANCH="${BASE_BRANCH:-main}"
+fi
+```
+
**If "none":** Skip to git_tag.
**For "phase" strategy:**
@@ -575,7 +584,7 @@ AskUserQuestion with options: Squash merge (Recommended), Merge with history, De
```bash
CURRENT_BRANCH=$(git branch --show-current)
-git checkout main
+git checkout ${BASE_BRANCH}
if [ "$BRANCHING_STRATEGY" = "phase" ]; then
for branch in $PHASE_BRANCHES; do
@@ -604,7 +613,7 @@ git checkout "$CURRENT_BRANCH"
```bash
CURRENT_BRANCH=$(git branch --show-current)
-git checkout main
+git checkout ${BASE_BRANCH}
if [ "$BRANCHING_STRATEGY" = "phase" ]; then
for branch in $PHASE_BRANCHES; do
diff --git a/get-shit-done/workflows/ship.md b/get-shit-done/workflows/ship.md
index 3c29de1ff..49ee35ccd 100644
--- a/get-shit-done/workflows/ship.md
+++ b/get-shit-done/workflows/ship.md
@@ -24,6 +24,15 @@ CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
```
Extract: `branching_strategy`, `branch_name`.
+
+Detect base branch for PRs and merges:
+```bash
+BASE_BRANCH=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get git.base_branch 2>/dev/null || echo "")
+if [ -z "$BASE_BRANCH" ] || [ "$BASE_BRANCH" = "null" ]; then
+ BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')
+ BASE_BRANCH="${BASE_BRANCH:-main}"
+fi
+```
@@ -46,7 +55,7 @@ Verify the work is ready to ship:
```bash
CURRENT_BRANCH=$(git branch --show-current)
```
- If on `main`/`master`: warn — should be on a feature branch.
+ If on `${BASE_BRANCH}`: warn — should be on a feature branch.
If branching_strategy is `none`: offer to create a branch now.
4. **Remote configured?**
@@ -74,7 +83,7 @@ If push fails (e.g., no upstream): set upstream:
git push --set-upstream origin ${CURRENT_BRANCH} 2>&1
```
-Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)"
+Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of ${BASE_BRANCH})"
@@ -141,7 +150,7 @@ Create the PR using the generated body:
gh pr create \
--title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
--body "${PR_BODY}" \
- --base main
+ --base ${BASE_BRANCH}
```
If `--draft` flag was passed: add `--draft`.
@@ -194,7 +203,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase
## ✓ Phase {X}: {Name} — Shipped
PR: #{number} ({url})
-Branch: {branch} → main
+Branch: {branch} → ${BASE_BRANCH}
Commits: {count}
Verification: ✓ Passed
Requirements: {N} REQ-IDs addressed
diff --git a/tests/config.test.cjs b/tests/config.test.cjs
index 6280df25e..bd731d8c1 100644
--- a/tests/config.test.cjs
+++ b/tests/config.test.cjs
@@ -242,6 +242,16 @@ describe('config-set command', () => {
assert.strictEqual(config.workflow.use_worktrees, false);
});
+ test('sets git.base_branch for non-main default branches', () => {
+ writeConfig(tmpDir, {});
+
+ const result = runGsdTools('config-set git.base_branch master', tmpDir);
+ assert.ok(result.success, `Command failed: ${result.error}`);
+
+ const config = readConfig(tmpDir);
+ assert.strictEqual(config.git.base_branch, 'master');
+ });
+
test('errors when no key path provided', () => {
const result = runGsdTools('config-set', tmpDir);
assert.strictEqual(result.success, false);
@@ -329,6 +339,27 @@ describe('config-get command', () => {
});
});
+ test('gets git.base_branch after it is set', () => {
+ runGsdTools('config-set git.base_branch master', tmpDir);
+ const result = runGsdTools('config-get git.base_branch', tmpDir);
+ assert.ok(result.success, `Command failed: ${result.error}`);
+
+ const output = JSON.parse(result.output);
+ assert.strictEqual(output, 'master');
+ });
+
+ test('errors for git.base_branch when not explicitly set', () => {
+ // Default config from config-ensure-section does not include git.base_branch,
+ // so config-get should return "Key not found" — this triggers auto-detect
+ // fallback in the workflow (origin/HEAD detection).
+ const result = runGsdTools('config-get git.base_branch', tmpDir);
+ assert.strictEqual(result.success, false);
+ assert.ok(
+ result.error.includes('Key not found'),
+ `Expected "Key not found" in error: ${result.error}`
+ );
+ });
+
test('errors when no key path provided', () => {
const result = runGsdTools('config-get', tmpDir);
assert.strictEqual(result.success, false);