Skip to content

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

module 'footman' has no attribute ⟨name⟩

The execution path: load tasks, refresh the manifest, run the chain

ConfigError

config key 'tasks' expects a filename string (got ⟨filename⟩)

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

⟨path⟩: expected an array, got ⟨_json_name(value)⟩

ValueError

⟨path⟩: expected an object for ⟨target.__name__⟩, got ⟨_json_name(value)⟩

ValueError

⟨path⟩: expected an object, got ⟨_json_name(value)⟩

ValueError

⟨path⟩: expected ⟨_phrase(target)⟩, got ⟨_json_name(value)⟩

ValueError

⟨path⟩: expected ⟨_phrase(target)⟩, got ⟨_json_name(value)⟩

ValueError

⟨path⟩: must be one of ⟨'|'.join(choices)⟩ (got ⟨value⟩)

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

⟨path⟩: the document has no ⟨name⟩ field and ⟨target.__name__⟩.⟨name⟩ has no default

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

enum members ⟨seen_tokens[token]⟩ and ⟨member.name⟩ both project to token ⟨token⟩ — rename one

SpecError

Annotated[…, ⟨name⟩]: a bare callable is not a marker — wrap it to say what it means: suggest(⟨name⟩)

ValueError

expects ⟨type_phrase(tags)⟩ (got ⟨value⟩)

ValueError

expects ⟨type_phrase(tags)⟩ (got ⟨value⟩)

ValueError

field ⟨fields[0].name⟩ takes ⟨tags[0]⟩, got ⟨value⟩

ValueError

⟨value⟩ is not a valid ⟨element.__name__⟩

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

⟨path⟩: a ⟨sniffed[0].upper()⟩ byte-order mark — ⟨_UTF8_ONLY⟩

ConfigError

⟨path⟩: not a regular file — config is read from one

ConfigError

⟨path⟩: not valid UTF-8 (⟨exc.reason⟩ at byte ⟨exc.start⟩) — ⟨_UTF8_ONLY⟩

Phrase manifest nodes for humans: labels, usage lines, examples

TypeError

⟨type(value).__name__⟩ is not JSON-serialisable

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

task.⟨name⟩ is read-only — per-plugin scratch goes on task.state, per-task environment in task.env

RuntimeError

task.args is not readable at pre_bind — nothing is bound yet; read the values in pre_task, the post-bind moment

RuntimeError

task.state is per-plugin, so it is only reachable inside a lifecycle hook

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

stdin is not valid UTF-8 (⟨exc⟩) — bind the parameter as `bytes` to read the raw stream

ValueError

stdin was empty; expected a JSON document

ValueError

⟨label⟩ cannot be a ⟨peeled.container.__name__⟩ of ⟨name⟩: ⟨name⟩ is not hashable

ValueError

⟨label⟩ expects a value, not true/false (got ⟨raw⟩)

ValueError

⟨label⟩ expects true or false (got ⟨raw⟩)

ValueError

⟨label⟩ must be one of ⟨'|'.join(choices)⟩⟨extra⟩ (got ⟨value⟩)

ValueError

⟨label⟩ takes values in groups of ⟨size⟩ (⟨group.label()⟩) — got ⟨len(parts)⟩, which leaves ⟨len(parts) % size⟩ over

ValueError

⟨label⟩: the ⟨marker.field⟩ field is a JSON ⟨type(value).__name__⟩, not a single value

ValueError

⟨label⟩: ⟨value⟩ does not satisfy ⟨peeled.path_req⟩

ValueError

⟨self._reader⟩ cannot read its own value — it is the one being resolved

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

fetch: no usable backend (not even urllib?)

FetchError

fetch: unknown backend ⟨name⟩ — choose one of ⟨options⟩

FetchError

fetch: ⟨url⟩ — HTTP ⟨exc.code⟩ ⟨exc.reason⟩

FetchError

fetch: ⟨url⟩ — HTTP ⟨response.status_code⟩

FetchError

fetch: ⟨url⟩ — curl: ⟨done.stderr.strip()⟩

FetchError

fetch: ⟨url⟩ — nothing downloaded and nothing cached

FetchError

fetch: ⟨url⟩ — sha256 mismatch
  expected ⟨sha256.lower()⟩
  received ⟨actual⟩

FetchError

fetch: ⟨url⟩ — the body ended early: ⟨received⟩ of ⟨total⟩ bytes

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

