Add gt park/resume commands for async gate coordination (gt-twjr5.5)
Implements agent parking and resumption on gates: - gt park <gate-id>: Parks work on a gate, saves context - gt resume: Checks for cleared gates and restores work - gt gate wake: Sends wake mail to waiters when gate closes These commands enable agents to safely suspend work while waiting for external conditions (timers, CI, human approval) and resume when gates clear. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
186
internal/cmd/gate.go
Normal file
186
internal/cmd/gate.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// Gate command provides gt wrappers for gate operations.
|
||||
// Most gate commands are in beads (bd gate ...), but gt provides
|
||||
// integration with the Gas Town mail system for wake notifications.
|
||||
|
||||
var gateCmd = &cobra.Command{
|
||||
Use: "gate",
|
||||
GroupID: GroupWork,
|
||||
Short: "Gate coordination commands",
|
||||
Long: `Gate commands for async coordination.
|
||||
|
||||
Most gate commands are in beads:
|
||||
bd gate create - Create a gate (timer, gh:run, human, mail)
|
||||
bd gate show - Show gate details
|
||||
bd gate list - List open gates
|
||||
bd gate close - Close a gate
|
||||
bd gate approve - Approve a human gate
|
||||
bd gate eval - Evaluate and close elapsed gates
|
||||
|
||||
The gt gate command provides Gas Town integration:
|
||||
gt gate wake - Send wake mail to gate waiters after close`,
|
||||
}
|
||||
|
||||
var gateWakeCmd = &cobra.Command{
|
||||
Use: "wake <gate-id>",
|
||||
Short: "Send wake mail to gate waiters",
|
||||
Long: `Send wake mail to all waiters on a gate.
|
||||
|
||||
This command should be called after a gate closes to notify waiting agents.
|
||||
Typically called by Deacon after 'bd gate eval' or after manual gate close.
|
||||
|
||||
The wake mail includes:
|
||||
- Gate ID and close reason
|
||||
- Instructions to run 'gt resume'
|
||||
|
||||
Examples:
|
||||
# After manual gate close
|
||||
bd gate close gt-xxx --reason "Approved"
|
||||
gt gate wake gt-xxx
|
||||
|
||||
# In Deacon patrol after gate eval
|
||||
for gate in $(bd gate eval --json | jq -r '.closed[]'); do
|
||||
gt gate wake $gate
|
||||
done`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runGateWake,
|
||||
}
|
||||
|
||||
var (
|
||||
gateWakeJSON bool
|
||||
gateWakeDryRun bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
gateWakeCmd.Flags().BoolVar(&gateWakeJSON, "json", false, "Output as JSON")
|
||||
gateWakeCmd.Flags().BoolVarP(&gateWakeDryRun, "dry-run", "n", false, "Show what would be done")
|
||||
|
||||
gateCmd.AddCommand(gateWakeCmd)
|
||||
rootCmd.AddCommand(gateCmd)
|
||||
}
|
||||
|
||||
// GateWakeResult represents the result of sending wake mail.
|
||||
type GateWakeResult struct {
|
||||
GateID string `json:"gate_id"`
|
||||
CloseReason string `json:"close_reason"`
|
||||
Waiters []string `json:"waiters"`
|
||||
Notified []string `json:"notified"`
|
||||
Failed []string `json:"failed,omitempty"`
|
||||
}
|
||||
|
||||
func runGateWake(cmd *cobra.Command, args []string) error {
|
||||
gateID := args[0]
|
||||
|
||||
// Get gate info
|
||||
gateCheck := exec.Command("bd", "gate", "show", gateID, "--json")
|
||||
gateOutput, err := gateCheck.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gate '%s' not found or not accessible", gateID)
|
||||
}
|
||||
|
||||
var gateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CloseReason string `json:"close_reason"`
|
||||
Waiters []string `json:"waiters"`
|
||||
}
|
||||
if err := json.Unmarshal(gateOutput, &gateInfo); err != nil {
|
||||
return fmt.Errorf("parsing gate info: %w", err)
|
||||
}
|
||||
|
||||
if gateInfo.Status != "closed" {
|
||||
return fmt.Errorf("gate '%s' is not closed (status: %s) - wake mail only sent for closed gates", gateID, gateInfo.Status)
|
||||
}
|
||||
|
||||
if len(gateInfo.Waiters) == 0 {
|
||||
if gateWakeJSON {
|
||||
result := GateWakeResult{
|
||||
GateID: gateID,
|
||||
CloseReason: gateInfo.CloseReason,
|
||||
Waiters: []string{},
|
||||
Notified: []string{},
|
||||
}
|
||||
return outputGateWakeResult(result)
|
||||
}
|
||||
fmt.Printf("%s Gate %s has no waiters to notify\n", style.Dim.Render("○"), gateID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if gateWakeDryRun {
|
||||
fmt.Printf("Would send wake mail for gate %s to:\n", gateID)
|
||||
for _, w := range gateInfo.Waiters {
|
||||
fmt.Printf(" - %s\n", w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find town root for mail routing
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding town root: %w", err)
|
||||
}
|
||||
|
||||
router := mail.NewRouter(townRoot)
|
||||
|
||||
result := GateWakeResult{
|
||||
GateID: gateID,
|
||||
CloseReason: gateInfo.CloseReason,
|
||||
Waiters: gateInfo.Waiters,
|
||||
Notified: []string{},
|
||||
Failed: []string{},
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("🚦 GATE CLEARED: %s", gateID)
|
||||
body := fmt.Sprintf("Gate %s has closed.\n\nReason: %s\n\nRun 'gt resume' to continue your parked work.",
|
||||
gateID, gateInfo.CloseReason)
|
||||
|
||||
for _, waiter := range gateInfo.Waiters {
|
||||
msg := &mail.Message{
|
||||
From: "deacon/",
|
||||
To: waiter,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
Type: mail.TypeNotification,
|
||||
Priority: mail.PriorityHigh,
|
||||
Wisp: true,
|
||||
}
|
||||
if err := router.Send(msg); err != nil {
|
||||
result.Failed = append(result.Failed, waiter)
|
||||
} else {
|
||||
result.Notified = append(result.Notified, waiter)
|
||||
}
|
||||
}
|
||||
|
||||
if gateWakeJSON {
|
||||
return outputGateWakeResult(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Sent wake mail for gate %s\n", style.Bold.Render("🚦"), gateID)
|
||||
if len(result.Notified) > 0 {
|
||||
fmt.Printf(" Notified: %v\n", result.Notified)
|
||||
}
|
||||
if len(result.Failed) > 0 {
|
||||
fmt.Printf(" Failed: %v\n", result.Failed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputGateWakeResult(result GateWakeResult) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
249
internal/cmd/park.go
Normal file
249
internal/cmd/park.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/wisp"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// Park command parks work on a gate, allowing agent to exit safely.
|
||||
// When the gate closes, waiters are notified and can resume.
|
||||
|
||||
var parkCmd = &cobra.Command{
|
||||
Use: "park <gate-id>",
|
||||
GroupID: GroupWork,
|
||||
Short: "Park work on a gate for async resumption",
|
||||
Long: `Park current work on a gate, allowing the agent to exit safely.
|
||||
|
||||
When you need to wait for an external condition (timer, CI, human approval),
|
||||
park your work on a gate. When the gate closes, you'll receive wake mail.
|
||||
|
||||
The park command:
|
||||
1. Saves your current hook state (molecule/bead you're working on)
|
||||
2. Adds you as a waiter on the gate
|
||||
3. Stores context notes in the parked state
|
||||
|
||||
After parking, you can exit the session safely. Use 'gt resume' to check
|
||||
for cleared gates and continue work.
|
||||
|
||||
Examples:
|
||||
# Create a timer gate and park work on it
|
||||
bd gate create --await timer:30m --title "Coffee break"
|
||||
gt park <gate-id> -m "Taking a break, will resume auth work"
|
||||
|
||||
# Park on a human approval gate
|
||||
bd gate create --await human:deploy-approval --notify ops/
|
||||
gt park <gate-id> -m "Deploy staged, awaiting approval"
|
||||
|
||||
# Park on a GitHub Actions gate
|
||||
bd gate create --await gh:run:123456789
|
||||
gt park <gate-id> -m "Waiting for CI to complete"`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runPark,
|
||||
}
|
||||
|
||||
var (
|
||||
parkMessage string
|
||||
parkDryRun bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
parkCmd.Flags().StringVarP(&parkMessage, "message", "m", "", "Context notes for resumption")
|
||||
parkCmd.Flags().BoolVarP(&parkDryRun, "dry-run", "n", false, "Show what would be done without executing")
|
||||
rootCmd.AddCommand(parkCmd)
|
||||
}
|
||||
|
||||
// ParkedWork represents work state saved when parking on a gate.
|
||||
type ParkedWork struct {
|
||||
// AgentID is the agent that parked (e.g., "gastown/crew/max")
|
||||
AgentID string `json:"agent_id"`
|
||||
|
||||
// GateID is the gate we're parked on
|
||||
GateID string `json:"gate_id"`
|
||||
|
||||
// BeadID is the bead/molecule we were working on
|
||||
BeadID string `json:"bead_id,omitempty"`
|
||||
|
||||
// Formula is the formula attached to the work (if any)
|
||||
Formula string `json:"formula,omitempty"`
|
||||
|
||||
// Context is additional context notes from the agent
|
||||
Context string `json:"context,omitempty"`
|
||||
|
||||
// ParkedAt is when the work was parked
|
||||
ParkedAt time.Time `json:"parked_at"`
|
||||
}
|
||||
|
||||
func runPark(cmd *cobra.Command, args []string) error {
|
||||
gateID := args[0]
|
||||
|
||||
// Verify gate exists and is open
|
||||
gateCheck := exec.Command("bd", "gate", "show", gateID, "--json")
|
||||
gateOutput, err := gateCheck.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gate '%s' not found or not accessible", gateID)
|
||||
}
|
||||
|
||||
// Parse gate info to verify it's open
|
||||
var gateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(gateOutput, &gateInfo); err != nil {
|
||||
return fmt.Errorf("parsing gate info: %w", err)
|
||||
}
|
||||
if gateInfo.Status == "closed" {
|
||||
return fmt.Errorf("gate '%s' is already closed - nothing to park on", gateID)
|
||||
}
|
||||
|
||||
// Detect agent identity
|
||||
agentID, _, cloneRoot, err := resolveSelfTarget()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detecting agent identity: %w", err)
|
||||
}
|
||||
|
||||
// Read current hook state (if any)
|
||||
var beadID, formula, hookContext string
|
||||
if hook, err := wisp.ReadHook(cloneRoot, agentID); err == nil {
|
||||
beadID = hook.BeadID
|
||||
formula = hook.Formula
|
||||
hookContext = hook.Context
|
||||
}
|
||||
|
||||
// Build context combining hook context and new message
|
||||
context := ""
|
||||
if hookContext != "" && parkMessage != "" {
|
||||
context = hookContext + "\n---\n" + parkMessage
|
||||
} else if hookContext != "" {
|
||||
context = hookContext
|
||||
} else if parkMessage != "" {
|
||||
context = parkMessage
|
||||
}
|
||||
|
||||
// Create parked work record
|
||||
parked := &ParkedWork{
|
||||
AgentID: agentID,
|
||||
GateID: gateID,
|
||||
BeadID: beadID,
|
||||
Formula: formula,
|
||||
Context: context,
|
||||
ParkedAt: time.Now(),
|
||||
}
|
||||
|
||||
if parkDryRun {
|
||||
fmt.Printf("Would park on gate %s\n", gateID)
|
||||
fmt.Printf(" Agent: %s\n", agentID)
|
||||
if beadID != "" {
|
||||
fmt.Printf(" Bead: %s\n", beadID)
|
||||
}
|
||||
if formula != "" {
|
||||
fmt.Printf(" Formula: %s\n", formula)
|
||||
}
|
||||
if context != "" {
|
||||
fmt.Printf(" Context: %s\n", context)
|
||||
}
|
||||
fmt.Printf("Would add %s as waiter on gate\n", agentID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add agent as waiter on the gate
|
||||
waitCmd := exec.Command("bd", "gate", "wait", gateID, "--notify", agentID)
|
||||
if err := waitCmd.Run(); err != nil {
|
||||
// Not fatal - might already be a waiter
|
||||
fmt.Printf("%s Note: could not add as waiter (may already be registered)\n", style.Dim.Render("⚠"))
|
||||
}
|
||||
|
||||
// Store parked work in a file (alongside hook files)
|
||||
parkedPath := wisp.WispPath(cloneRoot, fmt.Sprintf("parked-%s.json", strings.ReplaceAll(agentID, "/", "_")))
|
||||
parkedJSON, err := json.MarshalIndent(parked, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling parked work: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(parkedPath, parkedJSON, 0644); err != nil {
|
||||
return fmt.Errorf("writing parked state: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Parked work on gate %s\n", style.Bold.Render("🅿️"), gateID)
|
||||
if beadID != "" {
|
||||
fmt.Printf(" Working on: %s\n", beadID)
|
||||
}
|
||||
if context != "" {
|
||||
// Truncate for display
|
||||
displayContext := context
|
||||
if len(displayContext) > 80 {
|
||||
displayContext = displayContext[:77] + "..."
|
||||
}
|
||||
fmt.Printf(" Context: %s\n", displayContext)
|
||||
}
|
||||
fmt.Printf("\n%s You can now safely exit. Run 'gt resume' to check for cleared gates.\n",
|
||||
style.Dim.Render("→"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readParkedWork reads the parked work state for an agent.
|
||||
func readParkedWork(cloneRoot, agentID string) (*ParkedWork, error) {
|
||||
parkedPath := wisp.WispPath(cloneRoot, fmt.Sprintf("parked-%s.json", strings.ReplaceAll(agentID, "/", "_")))
|
||||
data, err := os.ReadFile(parkedPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parked ParkedWork
|
||||
if err := json.Unmarshal(data, &parked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parked, nil
|
||||
}
|
||||
|
||||
// clearParkedWork removes the parked work state for an agent.
|
||||
func clearParkedWork(cloneRoot, agentID string) error {
|
||||
parkedPath := wisp.WispPath(cloneRoot, fmt.Sprintf("parked-%s.json", strings.ReplaceAll(agentID, "/", "_")))
|
||||
err := os.Remove(parkedPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sendGateWakeMail sends wake mail to all waiters when a gate closes.
|
||||
// This should be called after gate close (from Deacon or gate eval).
|
||||
func sendGateWakeMail(gateID, closeReason string, waiters []string) error {
|
||||
// Find town root for mail routing
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding town root: %w", err)
|
||||
}
|
||||
|
||||
router := mail.NewRouter(townRoot)
|
||||
|
||||
for _, waiter := range waiters {
|
||||
msg := &mail.Message{
|
||||
From: "deacon/",
|
||||
To: waiter,
|
||||
Subject: fmt.Sprintf("🚦 GATE CLEARED: %s", gateID),
|
||||
Body: fmt.Sprintf("Gate %s has closed.\n\nReason: %s\n\nRun 'gt resume' to continue your parked work.", gateID, closeReason),
|
||||
Type: mail.TypeNotification,
|
||||
Priority: mail.PriorityHigh,
|
||||
Wisp: true,
|
||||
}
|
||||
if err := router.Send(msg); err != nil {
|
||||
// Log but don't fail on individual send errors
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to send wake mail to %s: %v\n", waiter, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
204
internal/cmd/resume.go
Normal file
204
internal/cmd/resume.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/wisp"
|
||||
)
|
||||
|
||||
// Resume command checks for cleared gates and resumes parked work.
|
||||
|
||||
var resumeCmd = &cobra.Command{
|
||||
Use: "resume",
|
||||
GroupID: GroupWork,
|
||||
Short: "Resume from parked work when gate clears",
|
||||
Long: `Resume work that was parked on a gate.
|
||||
|
||||
This command checks if you have parked work and whether its gate has cleared.
|
||||
If the gate is closed, it restores your work context so you can continue.
|
||||
|
||||
The resume command:
|
||||
1. Checks for parked work state
|
||||
2. Verifies the gate has closed
|
||||
3. Restores the hook with your previous work
|
||||
4. Displays context notes to help you continue
|
||||
|
||||
Examples:
|
||||
gt resume # Check for and resume parked work
|
||||
gt resume --status # Just show parked work status without resuming`,
|
||||
RunE: runResume,
|
||||
}
|
||||
|
||||
var (
|
||||
resumeStatusOnly bool
|
||||
resumeJSON bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
resumeCmd.Flags().BoolVar(&resumeStatusOnly, "status", false, "Just show parked work status")
|
||||
resumeCmd.Flags().BoolVar(&resumeJSON, "json", false, "Output as JSON")
|
||||
rootCmd.AddCommand(resumeCmd)
|
||||
}
|
||||
|
||||
// ResumeStatus represents the current resume state.
|
||||
type ResumeStatus struct {
|
||||
HasParkedWork bool `json:"has_parked_work"`
|
||||
ParkedWork *ParkedWork `json:"parked_work,omitempty"`
|
||||
GateClosed bool `json:"gate_closed"`
|
||||
CloseReason string `json:"close_reason,omitempty"`
|
||||
CanResume bool `json:"can_resume"`
|
||||
}
|
||||
|
||||
func runResume(cmd *cobra.Command, args []string) error {
|
||||
// Detect agent identity
|
||||
agentID, _, cloneRoot, err := resolveSelfTarget()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detecting agent identity: %w", err)
|
||||
}
|
||||
|
||||
// Check for parked work
|
||||
parked, err := readParkedWork(cloneRoot, agentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading parked work: %w", err)
|
||||
}
|
||||
|
||||
status := ResumeStatus{
|
||||
HasParkedWork: parked != nil,
|
||||
ParkedWork: parked,
|
||||
}
|
||||
|
||||
if parked == nil {
|
||||
if resumeJSON {
|
||||
return outputResumeStatus(status)
|
||||
}
|
||||
fmt.Printf("%s No parked work found\n", style.Dim.Render("○"))
|
||||
fmt.Printf(" Use 'gt park <gate-id>' to park work on a gate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check gate status
|
||||
gateCheck := exec.Command("bd", "gate", "show", parked.GateID, "--json")
|
||||
gateOutput, err := gateCheck.Output()
|
||||
if err != nil {
|
||||
// Gate might have been deleted or is inaccessible
|
||||
status.GateClosed = false
|
||||
status.CloseReason = "Gate not accessible"
|
||||
} else {
|
||||
var gateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CloseReason string `json:"close_reason"`
|
||||
}
|
||||
if err := json.Unmarshal(gateOutput, &gateInfo); err == nil {
|
||||
status.GateClosed = gateInfo.Status == "closed"
|
||||
status.CloseReason = gateInfo.CloseReason
|
||||
}
|
||||
}
|
||||
|
||||
status.CanResume = status.GateClosed
|
||||
|
||||
// Status-only mode
|
||||
if resumeStatusOnly {
|
||||
if resumeJSON {
|
||||
return outputResumeStatus(status)
|
||||
}
|
||||
return displayResumeStatus(status, parked)
|
||||
}
|
||||
|
||||
// JSON output
|
||||
if resumeJSON {
|
||||
return outputResumeStatus(status)
|
||||
}
|
||||
|
||||
// If gate not closed yet, show status and exit
|
||||
if !status.GateClosed {
|
||||
fmt.Printf("%s Work parked on gate %s (still open)\n",
|
||||
style.Bold.Render("🅿️"), parked.GateID)
|
||||
if parked.BeadID != "" {
|
||||
fmt.Printf(" Working on: %s\n", parked.BeadID)
|
||||
}
|
||||
fmt.Printf(" Parked at: %s\n", parked.ParkedAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("\n%s Gate still open. Check back later or run 'bd gate show %s'\n",
|
||||
style.Dim.Render("⏳"), parked.GateID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gate closed - resume work!
|
||||
fmt.Printf("%s Gate %s has cleared!\n", style.Bold.Render("🚦"), parked.GateID)
|
||||
if status.CloseReason != "" {
|
||||
fmt.Printf(" Reason: %s\n", status.CloseReason)
|
||||
}
|
||||
|
||||
// Restore hook if we have a bead
|
||||
if parked.BeadID != "" {
|
||||
hook := wisp.NewSlungWork(parked.BeadID, agentID)
|
||||
hook.Formula = parked.Formula
|
||||
hook.Context = parked.Context
|
||||
|
||||
if err := wisp.WriteSlungWork(cloneRoot, agentID, hook); err != nil {
|
||||
return fmt.Errorf("restoring hook: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s Restored work: %s\n", style.Bold.Render("🪝"), parked.BeadID)
|
||||
if parked.Formula != "" {
|
||||
fmt.Printf(" Formula: %s\n", parked.Formula)
|
||||
}
|
||||
}
|
||||
|
||||
// Show context
|
||||
if parked.Context != "" {
|
||||
fmt.Printf("\n%s Context:\n", style.Bold.Render("📝"))
|
||||
fmt.Println(parked.Context)
|
||||
}
|
||||
|
||||
// Clear parked work state
|
||||
if err := clearParkedWork(cloneRoot, agentID); err != nil {
|
||||
// Non-fatal
|
||||
fmt.Printf("%s Warning: could not clear parked state: %v\n", style.Dim.Render("⚠"), err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s Ready to continue!\n", style.Bold.Render("✓"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputResumeStatus(status ResumeStatus) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(status)
|
||||
}
|
||||
|
||||
func displayResumeStatus(status ResumeStatus, parked *ParkedWork) error {
|
||||
if !status.HasParkedWork {
|
||||
fmt.Printf("%s No parked work\n", style.Dim.Render("○"))
|
||||
return nil
|
||||
}
|
||||
|
||||
gateStatus := "open"
|
||||
gateIcon := "⏳"
|
||||
if status.GateClosed {
|
||||
gateStatus = "closed"
|
||||
gateIcon = "✓"
|
||||
}
|
||||
|
||||
fmt.Printf("%s Parked work status:\n", style.Bold.Render("🅿️"))
|
||||
fmt.Printf(" Gate: %s %s (%s)\n", gateIcon, parked.GateID, gateStatus)
|
||||
if parked.BeadID != "" {
|
||||
fmt.Printf(" Bead: %s\n", parked.BeadID)
|
||||
}
|
||||
if parked.Formula != "" {
|
||||
fmt.Printf(" Formula: %s\n", parked.Formula)
|
||||
}
|
||||
fmt.Printf(" Parked: %s\n", parked.ParkedAt.Format("2006-01-02 15:04:05"))
|
||||
|
||||
if status.GateClosed {
|
||||
fmt.Printf("\n%s Gate cleared! Run 'gt resume' (without --status) to restore work.\n",
|
||||
style.Bold.Render("→"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user