feat(tui): add self-documenting help with ASCII diagrams and table helpers
TUI improvements for Christmas launch: - Add phase transition table and lifecycle diagram to `gt molecule --help` - Add swarm lifecycle diagram to `gt swarm --help` - Add mail routing diagram to `gt mail --help` - Add sling mechanics diagram to `gt sling --help` - Create Lipgloss table helper (internal/style/table.go) - Migrate mq_list to use styled tables with color-coded priorities - Migrate molecule list to use styled tables - Add fuzzy matching "did you mean" suggestions for polecat not found errors - Add suggest package with Levenshtein distance implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,38 @@ var mailCmd = &cobra.Command{
|
||||
Long: `Send and receive messages between agents.
|
||||
|
||||
The mail system allows Mayor, polecats, and the Refinery to communicate.
|
||||
Messages are stored in beads as issues with type=message.`,
|
||||
Messages are stored in beads as issues with type=message.
|
||||
|
||||
MAIL ROUTING:
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Town (.beads/) │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ Mayor Inbox │ │
|
||||
│ │ └── mayor/ │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ gastown/ (rig mailboxes) │ │
|
||||
│ │ ├── witness ← gastown/witness │ │
|
||||
│ │ ├── refinery ← gastown/refinery │ │
|
||||
│ │ ├── Toast ← gastown/Toast │ │
|
||||
│ │ └── crew/max ← gastown/crew/max │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
ADDRESS FORMATS:
|
||||
mayor/ → Mayor inbox
|
||||
<rig>/witness → Rig's Witness
|
||||
<rig>/refinery → Rig's Refinery
|
||||
<rig>/<polecat> → Polecat (e.g., gastown/Toast)
|
||||
<rig>/crew/<name> → Crew worker (e.g., gastown/crew/max)
|
||||
--human → Special: human overseer
|
||||
|
||||
COMMANDS:
|
||||
inbox View your inbox
|
||||
send Send a message
|
||||
read Read a specific message
|
||||
mark Mark messages read/unread`,
|
||||
}
|
||||
|
||||
var mailSendCmd = &cobra.Command{
|
||||
|
||||
@@ -30,7 +30,39 @@ var moleculeCmd = &cobra.Command{
|
||||
Long: `Manage molecule workflow templates.
|
||||
|
||||
Molecules are composable workflow patterns stored as beads issues.
|
||||
When instantiated on a parent issue, they create child beads forming a DAG.`,
|
||||
When instantiated on a parent issue, they create child beads forming a DAG.
|
||||
|
||||
LIFECYCLE:
|
||||
Proto (template)
|
||||
│
|
||||
▼ instantiate/bond
|
||||
┌─────────────────┐
|
||||
│ Mol (durable) │ ← tracked in .beads/
|
||||
│ Wisp (ephemeral)│ ← tracked in .beads-wisp/
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
▼ ▼
|
||||
burn squash
|
||||
(no record) (→ digest)
|
||||
|
||||
PHASE TRANSITIONS (for pluggable molecules):
|
||||
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
|
||||
│ Phase │ Parallelism │ Blocks │ Purpose │
|
||||
├─────────────┼─────────────┼─────────────┼─────────────────────┤
|
||||
│ discovery │ full │ (nothing) │ Inventory, gather │
|
||||
│ structural │ sequential │ discovery │ Big-picture review │
|
||||
│ tactical │ parallel │ structural │ Detailed work │
|
||||
│ synthesis │ single │ tactical │ Aggregate results │
|
||||
└─────────────┴─────────────┴─────────────┴─────────────────────┘
|
||||
|
||||
COMMANDS:
|
||||
catalog List available molecule protos
|
||||
instantiate Create steps from a molecule template
|
||||
progress Show execution progress of an instantiated molecule
|
||||
status Show what's on an agent's hook
|
||||
burn Discard molecule without creating a digest
|
||||
squash Complete molecule and create a digest`,
|
||||
}
|
||||
|
||||
var moleculeListCmd = &cobra.Command{
|
||||
@@ -386,23 +418,35 @@ func runMoleculeList(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create styled table
|
||||
table := style.NewTable(
|
||||
style.Column{Name: "ID", Width: 20},
|
||||
style.Column{Name: "TITLE", Width: 35},
|
||||
style.Column{Name: "STEPS", Width: 5, Align: style.AlignRight},
|
||||
style.Column{Name: "SOURCE", Width: 10},
|
||||
)
|
||||
|
||||
for _, mol := range entries {
|
||||
sourceMarker := style.Dim.Render(fmt.Sprintf("[%s]", mol.Source))
|
||||
|
||||
stepCount := ""
|
||||
// Format steps count
|
||||
stepStr := ""
|
||||
if mol.StepCount > 0 {
|
||||
stepCount = fmt.Sprintf(" (%d steps)", mol.StepCount)
|
||||
stepStr = fmt.Sprintf("%d", mol.StepCount)
|
||||
}
|
||||
|
||||
statusMarker := ""
|
||||
// Format title with status
|
||||
title := mol.Title
|
||||
if mol.Status == "closed" {
|
||||
statusMarker = " " + style.Dim.Render("[closed]")
|
||||
title = style.Dim.Render(mol.Title + " [closed]")
|
||||
}
|
||||
|
||||
fmt.Printf(" %s: %s%s%s %s\n",
|
||||
style.Bold.Render(mol.ID), mol.Title, stepCount, statusMarker, sourceMarker)
|
||||
// Format source
|
||||
source := style.Dim.Render(mol.Source)
|
||||
|
||||
table.AddRow(mol.ID, title, stepStr, source)
|
||||
}
|
||||
|
||||
fmt.Print(table.Render())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -104,12 +104,17 @@ func runMQList(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print header
|
||||
fmt.Printf(" %-12s %-12s %-8s %-30s %-10s %s\n",
|
||||
"ID", "STATUS", "PRIORITY", "BRANCH", "WORKER", "AGE")
|
||||
fmt.Printf(" %s\n", strings.Repeat("-", 90))
|
||||
// Create styled table
|
||||
table := style.NewTable(
|
||||
style.Column{Name: "ID", Width: 12},
|
||||
style.Column{Name: "STATUS", Width: 12},
|
||||
style.Column{Name: "PRI", Width: 4},
|
||||
style.Column{Name: "BRANCH", Width: 28},
|
||||
style.Column{Name: "WORKER", Width: 10},
|
||||
style.Column{Name: "AGE", Width: 6, Align: style.AlignRight},
|
||||
)
|
||||
|
||||
// Print each MR
|
||||
// Add rows
|
||||
for _, issue := range filtered {
|
||||
fields := beads.ParseMRFields(issue)
|
||||
|
||||
@@ -127,9 +132,9 @@ func runMQList(cmd *cobra.Command, args []string) error {
|
||||
styledStatus := displayStatus
|
||||
switch displayStatus {
|
||||
case "ready":
|
||||
styledStatus = style.Bold.Render("ready")
|
||||
styledStatus = style.Success.Render("ready")
|
||||
case "in_progress":
|
||||
styledStatus = style.Bold.Render("in_progress")
|
||||
styledStatus = style.Warning.Render("active")
|
||||
case "blocked":
|
||||
styledStatus = style.Dim.Render("blocked")
|
||||
case "closed":
|
||||
@@ -144,13 +149,13 @@ func runMQList(cmd *cobra.Command, args []string) error {
|
||||
worker = fields.Worker
|
||||
}
|
||||
|
||||
// Truncate branch if too long
|
||||
if len(branch) > 30 {
|
||||
branch = branch[:27] + "..."
|
||||
}
|
||||
|
||||
// Format priority
|
||||
// Format priority with color
|
||||
priority := fmt.Sprintf("P%d", issue.Priority)
|
||||
if issue.Priority <= 1 {
|
||||
priority = style.Error.Render(priority)
|
||||
} else if issue.Priority == 2 {
|
||||
priority = style.Warning.Render(priority)
|
||||
}
|
||||
|
||||
// Calculate age
|
||||
age := formatMRAge(issue.CreatedAt)
|
||||
@@ -161,12 +166,24 @@ func runMQList(cmd *cobra.Command, args []string) error {
|
||||
displayID = displayID[:12]
|
||||
}
|
||||
|
||||
fmt.Printf(" %-12s %-12s %-8s %-30s %-10s %s\n",
|
||||
displayID, styledStatus, priority, branch, worker, style.Dim.Render(age))
|
||||
table.AddRow(displayID, styledStatus, priority, branch, worker, style.Dim.Render(age))
|
||||
}
|
||||
|
||||
// Show blocking info if blocked
|
||||
fmt.Print(table.Render())
|
||||
|
||||
// Show blocking details below table
|
||||
for _, issue := range filtered {
|
||||
displayStatus := issue.Status
|
||||
if issue.Status == "open" && (len(issue.BlockedBy) > 0 || issue.BlockedByCount > 0) {
|
||||
displayStatus = "blocked"
|
||||
}
|
||||
if displayStatus == "blocked" && len(issue.BlockedBy) > 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render(fmt.Sprintf(" (waiting on %s)", issue.BlockedBy[0])))
|
||||
displayID := issue.ID
|
||||
if len(displayID) > 12 {
|
||||
displayID = displayID[:12]
|
||||
}
|
||||
fmt.Printf(" %s %s\n", style.Dim.Render(displayID+":"),
|
||||
style.Dim.Render(fmt.Sprintf("waiting on %s", issue.BlockedBy[0])))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/suggest"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
@@ -212,7 +213,9 @@ func runSessionStart(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
|
||||
suggestions := suggest.FindSimilar(polecatName, r.Polecats, 3)
|
||||
hint := fmt.Sprintf("Create with: gt polecat add %s/%s", rigName, polecatName)
|
||||
return fmt.Errorf("%s", suggest.FormatSuggestion("Polecat", polecatName, suggestions, hint))
|
||||
}
|
||||
|
||||
opts := session.StartOptions{
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/suggest"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
@@ -43,21 +44,39 @@ Based on the Universal Gas Town Propulsion Principle:
|
||||
|
||||
"If you find something on your hook, YOU RUN IT."
|
||||
|
||||
Arguments:
|
||||
thing What to sling: proto name, issue ID, or epic ID
|
||||
target Who to sling at: agent address (polecat/name, deacon/, etc.)
|
||||
SLING MECHANICS:
|
||||
┌─────────┐ ┌───────────────────────────────────────────┐
|
||||
│ THING │─────▶│ SLING PIPELINE │
|
||||
└─────────┘ │ │
|
||||
proto │ 1. SPAWN Proto → Molecule instance │
|
||||
issue │ 2. ASSIGN Molecule → Target agent │
|
||||
epic │ 3. PIN Work → Agent's hook │
|
||||
│ 4. IGNITE Session starts automatically │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ 🪝 TARGET's HOOK │ │
|
||||
│ │ └── [work lands here] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
└───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Agent runs the work!
|
||||
|
||||
THING TYPES:
|
||||
proto Molecule template name (e.g., "feature", "bugfix")
|
||||
issue Beads issue ID (e.g., "gt-abc123")
|
||||
epic Epic ID for batch dispatch
|
||||
|
||||
TARGET FORMATS:
|
||||
gastown/Toast → Polecat in rig
|
||||
gastown/witness → Rig's Witness
|
||||
gastown/refinery → Rig's Refinery
|
||||
deacon/ → Global Deacon
|
||||
|
||||
Examples:
|
||||
gt sling feature polecat/alpha # Spawn feature mol, sling to alpha
|
||||
gt sling gt-xyz polecat/beta -m bugfix # Sling issue with bugfix workflow
|
||||
gt sling patrol deacon/ --wisp # Ephemeral patrol wisp
|
||||
gt sling gt-epic-batch refinery/ # Batch work to refinery
|
||||
|
||||
What Happens When You Sling:
|
||||
1. SPAWN (if proto) - Create molecule from template
|
||||
2. ASSIGN - Assign molecule/issue to target agent
|
||||
3. PIN - Put work on agent's hook (pinned bead)
|
||||
4. IGNITION - Agent wakes and runs the work`,
|
||||
gt sling feature gastown/Toast # Spawn feature, sling to Toast
|
||||
gt sling gt-abc gastown/Nux -m bugfix # Issue with workflow
|
||||
gt sling patrol deacon/ --wisp # Ephemeral patrol wisp`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runSling,
|
||||
}
|
||||
@@ -361,7 +380,10 @@ func slingToPolecat(townRoot string, target *SlingTarget, thing *SlingThing) err
|
||||
fmt.Printf("%s Fresh worktree created\n", style.Bold.Render("✓"))
|
||||
} else if err == polecat.ErrPolecatNotFound {
|
||||
if !slingCreate {
|
||||
return fmt.Errorf("polecat '%s' not found (use --create to create)", polecatName)
|
||||
suggestions := suggest.FindSimilar(polecatName, r.Polecats, 3)
|
||||
hint := fmt.Sprintf("Or use --create to create: gt sling %s %s/%s --create",
|
||||
thing.ID, target.Rig, polecatName)
|
||||
return fmt.Errorf("%s", suggest.FormatSuggestion("Polecat", polecatName, suggestions, hint))
|
||||
}
|
||||
fmt.Printf("Creating polecat %s...\n", polecatName)
|
||||
if _, err = polecatMgr.Add(polecatName); err != nil {
|
||||
|
||||
@@ -39,7 +39,42 @@ var swarmCmd = &cobra.Command{
|
||||
Long: `Manage coordinated multi-agent work units (swarms).
|
||||
|
||||
A swarm coordinates multiple polecats working on related tasks from a shared
|
||||
base commit. Work is merged to an integration branch, then landed to main.`,
|
||||
base commit. Work is merged to an integration branch, then landed to main.
|
||||
|
||||
SWARM LIFECYCLE:
|
||||
epic (tasks)
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ SWARM │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Polecat │ │ Polecat │ │ Polecat │ │
|
||||
│ │ Toast │ │ Nux │ │ Capable │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ integration/<epic> │ │
|
||||
│ └───────────────────┬──────────────────┘ │
|
||||
└──────────────────────┼────────────────────┘
|
||||
│
|
||||
▼ land
|
||||
main
|
||||
|
||||
STATES:
|
||||
creating → Swarm being set up
|
||||
active → Workers executing tasks
|
||||
merging → Work being integrated
|
||||
landed → Successfully merged to main
|
||||
cancelled → Swarm aborted
|
||||
|
||||
COMMANDS:
|
||||
create Create a new swarm from an epic
|
||||
status Show swarm progress
|
||||
list List swarms in a rig
|
||||
land Manually land completed swarm
|
||||
cancel Cancel an active swarm
|
||||
dispatch Assign next ready task to a worker`,
|
||||
}
|
||||
|
||||
var swarmCreateCmd = &cobra.Command{
|
||||
|
||||
Reference in New Issue
Block a user