> ## Documentation Index
> Fetch the complete documentation index at: https://usenaive.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Teams

> The durable runtime — declare a team of agents behind one lead, address it by (company, tenant, team), and submit work to it with one verb.

A **team** is N agents behind exactly one **lead**, running on one runtime, bound to one
partition of one [company brain](/docs/getting-started/brain). It is the unit the durable
runtime schedules, governs and bills.

<Info>
  **What is served depends on which runtime the tenant is on.** Of the 32 `/v1/teams/**`
  operations, a tenant on the **durable** runtime is served 22, a tenant on the frozen
  **hermes** runtime 13, and 10 are refused for both. A refusal is `501 not_configured`
  naming the missing dependency for that tenant's runtime in `error.details.missing`.
  Every response carries `provider`; every refusal carries `error.details.runtime`. See
  [What answers today](#what-answers-today) for the exact list.

  A tenant is placed on the durable runtime only by `naive up` — when a `teams:` block
  declares `runtime.durable(...)` **and** the operator exports
  `NAIVE_DURABLE_CREDENTIAL_<TEAM>` out of band. `POST …/migrate` is `501`; moving a
  tenant is an operator act, not a call — see
  [the durable runtime](/docs/architecture/durable-runtime#which-runtime-is-this-tenant-on).
</Info>

## Declaring a team

```ts naive.config.ts theme={"theme":"css-variables"}
import { defineConfig, runtime, team, agent, brain, skills } from "@usenaive-sdk/iac";

const acme = brain({
  retention: { beliefs: "180d", episodes: "30d" },
  writes: { mode: "review", promoteBy: "operator", scan: "enforce" },
  partitions: {
    support: { retention: { beliefs: "90d" } },
  },
});

export default defineConfig({
  project: "acme",

  company: {
    brain: acme,
    residency: { jurisdiction: "eu", allowEgressTo: ["eu"] },
  },

  teams: {
    support: team({
      runtime: runtime.durable({
        residency: "eu",
        sleepAfter: "15m",
        workspace: { isolation: "os-kernel", egress: { mode: "deny-all" } },
      }),
      brain: acme.partition("support"),
      lead: agent({
        instructions: "Triage the ticket, decide, and hand the refund to tier1.",
        brain: acme.view({ partition: "support", can: ["recall", "think", "propose"] }),
        can: [skills.email],
      }),
      agents: {
        tier1: agent({
          instructions: "Answer the ticket. Refunds over $50 need an approval.",
          brain: acme.view({ partition: "support", can: ["recall"] }),
          can: [skills.email, skills.payments],
        }),
      },
      edges: [["lead", "tier1"]],
    }),
  },
});
```

### Three things you cannot write

These are compile errors, not runtime failures — you find out in your editor.

| You cannot write                                                              | Because                                                   |
| ----------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Two leads.** `lead` is a single required field, not an array                | a team with two leads has no answer to "who decides"      |
| **A dangling edge.** `edges: [["lead", "tier2"]]` with no `tier2` in `agents` | the type of `edges` is built from the roster you declared |
| **Residency on a team.** `governance` at team scope omits it                  | residency is minted once, at `company.residency`          |

`brain: acme.view({ can: [] })` — an empty ability list — is how an agent gets **no** brain
access. There is no second concept for "none", and no field anywhere in which a *second*
brain can be named: `acme.partition()` and `acme.view()` are methods on the brain value
itself.

<Warning>
  **A `BrainView` is a plain object type today, not a branded one.** An object literal with
  the same shape typechecks where a real `acme.view({…})` is expected, and partitions are
  matched by **name**, so a second `brain()` value declaring a same-named partition binds
  without complaint. Treat "only the brain you declared can be bound" as a convention that
  the type system does not yet enforce.
</Warning>

## Addressing: (company, tenant, team)

A team is not addressable on its own. Every runtime operation names three things:

* the **company** — taken from the API key you present, never from the URL;
* the **tenant user** — the end-user this work is for, `--tenant <tenantUserId>`;
* the **team** — the key you declared under `teams:`.

```bash theme={"theme":"css-variables"}
naive teams board support --tenant tu_8f21
naive teams roster support --tenant tu_8f21 --edges
naive teams runs support --tenant tu_8f21
```

`--tenant` is **required on every subcommand except `list`** — there is no default user
fallback. `--tenant @self` resolves the credential's own subject explicitly.

The REST form is `/v1/teams/{team}/tenants/{tenantUserId}/…`.

One deployment currently serves one company: the control plane is addressed per
`(company, tenant)`, but the deployment is provisioned per company, so every "tenant"
answer below is scoped inside one company's deployment.

## `submit` is the work verb

There is one verb for handing work to a team, and it is `submit`. It is the same operation
on every surface — one action id, one fence, one audit row.

| Surface | Form                                                                                                                                                                           |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| CLI     | `naive teams submit <team> "<goal>" --tenant <tu>`                                                                                                                             |
| REST    | `POST /v1/teams/{team}/tenants/{tenantUserId}/submit`                                                                                                                          |
| MCP     | *not yet exposed* — the MCP server registers `naive_teams_status`, `naive_teams_board`, `naive_teams_run`, `naive_teams_comment` and `naive_teams_unblock`, and no submit tool |

```bash theme={"theme":"css-variables"}
naive teams submit support "refund the duplicate charge on invoice 4471" --tenant tu_8f21
naive teams submit support --brief ./brief.md --tenant tu_8f21 --task tsk_119
```

`--brief <file>` (or `-` for stdin) exists because prose on `argv` is mangled by every
shell — quoting, `!`, newlines. Use it for anything longer than a sentence.

`naive tasks create`, `naive ceo run` and `naive objectives create` all map to `submit`.
They keep working; see [Orchestration](/docs/getting-started/orchestration).

## Watching a run

```bash theme={"theme":"css-variables"}
naive teams runs support --tenant tu_8f21
naive teams watch support --tenant tu_8f21 --run run_2f9a --follow
```

<Warning>
  **`naive teams watch` works for a tenant on the durable runtime and refuses for one on
  hermes.** `GET …/runs/{id}/stream` proxies the runtime's own transcript frame for frame; for
  a hermes tenant it answers `501 not_configured` naming three things that would make the
  stream dishonest there — run-event frames are caller-forgeable on the legacy table, there is
  no `trace_id` column, and usage is recorded on `finish` so a long stream cannot be
  rate-limited. To read a finished run on either runtime, use `naive teams runs` and the paged
  transcript.

  The durable stream carries no `trace_id` either, and the response says so in a header rather
  than letting a consumer assume one is coming. It is bounded by the runtime rather than by
  naive: the Worker closes its own stream after 300s and rate-limits its front door.
</Warning>

`watch` requires `--run`. It emits NDJSON — one object per line — and is resumable:
`--cursor <seq>` is sent as `Last-Event-ID`, so a dropped connection resumes at a
**position**, not at a timestamp. `--fail-on-terminal` exits 3 when the final status is
`unverified`, `failed` or `blocked`, which is what you want in CI.

Tool arguments are printed as `args_digest` rather than as values. `--show-args` prints
them and requires an interactive terminal, so a CI log cannot accidentally capture
arguments that carried a secret.

## Approvals

Approvals are the one **write** on this surface that is fully served today.

```bash theme={"theme":"css-variables"}
naive teams approvals support --tenant tu_8f21 --status pending
naive teams decide support apr_71c2 --tenant tu_8f21 --allow --because "policy allows refunds under $50"
```

`--because` is **required**. The reason is recorded on the decision, so an approval that
was granted always carries why, and a later reader is not left inferring it.

`POST …/approvals/{id}/decide` is a second **address** for one write path, not a second
write path: it calls the same execute/deny code as
`/v1/users/{user_id}/approvals/{id}/approve`, so the human-resolver rule and the
no-self-approval rule hold identically. An agent cannot resolve its own approval through
this address either.

See [Approvals](/docs/getting-started/approvals) for the resolution rules.

## What answers today

**Served for every tenant, on either runtime**

| Operation                                         | CLI                          |
| ------------------------------------------------- | ---------------------------- |
| `GET …/tenants/{tu}` — the team/tenant header     | `naive teams show`           |
| `GET …/board`, `…/board/{card}`                   | `naive teams board` / `task` |
| `GET …/events`                                    | `naive teams events`         |
| `GET …/runs`, `…/runs/{id}`, `…/runs/{id}/events` | `naive teams runs`           |
| `GET …/roster`                                    | `naive teams roster`         |
| `GET …/cost`                                      | `naive teams cost`           |
| `GET …/diagnostics`                               | `naive teams diagnose`       |
| `GET …/plan`                                      | `naive teams plan`           |
| `GET …/approvals`                                 | `naive teams approvals`      |
| `POST …/approvals/{id}/decide`                    | `naive teams decide`         |

`board` and `board/{card}` answer from *different stores* depending on the runtime — the
durable runtime's own board for a durable tenant, the legacy mirror for a hermes one — which
is why every response names its `provider`.

**Served on the durable runtime; `501` on hermes, naming the legacy equivalent**

| Operation                                   | CLI                                   | On hermes                                                                                                                                            |
| ------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST …/submit`                             | `naive teams submit`                  | `POST /v1/tasks` creates a legacy card and stays available                                                                                           |
| `POST …/board/{card}/unblock`               | `naive teams unblock`                 | `POST /v1/tasks/{id}/unblock` stays available                                                                                                        |
| `POST …/sessions/{channel}/messages`        | `naive teams say`                     | no per-`(team, tenant)` session store exists                                                                                                         |
| `GET …/runs/{id}/stream`                    | `naive teams watch`                   | frames are caller-forgeable; no `trace_id`; not meterable on open                                                                                    |
| `POST …/schedule`, `DELETE …/schedule/{id}` | `naive teams schedule` / `unschedule` | `/v1/cron` schedules against the legacy runtime and stays available                                                                                  |
| `POST …/stop`                               | `naive teams stop`                    | a team-level stop is a state of the Durable Object; the legacy runtime has no equivalent row to set, and stops one run at a time through the sidecar |

**Refused for both, each naming what is absent**

| Operation                                     | What is missing                                                                                                                                                                                                                                                                                                                                           |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/teams` (enumeration)                 | a declared team's name is stored under a key the vocabulary gate has retired; no accessor may be added without widening that gate                                                                                                                                                                                                                         |
| `GET …/sessions`, `…/sessions/{channel}`      | the runtime has `channels` and `read` verbs; the control-plane translation is one-way — in, not out                                                                                                                                                                                                                                                       |
| `GET …/effects`, `POST …/effects/{id}/settle` | the runtime has `effects` and `effect-decide`; no control route reaches either. On hermes there is no effect ledger at all                                                                                                                                                                                                                                |
| `POST …/runs/{id}/stop`                       | there is no **per-run** kill, by design: a card in flight holds a lease and an attempt budget, and no abort channel reaches one. The team-level `POST …/stop` above is the interrupt — it stops the dispatcher claiming further work and fences it against re-arming, but does **not** recall an attempt already handed to a member; `start-loop` resumes |
| `POST …/model`                                | the runtime has `model`; no control route reaches it. On hermes it is a manifest field and no manifest is stored                                                                                                                                                                                                                                          |
| `POST …/apply`                                | the runtime has `apply` and refuses a stale digest; no control route reaches it. `GET …/plan` reports the digests it would compare                                                                                                                                                                                                                        |
| `POST …/migrate`, `POST …/rollback`           | migration is a control-plane act — it rewrites `company_containers.sidecar_url` — and is deliberately not tenant-addressable. It is also **not symmetric**: registering overwrites the tenant's Hermes coordinates, so returning means a fresh slot, a new task and a new volume                                                                          |

A refusal is a `501` with `error.details.missing` as a **list** of every unmet
prerequisite — deliberately not `404`, `403`, or an empty `200`. Where a refusal names a
legacy route it describes what *that tenant* has, not a recommendation to use it: writing
via `naive tasks create` goes to the frozen runtime, a different execution model with a
different governance path. A refusal from the runtime itself is carried through unchanged,
with `error.details.runtime_said` holding the runtime's own words.

## Next steps

* [Orchestration](/docs/getting-started/orchestration) — the frozen legacy runtime, and the mapping off it
* [Approvals](/docs/getting-started/approvals) — how a parked action is resolved
* [Brain](/docs/getting-started/brain) — the partition a team binds
* [Infrastructure as code](/docs/getting-started/iac) — the rest of `naive.config.ts`
