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:
1812
.beads/issues.jsonl
1812
.beads/issues.jsonl
File diff suppressed because one or more lines are too long
198
internal/cmd/activity.go
Normal file
198
internal/cmd/activity.go
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user