Skip to content

Agent Frontmatter Schema

Canonical schema for every agents/*.md file shipped by nubos-pilot. Enforced by lib/agents.cjs at load time.

Canonical schema

yaml
---
name: <string>              # required; MUST equal the filename stem
description: <string>       # required; single-line summary
tier: haiku|sonnet|opus     # required; see Tier Enum (D-11)
tools: <comma-list string>  # required; e.g. "Read, Write, Bash, Grep"
color: <string>             # optional; UI hint only
---

The agent body follows the closing ---. Body content is free-form Markdown, passed verbatim to the runtime when the agent is spawned.

Required fields

Every required field must be present with a truthy value. Missing or empty fields throw NubosPilotError('agent-invalid-frontmatter', …) with details.field set to the offender.

FieldTypeValidator ruleExample
namestringMust equal the filename stem (e.g. planner.mdplanner).name: planner
descriptionstringNon-empty single-line summary; shown in listAgents UIs.description: Creates executable phase plans …
tierenum stringMust be one of haiku, sonnet, opus.tier: opus
toolscomma-list stringFlat comma-separated list; parsed later by the runtime adapter.tools: Read, Write, Bash, Glob, Grep

Order of checks inside validateAgentFrontmatter: REQUIRED → FORBIDDEN → TIER_ENUM → name-match. First failure throws; the remaining checks are short-circuited.

Forbidden fields

Presence of any of these fields — even with a falsy value — throws NubosPilotError('agent-forbidden-field', …) with details.field and details.hint.

FieldWhy forbiddenHint returned
modelModel routing is tier-based. A concrete model id bypasses the tier abstraction and breaks multi-runtime adapters.Use "tier" instead.
model_profileSame reason as model: profile-based selection is an out-of-band concern resolved by np-tools.cjs resolve-model at spawn time; tier is the single source of truth inside the agent file.Use "tier" instead.
hooksRuntime-specific syntax (Claude Code ≠ Codex ≠ Gemini). Hooks live in the runtime-adapter layer; they are NOT part of the portable agent contract.hooks are runtime-specific and live in lib/runtime/.

The FORBIDDEN list is what makes the contract testable. Any agent file that slips a forbidden field in is rejected at load time, before the runtime adapter ever sees it.

Tier enum

TIER_ENUM = ['haiku', 'sonnet', 'opus']. Any other value throws NubosPilotError('agent-invalid-tier', …) with details.value (the offending input) and details.allowed (the canonical enum).

Tier assignments for every shipped agent:

AgentTier
np-plan-checkeropus
np-planneropus
np-architectsonnet
np-build-fixersonnet
np-codebase-documentersonnet
np-criticsonnet
np-critic-acceptancesonnet
np-critic-testssonnet
np-executorsonnet
np-researchersonnet
np-researcher-reconcilersonnet
np-security-reviewersonnet
np-verifiersonnet
np-critic-stylehaiku
np-nyquist-auditorhaiku
np-sc-extractorhaiku

The table above is regenerated from agents/*.md frontmatter by scripts/generate-docs.cjs. Rationale notes (why a given agent runs at a given tier) live in Catalog.

Model resolution

The concrete model ID is resolved at spawn time, not in the agent file. Workflows call:

bash
node np-tools.cjs resolve-model <agent-or-tier> --profile <profile>

Tier × Profile matrix (from lib/model-profiles.cjs):

Tier / Profilefrontierqualitybalancedbudgetinherit
opusopusopusopussonnet'' (runtime default)
sonnetopussonnetsonnethaiku'' (runtime default)
haikuopussonnethaikuhaiku'' (runtime default)

Profile defaults to frontier from .nubos-pilot/config.json. inherit emits an empty string, which the runtime adapter interprets as "omit the model: parameter at spawn and use the host's default".

Plan-checker finding categories

Canonical identifiers for findings that np-plan-checker emits:

  • missing-success-criterion — a success criterion (milestone-level or slice UAT) is not covered by any task.
  • non-atomic-task — a task bundles multiple distinct deliverables that should be split.
  • unbounded-scope<action> uses words like "etc.", "and related", "as needed" without concrete enumeration.
  • broken-dependencydepends_on references a task that does not exist, or points at a same-slice task (tasks within one slice are parallel by contract), or points backward (cross-slice deps must flow forward).
  • cyclic-dependency — cross-slice dependency graph has a cycle.
  • fake-promotion-trigger — a <task> block is missing one of the required attributes (id, depends_on, wave, tier) or wave does not match the enclosing slice number.
  • missing-coverage-annotation — a task modifies production code without a tdd="true" marker or a <verify><automated> command (Nyquist rule).
  • bare-askuser-call — workflow MD emits a bare host-specific prompt tool instead of node np-tools.cjs askuser --json '{…}'.
  • hook-field-present — agent frontmatter contains hooks:.
  • forbidden-agent-field — agent frontmatter contains model: or model_profile:.

Each finding carries one of these codes plus an anchor {file, line} pair.

Validation flow

validateAgentFrontmatter(fm, agentName) runs four gates in strict order, throwing the first violation and skipping the rest:

  1. REQUIRED — every field in REQUIRED = ['name', 'description', 'tier', 'tools'] must be truthy; otherwise agent-invalid-frontmatter with details.field.
  2. FORBIDDEN — no field in FORBIDDEN = ['model', 'model_profile', 'hooks'] may be defined; otherwise agent-forbidden-field with details.field and details.hint.
  3. TIER_ENUMfm.tier must be in TIER_ENUM = ['haiku', 'sonnet', 'opus']; otherwise agent-invalid-tier with details.value and details.allowed.
  4. Name matchfm.name must equal the agentName passed in (which loadAgent derives from the filename stem); otherwise agent-invalid-frontmatter with details.field === 'name', details.expected, details.got.

All error codes are stable identifiers; callers (workflows, plan-checker, test suites) match on err.code verbatim rather than on message strings.