next (1.2.1)

Published 2026-06-07 19:23:38 +00:00 by victor in victor/task-manager

Installation

[registries.forgejo]
index = "sparse+" # Sparse index
# index = "" # Git

[net]
git-fetch-with-cli = true
cargo add next@1.2.1 --registry forgejo

About this package

next

A CLI task manager with automatic urgency scoring. Tasks are stored as TOML files in a git repository, enabling offline-first sync across machines.


Quick start

# One-time setup: initialise a task repository
mkdir ~/tasks && cd ~/tasks
next init

# Capture a task
next add "Call dentist"

# Add with a deadline and context tag
next add "Submit tax forms" --due "April 15" --tag @home --priority high

# What should I work on now?
next next

# See the full list
next list

# Mark done
next done a1b2c3d4

Storage model

Every task is a .toml file inside a tasks/ directory at the repository root. git is the transport layer — next sync runs pull then push. The TOML files are the single source of truth.

next also maintains an SQLite database (.next.db) as a read cache. It is rebuilt automatically whenever the git HEAD changes (e.g. after a pull), so it is always consistent with the TOML files. Add it to .gitignore; next init does this for you.

Machine-local state (active contexts, active users, resource availability) is stored outside the repository in $XDG_STATE_HOME/task-manager/<repo-hash>/state.toml so it is never committed or synced.

my-tasks/
  .git/
  .gitignore                   # contains ".next.db"
  tasks/
    call-dentist-a1b2c3d4.toml
    water-plants.toml          # task with slug "water-plants"
  tags/
    __context__work.toml       # tag description for @work  (@ → __context__)
    __context__home/
      kitchen.toml             # tag description for @home/kitchen
  .next.db                     # SQLite read cache — not committed

~/.local/state/task-manager/<repo-hash>/
  state.toml                   # machine-local: active contexts, users, resource availability

Projects and subtasks

Any task can have subtasks — no special tag or type is required. Subtasks attach via --parent. A parent task is hidden from the default scored list until all its direct children are resolved.

next add "Launch blog" --slug launch-blog
next add "Write first post" --parent launch-blog
next add "Set up hosting" --parent launch-blog
next tree                      # show all tasks in a parent-child tree
next list parent:launch-blog   # list tasks within this project
next show launch-blog          # show full details for a single task

Tag system

All labels on a task are tags. Prefix conventions give some tags special meaning:

Prefix Kind Example Effect
@ Context @home, @work Hidden when a different context is active
# Resource #printer, #vacation Hidden when resource is unavailable
(none) Freeform python, urgent No implicit filter

Tasks with no @ tag are always shown regardless of the active context.

next context set @home          # global context filter
next context clear              # show all contexts
next context exclude @work      # always hide @work tasks
next context clear-excluded     # remove exclusions

next resource set #printer off  # hide printer tasks
next resource set #printer on   # show them again

Tag descriptions

Any tag (context, resource, or freeform) can carry a human-readable description. These descriptions are stored as individual TOML files under tags/ in the repository and are committed to git, making them visible to all machines. They appear in next tag, next context, and next resource output and serve as structured metadata for AI agents reading the repository.

next tag describe @work "Tasks at the standing desk — laptop required"
next context describe @home "Home tasks: kitchen, garden, errands"
next resource describe #printer "Office laser printer, 2nd floor"
next tag                        # list all tags with their descriptions

User filter

The user filter is for task organisation, not access control. When active users are set, tasks assigned to other users are hidden. Unassigned tasks are always visible (shared backlog).

next user set alice bob   # only show tasks for alice and bob
next user clear           # no user filter
next user list            # all assignees across all tasks

Urgency scoring

Every visible task receives a numeric score used for ranking:

score = due_factor + priority_factor + project_factor + age_factor + score_adjustment
  • Due factor — rises sharply as the deadline approaches; peaks when overdue (zeroed by no_time_urgency)
  • Priorityhigh (+2), medium (+1), low (0)
  • Project factor — parent task's priority offsets the score (+0.5 / 0.0 / −0.5)
  • Age — older tasks float up; capped at +2; zeroed when long_term = true or no_time_urgency
  • Tag factor — each tag with explicit priority metadata adds an offset (high +1.0, low −1.0); applied to both the task's own tags and the parent's tags
  • Started bonus — flat +4.0 when status = started; moves in-progress tasks above open peers
  • Adjustment — manual boost/penalty via --adjust

Tags can suppress time-based urgency entirely with next tag set-no-time-urgency <tag> — useful for wishlist or someday tags where age and deadlines should not drive priority.


Filter tokens

