task-plus
Go CLI tool that standardizes common development workflows across repositories.
Latest: v0.43.0Try https://task-plus.docs.bytestone.uk/ for documentation. The development method task-plus serves — stories, spec, ADRs — lives in the development manual.
Install
go install git.bytestone.uk/hum3/task-plus/cmd/task-plus@latest
tp is a shorter alias: a symlink to task-plus in GOBIN, which task-plus self update creates (or make it yourself):
ln -s "$(go env GOPATH)/bin/task-plus" "$(go env GOPATH)/bin/tp"
Commands
| Command | Description |
|---|---|
check |
Validate task-plus.yml and Taskfile.yml configuration |
release |
Interactive release workflow; on a protected main a lone story's pull request carries its release and a batch goes through release, tagged after the merge; release query reports the release in progress; release batch opens the release branch |
release:version-update |
Scaffold a Taskfile task to update version strings |
issue |
Issues on the release remote's forge (Forgejo or GitLab) |
story |
Scaffold development-method stories and file their issues |
roadmap |
ROADMAP.md as issues: lint against the forge, add an idea, archive closed items |
maturity |
Declared maturity levels: show, or code N written to task-plus.yml with the base branch protected on the forge |
repos |
Manage git remotes for release |
pages |
Serve, deploy, configure, and migrate documentation |
md2html |
Convert markdown files to Bulma-styled HTML |
md_update |
Update auto-marker sections in a markdown file (toc, pages, links) |
readme |
Update auto-marker sections in README.md |
wt |
Manage git worktrees for isolated Claude tasks |
claude |
Run claude with --dangerously-skip-permissions (requires worktree + sandbox) |
self |
Manage task-plus itself |
Global Flags
--init— create a defaulttask-plus.ymlconfig file (statichost.eu pre-configured)-a— list available commands--version— print version
tp check
Validates project configuration and prints a report. Checks:
task-plus.yml— parses YAML, validates type/forge/changelog_format/deploy targets, flags unknown fields;maturity.code1 or more needs a Forgejo or GitLab release remote with a token (token_env, orGITLAB_TOKENon GitLab), since the batch goes through pull requests, and refuses a hand-edited[Unreleased]or a.tp-release-commentTaskfile.yml— checks standard tasks (fmt, vet, test, check), detects name conflicts and inversions; a missingchecktask is an error whentp releasewould runtask check(nocheck:intask-plus.yml, or one naming it), a warning otherwise- Cross-repo — validates
-docssibling relationship, checks for staledocs/or misplaced config - In a
-docsrepo, warns about.mdfiles missing theDOC-prefix ROADMAP.md— every item an open issue labelledroadmap, astp roadmap check; and any release in flight
tp check
tp check --dir /path/to/project
tp release
Interactive release workflow. Replaces duplicated task release Taskfile patterns.
tp release
tp release --dry-run
tp release --yes --dir /path/to/project
Flags:
--dry-run— show what would happen without making changes--yes— take the plan with every default, without theProceed? [Y/n/e]prompt--dir <path>— project directory (default: the nearesttask-plus.ymlat or above the working directory, else the git top level)--merge— on a story branch in direct mode, fast-forward the base branch to it and release from there (without it the run stops)
Taskfile guard: If Taskfile.yml contains a release: task, tp refuses to run (to avoid conflict).
Release Workflow
- Run checks (e.g.
task check) - Refuse a dirty working tree (commit, stash or ignore first)
- Detect version (latest tag +
release_bump, minor by default) - Update CHANGELOG.md
- WASM build (if configured)
- Git tag
- Git push (branch + tags)
- Goreleaser (if binary project)
- Cleanup old releases (GitHub, Codeberg, GitLab)
- Local install
- Deploy documentation (if configured)
- Run
post_releasecommands (if configured)
That is the trunk process, maturity.code 0. At code level 1 and up
(tp maturity code 1) stories go by pull request (on GitLab, a merge
request), each carrying a changes/<n>.<kind>.md fragment. One story is
a lone release: tp release on its branch makes the release commits
there and opens its pull request straight into main, one merge. Two or
more are a batch: tp release batch, or the second story's tp release,
opens a release branch (converting a lone release in flight into it),
the stories' pull requests go into release, and tp release on main
makes the release commit and opens one pull request to main. Either
way the run waits up to merge_wait (default 10m) for the merge and tags
it. See
process by code level.
tp release:version-update
Scaffolds a sample Taskfile task for updating version strings during release. The release workflow calls this task (if it exists) with VERSION=vX.Y.Z after the version is confirmed.
tp release:version-update --init
tp repos
Manage which git remotes are pushed to during release.
tp repos # show configured and available remotes
tp repos info # same as above
tp repos add <name> # add a git remote to the release push list
tp repos remove <name> # remove a remote from the release push list
tp repos archive <name> # archive (freeze read-only) the remote's repo on its forge
tp pages
Serve, deploy, and manage documentation. When run from a main project repo with a -docs sibling, automatically delegates to the docs repo.
tp pages # build and serve docs/ over HTTP
tp pages --port 3000 # custom port
tp pages deploy # deploy to configured targets
tp pages deploy --dry-run # show what would happen
tp pages config # show configured deploy targets
tp pages migrate # create a -docs sibling repo from docs/
tp pages migrate clean # remove docs/ and pages config from main repo after migration
Flags (serve mode):
--port <n>— HTTP port (default:8080)--dir <path>— project directory (default: the nearesttask-plus.ymlat or above the working directory, else the git top level)
tp md2html
Converts markdown files to Bulma-styled HTML pages with breadcrumb navigation. Supports auto-marker comments:
| Marker | Output |
|---|---|
| <!-- auto:toc --> | Table of contents from h2+ headings |
| <!-- auto:pages --> | Bulma sidebar nav of all HTML pages, grouped by subdirectory |
| <!-- auto:links --> | Project links (git remotes, statichost) |
Breadcrumbs auto-detect the docs root by walking up from --dst to find index.html/index.md, so subdirectory pages link correctly to the root.
Relative links to .md files inside the docs root are rewritten to the .html page, relative to where the output lands — [x](x.md#part) becomes x.html#part, and docs/x.md in a root README.md rendered with --dst docs becomes x.html. Links with a scheme, absolute paths, and targets outside the docs root are left alone.
An embedded local .svg (resolved from --dst, as the browser resolves it) fails the build when the page would draw it illegibly (#112). That means either no fixed width on its root <svg>, which is d2's default fit-to-screen output (render with d2 --scale 1), or its smallest text drawn under 11px once a 960px page shrinks it to fit. The error names the width the image must stay under. A skipped page's images are still checked.
By default, files whose .html output is at least as new as the source .md are skipped. Pass --rebuild to force a full rebuild (e.g. after a template change).
Typical docs repo Taskfile pattern — convert subdirectories first, index.md last so auto:pages sees all HTML files:
docs:build:
cmds:
- task-plus md2html --src docs/research --dst docs/research
- task-plus md2html --src docs --dst docs
- task-plus md2html --file ../myapp/README.md --dst docs
- task-plus md2html --file docs/index.md --dst docs # index last (auto:pages)
tp md_update
Updates auto-marker sections in a markdown file without converting to HTML. Same markers as md2html (auto:toc, auto:pages, auto:links) but writes back to the source .md file.
tp md_update docs/index.md # scan file's directory for pages
tp md_update --dst docs docs/index.md # explicit pages directory
tp wt
Manage git worktrees for running Claude tasks in isolation. Each worktree gets its own branch (task/<name>), sandbox settings, and VS Code configuration. Task names are auto-prefixed with WT (e.g. demo → WTdemo, worktree dir project-WTdemo).
tp wt start my-feature # create worktree, add it to the project .code-workspace
tp wt start my-feature /c # shorthand: create, work, then clean up (delegates to wt clean)
tp wt agent my-feature --spec="implement login" # register agent + run claude
tp wt review my-feature # diff task branch against main
tp wt clean my-feature # merge, remove from workspace + recent list, clean up
tp wt clean my-feature --yes # same, skipping the confirmation (for agents/scripts)
tp wt list # list active worktrees
tp wt dashboard # agent dashboard (web UI; --term for terminal)
tp wt --init # print Taskfile snippets for wt: tasks
Task names "doc" and "docs" are reserved (they clash with the -docs repo convention).
Editor integration. wt keeps a <project>.code-workspace file beside the repo, listing the main checkout plus every active worktree. Open that workspace once in your editor — wt start then makes new worktrees appear inside that window and wt clean makes them disappear, with no extra windows. This works without relying on the editor's CLI control socket (often missing outside the integrated terminal, and absent entirely on some VSCodium setups). When a live control socket is reachable, wt additionally pushes --add/--remove straight into the running window, so plain folder windows update too.
tp claude
Runs claude --dangerously-skip-permissions with safety guards. Requires:
- Running inside a git worktree (not the main repo)
.claude/settings.jsonwith sandbox enabled
tp claude
tp claude "implement the search feature"
tp self
Manage the task-plus installation.
tp self update # install the latest release the Go proxy lists (exact version, not @latest)
Config
Optional task-plus.yml in project root (generate with task-plus --init):
type: library # or "binary" (auto-detected from .goreleaser.yaml)
check: [task check] # commands to run first
changelog_format: keepachangelog # or "simple"
wasm: [] # optional WASM build commands
goreleaser_config: .goreleaser.yaml
forge: github # forge override for the release remote (github, gitlab, forgejo)
release_remote: github # remote whose forge drives goreleaser/proxy decisions (default: first in remotes)
release_bump: minor # component the suggested version increments: major, minor (default) or patch
remotes: # git remotes pushed to during release (default: [origin])
- origin # plain string for recognised hosts
- name: forgejo-local # map form for hosts that need overrides
forge: forgejo # github, gitlab, forgejo, or none (dumb mirror, no release API)
token_env: HOMELAB_FORGEJO_TOKEN # env var holding this host's API token (GitLab default: GITLAB_TOKEN)
install: true # auto-run "go install" (skip prompt; omit to be asked)
cleanup:
keep_patches: 2 # per minor version
keep_minors: 5
pages_build: [task docs:build] # commands to build docs before serving/deploying
pages_deploy: # deploy docs during release (multiple targets supported)
- type: github # push docs/ to gh-pages branch
- type: statichost
site: myproject # site name on statichost.eu
post_release: [task deploy:fetch] # run after the release is published (TP_VERSION set); failures warn
maturity: # declared levels from the manual's maturity ladders
code: 1 # 0 trunk; 1 batch of stories through pull requests (Forgejo or GitLab release remote); 2 gated
merge_wait: 10m # batch: wait this long for each merge and go on in the same run (default 10m; 0: one step per run)
All fields optional — sensible defaults are auto-detected.
Forge arrangements
tp release supports Codeberg + GitHub mirror, GitHub only, Codeberg only, and
self-hosted Forgejo setups. Run the interactive interview to identify your
arrangement and write it to task-plus.yml:
tp check --setup
Release cleanup deletes old releases on every configured remote whose forge is
reachable (gh/glab CLI or Forgejo API token). GitHub/GitLab commands are pinned
to the right repo via -R owner/repo derived from each remote's URL.
Forgejo/Codeberg API tokens: if a remote sets token_env, only that
variable is used. Otherwise the default chain is CODEBERG_APIKEY, then
FORGEJO_TOKEN, then GITEA_TOKEN. Goreleaser publishing to a Forgejo host
expects GITEA_TOKEN; tp release passes the forge token automatically when
it's unset.
GitLab API tokens: token_env if the remote sets it, else GITLAB_TOKEN
(what glab and goreleaser read too). A batch opens its merge
request through the GitLab API with it. Release cleanup still goes through
glab, so self-hosted GitLab additionally needs glab host configuration —
not managed by tp.
Goreleaser and the release remote: .goreleaser.yaml must publish to the
release remote's forge, or goreleaser fails after the tag is pushed. For a
Forgejo remote that is a gitea_urls block (api: https://<host>/api/v1,
download: https://<host>) and release: gitea: {owner, name} matching the
remote; for GitHub, release: github: {owner, name}. tp check reports what
is missing and tp release refuses to push until it is right, showing the
block to paste (story 010).
Documentation Deployment
Configure pages_deploy in task-plus.yml to deploy documentation as part of the release workflow. Multiple targets can be active simultaneously.
Supported providers:
| Type | Description | Requirements |
|---|---|---|
github |
Pushes docs/ to gh-pages branch via git subtree push |
Git remote configured |
statichost |
Uploads docs/ to statichost.eu |
site field required; uses shcli (auto-downloaded if missing) |
rsync |
Rsyncs docs/ to /srv/sites/<site>/ on a self-hosted server over SSH |
site field required; host from host: or task-plus.local.yaml |
If pages_build commands are configured, they run before deployment.
Local config: values that describe the machine rather than the project
— and shouldn't be published with the repo — live in
~/.config/task-plus/local.yaml ($XDG_CONFIG_HOME/task-plus/local.yaml),
read for every project on the machine. A gitignored task-plus.local.yaml
next to task-plus.yml overrides it field by field for one project; tp check warns if that file isn't gitignored, and reports which of the two
files are in use. Keep the real file in a synced folder and symlink it to
the user path. Fields:
rsync_host:— default ssh target for rsync targets that don't set their ownhost:mirror_host:,mirror_token_env:— defaults formirrors:entries (pull mirrors, synced after a release push)secrets_provider:— built-in token source for commands that need API tokens (release,repos archive,pages deploy). Withbitwarden, tp fetches tokens via thebwCLI using the session cached bytp unlock(24h TTL, 0600 file; everytp unlockprompts and restarts the 24h;tp unlock queryprints the time left, exit 1 when locked) and theENV_VAR<TAB>item-id[<TAB>field]mapping in~/.config/bw-session/items.conf.tp secrets CMD...runs any other command with the same tokens in its environment. List the tokens a project needs intask-plus.yml(secrets: [GH_TOKEN, BYTESTONE_FORGEJO_TOKEN]) and only those are set; without the list every mapped token is, andsecrets: []fetches none (no vault call at all).secrets_wrapper:— alternative tosecrets_provider: an external command tp re-execs itself under (e.g. a script that loads tokens into the environment).secrets_providertakes precedence.
Example task-plus.yml for deploying to both GitHub Pages and statichost.eu:
pages_build: [task docs:build]
pages_deploy:
- type: github
- type: statichost
site: my-docs
Links
| Documentation | https://task-plus.docs.bytestone.uk/ |
| Source | https://git.bytestone.uk/hum3/task-plus |