summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md68
1 files changed, 56 insertions, 12 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index bb54c06..25e2a48 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -74,6 +74,11 @@ Config defaults to `~/.claudomator/config.toml`. Data is stored in `~/.claudomat
| `internal/config` | TOML config loading + data-dir layout helpers |
| `internal/cli` | Cobra commands: `run`, `serve`, `list`, `status`, `start`, `logs`, `create`, `report`, `init` |
| `internal/version` | VCS version detection (`debug.ReadBuildInfo`) |
+| `internal/provider` | Provider-neutral chat/tool-use interface (`Provider`); adapters: `anthropic`, `google`, `openaicompat` (wraps `internal/llm`, also backs the `groq`/`openrouter`/`openai` runners) |
+| `internal/agentloop` | Provider-neutral tool-use control-flow loop shared by all `NativeRunner`s |
+| `internal/sandbox` | `Sandbox` interface (`HostSandbox`/`DockerSandbox`) + pre-tool-use guardrail hooks |
+| `internal/role` | `RoleConfig`/`Tier`/`Rung` — per-role system prompt + provider/model escalation ladder |
+| `internal/scheduler` | `Scheduler` — polls role-typed FAILED tasks and retries/escalates them per their role's ladder |
| `web` | Embedded static UI (`embed.go`) |
### Key Data Flows
@@ -174,28 +179,40 @@ Batch files wrap multiple tasks under a `tasks:` key.
### Storage Schema
-Two tables. Schema is auto-migrated additively on `storage.Open()` — new columns are `ALTER TABLE ... ADD COLUMN` statements that silently succeed if the column already exists.
+Schema is auto-migrated additively on `storage.Open()` — new columns are `ALTER TABLE ... ADD COLUMN` statements that silently succeed if the column already exists.
```
-tasks: id, name, description, config_json, priority, timeout_ns, retry_json,
- tags_json, depends_on_json, parent_task_id, state, rejection_comment,
- question_json, summary, elaboration_input, interactions_json,
- created_at, updated_at
+tasks: id, name, description, config_json, priority, timeout_ns, retry_json,
+ tags_json, depends_on_json, parent_task_id, state, rejection_comment,
+ question_json, summary, elaboration_input, interactions_json,
+ created_at, updated_at
-executions: id, task_id, start_time, end_time, exit_code, status, stdout_path,
- stderr_path, artifact_dir, cost_usd, error_msg, session_id,
- sandbox_dir, changestats_json, commits_json, tokens_in, tokens_out,
- agent
+executions: id, task_id, start_time, end_time, exit_code, status, stdout_path,
+ stderr_path, artifact_dir, cost_usd, error_msg, session_id,
+ sandbox_dir, changestats_json, commits_json, tokens_in, tokens_out,
+ agent, escalation_rung
-events: id, task_id, seq, ts, kind, actor, payload_json
+events: id, task_id, seq, ts, kind, actor, payload_json
+
+role_configs: id, role, version, status, config_json, created_at, activated_at,
+ retired_at, proposed_by (UNIQUE(role, version))
```
The `events` table is the per-task observability stream (`internal/event`); see
the REST `GET /api/tasks/{id}/events` and the chatbot MCP `get_events` tool.
`executions.agent` records which provider ran each execution, for budget
-accounting (`SpendByProviderSince`).
+accounting (`SpendByProviderSince`). `executions.escalation_rung` records the
+0-based index into the active role's `EscalationLadder` a role-typed task's
+execution ran at (0 for non-role tasks); see "Role-based dispatch &
+escalation" below.
+
+`role_configs` holds versioned `internal/role.RoleConfig` blobs (`config_json`)
+per role, `status` one of `draft`/`active`/`retired`. `ActivateRoleConfigVersion`
+enforces "at most one active row per role" transactionally (retire-then-activate
+in one transaction), the same way `UpdateTaskState` enforces the task state
+machine in a transaction rather than in schema — see `internal/storage/roleconfig.go`.
-JSON blobs: `config_json` (AgentConfig), `retry_json`, `tags_json`, `depends_on_json`, `interactions_json`, `changestats_json`, `commits_json`.
+JSON blobs: `config_json` (AgentConfig on `tasks`; RoleConfig on `role_configs`), `retry_json`, `tags_json`, `depends_on_json`, `interactions_json`, `changestats_json`, `commits_json`.
---
@@ -276,6 +293,18 @@ tokens; GitHub webhooks use HMAC.
> **Implementation gap:** Output parsing is brittle — strips `"Loaded cached credentials."` lines and markdown fences by string matching. No fallback if Gemini CLI isn't installed. Classification results are not cached or logged for learning.
+### Role-based dispatch & escalation
+
+A task opts into role-based dispatch by setting `agent.role` (`task.AgentConfig.Role`) to a role name with an **active** `role_configs` version (see the REST endpoints below). Tasks with `agent.role` unset (every existing YAML/chatbot task shape) are completely unaffected — this is purely additive.
+
+- **Initial dispatch** (`internal/executor.Pool.execute()`): when `Agent.Role != ""` and `Agent.Type == ""`, the pool resolves tier 0 of the active `role_configs` row's `EscalationLadder`, setting `Agent.Type`/`Agent.Model` on a copy of the task (mirroring the `withFailureHistory` copy-before-mutate pattern) and applying `RoleConfig.SystemPrompt` to `Agent.SystemPromptAppend`. Multi-candidate `round_robin` tiers rotate via `Pool.selectRung`, skipping any provider currently in `Pool.rateLimited` (falling back to whichever clears soonest if all are limited).
+- **Retry/escalation** (`internal/scheduler.Scheduler`, started by `serve` only — not the one-shot `run` command): polls for role-typed tasks whose latest execution is `FAILED` and, per the ladder tier at that execution's `escalation_rung`: retries at the same rung while under `tier.MaxRetries`, or escalates to the next tier's first candidate if `budget.Accountant.Allow` permits it (recording an `event.KindEscalated` event), or leaves the task `FAILED` for human attention — recording a `final: true` `KindEscalated` event — if the budget denies it or the ladder is exhausted. An in-memory (per-process) "already handled" set keyed by execution ID keeps the poll loop convergent: a task stuck at the end of its ladder gets exactly one `final` event, not one per poll tick. This resets on restart (by design — it's idempotent bookkeeping, not orchestration state, so re-deriving it once is harmless).
+- **REST**: `POST /api/roles/{role}/versions` (create draft), `GET /api/roles/{role}/versions` (list), `POST /api/roles/{role}/activate?version=N` (activate, atomically retiring whatever was active).
+
+> **Stored but not yet enforced:** `RoleConfig.Tools`/`SandboxKind` are round-tripped through storage/API but don't affect tool availability or sandbox selection — those are still applied uniformly by `NativeRunner` regardless of role. `RoleConfig.DefaultBudgetUSD` is read narrowly by the scheduler as the estimated cost passed to `Allow()` when considering an escalation — it is not enforced at initial-dispatch time the way `task.AgentConfig.MaxBudgetUSD` is. See `internal/role/role.go`'s field docs.
+
+> **Known simplification:** the scheduler always escalates to `nextTier.Candidates[0]` — it does not round-robin across multiple candidates in a tier the way `Pool.execute()`'s initial-dispatch resolution does. A later phase could extend this if multi-candidate escalation tiers turn out to matter in practice.
+
---
@@ -283,6 +312,18 @@ tokens; GitHub webhooks use HMAC.
## Design Debt
+### RoleConfig.Tools/SandboxKind/DefaultBudgetUSD are stored but not fully enforced
+
+Same pattern as `task.Priority`/`RetryConfig` below: `internal/role.RoleConfig.Tools` and `.SandboxKind` round-trip through `role_configs`/the REST endpoints but have no effect on dispatch — tool availability and sandbox selection are still applied uniformly by `NativeRunner` regardless of role. `.DefaultBudgetUSD` is read only narrowly, as the scheduler's estimated escalation cost, not enforced generally at dispatch time.
+
+### Scheduler's escalation candidate selection doesn't round-robin
+
+`internal/scheduler.Scheduler` always escalates to `nextTier.Candidates[0]`, unlike `Pool.execute()`'s initial-dispatch tier-0 resolution, which round-robins across multi-candidate tiers. Same-rung retries intentionally reuse the exact same provider/model rather than re-resolving.
+
+### Scheduler's double-processing guard is in-memory only
+
+`Scheduler.handled` (keyed by execution ID) prevents re-emitting a `final: true` `KindEscalated` event on every poll tick once a role-typed task's ladder is exhausted, but it's per-process and resets on restart. A restart can produce one extra "reconsideration" (and, if still exhausted/denied, one more `KindEscalated` event) — not an infinite loop, just not persisted. A future phase could persist this via a `tasks` column if that turns out to matter.
+
### Deprecated task columns not yet dropped (Phase 8 follow-up)
`tasks.question_json`, `summary`, `interactions_json`, `elaboration_input` are
@@ -360,6 +401,9 @@ In `executor.go`, `withFailureHistory` creates a copy of the task struct (`copy
| GET | `/api/workspaces` | List directories under `workspace_root` |
| GET | `/api/health` | Server health |
| POST | `/api/webhooks/github` | GitHub CI webhook |
+| POST | `/api/roles/{role}/versions` | Create a new draft `role_configs` version |
+| GET | `/api/roles/{role}/versions` | List all versions for a role |
+| POST | `/api/roles/{role}/activate?version=N` | Activate a version (atomically retires the prior active one) |
---