Files
gastown/internal/cmd/molecule_attach.go
gastown/crew/jack e8d27e7212 fix: Create and lookup rig agent beads with correct prefix
Per docs/architecture.md, Witness and Refinery are rig-level agents that
should use the rig's configured prefix (e.g., pi- for pixelforge) instead
of hardcoded "gt-".

This extends PR #183's creation fix to also fix all lookup paths:
- internal/rig/manager.go: Create agent beads in rig beads with rig prefix
- internal/daemon/daemon.go: Use rig prefix when looking up agent state
- internal/daemon/lifecycle.go: Use rig prefix for identity-to-bead mapping
- internal/cmd/sling.go: Pass townRoot for prefix lookup
- internal/cmd/unsling.go: Pass townRoot for prefix lookup
- internal/cmd/molecule_status.go: Use rig prefix for agent bead lookups
- internal/cmd/molecule_attach.go: Use rig prefix for agent bead lookups
- internal/config/loader.go: Add GetRigPrefix helper

Without this fix, the daemon would:
- Create pi-gastown-witness but look for gt-gastown-witness
- Report agents as missing/dead when they are running
- Fail to manage agent lifecycle correctly

Based on work by Johann Taberlet in PR #183.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Johann Taberlet <johann.taberlet@gmail.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:39:57 -08:00

218 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
func runMoleculeAttach(cmd *cobra.Command, args []string) error {
var pinnedBeadID, moleculeID string
if len(args) == 2 {
// Explicit: gt mol attach <pinned-bead-id> <molecule-id>
pinnedBeadID = args[0]
moleculeID = args[1]
} else {
// Auto-detect: gt mol attach <molecule-id>
moleculeID = args[0]
var err error
pinnedBeadID, err = detectAgentBeadID()
if err != nil {
return fmt.Errorf("auto-detecting agent: %w", err)
}
if pinnedBeadID == "" {
return fmt.Errorf("could not detect agent from current directory - provide explicit pinned bead ID")
}
}
workDir, err := findLocalBeadsDir()
if err != nil {
return fmt.Errorf("not in a beads workspace: %w", err)
}
b := beads.New(workDir)
// Attach the molecule
issue, err := b.AttachMolecule(pinnedBeadID, moleculeID)
if err != nil {
return fmt.Errorf("attaching molecule: %w", err)
}
attachment := beads.ParseAttachmentFields(issue)
fmt.Printf("%s Attached %s to %s\n", style.Bold.Render("✓"), moleculeID, pinnedBeadID)
if attachment != nil && attachment.AttachedAt != "" {
fmt.Printf(" attached_at: %s\n", attachment.AttachedAt)
}
return nil
}
// detectAgentBeadID detects the current agent's bead ID from the working directory.
// Returns the agent bead ID (e.g., "hq-mayor", "gt-gastown-polecat-nux") or empty string if not detectable.
func detectAgentBeadID() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("getting current directory: %w", err)
}
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")
}
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,
}
identity := buildAgentIdentity(roleCtx)
if identity == "" {
return "", fmt.Errorf("cannot determine agent identity (role: %s)", roleCtx.Role)
}
beadID := buildAgentBeadID(identity, roleCtx.Role, townRoot)
if beadID == "" {
return "", fmt.Errorf("cannot build agent bead ID for identity: %s", identity)
}
return beadID, nil
}
func runMoleculeDetach(cmd *cobra.Command, args []string) error {
pinnedBeadID := args[0]
workDir, err := findLocalBeadsDir()
if err != nil {
return fmt.Errorf("not in a beads workspace: %w", err)
}
b := beads.New(workDir)
// Check current attachment first
attachment, err := b.GetAttachment(pinnedBeadID)
if err != nil {
return fmt.Errorf("checking attachment: %w", err)
}
if attachment == nil {
fmt.Printf("%s No molecule attached to %s\n", style.Dim.Render(""), pinnedBeadID)
return nil
}
previousMolecule := attachment.AttachedMolecule
// Detach the molecule with audit logging
_, err = b.DetachMoleculeWithAudit(pinnedBeadID, beads.DetachOptions{
Operation: "detach",
Agent: detectCurrentAgent(),
})
if err != nil {
return fmt.Errorf("detaching molecule: %w", err)
}
fmt.Printf("%s Detached %s from %s\n", style.Bold.Render("✓"), previousMolecule, pinnedBeadID)
return nil
}
func runMoleculeAttachment(cmd *cobra.Command, args []string) error {
pinnedBeadID := args[0]
workDir, err := findLocalBeadsDir()
if err != nil {
return fmt.Errorf("not in a beads workspace: %w", err)
}
b := beads.New(workDir)
// Get the issue
issue, err := b.Show(pinnedBeadID)
if err != nil {
return fmt.Errorf("getting issue: %w", err)
}
attachment := beads.ParseAttachmentFields(issue)
if moleculeJSON {
type attachmentOutput struct {
IssueID string `json:"issue_id"`
IssueTitle string `json:"issue_title"`
Status string `json:"status"`
AttachedMolecule string `json:"attached_molecule,omitempty"`
AttachedAt string `json:"attached_at,omitempty"`
}
out := attachmentOutput{
IssueID: issue.ID,
IssueTitle: issue.Title,
Status: issue.Status,
}
if attachment != nil {
out.AttachedMolecule = attachment.AttachedMolecule
out.AttachedAt = attachment.AttachedAt
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(out)
}
// Human-readable output
fmt.Printf("\n%s: %s\n", style.Bold.Render(issue.ID), issue.Title)
fmt.Printf("Status: %s\n", issue.Status)
if attachment == nil || attachment.AttachedMolecule == "" {
fmt.Printf("\n%s\n", style.Dim.Render("No molecule attached"))
} else {
fmt.Printf("\n%s\n", style.Bold.Render("Attached Molecule:"))
fmt.Printf(" ID: %s\n", attachment.AttachedMolecule)
if attachment.AttachedAt != "" {
fmt.Printf(" Attached at: %s\n", attachment.AttachedAt)
}
}
return nil
}
// detectCurrentAgent returns the current agent identity based on GT_ROLE or working directory.
// Returns empty string if identity cannot be determined.
func detectCurrentAgent() string {
cwd, err := os.Getwd()
if err != nil {
return ""
}
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return ""
}
roleInfo, err := GetRoleWithContext(cwd, townRoot)
if err != nil {
return ""
}
ctx := RoleContext{
Role: roleInfo.Role,
Rig: roleInfo.Rig,
Polecat: roleInfo.Polecat,
TownRoot: townRoot,
WorkDir: cwd,
}
return buildAgentIdentity(ctx)
}