diff --git a/docs/design/per-project-config.md b/docs/design/per-project-config.md deleted file mode 100644 index 05af2850..00000000 --- a/docs/design/per-project-config.md +++ /dev/null @@ -1,129 +0,0 @@ -# Design: typed per-project configuration - -Status: **partially implemented** — `ProjectConfig` is typed, validated, -persisted (one `projects.config` JSON column), and surfaced via -`ao project set-config` + `PUT /projects/{id}/config`. The struct deliberately -carries only fields with a live consumer: `defaultBranch`, `env`, `symlinks`, -`postCreate`, `agentConfig`, and the `worker`/`orchestrator` role overrides are -wired at spawn; `sessionPrefix` feeds the display prefix. Settings whose -consumers do not yet exist — per-project `tracker`/`scm` config and prompt -`rules` — are intentionally **not** modeled yet and land in focused follow-up -PRs alongside the code that reads them (see "Sequencing" below). Cross-agent -`agentConfig.model`/`permissions` support is tracked in #157. - -## Goal - -Every per-project setting the legacy `agent-orchestrator.yaml` carried under -`projects:` should live as **typed, validated state** in SQLite, reachable -through exactly two entry points: - -1. **CLI** — `ao project ...` (thin client → daemon HTTP) -2. **UI** — the dashboard project settings form - -There is no YAML loader in the Go rewrite, so this is not about parsing a file — -it is about giving each former YAML field a typed home, a validation owner, and a -CLI/API/UI surface. No setting should be a free-form `map[string]any`. - -## Principle: typed over map - -The legacy `agentConfig` was an open `map` (`.passthrough()`), which is why early -storage modeled it as `map[string]any`. That defers validation to spawn time and -forces the UI to render raw JSON. We instead model each setting as a **typed Go -struct** with a `Validate()` method, so: - -- bad values are rejected when **set** (CLI/API), not silently dropped at spawn; -- the OpenAPI spec and frontend TS types are generated with real fields; -- the UI renders a typed form instead of a JSON textarea. - -Adapter-specific keys, if ever needed, become typed fields owned by `domain` -rather than an escape-hatch map. - -## Field catalog (legacy `projects.`) and target home - -| YAML field | Type | Storage today | Target | -| --------------------------------- | ---------------------- | ----------------------------------- | ---------------------------------------------------- | -| `name` | string | `projects.display_name` | done | -| `repo` | string | `projects.repo_origin_url` | done | -| `path` | string | `projects.path` | done | -| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` | -| `sessionPrefix` | string | derived | `projects.session_prefix` | -| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** | -| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob | -| `env` | `map[string]string` | — | `project_env` table (key/value rows) | -| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) | -| `postCreate` | `[]string` | — | `projects.post_create` (JSON) | -| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` | -| `orchestratorRules` | string | — | `projects.orchestrator_rules` | -| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation | -| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation | -| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` | -| `reactions` | per-project overrides | — | `project_reactions` (own slice) | - -## Typed model - -```go -// domain -type AgentConfig struct { // implemented - Model string `json:"model,omitempty"` - Permissions PermissionMode `json:"permissions,omitempty"` -} -func (c AgentConfig) Validate() error { ... } - -// implemented today — only fields with a live consumer are modeled -type ProjectConfig struct { - DefaultBranch string - SessionPrefix string - AgentConfig AgentConfig - Worker RoleOverride // {Harness, AgentConfig} - Orchestrator RoleOverride - Env map[string]string - Symlinks []string - PostCreate []string - // future slices add fields here as their consumers land: - // AgentRules / AgentRulesFile / OrchestratorRules (prompt rules) - // Tracker TrackerConfig // adapter-validated - // SCM SCMConfig // adapter-validated -} -``` - -Each leaf type owns a `Validate()`. Plugin-shaped settings (`tracker`, `scm`) -delegate to the selected adapter, mirroring how `agentConfig` is consumed by the -agent adapter. - -## Storage strategy - -- **Scalar fields** (`default_branch`, `session_prefix`, `agent_rules`, enums) → - their own typed columns on `projects`. -- **Small structured blobs** (`agent_config`, `tracker`, `scm`, `symlinks`, - `post_create`) → nullable JSON columns, marshaled/unmarshaled in the store - (the pattern this PR established for `agent_config`). -- **Unbounded key/value sets** (`env`) → a child table keyed by `project_id`. -- **Its own domain** (`reactions`) → a separate slice; reactions already have a - reaction engine to integrate with. - -## Surface (per field) - -- **API** — extend the projects controller. Field groups get focused routes - (e.g. `PUT /projects/{id}/agent-config`, `PUT /projects/{id}/env`) rather than - one mega-PUT, so partial updates are clean and the OpenAPI stays legible. -- **CLI** — typed flags on `ao project` subcommands (e.g. - `ao project set-config --model --permission`, `ao project env set KEY=VAL`). -- **UI** — a generated typed form per group, driven by the OpenAPI schema. - -## Sequencing (one slice per PR) - -1. **agentConfig (typed)** — _this PR_. Establishes the typed+validated+surfaced - pattern end to end. -2. **Project identity scalars** — `default_branch`, `session_prefix` (stop - hardcoding/deriving them). -3. **Workspace provisioning** — `env`, `symlinks`, `postCreate` (these change - spawn/workspace wiring, so grouped). -4. **Rules** — `agentRules`, `agentRulesFile`, `orchestratorRules` (consolidate - the partial `SpawnConfig.AgentRules` path). -5. **Role overrides** — `worker` / `orchestrator` `{agent, agentConfig}`. -6. **Tracker / SCM per-project** — typed blobs with adapter-owned validation. -7. **Per-project reactions** — integrate with the reaction engine. - -Each slice is independently shippable and follows the same shape: domain type + -`Validate()` → storage (column or blob or table) → service set/get → API route → -CLI flags → UI form → tests.