fetch: ⟨url⟩ — ⟨type(exc).__name__⟩: ⟨exc⟩

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

str expected for environment keys and values

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

task ⟨name or '?'⟩ waiting for ⟨what⟩⟨held⟩

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

<⟨param.name⟩>: Arg[…] needs a default — an optional positional's absence must mean something

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

<⟨param.name⟩>: env() is not supported on dict parameters

SpecError

<⟨param.name⟩>: env(⟨peeled.env⟩) needs a default — an env fallback makes the parameter optional, so it needs somewhere to fall

SpecError

<⟨param.name⟩>: stdin(lines=True) needs a list parameter — each line binds as one element

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

dependency cycle: ⟨' -> '.join(cycle)⟩ (check the pre/post declarations of these tasks)

ChainError

group ⟨dep.name⟩ is a prerequisite but has no @group.default

Shell completion installers — fm --install-completion=bash|zsh|fish|pwsh

InstallError

could not update ⟨rc⟩ (⟨exc⟩) — remove this line yourself: ⟨line⟩

InstallError

could not update ⟨rc⟩ (⟨exc⟩) — remove this line yourself: ⟨line⟩

InstallError

nu not found on PATH — install nushell first

InstallError

pwsh (or powershell) not found on PATH — install PowerShell first

Separator-free chain splitting, driven purely by the manifest

ChainError

nested tasks use dots: '⟨'.'.join(walk_path)⟩', not '⟨spaced⟩'

ChainError

nested tasks use dots: '⟨'.'.join(walk_path)⟩', not '⟨spaced⟩'

ChainError

nested tasks use dots: '⟨prev_path⟩.⟨token⟩', not '⟨prev_path⟩ ⟨token⟩'

ChainError

⟨dotted⟩ is a group, not a task — name one of its tasks (know: ⟨known⟩)

ChainError

⟨seg.task⟩: --⟨cli⟩ expects KEY=VALUE (got ⟨pair⟩)

ChainError

⟨seg.task⟩: --⟨cli⟩ is a flag and takes no value

ChainError

⟨seg.task⟩: <⟨p['name']⟩> must be one of ⟨'|'.join(known)⟩ — ⟨tok⟩ looks like the next task; did you forget <⟨p['name']⟩>?

ChainError

⟨seg.task⟩: missing required option(s): ⟨', '.join(missing_opts)⟩

ChainError

⟨seg.task⟩: missing required positional(s): ⟨', '.join(missing)⟩

ChainError

⟨seg.task⟩: ⟨name⟩ is a global option — it goes before the first task name

ChainError

⟨token⟩ is an incomplete address (know: ⟨known⟩)

ChainError

⟨token⟩ is not a task address — addresses are dot-separated names with no empty segments, like 'docs.serve'

ChainError

⟨token⟩: ⟨dotted⟩ is a task, not a group — nothing lives beneath it

ChainError

⟨where⟩: ⟨label⟩ expects ⟨expected⟩ (got ⟨value⟩)

ChainError

⟨where⟩: ⟨label⟩ must be one of ⟨listing⟩⟨extra⟩ (got ⟨value⟩)⟨hint⟩

ChainError

⟨where⟩: ⟨label⟩ must be ⟨expect⟩ (got ⟨value⟩)

ChainError

⟨where⟩: ⟨label⟩ must be ⟨phrase⟩ (got ⟨value⟩)

ChainError

⟨where⟩: ⟨label⟩ takes values in groups of ⟨size⟩ (⟨group['label']⟩) — got ⟨count⟩, which leaves ⟨count % size⟩ over

ChainError

⟨where⟩: ⟨label⟩ takes ⟨want⟩ — got ⟨count⟩

KeyError

computed default ⟨name⟩ has no symbolic phrase

Steps you make yourself: the lifter, the built item, and the pump

RuntimeError

post_step hook ⟨name⟩ failed observing ⟨result.shown⟩: ⟨type(exc).__name__⟩: ⟨exc⟩

RuntimeError

pre_record hook ⟨name⟩ failed reviewing ⟨shown⟩: ⟨type(exc).__name__⟩: ⟨exc⟩

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

step() lifts a callable or opens a titled block; got ⟨type(target).__name__⟩

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

step .opts(color=⟨color⟩) expects one of auto|never|always — auto follows the run's own decision

Compose the task surface: two typed verbs over one engine

RegistrationError

