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@latestThree steps to your first run — no .dip file needed:
tracker doctor # 1. check setup: API keys, dippin binary, working dir
tracker ask_and_execute # 2. run a built-in workflow: describe what you want, answer one gate, watch it build
tracker diagnose # 3. inspect what happened (also: tracker audit / tracker list)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.
Four pipelines are embedded in the binary and available via tracker workflows:
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 on the tracker invocation: tool subprocesses (the hook included) run with *_TOKEN / *_API_KEY / *_SECRET / *_PASSWORD environment variables 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
tracker ask_and_execute # 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.
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/2389/tracker/.env. You can also export them directly:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GEMINI_API_KEY=...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 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.
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 |
| 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 |
Tracker exposes per-provider token and dollar cost from every run, and can halt pipelines that exceed configured ceilings.
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.
--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. 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 doctor Preflight health check
tracker diagnose [runID] Analyze failures in a run
tracker audit <runID> Full audit report for a run
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--autopilot <persona>— replace human gates with an LLM judge (lax/mid/hard/mentor)--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)--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)
# Run tests
go test ./... -short
# Validate all example pipelines
for f in examples/*.dip; do tracker validate "$f"; done
# Run dippin simulation tests
for f in examples/*.dip; do dippin test "$f"; done
# Check with dippin-lang tools
dippin doctor examples/build_product.dip
dippin simulate -all-paths examples/build_product.dipSee LICENSE.