Errors, exit codes and output streams
Normative, as-built. Companion documents: storage.md, commands.md.
Streams
| Stream | Carries |
|---|---|
| stdout | data only — task listings, show, path, log, status, summaries |
| stderr | error: , warning: , note: , and git’s own output when passed through |
This split is a contract, not a style choice: cd $(yman path 14) and
yman ls --json \| jq must stay usable. Nothing decorative goes to stdout, and
there is no colour anywhere.
The rm and tags rm confirmation prompts are on stderr too, since they
are not data. Every stderr line yman writes starts with error: ,
warning: , note: , or two spaces when it continues the line above: the
command to add under note: hook <name> exists; add this line to it:, each
file under the note: unmerged files: that sync prints when a merge stops,
git’s stderr quoted inside a message (git <sub> failed: …,
warning: refresh failed: …) and the ! [remote rejected] … line sync
prints for a refused push. Git’s own output, printed verbatim above a failure,
is the one exception.
Exit codes
| Code | Meaning | Produced by |
|---|---|---|
0 | success | |
1 | error | any anyhow error reaching main |
2 | usage error | the argument parser, before anything runs |
3 | a sync merge is unresolved | the MergePending marker error |
4 | no task has the given id | the NotFound marker error, from task::find |
yman git is outside this table: it exits with git’s own code, which can be
1, 128 or anything else git uses, or 128 + signal when git was killed, so a
3 or 4 after it means nothing to yman.
A write to a closed pipe is not an error either: yman ls | head -1 ends
with yman killed by SIGPIPE once head has gone, silently, the way git and
coreutils end. A shell reports that as status 141 (128 + 13), and only for the
command on the left of the pipe; command substitution such as
cd $(yman path 14) reads everything and never sees it.
Messages that carry code 3, beyond the preflight and merge ones: a
sync --continue that would leave two folders for one id reports
duplicate task id <id>: <relA>, <relB>; delete one folder, then: yman sync --continue.
Code 4 is only task <id> not found. A folder that exists but is broken or
duplicated is a repository problem, not a missing task, and stays at code 1.
A verb given several ids exits 4 at the first missing one, with the tasks
before it already committed.
Codes 3 and 4 travel as distinct error types (src/errors.rs) and are
recognised in main by downcasting. Returning the same text as a plain
anyhow error would silently degrade it to code 1, which scripts cannot
distinguish from a genuine failure.
Every error prints as error: {message}, using anyhow’s {:#} so the whole
context chain is shown.
Pinned messages
These strings are part of the interface. Several are asserted verbatim in
tests/cli.rs; changing one means changing the test and this table in the same
commit.
| Situation | Message |
|---|---|
| not in a repository | not inside a git repository |
.yman missing | .yman is not initialized; run: yman init |
.yman HEAD wrong | .yman worktree is not on refs/yman/local; run: yman init |
init cannot re-attach HEAD without losing work | .yman HEAD has commits or changes not on refs/yman/local; resolve them with yman git, then rerun: yman init |
.yman is someone else’s directory | .yman exists and is not a yman worktree; move it aside and rerun |
.yman is a standalone repo | .yman is a standalone git repository, not a worktree; move it aside and rerun |
| ref already checked out | refs/yman/local is already checked out in another worktree of this repo; only one .yman per clone is supported |
| unknown task | task <id> not found |
| task present but unloadable | task <id> is broken: <dir>: <why> |
| duplicate folders for one id | duplicate task id <id>: <relA>, <relB> — paths relative to .yman, so the two may differ only in their status directory |
| bad status | unknown status "<s>"; allowed: <list joined by ", "> |
| unsupported config version | invalid .yman/config.toml: unsupported version <n> (this yman understands 1 and 2) |
| a status role on a version 1 config | invalid .yman/config.toml: statuses.<key> needs version = 2; bump version in .yman/config.toml |
| a status role outside the list | invalid .yman/config.toml: statuses.<start|done|cancel> "<s>" is not in statuses.list |
a blank statuses.list entry | invalid .yman/config.toml: statuses.list has an empty entry |
a statuses.list entry with edge whitespace or a control character | invalid .yman/config.toml: statuses.list entry "<s>" has edge whitespace or a control character — <s> has control characters escaped |
a repeated statuses.list entry | invalid .yman/config.toml: statuses.list lists "<s>" twice |
| a terminal entry outside the list | invalid .yman/config.toml: statuses.terminal entry "<s>" is not in statuses.list |
| a terminal entry unusable as a directory | invalid .yman/config.toml: statuses.terminal entry "<s>" is not a usable directory name; use letters, digits, "_" and "-" |
| a repeated terminal entry | invalid .yman/config.toml: statuses.terminal lists "<s>" twice |
| terminal entries differing only in case | invalid .yman/config.toml: statuses.terminal entries "<a>" and "<b>" differ only in case; they would collide on a case-insensitive filesystem |
| several terminal statuses and no named done | invalid .yman/config.toml: statuses.terminal lists more than one closed status, so statuses.done must say which one \yman done` means` |
| the done status is not terminal | invalid .yman/config.toml: statuses.done "<s>" is not in statuses.terminal |
| the cancel status is not terminal | invalid .yman/config.toml: statuses.cancel "<s>" is not in statuses.terminal |
default or the start status is terminal | invalid .yman/config.toml: statuses.terminal must not contain statuses.default "<s>" / … must not contain the start status "<s>" |
| empty title | title must not be empty |
title with a line break or another control character (the Unicode line and paragraph separators U+2028 and U+2029 and the bidi controls U+202A-U+202E and U+2066-U+2069 count as control characters here) (add, set --title, each add --sections heading, the title left by add -e or edit; tab included) | invalid title "<t>"; titles must not contain line breaks or other control characters — <t> is the trimmed title with control characters escaped (\n, \t, \u{1b}); under add -e it arrives as t.md invalid after edit: invalid title …; task not added, under edit as t.md invalid after edit: invalid title …; fix the file then run: yman edit <id> |
| empty tag | tag must not be empty |
| tag with a separator or a control character (the Unicode line and paragraph separators U+2028 and U+2029 and the bidi controls U+202A-U+202E and U+2066-U+2069 count as control characters here) | invalid tag "<t>"; tags must not contain whitespace, commas or control characters — <t> is the trimmed value with control characters escaped |
blank --relate or --waits-on value | related id must not be empty |
set --relate or --waits-on naming the task itself | task <id> cannot relate to itself |
ids.scheme = "random" with every id of random_len taken | random id space exhausted (random_len = <n>); raise ids.random_len |
seq or author scheme past the largest id | id space exhausted: <max> is the largest id there can be |
| unknown attachment | no attachment "<name>" on task <id> |
attachment name already used, ignoring case (an m.yml entry or a file under f/) | attachment "<name>" already exists on task <id>; use --force |
| two sources with the same attachment name | attachment "<name>" given more than once |
--name with several files | --name only works with a single file |
| attachment source unusable | cannot attach <path>: <why> / … not a regular file |
attachment name with a separator, or . / .. | attachment name "<name>" must not contain a path separator |
| attachment name a Windows checkout cannot hold | attachment name "<name>" is not portable: <why> — <why> is contains '<c>' (one of :*?"<>|), contains a control character, ends with '.', ends with a space, it is a reserved device name or it is empty; <name> has control characters escaped |
add onto an existing folder | folder already exists: <dir> |
empty comment (comment, or -m on set and the verbs) | empty comment |
--body-file cannot be read | cannot read <path>: <why> — <why> is the OS error text |
cancel with no cancel status | no cancel status configured; set statuses.cancel in .yman/config.toml |
add --sections text above the first heading | <source>: line <n>: text before the first "# " heading — <source> is the path, or stdin for - |
add --sections heading with no title | <source>: line <n>: empty title |
add --sections with no heading at all | <source>: no "# " heading, so no tasks |
add --sections with a code fence never closed | <source>: line <n>: unclosed code fence — <n> is the line that opened it |
--waits-on with no blocked in statuses.list | --waits-on needs a "blocked" status; add it to statuses.list in .yman/config.toml |
reopen on an open task | task <id> is not closed (status "<s>"); closed statuses: <terminal joined by ", "> |
rm or tags rm with no terminal and no -f | refusing to remove without -f |
| editor could not be started | cannot run editor "<program>": <why> |
editor failed during edit | editor exited with status N; file left as is (or editor was killed by a signal; …) |
editor failed during add -e | editor exited with status N; task not added (or editor was killed by a signal; …) — the new folder is removed |
editor failed during comment -e | editor exited with status N; comment not added (or editor was killed by a signal; …) — the temp file is removed, nothing is committed |
neither $VISUAL nor $EDITOR set, stdin not a terminal | no terminal for vi; set $EDITOR, or use -m / --body-file |
rm or tags rm prompt declined | aborted |
t.md unparsable | t.md must start with "# Title" |
t.md unparsable after an edit | t.md invalid after edit: <why>; fix the file then run: yman edit <id> |
t.md unparsable after add -e | t.md invalid after edit: <why>; task not added — the new folder is removed |
| invalid config | invalid .yman/config.toml: <why> |
the history init checks out has no loadable config.toml | <source> is not a yman history (missing or invalid config.toml): <why> — <source> is refs/tasks/main on origin when init adopted origin’s tasks, refs/yman/local when it found or repaired a local history |
sync with no origin | no "origin" remote; tasks are local only. Connect one: yman init --remote <url> |
| unrelated histories | task history unrelated to origin refs/tasks/main; re-init from remote: rm -rf .yman && git update-ref -d refs/yman/local && yman init |
| author prefix unresolvable | author prefix unknown; run: git config yman.author <prefix> (or set YMAN_AUTHOR) |
| author prefix not a legal id prefix | author prefix "<p>" from <source> must be letters, digits, "_" or "-" — <source> is yman.author, $YMAN_AUTHOR or user.name initials |
| git identity missing | git identity missing; run: git config --global user.name "…" && git config --global user.email "…" |
| git binary absent | git not found in PATH |
| any other git failure | git <subcommand> failed: <stderr>, each further line of git’s stderr indented by two spaces, empty lines dropped |
push lost the race to a moving origin three times (sync) | origin keeps moving; retry yman sync |
| a sync renumber failed part-way | renumber aborted; .yman restored: <why> — .yman is back at the commit before the renumber |
| push rejected while finishing a merge | origin moved while finishing the merge; run: yman sync |
| network step failed | fetch failed / push failed / merge failed, with git’s stderr printed above; a push origin refused (any rejection but the race, commands.md step 8) also prints git’s ! [remote rejected] refs/yman/local -> refs/tasks/main (<reason>) line, and is not retried |
init fetch failed | fetch failed (see above); use --offline to skip |
init could not create .yman (any git worktree add failure but the second-checkout case above) | git worktree add failed, with git’s stderr printed above |
hooks install, remove or status with core.hooksPath set to the empty string | core.hooksPath is empty, which turns git hooks off; unset it or point it at a directory |
hooks install met a hook it does not own | <n> hook(s) not installed: <names>, after note: hook <name> exists; add this line to it: and the line itself on stderr for each |
hooks install met a hook it cannot read (permission denied, not UTF-8, a dangling symlink) | the same <n> hook(s) not installed: <names>, after warning: hook <name>: cannot read <path>: <why>; not replacing it on stderr for each; <why> is not valid UTF-8, dangling symlink or the OS error |
man --dir cannot write | cannot create <dir>: <why> / cannot write man pages to <dir>: <why> |
m.yml merge driver could not fall back | git merge-file failed, with git’s stderr printed above — reaches the user through git’s own merge output |
Exit-code-3 messages
| Situation | Message |
|---|---|
| mutating command during a merge | sync merge in progress; resolve conflicts then run: yman sync --continue (or: yman sync --abort) |
sync during a merge | merge in progress; resolve then: yman sync --continue (or --abort) |
| merge produced conflicts | conflicts in N file(s); edit them, remove markers, then: yman sync --continue (or: yman sync --abort) |
--continue with markers left | still unmerged: <files> |
--continue with a conflict that has no markers | still unmerged: <files>; no markers to remove in <files>, stage the version to keep: yman git checkout --ours|--theirs -- <file> && yman git add <file> |
--continue with an unloadable task | conflict markers or invalid task in <dir>: <why> |
--continue with an unloadable config.toml | invalid .yman/config.toml: <why> |
sync --continue or --abort with nothing to finish reports
no merge in progress with exit code 1, not 3: there is no merge pending.
Warnings and notes
Never fatal, always stderr:
| Message | When |
|---|---|
warning: id scheme is "<s>" (from config.toml); --id-scheme ignored | init on an existing history with a conflicting flag |
warning: origin already points at <url>; --remote ignored | init --remote <url> where origin already exists with a different URL; the same URL is accepted silently |
warning: remote already had tasks; adopted remote state | two clones initialized the tracker at once |
warning: not published to origin (<why>); run: yman sync | a fresh init created the tracker but its push failed (push failed, after git’s stderr), lost the race three times (origin keeps moving) or lost it and then failed the refetch (fetch failed, after git’s stderr); hooks and the summary still follow, exit 0 |
warning: refs/tasks/main disappeared from origin; will recreate it | the ref was deleted server-side |
warning: <name> is N MiB; git is not great at large binaries | attaching a file over 5 MiB |
warning: installing into core.hooksPath=<p> | hooks redirected away from .git/hooks (a whitespace-only value included, untrimmed); <p> is the directory as git resolves it (~/ expanded, a relative value joined onto the toplevel) |
warning: cannot read <path>: <why> | hooks status or hooks remove met a hook it cannot read; it reports the hook as unreadable and leaves it alone |
warning: refresh failed: <why> | the lazy refresh before a command failed; the command still runs |
warning: origin has new task commits; run: yman sync | yman.autosync = push and the push after a change was rejected; nothing is merged, the change stays committed |
warning: autosync failed: <why>; run: yman sync | yman.autosync = push and the push after a change failed (offline, auth); the command still succeeds |
warning: task <id> does not exist here; relating anyway | add --relate/--waits-on or set --relate/--waits-on naming an id no folder here carries |
warning: <id> closed with open tasks relating to it: <ids> | a close (as for the note below) of a task tagged epic while tasks relating to it are still open; the close is committed |
warning: skipped N unreadable task folder(s): <rels> | tags, tags rename, tags rm and rm walking every task |
warning: <path> not moved; <dest> already exists | sync rejoining a split folder found the same file on both sides |
note: added remote "origin" -> <url> | init --remote created the remote |
note: no "origin" remote; tasks stay local until you run: yman init --remote <url> | init in a repo with no origin and no --remote |
note: re-attached .yman HEAD to refs/yman/local | init repaired a .yman HEAD that had lost its symbolic ref |
note: .yman has uncommitted changes, refresh skipped | yman refresh backed off from a dirty .yman |
note: pushed N task commit(s) | yman.autosync = push published a change |
note: local has unpushed commits; run: yman sync | yman refresh found local ahead of or diverged from the remote |
note: rewrote N reference(s) to renumbered ids | a collision renumber moved ids other tasks related to |
note: moved N file(s) left under <old> into <rel> | a merge left files under a folder the other side had moved |
note: dropped <rel>: task <id> was removed on one side; N file(s) added on the other are gone | a merge left a comment or attachment under a task the other side had removed |
note: dropped N reference(s) to <id> | rm cleared the removed task out of other tasks’ related |
note: task <id> was closed to two different statuses; keep one of <relA>, <relB> | a merge moved one task into two different closed-status directories |
note: task folder is now <rel> | a status change moved the folder into or out of a status directory |
note: <id> no longer waits on anything open: yman move <id> <default status> | a close (done, cancel, move/set --status into a terminal status), or rm of an open task not tagged epic, left a blocked task relating to it with nothing open to wait on; nothing is moved |
warning: committed, but could not read the tasks it affects: <why> | rm, set or a status verb committed, then could not read the tasks back for its report; exit 0 |
Deliberate non-errors
- Committing when nothing is staged — an identical re-application is a no-op.
- Removing a tag, link or relation that was never there.
- A
statusvalue that is no longer inconfig.statuses.list: reported, so a config change cannot brick existing tasks. - A task folder sitting somewhere other than where its status says it belongs:
it is listed correctly, and the next
setmoves it. - A fetch that fails only because origin has no
refs/tasks/mainyet. - A task folder that fails to load: it becomes a broken entry that
lsshows and other commands refuse individually, rather than an error that hides every other task.