- Role templates (crew, polecat, mayor, deacon, witness, refinery) - prime.go startup protocol messages - Documentation (propulsion, reference, molecules, wisp architecture) - Session hints and sling prompts - Formula template instructions - CLAUDE.md The hook is the user-facing concept, molecules are implementation details. Agents should use `gt hook` to check what work is assigned to them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/mail"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
// runMoleculeAttachFromMail handles the "gt mol attach-from-mail <mail-id>" command.
|
|
// It reads a mail message, extracts the molecule ID from the body, and attaches
|
|
// it to the current agent's hook (pinned bead).
|
|
func runMoleculeAttachFromMail(cmd *cobra.Command, args []string) error {
|
|
mailID := args[0]
|
|
|
|
// Get current working directory and town root
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("getting current directory: %w", err)
|
|
}
|
|
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil || townRoot == "" {
|
|
return fmt.Errorf("not in a Gas Town workspace")
|
|
}
|
|
|
|
// Detect agent role and identity using env-aware detection
|
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("detecting role: %w", err)
|
|
}
|
|
roleCtx := RoleContext{
|
|
Role: roleInfo.Role,
|
|
Rig: roleInfo.Rig,
|
|
Polecat: roleInfo.Polecat,
|
|
TownRoot: townRoot,
|
|
WorkDir: cwd,
|
|
}
|
|
agentIdentity := buildAgentIdentity(roleCtx)
|
|
if agentIdentity == "" {
|
|
return fmt.Errorf("cannot determine agent identity (role: %s)", roleCtx.Role)
|
|
}
|
|
|
|
// Get the agent's mailbox
|
|
mailWorkDir, err := findMailWorkDir()
|
|
if err != nil {
|
|
return fmt.Errorf("finding mail workspace: %w", err)
|
|
}
|
|
|
|
router := mail.NewRouter(mailWorkDir)
|
|
mailbox, err := router.GetMailbox(agentIdentity)
|
|
if err != nil {
|
|
return fmt.Errorf("getting mailbox: %w", err)
|
|
}
|
|
|
|
// Read the mail message
|
|
msg, err := mailbox.Get(mailID)
|
|
if err != nil {
|
|
return fmt.Errorf("reading mail message: %w", err)
|
|
}
|
|
|
|
// Extract molecule ID from mail body
|
|
moleculeID := extractMoleculeIDFromMail(msg.Body)
|
|
if moleculeID == "" {
|
|
return fmt.Errorf("no attached_molecule field found in mail body")
|
|
}
|
|
|
|
// Find local beads directory
|
|
workDir, err := findLocalBeadsDir()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a beads workspace: %w", err)
|
|
}
|
|
|
|
b := beads.New(workDir)
|
|
|
|
// Find the agent's pinned bead (hook)
|
|
pinnedBeads, err := b.List(beads.ListOptions{
|
|
Status: beads.StatusPinned,
|
|
Assignee: agentIdentity,
|
|
Priority: -1,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("listing pinned beads: %w", err)
|
|
}
|
|
|
|
if len(pinnedBeads) == 0 {
|
|
return fmt.Errorf("no pinned bead found for agent %s - create one first", agentIdentity)
|
|
}
|
|
|
|
// Use the first pinned bead as the hook
|
|
hookBead := pinnedBeads[0]
|
|
|
|
// Check if molecule exists
|
|
_, err = b.Show(moleculeID)
|
|
if err != nil {
|
|
return fmt.Errorf("molecule %s not found: %w", moleculeID, err)
|
|
}
|
|
|
|
// Attach the molecule to the hook
|
|
issue, err := b.AttachMolecule(hookBead.ID, moleculeID)
|
|
if err != nil {
|
|
return fmt.Errorf("attaching molecule: %w", err)
|
|
}
|
|
|
|
// Mark mail as read
|
|
if err := mailbox.MarkRead(mailID); err != nil {
|
|
// Non-fatal: log warning but don't fail
|
|
style.PrintWarning("could not mark mail as read: %v", err)
|
|
}
|
|
|
|
// Output success
|
|
attachment := beads.ParseAttachmentFields(issue)
|
|
fmt.Printf("%s Attached molecule from mail\n", style.Bold.Render("✓"))
|
|
fmt.Printf(" Mail: %s\n", mailID)
|
|
fmt.Printf(" Hook: %s\n", hookBead.ID)
|
|
fmt.Printf(" Molecule: %s\n", moleculeID)
|
|
if attachment != nil && attachment.AttachedAt != "" {
|
|
fmt.Printf(" Attached at: %s\n", attachment.AttachedAt)
|
|
}
|
|
fmt.Printf("\n%s Run 'gt hook' to see progress\n", style.Dim.Render("Hint:"))
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractMoleculeIDFromMail extracts a molecule ID from a mail message body.
|
|
// It looks for patterns like:
|
|
// - attached_molecule: <id>
|
|
// - molecule_id: <id>
|
|
// - molecule: <id>
|
|
//
|
|
// The ID is expected to be on the same line after the colon.
|
|
func extractMoleculeIDFromMail(body string) string {
|
|
// Try various patterns for molecule ID in mail body (case-insensitive)
|
|
patterns := []string{
|
|
`(?i)attached_molecule:\s*(\S+)`,
|
|
`(?i)molecule_id:\s*(\S+)`,
|
|
`(?i)molecule:\s*(\S+)`,
|
|
`(?i)mol:\s*(\S+)`,
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
re := regexp.MustCompile(pattern)
|
|
matches := re.FindStringSubmatch(body)
|
|
if len(matches) >= 2 {
|
|
return strings.TrimSpace(matches[1])
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|