gptodo

v0.1.0 Task management and work queue generation utilities for gptme agents packages/gptodo View on GitHub

gptodo

Task management and work queue generation utilities for gptme agents.

Features

Installation

Install as a CLI tool:

# Using uv (recommended)
uv tool install git+https://github.com/gptme/gptme-contrib#subdirectory=packages/gptodo

# Using pipx
pipx install git+https://github.com/gptme/gptme-contrib#subdirectory=packages/gptodo

From gptme-contrib workspace

# Install with workspace
uv sync

# Or install package directly
uv pip install -e packages/gptodo

Usage

Task Management CLI

# View task status
gptodo status
gptodo status --compact

# Show specific task
gptodo show <task-id>

# Edit task metadata
gptodo edit <task-id> --set state active
gptodo edit <task-id> --set priority high
gptodo edit <task-id> --add tag feature

# Validate tasks
gptodo validate

# List tasks with filters
gptodo list --priority high
gptodo list --state active

# Auto-expire long-quiet tasks (default: 90 days since `created`)
gptodo expire --dry-run          # preview
gptodo expire                    # apply
gptodo expire --days 60          # tighter window
gptodo expire --state backlog    # only reap backlog

Sequential Planning (next --limit)

gptodo ready and a bare gptodo next answer a parallel question: what is unblocked right now. If task A unblocks task B, B is invisible to both until A is actually completed — so a deep queue can look one task shallow.

gptodo next --limit N answers the sequential question instead. It greedily simulates completing each pick (in memory — task files are never written), recomputes the ready set, and lets newly unblocked tasks join the list:

gptodo next                       # top ready task (unchanged)
gptodo next --limit 5             # next 5, in unblocking order
gptodo next -n 5 --order unblock  # critical path first
gptodo next --limit 5 --json      # ordered array with attribution

Every item past the first states why it is there — unblocked by #3 <task>, or (already ready) if it needed nothing.

Two orderings:

--order Behaviour
priority (default) Greedy by the normal next ordering — literally "what's next, then next".
unblock At each step take the task that unblocks the most downstream work — surfaces the critical path rather than the priority path.

If fewer than N tasks are reachable, that is stated explicitly (only 3 of 5 reachable — nothing further is unblocked by this sequence) rather than silently returning a short list. --pool / --exclude-pool / --use-cache apply at every step, not just the first pick, and dependency cycles terminate the simulation instead of hanging it.

--limit 1 (and omitting the flag) produce byte-identical output to the previous behaviour, including the --json shape — autonomous sessions calling gptodo next --json are unaffected. The --order flag is also ignored when --limit 1 (it only changes sequencing within a multi-task cascade). In multi-task mode the JSON gains sequence, order, requested, reachable, complete, and note keys alongside the unchanged next_task / alternatives.

Machine-Readable Output (--json)

Scripts should parse --json rather than grep the rendered human output (the emoji/formatting are not a stable contract).

# Detect whether any task is active (used by autonomous-run gates)
gptodo status --json | jq -e 'any(.tasks[]; .state == "active")'

# Per-state counts
gptodo status --json | jq '.summary.by_state'

Single-type shape (default):

{
  "type": "tasks",
  "tasks": [ { "id": "...", "state": "active", "priority": "high", ... } ],
  "summary": {
    "total": 12,
    "by_state": { "active": 1, "backlog": 11 },
    "issues": 0,
    "untracked": 0
  }
}

With --all, results are grouped under types keyed by directory type, each holding the same {type, tasks, summary} shape. gptodo list, ready, and next also support --json (and --jsonl).

Generate Work Queue

# Basic usage (current directory as workspace)
gptodo generate-queue

# Specify workspace path
gptodo generate-queue --workspace ~/my-agent

# Specify GitHub username for assignee filtering
gptodo generate-queue --github-username YourUsername

Task Locking (Multi-Agent)

# Acquire lock on a task
gptodo lock acquire <task-id>

# Release lock
gptodo lock release <task-id>

# Check lock status
gptodo lock status <task-id>

Output

Generates state/queue-generated.md with:

