Pipeline orchestration engine for multi-agent LLM workflows. Define pipelines in .dip files (Dippin, our pipeline DSL), execute them with parallel agents, and watch progress in a TUI dashboard.
Built by 2389.ai. Docs, walkthroughs & the TUI in action →
Install — Homebrew (macOS/Linux) or go install:
brew install 2389-research/tap/tracker
# or: go install github.com/2389-research/tracker/cmd/tracker@latestYour first run, on your Claude subscription — no API key, no .dip file. If you already use Claude Code (claude installed and signed in), cd into any repo:
tracker explain --param question="what does this repo do?"backend: auto → claude-code (claude 2.x at /opt/homebrew/bin/claude; no API key needed)
… Start → Answer → WriteAnswer → Done, the answer streamed in the activity log …
answer saved to .tracker/runs/<id>/explain.md
next: make a small change with
tracker quickfix --param change="<one sentence>"
explain only reads (it can't change a file). Then make one small change:
tracker quickfix --param change="fix the typo in the README"The change is made in a separate git worktree on its own branch, tracker/quickfix/<run-id>: Tracker never changes your checkout, and the review gate warns if anything outside the worktree changed while the agent worked. Tracker runs your tests and shows the diff: approve commits it to that branch (nothing is merged or pushed), revise tries again with your feedback, reject throws it away.
No Claude Code? Install it (npm i -g @anthropic-ai/claude-code && claude /login), or add an API key with tracker setup — the same two commands then run on the key. Leave off --param to be asked instead. If something doesn't work, tracker doctor checks your setup and tracker diagnose explains the last run.
From there:
- Bring a spec —
tracker init build_productscaffoldsbuild_product.dip+ a starterSPEC.md; edit the spec, thentracker build_product. - Hands-off — add
--autopilot midto replace human gates with an LLM judge (or--auto-approvefor deterministic approval). - Drive it elsewhere — from Slack (
trackerbot) or your terminal as a REPL (trackerchat). - Explore —
tracker workflows(list built-ins),tracker validate <wf>,tracker simulate <wf>,tracker estimate <wf>,tracker setup,tracker --help.
Release history has moved out of this README — see CHANGELOG.md for what's new in each version, and the Roadmap for what's next.
Six pipelines are embedded in the binary and available via tracker workflows. Start with the first two:
Answer one question about the repo you're in, read-only: tracker explain --param question="what does this repo do?". Runs on your Claude subscription when claude is installed (backend: auto, no API key needed), otherwise on an API key, and saves the answer to .tracker/runs/<id>/explain.md.
Make one small change on its own branch: tracker quickfix --param change="<one sentence>". The change happens in a separate git worktree on tracker/quickfix/<run-id>, the project's tests run, and you approve (commit to that branch; nothing is merged or pushed), revise (up to 3 times) or reject (worktree and branch removed). Tracker never changes your checkout; the agent is told to stay in its worktree, and the review gate warns if anything outside it changed.
Competitive implementation: ask the user what to build, fan out to 3 agents (Claude/Codex/Gemini) in isolated git worktrees, cross-critique the implementations, select the best one, apply it, clean up the rest.
Sequential milestone builder: read a SPEC.md, decompose into milestones, implement each with verification loops (opus-powered fix agent with 50 turns), cross-review the complete result, verify full spec compliance. Context-specific escalation gates let you override flaky tests or skip milestones without aborting the build.
graph LR
ReadSpec --> Decompose --> ApprovePlan
ApprovePlan -->|approve| PickNext
PickNext -->|milestone N| Implement --> Test
Test -->|pass| Verify --> MarkDone --> PickNext
Test -->|fail| Fix --> Test
Test -->|escalate| EscalateMilestone
EscalateMilestone -->|mark done| MarkDone
EscalateMilestone -->|retry| Implement
PickNext -->|all done| CrossReview --> FinalBuild --> FinalSpec --> Cleanup --> Done
Environment bootstrap and the deny-by-default test gate. Right after Setup, EnsureEnv runs the first found of build-setup.sh / .build/setup.sh / scripts/build-setup.sh — a hook the seed repo or operator ships to pin toolchains and dependencies (a .venv, npm ci, go mod download) — before any agent runs; no hook is fine, a failing hook stops the run at AbortRun with the hook's log replayed (.tracker/env-bootstrap.log / .status), so milestones never build against a partial environment. TestMilestone is then green only when a real oracle ran — a suite that executed a positive number of tests, or a project Makefile ci/check/lint/test target. In milestone mode the Go and Rust suites are scoped to what the milestone changed since its start SHA in the worktree (uncommitted edits, untracked files and deletions included): go test runs the changed packages plus their reverse dependents, and cargo test -p <crate> runs the crates owning the changed .rs files (nearest ancestor Cargo.toml with a [package]; a change that resolves to no crate, or no changed .rs at all, tests the whole workspace) — so a milestone that authored no test of its own cannot green on a prior milestone's passing suite; FinalBuild always runs the whole tree. A tree with no runnable oracle (a milestone-1 scaffold, a suite that ran zero tests, a pytest run that collected nothing) is neither green nor red: it routes to VerifyMilestone as tests-not-yet-verifiable and the verifier decides whether the milestone legitimately needs no executable verification. Language-native lint (vet, golangci-lint, tsc, eslint, ruff, mypy, clippy) always runs but is advisory — findings are printed with an ADVISORY: line and never fail a milestone; only a project-declared Makefile target blocks. A bootstrap hook that must reach a private registry needs TRACKER_PASS_ENV=1 exported in the shell that runs tracker (a project .env cannot set it — see the env trust policy): tool subprocesses (the hook included) run with *_TOKEN / *_API_KEY / *_SECRET / *_PASSWORD environment variables, DOCKER_AUTH_CONFIG and any variable whose value is a URL carrying credentials (an authenticated PIP_INDEX_URL or NPM_CONFIG_REGISTRY) stripped, so an npm ci or pip install against an authenticated index fails without it.
Contract tests. Each milestone in the plan names its **Contract tests** — the exact test names that prove its done-when (Go TestX / TestX/sub, Rust module::test_name, pytest path::test_name, a JS describe/it title), or none — <reason> for a genuinely test-free milestone. PickNextMilestone writes them to .ai/milestones/contract-tests; every verify.sh run rewrites .ai/build/executed-tests.txt with every test that actually executed (Go === RUN names including subtests, cargo's per-test lines, pytest's -rA summary, jest/vitest/mocha ✓/✕ titles when the reporter prints them); and TestMilestone reconciles the two after a green run — a declared test that never executed is an ordinary red (CONTRACT-TEST-MISSING) routed to FixMilestone, so a milestone that authored no test of its own can no longer go green on the prior suite. When the runner lists no names at all (jest's default reporter, vitest's per-file lines, pytest with its summary silenced, a Makefile-only oracle) the manifest says so (# names-unavailable) and an unprovable name is a WARNING … verifier decides rather than a red. VerifyMilestone cites the manifest line for each declared test (or corroborates an unprovable one from the test source) and fails an unjustified none.
Two empty marker files under .ai/build/ let the operator (never an agent) opt a repo out of a fail-closed check. Both survive Setup's per-plan state reset, ResetReviewBudget and Cleanup, so they persist across tracker -r resumes and EscalateVerification / EscalateReview → retry re-plans; remove the file to re-arm the check.
.ai/build/allow-dirty— dirty-tree preflight opt-out.Setuprefuses to start when the repository (the whole tree, not just the workdir) has uncommitted changes or untracked files, because the checkpoint commit after every milestone stages every tracked change and untracked file (only detected compiled binaries and secret-looking files such as.env/ private keys are left out), so stray WIP would be swept into the build's history and reviewed as milestone output. Safe when the tree is deliberately dirty — for instance a monorepo with unrelated in-flight work you accept being checkpointed, or a scratch repo. Create it before the run:mkdir -p .ai/build && touch .ai/build/allow-dirty..ai/build/no-tests-ok— ship-gate opt-out for a product with no test stack. Without it,FinalBuild(verify.sh --final) fails when no build system is detected anywhere in the tree (nogo.work/go.mod/package.json/pyproject.toml/Cargo.tomland no Makefileci/check/lint/testtarget) and routes toEscalateVerification(unattended defaultabandon: the run endsfailatAbortRun) — a product with no test runner cannot ship green. With it, the ship gate passes with aNOTEthat nothing was tested. Without it, milestone-modeTestMilestonereports such a tree astests-not-yet-verifiable(an early scaffolding/docs milestone legitimately has no runner;VerifyMilestoneconfirms the milestone's done-when needs no tests); with it, the milestone gate passes with the sameNOTE. The operator must create it before the run:PickNextMilestonesnapshots the hatch files at the start of every milestone, and a stamp that appears mid-milestone is reported byTestMilestone/FinalBuildas agent-created (+ .ai/build/no-tests-ok) and treated as a finding. Agents are instructed never to create either stamp.
Parallel stream execution for large structured specs: reads the spec's work streams and dependency graph, executes independent streams in parallel (with git worktree isolation), enforces quality gates between phases, cross-reviews with 3 specialized reviewers (architect/QA/product), and audits traceability.
Start with build_product — it is the default for a normal SPEC.md. It builds one milestone at a time in a single working tree, which keeps the run easy to follow and cheaper to escalate. Reach for build_product_with_superspec only when the spec is large enough to name independent work streams and a dependency graph the engine can parallelize; on a small or single-track spec its extra machinery is overhead with no payoff.
Both run the same SpecLint spec-coherence preflight before any decomposition (dangling refs, contradictory constants, contract/signature mismatch, unassignable mandated tests — fail-closed). Everything below is superspec-only:
- Committed scaffold first: after the plan is approved,
CommitScaffoldcommitsSPEC.md,docs/execution-plan.md,docs/traceability.yamland.gitignoreby name, so every stream worktree (forked fromHEAD) sees them. - Parallel work streams dispatched from the spec's stream/dependency graph, each in an isolated git worktree on a
build/<stream>branch, instead of a sequential milestone loop. A previous run's unmergedbuild/<stream>branch is renamedbuild/<stream>-abandoned-<sha>, never deleted. - Per-stream traceability overlays: a stream never edits
docs/traceability.yaml; it writesdocs/traceability.<stream>.yaml(only the requirement IDs it covers, in the master's flat one-line-per-requirement format) and each phase merge folds the overlays into the master — no add/add conflicts on one shared file. - Per-phase mechanical quality gates — the same
verify.shgreen-gate asbuild_product(every stack anywhere in the tree, Makefile target and language-native lint/vet), plus coverage and complexity as report-only evidence — between stream phases; mechanical, not LLM-judged. Because these gates are mechanical (there is no per-milestone verifier to hand the question to),verify.sh's NOT-YET-VERIFIABLE verdict (exit 3: no suite executed any test and no Makefile CI target ran) is a gate failure here — a greenfield phase that ships zero tests hard-fails atgate_verifyuntil the phase adds a runnable suite, or the operator stamps.ai/build/no-tests-okfor a genuinely test-free project. MergeConflictgate: a phase merge that conflicts is aborted with the conflicting paths listed, worktrees and branches untouched; the gate's unattended default is abandon (the run ends failed), and retry re-enters the same phase's merge after you resolve it by hand.- Three specialized cross-reviewers (architect / QA / product) rather than the base cross-review.
FinalGates+TraceabilityAudit:FinalGatesfails any requirement still pending or without animpl_ref, and any implemented requirement without atest_refunless waived indocs/traceability-waivers.txt(<ID> <reason>, one per line); theTraceabilityAuditgoal gate then verifies every spec requirement maps to real implementation and test coverage and challenges each waiver.
The two workflows are separate embedded files, not a base-plus-overlay; the shared SpecLint node is deliberately duplicated as a node declaration (built-in delivery cannot resolve subgraph file refs), but both declarations load the same prompts/build_product/SpecLint.md sidecar, and a parity test pins them byte-identical (issue #307).
Interview-driven codebase review: describe what you want reviewed, answer structured interview questions to scope the analysis, then three parallel agents analyze correctness, security, and design. A second interview presents findings for your context (is this intentional? known issue?), a third prioritizes remediation, and the pipeline produces an actionable remediation plan.
graph LR
DescribeGoal --> Explore --> ScopeInterview
ScopeInterview --> AnalyzeParallel
AnalyzeParallel --> Correctness & Security & Design
Correctness & Security & Design --> Join
Join --> Synthesize --> FindingsInterview
FindingsInterview --> PriorityInterview
PriorityInterview --> RemediationPlan --> ReviewPlan
ReviewPlan -->|approve| Finalize --> Done
ReviewPlan -->|revise| RemediationPlan
Pipelines are embedded in the binary so brew and go install users can run them without cloning the repo:
tracker workflows # List all built-in workflows (starters first)
tracker explain # Run directly by name — no files needed
tracker validate build_product # Validate works too
tracker simulate build_product # Simulate too
tracker init build_product # Copy .dip + scaffold a starter SPEC.md for editingbuild_product builds from a SPEC.md in the repo root; running it without one
exits with a pointer to tracker init build_product, which scaffolds both the
.dip and a starter SPEC.md so the first run succeeds.
Local .dip files always take precedence over built-ins. After tracker init build_product, running tracker build_product uses your local copy.
Pipelines are defined in .dip files using the Dippin language:
workflow MyPipeline
goal: "Build something great"
start: Begin
exit: Done
defaults
model: claude-sonnet-5
provider: anthropic
agent Begin
label: Start
human AskUser
label: "What should we build?"
mode: freeform
agent Implement
label: "Build It"
prompt: |
The user wants: ${ctx.human_response}
Implement it following the project's conventions.
agent Done
label: Done
edges
Begin -> AskUser
AskUser -> Implement
Implement -> Done
Workflows can declare what they need from the host environment with a requires: line in the header:
workflow BuildProduct
goal: "..."
requires: git
start: Start
exit: Done
Tracker checks these at startup (when you invoke tracker <workflow>). If the env doesn't satisfy them, the run fails in seconds with a copy-paste remediation instead of burning LLM spend before the first failure. Override per-run with --git=auto|off|warn|require|init (default auto respects requires:; --git=init --allow-init auto-initializes the workdir, with safety refusals for $HOME, /, and nested repos — including bare repos, linked worktrees, and submodules). v0.29.0 implements git; unrecognized entries warn and continue so workflow authors can forward-declare deps that future tracker versions will check.
| Type | Shape | Description |
|---|---|---|
agent |
box | LLM agent session (codergen) |
human |
hexagon | Human-in-the-loop gate (choice, freeform, or hybrid) |
tool |
parallelogram | Shell command execution |
parallel |
component | Fan-out to concurrent branches |
fan_in |
tripleoctagon | Join parallel branches |
subgraph |
tab | Execute a referenced sub-pipeline |
manager_loop |
house | Managed iteration loop |
conditional |
diamond | Condition-based routing |
Three namespaces for ${...} syntax in prompts:
${ctx.outcome}— runtime pipeline context (outcome, last_response, human_response, tool_stdout)${params.model}— workflow-levelvars(optionally overridden by--param key=valueat run time, v0.19.0) and subgraph parameters passed from a parent pipeline${graph.goal}— workflow-level attributes
Declare defaults in a top-level vars block and override them per-run:
workflow MyPipeline
vars
model: claude-sonnet-5
retries: 3
tracker --param model=claude-opus-5 --param retries=1 MyPipelineUnknown --param keys hard-fail at startup. Dippin-lang's lint (run automatically at .dip load) flags undeclared ${params.*} references and other variable-reference mistakes — see dippin doctor for the full lint catalog.
Variables are expanded in a single pass — resolved values are never re-scanned, preventing recursive expansion.
Important: Each agent node runs a fresh LLM session. Data flows between nodes via context keys, not conversation history. Per-node scoping (${ctx.node.<nodeID>.<key>}) lets you reference a specific earlier node's output without relying on the last-writer-wins last_response key. See Pipeline Context Flow for the full model, fidelity levels, and parallel-branch patterns.
edges
Check -> Pass when ctx.outcome = success
Check -> Fail when ctx.outcome = fail
Check -> Retry when ctx.outcome = retry
Gate -> Next when ctx.tool_stdout contains all-done
Gate -> Loop when ctx.tool_stdout not contains all-done
Supported operators: =, !=, contains, not contains, startswith, not startswith, endswith, not endswith, in, not in, &&, ||, not.
ctx.tool_stdout and ctx.tool_stderr capture the tail of a tool node's output (default cap 64KB per stream, configurable per-node via output_limit: 256KB; --max-output-limit is a hard global ceiling, default 10MB, that caps how high a per-node output_limit can go). Routing markers emitted at end-of-output via printf survive truncation by construction; tracker diagnose surfaces a tool_output_truncated suggestion when a stream was clipped so you know to raise the limit if the captured tail isn't what you expected.
Conditions support the ctx. namespace prefix (dippin convention) and internal.* references for engine-managed state.
Agent, tool, and mode: interview human nodes can declare the context keys they produce and consume (v0.21.0):
agent Planner
response_format: json_object
writes:
- milestone_id
- files
reads:
- spec_path
The node output must be a valid top-level JSON object; every declared key in writes: must be present or the node hard-fails. Extras are allowed (surfaced as warnings), strings are stored verbatim, non-string values are stored as compact JSON. reads: pins fidelity for upstream keys so downstream nodes see consistent data. See Pipeline Context Flow for the full contract, worked examples, and interview-mode semantics.
For git worktree isolation in parallel implementations:
agent ImplementClaude
working_dir: .ai/worktrees/claude
model: claude-sonnet-5
prompt: Implement the spec in this isolated worktree.
The working_dir attribute is validated against path traversal and shell metacharacters. A relative working_dir resolves under the run's workdir (-w), like tool nodes, not the tracker process's current directory.
Five gate modes:
- Choice mode (default): presents outgoing edge labels as a radio list. Arrow keys navigate, Enter selects.
- Freeform mode (
mode: freeform): captures text input. If the response matches an edge label (case-insensitive), it routes to that edge. Otherwise it's stored asctx.human_response. - Hybrid mode (automatic): when a freeform gate has labeled outgoing edges, the TUI presents a radio list of labels plus an "other" option for custom feedback. Selecting a label submits it directly; selecting "other" opens a textarea for specific instructions.
- Yes/No mode (
mode: yes_no): fixed two-option prompt. Yes maps toOutcomeSuccess, No maps toOutcomeFail— route withwhen ctx.outcome = success/when ctx.outcome = failedges. Distinct from choice mode, where the outcome is always success and routing usespreferred_label. - Interview mode (
mode: interview): structured multi-field form driven by upstream agent output. An agent generates markdown questions with inline options; the handler parses them into individual form fields and presents a fullscreen interview form. Answers are stored as JSON and markdown summary.
Long prompts with labels (e.g., escalation gates with agent output) automatically use a fullscreen review hybrid view: glamour-rendered scrollable viewport on top (PgUp/PgDn to scroll), radio label selection in the middle, and an "other" freeform option at the bottom for custom retry instructions. Long prompts without labels use a split-pane review: scrollable viewport on top, textarea on bottom.
human ApproveSpec
label: "Review the spec. Approve, refine, or reject."
mode: freeform
edges
ApproveSpec -> Build label: "approve"
ApproveSpec -> Revise label: "refine" restart: true
ApproveSpec -> Done label: "reject"
Interview gates let an agent generate structured questions that the user answers via a form:
human ScopeInterview
label: "Help us focus the review."
mode: interview
questions_key: interview_questions
answers_key: scope_answers
The upstream agent writes markdown questions to the questions_key context variable. The parser extracts:
- Numbered/bulleted questions ending in
?or imperative prompts ("Describe...", "List...") - Inline options from trailing parentheticals:
Auth model? (API key, OAuth, JWT)becomes a select field - Yes/no patterns detected automatically as confirm toggles
The TUI presents a fullscreen form with per-field navigation (arrow keys), pagination (PgUp/PgDn for 10+ questions), elaboration textareas (Tab), and pre-fill from previous answers on retry. Answers are stored as JSON at answers_key and as a markdown summary at human_response. If zero questions are parsed, the gate falls back to freeform. Cancellation returns outcome=fail.
A reusable interview loop pattern is available in examples/subgraphs/interview-loop.dip — embed it via subgraph nodes with topic and focus parameters.
Submit with Ctrl+S. Enter inserts newlines. Esc cancels (empty) or submits (with content). Ctrl+C cancels and unblocks the pipeline (no deadlock).
Tracker supports four LLM providers: anthropic, openai, gemini, and openai-compat (for any OpenAI-compatible API). Set up with:
# Interactive setup wizard
tracker setup
# Verify your configuration
tracker doctorKeys are stored in ~/.config/tracker/.env (under $XDG_CONFIG_HOME when that is an absolute path). You can also export them directly:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GEMINI_API_KEY=...A .env in the working directory may set provider *_API_KEYs only. Base URLs, the gateway and the other knobs belong in the shell or ~/.config/tracker/.env, and security switches such as TRACKER_PASS_ENV in the shell only. --env-files=all|config|none chooses which files load. See the env trust policy.
Important: Use gemini (not google) as the provider name in .dip files.
Non-retryable provider errors (quota exceeded, auth failure, model not found) immediately fail the pipeline with a clear message instead of silently retrying.
tracker setup can bill provider: openai nodes to your ChatGPT subscription instead of an API key. Check OpenAI, press tab until the cursor reaches its Auth row (the page opens on the API Key field), pick ChatGPT subscription with space, and press enter. Setup shows a link (https://auth.openai.com/codex/device) and a one-time code: open the link on any device, such as your phone, and enter the code within 15 minutes. Setup then saves the sign-in to ~/.config/tracker/chatgpt-auth.json (mode 0600; under $XDG_CONFIG_HOME when that is an absolute path) and writes TRACKER_OPENAI_AUTH=chatgpt to ~/.config/tracker/.env, which sends openai calls through that sign-in.
In this mode tracker drops a request's max output tokens, temperature, top_p and stop sequences before sending it, because the Codex backend's request has none of them, so those settings do nothing for openai nodes.
Usage bills to the plan, so tracker counts it at $0: --max-cost and every other dollar ceiling never see these calls, and a run that has both a dollar ceiling and an openai node prints a warning at start. --max-tokens still applies. When the plan's usage limit runs out, the run stops in the resumable paused_billing state; tracker -r resumes it once the limit resets. Autopilot's openai default model isn't served on a ChatGPT plan, so when autopilot lands on openai, pass --autopilot-model with a model your plan serves.
To go back to an API key, run setup again and pick API key on the Auth row; setup then writes TRACKER_OPENAI_AUTH=api-key. Unchecking OpenAI on the first page leaves the setting as it was. Deleting the token file signs you out.
The sign-in goes through Codex's OAuth client. Use it for your own ChatGPT account on your own machine, never for a bot or a hosted runner that other people use.
Tracker can route every provider through Cloudflare AI Gateway so you stop hitting rate limits (Anthropic, OpenAI, etc. cap per-account request rates; Cloudflare's gateway capacity is much higher), gain central analytics and caching, and enable model routing on the gateway side.
Set one env var or flag instead of four:
# The root URL of your Cloudflare AI Gateway:
# https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_slug>
export TRACKER_GATEWAY_URL="https://gateway.ai.cloudflare.com/v1/acc/gw"
# API keys still go to the provider — Cloudflare just proxies.
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GEMINI_API_KEY=...
tracker build_productOr as a CLI flag:
tracker --gateway-url https://gateway.ai.cloudflare.com/v1/acc/gw build_productTracker automatically appends the per-provider suffix:
| Provider | Resolved URL |
|---|---|
anthropic |
<gateway>/anthropic |
openai |
<gateway>/openai |
gemini |
<gateway>/google-ai-studio |
openai-compat |
<gateway>/compat |
Per-provider overrides still win. If you set ANTHROPIC_BASE_URL directly, Anthropic traffic goes there, and the gateway only proxies the providers you haven't explicitly overridden. This means you can point Anthropic at a self-hosted proxy while keeping OpenAI on Cloudflare with one command.
The claude-code backend doesn't use the gateway. These base URLs and the gateway route the native backend only. --backend claude-code launches claude without the five provider *_BASE_URLs (#677), and claude never reads TRACKER_GATEWAY_URL; to send claude through a gateway on purpose, export ANTHROPIC_BASE_URL and set TRACKER_PASS_API_KEYS=1, which keeps keys and base URLs together.
Troubleshooting:
429from Cloudflare: something bigger is wrong (account-level limits, bad gateway slug). 429s from direct provider calls are what the gateway is meant to prevent.401: check your provider API key, not the gateway — Cloudflare passes auth through.- Empty responses: verify the gateway slug is correct and the provider is enabled in the Cloudflare dashboard.
Tracker is a three-layer stack: an LLM client (provider adapters and token tracking), an agent session (turn loop, tool execution, context compaction), and a pipeline engine (graph execution, edge routing, checkpoints, decision audit, TUI). The dippin adapter converts parsed .dip IR into tracker's Graph model, and handlers implement per-node behavior.
graph TB
subgraph "Layer 3: Pipeline Engine"
Engine["Graph Execution<br/>Edge Routing<br/>Checkpoints<br/>Decision Audit"]
Handlers["Handlers: start, exit, codergen, tool,<br/>wait.human, parallel, parallel.fan_in,<br/>conditional, subgraph, stack.manager_loop"]
Adapter["Dippin Adapter<br/>IR → Graph"]
TUI["TUI: node list,<br/>activity log, modals"]
end
subgraph "Layer 2: Agent Session"
Session["Tool Execution<br/>Context Compaction<br/>Event Streaming"]
end
subgraph "Layer 1: LLM Client"
Anthropic & OpenAI & Gemini
end
Engine --> Handlers
Engine --> Adapter
Engine --> TUI
Handlers --> Session
Session --> Anthropic & OpenAI & Gemini
The core engine is UI-agnostic: the TUI, the Slack bot (trackerbot),
the terminal REPL (trackerchat), and any future
web/mobile front-end are peers on one library boundary — tracker.Config →
Engine, an Interviewer seam for human gates, the pipeline/agent event
streams, and RunManager for many concurrent runs. The Slack bot and the REPL
share one transport-neutral core (transport/chatops); each adds only its own
I/O. See docs/architecture/transport-boundary.md.
Declared inputs. A workflow can declare a typed input signature (a dippin
inputs block, requires dippin ≥ v0.51); a front-end introspects it with
tracker.DescribeInputs, validates a request with tracker.ValidateInputs
(structured per-input errors), and passes values via Config.Inputs. They are
validated + bound at run start — a missing required input fails closed before
any node runs, instead of the agent proceeding with nothing. File inputs are
staged into the run dir so a workflow reads them at a fixed, safe path
(build_product takes its spec this way). Secret inputs (SecretInput)
are staged the same way — the value goes to a 0600 file and ${inputs.<name>}
resolves to the path only, so a secret never enters a prompt, the provider wire,
the trace, or the checkpoint. See
docs/architecture/embedding.md §1a.
For subsystem-level architecture docs, see ARCHITECTURE.md and docs/architecture/.
The terminal UI shows:
- Pipeline panel: node list in topological execution order (Kahn's algorithm) with status lamps, thinking spinners, and tool execution indicators
- Activity log: per-node streaming with line-level formatting (headers, code blocks, bullets), node change separators, multi-node activity indicators for parallel execution, and inline
FAILED:/RETRYING:messages when nodes fail or retry - Subgraph nodes: dynamically inserted and indented under their parent
| Icon | Meaning |
|---|---|
| ○ | Pending — not yet reached |
| 🟡 (spinner) | Running — LLM thinking |
| ⚡ | Running — tool executing |
| ● (green) | Completed successfully |
| ✗ (red) | Failed |
| ↻ (amber) | Retrying |
| ⊘ (dim) | Skipped — pipeline took a different path |
tracker.Result.Status is one of:
| Value | Meaning | IsSuccess() |
|---|---|---|
success |
Run reached the success exit; all validations passed. | true |
validation_overridden |
Run reached the success exit, but a human, autopilot, or webhook accepted a failed validation along the way. See Result.ValidationOverrides. |
true |
budget_exceeded |
A BudgetGuard halted the run. |
false |
paused_billing |
Billing/quota exhaustion halted the run in a recoverable, resumable terminal (checkpoint + preserved WIP + tracker -r resume). Surfaced to embedders as RunManager.RunPaused. |
false |
fail |
Run halted via failure. | false |
The enum is open — future minor releases may add new values. Use IsSuccess() (or status_class in JSON output) instead of switching on the raw string.
| Key | Action |
|---|---|
| v | Cycle log verbosity (all / tools / errors / reasoning) |
| z | Toggle zen mode (full-width log, sidebar hidden) |
| / | Search the activity log (n/N next/prev, Esc exits) |
| ? | Help overlay with all shortcuts |
| Enter | Drill down into the selected node (Esc exits) |
| y | Copy the visible log to the clipboard |
| Ctrl+O | Toggle expand/collapse tool output |
| Ctrl+S | Submit human gate input |
| Esc | Cancel (empty) or submit (with content) |
| PgUp/PgDn | Scroll review viewport (plan approval) |
| q | Quit |
cmd/trackerbot is a Slack front-end built on the
transport boundary. Mention it and
it starts a pipeline in a thread, streams notifications and clarifying gate
questions there, and delivers the result — arbitrarily many runs at once, one
per thread:
@trackerbot make me a CLI that greets people
@trackerbot run build_product
@trackerbot status @trackerbot cancel @trackerbot runs
Natural-language requests are routed to a workflow by an LLM; all four gate
modes (choice / yes-no / freeform / interview) work in-thread; interrupted runs
resume after a restart. It connects via Socket Mode (no public endpoint). Setup
and configuration: cmd/trackerbot/README.md.
cmd/trackerchat is the same experience as a REPL:
type a request, answer gates inline, watch the run — no Slack required. It's the
second consumer of the transport boundary, built from the same
transport/chatops core, so it inherits every command, gate mode, cost estimate,
and steer/bump control the Slack bot has:
$ trackerchat
> run ask_and_execute
❓ Which approach should I take?
1) Minimal [default]
2) Full-featured
> 1
✅ done — success · $0.42 · 1m03s
Run tracker inside a Herdr terminal pane and it reports its
run state to the pane's agent manager automatically — no flags, no config. The
pane shows working while the pipeline runs, blocked while a human gate waits
for your answer, and idle when the run finishes.
Detection is by the environment Herdr injects (HERDR_ENV, HERDR_PANE_ID,
HERDR_BIN_PATH); outside a Herdr pane it is a complete no-op. Set
TRACKER_HERDR=0 to opt out even inside one. Reporting is best-effort — a slow
or failed Herdr call never affects your run — and blocked is reported only for
interactive runs (autopilot, --auto-approve, and --webhook-url resolve gates
with nobody waiting).
Every run produces an activity.jsonl log. Live writes go to the integrity-protected path under $XDG_STATE_HOME/tracker/runs/<id>/activity.jsonl (mode 0o600, default $HOME/.local/state/tracker/runs/<id>/, override via TRACKER_AUDIT_DIR; #213). At run-end a sentinel-stripped snapshot is mirrored back to .tracker/runs/<id>/activity.jsonl for bundle export and post-run grep/jq workflows. Captured content:
- Pipeline events: node start/complete/fail, checkpoint saves
- Agent events: LLM turns, tool calls, text output
- Decision events: edge selection (with priority level and context snapshot), condition evaluations (with match results), node outcomes (with token counts), restart detections
Run capture (v0.50.0). Beyond the event stream, a finished run is reconstructable: the executed spec (workflow.dip + workflow.ir.json + input files), the verbatim provider request bodies, and per-call/turn/session identity land on every activity.jsonl line and roll up into a run.json manifest (goal, terminal status, per-node kind/attempts/outcome/turns/usage, run totals). tracker run-json [runID] assembles the manifest after the fact, so a SIGKILLed or archived run gets one too. Library callers wire the same capture with Config.Capture (v0.51.0). Capture files are written 0600 with O_NOFOLLOW, and .tracker/ is excluded from exported bundles.
Reconstruct any routing decision after the fact (post-run snapshot path):
# See all edge decisions
grep 'decision_edge' .tracker/runs/<id>/activity.jsonl | python3 -m json.tool
# See condition evaluations
grep 'decision_condition' .tracker/runs/<id>/activity.jsonl | python3 -m json.tool
# See node outcomes with token counts
grep 'decision_outcome' .tracker/runs/<id>/activity.jsonl | python3 -m json.toolEnable git artifacts from the library via the WithGitArtifacts(true) option on the engine builder; the artifact run directory becomes a git repository and each terminal node outcome creates a commit. (There is no CLI flag for this today — use the ExportBundle helper or the --export-bundle CLI flag to produce a portable bundle from any run directory.)
node(start): start outcome=success
node(middle): codergen outcome=success
node(end): exit outcome=success
Checkpoint tags (checkpoint/<runID>/<nodeID>) mark each save point.
ExportBundle packages the entire git history — commits and tags — into a single file you can copy anywhere:
// Library usage
result, _ := engine.Run(ctx)
if err := tracker.ExportBundle(result.ArtifactRunDir, "/tmp/run.bundle"); err != nil {
log.Printf("bundle export failed: %v", err)
}# CLI usage: export bundle after the pipeline completes
tracker --export-bundle /tmp/run.bundle examples/ask_and_execute.dip
# Restore and inspect on any machine with git
git clone /tmp/run.bundle /tmp/run
cd /tmp/run && git log --onelineThe bundle is self-contained — no network access needed. Clone it on another machine, inspect the exact sequence of node outcomes, and replay from any checkpoint tag.
When a pipeline run doesn't go as expected, tracker gives you tools to understand what happened:
Analyzes a run's failures and surfaces the information you need — tool stdout/stderr, error messages, timing anomalies — without manually grepping through JSONL files.
# Diagnose the most recent run
tracker diagnose
# Diagnose a specific run (prefix matching works)
tracker diagnose 7813bThe output shows each failed node with its output, stderr, errors, and actionable suggestions. For example, it will tell you if a tool node failed because of a stale counter file, or if a node completed suspiciously fast (suggesting a configuration issue).
For a broader view of a run's timeline, retries, and recommendations:
# List all runs
tracker list
# Full audit report for a specific run
tracker audit <run-id>| Symptom | Cause | Fix |
|---|---|---|
| "no LLM providers configured" | Missing API keys | tracker setup or export env vars — or install Claude Code (npm i -g @anthropic-ai/claude-code && claude /login): backend: auto / claude-code workflows, the starters included, need no key |
claude-code nodes fail with unrecognized_model or an old-CLI error |
A stale claude earlier on PATH |
tracker doctor lists every claude on PATH with its version; export TRACKER_CLAUDE_BIN=/absolute/path/to/claude (shell only — a .env can't set it) |
| TestMilestone instantly escalates | Stale fix_attempts counter |
rm .ai/milestones/fix_attempts |
| Node fails with no visible error | Tool stderr not surfaced | tracker diagnose shows full output |
| Pipeline loops forever | Unconditional fallback to loop target | Ensure fallbacks go to an exit node (Done, escalation gate), not back into the loop |
| Tool retries same error 5 times | Deterministic command bug | tracker diagnose flags identical retries — fix the command in the .dip file |
| Every milestone needs fixing | known_failures has comments or bad format | Ensure bare test names only, no comments — v0.11.2 strips them automatically |
| Build loop skips all milestones | Milestone headers don't match expected format | Use ## Milestone N: Title format — v0.11.2 is flexible + fails loudly |
Tool command or setup hook can't authenticate (pip install / npm ci / docker pull 401) |
Tool nodes, the native agent's bash tool and verify commands get *_API_KEY / *_SECRET / *_TOKEN / *_PASSWORD, DOCKER_AUTH_CONFIG and any value carrying a credential (a URL with a password, an http(s) URL with userinfo, a Docker auths JSON blob) stripped |
Export TRACKER_PASS_ENV=1 in the shell that runs tracker (a project .env can't set it) |
| claude-code node fails with "Claude Code refused N tool call(s)" | A deny/ask rule in Claude Code's settings, permissions.blockReadsOutsideWorkingDirectories, or CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 blocked a call a headless run can't approve |
Allow the call in Claude Code's settings, or set tool_access, disallowed_tools or a non-bypass permission_mode on the node so the refusal is expected (a stderr warning, not a failure) |
Tracker exposes per-provider token and dollar cost from every run, and can halt pipelines that exceed configured ceilings.
Claude Code reports a notional cost on a subscription, so a run whose only
provider is claude-code shows it as ~$X.XX usage (included in your Claude plan); it still counts against --max-cost like any other cost.
Library consumers read cost via Result.Cost:
result, _ := tracker.Run(ctx, source, tracker.Config{
Budget: pipeline.BudgetLimits{
MaxTotalTokens: 100_000,
MaxCostCents: 500, // $5.00
MaxWallTime: 30 * time.Minute,
},
})
// IsSuccess() returns true for {success, validation_overridden}; classify by
// status_class for stable bucketing across future enum extensions.
if !result.Status.IsSuccess() {
log.Printf("run did not complete cleanly: status=%s, spent $%.4f",
result.Status, result.Cost.TotalUSD)
}
// To branch on overrides specifically:
if len(result.ValidationOverrides) > 0 {
log.Printf("run involved %d override(s)", len(result.ValidationOverrides))
}
for provider, pc := range result.Cost.ByProvider {
log.Printf("%s: %d tokens, $%.4f", provider, pc.Usage.InputTokens+pc.Usage.OutputTokens, pc.USD)
}CLI users pass flags directly to tracker:
tracker --max-tokens 100000 --max-cost 500 --max-wall-time 30m \
examples/ask_and_execute.dipA halted run prints a HALTED: budget exceeded section naming the dimension
that tripped. Run tracker diagnose to see the per-provider breakdown and
remediation guidance.
Streaming consumers subscribe to EventCostUpdated via
tracker.Config.EventHandler. Each terminal-node outcome emits a
CostSnapshot with aggregate tokens, dollar cost, per-provider totals,
and wall-clock elapsed time.
Budget ceilings can also be declared inline in the workflow's defaults: block (v0.19.0) and act as the fallback when neither Config.Budget nor the matching --max-* CLI flags are set:
workflow MyPipeline
defaults
model: claude-sonnet-5
max_total_tokens: 100000
max_cost_cents: 500
max_wall_time: 30m
Explicit library/CLI values still win over the .dip defaults.
Prices come from dippin-lang's
pricing catalog at the version pinned in go.mod (v0.80.0), so a new or
repriced model is adopted by bumping that pin; the full table is on the
models page. A model the
catalog doesn't price costs $0 with one warning. The catalog holds one price
per model, so prompts above a provider's long-context threshold (Haiku 5.5 above
100K tokens, GPT-6 above 272K) bill higher than Tracker estimates; leave
headroom in --max-cost for long-context runs (dippin-lang#329).
--webhook-url enables fully headless operation: instead of pausing the pipeline to wait for a human at a terminal, tracker POSTs every human gate as JSON to your URL and waits for a callback.
This is the integration point for Slack bots, email approval flows, mobile push notifications, factory workers, or any custom approval system.
- A human gate fires → tracker POSTs a
WebhookGatePayloadto--webhook-url. - Your service receives the payload, routes it to a human (Slack message, email, etc.).
- The human responds → your service POSTs a
WebhookGateResponseto thecallback_urlfield. - Tracker resumes the pipeline with the human's answer.
tracker --webhook-url https://factory.example.com/api/gate \
--gate-timeout 30m \
--gate-timeout-action fail \
--webhook-auth "Bearer sk_live_..." \
examples/build_product.dip| Flag | Default | Description |
|---|---|---|
--webhook-url |
(required to enable) | URL to POST gate payloads to |
--gate-callback-addr |
:8789 |
Local addr for the inbound callback server |
--gate-timeout |
10m |
How long to wait for a reply per gate |
--gate-timeout-action |
fail |
What to do on timeout: fail or success |
--webhook-auth |
(empty) | Authorization header on outbound POSTs |
--webhook-url is mutually exclusive with --autopilot and --auto-approve.
Tracker POSTs JSON with this shape:
{
"gate_id": "uuid",
"run_id": "optional-run-id",
"node_id": "ApproveSpec",
"prompt": "Review the spec. Approve, refine, or reject.",
"choices": [{"label": "approve", "value": "approve"}, ...],
"default": "approve",
"options": [
{"label": "approve", "target": "PickNextMilestone", "default": true, "meaning": "approve"},
{"label": "adjust", "target": "Decompose", "restart": true},
{"label": "reject", "target": "Done", "meaning": "reject"}
],
"callback_url": "http://localhost:8789/gate/f47ac10b-58cc-4372-a567-0e02b2c3d479",
"timeout_seconds": 1800,
"gate_token": "per-gate-secret"
}options is the structured form of choices (#631): one entry per routing
option, derived from the gate node's outgoing edge labels and its declared
default: — never from the prompt text. meaning is approve (an
override: true edge or an affirmative label), reject (the same abandon /
reject vocabulary the engine terminates the run fail on), or absent for a
neutral option such as adjust; restart: true marks a loop-back edge. Render
buttons from options; choices is kept for older consumers.
Your service POSTs back to callback_url with:
{
"choice": "approve",
"freeform": "optional free-text response",
"reasoning": "optional explanation"
}Include the gate_token value in the X-Tracker-Gate-Token header — the callback server rejects requests with missing or wrong tokens (HTTP 401).
⚠️ Stability note (pre-v1.0): tracker's library API is usable now, but breaking changes may still happen between minor releases while the surface is finalized. CheckCHANGELOG.mdbefore upgrading. Full policy — the supported surface, the open-enum rule, and the deprecation contract — is indocs/api-stability.md, mechanically guarded by an exported-surface golden snapshot (api_surface_test.go).
Library consumers set tracker.Config.WebhookGate instead of using CLI flags:
result, _ := tracker.Run(ctx, source, tracker.Config{
WebhookGate: &tracker.WebhookGateConfig{
WebhookURL: "https://factory.example.com/api/gate",
CallbackAddr: ":8789",
Timeout: 30 * time.Minute,
TimeoutAction: "fail",
AuthHeader: "Bearer sk_live_...",
},
})import (
"context"
tracker "github.com/2389-research/tracker"
)
ctx := context.Background()
report, err := tracker.DiagnoseMostRecent(ctx, ".")
if err != nil { log.Fatal(err) }
for _, f := range report.Failures {
fmt.Printf("failed: %s (handler=%s, retries=%d)\n",
f.NodeID, f.Handler, f.RetryCount)
}
for _, s := range report.Suggestions {
fmt.Printf(" %s: %s\n", s.Kind, s.Message)
}tracker.Audit, tracker.DiagnoseMostRecent, tracker.Simulate, and tracker.Doctor all accept context.Context as their first argument and return JSON-serializable reports. Simulate / SimulateGraph analyze the same finalized topology a run executes (pipeline.PrepareForExecution) and return an error when a graph fails its execution-critical invariants, such as an edge to an undeclared node (full validation is tracker validate / pipeline.Validate). tracker.ListRuns and DiagnoseMostRecent/Diagnose accept an optional config (AuditConfig, DiagnoseConfig) with a LogWriter for non-fatal parse warnings; if LogWriter is left unset, warnings are discarded, so embedded callers are silent by default. Set LogWriter to something like os.Stderr (or another writer/logger sink) if you want to receive those warnings. Audit and Simulate currently take just ctx (plus their payload); Doctor takes a required DoctorConfig plus optional functional options (e.g., tracker.WithVersionInfo).
If you currently shell out to tracker diagnose and scrape stdout, migrate to
tracker.Diagnose() / tracker.DiagnoseMostRecent() and read
DiagnoseReport directly instead of parsing formatted CLI text.
To stream events programmatically in the same NDJSON format as tracker --json, use tracker.NewNDJSONWriter:
w := tracker.NewNDJSONWriter(os.Stdout)
result, _ := tracker.Run(ctx, source, tracker.Config{
EventHandler: w.PipelineHandler(),
AgentEvents: w.AgentHandler(),
})tracker [flags] <pipeline> Run a pipeline (file path or built-in name)
tracker workflows List built-in workflows
tracker init <workflow> Copy a built-in to current directory
tracker setup Interactive provider configuration
tracker validate <pipeline> Check pipeline structure
tracker simulate <pipeline> Dry-run execution plan
tracker estimate <pipeline> Rough pre-run cost & scale estimate
tracker doctor [pipeline] Preflight health check (exit 0=pass 1=fail 2=warn)
tracker diagnose [runID] Analyze failures in a run
tracker audit [runID] Full audit report for a run
tracker status [runID] Agent-authored high-level timeline of a run
tracker run-json [runID] Assemble the run.json manifest for a run directory
tracker verify-tests [dir] Flag duplicate Go test bodies; --race, --coverage
tracker list List recent pipeline runs
tracker update Self-update to the latest GitHub release
tracker version Show version information
Flags:
-w, --workdir— working directory (default: current)-r, --resume— resume a previous run by ID--format— pipeline format override:dip(default) ordot(legacy; emits a deprecation warning)--json— stream events as NDJSON to stdout--no-tui— disable TUI dashboard, use plain console--verbose— show raw provider stream events--backend— agent backend:native(default),claude-code, oracp. An explicit value also overridesbackend: autonodes;autoitself is a node value only (--backend autois rejected). Aclaude-codenode fails (routably, not retried) when Claude Code refuses a tool call on a node left on the defaultbypassPermissionswith notool_accessordisallowed_tools, instead of reporting success on a reply nobody can approve--autopilot <persona>— replace human gates with an LLM judge (lax/mid/hard/mentor)--autopilot-model <model>— the model that judge uses (default: the default provider's autopilot model); set a model your plan serves whenTRACKER_OPENAI_AUTH=chatgpt--auto-approve— deterministically accept every human gate (no LLM)--param key=value— override a declared workflow var at run time (repeatable)--artifact-dir— override the node state directory (default:<workdir>/.tracker/runs);-rresolves the run there and fails if it is not there, so resume with the same--artifact-dir(and-w) the run used; the printed resume command includes both--max-tokens— halt if total tokens across the run exceed this value (0 = no limit)--max-cost— halt if total cost in cents exceeds this value (0 = no limit)--max-wall-time— halt if pipeline wall time exceeds this duration (0 = no limit)--gateway-url— Cloudflare AI Gateway root URL (per-provider*_BASE_URLenv vars win)--webhook-url— POST human gate prompts to this URL and wait for callback (headless)--gate-callback-addr— local addr for the webhook callback server (default::8789)--gate-timeout— per-gate wait timeout when--webhook-urlis set (default:10m)--gate-timeout-action— what to do on gate timeout:fail(default) orsuccess--webhook-auth—Authorizationheader for outbound webhook requests--export-bundle— write a portable git bundle of run artifacts to the given path after completion--bypass-denylist— disable the built-in tool command denylist (prints a stderr warning; sandboxed use only)--tool-allowlist <pattern>— glob pattern a tool command must match to execute (repeatable or comma-separated)--max-output-limit <bytes>— hard ceiling per tool command output stream (default: 10MB)--tool-denylist-add <pattern>— extra glob pattern(s) added to the built-in denylist (additive; cannot remove built-ins)--from <node>— with-r: re-enter the run at this node, re-running it and everything downstream--resume-no-rewind— with-r: resume exactly at the checkpoint's node instead of rewinding to the node whose failure routed the run into a fail-closed terminal--force-bundle-mismatch— allow resume when the bundle's content-addressed identity differs from the original run--sleep-aware-budget— exclude detected suspend spans (a closed laptop) from wall-time and stall budgets--fail-on-override— exit code 2 if the run endsvalidation_overridden(default: exit 0)--gateway-kind— gateway path convention:cf-aig(default) orbedrock--git <policy>— git preflight:auto(default, respects the workflow'srequires:),off,warn,require,init--allow-init— required latch for--git=initin non-interactive runs--env-files <mode>— which.envfiles to load:all(default),config(~/.config/tracker/.envonly),none
tracker doctor takes --probe=false (skip the live API auth check), --backend, --git, --allow-init, --env-files and -w / --workdir.
# Run tests
go test ./... -short
# All local quality gates (fmt, vet, tests, coverage, complexity, lint, docs, shell)
make ci
# Example-script fixture suites (hermetic HOME; SUITE=<Name>_test.sh for one)
make test-scripts
# Every core pipeline must grade A (dippin at the go.mod pin, one file per run)
make doctor
# Validate all example pipelines
for f in examples/*.dip; do tracker validate "$f"; done
# Run dippin simulation tests
make dippin-test
# Check with dippin-lang tools
dippin doctor examples/build_product.dip
dippin simulate -all-paths examples/build_product.dipSee LICENSE.