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

yman for agents

Tasks live in .yman/, a git worktree on a ref outside refs/heads/. Every mutation is its own commit, so there is nothing to save, stage or flush. All commands run from anywhere inside the project’s git repository.

Hot path

yman ls -n 10 --assignee -                        # pick: unassigned, best priority first
yman sync                                         # before any status change: take in other clones' work
yman show <id> -n 3                               # read: header, body, last 3 comments (current after the sync)
yman set <id> --status doing -a <me> -m "on it"   # claim: status, assignee, note, one commit
yman done <id> -m "what changed"                  # close and explain in one call
yman add "Title" -m "body" -a <me> --relate <id>  # follow-up, linked to its parent
yman sync                                         # publish: after the claim, and after the close

Syncing around status changes

Every change is a local commit; nobody else sees it until it reaches origin, and you do not see theirs until you sync.

Before any status change — set --status, start, done, move, cancel, reopen — run yman sync, then re-read the task with yman show <id> -n 0. If somebody else now has it in progress, or it is already closed, stop and say so instead of overwriting their change. This holds even with autosync: push, which only pushes and never pulls.

After claiming a task and after closing it, run yman sync again, batching any changes in between, and never finish with yman status showing ahead N. The exception: when yman status prints autosync: push, each change already pushes itself (note: pushed N task commit(s) on stderr), so there is nothing to run. If that push warns origin has new task commits; run: yman sync, do what it says.

Exit 3 from sync is a merge to resolve, below; resolve it before changing anything. no "origin" remote; tasks are local only means there is nobody to sync with: carry on without it.

When asked for the task list

  1. Run yman ls -l -n 20: open tasks with their bodies, already sorted by priority, status order and id. Raise the cap on a small tracker if the list was cut off; never run it uncapped on a large one.
  2. Show the rows as a markdown table, P | ID | Status | Title | Tags, in the order ls printed them. A ! <dir> broken <error> row goes under the table as-is, not dropped.
  3. Work out what can be taken next. Prefer the best priority; within it, a todo task that is unassigned (or assigned to you) over one somebody else already has in progress. ls rows carry no assignee: take it from the show in step 4, or list the unassigned ones with yman ls --assignee -. Skip a blocked task and one whose body or relations say it waits on another open task. An epic (tag epic) is closed last: offer its open steps, yman plan <id> -n 3, instead — its rows are enough; no show per step.
  4. Read only the top one to three candidates with yman show <id> -n 0, for their “Where”, “Done when” and relations. Do not show every task.
  5. Finish with a short recommendation: which id to take, why, and roughly how big it is.

Reading

ls prints one row per task and runs no git of its own (only the lazy refresh that precedes most commands may):

P  ID  STATUS  TITLE  TAGS  F  C

P is priority, 0 (highest) to 9. F is the attachment count, C the comment count; a zero renders blank, not 0, and trailing empty columns are dropped. The header line appears only when stdout is a terminal, so a piped ls is rows only. An empty result prints nothing at all. A folder yman cannot parse is always listed, as ! <dir> broken <error>.

Filters AND together; -s and -t are repeatable, and -t and -q compare lowercase:

FlagMeaning
-s, --status <STATUS>only these statuses; naming a closed status includes it
-t, --tag <TAG>tasks carrying all the given tags
--assignee <WHO>- means unassigned
-p, --priority <N>0 to 9
--related <ID>tasks whose related list carries ID (the steps of a plan); unknown ID lists nothing
-q, --grep <TEXT>case-insensitive substring of title or body, not a regex
-a, --allinclude closed tasks, hidden by default (or name one with -s)
-n, --limit <N>first N rows, applied after sorting
-l, --longeach task’s body under its row, indented four spaces; body in --json

Sorting is by priority, then status order, then id — so -n keeps the most important rows, not arbitrary ones.

