feat(dnd): Add notification level control (gt-xmsme)
Implements Do Not Disturb mode for Gas Town agents: - Add notification_level to agent bead schema (verbose, normal, muted) - gt dnd [on|off|status] - quick toggle for DND mode - gt notify [verbose|normal|muted] - fine-grained control - gt nudge checks target DND status, skips if muted - --force flag on nudge to override DND This allows agents (especially Mayor) to mute notifications during focused work like swarm coordination. Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -770,15 +770,23 @@ func (b *Beads) IsBeadsRepo() bool {
|
|||||||
// AgentFields holds structured fields for agent beads.
|
// AgentFields holds structured fields for agent beads.
|
||||||
// These are stored as "key: value" lines in the description.
|
// These are stored as "key: value" lines in the description.
|
||||||
type AgentFields struct {
|
type AgentFields struct {
|
||||||
RoleType string // polecat, witness, refinery, deacon, mayor
|
RoleType string // polecat, witness, refinery, deacon, mayor
|
||||||
Rig string // Rig name (empty for global agents like mayor/deacon)
|
Rig string // Rig name (empty for global agents like mayor/deacon)
|
||||||
AgentState string // spawning, working, done, stuck
|
AgentState string // spawning, working, done, stuck
|
||||||
HookBead string // Currently pinned work bead ID
|
HookBead string // Currently pinned work bead ID
|
||||||
RoleBead string // Role definition bead ID (canonical location; may not exist yet)
|
RoleBead string // Role definition bead ID (canonical location; may not exist yet)
|
||||||
CleanupStatus string // ZFC: polecat self-reports git state (clean, has_uncommitted, has_stash, has_unpushed)
|
CleanupStatus string // ZFC: polecat self-reports git state (clean, has_uncommitted, has_stash, has_unpushed)
|
||||||
ActiveMR string // Currently active merge request bead ID (for traceability)
|
ActiveMR string // Currently active merge request bead ID (for traceability)
|
||||||
|
NotificationLevel string // DND mode: verbose, normal, muted (default: normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification level constants
|
||||||
|
const (
|
||||||
|
NotifyVerbose = "verbose" // All notifications (mail, convoy events, etc.)
|
||||||
|
NotifyNormal = "normal" // Important events only (default)
|
||||||
|
NotifyMuted = "muted" // Silent/DND mode - batch for later
|
||||||
|
)
|
||||||
|
|
||||||
// FormatAgentDescription creates a description string from agent fields.
|
// FormatAgentDescription creates a description string from agent fields.
|
||||||
func FormatAgentDescription(title string, fields *AgentFields) string {
|
func FormatAgentDescription(title string, fields *AgentFields) string {
|
||||||
if fields == nil {
|
if fields == nil {
|
||||||
@@ -822,6 +830,12 @@ func FormatAgentDescription(title string, fields *AgentFields) string {
|
|||||||
lines = append(lines, "active_mr: null")
|
lines = append(lines, "active_mr: null")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fields.NotificationLevel != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("notification_level: %s", fields.NotificationLevel))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, "notification_level: null")
|
||||||
|
}
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +875,8 @@ func ParseAgentFields(description string) *AgentFields {
|
|||||||
fields.CleanupStatus = value
|
fields.CleanupStatus = value
|
||||||
case "active_mr":
|
case "active_mr":
|
||||||
fields.ActiveMR = value
|
fields.ActiveMR = value
|
||||||
|
case "notification_level":
|
||||||
|
fields.NotificationLevel = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,6 +1011,47 @@ func (b *Beads) UpdateAgentActiveMR(id string, activeMR string) error {
|
|||||||
return b.Update(id, UpdateOptions{Description: &description})
|
return b.Update(id, UpdateOptions{Description: &description})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAgentNotificationLevel updates the notification_level field in an agent bead.
|
||||||
|
// Valid levels: verbose, normal, muted (DND mode).
|
||||||
|
// Pass empty string to reset to default (normal).
|
||||||
|
func (b *Beads) UpdateAgentNotificationLevel(id string, level string) error {
|
||||||
|
// Validate level
|
||||||
|
if level != "" && level != NotifyVerbose && level != NotifyNormal && level != NotifyMuted {
|
||||||
|
return fmt.Errorf("invalid notification level %q: must be verbose, normal, or muted", level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First get current issue to preserve other fields
|
||||||
|
issue, err := b.Show(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse existing fields
|
||||||
|
fields := ParseAgentFields(issue.Description)
|
||||||
|
fields.NotificationLevel = level
|
||||||
|
|
||||||
|
// Format new description
|
||||||
|
description := FormatAgentDescription(issue.Title, fields)
|
||||||
|
|
||||||
|
return b.Update(id, UpdateOptions{Description: &description})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAgentNotificationLevel returns the notification level for an agent.
|
||||||
|
// Returns "normal" if not set (the default).
|
||||||
|
func (b *Beads) GetAgentNotificationLevel(id string) (string, error) {
|
||||||
|
_, fields, err := b.GetAgentBead(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if fields == nil {
|
||||||
|
return NotifyNormal, nil
|
||||||
|
}
|
||||||
|
if fields.NotificationLevel == "" {
|
||||||
|
return NotifyNormal, nil
|
||||||
|
}
|
||||||
|
return fields.NotificationLevel, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteAgentBead permanently deletes an agent bead.
|
// DeleteAgentBead permanently deletes an agent bead.
|
||||||
// Uses --hard --force for immediate permanent deletion (no tombstone).
|
// Uses --hard --force for immediate permanent deletion (no tombstone).
|
||||||
func (b *Beads) DeleteAgentBead(id string) error {
|
func (b *Beads) DeleteAgentBead(id string) error {
|
||||||
|
|||||||
129
internal/cmd/dnd.go
Normal file
129
internal/cmd/dnd.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dndCmd = &cobra.Command{
|
||||||
|
Use: "dnd [on|off|status]",
|
||||||
|
GroupID: GroupComm,
|
||||||
|
Short: "Toggle Do Not Disturb mode for notifications",
|
||||||
|
Long: `Control notification level for the current agent.
|
||||||
|
|
||||||
|
Do Not Disturb (DND) mode mutes non-critical notifications,
|
||||||
|
allowing you to focus on work without interruption.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
on Enable DND mode (mute notifications)
|
||||||
|
off Disable DND mode (resume normal notifications)
|
||||||
|
status Show current notification level
|
||||||
|
|
||||||
|
Without arguments, toggles DND mode.
|
||||||
|
|
||||||
|
Related: gt notify - for fine-grained notification level control`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: runDnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(dndCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDnd(cmd *cobra.Command, args []string) error {
|
||||||
|
// Get current agent bead ID
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("determining role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := RoleContext{
|
||||||
|
Role: roleInfo.Role,
|
||||||
|
Rig: roleInfo.Rig,
|
||||||
|
Polecat: roleInfo.Polecat,
|
||||||
|
TownRoot: townRoot,
|
||||||
|
WorkDir: cwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
agentBeadID := getAgentBeadID(ctx)
|
||||||
|
if agentBeadID == "" {
|
||||||
|
return fmt.Errorf("could not determine agent bead ID for role %s", roleInfo.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
bd := beads.New(townRoot)
|
||||||
|
|
||||||
|
// Get current level
|
||||||
|
currentLevel, err := bd.GetAgentNotificationLevel(agentBeadID)
|
||||||
|
if err != nil {
|
||||||
|
// Agent bead might not exist yet - default to normal
|
||||||
|
currentLevel = beads.NotifyNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine action
|
||||||
|
var action string
|
||||||
|
if len(args) == 0 {
|
||||||
|
// Toggle: if muted -> normal, else -> muted
|
||||||
|
if currentLevel == beads.NotifyMuted {
|
||||||
|
action = "off"
|
||||||
|
} else {
|
||||||
|
action = "on"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "on":
|
||||||
|
if err := bd.UpdateAgentNotificationLevel(agentBeadID, beads.NotifyMuted); err != nil {
|
||||||
|
return fmt.Errorf("enabling DND: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s DND enabled - notifications muted\n", style.SuccessPrefix)
|
||||||
|
fmt.Printf(" Run %s to resume notifications\n", style.Bold.Render("gt dnd off"))
|
||||||
|
|
||||||
|
case "off":
|
||||||
|
if err := bd.UpdateAgentNotificationLevel(agentBeadID, beads.NotifyNormal); err != nil {
|
||||||
|
return fmt.Errorf("disabling DND: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s DND disabled - notifications resumed\n", style.SuccessPrefix)
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
levelDisplay := currentLevel
|
||||||
|
if levelDisplay == "" {
|
||||||
|
levelDisplay = beads.NotifyNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := "🔔"
|
||||||
|
description := "All important notifications"
|
||||||
|
switch levelDisplay {
|
||||||
|
case beads.NotifyVerbose:
|
||||||
|
icon = "🔊"
|
||||||
|
description = "All notifications (verbose)"
|
||||||
|
case beads.NotifyMuted:
|
||||||
|
icon = "🔕"
|
||||||
|
description = "Notifications muted (DND)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Notification level: %s\n", icon, style.Bold.Render(levelDisplay))
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render(description))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown action %q: use on, off, or status", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
internal/cmd/dnd_test.go
Normal file
33
internal/cmd/dnd_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddressToAgentBeadID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
address string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"mayor", "gt-mayor"},
|
||||||
|
{"deacon", "gt-deacon"},
|
||||||
|
{"gastown/witness", "gt-gastown-witness"},
|
||||||
|
{"gastown/refinery", "gt-gastown-refinery"},
|
||||||
|
{"gastown/alpha", "gt-gastown-polecat-alpha"},
|
||||||
|
{"gastown/crew/max", "gt-gastown-crew-max"},
|
||||||
|
{"beads/witness", "gt-beads-witness"},
|
||||||
|
{"beads/beta", "gt-beads-polecat-beta"},
|
||||||
|
// Invalid addresses should return empty string
|
||||||
|
{"invalid", ""},
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.address, func(t *testing.T) {
|
||||||
|
got := addressToAgentBeadID(tt.address)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("addressToAgentBeadID(%q) = %q, want %q", tt.address, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
131
internal/cmd/notify.go
Normal file
131
internal/cmd/notify.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var notifyCmd = &cobra.Command{
|
||||||
|
Use: "notify [verbose|normal|muted]",
|
||||||
|
GroupID: GroupComm,
|
||||||
|
Short: "Set notification level",
|
||||||
|
Long: `Control the notification level for the current agent.
|
||||||
|
|
||||||
|
Notification levels:
|
||||||
|
verbose All notifications (mail, convoy events, status updates)
|
||||||
|
normal Important notifications only (default)
|
||||||
|
muted Silent/DND mode - batch notifications for later
|
||||||
|
|
||||||
|
Without arguments, shows the current notification level.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt notify # Show current level
|
||||||
|
gt notify verbose # Enable all notifications
|
||||||
|
gt notify normal # Default notification level
|
||||||
|
gt notify muted # Enable DND mode
|
||||||
|
|
||||||
|
Related: gt dnd - quick toggle for DND mode`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: runNotify,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(notifyCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNotify(cmd *cobra.Command, args []string) error {
|
||||||
|
// Get current agent bead ID
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("determining role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := RoleContext{
|
||||||
|
Role: roleInfo.Role,
|
||||||
|
Rig: roleInfo.Rig,
|
||||||
|
Polecat: roleInfo.Polecat,
|
||||||
|
TownRoot: townRoot,
|
||||||
|
WorkDir: cwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
agentBeadID := getAgentBeadID(ctx)
|
||||||
|
if agentBeadID == "" {
|
||||||
|
return fmt.Errorf("could not determine agent bead ID for role %s", roleInfo.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
bd := beads.New(townRoot)
|
||||||
|
|
||||||
|
// Get current level
|
||||||
|
currentLevel, err := bd.GetAgentNotificationLevel(agentBeadID)
|
||||||
|
if err != nil {
|
||||||
|
// Agent bead might not exist yet - default to normal
|
||||||
|
currentLevel = beads.NotifyNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
// No args: show current level
|
||||||
|
if len(args) == 0 {
|
||||||
|
showNotificationLevel(currentLevel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new level
|
||||||
|
newLevel := args[0]
|
||||||
|
switch newLevel {
|
||||||
|
case beads.NotifyVerbose, beads.NotifyNormal, beads.NotifyMuted:
|
||||||
|
// Valid level
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid level %q: use verbose, normal, or muted", newLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bd.UpdateAgentNotificationLevel(agentBeadID, newLevel); err != nil {
|
||||||
|
return fmt.Errorf("setting notification level: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Notification level set to %s\n", style.SuccessPrefix, style.Bold.Render(newLevel))
|
||||||
|
showNotificationLevelDescription(newLevel)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNotificationLevel(level string) {
|
||||||
|
if level == "" {
|
||||||
|
level = beads.NotifyNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
icon := "🔔"
|
||||||
|
switch level {
|
||||||
|
case beads.NotifyVerbose:
|
||||||
|
icon = "🔊"
|
||||||
|
case beads.NotifyMuted:
|
||||||
|
icon = "🔕"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Notification level: %s\n", icon, style.Bold.Render(level))
|
||||||
|
showNotificationLevelDescription(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showNotificationLevelDescription(level string) {
|
||||||
|
switch level {
|
||||||
|
case beads.NotifyVerbose:
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("All notifications: mail, convoy events, status updates"))
|
||||||
|
case beads.NotifyNormal:
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("Important notifications: convoy landed, escalations"))
|
||||||
|
case beads.NotifyMuted:
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("Silent mode: notifications batched for later review"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/events"
|
"github.com/steveyegge/gastown/internal/events"
|
||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
@@ -15,10 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var nudgeMessageFlag string
|
var nudgeMessageFlag string
|
||||||
|
var nudgeForceFlag bool
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(nudgeCmd)
|
rootCmd.AddCommand(nudgeCmd)
|
||||||
nudgeCmd.Flags().StringVarP(&nudgeMessageFlag, "message", "m", "", "Message to send")
|
nudgeCmd.Flags().StringVarP(&nudgeMessageFlag, "message", "m", "", "Message to send")
|
||||||
|
nudgeCmd.Flags().BoolVarP(&nudgeForceFlag, "force", "f", false, "Send even if target has DND enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
var nudgeCmd = &cobra.Command{
|
var nudgeCmd = &cobra.Command{
|
||||||
@@ -46,6 +49,10 @@ Channel syntax:
|
|||||||
~/gt/config/messaging.json under "nudge_channels".
|
~/gt/config/messaging.json under "nudge_channels".
|
||||||
Patterns like "gastown/polecats/*" are expanded.
|
Patterns like "gastown/polecats/*" are expanded.
|
||||||
|
|
||||||
|
DND (Do Not Disturb):
|
||||||
|
If the target has DND enabled (gt dnd on), the nudge is skipped.
|
||||||
|
Use --force to override DND and send anyway.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
gt nudge greenplace/furiosa "Check your mail and start working"
|
gt nudge greenplace/furiosa "Check your mail and start working"
|
||||||
gt nudge greenplace/alpha -m "What's your status?"
|
gt nudge greenplace/alpha -m "What's your status?"
|
||||||
@@ -100,6 +107,17 @@ func runNudge(cmd *cobra.Command, args []string) error {
|
|||||||
// Prefix message with sender
|
// Prefix message with sender
|
||||||
message = fmt.Sprintf("[from %s] %s", sender, message)
|
message = fmt.Sprintf("[from %s] %s", sender, message)
|
||||||
|
|
||||||
|
// Check DND status for target (unless force flag or channel target)
|
||||||
|
townRoot, _ := workspace.FindFromCwd()
|
||||||
|
if townRoot != "" && !nudgeForceFlag && !strings.HasPrefix(target, "channel:") {
|
||||||
|
shouldSend, level, _ := shouldNudgeTarget(townRoot, target, nudgeForceFlag)
|
||||||
|
if !shouldSend {
|
||||||
|
fmt.Printf("%s Target has DND enabled (%s) - nudge skipped\n", style.Dim.Render("○"), level)
|
||||||
|
fmt.Printf(" Use %s to override\n", style.Bold.Render("--force"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t := tmux.NewTmux()
|
t := tmux.NewTmux()
|
||||||
|
|
||||||
// Expand role shortcuts to session names
|
// Expand role shortcuts to session names
|
||||||
@@ -399,3 +417,74 @@ func resolveNudgePattern(pattern string, agents []*AgentSession) []string {
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldNudgeTarget checks if a nudge should be sent based on the target's notification level.
|
||||||
|
// Returns (shouldSend bool, level string, err error).
|
||||||
|
// If force is true, always returns true.
|
||||||
|
// If the agent bead cannot be found, returns true (fail-open for backward compatibility).
|
||||||
|
func shouldNudgeTarget(townRoot, targetAddress string, force bool) (bool, string, error) {
|
||||||
|
if force {
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to determine agent bead ID from address
|
||||||
|
agentBeadID := addressToAgentBeadID(targetAddress)
|
||||||
|
if agentBeadID == "" {
|
||||||
|
// Can't determine agent bead, allow the nudge
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bd := beads.New(townRoot)
|
||||||
|
level, err := bd.GetAgentNotificationLevel(agentBeadID)
|
||||||
|
if err != nil {
|
||||||
|
// Agent bead might not exist, allow the nudge
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow nudge if level is not muted
|
||||||
|
return level != beads.NotifyMuted, level, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addressToAgentBeadID converts a target address to an agent bead ID.
|
||||||
|
// Examples:
|
||||||
|
// - "mayor" -> "gt-mayor" (or similar)
|
||||||
|
// - "gastown/witness" -> "gt-gastown-witness"
|
||||||
|
// - "gastown/alpha" -> "gt-gastown-polecat-alpha"
|
||||||
|
//
|
||||||
|
// Returns empty string if the address cannot be converted.
|
||||||
|
func addressToAgentBeadID(address string) string {
|
||||||
|
// Handle special cases
|
||||||
|
switch address {
|
||||||
|
case "mayor":
|
||||||
|
return "gt-mayor"
|
||||||
|
case "deacon":
|
||||||
|
return "gt-deacon"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse rig/role format
|
||||||
|
if !strings.Contains(address, "/") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(address, "/", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
rig := parts[0]
|
||||||
|
role := parts[1]
|
||||||
|
|
||||||
|
switch role {
|
||||||
|
case "witness":
|
||||||
|
return fmt.Sprintf("gt-%s-witness", rig)
|
||||||
|
case "refinery":
|
||||||
|
return fmt.Sprintf("gt-%s-refinery", rig)
|
||||||
|
default:
|
||||||
|
// Assume polecat
|
||||||
|
if strings.HasPrefix(role, "crew/") {
|
||||||
|
crewName := strings.TrimPrefix(role, "crew/")
|
||||||
|
return fmt.Sprintf("gt-%s-crew-%s", rig, crewName)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("gt-%s-polecat-%s", rig, role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user