openvibers/openvibe.codes Commits

ADR-050 (proposed): the Codes harness fabric and its boundaries (#18)

OP OpenVibers committed 5 Oct 2026 · b2158cdf7f · 1 file, +158 −0

* ADR-050 (proposed): the Codes harness fabric and its boundaries

**Change**
- `docs/adr/050-codes-harness-fabric.md:1-158` (new) — proposed ADR-050 recording the Codes harness fabric: the committed catalog `server/data/harness-offers.json` validated at boot against `platform.harness-offer@1`/`platform.agent-offer@1` (`harnesses.js:30,36`), each harness×agent as a `platform.resource-offer@1` of kind `harness` carrying a `platform.harness-offer@1` detail (`harness-placement.js:31-58`), capability-first routing only through `openvibe-sdk/placement` `plan()` (`harness-placement.js:84`; hard constraint at SDK `placement.js:88-91`), the read surface `GET /api/v1/harnesses` + `POST /api/v1/harnesses/route` (`api.js:61-81`) and `/docs/harnesses` (`docs.js:383`), the deferred workspace/branch/PR half with its named T1/T2/T5/T14/T17 dependencies, and Forgejo as a Codes deliverable that must not gate Codes (plan T16). States the open choices (catalog ownership/registry, trust defaults, current-placement hysteresis, canonical forge) explicitly.

**Where the code differed from the brief's line numbers** (re-read; noted in the ADR where it matters)
- The offer mapping `toOffer` is `harness-placement.js:31-58`, not `:23-52`; `route` is `:78-99` with the `plan()` call at `:84` and result validation at `:86-93`, not `:69-90`.
- `api.js`: `GET /harnesses` is `:61-63`, `POST /harnesses/route` is `:66-81` (brief said `:61-78`).
- The brief's "each harness advertises runtime:<class>" is not universally true: `runtime:<class>` is emitted only for entries in a row's `capabilities.runtimes` (`:45`); the `deepseek` row declares none, so its offer has an empty capability list (asserted at `test/harness-placement.test.js:52-55`).
- The plan does not use the phrase "forge model"; T16 names T2 (projects/grants) and T14 (Run/Node execution) — the ADR quotes the plan's dependency line rather than the phrase.

**Checks**
- `git diff --check` → clean.
- `nice -n 15 ov test --no-cache test/harnesses.test.js test/harness-placement.test.js test/harnesses-api.test.js test/layout.test.js` → 4 passed in 10 s (0 failed). No other tests reach a docs-only change.

* ADR-050: correct plan citations and list the two §9 Codes gates still owed (review)

Docs-only change to `docs/adr/050-codes-harness-fabric.md`; no test reaches an ADR file, so no tests were run. `git diff --check` → clean (no output).

- **Finding 1 — fixed.** Acceptance tests: "the §9 universal gates do not yet cover Codes" → "the §9 gates have not yet been run for Codes", and the two §9 Codes rows ("never writes a protected branch directly"; "a failed harness continues via handoff") added to the "Still owed" list ahead of the existing Placement items — `docs/adr/050-codes-harness-fabric.md:154-158`.
- **Finding 2 — fixed.** Context first bullet: "§3 owner direction" → "The plan's owner-direction block (line 43)" — `docs/adr/050-codes-harness-fabric.md:10`.
- **Finding 3 — fixed.** Alternatives: "`openvibe-agents` is the *prototype* (§1.2)" → "(§1.1, line 97)" — `docs/adr/050-codes-harness-fabric.md:108`.

Nothing else changed; not committed or pushed.

Changes

ddiff --git a/docs/adr/050-codes-harness-fabric.md b/docs/adr/050-codes-harness-fabric.md
nnew file mode 100644
iindex 0000000..4399d9c
---- /dev/null
++++ b/docs/adr/050-codes-harness-fabric.md
@@@ -0,0 +1,158 @@
++# ADR-050: The Codes harness fabric and its boundaries
++
++**Status:** Proposed 2026-10-05. Governs the Codes product code that has not been written yet; the catalog, the
++placement adapter and the read API recorded under **Context and current evidence** are the seed this
++record is written against, as they stand on `main` (most recently `af3aab3`, `a1097e7`). Written in Codes'
++first `docs/adr/` — the plan (§8) lists **ADR-050 Codes Harness Fabric** among the ADRs "to write before their code", and no earlier record covers it.
++
++## Context and current evidence
++
++- **The plan makes Codes the coding-harness fabric, not one harness.** The plan's owner-direction block (line 43): "OpenVibe.Codes is the
++ coding-harness fabric and vibe-coding environment: OpenRouter for coding agents plus the contribution surface for
++ OpenVibe itself." T16's finish line is "OpenRouter for coding harnesses plus the cloud/local vibe-coding IDE ... all
++ routing through the universal Fabric, never through a second coding-harness router."
++- **One placement implementation is a build rule.** §2.1.11: "`openvibe-sdk/placement` is the only generic
++ placement/routing implementation. Product services may add policy and domain-specific eligibility, but must never
++ create independent cost, health or provider optimizers." T16 restates it: harnesses "Routed by
++ `openvibe-sdk/placement` like every other offer (§2.1.11)."
++- **What actually shipped in Codes is the catalog and the router, not an execution engine.** `server/data/harness-offers.json`
++ is a committed catalog of six harnesses — `claude-code`, `codex`, `command-code`, `opencode`, `aider`, `deepseek` —
++ each with at least one agent. Its shape is fixed by Contracts: each row (minus `agents`) is validated as
++ `platform.harness-offer@1` and each agent as `platform.agent-offer@1` at boot (`server/domain/harnesses.js:30,36`); a
++ bad seed row, a duplicate id, or an agent whose `harness` names another row fails boot
++ (`harnesses.js:32,38,39`; `server/app.js:79-80`, "a bad seed row fails boot"). The catalog is read once; the source
++ says ownership of offers is not settled: "Static harness catalog. Replace the file reader when offer ownership is
++ settled." (`harnesses.js:3`).
++- **Each harness × agent pair becomes one Fabric offer.** `toOffer()` (`server/domain/harness-placement.js:31-58`) maps
++ one catalog row + agent to a `platform.resource-offer@1` of `kind: 'harness'` (`:41`) whose `detail` is the row itself
++ as `platform.harness-offer@1` (`:50-54`), and whose `offer_id` is `<harness>:<agent.id or agent.model>` (`:40`). The
++ offer's `capabilities` are the task kinds, the true harness flags and the declared runtimes (`:45`): `task:<kind>`,
++ `harness:<flag>` (the `HARNESS_FLAGS` map, `:19-22`) and `runtime:<class>` for each entry in `capabilities.runtimes`.
++ A row without `task_capabilities` derives its kinds from `capabilities.edit` / `capabilities.review` (`tasksFor`,
++ `:14-16`; `TASK_FLAGS`, `:12`). Capacity comes from the harness's `limits.max_concurrent_runs` and pricing is the
++ agent's fresh token price as a marginal per-token figure (`:46,48`). The deepseek row declares no `runtimes` and no
++ task flags, so its offer carries an empty capability list (`test/harness-placement.test.js:52-55`) — the mapping is
++ what it is; it does not invent a runtime class.
++- **Routing is capability-first and goes only through the SDK planner.** `route()` (`harness-placement.js:78-99`) builds
++ the offers and calls `plan()` from `openvibe-sdk/placement` (`:8,84`). Capability is a *hard constraint* in the SDK,
++ evaluated before cost or latency: `excluded()` returns "lacks <c>" for any missing required capability
++ (`openvibe-sdk/src/placement.js:88-91`), and the planner's own contract is "hard constraints first: capabilities,
++ trust, residency/region, health, latency ceiling, capacity, cost ceiling. A cheaper candidate that misses one is
++ excluded, with the reason, never chosen" (`placement.js:12-14`). `requirementsFor()` turns a task into
++ `{ kind: 'harness.<task>', mobility: 'job', latency_class: 'interactive', objective: 'balanced', capabilities: [...] }`
++ (`harness-placement.js:60-76`) — Codes adds task vocabulary and eligibility, it adds no optimizer. The planner's
++ answer is validated as `platform.placement-result@1` unless nothing was selected (`:86-93`); the API response is
++ reshaped to `{ selected, reasons, candidates }` with each candidate's eligibility and exclusion reason (`:94-98`).
++ Note `route()` passes no `current` placement, so the SDK's hysteresis branch
++ (`placement.js:20-21,127,165-168`) is inert here today; whether Codes pins a current placement is **open**.
++- **The read surface is two JSON routes and one docs page.** `GET /api/v1/harnesses` returns the catalog with its
++ agents (`server/http/api.js:61-63`); `POST /api/v1/harnesses/route` validates the body (non-empty string `task`; a
++ plain-object `requirements`; a string array `requirements.capabilities`) and routes, answering 422
++ `harness.invalid` / `harness.task_unknown` for a bad request and 500 `harness.placement_invalid` if the planner's
++ result violates its contract (`api.js:66-81`). Both sit under the per-actor limits (`codes.read`,
++ `codes.harness.route`; `api.js:38-46`), and the routes are declared in the module header (`api.js:8-9`). A
++ server-rendered, read-only page `/docs/harnesses` lists the catalog (`server/http/docs.js:19,383`), with sitemap
++ and `llms.txt` entries (`server/http/discovery.js:43,100,224`). `STATUS.json` describes exactly this and nothing more:
++ "a validated catalog of harnesses and their agents ..., a task placement adapter over openvibe-sdk/placement, and a
++ public read API and read-only docs page".
++- **The product half of T16 does not exist in Codes yet.** There is no workspace, editor, terminal, task feed, diff,
++ review verdict, cost, or branch/PR code in `server/` — the strings do not appear. T16's product line is
++ "repository browser, workspace, editor, terminal, task feed, tests, diff, harness explanation, review verdict, cost,
++ branch/PR flow — usable hosted and on a Node (T14) for local harnesses", and T16's dependency line is
++ "T1 (Fabric offers), T2 (projects/grants), T5 (metering/Vibes), T17 (Actor for planning), T14 (Run/Node for
++ execution)". The plan does not use the phrase "forge model"; the repository/branch/PR authority it describes is
++ Forgejo/GitHub under T16, and the project/grant authority is T2.
++- **Forgejo is a Codes deliverable that must not gate Codes.** T16: "**Forgejo belongs here,** not in generic
++ operations: Codes is the product that understands repository, branch, fork, PR/MR, review, CI, provenance. The
++ platform supports GitHub and Forgejo adapters regardless of which is canonical; the Forgejo migration
++ (git.openvibe.codes, one repository at a time) is a Codes deliverable and must not gate Codes." The plan voices it too:
++ "Forgejo belongs to Codes; `openvibe-agents` is its prototype." Which forge is canonical, and when the migration runs, are **open**; neither may stop the
++ catalog, routing and read surface, which is what ships now.
++
++## Decision
++
++1. **Codes is the harness fabric, and the fabric's router is the SDK's.** Codes owns the harness *catalog*, the
++ harness-specific offer mapping and the task vocabulary; it does not own a router. Every routing call goes through
++ `openvibe-sdk/placement` `plan()` (`harness-placement.js:84`), as §2.1.11 requires. Capability remains a hard
++ constraint in the SDK, so a task's required capabilities decide eligibility before cost or latency
++ (`placement.js:88-91`).
++2. **The catalog is committed data validated against Contracts at boot.** `server/data/harness-offers.json` is the
++ single source of harnesses and agents; every row is checked against `platform.harness-offer@1` and every agent
++ against `platform.agent-offer@1` when Codes starts (`harnesses.js:30,36`), so an invalid catalog is a boot failure
++ (`app.js:79-80`), never a runtime surprise.
++3. **One harness × agent pair is one `platform.resource-offer@1` of `kind: harness`** with the catalog row as its
++ `platform.harness-offer@1` `detail` (`harness-placement.js:31-58`). Offers carry `task:<kind>`, `harness:<flag>` and
++ `runtime:<class>` capabilities; a row that declares no runtime declares none (no default is synthesized).
++4. **The read surface is `GET /api/v1/harnesses` and `POST /api/v1/harnesses/route`, plus the read-only docs page.**
++ These are additive and public-read; they expose the catalog and the placement decision with reasons and candidates
++ (`api.js:61-81`). Nothing in this surface launches a harness, streams a run, meters usage or resumes a session.
++5. **The write half is deferred, and its dependencies are named, not guessed.** Workspace, branch, PR/review, usage and
++ execution depend on T1 (`platform.agent-offer@1` / Fabric offers), T2 (projects and grants), T5 (metering/Vibes),
++ T14 (Run/Node execution) and T17 (Actor planning), per T16's dependency line. This record fixes no interface for
++ them; they are **open** until those tracks land.
++6. **Forgejo does not gate the fabric.** Codes must understand repository/branch/fork/PR/MR, CI and provenance, and
++ the platform supports GitHub and Forgejo adapters "regardless of which is canonical" (T16). The migration to
++ `git.openvibe.codes` is a Codes deliverable performed one repository at a time and must not block the catalog,
++ routing or read surface. Which forge is canonical, and the adapter interface, are **open**.
++7. **Where the catalog lives is open.** Today it is a committed file read once at boot, and the code says so
++ (`harnesses.js:3`). When ADR-046 (Universal Adaptive Fabric) and ADR-047 (Cells and Resource Registry) settle offer
++ ownership and registry publication, the catalog moves behind the registry and the file becomes a seed/fallback, or
++ it does not; this record does not decide that. Nor does it decide trust defaults: `toOffer()` defaults every harness
++ to `trust: 'external'` (`harness-placement.js:44`, asserted at `test/harness-placement.test.js:56`) — whether
++ first-party harnesses should advertise a different class is **open**.
++
++## Alternatives considered
++
++- **A second coding-harness router inside Codes (or reusing `openvibe-agents`' scored router as the optimizer):**
++ rejected. §2.1.11 allows products to add policy but not optimizers, and T16 requires routing "through the universal
++ Fabric, never through a second coding-harness router". `openvibe-agents` is the *prototype* (§1.1, line 97) and becomes
++ Actor's engine, not Codes' router.
++- **Putting the catalog in Contracts or in a database now:** rejected for the seed. The catalog is Codes' data and
++ changes per deployment; Contracts owns the *shape* (`platform.harness-offer@1`), not the rows. Moving the rows to the
++ registry is a plausible later step (decision 7) but would make today's read API depend on tracks that have not
++ landed.
++- **Launching harnesses from the read API's wake (an adapter in Codes now):** rejected as premature. There is no
++ execution substrate (T14), no project/grant scope (T2) and no usage metering (T5); T16's adapter interface ("each
++ harness is an adapter that declares how it launches, streams, reports usage and resumes") is a product deliverable
++ with those dependencies, and shipping it early would either be a placeholder (§2.1.3) or a second runtime.
++- **Making Forgejo canonical first and gating Codes on the migration:** rejected by T16, explicitly: the migration
++ "must not gate Codes".
++
++## Migration consequences
++
++- **No data migration:** this is Codes' first ADR; it adds documentation only. The catalog is committed data and the
++ routes are additive.
++- **When T1/T2/T5/T14/T17 land:** the catalog becomes registry-backed (decision 7), harness offers gain trust, health
++ and latency from telemetry rather than static defaults, and adapters (launch/stream/usage/resume) attach behind the
++ same offers. `route()` may begin passing a current placement so the SDK's hysteresis applies (decision 5's open
++ point). None of these change the contract shape recorded here.
++- **Contracts:** `platform.harness-offer@1`, `platform.agent-offer@1`, `platform.resource-offer@1` and
++ `platform.placement-result@1` are already pinned (openvibe-contracts v0.97.0; openvibe-sdk v0.26.0; openvibe-shared
++ v2.9.0). If the fabric needs a new harness capability name, it is an additive enum extension in Contracts, not a new
++ contract.
++- **Docs:** the `/docs/harnesses` page and the sitemap/`llms.txt` entries are generated from the catalog
++ (`docs.js:383`, `discovery.js:43,100,224`), so catalog changes propagate without hand edits.
++
++## Rollback
++
++- This record is documentation; deleting or superseding it leaves the shipped catalog and routes untouched.
++- The catalog and routes are additive to the existing developer portal: removing `GET/POST /api/v1/harnesses`,
++ `/docs/harnesses` and the `harness-offers.json` seed returns Codes to its pre-fabric release with no schema or data
++ change.
++
++## Acceptance tests
++
++- **Shipped and checked** (`test/harnesses.test.js`, `test/harness-placement.test.js`, `test/harnesses-api.test.js`,
++ `test/layout.test.js`): every seed row validates as `platform.harness-offer@1` and `platform.agent-offer@1`, ids are
++ unique and malformed catalogs are rejected with `harness.invalid` (`test/harnesses.test.js:15-62`); every seed offer
++ validates as `platform.resource-offer@1` of `kind: harness` and the field-by-field mapping is asserted, including the
++ deepseek offer's empty capabilities (`test/harness-placement.test.js:26-78`); `GET /api/v1/harnesses` returns every
++ harness with its agents, `POST /harnesses/route` returns a selected offer among all agents' candidates, a bad body is
++ 422 `harness.invalid` and an unknown task is 422 `harness.task_unknown`, both under their per-actor limits
++ (`test/harnesses-api.test.js:18-76`); `/docs/harnesses` server-renders every seed harness and hides local paths
++ (`test/layout.test.js:60-73`).
++- **Still owed before the product closes T16** (the §9 gates have not yet been run for Codes): never writes a
++ protected branch directly; a failed harness continues via handoff; a cheaper ineligible candidate never wins on the
++ *live* catalog; a broken harness fails over immediately and a healthy one is not re-picked without `minGain`
++ hysteresis once a current placement is supplied; a routed task actually launches a harness, streams progress, reports
++ usage and resumes — none of which exists in Codes today, and none of which this record claims.