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/abilities/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

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 executor, fno phase kill-check, fno event verify-evidence, ...). fno target and fno 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 plugin postinstall prefers the published PyPI platform wheel (binaries included), falling back to a source build of cli/ (Python-only; run fno 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, and the plugin postinstall's version guard 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 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, agents, megatron, and reality-check.

Backlog commands (top-level)

  • fno done [id-or-title] [--pr N] [--link URL] [--note TEXT] [--backfill] -- mark a graph node done. Auto-detects from current git branch + gh pr view for domain: code when no flags are given. 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 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 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 done --help, fno find --help, fno new --help for the full flag list. Each of these also works under the backlog/graph subapp (fno backlog find, fno backlog new, fno graph find, etc.).

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/settings.yaml (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 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/settings.yaml to identify the sender. Without this field, fno mail errors with the fix string.

Verbs

  • fno mail send --to-project <project> --kind <kind> --body "..." - send a message
  • fno mail unread [--json] - list unread messages in own inbox
  • fno mail ack <msg-id> [--triaged-into ab-...] - mark a message handled
  • fno mail reply --to <msg-id> --kind <kind> --body "..." - reply to a message
  • fno mail list [--all] - list all messages in own inbox
  • fno mail lint [<project>] - find malformed messages
  • fno 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 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 status (self-introspection)

Two read-only top-level commands that give an agent a curated view of its own operating context. Distinct from fno 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 + provider
  • fno 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-abi-agent-introspection.md
  • Plan: internal/fno/plans/2026-05-11-abi-agent-introspection/

Configuration

Project name (project:)

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

project: acme-web

Without this, fno 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.1.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.1.0
File
fno-0.1.0-py3-none-manylinux_2_17_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
fno-0.1.0-py3-none-manylinux_2_17_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details
fno-0.1.0-py3-none-macosx_14_0_arm64.whl Python 3 none macOS 14.0+ ARM64 Details
fno-0.1.0-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 19.5 MB

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

Download URL fno-0.1.0-py3-none-manylinux_2_17_x86_64.whl
Size 5.1 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
b15e97bcd60f66af59805dfa68a512eead743995162b3a636d54e551f4c184e9
BLAKE2b-256 checksum
How to use checksums
b2689133470c204f1adeecd1ef842853b36ba7d6fb86bc3203b832bf2450cbba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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.1.0-py3-none-manylinux_2_17_aarch64.whl

Download URL fno-0.1.0-py3-none-manylinux_2_17_aarch64.whl
Size 5.0 MB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b93b59788008a489259b3e2d7f813eeb5ea5f53dc1368c02ab254c03e7aebd54
BLAKE2b-256 checksum
How to use checksums
0ba0965a7e11d669689ea2f17af848f93566dedb4f026972a09743847b006f1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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.1.0-py3-none-macosx_14_0_arm64.whl

Download URL fno-0.1.0-py3-none-macosx_14_0_arm64.whl
Size 4.5 MB
Tags Python 3 macOS 14.0+ ARM64
SHA-256 checksum
How to use checksums
0c452f9b2813f35ae5f246c7be087b7ceb5699856e6b2182e5039aadb64e5533
BLAKE2b-256 checksum
How to use checksums
0f29009ccdefc9528fa2acd407b2458c87f3be0ff5424f288fc610120a5c004a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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.1.0-py3-none-macosx_10_12_x86_64.whl

Download URL fno-0.1.0-py3-none-macosx_10_12_x86_64.whl
Size 4.8 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
729af7804d7c62c0d79b5d3784e694ad8499c05d223e27acbee33f323aa50991
BLAKE2b-256 checksum
How to use checksums
847157d630e1ecda386573585f87e3893cfa191306fc86e849ca35386ea82cd9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","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