Add witness activity events to gt feed (gt-nfdyl)

Implement activity event emission for witness patrol operations:
- patrol_started: When witness begins patrol cycle
- polecat_checked: When witness checks a polecat
- polecat_nudged: When witness nudges a stuck polecat
- escalation_sent: When witness escalates to Mayor/Deacon
- patrol_complete: When patrol cycle finishes

Also adds refinery merge queue events for future use:
- merge_started, merge_complete, merge_failed, queue_processed

New command: `gt activity emit <event-type>` allows agents to emit
events from CLI. Events write to ~/gt/.events.jsonl for gt 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:47:48 -08:00
parent 65d4dbe222
commit 1bf2b54773
5 changed files with 398 additions and 1735 deletions

File diff suppressed because one or more lines are too long

198
internal/cmd/activity.go Normal file
View File

@@ -0,0 +1,198 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
// Activity emit command flags
var (
activityEventType string
activityActor string
activityRig string
activityPolecat string
activityTarget string
activityReason string
activityMessage string
activityStatus string
activityIssue string
activityTo string
activityCount int
)
var activityCmd = &cobra.Command{
Use: "activity",
GroupID: GroupDiag,
Short: "Emit and view activity events",
Long: `Emit and view activity events for the Gas Town activity feed.
Events are written to ~/gt/.events.jsonl and can be viewed with 'gt feed'.
Subcommands:
emit Emit an activity event`,
}
var activityEmitCmd = &cobra.Command{
Use: "emit <event-type>",
Short: "Emit an activity event",
Long: `Emit an activity event to the Gas Town activity feed.
Supported event types for witness patrol:
patrol_started - When witness begins patrol cycle
polecat_checked - When witness checks a polecat
polecat_nudged - When witness nudges a stuck polecat
escalation_sent - When witness escalates to Mayor/Deacon
patrol_complete - When patrol cycle finishes
Supported event types for refinery:
merge_started - When refinery starts a merge
merge_complete - When merge succeeds
merge_failed - When merge fails
queue_processed - When refinery finishes processing queue
Common options:
--actor Who is emitting the event (e.g., gastown/witness)
--rig Which rig the event is about
--message Human-readable message
Examples:
gt activity emit patrol_started --rig gastown --count 3
gt activity emit polecat_checked --rig gastown --polecat Toast --status working --issue gt-xyz
gt activity emit polecat_nudged --rig gastown --polecat Toast --reason "idle for 10 minutes"
gt activity emit escalation_sent --rig gastown --target Toast --to mayor --reason "unresponsive"
gt activity emit patrol_complete --rig gastown --count 3 --message "All polecats healthy"`,
Args: cobra.ExactArgs(1),
RunE: runActivityEmit,
}
func init() {
// Emit command flags
activityEmitCmd.Flags().StringVar(&activityActor, "actor", "", "Actor emitting the event (auto-detected if not set)")
activityEmitCmd.Flags().StringVar(&activityRig, "rig", "", "Rig the event is about")
activityEmitCmd.Flags().StringVar(&activityPolecat, "polecat", "", "Polecat involved (for polecat_checked, polecat_nudged)")
activityEmitCmd.Flags().StringVar(&activityTarget, "target", "", "Target of the action (for escalation)")
activityEmitCmd.Flags().StringVar(&activityReason, "reason", "", "Reason for the action")
activityEmitCmd.Flags().StringVar(&activityMessage, "message", "", "Human-readable message")
activityEmitCmd.Flags().StringVar(&activityStatus, "status", "", "Status (for polecat_checked: working, idle, stuck)")
activityEmitCmd.Flags().StringVar(&activityIssue, "issue", "", "Issue ID (for polecat_checked)")
activityEmitCmd.Flags().StringVar(&activityTo, "to", "", "Escalation target (for escalation_sent: mayor, deacon)")
activityEmitCmd.Flags().IntVar(&activityCount, "count", 0, "Polecat count (for patrol events)")
activityCmd.AddCommand(activityEmitCmd)
rootCmd.AddCommand(activityCmd)
}
func runActivityEmit(cmd *cobra.Command, args []string) error {
eventType := args[0]
// Validate we're in a Gas Town workspace
_, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Auto-detect actor if not provided
actor := activityActor
if actor == "" {
actor = detectActor()
}
// Build payload based on event type
var payload map[string]interface{}
switch eventType {
case events.TypePatrolStarted, events.TypePatrolComplete:
if activityRig == "" {
return fmt.Errorf("--rig is required for %s events", eventType)
}
payload = events.PatrolPayload(activityRig, activityCount, activityMessage)
case events.TypePolecatChecked:
if activityRig == "" || activityPolecat == "" {
return fmt.Errorf("--rig and --polecat are required for polecat_checked events")
}
if activityStatus == "" {
activityStatus = "checked"
}
payload = events.PolecatCheckPayload(activityRig, activityPolecat, activityStatus, activityIssue)
case events.TypePolecatNudged:
if activityRig == "" || activityPolecat == "" {
return fmt.Errorf("--rig and --polecat are required for polecat_nudged events")
}
payload = events.NudgePayload(activityRig, activityPolecat, activityReason)
case events.TypeEscalationSent:
if activityRig == "" || activityTarget == "" || activityTo == "" {
return fmt.Errorf("--rig, --target, and --to are required for escalation_sent events")
}
payload = events.EscalationPayload(activityRig, activityTarget, activityTo, activityReason)
case events.TypeMergeStarted, events.TypeMerged, events.TypeMergeFailed, events.TypeMergeSkipped:
// Refinery events - flexible payload
payload = make(map[string]interface{})
if activityRig != "" {
payload["rig"] = activityRig
}
if activityMessage != "" {
payload["message"] = activityMessage
}
if activityTarget != "" {
payload["branch"] = activityTarget
}
if activityReason != "" {
payload["reason"] = activityReason
}
default:
// Generic event - use whatever flags are provided
payload = make(map[string]interface{})
if activityRig != "" {
payload["rig"] = activityRig
}
if activityPolecat != "" {
payload["polecat"] = activityPolecat
}
if activityTarget != "" {
payload["target"] = activityTarget
}
if activityReason != "" {
payload["reason"] = activityReason
}
if activityMessage != "" {
payload["message"] = activityMessage
}
if activityStatus != "" {
payload["status"] = activityStatus
}
if activityIssue != "" {
payload["issue"] = activityIssue
}
if activityTo != "" {
payload["to"] = activityTo
}
if activityCount > 0 {
payload["count"] = activityCount
}
}
// Emit the event
if err := events.LogFeed(eventType, actor, payload); err != nil {
return fmt.Errorf("emitting event: %w", err)
}
// Print confirmation
payloadJSON, _ := json.Marshal(payload)
fmt.Printf("%s Emitted %s event\n", style.Success.Render("✓"), style.Bold.Render(eventType))
fmt.Printf(" Actor: %s\n", actor)
fmt.Printf(" Payload: %s\n", string(payloadJSON))
return nil
}
// Note: detectActor is defined in sling.go and reused here

