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

A task tracker that lives next to the code, travels through the project’s own git remote, and never shows up in git branch, your IDE’s branch picker, or CI.

No server, no account, no extra remote. Tasks are plain Markdown and YAML in your repository — but in a namespace ordinary git use never walks into.

$ yman init
$ yman add "Fix login" -p 2 -t auth
added 1  2.1.fix-login
$ yman start 1
1: status todo -> doing
$ yman comment 1 -m "reproduced on staging"
$ yman sync
synced  pulled 0, pushed 3, renumbered 0   refs/yman/local @ 3f2a1c9

Your teammate, in their own clone:

$ yman init
$ yman ls
2  1  doing  Fix login  auth    1

Install

Requires Rust 1.88 or newer (rust-version in Cargo.toml) and a git binary, 2.42 or newer. Nothing checks the git version at run time; CI runs the suite against the git that GitHub’s ubuntu-latest image ships.

cargo install yman-tracker      # from crates.io
cargo install --path .          # from a checkout

The binary is called yman. It must be on your PATH for the optional git hooks to find it.

Shell completions and man pages come from the binary itself, for example:

yman completions bash > ~/.local/share/bash-completion/completions/yman
yman completions zsh  > ~/.zfunc/_yman        # a directory on your $fpath
yman completions fish > ~/.config/fish/completions/yman.fish
yman man --dir ~/.local/share/man/man1

Then, in any repository with an origin you can push to:

yman init

That is the only setup step. It creates .yman/, adds a fetch refspec, the refresh policy and the m.yml merge driver to the repository config, and excludes .yman/ from the code worktree. Everyone else on the project runs the same command and gets the existing tasks.

A repository with no origin gets a local-only tracker: everything except yman sync works, and yman init --remote <url> connects it later. The next sync publishes the tasks you made in the meantime.


Why it stays out of the way

A task tracker inside a repository usually means a branch, and a branch means a CI run, an entry in every branch picker, and a merge conflict waiting to happen.

yman stores the task history under refs/yman/local — a ref outside refs/heads/. Git is perfectly happy to keep commits there; almost nothing goes looking for them:

CommandSees the task history?
git branch -ano
git status, git log, git diffno
your IDE’s branch listno
your CI providerno — refs/tasks/main is not a branch
git log --all, git show-ref, git worktree listyes

.yman/ is a linked git worktree of the same repository whose HEAD is that symbolic ref, so your code worktree keeps a clean git status no matter how many tasks you write.

ThingWhere it lives
Task history, locallyrefs/yman/local
What origin last told usrefs/yman/remote
The ref on the serverrefs/tasks/main — never a branch
Working copy of the tasks.yman/, listed in .git/info/exclude

The setting that matters here is the fetch refspec yman init appends to the repository config:

remote.origin.fetch = +refs/tasks/main:refs/yman/remote

so an ordinary git fetch or git pull brings task commits along at no cost. (init also sets yman.refresh and registers the m.yml merge driver as merge.ymanmeta.*; docs/setup.md lists everything it touches.) Pushing is always explicit — git push origin refs/yman/local:refs/tasks/main. remote.origin.push is never set, so your plain git push keeps doing exactly what it did before.


How tasks are stored

One task is one folder:

.yman/
  config.toml              # committed; shared by everyone on the project
  .gitignore
  .gitattributes           # **/d.md merge=union, **/m.yml merge=ymanmeta
  2.14.fix-login/
    t.md                   # "# Title" plus a free-form Markdown body
    m.yml                  # status, tags, assignee, timestamps, attachments…
    d.md                   # discussion, append-only  (only once commented)
    f/                     # attachments               (only once attached)
      screenshot.png
  done/                    # version 2 only: closed tasks, out of the way
    5.9.old-thing/

The folder name is the data

{priority}.{id}.{slug} is the source of truth for the priority and the id — m.yml does not repeat them. Changing either is a git mv, and yman log <id> follows the rename, so history is never lost to a retitling.

