feat: Complete agent bead lifecycle for ZFC compliance
Changes: 1. prime.go: Create agent beads on first prime for all roles including crew 2. done.go: Update agent state to done/stuck/idle when work completes 3. sling.go: Populate hook_bead when work is assigned to agents 4. mol-witness-patrol.formula.toml: Document dead and spawning states Closes: gt-hymm0, gt-0lop3, gt-59k2x, gt-c4j4j 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -187,8 +187,10 @@ Each polecat agent bead has fields in its description:
|
|||||||
|-------------|---------|--------|
|
|-------------|---------|--------|
|
||||||
| running | Actively working | Check progress (Step 3) |
|
| running | Actively working | Check progress (Step 3) |
|
||||||
| idle | No work assigned | Skip (no action needed) |
|
| idle | No work assigned | Skip (no action needed) |
|
||||||
| stuck | Self-reported stuck | Handle stuck protocol |
|
| stuck | Self-reported stuck (via gt done --exit ESCALATED) | Handle stuck protocol |
|
||||||
| done | Work complete | Verify cleanup triggered (see Step 4a) |
|
| done | Work complete (via gt done) | Verify cleanup triggered (see Step 4a) |
|
||||||
|
| dead | Marked dead by daemon (unresponsive) | Clean up and respawn |
|
||||||
|
| spawning | Recently created, initializing | Wait for running state |
|
||||||
|
|
||||||
**Step 3: For running polecats, assess progress**
|
**Step 3: For running polecats, assess progress**
|
||||||
|
|
||||||
@@ -216,6 +218,8 @@ Look for:
|
|||||||
| agent_state=running, idle 15+ min | Direct nudge with deadline |
|
| agent_state=running, idle 15+ min | Direct nudge with deadline |
|
||||||
| agent_state=stuck | Assess and help or escalate |
|
| agent_state=stuck | Assess and help or escalate |
|
||||||
| agent_state=done | Verify cleanup triggered (see Step 4a) |
|
| agent_state=done | Verify cleanup triggered (see Step 4a) |
|
||||||
|
| agent_state=dead | Create cleanup wisp, mark for respawn if needed |
|
||||||
|
| agent_state=spawning | Check if stuck in spawn; if >2 min, investigate |
|
||||||
|
|
||||||
**Step 4a: Handle agent_state=done**
|
**Step 4a: Handle agent_state=done**
|
||||||
|
|
||||||
|
|||||||
@@ -223,5 +223,55 @@ func runDone(cmd *cobra.Command, args []string) error {
|
|||||||
// Log done event
|
// Log done event
|
||||||
LogDone(townRoot, sender, issueID)
|
LogDone(townRoot, sender, issueID)
|
||||||
|
|
||||||
|
// Update agent bead state (ZFC: self-report completion)
|
||||||
|
updateAgentStateOnDone(cwd, townRoot, exitType, issueID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAgentStateOnDone updates the agent bead state when work is complete.
|
||||||
|
// Maps exit type to agent state:
|
||||||
|
// - COMPLETED → "done"
|
||||||
|
// - ESCALATED → "stuck"
|
||||||
|
// - DEFERRED → "idle"
|
||||||
|
func updateAgentStateOnDone(cwd, townRoot, exitType, issueID string) {
|
||||||
|
// Get role context
|
||||||
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := RoleContext{
|
||||||
|
Role: roleInfo.Role,
|
||||||
|
Rig: roleInfo.Rig,
|
||||||
|
Polecat: roleInfo.Polecat,
|
||||||
|
TownRoot: townRoot,
|
||||||
|
WorkDir: cwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
agentBeadID := getAgentBeadID(ctx)
|
||||||
|
if agentBeadID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map exit type to agent state
|
||||||
|
var newState string
|
||||||
|
switch exitType {
|
||||||
|
case ExitCompleted:
|
||||||
|
newState = "done"
|
||||||
|
case ExitEscalated:
|
||||||
|
newState = "stuck"
|
||||||
|
case ExitDeferred:
|
||||||
|
newState = "idle"
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update agent bead with new state and clear hook_bead (work is done)
|
||||||
|
bd := beads.New(cwd)
|
||||||
|
emptyHook := ""
|
||||||
|
if err := bd.UpdateAgentState(agentBeadID, newState, &emptyHook); err != nil {
|
||||||
|
// Silently ignore - beads might not be configured
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1055,6 +1055,7 @@ func acquireIdentityLock(ctx RoleContext) error {
|
|||||||
// reportAgentState updates the agent bead to report the agent's current state.
|
// reportAgentState updates the agent bead to report the agent's current state.
|
||||||
// This implements ZFC-compliant self-reporting of agent state.
|
// This implements ZFC-compliant self-reporting of agent state.
|
||||||
// Agents call this on startup (running) and shutdown (stopped).
|
// 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) {
|
func reportAgentState(ctx RoleContext, state string) {
|
||||||
agentBeadID := getAgentBeadID(ctx)
|
agentBeadID := getAgentBeadID(ctx)
|
||||||
if agentBeadID == "" {
|
if agentBeadID == "" {
|
||||||
@@ -1064,13 +1065,77 @@ func reportAgentState(ctx RoleContext, state string) {
|
|||||||
// Use the beads API directly to update agent state
|
// Use the beads API directly to update agent state
|
||||||
// This is more reliable than shelling out to bd
|
// This is more reliable than shelling out to bd
|
||||||
bd := beads.New(ctx.WorkDir)
|
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 {
|
if err := bd.UpdateAgentState(agentBeadID, state, nil); err != nil {
|
||||||
// Silently ignore errors - don't fail prime if state reporting fails
|
// Silently ignore errors - don't fail prime if state reporting fails
|
||||||
// This can fail if beads isn't set up or agent bead doesn't exist
|
|
||||||
return
|
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: fmt.Sprintf("gt-crew-%s-%s-role", ctx.Rig, ctx.Polecat),
|
||||||
|
}
|
||||||
|
case RolePolecat:
|
||||||
|
return &beads.AgentFields{
|
||||||
|
RoleType: "polecat",
|
||||||
|
Rig: ctx.Rig,
|
||||||
|
AgentState: state,
|
||||||
|
RoleBead: fmt.Sprintf("gt-polecat-%s-%s-role", ctx.Rig, ctx.Polecat),
|
||||||
|
}
|
||||||
|
case RoleMayor:
|
||||||
|
return &beads.AgentFields{
|
||||||
|
RoleType: "mayor",
|
||||||
|
AgentState: state,
|
||||||
|
RoleBead: "gt-mayor-role",
|
||||||
|
}
|
||||||
|
case RoleDeacon:
|
||||||
|
return &beads.AgentFields{
|
||||||
|
RoleType: "deacon",
|
||||||
|
AgentState: state,
|
||||||
|
RoleBead: "gt-deacon-role",
|
||||||
|
}
|
||||||
|
case RoleWitness:
|
||||||
|
return &beads.AgentFields{
|
||||||
|
RoleType: "witness",
|
||||||
|
Rig: ctx.Rig,
|
||||||
|
AgentState: state,
|
||||||
|
RoleBead: fmt.Sprintf("gt-witness-%s-role", ctx.Rig),
|
||||||
|
}
|
||||||
|
case RoleRefinery:
|
||||||
|
return &beads.AgentFields{
|
||||||
|
RoleType: "refinery",
|
||||||
|
Rig: ctx.Rig,
|
||||||
|
AgentState: state,
|
||||||
|
RoleBead: fmt.Sprintf("gt-refinery-%s-role", ctx.Rig),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getAgentBeadID returns the agent bead ID for the current role.
|
// getAgentBeadID returns the agent bead ID for the current role.
|
||||||
// Returns empty string for unknown roles.
|
// Returns empty string for unknown roles.
|
||||||
func getAgentBeadID(ctx RoleContext) string {
|
func getAgentBeadID(ctx RoleContext) string {
|
||||||
|
|||||||
@@ -239,6 +239,9 @@ func runSling(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("%s Work attached to hook (pinned bead)\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Work attached to hook (pinned bead)\n", style.Bold.Render("✓"))
|
||||||
|
|
||||||
|
// Update agent bead's hook_bead field (ZFC: agents track their current work)
|
||||||
|
updateAgentHookBead(targetAgent, beadID)
|
||||||
|
|
||||||
// Store args in bead description (no-tmux mode: beads as data plane)
|
// Store args in bead description (no-tmux mode: beads as data plane)
|
||||||
if slingArgs != "" {
|
if slingArgs != "" {
|
||||||
if err := storeArgsInBead(beadID, slingArgs); err != nil {
|
if err := storeArgsInBead(beadID, slingArgs); err != nil {
|
||||||
@@ -600,6 +603,9 @@ func runSlingFormula(args []string) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("%s Attached to hook (pinned bead)\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Attached to hook (pinned bead)\n", style.Bold.Render("✓"))
|
||||||
|
|
||||||
|
// Update agent bead's hook_bead field (ZFC: agents track their current work)
|
||||||
|
updateAgentHookBead(targetAgent, wispResult.RootID)
|
||||||
|
|
||||||
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
|
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
|
||||||
if slingArgs != "" {
|
if slingArgs != "" {
|
||||||
if err := storeArgsInBead(wispResult.RootID, slingArgs); err != nil {
|
if err := storeArgsInBead(wispResult.RootID, slingArgs); err != nil {
|
||||||
@@ -632,3 +638,62 @@ func runSlingFormula(args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAgentHookBead updates the agent bead's hook_bead field when work is slung.
|
||||||
|
// This enables the witness to see what each agent is working on.
|
||||||
|
func updateAgentHookBead(agentID, beadID string) {
|
||||||
|
// Convert agent ID to agent bead ID
|
||||||
|
// Format examples:
|
||||||
|
// gastown/crew/max -> gt-crew-gastown-max
|
||||||
|
// gastown/polecats/Toast -> gt-polecat-gastown-Toast
|
||||||
|
// mayor -> gt-mayor
|
||||||
|
// gastown/witness -> gt-witness-gastown
|
||||||
|
agentBeadID := agentIDToBeadID(agentID)
|
||||||
|
if agentBeadID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find beads directory - try current directory first
|
||||||
|
workDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bd := beads.New(workDir)
|
||||||
|
if err := bd.UpdateAgentState(agentBeadID, "running", &beadID); err != nil {
|
||||||
|
// Silently ignore - agent bead might not exist yet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// agentIDToBeadID converts an agent ID to its corresponding agent bead ID.
|
||||||
|
func agentIDToBeadID(agentID string) string {
|
||||||
|
// Handle simple cases
|
||||||
|
if agentID == "mayor" {
|
||||||
|
return "gt-mayor"
|
||||||
|
}
|
||||||
|
if agentID == "deacon" {
|
||||||
|
return "gt-deacon"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse path-style agent IDs
|
||||||
|
parts := strings.Split(agentID, "/")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
rig := parts[0]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(parts) == 2 && parts[1] == "witness":
|
||||||
|
return fmt.Sprintf("gt-witness-%s", rig)
|
||||||
|
case len(parts) == 2 && parts[1] == "refinery":
|
||||||
|
return fmt.Sprintf("gt-refinery-%s", rig)
|
||||||
|
case len(parts) == 3 && parts[1] == "crew":
|
||||||
|
return fmt.Sprintf("gt-crew-%s-%s", rig, parts[2])
|
||||||
|
case len(parts) == 3 && parts[1] == "polecats":
|
||||||
|
return fmt.Sprintf("gt-polecat-%s-%s", rig, parts[2])
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user