Run checks on every edit. Give coding agents results they can trust.
Funzzy (fzz) is a fast Rust watcher for the agentic coding era. It runs local workflows as code changes and exposes exact runs, fresh results, cancellation, and bounded failure output—no log scraping, no stale green.
fzz init # create .watch.yaml
fzz check # validate it
fzz # watch, run, repeatOne YAML workflow works for developers and coding agents. Use fzz run TARGET for finite execution or control a running watcher through a deterministic, machine-readable API.
Warning
V2 (2.0.0) is the current line. v1.5.0 remains on the v1 branch; see docs/MIGRATION.md for the flag and config mapping.
For a workflow as simple as:
find . -name '*.ts' | fzz exec -- npx eslint {{relative_filepath}}Or for more complex workflows like:
# .watch.yml
on:
change: ["src/**", "tests/**"]
ignore: ["target/**", "**/*.log"]
debounce: 500ms
socket: .tmp/funzzy/control.sock
execution:
concurrency: 2
recovery_policy: prompt # prompt | skip; default prompt
recovery_timeout: 60s # approval-only bound; default 60s
hooks:
success: "notify-send 'checks passed'"
failure: "notify-send 'checks failed'"
jobs:
- name: lint @quick
parallel: checks
run: cargo clippy
- name: test @quick
parallel: checks
run: cargo test
- name: dev-server
service: true
run: cargo run
change: "src/**"Declaration order is semantic. Only consecutive jobs with same parallel name overlap; ordinary jobs create serial barriers. Legacy root task lists and grouped tasks: configs remain accepted and can be rewritten with fzz migrate.
- Watch or run once: use the same workflow locally, in CI, or in an editor feedback loop.
- Precise matching: combine change and ignore globs, optional gitignore rules, path templates, and future-file discovery.
- Deterministic batches: debounce and deduplicate filesystem events before creating one generation.
- Ordered concurrency: run consecutive named parallel groups behind explicit serial barriers; force
--sequentialfor comparison. - Managed processes: cancel and reap complete process groups; opt into long-running jobs with
service: true. - Workflow automation: run generation-level
hooks.successandhooks.failurewithout changing the workflow result. - Approved recovery: optionally run a declared
jobs[].recoveryonce after an approved failure, then verify the original job once; unanswered approval defaults to failure afterexecution.recovery_timeout(60s by default); use--recovery-policy skipin CI. - Live configuration: valid config changes hot-reload without replacing watcher identity; invalid changes fail visibly instead of leaving stale behavior running.
- Agent-ready control: query capabilities, status, targets, exact generations, retained output, duration estimates, cancellation, and fresh terminal results over a permission-restricted Unix socket.
- Observable execution: mirror logs and append schema-versioned NDJSON events for runs, tasks, groups, services, and hooks.
- Self-describing config: generate schema and examples from installed binary, then validate with same parser watcher uses.
- Lifecycle hooks:
hooks.success/hooks.failureobserve generation outcomes;hooks.closeruns one finite cleanup command when a ready watcher shuts down gracefully.
Learn more:
- Getting started and daily workflows
- Advanced control and agent workflows
- V1 to V2 migration
- Configuration schema and agent discovery
- User-approved job recovery contract
- Current-run job duration report contract
- Dependency inventory and update policy
- Pi watcher extension
- Examples
Funzzy pairs well with these tools:
-
yq - A yaml querier similar to
jqto extract commands from GitHub Actions! -
nrr - For JS/TS projects, since Funzzy runs commands on change, a faster task runner makes a difference
Traditional watchers are optimized for human watching terminal output. Agentic coding also needs exact run identity, freshness, cancellation, structured state, and bounded evidence—without replacing the fast local workflow developers already use.
Funzzy brings GitHub Actions-like checks into the local edit loop and makes that loop observable by both humans and coding agents. Rust keeps watcher fast and lightweight.
Funzzy is inspired by antr and entr.
brew install funzzybrew install cristianoliveira/tap/funzzycurl -s https://raw.githubusercontent.com/cristianoliveira/funzzy/main/linux-install.sh | shThe installer detects the architecture (x86_64/aarch64), verifies the
published sha256 checksum, and installs both funzzy and fzz into
/usr/local/bin. You can pin a release:
curl -s https://raw.githubusercontent.com/cristianoliveira/funzzy/main/linux-install.sh | bash - 2.0.0nix-env -iA nixpkgs.funzzynix profile install 'github:cristianoliveira/funzzy'
# or
nix profile install 'github:cristianoliveira/nixpkgs#funzzy'Install nightly version:
nix profile install 'github:cristianoliveira/funzzy#nightly'or, if you use shell.nix:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.funzzy
];
};cargo install funzzy*Make sure you have $HOME/.cargo/bin in your PATH
export PATH=$HOME/.cargo/bin:$PATH
- From source
Make sure you have installed the following dependencies:
- Rust (>= 1.97, the minimum supported version — declared as
rust-versioninCargo.toml) - Cargo
Execute:
cargo install --git https://github.com/cristianoliveira/funzzy.git
Or, clone this repo and run:
make installCreate a config, validate it, run once, then watch — the five-step path (full guide):
fzz init # write a runnable .watch.yaml
fzz check # validate (same parser as the watcher)
fzz list # see the configured targets
fzz run build # run the exact target once, no watcher
fzz # zero-argument configured watchThe file fzz init writes is a comprehensive commented starter: a small
active hello/change example that runs immediately, plus every supported
setting documented as a comment next to its owning section (the same
metadata drives fzz config schema). Uncomment any documented example to
activate it; fzz init && fzz is the zero-dependency trial.
Both binary names work — funzzy and its short alias fzz; examples use
fzz. fzz init is create-only (it refuses an existing .watch.yaml);
pick a starter with fzz init --template minimal|parallel|agent. Rewrite a
legacy task-list config with fzz migrate (emits the preferred jobs:
form, atomically and idempotently). The installed binary is the config
reference: fzz config schema prints the JSON Schema and
fzz config example minimal prints a runnable example straight from
the parser.
Check all the options with fzz --help
Use a different config file:
fzz -c ~/watch.yamlFail fast stops execution when any task fails. Use it when later work depends on every earlier task succeeding. See its usage in our workflow
fzz --fail-fast # or fzz -b (bail)Filtering tasks by target:
fzz list
fzz watch "@quick"
# Assuming one or more tasks contain `@quick`, only those tasks are watched.Validate the configuration without starting a watcher:
fzz check
# Loads the same parser/validator the watcher uses: schema, globs, durations,
# concurrency, parallel groups, and path existence. Never runs tasks or opens
# a socket. Exit 0 when valid, non-zero with actionable errors when not.Run same configured workflow once, without watcher or control socket:
fzz run "@quick"
# Exits with combined configured task outcome; useful in CI.Every finite local run prints one JOB / RESULT / DURATION row per configured
job in declaration order. A row's duration is that job's monotonic elapsed
runtime; it remains meaningful for both serial and parallel jobs. The final
Duration: is the separate generation wall time, never a sum of job rows.
A cancelled job that started reports its partial duration. A skipped or
never-started job prints - (durationMs: null in structured snapshots). A
recovered job has one final row whose duration includes recovery and
verification. Use the control snapshot or NDJSON task_terminal event when an
exact integer durationMs is needed.
Inspect or control a watcher configured with on.socket:
fzz ctl capabilities --format toon
fzz ctl status --format toon
fzz ctl run "@quick" --wait --timeout 5m --format toonRun an arbitrary argv over paths from stdin:
find . -name '*.rs' | fzz exec -- cargo build
find . -name '*.[jt]s' | fzz exec -- npx eslint {{filepath}}Funzzy does not implicitly invoke a shell for exec; use fzz exec -- sh -c '...' when shell operators are required.
Restart busy policy cancels and reaps active work when a newer change batch arrives. It is useful for long-running workflows. See more in long task test
fzz --on-busy restart # or fzz --restartFilesystem events are collapsed into batches: one debounce window maps to one
generation, so a burst of writes to several files runs matching tasks once
per batch, not once per duplicate event. The window defaults to one second and
is configurable with on.debounce:
on:
debounce: 500ms # <number> seconds, or <number>ms/s/m; default 1sRules:
- One batch preserves the complete normalized changed-path set (deduplicated, deterministically ordered) and the stable event kind.
- Matching runs once per batch, never once per duplicate backend event.
- Templates expose the trigger path as
{{filepath}}(backward compatible) and the full batch as{{paths}}(shell-escaped, space-joined). control emitis an explicit immediate event: it routes through the same matching and busy-run policy as a native batch but does not wait for the debounce window.- Invalid
on.debouncevalues (zero, negative, unknown suffix) fail loudly; they never silently change timing.
By default Funzzy uses the native filesystem backend and automatically falls back to deterministic polling if native watch registration fails (containers, network filesystems, WSL, unusual platforms). You can force a backend:
on:
watch_backend: auto # native first, poll fallback (default)
# watch_backend: native # fail clearly if native is unavailable
# watch_backend: poll # always poll
poll_interval: 200ms # used with poll; default 500msPolling scans watched roots for create/modify/remove changes on a fixed interval and feeds the exact same batching, matching, and execution path as the native backend. Tradeoffs: polling adds a small per-interval scan cost and change latency up to the interval; prefer native for large trees. Forced native fails with an actionable error instead of silently changing semantics.
Agents (and humans) discover the current configuration surface from the installed binary, never from stale docs. Bootstrap commands:
fzz config schema # full JSON Schema for the preferred jobs: format
fzz config schema --section parallel # one bounded section + hint for the full schema
fzz config example minimal # runnable minimal .watch.yaml
fzz config example agent # control-socket + verify-style example
fzz check # validate .watch.yaml (semantic checks)
fzz list | fzz explain PATH # inspect what would run
fzz run TARGET | fzz watch # executeAll config commands are non-interactive and side-effect-free: they never
read a project config, start a watcher, open a socket, or run tasks. The
schema is the single source of truth for structure; fzz check adds semantic
validation. Legacy root-list configs remain accepted and are rewritten with fzz migrate.
hooks:
success: ./scripts/notify-success # once per passing generation
failure: ./scripts/notify-failure # once per failing generation
close: ./scripts/cleanup # once per graceful ready-watcher closeSee run and watcher hook lifecycle for signal, timeout, reload, and exit-code semantics.
Funzzy can run independent tasks concurrently. Concurrency is opt-in: a
task belongs to a named parallel group, and only consecutive tasks sharing
the same group name may overlap. This keeps existing sequential configs
running exactly as before — no migration needed.
on:
change: "src/**"
execution:
concurrency: 4 # optional global cap on active tasks
jobs:
- name: lint
parallel: checks # group membership is explicit
run: cargo clippy
- name: test
parallel: checks
run: cargo test
- name: package
run: cargo build # serial: runs alone, after the groupThis might be due to different causes, the most common issue when using VIM is because of its default backup setting which causes changes to multiple files on save. (See Why does Vim save files with a ~ extension?). For such cases either disable the backup or ignore them in your watch rules.
For other cases use the verbose fzz -V | grep 'Triggered by' to understand what is triggering a task to be executed.
Running unit tests:
cargo testor simple make tests
Running integration tests:
make integration
We use rustfmt to format the code. To format the code run:
cargo fmt- Fork it!
- Create your feature branch:
git checkout -b my-new-feature - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin my-new-feature - Submit a pull request
- Open pull requests
- Create Issues
- Report bugs
- Suggest new features or enhancements
Any help is appreciated!
Pull Request should have unit tests
This project was made under MIT License.