Task Sources

  1. Local Task Files (tasks/*.md):

    • Filter: priority=high/urgent AND state=new/active
    • Uses frontmatter metadata
  2. GitHub Issues:

    • Filter: label=priority:high/urgent AND state=open
    • Boosts score if assigned to configured username

Configuration

Via command-line arguments or environment variables:

Task Format

Task files should use frontmatter metadata:

---
state: active      # draft, backlog, todo, active, ready_for_review, waiting, someday, done, cancelled, expired
priority: high     # low, medium, high
task_type: project # project (multi-step) or action (single-step)
assigned_to: bob   # agent name
tags: [ai, dev]    # categorization tags
---
# Task Title

Task description...

## Subtasks
- [ ] First subtask
- [x] Completed subtask

State Semantics

The ten canonical states and what they mean — not just what they're called. The autonomous loop drifts when "active" gets used as an opaque "recently touched" tag; enforcing the semantics is the point of the gptodo transitions table and the --force gate on gptodo edit --set state.

State Meaning In next/ready?
draft In-progress plan, filed so it isn't lost, but not released to the fleet. Use while a planning session is still writing the plan. No
backlog Queued, not yet triaged. Default for newly-created tasks. Yes
todo Triaged and ready to start; unclaimed; nothing is blocking work. Yes
active A human or agent is working on it right now. Should be paired with assigned_to and assigned_at. Yes (already owned; still listed)
waiting Blocked on an external event (a date, a reply, an approval, a gate firing). Should carry wait: and/or waiting_for: explaining what it's waiting for. No
ready_for_review Work done, awaiting operator sign-off before done. Should reference a commit or PR in the body. No (query --state ready_for_review)
someday Parked idea; may or may not ever be picked up. Explicitly excluded from next/ready (GTD someday/maybe). No
done Terminal. Work merged / criterion met. No (terminal)
cancelled Terminal. Will not be picked up; rationale in body. No (terminal)
expired Soft-terminal. Auto-applied by gptodo expire when a backlog/todo/someday task has sat quiet longer than the expire window (default 90d since created). Revive to backlog/todo without --force. No

Legacy deprecated aliases (still accepted with a warning): newbacklog, pausedbacklog. paused is not a hold — it normalizes to backlog and is claimable. Do not file in-progress plans as paused; use draft.

Common confusions to avoid

stateDiagram-v2
    [*] --> backlog
    [*] --> draft
    draft --> backlog : released
    draft --> todo : released
    draft --> cancelled
    backlog --> todo
    backlog --> draft
    backlog --> someday
    backlog --> cancelled
    todo --> active
    todo --> backlog
    todo --> draft
    todo --> someday
    todo --> cancelled
    active --> ready_for_review
    active --> waiting
    active --> draft
    active --> someday
    active --> done
    active --> cancelled
    ready_for_review --> active : review fails
    ready_for_review --> done
    ready_for_review --> cancelled
    waiting --> active : blocker resolved
    waiting --> someday
    waiting --> cancelled
    someday --> backlog : revived
    someday --> todo : revived
    someday --> cancelled
    backlog --> expired : auto-reap
    todo --> expired : auto-reap
    someday --> expired : auto-reap
    expired --> backlog : revived
    expired --> todo : revived
    expired --> cancelled
    done --> [*]
    cancelled --> [*]
    expired --> [*]

Auto-expire

The queue grows without bound if long-quiet tasks never get closed. gptodo expire walks the tree, finds tasks in eligible states (backlog/todo/someday by default) whose created date is older than --days N (default 90, env GPTODO_EXPIRE_DAYS), and transitions them to expired. expired_from and expired_at are stamped so revival is a one-liner:

gptodo expire --dry-run       # preview what would be reaped
gptodo expire                 # apply
gptodo expire --days 60       # tighter window
gptodo expire --state backlog # only reap backlog
gptodo expire --json          # machine-readable output for cron/CI

# Revive an expired task (no --force needed — expired is soft-terminal)
gptodo edit <task> --set state backlog

Auto-expire deliberately skips:

Age is measured from created, not modified, because a task that only gets touched by lint/reformat still hasn't been worked — using mtime would let queue drift hide behind incidental edits.

gptodo transitions prints the machine-readable table. gptodo edit --set state X enforces legality and refuses illegal transitions unless you pass --force (e.g. reopening a done task, or dropping active back to todo without finishing / handing off). The escape hatch exists — the check is a nudge, not a wall — but every --force should be a conscious act, not a habit.

Frontmatter Schema — Known vs. Hallucinated Fields

The set of supported frontmatter fields lives in KNOWN_FRONTMATTER_FIELDS (see src/gptodo/utils.py). gptodo lint scans task files for anything outside that set and emits a warning.

Do not add ad-hoc fields. Autonomous LLM sessions repeatedly invent plausible-sounding fields under pressure — modified, last_modified, updated_at, last_completed — most of which duplicate information you can already get for free.

The canonical anti-example is modified: (proposed by an autonomous loop in 2026-07-01 as a "solution" to queue-health monitoring). It was rejected as an anti-design-goal: it's a high-churn field that would have to be wired into every edit path, and the answer it purports to provide is already available via:

python -c "import os; print(os.path.getmtime('tasks/foo.md'))"   # file mtime
git log -1 --format=%ai tasks/foo.md                             # last commit

Both are free. Adding a stored modified field creates a churn hazard (every edit forgets to update it, every test needs to inject it, every diff carries noise) with no net information gain.

The gptodo lint command surfaces these to keep the schema clean:

gptodo lint                        # scan all tasks
gptodo lint tasks/foo.md           # single file
gptodo lint --json                 # machine-readable
gptodo lint --strict               # non-zero exit if warnings found (CI)

Deprecated / anti-goal warnings suggest the correct alternative in the message body. Unknown-field warnings ask you to either add the field to KNOWN_FRONTMATTER_FIELDS (deliberate PR + test) or remove it. Warnings never reject a task — a fresh loop must still be able to write whatever frontmatter it decides on; the linter's job is to nudge toward the schema, not gate loop output.

GitHub Integration

Requires GitHub CLI (gh) installed and authenticated:

gh auth login

Priority labels:

Development

Running Tests

cd packages/gptodo
make test

Type Checking

cd packages/gptodo
make typecheck

Migration from tasks

If you were using scripts/tasks.py, the wrapper script will continue to work but will show a deprecation warning. To migrate:

  1. Install gptodo directly: uv tool install git+...
  2. Replace ./scripts/tasks.py calls with gptodo
  3. All commands remain the same

Integration

This package is designed to work with:

For full autonomous agent setup, see gptme-agent-template.