All list commands accept filter tokens in any order:

Token Example Meaning
+<tag> +@home, +python Task must have this tag
-<tag> -@work Task must not have this tag
parent:<slug> parent:launch-blog Task is a descendant of (or is) the task with this slug
context:<@tag> context:@home Override active context for this query
user:<name> user:alice Override user filter for this query
--future Include tasks with a future start date
--all Disable all implicit filtering

Command summary

Command Description
next init Initialise a task repository in the current directory
next add Add a task
next list [-n N] List tasks sorted by urgency score; -n/--limit caps output
next next [N] Show top N highest-scored tasks (default 10)
next show <id> Full details of a single task
next tree Show all tasks in a parent-child tree
next start <id> Mark as started (in-progress); logs a time entry
next stop <id> Stop a started task (returns to open); logs a time entry
next done <id> [--completed-at <date>] Mark done; triggers recurrence if applicable
next cancel <id> Mark cancelled
next edit <id> Modify fields on an existing task
next delete <id> Permanently remove a task
next move <id> Change parent task
next open <id> Open the task's URL in the browser
next data set/unset/get Manage arbitrary key-value data on a task
next tag [describe/set-priority/set-no-time-urgency/…] List tags; manage tag metadata
next context [set/clear/exclude/clear-excluded] Manage global context filter (include and exclude lists)
next resource [set] Manage resource availability
next user [set/clear/list] Manage user filter
next plugin [register/watch/unwatch/unregister/list] Manage export plugins (see Plugins)
next forecast Upcoming due dates grouped by time, including projected schedule-recurrence occurrences over the horizon
next sync Pull from remote, push local commits
Task IDs accept a full UUID, a slug, or any unambiguous 4+ character hex prefix.
All commands support --json for pipe-friendly output.

The --autosync global flag (or autosync = true in the config file) automatically runs next sync after every mutation command. The --no-autosync global flag disables it for a single invocation even when autosync = true in the config (the two flags conflict).


Configuration

next stores tasks locally as TOML files in a git repository.

Machine-local settings live at $XDG_CONFIG_HOME/task-manager/config.toml (CLI only):

repository            = "/home/alice/tasks"  # use next from any directory

autosync              = true                 # sync automatically after each mutation
list_limit            = 20                   # cap `next list` output (same as -n 20)
forecast_horizon_days = 90                   # days ahead shown by `next forecast`
next_count            = 10                   # tasks shown by `next next`

[sync]
git_subprocess        = true                 # use `git` subprocess instead of libgit2
pull_before_query     = true                 # pull before reads (default); use --offline to bypass
staleness_secs        = 3600                 # re-pull after this many seconds (default: 1 hour)

Scoring weights live in the repository at config/scoring.toml, committed to git and synced. next init seeds it with the defaults. Because it is part of the repo, the CLI, the MCP server, and plugins all score tasks the same way. Omitted fields fall back to the built-in defaults, so you can keep just the weights you change:

# config/scoring.toml
priority_high = 3.0   # only override what you want to change
age_max       = 3.0

Remote access is provided via MCP — see the MCP server section below.


Terminal UI (next-tui)

next-tui is an optional full-screen terminal UI built on ratatui. It links the core library directly (no shelling out to next) and edits the repository through the same transactions and git commits as the CLI, so it is safe to run alongside the CLI and the MCP server. It offers list / tree / forecast views, a full edit modal, per-task actions, a context/resource/user state panel, and background sync.

cargo build --release --features tui --bin next-tui
next-tui                     # or: next-tui --repo <path> --config <path>

It loads its own tui.toml, falling back to the CLI's config.toml, then defaults. See TUI.md for the full keymap and view reference.


MCP server (next-mcp)

next-mcp is an optional HTTP server that exposes the full task management API over the Model Context Protocol (MCP). It is designed to run as a Podman Quadlet container so that AI assistants (Claude, etc.) can manage tasks remotely.

Building

# Binary only
cargo build --release --features mcp --bin next-mcp

# Container image (published at forgejo.victorsavu.eu/victor/next-mcp)
podman build -f Containerfile -t localhost/next-mcp:latest .

Configuration

All configuration is passed via environment variables — no config file.

