diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6062047 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,506 @@ +--- +title: Architecture +description: Internal architecture of core/agent — task lifecycle, dispatch pipeline, agent loop, orchestration, and the PHP backend. +--- + +# Architecture + +Core Agent spans two runtimes (Go and PHP) that collaborate through a REST API. The Go side handles agent-side execution, CLI commands, and the autonomous agent loop. The PHP side provides the backend API, persistent storage, multi-provider AI services, and the admin panel. + +``` + Forgejo + | + [ForgejoSource polls] + | + v + +-- jobrunner Poller --+ +-- PHP Backend --+ + | ForgejoSource | | AgentApiController| + | DispatchHandler ----|----->| /v1/plans | + | CompletionHandler | | /v1/sessions | + | ResolveThreadsHandler| | /v1/plans/*/phases| + +----------------------+ +---------+---------+ + | + [database models] + AgentPlan, AgentPhase, + AgentSession, BrainMemory +``` + + +## Go: Task Lifecycle (`pkg/lifecycle/`) + +The lifecycle package is the core domain layer. It defines the data types and orchestration logic for task management. + +### Key Types + +**Task** represents a unit of work: + +```go +type Task struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Priority TaskPriority `json:"priority"` // critical, high, medium, low + Status TaskStatus `json:"status"` // pending, in_progress, completed, blocked, failed + Labels []string `json:"labels,omitempty"` + Files []string `json:"files,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + MaxRetries int `json:"max_retries,omitempty"` + RetryCount int `json:"retry_count,omitempty"` + // ...timestamps, claimed_by, etc. +} +``` + +**AgentInfo** describes a registered agent: + +```go +type AgentInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Capabilities []string `json:"capabilities,omitempty"` + Status AgentStatus `json:"status"` // available, busy, offline + LastHeartbeat time.Time `json:"last_heartbeat"` + CurrentLoad int `json:"current_load"` + MaxLoad int `json:"max_load"` +} +``` + +### Agent Registry + +The `AgentRegistry` interface tracks agent availability with heartbeats and reaping: + +```go +type AgentRegistry interface { + Register(agent AgentInfo) error + Deregister(id string) error + Get(id string) (AgentInfo, error) + List() []AgentInfo + All() iter.Seq[AgentInfo] + Heartbeat(id string) error + Reap(ttl time.Duration) []string +} +``` + +Three backends are provided: +- `MemoryRegistry` -- in-process, mutex-guarded, copy-on-read +- `SQLiteRegistry` -- persistent, single-file database +- `RedisRegistry` -- distributed, suitable for multi-node deployments + +Backend selection is driven by `RegistryConfig`: + +```go +registry, err := NewAgentRegistryFromConfig(RegistryConfig{ + RegistryBackend: "sqlite", // "memory", "sqlite", or "redis" + RegistryPath: "/path/to/registry.db", +}) +``` + +### Task Router + +The `TaskRouter` interface selects agents for tasks. The `DefaultRouter` implements capability matching and load-based scoring: + +1. **Filter** -- only agents that are `Available` (or `Busy` with capacity) and possess all required capabilities (matched via task labels). +2. **Critical tasks** -- pick the least-loaded agent directly. +3. **Other tasks** -- score by availability ratio (`1.0 - currentLoad/maxLoad`) and pick the highest-scored agent. Ties are broken alphabetically for determinism. + +### Allowance System + +The allowance system enforces quota limits to prevent runaway costs. It operates at two levels: + +**Per-agent quotas** (`AgentAllowance`): +- Daily token limit +- Daily job limit +- Concurrent job limit +- Maximum job duration +- Model allowlist + +**Per-model quotas** (`ModelQuota`): +- Daily token budget (global across all agents) +- Hourly rate limit (reserved, not yet enforced) +- Cost ceiling (reserved, not yet enforced) + +The `AllowanceService` provides: +- `Check(agentID, model)` -- pre-dispatch gate that returns `QuotaCheckResult` +- `RecordUsage(report)` -- updates counters based on `QuotaEvent` (started/completed/failed/cancelled) + +Quota recovery: failed jobs return 50% of tokens; cancelled jobs return 100%. + +Three storage backends mirror the registry: `MemoryStore`, `SQLiteStore`, `RedisStore`. + +### Dispatcher + +The `Dispatcher` orchestrates the full dispatch cycle: + +``` +1. List available agents (AgentRegistry) +2. Route task to agent (TaskRouter) +3. Check allowance (AllowanceService) +4. Claim task via API (Client) +5. Record usage (AllowanceService) +6. Emit events (EventEmitter) +``` + +`DispatchLoop` polls for pending tasks at a configurable interval, sorts by priority (critical first, oldest first as tie-breaker), and dispatches each one. Failed dispatches are retried with exponential backoff (5s, 10s, 20s, ...). Tasks exceeding their retry limit are dead-lettered with `StatusFailed`. + +### Event System + +Lifecycle events are published through the `EventEmitter` interface: + +| Event | When | +|-------|------| +| `task_dispatched` | Task successfully routed and claimed | +| `task_claimed` | API claim succeeded | +| `dispatch_failed_no_agent` | No eligible agent available | +| `dispatch_failed_quota` | Agent quota exceeded | +| `task_dead_lettered` | Task exceeded retry limit | +| `quota_warning` | Agent at 80%+ usage | +| `quota_exceeded` | Agent over quota | +| `usage_recorded` | Usage counters updated | + +Two emitter implementations: +- `ChannelEmitter` -- buffered channel, drops events when full (non-blocking) +- `MultiEmitter` -- fans out to multiple emitters + +### API Client + +`Client` communicates with the PHP backend over HTTP: + +```go +client := NewClient("https://api.lthn.sh", "your-token") +client.AgentID = "cladius" + +tasks, _ := client.ListTasks(ctx, ListOptions{Status: StatusPending}) +task, _ := client.ClaimTask(ctx, taskID) +_ = client.CompleteTask(ctx, taskID, TaskResult{Success: true}) +``` + +Additional endpoints for plans, sessions, phases, and brain (OpenBrain) are available. + +### Context Gathering + +`BuildTaskContext` assembles rich context for AI consumption: + +1. Reads files explicitly mentioned in the task +2. Runs `git status` and `git log` +3. Searches for related code using keyword extraction + `git grep` +4. Formats everything into a markdown document via `FormatContext()` + +### Service (Core DI Integration) + +The `Service` struct integrates with the Core DI container. It registers task handlers for `TaskCommit` and `TaskPrompt` messages, executing Claude via subprocess: + +```go +core.New( + core.WithService(lifecycle.NewService(lifecycle.ServiceOptions{ + DefaultTools: []string{"Bash", "Read", "Glob", "Grep"}, + AllowEdit: false, + })), +) +``` + +### Embedded Prompts + +Prompt templates are embedded at compile time from `prompts/*.md` and accessed via `Prompt(name)`. + + +## Go: Agent Loop (`pkg/loop/`) + +The loop package implements an autonomous agent loop that drives any `inference.TextModel`: + +```go +engine := loop.New( + loop.WithModel(myTextModel), + loop.WithTools(myTools...), + loop.WithMaxTurns(10), +) + +result, err := engine.Run(ctx, "Fix the failing test in pkg/foo") +``` + +### How It Works + +1. Build a system prompt describing available tools +2. Send the user message to the model +3. Parse the response for `\`\`\`tool` fenced blocks +4. Execute matched tool handlers +5. Append tool results to the conversation history +6. Loop until the model responds without tool blocks, or `maxTurns` is reached + +### Tool Definition + +```go +loop.Tool{ + Name: "read_file", + Description: "Read a file from disk", + Parameters: map[string]any{"type": "object", ...}, + Handler: func(ctx context.Context, args map[string]any) (string, error) { + path := args["path"].(string) + return os.ReadFile(path) + }, +} +``` + +### Built-in Tool Adapters + +- `LoadMCPTools(svc)` -- converts go-ai MCP tools into loop tools +- `EaaSTools(baseURL)` -- wraps the EaaS scoring API (score, imprint, atlas similar) + + +## Go: Job Runner (`pkg/jobrunner/`) + +The jobrunner implements a poll-dispatch engine for CI/CD-style agent automation. + +### Core Interfaces + +```go +type JobSource interface { + Name() string + Poll(ctx context.Context) ([]*PipelineSignal, error) + Report(ctx context.Context, result *ActionResult) error +} + +type JobHandler interface { + Name() string + Match(signal *PipelineSignal) bool + Execute(ctx context.Context, signal *PipelineSignal) (*ActionResult, error) +} +``` + +### Poller + +The `Poller` ties sources and handlers together. On each cycle it: + +1. Polls all sources for `PipelineSignal` values +2. Finds the first matching handler for each signal +3. Executes the handler (or logs in dry-run mode) +4. Records results in the `Journal` (JSONL audit log) +5. Reports back to the source + +### Forgejo Source (`forgejo/`) + +Polls Forgejo for epic issues (issues labelled `epic`), parses their body for linked child issues, and checks each child for a linked PR. Produces signals for: + +- Children with PRs (includes PR state, check status, merge status, review threads) +- Children without PRs but with agent assignees (`NeedsCoding: true`) + +### Handlers (`handlers/`) + +| Handler | Matches | Action | +|---------|---------|--------| +| `DispatchHandler` | `NeedsCoding` + known agent assignee | Creates ticket JSON, transfers via SSH to agent queue | +| `CompletionHandler` | Agent completion signals | Updates Forgejo issue labels, ticks parent epic | +| `EnableAutoMergeHandler` | All checks passing, no unresolved threads | Enables auto-merge on the PR | +| `PublishDraftHandler` | Draft PRs with passing checks | Marks the PR as ready for review | +| `ResolveThreadsHandler` | PRs with unresolved threads | Resolves outdated review threads | +| `SendFixCommandHandler` | PRs with failing checks | Comments with fix instructions | +| `TickParentHandler` | Merged PRs | Checks off the child in the parent epic | + +### Journal + +The `Journal` writes date-partitioned JSONL files to `{baseDir}/{owner}/{repo}/{date}.jsonl`. Path components are sanitised to prevent traversal attacks. + + +## Go: Orchestrator (`pkg/orchestrator/`) + +### Clotho Protocol + +The orchestrator implements the "Clotho Protocol" for dual-run verification. When enabled, a task is executed twice with different models and the outputs are compared: + +```go +spinner := orchestrator.NewSpinner(clothoConfig, agents) +mode := spinner.DeterminePlan(signal, agentName) +// mode is either ModeStandard or ModeDual +``` + +Dual-run is triggered when: +- The global strategy is `clotho-verified` +- The agent has `dual_run: true` in its config +- The repository is deemed critical (name is "core" or contains "security") + +### Agent Configuration + +```yaml +agentci: + agents: + cladius: + host: user@192.168.1.100 + queue_dir: /home/claude/ai-work/queue + forgejo_user: virgil + model: sonnet + runner: claude # claude, codex, or gemini + dual_run: false + active: true + clotho: + strategy: direct # direct or clotho-verified + validation_threshold: 0.85 +``` + +### Security + +- `SanitizePath` -- validates filenames against `^[a-zA-Z0-9\-\_\.]+$` and rejects traversal +- `EscapeShellArg` -- single-quote wrapping for safe shell insertion +- `SecureSSHCommandContext` -- strict host key checking, batch mode, 10-second connect timeout +- `MaskToken` -- redacts tokens for safe logging + + +## Go: Dispatch (`cmd/dispatch/`) + +The dispatch command runs **on the agent machine** and processes work from the PHP API: + +### `core ai dispatch watch` + +1. Connects to the PHP agentic API (`/v1/health` ping) +2. Lists active plans (`/v1/plans?status=active`) +3. Finds the first workable phase (in-progress or pending with `can_start`) +4. Starts a session via the API +5. Clones/updates the repository +6. Builds a prompt from the phase description +7. Invokes the runner (`claude`, `codex`, or `gemini`) +8. Reports success/failure back to the API and Forgejo + +**Rate limiting**: if an agent exits in under 30 seconds (fast failure), the poller backs off exponentially (2x, 4x, 8x the base interval, capped at 8x). + +### `core ai dispatch run` + +Processes a single ticket from the local file queue (`~/ai-work/queue/ticket-*.json`). Uses file-based locking to prevent concurrent execution. + + +## Go: Workspace (`cmd/workspace/`) + +### Task Workspaces + +Each task gets an isolated workspace at `.core/workspace/p{epic}/i{issue}/` containing git worktrees: + +``` +.core/workspace/ + p42/ + i123/ + core-php/ # git worktree on branch issue/123 + core-tenant/ # git worktree on branch issue/123 + agents/ + claude-opus/implementor/ + memory.md + artifacts/ +``` + +Safety checks prevent removal of workspaces with uncommitted changes or unpushed branches. + +### Agent Context + +Agents get persistent directories within task workspaces. Each agent has a `memory.md` file that persists across invocations, allowing QA agents to accumulate findings and implementors to record decisions. + + +## Go: MCP Server (`cmd/mcp/`) + +A standalone MCP server (stdio transport via mcp-go) exposing four tools: + +| Tool | Purpose | +|------|---------| +| `marketplace_list` | Lists available Claude Code plugins from `marketplace.json` | +| `marketplace_plugin_info` | Returns metadata, commands, and skills for a plugin | +| `core_cli` | Runs approved `core` CLI commands (dev, go, php, build only) | +| `ethics_check` | Returns the Axioms of Life ethics modal and kernel | + + +## PHP: Backend API + +### Service Provider (`Boot.php`) + +The module registers via Laravel's event-driven lifecycle: + +| Event | Handler | Purpose | +|-------|---------|---------| +| `ApiRoutesRegistering` | `onApiRoutes` | REST API endpoints at `/v1/*` | +| `AdminPanelBooting` | `onAdminPanel` | Livewire admin components | +| `ConsoleBooting` | `onConsole` | Artisan commands | +| `McpToolsRegistering` | `onMcpTools` | Brain MCP tools | + +Scheduled commands: +- `agentic:plan-cleanup` -- daily plan retention +- `agentic:scan` -- every 5 minutes (Forgejo pipeline scan) +- `agentic:dispatch` -- every 2 minutes (agent dispatch) +- `agentic:pr-manage` -- every 5 minutes (PR lifecycle management) + +### REST API Routes + +All authenticated routes use `AgentApiAuth` middleware with Bearer tokens and scope-based permissions. + +**Plans** (`/v1/plans`): +- `GET /v1/plans` -- list plans (filterable by status) +- `GET /v1/plans/{slug}` -- get plan with phases +- `POST /v1/plans` -- create plan +- `PATCH /v1/plans/{slug}` -- update plan +- `DELETE /v1/plans/{slug}` -- archive plan + +**Phases** (`/v1/plans/{slug}/phases/{phase}`): +- `GET` -- get phase details +- `PATCH` -- update phase status +- `POST .../checkpoint` -- add checkpoint +- `PATCH .../tasks/{idx}` -- update task +- `POST .../tasks/{idx}/toggle` -- toggle task completion + +**Sessions** (`/v1/sessions`): +- `GET /v1/sessions` -- list sessions +- `GET /v1/sessions/{id}` -- get session +- `POST /v1/sessions` -- start session +- `POST /v1/sessions/{id}/end` -- end session +- `POST /v1/sessions/{id}/continue` -- continue session + +### Data Model + +**AgentPlan** -- a structured work plan with phases, multi-tenant via `BelongsToWorkspace`: +- Status: draft -> active -> completed/archived +- Phases: ordered list of `AgentPhase` records +- Sessions: linked `AgentSession` records +- State: key-value `WorkspaceState` records + +**AgentSession** -- tracks an agent's work session for handoff: +- Status: active -> paused -> completed/failed +- Work log: timestamped entries (info, warning, error, checkpoint, decision) +- Artifacts: files created/modified/deleted +- Handoff notes: summary, next steps, blockers, context for next agent +- Replay: `createReplaySession()` spawns a continuation session with inherited context + +**BrainMemory** -- persistent knowledge stored in both MariaDB and Qdrant: +- Types: fact, decision, pattern, context, procedure +- Semantic search via Ollama embeddings + Qdrant vector similarity +- Supersession: new memories can replace old ones (soft delete + vector removal) + +### AI Provider Management (`AgenticManager`) + +Three providers are registered at boot: + +| Provider | Service | Default Model | +|----------|---------|---------------| +| Claude | `ClaudeService` | `claude-sonnet-4-20250514` | +| Gemini | `GeminiService` | `gemini-2.0-flash` | +| OpenAI | `OpenAIService` | `gpt-4o-mini` | + +Each implements `AgenticProviderInterface`. Missing API keys are logged as warnings at boot time. + +### BrainService (OpenBrain) + +The `BrainService` provides semantic memory using Ollama for embeddings and Qdrant for vector storage: + +``` +remember() -> embed(content) -> DB::transaction { + BrainMemory::create() + qdrantUpsert() + if supersedes_id: soft-delete old + qdrantDelete() +} + +recall(query) -> embed(query) -> qdrantSearch() -> BrainMemory::whereIn(ids) +``` + +Default embedding model: `embeddinggemma` (768-dimensional vectors, cosine distance). + + +## Data Flow: End-to-End Dispatch + +1. **PHP** `agentic:scan` scans Forgejo for issues labelled `agent-ready` +2. **PHP** `agentic:dispatch` creates plans with phases from issues +3. **Go** `core ai dispatch watch` polls `GET /v1/plans?status=active` +4. **Go** finds first workable phase, starts a session via `POST /v1/sessions` +5. **Go** clones the repository, builds a prompt, invokes the runner +6. **Runner** (Claude/Codex/Gemini) makes changes, commits, pushes +7. **Go** reports phase status via `PATCH /v1/plans/{slug}/phases/{phase}` +8. **Go** ends the session via `POST /v1/sessions/{id}/end` +9. **Go** comments on the Forgejo issue with the result diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..88ab7ce --- /dev/null +++ b/docs/development.md @@ -0,0 +1,572 @@ +--- +title: Development Guide +description: How to build, test, and contribute to core/agent — covering Go packages, PHP tests, MCP servers, Claude Code plugins, and coding standards. +--- + +# Development Guide + +Core Agent is a polyglot repository. Go and PHP live side by side, each with their own toolchain. The `core` CLI wraps both and is the primary interface for all development tasks. + + +## Prerequisites + +| Tool | Version | Purpose | +|------|---------|---------| +| Go | 1.26+ | Go packages, CLI commands, MCP servers | +| PHP | 8.2+ | Laravel package, Pest tests | +| Composer | 2.x | PHP dependency management | +| `core` CLI | latest | Wraps Go and PHP toolchains; enforced by plugin hooks | +| `jq` | any | Used by shell hooks for JSON parsing | + +### Go Workspace + +The module is `forge.lthn.ai/core/agent`. It participates in a Go workspace (`go.work`) that resolves all `forge.lthn.ai/core/*` dependencies locally. After cloning, ensure the workspace file includes a `use` entry for this module: + +``` +use ./core/agent +``` + +Then run `go work sync` from the workspace root. + +### PHP Dependencies + +```bash +composer install +``` + +The Composer package is `lthn/agent`. It depends on `lthn/php` (the foundation framework) at runtime, and on `orchestra/testbench`, `pestphp/pest`, and `livewire/livewire` for development. + + +## Building + +### Go Packages + +There is no standalone binary produced by this module. The Go packages (`pkg/lifecycle/`, `pkg/loop/`, `pkg/orchestrator/`, `pkg/jobrunner/`) are libraries imported by the `core` CLI binary (built from `forge.lthn.ai/core/cli`). + +To verify the packages compile: + +```bash +core go build +``` + +### MCP Servers + +Two MCP servers live in this repository: + +**Stdio server** (`cmd/mcp/`) — a standalone binary using `mcp-go`: + +```bash +cd cmd/mcp && go build -o agent-mcp . +``` + +It exposes four tools (`marketplace_list`, `marketplace_plugin_info`, `core_cli`, `ethics_check`) and is invoked by Claude Code over stdio. + +**HTTP server** (`google/mcp/`) — a plain `net/http` server on port 8080: + +```bash +cd google/mcp && go build -o google-mcp . +./google-mcp +``` + +It exposes `core_go_test`, `core_dev_health`, and `core_dev_commit` as POST endpoints. + + +## Testing + +### Go Tests + +```bash +# Run all Go tests +core go test + +# Run a single test by name +core go test --run TestMemoryRegistry_Register_Good + +# Full QA pipeline (fmt + vet + lint + test) +core go qa + +# QA with race detector, vulnerability scan, and security checks +core go qa full + +# Generate and view test coverage +core go cov +core go cov --open +``` + +Tests use `testify/assert` and `testify/require`. The naming convention is: + +| Suffix | Meaning | +|--------|---------| +| `_Good` | Happy-path tests | +| `_Bad` | Expected error conditions | +| `_Ugly` | Panic and edge cases | + +The test suite is substantial: ~65 test files across the Go packages, covering lifecycle (registry, allowance, dispatcher, router, events, client, brain, context), jobrunner (poller, journal, handlers, Forgejo source), loop (engine, parsing, prompts, tools), and orchestrator (Clotho, config, security). + +### PHP Tests + +```bash +# Run the full Pest suite +composer test + +# Run a specific test file +./vendor/bin/pest --filter=AgenticManagerTest + +# Fix code style +composer lint +``` + +The PHP test suite uses Pest with Orchestra Testbench for package testing. Feature tests use `RefreshDatabase` for clean database state. The test configuration lives in `src/php/tests/Pest.php`: + +```php +uses(TestCase::class)->in('Feature', 'Unit', 'UseCase'); +uses(RefreshDatabase::class)->in('Feature'); +``` + +Helper functions for test setup: + +```php +// Create a workspace for testing +$workspace = createWorkspace(); + +// Create an API key for testing +$key = createApiKey($workspace, 'Test Key', ['plan:read'], 100); +``` + +The test suite includes: + +- **Unit tests** (`src/php/tests/Unit/`): ClaudeService, GeminiService, OpenAIService, AgenticManager, AgentToolRegistry, AgentDetection, stream parsing, retry logic +- **Feature tests** (`src/php/tests/Feature/`): AgentPlan, AgentPhase, AgentSession, AgentApiKey, ForgejoService, security, workspace state, plan retention, prompt versioning, content service, Forgejo actions, scan-for-work +- **Livewire tests** (`src/php/tests/Feature/Livewire/`): Dashboard, Plans, PlanDetail, Sessions, SessionDetail, ApiKeys, Templates, ToolAnalytics, ToolCalls, Playground, RequestLog +- **Use-case tests** (`src/php/tests/UseCase/`): AdminPanelBasic + + +## Formatting and Linting + +### Go + +```bash +# Format all Go files +core go fmt + +# Run the linter +core go lint + +# Run go vet +core go vet +``` + +### PHP + +```bash +# Fix code style (Laravel Pint, PSR-12) +composer lint + +# Format only changed files +./vendor/bin/pint --dirty +``` + +### Automatic Formatting + +The `code` plugin includes PostToolUse hooks that auto-format files after every edit: + +- **Go files**: `scripts/go-format.sh` runs `gofmt` on any edited `.go` file +- **PHP files**: `scripts/php-format.sh` runs `pint` on any edited `.php` file +- **Debug check**: `scripts/check-debug.sh` warns about `dd()`, `dump()`, `fmt.Println()`, and similar statements left in code + + +## Claude Code Plugins + +### Installing + +Install all five plugins at once: + +```bash +claude plugin add host-uk/core-agent +``` + +Or install individual plugins: + +```bash +claude plugin add host-uk/core-agent/claude/code +claude plugin add host-uk/core-agent/claude/review +claude plugin add host-uk/core-agent/claude/verify +claude plugin add host-uk/core-agent/claude/qa +claude plugin add host-uk/core-agent/claude/ci +``` + +### Plugin Architecture + +Each plugin lives in `claude//` and contains: + +``` +claude// +├── .claude-plugin/ +│ └── plugin.json # Plugin metadata (name, version, description) +├── hooks.json # Hook declarations (optional) +├── hooks/ # Hook scripts (optional) +├── scripts/ # Supporting scripts (optional) +├── commands/ # Slash command definitions (*.md files) +└── skills/ # Skill definitions (optional) +``` + +The marketplace registry at `.claude-plugin/marketplace.json` lists all five plugins with their source paths and versions. + +### Available Commands + +| Plugin | Command | Purpose | +|--------|---------|---------| +| code | `/code:remember ` | Save context that persists across compaction | +| code | `/code:yes ` | Auto-approve mode with commit requirement | +| code | `/code:qa` | Run QA pipeline | +| review | `/review:review [range]` | Code review on staged changes or commits | +| review | `/review:security` | Security-focused review | +| review | `/review:pr` | Pull request review | +| verify | `/verify:verify [--quick\|--full]` | Verify work is complete | +| verify | `/verify:ready` | Check if work is ready to ship | +| verify | `/verify:tests` | Verify test coverage | +| qa | `/qa:qa` | Iterative QA fix loop (runs until all checks pass) | +| qa | `/qa:fix ` | Fix a specific QA issue | +| qa | `/qa:check` | Run checks without fixing | +| qa | `/qa:lint` | Lint check only | +| ci | `/ci:ci [status\|run\|logs\|fix]` | CI status and management | +| ci | `/ci:workflow ` | Generate GitHub Actions workflows | +| ci | `/ci:fix` | Fix CI failures | +| ci | `/ci:run` | Trigger a CI run | +| ci | `/ci:status` | Show CI status | + +### Hook System + +The `code` plugin defines hooks in `claude/code/hooks.json` that fire at different points in the Claude Code lifecycle: + +**PreToolUse** (before a tool runs): +- `prefer-core.sh` on `Bash` tool: blocks destructive commands (`rm -rf`, `sed -i`, `xargs rm`, `find -exec rm`, `grep -l | ...`) and enforces `core` CLI usage (blocks raw `go test`, `go build`, `composer test`, `golangci-lint`) +- `block-docs.sh` on `Write` tool: prevents creation of random `.md` files + +**PostToolUse** (after a tool completes): +- `go-format.sh` on `Edit` for `.go` files: auto-runs `gofmt` +- `php-format.sh` on `Edit` for `.php` files: auto-runs `pint` +- `check-debug.sh` on `Edit`: warns about debug statements +- `post-commit-check.sh` on `Bash` for `git commit`: warns about uncommitted work + +**PreCompact** (before context compaction): +- `pre-compact.sh`: saves session state to prevent amnesia + +**SessionStart** (when a session begins): +- `session-start.sh`: restores recent session context + +### Testing Hooks Locally + +```bash +echo '{"tool_input": {"command": "rm -rf /"}}' | bash ./claude/code/hooks/prefer-core.sh +# Output: {"decision": "block", "message": "BLOCKED: Recursive delete is not allowed..."} + +echo '{"tool_input": {"command": "core go test"}}' | bash ./claude/code/hooks/prefer-core.sh +# Output: {"decision": "approve"} +``` + +Hook scripts read JSON on stdin and output a JSON object with `decision` (`approve` or `block`) and an optional `message`. + +### Adding a New Plugin + +1. Create the directory structure: + ``` + claude// + ├── .claude-plugin/ + │ └── plugin.json + └── commands/ + └── .md + ``` + +2. Write `plugin.json`: + ```json + { + "name": "", + "description": "What this plugin does", + "version": "0.1.0", + "author": { + "name": "Host UK", + "email": "hello@host.uk.com" + }, + "license": "EUPL-1.2" + } + ``` + +3. Add command files as Markdown (`.md`) in `commands/`. The filename becomes the command name. + +4. Register the plugin in `.claude-plugin/marketplace.json`: + ```json + { + "name": "", + "source": "./claude/", + "description": "Short description", + "version": "0.1.0" + } + ``` + +### Codex Plugins + +The `codex/` directory mirrors the Claude plugin structure for OpenAI Codex. It contains additional plugins beyond the Claude five: `ethics`, `guardrails`, `perf`, `issue`, `coolify`, `awareness`, `api`, and `collect`. Each follows the same pattern with `.codex-plugin/plugin.json` and optional hooks, commands, and skills. + + +## Adding Go Functionality + +### New Package + +Create a directory under `pkg/`. Follow the existing convention: + +``` +pkg// +├── types.go # Public types and interfaces +├── .go +└── _test.go +``` + +Import the package from other modules as `forge.lthn.ai/core/agent/pkg/`. + +### New CLI Command + +Commands live in `cmd/`. Each command directory registers itself into the `core` binary via the CLI framework: + +```go +package mycmd + +import ( + "forge.lthn.ai/core/cli" + "github.com/spf13/cobra" +) + +func AddCommands(parent *cobra.Command) { + parent.AddCommand(&cobra.Command{ + Use: "mycommand", + Short: "What it does", + RunE: func(cmd *cobra.Command, args []string) error { + // implementation + return nil + }, + }) +} +``` + +Registration into the `core` binary happens in the CLI module, not here. This module exports the `AddCommands` function and the CLI module calls it. + +### New MCP Tool (stdio server) + +Tools are added in `cmd/mcp/server.go`. Each tool needs: + +1. A `mcp.Tool` definition with name, description, and input schema +2. A handler function with signature `func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)` +3. Registration via `s.AddTool(tool, handler)` in the `newServer()` function + +### New MCP Tool (HTTP server) + +Tools for the Google MCP server are plain HTTP handlers in `google/mcp/main.go`. Add a handler function and register it with `http.HandleFunc`. + + +## Adding PHP Functionality + +### New Model + +Create in `src/php/Models/`. All models use the `Core\Mod\Agentic\Models` namespace: + +```php +command(Console\Commands\MyCommand::class); + // ...existing commands... +} +``` + +### New Livewire Component + +Admin panel components go in `src/php/View/Modal/Admin/`. Blade views go in `src/php/View/Blade/admin/`. Register the component in `Boot::onAdminPanel()`: + +```php +$event->livewire('agentic.admin.my-component', View\Modal\Admin\MyComponent::class); +``` + + +## Writing Tests + +### Go Test Conventions + +Use the `_Good` / `_Bad` / `_Ugly` suffix pattern: + +```go +func TestMyFunction_Good(t *testing.T) { + // Happy path — expected input produces expected output + result := MyFunction("valid") + assert.Equal(t, "expected", result) +} + +func TestMyFunction_Bad_EmptyInput(t *testing.T) { + // Expected failure — invalid input returns error + _, err := MyFunction("") + require.Error(t, err) + assert.Contains(t, err.Error(), "input required") +} + +func TestMyFunction_Ugly_NilPointer(t *testing.T) { + // Edge case — nil receiver, concurrent access, etc. + assert.Panics(t, func() { MyFunction(nil) }) +} +``` + +Always use `require` for preconditions (stops test immediately on failure) and `assert` for verifications (continues to report all failures). + +### PHP Test Conventions + +Use Pest syntax: + +```php +it('creates a plan with phases', function () { + $workspace = createWorkspace(); + $plan = AgentPlan::factory()->create(['workspace_id' => $workspace->id]); + + expect($plan)->toBeInstanceOf(AgentPlan::class); + expect($plan->workspace_id)->toBe($workspace->id); +}); + +it('rejects invalid input', function () { + $this->postJson('/v1/plans', []) + ->assertStatus(422); +}); +``` + +Feature tests get `RefreshDatabase` automatically. Unit tests should not touch the database. + + +## Coding Standards + +### Language + +Use **UK English** throughout: colour, organisation, centre, licence, behaviour, catalogue. Never American spellings. + +### PHP + +- `declare(strict_types=1);` in every file +- All parameters and return types must have type hints +- PSR-12 formatting via Laravel Pint +- Pest syntax for tests (not PHPUnit) + +### Go + +- Standard `gofmt` formatting +- Errors via `core.E("scope.Method", "what failed", err)` pattern where the core framework is used +- Exported types get doc comments +- Test files co-locate with their source files + +### Shell Scripts + +- Shebang: `#!/bin/bash` +- Read JSON input with `jq` +- Hook output: JSON with `decision` and optional `message` fields + +### Commits + +Use conventional commits: `type(scope): description` + +``` +feat(lifecycle): add exponential backoff to dispatcher +fix(brain): handle empty embedding vectors +docs(architecture): update data flow diagram +test(registry): add concurrent access tests +``` + + +## Project Configuration + +### Go Client Config (`~/.core/agentic.yaml`) + +```yaml +base_url: https://api.lthn.sh +token: your-api-token +default_project: my-project +agent_id: cladius +``` + +Environment variables `AGENTIC_BASE_URL`, `AGENTIC_TOKEN`, `AGENTIC_PROJECT`, and `AGENTIC_AGENT_ID` override the YAML values. + +### PHP Config + +The service provider merges two config files on boot: + +- `src/php/config.php` into the `mcp` config key (brain database, Ollama URL, Qdrant URL) +- `src/php/agentic.php` into the `agentic` config key (Forgejo URL, token, general settings) + +Environment variables: + +| Variable | Purpose | +|----------|---------| +| `ANTHROPIC_API_KEY` | Claude API key | +| `GOOGLE_AI_API_KEY` | Gemini API key | +| `OPENAI_API_KEY` | OpenAI API key | +| `BRAIN_DB_HOST` | Dedicated brain database host | +| `BRAIN_DB_DATABASE` | Dedicated brain database name | + +### Workspace Config (`.core/workspace.yaml`) + +Controls `core` CLI behaviour when running from the repository root: + +```yaml +version: 1 +active: core-php +packages_dir: ./packages +settings: + suggest_core_commands: true + show_active_in_prompt: true +``` + + +## Licence + +EUPL-1.2 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9ccf57e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,186 @@ +--- +title: Core Agent +description: AI agent orchestration, Claude Code plugins, and lifecycle management for the Host UK platform — a polyglot Go + PHP repository. +--- + +# Core Agent + +Core Agent (`forge.lthn.ai/core/agent`) is a polyglot repository containing **Go libraries**, **CLI commands**, **MCP servers**, and a **Laravel PHP package** that together provide AI agent orchestration for the Host UK platform. + +It answers three questions: + +1. **How do agents get work?** -- The lifecycle package manages tasks, dispatching, and quota enforcement. The PHP side exposes a REST API for plans, sessions, and phases. +2. **How do agents run?** -- The dispatch and jobrunner packages poll for work, clone repositories, invoke Claude/Codex/Gemini, and report results back to Forgejo. +3. **How do agents collaborate?** -- Sessions, plans, and the OpenBrain vector store enable multi-agent handoff, replay, and persistent memory. + + +## Quick Start + +### Go (library / CLI commands) + +The Go module is `forge.lthn.ai/core/agent`. It requires Go 1.26+. + +```bash +# Run tests +core go test + +# Full QA pipeline +core go qa +``` + +Key CLI commands (registered into the `core` binary via `cli.RegisterCommands`): + +| Command | Description | +|---------|-------------| +| `core ai tasks` | List available tasks from the agentic API | +| `core ai task [id]` | View or claim a specific task | +| `core ai task --auto` | Auto-select the highest-priority pending task | +| `core ai agent list` | List configured AgentCI dispatch targets | +| `core ai agent add ` | Register a new agent machine | +| `core ai agent fleet` | Show fleet status from the agent registry | +| `core ai dispatch watch` | Poll the PHP API for work and execute phases | +| `core ai dispatch run` | Process a single ticket from the local queue | + +### PHP (Laravel package) + +The PHP package is `lthn/agent` (Composer name). It depends on `lthn/php` (the foundation framework). + +```bash +# Run tests +composer test + +# Fix code style +composer lint +``` + +The package auto-registers via Laravel's service provider discovery (`Core\Mod\Agentic\Boot`). + + +## Package Layout + +### Go Packages + +| Package | Path | Purpose | +|---------|------|---------| +| `lifecycle` | `pkg/lifecycle/` | Core domain: tasks, agents, dispatcher, allowance quotas, events, API client, brain (OpenBrain), embedded prompts | +| `loop` | `pkg/loop/` | Autonomous agent loop: prompt-parse-execute cycle with tool calling against any `inference.TextModel` | +| `orchestrator` | `pkg/orchestrator/` | Clotho protocol: dual-run verification, agent configuration, security helpers | +| `jobrunner` | `pkg/jobrunner/` | Poll-dispatch engine: `Poller`, `Journal`, Forgejo source, pipeline handlers | +| `plugin` | `pkg/plugin/` | Plugin contract tests | +| `workspace` | `pkg/workspace/` | Workspace contract tests | + +### Go Commands + +| Directory | Registered As | Purpose | +|-----------|---------------|---------| +| `cmd/tasks/` | `core ai tasks`, `core ai task` | Task listing, viewing, claiming, updating | +| `cmd/agent/` | `core ai agent` | AgentCI machine management (add, list, status, setup, fleet) | +| `cmd/dispatch/` | `core ai dispatch` | Work queue processor (runs on agent machines) | +| `cmd/workspace/` | `core workspace task`, `core workspace agent` | Isolated git-worktree workspaces for task execution | +| `cmd/taskgit/` | *(internal)* | Git operations for task branches | +| `cmd/mcp/` | Standalone binary | MCP server (stdio) with marketplace, ethics, and core CLI tools | + +### MCP Servers + +| Directory | Transport | Tools | +|-----------|-----------|-------| +| `cmd/mcp/` | stdio (mcp-go) | `marketplace_list`, `marketplace_plugin_info`, `core_cli`, `ethics_check` | +| `google/mcp/` | HTTP (:8080) | `core_go_test`, `core_dev_health`, `core_dev_commit` | + +### Claude Code Plugins + +| Plugin | Path | Commands | +|--------|------|----------| +| **code** | `claude/code/` | `/code:remember`, `/code:yes`, `/code:qa` | +| **review** | `claude/review/` | `/review:review`, `/review:security`, `/review:pr` | +| **verify** | `claude/verify/` | `/verify:verify`, `/verify:ready`, `/verify:tests` | +| **qa** | `claude/qa/` | `/qa:qa`, `/qa:fix` | +| **ci** | `claude/ci/` | `/ci:ci`, `/ci:workflow`, `/ci:fix`, `/ci:run`, `/ci:status` | + +Install all plugins: `claude plugin add host-uk/core-agent` + +### Codex Plugins + +The `codex/` directory mirrors the Claude plugin structure for OpenAI Codex, plus additional plugins for ethics, guardrails, performance, and issue management. + +### PHP Package + +| Directory | Namespace | Purpose | +|-----------|-----------|---------| +| `src/php/` | `Core\Mod\Agentic\` | Laravel service provider, models, controllers, services | +| `src/php/Actions/` | `...\Actions\` | Single-purpose business logic (Brain, Forge, Phase, Plan, Session, Task) | +| `src/php/Controllers/` | `...\Controllers\` | REST API controllers for go-agentic client consumption | +| `src/php/Models/` | `...\Models\` | Eloquent models: AgentPlan, AgentPhase, AgentSession, AgentApiKey, BrainMemory, Task, Prompt, WorkspaceState | +| `src/php/Services/` | `...\Services\` | AgenticManager (multi-provider), BrainService (Ollama+Qdrant), ForgejoService, Claude/Gemini/OpenAI services | +| `src/php/Mcp/` | `...\Mcp\` | MCP tool implementations: Brain, Content, Phase, Plan, Session, State, Task, Template | +| `src/php/View/` | `...\View\` | Livewire admin components (Dashboard, Plans, Sessions, ApiKeys, Templates, ToolAnalytics) | +| `src/php/Migrations/` | | 10 database migrations | +| `src/php/tests/` | | Pest test suite | + + +## Dependencies + +### Go + +| Dependency | Purpose | +|------------|---------| +| `forge.lthn.ai/core/go` | DI container and service lifecycle | +| `forge.lthn.ai/core/cli` | CLI framework (cobra + bubbletea TUI) | +| `forge.lthn.ai/core/go-ai` | AI meta-hub (MCP facade) | +| `forge.lthn.ai/core/go-config` | Configuration management (viper) | +| `forge.lthn.ai/core/go-inference` | TextModel/Backend interfaces | +| `forge.lthn.ai/core/go-io` | Filesystem abstraction | +| `forge.lthn.ai/core/go-log` | Structured logging | +| `forge.lthn.ai/core/go-ratelimit` | Rate limiting primitives | +| `forge.lthn.ai/core/go-scm` | Source control (Forgejo client, repo registry) | +| `forge.lthn.ai/core/go-store` | Key-value store abstraction | +| `forge.lthn.ai/core/go-i18n` | Internationalisation | +| `github.com/mark3labs/mcp-go` | Model Context Protocol SDK | +| `github.com/redis/go-redis/v9` | Redis client (registry + allowance backends) | +| `modernc.org/sqlite` | Pure-Go SQLite (registry + allowance backends) | +| `codeberg.org/mvdkleijn/forgejo-sdk` | Forgejo API SDK | + +### PHP + +| Dependency | Purpose | +|------------|---------| +| `lthn/php` | Foundation framework (events, modules, lifecycle) | +| `livewire/livewire` | Admin panel reactive components | +| `pestphp/pest` | Testing framework | +| `orchestra/testbench` | Laravel package testing | + + +## Configuration + +### Go Client (`~/.core/agentic.yaml`) + +```yaml +base_url: https://api.lthn.sh +token: your-api-token +default_project: my-project +agent_id: cladius +``` + +Environment variables override the YAML file: + +| Variable | Purpose | +|----------|---------| +| `AGENTIC_BASE_URL` | API base URL | +| `AGENTIC_TOKEN` | Authentication token | +| `AGENTIC_PROJECT` | Default project | +| `AGENTIC_AGENT_ID` | Agent identifier | + +### PHP (`.env`) + +```env +ANTHROPIC_API_KEY=sk-ant-... +GOOGLE_AI_API_KEY=... +OPENAI_API_KEY=sk-... +``` + +The agentic module also reads `BRAIN_DB_*` for the dedicated brain database connection and Ollama/Qdrant URLs from `mcp.brain.*` config keys. + + +## Licence + +EUPL-1.2