TeamAPI latest
On this page
  1. Trends
  2. Gaps
  3. Severity overrides and waivers
  4. Policy
  5. Topology
  6. Shadow AI

Organizational health

Since your org is just files in git, its history is git history. teamapi diff <patterns...> --against <ref> resolves the same patterns two ways — the working tree, and as they existed at any commit, tag, or branch — and reports what changed: teams added/removed, roles/members/services added/removed per team, cognitive-load deltas, and edge changes (interactions, dependencies, cross-team reporting lines). Requires running inside a git repository.

Example, run against this very repo — teamapi diff examples/acme-org --against 931fe6b (the initial commit, before the org-wide role hierarchy was added):

$ teamapi diff examples/acme-org --against 931fe6b
~ platform-payments
  + role added: head-of-engineering

Role edges:
  + reports-to stream-checkout.tech-lead -> platform-payments.head-of-engineering
  + aligns-with stream-checkout.tech-lead -> enabling-devex.coach
  + reports-to stream-onboarding.tech-lead -> platform-payments.head-of-engineering
  + aligns-with stream-onboarding.tech-lead -> enabling-devex.coach

When nothing has changed, it prints a single line rather than an empty report. Either way it exits 0 — this is an inspection tool, not a validation gate (see CI integration for that).

diff compares two points. Some questions only have an answer over time, and therefore had none: is cognitive load creeping up across quarters, is agent adoption accelerating, is supervision load growing without anybody scoring it, how much team churn is there really. teamapi history resolves the org at a series of past revisions and reports the series.

teamapi history examples/acme-org --period quarter --since "2 years ago"
      date  teams  people  services  vacant  load~  load^  over  agents  sup~  unscored  gaps!
2026-07-26      4       9         4       1  15.67     18     1       5     0         1      0
2026-07-31      4       9         4       1  15.67     18     1       5     6         0      0
2026-08-13      4       9         4       1  15.67     18     1       5     6         0      0

    change      0       0         0       0      0      0     0       0    +6        -1      0

That +6 in sup~ is the case this exists for: supervision load went from unmeasured to a mean of 6 across the org, and no single snapshot could have told you.

--period commit | day | week | month | quarter keeps the last commit in each period, so a row reads as "where the org ended up". --format csv for the spreadsheet this is going to end up in anyway, --format json for everything else. Revisions the current seed list can't resolve — the org had fewer teams then — are skipped with a note rather than failing the report.

Everything it reports is already in git. It just needed resolving at more than one point, with the same resolver the live graph uses: a historical snapshot built by a simpler one would differ from today's for reasons that have nothing to do with the org changing.

Gaps#

Every other check here compares the spec to an outside system. This one compares it to itself, because the holes it looks for are invisible from any single teamapi.yml — each document is individually valid, and the gap only appears once the graph is resolved. A service subscribing to an event nobody publishes reads as complete from inside the subscriber. A vacant seat reads as ordinary from inside the team that declared it; it's the two other teams reporting into it that make the vacancy load-bearing.

teamapi gaps examples/acme-org
- unconsumed-event: 'ledger' publishes 'LedgerEntryPosted', which no declared service subscribes to
- unconsumed-event: 'checkout-api' publishes 'OrderPlaced', which no declared service subscribes to
? vacant-load-bearing: 'head-of-engineering' on platform-payments is vacant, but stream-checkout, stream-onboarding report(s) into it
~ unacknowledged: stream-checkout declares a collaboration with stream-onboarding, which declares nothing back

4 finding(s), 0 blocking; 9 seam(s) checked.

Only orphan-subscription and dangling-owner exit non-zero, and they share a shape: the declaration looks complete and isn't. An agent whose ownerId resolves to nobody presents to every downstream consumer — AGENTS.md, the context bundle, a generated crew — exactly like an agent with a real human behind it, which makes it strictly worse than an agent with no owner at all. Only collaboration is expected to be mutual; x-as-a-service is deliberately one-directional, so consuming a platform is never reported. Pure, offline, no token — so unlike the drift checks it's also served over HTTP as GET /gaps and as the get_org_gaps MCP tool, which is what lets an assistant answer "what is nobody responsible for here?" without being handed a report. Details in docs/integrations/gaps.md.

Severity overrides and waivers#

Running teamapi gaps against a long-lived org reports years of accumulated findings at once. If a new check immediately turns red with dozens of old findings, teams tend to switch it off. A teamapi.config.yml, found by walking up from the working directory, can defer specific findings without disabling the check:

gaps:
  severity:
    # This org treats every published event as a public contract; nobody consuming one is fine.
    unconsumed-event: "off"
    orphan-subscription: warning # graded down from blocking
  waivers:
    - kind: dangling-owner
      teamId: platform-data
      subject: pipeline-reviewer
      reason: Dana left; replacement owner named in Q3 planning
      expires: "2026-12-31"

The two do different jobs. severity re-grades a whole kind, permanently — the org saying this class of thing is, or isn't, a gate for us. Waivers exempt one specific finding, temporarily, with a reason.

= waived dangling-owner: agent 'pipeline-reviewer' is owned by 'dana-whitfield', who is not a member of platform-data (Dana left; replacement owner named in Q3 planning, until 2026-12-31)
! expired waiver for orphan-subscription: expired 2026-05-31, 1 finding(s) reported again

! orphan-subscription: 'feature-store' subscribes to 'ModelTrained', which no declared service publishes
...

Waivers expire so that somebody reviews the recorded reason again. An exemption with no expiry would effectively delete the finding. A lapsed waiver is reported on its own line before it can quietly turn the build red.

reason is mandatory. A waiver without one is indistinguishable, six months later, from one added to make a build pass.