View File

@@ -201,6 +201,16 @@ func printEvent(e townlog.Event) {
typeStr = style.Warning.Render("[kill]")
case townlog.EventCallback:
typeStr = style.Bold.Render("[callback]")
case townlog.EventPatrolStarted:
typeStr = style.Bold.Render("[patrol_started]")
case townlog.EventPolecatChecked:
typeStr = style.Dim.Render("[polecat_checked]")
case townlog.EventPolecatNudged:
typeStr = style.Warning.Render("[polecat_nudged]")
case townlog.EventEscalationSent:
typeStr = style.Error.Render("[escalation_sent]")
case townlog.EventPatrolComplete:
typeStr = style.Success.Render("[patrol_complete]")
default:
typeStr = fmt.Sprintf("[%s]", e.Type)
}
@@ -252,6 +262,31 @@ func formatEventDetail(e townlog.Event) string {
return fmt.Sprintf("callback: %s", e.Context)
}
return "callback processed"
case townlog.EventPatrolStarted:
if e.Context != "" {
return fmt.Sprintf("started patrol (%s)", e.Context)
}
return "started patrol"
case townlog.EventPolecatChecked:
if e.Context != "" {
return fmt.Sprintf("checked %s", e.Context)
}
return "checked polecat"
case townlog.EventPolecatNudged:
if e.Context != "" {
return fmt.Sprintf("nudged (%s)", e.Context)
}
return "nudged polecat"
case townlog.EventEscalationSent:
if e.Context != "" {
return fmt.Sprintf("escalated (%s)", e.Context)
}
return "escalated"
case townlog.EventPatrolComplete:
if e.Context != "" {
return fmt.Sprintf("patrol complete (%s)", e.Context)
}
return "patrol complete"
default:
if e.Context != "" {
return fmt.Sprintf("%s (%s)", e.Type, e.Context)

View File

@@ -46,6 +46,13 @@ const (
TypeBoot = "boot"
TypeHalt = "halt"
// Witness patrol events
TypePatrolStarted = "patrol_started"
TypePolecatChecked = "polecat_checked"
TypePolecatNudged = "polecat_nudged"
TypeEscalationSent = "escalation_sent"
TypePatrolComplete = "patrol_complete"
// Merge queue events (emitted by refinery)
TypeMergeStarted = "merge_started"
TypeMerged = "merged"
@@ -195,3 +202,47 @@ func MergePayload(mrID, worker, branch, reason string) map[string]interface{} {
}
return p
}
// PatrolPayload creates a payload for patrol start/complete events.
func PatrolPayload(rig string, polecatCount int, message string) map[string]interface{} {
p := map[string]interface{}{
"rig": rig,
"polecat_count": polecatCount,
}
if message != "" {
p["message"] = message
}
return p
}
// PolecatCheckPayload creates a payload for polecat check events.
func PolecatCheckPayload(rig, polecat, status, issue string) map[string]interface{} {
p := map[string]interface{}{
"rig": rig,
"polecat": polecat,
"status": status,
}
if issue != "" {
p["issue"] = issue
}
return p
}
// NudgePayload creates a payload for nudge events.
func NudgePayload(rig, target, reason string) map[string]interface{} {
return map[string]interface{}{
"rig": rig,
"target": target,
"reason": reason,
}
}
// EscalationPayload creates a payload for escalation events.
func EscalationPayload(rig, target, to, reason string) map[string]interface{} {
return map[string]interface{}{
"rig": rig,
"target": target,
"to": to,
"reason": reason,
}
}

View File

@@ -29,6 +29,13 @@ const (
EventKill EventType = "kill"
// EventCallback indicates a callback was processed during patrol.
EventCallback EventType = "callback"
// Witness patrol events
EventPatrolStarted EventType = "patrol_started"
EventPolecatChecked EventType = "polecat_checked"
EventPolecatNudged EventType = "polecat_nudged"
EventEscalationSent EventType = "escalation_sent"
EventPatrolComplete EventType = "patrol_complete"
)
// Event represents a single agent lifecycle event.
@@ -151,6 +158,36 @@ func formatLogLine(e Event) string {
} else {
detail = "callback processed"
}
case EventPatrolStarted:
if e.Context != "" {
detail = fmt.Sprintf("started patrol (%s)", e.Context)
} else {
detail = "started patrol"
}
case EventPolecatChecked:
if e.Context != "" {
detail = fmt.Sprintf("checked polecat %s", e.Context)
} else {
detail = "checked polecat"
}
case EventPolecatNudged:
if e.Context != "" {
detail = fmt.Sprintf("nudged polecat (%s)", e.Context)
} else {
detail = "nudged polecat"
}
case EventEscalationSent:
if e.Context != "" {
detail = fmt.Sprintf("escalated (%s)", e.Context)
} else {
detail = "escalated"
}
case EventPatrolComplete:
if e.Context != "" {
detail = fmt.Sprintf("patrol complete (%s)", e.Context)
} else {
detail = "patrol complete"
}
default:
detail = string(e.Type)
if e.Context != "" {