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.
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).
# 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 -gThe 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@tpUpdate it with:
claude plugin update tp@tpIt 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# 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-committp 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
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
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.
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.
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 unittp next # Resume WIP or claim the next ready task (--minimal, --peek, --brief)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)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)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)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)--file, --json, --compact, --quiet, --no-color and their negations apply to every command;
SKILL.md documents what each one does.
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.
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
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.
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.
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.
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_coveragetp 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.
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|unlinkfor one edge andmilestone addlock 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 lintchecks 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 awhythat only restates its target and an epic outside a track's first milestone with no dependencies, unless it is markedroot: 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 eachwhyis 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);regressionjoins 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, anddoneonce every bound file reachesrelease. Nothing is stored that the task layer could contradict; an epic finished outside tp, with no file, is closed bytp 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 evidencedtp epic gate <m> --metrecord. By default a milestone opens after the previous one of its track;opens_afternames other milestones, of any track, or[]for siblings that are always open. Only an open milestone's epics are ready, anddeferrableepics never hold a gate. - Conditions. An external precondition is an epic with
condition: true, itsacceptancesaying 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 isactionable: it holds an open milestone's gate, or some epic waits on it and on nothing else undone; otherwisenot_actionablesays why. - The graph gates the work. While a bound file's epic has a dependency not done,
tp next,tp readyandtp claimoffer no task and nameblocked_by_epics, andtp resumereportsepic-blocked, sotp runstops instead of driving a unit that cannot move; awiptask still finishes. When only conditions block it,tp nextandtp resumename the operator'stp epic done <c> --evidence. - Read as one contract.
tp epic readysays what can start, ranked by the not-done work that waits on each epic (downstream), and who holds a claim;tp epic statusadds 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 exportprints 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.--mdkeeps 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.
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 |
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.
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.
MIT