Skip to content
deligoezPublic

About

Spec-to-task lifecycle manager for AI coding agents. Decomposes specs into atomic, dependency-ordered tasks. Agents execute with 2 tool calls instead of hundreds.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

tp — Task Plan

Spec-to-task lifecycle manager for AI coding agents.

Break specs into atomic, dependency-ordered tasks. Agents execute them with 2 tool calls instead of hundreds. Above the specs, a project's whole plan can live in tp as a reviewed graph of epics, one spec per epic.

tp's primary user is the AI coding agent, not the human. Every command, flag, and output is designed for AX (Agent Experience): minimal round-trips, minimal output tokens, deterministic behavior (no prompts), and actionable error hints. The human authors specs and approves releases; the agent plans and the tool executes.

Why

AI agents fail at long tasks. Research shows:

  • <15 min tasks: 70%+ success (SWE-bench)
  • >50 min tasks: ~23% success (SWE-bench Pro)
  • Each tool call costs ~200 tokens of agent context

tp solves this with atomic task decomposition and a 2-call architecture:

tp plan --minimal --json       # ONE call: get execution plan
# [agent implements each task, commits each one]
tp done --batch results.ndjson # ONE call: close everything

Token overhead: ~5K (vs ~54K with naive per-task tool calls).

Install

# Homebrew (recommended)
brew tap deligoez/tap
brew install tp

# Go install
go install github.com/deligoez/tp/cmd/tp@latest

# Or build from source
git clone https://github.com/deligoez/tp.git
cd tp && go build -ldflags="-s -w" -o tp ./cmd/tp

# Install Claude Code skill (first time)
npx skills add -g deligoez/tp

# Update skill (after tp updates)
npx skills update -g

Claude Code plugin (optional)

The skill above teaches the agent tp's workflow. The plugin adds what has to be true inside an agent process: the brief arrives at session start, the scope fence is denied rather than requested, and a role unit cannot stop before writing its findings. Install it if you use tp run or the review/audit role units.

claude plugin marketplace add deligoez/tp
claude plugin install tp@tp

Update it with:

claude plugin update tp@tp

It ships the same skills/tp, so install either the plugin or the npx skills package, not both. Claude Code namespaces a plugin skill (/tp:tp) separately from a standalone one (/tp), so both load rather than one overriding the other — two entries for the same skill, kept up to date by two different commands. Pick the plugin if you use Claude Code; it is the superset. To switch, run npx skills remove -g tp after installing the plugin.

The binary is not inside the plugin: a marketplace is a git repository, so tp itself still comes from Homebrew or go install. The SessionStart hook preflights tp's presence and version and fails with the install command rather than degrading quietly, so a plugin newer than the installed binary tells you so instead of misbehaving.

Verify what got installed — component inventory and its token cost — with:

claude plugin details tp@tp

Quick Start

# 1. Create a task file from a spec
tp init spec/my-feature.md

# 2. Add tasks (or use tp import for bulk)
tp add '{"id":"create-model","title":"Create User model","estimate_minutes":8,
  "acceptance":"Model exists. Migration runs.","source_sections":["### User Model"],
  "source_lines":"15-42","depends_on":[]}'

# 3. Get the execution plan
tp plan --minimal --json

# 4. Implement, commit, and close each task
tp done create-model "User model at app/Models/User.php. Migration runs." --gate-passed --auto-commit

Lifecycle

tp resume reports exactly five phases — review, decompose, implement, audit, release (internal/engine/phase.go), derivable with tp resume --json | jq .phase across a cycle. tp lint and tp ground are deliberately not among them. They are steps you run, not phases an oracle tracks, and tp run makes the same omission concrete: it drives eight unit kinds (internal/engine/unitkind.go) and none of them grounds anything, so no unattended run ever reaches grounding. Ground is the one loop you have to remember inside a spec's cycle.

Above the spec sits the project's plan, when it has one: a graph of epics in .tp/epics.json, one spec per epic (Epic Graph). Its loop is not a phase either — tp resume tracks one spec at a time and tp run has no epic unit — so the graph is designed and reviewed by hand, slice by slice, before and between the specs it plans. It tracks the work from the specs' side: an epic bound to a task file is done when that file reaches release, and a milestone opens once the gate before it is green.

