Instrument gt commands to appear in activity feed (gt-7aw1m)

Phase 1 of activity feed improvements: gt commands now log events to
~/gt/.events.jsonl. This is the raw audit log that the feed daemon
(phase 2) will curate into the user-facing feed.

Instrumented commands:
- gt sling: logs sling events with bead and target
- gt hook: logs hook events with bead
- gt handoff: logs handoff events with subject
- gt done: logs done events with bead and branch
- gt mail send: logs mail events with to and subject

Event format follows the specification:
```json
{"ts":"2025-12-30T07:36:28Z","source":"gt","type":"mail",
 "actor":"gastown/crew/joe","payload":{...},"visibility":"feed"}
```

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-29 23:37:50 -08:00
parent fcea70efa1
commit 2844edb541
6 changed files with 209 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
@@ -213,8 +214,9 @@ func runDone(cmd *cobra.Command, args []string) error {
fmt.Printf("%s Witness notified of %s\n", style.Bold.Render("✓"), exitType) fmt.Printf("%s Witness notified of %s\n", style.Bold.Render("✓"), exitType)
} }
// Log done event // Log done event (townlog and activity feed)
LogDone(townRoot, sender, issueID) LogDone(townRoot, sender, issueID)
_ = events.LogFeed(events.TypeDone, sender, events.DonePayload(issueID, branch))
// Update agent bead state (ZFC: self-report completion) // Update agent bead state (ZFC: self-report completion)
updateAgentStateOnDone(cwd, townRoot, exitType, issueID) updateAgentStateOnDone(cwd, townRoot, exitType, issueID)

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/tmux"
@@ -127,13 +128,15 @@ func runHandoff(cmd *cobra.Command, args []string) error {
// Handing off ourselves - print feedback then respawn // Handing off ourselves - print feedback then respawn
fmt.Printf("%s Handing off %s...\n", style.Bold.Render("🤝"), currentSession) fmt.Printf("%s Handing off %s...\n", style.Bold.Render("🤝"), currentSession)
// Log handoff event // Log handoff event (both townlog and events feed)
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" { if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
agent := sessionToGTRole(currentSession) agent := sessionToGTRole(currentSession)
if agent == "" { if agent == "" {
agent = currentSession agent = currentSession
} }
LogHandoff(townRoot, agent, handoffSubject) LogHandoff(townRoot, agent, handoffSubject)
// Also log to activity feed
_ = events.LogFeed(events.TypeHandoff, agent, events.HandoffPayload(handoffSubject, true))
} }
// Dry run mode - show what would happen (BEFORE any side effects) // Dry run mode - show what would happen (BEFORE any side effects)

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
) )
@@ -164,6 +165,9 @@ func runHook(cmd *cobra.Command, args []string) error {
fmt.Printf(" Use 'gt handoff' to restart with this work\n") fmt.Printf(" Use 'gt handoff' to restart with this work\n")
fmt.Printf(" Use 'gt mol status' to see hook status\n") fmt.Printf(" Use 'gt mol status' to see hook status\n")
// Log hook event to activity feed
_ = events.LogFeed(events.TypeHook, agentID, events.HookPayload(beadID))
return nil return nil
} }

View File