Other readers: show <id> (one id; -n <N> keeps the last N discussion entries, -n 0 drops the discussion entirely; related by: lists the tasks whose related carries this id, closed ones included, related_by in --json; an attachment whose file was deleted outside yman is listed marked (missing), "missing":true in --json), path <id> (prints the absolute folder path and nothing else, so cd "$(yman path 14)" works), log [<id>] (-n, default 20; with an id it follows the task across renames), tags (every tag with its task count, closed tasks included) and status.

--json exists on ls, plan, show, status and tags only. There is no global --json, no -C <dir>, no --verbose. JSON keeps empty keys: empty lists are [], an absent assignee is null. The one exception is body, which ls and plan include only with -l; show always has it.

Writing

add "<title>" takes -p, -s, -t (repeatable), -m <text> for the body, --body-file <path|->, -a <who>, --link <url> and --relate <id> (both repeatable). It prints added <id> <dir>.

set <id> changes any field and needs at least one change flag, or the parser exits 2: --status, --priority, --title (re-slugs the folder), -a/--assignee, --no-assignee, --tag/--untag, --link/--unlink, --relate/--unrelate, --body <text> (empty string clears it) or --body-file <path|->, and -m <text> to comment in the same commit. List fields are sets: adding a tag that is already there, or removing one that is not, is silently a no-op. When nothing actually changed it prints no changes and commits nothing — that is success, not failure.

start, done, cancel, reopen, move <id>... <status> and prio <id>... <0-9> all take several ids and all take -m <text>. Each id is its own commit, processed in order.

comment <id> -m "<text>" appends to the discussion. Always pass -m: with no -m and stdin not a terminal it reads the comment from stdin until end of file, so a shell whose stdin is an open pipe that never closes hangs there. Reading stdin is only safe when it has a definite end, as in yman comment 14 < note.md or git log -1 | yman comment 14. attach <id> <file>... (--name <name> for a single file, --force to overwrite; over 5 MiB warns but never refuses) and detach <id> <name> handle files. tags rename <old> <new> and tags rm <tag> -f rewrite every task carrying a tag in one commit.

Prefer one verb carrying -m over a verb followed by a separate comment: half the calls, half the commits.

Plans

A plan is an ordinary task, the epic, plus steps that relate to it. related is one-way and untyped: direction and status carry the meaning, and plan, ls --related and show’s related by: read it from the epic’s end, so adding a step never edits the epic.

yman add "Ship sync v2" -t epic -p 2 -m "Goal: ... Done when: yman plan <id> lists no steps"
yman add "Fetch before merge" -p 2 --relate 30 -m "Where: ... Done when: ..."   # 30 is the epic
yman add --sections - --relate 30 -p 2 <<'EOF'   # many steps, one task per `# Title`
# Fetch before merge
Where: ... Done when: ...
# Push after merge
Where: ... Done when: ...
EOF
yman plan 30 -n 3              # what is next: counts per status, open steps, best priority first
yman plan 30 -a                # progress: every step with its status
yman show 30 -n 0              # the goal and its body

plan prints the epic, status: <s> steps: <N> todo 2, doing 1, ... (every step, closed included), then one row per open step: P ID STATUS TITLE, plus waits on <ids> on a blocked step. --json has counts and steps[].waits_on. That output is the whole answer to “what is next in this plan”: each open step’s id, priority, status, title and what it waits on are already there, so do not show the steps it lists. show a step only when its body is needed for the work itself, and plan <id> -l prints the bodies too.

  • Order steps with priority. A step that must wait gets --waits-on <id> (on add or set, repeatable): it relates to the step it waits on as well as the epic and sits in status blocked, in one commit. blocked has to be in statuses.list; without it --waits-on fails, so relate with --relate and say Waits on <id> in the body, which the task-list rule already honours.
  • Closing a step reports on stderr what it freed: note: 34 no longer waits on anything open: yman move 34 todo. Run that move (with -m "unblocked"); nothing moves by itself. An epic tag means “container”, so the epic is never counted as something to wait on.
  • Close the epic last, when yman plan <epic> lists no steps, with a done -m that sums up the result. Closing it earlier still succeeds but warns warning: 30 closed with open tasks relating to it: 35, 36; reopen it or finish those. Nest plans by relating an epic to a bigger one.
  • The link is always --relate: a renumber rewrites related and never the prose, so an id in a body is a hint for people, not a relation.

