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:
dementus
2026-01-01 18:45:22 -08:00
committed by Steve Yegge
parent f883a09317
commit b5ac9a2e55
5 changed files with 446 additions and 7 deletions

View File

@@ -770,15 +770,23 @@ func (b *Beads) IsBeadsRepo() bool {
// AgentFields holds structured fields for agent beads.
// These are stored as "key: value" lines in the description.
type AgentFields struct {
RoleType string // polecat, witness, refinery, deacon, mayor
Rig string // Rig name (empty for global agents like mayor/deacon)
AgentState string // spawning, working, done, stuck
HookBead string // Currently pinned work bead ID
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)
ActiveMR string // Currently active merge request bead ID (for traceability)
RoleType string // polecat, witness, refinery, deacon, mayor
Rig string // Rig name (empty for global agents like mayor/deacon)
AgentState string // spawning, working, done, stuck
HookBead string // Currently pinned work bead ID
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)
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.
func FormatAgentDescription(title string, fields *AgentFields) string {
if fields == nil {
@@ -822,6 +830,12 @@ func FormatAgentDescription(title string, fields *AgentFields) string {
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")
}
@@ -861,6 +875,8 @@ func ParseAgentFields(description string) *AgentFields {
fields.CleanupStatus = value
case "active_mr":
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})
}
// 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.
// Uses --hard --force for immediate permanent deletion (no tombstone).
func (b *Beads) DeleteAgentBead(id string) error {