Outfitter Architecture
Synced from
outfitter/docs/architecture/README.md. The repository is the source of truth.
Purpose
Section titled “Purpose”Outfitter is a TypeScript CLI that resolves, composes, bakes, and launches agent configuration stored in the Dotagents .agents protocol (pinned at revision 502a9d5).
It is generic enough for organizations to define resources once and run them across multiple agent CLIs, while supporting pi first and most deeply, plus Claude Code as an additional supported adapter.
Outfitter’s differentiated value is source resolution, selective composition, task baking, validation, provenance, and harness projection — not ownership of a configuration format. The protocol tree is the source of truth and remains useful without Outfitter.
Formal implementation requirements live in ../requirements/; note the transition status — this document describes the RFC #165 target architecture.
Architectural Principles
Section titled “Architectural Principles”- TypeScript first: production code, tests, schemas, and tooling are centered on TypeScript.
- Protocol-native storage: all authored configuration is a protocol
.agentspayload. Outfitter-specific metadata (settings, provenance) is optional, namespaced, and removable without losing the underlying resources. No parallel manifest, persona, or task YAML formats. - Pin the protocol: the implementation and its conformance fixtures target one pinned protocol revision.
Protocol upgrades are deliberate, reviewed changes — never silent tracking of upstream
main. - One resolver: listing, validation, task baking, running, and dumping share a single effective-resolution path. There is no code path that composes resources differently from another.
- Deterministic composition: identical sources, refs, selections, and inputs produce identical results — the same effective identity for a run, and byte-identical output for a dump.
- Generic composition, adapter projection: compositions are harness-neutral; adapters project them to agent-specific files, flags, and environment variables.
If a composition asks for something an adapter cannot project, Outfitter warns to stderr;
--strictmakes it fatal. - Pi-native by design: pi is not merely the first adapter — targeting pi natively and completely is a deliberate design choice. Pi’s configuration surface (extensions, plugins, subagents, keybindings, bootstrap) is treated as first-class, and the composition model is validated against it before being generalized to other harnesses. Claude Code and Codex CLI are supported additional adapters with documented gaps, not co-equal design targets; Codex currently projects only model and MCP servers.
- Command objects for complexity: non-trivial CLI commands are implemented as command objects with explicit dependencies and typed inputs/outputs.
- Schema validation at boundaries: every persisted file format read by Outfitter is validated at the read boundary (JSON Schema for JSON and YAML files, frontmatter schemas for markdown resources).
- Complete test coverage early: the project maintains a test framework with a 100% global coverage requirement.
- Complexity limits early: ESLint is configured with maximum complexity
10.
Runtime and Tooling Baseline
Section titled “Runtime and Tooling Baseline”- Runtime: Node.js
>=24.18.0 <25, tested with the exact version pinned in.node-version. - Language: TypeScript. Package manager: npm.
- CLI framework: Commander
^14(default commands, aliases,allowUnknownOption, pass-through argument collection, testable parser construction). - Test framework: Vitest
^4with@vitest/coverage-v8; 100% global coverage thresholds. - Linting: ESLint
^10,@eslint/js,typescript-eslint,complexity: ["error", 10]. - Schema and validation: TypeBox for schema authoring, JSON Schema artifacts for persisted contracts, AJV for runtime validation.
- YAML (
yaml) for Outfitter settings; JSON for protocol files (mcp.json,models.json,config.json) per the protocol. - Merge behavior:
defufor settings; protocol merge-by-ID and JSON merge semantics implemented per the pinned revision with conformance fixtures. - Process launch:
cross-spawn. Discovery and URI parsing:glob,hosted-git-info. Diagnostics:chalk. Templating of generated harness files:liquidjswith Outfitter-specific delimiters.
The CLI workspace includes code/cli/tsconfig.json for strict typechecking and code/cli/tsconfig.build.json for production emission from code/cli/src/ to code/cli/dist/.
Repository File Structure
Section titled “Repository File Structure”The repository layout and source/test directory boundaries live in ./file_structure.md.
Onboarding
Section titled “Onboarding”Pi-native first-run onboarding, .agents detection, and the ~/.claude porting flow live in ./onboarding.md.
Resource Model
Section titled “Resource Model”Outfitter resolves protocol resources from layered .agents trees:
~/.agents/ # global layer agents.md system-prompt.md mcp.json models.json agents/<id>/agent.md # + optional config.json skills/<id>/... tasks/<id>/task.md knowledge/... commands/... settings.yml # Outfitter settings (namespaced, optional) settings.local.yml # gitignored machine-local overrides cache/ # synced remote sources (Outfitter-owned)
<project>/.agents/ # workspace overlay: same shape, minus cache settings.yml settings.local.ymlRemote sources (catalog repositories) supply additional layers below the local ones, in configured order.
A standalone catalog repository’s root is the payload; a colocated source nests it under .agents/ (selected with the source’s path:).
Resolution semantics
Section titled “Resolution semantics”- Resources merge by ID across layers: workspace over global over remote sources in configured order. The winning definition replaces lower ones; there is no partial merge of markdown resources.
- After layer resolution, an agent’s
inheritsgraph resolves through that same effective set. Traversal is recursive parent-first, multiple parents retain authored order, diamond ancestors contribute once, and cycles or missing parents fail with their chain. - Inherited selections retain the declaring agent and winning layer. This provenance is required for parent-local skills, per-agent configuration, and catalog prompt files to resolve against their owner rather than the selected child.
- JSON files (
mcp.json,models.json, per-agentconfig.json) follow the protocol’s JSON merge behavior across layers. - The resolver produces one immutable effective resource set per invocation: every slug mapped to its winning source, with shadowed definitions retained for diagnostics.
list,validate,run, anddumpall consume the same effective resource set. This is a hard invariant, enforced by sharing oneResolvercomponent.- Conformance fixtures (layer trees + expected effective output) pin the merge semantics to the protocol revision; a protocol upgrade updates fixtures in the same change.
Settings
Section titled “Settings”Outfitter settings are the only Outfitter-specific files in a tree: settings.yml plus a flat sibling settings.local.yml at both scopes.
There is no nested local settings directory.
Precedence
Section titled “Precedence”Highest to lowest:
<project>/.agents/settings.local.yml<project>/.agents/settings.yml~/.agents/settings.local.yml~/.agents/settings.yml- Cached remote settings referenced by
remote_settings - Built-in defaults
The internal Settings object is the single conceptual result of reading all settings sources and applying precedence.
Every settings file MUST validate against settings.schema.json.
Schema
Section titled “Schema”default_agent: engineer # which agent runs by defaultdefault_harness: pi # which harness to launch: pi, claude, or codexcache_directory: ./cache
sources: - github: ai-outfitter/.agent ref: <commit-sha> - github: example/payments-service ref: v1.2.0 path: .agents - uri: git+https://git.example.com/team/agents.git ref: main - path: ../shared-agents
remote_settings: - github: example/.outfitter ref: <commit-sha> path: .agents/settings.yml
state_persistence: {}custom_settings: {}Rules:
- Settings do not carry resource selections.
An agent’s loadout — skills, subagents, mcp, extensions, plugins, model, thinking, tools — is declared on the agent (
agents/<id>/agent.mdfrontmatter orconfig.json), not in settings. There is noprofilesmap and nodefault_profile. default_agentnames the agent plainoutfitterruns;default_harnessselects the harness (pi,claude, orcodex).sourcesentries MUST specify a localpath, a remoteuri, or agithubshorthand; remote entries acceptrefand payload-subdirectorypath. Relativepathvalues resolve relative to the settings file containing them.remote_settingsentries point at settings-style YAML files inside synced remote repositories; fetched byoutfitter syncand loaded at lower precedence.cache_directoryselects the repository cache root for sync, remote-settings loading, remote layer discovery, and default-catalog bootstrap. Its default is~/.agents/cache.custom_settingsmay contain arbitrary YAML-compatible nested data, deep-merged by precedence.
Composition
Section titled “Composition”For a selected agent, the composer builds the run’s effective configuration from the effective resource set:
- Identity: the nearest inherited
system_promptdeclaration (or tree rootsystem-prompt.md) is the base; rootagents.mdfollows; inherited/childappend_system_promptfragments and inherited/child agent bodies append parent-first. Each fragment carries source, owner, layer, and trust provenance. - Prompt trust:
{ file }reads required trusted catalog content inside the declaring agent’s winning layer.{ repo_file }reads optional untrusted active-repository content inside the project root. Both are checked after symlink resolution; named prompt slugs are not part of this contract. - Subagents: agents named in the selected agent’s
subagentsloadout are marked for projection into the harness’s delegation surface; their own selected skills are resolved and materialized separately from the leader’s active skills. - Skills: skills named in the loadout are materialized (frontmatter
references/scripts/assetsresolved from their two-root model, validated for escapes and collisions) into generated skill directories. - Harness elements: extensions and plugins named in the loadout are marked for the adapter (first-class for pi).
- Structured configuration:
models.json,mcp.json, and per-agentconfig.jsonmerge per protocol JSON semantics; the loadout’smodel,thinking, andtoolsselect and constrain within them. Aprovider/modelselection resolves once into a harness-neutral model target (dialect, endpoint, credential-variable reference, headers, capabilities, and provenance); adapters translate that target to native controls without changing its endpoint.
The composed result is a harness-neutral composition plan — the input to dump and adapter projection.
Tasks and bake — freezing a named work contract and its typed inputs into an immutable execution artifact — are the subject of a separate upcoming RFC and are not part of this architecture yet. The
ai-outfitter/actionsAction currently launches a composed agent headlessly with structured inputs through the same resolver.
outfitter dump writes the effective resource set (or one selection’s transitive closure) as a plain .agents/ directory.
The removable .agents/.outfitter/composition.json audit record lists each composed agent’s resolved inheritance chain and ordered prompt provenance without prompt contents or absolute paths.
- Deterministic: byte-identical for identical inputs.
- Safe: never contains credentials, auth state, sessions, transcripts, caches, backups, mutable harness state, or symlinks resolving outside the tree. Dump shares the state-path knowledge of the adapters to guarantee exclusion.
- Protocol-shaped: valid payload for any protocol consumer; Outfitter provenance metadata is optional and removable.
Generated-File Templating
Section titled “Generated-File Templating”Generated harness files may use LiquidJS templating with Outfitter-specific delimiters ([[= expression ]], [[% tag %]]) so templates do not collide with agent prompt, skill, Handlebars, Mustache, Jinja, or Claude Code syntaxes.
Plain [[ -f file ]] shell text passes through unchanged.
The template context exposes resolved custom_settings, settings, the composition, the agent id, and the project root.
Undefined output variables and unknown filters are template errors; undefined variables in conditions evaluate falsy.
Source files are never rewritten; templating applies only to generated composition files.
Runtime Model and State
Section titled “Runtime Model and State”A launch materializes the composed agent under the system temp directory:
$TMPDIR/outfitter-<agent>-<harness>-<random>/Adapter-declared state paths symlink to durable native CLI locations (~/.pi/agent/..., ~/.claude/...) per the functional state model — see ./state_writeback_strategy.md.
State handling strategies are symlink, discard, warn, error, and prompt; unknown writes are never silently persisted.
state_persistence policy lives in settings.
The Pi adapter reconciles generated runtime settings.json and keybindings.json (reserving Shift+Tab for Outfitter mode switching) as non-durable generated files.
Interactive Pi launches inject the Outfitter bootstrap extension for build/plan mode switching; non-interactive launches skip it.
After projection and bootstrap attachment, Outfitter reads root-owned system extension hooks and prepends their Pi extensions to every launch plan, including non-interactive and RPC launches. This post-projection boundary keeps the hook out of .agents layers, settings, composition, loadout support checks, and --strict handling. It is launcher-scope default injection rather than managed enforcement; the hook source is stamped into the child environment so downstream evidence can identify system-path versus session-provided configuration.
During outfitter run, the Outfitter process remains alive while the child agent CLI runs and owns the temporary composition lifecycle.
Agent Adapter Boundary
Section titled “Agent Adapter Boundary”Each supported CLI has an AgentAdapter implementation that projects a composition plan to native configuration:
interface AgentAdapter { readonly id: string; readonly supportedElements: readonly string[]; readonly statePaths?: Readonly<Record<string, StatePathDeclaration>>; createProjection( composition: CompositionPlan, input: { readonly rootDirectory: string; readonly homeDirectory?: string; readonly cacheDirectory?: string; readonly settings?: Settings; readonly projectDirectory?: string; }, ): AgentProjectionPlan; createLaunchPlan(projection: AgentProjection, passThroughArgs?: readonly string[]): AgentLaunchPlan; getUnsupportedElements(composition: CompositionPlan): readonly string[];}The adapter owns CLI-specific details: env vars, flags, state path declarations, warnings, and unsupported elements.
Pi is the default adapter.
Outfitter prefers native pi mechanisms: PI_CODING_AGENT_DIR, PI_CODING_AGENT_SESSION_DIR/--session-dir, --extension, --skill, --prompt-template, --system-prompt/--append-system-prompt, model/provider/thinking flags, and generated settings reconciliation where flags are not the right mechanism.
Startup-order-impossible requests surface as adapter warnings (--strict makes them fatal).
Because the projection root is removed after the run, launches default PI_CODING_AGENT_SESSION_DIR to pi’s durable per-project session folder (~/.pi/agent/sessions/--<encoded cwd>--) so sessions outlive the projection and stay shared with a standalone pi; an inherited PI_CODING_AGENT_SESSION_DIR is left in place, and pass-through --session-dir/--no-session still win by pi’s own precedence.
If generic naming conflicts with pi behavior, prefer pi’s terminology.
Claude Code
Section titled “Claude Code”Outfitter launches claude over the user’s own Claude configuration by default, passing the projection root through --plugin-dir, and maps the elements supported by the current baseline adapter to native files and flags. An isolated run (--isolated) points CLAUDE_CONFIG_DIR at the projection root instead, making the composition the whole configuration.
Persistent state projection and managed harness symlinks are deferred to #187; setup does not create them.
Codex CLI
Section titled “Codex CLI”Outfitter launches codex, maps model selection to -m, and injects selected MCP servers through repeated TOML-valued -c mcp_servers.<id>.<key>=... overrides. The overrides merge with Codex user and project configuration because Codex has no way to run with only the projected MCP servers, so every launch warns that lower-layer servers remain active, including when the composition selects no servers. Codex cannot project the composition identity and reports it unsupported on every launch, so --strict aborts every Codex launch. Pass exec through to select Codex’s non-interactive subcommand; without it, the normal interactive CLI launches. Other loadout elements remain unsupported and use the normal warning mechanism.
CLI Commands
Section titled “CLI Commands”outfitter run [agent](default command): resolve → compose → project → launch. A positional agent slug selects what to run (default from settingsdefault_agent);--harness <pi|claude|codex>selects the harness (default from settingsdefault_harness, thenpi); unknown args pass through to the child CLI;--strictmakes warnings fatal. Interactive clean-home launches start Pi-native onboarding; non-interactive clean-home launches requireoutfitter setupfirst.outfitter setup [source]: launch the bundled, model-free Pi walkthrough with the original three setup modes (default catalog, create a profile, or another catalog), original profile/target wording, and optional direct-source path; append only the default CLI-agent choice (Pi/Outfitter preselected); then atomically apply the result through the CLI state machine.outfitter sync: validate local settings; atomically fetch configured remote settings; reload the merged settings; then atomically fetch every resulting remote source into<cache_directory>/repos/<encoded-uri-and-ref>/and record its verified commit under<cache_directory>/source-state/. Validate before swapping, preserve the last good checkout on failure, report per-source status, and redact embedded credentials from all output. Required-source failures exit nonzero.outfitter run: enforce the selected source-cache policy before resolution. Healthy manifest-backed caches stay network-free; repair mode atomically rebuilds unhealthy remote caches, locked mode requires full commit pins, and offline mode fails closed without network access. Explicit local paths are never cache-managed.outfitter sources: report local, direct, remote-settings, and transitive source precedence plus credential-redacted cache health and revisions.outfitter list [kind]: report the effective resource set — slugs, winning sources, shadowed definitions — deterministically.outfitter validate [--strict] [--json]: protocol layout, frontmatter and JSON schemas, unresolved loadout slugs, skill reference escapes/collisions, settings schema.outfitter dump [--agent <id>] [--out]: write the deterministic tree, optionally restricted to one agent’s transitive closure.
outfitter task bakeis deferred to the tasks RFC.
Command Object Pattern
Section titled “Command Object Pattern”Each non-trivial command is a command object with explicit dependencies (SettingsLoader, Resolver, Composer, Baker, ProcessRunner) and typed inputs/outputs — no direct process.argv access inside command logic.
Benefits: unit testability, low parser coupling, explicit dependencies, natural complexity limits.
Validation Strategy
Section titled “Validation Strategy”Validation happens at every file boundary: settings (settings.schema.json), protocol JSON files (schemas per the pinned revision), resource frontmatter, task input contracts, command inputs, and cache metadata.
Diagnostics point to the file and key that failed where practical.
Test Strategy
Section titled “Test Strategy”- Global coverage threshold: 100% for statements, branches, functions, and lines.
- Protocol conformance fixtures: layered
.agentstrees with expected effective resolution output, pinned to the protocol revision. - Deterministic tests for settings precedence, task input validation, bake determinism (content-hash stability), dump byte-identity and hygiene (no credentials/state), cache path encoding, generated launch env/argv, and unsupported elements with
--strict. - Scenario fixtures for common combinations instead of one-off bespoke setup; conventions in
./file_structure.md.
Settled Decisions
Section titled “Settled Decisions”- npm with committed
package-lock.json; Commander; Vitest with V8 coverage. - Resource IDs are filesystem-safe slugs; display names live in frontmatter.
- The protocol revision is pinned; upgrades are explicit, fixture-backed changes.
- There is no legacy profile compatibility layer: no readers, writers, aliases, detection, or migration commands. The only old-version material is the manual migration reference routed from the bundled Outfitter skill.
- A remote repository named
.outfitteris supported only as a distribution convention for the new protocol payload. - Claude Code and Codex CLI are supported as additional adapters; Codex currently projects only model and MCP servers, and pi remains the default.