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
- 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. - Show the rows as a markdown table,
P | ID | Status | Title | Tags, in the orderlsprinted them. A! <dir> broken <error>row goes under the table as-is, not dropped. - Work out what can be taken next. Prefer the best priority; within it, a
todotask that is unassigned (or assigned to you) over one somebody else already has in progress.lsrows carry no assignee: take it from theshowin step 4, or list the unassigned ones withyman ls --assignee -. Skip ablockedtask and one whose body or relations say it waits on another open task. An epic (tagepic) is closed last: offer its open steps,yman plan <id> -n 3, instead — its rows are enough; noshowper step. - Read only the top one to three candidates with
yman show <id> -n 0, for their “Where”, “Done when” and relations. Do notshowevery task. - 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:
| Flag | Meaning |
|---|---|
-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, --all | include closed tasks, hidden by default (or name one with -s) |
-n, --limit <N> | first N rows, applied after sorting |
-l, --long | each 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>(onaddorset, repeatable): it relates to the step it waits on as well as the epic and sits in statusblocked, in one commit.blockedhas to be instatuses.list; without it--waits-onfails, so relate with--relateand sayWaits 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 adone -mthat sums up the result. Closing it earlier still succeeds but warnswarning: 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 rewritesrelatedand never the prose, so an id in a body is a hint for people, not a relation.
Never do this
| Do not | Why | Instead |
|---|---|---|
yman edit <id> | always opens $VISUAL/$EDITOR/vi | yman set <id> --body "<text>" or --body-file - |
yman add -e | opens the editor before the first commit | yman add "<title>" -m "<body>" |
yman comment -e | opens the editor | yman comment <id> -m "<text>", or yman comment <id> < note.md |
yman comment <id> without -m | reads stdin until EOF; an open pipe hangs | yman comment <id> -m "<text>" |
yman rm <id> without -f | prompts on a terminal | yman rm <id> -f |
yman tags rm <tag> without -f | prompts on a terminal | yman 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.
| Code | Meaning |
|---|---|
0 | ok |
1 | error; stderr says what |
2 | usage error, from the argument parser, before anything ran |
3 | a sync merge is unresolved |
4 | no 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.