Skip to content

Latest commit

 

History

802 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

phax

phax is a deterministic orchestrator for AI coding agents: you give it a plan, and it executes that plan as a sequence of isolated, gated phases. The orchestration itself is plain code — phase sequencing, gates, retries, and state transitions are all deterministic — so the only non-deterministic part is the agent working inside each phase.

Because every phase declares the files it expects to create or edit, phax makes each run reviewable: it reconciles the plan against what actually changed and surfaces every deviation — planned-but-not-done, done-but-not-planned, deleted, renamed — which the agent must justify in its handoff. Instead of landing on a pull request full of touched files with no idea what was intended, you get a review that's already framed, phase by phase and across the whole run.

Each phase runs in its own Git worktree, must pass its gates before the next one starts, and has a same-session fix loop for repairing gate failures; the final phase is kept open for human review. The agent itself is interchangeable — Claude Code, Mistral Vibe, or OpenAI Codex — selected by the model-routing layer, with Claude Code as the default and terminal fallback.

Quickstart

#1. install the CLI
npm install -g @lbdremy/phax

# 1.1 Install the phax skills (phax-planning + phax-cli) into your agent
phax skills install --target claude
# 1.2 Install phax CLI auto completions (optional - you need usage CLI to be installed first)
brew install usage
echo 'source <(phax completions zsh)' >> ~/.zshrc

# 2. Have any coding agent draft the plan (here: Claude Code). Point it at the
#    phax-planning skill so the plan.md it writes matches the format phax expects:
claude -p "Write plan.md for specs/<your spec> using the phax-planning skill."

# 3. extract the plan + run every gated phase
phax run --plan plan.md
# 4. review the agent's work in a pre-prompted session (optional)
phax review-code <short-name>
# 5. push the final branch and open the PR
phax publish-pr <short-name>

Install

Via npm (recommended) — the wrapper resolves and downloads the correct platform binary on first run:

npm install -g @lbdremy/phax
# or without installing:
npx @lbdremy/phax

Direct binary download — grab the binary and checksum for your platform from GitHub Releases:

# Example: macOS Apple Silicon
curl -LO https://github.com/lbdremy/phax/releases/latest/download/phax-darwin-arm64
curl -LO https://github.com/lbdremy/phax/releases/latest/download/phax-darwin-arm64.sha256
sha256sum --check phax-darwin-arm64.sha256
chmod +x phax-darwin-arm64
sudo mv phax-darwin-arm64 /usr/local/bin/phax
# macOS: remove the quarantine attribute added by the browser/curl
xattr -dr com.apple.quarantine /usr/local/bin/phax

macOS Gatekeeper note: binaries are not yet code-signed or notarized. Without the xattr step above, macOS will block the binary on first run. Go to System Settings → Privacy & Security to allow it, or run the xattr command.

Available targets: phax-darwin-arm64, phax-darwin-x64, phax-linux-x64, phax-linux-arm64.

Binary size note: the compiled binary is ~74 MB. The release build bundles the CLI with esbuild first (tree-shaken to ~1.5 MB of actually-used code) and then runs deno compile --include to embed the three runtime-read data files (package.json, phax.usage.kdl, .claude/skills). This avoids the un-bundled path which would embed ~274 MB of node_modules files (~360 MB total). The npm wrapper downloads the binary once and caches it per version at ~/.phax/bin/<version>/, so the cost is a one-time download per upgrade.

Runtime permission posture

The distributed phax binary is compiled with an explicit Deno permission set:

Permission Status Notes
Filesystem read/write allowed Required to manage run state, worktrees, locks, and artifacts
Network denied phax itself makes no network calls
Environment allowed Required so subprocesses can resolve executables via PATH
Subprocess execution unrestricted phax may spawn any executable; security comes from the provider-native jail and structured argv invocation, not an executable allowlist

Important: Deno's permissions sandbox phax, not the provider CLIs it launches. Once phax spawns claude, codex, or vibe, those processes run with their own provider-native permissions and are not constrained by phax's Deno permission set. Provider-level security (filesystem jail, network restrictions, tool allowlists) comes from the provider's own sandbox — see Security modes and the Security notes section.

phax open uses the OS opener (open on macOS, xdg-open on Linux) so no editor binary needs to be installed or configured. The meaningful security boundaries are the provider-native jail (filesystem, network, tool restrictions) and phax's structured argv invocation — phax never interpolates user input into shell strings.

