feat: agent completion events + plugin hooks

spawnAgent() now writes completion events to events.jsonl.
Plugin hooks check for completions on:
- SessionStart: report agents that finished since last session
- Notification(idle_prompt): check when Claude is idle

Event format: {"type":"agent_completed","agent":"...","workspace":"...","timestamp":"..."}

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 03:05:26 +00:00
parent 662217c6f5
commit 58749c87f8
5 changed files with 122 additions and 0 deletions

View file

@ -91,6 +91,28 @@
}
],
"description": "Restore recent session context on startup"
},
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-completions.sh"
}
],
"description": "Report agent completions from previous session"
}
],
"Notification": [
{
"matcher": "idle_prompt",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-completions.sh"
}
],
"description": "Check for agent completions when idle"
}
]
}

View file

@ -0,0 +1,47 @@
#!/bin/bash
# Check for agent completion events since last check.
# Called by plugin hooks to notify the orchestrating agent.
EVENTS_FILE="$HOME/Code/host-uk/core/.core/workspace/events.jsonl"
MARKER_FILE="$HOME/Code/host-uk/core/.core/workspace/.events-read"
if [ ! -f "$EVENTS_FILE" ]; then
exit 0
fi
# Get events newer than last read
if [ -f "$MARKER_FILE" ]; then
LAST_READ=$(cat "$MARKER_FILE")
NEW_EVENTS=$(awk -v ts="$LAST_READ" '$0 ~ "timestamp" && $0 > ts' "$EVENTS_FILE" 2>/dev/null)
else
NEW_EVENTS=$(cat "$EVENTS_FILE")
fi
if [ -z "$NEW_EVENTS" ]; then
exit 0
fi
# Update marker
date -u +%Y-%m-%dT%H:%M:%SZ > "$MARKER_FILE"
# Count completions
COUNT=$(echo "$NEW_EVENTS" | grep -c "agent_completed")
if [ "$COUNT" -gt 0 ]; then
# Format for hook output
AGENTS=$(echo "$NEW_EVENTS" | grep "agent_completed" | python3 -c "
import sys, json
events = [json.loads(l) for l in sys.stdin if l.strip()]
for e in events:
print(f\" {e.get('agent','?')}{e.get('workspace','?')}\")
" 2>/dev/null)
cat << EOF
{
"hookSpecificOutput": {
"hookEventName": "Notification",
"additionalContext": "$COUNT agent(s) completed:\n$AGENTS\n\nRun /core:status to review."
}
}
EOF
fi

Binary file not shown.

View file

@ -121,6 +121,9 @@ func (s *PrepSubsystem) spawnAgent(agent, prompt, wsDir, srcDir string) (int, st
writeStatus(wsDir, st)
}
// Emit completion event
emitCompletionEvent(agent, filepath.Base(wsDir))
// Ingest scan findings as issues
s.ingestFindings(wsDir)

50
pkg/agentic/events.go Normal file
View file

@ -0,0 +1,50 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"encoding/json"
"os"
"path/filepath"
"time"
coreio "forge.lthn.ai/core/go-io"
)
// CompletionEvent is emitted when a dispatched agent finishes.
// Written to ~/.core/workspace/events.jsonl as append-only log.
type CompletionEvent struct {
Type string `json:"type"`
Agent string `json:"agent"`
Workspace string `json:"workspace"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
// emitCompletionEvent appends a completion event to the events log.
// The plugin's hook watches this file to notify the orchestrating agent.
func emitCompletionEvent(agent, workspace string) {
home, err := os.UserHomeDir()
if err != nil {
return
}
eventsFile := filepath.Join(home, "Code", "host-uk", "core", ".core", "workspace", "events.jsonl")
event := CompletionEvent{
Type: "agent_completed",
Agent: agent,
Workspace: workspace,
Status: "completed",
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
data, err := json.Marshal(event)
if err != nil {
return
}
// Append to events log
existing, _ := coreio.Local.Read(eventsFile)
coreio.Local.Write(eventsFile, existing+string(data)+"\n")
}