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:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
174
internal/events/events.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user