Priority runs 0–9 with 0 highest, which means a plain ls .yman/ already sorts the most urgent work to the top. The slug is the title lowercased with every non-alphanumeric run collapsed to -; Unicode survives intact (1.1.первая-задача).

t.md

# Fix login

Anything you like down here. Headings, code fences, checklists — it is
just Markdown, and it is written back verbatim.

The first # line is the title. A file without one is a broken task: yman ls prints it with a ! marker and the parse error instead of failing, and commands that target it say so.

m.yml

status: doing
tags:
- auth
- bug
assignee: Ivan
created: 2026-09-16T10:00:00Z
updated: 2026-09-16T10:12:30Z
attachments:
- path: f/screenshot.png
  name: screenshot.png
  added: 2026-09-16T10:05:00Z
  by: Ivan
links:
- https://example.com/issues/12
related:
- '5'

Timestamps are RFC 3339, UTC, second precision. Keys yman does not know are dropped when it rewrites the file.

d.md

Append-only, one entry per comment:

## 2026-09-16T10:10:00Z — Ivan

reproduced on staging

.gitattributes marks **/d.md as merge=union, so two people commenting on the same task at the same time merge cleanly instead of conflicting. That holds when one of them retitled or closed the task meanwhile, too: a comment or an attachment written against the old folder name follows the task into the new one.

m.yml gets a merge driver of its own, merge=ymanmeta, which merges the file one field at a time instead of one line at a time: a status change on one clone and a tag change on another no longer collide just because they sit on adjacent lines. Lists go one entry further and merge per entry, so two clones each adding a tag keep both. Two edits to the same field — or the same tag, or the same attachment — still conflict, and land in the file as the usual markers.


Command reference

Creating and reading

yman add <title> [-p 0-9] [-s <status> | --waits-on <id>...] [-t <tag>]...
         [-m <body> | --body-file <path>] [-e]
         [-a <who>] [--link <url>]... [--relate <id>]...
yman add --sections <path|-> [same flags, minus the title, -m, --body-file, -e]

Creates a task and commits it. -t, --link, --relate and --waits-on repeat; --waits-on <id> is --relate <id> plus -s blocked. Relating to an id this clone does not have warns on stderr and commits anyway — another clone may not have synced yet. --body-file - reads the body from stdin. -e opens $VISUAL or $EDITOR on the new t.md first, starting from the -m body when both are given — whatever title you type there wins, and the folder is named after it. --sections plan.md creates one task per # Title section of a markdown file instead, each with the text up to the next heading as its body (a # line inside a code fence does not count) and every other flag applied to each — the steps of a plan in one call, one commit apiece.

yman ls [-s <status>]... [-t <tag>]... [-a] [--assignee <who>] [-p 0-9]
        [--related <id>] [-q <text>] [-n <N>] [-l] [--json]
yman tags [--json]
yman tags rename <old> <new>
yman tags rm <tag> [-f]

Lists tasks sorted by priority, then status order, then id. Tasks in a closed status are hidden unless you pass -a or name that status with -s. -t requires all the tags given and matches case-insensitively. --assignee - means unassigned, --related <id> keeps the tasks that relate to that id, -q is a case-insensitive search over title and body, -n caps the rows after sorting, and -l prints each task’s body indented under its row. Column headers appear only when stdout is a terminal, so yman ls | grep stays predictable. yman tags is the inventory of the tags in use, one row per tag with the number of tasks carrying it — closed tasks included, since the question is what exists rather than what is open. yman tags rename and yman tags rm edit that vocabulary across every task carrying the tag, in one commit; rm confirms first, like yman rm. --json prints one object per task — including its links and related ids — plus {dir, error} for anything broken; -l adds each task’s body.

P  ID  STATUS  TITLE       TAGS      F  C
2  1   doing   Fix login   auth,bug  1  3
5  2   todo    Write docs

F is the attachment count, C the comment count; both blank at zero.

