Appearance
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.
| Field | Type | Validator rule | Example |
|---|---|---|---|
name | string | Must equal the filename stem (e.g. planner.md → planner). | name: planner |
description | string | Non-empty single-line summary; shown in listAgents UIs. | description: Creates executable phase plans … |
tier | enum string | Must be one of haiku, sonnet, opus. | tier: opus |
tools | comma-list string | Flat 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.
| Field | Why forbidden | Hint returned |
|---|---|---|
model | Model routing is tier-based. A concrete model id bypasses the tier abstraction and breaks multi-runtime adapters. | Use "tier" instead. |
model_profile | Same 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. |
hooks | Runtime-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:
| Agent | Tier |
|---|---|
np-plan-checker | opus |
np-planner | opus |
np-architect | sonnet |
np-build-fixer | sonnet |
np-codebase-documenter | sonnet |
np-critic | sonnet |
np-critic-acceptance | sonnet |
np-critic-tests | sonnet |
np-executor | sonnet |
np-researcher | sonnet |
np-researcher-reconciler | sonnet |
np-security-reviewer | sonnet |
np-verifier | sonnet |
np-critic-style | haiku |
np-nyquist-auditor | haiku |
np-sc-extractor | haiku |
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 / Profile | frontier | quality | balanced | budget | inherit |
|---|---|---|---|---|---|
| opus | opus | opus | opus | sonnet | '' (runtime default) |
| sonnet | opus | sonnet | sonnet | haiku | '' (runtime default) |
| haiku | opus | sonnet | haiku | haiku | '' (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-dependency—depends_onreferences 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) orwavedoes not match the enclosing slice number.missing-coverage-annotation— a task modifies production code without atdd="true"marker or a<verify><automated>command (Nyquist rule).bare-askuser-call— workflow MD emits a bare host-specific prompt tool instead ofnode np-tools.cjs askuser --json '{…}'.hook-field-present— agent frontmatter containshooks:.forbidden-agent-field— agent frontmatter containsmodel:ormodel_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:
- REQUIRED — every field in
REQUIRED = ['name', 'description', 'tier', 'tools']must be truthy; otherwiseagent-invalid-frontmatterwithdetails.field. - FORBIDDEN — no field in
FORBIDDEN = ['model', 'model_profile', 'hooks']may be defined; otherwiseagent-forbidden-fieldwithdetails.fieldanddetails.hint. - TIER_ENUM —
fm.tiermust be inTIER_ENUM = ['haiku', 'sonnet', 'opus']; otherwiseagent-invalid-tierwithdetails.valueanddetails.allowed. - Name match —
fm.namemust equal theagentNamepassed in (whichloadAgentderives from the filename stem); otherwiseagent-invalid-frontmatterwithdetails.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.