Waivers that match nothing are reported too (- unused waiver … matched nothing, delete it), so the file doesn't silently accumulate exemptions for gaps that were fixed years ago. And an unknown gap kind is an error, not a shrug — a waviers: typo that does nothing while the org believes a rule is in force is worse than no config at all.

--config <file> points at a specific file; --no-config ignores any and reports everything at its declared severity.

Policy#

policies[] supports governance enforced by external automation. A rule such as min_approvals describes branch protection, which the graph cannot verify. Other policies describe the org graph itself, such as "no agents on this team" or "every service names a repository." TeamAPI can evaluate those rules completely offline and without credentials.

teamapi policy examples/acme-org
~ delegated [info] platform-payments / pr-requires-two-approvals / min_approvals: not checkable from the org graph; enforced by github-actions:pr-gate

1/1 rule(s) checked here pass; 2 rule(s) declared in total.
1 finding(s), 0 blocking.

Every rule lands in one of five outcomes:

Outcome Meaning
satisfied A built-in evaluator ran and the team complies
violated A built-in evaluator ran and the team does not
delegated No evaluator here, but enforcedBy names the automation that does check it
unenforced No evaluator here and no enforcedBy — nothing, anywhere, checks this
misconfigured An evaluator exists, but the rule's value is the wrong shape for it

The command exists to find unenforced policies. Inside the document, an unenforced policy looks complete: it has the same severity: blocking and confident prose as an enforced one, while behaving only as a comment. TeamAPI reports it at its declared severity, and a blocking policy exits non-zero.

delegated never fails a build: naming an external enforcer is the right thing to do, not a finding. It's reported at info and deliberately kept out of the "checked here pass" ratio, so that number never implies this tool verified something it didn't. misconfigured stays at warning even on a blocking policy — a typo in a document isn't evidence a team is out of compliance.

The rule keys with built-in evaluators:

Key Value Checks
agents_allowed boolean false forbids the team from running active agents
max_agents number Active agent count is at or below the limit
agents_require_owner boolean Every agent's ownerId resolves to a member of the team
allowed_agent_providers string[] Active agents only use approved providers
max_cognitive_load number The three-type total is at or below the limit
max_supervision_load number cognitiveLoad.supervision is at or below the limit
required_steering_categories string[] Effective steering (including inherited) covers each category
required_playbook_categories string[] The team declares a playbook in each category
services_require_repository boolean Every service names a repository
services_require_bounded_context boolean Every service declares a boundedContext
max_dependencies number Outgoing dependency count is at or below the limit

The set is deliberately small, and every key on it is fully decidable from the graph. A rule that can only be half-checked here is worse than one that's honestly delegated: a partial check reporting "satisfied" is how a policy stops being read.

Wire it into CI with check-policies: true on the bundled action.

Topology#

gaps finds things nobody owns. topology checks the shape of work that is already owned and declared. It detects the Team Topologies design smells for which the schema already carries enough information.

teamapi topology examples/acme-org
! collaboration-overrun: stream-checkout: collaboration with Stream Onboarding was due to end 2026-06-29 and is still declared

1 finding(s), 0 blocking; 4 team(s) checked.
Kind What it means
collaboration-overrun A collaboration past the duration it declared for itself
collaboration-untimed A collaboration that never said when it should end
collaboration-overload A team in more concurrent collaborations than it can sustain (default 3)
team-too-large A team past the size at which it holds shared context (default 9)
platform-depends-on-stream A platform team depending on a team it exists to serve
blocking-dependency A dependency the team itself labelled Blocking

The collaboration checks are the ones worth having. Team Topologies is emphatic that collaboration is the expensive mode — high bandwidth, both teams paying for it — and therefore deliberately temporary. A collaboration with no expectedDuration isn't a collaboration, it's two teams that have merged without saying so; one still declared six months past its end date is the same thing arrived at by drift. Both are invisible until something reads the dates, which is what this does.

platform-depends-on-stream catches inverted flow: a platform exists to be consumed, so one that depends on a team it serves has the consumer waiting on the platform which is waiting on the consumer.

Every default finding is a warning and exits 0. A nine-month collaboration, for example, may be intentional, so these findings call for review without declaring a defect. Orgs can configure thresholds and severities when they decide a finding should gate changes:

topology:
  maxTeamSize: 7
  maxCollaborations: 2
  severity:
    collaboration-untimed: blocking
    blocking-dependency: "off"

Shadow AI#

Paperclip drift answers "which agents are running that nothing declares" — for one runtime, behind one gateway. Most shadow AI never reaches a runtime. It's a .mcp.json somebody committed during a crunch, an SDK added to a manifest, a workflow step that calls a model. None of those needed anyone's approval, which is why they spread faster than the process meant to sanction them — and all of them are checked into git, so they can be read off the same source of truth as everything else.

teamapi shadow-ai examples/acme-org --scan ~/src
+ undeclared: 'checkout-api' carries AI artifacts (CLAUDE.md, package.json (openai)) but stream-checkout declares no agents[]
? unowned: 'legacy-batch' carries AI artifacts (.github/workflows/ai.yml (anthropics/claude-code-action@v1)) but no team declares the repository
! forbidden: 'onboarding-api' carries AI artifacts (.mcp.json) but stream-onboarding's policy 'no-agents-on-applicant-pii' forbids agents

3 finding(s), 1 blocking; 1 repo(s) matched, 1 quiet.

--scan reads repository checkouts already on disk — no clone, no fetch, no token. Only forbidden exits non-zero: undeclared usage is a conversation, but a team that wrote down "no agents on this code" in review and has one anyway is not a documentation problem. The report counts quiet repos separately and names that number when it finds nothing, because this detects declaration, not use — a clean result over an empty tree must not read like a clean bill of health. Details in docs/integrations/shadow-ai.md.