yman show <id> [-n N] [--json]   # everything about one task; -n keeps the last N comments
yman plan <id> [-a] [-n N] [-l] [--json]   # a task and the tasks relating to it
yman path <id>     # just the absolute path:  cd $(yman path 14)
yman log [<id>] [-n N]

yman show lists the task’s own related ids and, as related by:, the tasks that relate to it (related_by in --json), so an epic sees its steps without being edited. yman plan <id> reads a whole plan at once: the task, a count per status over every task relating to it, and the open ones sorted like ls, a blocked one followed by waits on <ids> — what it relates to that is still open, its epic (tag epic) aside. yman log <id> follows the task across every rename it has been through.

Changing things

yman set <id> [--status S] [--priority 0-9] [--title T]
              [--assignee A | --no-assignee]
              [--tag X]... [--untag X]...
              [--link URL]... [--unlink URL]...
              [--relate ID]... [--unrelate ID]... [--waits-on ID]...
              [--body <text> | --body-file <path>] [-m <comment>]
yman start <id>...          # = set --status <start status>
yman done  <id>...          # = set --status <done status>
yman move <id>... <status>  # = set --status <status>
yman cancel <id>...         # = set --status <cancel status>   (version 2)
yman reopen <id>...         # a closed task back to the default status
yman prio  <id>... <0-9>    # = set --priority
                            # every one of these also takes -m <comment>
yman edit  <id>          # $VISUAL, else $EDITOR, else vi (vi only on a terminal)
yman rm    <id> [-f]

List fields keep their insertion order. Adding a value that is already there is not a change and commits nothing; removing one that was never there is quietly accepted. A set that changes nothing prints no changes and leaves the history alone. -m appends a comment in the same commit, so yman done 14 -m "fixed in 3f2a" closes and explains in one step. --body rewrites the description without an editor; --body "" clears it. The verbs take several ids — yman done 14 15 16 — and commit each task on its own, stopping at the first error.

--waits-on <id> marks a dependency in one step: it relates to the task waited on and sets status blocked, which has to be in statuses.list. Closing a task then says what it freed, on stderr — a blocked task left waiting on nothing open gets note: 34 no longer waits on anything open: yman move 34 todo. Nothing moves by itself; tasks tagged epic are containers and never count as something to wait on. Closing an epic with open steps still goes through, but warns: warning: 30 closed with open tasks relating to it: 35, 36.

yman rm asks for confirmation on a terminal and refuses outright without -f when there is no terminal to ask at. It also drops the removed id from every other task’s related list in the same commit, reporting the count on stderr.

related is one-way — relating 14 to 15 says nothing about 15 — and only the related field is maintained: an id written into the text of t.md or d.md is prose, and is never checked or rewritten.

Attachments and discussion

yman attach <id> <file>... [--name <n>] [--force]
yman detach <id> <name>
yman comment <id> [-m <text>] [-e]

--name renames a single file on the way in. --force replaces an attachment that already exists. Files over 5 MiB get a warning, never a refusal. With neither -m nor -e, yman comment reads the comment from stdin until end of file when stdin is not a terminal — so git log -1 | yman comment 14 works, but a caller whose stdin is a pipe it never closes waits forever; typed at a terminal, it fails with empty comment. -e edits a private temp file that is removed afterwards, whether or not the comment was added.

Plumbing

yman init [--remote <url>] [--offline] [--id-scheme random|seq|author]
          [--author <prefix>] [--hooks] [--refresh lazy|manual]
          [--autosync off|push]
yman status [--json]     # where everything stands; never changes anything
yman refresh [--quiet]   # fast-forward onto already-fetched task commits
yman sync [--continue] [--abort] [--no-push]
yman hooks install|remove|status
yman git [--] <args>...  # raw git, run inside .yman
yman guide               # the short manual for scripts and agents; works anywhere
yman completions <shell> # bash, zsh, fish, elvish or powershell; works anywhere
yman man [--dir <dir>]   # the man page in roff, or every page written to <dir>
$ yman status
.yman  refs/yman/local @ 3f2a1c9   (refresh: lazy, hooks: installed)
remote: refs/tasks/main @ 9b8c7d6   ahead 2, behind 1   → run: yman sync
worktree: clean
tasks:   todo 4, doing 1, done 7