Never do this

Do notWhyInstead
yman edit <id>always opens $VISUAL/$EDITOR/viyman set <id> --body "<text>" or --body-file -
yman add -eopens the editor before the first commityman add "<title>" -m "<body>"
yman comment -eopens the editoryman comment <id> -m "<text>", or yman comment <id> < note.md
yman comment <id> without -mreads stdin until EOF; an open pipe hangsyman comment <id> -m "<text>"
yman rm <id> without -fprompts on a terminalyman rm <id> -f
yman tags rm <tag> without -fprompts on a terminalyman tags rm <tag> -f

Off a terminal, a removal without -f fails fast with refusing to remove without -f, and an editor path with neither $VISUAL nor $EDITOR set fails with no terminal for vi; set $EDITOR, or use -m / --body-file. Both mean “use the non-interactive form”, not “retry”. With $VISUAL or $EDITOR set, the editor path does not fail: that editor is started even without a terminal, and the command may hang waiting on it.

Streams and exit codes

stdout carries data only — listings, show, path, log, status, and the one-line summaries. error:, warning:, note: and pass-through git output go to stderr. There is no colour.

CodeMeaning
0ok
1error; stderr says what
2usage error, from the argument parser, before anything ran
3a sync merge is unresolved
4no task has that id (task <id> not found)

Two consequences worth planning for:

  • While a merge is unresolved, every mutating command exits 3, not just sync. Reading still works.
  • A multi-id verb that exits 4 stopped at the first missing id. The ids before it are already committed; do not re-run the whole list blindly.

Recovering from exit 3

yman status                  # names the unmerged files under .yman/
# edit them, remove the conflict markers
yman sync --continue         # or: yman sync --abort

--continue can also stop with duplicate task id <id>: <a>, <b>; delete one folder, then: yman sync --continue (still exit 3): both sides closed the same task, to two different statuses. There are no markers to remove; delete the folder you do not want and continue.

A file listed with (no conflict markers: binary, modify/delete, or the merge driver failed) holds only one side, so --continue keeps refusing it with still unmerged: <file>; no markers to remove in <file>, …. Pick a side and stage it yourself, then continue:

yman git checkout --theirs -- <file>   # or --ours; paths are relative to .yman
yman git add <file>                    # or: yman git rm <file> to keep a deletion
yman sync --continue

yman sync is the command that touches the network day to day: it fetches, merges and pushes. --no-push merges without publishing. init also fetches and pushes unless given --offline, and with yman.autosync = push every changing command pushes (never merges) after it commits.

Attribution

Export YMAN_ACTOR=<agent name> and comments and attachments are recorded as yours. It does not change the git committer, which stays whatever git says it is.

Cost

Plain output is the cheapest thing yman prints. --json repeats every key on every row, so reach for it only when the output goes into jq. Cap with -n, use show <id> -n 0 when you only need the header and body, and never run ls without -n or a filter on a large tracker. show reads every task once for related by; plan <id> costs the same and lists more.

If .yman does not exist

Every command except init, hooks, git, guide, completions and man fails with .yman is not initialized; run: yman init. The common case is one command:

yman init --remote <url>     # omit --remote when the repo already has an origin
yman init                    # no origin at all: a local-only tracker; sync refuses until --remote

If origin already carries a task history, init adopts it — there is no separate clone step. The full runbook is docs/setup.md in the yman-tracker repository.

yman guide prints a condensed version of this skill straight from the binary, works in any directory, and needs no repository.