diff --git a/CLAUDE.md b/CLAUDE.md index 6816262..33754d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,163 +1,138 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code when working with code in this repository. + +## Session Context + +Running on **Claude Max20 plan** with **1M context window** (Opus 4.6). ## Overview -**core-agent** is a polyglot monorepo (Go + PHP) for AI agent orchestration. The Go side handles agent-side execution, CLI commands, and autonomous agent loops. The PHP side (Laravel package `lthn/agent`) provides the backend API, persistent storage, multi-provider AI services, and admin panel. They communicate via REST API. +**core-agent** is the AI agent orchestration platform for the Core ecosystem. Single Go binary (`core-agent`) that runs as an MCP server — either via stdio (Claude Code integration) or HTTP daemon (cross-agent communication). -The repo also contains Claude Code plugins (5), Codex plugins (13), a Gemini CLI extension, and two MCP servers. +**Module:** `forge.lthn.ai/core/agent` -## Core CLI — Always Use It - -**Never use raw `go`, `php`, or `composer` commands.** The `core` CLI wraps both toolchains and is enforced by PreToolUse hooks that will block violations. - -| Instead of... | Use... | -|---------------|--------| -| `go test` | `core go test` | -| `go build` | `core build` | -| `go fmt` | `core go fmt` | -| `go vet` | `core go vet` | -| `golangci-lint` | `core go lint` | -| `composer test` / `./vendor/bin/pest` | `core php test` | -| `./vendor/bin/pint` / `composer lint` | `core php fmt` | -| `./vendor/bin/phpstan` | `core php stan` | -| `php artisan serve` | `core php dev` | - -## Build & Test Commands +## Build & Test ```bash -# Go -core go test # Run all Go tests -core go test --run TestMemoryRegistry_Register_Good # Run single test -core go qa # Full QA: fmt + vet + lint + test -core go qa full # QA + race detector + vuln scan -core go cov # Test coverage -core build # Verify Go packages compile +go build ./... # Build all packages +go build ./cmd/core-agent/ # Build the binary +go test ./... -count=1 -timeout 60s # Run tests +go vet ./... # Vet +go install ./cmd/core-agent/ # Install to $GOPATH/bin +``` -# PHP -core php test # Run Pest suite -core php test --filter=AgenticManagerTest # Run specific test file -core php fmt # Format (Laravel Pint) -core php stan # Static analysis (PHPStan) -core php qa # Full PHP QA pipeline - -# MCP servers (standalone builds) -cd cmd/mcp && go build -o agent-mcp . # Stdio MCP server -cd google/mcp && go build -o google-mcp . # HTTP MCP server (port 8080) - -# Workspace -make setup # Full bootstrap (deps + core + clone repos) -core dev health # Status across repos +Cross-compile for Charon (Linux): +```bash +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o core-agent-linux ./cmd/core-agent/ ``` ## Architecture ``` - Forgejo - | - [ForgejoSource polls] - | - v -+-- Go: jobrunner Poller --+ +-- PHP: Laravel Backend --+ -| ForgejoSource | | AgentApiController | -| DispatchHandler ---------|----->| /v1/plans | -| CompletionHandler | | /v1/sessions | -| ResolveThreadsHandler | | /v1/plans/*/phases | -+--------------------------+ +-------------+------------+ - | - [Eloquent models] - AgentPlan, AgentPhase, - AgentSession, BrainMemory +cmd/core-agent/main.go Entry point (mcp + serve commands) +pkg/agentic/ MCP tools — dispatch, verify, remote, mirror, review queue +pkg/brain/ OpenBrain — recall, remember, messaging +pkg/monitor/ Background monitoring + repo sync +pkg/prompts/ Embedded templates + personas (go:embed) ``` -### Go Packages (`pkg/`) +### Binary Modes -- **`lifecycle/`** — Core domain layer. Task, AgentInfo, Plan, Phase, Session types. Agent registry (Memory/SQLite/Redis backends), task router (capability matching + load scoring), allowance system (quota enforcement), dispatcher (orchestrates dispatch with exponential backoff), event system, brain (vector store), context (git integration). -- **`loop/`** — Autonomous agent reasoning engine. Prompt-parse-execute cycle against any `inference.TextModel` with tool calling and streaming. -- **`orchestrator/`** — Clotho protocol for dual-run verification and agent orchestration. -- **`jobrunner/`** — Poll-dispatch engine for agent-side work execution. Polls Forgejo for work items, executes phases, reports results. +- `core-agent mcp` — stdio MCP server for Claude Code +- `core-agent serve` — HTTP daemon (Charon, CI, cross-agent). PID file, health check, registry. -### Go Commands (`cmd/`) +### MCP Tools (33) -- **`tasks/`** — `core ai tasks`, `core ai task [id]` — task management -- **`agent/`** — `core ai agent` — agent machine management (add, list, status, fleet) -- **`dispatch/`** — `core ai dispatch` — work queue processor (watch, run) -- **`workspace/`** — `core workspace task`, `core workspace agent` — git worktree isolation -- **`mcp/`** — Standalone stdio MCP server exposing `marketplace_list`, `marketplace_plugin_info`, `core_cli`, `ethics_check` +| Category | Tools | +|----------|-------| +| Dispatch | `agentic_dispatch`, `agentic_dispatch_remote`, `agentic_status`, `agentic_status_remote` | +| Workspace | `agentic_prep_workspace`, `agentic_resume`, `agentic_watch` | +| PR/Review | `agentic_create_pr`, `agentic_list_prs`, `agentic_create_epic`, `agentic_review_queue` | +| Mirror | `agentic_mirror` (Forge → GitHub sync) | +| Scan | `agentic_scan` (Forge issues) | +| Brain | `brain_recall`, `brain_remember`, `brain_forget` | +| Messaging | `agent_send`, `agent_inbox`, `agent_conversation` | +| Plans | `agentic_plan_create`, `agentic_plan_read`, `agentic_plan_update`, `agentic_plan_delete`, `agentic_plan_list` | +| Files | `file_read`, `file_write`, `file_edit`, `file_delete`, `file_rename`, `file_exists`, `dir_list`, `dir_create` | +| Language | `lang_detect`, `lang_list` | -### PHP (`src/php/`) +### Agent Types -- **Namespace**: `Core\Mod\Agentic\` (service provider: `Boot`) -- **Models/** — 19 Eloquent models (AgentPlan, AgentPhase, AgentSession, BrainMemory, Task, Prompt, etc.) -- **Services/** — AgenticManager (multi-provider: Claude/Gemini/OpenAI), BrainService (Ollama+Qdrant), ForgejoService, AI services with stream parsing and retry traits -- **Controllers/** — AgentApiController (REST endpoints) -- **Actions/** — Single-purpose action classes (Brain, Forge, Phase, Plan, Session, Task) -- **View/** — Livewire admin panel components (Dashboard, Plans, Sessions, ApiKeys, Templates, Playground, etc.) -- **Mcp/** — MCP tool implementations (Brain, Content, Phase, Plan, Session, State, Task, Template) -- **Migrations/** — 10 migrations (run automatically on boot) +| Agent | Command | Use | +|-------|---------|-----| +| `claude:opus` | Claude Code | Complex coding, architecture | +| `claude:sonnet` | Claude Code | Standard tasks | +| `claude:haiku` | Claude Code | Quick/cheap tasks, discovery | +| `gemini` | Gemini CLI | Fast batch ops | +| `codex` | Codex CLI | Autonomous coding | +| `codex:review` | Codex review | Deep security analysis | +| `coderabbit` | CodeRabbit CLI | Code quality review | -## Claude Code Plugins (`claude/`) +### Dispatch Flow -Five plugins installable individually or via marketplace: +``` +dispatch → agent works → closeout sequence (review → fix → simplify → re-review) + → commit → auto PR → inline tests → pass → auto-merge on Forge + → push to GitHub → CodeRabbit reviews → merge or dispatch fix agent +``` -| Plugin | Commands | -|--------|----------| -| **code** | `/code:remember`, `/code:yes`, `/code:qa` | -| **review** | `/review:review`, `/review:security`, `/review:pr`, `/review:pipeline` | -| **verify** | `/verify:verify`, `/verify:ready`, `/verify:tests` | -| **qa** | `/qa:qa`, `/qa:fix`, `/qa:check`, `/qa:lint` | -| **ci** | `/ci:ci`, `/ci:workflow`, `/ci:fix`, `/ci:run`, `/ci:status` | +### Personas (pkg/prompts/lib/personas/) -### Hooks (code plugin) +116 personas across 16 domains. Path = context, filename = lens. -**PreToolUse**: `prefer-core.sh` blocks destructive operations (`rm -rf`, `sed -i`, `xargs rm`, `find -exec rm`, `grep -l | ...`, `mv/cp *`) and raw go/php commands. `block-docs.sh` prevents random `.md` file creation. +``` +prompts.Persona("engineering/security-developer") # code-level security review +prompts.Persona("smm/security-secops") # social media incident response +prompts.Persona("devops/senior") # infrastructure architecture +``` -**PostToolUse**: Auto-formats Go (`gofmt`) and PHP (`pint`) after edits. Warns about debug statements (`dd()`, `dump()`, `fmt.Println()`). +### Templates (pkg/prompts/lib/templates/) -**PreCompact**: Saves session state. **SessionStart**: Restores session context. +Prompt templates for different task types: `coding`, `conventions`, `security`, `verify`, plus YAML plan templates (`bug-fix`, `code-review`, `new-feature`, `refactor`, etc.) -## Other Directories +## Key Patterns -- **`codex/`** — 13 Codex plugins mirroring Claude structure plus ethics, guardrails, perf, issue, coolify, awareness -- **`agents/`** — 13 specialist agent categories (design, engineering, marketing, product, testing, etc.) with example configs and system prompts -- **`google/gemini-cli/`** — Gemini CLI extension (TypeScript, `npm run build`) -- **`google/mcp/`** — HTTP MCP server exposing `core_go_test`, `core_dev_health`, `core_dev_commit` -- **`docs/`** — `architecture.md` (deep dive), `development.md` (comprehensive dev guide), `docs/plans/` (design documents) -- **`scripts/`** — Environment setup scripts (`install-core.sh`, `install-deps.sh`, `agent-runner.sh`, etc.) +### Shared Paths (pkg/agentic/paths.go) + +All paths use `CORE_WORKSPACE` env var, fallback `~/Code/.core`: +- `WorkspaceRoot()` — agent workspaces +- `CoreRoot()` — ecosystem config +- `PlansRoot()` — agent plans +- `AgentName()` — `AGENT_NAME` env or hostname detection +- `GitHubOrg()` — `GITHUB_ORG` env or "dAppCore" + +### Error Handling + +`coreerr.E("pkg.Method", "message", err)` from go-log. Always 3 args. NEVER `fmt.Errorf`. + +### File I/O + +`coreio.Local.Read/Write/EnsureDir` from go-io. `WriteMode(path, content, 0600)` for sensitive files. NEVER `os.ReadFile/WriteFile`. + +### HTTP Responses + +Always check `err != nil` BEFORE accessing `resp.StatusCode`. Split into two checks. + +## Plugin (claude/core/) + +The Claude Code plugin provides: +- **MCP server** via `mcp.json` (auto-registers core-agent) +- **Hooks** via `hooks.json` (PostToolUse inbox notifications, auto-format, debug warnings) +- **Agents**: `agent-task-code-review`, `agent-task-code-simplifier` +- **Commands**: dispatch, status, review, recall, remember, scan, etc. +- **Skills**: security review, architecture review, test analysis, etc. ## Testing Conventions -### Go - -Uses `testify/assert` and `testify/require`. Name tests with suffixes: - `_Good` — happy path - `_Bad` — expected error conditions - `_Ugly` — panics and edge cases - -Use `require` for preconditions (stops on failure), `assert` for verifications (reports all failures). - -### PHP - -Pest with Orchestra Testbench. Feature tests use `RefreshDatabase`. Helpers: `createWorkspace()`, `createApiKey($workspace, ...)`. +- Use `testify/assert` + `testify/require` ## Coding Standards -- **UK English**: colour, organisation, centre, licence, behaviour -- **Go**: standard `gofmt`, errors via `core.E("scope.Method", "what failed", err)` -- **PHP**: `declare(strict_types=1)`, full type hints, PSR-12 via Pint, Pest syntax for tests -- **Shell**: `#!/bin/bash`, JSON input via `jq`, output `{"decision": "approve"|"block", "message": "..."}` -- **Commits**: conventional — `type(scope): description` (e.g. `feat(lifecycle): add exponential backoff`) -- **Licence**: EUPL-1.2 CIC - -## Prerequisites - -| Tool | Version | Purpose | -|------|---------|---------| -| Go | 1.26+ | Go packages, CLI, MCP servers | -| PHP | 8.2+ | Laravel package, Pest tests | -| Composer | 2.x | PHP dependencies | -| `core` CLI | latest | Wraps Go/PHP toolchains (enforced by hooks) | -| `jq` | any | JSON parsing in shell hooks | - -Go module is `forge.lthn.ai/core/agent`, participates in a Go workspace (`go.work`) resolving all `forge.lthn.ai/core/*` dependencies locally. +- **UK English**: colour, organisation, centre, initialise +- **Commits**: `type(scope): description` with `Co-Authored-By: Virgil ` +- **Licence**: EUPL-1.2 +- **SPDX**: `// SPDX-License-Identifier: EUPL-1.2` on every file diff --git a/pkg/agentic/paths_test.go b/pkg/agentic/paths_test.go new file mode 100644 index 0000000..4735d5b --- /dev/null +++ b/pkg/agentic/paths_test.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCoreRoot_Good_EnvVar(t *testing.T) { + t.Setenv("CORE_WORKSPACE", "/tmp/test-core") + assert.Equal(t, "/tmp/test-core", CoreRoot()) +} + +func TestCoreRoot_Good_Fallback(t *testing.T) { + t.Setenv("CORE_WORKSPACE", "") + home, _ := os.UserHomeDir() + assert.Equal(t, home+"/Code/.core", CoreRoot()) +} + +func TestWorkspaceRoot_Good(t *testing.T) { + t.Setenv("CORE_WORKSPACE", "/tmp/test-core") + assert.Equal(t, "/tmp/test-core/workspace", WorkspaceRoot()) +} + +func TestPlansRoot_Good(t *testing.T) { + t.Setenv("CORE_WORKSPACE", "/tmp/test-core") + assert.Equal(t, "/tmp/test-core/plans", PlansRoot()) +} + +func TestAgentName_Good_EnvVar(t *testing.T) { + t.Setenv("AGENT_NAME", "clotho") + assert.Equal(t, "clotho", AgentName()) +} + +func TestAgentName_Good_Fallback(t *testing.T) { + t.Setenv("AGENT_NAME", "") + name := AgentName() + assert.True(t, name == "cladius" || name == "charon", "expected cladius or charon, got %s", name) +} + +func TestGitHubOrg_Good_EnvVar(t *testing.T) { + t.Setenv("GITHUB_ORG", "myorg") + assert.Equal(t, "myorg", GitHubOrg()) +} + +func TestGitHubOrg_Good_Fallback(t *testing.T) { + t.Setenv("GITHUB_ORG", "") + assert.Equal(t, "dAppCore", GitHubOrg()) +} + +func TestBaseAgent_Good(t *testing.T) { + assert.Equal(t, "claude", baseAgent("claude:opus")) + assert.Equal(t, "claude", baseAgent("claude:haiku")) + assert.Equal(t, "gemini", baseAgent("gemini:flash")) + assert.Equal(t, "codex", baseAgent("codex")) +} + +func TestExtractPRNumber_Good(t *testing.T) { + assert.Equal(t, 123, extractPRNumber("https://forge.lthn.ai/core/go-io/pulls/123")) + assert.Equal(t, 1, extractPRNumber("https://forge.lthn.ai/core/agent/pulls/1")) +} + +func TestExtractPRNumber_Bad_Empty(t *testing.T) { + assert.Equal(t, 0, extractPRNumber("")) + assert.Equal(t, 0, extractPRNumber("https://forge.lthn.ai/core/agent/pulls/")) +} + +func TestTruncate_Good(t *testing.T) { + assert.Equal(t, "hello", truncate("hello", 10)) + assert.Equal(t, "hel...", truncate("hello world", 3)) +} + +func TestCountFindings_Good(t *testing.T) { + assert.Equal(t, 0, countFindings("No findings")) + assert.Equal(t, 2, countFindings("- Issue one\n- Issue two\nSummary")) + assert.Equal(t, 1, countFindings("⚠ Warning here")) +} + +func TestParseRetryAfter_Good(t *testing.T) { + d := parseRetryAfter("please try after 4 minutes and 56 seconds") + assert.InDelta(t, 296.0, d.Seconds(), 1.0) +} + +func TestParseRetryAfter_Good_MinutesOnly(t *testing.T) { + d := parseRetryAfter("try after 5 minutes") + assert.InDelta(t, 300.0, d.Seconds(), 1.0) +} + +func TestParseRetryAfter_Bad_NoMatch(t *testing.T) { + d := parseRetryAfter("some random text") + assert.InDelta(t, 300.0, d.Seconds(), 1.0) // defaults to 5 min +} + +func TestResolveHost_Good(t *testing.T) { + assert.Equal(t, "10.69.69.165:9101", resolveHost("charon")) + assert.Equal(t, "127.0.0.1:9101", resolveHost("cladius")) + assert.Equal(t, "127.0.0.1:9101", resolveHost("local")) +} + +func TestResolveHost_Good_CustomPort(t *testing.T) { + assert.Equal(t, "192.168.1.1:9101", resolveHost("192.168.1.1")) + assert.Equal(t, "192.168.1.1:8080", resolveHost("192.168.1.1:8080")) +} + +func TestExtractJSONField_Good(t *testing.T) { + json := `[{"url":"https://github.com/dAppCore/go-io/pull/1"}]` + assert.Equal(t, "https://github.com/dAppCore/go-io/pull/1", extractJSONField(json, "url")) +} + +func TestExtractJSONField_Bad_Missing(t *testing.T) { + assert.Equal(t, "", extractJSONField(`{"name":"test"}`, "url")) + assert.Equal(t, "", extractJSONField("", "url")) +} + +func TestValidPlanStatus_Good(t *testing.T) { + assert.True(t, validPlanStatus("draft")) + assert.True(t, validPlanStatus("in_progress")) + assert.True(t, validPlanStatus("draft")) +} + +func TestValidPlanStatus_Bad(t *testing.T) { + assert.False(t, validPlanStatus("invalid")) + assert.False(t, validPlanStatus("")) +} + +func TestGeneratePlanID_Good(t *testing.T) { + id := generatePlanID("Fix the login bug in auth service") + assert.True(t, len(id) > 0) + assert.True(t, strings.Contains(id, "fix-the-login-bug")) +} diff --git a/pkg/prompts/lib/personas/support/support-responder.md b/pkg/prompts/lib/personas/support/responder.md similarity index 100% rename from pkg/prompts/lib/personas/support/support-responder.md rename to pkg/prompts/lib/personas/support/responder.md diff --git a/pkg/prompts/prompts_test.go b/pkg/prompts/prompts_test.go new file mode 100644 index 0000000..40f49e5 --- /dev/null +++ b/pkg/prompts/prompts_test.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package prompts + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemplate_Good_YAML(t *testing.T) { + content, err := Template("bug-fix") + require.NoError(t, err) + assert.Contains(t, content, "name:") +} + +func TestTemplate_Good_MD(t *testing.T) { + content, err := Template("prod-push-polish") + require.NoError(t, err) + assert.True(t, len(content) > 0) +} + +func TestTemplate_Bad_NotFound(t *testing.T) { + _, err := Template("nonexistent-template") + assert.Error(t, err) +} + +func TestPersona_Good(t *testing.T) { + content, err := Persona("engineering/security-developer") + require.NoError(t, err) + assert.Contains(t, content, "name:") + assert.Contains(t, content, "Security") +} + +func TestPersona_Good_SMM(t *testing.T) { + content, err := Persona("smm/security-developer") + require.NoError(t, err) + assert.Contains(t, content, "OAuth") +} + +func TestPersona_Bad_NotFound(t *testing.T) { + _, err := Persona("nonexistent/persona") + assert.Error(t, err) +} + +func TestListTemplates_Good(t *testing.T) { + templates := ListTemplates() + assert.True(t, len(templates) >= 10, "expected at least 10 templates, got %d", len(templates)) + assert.Contains(t, templates, "bug-fix") + assert.Contains(t, templates, "code-review") +} + +func TestListPersonas_Good(t *testing.T) { + personas := ListPersonas() + assert.True(t, len(personas) >= 90, "expected at least 90 personas, got %d", len(personas)) + + // Check cross-domain security-developer exists + hasEngSec := false + hasSMMSec := false + for _, p := range personas { + if p == "engineering/security-developer" { + hasEngSec = true + } + if p == "smm/security-developer" { + hasSMMSec = true + } + } + assert.True(t, hasEngSec, "engineering/security-developer not found") + assert.True(t, hasSMMSec, "smm/security-developer not found") +} + +func TestListPersonas_Good_NoPrefixDuplication(t *testing.T) { + for _, p := range ListPersonas() { + parts := strings.Split(p, "/") + if len(parts) == 2 { + domain := parts[0] + file := parts[1] + assert.False(t, strings.HasPrefix(file, domain+"-"), + "persona %q has redundant domain prefix in filename", p) + } + } +}