Requirements: at least one provider CLI on $PATH:

  • claude — Claude Code (default, and the terminal fallback provider)
  • vibe — Mistral Vibe (optional)
  • codex — OpenAI Codex (optional)

Most setups want claude installed even when routing prefers another provider, because phax falls back to Claude Code when the preferred provider is unavailable or cannot satisfy the active security posture.

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:

phax init           # interactive wizard (TTY) or non-interactive (detected defaults)
phax init --yes     # non-interactive: accept detected defaults without prompting
phax init --force   # reconfigure an existing phax.json (prompts again in a TTY)

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:

{
  "$schema": "./phax.schema.json",
  "version": 1,
  "name": "my-project",
  "security": { "profile": "secure" },
  "fileReconciliation": { "mode": "report_only" },
  "review": { "compliance": { "enabled": true } },
  "publish": { "auto": true, "remote": "origin", "baseBranch": "main" },
  "commands": {
    "setup": ["pnpm install"],
    "cleanup": ["rm -rf node_modules"]
  },
  "gateProfiles": {
    "full": [
      { "command": "pnpm typecheck", "surface": "local", "firing": "every-phase" },
      { "command": "pnpm test:unit", "surface": "local", "firing": "every-phase" },
      { "command": "pnpm lint", "surface": "structural", "firing": "every-phase" },
      { "command": "pnpm build", "surface": "product", "firing": "terminal" },
      {
        "command": "pnpm audit:security",
        "surface": "structural",
        "firing": "every-phase",
        "output": "diagnostics"
      }
    ]
  }
}

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.
  • firingevery-phase | terminal. This is behavioral: every-phase steps run at every phase gate; terminal steps run only at the final phase gate, in addition to the every-phase steps.
  • output — optional, log | diagnostics, defaults to log. 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 a class: an "invariant" diagnostic always fails the step. A "completion" diagnostic names one or more scopes and fails the step only once every scope it names is closed, as reported by the registered scopes provider (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 records pending (not pass/fail) in gate-attribution.json and never counts its surface as verified. Pending findings are persisted as checks-attempt-NN.pending.json next to the attempt log; a failing document is persisted as checks-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.

Orient provider

Add an "orient" block to tell phax how to fetch orientation rows for the current project:

{
  "orient": { "command": "node ./orient.mjs" }
}

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. severity is 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 orient is implicitly granted to the in-phase agent without an agentCommands entry.

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:

{
  "scopes": { "command": "node ./scopes.mjs" }
}

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:

{
  "phase": "phase-02",
  "phases": [
    { "id": "phase-01", "files": ["src/core/billing/port.ts"] },
    { "id": "phase-02", "files": ["src/core/billing/invoice.ts"] },
    { "id": "phase-03", "files": ["src/adapters/billing/stripe.ts"] }
  ]
}

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:

phax validate
# also validate a phax-plan.json:
phax validate --plan phax-plan.json

Plan auditor

Add a "planAuditor" block to register a provider that reviews a plan's shape before a run touches it:

{
  "planAuditor": { "command": "node ./audit-plan.mjs" }
}

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:

{
  "phases": [
    { "id": "phase-01", "files": ["src/greet.ts"] },
    { "id": "phase-02", "files": ["tests/greet.test.ts"] }
  ]
}

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):

Layer File Purpose
Built-in defaults ~/.phax state root, maxFixAttempts: 1, etc.
Project config phax.json (committed) Team baseline: gate profiles, identity, security grants
Global user config ~/.phax/config.json Machine-wide preferences: model, state root, MCP mode
Per-project user config phax.local.json (gitignored) This user × this repo overrides

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:

phax schema upgrade

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.

Write a plan