include(⟨dotted⟩): failed to import (⟨type(exc).__name__⟩: ⟨exc⟩)

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

include(⟨source⟩): no importable module matches — ⟨segments[0]⟩ is not importable

RegistrationError

plugin ⟨name⟩: claimed by more than one distribution (⟨dists⟩)

RegistrationError

plugin ⟨name⟩: entry point must resolve to a footman Group (or a module of tasks), got ⟨type(loaded).__name__⟩

RegistrationError

plugin ⟨name⟩: failed to import (⟨type(exc).__name__⟩: ⟨exc⟩)

RegistrationError

plugin(⟨source⟩): no ⟨ENTRY_POINT_GROUP⟩ entry point matches (installed: ⟨names⟩)

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

⟨verb⟩(): no task or group at ⟨'.'.join(walked)⟩ (⟨parent⟩ has: ⟨known⟩)

RegistrationError

⟨verb⟩(): only=/exclude= filter a group's children, and ⟨landing_name⟩ is a task — mount it bare

RegistrationError

⟨verb⟩(): ⟨'.'.join(walked)⟩ is a task, not a group — nothing lives beneath it

RegistrationError

⟨verb⟩(into=⟨into⟩): ⟨addr⟩ is a task — into= names a group to graft into

RegistrationError

⟨verb⟩(⟨source⟩): no task or group at ⟨bad⟩ in the provider's tree (has: ⟨known⟩)

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

pre_record hook ⟨hook⟩ failed reviewing ⟨show_label⟩: ⟨type(exc).__name__⟩: ⟨exc⟩

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

chdir(cwd='unmanaged') has no directory to change to

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

Stream.section(): give both start= and end=, or neither — half a window places nothing

ValueError

Stream.section(⟨name⟩): end is before start — the window is ⟨begin - finish⟩s inside out

ValueError

fail() means failure and exit code 0 is success — return 0 (or just return) spells stopping early with success

ValueError

given(⟨name⟩): ⟨ctx.task or 'this task'⟩ has no parameter ⟨name⟩⟨hint⟩ (it has: ⟨listed⟩)

ValueError

prompt() takes one value — for several, use select(multiple=True) or declare a parameter (Many[…])

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

run(color=⟨color⟩) expects one of auto|never|always — auto follows the run's own decision

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

shell='cmd' is Windows-only; use 'bash' or 'pwsh'.

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

shell=⟨strat⟩: ⟨strat⟩ was not found on PATH.

ValueError

shell=⟨strat⟩: ⟨strat⟩ was not found on PATH.

ValueError

stream(''): the empty name is the task's own timeline — sections land there via footman.section()

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

no task or group named ⟨name⟩ under ⟨where⟩ (know: ⟨', '.join(known) or 'nothing'⟩)

Public parameter markers, used inside Annotated annotations

ValueError

matching() needs a glob, e.g. matching('*.json')

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

@task(uses=...) on ⟨key⟩ takes GlobalOption singletons — got ⟨type(used).__name__⟩

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

@⟨kind⟩ ⟨name⟩ takes ⟨len(positional)⟩ argument(s); ⟨kind⟩ is handed ⟨wanted⟩: def ⟨name⟩(⟨shape⟩)

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

expose=⟨value⟩ is not one of ⟨', '.join(EXPOSE_VALUES)⟩

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

@wrap_bind ⟨name⟩ finished after one yield — it takes two: `bound = yield`, then `result = yield`

RuntimeError

@wrap_bind ⟨name⟩ returned without yielding — it takes two: `bound = yield`, then `result = yield`

RuntimeError

@wrap_bind ⟨name⟩ yielded again after the result — two yields exactly

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

@⟨task_name(fn)⟩.wrap_bind ⟨name⟩ finished after one yield — two yields exactly (bind, then body)

RuntimeError

@⟨task_name(fn)⟩.wrap_bind ⟨name⟩ returned without yielding — two yields exactly

RuntimeError

@⟨task_name(fn)⟩.wrap_bind ⟨name⟩ yielded a third time — two yields exactly

RuntimeError

@⟨task_name(fn)⟩.wrap_task ⟨name⟩ returned without yielding — exactly one `result = yield` marks where the body runs

RuntimeError

@⟨task_name(fn)⟩.wrap_task ⟨name⟩ yielded a second time — one yield exactly

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

timeout=⟨timeout⟩ — a deadline must be a positive number of seconds; omit it for no deadline