Skip to main content

fno (footnote CLI)

CLI for the footnote autonomous delivery pipeline.

Quickstart

# Install (from a local checkout of this repo)
uv tool install /path/to/fno/cli

# Sanity check
fno --version
fno --help

# Walk the backlog via the unified Rust loop (the old `fno loop` verb is removed)
fno-agents loop run --driver megawalk

Released wheels also bundle the fno-agents Rust supervisor binary, which is the default fno agents runtime when installed: the daemon-native verbs plus four of the five shared verbs (list/stop/rm/reconcile, which reached flag + stdout parity in ab-544c544e) auto-route to it. ask stays on Python — the daemon only PTY-manages codex/gemini (claude is a shellout) and returns the whole settled screen rather than the extracted reply, so routing ask would regress claude and reply output. (=python pins everything to Python, =rust forces the binary for every verb, ask included.) See docs/distribution.md for the runtime-selection table, the artifacts, the wheel binary-bundling mechanism, and the gated publish runbook.

Install channels

fno vs fno-py. The Rust mux front door owns the fno name. Bare fno launches the mux. fno mux <verb> runs natively. Other verbs forward to the Python CLI. That CLI ships as fno-py, the forward target rather than a typed command. Today, the cargo channel installs the fno front door. Manual cargo install --path crates/fno and fno doctor update do the same. PyPI/uv, brew, and fno.sh install fno-py plus the fno-agents* binaries. A follow-up tracks mux-binary delivery through those channels. Run fno doctor to inspect front-door activation or Python shadowing. In the py-wheel rows below, "the fno CLI" means "the fno-py CLI".

A channel is a complete front door - you pick one, never "install both". What each delivers:

