Use when capturing, listing, completing, or reviewing cross-session todos for a repo. Trigger when the user says "track this", "add a todo", "what todos are open", "mark that done", or wants a persistent backlog that survives across sessions (unlike the built-in TodoWrite tool, which is session-only).
Resources
1Install
npx skillscat add taloncjones/dotfiles/todos Install via the SkillsCat registry.
Todos
A file-based todo tracker: one markdown file per work item under<repo>/.todos/, surviving across sessions where the built-in TodoWrite tool
does not. Replaces GSD's .planning/todos/ without the worktree/hook machinery.
All mechanics go through the backing script — never hand-create or hand-move
todo files, because the script also keeps TODO.md in sync. Call it by
absolute path: resolve scripts/todos.sh from the directory of this loaded
skill. Both runtimes link this repo-owned source. The existing cross-repo
registry remains shared at ~/.claude/todos/repos.txt; TODOS_REGISTRY
can override it explicitly without moving either runtime's plugin files.
Layout
<repo>/.todos/
pending/ YYYY-MM-DD-<slug>.md open items
completed/ YYYY-MM-DD-<slug>.md finished items
TODO.md auto-generated index of pending/Store-backed .todos
A repo's .todos may be a symlink into a separate git repository that opted
in with git config todos.store true. Then the script keeps that
repository in sync on every call: it takes a lock, commits hand edits one
file at a time, pulls (rebase --autostash), commits its own change and
pushes. Offline, verbs still work and print todos: sync skipped: ...;
the next online call pushes. todos.sh sync does the same with no other
change, and todos.sh import <dir> merges a directory of todos in (newer
file wins, a completed todo beats a pending one). ready never touches
the network.
Only the personal Claude config may use a store-backed .todos: withCLAUDE_CONFIG_DIR set to any other config dir, verbs refuse withtodos: .todos is not available under this account, and brief skips
that repo. TODOS_OFFLINE=1 skips the network; TODOS_SYNC_TIMEOUT andTODOS_LOCK_WAIT (seconds, default 10) bound network calls and the lock.
Todo file:
---
created: 2026-06-02
title: <human-readable title>
area: <optional tag, e.g. eol>
due: <optional YYYY-MM-DD deadline>
surface: <optional YYYY-MM-DD; hide from the brief until this date>
priority: <high|med|low; set at creation>
status: <optional; waiting|someday, see the dashboard's Open grouping>
files:
- path/to/relevant.py:42
depends_on:
- todo:<YYYY-MM-DD-slug> another todo (exact basename, no .md)
- branch:<name> a git branch, satisfied when merged into origin/main
- pr:<number> a GitHub PR, satisfied when merged
---
## Problem
What's wrong / what needs doing, with enough context for a future session.
## Solution
The intended fix or approach. "TBD" is fine when not yet known.Priority (assigned at creation)
Every new todo gets a priority. When you create one, infer it from the
context you have at that moment - area, whether it references a Jira key, and
urgency language ("HOLD", "blocker", "before review"). If you cannot decide
confidently, ask the user for a level (high | med | low). Pass it via--priority. The brief only reads priority; it never re-infers. Existing
todos without a priority are treated as the lowest rank and shown blank.
high items surface in the brief regardless of date - except when deferred
(surface in the future), which always hides them until the surface date.
Visibility (local by default)
init (and the first new) add .todos/ to the repo's git exclude
(info/exclude, resolved via git rev-parse --git-path so it works inside
worktrees). Todos stay local-only — they never reach the remote. This is
the right default for work repos.
For a personal repo where you want the backlog committed and shared, runtodos.sh share once: it removes the exclude line so .todos/ commits
normally on the next git add.
Commands
| Command | What it does |
|---|---|
todos.sh init |
Create .todos/{pending,completed}/, set local-only exclude |
todos.sh new "<title>" [--area A] [--file P]... [--depends-on REF]... |
Create a pending todo; prints the file path |
todos.sh new "<t>" [--due D] [--surface D] [--priority L] |
Time/priority fields on a new todo |
todos.sh register |
Register the current repo for the cross-repo brief |
todos.sh repos |
List registered repos (warns on missing paths) |
todos.sh brief [--soon N] [--stale M] [--stale-cap K] |
Print time-relevant todos across all registered repos |
todos.sh list [--all] [--offline] |
List pending todos with blocked-on annotations (--all adds completed; --offline skips gh) |
todos.sh ready <exact-todo-id> [--offline|--online] |
Read-only JSON dependency verdict for one exact pending todo; offline by default |
todos.sh done <slug-or-substring> |
Move a todo pending/ -> completed/ |
todos.sh depend <slug-or-substring> REF... |
Add dependency refs to a pending todo (todo:<id>, branch:<name>, pr:<n>); re-indexes |
todos.sh note <exact-id> <Problem|Solution|Verification> [--expect-sha HEX] |
Append stdin as a note to one PRD section of a pending todo (see "Serve and notes") |
todos.sh index |
Regenerate TODO.md |
todos.sh share |
Stop ignoring .todos/ in this repo (opt into committing it) |
todos.sh path |
Print the .todos/ directory path |
todos.sh dashboard [--runtime claude|codex] [--personal] [--open] [--online] [--out PATH] [--completed N] |
Render the local HTML board in the selected account scope |
todos.sh serve [--runtime claude|codex] [--personal] [--port N] [--open] [--online] [--completed N] |
Serve the board on 127.0.0.1 with search, filters, copy chips and note forms |
new, done, depend and note regenerate TODO.md automatically, so the index never drifts.
Time model & the brief
due- deadline. Drives Overdue (due < today), Today (due == today), and
Due soon (today < due <= today + N, N default 3).surface- defer date. The todo is hidden from the brief until then; on the
day it equals today it appears under Today. Defer wins over priority.- Undated todos surface via age: once
createdis older than M days (default 14) they appear under Stale (oldest-first, capped, with a "+K more" note). todos.sh briefreads the registry (~/.claude/todos/repos.txt) and scans
every registered repo. Register each repo you want included withtodos.sh register.
Dependencies
depends_on: lists what must land before a todo is actionable. Each ref
resolves to one state:
| State | Means | Satisfied |
|---|---|---|
done |
the todo is in completed/ |
yes |
merged |
branch is an ancestor of origin/main, or gh says MERGED |
yes |
open |
todo pending; branch not merged; PR open | no |
closed |
PR closed without merging | no |
missing |
no such todo | no |
unknown |
offline, gh absent or failing, no local ref |
no |
invalid |
ref does not parse | no |
self |
a todo naming itself | no |
todos.sh list shows unsatisfied refs as [blocked-on: <ref> (<state>)].TODO.md is resolved offline (todos and local git refs only): refs that
are determinately unsatisfied appear under [blocked-on: ...], refs the
index cannot verify (PRs, absent branches) under [unverified: ...].
Network rule: list and explicit ready --online may call gh.--offline (or TODOS_OFFLINE=1) disables that. new, done, depend,
and index never touch the network. ready --online shares a five-second
budget across all gh calls, with up to one additional second to kill a
stalled process. It uses coreutils timeout or gtimeout; if neither is
available, network-dependent refs remain unverified. list retains its
existing unbounded gh calls; use list --offline when needed.
Squash merges never make a branch an ancestor of origin/main, so a
branch ref resolves merged only through gh (gh pr list --head),
whether or not the branch still exists locally; offline it reads open
while the local ref exists and unknown once post-merge deletes it.
Prefer a pr: ref once the PR number is known. TODOS_BASE_REF
(default origin/main) and TODOS_GH (default gh) are overrides for
tests and unusual setups.
For controller dispatch, the herdr dispatch gate runstodos.sh ready <id> --online, so a merged pr: dependency clears it.
Run ready from the source repository and pass
the task's explicitly bound todo ID, such as 2026-09-06-fix-cache.
The command accepts the exact pending basename without .md; it never
selects a substring, the newest item, or a task from another repository.
Completed or duplicate pending/completed targets cannot dispatch.
{
"ready": false,
"task_id": "2026-09-06-fix-cache",
"dependencies": [{ "ref": "pr:85", "state": "unknown" }]
}The JSON includes every direct dependency in file order, including satisfied
ones. Exit status is 0 when every dependency is done or merged, 3
when any dependency is unsatisfied or unverified, and 2 for invalid input,
a missing/ambiguous pending target, or malformed dependency frontmatter.
Status 2 includes an error field. Unknown, invalid, missing, and self refs
never count as satisfied. Direct cycles remain blocked by their pending
dependency; the resolver does not recursively schedule work.
ready requires jq and changes no todo, index, Git exclude, cache, or repo
registry. It neither initializes a missing backlog nor starts any work.
Removing a dependency is a hand edit of the todo's frontmatter followed bytodos.sh index.
Dashboard
todos.sh dashboard renders the whole board for the current repo into
one static HTML file and prints its path:${XDG_STATE_HOME:-~/.local/state}/dotfiles/dashboard/<account_id>/<repo_slug>.html
(<repo_slug> is the herdr repo slug, so worktrees share one page within
the same account). Select the caller explicitly with --runtime claude or--runtime codex; retain --personal for personal quota in a work repo.TODOS_DASHBOARD_DIR explicitly overrides the output directory. --open opens it (open on macOS, xdg-open
elsewhere); --out PATH writes elsewhere; --completed N sets how many
completed todos to show (default 10, 0 hides the section).
The page is inert: no script, no remote assets, no server. Regenerate
and reload the tab to refresh. To add notes from the browser, usetodos.sh serve instead. For repeated refresh, resolve scripts/todos.sh from this loaded skill
and rerun dashboard with the same runtime/account options. Do not resolve
it from another runtime's personal installation.
The board is lanes of compact cards, in this order: In flight (herdr
is already working it), Ready, Blocked (an unsatisfieddepends_on), Waiting (status: waiting), Someday (status: someday, collapsed), and Recently done (the newest --completed N
completed todos, collapsed). Computed state always wins: a todo that
is blocked or in flight sorts there regardless of its status: field,
since that field can go stale against the dependency graph or the live
herdr record. Add status: waiting or status: someday by hand-editing
a todo's frontmatter (the same pattern already used for depends_on);
any other value, or none, defaults to Ready. The count pills at the top
mirror the five open lanes and jump to each one.
A card shows the title, a one-sentence summary (the first sentence of
the first ## Problem paragraph, at most 220 characters) and badges: the
task state token (the herdr workspace-label vocabulary, such as impl,review, merge?; done for an archived merged task), PR #<n>, the
Jira key when the todo text holds an Atlassian /browse/<KEY> URL (other hosts need a jira_url: front-matter line), blocked by <n> or <n> deps met, the
priority, the area, and due <date> (red once overdue) or surfaces <date> while the surface date is still ahead. Each lane header's
underline and each card's thin left stripe carry the lane's status colour
(blue in flight, red blocked, amber waiting, green done, grey otherwise).
The static dashboard follows the OS light or dark setting and has no theme
toggle; the served board follows it until the Theme button overrides it.
Click a card, or open a URL ending in #todo-<id>, to show that todo's
modal: its rendered body (the text before the first ## heading and
every section, with paragraphs, lists, ### headings, inline code, bold
and http(s) links; fenced blocks and tables show as preformatted text,
and everything else as escaped text) beside a facts panel. The close
button or a click outside the sheet closes it; the back button returns
to the previous entry (the board, or the modal a dependency link came
from). The modal is plain CSS (:target), so it needs no script.
The facts panel shows:
- Id and Dates (created, due, surfaces), then maturity and tier.
- Depends on: every
depends_onref with its state, resolved by the
same resolver aslist. Offline by default (likeindex);--online
lets it callghand overrides an exportedTODOS_OFFLINE. Atodo:<id>ref with a card on the board links to its modal. - Herdr: the task record under
<selected-account-root>/herdr-orch/<repo_slug>/tasks/(thenarchive/<YYYY-MM>/tasks/, newest month first) whosetodo_id, else file
name (atd-prefix ignored), is the todo id; a file named for the todo
wins (resolved by the shared workflow context;TODOS_STATE_ROOTis an
explicit test/development override)
when one exists: state token pill,phase role model, the PR (linked
when origin is a GitHub remote),updated <time>,archived <YYYY-MM>,
the review verdict
(tagged(stale)unless itsreviewed_head_shamatches the record),
and the done record (tagged(stale)unless its phase and agent match
the live worker). Records are read only, never written. An unreadable
record showsunreadable. - Files: the
files:list. - Links:
http(s)URLs from the body, GitHub PRs asPR #n,claude.aiartifacts asartifact, Jira issues as their key.
The modal header repeats the card badges, with the PR and Jira key as links.
The renderer never writes under .todos/ or the state root (it refuses
such an output path), never regenerates TODO.md, and writes the page
atomically so a half-written file is never seen.
Serve and notes
todos.sh serve renders the same board at http://127.0.0.1:<port>/
(--port 0, the default, lets the OS pick). It prints the bare address on
stdout and the URL with the per-run token (/?t=<token>) on stderr;--open opens a private redirect file so the token stays out of the process list. Every request re-reads .todos/, so a reload always shows the
files as they are now. In each pending todo's modal there is a form
under its Problem, Solution and Verification sections; completed todos
are read-only, and a saved note reloads the board with that modal open.
Stop the server with Ctrl-C.
A filter form in the top bar narrows the board. The search box
(q) keeps a todo when every word appears in its id or file text, in any
case. The area and priority selects (area, priority) list the exact
values on the board. The sort (sort=priority, or sort=created for
newest first) defaults to due date, then priority. The filter applies to
open and completed cards (Recently done opens while a search or filter is
set), and the line under the form counts the matching
open todos. The choice lives in the URL, so a reload or a bookmark keeps it
for the life of the server run. After a restart the old token gets a 403:
open the newly printed URL and filter again.
Each pending todo's modal has chips that copy its id, todos.sh done <id>
and kick off <id> (the director's phrase). On the served page, Esc, the
close button and the backdrop close the modal by replacing its history
entry, so the first back press does not reopen it; focus moves into an
open modal and back to its card on close; / focuses the search box. The
header's Theme button cycles System, Light and Dark and remembers the choice
in the browser's localStorage (the static page follows the OS only).
Chips and these keys need the page's one script; the server'sContent-Security-Policy allows only that script, by hash
(script-src 'sha256-...'). The static dashboard page has none of these
controls; its modals open and close through links alone.
The form goes through todos.sh note, the only writer, which agents can
call directly:
printf 'An idea for later.\n' | todos.sh note 2026-10-01-some-todo Solutionnotetakes an exact pending id, never a substring, and one ofProblem,SolutionorVerification.- It inserts a blank line and the note after the section's last line (in
the last such section when a heading repeats), regeneratesTODO.md, and
commits and pushes a store-backed.todoslikedependdoes. - A note is printable ASCII, tabs and newlines, at most 4000 bytes, with
no heading or code-fence lines. --expect-sha HEXrefuses the write (exit 3) when the file's SHA-256 is
not HEX. The form always sends the hash of the file it rendered, so a
todo changed on disk after the page loaded is never overwritten: reload
and add the note again.- Exit 2 is a refused note, section or id. On every non-zero exit the file
is unchanged; exit 0 means the note is in the file.
note, depend and done hold todos-sync.lock in the git common dir of
the repo that holds .todos, in local mode too, so they never lose each
other's writes. A busy lock exits 1 after TODOS_LOCK_WAIT seconds.
The server binds 127.0.0.1 only, answers only requests whose Host
header names that address and port, and requires the per-run token on
every request: ?t=<token> on a GET, a hidden field on each form. A
request without the right token gets a 403 that never shows a todo or the
token, so other websites and other local processes can neither read the
board nor post notes. Pages send Referrer-Policy: no-referrer. A tab
left open across a server restart gets a 403; reopen the printed URL.
Research reports
.todos/research/ holds durable research the board should keep:
orchestrator field notes, studies, decision memos, and (future, written
by post-merge) per-task review findings underresearch/<task_id>/review-findings.md. The dashboard indexes every*.md under it, newest created first.
---
created: 2026-09-06
title: Orchestrator field notes, 2026-09-06
kind: field-notes
task: 2026-09-06-render-a-local-dashboard-of-open-todos-and-researc
artifact: https://claude.ai/code/artifacts/...
---
One-paragraph summary, then the report body.created and title are expected; kind (free text such asfield-notes, review-findings, report), task (a task id or todo
basename; links to that row when it is on the page), and artifact
(an http(s) URL; anything else is shown as text, never linked) are
optional. The first body line is the summary.
Visibility follows .todos/: it is git-ignored only after todos.sh init
(or the first new) has written the exclude line, so run todos.sh init
before saving research in a repo that has never used todos. The
dashboard warns when .todos/ is neither ignored nor tracked. A repo
where todos.sh share was run commits research with the backlog.
Workflow
Capturing a todo. Run todos.sh new "<title>" (add --area and --file
when known). The script prints the new file path with empty ## Problem /## Solution sections — then edit that file to fill in the prose. Write enough
context that a future session with no memory of this one can act on it.
Reviewing. todos.sh list for open items, --all to include completed.
Read TODO.md for the glance-able summary view.
Completing. When an item is finished, todos.sh done <substring> using a
distinctive part of the filename slug. If the substring is ambiguous the script
lists the matches so you can disambiguate.
Common Mistakes
| Mistake | Why it breaks | Do instead |
|---|---|---|
Editing TODO.md by hand |
Clobbered on the next new/done (it's regenerated) |
Edit the underlying pending/*.md, then todos.sh index |
Hand-creating or mv-ing todo files |
TODO.md drifts out of sync |
Use todos.sh new / todos.sh done — they re-index |
| Running commands outside the repo | git rev-parse --show-toplevel fails or targets the wrong repo |
cd into the target repo first |
Leaving Problem/Solution blank after new |
A future session has no context to act on | Fill in the prose immediately; new only stubs the sections |
Writing #85 unquoted in depends_on: |
# starts a YAML comment; the item reads as empty |
Use pr:85 (the script writes this form) |
Notes
- Todos are per-repo and live at the repo root. Run commands from inside
the target repo (the script usesgit rev-parse --show-toplevel). - There is no separate "in progress" state — a todo is either in
pending/orcompleted/. Track active-work nuance in the body if needed. - Migrating from GSD: copy files from
.planning/todos/{pending,completed}/
into.todos/{pending,completed}/; the frontmatter is compatible. Then runtodos.sh index.