yman init sets the tracker up, or joins one another clone already published; its flags and every change it makes are in docs/setup.md.

yman status --json reports the same facts — refs, ahead/behind, the merge state and the per-status counts — as one object, for a script that would otherwise parse those four lines.

Exit codes

CodeMeaning
0success
1error — the message says what happened
2usage error, from the argument parser
3a sync merge is waiting to be resolved
4no task has that id

Errors go to stderr prefixed error: , warnings warning: , notes note: . Everything a script would want to read goes to stdout.

Scripts and agents

yman guide prints docs/agents.md, a one-page manual for a reader that pays per token: which calls to make, what ls columns mean, the exit codes, and how to recover from a stuck sync. It starts with a block to paste into a project’s CLAUDE.md. The short version: filter with ls -n, skim bodies with ls -l -n, read with show -n, close with done <id> -m, run yman sync before a status change and after it (the after is automatic under yman.autosync = push), never open an editor, always give comment its text with -m (without it the comment is read from stdin until end of file, which blocks on a pipe left open), and set YMAN_ACTOR so the work is attributed to the agent. Big work is a plan: an epic task plus steps that --relate it, read from the epic with yman plan <id> (progress and open steps in one call, enough to pick the next step without a show per step) and created in one go with add --sections plan.md --relate <id>, so the epic is never edited; a step that waits sits in blocked and relates to what it waits on.

For a harness that loads skills, skills/yman/ is the fuller version of the same rules as a Claude Code skill — the task-list procedure, the commands never to run and why, and recovery from each exit-3 case — so it arrives without anyone pasting anything. Copy the directory into any project that tracks its work with yman; it assumes nothing about where the yman source tree is. cargo install puts only the binary on PATH, but the crate ships the skill, unpacked under the registry cache:

