The agent_state field was recording observable state like "running", "dead", "idle" which violated the "Discover, Don't Track" principle. This caused stale state bugs where agents were marked "dead" in beads but actually running in tmux. Changes: - Remove daemon's checkStaleAgents() which marked agents "dead" - Simplify ensureXxxRunning() to use tmux.IsClaudeRunning() directly - Remove reportAgentState() calls from gt prime and gt handoff - Add SetHookBead/ClearHookBead helpers that don't update agent_state - Use ClearHookBead in gt done and gt unsling - Simplify gt status to derive state from tmux, not bead Non-observable states (stuck, awaiting-gate, muted, paused) are still set because they represent intentional agent decisions that can't be discovered from tmux state. Fixes: gt-zecmc 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1672 lines
52 KiB
Go
1672 lines
52 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/checkpoint"
|
|
"github.com/steveyegge/gastown/internal/constants"
|
|
"github.com/steveyegge/gastown/internal/events"
|
|
"github.com/steveyegge/gastown/internal/lock"
|
|
"github.com/steveyegge/gastown/internal/rig"
|
|
"github.com/steveyegge/gastown/internal/session"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/templates"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
var primeHookMode bool
|
|
|
|
// Role represents a detected agent role.
|
|
type Role string
|
|
|
|
const (
|
|
RoleMayor Role = "mayor"
|
|
RoleDeacon Role = "deacon"
|
|
RoleWitness Role = "witness"
|
|
RoleRefinery Role = "refinery"
|
|
RolePolecat Role = "polecat"
|
|
RoleCrew Role = "crew"
|
|
RoleUnknown Role = "unknown"
|
|
)
|
|
|
|
var primeCmd = &cobra.Command{
|
|
Use: "prime",
|
|
GroupID: GroupDiag,
|
|
Short: "Output role context for current directory",
|
|
Long: `Detect the agent role from the current directory and output context.
|
|
|
|
Role detection:
|
|
- Town root, mayor/, or <rig>/mayor/ → Mayor context
|
|
- <rig>/witness/rig/ → Witness context
|
|
- <rig>/refinery/rig/ → Refinery context
|
|
- <rig>/polecats/<name>/ → Polecat context
|
|
|
|
This command is typically used in shell prompts or agent initialization.
|
|
|
|
HOOK MODE (--hook):
|
|
When called as an LLM runtime hook, use --hook to enable session ID handling.
|
|
This reads session metadata from stdin and persists it for the session.
|
|
|
|
Claude Code integration (in .claude/settings.json):
|
|
"SessionStart": [{"hooks": [{"type": "command", "command": "gt prime --hook"}]}]
|
|
|
|
Claude Code sends JSON on stdin:
|
|
{"session_id": "uuid", "transcript_path": "/path", "source": "startup|resume"}
|
|
|
|
Other agents can set GT_SESSION_ID environment variable instead.`,
|
|
RunE: runPrime,
|
|
}
|
|
|
|
func init() {
|
|
primeCmd.Flags().BoolVar(&primeHookMode, "hook", false,
|
|
"Hook mode: read session ID from stdin JSON (for LLM runtime hooks)")
|
|
rootCmd.AddCommand(primeCmd)
|
|
}
|
|
|
|
// RoleContext is an alias for RoleInfo for backward compatibility.
|
|
// New code should use RoleInfo directly.
|
|
type RoleContext = RoleInfo
|
|
|
|
func runPrime(cmd *cobra.Command, args []string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("getting current directory: %w", err)
|
|
}
|
|
|
|
// Find town root
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil {
|
|
return fmt.Errorf("finding workspace: %w", err)
|
|
}
|
|
if townRoot == "" {
|
|
return fmt.Errorf("not in a Gas Town workspace")
|
|
}
|
|
|
|
// Handle hook mode: read session ID from stdin and persist it
|
|
if primeHookMode {
|
|
sessionID, source := readHookSessionID()
|
|
persistSessionID(townRoot, sessionID)
|
|
if cwd != townRoot {
|
|
persistSessionID(cwd, sessionID)
|
|
}
|
|
// Set environment for this process (affects event emission below)
|
|
_ = os.Setenv("GT_SESSION_ID", sessionID)
|
|
_ = os.Setenv("CLAUDE_SESSION_ID", sessionID) // Legacy compatibility
|
|
// Output session beacon
|
|
fmt.Printf("[session:%s]\n", sessionID)
|
|
if source != "" {
|
|
fmt.Printf("[source:%s]\n", source)
|
|
}
|
|
}
|
|
|
|
// Get role using env-aware detection
|
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("detecting role: %w", err)
|
|
}
|
|
|
|
// Warn prominently if there's a role/cwd mismatch
|
|
if roleInfo.Mismatch {
|
|
fmt.Printf("\n%s\n", style.Bold.Render("⚠️ ROLE/LOCATION MISMATCH"))
|
|
fmt.Printf("You are %s (from $GT_ROLE) but your cwd suggests %s.\n",
|
|
style.Bold.Render(string(roleInfo.Role)),
|
|
style.Bold.Render(string(roleInfo.CwdRole)))
|
|
fmt.Printf("Expected home: %s\n", roleInfo.Home)
|
|
fmt.Printf("Actual cwd: %s\n", cwd)
|
|
fmt.Println()
|
|
fmt.Println("This can cause commands to misbehave. Either:")
|
|
fmt.Println(" 1. cd to your home directory, OR")
|
|
fmt.Println(" 2. Use absolute paths for gt/bd commands")
|
|
fmt.Println()
|
|
}
|
|
|
|
// Build RoleContext for compatibility with existing code
|
|
ctx := RoleContext{
|
|
Role: roleInfo.Role,
|
|
Rig: roleInfo.Rig,
|
|
Polecat: roleInfo.Polecat,
|
|
TownRoot: townRoot,
|
|
WorkDir: cwd,
|
|
}
|
|
|
|
// Check and acquire identity lock for worker roles
|
|
if err := acquireIdentityLock(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure beads redirect exists for worktree-based roles
|
|
ensureBeadsRedirect(ctx)
|
|
|
|
// NOTE: reportAgentState("running") removed (gt-zecmc)
|
|
// Agent liveness is observable from tmux - no need to record it in bead.
|
|
// "Discover, don't track" principle: reality is truth, state is derived.
|
|
|
|
// Emit session_start event for seance discovery
|
|
emitSessionEvent(ctx)
|
|
|
|
// Output session metadata for seance discovery
|
|
outputSessionMetadata(ctx)
|
|
|
|
// Output context
|
|
if err := outputPrimeContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Output handoff content if present
|
|
outputHandoffContent(ctx)
|
|
|
|
// Output attachment status (for autonomous work detection)
|
|
outputAttachmentStatus(ctx)
|
|
|
|
// Check for slung work on hook (from gt sling)
|
|
// If found, we're in autonomous mode - skip normal startup directive
|
|
hasSlungWork := checkSlungWork(ctx)
|
|
|
|
// Output molecule context if working on a molecule step
|
|
outputMoleculeContext(ctx)
|
|
|
|
// Output previous session checkpoint for crash recovery
|
|
outputCheckpointContext(ctx)
|
|
|
|
// Run bd prime to output beads workflow context
|
|
runBdPrime(cwd)
|
|
|
|
// Run gt mail check --inject to inject any pending mail
|
|
runMailCheckInject(cwd)
|
|
|
|
// For Mayor, check for pending escalations
|
|
if ctx.Role == RoleMayor {
|
|
checkPendingEscalations(ctx)
|
|
}
|
|
|
|
// Output startup directive for roles that should announce themselves
|
|
// Skip if in autonomous mode (slung work provides its own directive)
|
|
if !hasSlungWork {
|
|
outputStartupDirective(ctx)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func detectRole(cwd, townRoot string) RoleInfo {
|
|
ctx := RoleInfo{
|
|
Role: RoleUnknown,
|
|
TownRoot: townRoot,
|
|
WorkDir: cwd,
|
|
Source: "cwd",
|
|
}
|
|
|
|
// Get relative path from town root
|
|
relPath, err := filepath.Rel(townRoot, cwd)
|
|
if err != nil {
|
|
return ctx
|
|
}
|
|
|
|
// Normalize and split path
|
|
relPath = filepath.ToSlash(relPath)
|
|
parts := strings.Split(relPath, "/")
|
|
|
|
// Check for mayor role
|
|
// At town root, or in mayor/ or mayor/rig/
|
|
if relPath == "." || relPath == "" {
|
|
ctx.Role = RoleMayor
|
|
return ctx
|
|
}
|
|
if len(parts) >= 1 && parts[0] == "mayor" {
|
|
ctx.Role = RoleMayor
|
|
return ctx
|
|
}
|
|
|
|
// Check for deacon role: deacon/
|
|
if len(parts) >= 1 && parts[0] == "deacon" {
|
|
ctx.Role = RoleDeacon
|
|
return ctx
|
|
}
|
|
|
|
// At this point, first part should be a rig name
|
|
if len(parts) < 1 {
|
|
return ctx
|
|
}
|
|
rigName := parts[0]
|
|
ctx.Rig = rigName
|
|
|
|
// Check for mayor: <rig>/mayor/ or <rig>/mayor/rig/
|
|
if len(parts) >= 2 && parts[1] == "mayor" {
|
|
ctx.Role = RoleMayor
|
|
return ctx
|
|
}
|
|
|
|
// Check for witness: <rig>/witness/rig/
|
|
if len(parts) >= 2 && parts[1] == "witness" {
|
|
ctx.Role = RoleWitness
|
|
return ctx
|
|
}
|
|
|
|
// Check for refinery: <rig>/refinery/rig/
|
|
if len(parts) >= 2 && parts[1] == "refinery" {
|
|
ctx.Role = RoleRefinery
|
|
return ctx
|
|
}
|
|
|
|
// Check for polecat: <rig>/polecats/<name>/
|
|
if len(parts) >= 3 && parts[1] == "polecats" {
|
|
ctx.Role = RolePolecat
|
|
ctx.Polecat = parts[2]
|
|
return ctx
|
|
}
|
|
|
|
// Check for crew: <rig>/crew/<name>/
|
|
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 RoleDeacon:
|
|
roleName = "deacon"
|
|
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
|
|
// Get town name for session names
|
|
townName, _ := workspace.GetTownName(ctx.TownRoot)
|
|
|
|
// Get default branch from rig config (default to "main" if not set)
|
|
defaultBranch := "main"
|
|
if ctx.Rig != "" && ctx.TownRoot != "" {
|
|
rigPath := filepath.Join(ctx.TownRoot, ctx.Rig)
|
|
if rigCfg, err := rig.LoadRigConfig(rigPath); err == nil && rigCfg.DefaultBranch != "" {
|
|
defaultBranch = rigCfg.DefaultBranch
|
|
}
|
|
}
|
|
|
|
data := templates.RoleData{
|
|
Role: roleName,
|
|
RigName: ctx.Rig,
|
|
TownRoot: ctx.TownRoot,
|
|
TownName: townName,
|
|
WorkDir: ctx.WorkDir,
|
|
DefaultBranch: defaultBranch,
|
|
Polecat: ctx.Polecat,
|
|
MayorSession: session.MayorSessionName(),
|
|
DeaconSession: session.DeaconSessionName(),
|
|
}
|
|
|
|
// 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)
|
|
case RoleWitness:
|
|
outputWitnessContext(ctx)
|
|
case RoleRefinery:
|
|
outputRefineryContext(ctx)
|
|
case RolePolecat:
|
|
outputPolecatContext(ctx)
|
|
case RoleCrew:
|
|
outputCrewContext(ctx)
|
|
default:
|
|
outputUnknownContext(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func outputMayorContext(ctx RoleContext) {
|
|
fmt.Printf("%s\n\n", style.Bold.Render("# Mayor Context"))
|
|
fmt.Println("You are the **Mayor** - the global coordinator of Gas Town.")
|
|
fmt.Println()
|
|
fmt.Println("## Responsibilities")
|
|
fmt.Println("- Coordinate work across all rigs")
|
|
fmt.Println("- Delegate to Refineries, not directly to polecats")
|
|
fmt.Println("- Monitor overall system health")
|
|
fmt.Println()
|
|
fmt.Println("## Key Commands")
|
|
fmt.Println("- `gt mail inbox` - Check your messages")
|
|
fmt.Println("- `gt mail read <id>` - Read a specific message")
|
|
fmt.Println("- `gt status` - Show overall town status")
|
|
fmt.Println("- `gt rig list` - List all rigs")
|
|
fmt.Println("- `bd ready` - Issues ready to work")
|
|
fmt.Println()
|
|
fmt.Println("## Hookable Mail")
|
|
fmt.Println("Mail can be hooked for ad-hoc instructions: `gt hook attach <mail-id>`")
|
|
fmt.Println("If mail is on your hook, read and execute its instructions (GUPP applies).")
|
|
fmt.Println()
|
|
fmt.Println("## Startup")
|
|
fmt.Println("Check for handoff messages with 🤝 HANDOFF in subject - continue predecessor's work.")
|
|
fmt.Println()
|
|
fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot))
|
|
}
|
|
|
|
func outputWitnessContext(ctx RoleContext) {
|
|
fmt.Printf("%s\n\n", style.Bold.Render("# Witness Context"))
|
|
fmt.Printf("You are the **Witness** for rig: %s\n\n", style.Bold.Render(ctx.Rig))
|
|
fmt.Println("## Responsibilities")
|
|
fmt.Println("- Monitor polecat health via heartbeat")
|
|
fmt.Println("- Spawn replacement agents for stuck polecats")
|
|
fmt.Println("- Report rig status to Mayor")
|
|
fmt.Println()
|
|
fmt.Println("## Key Commands")
|
|
fmt.Println("- `gt witness status` - Show witness status")
|
|
fmt.Println("- `gt polecat list` - List polecats in this rig")
|
|
fmt.Println()
|
|
fmt.Println("## Hookable Mail")
|
|
fmt.Println("Mail can be hooked for ad-hoc instructions: `gt hook attach <mail-id>`")
|
|
fmt.Println("If mail is on your hook, read and execute its instructions (GUPP applies).")
|
|
fmt.Println()
|
|
fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig))
|
|
}
|
|
|
|
func outputRefineryContext(ctx RoleContext) {
|
|
fmt.Printf("%s\n\n", style.Bold.Render("# Refinery Context"))
|
|
fmt.Printf("You are the **Refinery** for rig: %s\n\n", style.Bold.Render(ctx.Rig))
|
|
fmt.Println("## Responsibilities")
|
|
fmt.Println("- Process the merge queue for this rig")
|
|
fmt.Println("- Merge polecat work to integration branch")
|
|
fmt.Println("- Resolve merge conflicts")
|
|
fmt.Println("- Land completed swarms to main")
|
|
fmt.Println()
|
|
fmt.Println("## Key Commands")
|
|
fmt.Println("- `gt merge queue` - Show pending merges")
|
|
fmt.Println("- `gt merge next` - Process next merge")
|
|
fmt.Println()
|
|
fmt.Println("## Hookable Mail")
|
|
fmt.Println("Mail can be hooked for ad-hoc instructions: `gt hook attach <mail-id>`")
|
|
fmt.Println("If mail is on your hook, read and execute its instructions (GUPP applies).")
|
|
fmt.Println()
|
|
fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig))
|
|
}
|
|
|
|
func outputPolecatContext(ctx RoleContext) {
|
|
fmt.Printf("%s\n\n", style.Bold.Render("# Polecat Context"))
|
|
fmt.Printf("You are polecat **%s** in rig: %s\n\n",
|
|
style.Bold.Render(ctx.Polecat), style.Bold.Render(ctx.Rig))
|
|
fmt.Println("## Startup Protocol")
|
|
fmt.Println("1. Run `gt prime` - loads context and checks mail automatically")
|
|
fmt.Println("2. Check inbox - if mail shown, read with `gt mail read <id>`")
|
|
fmt.Println("3. Look for '📋 Work Assignment' messages for your task")
|
|
fmt.Println("4. If no mail, check `bd list --status=in_progress` for existing work")
|
|
fmt.Println()
|
|
fmt.Println("## Key Commands")
|
|
fmt.Println("- `gt mail inbox` - Check your inbox for work assignments")
|
|
fmt.Println("- `bd show <issue>` - View your assigned issue")
|
|
fmt.Println("- `bd close <issue>` - Mark issue complete")
|
|
fmt.Println("- `gt done` - Signal work ready for merge")
|
|
fmt.Println()
|
|
fmt.Println("## Hookable Mail")
|
|
fmt.Println("Mail can be hooked for ad-hoc instructions: `gt hook attach <mail-id>`")
|
|
fmt.Println("If mail is on your hook, read and execute its instructions (GUPP applies).")
|
|
fmt.Println()
|
|
fmt.Printf("Polecat: %s | Rig: %s\n",
|
|
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 <issue>` - View issue details")
|
|
fmt.Println("- `bd close <issue>` - Mark issue complete")
|
|
fmt.Println()
|
|
fmt.Println("## Hookable Mail")
|
|
fmt.Println("Mail can be hooked for ad-hoc instructions: `gt hook attach <mail-id>`")
|
|
fmt.Println("If mail is on your hook, read and execute its instructions (GUPP applies).")
|
|
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.")
|
|
fmt.Println()
|
|
if ctx.Rig != "" {
|
|
fmt.Printf("You appear to be in rig: %s\n\n", style.Bold.Render(ctx.Rig))
|
|
}
|
|
fmt.Println("Navigate to a specific agent directory:")
|
|
fmt.Println("- `<rig>/polecats/<name>/` - Polecat role")
|
|
fmt.Println("- `<rig>/witness/rig/` - Witness role")
|
|
fmt.Println("- `<rig>/refinery/rig/` - Refinery role")
|
|
fmt.Println("- Town root or `mayor/` - Mayor role")
|
|
fmt.Println()
|
|
fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot))
|
|
}
|
|
|
|
// outputHandoffContent reads and displays the pinned handoff bead for the role.
|
|
func outputHandoffContent(ctx RoleContext) {
|
|
if ctx.Role == RoleUnknown {
|
|
return
|
|
}
|
|
|
|
// Get role key for handoff bead lookup
|
|
roleKey := string(ctx.Role)
|
|
|
|
bd := beads.New(ctx.TownRoot)
|
|
issue, err := bd.FindHandoffBead(roleKey)
|
|
if err != nil {
|
|
// Silently skip if beads lookup fails (might not be a beads repo)
|
|
return
|
|
}
|
|
if issue == nil || issue.Description == "" {
|
|
// No handoff content
|
|
return
|
|
}
|
|
|
|
// Display handoff content
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🤝 Handoff from Previous Session"))
|
|
fmt.Println(issue.Description)
|
|
fmt.Println()
|
|
fmt.Println(style.Dim.Render("(Clear with: gt rig reset --handoff)"))
|
|
}
|
|
|
|
// runBdPrime runs `bd prime` and outputs the result.
|
|
// This provides beads workflow context to the agent.
|
|
func runBdPrime(workDir string) {
|
|
cmd := exec.Command("bd", "prime")
|
|
cmd.Dir = workDir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
// Skip if bd prime fails (beads might not be available)
|
|
// But log stderr if present for debugging
|
|
if errMsg := strings.TrimSpace(stderr.String()); errMsg != "" {
|
|
fmt.Fprintf(os.Stderr, "bd prime: %s\n", errMsg)
|
|
}
|
|
return
|
|
}
|
|
|
|
output := strings.TrimSpace(stdout.String())
|
|
if output != "" {
|
|
fmt.Println()
|
|
fmt.Println(output)
|
|
}
|
|
}
|
|
|
|
// outputStartupDirective outputs role-specific instructions for the agent.
|
|
// This tells agents like Mayor to announce themselves on startup.
|
|
func outputStartupDirective(ctx RoleContext) {
|
|
switch ctx.Role {
|
|
case RoleMayor:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are the Mayor. Please:")
|
|
fmt.Println("1. Announce: \"Mayor, checking in.\"")
|
|
fmt.Println("2. Check mail: `gt mail inbox` - look for 🤝 HANDOFF messages")
|
|
fmt.Println("3. Check for attached work: `gt hook`")
|
|
fmt.Println(" - If mol attached → **RUN IT** (no human input needed)")
|
|
fmt.Println(" - If no mol → await user instruction")
|
|
case RoleWitness:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are the Witness. Please:")
|
|
fmt.Println("1. Announce: \"Witness, checking in.\"")
|
|
fmt.Println("2. Check mail: `gt mail inbox` - look for 🤝 HANDOFF messages")
|
|
fmt.Println("3. Check for attached patrol: `gt hook`")
|
|
fmt.Println(" - If mol attached → **RUN IT** (resume from current step)")
|
|
fmt.Println(" - If no mol → create patrol: `bd mol wisp mol-witness-patrol`")
|
|
case RolePolecat:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are a polecat. Please:")
|
|
fmt.Printf("1. Announce: \"%s Polecat %s, checking in.\"\n", ctx.Rig, ctx.Polecat)
|
|
fmt.Println("2. Check mail: `gt mail inbox`")
|
|
fmt.Println("3. If there's a 🤝 HANDOFF message, read it for context")
|
|
fmt.Println("4. Check for attached work: `gt hook`")
|
|
fmt.Println(" - If mol attached → **RUN IT** (you were spawned with this work)")
|
|
fmt.Println(" - If no mol → ERROR: polecats must have work attached; escalate to Witness")
|
|
case RoleRefinery:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are the Refinery. Please:")
|
|
fmt.Println("1. Announce: \"Refinery, checking in.\"")
|
|
fmt.Println("2. Check mail: `gt mail inbox` - look for 🤝 HANDOFF messages")
|
|
fmt.Println("3. Check for attached patrol: `gt hook`")
|
|
fmt.Println(" - If mol attached → **RUN IT** (resume from current step)")
|
|
fmt.Println(" - If no mol → create patrol: `bd mol wisp mol-refinery-patrol`")
|
|
case RoleCrew:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are a crew worker. Please:")
|
|
fmt.Printf("1. Announce: \"%s Crew %s, checking in.\"\n", ctx.Rig, ctx.Polecat)
|
|
fmt.Println("2. Check mail: `gt mail inbox`")
|
|
fmt.Println("3. If there's a 🤝 HANDOFF message, read it and continue the work")
|
|
fmt.Println("4. Check for attached work: `gt hook`")
|
|
fmt.Println(" - If attachment found → **RUN IT** (no human input needed)")
|
|
fmt.Println(" - If no attachment → await user instruction")
|
|
case RoleDeacon:
|
|
fmt.Println()
|
|
fmt.Println("---")
|
|
fmt.Println()
|
|
fmt.Println("**STARTUP PROTOCOL**: You are the Deacon. Please:")
|
|
fmt.Println("1. Announce: \"Deacon, checking in.\"")
|
|
fmt.Println("2. Signal awake: `gt deacon heartbeat \"starting patrol\"`")
|
|
fmt.Println("3. Check mail: `gt mail inbox` - look for 🤝 HANDOFF messages")
|
|
fmt.Println("4. Check for attached patrol: `gt hook`")
|
|
fmt.Println(" - If mol attached → **RUN IT** (resume from current step)")
|
|
fmt.Println(" - If no mol → create patrol: `bd mol wisp mol-deacon-patrol`")
|
|
}
|
|
}
|
|
|
|
// runMailCheckInject runs `gt mail check --inject` and outputs the result.
|
|
// This injects any pending mail into the agent's context.
|
|
func runMailCheckInject(workDir string) {
|
|
cmd := exec.Command("gt", "mail", "check", "--inject")
|
|
cmd.Dir = workDir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
// Skip if mail check fails, but log stderr for debugging
|
|
if errMsg := strings.TrimSpace(stderr.String()); errMsg != "" {
|
|
fmt.Fprintf(os.Stderr, "gt mail check: %s\n", errMsg)
|
|
}
|
|
return
|
|
}
|
|
|
|
output := strings.TrimSpace(stdout.String())
|
|
if output != "" {
|
|
fmt.Println()
|
|
fmt.Println(output)
|
|
}
|
|
}
|
|
|
|
// outputAttachmentStatus checks for attached work molecule and outputs status.
|
|
// This is key for the autonomous overnight work pattern.
|
|
// The Propulsion Principle: "If you find something on your hook, YOU RUN IT."
|
|
func outputAttachmentStatus(ctx RoleContext) {
|
|
// Skip only unknown roles - all valid roles can have pinned work
|
|
if ctx.Role == RoleUnknown {
|
|
return
|
|
}
|
|
|
|
// Check for pinned beads with attachments
|
|
b := beads.New(ctx.WorkDir)
|
|
|
|
// Build assignee string based on role (same as getAgentIdentity)
|
|
assignee := getAgentIdentity(ctx)
|
|
if assignee == "" {
|
|
return
|
|
}
|
|
|
|
// Find pinned beads for this agent
|
|
pinnedBeads, err := b.List(beads.ListOptions{
|
|
Status: beads.StatusPinned,
|
|
Assignee: assignee,
|
|
Priority: -1,
|
|
})
|
|
if err != nil || len(pinnedBeads) == 0 {
|
|
// No pinned beads - interactive mode
|
|
return
|
|
}
|
|
|
|
// Check first pinned bead for attachment
|
|
attachment := beads.ParseAttachmentFields(pinnedBeads[0])
|
|
if attachment == nil || attachment.AttachedMolecule == "" {
|
|
// No attachment - interactive mode
|
|
return
|
|
}
|
|
|
|
// Has attached work - output prominently with current step
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🎯 ATTACHED WORK DETECTED"))
|
|
fmt.Printf("Pinned bead: %s\n", pinnedBeads[0].ID)
|
|
fmt.Printf("Attached molecule: %s\n", attachment.AttachedMolecule)
|
|
if attachment.AttachedAt != "" {
|
|
fmt.Printf("Attached at: %s\n", attachment.AttachedAt)
|
|
}
|
|
if attachment.AttachedArgs != "" {
|
|
fmt.Println()
|
|
fmt.Printf("%s\n", style.Bold.Render("📋 ARGS (use these to guide execution):"))
|
|
fmt.Printf(" %s\n", attachment.AttachedArgs)
|
|
}
|
|
fmt.Println()
|
|
|
|
// Show current step from molecule
|
|
showMoleculeExecutionPrompt(ctx.WorkDir, attachment.AttachedMolecule)
|
|
}
|
|
|
|
// MoleculeCurrentOutput represents the JSON output of bd mol current.
|
|
type MoleculeCurrentOutput struct {
|
|
MoleculeID string `json:"molecule_id"`
|
|
MoleculeTitle string `json:"molecule_title"`
|
|
NextStep *struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
} `json:"next_step"`
|
|
Completed int `json:"completed"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// showMoleculeExecutionPrompt calls bd mol current and shows the current step
|
|
// with execution instructions. This is the core of the Propulsion Principle.
|
|
func showMoleculeExecutionPrompt(workDir, moleculeID string) {
|
|
// Call bd mol current with JSON output
|
|
cmd := exec.Command("bd", "--no-daemon", "mol", "current", moleculeID, "--json")
|
|
cmd.Dir = workDir
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
// Fall back to simple message if bd mol current fails
|
|
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
|
|
fmt.Println(" Begin working on this molecule immediately.")
|
|
fmt.Printf(" Check status with: bd mol current %s\n", moleculeID)
|
|
return
|
|
}
|
|
|
|
// Parse JSON output - it's an array with one element
|
|
var outputs []MoleculeCurrentOutput
|
|
if err := json.Unmarshal(stdout.Bytes(), &outputs); err != nil || len(outputs) == 0 {
|
|
// Fall back to simple message
|
|
fmt.Println(style.Bold.Render("→ PROPULSION PRINCIPLE: Work is on your hook. RUN IT."))
|
|
fmt.Println(" Begin working on this molecule immediately.")
|
|
return
|
|
}
|
|
output := outputs[0]
|
|
|
|
// Show molecule progress
|
|
fmt.Printf("**Progress:** %d/%d steps complete\n\n",
|
|
output.Completed, output.Total)
|
|
|
|
// Show current step if available
|
|
if output.NextStep != nil {
|
|
step := output.NextStep
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🎬 CURRENT STEP: "+step.Title))
|
|
fmt.Printf("**Step ID:** %s\n", step.ID)
|
|
fmt.Printf("**Status:** %s (ready to execute)\n\n", step.Status)
|
|
|
|
// Show step description if available
|
|
if step.Description != "" {
|
|
fmt.Println("### Instructions")
|
|
fmt.Println()
|
|
// Indent the description for readability
|
|
lines := strings.Split(step.Description, "\n")
|
|
for _, line := range lines {
|
|
fmt.Printf("%s\n", line)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// The propulsion directive
|
|
fmt.Println(style.Bold.Render("→ EXECUTE THIS STEP NOW."))
|
|
fmt.Println()
|
|
fmt.Println("When complete:")
|
|
fmt.Printf(" 1. Close the step: bd close %s\n", step.ID)
|
|
fmt.Println(" 2. Check for next step: bd ready")
|
|
fmt.Println(" 3. Continue until molecule complete")
|
|
} else {
|
|
// No next step - molecule may be complete
|
|
fmt.Println(style.Bold.Render("✓ MOLECULE COMPLETE"))
|
|
fmt.Println()
|
|
fmt.Println("All steps are done. You may:")
|
|
fmt.Println(" - Report completion to supervisor")
|
|
fmt.Println(" - Check for new work: bd ready")
|
|
}
|
|
}
|
|
|
|
// outputMoleculeContext checks if the agent is working on a molecule step and shows progress.
|
|
func outputMoleculeContext(ctx RoleContext) {
|
|
// Applies to polecats, crew workers, deacon, witness, and refinery
|
|
if ctx.Role != RolePolecat && ctx.Role != RoleCrew && ctx.Role != RoleDeacon && ctx.Role != RoleWitness && ctx.Role != RoleRefinery {
|
|
return
|
|
}
|
|
|
|
// For Deacon, use special patrol molecule handling
|
|
if ctx.Role == RoleDeacon {
|
|
outputDeaconPatrolContext(ctx)
|
|
return
|
|
}
|
|
|
|
// For Witness, use special patrol molecule handling (auto-bonds on startup)
|
|
if ctx.Role == RoleWitness {
|
|
outputWitnessPatrolContext(ctx)
|
|
return
|
|
}
|
|
|
|
// For Refinery, use special patrol molecule handling (auto-bonds on startup)
|
|
if ctx.Role == RoleRefinery {
|
|
outputRefineryPatrolContext(ctx)
|
|
return
|
|
}
|
|
|
|
// Check for in-progress issues
|
|
b := beads.New(ctx.WorkDir)
|
|
issues, err := b.List(beads.ListOptions{
|
|
Status: "in_progress",
|
|
Assignee: ctx.Polecat,
|
|
Priority: -1,
|
|
})
|
|
if err != nil || len(issues) == 0 {
|
|
return
|
|
}
|
|
|
|
// Check if any in-progress issue is a molecule step
|
|
for _, issue := range issues {
|
|
moleculeID := parseMoleculeMetadata(issue.Description)
|
|
if moleculeID == "" {
|
|
continue
|
|
}
|
|
|
|
// Get the parent (root) issue ID
|
|
rootID := issue.Parent
|
|
if rootID == "" {
|
|
continue
|
|
}
|
|
|
|
// This is a molecule step - show context
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🧬 Molecule Workflow"))
|
|
fmt.Printf("You are working on a molecule step.\n")
|
|
fmt.Printf(" Current step: %s\n", issue.ID)
|
|
fmt.Printf(" Molecule: %s\n", moleculeID)
|
|
fmt.Printf(" Root issue: %s\n\n", rootID)
|
|
|
|
// Show molecule progress by finding sibling steps
|
|
showMoleculeProgress(b, rootID)
|
|
|
|
fmt.Println()
|
|
fmt.Println("**Molecule Work Loop:**")
|
|
fmt.Println("1. Complete current step, then `bd close " + issue.ID + "`")
|
|
fmt.Println("2. Check for next steps: `bd ready --parent " + rootID + "`")
|
|
fmt.Println("3. Work on next ready step(s)")
|
|
fmt.Println("4. When all steps done, run `gt done`")
|
|
break // Only show context for first molecule step found
|
|
}
|
|
}
|
|
|
|
// parseMoleculeMetadata extracts molecule info from a step's description.
|
|
// Looks for lines like:
|
|
//
|
|
// instantiated_from: mol-xyz
|
|
func parseMoleculeMetadata(description string) string {
|
|
lines := strings.Split(description, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "instantiated_from:") {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "instantiated_from:"))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// showMoleculeProgress displays the progress through a molecule's steps.
|
|
func showMoleculeProgress(b *beads.Beads, rootID string) {
|
|
if rootID == "" {
|
|
return
|
|
}
|
|
|
|
// Find all children of the root issue
|
|
children, err := b.List(beads.ListOptions{
|
|
Parent: rootID,
|
|
Status: "all",
|
|
Priority: -1,
|
|
})
|
|
if err != nil || len(children) == 0 {
|
|
return
|
|
}
|
|
|
|
total := len(children)
|
|
done := 0
|
|
inProgress := 0
|
|
var readySteps []string
|
|
|
|
for _, child := range children {
|
|
switch child.Status {
|
|
case "closed":
|
|
done++
|
|
case "in_progress":
|
|
inProgress++
|
|
case "open":
|
|
// Check if ready (no open dependencies)
|
|
if len(child.DependsOn) == 0 {
|
|
readySteps = append(readySteps, child.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Progress: %d/%d steps complete", done, total)
|
|
if inProgress > 0 {
|
|
fmt.Printf(" (%d in progress)", inProgress)
|
|
}
|
|
fmt.Println()
|
|
|
|
if len(readySteps) > 0 {
|
|
fmt.Printf("Ready steps: %s\n", strings.Join(readySteps, ", "))
|
|
}
|
|
}
|
|
|
|
// outputDeaconPatrolContext shows patrol molecule status for the Deacon.
|
|
// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles.
|
|
func outputDeaconPatrolContext(ctx RoleContext) {
|
|
cfg := PatrolConfig{
|
|
RoleName: "deacon",
|
|
PatrolMolName: "mol-deacon-patrol",
|
|
BeadsDir: ctx.WorkDir,
|
|
Assignee: "deacon",
|
|
HeaderEmoji: "🔄",
|
|
HeaderTitle: "Patrol Status (Wisp-based)",
|
|
CheckInProgress: false,
|
|
WorkLoopSteps: []string{
|
|
"Check next step: `bd ready`",
|
|
"Execute the step (heartbeat, mail, health checks, etc.)",
|
|
"Close step: `bd close <step-id>`",
|
|
"Check next: `bd ready`",
|
|
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-deacon-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Deacon patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
|
|
},
|
|
}
|
|
outputPatrolContext(cfg)
|
|
}
|
|
|
|
// outputWitnessPatrolContext shows patrol molecule status for the Witness.
|
|
// Witness AUTO-BONDS its patrol molecule on startup if one isn't already running.
|
|
func outputWitnessPatrolContext(ctx RoleContext) {
|
|
cfg := PatrolConfig{
|
|
RoleName: "witness",
|
|
PatrolMolName: "mol-witness-patrol",
|
|
BeadsDir: ctx.WorkDir,
|
|
Assignee: ctx.Rig + "/witness",
|
|
HeaderEmoji: constants.EmojiWitness,
|
|
HeaderTitle: "Witness Patrol Status",
|
|
CheckInProgress: true,
|
|
WorkLoopSteps: []string{
|
|
"Check inbox: `gt mail inbox`",
|
|
"Check next step: `bd ready`",
|
|
"Execute the step (survey polecats, inspect, nudge, etc.)",
|
|
"Close step: `bd close <step-id>`",
|
|
"Check next: `bd ready`",
|
|
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-witness-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Witness patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
|
|
},
|
|
}
|
|
outputPatrolContext(cfg)
|
|
}
|
|
|
|
// outputRefineryPatrolContext shows patrol molecule status for the Refinery.
|
|
// Refinery AUTO-BONDS its patrol molecule on startup if one isn't already running.
|
|
func outputRefineryPatrolContext(ctx RoleContext) {
|
|
cfg := PatrolConfig{
|
|
RoleName: "refinery",
|
|
PatrolMolName: "mol-refinery-patrol",
|
|
BeadsDir: ctx.WorkDir,
|
|
Assignee: ctx.Rig + "/refinery",
|
|
HeaderEmoji: "🔧",
|
|
HeaderTitle: "Refinery Patrol Status",
|
|
CheckInProgress: true,
|
|
WorkLoopSteps: []string{
|
|
"Check inbox: `gt mail inbox`",
|
|
"Check next step: `bd ready`",
|
|
"Execute the step (queue scan, process branch, tests, merge)",
|
|
"Close step: `bd close <step-id>`",
|
|
"Check next: `bd ready`",
|
|
"At cycle end (loop-or-exit step):\n - If context LOW:\n * Squash: `bd mol squash <mol-id> --summary \"<summary>\"`\n * Create new patrol: `bd mol wisp mol-refinery-patrol`\n * Continue executing from inbox-check step\n - If context HIGH:\n * Send handoff: `gt handoff -s \"Refinery patrol\" -m \"<observations>\"`\n * Exit cleanly (daemon respawns fresh session)",
|
|
},
|
|
}
|
|
outputPatrolContext(cfg)
|
|
}
|
|
|
|
// checkSlungWork checks for hooked work on the agent's hook.
|
|
// If found, displays AUTONOMOUS WORK MODE and tells the agent to execute immediately.
|
|
// Returns true if hooked work was found (caller should skip normal startup directive).
|
|
func checkSlungWork(ctx RoleContext) bool {
|
|
// Determine agent identity
|
|
agentID := getAgentIdentity(ctx)
|
|
if agentID == "" {
|
|
return false
|
|
}
|
|
|
|
// Check for hooked beads (work on the agent's hook)
|
|
b := beads.New(ctx.WorkDir)
|
|
hookedBeads, err := b.List(beads.ListOptions{
|
|
Status: beads.StatusHooked,
|
|
Assignee: agentID,
|
|
Priority: -1,
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// If no hooked beads found, also check in_progress beads assigned to this agent.
|
|
// This handles the case where work was claimed (status changed to in_progress)
|
|
// but the session was interrupted before completion. The hook should persist.
|
|
if len(hookedBeads) == 0 {
|
|
inProgressBeads, err := b.List(beads.ListOptions{
|
|
Status: "in_progress",
|
|
Assignee: agentID,
|
|
Priority: -1,
|
|
})
|
|
if err != nil || len(inProgressBeads) == 0 {
|
|
return false
|
|
}
|
|
hookedBeads = inProgressBeads
|
|
}
|
|
|
|
// Use the first hooked bead (agents typically have one)
|
|
hookedBead := hookedBeads[0]
|
|
|
|
// Build the role announcement string
|
|
roleAnnounce := buildRoleAnnouncement(ctx)
|
|
|
|
// Found hooked work! Display AUTONOMOUS MODE prominently
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🚨 AUTONOMOUS WORK MODE 🚨"))
|
|
fmt.Println("Work is on your hook. After announcing your role, begin IMMEDIATELY.")
|
|
fmt.Println()
|
|
fmt.Println("This is physics, not politeness. Gas Town is a steam engine - you are a piston.")
|
|
fmt.Println("Every moment you wait is a moment the engine stalls. Other agents may be")
|
|
fmt.Println("blocked waiting on YOUR output. The hook IS your assignment. RUN IT.")
|
|
fmt.Println()
|
|
fmt.Println("Remember: Every completion is recorded in the capability ledger. Your work")
|
|
fmt.Println("history is visible, and quality matters. Execute with care - you're building")
|
|
fmt.Println("a track record that proves autonomous execution works at scale.")
|
|
fmt.Println()
|
|
fmt.Println("1. Announce: \"" + roleAnnounce + "\" (ONE line, no elaboration)")
|
|
fmt.Printf("2. Then IMMEDIATELY run: `bd show %s`\n", hookedBead.ID)
|
|
fmt.Println("3. Begin execution - no waiting for user input")
|
|
fmt.Println()
|
|
fmt.Println("**DO NOT:**")
|
|
fmt.Println("- Wait for user response after announcing")
|
|
fmt.Println("- Ask clarifying questions")
|
|
fmt.Println("- Describe what you're going to do")
|
|
fmt.Println("- Check mail first (hook takes priority)")
|
|
fmt.Println()
|
|
|
|
// Show the hooked work details
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## Hooked Work"))
|
|
fmt.Printf(" Bead ID: %s\n", style.Bold.Render(hookedBead.ID))
|
|
fmt.Printf(" Title: %s\n", hookedBead.Title)
|
|
if hookedBead.Description != "" {
|
|
// Show first few lines of description
|
|
lines := strings.Split(hookedBead.Description, "\n")
|
|
maxLines := 5
|
|
if len(lines) > maxLines {
|
|
lines = lines[:maxLines]
|
|
lines = append(lines, "...")
|
|
}
|
|
fmt.Println(" Description:")
|
|
for _, line := range lines {
|
|
fmt.Printf(" %s\n", line)
|
|
}
|
|
}
|
|
fmt.Println()
|
|
|
|
// Show bead preview using bd show
|
|
fmt.Println("**Bead details:**")
|
|
cmd := exec.Command("bd", "show", hookedBead.ID)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
if errMsg := strings.TrimSpace(stderr.String()); errMsg != "" {
|
|
fmt.Fprintf(os.Stderr, " bd show %s: %s\n", hookedBead.ID, errMsg)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, " bd show %s: %v\n", hookedBead.ID, err)
|
|
}
|
|
} else {
|
|
lines := strings.Split(stdout.String(), "\n")
|
|
maxLines := 15
|
|
if len(lines) > maxLines {
|
|
lines = lines[:maxLines]
|
|
lines = append(lines, "...")
|
|
}
|
|
for _, line := range lines {
|
|
fmt.Printf(" %s\n", line)
|
|
}
|
|
}
|
|
fmt.Println()
|
|
|
|
return true
|
|
}
|
|
|
|
// buildRoleAnnouncement creates the role announcement string for autonomous mode.
|
|
func buildRoleAnnouncement(ctx RoleContext) string {
|
|
switch ctx.Role {
|
|
case RoleMayor:
|
|
return "Mayor, checking in."
|
|
case RoleDeacon:
|
|
return "Deacon, checking in."
|
|
case RoleWitness:
|
|
return fmt.Sprintf("%s Witness, checking in.", ctx.Rig)
|
|
case RoleRefinery:
|
|
return fmt.Sprintf("%s Refinery, checking in.", ctx.Rig)
|
|
case RolePolecat:
|
|
return fmt.Sprintf("%s Polecat %s, checking in.", ctx.Rig, ctx.Polecat)
|
|
case RoleCrew:
|
|
return fmt.Sprintf("%s Crew %s, checking in.", ctx.Rig, ctx.Polecat)
|
|
default:
|
|
return "Agent, checking in."
|
|
}
|
|
}
|
|
|
|
// getGitRoot returns the root of the current git repository.
|
|
func getGitRoot() (string, error) {
|
|
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// getAgentIdentity returns the agent identity string for hook lookup.
|
|
func getAgentIdentity(ctx RoleContext) string {
|
|
switch ctx.Role {
|
|
case RoleCrew:
|
|
return fmt.Sprintf("%s/crew/%s", ctx.Rig, ctx.Polecat)
|
|
case RolePolecat:
|
|
return fmt.Sprintf("%s/polecats/%s", ctx.Rig, ctx.Polecat)
|
|
case RoleMayor:
|
|
return "mayor"
|
|
case RoleDeacon:
|
|
return "deacon"
|
|
case RoleWitness:
|
|
return fmt.Sprintf("%s/witness", ctx.Rig)
|
|
case RoleRefinery:
|
|
return fmt.Sprintf("%s/refinery", ctx.Rig)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// acquireIdentityLock checks and acquires the identity lock for worker roles.
|
|
// This prevents multiple agents from claiming the same worker identity.
|
|
// Returns an error if another agent already owns this identity.
|
|
func acquireIdentityLock(ctx RoleContext) error {
|
|
// Only lock worker roles (polecat, crew)
|
|
// Infrastructure roles (mayor, witness, refinery, deacon) are singletons
|
|
// managed by tmux session names, so they don't need file-based locks
|
|
if ctx.Role != RolePolecat && ctx.Role != RoleCrew {
|
|
return nil
|
|
}
|
|
|
|
// Create lock for this worker directory
|
|
l := lock.New(ctx.WorkDir)
|
|
|
|
// Determine session ID from environment or context
|
|
sessionID := os.Getenv("TMUX_PANE")
|
|
if sessionID == "" {
|
|
// Fall back to a descriptive identifier
|
|
sessionID = fmt.Sprintf("%s/%s", ctx.Rig, ctx.Polecat)
|
|
}
|
|
|
|
// Try to acquire the lock
|
|
if err := l.Acquire(sessionID); err != nil {
|
|
if errors.Is(err, lock.ErrLocked) {
|
|
// Another agent owns this identity
|
|
fmt.Printf("\n%s\n\n", style.Bold.Render("⚠️ IDENTITY COLLISION DETECTED"))
|
|
fmt.Printf("Another agent already claims this worker identity.\n\n")
|
|
|
|
// Show lock details
|
|
if info, readErr := l.Read(); readErr == nil {
|
|
fmt.Printf("Lock holder:\n")
|
|
fmt.Printf(" PID: %d\n", info.PID)
|
|
fmt.Printf(" Session: %s\n", info.SessionID)
|
|
fmt.Printf(" Acquired: %s\n", info.AcquiredAt.Format("2006-01-02 15:04:05"))
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Printf("To resolve:\n")
|
|
fmt.Printf(" 1. Find the other session and close it, OR\n")
|
|
fmt.Printf(" 2. Run: gt doctor --fix (cleans stale locks)\n")
|
|
fmt.Printf(" 3. If lock is stale: rm %s/.runtime/agent.lock\n", ctx.WorkDir)
|
|
fmt.Println()
|
|
|
|
return fmt.Errorf("cannot claim identity %s/%s: %w", ctx.Rig, ctx.Polecat, err)
|
|
}
|
|
return fmt.Errorf("acquiring identity lock: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// reportAgentState updates the agent bead to report the agent's current state.
|
|
// This implements ZFC-compliant self-reporting of agent state.
|
|
// Agents call this on startup (running) and shutdown (stopped).
|
|
// For crew workers, creates the agent bead if it doesn't exist.
|
|
func reportAgentState(ctx RoleContext, state string) {
|
|
agentBeadID := getAgentBeadID(ctx)
|
|
if agentBeadID == "" {
|
|
return
|
|
}
|
|
|
|
// Use the beads API directly to update agent state
|
|
// This is more reliable than shelling out to bd
|
|
bd := beads.New(ctx.WorkDir)
|
|
|
|
// Check if agent bead exists, create if needed (especially for crew workers)
|
|
if _, err := bd.Show(agentBeadID); err != nil {
|
|
// Agent bead doesn't exist - create it
|
|
fields := getAgentFields(ctx, state)
|
|
if fields != nil {
|
|
_, createErr := bd.CreateAgentBead(agentBeadID, agentBeadID, fields)
|
|
if createErr != nil {
|
|
// Silently ignore - beads might not be configured
|
|
return
|
|
}
|
|
// Bead created with initial state, no need to update
|
|
return
|
|
}
|
|
}
|
|
|
|
// Update existing agent bead state
|
|
if err := bd.UpdateAgentState(agentBeadID, state, nil); err != nil {
|
|
// Silently ignore errors - don't fail prime if state reporting fails
|
|
return
|
|
}
|
|
}
|
|
|
|
// getAgentFields returns the AgentFields for creating a new agent bead.
|
|
func getAgentFields(ctx RoleContext, state string) *beads.AgentFields {
|
|
switch ctx.Role {
|
|
case RoleCrew:
|
|
return &beads.AgentFields{
|
|
RoleType: "crew",
|
|
Rig: ctx.Rig,
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("crew"),
|
|
}
|
|
case RolePolecat:
|
|
return &beads.AgentFields{
|
|
RoleType: "polecat",
|
|
Rig: ctx.Rig,
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("polecat"),
|
|
}
|
|
case RoleMayor:
|
|
return &beads.AgentFields{
|
|
RoleType: "mayor",
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("mayor"),
|
|
}
|
|
case RoleDeacon:
|
|
return &beads.AgentFields{
|
|
RoleType: "deacon",
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("deacon"),
|
|
}
|
|
case RoleWitness:
|
|
return &beads.AgentFields{
|
|
RoleType: "witness",
|
|
Rig: ctx.Rig,
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("witness"),
|
|
}
|
|
case RoleRefinery:
|
|
return &beads.AgentFields{
|
|
RoleType: "refinery",
|
|
Rig: ctx.Rig,
|
|
AgentState: state,
|
|
RoleBead: beads.RoleBeadIDTown("refinery"),
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// getAgentBeadID returns the agent bead ID for the current role.
|
|
// Town-level agents (mayor, deacon) use hq- prefix; rig-scoped agents use the rig's prefix.
|
|
// Returns empty string for unknown roles.
|
|
func getAgentBeadID(ctx RoleContext) string {
|
|
switch ctx.Role {
|
|
case RoleMayor:
|
|
return beads.MayorBeadIDTown()
|
|
case RoleDeacon:
|
|
return beads.DeaconBeadIDTown()
|
|
case RoleWitness:
|
|
if ctx.Rig != "" {
|
|
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
|
|
return beads.WitnessBeadIDWithPrefix(prefix, ctx.Rig)
|
|
}
|
|
return ""
|
|
case RoleRefinery:
|
|
if ctx.Rig != "" {
|
|
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
|
|
return beads.RefineryBeadIDWithPrefix(prefix, ctx.Rig)
|
|
}
|
|
return ""
|
|
case RolePolecat:
|
|
if ctx.Rig != "" && ctx.Polecat != "" {
|
|
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
|
|
return beads.PolecatBeadIDWithPrefix(prefix, ctx.Rig, ctx.Polecat)
|
|
}
|
|
return ""
|
|
case RoleCrew:
|
|
if ctx.Rig != "" && ctx.Polecat != "" {
|
|
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
|
|
return beads.CrewBeadIDWithPrefix(prefix, ctx.Rig, ctx.Polecat)
|
|
}
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// ensureBeadsRedirect ensures the .beads/redirect file exists for worktree-based roles.
|
|
// This handles cases where git clean or other operations delete the redirect file.
|
|
// Uses the shared SetupRedirect helper which handles both tracked and local beads.
|
|
func ensureBeadsRedirect(ctx RoleContext) {
|
|
// Only applies to worktree-based roles that use shared beads
|
|
if ctx.Role != RoleCrew && ctx.Role != RolePolecat && ctx.Role != RoleRefinery {
|
|
return
|
|
}
|
|
|
|
// Check if redirect already exists
|
|
redirectPath := filepath.Join(ctx.WorkDir, ".beads", "redirect")
|
|
if _, err := os.Stat(redirectPath); err == nil {
|
|
// Redirect exists, nothing to do
|
|
return
|
|
}
|
|
|
|
// Use shared helper - silently ignore errors during prime
|
|
_ = beads.SetupRedirect(ctx.TownRoot, ctx.WorkDir)
|
|
}
|
|
|
|
// checkPendingEscalations queries for open escalation beads and displays them prominently.
|
|
// This is called on Mayor startup to surface issues needing human attention.
|
|
func checkPendingEscalations(ctx RoleContext) {
|
|
// Query for open escalations using bd list with tag filter
|
|
cmd := exec.Command("bd", "list", "--status=open", "--tag=escalation", "--json")
|
|
cmd.Dir = ctx.WorkDir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
// Silently skip - escalation check is best-effort
|
|
return
|
|
}
|
|
|
|
// Parse JSON output
|
|
var escalations []struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Priority int `json:"priority"`
|
|
Description string `json:"description"`
|
|
Created string `json:"created"`
|
|
}
|
|
|
|
if err := json.Unmarshal(stdout.Bytes(), &escalations); err != nil || len(escalations) == 0 {
|
|
// No escalations or parse error
|
|
return
|
|
}
|
|
|
|
// Count by severity
|
|
critical := 0
|
|
high := 0
|
|
medium := 0
|
|
for _, e := range escalations {
|
|
switch e.Priority {
|
|
case 0:
|
|
critical++
|
|
case 1:
|
|
high++
|
|
default:
|
|
medium++
|
|
}
|
|
}
|
|
|
|
// Display prominently
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 🚨 PENDING ESCALATIONS"))
|
|
fmt.Printf("There are %d escalation(s) awaiting human attention:\n\n", len(escalations))
|
|
|
|
if critical > 0 {
|
|
fmt.Printf(" 🔴 CRITICAL: %d\n", critical)
|
|
}
|
|
if high > 0 {
|
|
fmt.Printf(" 🟠 HIGH: %d\n", high)
|
|
}
|
|
if medium > 0 {
|
|
fmt.Printf(" 🟡 MEDIUM: %d\n", medium)
|
|
}
|
|
fmt.Println()
|
|
|
|
// Show first few escalations
|
|
maxShow := 5
|
|
if len(escalations) < maxShow {
|
|
maxShow = len(escalations)
|
|
}
|
|
for i := 0; i < maxShow; i++ {
|
|
e := escalations[i]
|
|
severity := "MEDIUM"
|
|
switch e.Priority {
|
|
case 0:
|
|
severity = "CRITICAL"
|
|
case 1:
|
|
severity = "HIGH"
|
|
}
|
|
fmt.Printf(" • [%s] %s (%s)\n", severity, e.Title, e.ID)
|
|
}
|
|
if len(escalations) > maxShow {
|
|
fmt.Printf(" ... and %d more\n", len(escalations)-maxShow)
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Println("**Action required:** Review escalations with `bd list --tag=escalation`")
|
|
fmt.Println("Close resolved ones with `bd close <id> --reason \"resolution\"`")
|
|
fmt.Println()
|
|
}
|
|
|
|
// outputCheckpointContext reads and displays any previous session checkpoint.
|
|
// This enables crash recovery by showing what the previous session was working on.
|
|
func outputCheckpointContext(ctx RoleContext) {
|
|
// Only applies to polecats and crew workers
|
|
if ctx.Role != RolePolecat && ctx.Role != RoleCrew {
|
|
return
|
|
}
|
|
|
|
// Read checkpoint
|
|
cp, err := checkpoint.Read(ctx.WorkDir)
|
|
if err != nil {
|
|
// Silently ignore read errors
|
|
return
|
|
}
|
|
if cp == nil {
|
|
// No checkpoint exists
|
|
return
|
|
}
|
|
|
|
// Check if checkpoint is stale (older than 24 hours)
|
|
if cp.IsStale(24 * time.Hour) {
|
|
// Remove stale checkpoint
|
|
_ = checkpoint.Remove(ctx.WorkDir)
|
|
return
|
|
}
|
|
|
|
// Display checkpoint context
|
|
fmt.Println()
|
|
fmt.Printf("%s\n\n", style.Bold.Render("## 📌 Previous Session Checkpoint"))
|
|
fmt.Printf("A previous session left a checkpoint %s ago.\n\n", cp.Age().Round(time.Minute))
|
|
|
|
if cp.StepTitle != "" {
|
|
fmt.Printf(" **Working on:** %s\n", cp.StepTitle)
|
|
}
|
|
if cp.MoleculeID != "" {
|
|
fmt.Printf(" **Molecule:** %s\n", cp.MoleculeID)
|
|
}
|
|
if cp.CurrentStep != "" {
|
|
fmt.Printf(" **Step:** %s\n", cp.CurrentStep)
|
|
}
|
|
if cp.HookedBead != "" {
|
|
fmt.Printf(" **Hooked bead:** %s\n", cp.HookedBead)
|
|
}
|
|
if cp.Branch != "" {
|
|
fmt.Printf(" **Branch:** %s\n", cp.Branch)
|
|
}
|
|
if len(cp.ModifiedFiles) > 0 {
|
|
fmt.Printf(" **Modified files:** %d\n", len(cp.ModifiedFiles))
|
|
// Show first few files
|
|
maxShow := 5
|
|
if len(cp.ModifiedFiles) < maxShow {
|
|
maxShow = len(cp.ModifiedFiles)
|
|
}
|
|
for i := 0; i < maxShow; i++ {
|
|
fmt.Printf(" - %s\n", cp.ModifiedFiles[i])
|
|
}
|
|
if len(cp.ModifiedFiles) > maxShow {
|
|
fmt.Printf(" ... and %d more\n", len(cp.ModifiedFiles)-maxShow)
|
|
}
|
|
}
|
|
if cp.Notes != "" {
|
|
fmt.Printf(" **Notes:** %s\n", cp.Notes)
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Println("Use this context to resume work. The checkpoint will be updated as you progress.")
|
|
fmt.Println()
|
|
}
|
|
|
|
// emitSessionEvent emits a session_start event for seance discovery.
|
|
// The event is written to ~/gt/.events.jsonl and can be queried via gt seance.
|
|
// Session ID resolution order: GT_SESSION_ID, CLAUDE_SESSION_ID, persisted file, fallback.
|
|
func emitSessionEvent(ctx RoleContext) {
|
|
if ctx.Role == RoleUnknown {
|
|
return
|
|
}
|
|
|
|
// Get agent identity for the actor field
|
|
actor := getAgentIdentity(ctx)
|
|
if actor == "" {
|
|
return
|
|
}
|
|
|
|
// Get session ID from multiple sources
|
|
sessionID := resolveSessionIDForPrime(actor)
|
|
|
|
// Determine topic from hook state or default
|
|
topic := ""
|
|
if ctx.Role == RoleWitness || ctx.Role == RoleRefinery || ctx.Role == RoleDeacon {
|
|
topic = "patrol"
|
|
}
|
|
|
|
// Emit the event
|
|
payload := events.SessionPayload(sessionID, actor, topic, ctx.WorkDir)
|
|
_ = events.LogFeed(events.TypeSessionStart, actor, payload)
|
|
}
|
|
|
|
// outputSessionMetadata prints a structured metadata line for seance discovery.
|
|
// Format: [GAS TOWN] role:<role> pid:<pid> session:<session_id>
|
|
// This enables gt seance to discover sessions from gt prime output.
|
|
func outputSessionMetadata(ctx RoleContext) {
|
|
if ctx.Role == RoleUnknown {
|
|
return
|
|
}
|
|
|
|
// Get agent identity for the role field
|
|
actor := getAgentIdentity(ctx)
|
|
if actor == "" {
|
|
return
|
|
}
|
|
|
|
// Get session ID from multiple sources
|
|
sessionID := resolveSessionIDForPrime(actor)
|
|
|
|
// Output structured metadata line
|
|
fmt.Printf("[GAS TOWN] role:%s pid:%d session:%s\n", actor, os.Getpid(), sessionID)
|
|
}
|
|
|
|
// resolveSessionIDForPrime finds the session ID from available sources.
|
|
// Priority: GT_SESSION_ID env, CLAUDE_SESSION_ID env, persisted file, fallback.
|
|
func resolveSessionIDForPrime(actor string) string {
|
|
// 1. GT_SESSION_ID (new canonical)
|
|
if id := os.Getenv("GT_SESSION_ID"); id != "" {
|
|
return id
|
|
}
|
|
|
|
// 2. CLAUDE_SESSION_ID (legacy/Claude Code)
|
|
if id := os.Getenv("CLAUDE_SESSION_ID"); id != "" {
|
|
return id
|
|
}
|
|
|
|
// 3. Persisted session file (from gt prime --hook)
|
|
if id := ReadPersistedSessionID(); id != "" {
|
|
return id
|
|
}
|
|
|
|
// 4. Fallback to generated identifier
|
|
return fmt.Sprintf("%s-%d", actor, os.Getpid())
|
|
}
|
|
|
|
// hookInput represents the JSON input from LLM runtime hooks.
|
|
// Claude Code sends this on stdin for SessionStart hooks.
|
|
type hookInput struct {
|
|
SessionID string `json:"session_id"`
|
|
TranscriptPath string `json:"transcript_path"`
|
|
Source string `json:"source"` // startup, resume, clear, compact
|
|
}
|
|
|
|
// readHookSessionID reads session ID from available sources in hook mode.
|
|
// Priority: stdin JSON, GT_SESSION_ID env, CLAUDE_SESSION_ID env, auto-generate.
|
|
func readHookSessionID() (sessionID, source string) {
|
|
// 1. Try reading stdin JSON (Claude Code format)
|
|
if input := readStdinJSON(); input != nil {
|
|
if input.SessionID != "" {
|
|
return input.SessionID, input.Source
|
|
}
|
|
}
|
|
|
|
// 2. Environment variables
|
|
if id := os.Getenv("GT_SESSION_ID"); id != "" {
|
|
return id, ""
|
|
}
|
|
if id := os.Getenv("CLAUDE_SESSION_ID"); id != "" {
|
|
return id, ""
|
|
}
|
|
|
|
// 3. Auto-generate
|
|
return uuid.New().String(), ""
|
|
}
|
|
|
|
// readStdinJSON attempts to read and parse JSON from stdin.
|
|
// Returns nil if stdin is empty, not a pipe, or invalid JSON.
|
|
func readStdinJSON() *hookInput {
|
|
// Check if stdin has data (non-blocking)
|
|
stat, err := os.Stdin.Stat()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Only read if stdin is a pipe or has data
|
|
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
|
// stdin is a terminal, not a pipe - no data to read
|
|
return nil
|
|
}
|
|
|
|
// Read first line (JSON should be on one line)
|
|
reader := bufio.NewReader(os.Stdin)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return nil
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
var input hookInput
|
|
if err := json.Unmarshal([]byte(line), &input); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &input
|
|
}
|
|
|
|
// persistSessionID writes the session ID to .runtime/session_id
|
|
// This allows subsequent gt prime calls to find the session ID.
|
|
func persistSessionID(dir, sessionID string) {
|
|
runtimeDir := filepath.Join(dir, ".runtime")
|
|
if err := os.MkdirAll(runtimeDir, 0755); err != nil {
|
|
return // Non-fatal
|
|
}
|
|
|
|
sessionFile := filepath.Join(runtimeDir, "session_id")
|
|
content := fmt.Sprintf("%s\n%s\n", sessionID, time.Now().Format(time.RFC3339))
|
|
_ = os.WriteFile(sessionFile, []byte(content), 0644) // Non-fatal
|
|
}
|
|
|
|
// ReadPersistedSessionID reads a previously persisted session ID.
|
|
// Checks cwd first, then town root.
|
|
// Returns empty string if not found.
|
|
func ReadPersistedSessionID() string {
|
|
// Try cwd first
|
|
cwd, err := os.Getwd()
|
|
if err == nil {
|
|
if id := readSessionFile(cwd); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
|
|
// Try town root
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err == nil && townRoot != "" {
|
|
if id := readSessionFile(townRoot); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func readSessionFile(dir string) string {
|
|
sessionFile := filepath.Join(dir, ".runtime", "session_id")
|
|
data, err := os.ReadFile(sessionFile)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
if len(lines) > 0 {
|
|
return strings.TrimSpace(lines[0])
|
|
}
|
|
return ""
|
|
}
|