@@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace" "github.com/steveyegge/gastown/internal/workspace"
@@ -384,6 +385,9 @@ func runMailSend(cmd *cobra.Command, args []string) error {
return fmt.Errorf("sending message: %w", err) return fmt.Errorf("sending message: %w", err)
} }
// Log mail event to activity feed
_ = events.LogFeed(events.TypeMail, from, events.MailPayload(to, mailSubject))
fmt.Printf("%s Message sent to %s\n", style.Bold.Render("✓"), to) fmt.Printf("%s Message sent to %s\n", style.Bold.Render("✓"), to)
fmt.Printf(" Subject: %s\n", mailSubject) fmt.Printf(" Subject: %s\n", mailSubject)
if len(msg.CC) > 0 { if len(msg.CC) > 0 {

View File

@@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/tmux"
@@ -330,6 +331,10 @@ func runSling(cmd *cobra.Command, args []string) error {
fmt.Printf("%s Work attached to hook (status=hooked)\n", style.Bold.Render("✓")) fmt.Printf("%s Work attached to hook (status=hooked)\n", style.Bold.Render("✓"))
// Log sling event to activity feed
actor := detectActor()
_ = events.LogFeed(events.TypeSling, actor, events.SlingPayload(beadID, targetAgent))
// Update agent bead's hook_bead field (ZFC: agents track their current work) // Update agent bead's hook_bead field (ZFC: agents track their current work)
updateAgentHookBead(targetAgent, beadID) updateAgentHookBead(targetAgent, beadID)
@@ -688,6 +693,12 @@ func runSlingFormula(args []string) error {
} }
fmt.Printf("%s Attached to hook (status=hooked)\n", style.Bold.Render("✓")) fmt.Printf("%s Attached to hook (status=hooked)\n", style.Bold.Render("✓"))
// Log sling event to activity feed (formula slinging)
actor := detectActor()
payload := events.SlingPayload(wispResult.RootID, targetAgent)
payload["formula"] = formulaName
_ = events.LogFeed(events.TypeSling, actor, payload)
// Update agent bead's hook_bead field (ZFC: agents track their current work) // Update agent bead's hook_bead field (ZFC: agents track their current work)
updateAgentHookBead(targetAgent, wispResult.RootID) updateAgentHookBead(targetAgent, wispResult.RootID)
@@ -768,6 +779,15 @@ func wakeRigAgents(rigName string) {
_ = t.NudgeSession(refinerySession, "Polecat dispatched - check for merge requests") _ = t.NudgeSession(refinerySession, "Polecat dispatched - check for merge requests")
} }
// detectActor returns the current agent's actor string for event logging.
func detectActor() string {
roleInfo, err := GetRole()
if err != nil {
return "unknown"
}
return roleInfo.ActorString()
}
// agentIDToBeadID converts an agent ID to its corresponding agent bead ID. // agentIDToBeadID converts an agent ID to its corresponding agent bead ID.
// Uses canonical naming: prefix-rig-role-name // Uses canonical naming: prefix-rig-role-name
func agentIDToBeadID(agentID string) string { func agentIDToBeadID(agentID string) string {

174
internal/events/events.go Normal file
View File

@@ -0,0 +1,174 @@
// Package events provides event logging for the gt activity feed.
//
// Events are written to ~/gt/.events.jsonl (raw audit log) and later
// curated by the feed daemon into ~/.feed.jsonl (user-facing).
package events
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/steveyegge/gastown/internal/workspace"
)
// Event represents an activity event in Gas Town.
type Event struct {
Timestamp string `json:"ts"`
Source string `json:"source"`
Type string `json:"type"`
Actor string `json:"actor"`
Payload map[string]interface{} `json:"payload,omitempty"`
Visibility string `json:"visibility"`
}
// Visibility levels for events.
const (
VisibilityAudit = "audit" // Only in raw events log
VisibilityFeed = "feed" // Appears in curated feed
VisibilityBoth = "both" // Both audit and feed
)
// Common event types for gt commands.
const (
TypeSling = "sling"
TypeHook = "hook"
TypeUnhook = "unhook"
TypeHandoff = "handoff"
TypeDone = "done"
TypeMail = "mail"
TypeSpawn = "spawn"
TypeKill = "kill"
TypeNudge = "nudge"
TypeBoot = "boot"
TypeHalt = "halt"
)
// EventsFile is the name of the raw events log.
const EventsFile = ".events.jsonl"
// mutex protects concurrent writes to the events file.
var mutex sync.Mutex
// Log writes an event to the events log.
// The event is appended to ~/gt/.events.jsonl.
// Returns nil if logging fails (events are best-effort).
func Log(eventType, actor string, payload map[string]interface{}, visibility string) error {
event := Event{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Source: "gt",
Type: eventType,
Actor: actor,
Payload: payload,
Visibility: visibility,
}
return write(event)
}
// LogFeed is a convenience wrapper for feed-visible events.
func LogFeed(eventType, actor string, payload map[string]interface{}) error {
return Log(eventType, actor, payload, VisibilityFeed)
}
// LogAudit is a convenience wrapper for audit-only events.
func LogAudit(eventType, actor string, payload map[string]interface{}) error {
return Log(eventType, actor, payload, VisibilityAudit)
}
// write appends an event to the events file.
func write(event Event) error {
// Find town root
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
// Silently ignore - we're not in a Gas Town workspace
return nil
}
eventsPath := filepath.Join(townRoot, EventsFile)
// Marshal event to JSON
data, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshaling event: %w", err)
}
data = append(data, '\n')
// Append to file with proper locking
mutex.Lock()
defer mutex.Unlock()
f, err := os.OpenFile(eventsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("opening events file: %w", err)
}
defer f.Close()
if _, err := f.Write(data); err != nil {
return fmt.Errorf("writing event: %w", err)
}
return nil
}
// Payload helpers for common event structures.
// SlingPayload creates a payload for sling events.
func SlingPayload(beadID, target string) map[string]interface{} {
return map[string]interface{}{
"bead": beadID,
"target": target,
}
}
// HookPayload creates a payload for hook events.
func HookPayload(beadID string) map[string]interface{} {
return map[string]interface{}{
"bead": beadID,
}
}
// HandoffPayload creates a payload for handoff events.
func HandoffPayload(subject string, toSession bool) map[string]interface{} {
p := map[string]interface{}{
"to_session": toSession,
}
if subject != "" {
p["subject"] = subject
}
return p
}
// DonePayload creates a payload for done events.
func DonePayload(beadID, branch string) map[string]interface{} {
return map[string]interface{}{
"bead": beadID,
"branch": branch,
}
}
// MailPayload creates a payload for mail events.
func MailPayload(to, subject string) map[string]interface{} {
return map[string]interface{}{
"to": to,
"subject": subject,
}
}
// SpawnPayload creates a payload for spawn events.
func SpawnPayload(rig, polecat string) map[string]interface{} {
return map[string]interface{}{
"rig": rig,
"polecat": polecat,
}
}
// BootPayload creates a payload for rig boot events.
func BootPayload(rig string, agents []string) map[string]interface{} {
return map[string]interface{}{
"rig": rig,
"agents": agents,
}
}