docs: add human-friendly documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-11 13:02:40 +00:00
parent b75340b7fa
commit 3c25feb78f
3 changed files with 1264 additions and 0 deletions

506
docs/architecture.md Normal file
View file

@ -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

572
docs/development.md Normal file
View file

@ -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/<name>/` and contains:
```
claude/<name>/
├── .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 <fact>` | Save context that persists across compaction |
| code | `/code:yes <task>` | 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 <issue>` | 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 <type>` | 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/<name>/
├── .claude-plugin/
│ └── plugin.json
└── commands/
└── <command>.md
```
2. Write `plugin.json`:
```json
{
"name": "<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": "<name>",
"source": "./claude/<name>",
"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/<name>/
├── types.go # Public types and interfaces
├── <implementation>.go
└── <implementation>_test.go
```
Import the package from other modules as `forge.lthn.ai/core/agent/pkg/<name>`.
### 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
<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Models;
use Illuminate\Database\Eloquent\Model;
class MyModel extends Model
{
protected $fillable = ['name', 'status'];
}
```
### New Action
Actions follow the single-purpose pattern in `src/php/Actions/`:
```php
<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Actions;
use Core\Mod\Agentic\Concerns\Action;
class DoSomething
{
use Action;
public function handle(string $input): string
{
return strtoupper($input);
}
}
// Usage: DoSomething::run('hello');
```
### New Controller
API controllers go in `src/php/Controllers/`. Routes are registered in `src/php/Routes/api.php`, which is loaded by the service provider's `onApiRoutes` handler.
### New Artisan Command
Console commands go in `src/php/Console/Commands/`. Register them in `Boot::onConsole()`:
```php
public function onConsole(ConsoleBooting $event): void
{
$event->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

186
docs/index.md Normal file
View file

@ -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 <name> <host>` | 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