flowchart TD
    subgraph PLAN["the plan: .tp/epics.json"]
      direction TB
      EPICS["tp epic add / set / import<br/>milestones, epics, edges with a why"]
      ELINT["tp epic lint<br/>is the graph well formed?"]
      EREVIEW["tp epic review<br/>one milestone or epic at a time"]
      EREADY["tp epic ready / claim<br/>what can start, who has it"]
      EGATE["tp epic gate / status<br/>members done, threshold met: the next milestone opens"]
      EPICS --> ELINT --> EREVIEW
      EREVIEW -->|"repair the graph"| EREVIEW
      EREVIEW --> EREADY
      EGATE --> EREADY
    end
    EREADY -->|"tp init --epic: one epic, one spec"| SPEC

    SPEC["spec/my-feature.md<br/>decisions and test rows"] --> LINT["tp lint<br/>does the document hold together?"]
    SIDECAR["spec/my-feature-measurements.md<br/>measurements and derivations"] -.->|"cited by the spec, graded by nothing"| SPEC
    LINT --> GROUND["tp ground<br/>do its claims hold in the world?"]
    GROUND -->|"repair the spec"| GROUND
    GROUND --> REVIEW

    subgraph PHASES["the five phases tp resume reports"]
      direction TB
      REVIEW["review<br/>tp review"]
      DECOMPOSE["decompose<br/>tp import"]
      IMPLEMENT["implement<br/>tp next, tp done"]
      AUDIT["audit<br/>tp audit"]
      RELEASE["release"]
      REVIEW -->|"repair the spec"| REVIEW
      REVIEW --> DECOMPOSE --> IMPLEMENT --> AUDIT --> RELEASE
      AUDIT -->|"fix the code"| AUDIT
    end
    RELEASE -->|"every bound file at release: the epic is done"| EGATE
Loading

Ground, review, epic review and audit are loops rather than steps, and they are the same loop — one shape, run four times against four different questions:

flowchart LR
    EMIT["emit the prompt<br/>one for ground, one per role for review, epic review and audit"]
    GRADE["grade it<br/>one fresh sub-agent context each"]
    RECORD["record the round<br/>--record round.ndjson"]
    CHECK{"--status --check"}
    REPAIR["repair the graph, the spec or the code"]
    NEXT["next phase"]

    EMIT --> GRADE --> RECORD --> CHECK
    CHECK -->|"exit 1"| REPAIR
    REPAIR --> EMIT
    CHECK -->|"exit 0"| NEXT
Loading

What ends each loop is not the same condition, and that difference is the point:

loop --status --check exits 0 when
tp ground coverage: every emitted floor unit carries one of the six verdicts, the floor is not empty, and the latest round holds no FAIL
tp epic review the same as tp review, for one slice of .tp/epics.json and from the project layer alone, and no unit of the slice changed since its last recorded round; every error rule of tp epic lint passes over the whole file
tp review absence: review_clean_rounds trailing rounds carry no finding that review_converge_on counts and the spec has not moved since — or disposition: at review_max_rounds, every finding of the latest round carries a disposition. Either way no finding that review_converge_on counts is left unresolved in an earlier round, since the next round is told not to re-report it, and every registered checks entry passes
tp audit the same with audit_clean_rounds, audit_converge_on and audit_max_rounds; audit runs no checks

A finding leaves a round as a change or as a recorded disposition (--resolve). --status reports which condition ended the loop as done_by — converged or cap — and tp import, next_action and tp resume read the same verdict. The built-in defaults are 2 clean rounds and a cap of 3 rounds for both (0 uncaps), blocking for review_converge_on and all for audit_converge_on; tp config --resolved prints what actually resolves in your repo and where each value came from.

Commands

One line per command — the index a human reads once to learn what exists. The exact forms, every flag, and the workflows that use them live in the skill an agent loads every cycle: SKILL.md.

Primary Workflow

tp plan                        # Full execution plan (THE primary command; --minimal, --compact, --from, --level)
tp commit <id> [reason]        # Stage + structured commit + record SHA (--files)
tp done <id> <reason>          # Close with implicit claim + verification; runs the quality gate
tp done --batch file.ndjson    # Batch close from NDJSON
tp resume [spec]               # Report phase + the single next action from durable state (read-only; --epic <id>)
tp brief [id]                  # The unit brief: identity, scope fence, prior work, close recipe
tp run [spec]                  # Drive the whole cycle unattended, one unit at a time (--status, --dry-run)
tp escalate --decision <name>  # Record an operator-only decision from inside a run and stop the unit

Incremental (fallback)

tp next                        # Resume WIP or claim the next ready task (--minimal, --peek, --brief)

Task State

tp claim <id> [id...]          # open → wip (--all-ready)
tp unclaim <id> [id...]        # wip → open (undo a claim)
tp close <id> <reason>         # wip → done (low-level, prefer tp done)
tp reopen <id>                 # done → open (clears timestamps + SHAs)
tp remove <id>                 # Remove a task (--force cleans dependents)
tp set <id> field=value        # Update a field (--workflow, --project, --local, --bulk)
tp set --epic <id>             # Bind the task file to an epic ("" unbinds); its status follows the file
tp keep <path> "<reason>"      # Keep-list a deliberately-uncommitted file (--remove, --list)

Query

