Errors & notes¶
Everything footman can say when it refuses, warns, or teaches — extracted from the source on every docs build, so this page cannot drift from the runner. Angle-bracketed parts (⟨like this⟩) are filled in at runtime; note entries print once per task on stderr and never stop the run.
footman — a task runner with typed commands and instant completion¶
AttributeError
The execution path: load tasks, refresh the manifest, run the chain¶
ConfigError
The document binder: a JSON value from the boundary into a typed shape¶
ValueError
⟨path⟩: a JSON object's keys are text — dict keys must be str, not ⟨getattr(args[0], '__name__', args[0])⟩
ValueError
ValueError
ValueError
ValueError
ValueError
ValueError
ValueError
⟨path⟩: the annotations of ⟨target.__name__⟩ did not resolve (⟨exc⟩) — a stdin payload dataclass and everything it names must be module-level, where `eval_str` can see them
ValueError
Annotation normalization and value coercion¶
EnumContractError
enum member ⟨name⟩ projects to ⟨token⟩, outside the contract's token grammar (lowercase ASCII letters, digits, '-') — rename the member
EnumContractError
enum members ⟨a.member.name⟩ and ⟨b.member.name⟩ spell ⟨sorted(clash)⟩ ambiguously on the command line — the contract refuses rather than picking
EnumContractError
SpecError
Annotated[…, ⟨name⟩]: a bare callable is not a marker — wrap it to say what it means: suggest(⟨name⟩)
ValueError
ValueError
ValueError
ValueError
Behavioural settings, discovered the same way tasks are¶
BuiltinError
builtins.discovery_mode = ⟨value⟩ in ⟨_paths.footman_config_file()⟩: one of ⟨', '.join(DISCOVERY_MODES)⟩
BuiltinError
builtins.user in ⟨_paths.footman_config_file()⟩: a list of entry-point names (user = ["acme_devkit"]), not ⟨value⟩
CascadeError
⟨source⟩: unknown cascade mode ⟨value⟩ — use 'none' (this directory only), 'repo' (the repository, default), or 'filesystem' (across repositories)
ConfigError
`completion.max_age` expects a duration — seconds, or a number with a unit ("30s", "10m", "1h", "1d"), or "off" (got ⟨value⟩)
ConfigError
`sort` expects true (list tasks alphabetically) or false (definition order, the default) — got ⟨raw⟩
ConfigError
ConfigError
ConfigError
Phrase manifest nodes for humans: labels, usage lines, examples¶
TypeError
Bind resolved segments to their task functions and run them¶
AttributeError
result.⟨name⟩ is read-only in post_task — observers see, never judge. Amend verdicts in the review window instead (pre_record, where set_returned also lives), or veto with fail(reason, code).
AttributeError
RuntimeError
task.args is not readable at pre_bind — nothing is bound yet; read the values in pre_task, the post-bind moment
RuntimeError
ValueError
--⟨cli⟩ is required and nothing supplied it — pass --⟨cli⟩ (a terminal is needed to prompt; --no-input and --json never ask).
ValueError
--⟨cli⟩ is required and reads stdin — pipe a document in, redirect a file (< payload), or pass --⟨cli⟩
ValueError
--⟨cli⟩ is required and the JSON document on stdin has no ⟨peeled.stdin.field⟩ field — add the field, or pass --⟨cli⟩
ValueError
<⟨param.name⟩> is required and reads a JSON document from stdin — pipe one in, or replay a fixture (< payload.json)
ValueError
rel=… needs a managed base and cwd='unmanaged' has none — use cwd='asinvoked' for a pinned launch-directory base
ValueError
stdin holds a JSON ⟨type(document).__name__⟩, not an object — a stdin(field) parameter reads one top-level key
ValueError
ValueError
ValueError
ValueError
ValueError
ValueError
ValueError
⟨label⟩ takes values in groups of ⟨size⟩ (⟨group.label()⟩) — got ⟨len(parts)⟩, which leaves ⟨len(parts) % size⟩ over
ValueError
ValueError
ValueError
ValueError
⟨self._reader⟩ may only read parameters declared before it, and ⟨key⟩ comes after — so it has no value yet. Move ⟨key⟩ above ⟨self._reader⟩ in the signature.
Download files into footman's cache — fetch()¶
FetchError
fetch: backend ⟨name⟩ is not installed — `pip install ⟨name⟩`, or leave [fetch] backend unset to use the stdlib
FetchError
FetchError
FetchError
FetchError
FetchError
FetchError
FetchError
FetchError
FetchError
fetch: ⟨url⟩ — ⟨exc.reason⟩. If this machine needs the system curl (a corporate proxy or TLS store), set `backend = "curl"` under [fetch] — in this project, or once for every project in ⟨_paths.footman_config_file()⟩
FetchError
Tasks as run-scoped futures: what happens when a task body calls a task¶
ChainError
⟨label⟩: this call can never return — ⟨' → '.join([*chain, chain[0]])⟩ waits on itself. A task cannot call (directly or through another task) a task that is waiting for it; declare the order with pre= instead.
ChainError
⟨name⟩ cannot be called from @⟨moment⟩, which ⟨when⟩. Edit the tree instead (inv.tasks — say pre=[⟨name⟩] on the tasks that need it), or move the call to a per-task moment (@pre_task / @post_task), which runs inside the run and gives the call a real task boundary.
ChainError
⟨name⟩ declares a serial/exclusive lane, so it cannot be called from a task body — its lane is taken at the task boundary, never mid-body. Declare it as pre=[⟨name⟩] instead.
ChainError
⟨name⟩ is an infinite task, so calling it from a task body would never return. Run it as its own segment, or declare it as pre=[⟨name⟩].
Process-globals routers — the parallel regime made real for env (and kin)¶
RuntimeError
task ⟨ctx.task or '?'⟩ reads stdin in a parallel task — declare the value with ask(), or mark the task interactive=True to own the terminal.
TypeError
ValueError
lane ⟨name⟩ is already declared at ⟨taken⟩ — one binding per resource. Share it by importing that handle; a second declaration would make two lanes that never contend.
note
task ⟨ctx.task or '?'⟩ forks — forking a threaded process is unsafe (the child can inherit locks mid-hold). Prefer run()/subprocess, or mark the task serial.
note
task ⟨ctx.task or '?'⟩ reads the process cwd, which is not this task's directory (⟨ctx.cwd⟩) — a parallel run never chdirs. cwd() is the directory you want.
note
task ⟨ctx.task or '?'⟩ spawns via raw subprocess — footman filled in ⟨' and '.join(filled)⟩ from the task context. Prefer run() for capture and reporting, or pass cwd=/env= to make it deliberate.
note
task ⟨ctx.task or '?'⟩ spawns worker processes in-process — they inherit the real environment (run-wide state like colour is published there), but not the task's overlay. A tool that parallelises itself loses little in the serial lane: mark the task serial.
note
task ⟨ctx.task or key⟩ sets ⟨key⟩ via os.environ — footman scoped it to this task (children see it, siblings don't). Say it on purpose with env= / ctx.env.
note
Manifest generation and caching — the "cold" path¶
CompleterError
dynamic choices from ⟨name⟩() failed: ⟨type(exc).__name__⟩: ⟨exc⟩ — fix the completer, or pass suggest(fn, strict=False) if this data is best-effort
SpecError
**⟨param.name⟩ is not supported — declare named parameters, or accept KEY=VALUE pairs with a dict[str, str] parameter
SpecError
<help>: 'help' is a reserved parameter name — it maps to --help, which footman intercepts anywhere on the line to show help and never run a task, so the option could never be reached. Rename it (e.g. show_help). It is the only reserved name: every other global must come before the first task, so a task parameter may reuse it (fm deploy --json binds --json to deploy).
SpecError
SpecError
<⟨param.name⟩>: Arg[…] takes at most one token; a many-valued positional is already optionalable as `Many[…]` with a default
SpecError
<⟨param.name⟩>: a dict parameter reads stdin whole (a JSON object) — stdin(field)/stdin(lines=True) do not apply
SpecError
<⟨param.name⟩>: annotation ⟨element⟩ did not resolve — a stdin payload type and everything it names must be module-level, where `eval_str` can see them
SpecError
<⟨param.name⟩>: annotation ⟨peeled.element⟩ did not resolve — a stdin payload type and everything it names must be module-level, where `eval_str` can see them
SpecError
<⟨param.name⟩>: default(…) needs a declared default to sit on — a plain Python call of the task, with no run around it, has nothing else to fall back to
SpecError
SpecError
<⟨param.name⟩>: env(⟨peeled.env⟩) needs a default — an env fallback makes the parameter optional, so it needs somewhere to fall
SpecError
SpecError
<⟨param.name⟩>: stdin(⟨marker.field⟩) binds a single value — a list parameter reads lines (stdin(lines=True)) or a JSON array (bare stdin)
SpecError
<⟨seen_optional⟩>: an Arg[…] optional positional must come last — <⟨spec['name']⟩> follows it, so which token belongs to whom would be a guess. Reorder, or make <⟨spec['name']⟩> an option.
SpecError
⟨getattr(fn, '__name__', fn)⟩: Stdout[…] and interactive=True cannot both hold — an interactive task owns the real terminal, uncaptured, and a declaring task's stdout belongs to its return value. Drop one.
Filesystem locations shared by the execution path and the completion hot¶
LocationError
the cache and ⟨what⟩ directories are both ⟨cache⟩ — they must differ, because the collector deletes from the cache by age and would eventually delete durable ⟨what⟩. Set ⟨env_var('CACHE_DIR')⟩ or ⟨env_var(var)⟩, or place them apart in the App(...).
Build the task DAG and run it — in parallel by default, or sequentially¶
ChainError
ChainError
Shell completion installers — fm --install-completion=bash|zsh|fish|pwsh¶
InstallError
InstallError
InstallError
InstallError
Separator-free chain splitting, driven purely by the manifest¶
ChainError
ChainError
ChainError
ChainError
ChainError
ChainError
ChainError
⟨seg.task⟩: <⟨p['name']⟩> must be one of ⟨'|'.join(known)⟩ — ⟨tok⟩ looks like the next task; did you forget <⟨p['name']⟩>?
ChainError
ChainError
ChainError
ChainError
ChainError
⟨token⟩ is not a task address — addresses are dot-separated names with no empty segments, like 'docs.serve'
ChainError
ChainError
ChainError
ChainError
ChainError
ChainError
⟨where⟩: ⟨label⟩ takes values in groups of ⟨size⟩ (⟨group['label']⟩) — got ⟨count⟩, which leaves ⟨count % size⟩ over
ChainError
KeyError
Steps you make yourself: the lifter, the built item, and the pump¶
RuntimeError
RuntimeError
TypeError
step .opts() got unknown option(s) ⟨unknown⟩; valid options are ⟨valid⟩. A step is anonymous, so boundary policy (confirm, sharing, gates) needs a declared task to live on.
TypeError
step ⟨label⟩ yielded ⟨yielded⟩ — a step's yields are checkpoints and carry nothing out. Write a bare `yield`; read the draft with `view = yield`; return the step's value with `return`.
TypeError
step('title') is the block form — pass the title once, positionally; title= belongs to the decorator/expression forms (step(fn, title=…))
TypeError
TypeError
step() needs its subject: a function to lift (@step / step(fn, title=…)) or a title string for the block form (with step('…'):)
TypeError
the console follows interactive task bodies — claim it with @task(interactive=True) (or lanes=(console_lane,) on the task); a step-level console hold waits for a payload that needs one.
ValueError
Compose the task surface: two typed verbs over one engine¶
RegistrationError
RegistrationError
include(⟨module.__name__⟩): the module registered no tasks and has ⟨detail⟩ to adopt — define tasks with @task/group(), or expose exactly one Group
RegistrationError
include(⟨name⟩): the module was already imported outside include(), so its tasks were never captured — call include() before anything else imports it, or have the module expose an explicit Group and pass that instead
RegistrationError
RegistrationError
RegistrationError
plugin ⟨name⟩: entry point must resolve to a footman Group (or a module of tasks), got ⟨type(loaded).__name__⟩
RegistrationError
RegistrationError
RegistrationError
⟨addr⟩ claimed by both ⟨mounted_from(theirs)⟩ and ⟨src.mounted_from⟩ — exclude= one side, retarget with into=, or pass override=True to take the later mount's
RegistrationError
RegistrationError
RegistrationError
RegistrationError
RegistrationError
The run context, the run() helper, and parallel()¶
AttributeError
Result.⟨name⟩ is sealed — a committed record is immutable. Amend verdicts in the review window (pre_record), or veto with fail(reason, code) from an observer.
LookupError
recording(answers=): the sequence for ⟨best⟩ answered its ⟨len(value)⟩ entr⟨plural⟩ and `⟨command⟩` asked for one more — the script is shorter than the story; add an entry, or make it a single value to answer every match
RuntimeError
built inside the block but never handed to it: ⟨names⟩. Building an item runs nothing — hand it to the block, p(item), or call it to run it in place. Nothing ran.
RuntimeError
given(⟨name⟩) has no answer here — presence is decided when a task's arguments are bound, so call it inside a task body during a run
RuntimeError
inherited() works inside a running task — footman resolves the task being shadowed from the one currently running
RuntimeError
no terminal is attached, so there is no one to ask. Pass a default for unattended runs, or take the value as a task parameter (a CLI flag) instead.
RuntimeError
p(item) queues work for a block, so call it inside the `with` — outside one there is nothing to join.
RuntimeError
RuntimeError
prompt(): ⟨why⟩, so nothing can be asked. Pass a default, or supply the value as a task parameter (a CLI flag).
RuntimeError
select(): nothing can be asked (no terminal, or --no-input). Pass default=…, or take the choice as a task parameter.
RuntimeError
task ⟨ctx.task or '?'⟩ calls footman.chdir() in a parallel task — the process directory belongs to no one there. The ladder: build paths from footman.cwd() (no chdir at all); or claim lanes=(cwd_lane,) if the real directory only needs to *be* the task's cwd for the duration; or mark the task serial (or exclusive) to own the real globals — footman.chdir() is legal there.
RuntimeError
⟨name⟩ does not shadow an inherited task — nothing above it in the cascade defines that name (fm --where ⟨name⟩ lists the chain)
RuntimeError
⟨self._task⟩ has not run yet — inside a `with parallel()` block a call is queued, not executed, so it has no value to use there. Read the values from the block's results after it ends.
RuntimeError
⟨what⟩ was called inside task ⟨ctx.task or '?'⟩, which is not interactive. Either mark it `@task(interactive=True)` so it owns the terminal, or declare the value as a parameter with `ask()` so footman asks before the task runs.
RuntimeError
⟨where⟩ records timing on the *running task*, so it belongs inside a task body, during a run. From a helper thread, make the stream in the body and hand the handle over — the handle remembers its task.
TypeError
Stream.section(⟨arg⟩=…) takes a datetime or epoch seconds (time.time()), got ⟨type(moment).__name__⟩
TypeError
a parallel block queues built step items — p(step(fn)(…)) — got ⟨type(item).__name__⟩. Task calls queue themselves; write them as ordinary calls.
TypeError
TypeError
chdir(⟨str(target)⟩) is relative — chdir takes a policy token or an absolute path; a relative suffix goes in rel=…
TypeError
parallel() runs tasks and steps — ⟨label⟩ is neither. footman only schedules, records, and safely cancels work it owns, and a bare callable is a stranger. Lift it and it earns a receipt too: parallel(step(fn)(…)), or step(fn, title='…') to name a lambda.
TypeError
recording(answers=): a key is a command prefix — one string ("uv tool list") or a tuple of tokens — not ⟨key⟩
TypeError
recording(answers=): the answer for ⟨key⟩ is an exception class — pass an instance (⟨value.__name__⟩(...)), which is what the call will raise
TypeError
recording(answers=): the answer for ⟨key⟩ must be a str (stdout), an int (exit code), a Result, an exception instance, or a list of those — not ⟨type(value).__name__⟩
TypeError
recording(answers=): the sequence for ⟨key⟩ must be a non-empty list of single answers — one per match
TypeError
rel=⟨str(rel)⟩ is absolute — rel is a suffix appended to the resolved base; an absolute directory goes first.
TypeError
run() runs commands; in-process work is a step. Lift the callable — @step on a def, step(fn, title='…') around one you didn't write, or `with step('…'):` over inline code — and it earns a real receipt.
TypeError
run() takes one command — a string, or a list of tokens — and the ⟨len(args)⟩ after it would be dropped rather than passed to it. Put them in the list: run([⟨spelled⟩])
TypeError
run(input=…) feeds a subprocess's standard input, and an in-process tool has none — spawn it (in_process=False) to feed it, or pass the value as an argument
ValueError
ValueError
ValueError
fail() means failure and exit code 0 is success — return 0 (or just return) spells stopping early with success
ValueError
ValueError
ValueError
prompt(): a secret answer is text — drop type=, or declare the value as a typed parameter with ask(secret=True)
ValueError
prompt(type=…): ⟨', '.join(named)⟩ decides how a parameter is filled, which a live prompt already settles — declare the value as a task parameter instead (or use select() for a menu)
ValueError
rel=… needs a managed base and cwd='unmanaged' has none — pass cwd=… explicitly, or use the asinvoked policy
ValueError
rel=… needs a managed base and cwd='unmanaged' has none — pass cwd=… explicitly, or use the asinvoked policy
ValueError
rel=⟨str(rel)⟩ is absolute — rel is a suffix on the call's cwd base; pass an absolute directory as cwd=…
ValueError
ValueError
run(shell=…) runs a command *string* through a shell; pass a str, not a list (a list is the shell-free form).
ValueError
run(timeout=…) needs a process to bound, and this call runs in-process. Spawn it (a list/str command, or .opts(in_process=False) on a tool), or drop the timeout.
ValueError
run(⟨cmd⟩): ⟨op⟩ is a shell operator, but run() does not use a shell, so it would be passed as a literal argument (the pipeline/redirect would silently not happen). Ask for a shell — run(..., shell=True) or shell='bash' — split into separate run() steps, or pass a list to use ⟨op⟩ as a literal argument.
ValueError
run(⟨which⟩=True) only applies with a shell — it hardens a shell run. Pass shell=True (or a shell name), or drop ⟨which⟩.
ValueError
ValueError
shell=True needs a POSIX shell and none was found. Install one (git bash on Windows), or use shell='pwsh' / shell='cmd'.
ValueError
shell=⟨kind⟩ is not a known shell. Use True (the policy), a strategy ('posix' / 'native'), or a shell name ('bash', 'zsh', 'sh', 'fish', 'nu', 'pwsh', 'cmd').
ValueError
ValueError
ValueError
ValueError
strict=True is not supported for the ⟨kind⟩ shell — it has no errexit/pipefail. Use bash, zsh, sh, or pwsh, or drop strict.
ValueError
this in-process call needs cwd ⟨cwd⟩ but the process is at ⟨live⟩ — footman no longer chdirs in a parallel task. Run it as a subprocess (which gets cwd= for free), build paths from footman.cwd(), declare @task(cwd='unmanaged'), or pass cwd='unmanaged' on this call if it genuinely doesn't care.
ValueError
to_argv(): no argv was recorded for `⟨self._shown⟩`. Only a spawned command has separable tokens — an in-process call, a Python callable, or a `run()` given a command *string* never had them apart. Pass a list (`run(['git', 'push'])`) or use a toolroom handle, whose calls always record their argv.
The handle a lifecycle hook is given: one object per fm line¶
Frozen
inv.⟨name⟩ cannot be set now: the invocation is editable only at pre_tasks, before anything runs. Per-task state belongs on task.state, and per-task environment on ctx.env.
Render a task tree as markdown: one page, or a linked site of pages¶
ValueError
Public parameter markers, used inside Annotated annotations¶
ValueError
ValueError
stdin(field) and stdin(lines=True) are exclusive — a field binds one JSON value, lines bind a list of tokens
The task registry: the @task / group() decorator surface¶
RegistrationError
RegistrationError
@wrap_bind ⟨name⟩ must be a generator function — write `bound = yield` then `result = yield` (a plain trio of functions is @pre_bind + @pre_task + @post_task)
RegistrationError
@wrap_task ⟨name⟩ must be a generator function — write `result = yield` where the body runs (a plain pair of functions is @pre_task + @post_task)
RegistrationError
RegistrationError
@⟨task_name(fn)⟩.wrap_bind ⟨name⟩ must be a generator function — two yields: the bind boundary, then the body
RegistrationError
@⟨task_name(fn)⟩.wrap_task ⟨name⟩ must be a generator function — write `result = yield` where the body runs
RegistrationError
GlobalOption(⟨name⟩): give the bare name — the dashes are the grammar's (and plugin globals are long-form only)
RegistrationError
a group cannot be named 'default': the name means "this group's default action" and belongs to a task — declare @⟨self.name⟩.default, or name a task 'default'
RegistrationError
RegistrationError
⟨where⟩'s default ⟨task_name(fn)⟩ is interactive but has an empty body, so it fans the group's tasks out in parallel — there is no single body to own the terminal. Give it a real body, or drop interactive.
RegistrationError
⟨where⟩: ⟨key⟩ is not a legal name — '.' is the address separator and spaces cannot be addressed; use '_' or '-' inside a name, or nest a group instead
RuntimeError
--⟨self.name⟩ has no answer here — a global option is parsed from the command line, so read ⟨type(self).__name__⟩.given inside a task or lifecycle hook, during a run
RuntimeError
--⟨self.name⟩ has no value here — a global option is parsed from the command line, so read ⟨type(self).__name__⟩.value inside a task or lifecycle hook, during a run
RuntimeError
RuntimeError
RuntimeError
RuntimeError
@wrap_task ⟨name⟩ returned without yielding — exactly one `result = yield` marks where the body runs
RuntimeError
@wrap_task ⟨name⟩ yielded a second time — one yield exactly; @wrap_bind is the two-yield wrapper that enters at the bind boundary
RuntimeError
RuntimeError
RuntimeError
RuntimeError
@⟨task_name(fn)⟩.wrap_task ⟨name⟩ returned without yielding — exactly one `result = yield` marks where the body runs
RuntimeError
TypeError
.opts() got unknown option(s) ⟨unknown⟩; valid options are ⟨valid⟩. A task's own parameters go in the call — `t.opts(atomic=True)(x=1)` — not in .opts().
TypeError
.opts(⟨name⟩=…) needs a hashable value — options key the run's deduplication — but got an unhashable ⟨type(value).__name__⟩
TypeError
cwd=⟨str(value)⟩ is relative — cwd takes a policy token (⟨', '.join(CWD_TOKENS)⟩) or an absolute path; a relative suffix goes in rel=…
TypeError
group ⟨self.name⟩ is not runnable: it has no @⟨self.name⟩.default, so there is no action to call. Add a default action, or call a task inside the group directly.
TypeError
lanes= takes Lane handles, not ⟨type(entry).__name__⟩ (⟨entry⟩). Declare the resource once — db = footman.lane('db', reason='…') — and pass the handle: lanes=(db,). Handles are what make a typo an undefined name instead of a second, silently distinct lane.
TypeError
rel=… needs a managed base and cwd='unmanaged' has none — use cwd='asinvoked' for a pinned launch-directory base
TypeError
rel=… needs a managed base and cwd='unmanaged' has none — use cwd='asinvoked' for a pinned launch-directory base
TypeError
rel=⟨str(value)⟩ is absolute — rel is a suffix appended to the resolved cwd base; an absolute directory goes in cwd=…
TypeError
retries=⟨retries⟩ — a whole number of EXTRA attempts after the first (retries=2 means up to three runs); omit it for none
TypeError