The execution layer for AI agents. Act across more of your systems with precise controls: safer, faster and more efficient than MCP tools and gateways.
# Repository guidance
## Canonical context
- Use `GLOSSARY.md` as the product glossary.
- Issues live in `withcoral/coral`. Always pass `--repo withcoral/coral` to
`gh`; this checkout also has an unrelated `upstream` remote.
- The PR title becomes the squash commit that release-please reads. Use
Conventional Commits, `type(scope): summary`, and keep the title current as
the branch changes. Only `feat`, `fix`, `perf`, and breaking (`type!:`)
titles open a release PR and reach the changelog, so give a user-visible
change one of those. The scope appears in the release notes, so name what a
user sees, not an internal codename: `ui` for Reef and Wax, never `reef`.
`.github/workflows/pr.yml` lists the rejected scopes.
- Tracker conventions, triage labels, and domain-document rules are under
`docs/agents/`.
## Durable boundaries
- Keep transport and presentation adapters thin. Put domain behavior behind a
transport-neutral interface.
- Read process environment only at process bootstrap. Pass explicit values to
domain code.
- Route untrusted outbound HTTP through `coral-outbound::Client`. Pass the
bootstrap-resolved operator destination policy to domain modules. Identity and
inbound listener selection do not determine outbound permissions. See
[ADR 0008](docs/adr/0008-outbound-network-permissions.md) for composition defaults.
- Keep cross-crate W3C propagation, safe telemetry fields, and export filtering
in `coral-telemetry`. Domain modules own signal timing. Application bootstrap
owns exporter configuration.
- Keep generated files marked `linguist-generated` in `.gitattributes`.
### Persistence
- Keep domain database queries, table identifiers, row mapping, and persistence
encoding in `repository.rs`, `*_repository.rs`, or `repository/` modules.
Keep authorization decisions, lifecycle rules, and outcome selection in domain
code outside those files. Domain code owns transaction choreography; repository
facets join its transaction and retain scoping, locking, and conditional-write
predicates needed for atomicity. Return typed evidence rather than raw rows or
query expressions to domain callers.
- Mark observable mutations with `Transaction::execute_change` in the owning
repository (or through its existing lifecycle/Run evidence write). Reads, row
locks, and worker-lease renewals use `execute`, or `fetch_optional_returning`
for a lock that returns its row; they must not wake activity observers or Run
waits. Time-dependent visibility also needs an observation deadline.
- Open read-only work with `Database::begin_read_snapshot` and non-holding
checks. `Database::begin` takes SQLite's write lock at `BEGIN`, so it waits
behind every other writer.
- The Dispatch Fence and the held rechecks before it read the fact rows
they never write (a Workspace, a Provider, a policy rule, an Account) with
`RowLock::Shared`
(`Transaction::fetch_optional_locked`; `coral_db::RowLock` documents what
each backend does). Other decisions still take the exclusive no-op `UPDATE`
hold on those rows; a row the transaction goes on to write always keeps it.
A row that two shared-hold decisions may both create for the first time (an
Account placeholder, a connection request) is inserted with
`ON CONFLICT DO NOTHING` and reread, since no shared hold serializes its
creators.
- A waiting Invocation parks with its Run Owner and never polls or
subscribes to the change signal itself; the owner wakes it (the
Wake-ups bullet of
[ADR 0018](docs/adr/0018-run-owner-coordinates-each-run.md)). A
wake-up is a hint; the held recheck decides.
- A nonterminal Run's row and Events are written only through its Run
Owner: the Program paths (Emissions, Logs, Invocation admission, held
rechecks, the Dispatch Fence, Invocation completion), the outside
requests (an approval decision, a cancellation) and the owner's timers
(the Run deadline, each Approval Request's and connection request's
deadline) alike. An Event is applied in owner memory, a row write is a
`ChunkWork` that joins the owner's chunk commit, and an intent (a
cancellation, the deadline) rides a chunk's head update or the owner's
terminal write; every chunk commit asserts the head the owner tracks in
memory. A timer is armed as an effect and fires as a message into the
owner's mailbox, so the clock never decides state outside the owner (ADR
0018): a Run's timeout and an Approval Request's expiry are decided in the
owner's pure `apply`, in mailbox order (when the timer fires, or first
when a decision, a cancel or the periodic precheck of parked waits applied
before it finds the deadline passed),
the owner's own writes read the
clock only for their own sample (the start mark, the terminal write and a
recovered owner's one chunk date a timeout they find passed; the Dispatch
Fence judges an approved
request's deadline at dispatch, contract line 3, and records no expiry),
and no chunk commit is predicated on the clock (contract line 5). Nothing
outside the owner compares `now` with those deadlines to write TimedOut
or Expired: readers show the recorded state beside the deadline. A
connection request is Account-owned and shared across Runs: the Account
module records its expiry when a holder reads it, and the owner's timer
only wakes the Invocations parked on it. Do not open a transaction that
holds the Run row from any of these paths, and do not write a nonterminal
Run's row from outside its owner. A Run whose owner retired or whose
process is gone without ending it gets its owner back from the
expired-lease sweep (the Recovery section of ADR 0018); a panicked owner
task's supervisor ends its Run at once instead. Retention holds and
deletes terminal Runs only.
[ADR 0018](docs/adr/0018-run-owner-coordinates-each-run.md) has the
request model (requests keyed by the entity they decide, the first wins).
- A Run Owner's write ends on the database's answer, never on the clock: an
unanswered write (`Error::is_operational`: a lost connection, a pool
timeout, a lock race) means a reread and a retry with bounded backoff until
the answer comes or the owner has retired, and a definite answer (a
rejection, a claim mismatch, an ended Run, a corrupt head) stops it; the
Interrupted record alone retries every answer but a claim mismatch or an
ended Run. The worker lease is ownership between processes, renewed by
every commit: never write a Run whose live lease another process holds.
The lease never ends a Run from inside its own process. The owner's
own bounds do (suspect after one lease without a commit, self-interrupt
after three, constants in `crates/coral-run/src/execution.rs`), and the
supervisor of a panicked owner task writes the same one Interrupted chunk.
- Give each independently identified entity one stable ID as its sole primary
key. Use a composite primary key only when the tuple is the domain identity.
- For Workspace-owned data, every externally influenced read or write must
match both the Workspace and entity identity, even when the entity ID is
globally unique.
- Add every schema migration to `crates/coral-db/migrations`, the one shared
chain. Write one portable file that both databases run. When SQLite and
Postgres must perform a step differently, add one file with the same name to
both `postgres/` and `sqlite/` instead.
- Change a schema only by adding a migration. Never delete a migration that
is on `main`, or change its file name or bytes: databases created from an
earlier `main` must keep booting.
`scripts/check-migrations.sh` checks this.
Keep the fence that rejects state written by Coral 0.x.
- Keep schemas portable across SQLite and Postgres and fail on schema drift;
do not use `IF NOT EXISTS` to hide unexpected state.
- When a migration needs a database feature whose availability it cannot
ensure or diagnose clearly, such as `pg_trgm` or FTS5's trigram tokenizer,
check it at boot before the chain runs, as `coral_search::prepare_storage`
does. The check names the feature and what the operator can do about it.
- Run shared repository behavior tests against SQLite and Postgres. Use
`make postgres-tests` for Rust repository tests. For Reel's Postgres product
evidence, use the E2E scope and cadence under Validation.
Both own disposable databases through `scripts/postgres.ts`.
### Reef and Desktop
- Wax (`apps/reef/app/wax`) owns reusable visual behavior and styling. Extend
its primitives when a UI needs a missing capability; app-specific modules
compose Wax and own layout, not a parallel set of controls or theme values.
- When a table row opens a page, pass the path to `Table.Row`'s `to` so a
click anywhere in the row opens it, and link its name with `Table.RowLink`.
Do not make only the name a link, or style the name as a link.
Use `Table.useRowCursor` so β, β and Enter reach the rows.
- Match a filter field's text with `filterQuery` (`apps/reef/app/utils/filter-query.ts`),
not `includes`, so every filter reads `^`, `$` and `*` the same way. To bold
the matches, use Wax's `HighlightedMatches`.
- Do not show status or meaning with color alone. Pair color with a distinct
icon shape or text, so color-blind users can read it (WCAG 1.4.1).
- Browser access to Coral belongs in React Router server loaders, actions, and
explicit resource routes. Live updates are the one exception:
`apps/reef/app/lib/activity-stream.ts` holds the only streaming connection,
to a same-origin resource route. Loaders supply initial data; streams only
push later changes. Reef's `.oxlintrc.json` and
`app/__tests__/architecture.test.ts` enforce the rest of this boundary.
- Add or extend Reef Vitest coverage when a change fixes a reproducible bug,
introduces branching behavior (validation, state transitions, or error handling),
or affects security or an architecture invariant. Name the failure the test
catches and assert observable behavior through the owning module's interface.
For bug fixes, verify that the regression test fails without the fix.
- Keep tests beside the behavior they protect. Exercise existing module boundaries;
do not extract single-use helpers solely to test them or assert implementation
details. Use Storybook and Chromatic for visual states and browser tests for
behavior that depends on browser APIs or a complete user journey. `npm test`
runs every story's `play` function in Chromium, so an assertion there is a
test; give each behavior one home, a story or a Vitest test, and query by role
and label with Testing Library in both. Styling and
static markup changes alone do not need Vitest coverage. Vitest coverage does
not replace required product-journey or local/remote parity evidence.
- Use vanilla-extract, not Tailwind.
- Reef has one build for every deployment. Do not add a Desktop build marker
or branch Reef on its host. Desktop's window behavior belongs in the
`apps/desktop` main process.
- Reef copy follows simplified technical English (ASD-STE100): address the
reader as "you", give one instruction per sentence in the imperative, keep
sentences under 20 words with one idea each, use the active voice and the
present tense. Do not narrate what Coral does behind the scenes ("Coral then
returns you here.", "Coral reads it once.", "Coral is updating this page.").
Name a consequence only when the reader must act on it or would otherwise
lose something. Show progress as a short status: "Completing sign-inβ¦".
Use one word for one thing: provider, workspace, organization, operation,
program, account, sign in. These words are lowercase in the interface; the
glossary and code keep their capitals.
### Tests
- Test each rule once, at its owner: the crate or module that implements it.
Transport and route tests assert their contract (status, reason, projection)
and leave domain rules to the owner's tests.
- Assert a denial's exact error variant and that the denial changed nothing. A
bare error check also passes on a database failure.
- Assert deltas and domain properties, not totals. Event counts, byte sizes, and
page sizes move whenever unrelated behavior changes.
- Extend the nearest shared helper (`coral-test-support` or the crate's test
harness) and write near-duplicate cases as one table test. After a TDD loop,
fold the tracer tests into that table before you open the PR.
- Inject faults through the owning crate's API or
`TestDatabase::execute_fixture_sql`, so the test runs on both databases.
Production `#[cfg(test)]` hooks only synchronize timing; every tested path is
one production runs.
### Credentials
- Tests that use dummy credentials must select filesystem credential storage.
Do not write test credentials to the OS keychain.
## Validation
Use focused unit/integration tests during implementation. Run the applicable
required gates below after the change stabilizes, then report what ran:
| Change | Required gate |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dev scripts | `./dev test` |
| Rust | `make rust-checks` |
| coral-v8 embedded JS | `npm run check --prefix crates/coral-v8` |
| Postgres-backed behavior | `make postgres-tests` |
| Reef | `npm run check --prefix apps/reef`, `npm run typecheck --prefix apps/reef`, `npm test --prefix apps/reef`, `npm run build --prefix apps/reef`, then `npm run test:server --prefix apps/reef` |
| Desktop | `npm run check --prefix apps/desktop`, `npm test --prefix apps/desktop`, then the Reel `desktop` suite: `npm run test:e2e --prefix apps/reef -- --suite desktop` |
| Runtime or Reef image | `make docker-test` |
### E2E scope and cadence
- Give each scenario a cross-process journey or local/remote parity claim that
lower-level tests cannot prove. Test form state, copy, and status tables in
Vitest or server tests.
- Treat E2E as an acceptance gate, not the default editβtest loop. Batch changes
before running affected scenarios; use a focused browser test during debugging
when lower-level tests cannot reproduce the failure.
- Select affected scenarios for browser-dependent changes. Read
[Reel's run guide](apps/reef/e2e/README.md) for file/folder filters and suite
selection. Run database and deployment variants required by the affected
behavior or the issue's acceptance criteria.
- Reserve full-suite runs for the final integration checkpoint or explicit
acceptance requirements. With multiple agents, designate one integration agent
to own full-suite validation of the combined changes.
- After a failure, rerun the failing selection while fixing it. Repeat required
broader validation at the final checkpoint after fixes.
- Preserve required product-journey and local/remote parity evidence. Report
scenario selection, database, deployment suite, and unrun required checks;
a focused pass is not a full-suite pass.
## Maintaining agent guidance
- Keep an `AGENTS.md` rule only when it is durable, non-obvious, and changes a
contributor's decision.
- Do not copy repo maps, temporary implementation details, generic engineering
advice, or facts that a nearby manifest already makes clear.
- Link to a canonical spec or tool instead of copying its full contract.
- When a rule becomes stale or enforced clearly by code, tests, or tooling,
delete it.
- If a change creates a new durable contributor constraint, update the nearest
relevant `AGENTS.md` and validate that the guidance matches the behavior.