Skip to content

Commit 950e353

Browse files
steveyeggeclaude
andcommitted
feat(status): add compact one-line-per-worker output as default
- Add --verbose/-v flag to show detailed multi-line output (old behavior) - Compact mode shows: name + status indicator (●/○) + hook + mail count - MQ info displayed inline with refinery - Fix Makefile install target to use ~/.local/bin 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6dbb841 commit 950e353

9 files changed

Lines changed: 264 additions & 244 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ ifeq ($(shell uname),Darwin)
2323
endif
2424

2525
install: build
26-
cp $(BUILD_DIR)/$(BINARY) ~/bin/$(BINARY)
26+
cp $(BUILD_DIR)/$(BINARY) ~/.local/bin/$(BINARY)
2727

2828
clean:
2929
rm -f $(BUILD_DIR)/$(BINARY)

internal/cmd/status.go

Lines changed: 226 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var statusJSON bool
2929
var statusFast bool
3030
var statusWatch bool
3131
var statusInterval int
32+
var statusVerbose bool
3233

3334
var statusCmd = &cobra.Command{
3435
Use: "status",
@@ -49,6 +50,7 @@ func init() {
4950
statusCmd.Flags().BoolVar(&statusFast, "fast", false, "Skip mail lookups for faster execution")
5051
statusCmd.Flags().BoolVarP(&statusWatch, "watch", "w", false, "Watch mode: refresh status continuously")
5152
statusCmd.Flags().IntVarP(&statusInterval, "interval", "n", 2, "Refresh interval in seconds")
53+
statusCmd.Flags().BoolVarP(&statusVerbose, "verbose", "v", false, "Show detailed multi-line output per agent")
5254
rootCmd.AddCommand(statusCmd)
5355
}
5456

@@ -456,8 +458,16 @@ func outputStatusText(status TownStatus) error {
456458
if icon == "" {
457459
icon = roleIcons[agent.Name]
458460
}
459-
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
460-
renderAgentDetails(agent, " ", nil, status.Location)
461+
if statusVerbose {
462+
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
463+
renderAgentDetails(agent, " ", nil, status.Location)
464+
fmt.Println()
465+
} else {
466+
// Compact: icon + name on one line
467+
renderAgentCompact(agent, icon+" ", nil, status.Location)
468+
}
469+
}
470+
if !statusVerbose && len(status.Agents) > 0 {
461471
fmt.Println()
462472
}
463473

@@ -488,73 +498,86 @@ func outputStatusText(status TownStatus) error {
488498

489499
// Witness
490500
if len(witnesses) > 0 {
491-
fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
492-
for _, agent := range witnesses {
493-
renderAgentDetails(agent, " ", r.Hooks, status.Location)
501+
if statusVerbose {
502+
fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
503+
for _, agent := range witnesses {
504+
renderAgentDetails(agent, " ", r.Hooks, status.Location)
505+
}
506+
fmt.Println()
507+
} else {
508+
for _, agent := range witnesses {
509+
renderAgentCompact(agent, roleIcons["witness"]+" ", r.Hooks, status.Location)
510+
}
494511
}
495-
fmt.Println()
496512
}
497513

498514
// Refinery
499515
if len(refineries) > 0 {
500-
fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
501-
for _, agent := range refineries {
502-
renderAgentDetails(agent, " ", r.Hooks, status.Location)
503-
}
504-
// MQ summary (shown under refinery)
505-
if r.MQ != nil {
506-
mqParts := []string{}
507-
if r.MQ.Pending > 0 {
508-
mqParts = append(mqParts, fmt.Sprintf("%d pending", r.MQ.Pending))
509-
}
510-
if r.MQ.InFlight > 0 {
511-
mqParts = append(mqParts, style.Warning.Render(fmt.Sprintf("%d in-flight", r.MQ.InFlight)))
516+
if statusVerbose {
517+
fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
518+
for _, agent := range refineries {
519+
renderAgentDetails(agent, " ", r.Hooks, status.Location)
512520
}
513-
if r.MQ.Blocked > 0 {
514-
mqParts = append(mqParts, style.Dim.Render(fmt.Sprintf("%d blocked", r.MQ.Blocked)))
515-
}
516-
if len(mqParts) > 0 {
517-
// Add state indicator
518-
stateIcon := "○" // idle
519-
switch r.MQ.State {
520-
case "processing":
521-
stateIcon = style.Success.Render("●")
522-
case "blocked":
523-
stateIcon = style.Error.Render("○")
521+
// MQ summary (shown under refinery)
522+
if r.MQ != nil {
523+
mqStr := formatMQSummary(r.MQ)
524+
if mqStr != "" {
525+
fmt.Printf(" MQ: %s\n", mqStr)
524526
}
525-
// Add health warning if stale
526-
healthSuffix := ""
527-
if r.MQ.Health == "stale" {
528-
healthSuffix = style.Error.Render(" [stale]")
527+
}
528+
fmt.Println()
529+
} else {
530+
for _, agent := range refineries {
531+
// Compact: include MQ on same line if present
532+
mqSuffix := ""
533+
if r.MQ != nil {
534+
mqStr := formatMQSummaryCompact(r.MQ)
535+
if mqStr != "" {
536+
mqSuffix = " " + mqStr
537+
}
529538
}
530-
fmt.Printf(" MQ: %s %s%s\n", stateIcon, strings.Join(mqParts, ", "), healthSuffix)
539+
renderAgentCompactWithSuffix(agent, roleIcons["refinery"]+" ", r.Hooks, status.Location, mqSuffix)
531540
}
532541
}
533-
fmt.Println()
534542
}
535543

536544
// Crew
537545
if len(crews) > 0 {
538-
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
539-
for _, agent := range crews {
540-
renderAgentDetails(agent, " ", r.Hooks, status.Location)
546+
if statusVerbose {
547+
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
548+
for _, agent := range crews {
549+
renderAgentDetails(agent, " ", r.Hooks, status.Location)
550+
}
551+
fmt.Println()
552+
} else {
553+
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
554+
for _, agent := range crews {
555+
renderAgentCompact(agent, " ", r.Hooks, status.Location)
556+
}
541557
}
542-
fmt.Println()
543558
}
544559

545560
// Polecats
546561
if len(polecats) > 0 {
547-
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
548-
for _, agent := range polecats {
549-
renderAgentDetails(agent, " ", r.Hooks, status.Location)
562+
if statusVerbose {
563+
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
564+
for _, agent := range polecats {
565+
renderAgentDetails(agent, " ", r.Hooks, status.Location)
566+
}
567+
fmt.Println()
568+
} else {
569+
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
570+
for _, agent := range polecats {
571+
renderAgentCompact(agent, " ", r.Hooks, status.Location)
572+
}
550573
}
551-
fmt.Println()
552574
}
553575

554576
// No agents
555577
if len(witnesses) == 0 && len(refineries) == 0 && len(crews) == 0 && len(polecats) == 0 {
556-
fmt.Printf(" %s\n\n", style.Dim.Render("(no agents)"))
578+
fmt.Printf(" %s\n", style.Dim.Render("(no agents)"))
557579
}
580+
fmt.Println()
558581
}
559582

560583
return nil
@@ -665,6 +688,165 @@ func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo
665688
}
666689
}
667690

691+
// formatMQSummary formats the MQ status for verbose display
692+
func formatMQSummary(mq *MQSummary) string {
693+
if mq == nil {
694+
return ""
695+
}
696+
mqParts := []string{}
697+
if mq.Pending > 0 {
698+
mqParts = append(mqParts, fmt.Sprintf("%d pending", mq.Pending))
699+
}
700+
if mq.InFlight > 0 {
701+
mqParts = append(mqParts, style.Warning.Render(fmt.Sprintf("%d in-flight", mq.InFlight)))
702+
}
703+
if mq.Blocked > 0 {
704+
mqParts = append(mqParts, style.Dim.Render(fmt.Sprintf("%d blocked", mq.Blocked)))
705+
}
706+
if len(mqParts) == 0 {
707+
return ""
708+
}
709+
// Add state indicator
710+
stateIcon := "○" // idle
711+
switch mq.State {
712+
case "processing":
713+
stateIcon = style.Success.Render("●")
714+
case "blocked":
715+
stateIcon = style.Error.Render("○")
716+
}
717+
// Add health warning if stale
718+
healthSuffix := ""
719+
if mq.Health == "stale" {
720+
healthSuffix = style.Error.Render(" [stale]")
721+
}
722+
return fmt.Sprintf("%s %s%s", stateIcon, strings.Join(mqParts, ", "), healthSuffix)
723+
}
724+
725+
// formatMQSummaryCompact formats MQ status for compact single-line display
726+
func formatMQSummaryCompact(mq *MQSummary) string {
727+
if mq == nil {
728+
return ""
729+
}
730+
// Very compact: "MQ:12" or "MQ:12 [stale]"
731+
total := mq.Pending + mq.InFlight + mq.Blocked
732+
if total == 0 {
733+
return ""
734+
}
735+
healthSuffix := ""
736+
if mq.Health == "stale" {
737+
healthSuffix = style.Error.Render("[stale]")
738+
}
739+
return fmt.Sprintf("MQ:%d%s", total, healthSuffix)
740+
}
741+
742+
// renderAgentCompactWithSuffix renders a single-line agent status with an extra suffix
743+
func renderAgentCompactWithSuffix(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string, suffix string) {
744+
// Build status indicator
745+
var statusIndicator string
746+
beadState := agent.State
747+
sessionExists := agent.Running
748+
beadSaysRunning := beadState == "running" || beadState == "idle" || beadState == ""
749+
750+
switch {
751+
case beadSaysRunning && sessionExists:
752+
statusIndicator = style.Success.Render("●")
753+
case beadSaysRunning && !sessionExists:
754+
statusIndicator = style.Error.Render("●") + style.Warning.Render(" dead")
755+
case !beadSaysRunning && sessionExists:
756+
statusIndicator = style.Success.Render("●") + style.Warning.Render(" ["+beadState+"]")
757+
default:
758+
statusIndicator = style.Error.Render("○")
759+
}
760+
761+
// Get hook info
762+
hookBead := agent.HookBead
763+
hookTitle := agent.WorkTitle
764+
if hookBead == "" && hooks != nil {
765+
for _, h := range hooks {
766+
if h.Agent == agent.Address && h.HasWork {
767+
hookBead = h.Molecule
768+
hookTitle = h.Title
769+
break
770+
}
771+
}
772+
}
773+
774+
// Build hook suffix
775+
hookSuffix := ""
776+
if hookBead != "" {
777+
if hookTitle != "" {
778+
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
779+
} else {
780+
hookSuffix = style.Dim.Render(" → ") + hookBead
781+
}
782+
} else if hookTitle != "" {
783+
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
784+
}
785+
786+
// Mail indicator
787+
mailSuffix := ""
788+
if agent.UnreadMail > 0 {
789+
mailSuffix = fmt.Sprintf(" 📬%d", agent.UnreadMail)
790+
}
791+
792+
// Print single line: name + status + hook + mail + suffix
793+
fmt.Printf("%s%-12s %s%s%s%s\n", indent, agent.Name, statusIndicator, hookSuffix, mailSuffix, suffix)
794+
}
795+
796+
// renderAgentCompact renders a single-line agent status
797+
func renderAgentCompact(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string) {
798+
// Build status indicator
799+
var statusIndicator string
800+
beadState := agent.State
801+
sessionExists := agent.Running
802+
beadSaysRunning := beadState == "running" || beadState == "idle" || beadState == ""
803+
804+
switch {
805+
case beadSaysRunning && sessionExists:
806+
statusIndicator = style.Success.Render("●")
807+
case beadSaysRunning && !sessionExists:
808+
statusIndicator = style.Error.Render("●") + style.Warning.Render(" dead")
809+
case !beadSaysRunning && sessionExists:
810+
statusIndicator = style.Success.Render("●") + style.Warning.Render(" ["+beadState+"]")
811+
default:
812+
statusIndicator = style.Error.Render("○")
813+
}
814+
815+
// Get hook info
816+
hookBead := agent.HookBead
817+
hookTitle := agent.WorkTitle
818+
if hookBead == "" && hooks != nil {
819+
for _, h := range hooks {
820+
if h.Agent == agent.Address && h.HasWork {
821+
hookBead = h.Molecule
822+
hookTitle = h.Title
823+
break
824+
}
825+
}
826+
}
827+
828+
// Build hook suffix
829+
hookSuffix := ""
830+
if hookBead != "" {
831+
if hookTitle != "" {
832+
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
833+
} else {
834+
hookSuffix = style.Dim.Render(" → ") + hookBead
835+
}
836+
} else if hookTitle != "" {
837+
hookSuffix = style.Dim.Render(" → ") + truncateWithEllipsis(hookTitle, 30)
838+
}
839+
840+
// Mail indicator
841+
mailSuffix := ""
842+
if agent.UnreadMail > 0 {
843+
mailSuffix = fmt.Sprintf(" 📬%d", agent.UnreadMail)
844+
}
845+
846+
// Print single line: name + status + hook + mail
847+
fmt.Printf("%s%-12s %s%s%s\n", indent, agent.Name, statusIndicator, hookSuffix, mailSuffix)
848+
}
849+
668850
// formatHookInfo formats the hook bead and title for display
669851
func formatHookInfo(hookBead, title string, maxLen int) string {
670852
if hookBead == "" {

internal/formula/formulas/mol-deacon-patrol.formula.toml

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -340,43 +340,10 @@ gt mail send mayor/ -s "Health: <rig> <component> unresponsive" \\
340340
341341
Reset unresponsive_cycles to 0 when component responds normally."""
342342

343-
[[steps]]
344-
id = "stale-hook-check"
345-
title = "Cleanup stale hooked beads"
346-
needs = ["health-scan"]
347-
description = """
348-
Find and unhook beads stuck in 'hooked' status.
349-
350-
Beads can get stuck in 'hooked' status when agents die or abandon work without
351-
properly unhooking. This step cleans them up so the work can be reassigned.
352-
353-
**Step 1: Preview stale hooks**
354-
```bash
355-
gt deacon stale-hooks --dry-run
356-
```
357-
358-
Review the output - it shows:
359-
- Hooked beads older than 1 hour
360-
- Whether the assignee agent is still alive
361-
- What action would be taken
362-
363-
**Step 2: If stale hooks found with dead agents, unhook them**
364-
```bash
365-
gt deacon stale-hooks
366-
```
367-
368-
This sets status back to 'open' for beads whose assignee agent is no longer running.
369-
370-
**Step 3: If no stale hooks**
371-
No action needed - hooks are healthy.
372-
373-
**Note**: This is a backstop. Primary fix is ensuring agents properly unhook
374-
beads when they exit or hand off work."""
375-
376343
[[steps]]
377344
id = "zombie-scan"
378345
title = "Backup check for zombie polecats"
379-
needs = ["stale-hook-check"]
346+
needs = ["health-scan"]
380347
description = """
381348
Defense-in-depth check for zombie polecats that Witness should have cleaned.
382349

internal/formula/formulas/mol-digest-generate.formula.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Gather activity data from each rig in the town.
7878
7979
**1. List accessible rigs:**
8080
```bash
81-
gt rig list
81+
gt rigs
8282
# Returns list of rigs: gastown, beads, etc.
8383
```
8484

0 commit comments

Comments
 (0)