tp list                        # All tasks (--status, --tag, --ids, --compact)
tp ready                       # Tasks with all deps satisfied (--first, --count, --ids)
tp show <id>                   # Full details + spec_excerpt + blocks
tp status                      # Progress summary (open/wip/done counts)
tp blocked                     # Tasks waiting on unsatisfied deps
tp graph                       # Dependency tree (--tag, --from)
tp stats                       # Parallelism analysis
tp report                      # Per-task duration + estimation accuracy
tp export                      # One task file's read model as JSON: spec sections, tasks with state, loops, tp resume's view (--compact)
tp export --list               # One row per task file of the repository: phase, tasks, review and audit loops, next step
tp dashboard                   # Serve the read-only dashboard on http://127.0.0.1:7377 until interrupted (--port N)

Spec & Validation

tp lint spec.md                # Spec quality + structured element detection
tp ground spec.md              # Check the spec's claims against the world (--units, --record, --status --check)
tp review spec.md              # Review prompts, rounds, merge/resolve/record/status/rewritten
tp review spec.md --role NAME  # Emit one role's prompt only (also on tp audit)
tp audit spec.md               # Post-implementation: verify the code matches the spec
tp validate                    # Task file + coverage + atomicity (--strict, --project)
tp epic lint                   # Lint the epic graph in .tp/epics.json (exit 1 on an error, 3 with no file)
tp epic export                 # The plan as versioned JSON: critical path, graph, derived status, bound files' loops, review slices (--md: tables)
tp epic add '<json>'           # Add an epic (--stdin, --bulk); a write adding a lint error exits 1, writes nothing
tp epic set <id> field=value   # Set an epic field, milestones|opens_after|gates|tag_checks=<json> whole, or gates.<m>.<field>=<value>
tp epic remove <id>            # Remove an epic; one depended on needs --force, a tag_checks provider is refused
tp epic link <from> <to> <why> # Add one edge, or reword its why
tp epic unlink <from> <to>     # Remove one edge; every writer takes --if-revision <sha256 of the file> and prints revision
tp epic apply <ndjson>         # add/link/unlink/set/remove rows in order as one write, linting only the end state (--dry-run)
tp epic milestone add <name>   # Insert a milestone --after|--before <m>, or open a --new-track with it
tp epic gate <m>               # A milestone's gate: members green or red, deferrable apart, threshold; exit 0 only when green
tp epic scope <m>              # What a gate pulls in: closure per milestone, outside, removal shrink, members holding its opening, fingerprint
tp epic done <id> --evidence e # Record an epic with no bound file done (--undo removes it); a condition needs its acceptance
tp epic claim <id> --by <name> # Reserve an epic (--release frees it); refused when claimed, on a condition, a done or a bound epic
tp epic ready                  # Work epics that can start, ranked by downstream; claimed ones and actionable conditions apart (--all)
tp epic status                 # Critical path; per milestone: open, waiting_on, counts, gate, ready, blocked with held_by (--milestone, --open, --compact)
tp epic import <file>          # Replace the graph with a file (--force over one holding epics); reports its lint findings
tp epic review --epic <id>     # One prompt per graph role over a slice (or --milestone <m>, --epics <ids> [--name]; --role narrows)
tp epic review <slice> --record-inbox # Record the round from the role files its emission named, each row stamped with its role
tp epic review <slice> --discard # Remove an unrecorded emission and its role files, emitting nothing (the operator's decision)
tp epic review --all           # Every milestone's round in one payload (or --milestones <m,m>; --status reads every slice)
tp epic review --report --all  # Each finding class with the slices, rounds and distinct epics it was raised on
tp epic review --epic <id> --history # A slice's recorded rounds with findings by role and the graph diff between them (--from/--to; --of-epic <id>: one epic's edges across every slice)

Data

tp init spec.md                # Create the task file shell (--eject-roles, --epic <id> binds it to an epic)
tp add <json>                  # Add a task (--stdin, --bulk)
tp import file.json            # Import + validate (--force, --spec)
tp use spec.tasks.json         # Set the active task file (--clear)
tp config                      # Effective project configuration (--resolved, --extract)

Global Flags

--file, --json, --compact, --quiet, --no-color and their negations apply to every command; SKILL.md documents what each one does.

Project Configuration

Multi-spec repos share one workflow policy instead of copying it into every *.tasks.json — so an agent working across specs reads a single source of truth and cannot silently drift. A repo-root .tp/ directory holds it: .tp/config.json carries the shared workflow defaults (commit it), .tp/local.json carries per-checkout state — the active task-file pointer, CLI flag defaults, and tp run's notify_cmd, which is per-operator rather than per-project — and is git-ignored automatically, along with the run artifacts (run-*.json, runs/, rounds/, locks/, last_failure-*.json) and the epic review inboxes (epic-review/*/inbox/). Effective values resolve at read time, so a task file's workflow block holds only explicit overrides.

The layers, the tp config / tp set --project / tp set --local forms and the task-file discovery order are in REFERENCE.md.

