feat(lifecycle): immediate daemon notification via SIGUSR1

- daemon.go: Add SIGUSR1 handler to process lifecycle requests immediately
- handoff.go: Signal daemon after sending lifecycle mail to deacon/
- mail.go: Show message IDs in hook output for direct reading

Previously, gt handoff would wait up to 5 min for daemon heartbeat poll.
Now lifecycle requests are processed within milliseconds.

Closes gt-zut3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 20:28:36 -08:00
parent 57917521e6
commit e5c88ae9d4
3 changed files with 47 additions and 4 deletions

View File

@@ -6,7 +6,9 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
@@ -111,6 +113,16 @@ func runHandoff(cmd *cobra.Command, args []string) error {
}
fmt.Printf("%s Sent %s request to %s\n", style.Bold.Render("✓"), action, manager)
// Signal daemon for immediate processing (if manager is deacon)
if manager == "deacon/" {
if err := signalDaemon(townRoot); err != nil {
// Non-fatal: daemon will eventually poll
fmt.Printf("%s Could not signal daemon (will poll): %v\n", style.Dim.Render("○"), err)
} else {
fmt.Printf("%s Signaled daemon for immediate processing\n", style.Bold.Render("✓"))
}
}
// Set requesting state
if err := setRequestingState(role, action, townRoot); err != nil {
fmt.Printf("Warning: failed to set state: %v\n", err)
@@ -357,3 +369,28 @@ func setRequestingState(role Role, action HandoffAction, townRoot string) error
return os.WriteFile(stateFile, data, 0644)
}
// signalDaemon sends SIGUSR1 to the daemon to trigger immediate lifecycle processing.
func signalDaemon(townRoot string) error {
pidFile := filepath.Join(townRoot, "daemon", "daemon.pid")
data, err := os.ReadFile(pidFile)
if err != nil {
return fmt.Errorf("reading daemon PID: %w", err)
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return fmt.Errorf("parsing daemon PID: %w", err)
}
process, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("finding daemon process: %w", err)
}
if err := process.Signal(syscall.SIGUSR1); err != nil {
return fmt.Errorf("signaling daemon: %w", err)
}
return nil
}

View File

@@ -597,7 +597,7 @@ func runMailCheck(cmd *cobra.Command, args []string) error {
messages, _ := mailbox.ListUnread()
var subjects []string
for _, msg := range messages {
subjects = append(subjects, fmt.Sprintf("- From %s: %s", msg.From, msg.Subject))
subjects = append(subjects, fmt.Sprintf("- %s from %s: %s", msg.ID, msg.From, msg.Subject))
}
fmt.Println("<system-reminder>")