feat(ci): Add integration test job; refactor crew startup to use beacon prompt
CI: Add integration test job that runs go test -tags=integration for install, rig, and beads routing tests. Crew lifecycle: Pass startup beacon as Claude's initial prompt instead of nudging after startup. Removes timing-dependent sleep/nudge sequence. Also removes redundant SetEnvironment calls (env vars already exported in BuildCrewStartupCommand). 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -71,3 +71,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=5m
|
args: --timeout=5m
|
||||||
|
|
||||||
|
integration:
|
||||||
|
name: Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "CI Bot"
|
||||||
|
git config --global user.email "ci@gastown.test"
|
||||||
|
|
||||||
|
- name: Install beads (bd)
|
||||||
|
run: go install github.com/steveyegge/beads/cmd/bd@latest
|
||||||
|
|
||||||
|
- name: Build gt
|
||||||
|
run: go build -v -o gt ./cmd/gt
|
||||||
|
|
||||||
|
- name: Add to PATH
|
||||||
|
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Integration Tests
|
||||||
|
run: go test -tags=integration -timeout=5m -v ./internal/cmd/...
|
||||||
|
|||||||
@@ -181,34 +181,35 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("creating session: %w", err)
|
return fmt.Errorf("creating session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment (non-fatal: session works without these)
|
|
||||||
_ = t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
|
||||||
_ = t.SetEnvironment(sessionID, "GT_CREW", name)
|
|
||||||
|
|
||||||
// Wait for shell to be ready
|
// Wait for shell to be ready
|
||||||
if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil {
|
if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil {
|
||||||
return fmt.Errorf("waiting for shell: %w", err)
|
return fmt.Errorf("waiting for shell: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start claude (refresh uses regular permissions, reads handoff mail)
|
// Build the startup beacon for predecessor discovery via /resume
|
||||||
if err := t.SendKeys(sessionID, "claude"); err != nil {
|
// Pass it as Claude's initial prompt - processed when Claude is ready
|
||||||
|
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||||
|
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
|
||||||
|
Recipient: address,
|
||||||
|
Sender: "human",
|
||||||
|
Topic: "refresh",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start claude with environment exports and beacon as initial prompt
|
||||||
|
// Refresh uses regular permissions (no --dangerously-skip-permissions)
|
||||||
|
// SessionStart hook handles context loading (gt prime --hook)
|
||||||
|
claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, beacon)
|
||||||
|
// Remove --dangerously-skip-permissions for refresh (interactive mode)
|
||||||
|
claudeCmd = strings.Replace(claudeCmd, " --dangerously-skip-permissions", "", 1)
|
||||||
|
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
||||||
return fmt.Errorf("starting claude: %w", err)
|
return fmt.Errorf("starting claude: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for Claude to start
|
// Wait for Claude to start (optional, for status feedback)
|
||||||
shells := constants.SupportedShells
|
shells := constants.SupportedShells
|
||||||
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
||||||
// Non-fatal
|
// Non-fatal
|
||||||
}
|
}
|
||||||
time.Sleep(constants.ShutdownNotifyDelay)
|
|
||||||
|
|
||||||
// Inject startup nudge for predecessor discovery via /resume
|
|
||||||
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
|
||||||
_ = session.StartupNudge(t, sessionID, session.StartupNudgeConfig{
|
|
||||||
Recipient: address,
|
|
||||||
Sender: "human",
|
|
||||||
Topic: "refresh",
|
|
||||||
}) // Non-fatal
|
|
||||||
|
|
||||||
fmt.Printf("%s Refreshed crew workspace: %s/%s\n",
|
fmt.Printf("%s Refreshed crew workspace: %s/%s\n",
|
||||||
style.Bold.Render("✓"), r.Name, name)
|
style.Bold.Render("✓"), r.Name, name)
|
||||||
@@ -337,9 +338,6 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Set environment
|
// Set environment
|
||||||
t.SetEnvironment(sessionID, "GT_ROLE", "crew")
|
t.SetEnvironment(sessionID, "GT_ROLE", "crew")
|
||||||
t.SetEnvironment(sessionID, "GT_RIG", r.Name)
|
|
||||||
t.SetEnvironment(sessionID, "GT_CREW", name)
|
|
||||||
|
|
||||||
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
|
// Apply rig-based theming (non-fatal: theming failure doesn't affect operation)
|
||||||
theme := getThemeForRig(r.Name)
|
theme := getThemeForRig(r.Name)
|
||||||
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
|
_ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew")
|
||||||
@@ -351,44 +349,30 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start claude with skip permissions (crew workers are trusted)
|
// Build the startup beacon for predecessor discovery via /resume
|
||||||
// Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes
|
// Pass it as Claude's initial prompt - processed when Claude is ready
|
||||||
claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "")
|
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||||
|
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
|
||||||
|
Recipient: address,
|
||||||
|
Sender: "human",
|
||||||
|
Topic: "restart",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start claude with environment exports and beacon as initial prompt
|
||||||
|
// SessionStart hook handles context loading (gt prime --hook)
|
||||||
|
// The startup protocol tells agent to check mail/hook, no explicit prompt needed
|
||||||
|
claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, beacon)
|
||||||
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
||||||
fmt.Printf("Error starting claude for %s: %v\n", arg, err)
|
fmt.Printf("Error starting claude for %s: %v\n", arg, err)
|
||||||
lastErr = err
|
lastErr = err
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for Claude to start, then prime it
|
// Wait for Claude to start (optional, for status feedback)
|
||||||
shells := constants.SupportedShells
|
shells := constants.SupportedShells
|
||||||
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
||||||
style.PrintWarning("Timeout waiting for Claude to start for %s: %v", arg, err)
|
style.PrintWarning("Timeout waiting for Claude to start for %s: %v", arg, err)
|
||||||
}
|
}
|
||||||
// Give Claude time to initialize after process starts
|
|
||||||
time.Sleep(constants.ShutdownNotifyDelay)
|
|
||||||
|
|
||||||
// Inject startup nudge for predecessor discovery via /resume
|
|
||||||
address := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
|
||||||
_ = session.StartupNudge(t, sessionID, session.StartupNudgeConfig{
|
|
||||||
Recipient: address,
|
|
||||||
Sender: "human",
|
|
||||||
Topic: "restart",
|
|
||||||
}) // Non-fatal: session works without nudge
|
|
||||||
|
|
||||||
if err := t.NudgeSession(sessionID, "gt prime"); err != nil {
|
|
||||||
// Non-fatal: Claude started but priming failed
|
|
||||||
style.PrintWarning("Could not send prime command to %s: %v", arg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send crew resume prompt after prime completes
|
|
||||||
// Use NudgeSession (the canonical way to message Claude) with longer pre-delay
|
|
||||||
// to ensure gt prime has finished processing
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
crewPrompt := "Read your mail, act on anything urgent, else await instructions."
|
|
||||||
if err := t.NudgeSession(sessionID, crewPrompt); err != nil {
|
|
||||||
style.PrintWarning("Could not send resume prompt to %s: %v", arg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s Restarted crew workspace: %s/%s\n",
|
fmt.Printf("%s Restarted crew workspace: %s/%s\n",
|
||||||
style.Bold.Render("✓"), r.Name, name)
|
style.Bold.Render("✓"), r.Name, name)
|
||||||
@@ -514,11 +498,6 @@ func restartCrewSession(rigName, crewName, clonePath string) error {
|
|||||||
return fmt.Errorf("creating session: %w", err)
|
return fmt.Errorf("creating session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment
|
|
||||||
t.SetEnvironment(sessionID, "GT_ROLE", "crew")
|
|
||||||
t.SetEnvironment(sessionID, "GT_RIG", rigName)
|
|
||||||
t.SetEnvironment(sessionID, "GT_CREW", crewName)
|
|
||||||
|
|
||||||
// Apply rig-based theming
|
// Apply rig-based theming
|
||||||
theme := getThemeForRig(rigName)
|
theme := getThemeForRig(rigName)
|
||||||
_ = t.ConfigureGasTownSession(sessionID, theme, rigName, crewName, "crew")
|
_ = t.ConfigureGasTownSession(sessionID, theme, rigName, crewName, "crew")
|
||||||
@@ -528,35 +507,27 @@ func restartCrewSession(rigName, crewName, clonePath string) error {
|
|||||||
return fmt.Errorf("waiting for shell: %w", err)
|
return fmt.Errorf("waiting for shell: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start claude with skip permissions
|
// Build the startup beacon for predecessor discovery via /resume
|
||||||
claudeCmd := config.BuildCrewStartupCommand(rigName, crewName, "", "")
|
// Pass it as Claude's initial prompt - processed when Claude is ready
|
||||||
|
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
||||||
|
beacon := session.FormatStartupNudge(session.StartupNudgeConfig{
|
||||||
|
Recipient: address,
|
||||||
|
Sender: "human",
|
||||||
|
Topic: "restart",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start claude with environment exports and beacon as initial prompt
|
||||||
|
// SessionStart hook handles context loading (gt prime --hook)
|
||||||
|
claudeCmd := config.BuildCrewStartupCommand(rigName, crewName, "", beacon)
|
||||||
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
|
||||||
return fmt.Errorf("starting claude: %w", err)
|
return fmt.Errorf("starting claude: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for Claude to start, then prime it
|
// Wait for Claude to start (optional, for status feedback)
|
||||||
shells := constants.SupportedShells
|
shells := constants.SupportedShells
|
||||||
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil {
|
||||||
// Non-fatal warning
|
// Non-fatal warning
|
||||||
}
|
}
|
||||||
time.Sleep(constants.ShutdownNotifyDelay)
|
|
||||||
|
|
||||||
// Inject startup nudge for predecessor discovery via /resume
|
|
||||||
address := fmt.Sprintf("%s/crew/%s", rigName, crewName)
|
|
||||||
_ = session.StartupNudge(t, sessionID, session.StartupNudgeConfig{
|
|
||||||
Recipient: address,
|
|
||||||
Sender: "human",
|
|
||||||
Topic: "restart",
|
|
||||||
}) // Non-fatal
|
|
||||||
|
|
||||||
if err := t.NudgeSession(sessionID, "gt prime"); err != nil {
|
|
||||||
// Non-fatal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send crew resume prompt after prime completes
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
crewPrompt := "Read your mail, act on anything urgent, else await instructions."
|
|
||||||
_ = t.NudgeSession(sessionID, crewPrompt)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user