Variable Required Default Description
NEXT_BEARER_TOKEN MCP client auth token
NEXT_GIT_URL first start HTTPS URL to clone the tasks repo
NEXT_GIT_USER Git username (or embed in URL as https://user:token@…)
NEXT_GIT_TOKEN Git password/token
NEXT_REPO_PATH /data/tasks Local path for the tasks repository
NEXT_BIND_ADDR 0.0.0.0:3000 Listen address
NEXT_WEBHOOK_TOKEN If set, enables POST /webhook/sync with this token
NEXT_SYNC_INTERVAL 86400 Periodic pull+push interval in seconds; 0 disables
NEXT_DEFERRED_SYNC_DELAY_SECS 30 Seconds before deferred sync fires after autosync=false

Credentials embedded in NEXT_GIT_URL are stripped before logging.

NEXT_BEARER_TOKEN=secret \
NEXT_GIT_URL=https://git.example.com/user/tasks.git \
NEXT_GIT_TOKEN=my-pat \
next-mcp

Quadlet (Podman)

Copy quadlets/next-mcp.container to ~/.config/containers/systemd/ and create ~/.config/next-mcp/env (chmod 600) — use quadlets/next-mcp.env.example as a starting point:

NEXT_BEARER_TOKEN=…
NEXT_WEBHOOK_TOKEN=…          # optional; enables POST /webhook/sync
NEXT_GIT_URL=https://git.example.com/user/tasks.git
NEXT_GIT_USER=user
NEXT_GIT_TOKEN=…
NEXT_SYNC_INTERVAL=86400

Two named volumes are used — Podman creates them automatically on first start:

Volume Mount Contents
next-tasks /data/tasks Cloned tasks git repository
next-state /data/state Machine-local state (active context, user filter, resources)

Then:

systemctl --user daemon-reload
systemctl --user start next-mcp

MCP tools (13)

Tool R/M Description
list_tasks R List scored tasks; accepts filter tokens + context override
get_task R Full details of one task + direct children + score breakdown
add_task M Create a task (auto-applies active context if task has none)
update_task M Edit fields or transition state (start/stop/done/cancel/move); done accepts completed_at
delete_task M Permanently remove a task
sync M Pull then push; fails fast if sync already in progress
get_state R Active contexts, excluded contexts, users, resource availability
set_context M Replace active and/or excluded context filters
set_resource M Toggle resource availability
set_user_filter M Replace active user filter
manage_tag R/M Tag metadata CRUD (list/show/describe/set_priority/set_no_time_urgency/…)
manage_task_data R/M Task data key-value pairs (get/list/set/unset)
get_forecast R Upcoming due dates within a configurable horizon; accepts context override

All mutation tools (M) accept an autosync: bool parameter (default true):

  • autosync: true — sync runs inline before the response is returned. Sync errors are logged but do not fail the tool call.
  • autosync: false — mutation returns immediately; a deferred sync fires NEXT_DEFERRED_SYNC_DELAY_SECS seconds after the last mutation in a batch. Useful when making many changes and calling sync explicitly at the end.

At most one sync runs at a time — the sync tool and webhook return an error immediately if a sync is already in progress rather than queuing.

Slug format: letters, digits, - and _ only (e.g. water-plants, work_infra).

Server instructions

The initialize response includes an instructions string (a standard MCP field) that clients MAY inject into the model's system prompt. It carries the tagging conventions (@context, #resource, freeform; / hierarchy; tag priority / no-time-urgency metadata) plus a connect-time snapshot of the active context, unavailable resources, and the catalog of known tags with their descriptions. This pushes tag knowledge onto the AI up front so it reuses existing tags and respects the active context without having to call manage_tag list / get_state first. The snapshot is taken at connection time and refreshes on reconnect.

Webhook

POST /webhook/sync triggers an immediate pull+push using NEXT_WEBHOOK_TOKEN for auth (strictly separate from the MCP bearer token — neither token is accepted on the other route). Wire it to your git host's push webhook to keep the container up to date when others push.

Security notes

  • Container runs as unprivileged user next (UID 1000)
  • Request bodies are capped at 64 KB
  • Bearer token comparison is constant-time and does not leak the expected token's length
  • Embedded git credentials are stripped from NEXT_GIT_URL before any logging

Plugins

Integrations (e.g. Forgejo, WebDAV/CalDAV) live outside the core as external plugin binaries. A plugin subscribes to individual tasks and is notified whenever one of them changes; it then does its work by linking the next library or calling the next CLI.

Registering a plugin

next plugin register <name> -- <program> [args…]   # define/replace a plugin's command
next plugin watch   <name> <task>                  # notify <name> on any update to <task>
next plugin unwatch <name> <task>
next plugin unregister <name>
next plugin list

A plugin typically registers itself: after importing an external item as a task, it runs next plugin watch <name> <task-id> so it learns about later changes. Registrations are machine-local — stored in the [[plugin]] section of state.toml under $XDG_STATE_HOME/task-manager/<hash>/, never committed to git (plugin binaries are per-machine).

Notification contract

When a watched task is updated (any of add/start/stop/done/cancel/edit/move/delete/data), next spawns the plugin's command fire-and-forget, after the repository lock is released, with:

  • stdin: a JSON event, also provided in NEXT_PLUGIN_EVENT:
    { "event": "done", "task_id": "<uuid>", "repo": "<path>", "timestamp": "<rfc3339>" }
    
  • env: NEXT_REPO (repo root), NEXT_PLUGIN_EVENT (the JSON above), NEXT_PLUGIN_ORIGIN (the plugin's own name).
  • cwd: the repository root.

Delivery is best-effort (a missed event is reconciled on the plugin's next run); plugins should be idempotent. Loop guard: a plugin is never notified of changes it caused itself — next sets NEXT_PLUGIN_ORIGIN when spawning the plugin, and any next mutations the plugin makes (which inherit that env) skip notifying that same plugin.

Forgejo plugin

The first bundled plugin maps Forgejo repositories to next contexts. It ships in this crate behind the forgejo Cargo feature (off by default, like mcp) as the next-forgejo binary:

cargo build --release --features forgejo   # builds next-forgejo

Configure ~/.config/next-forgejo/config.toml:

forgejo_url   = "https://forgejo.victorsavu.eu"
forgejo_token = "<api token>"
# next_repo = "/home/you/tasks"   # optional; else next's configured/default repo

[[map]]
repo    = "victor/task-manager"   # owner/repo on Forgejo
context = "@ai/task-manager"      # imported tasks get this context tag

Commands:

next-forgejo register      # register the export hook (sync also does this)
next-forgejo sync          # import issues + reconcile resolution (both ways)
next-forgejo sync --dry-run
next-forgejo hook          # internal: invoked by next's export hook

sync imports each open issue with no task yet (title, issue url, the mapped @context, body → description), links them via __forgejo-* task data, and subscribes the plugin so local resolution propagates. Resolution syncs both ways (close-only in v1):

  • A Forgejo issue closed → the linked task is marked done on the next sync.
  • A task resolved locally (next done/cancel) → the export hook closes the Forgejo issue in real time; sync also reconciles any resolution the hook missed.

Link data attributes on each task (__forgejo- prefix): __forgejo-repo (owner/repo), __forgejo-issue (number), __forgejo-url, and __forgejo-labels (the issue's labels, as a JSON array — kept as data rather than local tags for now). Reopening is manual in v1.

Run sync periodically (cron / systemd timer) to keep imports current.


Cargo features

next is one crate with feature-gated binaries on top of a feature-free core library:

Feature Default Builds
cli the next CLI binary (pulls in clap)
mcp the next-mcp server (--features mcp)
forgejo the next-forgejo plugin (--features forgejo)
tui the next-tui terminal UI (--features tui)
cargo build                          # the next CLI (default)
cargo build --no-default-features    # core library only — no CLI, no clap
cargo build --features mcp           # + the MCP server
cargo build --features forgejo       # + the Forgejo plugin
cargo build --features tui           # + the terminal UI

With --no-default-features the crate is just the core library under src/core/ (domain, storage, store, plugin, config, scoring, service, task_repository, …) that other crates can link without the CLI or its dependencies. Logic shared between the CLI, MCP server, and plugins lives in the core module so the feature modules depend only on the core. (AppContext is part of the cli feature, not the no-features core.)


See also

  • REQUIREMENTS.md — functional requirements
  • ARCHITECTURE.md — internal design and module layout
  • CLI.md — full command reference
  • TUI.md — terminal UI keymap and view reference

Dependencies

ID Version
anyhow ^1
axum ^0.8
chrono ^0.4
clap ^4
dirs ^5
forgejo-api ^0.10
fs4 ^0.8
git2 ^0.19
interim ^0.2
ratatui ^0.30
rusqlite ^0.32
serde ^1
serde_json ^1
thiserror ^2
tokio ^1
toml ^0.8
tracing ^0.1
tracing-subscriber ^0.3
tui-input ^0.15
tui-textarea-2 ^0.11
tui-tree-widget ^0.24
url ^2
uuid ^1
reqwest ^0.12
tempfile ^3
testcontainers ^0.23
tokio ^1
Details
Cargo
2026-06-07 19:23:38 +00:00
0
316 KiB
Assets (1)
Versions (7) View all
1.2.1 2026-06-07
1.2.0 2026-06-07
1.1.0 2026-06-02
1.0.0 2026-05-31
0.4.0 2026-05-30