Task File Format

Tasks live in a JSON file alongside the spec:

spec/
  my-feature.md              # spec (source of truth)
  my-feature.tasks.json      # tasks (derived, git-tracked)

Each task is atomic — one commit, one verb, ≤15 minutes:

{
  "id": "create-model",
  "title": "Create User model",
  "status": "open",
  "estimate_minutes": 8,
  "acceptance": "Model exists. Migration runs.",
  "depends_on": [],
  "source_sections": ["### User Model"],
  "source_lines": "15-42"
}

Status is three values and no more — open, wip, done (internal/model/task.go). Nothing writes a fourth:

stateDiagram-v2
    [*] --> open
    open --> wip: tp claim, tp next
    wip --> open: tp unclaim
    wip --> done: tp done, tp close
    done --> open: tp reopen
    done --> [*]

    note right of open
        blocked is not a fourth state.
        tp blocked computes it from depends_on,
        so it can never disagree with the graph.
    end note
Loading

The file's workflow block carries the gate and the convergence policy. Every field with its type, default and range, how acceptance criteria are counted, and the JSON field aliases are in REFERENCE.md; how to write source_sections is in SKILL.md.

Closure Verification

tp refuses a lazy close, deterministically and language-agnostically: a task with N ≥ 2 acceptance criteria needs N top-level evidence lines, "deferred"-style reasons are rejected, and a trailing Out of scope: line is accepted. When workflow.quality_gate is set, tp done/tp close run it automatically and a failing gate blocks the close.

The exact rules, the close recipes per commit_strategy, and the user-approval escape hatches are in SKILL.md.

Reset-Native Workflow

tp's user is an AI agent whose context degrades over long runs, so every unit is meant to run in a fresh context, with tp as the durable state machine between resets. tp resume reports the lifecycle phase and the single next action from durable state alone, and tp brief / tp next --brief hands a fresh unit everything it needs: identity, scope fence, prior work, verbatim acceptance, and the close recipe for the effective commit_strategy.

tp run drives that loop unattended. It reads the cycle, spawns one runner process per unit — the two role kinds concurrently, everything else alone — re-reads the state from disk, checks its caps, and repeats. A unit's result is whatever it wrote to disk: tp reads a child's exit code and one spend number, never its prose. The run exits 0 only when the cycle converged and 4 on every other stop reason, so a caller never has to read the output to know what happened. Units run with TP_UNATTENDED=1, under which the decisions reserved for a human — skipping the quality gate, raising a round or run cap, forcing an import, relaxing the audit convergence policy, accepting a blocking finding, discarding an emission — fail closed; a unit records what it needs decided with tp escalate and the run stops for the operator. Which runner to spawn, the caps, and the notification command are configuration, so the loop stays runtime-neutral: built-in templates for claude and opencode, and a runner object for anything else.

The loop, the unattended restrictions and the briefing duty are in SKILL.md; the unit kinds, stop reasons, run state, child environment, commit_strategy and the keep-list are in REFERENCE.md.

Spec Quality

tp lint reports a spec's structured elements (tables, numbered lists, code blocks) — the decomposition checklist — and its quality issues:

tp lint spec.md --json | jq '.findings[] | select(.rule)'

Every rule identifier tp can put in a findings[].rule field, and the command that emits it:

