Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errors, exit codes and output streams

Normative, as-built. Companion documents: storage.md, commands.md.

Streams

StreamCarries
stdoutdata only — task listings, show, path, log, status, summaries
stderrerror: , 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

CodeMeaningProduced by
0success
1errorany anyhow error reaching main
2usage errorthe argument parser, before anything runs
3a sync merge is unresolvedthe MergePending marker error
4no task has the given idthe 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.

SituationMessage
not in a repositorynot 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 outrefs/yman/local is already checked out in another worktree of this repo; only one .yman per clone is supported
unknown tasktask <id> not found
task present but unloadabletask <id> is broken: <dir>: <why>
duplicate folders for one idduplicate task id <id>: <relA>, <relB> — paths relative to .yman, so the two may differ only in their status directory
bad statusunknown status "<s>"; allowed: <list joined by ", ">
unsupported config versioninvalid .yman/config.toml: unsupported version <n> (this yman understands 1 and 2)
a status role on a version 1 configinvalid .yman/config.toml: statuses.<key> needs version = 2; bump version in .yman/config.toml
a status role outside the listinvalid .yman/config.toml: statuses.<start|done|cancel> "<s>" is not in statuses.list
a blank statuses.list entryinvalid .yman/config.toml: statuses.list has an empty entry
a statuses.list entry with edge whitespace or a control characterinvalid .yman/config.toml: statuses.list entry "<s>" has edge whitespace or a control character — <s> has control characters escaped
a repeated statuses.list entryinvalid .yman/config.toml: statuses.list lists "<s>" twice
a terminal entry outside the listinvalid .yman/config.toml: statuses.terminal entry "<s>" is not in statuses.list
a terminal entry unusable as a directoryinvalid .yman/config.toml: statuses.terminal entry "<s>" is not a usable directory name; use letters, digits, "_" and "-"
a repeated terminal entryinvalid .yman/config.toml: statuses.terminal lists "<s>" twice
terminal entries differing only in caseinvalid .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 doneinvalid .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 terminalinvalid .yman/config.toml: statuses.done "<s>" is not in statuses.terminal
the cancel status is not terminalinvalid .yman/config.toml: statuses.cancel "<s>" is not in statuses.terminal
default or the start status is terminalinvalid .yman/config.toml: statuses.terminal must not contain statuses.default "<s>" / … must not contain the start status "<s>"
empty titletitle 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 tagtag 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 valuerelated id must not be empty
set --relate or --waits-on naming the task itselftask <id> cannot relate to itself
ids.scheme = "random" with every id of random_len takenrandom id space exhausted (random_len = <n>); raise ids.random_len
seq or author scheme past the largest idid space exhausted: <max> is the largest id there can be
unknown attachmentno 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 nameattachment "<name>" given more than once
--name with several files--name only works with a single file
attachment source unusablecannot 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 holdattachment 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 folderfolder already exists: <dir>
empty comment (comment, or -m on set and the verbs)empty comment
--body-file cannot be readcannot read <path>: <why> — <why> is the OS error text
cancel with no cancel statusno 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 tasktask <id> is not closed (status "<s>"); closed statuses: <terminal joined by ", ">
rm or tags rm with no terminal and no -frefusing to remove without -f
editor could not be startedcannot run editor "<program>": <why>
editor failed during editeditor exited with status N; file left as is (or editor was killed by a signal; …)
editor failed during add -eeditor exited with status N; task not added (or editor was killed by a signal; …) — the new folder is removed
editor failed during comment -eeditor 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 terminalno terminal for vi; set $EDITOR, or use -m / --body-file
rm or tags rm prompt declinedaborted
t.md unparsablet.md must start with "# Title"
t.md unparsable after an editt.md invalid after edit: <why>; fix the file then run: yman edit <id>
t.md unparsable after add -et.md invalid after edit: <why>; task not added — the new folder is removed
invalid configinvalid .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 originno "origin" remote; tasks are local only. Connect one: yman init --remote <url>
unrelated historiestask 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 unresolvableauthor prefix unknown; run: git config yman.author <prefix> (or set YMAN_AUTHOR)
author prefix not a legal id prefixauthor prefix "<p>" from <source> must be letters, digits, "_" or "-" — <source> is yman.author, $YMAN_AUTHOR or user.name initials
git identity missinggit identity missing; run: git config --global user.name "…" && git config --global user.email "…"
git binary absentgit not found in PATH
any other git failuregit <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-wayrenumber aborted; .yman restored: <why> — .yman is back at the commit before the renumber
push rejected while finishing a mergeorigin moved while finishing the merge; run: yman sync
network step failedfetch 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 failedfetch 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 stringcore.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 writecannot create <dir>: <why> / cannot write man pages to <dir>: <why>
m.yml merge driver could not fall backgit merge-file failed, with git’s stderr printed above — reaches the user through git’s own merge output

Exit-code-3 messages

SituationMessage
mutating command during a mergesync merge in progress; resolve conflicts then run: yman sync --continue (or: yman sync --abort)
sync during a mergemerge in progress; resolve then: yman sync --continue (or --abort)
merge produced conflictsconflicts in N file(s); edit them, remove markers, then: yman sync --continue (or: yman sync --abort)
--continue with markers leftstill unmerged: <files>
--continue with a conflict that has no markersstill 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 taskconflict markers or invalid task in <dir>: <why>
--continue with an unloadable config.tomlinvalid .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:

MessageWhen
warning: id scheme is "<s>" (from config.toml); --id-scheme ignoredinit on an existing history with a conflicting flag
warning: origin already points at <url>; --remote ignoredinit --remote <url> where origin already exists with a different URL; the same URL is accepted silently
warning: remote already had tasks; adopted remote statetwo clones initialized the tracker at once
warning: not published to origin (<why>); run: yman synca 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 itthe ref was deleted server-side
warning: <name> is N MiB; git is not great at large binariesattaching 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 syncyman.autosync = push and the push after a change was rejected; nothing is merged, the change stays committed
warning: autosync failed: <why>; run: yman syncyman.autosync = push and the push after a change failed (offline, auth); the command still succeeds
warning: task <id> does not exist here; relating anywayadd --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 existssync 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/localinit repaired a .yman HEAD that had lost its symbolic ref
note: .yman has uncommitted changes, refresh skippedyman 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 syncyman refresh found local ahead of or diverged from the remote
note: rewrote N reference(s) to renumbered idsa 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 gonea 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 status value that is no longer in config.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 set moves it.
  • A fetch that fails only because origin has no refs/tasks/main yet.
  • A task folder that fails to load: it becomes a broken entry that ls shows and other commands refuse individually, rather than an error that hides every other task.