task-plus

Go CLI tool that standardizes common development workflows across repositories.

Latest: v0.43.0

Try 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 default task-plus.yml config 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.code 1 or more needs a Forgejo or GitLab release remote with a token (token_env, or GITLAB_TOKEN on GitLab), since the batch goes through pull requests, and refuses a hand-edited [Unreleased] or a .tp-release-comment
  • Taskfile.yml — checks standard tasks (fmt, vet, test, check), detects name conflicts and inversions; a missing check task is an error when tp release would run task check (no check: in task-plus.yml, or one naming it), a warning otherwise
  • Cross-repo — validates -docs sibling relationship, checks for stale docs/ or misplaced config
  • In a -docs repo, warns about .md files missing the DOC- prefix
  • ROADMAP.md — every item an open issue labelled roadmap, as tp 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 the Proceed? [Y/n/e] prompt
  • --dir <path> — project directory (default: the nearest task-plus.yml at 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

  1. Run checks (e.g. task check)
  2. Refuse a dirty working tree (commit, stash or ignore first)
  3. Detect version (latest tag + release_bump, minor by default)
  4. Update CHANGELOG.md
  5. WASM build (if configured)
  6. Git tag
  7. Git push (branch + tags)
  8. Goreleaser (if binary project)
  9. Cleanup old releases (GitHub, Codeberg, GitLab)
  10. Local install
  11. Deploy documentation (if configured)
  12. Run post_release commands (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 nearest task-plus.yml at 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:

  1. Running inside a git worktree (not the main repo)
  2. .claude/settings.json with 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 own host:
  • mirror_host:, mirror_token_env: — defaults for mirrors: 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). With bitwarden, tp fetches tokens via the bw CLI using the session cached by tp unlock (24h TTL, 0600 file; every tp unlock prompts and restarts the 24h; tp unlock query prints the time left, exit 1 when locked) and the ENV_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 in task-plus.yml (secrets: [GH_TOKEN, BYTESTONE_FORGEJO_TOKEN]) and only those are set; without the list every mapped token is, and secrets: [] fetches none (no vault call at all).
  • secrets_wrapper: — alternative to secrets_provider: an external command tp re-execs itself under (e.g. a script that loads tokens into the environment). secrets_provider takes 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
Documentation https://task-plus.docs.bytestone.uk/
Source https://git.bytestone.uk/hum3/task-plus