Rule Emitted by Severity What it checks
heading-hierarchy tp lint error A heading skips a level (e.g. ## straight to ####)
empty-section tp lint error A leaf heading with no body content (container headings — whose next heading is deeper — are skipped)
duplicate-heading tp lint error Two headings with identical text under the same parent
orphan-reference tp lint error [text](#anchor) whose anchor matches no heading
frontmatter tp lint error/warning tp: frontmatter that is unclosed or unparseable (error), or whose shape is wrong or that carries an unknown key under tp: (warning); tp review and tp audit print the same on stderr
section-size tp lint warning A section longer than 50 lines — consider splitting
long-spec tp lint info Spec longer than 500 lines — consider modular sub-specs
vague-language tp lint warning Vague wording: appropriate, relevant, as needed, etc., various, some, proper, properly
duplicate-line tp lint warning Consecutive identical non-empty lines (edit artifacts)
duplicate-paragraph tp lint warning Two consecutive identical paragraphs — copy-paste artifacts the line-level check misses
numbering-gap tp lint warning Gaps in numbered section headings (e.g., 4.1 → 4.3, missing 4.2)
orphan-list-item tp lint info Numbered lists starting at >1 or with gaps (e.g., 1, 3 — missing 2)
broken-cross-ref tp lint warning §X.Y step N where section X.Y has fewer than N numbered steps
structured-elements tp lint info Tables, numbered lists, code blocks in spec
acceptance-quality tp lint warning/info Task acceptance describing removal without final state or using a vague completion verb (warning), or shorter than 10 words (info)
affected-files-scope tp lint warning Modify rows in affected files table without scope description
schema tp validate error/warning Missing or invalid required task field, or a touches glob that is empty, absolute, escapes the repository or does not parse (error); out-of-range workflow value, clamped at resolution (warning)
atomicity tp validate warning estimate_minutes outside 1–15, title over 8 words or with a conjunction, over 2 source_sections, description over 300 chars
self-dependency tp validate error A task that depends on itself
dangling-reference tp validate error depends_on names a task id that does not exist
circular-dependency tp validate error A dependency cycle between tasks
duplicate-id tp validate error Two tasks share an id
coverage tp validate error/warning Wrong total_sections, unmapped sections, coverage arithmetic that does not add up (error); spec missing or unparseable (warning)
section-anchor tp validate warning A source_sections entry that matches no heading, or is ambiguous between several
touches tp validate warning An open task without touches while the effective run_max_parallel is above 1: it never runs beside another task under tp run
line-coverage tp validate warning/info Uncovered spec lines, unusable or invalid source_lines (warning); the "…and N more gap(s)" continuation (info)
any-of-shape tp epic lint error A depends_on entry carries both or neither of id and any_of, an any-of group has fewer than two distinct members or sits beside a plain edge to one of its members, or two groups of one epic have one member set
cycle tp epic lint error The epic dependency graph has a cycle, an edge to any member of an any-of group counting; one finding per strongly connected component
dangling-reference tp epic lint error, or warning for a forbidden_edges tag selector no epic carries A depends_on id or any-of member, a gate member or deferrable, or a provided_by names no epic; a gates key names no milestone; a forbidden_edges epic, milestone or track_of selector names nothing the graph holds (error), or its tag selector names a tag no epic carries (warning)
duplicate-name tp epic lint error An epic id or a milestone name appears twice
unknown-key tp epic lint error A key outside the epic graph file's schema
missing-why tp epic lint error An edge, an any-of group or a forbidden_edges rule has no why, or one that is empty once trimmed
forbidden-dependency tp epic lint error A direct edge, or an any-of member, from an epic a forbidden_edges rule's from selects to one its to selects (selectors: epic, milestone, tag, or track_of, the track holding the named milestone); one finding per edge, naming both epics and the rule's why. Transitive paths are not checked
unknown-milestone tp epic lint error An epic's milestone is absent or in no track, or opens_after names a milestone in no track
later-milestone-dependency tp epic lint error An epic depends on one in a later milestone of the same track; edges across tracks are free. An any_of group is reported only when every member is in a later milestone of the track
gate-member-outside-milestone tp epic lint error A gate member or deferrable epic is not in that milestone
gate-member-deferrable tp epic lint error A gate lists an epic both in members and in deferrable
unprovided-tag-check tp epic lint error An epic carries a tag whose provided_by is not in its transitive dependencies, or, under "direct": true, not a plain direct dependency; the provider itself is exempt. A provider counts through an any-of group only when every member is it or depends on it, and never under direct
opens-after-cycle tp epic lint error The milestones' opens_after, track defaults applied, has a cycle (a self-reference too)
opens-after-not-list tp epic lint error An opens_after value is null or not a list, which only a hand edit writes (tp epic set and tp epic import refuse it)
dependency-opens-later tp epic lint error, or warning when no gate member reaches the epic Milestones wait on each other in a cycle: an opening waits on the gates of the milestones it opens after, transitively, and a gate on the opening of each milestone an undone epic of which an effective member reaches through not-done epics, so no gate on it can go green (error, one per cycle, naming the milestones in order and each reach's path); only an epic outside a gate's members reaches an epic of a milestone opening after it, and it can finish once that milestone opens (warning). An any_of group a gate member reaches is an error when no member can be done before that gate goes green, each through its own opening and through other gates, one per epic and group. A plain edge later-milestone-dependency reports is left to that rule
deferrable-required tp epic lint warning A gate's deferrable epic that an effective member reaches through plain edges and epics without a done record, so the deferral changes nothing; one finding per gate and deferrable, naming every direct requirer (counted past three) and one member path as the example (m -> x -> d). An epic listed both as member and as deferrable is gate-member-deferrable's, and a deferrable reached only through an any_of alternative is not reported
no-dependencies tp epic lint warning An epic outside the first milestone of its track has no dependencies, an any-of group counting as one, and is neither marked root: true nor a condition
root-with-dependencies tp epic lint warning An epic marked root: true has dependencies, an any-of group counting as one
thin-why tp epic lint warning A plain edge's why, case-folded and split into words, is non-empty and occurs as a run of whole words in the target epic's id or title; any-of groups are not checked
gate-threshold-unwritten tp epic lint warning A gate with no met record carries a threshold that is a placeholder: the trimmed text matches .tp/config.json's epic.threshold_placeholder, or, when that is unset, the default: the whole text one bracketed or angle-bracketed run, or TBD, TODO or TBC in any case ([threshold], <exit condition>, tbd; the exact pattern is in REFERENCE.md); one finding per gate, in milestone name order. An absent or blank threshold is not a placeholder. Writers do not refuse it
gate-empty tp epic lint warning A milestone holding epics whose gate has no effective members and no threshold while a milestone opens after it, say one holding only a condition with no gates entry: the gate is green at once, so what opens after it waits on nothing; one finding per milestone, naming its epics and every milestone opening after it, and offering its members, a threshold, or a trigger condition moved into the milestone that opens after it. A milestone with no epics, or whose epics are all conditions with a done record (a trigger that has fired), is not reported
partial-parallel tp epic lint warning, or info when the milestone's opens_after entry is written A milestone does not open after an earlier milestone of its track, even transitively, yet some of its epics depend directly on that milestone's epics, or on an any_of group whose every member is one; one finding per pair, naming them
gate-held-by-condition tp epic lint info Effective gate members reach an undone condition of another milestone through epics without a done record; one finding per gate milestone and condition, naming every member that reaches it, with one path. An any_of group is reported, as "one of {A, B}", only when every member is such a condition. A condition in the gate's own milestone is not reported

tp import runs the same task-file checks as tp validate, so it emits that half of the table too.

tp ground is the step before review, and the release that made tp 1.0. tp lint checks the document's form, tp validate checks the plan against the spec, tp audit checks the code against the spec — and until then nothing checked the spec against the world. Review reads the opposite instruction: every role is handed "the spec content above is complete and authoritative".

tp ground emits one prompt over a mechanically derived floor of the spec's own sentences and asks for a disposition for each — PASS, PARTIAL, FAIL, UNVERIFIABLE, QUESTION or NOT-A-CLAIM, with the kind of claim and the tier of evidence actually reached. A PASS whose tier says nothing about that kind of claim, such as a behavioural claim confirmed by reading rather than by running, is rejected at record. Convergence here is coverage, not absence: tp ground <spec> --status --check exits 0 when every emitted floor unit carries a disposition and the latest round holds no FAIL. A document whose every sentence the derivation's arms dropped exits 1 rather than certifying itself, and --status reports the cut count that decides it beside the per-verdict breakdown. From the second round a disposition carries forward for every unit whose text has not moved, so a repaired spec is re-grounded where it changed rather than from scratch.

tp review is told, not stopped: its envelope carries an ungrounded key while any floor unit is undispositioned, and its exit code is the same with it and without it. The loop is in SKILL.md; the row schema, the kind-tier sets and the exit codes are in REFERENCE.md.

Writing the spec. The body a round grades is a decision record: decisions, why, non-goals, and test rows that name the mutant that would make them red. Measurements, derivations and the history of how a decision was reached live beside it in <base>-measurements.md, which no round grades and which the body cites instead of quoting a figure — a figure copied into a spec is a claim every grader re-derives, and that finding class was measured to be the most expensive and the least valuable. A sentence that would have to change if the implementation changed is not spec either; it belongs to the task that builds it. The rules are Step 0.5 of SKILL.md. References rot silently, so this repository registers a code-citation-drift entry in .tp/config.json's workflow.checks and tp review runs it every round.

What grounding finds. Over this repository's own corpus, rows come back FAIL and PARTIAL that a document review structurally cannot reach. Grounding reaches them because it is allowed to build and run things: rows settled at run, probe, red-green or break-and-control rather than by reading. Two field reports from unrelated repositories — different languages, different domains — converged on the same shape: what failed was rarely a domain rule, it was the author's own measurement, quoted from a query they had run themselves, which is exactly why it read as authoritative. The counts behind this are derived from this repository's round files in docs/lessons.md.

tp review generates the review prompts an agent feeds to sub-agents, records each round, and makes convergence a recorded fact rather than a judgement. Reviewers report only what would make the implementation wrong; detail the code will settle is left to its tests. Roles are project-owned files, per-spec focus comes from the spec's tp: frontmatter, and a recurring finding class can be mechanized into a check. The round-by-round recipe is in SKILL.md; the roles, frontmatter and finding contract are in REFERENCE.md.

tp lint also reports what a round-1 grading of the spec will read, and makes no judgement about it — no threshold, no warning, no gate. Three keys sit beside structured_elements on every invocation:

Key What it reports
floor_size The spec's uncut floor units — the sentences a grading round reads. On a spec carrying no frontmatter, equal to the line count of tp ground <spec> --units; lint counts the frontmatter-blanked text and --units counts the raw bytes, so a tp: block can become a unit of its own
cut The floor-index rows the derivation's arms dropped, so a small floor is not read as a cheap round when it is a narrow one
review_panel The reviewer role ids a round-1 tp review <spec> would emit, resolved without emitting a round

cut is the floor index's row count less floor_size, so it needs an index an emission freezes — tp ground <spec> --status refuses on a spec with no emitted round. review_panel is resolved without emitting one: tp review gets it by emitting the round, and tp resume gets it from the same resolver far more cheaply. floor_size is the number tp ground <spec> --units already gives without writing anything, moved to the moment a spec is being written rather than after a round exists. tp now spells floor_size in three payloads that count the same quantity over different text; which text each one reads is in REFERENCE.md.

tp validate checks line coverage — verifying that task source_lines cover the entire spec:

tp validate --json | jq .checks.line_coverage

Post-Implementation Audit

tp audit verifies that the spec's requirements actually made it into the code. It emits one prompt per auditor role over a spec-derived checklist and the affected files, records each round, and reports whether the implementation diverges from the spec or the general lenses are simply reading the rest of the repository. What a round must be clean of is audit_converge_on: the default all counts every non-PASS row, and blocking — which stops advisory rows from holding a phase open — is opt-in, human-only, and fenced at all four of its write paths under TP_UNATTENDED. An evidenced wontfix/duplicate clears its round, and accepting a finding without a code change is the operator's decision. The round-by-round recipe is in SKILL.md; the fields and the audit JSON schema are in REFERENCE.md.

Epic Graph

One spec covers one feature. A project whose plan is hundreds of features with dependencies between them keeps that plan in .tp/epics.json: ordered milestone tracks, each milestone's exit gate, and epics whose depends_on edges each carry a required why. One epic is one node of the plan and owns the spec written for it; the graph is designed and reviewed the way a spec is.

  • Written only through tp. tp epic add|set|remove|import, link|unlink for one edge and milestone add lock the file and refuse a write that adds a lint error the file did not already hold, so the graph is never left broken by a write; the plugin's write-deny hook fences the file against direct edits. Every writer prints the file's revision (the SHA-256 of its bytes) and takes --if-revision, so an edit made from a stale read is refused instead of reverting another.
  • Linted. tp epic lint checks the structure (cycles, dangling ids, an epic depending on a later milestone, a gate member outside its milestone) and the one semantic gap with a structure: an epic tagged for a checker it does not transitively depend on. Warnings flag a why that only restates its target and an epic outside a track's first milestone with no dependencies, unless it is marked root: true.
  • Reviewed in slices. tp epic review --milestone <m>, --epic <id> or --epics <ids> --name <topic> (a topic across milestones, one slice as it grows) emits one prompt per graph role over the asked epics, with their neighbours as context, and runs the spec loop's rules: record, carry unchanged units, converge, cap. The built-in roles ask whether an epic's definition of done names a capability none of its dependencies provides (dod-satisfiability), whether each why is a reason (why-auditor), whether an epic sits where the plan needs it (phase-economist), and whether a gate taken whole is the work its threshold and purpose need, missing and surplus both (gate-scope); regression joins a round that follows a change.
  • Bound to the work. A task file names its epic (tp init <spec> --epic <id>, tp set --epic <id>), and the epic's status is derived from its bound files whenever it is read: planned, claimed, specced, in_progress, and done once every bound file reaches release. Nothing is stored that the task layer could contradict; an epic finished outside tp, with no file, is closed by tp epic done <id> --evidence.
  • Gates open milestones. A milestone is open when every milestone it opens after is open and has a green gate: every member done and, when the gate carries a threshold, the operator's evidenced tp epic gate <m> --met record. By default a milestone opens after the previous one of its track; opens_after names other milestones, of any track, or [] for siblings that are always open. Only an open milestone's epics are ready, and deferrable epics never hold a gate.
  • Conditions. An external precondition is an epic with condition: true, its acceptance saying what it waits for: no task file, no claim, done only by an evidenced record. Work that must wait for it depends on it like any edge. The operator is asked for a condition only once it is actionable: it holds an open milestone's gate, or some epic waits on it and on nothing else undone; otherwise not_actionable says why.
  • The graph gates the work. While a bound file's epic has a dependency not done, tp next, tp ready and tp claim offer no task and name blocked_by_epics, and tp resume reports epic-blocked, so tp run stops instead of driving a unit that cannot move; a wip task still finishes. When only conditions block it, tp next and tp resume name the operator's tp epic done <c> --evidence.
  • Read as one contract. tp epic ready says what can start, ranked by the not-done work that waits on each epic (downstream), and who holds a claim; tp epic status adds the graph's critical path and, per milestone, the gate, what it opens after and waits on, the blocked list, throughput and the estimate roll-up; tp epic export prints all of it as versioned JSON, with each bound file's phase and loop states and every review slice, so a view of the plan never parses tp's state files. --md keeps the tables; tp renders no map.

The loop is Workflow E in SKILL.md; the file, the lint rules, the prompt shape, the state directory, the status table and the payloads are in REFERENCE.md. tp measures nothing to decide any of it: a threshold is met when the operator records it.

AX (Agent Experience)

tp is designed for AI agents first (AX), not humans (DX):

Principle How
Minimal tokens --minimal ~80%, --compact ~40% smaller. 2-call architecture saves ~90%
Batch parity tp claim --all-ready, tp done --batch, tp set --bulk
Dependency-aware batch tp done --batch auto-toposorts by in-batch deps — no manual ordering needed
Actionable errors Every error includes hint field with recovery action
Did-you-mean --covered-by typos suggest similar task IDs
Structured commits tp commit generates conventional commit messages with task metadata
Implicit claim tp done and tp commit auto-claim open tasks
WIP resume tp next returns existing WIP task (crash recovery)
Covered-by Close tasks covered by other tasks without duplicate work
Auto-normalize source_lines accepts "72" (normalized to "72-72")
Import flexibility tp import accepts bare JSON arrays with --spec flag
Spec-only review Review prompts include disclaimer to prevent code-checking
Edit hygiene lint tp lint detects duplicate lines/paragraphs, numbering gaps, and broken cross-refs
Estimation calibration tp add warns when historical estimates are consistently high
Duration tracking tp report shows per-task timing and estimation accuracy
Entry validation tp add rejects bad tasks at entry (no id/title/acceptance/anchor), normalizes slices to []
Coverage on write tp add/set/remove recompute coverage, so init+add+validate is clean
Audit file hint tp audit suggests files from done tasks' commits when none are detected
Loop budget --status shows max_rounds/rounds_remaining/in_flight_round; at the cap (default 3) a loop ends once every finding carries a disposition, reported as done_by: cap
Divergence signal tp audit --status/--record report role_streaks, spec_coverage_clean_rounds and a divergence object
Candidate retirement a registered check that ran retires its mechanize candidate; mechanized_classes names what was withheld
Unattended run tp run drives the whole cycle; exit 0 means converged, exit 4 names one of the other eight stop reasons
Fail-closed decisions under TP_UNATTENDED the user-only decisions exit 2 and tp escalate records what needs deciding
Audit convergence policy audit_converge_on (default all) decides what an audit round must be clean of; blocking is human-only, fenced at all four write paths
Accepted rows are visible tp audit --merge breaks the round's non-PASS rows down as by_severity, and next_action names the accepted count
Honest merges --merge reports inputs per file and exits 1 when a role's whole file failed to parse
One prompt per unit tp review/tp audit --role <name> emit a single role's prompt, so a lost sub-agent costs one role, not the round
Honest audits file_summary.truncated is true whenever a cap cut what a role reads — the 50-file auto-detect cap or a role's own list cap — and notices names each cut (it survives --compact and --quiet); a recorded round says what each code role graded of its pool, and spec_truncated names a spec cut at 10,000 bytes
Evidence at record every finding row needs severity, finding, location and evidence, each non-empty after strings.TrimSpace; --record refuses the whole file and names every offending line, --merge drops the row and counts it under inputs[].skipped
A refused merge writes nothing tp review --merge and tp audit --merge decline the -o write when an input parsed nothing, so a refused merge leaves no file there and an existing one byte-identical, and --record then exits 3 on the missing input

Claude Code Integration

tp ships a Claude Code skill via the Agent Skills standard — installed and updated with the npx skills commands under Install. The skill teaches the agent the 2-call workflow, decomposition rules, NDJSON format, closure verification, and commit conventions.

The repository is also a Claude Code plugin: .claude-plugin/plugin.json beside the marketplace manifest, that same skills/tp, plus hooks/ and agents/. The binary is not inside it — a marketplace is a git repository, so installation stays Homebrew or go install, and the SessionStart hook preflights tp's presence and version and fails with the install command rather than degrading quietly. It also injects tp resume --compact as orientation. A PreToolUse hook denies hand-writes to tp's own state — .tp-review/ contents, *.tasks.json, .tp/config.json, .tp/local.json — and a Stop hook refuses a review or audit role unit's stop, once, while its findings file is still missing and it has written no escalation record. agents/ declares tp-implementer, tp-reviewer and tp-auditor, carrying tool restrictions only: a role's content lives in the corpus and reaches the unit through the prompt tp emits. Every hook is bounded by a timeout of 10 seconds.

Research

tp's design is backed by:

Finding Source
<15 min tasks = 70%+ success SWE-bench
ACI design: 3-4x improvement SWE-agent, Princeton
Planning: 9.85% → 57.58% success Plan-and-Act
100:1 input-to-output token ratio Manus
64% token reduction with upfront planning ReWOO

See spec/0.1.0.md for the full specification with 22 research references.

License

MIT

About

Spec-to-task lifecycle manager for AI coding agents. Decomposes specs into atomic, dependency-ordered tasks. Agents execute with 2 tool calls instead of hundreds.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages