Home of tskflwctl — a local-first planning CLI over markdown+frontmatter
task/epic/audit files. It dogfoods on its own planning under
planning/.
The interactive TUI (tskflwctl ui) — tab across tasks, epics, and audits;
status glyphs, epic rollup bars, and an audit's segmented finding bar over its
status-grouped finding tree:
…and the same vocabulary on the CLI:
▸ All demos, how they're recorded, and the demo fixture →
assets/README.md — rendered with
vhs against a curated planning tree;
regenerate with just gifs.
| Path | Purpose |
|---|---|
cmd/tskflwctl/ |
The CLI entrypoint (thin composition root). |
internal/ |
domain (pure) · core (use cases) · store (markdown adapter) · cli (cobra) · tui (Bubble Tea) · theme (shared glyphs/colors) · config. |
planning/ |
This repo's own epics, tasks, and research (self-hosted). |
docs/ARCHITECTURE.md |
One-screen orientation: the primary/secondary-adapter design. |
Distribution is GitHub Releases only — three paths, no Homebrew/external channels:
# 1) go install (needs Go toolchain)
go install github.com/andy-esch/taskflow/cmd/tskflwctl@latest # or @vX.Y.Z
# 2) Prebuilt binary (no Go toolchain) — download the archive for your platform
# (darwin/linux × amd64/arm64) from the GitHub Releases page, then:
tar xzf tskflwctl_*_linux_arm64.tar.gz && ./tskflwctl version
# 3) From a checkout
just install # → go install onto $PATH (version-stamped)just build # → bin/tskflwctl
just run task list # run without installing
just install # put tskflwctl on $PATH
just release-snapshot # dry-run a full release into ./dist (publishes nothing)Releases are cut by pushing a tag (vX.Y.Z), which runs .github/workflows/release.yml
(goreleaser). A manual workflow_dispatch run builds a --snapshot and uploads
the binaries as workflow artifacts without minting a Release.
tskflwctl anchors to the nearest planning repo (walks up for tasks/; -C to
override). All commands take --json for scripting/agents, and mutating
commands take --dry-run to preview the write (full validation runs; nothing
is written) — except the interactive task edit, which has no preview.
tskflwctl init # scaffold a planning tree here
tskflwctl status # at-a-glance board: counts, in-progress, epic progress
# create
tskflwctl task new "Add retry backoff" --epic <epic-id> --tags net
tskflwctl task new "Triage flake" --epic <epic-id> --tags ci --description "is CI red?" --start # straight to in-progress (--next/--start need --description)
echo "$BODY" | tskflwctl task new "Long writeup" --epic <epic-id> --tags x --body-file - # body from stdin/file
tskflwctl epic new "Billing overhaul" --description "Replace legacy pipeline"
tskflwctl audit new dispatcher # → audits/<id>-YYYY-MM-DD-dispatcher.md (--date to override)
tskflwctl audit new auth --template security # pick a body scaffold (default|security); --template is shell-completable
# read
tskflwctl task list # active tasks (--all / --status / --epic / --tag)
tskflwctl task list --revisit-due # deferred tasks whose snooze date has arrived
tskflwctl task show <slug> # metadata + body (--section <name> / --frontmatter-only to narrow)
tskflwctl task info <slug> --json # token-cheap metadata: path, status, epic, ac:{checked,total} (no body)
tskflwctl task path <slug> # just the absolute file path — $EDITOR "$(tskflwctl task path <slug>)"
tskflwctl epic list # rollup: done/total per epic
tskflwctl epic show <id> --section goal # epic body section (or --frontmatter-only); epic path <id> for the file
tskflwctl audit list # open audits (--all / --closed / --deferred)
tskflwctl audit show <slug> --section findings # audit body section (or --frontmatter-only)
tskflwctl audit info <slug> --json # token-cheap: path, bucket, findings:{total,open,in_progress,done,dropped}
tskflwctl audit path <slug> # just the absolute file path (like task path)
tskflwctl audit findings --status open --effort XS,S --json # query findings across audits
tskflwctl audit lint # validate finding status vocab + missing status + bucket↔state
tskflwctl schema # the tool's contract for agents (statuses, fields, codes)
tskflwctl schema task --json # how to author a task: sections, fields, conventions
tskflwctl template list # body scaffolds `new --template` can use (--kind to filter)
tskflwctl template show audit security # inspect a template's rendered body (--json for the envelope)
tskflwctl ui # interactive Bubble Tea browser (tasks/epics/audits)
# update + lifecycle
tskflwctl task set <slug> --priority high --tags a,b
tskflwctl task edit <slug> # open the whole file in $EDITOR (human; re-validated on save)
echo "## Findings" | tskflwctl task append <slug> --body-file - # add a section (agent; atomic)
tskflwctl task set <slug> --body-file notes.md # replace the body (agent; its own call)
tskflwctl task ac <slug> # numbered acceptance criteria; --check/--uncheck <n> to flip one
tskflwctl task start|next|ready|complete|defer|deprecate <slug>... # defer takes --until <date>
tskflwctl task defer <slug> --until 2026-09-01 # snooze (revisit_at); on a TTY, prompts for the date
tskflwctl audit close|reopen|defer <slug>...
# hygiene
tskflwctl lint # validate active task frontmatter
tskflwctl lint --fix # auto-repair frontmatter (quote ':' values, normalize lists, backfill ids)Tasks and audits are stored flat and id-led (tasks/<id>-<slug>.md,
audits/<id>-<slug>.md); status: / bucket: is authoritative in frontmatter,
with no mirror directory. Lifecycle verbs edit that field in place and stamp the
dates atomically — no file moves (lint --fix re-normalizes a hand-edited
drift). Errors carry semantic exit codes — 10 not-found, 11 validation, 13
ambiguous, 14 conflict (e.g. a name already taken).
Body templates. Each kind ships named body scaffolds; task/epic/audit new --template <name> picks one (omit it for default). Names are shell-completable
and an unknown one fails with exit 11 listing what's available. audit ships a
security template (threat model + checklist) alongside default. --template is
mutually exclusive with --body/--body-file (pick a scaffold or supply your
own). Discover what's available with template list (--kind to filter, --json
for agents) and template show <kind> [name]. Repo-local and custom templates are
the next step (see planning/epics/22-selectable-template-library.md).
Human output is colorized with status glyphs on a terminal and falls back to
plain text when piped. Control it with --color=auto|always|never, --no-color,
or the env vars NO_COLOR / FORCE_COLOR (the latter
forces color even off a TTY — handy for agents). --json is always plain.
tskflwctl version / --version report the build.
The list commands (task/epic/audit list) share one output-format flag,
-o/--output, plus an orthogonal column selector:
-o |
Output | For |
|---|---|---|
human (default) |
colorized table | reading on a terminal |
name |
ids only, one per line | … | xargs |
table |
tab-separated, header row, absolute dates, no color/truncation | cut/awk; stable across versions |
csv |
RFC 4180 comma-separated, header row | spreadsheets; cells with commas are quoted |
json |
full records + schema_version |
jq |
-q/--quiet is shorthand for -o name; --json (on every command) equals
-o json. -c/--columns slug,status,… projects the columnar formats (table,
csv) to the columns you name, in the order you name them (and implies
-o table) — both the formats and the column names are shell-completable. -o table is a documented contract under
the one schema_version (a column add/reorder is a schema bump), and always
emits the header row — even with zero results — so a consumer gets a stable
schema and detects "no rows" by line count. Recipes:
# start every ready-to-start task tagged `tui`
tskflwctl task list -q --tag tui | xargs tskflwctl task start
# audits with open findings, projected to slug + open count
tskflwctl audit list --all -o table -c slug,open | awk -F'\t' 'NR>1 && $2>0 {print $1}'
# in-progress slugs via jq
tskflwctl task list --status in-progress -o json | jq -r '.tasks[].slug'stdout is data, stderr is diagnostics — per-item transition failures,
file-read problems, and prompts go to stderr, so a partial … | xargs never
interleaves errors into the data stream.
When a required input is missing, tskflwctl picks the face based on the
terminal, never the capability (clig.dev):
- On a TTY (a human): it prompts —
task newwithout--epicopens an epic picker; a baretask startopens a picker over ready-to-start tasks. Prompts render to stderr, so stdout stays byte-identical to the flag-driven run. - Off a TTY (a pipe, an agent,
--json, or--no-input/TSKFLW_NO_INPUT=1): it never prompts — it fails with today's exit code (11) naming the flag to pass.
So every prompt has a flag twin and nothing interactive can ever block a script.
Ctrl-C out of a prompt exits 130 (the SIGINT convention) with a quiet
aborted, not an error.
Long human output pages the same way: show / schema pipe through a pager (like
git) only on a TTY — never under a pipe, --json, or --no-input, so machine
output stays byte-identical. Program precedence: TSKFLW_PAGER → [pager].command
(in .tskflwctl.toml) → $PAGER → less -FRX. On/off: --no-pager → --paginate
→ [pager].enabled → default on.
tskflwctl ships cobra-generated completion for bash/zsh/fish. For zsh:
just install # put tskflwctl on $PATH (completion shells out to it)
just completion-zsh # writes ~/.zsh/completions/_tskflwctl + prints the one-time setupIf ~/.zsh/completions isn't already on your $fpath, add once to ~/.zshrc:
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinitOther shells: just completion bash / just completion fish print the script
to stdout (see tskflwctl completion --help). Completion covers the command
tree, flags, and task/audit/epic slugs — e.g. task show <TAB>,
audit close <TAB>, epic show <TAB> offer the real slugs (and still work when
a file's frontmatter is malformed).
just wraps the common tasks:
just build— buildbin/tskflwctljust run *ARGS—go run ./cmd/tskflwctl …just test—go test ./...just lint—golangci-lint run ./...just fmt— gofmt + lint formattingjust tidy—go mod tidy
Design rationale lives in docs/ARCHITECTURE.md.
A Bubble Tea browser over the same core the CLI uses (a second primary
adapter — never the filesystem directly). Two panes: an entity list (tasks /
epics / audits) and a detail preview rendered as glamour markdown (R
toggles raw). Vim-first keys: ctrl+p command palette (fuzzy-jump to any
task/epic/audit or run a command), : command-jump, / filter (slug/desc/tags),
F toggles the filter between fuzzy and substring,
o/O sort, s/S status views, [/] tabs, m move (lifecycle:
start/complete/defer/…), e to edit a task's fields inline, E to open the
whole file in $EDITOR (any entity; re-read on save via live-reload), f to
follow a reference (task ⇄ epic) with ctrl+o to
jump back, y/Y to copy the selection's slug / file path to the system
clipboard (a native tool — pbcopy/wl-copy/xclip — when available, else OSC 52 so
it still works over SSH), /+n/N find-in-body when the
detail is focused, ? for the
full keymap, r to refresh. The detail pane's title is a click-to-open link
(OSC 8) to the entity's file, and the terminal window/tab title tracks the current
selection. It live-reloads via fsnotify — edits from
your editor or a CLI task move in another terminal show up within ~200ms,
cursor preserved. See
planning/epics/18-tui-bubble-tea-interactive-planning-browser.md.
Clickable titles under tmux + Ghostty. The detail-title link is correct OSC 8 and works in a bare terminal as-is; tmux needs two things. (1) Let tmux pass the hyperlink through — tmux ≥ 3.4 with
set -as terminal-features ',xterm-ghostty:hyperlinks'(match your real$TERM), then a fulltmux kill-server— asource-filereload often does not applyterminal-features. Confirm withtmux info | grep -i hyperlink. (2) Click through tmux's mouse capture — withmouse on, open links via shift+cmd+click (Shift bypasses tmux's grab; make sure Ghostty'smouse-shift-captureisn'ttrue), orset -g mouse offfor plain cmd+click (you lose tmux mouse scroll/select). Quick isolate, in a tmux pane:printf '\e]8;;https://example.com\e\\Click Me\e]8;;\e\\\n'— if Click Me is underlined, rendering already works and it's only the click modifier (step 2); if it's plain text, tmux isn't passing OSC 8 yet (step 1).