src=$(ls -d ~/.cargo/registry/src/*/yman-tracker-$(yman --version | cut -d' ' -f2)/skills/yman)
cp -r "$src" .claude/skills/        # this project
cp -r "$src" ~/.claude/skills/      # every project

skills/yman-task/ sits next to it and installs the same way: /yman-task <text> files the user’s own words as tasks tagged user-request and needs-agent, with the original quoted, and leaves them unassigned so the next agent re-checks each claim against the code before claiming it.

Setting a tracker up from scratch is docs/setup.md.


Configuration

.yman/config.toml is committed, so a project agrees on these once:

version = 1

[ids]
scheme = "seq"        # "random" | "seq" | "author"
random_len = 4        # hex chars, random scheme only

[statuses]
list = ["todo", "doing", "done"]
default = "todo"

[priorities]
default = 5           # 0..=9

[slug]
max_bytes = 200       # hard cap 240

statuses.list is yours to change — the order is meaningful. yman start moves a task to the second entry, yman done to the last, and yman ls hides the last one by default. A status that is no longer in the list never breaks a task; it is reported, not rejected.

Those last three are derived from position, which means inserting a status quietly changes what yman start does. At version = 2 you can name them instead, and say which statuses count as closed:

version = 2

[statuses]
list = ["todo", "doing", "blocked", "done", "cancelled"]
default = "todo"
start = "doing"
done = "done"
cancel = "cancelled"
terminal = ["done", "cancelled"]

ls then hides every terminal status, not just the last one. All four keys are optional; leaving them out reproduces the positional behaviour exactly.

Version 2 also files closed tasks one directory down, at .yman/done/5.1.fix-login/, so the top level only ever holds open work. yman done moves the folder there in the same commit as the status change, and setting the task back to an open status moves it out again and takes the emptied directory with it. m.yml stays the source of truth: a task in the wrong directory is still listed correctly, and the next set puts it where it belongs.

Raising the version is a hand edit. Existing closed tasks are not moved for you — yman done <id> on each one does it, and reports folder <old> -> <new>.

Two people closing the same task to different statuses is the one case that gets harder: git keeps both folders, and yman sync names the pair and asks you to delete one. yman sync --continue refuses until you do.

Choosing an id scheme

Pick this at yman init; it is stored in config.toml and cannot be changed per-clone afterwards.

SchemeIds look likeBest when
seq (default)1, 2, 14Small team, short ids worth typing
authoriv-1, an-3Several people adding tasks offline a lot
randomt-7f3aIds must never collide, at any cost

seq and author can mint the same id on two machines at once; yman sync sorts that out (see below). random cannot collide in practice, at the price of ids nobody remembers.

The author prefix comes from git config yman.author, else $YMAN_AUTHOR, else the initials of your user.name.

Repository-local settings

KeyValuesSet by
yman.refreshlazy (default), manualyman init --refresh
yman.autosyncoff (default), pushyman init --autosync
yman.authorletters, digits, _ or -yman init --author

Two environment variables, easy to confuse:

VariableUsed for
YMAN_AUTHORfallback id prefix for the author scheme, after yman.author
YMAN_ACTORdisplay name on comments and attachments, before user.name

YMAN_ACTOR is meant for an agent or a script working under a person’s git account. The git committer is never changed by it.


Sharing work: sync, refresh, hooks

yman sync is the command that talks to the network (yman init does too, once, unless given --offline, and so does every change under yman.autosync = push, below). It snapshots any uncommitted edits in .yman, fetches, merges, and pushes:

$ yman sync
snapshotted 1 local change(s)
synced  pulled 3, pushed 2, renumbered 0   refs/yman/local @ 3f2a1c9

Publishing your own changes can happen without a sync too. With yman init --autosync push (stored as yman.autosync), every command that changes a task pushes it to origin once it has committed, and says so on stderr with note: pushed N task commit(s). It only pushes: when someone else pushed first it warns origin has new task commits; run: yman sync and leaves the merge to you, and when the network is down it warns and the command still succeeds. This is the setting to turn on when agents work the backlog, since they close tasks and rarely remember to sync.

Getting other people’s tasks does not need a sync at all, because they arrive with any ordinary fetch:

yman.refreshWhat happens
lazy (default)Every read or write command fast-forwards .yman onto whatever refs/yman/remote already holds
manualOnly yman refresh and yman sync move .yman

A refresh is always a fast-forward, never touches the network, and backs off silently when .yman has uncommitted changes or local commits you have not pushed. yman refresh and yman status explain why when it does.

yman hooks install adds post-merge and post-checkout hooks that run yman refresh --quiet, so a plain git pull updates your tasks as a side effect. Hooks somebody else wrote are reported and left alone, never overwritten or deleted — yman tells you the one line to add instead; a hook it cannot read is refused the same way. core.hooksPath is honoured, ~/ included.


When two people collide

Same id, two machines

With seq or author, two people working offline will both mint id 2. On sync, whoever is behind renumbers their own new task, because the other side’s id is already published:

$ yman sync
renumbered 2 -> 3  (id taken on origin)
note: rewrote 1 reference(s) to renumbered ids
synced  pulled 1, pushed 3, renumbered 1   refs/yman/local @ 1f7d06f

References to the old id in other tasks’ related lists follow the move, in the same commit as the rename.

Same field, two edits

Two people changing the status of the same task conflict like any other git conflict. Sync stops with exit code 3 and tells you which files:

$ yman sync
  5.1.fix-login/m.yml
error: conflicts in 1 file(s); edit them, remove markers, then: yman sync --continue  (or: yman sync --abort)

Edit the files and remove the markers — staging is yman’s job, not yours — then:

yman sync --continue     # or: yman sync --abort

The one exception is a file sync lists as (no conflict markers: …): a binary attachment, an edit to what the other side deleted, or an m.yml whose merge driver could not run. There is nothing to edit, so pick a side with yman git checkout --ours|--theirs -- <file> and yman git add <file> before continuing.

--continue refuses while any conflict marker is still in place, and checks that every task the merge touched still parses before it commits. Until the merge is settled, commands that would change a task exit 3 rather than build on a half-merged state. yman status shows the same information at any time.

Comments never conflict: merge=union keeps both sides. Edits to different fields of one task’s m.yml do not conflict either, and neither do two clones adding different tags: merge=ymanmeta merges that file field by field, and its lists entry by entry.

The exception is closing one task to two different statuses at version 2. Each side moves the folder somewhere else, git keeps both, and yman sync says so:

$ yman sync
note: task 1 was closed to two different statuses; keep one of cancelled/5.1.fix-login, done/5.1.fix-login
error: conflicts in 3 file(s); edit them, remove markers, then: yman sync --continue

$ rm -rf .yman/cancelled/5.1.fix-login
$ yman sync --continue

--continue refuses while both folders are there, so the tracker cannot end up with two folders for one id.


Troubleshooting

git clean -fdx deleted .yman/. Nothing is lost — the history lives in refs/yman/local, which is not in the worktree. Run yman init again and everything comes back, including tasks you had never pushed.

.yman is not initialized; run: yman init. Either this clone has never run init, or .yman/ was removed. Same fix.

refs/yman/local is already checked out in another worktree. One .yman per clone is supported. Remove the other worktree, or use a separate clone.

git identity missing. yman commits like any other tool; set user.name and user.email.

yman path prints a different path than it used to. At version 2 a closed task lives in .yman/<status>/. Shell bookmarks to the old location break; yman path <id> always knows where it is now.

A task shows as broken. Its t.md lost its # Title, or its m.yml will not parse — usually a hand-edit or a merge resolved carelessly. yman ls shows the error; fix the file and the task comes back. Nothing else is affected.

Your tasks are not on origin. Nothing ran yman sync: yman status shows ahead N. Run yman sync, or yman init --autosync push so every change publishes itself.

Tasks are not appearing after someone else pushed. You have not fetched (yman refresh never uses the network on its own), or yman.refresh is manual, or your .yman has uncommitted changes. yman status says which.


Limits and future work

  • A merge can reorder a task’s tags, links or related relative to the order they were typed in: entries new on either side are appended sorted by key, because that is the only ordering both clones agree on.
  • Attachments go straight into git; there is no git-lfs integration.
  • add slows as the project ages. An id is never reused, which means reading every id the history ever assigned before minting one. Measured with scripts/synthetic-project.sh on a Ryzen 7 4700U, git 2.55.0, release build: 32 ms on an empty tracker, 157 ms at 1000 tasks over 1081 commits — a linear 18 ms + 0.13 ms per existing task.
  • A ref is resolved by reading its file, not by spawning git; only a layout src/refs.rs does not recognise falls back to rev-parse. On that same repository yman ls costs 1 process and 18 ms, yman show 1 and 6 ms, yman add 4. A clone holding unpushed commits pays one more, for the ancestry question no file answers.
  • yman log <id> finds a task’s history by a pathspec on its id, so a task with fewer commits than -n still walks the whole history: 125 ms for a 17-commit task on the same repository, down from 400 ms when it rebuilt the rename graph. There is no index to make that cheaper.
  • One .yman worktree per clone.
  • Windows is untested. Deep task paths may need core.longpaths.
  • No colour, no TUI, no web UI, no GitHub Issues bridge.

Development

cargo test                                  # unit tests + the two-clone suite
cargo clippy --all-targets -- -D warnings
sh scripts/spike-symref.sh                  # the git invariant this rests on
sh scripts/synthetic-project.sh             # a 1000-task repository, driven (~3 min)
sh scripts/book.sh                          # the documentation site, into target/book

The integration suite builds a bare remote and two clones in a temp directory and drives both through init, add, sync, id collisions, conflicts and hooks. Nothing touches the network or your real git configuration.

scripts/synthetic-project.sh covers what a suite of short-lived fixtures cannot: it builds four clones and a thousand tasks over a couple of thousand commits, churns them, closes them, crosses them in and out of the archive directories, and then reports what add, ls and log cost on the result. The repository is regenerated rather than committed — it lands in target/synthetic, is kept so you can cd in and poke at it, and is replaced by the next --force run. Run it before a release; it is deliberately not part of CI.

skills/yman/evals/run.sh tests the agent-facing side: it hands a headless agent one prompt per case against a throwaway tracker, then grades the repo it leaves behind and the commands it reached for — so a run catches the skill drifting from the CLI, not just the CLI breaking. Its ten cases run from claiming and closing a task to plan, --waits-on, add --sections, the task-list procedure and recovering from an exit-3 merge. It spends real tokens, so it is not part of CI either; see skills/yman/evals/.

Layout:

PathWhat it holds
src/git.rsthe git runner; every invocation goes through it
src/repo.rsrepository discovery, .yman state, preflight checks
src/task.rsfolder names, slugs, t.md/m.yml, load and save
src/yml.rsthe m.yml reader and writer, hand-written
src/ids.rsid generation and what counts as taken
src/refresh.rsthe fast-forward policy
src/commands/one module per subcommand
scripts/spike-symref.shchecks the git behaviour the .yman worktree depends on
scripts/synthetic-project.shbuilds a large repository and measures what commands cost on it
scripts/book.sh, book.toml, docs/SUMMARY.mdthe documentation site
skills/yman/the agent skill, and the evals that keep it honest
skills/yman-task/the skill that files the user’s words as unverified tasks
docs/the normative specification

docs/ holds the normative, as-built specification — storage.md for the ref and on-disk contract, commands.md for per-command semantics, errors.md for exit codes and the pinned messages, and setup.md for getting from no .yman to a working tracker — plus agents.md, the short manual yman guide prints. They describe what the code does, so a disagreement between them and the code is a bug in one or the other.

The same files are published as a documentation site at https://brillliand.github.io/yman-tracker/. scripts/book.sh copies them under target/book-src, rewrites the links that only work in the repository layout, builds with mdBook and fails on any local link or anchor that leads nowhere. It needs mdbook on PATH (cargo install mdbook, or a release binary); mdbook serve previews the result after a build, though edits to the sources need the script run again. .github/workflows/ci.yml runs it as part of the gate, and .github/workflows/pages.yml builds it on every push and deploys it from main. Adding a page means adding it to docs/SUMMARY.md; the script fails when a docs/*.md is missing from it. docs/SUMMARY.md is mdBook’s table of contents rather than a page: its links to README.md, skill.md and CHANGELOG.md name the copies the script makes under target/book-src, so they do not resolve when GitHub renders the file.

Releasing

Versions follow semver and are tagged vX.Y.Z. CHANGELOG.md collects user-visible changes under [Unreleased] as they land; a release freezes that section. To cut X.Y.Z:

  1. Branch release/X.Y.Z from main.
  2. In CHANGELOG.md, rename [Unreleased] to [X.Y.Z] - YYYY-MM-DD, open a fresh empty [Unreleased] above it, and update the compare links at the bottom.
  3. Bump version in Cargo.toml; cargo build updates Cargo.lock.
  4. Run the full gate (cargo fmt --check, clippy, cargo test, scripts/spike-symref.sh, scripts/book.sh) and scripts/synthetic-project.sh.
  5. Commit as chore: release X.Y.Z and merge into main with --no-ff.
  6. Tag the merge: git tag -a vX.Y.Z -m "yman X.Y.Z", then push main and the tag.
  7. cargo publish --dry-run, then cargo publish. A published version can be yanked but never replaced, so the dry run is the last look.

The package on crates.io is yman-tracker and the binary yman. exclude in Cargo.toml keeps the agent configuration, the CI workflow and the skill evals out of the tarball; cargo package --list shows what ships.


License

MIT. See LICENSE.