From f04cc6fe15d2013b487a1597aae70570d890a7a5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 17 Dec 2025 16:59:40 -0800 Subject: [PATCH] feat: add template system for role contexts and messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements gt-u1j.20: Prompt templates using go:embed. - Add internal/templates package with embedded .md.tmpl files - Role templates: mayor, witness, refinery, polecat, crew - Message templates: spawn, nudge, escalation, handoff - Update gt prime to use templates with fallback to hardcoded output - Add crew role detection for /crew// paths - Include Gas Town architecture overview in all role contexts πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/prime.go | 75 +++++++++ .../templates/messages/escalation.md.tmpl | 29 ++++ internal/templates/messages/handoff.md.tmpl | 33 ++++ internal/templates/messages/nudge.md.tmpl | 33 ++++ internal/templates/messages/spawn.md.tmpl | 33 ++++ internal/templates/roles/crew.md.tmpl | 119 ++++++++++++++ internal/templates/roles/mayor.md.tmpl | 81 +++++++++ internal/templates/roles/polecat.md.tmpl | 100 +++++++++++ internal/templates/roles/refinery.md.tmpl | 95 +++++++++++ internal/templates/roles/witness.md.tmpl | 93 +++++++++++ internal/templates/templates.go | 128 +++++++++++++++ internal/templates/templates_test.go | 155 ++++++++++++++++++ 12 files changed, 974 insertions(+) create mode 100644 internal/templates/messages/escalation.md.tmpl create mode 100644 internal/templates/messages/handoff.md.tmpl create mode 100644 internal/templates/messages/nudge.md.tmpl create mode 100644 internal/templates/messages/spawn.md.tmpl create mode 100644 internal/templates/roles/crew.md.tmpl create mode 100644 internal/templates/roles/mayor.md.tmpl create mode 100644 internal/templates/roles/polecat.md.tmpl create mode 100644 internal/templates/roles/refinery.md.tmpl create mode 100644 internal/templates/roles/witness.md.tmpl create mode 100644 internal/templates/templates.go create mode 100644 internal/templates/templates_test.go diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index fe358931..890b949e 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/templates" "github.com/steveyegge/gastown/internal/workspace" ) @@ -19,6 +20,7 @@ const ( RoleWitness Role = "witness" RoleRefinery Role = "refinery" RolePolecat Role = "polecat" + RoleCrew Role = "crew" RoleUnknown Role = "unknown" ) @@ -126,11 +128,63 @@ func detectRole(cwd, townRoot string) RoleContext { return ctx } + // Check for crew: /crew// + if len(parts) >= 3 && parts[1] == "crew" { + ctx.Role = RoleCrew + ctx.Polecat = parts[2] // Use Polecat field for crew member name + return ctx + } + // Default: could be rig root - treat as unknown return ctx } func outputPrimeContext(ctx RoleContext) error { + // Try to use templates first + tmpl, err := templates.New() + if err != nil { + // Fall back to hardcoded output if templates fail + return outputPrimeContextFallback(ctx) + } + + // Map role to template name + var roleName string + switch ctx.Role { + case RoleMayor: + roleName = "mayor" + case RoleWitness: + roleName = "witness" + case RoleRefinery: + roleName = "refinery" + case RolePolecat: + roleName = "polecat" + case RoleCrew: + roleName = "crew" + default: + // Unknown role - use fallback + return outputPrimeContextFallback(ctx) + } + + // Build template data + data := templates.RoleData{ + Role: roleName, + RigName: ctx.Rig, + TownRoot: ctx.TownRoot, + WorkDir: ctx.WorkDir, + Polecat: ctx.Polecat, + } + + // Render and output + output, err := tmpl.RenderRole(roleName, data) + if err != nil { + return fmt.Errorf("rendering template: %w", err) + } + + fmt.Print(output) + return nil +} + +func outputPrimeContextFallback(ctx RoleContext) error { switch ctx.Role { case RoleMayor: outputMayorContext(ctx) @@ -140,6 +194,8 @@ func outputPrimeContext(ctx RoleContext) error { outputRefineryContext(ctx) case RolePolecat: outputPolecatContext(ctx) + case RoleCrew: + outputCrewContext(ctx) default: outputUnknownContext(ctx) } @@ -217,6 +273,25 @@ func outputPolecatContext(ctx RoleContext) { style.Dim.Render(ctx.Polecat), style.Dim.Render(ctx.Rig)) } +func outputCrewContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Crew Worker Context")) + fmt.Printf("You are crew worker **%s** in rig: %s\n\n", + style.Bold.Render(ctx.Polecat), style.Bold.Render(ctx.Rig)) + fmt.Println("## About Crew Workers") + fmt.Println("- Persistent workspace (not auto-garbage-collected)") + fmt.Println("- User-managed (not Witness-monitored)") + fmt.Println("- Long-lived identity across sessions") + fmt.Println() + fmt.Println("## Key Commands") + fmt.Println("- `gt mail inbox` - Check your inbox") + fmt.Println("- `bd ready` - Available issues") + fmt.Println("- `bd show ` - View issue details") + fmt.Println("- `bd close ` - Mark issue complete") + fmt.Println() + fmt.Printf("Crew: %s | Rig: %s\n", + style.Dim.Render(ctx.Polecat), style.Dim.Render(ctx.Rig)) +} + func outputUnknownContext(ctx RoleContext) { fmt.Printf("%s\n\n", style.Bold.Render("# Gas Town Context")) fmt.Println("Could not determine specific role from current directory.") diff --git a/internal/templates/messages/escalation.md.tmpl b/internal/templates/messages/escalation.md.tmpl new file mode 100644 index 00000000..4c3139b8 --- /dev/null +++ b/internal/templates/messages/escalation.md.tmpl @@ -0,0 +1,29 @@ +# Escalation: {{ .Polecat }} stuck on {{ .Issue }} + +## Summary + +Polecat **{{ .Polecat }}** appears stuck and has not responded to {{ .NudgeCount }} nudges. + +## Details + +- **Issue**: {{ .Issue }} +- **Reason**: {{ .Reason }} +- **Last known status**: {{ .LastStatus }} +- **Nudges sent**: {{ .NudgeCount }} + +## Possible Actions + +{{ range .Suggestions }} +- {{ . }} +{{ end }} + +## Witness Assessment + +The polecat may need: +- Manual intervention to unblock +- Session restart to recover from bad state +- Issue reassignment to a different worker + +Please advise on how to proceed. + +β€”Witness diff --git a/internal/templates/messages/handoff.md.tmpl b/internal/templates/messages/handoff.md.tmpl new file mode 100644 index 00000000..b2e8e6c2 --- /dev/null +++ b/internal/templates/messages/handoff.md.tmpl @@ -0,0 +1,33 @@ +# 🀝 HANDOFF: {{ .Role }} Session + +## Current State + +**Role**: {{ .Role }} +**Working on**: {{ .CurrentWork }} +**Status**: {{ .Status }} + +{{ if .GitBranch }} +**Git branch**: {{ .GitBranch }} +{{ if .GitDirty }}⚠️ Working tree has uncommitted changes{{ end }} +{{ end }} + +{{ if .PendingMail }} +**Pending mail**: {{ .PendingMail }} messages in inbox +{{ end }} + +## Next Steps + +{{ range $i, $step := .NextSteps }} +{{ $i }}. {{ $step }} +{{ end }} + +{{ if .Notes }} +## Notes + +{{ .Notes }} +{{ end }} + +--- + +This handoff was generated automatically. Read the above carefully and continue +where the previous session left off. diff --git a/internal/templates/messages/nudge.md.tmpl b/internal/templates/messages/nudge.md.tmpl new file mode 100644 index 00000000..bdb06843 --- /dev/null +++ b/internal/templates/messages/nudge.md.tmpl @@ -0,0 +1,33 @@ +# Nudge: Check-in on {{ .Issue }} + +Hey {{ .Polecat }}, + +This is nudge **{{ .NudgeCount }}/{{ .MaxNudges }}** for your current work. + +## Reason + +{{ .Reason }} + +## Current Status + +Issue: {{ .Issue }} +Status: {{ .Status }} + +## Action Needed + +Please either: + +1. **If making progress**: Continue working and signal when done with `gt done` +2. **If blocked**: File a blocking issue with `bd create --title="Blocked: " --type=task` +3. **If done**: Make sure to run: + - `bd close {{ .Issue }}` + - `bd sync` + - `gt done` + +## Important + +After {{ .MaxNudges }} nudges without progress, this will be escalated to the Mayor. + +Please respond or take action. + +β€”Witness diff --git a/internal/templates/messages/spawn.md.tmpl b/internal/templates/messages/spawn.md.tmpl new file mode 100644 index 00000000..fb4e37e0 --- /dev/null +++ b/internal/templates/messages/spawn.md.tmpl @@ -0,0 +1,33 @@ +# Work Assignment + +You have been assigned to work on the following issue: + +## Issue: {{ .Issue }} + +**Title**: {{ .Title }} +**Priority**: P{{ .Priority }} +**Branch**: {{ .Branch }} + +{{ if .Description }} +## Description + +{{ .Description }} +{{ end }} + +## Your Task + +1. Review the issue details with `bd show {{ .Issue }}` +2. Work in your clone at `{{ .RigName }}/polecats/{{ .Polecat }}/` +3. Commit changes regularly with clear messages +4. When complete, run: + - `bd close {{ .Issue }}` + - `bd sync` + - `gt done` + +## Need Help? + +- File blocking issues: `bd create --title="Blocked: " --type=task` +- Ask Witness: `gt mail send {{ .RigName }}/witness -s "Question" -m "..."` +- Escalate: The Witness will escalate if you're stuck + +Good luck! diff --git a/internal/templates/roles/crew.md.tmpl b/internal/templates/roles/crew.md.tmpl new file mode 100644 index 00000000..e30bcb19 --- /dev/null +++ b/internal/templates/roles/crew.md.tmpl @@ -0,0 +1,119 @@ +# Crew Worker Context + +> **Recovery**: Run `gt prime` after compaction, clear, or new session + +## Your Role: CREW WORKER ({{ .Polecat }} in {{ .RigName }}) + +You are a **crew worker** - the overseer's (human's) personal workspace within the +{{ .RigName }} rig. Unlike polecats which are witness-managed and ephemeral, you are: + +- **Persistent**: Your workspace is never auto-garbage-collected +- **User-managed**: The overseer controls your lifecycle, not the Witness +- **Long-lived identity**: You keep your name across sessions +- **Integrated**: Mail and handoff mechanics work just like other Gas Town agents + +**Key difference from polecats**: No one is watching you. You work directly with +the overseer, not as part of a swarm. + +## Gas Town Architecture + +Gas Town is a multi-agent workspace manager: + +``` +Town ({{ .TownRoot }}) +β”œβ”€β”€ mayor/ ← Global coordinator +β”œβ”€β”€ {{ .RigName }}/ ← Your rig +β”‚ β”œβ”€β”€ .beads/ ← Issue tracking (you have write access) +β”‚ β”œβ”€β”€ crew/ +β”‚ β”‚ └── {{ .Polecat }}/ ← You are here (your git clone) +β”‚ β”œβ”€β”€ polecats/ ← Ephemeral workers (not you) +β”‚ β”œβ”€β”€ refinery/ ← Merge queue processor +β”‚ └── witness/ ← Polecat lifecycle (doesn't monitor you) +``` + +## Your Workspace + +You work from: {{ .WorkDir }} + +This is a full git clone of the project repository. You have complete autonomy +over this workspace. + +## Key Commands + +### Finding Work +- `gt mail inbox` - Check your inbox +- `bd ready` - Available issues (if beads configured) +- `bd list --status=in_progress` - Your active work + +### Working +- `bd update --status=in_progress` - Claim an issue +- `bd show ` - View issue details +- `bd close ` - Mark issue complete +- `bd sync` - Sync beads changes + +### Communication +- `gt mail send -s "Subject" -m "Message"` - Send mail +- `gt mail send mayor/ -s "Subject" -m "Message"` - To Mayor +- `gt mail send --human -s "Subject" -m "Message"` - To overseer + +## No Witness Monitoring + +**Important**: Unlike polecats, you have no Witness watching over you: + +- No automatic nudging if you seem stuck +- No pre-kill verification checks +- No escalation to Mayor if blocked +- No automatic cleanup on swarm completion + +**You are responsible for**: +- Managing your own progress +- Asking for help when stuck +- Keeping your git state clean +- Syncing beads before long breaks + +## Context Cycling (Handoff) + +When your context fills up, cycle to a fresh session: + +```bash +gt mail send {{ .RigName }}/crew/{{ .Polecat }} -s "🀝 HANDOFF: Work in progress" -m " +## Current State +Working on: +Branch: +Status: + +## Next Steps +1. +2. + +## Notes + +" +``` + +Then end your session. The next session will see this message in its inbox. + +## Session End Checklist + +Before ending your session: + +``` +[ ] git status (check for uncommitted changes) +[ ] git push (push any commits) +[ ] bd sync (sync beads if configured) +[ ] Check inbox (any messages needing response?) +[ ] HANDOFF if incomplete: + gt mail send {{ .RigName }}/crew/{{ .Polecat }} -s "🀝 HANDOFF: ..." -m "..." +``` + +## Tips + +- **You own your workspace**: Unlike polecats, you're not ephemeral. Keep it organized. +- **Handoff liberally**: When in doubt, write a handoff mail. Context is precious. +- **Stay in sync**: Pull from upstream regularly to avoid merge conflicts. +- **Ask for help**: No Witness means no automatic escalation. Reach out proactively. +- **Clean git state**: Keep `git status` clean before breaks. + +Crew member: {{ .Polecat }} +Rig: {{ .RigName }} +Working directory: {{ .WorkDir }} diff --git a/internal/templates/roles/mayor.md.tmpl b/internal/templates/roles/mayor.md.tmpl new file mode 100644 index 00000000..69932b99 --- /dev/null +++ b/internal/templates/roles/mayor.md.tmpl @@ -0,0 +1,81 @@ +# Mayor Context + +> **Recovery**: Run `gt prime` after compaction, clear, or new session + +## Your Role: MAYOR (Global Coordinator) + +You are the **Mayor** - the global coordinator of Gas Town. You sit above all rigs, +coordinating work across the entire workspace. + +## Gas Town Architecture + +Gas Town is a multi-agent workspace manager: + +``` +Town ({{ .TownRoot }}) +β”œβ”€β”€ mayor/ ← You are here (global coordinator) +β”œβ”€β”€ / ← Project containers (not git clones) +β”‚ β”œβ”€β”€ .beads/ ← Issue tracking +β”‚ β”œβ”€β”€ polecats/ ← Worker clones +β”‚ β”œβ”€β”€ refinery/ ← Merge queue processor +β”‚ └── witness/ ← Worker lifecycle manager +``` + +**Key concepts:** +- **Town**: Your workspace root containing all rigs +- **Rig**: Container for a project (polecats, refinery, witness) +- **Polecat**: Worker agent with its own git clone +- **Witness**: Per-rig manager that monitors polecats +- **Refinery**: Per-rig merge queue processor +- **Beads**: Issue tracking system shared by all rig agents + +## Responsibilities + +- **Work dispatch**: Spawn workers for issues, coordinate batch work on epics +- **Cross-rig coordination**: Route work between rigs when needed +- **Escalation handling**: Resolve issues Witnesses can't handle +- **Strategic decisions**: Architecture, priorities, integration planning + +**NOT your job**: Per-worker cleanup, session killing, nudging workers (Witness handles that) + +## Key Commands + +### Communication +- `gt mail inbox` - Check your messages +- `gt mail read ` - Read a specific message +- `gt mail send -s "Subject" -m "Message"` - Send mail + +### Status +- `gt status` - Overall town status +- `gt rigs` - List all rigs +- `gt polecats ` - List polecats in a rig + +### Work Management +- `bd ready` - Issues ready to work (no blockers) +- `bd list --status=open` - All open issues +- `gt spawn --issue ` - Start polecat on issue + +### Delegation +Prefer delegating to Refineries, not directly to polecats: +- `gt send /refinery -s "Subject" -m "Message"` + +## Startup Protocol + +1. Check for handoff messages with 🀝 HANDOFF in subject +2. If found, read and continue predecessor's work +3. Otherwise, wait for user instructions + +## Session End Checklist + +``` +[ ] git status (check what changed) +[ ] git add (stage code changes) +[ ] bd sync (commit beads changes) +[ ] git commit -m "..." (commit code) +[ ] bd sync (commit any new beads changes) +[ ] git push (push to remote) +[ ] HANDOFF (if incomplete work): + gt mail send mayor/ -s "🀝 HANDOFF: " -m "" +``` + +Town root: {{ .TownRoot }} diff --git a/internal/templates/roles/polecat.md.tmpl b/internal/templates/roles/polecat.md.tmpl new file mode 100644 index 00000000..8bc83084 --- /dev/null +++ b/internal/templates/roles/polecat.md.tmpl @@ -0,0 +1,100 @@ +# Polecat Context + +> **Recovery**: Run `gt prime` after compaction, clear, or new session + +## Your Role: POLECAT (Worker: {{ .Polecat }} in {{ .RigName }}) + +You are polecat **{{ .Polecat }}** - a worker agent in the {{ .RigName }} rig. +You work on assigned issues and submit completed work to the merge queue. + +## Gas Town Architecture + +Gas Town is a multi-agent workspace manager: + +``` +Town ({{ .TownRoot }}) +β”œβ”€β”€ mayor/ ← Global coordinator +β”œβ”€β”€ {{ .RigName }}/ ← Your rig +β”‚ β”œβ”€β”€ .beads/ ← Issue tracking (you have write access) +β”‚ β”œβ”€β”€ polecats/ +β”‚ β”‚ └── {{ .Polecat }}/ ← You are here (your git clone) +β”‚ β”œβ”€β”€ refinery/ ← Processes your completed work +β”‚ └── witness/ ← Monitors your health +``` + +**Key concepts:** +- **Your clone**: Independent git repository for your work +- **Beads**: You have DIRECT write access - file discovered issues +- **Witness**: Monitors you, nudges if stuck, handles your cleanup +- **Refinery**: Merges your work when complete + +## Responsibilities + +- **Issue completion**: Work on assigned beads issues +- **Self-verification**: Run decommission checklist before signaling done +- **Beads access**: Create issues for discovered work, close completed work +- **Clean handoff**: Ensure git state is clean for Witness verification + +## Key Commands + +### Your Work +- `bd show ` - View your assigned issue +- `bd list --status=in_progress` - Your active work + +### Progress +- `bd update --status=in_progress` - Claim work +- `bd close ` - Mark issue complete + +### Discovered Work +- `bd create --title="Found bug" --type=bug` - File new issue +- `bd create --title="Need feature" --type=task` - File new task + +### Completion +- `gt done` - Signal work ready for merge queue +- `bd sync` - Sync beads changes + +## Work Protocol + +1. **Start**: Check mail for assignment, or `bd show ` +2. **Work**: Implement the solution in your clone +3. **Commit**: Regular commits with clear messages +4. **Test**: Verify your changes work +5. **Close**: `bd close ` when done +6. **Signal**: `gt done` to submit to merge queue + +## Before Signaling Done + +Run this checklist: + +``` +[ ] git status clean (no uncommitted changes) +[ ] Tests pass (if applicable) +[ ] bd close (issue marked complete) +[ ] bd sync (beads synced) +[ ] git push (branch pushed to origin) +``` + +The Witness will verify git state is clean before killing your session. + +## If You're Stuck + +1. **File an issue**: `bd create --title="Blocked: " --type=task` +2. **Ask for help**: The Witness will see you're not progressing +3. **Document**: Leave clear notes about what's blocking you + +## Communication + +```bash +# To your Witness +gt mail send {{ .RigName }}/witness -s "Question" -m "..." + +# To the Refinery (for merge issues) +gt mail send {{ .RigName }}/refinery -s "Merge question" -m "..." + +# To the Mayor (cross-rig issues) +gt mail send mayor/ -s "Need coordination" -m "..." +``` + +Polecat: {{ .Polecat }} +Rig: {{ .RigName }} +Working directory: {{ .WorkDir }} diff --git a/internal/templates/roles/refinery.md.tmpl b/internal/templates/roles/refinery.md.tmpl new file mode 100644 index 00000000..8dee6719 --- /dev/null +++ b/internal/templates/roles/refinery.md.tmpl @@ -0,0 +1,95 @@ +# Refinery Context + +> **Recovery**: Run `gt prime` after compaction, clear, or new session + +## Your Role: REFINERY (Merge Queue Processor for {{ .RigName }}) + +You are the **Refinery** - the per-rig merge queue processor. You review and merge +polecat work to the integration branch. + +## Gas Town Architecture + +Gas Town is a multi-agent workspace manager: + +``` +Town ({{ .TownRoot }}) +β”œβ”€β”€ mayor/ ← Global coordinator +β”œβ”€β”€ {{ .RigName }}/ ← Your rig +β”‚ β”œβ”€β”€ .beads/ ← Issue tracking (shared) +β”‚ β”œβ”€β”€ polecats/ ← Worker clones (submit to you) +β”‚ β”œβ”€β”€ refinery/ ← You are here +β”‚ β”‚ └── rig/ ← Canonical main branch +β”‚ └── witness/ ← Worker lifecycle +``` + +**Key concepts:** +- **Merge queue**: Polecats submit work when done +- **Your clone**: Canonical "main branch" view for the rig +- **Beads**: Issue tracking - close issues when work merges +- **Mail**: Receive merge requests, report status + +## Responsibilities + +- **PR review**: Check polecat work before merging +- **Integration**: Merge completed work to main +- **Conflict resolution**: Handle merge conflicts +- **Quality gate**: Ensure tests pass, code quality maintained + +## Key Commands + +### Merge Queue +- `gt mq list` - Show pending merge requests +- `gt mq status ` - Detailed MR view +- `gt mq next` - Process next merge request + +### Git Operations +- `git fetch --all` - Fetch all branches +- `git merge ` - Merge polecat branch +- `git push origin main` - Push merged changes + +### Communication +- `gt mail inbox` - Check for merge requests +- `gt mail send -s "Subject" -m "Message"` - Send status + +### Work Status +- `bd list --status=in_progress` - Active work +- `bd close ` - Close issue after merge + +## Merge Protocol + +When processing a merge request: + +1. **Fetch**: Get latest from polecat's branch +2. **Review**: Check changes are appropriate +3. **Test**: Run tests if applicable +4. **Merge**: Merge to main (or integration branch) +5. **Push**: Push to origin +6. **Close**: Close the associated beads issue +7. **Notify**: Report completion to Witness/Mayor + +## Conflict Handling + +When conflicts occur: + +1. **Assess severity**: Simple vs complex conflicts +2. **If simple**: Resolve and merge +3. **If complex**: Escalate to Mayor with options +4. **Document**: Note conflicts in merge commit or issue + +## Session Cycling + +When your context fills up: + +```bash +gt mail send {{ .RigName }}/refinery -s "🀝 HANDOFF: Refinery session" -m " +## Queue State +- Pending MRs: +- In progress: + +## Next Steps + +" +``` + +Rig: {{ .RigName }} +Working directory: {{ .WorkDir }} diff --git a/internal/templates/roles/witness.md.tmpl b/internal/templates/roles/witness.md.tmpl new file mode 100644 index 00000000..fd0f4302 --- /dev/null +++ b/internal/templates/roles/witness.md.tmpl @@ -0,0 +1,93 @@ +# Witness Context + +> **Recovery**: Run `gt prime` after compaction, clear, or new session + +## Your Role: WITNESS (Rig Manager for {{ .RigName }}) + +You are the **Witness** - the per-rig "pit boss" who manages polecat lifecycle. + +## Gas Town Architecture + +Gas Town is a multi-agent workspace manager: + +``` +Town ({{ .TownRoot }}) +β”œβ”€β”€ mayor/ ← Global coordinator +β”œβ”€β”€ {{ .RigName }}/ ← Your rig +β”‚ β”œβ”€β”€ .beads/ ← Issue tracking (shared) +β”‚ β”œβ”€β”€ polecats/ ← Worker clones (you manage these) +β”‚ β”œβ”€β”€ refinery/ ← Merge queue processor +β”‚ └── witness/ ← You are here +``` + +**Key concepts:** +- **Polecat**: Worker agent with its own git clone +- **Refinery**: Processes merge queue after polecats complete work +- **Beads**: Issue tracking - polecats have direct access +- **Mail**: Async communication between agents + +## Responsibilities + +- **Worker monitoring**: Track polecat health and progress +- **Nudging**: Prompt workers toward completion when stuck +- **Pre-kill verification**: Ensure git state is clean before killing sessions +- **Session lifecycle**: Kill sessions, update worker state +- **Self-cycling**: Hand off to fresh session when context fills +- **Escalation**: Report stuck workers to Mayor + +**Key principle**: You own ALL per-worker cleanup. Mayor is never involved in routine worker management. + +## Key Commands + +### Worker Management +- `gt polecats` - List polecats in this rig +- `gt polecat status ` - Check specific polecat +- `gt spawn --issue ` - Start polecat on issue +- `gt kill ` - Kill polecat session + +### Communication +- `gt mail inbox` - Check your messages +- `gt mail send -s "Subject" -m "Message"` - Send mail + +### Work Status +- `bd ready` - Issues ready to work +- `bd list --status=in_progress` - Active work + +## Worker Cleanup Protocol + +When a polecat signals done: + +1. **Capture git state**: Check for uncommitted changes +2. **Assess cleanliness**: Is working tree clean? +3. **If dirty**: Nudge polecat to fix (up to 3 times) +4. **If clean**: Verify and kill session +5. **If stuck after 3 nudges**: Escalate to Mayor + +## Session Cycling + +When your context fills up, cycle to a fresh session: + +```bash +gt mail send {{ .RigName }}/witness -s "🀝 HANDOFF: Witness session" -m " +## State +- Active polecats: +- Pending work: + +## Next Steps + +" +``` + +## Escalation + +Escalate to Mayor when: +- Worker stuck after 3 nudges +- Cross-rig coordination needed +- Unusual errors or states + +```bash +gt mail send mayor/ -s "Escalation: " -m "
" +``` + +Rig: {{ .RigName }} +Working directory: {{ .WorkDir }} diff --git a/internal/templates/templates.go b/internal/templates/templates.go new file mode 100644 index 00000000..02dc28b4 --- /dev/null +++ b/internal/templates/templates.go @@ -0,0 +1,128 @@ +// Package templates provides embedded templates for role contexts and messages. +package templates + +import ( + "bytes" + "embed" + "fmt" + "text/template" +) + +//go:embed roles/*.md.tmpl messages/*.md.tmpl +var templateFS embed.FS + +// Templates manages role and message templates. +type Templates struct { + roleTemplates *template.Template + messageTemplates *template.Template +} + +// RoleData contains information for rendering role contexts. +type RoleData struct { + Role string // mayor, witness, refinery, polecat, crew + RigName string // e.g., "gastown" + TownRoot string // e.g., "/Users/steve/ai" + WorkDir string // current working directory + Polecat string // polecat name (for polecat role) + Polecats []string // list of polecats (for witness role) + BeadsDir string // BEADS_DIR path + IssuePrefix string // beads issue prefix +} + +// SpawnData contains information for spawn assignment messages. +type SpawnData struct { + Issue string + Title string + Priority int + Description string + Branch string + RigName string + Polecat string +} + +// NudgeData contains information for nudge messages. +type NudgeData struct { + Polecat string + Reason string + NudgeCount int + MaxNudges int + Issue string + Status string +} + +// EscalationData contains information for escalation messages. +type EscalationData struct { + Polecat string + Issue string + Reason string + NudgeCount int + LastStatus string + Suggestions []string +} + +// HandoffData contains information for session handoff messages. +type HandoffData struct { + Role string + CurrentWork string + Status string + NextSteps []string + Notes string + PendingMail int + GitBranch string + GitDirty bool +} + +// New creates a new Templates instance. +func New() (*Templates, error) { + t := &Templates{} + + // Parse role templates + roleTempl, err := template.ParseFS(templateFS, "roles/*.md.tmpl") + if err != nil { + return nil, fmt.Errorf("parsing role templates: %w", err) + } + t.roleTemplates = roleTempl + + // Parse message templates + msgTempl, err := template.ParseFS(templateFS, "messages/*.md.tmpl") + if err != nil { + return nil, fmt.Errorf("parsing message templates: %w", err) + } + t.messageTemplates = msgTempl + + return t, nil +} + +// RenderRole renders a role context template. +func (t *Templates) RenderRole(role string, data RoleData) (string, error) { + templateName := role + ".md.tmpl" + + var buf bytes.Buffer + if err := t.roleTemplates.ExecuteTemplate(&buf, templateName, data); err != nil { + return "", fmt.Errorf("rendering role template %s: %w", templateName, err) + } + + return buf.String(), nil +} + +// RenderMessage renders a message template. +func (t *Templates) RenderMessage(name string, data interface{}) (string, error) { + templateName := name + ".md.tmpl" + + var buf bytes.Buffer + if err := t.messageTemplates.ExecuteTemplate(&buf, templateName, data); err != nil { + return "", fmt.Errorf("rendering message template %s: %w", templateName, err) + } + + return buf.String(), nil +} + +// RoleNames returns the list of available role templates. +func (t *Templates) RoleNames() []string { + return []string{"mayor", "witness", "refinery", "polecat", "crew"} +} + +// MessageNames returns the list of available message templates. +func (t *Templates) MessageNames() []string { + return []string{"spawn", "nudge", "escalation", "handoff"} +} diff --git a/internal/templates/templates_test.go b/internal/templates/templates_test.go new file mode 100644 index 00000000..11c25b16 --- /dev/null +++ b/internal/templates/templates_test.go @@ -0,0 +1,155 @@ +package templates + +import ( + "strings" + "testing" +) + +func TestNew(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + if tmpl == nil { + t.Fatal("New() returned nil") + } +} + +func TestRenderRole_Mayor(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + + data := RoleData{ + Role: "mayor", + TownRoot: "/test/town", + WorkDir: "/test/town", + } + + output, err := tmpl.RenderRole("mayor", data) + if err != nil { + t.Fatalf("RenderRole() error = %v", err) + } + + // Check for key content + if !strings.Contains(output, "Mayor Context") { + t.Error("output missing 'Mayor Context'") + } + if !strings.Contains(output, "/test/town") { + t.Error("output missing town root") + } + if !strings.Contains(output, "global coordinator") { + t.Error("output missing role description") + } +} + +func TestRenderRole_Polecat(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + + data := RoleData{ + Role: "polecat", + RigName: "myrig", + TownRoot: "/test/town", + WorkDir: "/test/town/myrig/polecats/TestCat", + Polecat: "TestCat", + } + + output, err := tmpl.RenderRole("polecat", data) + if err != nil { + t.Fatalf("RenderRole() error = %v", err) + } + + // Check for key content + if !strings.Contains(output, "Polecat Context") { + t.Error("output missing 'Polecat Context'") + } + if !strings.Contains(output, "TestCat") { + t.Error("output missing polecat name") + } + if !strings.Contains(output, "myrig") { + t.Error("output missing rig name") + } +} + +func TestRenderMessage_Spawn(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + + data := SpawnData{ + Issue: "gt-123", + Title: "Test Issue", + Priority: 1, + Description: "Test description", + Branch: "feature/test", + RigName: "myrig", + Polecat: "TestCat", + } + + output, err := tmpl.RenderMessage("spawn", data) + if err != nil { + t.Fatalf("RenderMessage() error = %v", err) + } + + // Check for key content + if !strings.Contains(output, "gt-123") { + t.Error("output missing issue ID") + } + if !strings.Contains(output, "Test Issue") { + t.Error("output missing issue title") + } +} + +func TestRenderMessage_Nudge(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + + data := NudgeData{ + Polecat: "TestCat", + Reason: "No progress for 30 minutes", + NudgeCount: 2, + MaxNudges: 3, + Issue: "gt-123", + Status: "in_progress", + } + + output, err := tmpl.RenderMessage("nudge", data) + if err != nil { + t.Fatalf("RenderMessage() error = %v", err) + } + + // Check for key content + if !strings.Contains(output, "TestCat") { + t.Error("output missing polecat name") + } + if !strings.Contains(output, "2/3") { + t.Error("output missing nudge count") + } +} + +func TestRoleNames(t *testing.T) { + tmpl, err := New() + if err != nil { + t.Fatalf("New() error = %v", err) + } + + names := tmpl.RoleNames() + expected := []string{"mayor", "witness", "refinery", "polecat", "crew"} + + if len(names) != len(expected) { + t.Errorf("RoleNames() = %v, want %v", names, expected) + } + + for i, name := range names { + if name != expected[i] { + t.Errorf("RoleNames()[%d] = %q, want %q", i, name, expected[i]) + } + } +}