Author plan.md with the phax-planning skill — it is the source of truth for the plan format that phax run extracts and phax plans lint checks. The skill defines the per-phase template contract (heading + {#phase-NN-<slug>} anchor, recommended model/effort, the three planned-file lists, gate-profile verification, commit subject/body) and the planning doctrine (plan outside-in, implement inside-out, verify outside-in). Point your agent at that skill when drafting or reviewing a plan; don't hand-roll the format.

In short: plan.md is a Markdown document with one ## phase-NN — <title> {#phase-NN-<slug>} section per phase, each carrying an objective, detailed instructions, planned-file lists, a gate-profile verification step, and a commit subject/body. See examples/hello-world/plan.md for a worked example and .claude/skills/phax-planning/SKILL.md for the full template contract.

Lint the plan

phax plans lint docs/plans/NN-<slug>-plan.md

This is a read-only, model-free check: it reports every structural defect the deterministic parser can find, whether the planned-file lists are coherent with the working tree and with earlier phases, whether every required command is covered, whether each phase's model/effort is in the routing catalog, and — when a plan auditor is registered — every advisory finding it returns about the plan's shape. It exits 1 when any finding is an error; advisory findings are always warnings.

Run

phax run --plan plan.md                         # full execution (extracts plan.md, runs every phase)
phax run my-feature --plan plan.md              # set the run short name explicitly
phax run --plan plan.md --dry-run               # preview only — zero side effects
phax run --plan plan.md --allow-dirty           # skip clean-tree guard
phax run --plan plan.md --provider-priority mistral-vibe,claude-code  # override provider priority for this run
phax run --plan plan.md --security unsafe       # override the security mode for this run

Each phase:

  1. Creates a Git worktree at ~/.phax/worktrees/<short-name>/phase-NN/ on its own branch <run.branch>--phase-NN.
  2. Runs commands.setup inside the worktree.
  3. Builds a prompt from the plan and the previous phase's handoff, sends it to the selected provider's agent (resolved by the routing layer; see Multi-provider model routing).
  4. Runs the gate profile; on failure, resumes the same agent session once and retries.
  5. After passing gates, resumes the agent to produce phase-handoff.md.
  6. Commits with the planned message. If the worktree is clean (no changes), the run stops with a non-zero exit and writes resume-instructions.md — use phax resume to continue from the next phase.
  7. Reconciles the files actually touched against the phase's planned files, writing file-reconciliation.{json,md} to the phase folder. Deviations (unplanned creates/edits, missing planned changes) are injected into the next phase's prompt so the agent sees how the prior phase diverged from its plan; with fileReconciliation.mode: "warn" they are also logged (default report_only only records them).

Each phase gets its own branch (<run.branch>--phase-01, <run.branch>--phase-02, …), chained: phase-01 branches off <run.branch>, phase-N branches off the previous phase's branch. The base <run.branch> stays at the run-start commit. The final phase's branch carries the full commit chain and is the ref to review, merge, or push.

Worktrees from every phase persist on disk for the lifetime of the run and are available for inspection until phax archive is run.

The final phase stays open for review. A review-handoff.md is written to the run folder showing the final phase branch as the review target. When the run reaches review, two optional steps run automatically if enabled in phax.json (both are non-fatal — the run stays review_open if they fail):

  1. Compliance review (review.compliance.enabled) — a non-mutating plan-compliance pass writes its verdict to the run folder, so it can land in the PR body.
  2. Publish (publish.auto) — pushes the final phase branch to the configured remote and opens (or reuses) a pull request; details are recorded in publication.json.

See Compliance review & publishing.

When phax run finishes (or is interrupted), it prints an end-of-run recap to the terminal summarizing the run state, the review target branch, any published PR URL, and the next command to run.

macOS sleep prevention — long-running phax run sessions can be wrapped with caffeinate to prevent macOS from sleeping while phax executes:

caffeinate -ims phax run --plan plan.md

Review loop

phax review-code <short-name>   # interactive, pre-prompted code-review session in the worktree
phax enter <short-name>         # resume the final agent session
phax shell <short-name>         # open $SHELL in the final worktree
phax path  <short-name>         # print the worktree path (script-friendly)
phax open  <short-name>         # open the worktree in the configured editor

phax review-code launches the AI agent in the final worktree already primed with a code-review prompt — seeded with the file reconciliation and, if present, the compliance findings — so the review starts from context instead of a blank prompt. The session is resumable: re-running resumes it, --new-session starts fresh. Override the model/effort with --model/--effort (defaults from review.code, else claude-opus-5 at high effort). You take over the session to investigate, discuss, and apply fixes.

Compliance review & publishing

Both steps run automatically at the end of a run when enabled (see Run), and can also be invoked on their own against a review_open run:

phax review-compliance <short-name>   # non-mutating plan-compliance review of the agent's work
phax publish-pr <short-name>          # push the final branch and open (or reuse) a PR

phax review-compliance re-invokes the AI agent with the run's handoff artifacts and the original plan and writes a verdict; it never touches the worktree, registry, or any files. Configure its model/effort under review.compliance in phax.json (default model claude-sonnet-5, effort medium).

phax publish-pr pushes the final worktree branch to the GitHub remote and creates a pull request, reusing an existing PR for the same branch if one exists. It requires a GitHub remote and an authenticated gh CLI. Configure the remote, base branch, and title under publish in phax.json.

Coordinating multiple plans

When you have more than one plan in flight, these commands answer "is this plan still fresh?", "can these run together?", and "did a landed run invalidate the others?" — using the same declared-file lists the per-phase reconciliation relies on.

phax plans status                                          # report every Approved plan as fresh or stale
phax plans status --apply                                  # flip stale-computed plans Approved -> Stale
phax plans overlap docs/plans/33-a.md docs/plans/35-b.md    # predicted: which plans are parallel-safe
phax plans overlap --landed <run> docs/plans/40-other.md    # confirmed: which plans the run's real diff invalidates
phax adjust-plan docs/plans/40-other.md --landed <run>      # interactively reconcile a plan against a landed run

phax plans status reports every live, Approved plan's staleness against what its approval was recorded against: the declared source spec's content, the plan's own content, and the files changed since the recorded baseline intersected with the plan's footprint. Stale entries name their reasons — spec-changed, ground-changed, self-changed — with evidence; a plan with no approval record, or one whose baseline commit no longer exists, reports missing-record and renders as stale. It is a report, not a gate — it exits 0 whether or not stale plans exist. --apply flips the stale-computed plans Approved → Stale as an explicit gesture; --json emits machine-readable output.

phax plans overlap reports which plans can run in parallel without a merge conflict. Without --landed, it reads each plan.md through the content-addressed extraction cache (a cold miss extracts once via the LLM and caches it; --no-extract fails on a miss instead), unions each plan's declared phase file-sets into a footprint, intersects them pairwise, and reports a severity-graded conflict matrix, the clean pairs, the largest fully-disjoint parallel-safe set, and a greedy wave schedule. With --landed <run>, it reads that run's actual git diff from its persisted global-file-reconciliation.json and reports which of the given plans now need re-adjustment. Conflicts are file-level, not hunk-level, and --json emits the raw result.

phax adjust-plan <plan> --landed <run> opens an interactive, pre-prompted session that reconciles a plan against what a landed run actually changed: it establishes which declared files, line references, and decisions are now invalidated, asks clarifying questions, proposes concrete edits, and — only after your explicit approval — edits and commits the plan. The landed run must have reached review (it needs a global-file-reconciliation.json). The session is resumable; --new-session starts fresh.

List runs

phax ls                   # all runs
phax ls --active          # created or running
phax ls --failed
phax ls --review-open
phax ls --archived
phax ls --json            # machine-readable

Archive

Archive is the only operation that touches worktrees/. It moves:

  • ~/.phax/runs/<short-name>~/.phax/archive/<short-name>/runs/
  • ~/.phax/worktrees/<short-name>/~/.phax/archive/<short-name>/worktrees/

Then runs git worktree prune to drop stale admin records. Nothing is destructively deleted — every phase's working state is preserved for later inspection.

phax archive <short-name>        # any non-running run; unfinished states require --force
phax archive <short-name> --force  # archive an unfinished run, or bypass the dirty-worktree check

Finished runs (review_open, completed) archive without --force. Unfinished runs (created, failed, interrupted, rate_limited, stopped) are refused unless --force is passed — the refusal message names the state. Running, locked, and already-archived runs are never archivable. The run's stoppedReason and lastError survive archival intact.

Multi-provider model routing

PHAX can route phase execution through Claude Code, Mistral Vibe, or OpenAI Codex based on a user-editable global routing config (~/.phax/model-routing.json). The routing layer maps requested model IDs to stable model families and PHAX tiers, then selects the best available provider from providerPriority.

phax agent models                              # print routing table + provider priority
phax agent resolve --model claude-sonnet-4-6 --effort medium [--json]
phax agent probe                               # check provider executable availability
phax agent setup mistral-vibe --dry-run        # preview Vibe alias installation
phax agent setup mistral-vibe --install-model-aliases  # install PHAX Vibe aliases

The default providerPriority is ["mistral-vibe", "codex-cli", "claude-code"] (the spec §12 multi-provider table). On a clean install, mistral-vibe and codex-cli are enabled: false in the default provider config, so all phases run through Claude Code as before. Enabling them via phax agent setup providers (or editing ~/.phax/providers.json) activates the richer routing. See docs/model-routing.md for the full resolution pipeline, tier table, relationship semantics, and worked examples.

Security modes

Every run executes under a security posture, set by security.profile in phax.json and overridable per run with --security:

Mode Behavior
secure Default. Provider-native sandboxing — filesystem jailed to the worktree, network governed by network.profile (enforced only where the provider supports it), MCP disabled.
unsafe Host-unrestricted: full filesystem/network access. Prints a warning. Use only for trusted plans.
isolated External-sandbox mode — planned, not yet available (the CLI rejects it today).

Provider capability matters under secure: Claude Code and Codex have strong filesystem jails and run natively, while Mistral Vibe has only a partial jail. In strict secure mode a partial-jail provider cannot satisfy the policy, so routing skips it and falls back to Claude Code; the applied posture (including any downgrade) is recorded in each phase's security.json and the final report. Network controls differ too: no provider enforces a domain allowlist, and Codex is the only one with a hard egress toggle (provider-only disables subprocess network); for Claude and Vibe the network.profile is recorded but not enforced as a domain filter.

Shell access for the agent (used to run and fix the phase's gate commands) is also constrained per provider, at different granularities — Claude allowlists exactly the gate commands, while Codex and Vibe rely on their sandbox/approval models. See Shell command execution in the security docs for the details.

Testing

pnpm test               # unit + integration — fast, no network, no provider CLIs
pnpm test:e2e:real      # opt-in real E2E — drives the installed provider CLIs, costs tokens

The E2E suite skips automatically unless PHAX_E2E_RUN=1 is set, so it never runs by accident. It runs one real-flow suite per provider (Claude Code, Mistral Vibe, Codex), each forcing its provider with --provider-priority and gated on that provider's CLI being installed — so only the providers you have set up actually run. See docs/e2e-testing.md for prerequisites, isolation model, and how to read failure artifacts.

Debugging

Add --verbose to any command to print semantic events (state transitions, adapter calls, gate results) to the terminal:

phax run --plan plan.md --verbose
phax resume <short-name> --verbose

Add --trace to also write one JSON line per semantic event to semantic.jsonl in the run folder (~/.phax/runs/<short-name>/):

phax run --plan plan.md --trace

Both flags can be combined. See docs/observability.md for the full observability architecture and docs/plan-extraction-model.md for how to configure the model phax run uses for the fallback extraction.

Observability

phax emits structured semantic telemetry through the SystemTelemetry port — state transitions, adapter calls, gate results, and artifacts. Telemetry is on by default and recorded to a per-run journal; toggle it globally with "enabled": false in ~/.phax/telemetry.json. Two opt-in flags surface it live:

Flag Effect
--verbose Print semantic events to the terminal
--trace Write semantic events as JSONL to semantic.jsonl in the run folder

See docs/observability.md for architecture details, the snapshot rule, and the adapter-boundary failure contract.

Resume

phax resume <short-name>        # restart from the next pending phase
phax resume <short-name> --yes  # skip confirmation
phax resume <short-name> --yes --provider-priority codex-cli,claude-code  # override provider priority

Resume validates the run state, lock, and worktree before proceeding. It never re-runs committed phases. If the run is review_open, it refuses and points you at phax enter.

Locks

phax writes a lock file at ~/.phax/locks/<short-name>.lock for every active run. If a process dies, the lock can become stale:

phax unlock <short-name>        # remove stale lock
phax unlock <short-name> --force  # remove any lock

Exit codes

Code Meaning
0 Success
1 Validation error, config error, or plan error
2 Gate failure (after fix loop exhausted)
3 Lock conflict
4 Unsafe git state (dirty worktree)
5 Agent invocation error (Claude, Vibe, or Codex)
6 Handoff generation failed
8 Rate limit or usage limit hit (resumable)
9 Phase produced no changes (resumable)

Environment variables

phax has no runtime-configuration env vars — the state root, telemetry, and security posture are all set in phax.json (and ~/.phax/telemetry.json), not via the environment. The one variable phax honors gates the opt-in real E2E suite:

Variable Purpose
PHAX_E2E_RUN Set to 1 to enable the real E2E suite (pnpm test:e2e:real)

Troubleshooting

claude not found — install Claude Code and ensure the binary is on $PATH.

Lock conflict — another phax process is running, or a previous process died. Run phax unlock <short-name> to clear a stale lock.

Gate failure loop — increase maxFixAttempts in phax.json, or reduce gate scope. Check ~/.phax/runs/<short-name>/phase-NN/checks-attempt-01.log for details.

Missing phase-handoff.md sections — the phase transitioned to handoff_failed. Check the phase status file and resume the agent session with phax enter.

Format conflicts — run pnpm format, do not add lint exceptions. Knip failures — remove the dead code or wire it into an entry point, do not add ignoreDependencies entries casually.

State Machine

phax is implemented as an explicit hierarchical state machine. Every signal (gate result, rate limit, agent completion, archive request) is a typed PhaxEvent. The pure reducer returns a DispositionHandled, Ignored, Stale, Rejected, or Unexpected — plus optional side-effect commands. The single dispatch() entry point is the only writer to status.json and run-status.json.

See docs/state-machine.md for:

  • Mermaid diagrams of the run and phase hierarchies
  • The full event-disposition matrix
  • The event and command vocabularies
  • A worked example of adding a new signal

Security notes

phax never interpolates user-controlled data (branch names, workspace paths, plan fields) into shell command strings. All git and shell invocations pass arguments as separate argv tokens. Gate commands from phax.json are treated as opaque pre-validated arrays, not shell strings.

CLI specification (phax.usage.kdl)

phax.usage.kdl is a machine-readable CLI contract generated from the Commander.js program in src/cli/. It is a derived artifact — Commander is the source of truth — and must be regenerated after any change to a command, flag, or argument:

pnpm gen:usage-spec

The integration gate tests/integration/usageSpecDrift.test.ts asserts the committed file is byte-identical to the generator output, so a CLI change without regenerating the spec will fail the gate. Downstream tooling (phax --usage, shell completions, docs/cli/reference.md, and external consumers such as a generated client library or editor integration) all derive from this spec.

Shell completions

phax ships a generated shell completion script via phax completions <shell>. Supported shells: zsh, bash, fish, nu, powershell.

Prerequisite: the usage CLI must be installed — it is needed both to generate the script and at Tab-time (the generated script calls back into usage complete-word):

brew install usage

Per-shell install:

# zsh (default shell on macOS) — write a _phax completion onto your $fpath.
# If you already have a completions dir on $fpath, just drop the file in:
phax completions zsh > "${fpath[1]}/_phax"

# bash
source <(phax completions bash)
# or add to ~/.bashrc:
echo 'source <(phax completions bash)' >> ~/.bashrc

# fish
phax completions fish > ~/.config/fish/completions/phax.fish

# nushell — add to your nu config
phax completions nu | save --force ~/.config/nushell/completions/phax.nu
# source it in env.nu or config.nu

# powershell
phax completions powershell >> $PROFILE

zsh on macOS, from scratch — if you don't already have a completions directory on $fpath, set one up once:

# 1. Create a directory for personal completions and put the _phax file in it.
mkdir -p ~/.zsh/completions
phax completions zsh > ~/.zsh/completions/_phax

# 2. Make zsh load it (add to ~/.zshrc). Skip the compinit line if your setup
#    already runs it — frameworks like oh-my-zsh do.
cat >> ~/.zshrc <<'RC'
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
RC

# 3. Reload your shell, then Tab-complete:
exec zsh
phax <Tab>

After this, phax <Tab> lists subcommands and phax enter <Tab> completes run short-names live from your registry.

phax --usage and phax completions work from the release binary as well as from source. Both commands read phax.usage.kdl, which is embedded in the binary at build time via deno compile --include.

Once the completion script is installed, Tab also completes run short-names for commands that take one (phax enter, phax resume, phax archive, and others). Candidates are fetched live from phax ls --complete, so they reflect the actual runs in your registry at Tab-time.

CLI command reference

Full CLI reference: docs/cli/reference.md.

  • phax validate [--plan <path>] — Validate phax.json and its user overlays without any side effects; pass --plan to also validate a phax-plan.json
  • phax unlock [--force] <short-name> — Remove a stale run lock; use --force to remove any lock
  • phax enter <short-name> — Attaches to the kept-open agent session in the final worktree, so you can review the agent's work, ask follow-up questions, or apply manual fixes interactively.
  • phax enter-phase <short-name> <phase-id> — Attaches to the agent session for a specific phase worktree. Useful for inspecting intermediate state or debugging a phase that has not yet been committed to main.
  • phax session-info [--debug] <short-name> — Prints diagnostic information about a run: its current state, active phase, worktree path, and agent session id. Read-only — no side effects.
  • phax shell <short-name> — Opens an interactive shell in the final worktree. Useful for manually inspecting files, running tests, or executing commands outside the agent session.
  • phax path <short-name> — Prints the absolute path to the final worktree on a single line. Useful in scripts: cd $(phax path my-run) or for piping to other tools.
  • phax open <short-name> — Opens the final worktree in the editor configured in phax.json (or the EDITOR environment variable). Equivalent to running your editor with the worktree path as an argument.
  • phax ls [FLAGS] — Lists runs from the local registry (~/.phax/runs/). With no filter flags, shows all runs. Use status filters to narrow output: --active (created or running), --failed, --review-open (awaiting human review), or --archived. Use --json for machine-readable output.
  • phax archive [--force] <short-name> — Archives a run by moving its worktrees under ~/.phax/archive/./ and marking it archived in the registry. Nothing is destructively deleted — every phase's working state is preserved.
  • phax run <FLAGS> [short-name] — Extracts a plan from the plan.md given by --plan, creates a run entry in the registry, and executes each phase sequentially in its own Git worktree using the configured AI agent. Each phase runs its gate profile's every-phase steps after execution; the final phase also runs the profile's terminal steps. Each step's surface (local, structural, or product) is recorded per phase and the run's verified surfaces are reported at run end.
  • phax review-handoff [--allow-partial] <short-name> — Regenerate review-handoff.md and global file reconciliation for a review_open run
  • phax publish-pr <short-name> — Pushes the final worktree branch to the GitHub remote and creates a pull request, or reuses an existing PR for the same branch. Requires a GitHub remote and gh CLI authentication.
  • phax review-compliance <short-name> — Runs a non-mutating plan-compliance review by invoking the AI agent with the run's handoff artifacts and the original plan. Does not modify the worktree, registry, or any files.
  • phax review-code [FLAGS] <short-name> — Opens an interactive, pre-prompted code-review session for a review_open run by launching the AI agent in the run's worktree with the code-review prompt. The session is resumable: re-running resumes the existing session, while --new-session starts fresh. The developer takes over the session to investigate, discuss, and apply fixes.
  • phax adjust-plan <FLAGS> <plan> — Opens an interactive, pre-prompted session to help you adjust a plan.md after a landed run has introduced drift. The session establishes which of the plan's declared files, line references, and decisions are invalidated by the landed run's actual changes, asks clarifying questions where needed, proposes concrete edits and waits for your explicit approval, and only then edits and commits the plan — all interactively within the session. The command itself mutates nothing.
  • phax init [--force] [--yes] — Creates phax.json and phax.schema.json in the current directory. Use --force to overwrite an existing phax.json. Does not connect to any network or external service.
  • phax report [--no-gist] [short-name] — Creates a GitHub issue from local run telemetry. By default, uploads the full log as a secret GitHub gist and links it in the issue body. Use --no-gist to inline the log directly.
  • phax orient [--file <path>] [id] — Requires an orient provider in phax.json:
  • phax completions <shell> — Generate a shell completion script (zsh, bash, fish, nu, powershell). Requires the usage CLI.
  • phax resume [FLAGS] <short-name> — Picks up a run from its next pending phase, re-entering the same execution loop as phax run. Prompts for confirmation before proceeding unless --yes is set.
  • phax reset-phase [FLAGS] <short-name> [phase-id] — Reset a stuck or failed phase so phax resume re-runs it from scratch
  • phax agent <SUBCOMMAND> — Inspect and manage model routing and provider configuration
  • phax agent models — Print the routing table and provider priority
  • phax agent resolve <FLAGS> — Show how a model+effort request resolves to a provider and concrete model
  • phax agent probe — Check which provider executables are available on PATH; never throws on an unavailable provider
  • phax agent setup <SUBCOMMAND> — Set up provider integrations
  • phax agent setup mistral-vibe [--dry-run] [--install-model-aliases] — Append PHAX-owned Mistral Vibe model aliases to ~/.vibe/config.toml (append-only, atomic)
  • phax agent setup providers [FLAGS] — Reconcile ~/.phax/providers.json enabled flags from live executable probes (dry-run by default)
  • phax security [--verbose] [--trace] <SUBCOMMAND> — Security-related commands
  • phax security status [--verbose] [--trace] — Show provider security capabilities and availability
  • phax skills <SUBCOMMAND> — Manage PHAX skills
  • phax skills install <--target <target>> [--scope <scope>] [skill] — Install bundled PHAX skills into an agent's native skill directory
  • phax schema <SUBCOMMAND> — Manage the local phax.schema.json
  • phax schema upgrade — Regenerate phax.schema.json from the installed binary's config contract; never modifies phax.json
  • phax artifact <SUBCOMMAND> — Parent command for inspecting and transitioning the lifecycle status of a spec (docs/specs/) or plan (docs/plans/). Specs carry Draft, Approved, Abandoned, or Completed; plans additionally carry Stale. Transitioning to a terminal status (Abandoned, Completed) moves the file into the artifact's archive/ subdirectory as part of the transition. Illegal transitions and validation failures (missing frontmatter block, unknown status, status/location disagreement) refuse with exit code 12.
  • phax artifact status <path> — Reports an artifact's kind (spec or plan), current status, and the legal transitions from that status. For Approved specs, also reports the approval date and baseline, and whether the spec has been edited since that approval (recorded) or has no approval record (unrecorded). Read-only — no side effects.
  • phax artifact approve <path> — Transitions an artifact to Approved. Legal from Draft (both kinds) and from Stale (plans only); re-approving an already-Approved artifact re-records the approval, refreshing its timestamp and baseline — this is the correct way to record an in-place revision of a spec, not editing the date by hand. Rewrites the frontmatter status key in place.
  • phax artifact stale <path> — Manually marks a plan Stale. Legal from Approved only — Stale has no automatic trigger (that belongs to a future lineage spec). Rewrites the frontmatter status key in place.
  • phax artifact abandon <path> — Abandons an artifact — a terminal status distinct from Completed, for work dropped without execution. Legal from Draft or Approved (specs) or Draft, Approved, or Stale (plans).
  • phax artifact complete <path> — Completes an artifact — a terminal status for work that ran to completion. Legal from Approved (specs) or Approved or Stale (plans).
  • phax artifact reopen <path> — Reopens a Stale plan back to Draft, for when re-planning is needed before re-approval. Legal from Stale only. Rewrites the frontmatter status key in place.
  • phax plans <SUBCOMMAND> — Parent command for reporting on plans: the mechanical defects of a single plan (lint), staleness of Approved plans against their recorded approval, and cross-plan file overlap.
  • phax plans status [--apply] [--json] — Reports every live, Approved plan's staleness against the ground it was approved against: the declared source spec's content, the plan's own content, and the files changed since the recorded baseline intersected with the plan's footprint. Each stale entry names its reasons (spec-changed, ground-changed, self-changed) with evidence; a plan with no approval record — or one whose baseline commit no longer exists — reports missing-record, which renders as stale. This is a report, not a gate: it exits 0 whether or not stale plans exist. Use --apply to flip stale-computed plans Approved -> Stale as an explicit gesture (the flip is never automatic). Use --json for machine-readable output.
  • phax plans overlap [FLAGS] <plan> — Reports which of two or more plans can run in parallel without a merge conflict — predicted from each plan's declared file-sets, or confirmed against a landed run's actual diff.
  • phax plans lint [--json] <plan> — Reports every mechanical defect of a plan.md that phax can establish before a run, as findings with a severity (error or warning), a check, and the phase they concern.
  • phax records <SUBCOMMAND> — Manage phax run records
  • phax records init [--force] — Configure records for this project (transcript, destination, auto-push)
  • phax records sync — Bring the local records clone in line with its configured remote
  • phax records status — Show pending (unpushed) records, by run and phase
  • phax records list [--run <id>] — List records present, by run, phase, and verified surfaces
  • phax records explain [FLAGS] <sha> — Explain a commit from its record: prompt, diff, gates and verified surfaces, handoff, transcript, usage

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages