fix(prime): use flush-only workflow when no git remote configured (#940)
Use flush-only workflow when no git remote is configured Detects local-only repos (no git remote) and provides appropriate instructions: - bd sync --flush-only instead of full git workflow - Clear note about local-only storage - Prevents confusing git errors for non-git users
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/steveyegge/beads"
|
"github.com/steveyegge/beads"
|
||||||
"github.com/steveyegge/beads/internal/config"
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isDaemonAutoSyncing checks if daemon is running with auto-commit and auto-push enabled.
|
// isDaemonAutoSyncing checks if daemon is running with auto-commit and auto-push enabled.
|
||||||
@@ -179,6 +181,11 @@ var isEphemeralBranch = func() bool {
|
|||||||
return err != nil
|
return err != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// primeHasGitRemote detects if any git remote is configured (stubbable for tests)
|
||||||
|
var primeHasGitRemote = func() bool {
|
||||||
|
return syncbranch.HasGitRemote(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
// getRedirectNotice returns a notice string if beads is redirected
|
// getRedirectNotice returns a notice string if beads is redirected
|
||||||
func getRedirectNotice(verbose bool) string {
|
func getRedirectNotice(verbose bool) string {
|
||||||
redirectInfo := beads.GetRedirectInfo()
|
redirectInfo := beads.GetRedirectInfo()
|
||||||
@@ -208,10 +215,11 @@ func outputMCPContext(w io.Writer, stealthMode bool) error {
|
|||||||
ephemeral := isEphemeralBranch()
|
ephemeral := isEphemeralBranch()
|
||||||
noPush := config.GetBool("no-push")
|
noPush := config.GetBool("no-push")
|
||||||
autoSync := isDaemonAutoSyncing()
|
autoSync := isDaemonAutoSyncing()
|
||||||
|
localOnly := !primeHasGitRemote()
|
||||||
|
|
||||||
var closeProtocol string
|
var closeProtocol string
|
||||||
if stealthMode {
|
if stealthMode || localOnly {
|
||||||
// Stealth mode: only flush to JSONL as there's nothing to commit.
|
// Stealth mode or local-only: only flush to JSONL, no git operations
|
||||||
closeProtocol = "Before saying \"done\": bd sync --flush-only"
|
closeProtocol = "Before saying \"done\": bd sync --flush-only"
|
||||||
} else if autoSync && !ephemeral && !noPush {
|
} else if autoSync && !ephemeral && !noPush {
|
||||||
// Daemon is auto-syncing - no bd sync needed
|
// Daemon is auto-syncing - no bd sync needed
|
||||||
@@ -248,6 +256,7 @@ func outputCLIContext(w io.Writer, stealthMode bool) error {
|
|||||||
ephemeral := isEphemeralBranch()
|
ephemeral := isEphemeralBranch()
|
||||||
noPush := config.GetBool("no-push")
|
noPush := config.GetBool("no-push")
|
||||||
autoSync := isDaemonAutoSyncing()
|
autoSync := isDaemonAutoSyncing()
|
||||||
|
localOnly := !primeHasGitRemote()
|
||||||
|
|
||||||
var closeProtocol string
|
var closeProtocol string
|
||||||
var closeNote string
|
var closeNote string
|
||||||
@@ -255,8 +264,8 @@ func outputCLIContext(w io.Writer, stealthMode bool) error {
|
|||||||
var completingWorkflow string
|
var completingWorkflow string
|
||||||
var gitWorkflowRule string
|
var gitWorkflowRule string
|
||||||
|
|
||||||
if stealthMode {
|
if stealthMode || localOnly {
|
||||||
// Stealth mode: only flush to JSONL, no git operations
|
// Stealth mode or local-only: only flush to JSONL, no git operations
|
||||||
closeProtocol = `[ ] bd sync --flush-only (export beads to JSONL only)`
|
closeProtocol = `[ ] bd sync --flush-only (export beads to JSONL only)`
|
||||||
syncSection = `### Sync & Collaboration
|
syncSection = `### Sync & Collaboration
|
||||||
- ` + "`bd sync --flush-only`" + ` - Export to JSONL`
|
- ` + "`bd sync --flush-only`" + ` - Export to JSONL`
|
||||||
@@ -265,7 +274,13 @@ func outputCLIContext(w io.Writer, stealthMode bool) error {
|
|||||||
bd close <id1> <id2> ... # Close all completed issues at once
|
bd close <id1> <id2> ... # Close all completed issues at once
|
||||||
bd sync --flush-only # Export to JSONL
|
bd sync --flush-only # Export to JSONL
|
||||||
` + "```"
|
` + "```"
|
||||||
gitWorkflowRule = "Git workflow: stealth mode (no git ops)"
|
// Only show local-only note if not in stealth mode (stealth is explicit user choice)
|
||||||
|
if localOnly && !stealthMode {
|
||||||
|
closeNote = "**Note:** No git remote configured. Issues are saved locally only."
|
||||||
|
gitWorkflowRule = "Git workflow: local-only (no git remote)"
|
||||||
|
} else {
|
||||||
|
gitWorkflowRule = "Git workflow: stealth mode (no git ops)"
|
||||||
|
}
|
||||||
} else if autoSync && !ephemeral && !noPush {
|
} else if autoSync && !ephemeral && !noPush {
|
||||||
// Daemon is auto-syncing - simplified protocol (no bd sync needed)
|
// Daemon is auto-syncing - simplified protocol (no bd sync needed)
|
||||||
closeProtocol = `[ ] 1. git status (check what changed)
|
closeProtocol = `[ ] 1. git status (check what changed)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode bool
|
mcpMode bool
|
||||||
stealthMode bool
|
stealthMode bool
|
||||||
ephemeralMode bool
|
ephemeralMode bool
|
||||||
|
localOnlyMode bool
|
||||||
expectText []string
|
expectText []string
|
||||||
rejectText []string
|
rejectText []string
|
||||||
}{
|
}{
|
||||||
@@ -20,6 +21,7 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode: false,
|
mcpMode: false,
|
||||||
stealthMode: false,
|
stealthMode: false,
|
||||||
ephemeralMode: false,
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: false,
|
||||||
expectText: []string{"Beads Workflow Context", "bd sync", "git push"},
|
expectText: []string{"Beads Workflow Context", "bd sync", "git push"},
|
||||||
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
||||||
},
|
},
|
||||||
@@ -28,6 +30,7 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode: false,
|
mcpMode: false,
|
||||||
stealthMode: false,
|
stealthMode: false,
|
||||||
ephemeralMode: true,
|
ephemeralMode: true,
|
||||||
|
localOnlyMode: false,
|
||||||
expectText: []string{"Beads Workflow Context", "bd sync --from-main", "ephemeral branch"},
|
expectText: []string{"Beads Workflow Context", "bd sync --from-main", "ephemeral branch"},
|
||||||
rejectText: []string{"bd sync --flush-only", "git push"},
|
rejectText: []string{"bd sync --flush-only", "git push"},
|
||||||
},
|
},
|
||||||
@@ -36,14 +39,43 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode: false,
|
mcpMode: false,
|
||||||
stealthMode: true,
|
stealthMode: true,
|
||||||
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
||||||
|
localOnlyMode: false,
|
||||||
expectText: []string{"Beads Workflow Context", "bd sync --flush-only"},
|
expectText: []string{"Beads Workflow Context", "bd sync --flush-only"},
|
||||||
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "CLI Local-only (no git remote)",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: true,
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync --flush-only", "No git remote configured"},
|
||||||
|
rejectText: []string{"git push", "git pull", "--from-main"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CLI Local-only overrides ephemeral",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: true, // ephemeral is true but local-only takes precedence
|
||||||
|
localOnlyMode: true,
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync --flush-only", "No git remote configured"},
|
||||||
|
rejectText: []string{"git push", "--from-main", "ephemeral branch"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CLI Stealth overrides local-only",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: true,
|
||||||
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: true, // local-only is true but stealth takes precedence
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add", "No git remote configured"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "MCP Normal (non-ephemeral)",
|
name: "MCP Normal (non-ephemeral)",
|
||||||
mcpMode: true,
|
mcpMode: true,
|
||||||
stealthMode: false,
|
stealthMode: false,
|
||||||
ephemeralMode: false,
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: false,
|
||||||
expectText: []string{"Beads Issue Tracker Active", "bd sync", "git push"},
|
expectText: []string{"Beads Issue Tracker Active", "bd sync", "git push"},
|
||||||
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
||||||
},
|
},
|
||||||
@@ -52,6 +84,7 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode: true,
|
mcpMode: true,
|
||||||
stealthMode: false,
|
stealthMode: false,
|
||||||
ephemeralMode: true,
|
ephemeralMode: true,
|
||||||
|
localOnlyMode: false,
|
||||||
expectText: []string{"Beads Issue Tracker Active", "bd sync --from-main", "ephemeral branch"},
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --from-main", "ephemeral branch"},
|
||||||
rejectText: []string{"bd sync --flush-only", "git push"},
|
rejectText: []string{"bd sync --flush-only", "git push"},
|
||||||
},
|
},
|
||||||
@@ -60,6 +93,34 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
mcpMode: true,
|
mcpMode: true,
|
||||||
stealthMode: true,
|
stealthMode: true,
|
||||||
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
||||||
|
localOnlyMode: false,
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Local-only (no git remote)",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: true,
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "git pull", "--from-main"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Local-only overrides ephemeral",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: true, // ephemeral is true but local-only takes precedence
|
||||||
|
localOnlyMode: true,
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "--from-main", "ephemeral branch"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Stealth overrides local-only",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: true,
|
||||||
|
ephemeralMode: false,
|
||||||
|
localOnlyMode: true, // local-only is true but stealth takes precedence
|
||||||
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
||||||
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
||||||
},
|
},
|
||||||
@@ -68,7 +129,8 @@ func TestOutputContextFunction(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
defer stubIsEphemeralBranch(tt.ephemeralMode)()
|
defer stubIsEphemeralBranch(tt.ephemeralMode)()
|
||||||
defer stubIsDaemonAutoSyncing(false)() // Default: no auto-sync in tests
|
defer stubIsDaemonAutoSyncing(false)() // Default: no auto-sync in tests
|
||||||
|
defer stubPrimeHasGitRemote(!tt.localOnlyMode)() // localOnly = !primeHasGitRemote
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := outputPrimeContext(&buf, tt.mcpMode, tt.stealthMode)
|
err := outputPrimeContext(&buf, tt.mcpMode, tt.stealthMode)
|
||||||
@@ -121,3 +183,20 @@ func stubIsDaemonAutoSyncing(isAutoSync bool) func() {
|
|||||||
isDaemonAutoSyncing = original
|
isDaemonAutoSyncing = original
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stubPrimeHasGitRemote temporarily replaces primeHasGitRemote
|
||||||
|
// with a stub returning returnValue.
|
||||||
|
//
|
||||||
|
// Returns a function to restore the original primeHasGitRemote.
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// defer stubPrimeHasGitRemote(true)()
|
||||||
|
func stubPrimeHasGitRemote(hasRemote bool) func() {
|
||||||
|
original := primeHasGitRemote
|
||||||
|
primeHasGitRemote = func() bool {
|
||||||
|
return hasRemote
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
primeHasGitRemote = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user