taloncjones

todos

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).

taloncjones 0 Updated 1w ago

Resources

1
GitHub

Install

npx skillscat add taloncjones/dotfiles/todos

Install via the SkillsCat registry.

SKILL.md

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: with
CLAUDE_CONFIG_DIR set to any other config dir, verbs refuse with
todos: .todos is not available under this account, and brief skips
that repo. TODOS_OFFLINE=1 skips the network; TODOS_SYNC_TIMEOUT and
TODOS_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, run
todos.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 created is older than M days (default 14) they appear under Stale (oldest-first, capped, with a "+K more" note).
  • todos.sh brief reads the registry (~/.claude/todos/repos.txt) and scans
    every registered repo. Register each repo you want included with
    todos.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 runs
todos.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 by
todos.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, use
todos.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 unsatisfied
depends_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_on ref with its state, resolved by the
    same resolver as list. Offline by default (like index); --online
    lets it call gh and overrides an exported TODOS_OFFLINE. A
    todo:<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/ (then
    archive/<YYYY-MM>/tasks/, newest month first) whose todo_id, else file
    name (a td- prefix ignored), is the todo id; a file named for the todo
    wins (resolved by the shared workflow context; TODOS_STATE_ROOT is 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 its reviewed_head_sha matches 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 shows unreadable.
  • Files: the files: list.
  • Links: http(s) URLs from the body, GitHub PRs as PR #n,
    claude.ai artifacts as artifact, 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's
Content-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 Solution
  • note takes an exact pending id, never a substring, and one of
    Problem, Solution or Verification.
  • It inserts a blank line and the note after the section's last line (in
    the last such section when a heading repeats), regenerates TODO.md, and
    commits and pushes a store-backed .todos like depend does.
  • A note is printable ASCII, tabs and newlines, at most 4000 bytes, with
    no heading or code-fence lines.
  • --expect-sha HEX refuses 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 under
research/<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 as
field-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 uses git rev-parse --show-toplevel).
  • There is no separate "in progress" state — a todo is either in pending/ or
    completed/. 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 run
    todos.sh index.