Channel Command You get
PyPI / uv uv tool install fno or pip install fno The fno CLI, all three Rust binaries (fno-agents, fno-agents-daemon, fno-agents-worker) on PATH, and the internalized verbs (fno plan, fno notify, fno phase kill-check, ...). fno target and fno doctor bundle need the plugin clone - on a bare install they exit non-zero with an "install the plugin" message rather than a traceback.
Plugin clone claude plugin install footnote, or clone + claude --plugin-dir /path/to/footnote The full think -> plan -> do -> review -> ship pipeline (skills + hooks), plus a binary-complete fno: the installer the plugin runs at its first session prefers the published PyPI platform wheel (binaries included), falling back to a source build of cli/ (Python-only; run fno doctor update --rust for the binaries) when the wheel is unavailable.
cargo cargo install fno The fno CLI plus all three Rust binaries on PATH, via a lazy first-run bootstrap (ab-4040eee8). cargo install fno compiles a tiny shim; the first fno run provisions uv (downloading the standalone uv if absent), runs uv tool install fno (the same PyPI wheel, binaries bundled), verifies the installed package is this project's, then forwards by absolute path. The provision is a one-time multi-second step; later runs forward instantly via a sentinel. The fno-agents crate is published only for name-reservation and to back the bootstrapper; cargo install fno-agents (no Python CLI) is not a supported front door.
fno.sh curl -fsSL fno.sh | sh The no-prerequisite front door (ab-f49b54c1): the only channel that needs nothing but curl (or wget) to start. The script ensures uv is present (chaining to Astral's standalone installer when absent, reusing an existing uv when present), runs uv tool install fno (the same PyPI wheel, binaries bundled), and verifies the installed package is this project's before reporting success. uv provisions its own Python, so a bare machine with no Python and no uv reaches a working fno. Pin a release by passing the variable to the shell that runs the script (not to curl): curl -fsSL fno.sh | FNO_VERSION=1.2.3 sh. The script is repo-sourced (scripts/install/fno.sh), served verbatim over HTTPS, and inspectable in a browser.
brew brew install <owner>/fno/fno A Homebrew own-tap formula (ab-d59d219a) that installs the same PyPI platform wheel into a brew-managed venv: brew provides Python (depends_on "python@3.13"), the wheel provides the fno CLI plus all three Rust binaries, and the formula symlinks the binaries onto PATH. brew upgrade/brew uninstall manage the venv cleanly. Launch-gated on the PyPI publish + tap creation (see below).

The PyPI/uv channel is binary-complete as of ab-18563bcc: the release wheel carries all three Rust binaries, so daemon-backed verbs work with no second install. Publishing the real fno package to PyPI (over the reserved 0.0.0 placeholder) is the remaining launch step. Until then, uv tool install fno by name resolves the placeholder. The plugin installer's version guard then falls back to the source build. See docs/distribution.md for the gated publish runbook.

The cargo channel shares that PyPI gate and adds a crates.io publish (the reserved fno and fno-agents crates over their 0.0.0 placeholders). Until both land, cargo install fno by name resolves the crates.io placeholder and the first run's uv tool install fno resolves the PyPI placeholder; the channel's clean-machine smoke (cli/tests/smoke/cargo_bootstrap_smoke.sh, run on the release-wheels matrix) proves the bootstrap against the freshly built wheel today, and the by-name cargo install fno variant becomes a required check once both publishes are live.

The fno.sh channel shares the same PyPI gate (the one-liner ends in uv tool install fno by name) and adds two operator launch steps: wiring DNS for fno.sh to Cloudflare with CF-managed TLS, and a repo-to-host deploy of scripts/install/fno.sh (Cloudflare Pages git-integration or a wrangler CI step) with purge-on-deploy so a script update propagates. Until those land, the channel's clean-machine smoke (cli/tests/smoke/fno_sh_smoke.sh, run on the release-wheels matrix) proves the installer against the freshly built wheel via FNO_INSTALL_WHEEL, and a PR-time portability gate (sh -n + dash -n + shellcheck --shell=sh) keeps a non-POSIX script from ever reaching the host. The by-name curl -fsSL fno.sh | sh becomes live once the PyPI publish and the Cloudflare wiring are both in place.

The brew channel shares the same PyPI gate and is the most standard of the four: a Homebrew formula (scripts/install/homebrew/fno.rb) builds a venv from the python@3.13 dependency and pip-installs the published platform wheel into it (the wheel url uses :nounzip so it stays a file, since an unpacked wheel directory is not pip-installable; deps resolve from PyPI because an own-tap formula installs with network). The one wrinkle is the shared "py-wheel flywheel" fact - the three fno-agents* binaries ride in the wheel as shared_scripts, which pip does not link into the keg bin (only the fno-py console_script), so the formula symlinks them onto PATH explicitly (bin.install_symlink Dir[libexec/"bin/fno-agents*"]). The formula lives in an own tap (github.com/<owner>/homebrew-fno), movable later to an org tap by GitHub repo transfer + redirect (never delete the old repo - that breaks the redirect). Until launch the committed fno.rb is a skeleton (placeholder url/sha256); its clean-machine smoke (cli/tests/smoke/brew_formula_smoke.sh, run on the release-wheels macOS matrix) installs a concrete local-wheel formula with the SAME mechanism end to end - brew install -> three binaries on the keg bin -> brew test -> clean brew uninstall - against the freshly built wheel. The by-name brew install <owner>/fno/fno and the full brew audit --strict --new go live once the PyPI publish and the tap creation are both done.

Compat alias: footnote

The binary was renamed from footnote to fno. The legacy name still works for one release but prints a deprecation warning on stderr:

footnote --version
# footnote: deprecated; use 'fno' instead. The 'footnote' alias will be removed in the next release.
# footnote 0.1.0

Prefer fno in scripts so upgrades don't break you.

Removed binary: megawalk

Older releases shipped a standalone megawalk console script that pointed at the same backlog-iteration loop now reachable through fno megawalk. The standalone entry point was removed because the two surfaces had drifted apart in flag declarations and bootstrap defaults; a single canonical surface eliminates the drift hazard.

After upgrade, which megawalk returns nothing and megawalk --help reports command not found. The replacement is the fno megawalk subcommand (or its deprecated footnote megawalk alias):

# old
megawalk --json

# new
fno megawalk --json

Subcommands status, pause, resume, bootstrap, and reset continue to work under fno megawalk. The Python module fno.megawalk and the public run_megawalk function are unchanged except for a new optional scope_all keyword (default False), so existing importers require no edits.

Selection is project-scoped by default (ab-82e65b72): fno megawalk only walks nodes in the current project (derived from cwd, matching fno backlog next). Pass --all / -A for cross-project selection:

fno megawalk          # current project only
fno megawalk --all    # all projects (prior global behavior)

v2 state layout (vestigial)

The v2 state layout was a migration-era scheme that separated CLI-native loop state from the bash pipeline's .fno/ directory. It never became the live layout, and the control-plane collapse (ab-d0337fbc) superseded it: the session manifest is now the inputs-only, immutable .fno/target-state.md (see docs/architecture/control-plane-loop.md), and there is no gate machinery, so the .fno/v2/artifacts/ gate directory the scheme described no longer exists anywhere.

What remains is vestigial:

  • fno state show --v2 still accepts the flag and reads .fno/v2/target-state.md if that file happens to exist, falling back to v1 with a stderr note otherwise. Nothing writes that path.
  • config.v2_enabled is still parseable in settings but no longer gates a live code path (the fno loop --v2 enable path was removed with the fno loop verb).

Do not treat v2 as a layout you can opt into; use the canonical .fno/target-state.md manifest.

Subcommand trees

Run fno --help for the full list. The main trees are state, graph (backlog), runtime, worker, event, and agents.

Backlog commands (top-level)

  • fno backlog done [id-or-title] [--pr N] [--link URL] [--note TEXT] [--backfill] -- mark a graph node done. A bare node id runs the canonical close (gates, plan stamp, epic cascade); the completion surface (no id, or any of the flags) auto-detects from the current git branch + gh pr view for domain: code. For non-code domains (research, design, trading, etc.) pass --link for an artifact URL or --note for a free-text marker. Also rolls up session_id, cost_usd, cost_sessions, and points from ~/.fno/ledger.json entries matching the node's plan_path. --backfill runs only the rollup (no status change); without a query it sweeps every node with status=done, useful for reconciling nodes marked done before the rollup existed.
  • fno backlog find <query> [--domain X] [--project Y] [--status S] [--json] -- fuzzy search across graph entries. Output is tab-separated by default (id, status, domain, project, title); --json emits an array.
  • fno backlog new <title> [--domain X] [--project Y] [--priority P] [--force-domain] -- create a new graph entry from the CLI without a plan file. Ambiguous --domain values (fuzzy prefix match against history) exit 2 with a "did you mean" suggestion; pass --force-domain to introduce a genuinely new domain.

See fno backlog done --help, fno backlog find --help, fno backlog new --help for the full flag list.

Key exit codes (see fno <sub> --help for per-command specifics):

Code Meaning
0 Success
2 Hard error
3 State file missing/invalid
4 triage health --check: thresholds breached
11 Resource locked (e.g. fno agents ask blocked by a busy worker)
42 Dispatch required; stdout carries a JSON handoff payload

Backlog health monitoring

# Default report (always exit 0; same shape as before --check landed)
fno backlog triage health [--all] [--json]

# Loop-safe check: exit 0 healthy, exit 4 breach, no stdout when healthy
fno backlog triage health --check --quiet [--all]

# History readout (rolling window)
fno backlog triage trend [--days N] [--json]

Configuration lives under config.health_monitor in ~/.fno/config.toml (or the project-local override). Defaults ship as documented in CLAUDE.md > Backlog Health Monitoring; the typical hourly invocation is:

/loop 1h fno backlog triage health --check --quiet

/loop survives across context compactions and Claude Code session boundaries. Same command works under cron, launchd, or GitHub Actions schedules - non-zero exit pages a monitoring tool.

fno agents mail (cross-project messaging)

Each agent owns a folder at ~/your-vault/internal/agents/{project}/ with inbox.md as the live inbox inside it. Agents send messages to other agents' inboxes, read their own inbox at the top of every megawalk iteration, and ack each message after handling.

Set project: <name> in your .fno/config.toml to identify the sender. Without this field, fno agents mail errors with the fix string.

Verbs

  • fno agents mail send --to-project <project> --kind <kind> --body "..." - send a message
  • fno agents mail unread [--json] - list unread messages in own inbox
  • fno agents mail ack <msg-id> [--triaged-into ab-...] - mark a message handled
  • fno agents mail reply --to <msg-id> --kind <kind> --body "..." - reply to a message
  • fno agents mail list [--all] - list all messages in own inbox
  • fno agents mail lint [<project>] - find malformed messages
  • fno agents mail triage <msg-id> - LLM triage for heads-up kind (returns JSON plan)

Message kinds

Kind Use
question Ask another agent something. Interrupts mid-feature work.
answer Reply to a question. Threads via reply_to.
heads-up "This might affect you, you decide what to do." Triggers LLM triage.
notification Pure FYI, no action expected.
lesson Cross-project memory write (supervisor -> worker).

Cross-project behavior

fno agents mail writes to the recipient's inbox file. Each agent reads only its own inbox. Replies go to the original sender's inbox with reply_to: set.

The com.fno.backlog-sync launchd job already mirrors ~/.fno/graph.json to obsidian; the inbox files live directly in the obsidian vault, so no separate sync is needed.

See also

  • Design doc: ~/your-vault/internal/fno/design/2026-05-04-cross-project-inbox-fleet.md
  • Megawalk drain integration: skills/megawalk/references/inbox-handlers.md

fno whoami / fno whoami status (self-introspection)

Two read-only top-level commands that give an agent a curated view of its own operating context. Distinct from fno agents mail (cross-agent messaging) and from fno agents (the dispatch mesh); these are man self for the agent in its current operating layer. (Formerly fno agent whoami / fno agent status; the fno agent singular namespace was retired in ab-12dd2a5d when the never-auto-invoked suggest / capabilities verbs were trimmed.)

They detect three layers independently and report on all that are present:

Layer Source
fleet ~/.fno/fleet/*/00-INDEX.md whose projects: cwd matches this repo
walker .fno/megawalk-state.md
session .fno/target-state.md (preferred) or .fno/session-state.md

Commands

  • fno whoami - one-line per layer: project + fleet + walker + session + harness
  • fno whoami status - session phase + status + bounded events tail + inconsistency flags. The immutable post-wedge manifest carries no gate booleans, so phase shows n/a (collapsed) and status is derived from the latest termination event in events.jsonl; a legacy manifest with *_passed-style keys still prints a per-gate section

Shared options

  • --json / -J - structured output to stdout
  • --state-file PATH - override session-state detection (file missing -> rc=2)
  • --no-walker - suppress walker layer in output
  • --no-fleet - suppress fleet layer in output

Invariants

Both commands are read-only. No state is mutated, no events are emitted, no side effects. Tests assert this via repeated-invocation paired md5 hashing across target-state.md, megawalk-state.md, events.jsonl, and the fleet 00-INDEX.md. --json emits isoformat strings for any datetime parsed out of the manifest frontmatter, so it never crashes on a real manifest.

See also

  • Design doc: internal/fno/plans/2026-05-11-fno-agent-introspection.md
  • Plan: internal/fno/plans/2026-05-11-fno-agent-introspection/

Configuration

Project name (project:)

Each project must declare its identity for cross-project messaging:

project: acme-web

Without this, fno agents mail cannot resolve the sender. CLI errors loudly.

Inbox rotation (config.inbox.*)

config:
  inbox:
    auto_rotate: true
    max_size_bytes: 1048576
    max_read_messages: 200
    keep_recent_read: 50
    triage:
      timeout_sec: 60
      log_decisions: true

Metadata

Release files for fno 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for fno 0.4.0
File
fno-0.4.0-py3-none-manylinux_2_17_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
fno-0.4.0-py3-none-manylinux_2_17_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details
fno-0.4.0-py3-none-macosx_14_0_arm64.whl Python 3 none macOS 14.0+ ARM64 Details
fno-0.4.0-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 147.4 MB

Release files / fno-0.4.0-py3-none-manylinux_2_17_x86_64.whl

Download URL fno-0.4.0-py3-none-manylinux_2_17_x86_64.whl
Size 39.4 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
8a2f9d732228fdb6e2f49fc8339150c2bce50e1852c69e19ce4f4f600ac76c65
BLAKE2b-256 checksum
How to use checksums
7fc6331c63d22d817191e84ffcdf97352eaaf8409edb8d5b0cf89465cf54cad5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fno-0.4.0-py3-none-manylinux_2_17_aarch64.whl

Download URL fno-0.4.0-py3-none-manylinux_2_17_aarch64.whl
Size 37.2 MB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
f1176f0120c505050a6419b95490a000e7dd58dfa6072b33b5d54ba92d3b1e2f
BLAKE2b-256 checksum
How to use checksums
33fa67378eda1bcc7b407f15b924ac423b60d75a0f44ab7187b2975b7e6c2bf7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fno-0.4.0-py3-none-macosx_14_0_arm64.whl

Download URL fno-0.4.0-py3-none-macosx_14_0_arm64.whl
Size 34.5 MB
Tags Python 3 macOS 14.0+ ARM64
SHA-256 checksum
How to use checksums
e2a87137f44b216d66b08cd2bca908b3e8f9240cf513d078106b43a81803c077
BLAKE2b-256 checksum
How to use checksums
c179c5166e310a7543341519e507c5bef30cb92a68e194b4c8db892d6f214cee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fno-0.4.0-py3-none-macosx_10_12_x86_64.whl

Download URL fno-0.4.0-py3-none-macosx_10_12_x86_64.whl
Size 36.3 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
d59b3c175944152160d6409066208b8dbf040c53a172c7820d7e1655362c85db
BLAKE2b-256 checksum
How to use checksums
157377e0c879d9d54339939cf5849ac49504692a8e8a1d77a92c7907e8dceaf5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page