V2 status: current
developis an unreleased 2.0.0 with intentional CLI breaks (RELEASE-BOUNDARY). Both binary names work:funzzyand its short aliasfzz— examples usefzzconsistently.
The shortest accurate path from installation to a running workflow.
fzz init # writes the comprehensive commented starter
fzz init --template minimal # or: minimal | parallel | agentfzz init is create-only: it refuses an existing .watch.yaml
(exit 1, bytes untouched) and never overwrites or migrates. To copy a
starter into a custom path, pipe the side-effect-free export:
fzz config example minimal # prints runnable .watch.yaml bytes (byte-identical to init --template minimal)
fzz config schema # prints the JSON Schema for the jobs: format
fzz config schema --section on # one bounded section + hint for the full schemaTo rewrite a legacy config in place, use the explicit transform:
fzz migrate # .watch.yaml (or -c PATH): legacy -> jobs:, atomic, idempotentThe preferred V2 shape is an ordered jobs: list:
on:
change: "**/*"
jobs:
- name: build
run: "cargo build"Declaration order is semantic: consecutive jobs sharing the same parallel
group name may overlap; everything else runs in order.
fzz check # loads the same parser/validator the watcher usescheck reports schema errors, invalid globs/durations/concurrency, and path
existence. Exit 0 when valid.
fzz run build # run the exact target once; no watcher, no socket
fzz run "@quick" # target can be a name, @tag, or unambiguous substringrun exits with the combined outcome (0 all pass, 1 any fail).
Finite runs render one declaration-ordered JOB / RESULT / DURATION row for
each configured job. Per-job duration is the executor's monotonic elapsed
measurement, valid whether jobs are serial or parallel. The generation
Duration: remains separate wall-clock time and is never calculated by adding
job rows.
Started cancellations retain a partial duration; skipped or never-started jobs
render - and structured snapshots/events expose durationMs: null. A
recovered job appears once with its final state and a duration through final
verification. For exact integer milliseconds, inspect tasks[].durationMs in
a control snapshot or a task_terminal NDJSON event.
fzz list # configured jobs (targets)
fzz explain src/lib.rs # which jobs match/ignore a path + the filtered planfzz # zero-argument: configured watch (hero path)
fzz watch "@quick" # watch only matching targetsThis is the whole loop: config → check → run → watch.
| Goal | Command | Watcher? | Side effects |
|---|---|---|---|
| Watch + run on change | fzz / fzz watch [TARGET] |
yes | runs tasks, may open socket |
| Run once, finite | fzz run TARGET |
no | runs tasks, exits |
| Validate config | fzz check [-c PATH] |
no | none |
| List targets | fzz list |
no | none |
| Explain a path | fzz explain PATH |
no | none |
| Ad-hoc over stdin | fzz exec -- PROGRAM ARG... |
no | runs PROGRAM per stdin path |
| Control running watcher | fzz control status|list|run|emit|await|cancel|output|capabilities |
no | talks to the socket |
| Init a starter config | fzz init [--template P] |
no | writes .watch.yaml (create-only, refuses existing) |
| Migrate a legacy config | fzz migrate [-c PATH] |
no | atomic in-place rewrite (idempotent) |
| Config discovery | fzz config schema|example |
no | none (never reads project config) |
Busy policy: --on-busy wait|restart (default wait); --restart
cancels and reaps active work on a newer event. Fail fast: --fail-fast
stops at the first failing task. Logging: --log-file FILE mirrors all
output; events: --events FILE appends NDJSON run events.
Exit codes: 0 success/no-op, 1 workflow/operational failure, 2 usage. --recovery-policy prompt|skip overrides the configured policy for fzz run and watch sessions; it does not edit the config.
The preferred grouped shape (JOBS-CONFIG-CONTRACT):
on:
change: "src/**" # common change globs for all jobs
ignore: "**/*.log" # common ignore globs
socket: .tmp/funzzy/control.sock # enable the control surface
debounce: 500ms # filesystem batch window (default 1s)
watch_backend: auto # native | poll | auto (native first, poll fallback)
respect_gitignore: true # respect workspace .gitignore (default false)
execution:
concurrency: 2 # scheduler bound (default: available parallelism)
output: show-on-failure # default job output policy
recovery_policy: prompt # prompt | skip; default prompt
hooks:
success: echo "checks passed"
failure: echo "checks failed"
close: echo "watcher stopped"
jobs:
- name: lint
parallel: checks # contiguous members may overlap
run: cargo clippy
cwd: packages/core # per-job working directory
env: { MODE: prod } # per-job environment
change: "src/**" # per-job triggers
ignore: "target/**" # per-job ignores (strongest precedence)
run_on_init: true # run when the watcher starts
trigger: manual # explicit run only: never matches events or init
# (see ADVANCED-GUIDE §8)
timeout: 30m # bound execution; elapse terminates the job and
# fails the generation (not a client wait bound)
- name: format-check
run: cargo fmt --all -- --check
recovery: cargo fmt --all # offered only after an explicit failure approval- Matching: a job runs when a change glob matches and no ignore glob wins.
Explicit config
ignorebeats gitignore; gitignore applies only withrespect_gitignore: true(GITIGNORE-CONTRACT). - Manual trigger:
trigger: manualremoves a job from every automatic surface — no init run, no filesystem matching, no rooton.changeinheritance; it starts only viafzz run TARGETorfzz ctl run TARGET(MANUAL-TRIGGER-CONTRACT, ADVANCED-GUIDE §8). - Execution timeout:
timeout: <duration>(30m,90s,500ms; a bare number means seconds) bounds the job's whole invocation. On elapse the complete process group is terminated, the job records the typedtimedoutstate, the generation fails, and pre-kill output stays retrievable. It is independent from control--timeout, which bounds only the caller's wait (FINITE-JOB-TIMEOUT-CONTRACT, ADVANCED-GUIDE §8.5). - Templates:
{{filepath}}(trigger path, backward compatible),{{paths}}(whole batch, shell-escaped),{{relative_filepath}}. - Parallel groups: only consecutive jobs sharing one
parallelname may overlap; reused names across a serial job start a new barrier. Order inside a group is unspecified (PARALLEL-EXECUTION-CONTRACT). - Hooks:
hooks.successruns after each passing generation andhooks.failureafter each failing generation; neither changes the result.hooks.closeruns once, only when a ready watcher shuts down gracefully after active jobs/services are reaped. Finite commands do not run it (RUN-HOOKS-CONTRACT). - Recovery:
jobs[].recoveryis an ordered scalar or command list. A failed finite job is recoverable only underexecution.recovery_policy: prompt, after an attached TTY answersy/yes; recovery commands run once, then the original job is verified once.n, EOF, invalid input, no TTY, andskippreserve the original failure without spawning recovery. - Legacy input: root task lists and grouped
tasks:remain accepted and are rewritten deterministically withfzz migrate.
Use recovery only for a bounded, known-safe mutation. Configuration declares what may be offered; it never authorizes execution by itself:
execution:
recovery_policy: prompt
jobs:
- name: format-check @quick
run: cargo fmt --all -- --check
recovery: cargo fmt --allAfter the original check fails, Funzzy prints the exact generation, job, and
commands and asks [y/N]. Only y or yes authorizes the commands. The
recovery runs sequentially and fail-fast, followed by one verification rerun.
A declined, skipped, headless, cancelled, or failed recovery remains a final
failure. There is no automatic acceptance and Funzzy does not infer approval
from CI=true.
For CI or detached watchers, opt out explicitly:
fzz run --recovery-policy skip "@quick"
fzz watch --recovery-policy skiphooks.failure is different: it observes the final failed generation and
cannot change its result; jobs[].recovery runs before the final result and can
make verification pass. Service jobs cannot declare recovery.
| Symptom | Action |
|---|---|
fzz init refused: .watch.yaml exists |
deliberate create-only refusal; edit the file or fzz config example P to compare |
Legacy task list needs jobs: |
back up/commit, run fzz migrate [-c PATH], inspect, then fzz check; section ownership edits are manual |
| Config rejected | fzz check names the exact path; fix, re-check |
| Target not found / ambiguous | fzz list shows valid targets |
| Race-like parallel failure | rerun fzz run TARGET --sequential; parallel fail + sequential pass is parallel-sensitive evidence, not a proven race |
| Watcher noise from generated files | add ignore or set respect_gitignore: true |
| Want machine-readable output | --format toon|json|human on control; --events FILE for run events |