agent/pkg/monitor/sync.go
Snider 90b03191b2 feat(agent): v0.2.0 — HTTP daemon, remote dispatch, review queue, verify+merge
Major additions:
- core-agent serve: persistent HTTP daemon with PID file, health check, registry
- agentic_dispatch_remote: dispatch tasks to remote agents (Charon) over MCP HTTP
- agentic_status_remote: check remote agent workspace status
- agentic_mirror: sync Forge repos to GitHub mirrors with file count limits
- agentic_review_queue: CodeRabbit/Codex review queue with rate-limit awareness
- verify.go: auto-verify (run tests) + auto-merge + retry with rebase + needs-review label
- monitor sync: checkin API integration for cross-agent repo sync
- PostToolUse inbox notification hook (check-notify.sh)

Dispatch improvements:
- --dangerously-skip-permissions (CLI flag changed)
- proc.CloseStdin() after spawn (Claude CLI stdin pipe fix)
- GOWORK=off in agent env and verify
- Exit code / BLOCKED.md / failure detection
- Monitor poke for instant notifications

New agent types:
- coderabbit: CodeRabbit CLI review (--plain --base)
- codex:review: OpenAI Codex review mode

Integrations:
- CODEX.md: OpenAI Codex conventions file
- Gemini extension: points at core-agent MCP (not Node server)
- Codex config: core-agent MCP server added
- GitHub webhook handler + CodeRabbit KPI tables (PHP)
- Forgejo provider for uptelligence webhooks
- Agent checkin endpoint for repo sync

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 17:45:04 +00:00

136 lines
3.2 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package monitor
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// CheckinResponse is what the API returns for an agent checkin.
type CheckinResponse struct {
// Repos that have new commits since the agent's last checkin.
Changed []ChangedRepo `json:"changed,omitempty"`
// Server timestamp — use as "since" on next checkin.
Timestamp int64 `json:"timestamp"`
}
// ChangedRepo is a repo that has new commits.
type ChangedRepo struct {
Repo string `json:"repo"`
Branch string `json:"branch"`
SHA string `json:"sha"`
}
// syncRepos calls the checkin API and pulls any repos that changed.
// Returns a human-readable message if repos were updated, empty string otherwise.
func (m *Subsystem) syncRepos() string {
apiURL := os.Getenv("CORE_API_URL")
if apiURL == "" {
apiURL = "https://api.lthn.sh"
}
agentName := agentName()
url := fmt.Sprintf("%s/v1/agent/checkin?agent=%s&since=%d", apiURL, agentName, m.lastSyncTimestamp)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}
// Use brain key for auth
brainKey := os.Getenv("CORE_BRAIN_KEY")
if brainKey == "" {
home, _ := os.UserHomeDir()
if data, err := os.ReadFile(filepath.Join(home, ".claude", "brain.key")); err == nil {
brainKey = strings.TrimSpace(string(data))
}
}
if brainKey != "" {
req.Header.Set("Authorization", "Bearer "+brainKey)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ""
}
var checkin CheckinResponse
if json.NewDecoder(resp.Body).Decode(&checkin) != nil {
return ""
}
// Update timestamp for next checkin
m.mu.Lock()
m.lastSyncTimestamp = checkin.Timestamp
m.mu.Unlock()
if len(checkin.Changed) == 0 {
return ""
}
// Pull changed repos
basePath := os.Getenv("CODE_PATH")
if basePath == "" {
home, _ := os.UserHomeDir()
basePath = filepath.Join(home, "Code", "core")
}
var pulled []string
for _, repo := range checkin.Changed {
repoDir := filepath.Join(basePath, repo.Repo)
if _, err := os.Stat(repoDir); err != nil {
continue
}
// Check if we're already on main and clean
branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
branchCmd.Dir = repoDir
branch, err := branchCmd.Output()
if err != nil || strings.TrimSpace(string(branch)) != "main" {
continue // Don't pull if not on main
}
statusCmd := exec.Command("git", "status", "--porcelain")
statusCmd.Dir = repoDir
status, _ := statusCmd.Output()
if len(strings.TrimSpace(string(status))) > 0 {
continue // Don't pull if dirty
}
// Fast-forward pull
pullCmd := exec.Command("git", "pull", "--ff-only", "origin", "main")
pullCmd.Dir = repoDir
if pullCmd.Run() == nil {
pulled = append(pulled, repo.Repo)
}
}
if len(pulled) == 0 {
return ""
}
return fmt.Sprintf("Synced %d repo(s): %s", len(pulled), strings.Join(pulled, ", "))
}
// lastSyncTimestamp is stored on the subsystem — add it via the check cycle.
// Initialised to "now" on first run so we don't pull everything on startup.
func (m *Subsystem) initSyncTimestamp() {
m.mu.Lock()
if m.lastSyncTimestamp == 0 {
m.lastSyncTimestamp = time.Now().Unix()
}
m.mu.Unlock()
}