Configuration
Configure
Run phax init to create phax.json and phax.schema.json in the current directory. When stdin is a TTY it launches an interactive wizard (like npm init) that prompts for the project slug, gate commands, and optional compliance/publish toggles:
In a non-TTY environment (CI, pipes) phax init automatically falls back to detected defaults with all optional toggles off — it never hangs waiting for input.
The wizard pre-fills the project slug from package.json's name field (slugified), detects the package manager from the packageManager field, and suggests gate commands from existing scripts (typecheck, lint, test:unit, format:check, build). It writes phax.json, phax.schema.json, and phax.user.schema.json.
phax.schema.json is a JSON Schema generated from the installed binary's config contract — wire it up as "$schema": "./phax.schema.json" for editor validation. After upgrading phax, run phax schema upgrade to regenerate it (see Schema upgrade).
Or add a phax.json manually at your repo root:
Each gate profile is a named list of attributed steps, not a flat command list. Every step carries these dimensions:
surface— a closed enum,local | structural | product, describing what the step verifies (local dev checks, structural/repo-wide checks, or product/build output). This is pure attribution: phax records it and never branches on it.firing—every-phase | terminal. This is behavioral:every-phasesteps run at every phase gate;terminalsteps run only at the final phase gate, in addition to the every-phase steps.output— optional,log | diagnostics, defaults tolog. A"log"step's stdout/stderr are appended to the attempt log as raw text, same as today. A"diagnostics"step's stdout is decoded as a JSON document{ "diagnostics": [{ "rule", "class": "invariant"|"completion", "scopes"?: [...], "location": { "file", "line"? }, "message", "repair" }, ...] }; the verdict comes from that document instead of the exit code. Every diagnostic declares aclass: an"invariant"diagnostic always fails the step. A"completion"diagnostic names one or morescopesand fails the step only once every scope it names is closed, as reported by the registeredscopesprovider (see Scope provider) — otherwise it is pending: it does not fail the phase, and is shown to the fix-loop agent as optional work. A step with only pending diagnostics recordspending(notpass/fail) ingate-attribution.jsonand never counts its surface as verified. Pending findings are persisted aschecks-attempt-NN.pending.jsonnext to the attempt log; a failing document is persisted aschecks-attempt-NN.diagnostics.json, and its failing diagnostics — not the raw log — drive the fix prompt. A missing/undecodable document, or a non-zero exit with an empty list, is a provider error that still fails the step (with the raw log, since there is no document to show).
There is no fast/full depth convention to pick between — a project defines a single profile, and firing carries the cadence that used to be encoded in separate fast/full profile keys. The old flat { "full": ["pnpm test", ...] } array form is rejected at validation, naming the offending profile.
After each phase gate, phax records which steps ran, their surface, and their pass/fail result in <phase>/gate-attribution.json. At run end, the final report's ## Run Summary lists the set of surfaces verified during the run — a surface counts as verified only when every step of it that ran passed. Each phase's run record also names its verified surfaces, so phax records list and phax records explain can show surface coverage without opening artifacts.
The top-level name is the run namespace — run short-names are scoped under it. Provider routing is not configured here — it lives in the global ~/.phax/ config (see Multi-provider model routing). The optional security.profile (secure | unsafe | isolated, default secure) sets the default security posture for runs; see Security modes. The optional fileReconciliation.mode (report_only | warn, default report_only) controls how per-phase file reconciliation reports deviations from the plan; see Run. The optional review.compliance and publish blocks turn on an automatic plan-compliance review and a pushed pull request when each run reaches review; see Compliance review & publishing.
The optional authoring.spec and authoring.plan blocks each take an optional model and effort, resolved flag → config → catalog default (claude-opus-5-5 at high effort) for the headless authoring command:
Both keys are optional and independent — an absent model or effort falls back to the catalog default, so a version: 1 config without an authoring block loads unchanged.
Orient provider
Add an "orient" block to tell phax how to fetch orientation rows for the current project:
The command string is split on whitespace with no shell — use a wrapper script if the path contains spaces or you need a pipeline. phax writes a JSON request to the provider's stdin and reads a JSON response from stdout; the provider must exit 0 on both success and "not found" responses.
- Index request —
{"files": ["src/foo.ts", ...]}: respond with{"rows": [{"id", "title", "severity", "trigger"}, ...]}for every row whose trigger prefix matches any file in the list.severityis one of"error" | "warn" | "info". - Expand request —
{"expand": "<id>"}: respond with{"row": {"id", "title", "severity", "trigger", "body"}}for a known id, or{"row": null}for an unknown one. - All fields are non-empty strings. A non-zero exit, non-JSON stdout, or a response that fails validation is a provider error (exit 1). An empty index or a null row prints "No orientation available." and exits 0.
- During a run phax sends the index request for each phase's planned files and weaves the rows into the phase prompt. When orient is configured,
phax orientis implicitly granted to the in-phase agent without anagentCommandsentry.
Full contract: phax orient.
Scope provider
Add a "scopes" block to register the provider that answers, for a completion diagnostic (see Configure above), which scopes are already closed:
The command string is split on whitespace with no shell, same as orient. Before each non-terminal phase's gate — only when that gate has at least one output: "diagnostics" step — phax writes the plan projection to the provider's stdin:
phases[].files is each phase's planned files to create and edit, deduplicated, in plan order (optionalFilesToEdit is never included). The provider responds on stdout with {"closed": ["<scope>", ...]} and must exit 0. A completion diagnostic fails the step once every scope it names appears in closed; otherwise it is pending. The terminal phase closes every scope without querying the provider — it is never called. If a gate step returns a completion diagnostic but no scopes provider is registered, the gate fails through the fix loop with a configuration-error message naming phax.json. A non-zero exit, non-JSON stdout, or a response that fails validation likewise fails the gate through the fix loop, with the reason in the attempt log — the same treatment as an orient provider error.
Validate it before running:
Plan auditor
Add a "planAuditor" block to register a provider that reviews a plan's shape
before a run touches it:
The command string is split on whitespace with no shell, same as orient and
scopes. phax plans lint queries it — never phax run — and only once the
plan's deterministic extraction succeeds. It writes the plan projection to the
provider's stdin:
This is the same projection the scope provider receives, minus the gated
phase id: phases[].files is each phase's planned files to create and edit,
deduplicated, in plan order (optionalFilesToEdit is never included). Models,
efforts, prompts, anchors and commit metadata never leave phax. The provider
responds on stdout with {"findings": [{"message", "phases": [...]}]} and
must exit 0. Every finding renders as a warning on the lint's advisory
check, one row per phase named in phases (- when the list is empty). A
non-zero exit, non-JSON stdout, or a response that fails validation is a
single advisory warning naming the reason, as is an auditor that outruns the
30s cap phax spawns it under. Advisory findings never set the lint's exit code,
and with no planAuditor registered — or on a plan the deterministic parser
cannot read — there are no advisory findings.
Configuration layers
phax resolves configuration from four layers, least-to-most specific (most personal wins):
Scalars — the highest present layer wins (e.g. state.root, agent.maxFixAttempts, security.profile).
Allowlists — union across all layers, so user layers can only add to the project's security baseline, never silently remove it. This applies to security.filesystem.allowRead, security.filesystem.allowWrite, security.agentCommands, security.mcp.allow, and gateProfiles (union by key; the higher layer's command array wins for a shared key).
phax.local.json is gitignored — it is the right place for per-developer preferences like model selection or trust overrides that should not be committed. ~/.phax/config.json is for preferences that apply to all repos on your machine. A JSON Schema for both user layers is generated alongside phax.schema.json as phax.user.schema.json.
Schema upgrade
After upgrading phax, regenerate phax.schema.json to match the new binary's config contract:
This rewrites phax.schema.json and phax.user.schema.json next to the nearest phax.json and reports whether the files changed or were already current. It never modifies phax.json.