Merge pull request #455 from abhinav/add-prime-stealth-flag
feat(prime): add --stealth flag for flush-only workflow
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -13,8 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
primeFullMode bool
|
primeFullMode bool
|
||||||
primeMCPMode bool
|
primeMCPMode bool
|
||||||
|
primeStealthMode bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var primeCmd = &cobra.Command{
|
var primeCmd = &cobra.Command{
|
||||||
@@ -47,8 +49,8 @@ agents from forgetting bd workflow after context compaction.`,
|
|||||||
mcpMode = true
|
mcpMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output workflow context (adaptive based on MCP)
|
// Output workflow context (adaptive based on MCP and stealth mode)
|
||||||
if err := outputPrimeContext(mcpMode); err != nil {
|
if err := outputPrimeContext(os.Stdout, mcpMode, primeStealthMode); err != nil {
|
||||||
// Suppress all errors - silent exit with success
|
// Suppress all errors - silent exit with success
|
||||||
// Never write to stderr (breaks Windows compatibility)
|
// Never write to stderr (breaks Windows compatibility)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@@ -59,6 +61,7 @@ agents from forgetting bd workflow after context compaction.`,
|
|||||||
func init() {
|
func init() {
|
||||||
primeCmd.Flags().BoolVar(&primeFullMode, "full", false, "Force full CLI output (ignore MCP detection)")
|
primeCmd.Flags().BoolVar(&primeFullMode, "full", false, "Force full CLI output (ignore MCP detection)")
|
||||||
primeCmd.Flags().BoolVar(&primeMCPMode, "mcp", false, "Force MCP mode (minimal output)")
|
primeCmd.Flags().BoolVar(&primeMCPMode, "mcp", false, "Force MCP mode (minimal output)")
|
||||||
|
primeCmd.Flags().BoolVar(&primeStealthMode, "stealth", false, "Stealth mode (no git operations, flush only)")
|
||||||
rootCmd.AddCommand(primeCmd)
|
rootCmd.AddCommand(primeCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ func isMCPActive() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isEphemeralBranch detects if current branch has no upstream (ephemeral/local-only)
|
// isEphemeralBranch detects if current branch has no upstream (ephemeral/local-only)
|
||||||
func isEphemeralBranch() bool {
|
var isEphemeralBranch = func() bool {
|
||||||
// git rev-parse --abbrev-ref --symbolic-full-name @{u}
|
// git rev-parse --abbrev-ref --symbolic-full-name @{u}
|
||||||
// Returns error code 128 if no upstream configured
|
// Returns error code 128 if no upstream configured
|
||||||
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||||
@@ -113,19 +116,22 @@ func isEphemeralBranch() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// outputPrimeContext outputs workflow context in markdown format
|
// outputPrimeContext outputs workflow context in markdown format
|
||||||
func outputPrimeContext(mcpMode bool) error {
|
func outputPrimeContext(w io.Writer, mcpMode bool, stealthMode bool) error {
|
||||||
if mcpMode {
|
if mcpMode {
|
||||||
return outputMCPContext()
|
return outputMCPContext(w, stealthMode)
|
||||||
}
|
}
|
||||||
return outputCLIContext()
|
return outputCLIContext(w, stealthMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputMCPContext outputs minimal context for MCP users
|
// outputMCPContext outputs minimal context for MCP users
|
||||||
func outputMCPContext() error {
|
func outputMCPContext(w io.Writer, stealthMode bool) error {
|
||||||
ephemeral := isEphemeralBranch()
|
ephemeral := isEphemeralBranch()
|
||||||
|
|
||||||
var closeProtocol string
|
var closeProtocol string
|
||||||
if ephemeral {
|
if stealthMode {
|
||||||
|
// Stealth mode: only flush to JSONL as there's nothing to commit.
|
||||||
|
closeProtocol = "Before saying \"done\": bd sync --flush-only"
|
||||||
|
} else if ephemeral {
|
||||||
closeProtocol = "Before saying \"done\": git status → git add → bd sync --from-main → git commit (no push - ephemeral branch)"
|
closeProtocol = "Before saying \"done\": git status → git add → bd sync --from-main → git commit (no push - ephemeral branch)"
|
||||||
} else {
|
} else {
|
||||||
closeProtocol = "Before saying \"done\": git status → git add → bd sync → git commit → bd sync → git push"
|
closeProtocol = "Before saying \"done\": git status → git add → bd sync → git commit → bd sync → git push"
|
||||||
@@ -143,12 +149,12 @@ func outputMCPContext() error {
|
|||||||
|
|
||||||
Start: Check ` + "`ready`" + ` tool for available work.
|
Start: Check ` + "`ready`" + ` tool for available work.
|
||||||
`
|
`
|
||||||
fmt.Print(context)
|
_, _ = fmt.Fprint(w, context)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputCLIContext outputs full CLI reference for non-MCP users
|
// outputCLIContext outputs full CLI reference for non-MCP users
|
||||||
func outputCLIContext() error {
|
func outputCLIContext(w io.Writer, stealthMode bool) error {
|
||||||
ephemeral := isEphemeralBranch()
|
ephemeral := isEphemeralBranch()
|
||||||
|
|
||||||
var closeProtocol string
|
var closeProtocol string
|
||||||
@@ -156,7 +162,17 @@ func outputCLIContext() error {
|
|||||||
var syncSection string
|
var syncSection string
|
||||||
var completingWorkflow string
|
var completingWorkflow string
|
||||||
|
|
||||||
if ephemeral {
|
if stealthMode {
|
||||||
|
// Stealth mode: only flush to JSONL, no git operations
|
||||||
|
closeProtocol = `[ ] bd sync --flush-only (export beads to JSONL only)`
|
||||||
|
syncSection = `### Sync & Collaboration
|
||||||
|
- ` + "`bd sync --flush-only`" + ` - Export to JSONL`
|
||||||
|
completingWorkflow = `**Completing work:**
|
||||||
|
` + "```bash" + `
|
||||||
|
bd close <id1> <id2> ... # Close all completed issues at once
|
||||||
|
bd sync --flush-only # Export to JSONL
|
||||||
|
` + "```"
|
||||||
|
} else if ephemeral {
|
||||||
closeProtocol = `[ ] 1. git status (check what changed)
|
closeProtocol = `[ ] 1. git status (check what changed)
|
||||||
[ ] 2. git add <files> (stage code changes)
|
[ ] 2. git add <files> (stage code changes)
|
||||||
[ ] 3. bd sync --from-main (pull beads updates from main)
|
[ ] 3. bd sync --from-main (pull beads updates from main)
|
||||||
@@ -258,6 +274,6 @@ bd create --title="Write tests for X" --type=task
|
|||||||
bd dep add beads-yyy beads-xxx # Tests depend on Feature (Feature blocks tests)
|
bd dep add beads-yyy beads-xxx # Tests depend on Feature (Feature blocks tests)
|
||||||
` + "```" + `
|
` + "```" + `
|
||||||
`
|
`
|
||||||
fmt.Print(context)
|
_, _ = fmt.Fprint(w, context)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
110
cmd/bd/prime_test.go
Normal file
110
cmd/bd/prime_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOutputContextFunction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mcpMode bool
|
||||||
|
stealthMode bool
|
||||||
|
ephemeralMode bool
|
||||||
|
expectText []string
|
||||||
|
rejectText []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "CLI Normal (non-ephemeral)",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: false,
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync", "git push"},
|
||||||
|
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CLI Normal (ephemeral)",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: true,
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync --from-main", "ephemeral branch"},
|
||||||
|
rejectText: []string{"bd sync --flush-only", "git push"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CLI Stealth",
|
||||||
|
mcpMode: false,
|
||||||
|
stealthMode: true,
|
||||||
|
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
||||||
|
expectText: []string{"Beads Workflow Context", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Normal (non-ephemeral)",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: false,
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync", "git push"},
|
||||||
|
rejectText: []string{"bd sync --flush-only", "--from-main"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Normal (ephemeral)",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: false,
|
||||||
|
ephemeralMode: true,
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --from-main", "ephemeral branch"},
|
||||||
|
rejectText: []string{"bd sync --flush-only", "git push"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MCP Stealth",
|
||||||
|
mcpMode: true,
|
||||||
|
stealthMode: true,
|
||||||
|
ephemeralMode: false, // stealth mode overrides ephemeral detection
|
||||||
|
expectText: []string{"Beads Issue Tracker Active", "bd sync --flush-only"},
|
||||||
|
rejectText: []string{"git push", "git pull", "git commit", "git status", "git add"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer stubIsEphemeralBranch(tt.ephemeralMode)()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := outputPrimeContext(&buf, tt.mcpMode, tt.stealthMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("outputPrimeContext failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
for _, expected := range tt.expectText {
|
||||||
|
if !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("Expected text not found: %s", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rejected := range tt.rejectText {
|
||||||
|
if strings.Contains(output, rejected) {
|
||||||
|
t.Errorf("Unexpected text found: %s", rejected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubIsEphemeralBranch temporarily replaces isEphemeralBranch
|
||||||
|
// with a stub returning returnValue.
|
||||||
|
//
|
||||||
|
// Returns a function to restore the original isEphemeralBranch.
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// defer stubIsEphemeralBranch(true)()
|
||||||
|
func stubIsEphemeralBranch(isEphem bool) func() {
|
||||||
|
original := isEphemeralBranch
|
||||||
|
isEphemeralBranch = func() bool {
|
||||||
|
return isEphem
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
isEphemeralBranch = original
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user