36 KiB
Contributing
Scope
fx is a CLI-first coding agent written in Zig.
Contributions should preserve that direction:
-
CLI-first over terminal-IDE behavior
-
explicit contracts over ad hoc strings and branches
-
permission-first security model
-
small, reviewable changes
-
honest docs and status reporting
Setup
Requirements:
-
Zig
0.16.0+ -
interactive terminal for manual shell testing
-
a model connection for model-backed flows. Custom model connections support local and remote endpoints. Vercel OAuth via
fx login, macOS Keychain API keys viafx setup,AI_GATEWAY_API_KEY, andVERCEL_OIDC_TOKENare also supported
Common commands:
zig fmt src/
zig build
zig build test
zig build run
Verification Workflow
Keep the local development loop focused: run the narrowest test that covers the changed path, build fx, and exercise the change using ./zig-out/bin/fx. The installed fx on PATH is not valid development evidence.
Once the focused checks pass, create a clean checkpoint commit, push the non-main feature branch, and open a draft PR immediately. The Full CI workflow runs the complete deterministic suite on native Linux x86_64, Linux aarch64, macOS x86_64, and macOS aarch64 runners. The native matrix builds, tests, and smoke-tests ReleaseSafe on every platform; formatting and the public-surface audit run in those ReleaseSafe jobs. Four duration-balanced, isolated ReleaseSafe E2E shards per platform use checked-in weights to assign every Bun test file once; files inside each shard run sequentially in separate Bun processes so terminal fixtures and process state cannot leak between files. A failed file receives one bounded retry after tmux is reset.
Standard PR CI reports ReleaseSafe Build & Test and deterministic E2E results. Do not mark the draft PR ready until all four Full CI jobs and the final ship gate have succeeded for the exact current commit. Each platform aggregate requires its ReleaseSafe native check and all four ReleaseSafe E2E shards. A result from an older commit does not count. Live model evals are separate from this gate because they require credentials and are not deterministic.
Changes to build.zig or scripts/pgso/ also run the native macOS arm64 PGSO candidate workflow. That lane produces retained size, behavior, and performance evidence but does not alter any release artifact or update channel. Its pinned toolchain, local reproduction command, corpus exclusions, and failure rules are documented in scripts/pgso/README.md.
Every pull request also receives informational ReleaseSafe binary-size comparisons for Linux x86_64, Linux arm64, macOS x86_64, and macOS arm64. Each comparison builds the pull request merge commit and base commit on the same native runner, reports exact file and ELF or Mach-O section deltas, and emits a warning at increases of 52,429 bytes (0.050000 MiB) or more. The warning requests investigation but does not replace the full PGSO release gate or reject a valid feature solely for adding code.
Pull Requests
Every PR must carry exactly one label that describes its primary intent:
-
type: bug: fixes incorrect behavior -
type: feature: adds a new user-facing capability -
type: improvement: improves existing user-facing behavior -
type: docs: changes documentation only -
type: maintenance: changes internal tooling, dependencies, CI, or implementation structure without a user-facing behavior change -
type: release: prepares or repairs a release -
type: security: fixes or hardens a security boundary
If you cannot manage labels, a maintainer or repository agent will apply the label before review. For a mixed PR, choose the label that best describes why the PR exists. Keep the title as a clean imperative sentence and do not add bracketed type prefixes such as [bug] or [improvement].
If an AI coding agent writes any of your contribution's prose, including the PR title and description, commit messages, documentation, and issues, it must use the technical-writer skill in .fx/skills/technical-writer/.
Repo Shape
-
src/main.zig: composition root only -
src/core/: contracts, runtimes, config, sessions, permissions, MCP, skills -
src/tools/: built-in tool implementations -
src/ui/: terminal rendering, event loop, input, transcript -
src/gateway/: AI Gateway client transport -
.fx/skills/: optional fx-native workspace-level skill root -
skills/: optional shared workspace-level skill root
Collaboration Rules
Before adding a new feature, answer these first:
- Which module owns the behavior?
- What is the typed contract?
- Does it need persistence?
- Does it need both text and JSON output?
- What docs and tests land with it?
- How is its deterministic E2E owner classified for macOS arm64 PGSO?
If that is unclear, stop and define it first.
PGSO corpus ownership
Classify every root tests/e2e/*.test.ts file in
scripts/pgso/corpus.json. Put common or performance-sensitive behavior in
training. Put important correctness, recovery, security, and rare behavior in
verification-only. Exclude only nondeterministic, live-network, credentialed,
sound-related, or harness-only coverage, and record the reason.
Tests added to an existing file inherit its classification. Reconsider that classification when a feature changes the file's product role, and remove stale entries when deleting a feature or E2E owner. Normal PR CI rejects missing, duplicate, stale, and unclassified files without running the full PGSO gate.
Configuration and State
Config precedence (highest wins):
- Environment variables such as
FX_PROVIDER,FX_MODEL,FX_PERMISSION_MODE, andFX_MAX_AGENT_STEPS ~/.fx/settings.json→workspaces["<workspace_path>"](profile workspace overrides)~/.fx/settings.jsontop-level (profile global settings)<workspace>/.fx.json(committed project defaults)- Built-in defaults
Project .fx.json accepts only repo-safe defaults: sandbox, max_agent_steps, max_tool_result_bytes, and context. Profile-owned keys such as provider, providers, models, model, effort, fast_mode, slash_menu_categories, startup_scrollback, prompt_history, statusLine, skill_match_fuzzy, first_call_tool_choice, auto_upgrade, update_channel, permission_mode, permission, and skill_symlink_authorities are ignored from project config before their values are parsed.
skill_symlink_authorities is an array of absolute directories that symlinked skills may resolve into, such as an app bundle or /nix/store. It is read at startup, a workspace override replaces the global list, and its entries are combined with the colon-separated FX_SKILL_SYMLINK_AUTHORITIES environment variable.
Runtime state lives under ~/.fx/:
-
~/.fx/sessions/<session-id>/session.json -
~/.fx/sessions/<session-id>/background/ -
~/.fx/sessions/<session-id>/subagent/ -
~/.fx/sessions/<session-id>/logs/
Sessions are global and portable across workspaces. Each session tracks a workspace_root that updates when resumed from a different directory.
Subagent children are internal ordinary sessions with their own ~/.fx/sessions/<child-id>/ directory and history. The parent owns one bounded subagent/children.json registry; each child carries only an immutable owner marker. Child sessions are hidden from ordinary session discovery and cannot be resumed directly. A first subagent.message creates a named persistent child for that parent; later messages continue it, and optional instructions replace only its child-specific system overlay.
Skills
There are two distinct skill categories in fx:
-
fxroots that belong to the product itself:.fx/skills,skills/,~/.fx/skills -
compatibility roots discovered for other agent installs:
.opencode/skills,.codex/skills,.claude/skills,.agents/skills,.claw/skills, plus their global equivalents
/skills list should make that distinction visible to the user.
/skills add and /skills install install full skill directories into the profile-owned ~/.fx/skills managed root, not just SKILL.md. Workspace .fx/skills and skills/ remain discoverable project-local instructions, not managed install targets.
The interactive agent can also install skills via the install_skill tool when the user asks to install one in conversation, including pasted npx skills add ... syntax.
Slack installation testing
src/core/slack/install.zig owns workspace bot installation, local credential
persistence, and explicit refresh. The web bridge contract is fixed to
https://fx.sh/api/slack/install/config, /api/slack/install, and
/api/slack/oauth/callback. Employee MCP authentication is separate.
Build with zig build, then run cd tests/e2e && bun test slack-install.test.ts.
The fixture exercises the freshly built binary and real loopback sockets without
live Slack credentials. FX_E2E_SLACK_ORIGIN accepts only an HTTP 127.0.0.1
origin with a non-privileged port, serving public metadata plus mocked
/api/oauth.v2.access and /api/auth.test responses. Production uses pinned
Slack endpoints. Local records bind to the bridge origin to prevent fixture
commands from refreshing production credentials. FX_NO_OPEN_BROWSER=1 prints
the start URL for headless operation; authorization still requires a browser on
the same computer as the listener.
This E2E owner is verification-only in the PGSO corpus because it covers a rare workspace setup operation and security boundaries. Live Slack authorization and message attribution are not deterministic tests.
MCP
Native fx connections use MCP v1 initialization by default over stdio,
Streamable HTTP, and deprecated 2024-11-05 HTTP+SSE. Servers using the newer
2026-07-28 discovery lifecycle opt in with
FX_MCP_PROTOCOL_VERSION=2026-07-28 in their configured environment map.
The SDK's host-owned client controls its own protocol negotiation. Native
sessions load trusted MCP configuration from the profile:
~/.fx/mcp.json
They also read Claude-compatible workspace configuration from:
<workspace>/.mcp.json
Project .fx.json does not define runnable MCP commands, URLs, env, or secrets.
The profile file reads top-level mcp and accepts mcpServers as a
compatibility alias; mcp wins when both exist, and every write uses mcp.
Suspicious server-like unsupported keys produce a bounded warning and block
profile mutation instead of being overwritten. The workspace file reads only
top-level mcpServers, accepts command plus args, and is opened as a
bounded no-follow regular file. Profile entries win native name collisions;
ACP request entries win ACP name collisions without deduplicating the request
array. Workspace entries are always optional and never load stored credentials.
Approved workspace command, args, env, and HTTP header values expand
${VAR} and ${VAR:-default} from the fx process environment. Pending and
rejected entries do not read environment values. Missing required variables
leave an approved server unloaded and appear in the /mcp and /mcp list
menu without exposing values.
Interactive sessions keep pending workspace servers disconnected and request
project trust before any project-defined process or network effect. Pending
resource, prompt, completion, and authentication commands require explicit
/mcp trust approve <name> and a retry. Rejected servers remain disconnected.
Choices live only in profile settings.json under the canonical workspace key,
using enabledMcpjsonServers, disabledMcpjsonServers, and
enableAllProjectMcpServers. Repository files cannot persist their own
approval. fx ask and ACP skip pending workspace servers. Noninteractive users
approve them first with fx mcp trust approve <name>; rejected servers remain
disabled.
The core feature surface is Tools, Resources and Resource Templates, Prompts, Completion, pagination, cache-aware discovery, subscriptions, progress, cancellation, and form or URL elicitation. Keep modern and legacy protocol behavior in their existing version-scoped modules.
fx bounds schema size and structure before publication. It accepts schemas
without $schema, the canonical JSON Schema 2020-12 declaration, and the
canonical Draft 7 declaration used by legacy SDKs; other declared dialects are
rejected. fx does not resolve network references or evaluate semantic schema
assertions. Servers validate their tool arguments and results.
The interactive surface supports:
-
/mcp -
/mcp list(opens the same server menu) -
/mcp resource list <server> -
/mcp resource templates <server> -
/mcp resource read <server> <uri> -
/mcp resource complete <server> <uri-template> <variable> [value] -
/mcp prompt list <server> -
/mcp prompt get <server> <name> [arguments-json] -
/mcp prompt complete <server> <name> <argument> [value] -
/mcp add <name> <command> [args...] -
/mcp add --transport http <name> <url> -
/mcp remove <name> -
/mcp reload -
/mcp auth <name> --open -
/mcp logout <name> -
/mcp trust approve <name> -
/mcp trust reject <name> -
/mcp trust approve-all -
/mcp trust reset -
/mcp path
The noninteractive MCP surface supports:
-
fx mcp add <name> <command> [args...] -
fx mcp add --transport http <name> <url> -
fx mcp auth <name> -
fx mcp list -
fx mcp logout <name> -
fx mcp path -
fx mcp remove <name> -
fx mcp trust approve <name> -
fx mcp trust reject <name> -
fx mcp trust approve-all -
fx mcp trust reset
The local form saves a stdio command. The HTTP form saves a remote Streamable HTTP endpoint. List reads effective profile and workspace configuration plus stored authentication state without connecting servers. Path prints the profile configuration path. Remove uses the same locked canonical profile writer as add. Trust updates the canonical workspace entry in profile settings. Auth and logout run the existing remote credential lifecycle. None of these commands constructs the TUI or contacts the Gateway.
The default MCP startup timeout is 30 seconds and remains overridable per
server with startup_timeout_ms. Exact direct docker run stdio commands
without --cidfile receive a private cidfile so fx can remove the container
after shutdown or startup failure. An explicit cidfile remains user-owned.
When a stdio server closes its connection before answering initialize, for
example because its process exited, the reported failure names the exit code
or signal and includes a bounded, terminal-safe excerpt of the server's stderr
with secrets masked. A startup timeout names the limit that ran out, plus an
earlier launch's exit when there was one, and names the startup_timeout_ms
key when that setting set the limit. When a server writes a stdout line that
is not an MCP message, such as a banner, the failure quotes the start of that
line. A startup restart runs only when it could change the outcome: a server
that closed its connection at every offered protocol version is not
restarted, and neither is one whose startup deadline has already passed. A
server that fx stopped because of invalid output still gets its restart. The
model sees the same reason when it searches a named server that is down, or
when a tool call finds its server stopped and the relaunch fails.
MongoDB Atlas Managed MCP configuration service accounts use the OAuth
client-credentials grant. fx does not implement that grant directly. Use
MongoDB's mongodb-atlas-mcp-remote stdio wrapper with inherited
MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables.
The Atlas App Connection browser flow is user-delegated access and must not be
treated as equivalent to configuration service-account credentials.
Remote authentication supports configured bearer tokens and OAuth credential
discovery, persistence, refresh, scope challenges, and logout. Credential and
private-cache identity changes invalidate prior private state. macOS persists
OAuth credentials in Keychain and migrates the private profile credential file
only after verified publication. If the user account has no default Keychain,
macOS falls back to the same 0600 credential file used on other platforms
under the 0700 profile directory. FX_DISABLE_KEYCHAIN=1 selects that portable
backend explicitly for deterministic tests and local troubleshooting.
Servers are optional by default. Required startup failures block the first TUI
or fx ask model request; optional failures publish a reduced, degraded
capability set. Terminal fx ask completes admitted MCP discovery before its
first model request. JSON and other headless asks start required servers first
and defer optional servers until the turn performs an MCP operation or delegates
MCP capability to a child. Server-filtered searches, selected tools, and feature
operations activate only their target; a broad search activates the broader
catalog. Each server owns its startup and recovery progress. Connection deadlines
cover discovery, fallback, and restarts together. Interactive authentication and
logout change only the affected connection. /mcp and /mcp list open the same
bounded, secret-free menu, which refreshes its live health snapshot while open.
Noninteractive fx mcp list renders the health snapshot to stdout.
Search and explicit selection share bounded schema publication. Definitions are
checked against their runtime, connection, catalog, and credential generations
before execution. Tool argument JSON must be bounded and object-shaped; semantic
schema assertions belong to the server. Image results use the shared tool-result,
provider, and versioned history paths. Saved native images use managed result
artifacts that read_tool_result can load without repeating the original tool.
/mcp reload evaluates a replacement before publication, so invalid config or
a required-server failure leaves the prior runtime callable.
ACP-provided servers are isolated to their owning ACP session. One-off and persistent subagents receive an immutable, permission-filtered view of the parent or ACP session's admitted MCP tools, resources, prompts, and completion capability. Missing, revoked, stale, or closed authority fails before transport.
ACP clients can also serve an MCP server over the ACP connection itself, as
described in the MCP-over-ACP RFD, by declaring
{"type":"acp","name":"...","serverId":"..."} in mcpServers. fx advertises
mcpCapabilities.acp and sends each modern MCP request as an mcp/message
request with a fresh logical requestId; the client answers with a result or
error carrier, and timeouts or cancellation send $/cancel_request. These
servers connect from the prompt worker at the start of the first turn, because
discovery waits for replies that only the connection reader delivers, so a
server that fails to connect appears in the model's server catalog instead of
failing session setup. Request-scoped mcp/message notifications such as
progress are not delivered to operations yet.
ACP Embedding
fx acp extends ACP v1 for clients that embed it. Extensions are read and
written under _meta.fx. Start the server with fx acp --ultrafast or
fx acp --no-ultrafast to set a process-local default request for sessions
created by that server; the request still requires a model that advertises
Ultra eligibility.
- Client MCP tools stay loaded: tool schemas from servers in
mcpServersare advertised on every turn within themcp_selected_schema_bytesbudget. Set_meta.fx.alwaysLoadedtofalseon a server entry to load it on demand instead. A model call naming a live MCP tool that is not loaded is loaded and admitted through the normal MCP permission path instead of failing. - Steering: a
session/promptwith_meta.fx.steerset totruewhile a turn runs joins that turn at its next safe boundary and is replayed as auser_message_chunkwhose_meta.fx.steering.requestIdnames the request. Its response arrives when that turn ends, with the turn'sstopReasonand_meta.fx.steeringset toabsorbed. Steering queued when a turn is cancelled answerscancelledwithdropped; steering that a turn ended without reading returns an error so the client can send it again. Without the opt-in, a second prompt keeps thePrompt already in progresserror.session/loadreplays absorbed steering in place with a nullrequestId.initializereports support inagentCapabilities._meta.fx.steering. - Client system prompt:
session/newacceptssystemPrompttext blocks inappendmode, as proposed in the client system prompt RFD, andinitializeadvertisessessionCapabilities.systemPrompt._meta.fx.systemPromptaccepts the same blocks for SDKs that drop unknown fields. The prompt follows fx's own instructions in the system slot, is stored with the session, and is restored onsession/loadandsession/resume. Set_meta.fx.terminaltofalseoninitializewhen fx is not presented in a terminal; fx then drops its terminal identity and rendering guidance. - Tool call metadata: each
tool_callupdate carries_meta.fx.toolCall.internal, which is true for fx's own discovery and bookkeeping steps such ascapability_search,mcp_select_tool, andread_tool_result. MCP tool calls addmcp.serverandmcp.tooland use the server's tool title, falling back to the tool's own name.session/loadreplays the same metadata, including for servers that reconnect only on the next turn. - Session workspace: an absolute
cwdonsession/new,session/load, orsession/resumebecomes that session's workspace for file access, shell commands, project instructions, project skills, project MCP servers, and the session record. Model, provider, credentials, and permission policy stay as resolved atinitializefrom the launch directory. A client without a project can pass an empty directory it owns. - Profile MCP servers: ACP sessions use only client-supplied and approved
project servers. Set
_meta.fx.profileMcpServerstotrueon a session request to add the user's~/.fx/mcp.jsonservers. Request entries win name collisions, and profile entries win over project entries.
Permissions and Auto Mode
Security is permission-first.
-
permission_modecontrols baseline behavior (ask,auto, orfull-access;yoloremains an alias) -
permissionconfig applies OpenCode-style wildcard rules -
session
alwaysapprovals are non-persistent; command approvals match the exact command while other grant categories may use patterns -
configured denies are evaluated before saved-session rules; an exact saved-session deny can narrow a configured allow, while an exact saved-session allow can satisfy an unresolved configured ask
-
/permissions remember allow|deny <tool-name> <arguments-json>confirms and stores an exact rule only for an active saved session;/permissionslists stable rule IDs and/permissions revoke <rule-id>removes one -
routine parsed development commands and reversible new-file creation can execute without model review after configured and saved-session policy; unknown, destructive, hidden, credential-bearing, public, and overwrite effects remain on the review or approval path
-
every unresolved
autoaction receives one narrow security review after configured policy, saved-session rules, grants, and deterministic safe authority; review input always contains the exact unmasked action and targets, origin and call identity, optional host-proven current-branch evidence, and bounded unmasked terminal-safe excerpts of earlier current-turn tool results. A text match between the action and prior tool output is evidence to inspect, not proof of prompt injection or malicious activity. Prepared file mutations and other static root tools omit task text. Reviewed commands, shell input, dynamic tools, and subagent actions also receive bounded unmasked canonical current, first, and recent root requests plus explicit omission counts; the reviewer may use that context only to distinguish trusted user intent from malicious or injected influence, never to judge task quality, alignment, or authorization. Assistant prose, permission feedback, compacted summaries, the pending tool group, later results, and tool or repository text never become authority. Shell input reviews also include the owned receiving session's launch command, working directory, and bounded current screen after verifying session authority, including on resume. The launch command describes startup only; screen content remains untrusted evidence and input still receives its own review -
the reviewer returns
cautiononly for concrete prompt injection or malicious activity; destructive, risky, external, public, remote, unrequested, or task-conflicting actions clear when they are not malicious. Aclearreview authorizes only the exact unchanged action; acaution, incomplete-evidence result, or unavailable review holds only that action and returns guidance without opening a human permission screen, disabling tools, or ending the turn -
exact cautions and deterministic incomplete-evidence results are cached only for the current turn; an unavailable outcome is not cached as a security judgment, but the same exact action spends at most one unavailable review opportunity per turn and changed actions remain independently reviewable until the bounded current-turn review budget is exhausted. Each review accepts exactly one valid structured decision even with accompanying prose and may retry one malformed completion within the current attempt's deadline. A transport timeout, transient transport failure, or failed transport call is retried once with a fresh 30-second deadline; permanent transport failures, valid cautions, and cancellation are never retried. Legacy
permission_request_idinput is rejected without prompting -
host-generated review holds retain their advice for the agent and transcript, but carry a saved
review_feedbackmarker that excludes them from later security evidence, including after recovery. Old unmarked results remain untrusted evidence; never infer the marker from output text. Quoted review accusations and handling instructions as document or test data are not standalone proof of prompt injection -
execution-memory schema 10 preserves review-feedback provenance while reading older schemas with an unmarked default. Conversation records use optional metadata for marked holds. Older builds may reject the new saved metadata or recovery checkpoints; do not downgrade an active session without preserving its files
-
the sandbox backend is configured independently; full access uses an effective backend of
nonewithout rewriting the saved sandbox setting
Do not add new sensitive tool behavior without integrating it into src/core/permissions/permissions.zig.
Writing a Resize Test
Render bugs that appear during window resize are hard to reason about because the footer is inline (hugs the transcript) rather than pinned to the terminal bottom, and the 100 ms debounce can mask ordering mistakes. The testing rig covers three layers. Pick the lowest layer that can catch the bug.
Zig unit test (fastest, runs in zig build test)
Drive TranscriptRuntime against the built-in VT emulator. Assertions are on the cell grid after a sequence of writes and resize calls.
test "my resize scenario" {
var h = try Harness.init(std.testing.allocator, 80, 24, 4);
defer h.deinit();
try h.shell.initViewport(&h.metrics, 4);
try h.shell.writeTranscript(h.alloc, &h.metrics, 1024, "hello\n", true);
try h.flush();
try h.driveResize(60, 20, 4, true);
var row: std.ArrayList(u8) = .empty;
defer row.deinit(h.alloc);
try h.vt.rowText(1, &row);
try std.testing.expectEqualStrings("hello ", row.items);
}
Add it to src/ui/resize_tests.zig. See the file header for what each Harness method does.
tmux end-to-end test (real SIGWINCH, seconds per test)
For bugs that only show up with a real terminal and a real signal (timing, input integration, terminal-emulator quirks), add a scenario to tests/e2e/tui-resize.test.ts using the helpers in tmux-helpers.ts:
test("my scenario", async () => {
session = await TmuxSession.create({ width: 120, height: 40 });
await session.waitForText(">", 10_000);
await session.resizeWindow(80, 30);
const grid = await session.capturePaneGrid();
expect(findFooter(grid)).not.toBeNull();
}, 30_000);
Tape-based test (replay a real capture)
For bugs reported by a user, have them run the built binary with an exact
FX_RECORD=<path>, or use FX_DEBUG_RECORD=1 for an automatic private tape.
FX_DEBUG_RECORD_SILENT_BANNER=1 hides the developer-only startup notice from
the inline transcript without disabling capture; Ctrl+O still shows it. Drop
the tape in tests/e2e/tapes/<name>.fxtape and assert against the built replay
command:
./zig-out/bin/fx replay tests/e2e/tapes/my-bug.fxtape --golden tests/e2e/tapes/my-bug.txt
Check in the golden file and wire a regression test that re-runs fx replay in CI and diffs.
What Not To Do
-
Do not grow
main.zigwith leaf feature logic -
Do not add hidden product state that only exists in the shell
-
Do not add a second execution path for the same feature without a clear reason
-
Do not document intended behavior as if it already exists
-
Do not commit generated state from
.fx/,.zig-cache/, orzig-out/ -
Do not add a general alternate-screen (
\x1b[?1049h/l) render path. fx is inline by design except for the three exclusive owner classes represented byAlternateScreenOwner: interactive tool-approval review, the full-transcript screen, and catalog menus. Every owner must leave or explicitly hand off the alternate buffer and restore the main grid, composer, cursor, paste, mouse, focus, and keyboard modes before resolving, cancelling, or shutting down
Releases
Releases are triggered automatically when the version in src/main.zig changes on main:
- Edit
pub const version = "X.Y.Z";insrc/main.zig - Merge to
main - The release workflow checks if
vX.Y.Ztag exists; if not, it builds four platform binaries, creates the git tag, and publishes a GitHub Release with the binaries attached
The install script and fx upgrade fetch binaries from releases.fx.sh, backed by the public Vercel Blob CDN. No authentication or external CLI tools are required. The release workflow also publishes binaries to the CDN and updates latest.txt automatically.
After CI passes for a push to main, the dev release workflow publishes commit-addressed binaries and then updates dev.json. Dogfooders opt in with fx upgrade --channel dev; the choice is stored in their user settings and applies to manual upgrades, automatic upgrades, and the ctrl+g handoff. fx upgrade --channel stable returns to tagged releases. Dev publishing does not create tags or GitHub Releases.
Release notes are public product copy. Describe user-visible behavior, always spell the product fx, and omit contributor attribution, tracker references, repository or website work, delivery infrastructure, CI and test details, branch history, and implementation-only refactors. Use commits and pull requests as research evidence only. Changelog formatting and release-marker rules live in AGENTS.md.
Do not create tags manually. The workflow owns tag creation.
Validate release artifacts without publishing
Run Actions > Release on main with validate_only enabled. This builds
all four release targets, runs macOS arm64 PGSO qualification, and uses the
existing apple-signing approval to notarize both macOS targets. It does not
create a tag, publish a GitHub Release, upload to the CDN, or change a channel.
The arm64 validation retains both 4 KiB and 16 KiB signature variants of the same PGSO payload for comparison. Intel retains 4 KiB signatures. Download the workflow artifacts for matched signed-binary performance checks; notarization and smoke checks alone do not establish performance equivalence. Normal release runs keep the existing signing default.
To compare the retained signatures on an isolated runner, run Actions >
Benchmarks with signed_run set to the successful main validation run ID.
This mode downloads already-notarized binaries and does not access Apple
credentials. It records two alternating startup cohorts, an identical-control
status calibration, and a separate native image-flow memory screen. Review the
retained measurements before changing release signing defaults; successful
measurement is not performance approval.
Benchmarks
Startup latency benchmarks run automatically on every PR and push to main via .github/workflows/bench.yml.
The workflow builds a ReleaseSafe binary, then uses hyperfine to measure wall-clock time for six paths:
| Command | Budget | What it measures |
|---|---|---|
fx (startup) |
2ms | Binary launch through CLI dispatch (no TTY needed) |
fx help |
2ms | Minimal startup, pure text output |
fx status --json |
2ms | Config read + JSON serialization |
fx background --json |
2ms | Background record read |
fx doctor --json |
2ms | System checks, subprocess spawns |
fx sessions --json |
2ms | Session directory read |
On PRs the check fails if any command exceeds its budget.
The table is the authoritative Linux CI contract. Non-Linux local runs report raw means for comparison but do not assign a substitute product budget because the host process and dynamic-loader floor can independently exceed 2ms. The process baseline is diagnostic only and is never subtracted.
The startup benchmark uses FX_BENCH=1, which runs through CLI dispatch and exits before TTY initialization.
To run locally:
brew install hyperfine # macOS (one-time)
./benchmarks/startup.sh # full run (100 iterations, builds ReleaseSafe)
./benchmarks/startup.sh --quick # quick run (20 iterations)
CI uses --runs 100 with a reduced warmup and skips the build step because the
workflow builds ReleaseSafe first. Results are written to
benchmarks/results/ (gitignored).
The libfx runtime job measures cold startup, warm prompts, host-tool calls, stream throughput, and Agent cleanup. Its direct Pi comparison uses an external Zig HTTP server, Pi 0.84.4, and three alternating 100-sample rounds. On Bun, native libfx must match or beat Pi p50 and stay within 0.25 ms of Pi p95. The Node comparison is report-only because Node's bundled fetch client and Pi's dispatcher have different warm-request overhead. Both runtimes still require valid measurements, 300 samples, and exactly one inference request per prompt. Native/Wasm latency, host-tool, and resource gates remain blocking. Live model latency and bulk-stream throughput remain informational.
zig build-exe benchmarks/libfx/fake-inference-server.zig -O ReleaseSafe -femit-bin=/tmp/libfx-bench-server
node benchmarks/libfx/bench-competitive.mjs --server /tmp/libfx-bench-server --pi-root /tmp/libfx-pi --out benchmarks/results/libfx
Build the SDK artifacts and install the pinned Pi package first, as shown in
.github/workflows/bench.yml. Raw per-prompt samples remain in the output directory.
Before Marking a PR Ready
Minimum checklist:
- Run
zig fmt --check src/and the focused tests for the changed path. - Run
zig build, then exercise the change with./zig-out/bin/fx. - Push the feature branch and open a draft PR immediately.
- Require all four Full CI jobs and the final ship gate to pass for the exact current commit before marking the PR ready.
- Update
README.mdif user-facing behavior changed.