diff --git a/claude/core/hooks.json b/claude/core/hooks.json index cbc7c2f..56fe871 100644 --- a/claude/core/hooks.json +++ b/claude/core/hooks.json @@ -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" } ] } diff --git a/claude/core/scripts/check-completions.sh b/claude/core/scripts/check-completions.sh new file mode 100755 index 0000000..3b567df --- /dev/null +++ b/claude/core/scripts/check-completions.sh @@ -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 diff --git a/core-agent b/core-agent index b9bbf26..5f03da8 100755 Binary files a/core-agent and b/core-agent differ diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index ba29053..657ac0b 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -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) diff --git a/pkg/agentic/events.go b/pkg/agentic/events.go new file mode 100644 index 0000000..ac8d329 --- /dev/null +++ b/pkg/agentic/events.go @@ -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") +}