Help us shape the future of Go! Take the 2026 Go Developer Survey

tracker

package module
v0.81.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 46 Imported by: 0

README

Tracker by 2389

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 →

Quick Start

Install — Homebrew (macOS/Linux) or go install:

brew install 2389-research/tap/tracker
# or:  go install github.com/2389-research/tracker/cmd/tracker@latest

Your 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_product scaffolds build_product.dip + a starter SPEC.md; edit the spec, then tracker build_product.
  • Hands-off — add --autopilot mid to replace human gates with an LLM judge (or --auto-approve for 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.

Pipeline Examples

Six pipelines are embedded in the binary and available via tracker workflows. Start with the first two:

explain

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.

quickfix

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.

ask_and_execute

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.

build_product

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.

Operator stamps (build_product)

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. Setup refuses 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 (no go.work / go.mod / package.json / pyproject.toml / Cargo.toml and no Makefile ci / check / lint / test target) and routes to EscalateVerification (unattended default abandon: the run ends fail at AbortRun) — a product with no test runner cannot ship green. With it, the ship gate passes with a NOTE that nothing was tested. Without it, milestone-mode TestMilestone reports such a tree as tests-not-yet-verifiable (an early scaffolding/docs milestone legitimately has no runner; VerifyMilestone confirms the milestone's done-when needs no tests); with it, the milestone gate passes with the same NOTE. The operator must create it before the run: PickNextMilestone snapshots the hatch files at the start of every milestone, and a stamp that appears mid-milestone is reported by TestMilestone / FinalBuild as agent-created (+ .ai/build/no-tests-ok) and treated as a finding. Agents are instructed never to create either stamp.
build_product_with_superspec

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.

Which one? build_product vs build_product_with_superspec

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, CommitScaffold commits SPEC.md, docs/execution-plan.md, docs/traceability.yaml and .gitignore by name, so every stream worktree (forked from HEAD) 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 unmerged build/<stream> branch is renamed build/<stream>-abandoned-<sha>, never deleted.
  • Per-stream traceability overlays: a stream never edits docs/traceability.yaml; it writes docs/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.sh green-gate as build_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 at gate_verify until the phase adds a runnable suite, or the operator stamps .ai/build/no-tests-ok for a genuinely test-free project.
  • MergeConflict gate: 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: FinalGates fails any requirement still pending or without an impl_ref, and any implemented requirement without a test_ref unless waived in docs/traceability-waivers.txt (<ID> <reason>, one per line); the TraceabilityAudit goal 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).

deep_review

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

Built-in Workflows

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 editing

build_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.

Dippin Language

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
Declaring Environmental Dependencies — requires: (v0.29.0)

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.

Node Types
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
Variable Interpolation

Three namespaces for ${...} syntax in prompts:

  • ${ctx.outcome} — runtime pipeline context (outcome, last_response, human_response, tool_stdout)
  • ${params.model} — workflow-level vars (optionally overridden by --param key=value at 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 MyPipeline

Unknown --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.

Edge Conditions
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.

Declarative Structured Output — writes: / reads:

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.

Per-Node Working Directory

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.

Human Gates

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 as ctx.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 to OutcomeSuccess, No maps to OutcomeFail — route with when ctx.outcome = success / when ctx.outcome = fail edges. Distinct from choice mode, where the outcome is always success and routing uses preferred_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 Mode

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).

Providers

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 doctor

Keys 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.

OpenAI on your ChatGPT subscription

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.

Cloudflare AI Gateway

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_product

Or as a CLI flag:

tracker --gateway-url https://gateway.ai.cloudflare.com/v1/acc/gw build_product

Tracker 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:

  • 429 from 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.

Architecture

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/.

TUI

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
Status Icons
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
Run terminal status

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.

Keyboard
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

Drive it from Slack

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.

Drive it from your terminal

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

Report status to Herdr panes

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).

Decision Audit Trail

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.tool

Git Integration

Enable 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.

Exporting a run as a portable bundle

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 --oneline

The 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.

Troubleshooting

When a pipeline run doesn't go as expected, tracker gives you tools to understand what happened:

tracker diagnose

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 7813b

The 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).

tracker audit

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>
Common issues
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)

Cost Governance

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.dip

A 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).

Headless Execution (Webhook Gate)

--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.

Flow
  1. A human gate fires → tracker POSTs a WebhookGatePayload to --webhook-url.
  2. Your service receives the payload, routes it to a human (Slack message, email, etc.).
  3. The human responds → your service POSTs a WebhookGateResponse to the callback_url field.
  4. Tracker resumes the pipeline with the human's answer.
CLI
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
Flags
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.

Payload format

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).

Library API

⚠️ 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. Check CHANGELOG.md before upgrading. Full policy — the supported surface, the open-enum rule, and the deprecation contract — is in docs/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_...",
    },
})
Analyzing past runs from code
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(),
})

CLI Reference

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) or dot (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, or acp. An explicit value also overrides backend: auto nodes; auto itself is a node value only (--backend auto is rejected). A claude-code node fails (routably, not retried) when Claude Code refuses a tool call on a node left on the default bypassPermissions with no tool_access or disallowed_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 when TRACKER_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); -r resolves 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_URL env 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-url is set (default: 10m)
  • --gate-timeout-action — what to do on gate timeout: fail (default) or success
  • --webhook-auth — Authorization header 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 ends validation_overridden (default: exit 0)
  • --gateway-kind — gateway path convention: cf-aig (default) or bedrock
  • --git <policy> — git preflight: auto (default, respects the workflow's requires:), off, warn, require, init
  • --allow-init — required latch for --git=init in non-interactive runs
  • --env-files <mode> — which .env files to load: all (default), config (~/.config/tracker/.env only), none

tracker doctor takes --probe=false (skip the live API auth check), --backend, --git, --allow-init, --env-files and -w / --workdir.

Development

# 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.dip

License

See LICENSE.

Documentation

Overview

Package tracker is the public library API for running and inspecting tracker pipelines.

API stability: tracker is pre-v1.0. Public APIs are intended to be production-usable, but breaking changes may still occur between minor releases while the surface is finalized. Read CHANGELOG.md before upgrading.

ABOUTME: Top-level convenience API for running pipelines (.dip preferred, .dot deprecated) with auto-wired dependencies. ABOUTME: Consumers import only this package — LLM clients, registries, and environments are built automatically.

ABOUTME: Shared helpers for resolving run directories and parsing activity.jsonl. ABOUTME: Promoted from cmd/tracker/ so library and CLI use one implementation.

ABOUTME: Small graph-attribute readers used to fall back to workflow-level ABOUTME: defaults (budget, timeouts, opt-in flags) when a Config field is zero.

ABOUTME: Library API for auditing a completed pipeline run. ABOUTME: Returns structured timeline, retries, errors, and recommendations.

ABOUTME: Terminal-status classification and recommendation helpers for Audit. ABOUTME: Maps activity + checkpoint into a Status string, a stable StatusClass, and recs.

ABOUTME: Git bundle export for pipeline run artifact repositories. ABOUTME: ExportBundle wraps `git bundle create --all` so a completed run can be ABOUTME: shipped as a single portable file and restored with `git clone <bundle>`.

ABOUTME: Config.Capture seam — the library-owned wiring that makes tracker.Run produce the same on-disk run capture (run.json + spec artifacts + identity-bearing activity.jsonl) the CLI does. ABOUTME: Wires one JSONLEventHandler across EventHandler/AgentEvents/LLMTrace with the SessionOwned de-dup correct, so embedders never hit the double-log trap or hand-compose the three seams.

ABOUTME: Cost-ceiling visibility for openai calls billed to a ChatGPT subscription ABOUTME: (design §3 "Cost (D6)") — a run-start warning plus the EstimateRun pricing switch.

ABOUTME: LLM client provider-adapter construction and gateway base-URL resolution. ABOUTME: Split from tracker.go to keep the root API file under the size ceiling (#450).

ABOUTME: Native LLM client resolution for an engine: tolerate a missing client only ABOUTME: when no node needs one ("no providers configured" with an auto/claude-code/acp graph).

ABOUTME: Public seam for injecting the library's diagnostic sink (#449). ABOUTME: Embedders redirect/suppress/level-filter tracker's internal diagnostics here.

ABOUTME: Library API for diagnosing pipeline run failures. ABOUTME: Reads checkpoint + status.json + activity.jsonl and returns a structured report.

ABOUTME: Diagnose detector for cost asymmetry — one backend with no prompt ABOUTME: caching dominating a run's cost, unnoticed until the bill (#353).

ABOUTME: Restart-budget-reset records surfaced by Diagnose (#643). ABOUTME: Informational: an enclosing loop advanced and re-armed nested budgets/latches.

ABOUTME: Activity-log scanning helpers for Diagnose — line loop, per-line ABOUTME: decoding, anomaly recording, and per-node failure enrichment.

ABOUTME: Suggestion-building helpers for Diagnose — turns node failures and ABOUTME: runtime anomalies into actionable Suggestion entries.

ABOUTME: Library API for preflight health checks. ABOUTME: Pure read-only — no network probes unless ProbeProviders: true.

ABOUTME: Which backends a pipeline needs, for tracker doctor (#716): the provider-key ABOUTME: check passes when no node needs native, and claude is required when a node needs it.

ABOUTME: Doctor check for optional binaries (git, claude CLI). ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor's and `tracker version`'s report of the ChatGPT subscription sign-in for the ABOUTME: openai provider slot: offline status, a live refresh probe, and the hints ChatGPT mode adds.

ABOUTME: Doctor checks for the dippin binary and dippin<->go.mod version compatibility. ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor check for a git-tracked <workdir>/.env (#659): a committed .env is readable by every clone ABOUTME: and is loaded by `tracker run`, so it is warned about — never refused.

ABOUTME: Doctor checks for environment-variable warnings and gateway routing caveats. ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor Git Requires check — previews the run-time git preflight gate. ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor checks for pipeline files (.dip/.dot) and .dipx bundles. ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor check for configured LLM providers (key presence, format, optional auth probe). ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: Doctor checks for the working directory, .gitignore hygiene, and artifact dirs. ABOUTME: Split from tracker_doctor.go (#453) — behavior-preserving extraction.

ABOUTME: buildEngineOpts and its persistence helper: the Engine options a Config ABOUTME: implies (events, steering, budgets, bundle identity, checkpoints).

ABOUTME: EstimateRun — a rough pre-run cost/scale ballpark from static analysis. ABOUTME: Prices each agent node's model against a turn heuristic; deliberately a range.

ABOUTME: Public NDJSON event writer for the tracker --json wire format. ABOUTME: Threaded from pipeline/LLM/agent event streams; thread-safe for concurrent writers.

ABOUTME: Populates the structured StreamEvent payload fields from pipeline/agent events. ABOUTME: Field names mirror pipeline's jsonlLogEntry so one decoder serves NDJSON and activity.jsonl.

ABOUTME: Classifies a terminal run error into a human-first cause + remediation ABOUTME: so failure surfaces lead with the problem, not the plumbing (#492).

ABOUTME: Public API for declared pipeline inputs — introspect, validate, bind ABOUTME: at run start (#553). Pairs with dippin's `inputs` declaration (#190).

ABOUTME: Interviewer resolution — selects the human-gate handler from Config. ABOUTME: Split out of tracker.go so the root file stays focused on the API surface (#450).

ABOUTME: Library-facing helpers for resolving pipeline sources and run checkpoints. ABOUTME: Mirrors the CLI's bare-name and run-ID resolution for library consumers.

ABOUTME: RunManager owns multiple concurrent pipeline runs keyed by an external id. ABOUTME: Transport-neutral infra for services (Slack/web) that drive many runs at once (#479).

ABOUTME: Library API for dry-running a pipeline: returns the parsed graph, ABOUTME: BFS execution plan, and list of unreachable nodes.

ABOUTME: Pipeline source parsing for the library entry points (.dip / DOT detection and load). ABOUTME: Routes a built-in's own text to the embed FS so its *_file sidecars resolve without disk.

ABOUTME: Reads a run's agent-authored status-update timeline from the activity ABOUTME: log — a clean, high-level "what got done" history, distinct from the firehose (#494).

ABOUTME: Test-fidelity analysis — flags duplicate/near-duplicate Go test bodies ABOUTME: so a verify gate can't bless byte-for-byte-copied "distinct" tests (#489).

ABOUTME: Structural near-duplicate detection (#532 heuristic 1) — flags test ABOUTME: bodies the exact/literal-strip hashes miss (reordered stmts, renamed ABOUTME: locals, wrapped asserts) that also call the same production symbols.

ABOUTME: Test-race analysis — runs the Go race detector so a verify gate can't ABOUTME: bless tests that pass only by timing luck and fail under -race (#489).

ABOUTME: Coverage-attribution fidelity (#532 heuristics 2 & 3) — runs per-test ABOUTME: Go coverage to flag tests that reach no production code (unreached path) ABOUTME: and test-local functions that re-implement uncovered production logic.

ABOUTME: Coverage-profile parsing, module/production-func indexing, and the ABOUTME: heuristic-3 test↔production re-implementation match for #532.

ABOUTME: Resolves ${graph.workflow_dir} for the library path: seeds it for disk ABOUTME: loads and materializes the embed FS tree for built-ins at engine construction.

ABOUTME: Built-in workflow catalog — embedded .dip files and name resolution. ABOUTME: Library consumers can list, read, and resolve workflows without shelling to the CLI.

Index

Examples

Constants

View Source
const (
	FormatDip = "dip" // Dippin format (current, default)
	FormatDOT = "dot" // DOT/Graphviz format (deprecated)
)

Pipeline format identifiers.

View Source
const (
	GitPreflightAuto    = pipeline.GitPreflightAuto
	GitPreflightOff     = pipeline.GitPreflightOff
	GitPreflightWarn    = pipeline.GitPreflightWarn
	GitPreflightRequire = pipeline.GitPreflightRequire
	GitPreflightInit    = pipeline.GitPreflightInit
)

GitPreflight values re-exported from pipeline for caller convenience.

View Source
const PinnedDippinVersion = "v0.80.0"

PinnedDippinVersion is the dippin-lang version from go.mod. Kept in sync with go.mod by TestPinnedDippinVersionMatchesGoMod.

Variables

View Source
var (
	// ErrRunKeyActive is returned by Start when a run with the same key is still active.
	ErrRunKeyActive = errors.New("a run with this key is already active")
	// ErrAtCapacity is returned by Start when the concurrency cap is reached. The
	// caller decides whether to queue, reject, or retry later.
	ErrAtCapacity = errors.New("run manager is at capacity")
)
View Source
var ErrGatewayRouteRefused = errors.New("gateway route refused: kind/provider combination unsupported or unknown")

ErrGatewayRouteRefused is returned by the strict resolver functions when a gateway URL is configured but the (kind, provider) pair is unsupported or the kind is unknown. Surfacing this as an error prevents the silent SDK-default fallback (e.g. openai-compat defaulting to openrouter.ai) that contradicts the documented fail-closed semantics of #276.

Functions

func DescribeInputs added in v0.58.0

func DescribeInputs(source, format string, opts ...SourceOption) ([]pipeline.InputSpec, error)

DescribeInputs parses source and returns its declared input schema WITHOUT running — the read-only introspection a host uses to render a form or ask conversationally before it has any values. Returns nil for a pipeline that declares no inputs. format follows Config.Format ("dip" default). Pass WithSource so *_file directives resolve against the source's origin.

func EmbeddedWorkflowFS added in v0.74.0

func EmbeddedWorkflowFS() fs.FS

EmbeddedWorkflowFS returns the read-only filesystem holding the built-in workflows and their sidecar files, rooted so that WorkflowInfo.File (e.g. "examples/build_product.dip") is a valid path within it. Pair it with pipeline.LoadDippinWorkflowFS to load a built-in with its prompt_file / command_file directives resolved from the binary rather than from disk.

func ExportBundle

func ExportBundle(runDir, outPath string) error

ExportBundle writes a git bundle of the run directory's artifact repository to outPath. The bundle captures all commits and tags (including checkpoint tags) produced by WithGitArtifacts during the run. Users can restore the run with `git clone <bundlePath> <dir>` and inspect history with `git log`.

Returns an error if runDir is not a git repository, if git is not in PATH, or if `git bundle create` fails. The error wraps the command's stderr so callers can surface meaningful diagnostics.

The bundle file is portable — it can be copied to another machine and cloned there without network access. This is the recommended way for a remote factory-worker instance to hand a completed run back to the user.

func MostRecentRunID

func MostRecentRunID(workdir string) (string, error)

MostRecentRunID returns the run ID of the most recent run (by checkpoint timestamp) under workdir. Returns an error if no runs with valid checkpoints exist.

func NewLLMClient added in v0.46.0

func NewLLMClient(cfg Config) (*llm.Client, error)

NewLLMClient builds a standalone LLM client from Config for embedders that need model calls outside a pipeline run — e.g. request classification or routing. Only Provider, GatewayURL, and GatewayKind are consulted; Model, LLMClient, and the rest are ignored. It carries the same transport-retry middleware as a run's client. The caller owns Close(). With TRACKER_OPENAI_AUTH=chatgpt, openai uses the ChatGPT sign-in, and no gateway or base-URL setting applies to it.

func ParseSource added in v0.75.0

func ParseSource(source, format string, opts ...SourceOption) (*pipeline.Graph, error)

ParseSource parses a pipeline source string into a *pipeline.Graph without constructing an engine, resolving *_file sidecars per the anchored SourceRef exactly as Run / NewEngine do. It is the seam for an embedder that must hold and MUTATE the graph before NewEngineFromGraph — per-node model tiering, a forced provider, attr injection — for a source whose sidecars are not on disk: with WithSource(SourceRef{Builtin: name}) the prompt_file / command_file directives resolve from the embed FS and the graph is marked as that built-in, so NewEngineFromGraph materializes its ${graph.workflow_dir} tree. Parsing the raw text with pipeline.LoadDippinWorkflow instead fails on the first prompt_file.

format is "dip", "dot" (deprecated) or "" to auto-detect. Validation and lint diagnostics are logged, and a validation error is returned as err.

func ResolveActivityLogPath

func ResolveActivityLogPath(runDir string) (path string, secureUsed bool)

ResolveActivityLogPath returns the on-disk location of the activity log for runDir. It prefers the integrity-protected secure path (#213) when present, falling back to <runDir>/activity.jsonl for pre-#213 runs and post-run snapshots. The returned secureUsed flag is true when the path came from the secure location — callers that validate the runtime sentinel should only do so in that case.

runID is derived from runDir's basename, matching the .tracker/runs/<runID> layout enforced by ResolveRunDir.

func ResolveBudgetLimits

func ResolveBudgetLimits(cfg pipeline.BudgetLimits, graph *pipeline.Graph) pipeline.BudgetLimits

ResolveBudgetLimits fills any zero field on cfg from the matching workflow-level default in graph.Attrs. Config values take precedence — the graph attrs are only consulted for fields the caller left unset. Returns the original cfg unchanged if graph is nil or has no attrs.

The graph-level keys consulted are max_total_tokens, max_cost_cents, max_wall_time, and stall_timeout, which the dippin adapter writes from WorkflowDefaults fields in v0.21.0+.

Exported so the tracker CLI can merge its --max-* flag values with workflow defaults without re-implementing the same logic.

func ResolveCheckpoint

func ResolveCheckpoint(workDir, runID string) (string, error)

ResolveCheckpoint finds the checkpoint file path for a given run ID under the working directory's default artifact dir, <workDir>/.tracker/runs. The runID argument may be a unique prefix of a real run ID. It is ResolveCheckpointIn(filepath.Join(workDir, ".tracker", "runs"), runID); a run started with a custom artifact dir (--artifact-dir / Config.ArtifactDir) must be resolved with ResolveCheckpointIn and that dir instead.

Unlike ResolveCheckpointIn, a full run ID whose run dir is absent from <workDir>/.tracker/runs (an excluded or cleaned snapshot dir) still resolves to its secure checkpoint, with a warning: the resumed run then writes its artifacts to <workDir>/.tracker/runs.

This is the library equivalent of the CLI's `tracker -r <runID>` flag. Library consumers can set Config.ResumeRunID to have NewEngine resolve the checkpoint automatically.

func ResolveCheckpointIn added in v0.80.0

func ResolveCheckpointIn(runsDir, runID string) (string, error)

ResolveCheckpointIn finds the checkpoint to resume runID from, where runsDir is the run's artifact dir (the directory holding <runID>/ run dirs: the --artifact-dir value, or <workDir>/.tracker/runs by default). runID may be a unique prefix of a run dir name in runsDir.

The AUTHORITATIVE secure checkpoint (pipeline.SecureCheckpointPath, #559) is returned when it exists; the <runsDir>/<runID>/checkpoint.json snapshot is tool-reachable, so it is only the legacy fallback for runs with no secure copy. The run's dir must be in runsDir: when runsDir is unreadable or holds no match, it is an error even for a full run ID with a secure checkpoint. The checkpoint records neither the run's workdir nor its artifact dir, so resuming from a runsDir that does not hold the run would write its artifacts there and split the run across two dirs. Prefixes are matched in runsDir only, never in the secure base: that base is per-user and shared across projects, so a prefix there could resume another project's run.

func ResolveProviderBaseURL

func ResolveProviderBaseURL(provider string) string

ResolveProviderBaseURL returns the base URL a provider's HTTP client should use. Resolution order:

  1. The provider-specific env var (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_BASE_URL, OPENAI_COMPAT_BASE_URL).
  2. TRACKER_GATEWAY_URL with a per-provider suffix appended; the suffix map is selected by TRACKER_GATEWAY_KIND (default cf-aig — Cloudflare AI Gateway conventions).
  3. Empty string, meaning the provider's SDK default.

Per-provider env vars always win over TRACKER_GATEWAY_URL.

**Lax variant.** This function returns the empty string for BOTH "no gateway configured" AND "gateway configured but routing refused." It is preserved for backward compatibility with library callers that existed before #276 added kind dispatch. New code on the adapter construction path MUST use ResolveProviderBaseURLStrict so that refuse-to-route surfaces as an error rather than a silent SDK-default fallback.

func ResolveProviderBaseURLStrict added in v0.36.0

func ResolveProviderBaseURLStrict(provider string) (string, error)

ResolveProviderBaseURLStrict is the fail-closed sibling of ResolveProviderBaseURL. It returns the same URL resolution but distinguishes "no gateway needed" (returns "", nil) from "gateway configured but routing refused" (returns "", ErrGatewayRouteRefused wrapped). Adapter constructors call this so a misconfigured gateway cannot silently leak requests to public SDK default endpoints.

func ResolveRunDir

func ResolveRunDir(workdir, runID string) (string, error)

ResolveRunDir finds the run directory under <workdir>/.tracker/runs matching runID by exact name or unique prefix. Returns an absolute path.

func SetDiagnosticLogger added in v0.48.0

func SetDiagnosticLogger(logger *slog.Logger)

SetDiagnosticLogger installs the sink for tracker's library-internal diagnostics — LLM adapter warnings, pricing warnings, backend subprocess notes, condition-variable warnings, autopilot fallbacks. These are NOT pipeline-lifecycle events (those ride Config.EventHandler); they are the call sites that previously wrote to the process-global logger/stderr.

By default the library discards them (a no-op sink) so an embedded control plane's structured logs stay clean. Pass a *slog.Logger to capture, redirect, or level-filter them; pass nil to reset to the no-op sink.

The sink is process-wide, not per-engine: several diagnostics originate in free functions (e.g. llm.EstimateCost) that cannot carry a per-run logger, so the library funnels all of them through one injectable sink. Genuine per-run, lifecycle observability belongs on Config.EventHandler / Config.AgentEvents instead.

func SortActivityByTime

func SortActivityByTime(entries []ActivityEntry)

SortActivityByTime sorts entries ascending by Timestamp.

func ValidateInputs added in v0.58.0

func ValidateInputs(specs []pipeline.InputSpec, values []Input) []pipeline.InputError

ValidateInputs checks caller-supplied values against a schema and returns structured, per-input errors (empty when valid). Standalone — call it to gate a request before Run. Includes unknown_input (a supplied key the workflow does not declare) so a host can surface a typo; Run treats unknown_input as non-fatal.

Types

type ActivityEntry

type ActivityEntry struct {
	// Core identity and payload — present on most lines.
	// Timestamp is always set — a line whose ts does not parse is rejected
	// outright.
	Timestamp time.Time
	// Source is the emitting subsystem: "pipeline" (engine), "agent" (LLM
	// session), "llm" (raw provider events), or "cli" (CLI-level audit).
	Source  string
	Type    string
	RunID   string
	NodeID  string
	Message string
	Error   string
	// Identity of the emitting LLM call / tool, and its payload text
	// (tool output or response preview). Set on agent and llm lines.
	Provider string
	Model    string
	ToolName string
	Content  string
	// BundleIdentity is the content-addressed identity of the .dipx bundle
	// the run executed against ("sha256:<hex>"); empty for a plain .dip run.
	BundleIdentity string

	// Decision fields — populated for decision_edge / decision_condition /
	// decision_outcome / decision_restart / conditional_fallthrough /
	// restart_budget_reset / resume_rewound entries.
	// ConditionMatch and RestartCount are pointers precisely so a consumer can
	// tell false/0 from absent.
	EdgeFrom        string
	EdgeTo          string
	EdgeCondition   string
	EdgePriority    string
	ConditionMatch  *bool
	OutcomeStatus   string
	ContextSnapshot map[string]string
	ContextUpdates  map[string]string
	RestartCount    *int
	ClearedNodes    []string
	// ResetBy / FallbackLatchCleared are populated on restart_budget_reset
	// entries (#643): the enclosing loop header whose restart reset this
	// node's per-target budget, and whether its one-shot fallback latch was
	// re-armed too.
	ResetBy              string
	FallbackLatchCleared bool
	// RewindReason is populated on resume_rewound entries (#651): why the
	// resume re-entered at edge_to (the origin node) instead of the
	// checkpoint's current node — an automatic rewind past a fail-closed
	// terminal, or an explicit --from.
	RewindReason    string
	ConditionsTried []pipeline.ConditionEval
	// TokenInput / TokenOutput are the node's session token counts on a
	// decision entry — never run-cumulative (that is TotalTokens). Per-turn
	// cache-token counts and cost ride on the capture group below.
	TokenInput  int
	TokenOutput int

	// Cost snapshot fields — populated for cost_updated and budget_exceeded
	// entries. Run-cumulative, not per-node. Estimated is true when any
	// contributing session was heuristic-derived.
	TotalTokens    int
	TotalCostUSD   float64
	ProviderTotals map[string]pipeline.ProviderUsage
	WallElapsedMs  int64
	Estimated      bool

	// Truncation fields — populated for tool_output_truncated entries (#208).
	TruncStream   string
	TruncLimit    int
	TruncCaptured int
	TruncDropped  int
	TruncTotal    int

	// Marker fields — populated for tool_marker_missing entries (#210).
	MarkerPattern string
	MarkerTail    string
	MarkerError   string

	// RouteTail is populated for tool_route_missing entries (#212).
	RouteTail string

	// Tool-timeout fields — populated for tool_timeout entries (#644).
	ToolTimeoutMs       int64
	ToolTimeoutCaptured int

	// Jail fields — populated for jail_degraded entries (#648): the node's
	// writable_paths_mode (always "prefer"), the host-capability reason the jail
	// could not be applied, and the declared globs the Bash subprocess is NOT
	// bounded by on this run.
	JailMode          string
	JailReason        string
	JailDeclaredGlobs []string

	// Backend fields — populated for backend_resolved entries: what a node's
	// backend: auto resolved to and why.
	BackendResolved   string
	BackendReason     string
	ClaudePath        string
	ClaudeVersion     string
	BackendFromRecord bool

	// Auto-status fields — populated for auto_status_missing entries (#346).
	AutoStatusTail       string
	AutoStatusFailClosed bool

	// Override fields — populated for "validation_overridden" entries. Mirror
	// the wire-format fields written by the runtime's jsonlLogEntry: the gate
	// that produced the override, the label that selected the override edge, who
	// acted, and the subgraph_path when propagated up from a child run.
	OverrideGate         string
	OverrideLabel        string
	OverrideActor        pipeline.Actor
	OverrideSubgraphPath []string

	// Gate lifecycle fields — populated for gate_opened / gate_resolved entries
	// (#509). GateID correlates the pair; NodeID identifies the gate node on
	// both. Open-time: GateMode, GateLabel, GatePrompt, GateChoices,
	// GateDefault, GateOptions (#631), GateQuestions. Resolve-time: GateResponse, GateOutcome, GateActor,
	// GateTimedOut (plus Error when the gate failed to collect an answer).
	GateID        string
	GateMode      string
	GateLabel     string
	GatePrompt    string
	GateChoices   []string
	GateDefault   string
	GateOptions   []pipeline.GateOption
	GateQuestions []pipeline.GateQuestion
	GateResponse  string
	GateOutcome   string
	GateActor     pipeline.Actor
	GateTimedOut  bool

	// Run-reconstruction / capture fields (#519). Mirror the same-named keys on
	// pipeline's jsonlLogEntry so the supported reader is lossless (E2).
	// SessionID/TurnNo/CallID identify the emitting session/turn/LLM call;
	// NodeKind and AttemptNo come from engine state; ToolInput is the
	// untruncated tool arguments; the token/cost/duration fields carry per-turn
	// economics; FinishReason classifies a turn's end; TerminalStatus is the
	// run's authoritative outcome, set on exactly one entry per run.
	SessionID          string
	ParentSessionID    string
	TurnNo             int
	AttemptNo          int
	NodeKind           string
	ToolInput          string
	ToolDurationMs     int64
	CacheReadTokens    int
	CacheWriteTokens   int
	ReasoningTokens    int
	EstimatedCost      float64
	ContextUtilization float64
	ToolCacheHits      int
	ToolCacheMisses    int
	TurnDurationMs     int64
	CallID             string
	FinishReason       string
	TerminalStatus     string
	// ResumeAfter is the provider's rate/usage reset time (RFC3339 string), set
	// only on a billing_paused line whose pause carried it (#591). Kept as the
	// on-disk string — like TerminalStatus — rather than a typed time.Time so
	// the reader stays a pure decode with no reparse.
	ResumeAfter string
}

ActivityEntry is a parsed line from activity.jsonl. Populate via ParseActivityLine — ActivityEntry is not itself a JSON-wire type because tracker has historically used two timestamp formats and time.Time's default unmarshal handles only RFC3339Nano.

Marshal/unmarshal contract: do not json.Marshal/json.Unmarshal ActivityEntry directly. Use ParseActivityLine and LoadActivityLog for decoding and map to your own wire type when encoding.

Field contract: the reader is lossless — every field the runtime writes to activity.jsonl is decoded here, and each field's Go name is the same as the matching StreamEvent field (tracker_events.go), so replaying a finished run from the audit log and following it live over NDJSON read the same names. Every field except Timestamp is optional: a line carries only the fields its event type emits, so a zero value means "this line does not carry it".

Generated from scripts/gen/activitylog/schema.go, the single field authority shared with activityRawLine and the toEntry copier.

func LoadActivityLog

func LoadActivityLog(runDir string) ([]ActivityEntry, error)

LoadActivityLog reads and parses the activity log for runDir, preferring the integrity-protected secure path with fallback to the legacy <runDir>/activity.jsonl. Returns (nil, nil) if neither location has a file. Malformed lines are skipped. Sentinel-stripped lines that don't parse as JSON are dropped silently — callers needing tamper-detection granularity should use ScanActivityLog (or the Diagnose path).

func ParseActivityLine

func ParseActivityLine(line string) (ActivityEntry, bool)

ParseActivityLine decodes a single JSONL line. Returns (zero, false) on any parse error, including an unparseable timestamp. Unknown keys are ignored: a line written by a newer runtime still parses, minus the fields this build does not know. The decode target (activityRawLine) and the toEntry copier are generated into tracker_activity_raw_gen.go from scripts/gen/activitylog.

type ActivityError

type ActivityError struct {
	Timestamp time.Time `json:"ts"`
	NodeID    string    `json:"node_id,omitempty"`
	Message   string    `json:"message"`
}

ActivityError is an error entry extracted from the activity log.

type ActivityLogScan

type ActivityLogScan struct {
	Path          string
	SecureUsed    bool
	Entries       []ActivityEntry
	InjectedLines int
	TotalLines    int
	SentinelLines int
}

ActivityLogScan is the structured result of reading an activity log. Path is the on-disk location read; SecureUsed reflects whether the integrity-protected secure log was the source; InjectedLines counts non-sentinel lines observed when reading from the secure path (always 0 when SecureUsed is false — legacy/snapshot files don't carry the runtime sentinel, so absence is not a signal).

func ScanActivityLog

func ScanActivityLog(runDir string) (*ActivityLogScan, error)

ScanActivityLog is LoadActivityLog with tamper-detection counters exposed for callers (e.g. Diagnose) that need to surface injection signals. Lines without the runtime sentinel prefix in the secure file count toward InjectedLines; the line is still parsed best-effort so its content is visible to forensics.

type AuditConfig

type AuditConfig struct {
	// LogWriter receives non-fatal warnings (unreadable activity.jsonl
	// in a run directory, etc.). Nil is treated as io.Discard so
	// embedded library callers do not see warnings on os.Stderr. The
	// tracker CLI sets this to io.Discard for user-facing commands.
	LogWriter io.Writer
}

AuditConfig configures an Audit() or ListRuns() call.

type AuditReport

type AuditReport struct {
	RunID string `json:"run_id"`
	// Status is one of:
	//   - "success"
	//   - "fail"
	//   - "budget_exceeded"
	//   - "validation_overridden"
	//   - "paused_billing"
	// The enum is open — future minor releases may add new values. Consumers
	// should use StatusClass for stable {succeeded|failed|paused} bucketing.
	// See classifyStatus for the resolution algorithm.
	Status string `json:"status"`
	// StatusClass is one of "succeeded", "failed", or "paused" — stable
	// companion to Status for downstream consumers that need bucket
	// categorization that survives future enum extensions. "paused" flags the
	// recoverable, resumable paused_billing terminal (#514); everything else is
	// classified via pipeline.TerminalStatus(Status).IsSuccess().
	StatusClass string `json:"status_class"`
	// TotalDuration is encoded as integer nanoseconds in JSON
	// ("total_duration_ns"), not as a duration string.
	TotalDuration   time.Duration   `json:"total_duration_ns"`
	Timeline        []TimelineEntry `json:"timeline"`
	Retries         []RetryRecord   `json:"retries,omitempty"`
	Errors          []ActivityError `json:"errors,omitempty"`
	Recommendations []string        `json:"recommendations,omitempty"`
	// CompletedNodes is the number of completed nodes recorded in checkpoint.json.
	CompletedNodes int `json:"completed_nodes"`
	// RestartCount is the checkpoint restart counter for the run.
	RestartCount int `json:"restart_count"`
	// CheckpointTimestamp is the last checkpoint write time.
	CheckpointTimestamp time.Time `json:"checkpoint_timestamp"`
	// BundleIdentity is the content-addressed identity ("sha256:<hex>") of
	// the .dipx bundle the run was executed against. Read from the run's
	// checkpoint. Empty for runs from a plain .dip file.
	BundleIdentity string `json:"bundle_identity,omitempty"`
	// ValidationOverrides is populated when one or more override edges were
	// traversed during the run. Sourced from activity-log
	// EventValidationOverridden entries (chronological order); falls back to
	// Checkpoint.ValidationOverrides when the activity log carries no
	// override entries. Empty for runs with no override edges.
	ValidationOverrides []pipeline.OverrideDetail `json:"validation_overrides,omitempty"`
	// OverrideCount is len(ValidationOverrides). Kept as its own field so
	// thin consumers can read it without unmarshaling the slice.
	OverrideCount int `json:"override_count,omitempty"`
}

AuditReport is the structured result of Audit().

func Audit

func Audit(ctx context.Context, runDir string) (*AuditReport, error)

Audit reads checkpoint.json and activity.jsonl under runDir and returns a structured report.

The runDir argument must be a trusted path — Audit reads checkpoint.json and activity.jsonl directly under it. For user-supplied input, resolve the path via ResolveRunDir or use MostRecentRunID first, which enforce the .tracker/runs/<runID> layout.

ctx is checked at entry so a caller that passes an already-cancelled context gets an immediate error instead of silent work. Full cancellation mid-parse would require threading ctx through pipeline.LoadCheckpoint and LoadActivityLog, which is out of scope today (both are fast and bounded). Nil is coalesced to context.Background().

Audit does not accept AuditConfig — it emits no warnings to suppress. Use ListRuns + AuditConfig{LogWriter} for bulk enumeration where the summary builder may skip unreadable activity logs.

type BudgetHalt

type BudgetHalt struct {
	TotalTokens   int     `json:"total_tokens"`
	TotalCostUSD  float64 `json:"total_cost_usd"`
	WallElapsedMs int64   `json:"wall_elapsed_ms"`
	Message       string  `json:"message"`
}

BudgetHalt holds information about a budget halt detected in the activity log.

type CaptureConfig added in v0.51.0

type CaptureConfig struct {
	// Source is the .dip verbatim, stored as a spec artifact (workflow.dip).
	// Left empty on the Run/NewEngineWithContext path, the library fills it with
	// the source string it parsed.
	Source string
	// SourcePath is where Source was read from, recorded for display.
	SourcePath string
	// Workflow is the expanded IR, stored as workflow.ir.json. dippin expands
	// subgraphs at compile time, so the expanded IR — not the authored source
	// alone — is what explains the run's events. Left nil on the
	// Run/NewEngineWithContext path, the library fills it by parsing.
	Workflow *ir.Workflow
	// CaptureRawLLM stores raw provider streaming chunks in activity.jsonl. Off
	// by default: per-token raw chunks are debugging payload, not run telemetry.
	// The CLI wires this from --verbose.
	CaptureRawLLM bool
	// ForcedBundleMismatch, when set, makes capture write a
	// bundle_mismatch_forced audit entry at run-start (before any engine event),
	// recording that a resume proceeded against a different .dipx bundle than its
	// checkpoint claimed. Nil for new runs and matched resumes.
	ForcedBundleMismatch *ForcedBundleMismatch
}

CaptureConfig turns on run capture for a library run. Set Config.Capture to a (possibly empty) value and the library produces the same artifacts the CLI does — run.json, the spec (source + expanded IR + redacted params), and an identity-bearing activity.jsonl — without the caller wiring a JSONLEventHandler across EventHandler, AgentEvents, and LLMTrace, and without tripping the SessionOwned double-log trap that hand-wiring invites.

The zero value is enough: on the Run / NewEngineWithContext path the library fills Source and Workflow from the source it parses. The raw EventHandler/AgentEvents/LLMTrace seams keep working alongside capture — the capture handler is combined with whatever the caller also set.

type CheckDetail

type CheckDetail struct {
	Status  CheckStatus `json:"status"` // "ok" | "warn" | "error" | "hint"
	Message string      `json:"message"`
	Hint    string      `json:"hint,omitempty"`
}

CheckDetail is one sub-line within a CheckResult — used for per-item status lines (per-provider, per-binary, per-subdirectory).

type CheckResult

type CheckResult struct {
	Name    string        `json:"name"`
	Status  CheckStatus   `json:"status"` // "ok" | "warn" | "error" | "skip"
	Message string        `json:"message,omitempty"`
	Hint    string        `json:"hint,omitempty"`
	Details []CheckDetail `json:"details,omitempty"`
}

CheckResult is one section of a DoctorReport.

type CheckStatus

type CheckStatus string

CheckStatus is the status of a CheckResult or CheckDetail. Enum-like typed string so consumers can switch-exhaust. "hint" is only valid on CheckDetail.Status (informational sub-items such as optional providers not configured).

const (
	CheckStatusOK    CheckStatus = "ok"
	CheckStatusWarn  CheckStatus = "warn"
	CheckStatusError CheckStatus = "error"
	CheckStatusSkip  CheckStatus = "skip"
	CheckStatusHint  CheckStatus = "hint"
)

CheckStatus values.

type Config

type Config struct {
	WorkingDir    string // default: os.Getwd()
	CheckpointDir string // checkpoint file path (checkpoint.json); default: empty (engine auto-generates)
	ResumeRunID   string // optional: resume a previous run by ID or unique prefix, resolved in ArtifactDir (default <WorkingDir>/.tracker/runs); see ResolveCheckpoint
	// ResumeFrom names a node to re-enter a resumed run at (#651, `tracker -r
	// <id> --from <node>`): the node and everything downstream are
	// un-completed and re-run. It must exist and have been reached
	// (completed, or the checkpoint's current node); otherwise Run fails
	// closed before any node runs. Only meaningful with ResumeRunID /
	// CheckpointDir.
	ResumeFrom string
	// ResumeExact disables the automatic resume rewind (#651, `tracker -r
	// --resume-no-rewind`). By default a run that halted at a node it reached
	// by fail-routing (a `when ctx.outcome = fail` edge, `on_failure`, or an
	// exhausted retry's fallback — e.g. build_product's AbortRun terminal)
	// resumes at the node that FAILED, so the step is retried with its cause
	// presumably fixed. ResumeExact re-enters at the checkpoint's current
	// node instead (the terminal re-runs and the run fails again).
	ResumeExact bool
	ArtifactDir string // default: empty (engine auto-generates)
	// GitArtifacts, when true, makes the artifact dir a git repo and commits
	// after every terminal node outcome (the basis for branch-per-run / PR
	// delivery and portable ExportBundle history). Requires git in PATH and is
	// a no-op unless ArtifactDir is set. Off by default.
	GitArtifacts bool
	Format       string // "dip" (default), "dot" (deprecated); empty = auto-detect
	// Source says where the source string came from so its *_file directives
	// resolve correctly: Path for an on-disk file (sidecars next to it),
	// Builtin for an embedded built-in (sidecars from the embed FS).
	// ResolveSource's WorkflowInfo.Ref() produces the right value. Zero value:
	// see SourceRef for the fallback order.
	Source       SourceRef
	Model        string                        // default: env or claude-sonnet-4-6; graph-level attrs take precedence
	Provider     string                        // default: auto-detect from env
	RetryPolicy  string                        // "none" (default), "standard", "aggressive"; graph-level attrs take precedence
	EventHandler pipeline.PipelineEventHandler // optional: live pipeline events
	AgentEvents  agent.EventHandler            // optional: live agent session events
	// LLMTrace attaches a raw-trace observer to the auto-created client or a
	// *llm.Client passed as LLMClient. A custom agent.Completer carries no
	// observable transport, so LLMTrace is a no-op there (as is TokenTracker).
	LLMTrace  llm.TraceObserver // optional: raw LLM trace events
	LLMClient agent.Completer   // optional: override auto-created client
	// TokenTracker optionally injects the per-provider token/cost tracker instead
	// of the engine creating its own. An in-process transport that renders spend
	// (the TUI) shares one tracker between its view model and the engine. The
	// engine attaches it idempotently — re-adding the same tracker to a supplied
	// *llm.Client is skipped, so usage is never double-counted.
	TokenTracker *llm.TokenTracker
	Context      map[string]string // optional: initial pipeline context
	Params       map[string]string // optional: override declared workflow params (keys without "params." prefix)
	// Inputs supplies values for the workflow's declared `inputs` signature
	// (#553). Validated against the schema at run start; a missing required
	// input or a type/constraint violation fails the run before any node
	// executes. Empty for pipelines that declare no inputs. See DescribeInputs
	// / ValidateInputs to pre-check a request before calling Run.
	Inputs []Input
	// Subgraphs are pre-loaded child graphs keyed by subgraph_ref, for a graph
	// with subgraph nodes; nil/empty for flat pipelines. See NewEngineFromGraph.
	Subgraphs      map[string]*pipeline.Graph
	Backend        string                      // "native" (default), "claude-code", "acp"; selects agent backend
	ToolSafety     *handlers.ToolHandlerConfig // tool-handler security overrides (denylist/allowlist, output limits, env passthrough); nil = registry defaults
	Autopilot      string                      // "" (interactive), "lax", "mid", "hard", "mentor"; LLM-driven gate decisions
	AutopilotModel string                      // model native autopilot asks for at each gate; "" = the default provider's autopilot model; claude-code autopilot ignores it
	AutoApprove    bool                        // auto-approve all human gates with default/first option
	Budget         pipeline.BudgetLimits       // configures pipeline-level token, cost, and wall-time ceilings
	// GatewayURL is the root URL of a Cloudflare AI Gateway (or any compatible
	// proxy). When non-empty it is used as the base for all provider URLs, with
	// the per-provider suffix appended (e.g. "<gateway>/anthropic"). A
	// per-provider *_BASE_URL env var always takes precedence over GatewayURL so
	// library callers can still override individual providers. The TRACKER_GATEWAY_URL
	// env var is the fallback when GatewayURL is empty.
	GatewayURL string
	// GatewayKind selects the path convention used with GatewayURL (or its
	// TRACKER_GATEWAY_URL env-var fallback). Empty or GatewayKindCFAIG
	// (default, backcompat) routes via Cloudflare AI Gateway conventions:
	// /anthropic, /openai, /google-ai-studio, /compat. GatewayKindBedrock
	// targets the 2389 bedrock-gateway Worker which uses native SDK paths.
	// The TRACKER_GATEWAY_KIND env var is the fallback when GatewayKind is
	// empty. See ResolveProviderBaseURL.
	GatewayKind GatewayKind
	// Interviewer optionally injects a custom in-process human-gate handler.
	// This is the seam for interactive transports (TUI, Slack, web, mobile):
	// the transport implements handlers.Interviewer and, optionally, the richer
	// handlers.FreeformInterviewer / LabeledFreeformInterviewer /
	// InterviewInterviewer extensions plus the optional Actor() / Cancel() /
	// ContextSetter side-interfaces — the human handler upgrades via type
	// assertion and picks the richest supported mode. When set, it takes
	// precedence over AutoApprove, WebhookGate, and Autopilot. Nil is a no-op.
	Interviewer handlers.Interviewer
	WebhookGate *WebhookGateConfig // optional: post human gates to an HTTP webhook and wait for callback
	// BundleIdentity is the content-addressed identity ("sha256:<hex>") of
	// the .dipx bundle this run was loaded from. Stamped onto every emitted
	// PipelineEvent and persisted to the checkpoint for resume verification.
	// Empty (the default) is a no-op and matches plain .dip behavior.
	//
	// Callers that build their own JSONLEventHandler should also call
	// activityLog.SetBundleIdentity(cfg.BundleIdentity) so agent/llm writes
	// outside the engine event chain carry the same provenance.
	BundleIdentity string
	// Git configures the v0.29.0 git preflight check. Nil = auto, which
	// respects the workflow's `requires:` block. See GitConfig.
	Git *GitConfig
	// SteeringChan optionally injects mid-run context updates from an external
	// supervisor (a chat "steer" command, a web control, a manager loop). Each
	// map sent on it is merged into the pipeline context between node
	// executions, so steered values are visible to the next node's edge
	// selection and prompt expansion. Values are namespaced by the sender
	// (e.g. "steer.guidance"); a workflow references them like any context key.
	// Nil disables steering. The channel is drained non-blockingly — sends never
	// block the engine, and updates surface at the next inter-node boundary.
	SteeringChan <-chan map[string]string
	// Capture, when set, makes the library produce the same on-disk run capture
	// the CLI does — run.json, spec artifacts, and an identity-bearing
	// activity.jsonl — without the caller hand-wiring a JSONLEventHandler across
	// EventHandler/AgentEvents/LLMTrace (and without the SessionOwned double-log
	// trap that invites). It combines with those seams when they are also set.
	// See CaptureConfig (tracker_capture.go); an empty value suffices on the
	// Run/NewEngineWithContext path. Nil disables it.
	Capture *CaptureConfig
}

Config controls pipeline execution. All fields are optional. Zero-value Config uses environment variables for LLM credentials, the current working directory, and auto-generated run directories.

type CostReport

type CostReport struct {
	TotalUSD   float64
	ByProvider map[string]llm.ProviderCost
	LimitsHit  []string
}

CostReport summarizes spend for a pipeline run. TotalUSD is the sum of ByProvider[*].USD. LimitsHit names the budget dimensions that halted the run (empty when the run completed normally).

type DiagnoseConfig

type DiagnoseConfig struct {
	// LogWriter receives non-fatal parse/read warnings — specifically
	// malformed status.json content (one warning per bad file) and
	// bufio.Scanner errors while reading activity.jsonl (e.g. lines
	// exceeding the 1 MB buffer limit, I/O failures). Nil is treated
	// as io.Discard so library callers do not see stray warnings on
	// os.Stderr. The tracker CLI sets this to io.Discard for user-
	// facing commands.
	LogWriter io.Writer
}

DiagnoseConfig configures a Diagnose() run.

type DiagnoseReport

type DiagnoseReport struct {
	RunID          string      `json:"run_id"`
	CompletedNodes int         `json:"completed_nodes"`
	BudgetHalt     *BudgetHalt `json:"budget_halt,omitempty"`
	// ValidationOverrides is the list of override edges traversed during the run.
	// Sourced from activity log first, with checkpoint fallback for runs whose
	// activity log is missing (legacy / archived). Empty for runs with no
	// override edges. Populated for every terminal status (success, fail,
	// budget_exceeded, validation_overridden) so override forensics survive
	// failure-after-override scenarios. Per spec §9.4 the override section in
	// `tracker diagnose` is informational only — override is NOT a failure and
	// does NOT raise a Suggestion of its own.
	ValidationOverrides []pipeline.OverrideDetail `json:"validation_overrides,omitempty"`
	// OverrideCount is len(ValidationOverrides). Kept as its own field so
	// JSON consumers can branch without iterating the slice.
	OverrideCount int           `json:"override_count,omitempty"`
	Failures      []NodeFailure `json:"failures"`
	Suggestions   []Suggestion  `json:"suggestions"`
	// RestartBudgetResets lists every restart_budget_reset event (#643) in
	// log order. Informational (no Suggestion); see RestartBudgetReset.
	RestartBudgetResets []RestartBudgetReset `json:"restart_budget_resets,omitempty"`
	// BackendResolved is what backend: auto resolved to (claude-code or
	// native) when the run used it. Informational.
	BackendResolved string `json:"backend_resolved,omitempty"`
}

DiagnoseReport is the structured output of Diagnose / DiagnoseMostRecent.

func Diagnose

func Diagnose(ctx context.Context, runDir string, opts ...DiagnoseConfig) (*DiagnoseReport, error)

Diagnose analyzes a run directory and returns a structured report.

The runDir argument must be a trusted path — Diagnose reads checkpoint.json, activity.jsonl, and every <nodeID>/status.json under it. For user-supplied input, resolve the path via ResolveRunDir or DiagnoseMostRecent first, which enforce the .tracker/runs/<runID> layout.

If ctx is cancelled mid-parse, Diagnose returns ctx.Err() — a partial report is never returned as a success, so callers using deadlines can distinguish complete from truncated analysis. A nil ctx is treated as context.Background() (no cancellation possible).

Example
package main

import (
	"context"
	"fmt"

	tracker "github.com/2389-research/tracker"
)

func main() {
	report, err := tracker.Diagnose(context.Background(), "testdata/runs/failed")
	if err != nil {
		fmt.Println("diagnose failed")
		return
	}
	if len(report.Failures) == 0 {
		fmt.Println("no failures")
		return
	}
	fmt.Println("failed node:", report.Failures[0].NodeID)
	fmt.Println("retry count:", report.Failures[0].RetryCount)
}
Output:
failed node: Build
retry count: 2

func DiagnoseMostRecent

func DiagnoseMostRecent(ctx context.Context, workdir string, opts ...DiagnoseConfig) (*DiagnoseReport, error)

DiagnoseMostRecent finds the most recent run under workdir and diagnoses it.

type DoctorConfig

type DoctorConfig struct {
	// WorkDir is the working directory to check. If empty, os.Getwd() is used.
	WorkDir string
	// Backend is the agent backend ("", "native", "claude-code"). When
	// "claude-code", a missing claude binary is a hard error.
	Backend string
	// ProbeProviders, when true, makes a minimal network call to each
	// configured provider to verify auth. Default false — key presence only.
	ProbeProviders bool
	// PipelineFile, when non-empty, adds a "Pipeline File" check that parses
	// and validates the given .dip / .dot file.
	PipelineFile string
	// contains filtered or unexported fields
}

DoctorConfig configures a Doctor() run.

type DoctorOption

type DoctorOption func(*DoctorConfig)

DoctorOption configures a Doctor run via a functional option.

func WithDoctorGraphs added in v0.81.0

func WithDoctorGraphs(graph *pipeline.Graph, subgraphs map[string]*pipeline.Graph) DoctorOption

WithDoctorGraphs gives Doctor the already-loaded pipeline graph and its subgraph children, so the backend checks see every agent node. Without it Doctor parses the top-level PipelineFile itself and, if that calls a subgraph it can't see, assumes the children need the native backend.

func WithGitConfig

func WithGitConfig(policy GitPreflight, allowInit bool) DoctorOption

WithGitConfig sets the git preflight policy considered by the Git Requires check. Library callers that don't set this get GitPreflightAuto behavior (respect workflow `requires:`). CLI doctor mode passes --git/--allow-init through this option.

func WithVersionInfo

func WithVersionInfo(version, commit string) DoctorOption

WithVersionInfo attaches a tracker version and commit hash for display in the "Version Compatibility" check. CLI callers populate these from build-time ldflags; library callers typically do not need this.

type DoctorReport

type DoctorReport struct {
	Checks   []CheckResult `json:"checks"`
	OK       bool          `json:"ok"`
	Warnings int           `json:"warnings"`
	Errors   int           `json:"errors"`
}

DoctorReport is the structured result of a Doctor() call.

func Doctor

func Doctor(ctx context.Context, cfg DoctorConfig, opts ...DoctorOption) (*DoctorReport, error)

Doctor runs a suite of preflight checks and returns a structured report.

By default Doctor makes no network calls: provider configuration is detected via env-var presence and basic format validation. Set cfg.ProbeProviders = true to additionally make a 1-token API call per provider to verify auth. The CLI's "tracker doctor" command sets that flag; library callers should leave it false unless they specifically want live credential verification.

Provider probes and binary version lookups honor ctx: cancelling the context aborts in-flight checks. A nil context is treated as context.Background().

Write side effects (gitignore fix-up, workdir creation prompts) are NOT performed by Doctor — callers inspect the report and apply any fixes themselves.

Example
package main

import (
	"context"
	"fmt"
	"os"

	tracker "github.com/2389-research/tracker"
)

func main() {
	workDir, err := os.MkdirTemp("", "tracker-example-doctor-*")
	if err != nil {
		fmt.Println("doctor failed")
		return
	}
	defer os.RemoveAll(workDir)

	report, err := tracker.Doctor(context.Background(), tracker.DoctorConfig{
		WorkDir: workDir,
	})
	if err != nil {
		fmt.Println("doctor failed")
		return
	}
	fmt.Println("checks:", len(report.Checks) > 0)
}
Output:
checks: true

type DuplicateTestGroup added in v0.47.0

type DuplicateTestGroup struct {
	Kind  string         `json:"kind"` // "identical" | "near-identical"
	Tests []TestLocation `json:"tests"`
}

DuplicateTestGroup is a set of test functions sharing a body.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine wraps pipeline.Engine with auto-wired internals.

func NewEngine

func NewEngine(source string, cfg Config) (*Engine, error)

NewEngine parses a pipeline source (.dip preferred, DOT deprecated), auto-wires all internals, and returns an Engine. Format is auto-detected from content if Config.Format is empty: sources starting with "digraph" or "strict digraph" are treated as DOT, everything else as .dip. The caller must call Close() when done to release resources.

This wrapper exists for backward compatibility: it uses context.Background() for the v0.29.0 git preflight. New callers that want to support cancellation of the preflight (especially with `--git=init` which has a `git init` side effect) should use NewEngineWithContext instead.

func NewEngineFromGraph added in v0.46.0

func NewEngineFromGraph(ctx context.Context, graph *pipeline.Graph, cfg Config) (*Engine, error)

NewEngineFromGraph assembles an Engine from an already-parsed graph, skipping source parsing. Use it when the caller has loaded the graph itself and, for a pipeline with subgraph nodes, resolved the subgraph_ref files into Config.Subgraphs — as the CLI does. Otherwise identical to NewEngineWithContext (validate → git preflight → resume → client → assemble).

func NewEngineWithContext

func NewEngineWithContext(ctx context.Context, source string, cfg Config) (*Engine, error)

NewEngineWithContext is the context-aware form of NewEngine. The supplied ctx threads into the v0.29.0 git preflight check; a canceled context aborts preflight (including the `--git=init` `git init` side effect) rather than letting it complete before Engine.Run(ctx) observes the cancellation. tracker.Run(ctx, ...) calls this form so library callers who pass a real ctx get end-to-end cancellation coverage.

func (*Engine) Close

func (e *Engine) Close() error

Close releases resources. Must be called if the engine was created with NewEngine. Safe for concurrent use; idempotent.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context) (*Result, error)

Run executes the pipeline to completion.

func (*Engine) TokenTracker added in v0.46.0

func (e *Engine) TokenTracker() *llm.TokenTracker

TokenTracker returns the per-provider token/cost tracker attached to this engine's run. A transport that renders spend in-process (e.g. the TUI status bar and per-node cost) shares this rather than reconstructing usage from the event stream. Always non-nil for a successfully constructed engine (it reports zeros when no LLM client is attached, e.g. --backend claude-code without keys).

type FailureCause added in v0.47.0

type FailureCause struct {
	Kind      string // "billing" | "auth" | "rate_limit" | "context_length" | "network" | "timeout" | "config" | "content_filter" | "generic"
	Icon      string
	Title     string // the headline — the most important, actionable thing
	Detail    string // the underlying cause (e.g. the provider message), may be ""
	NextSteps string // what the user should do, may be ""
}

FailureCause is a human-first explanation of why a run failed: an icon + title to lead with, the underlying cause, and the concrete next step. Renderers show Title first and keep node/stage context secondary — the opposite of the raw "handler error at node X" wrapper.

func ClassifyFailure added in v0.47.0

func ClassifyFailure(err error) FailureCause

ClassifyFailure maps a terminal run error to a FailureCause. It unwraps the node/handler wrappers to find the real cause (a typed provider error, a billing signal, etc.) and returns a generic "Run failed" carrying the raw message when nothing more specific matches. err==nil yields the zero value.

type ForcedBundleMismatch added in v0.51.0

type ForcedBundleMismatch struct {
	RunID            string // resume run ID (needed to open the correct activity log)
	OriginalIdentity string // identity stored in the checkpoint ("sha256:<hex>" or "")
	CurrentIdentity  string // identity of the bundle actually executed ("sha256:<hex>" or "")
}

ForcedBundleMismatch records that a resume ran against a bundle whose content-addressed identity differs from the one stored in the checkpoint, allowed through by an explicit override. Both identities are preserved so a post-hoc auditor sees exactly what was overridden.

type GatewayKind added in v0.36.0

type GatewayKind string

GatewayKind selects the path convention used when TRACKER_GATEWAY_URL is set. The default (cf-aig) matches Cloudflare AI Gateway's per-provider subpath convention; bedrock targets the 2389 bedrock-gateway Worker which uses native SDK URL paths.

See docs/superpowers/specs/2026-06-01-issue-274-bedrock-gateway-integration-design.md.

const (
	// GatewayKindCFAIG routes via Cloudflare AI Gateway path conventions:
	// /anthropic, /openai, /google-ai-studio, /compat. Default.
	GatewayKindCFAIG GatewayKind = "cf-aig"

	// GatewayKindBedrock routes via the 2389 bedrock-gateway Worker which
	// translates SDK requests to AWS Bedrock Converse. Uses native SDK
	// URL conventions: empty suffix for Anthropic, /v1 for OpenAI and
	// Gemini. openai-compat is not supported on this gateway.
	GatewayKindBedrock GatewayKind = "bedrock"
)

type GitConfig

type GitConfig struct {
	Preflight GitPreflight
	AllowInit bool
}

GitConfig configures the git preflight check that runs before any node executes. Zero value (or nil *GitConfig on Config.Git) resolves to GitPreflightAuto, which respects the workflow's `requires:` block.

AllowInit is required when Preflight == GitPreflightInit and stdin is not a TTY — it is the second safety latch on automatic `git init`.

type GitPreflight

type GitPreflight = pipeline.GitPreflight

GitPreflight is the resolved preflight policy that controls the v0.29.0 git environment check. Type alias to pipeline.GitPreflight so callers don't have to import the pipeline package for this single value.

func ResolveGitConfig

func ResolveGitConfig(cfg Config) (GitPreflight, bool)

ResolveGitConfig returns the (policy, allowInit) pair to apply for this run, considering Config.Git. The zero value resolves to (auto, false).

type Input added in v0.58.0

type Input struct {
	Name      string
	String    string // text/number/bool/enum — canonical string form
	FilePath  string // file: path to read and stage
	FileBytes []byte // file: inline contents to stage (mutually exclusive with FilePath)
}

Input is a caller-supplied value for a declared workflow input. Use the constructors: StringInput for text/number/bool/enum, FileInput / FileInputBytes for a file input (staged into the run dir at <workDir>/.tracker/inputs/<name>).

func FileInput added in v0.58.0

func FileInput(name, path string) Input

FileInput builds a file input from a path; its contents are staged at run start.

func FileInputBytes added in v0.58.0

func FileInputBytes(name string, contents []byte) Input

FileInputBytes builds a file input from inline contents (for values that arrive over the wire rather than as a local path).

func SecretInput added in v0.60.0

func SecretInput(name, value string) Input

SecretInput builds a secret input. The value is staged to a 0600 file at run start and ${inputs.<name>} resolves to that PATH, never the value — so the secret stays out of prompts, the provider wire, the trace, and the checkpoint. Read it from the staged path in a tool (e.g. API_KEY=$(cat "$path")); never interpolate ${inputs.<name>} expecting the value.

func StringInput added in v0.58.0

func StringInput(name, value string) Input

StringInput builds a value input. Numbers and bools pass their canonical string form ("42", "true"); validation coerces and checks them.

type InputValidationError added in v0.58.0

type InputValidationError struct {
	Errors []pipeline.InputError
}

InputValidationError is returned by Run/NewEngineFromGraph when supplied inputs fail validation. It carries the structured per-input failures so a host can re-prompt precisely.

func (*InputValidationError) Error added in v0.58.0

func (e *InputValidationError) Error() string

type ManagedRun added in v0.46.0

type ManagedRun struct {
	Key     string // caller-chosen external id (e.g. a Slack thread_ts)
	WorkDir string // isolated working directory for this run
	// contains filtered or unexported fields
}

ManagedRun is a single run owned by a RunManager. Safe for concurrent reads.

func (*ManagedRun) Done added in v0.46.0

func (m *ManagedRun) Done() <-chan struct{}

Done is closed when the run reaches a terminal state.

func (*ManagedRun) Result added in v0.46.0

func (m *ManagedRun) Result() (*Result, error)

Result returns the run's result and error once it has finished. Before the run is Done, it returns (nil, nil). After Done it mirrors tracker.Run: a run that produced a terminal result has a non-nil *Result (with RunID and a terminal Status) — accompanied by a non-nil error for handler-error, strict-failure, paused (see RunPaused), or cancelled exits. A non-nil error therefore does not by itself mean the run failed; read State(). Only an init/invariant failure before any terminal result yields (nil, err).

func (*ManagedRun) ResumeRunID added in v0.47.0

func (m *ManagedRun) ResumeRunID() string

ResumeRunID returns the tracker run id to resume from when the run stopped in a recoverable paused state (State() == RunPaused), else "". Pass it as Config.ResumeRunID (with this run's WorkDir and the same Config.ArtifactDir it used, #696) to continue from the paused node. If the run was started with an explicit Config.CheckpointDir, resume with that same CheckpointDir instead: a set CheckpointDir is loaded as-is and bypasses ResumeRunID resolution, and such a run writes no secure or artifact-dir checkpoint copy for ResumeRunID to find. Capture it before Forget() drops the run.

func (*ManagedRun) RunID added in v0.46.0

func (m *ManagedRun) RunID() string

RunID returns the tracker run id once the run has finished, else "".

func (*ManagedRun) State added in v0.46.0

func (m *ManagedRun) State() RunState

State returns the run's current lifecycle state.

type NDJSONWriter

type NDJSONWriter struct {
	// contains filtered or unexported fields
}

NDJSONWriter is a thread-safe writer that serializes StreamEvents line by line onto an io.Writer. Library consumers use it to produce the same stream as the tracker CLI's --json mode.

Backpressure note: Write holds an internal mutex for the duration of the underlying io.Writer.Write call. When three handler sources (pipeline, agent, LLM trace) share one writer, a slow backing writer serializes handler callbacks across those sources. If the backing writer can block (network socket, pipe), wrap it in a bufio.Writer or a channel-backed forwarder to decouple producers from the slow sink.

func NewNDJSONWriter

func NewNDJSONWriter(w io.Writer) *NDJSONWriter

NewNDJSONWriter returns a new writer backed by w.

Example
package main

import (
	"bytes"
	"fmt"

	tracker "github.com/2389-research/tracker"
)

func main() {
	var buf bytes.Buffer
	w := tracker.NewNDJSONWriter(&buf)
	_ = w.Write(tracker.StreamEvent{
		Timestamp: "2026-04-17T10:00:00.000Z",
		Source:    "pipeline",
		Type:      "pipeline_started",
		RunID:     "run-123",
	})
	fmt.Println(buf.Len() > 0)
}
Output:
true

func (*NDJSONWriter) AgentHandler

func (s *NDJSONWriter) AgentHandler() agent.EventHandler

AgentHandler returns an agent.EventHandler that writes agent events to this stream. Panics in the underlying writer are recovered (see PipelineHandler).

func (*NDJSONWriter) PipelineHandler

func (s *NDJSONWriter) PipelineHandler() pipeline.PipelineEventHandler

PipelineHandler returns a pipeline.PipelineEventHandler that writes events to this stream. Panics in the underlying writer are recovered and logged to os.Stderr once (per writer instance) so a misbehaving sink cannot crash the pipeline goroutine.

func (*NDJSONWriter) TraceObserver

func (s *NDJSONWriter) TraceObserver() llm.TraceObserver

TraceObserver returns an llm.TraceObserver that writes trace events to this stream. Panics in the underlying writer are recovered (see PipelineHandler).

func (*NDJSONWriter) Write

func (s *NDJSONWriter) Write(evt StreamEvent) error

Write serializes evt as a JSON line. Safe to call from multiple goroutines. Returns a non-nil error if marshalling or writing to the underlying io.Writer fails, including short writes (io.Writer.Write may legally return n < len(data) with a nil error). The first write error is also logged to os.Stderr once so long-running callers that ignore the return value still surface it.

type NameAttribution added in v0.72.0

type NameAttribution struct {
	Test   TestLocation `json:"test"`
	Symbol string       `json:"symbol"`
}

NameAttribution is an advisory name-vs-coverage mismatch.

type NodeFailure

type NodeFailure struct {
	NodeID  string `json:"node_id"`
	Outcome string `json:"outcome"`
	Handler string `json:"handler,omitempty"`
	// Duration is the elapsed time for the most recent attempt of the node,
	// encoded as integer nanoseconds in JSON ("duration_ns"), not a string.
	Duration time.Duration `json:"duration_ns,omitempty"`
	// RetryCount is the number of stage_failed events observed for this node
	// — i.e., the total failure count, not "retries beyond the first attempt."
	// A node that failed once (no retry) has RetryCount == 1.
	RetryCount int `json:"retry_count,omitempty"`
	// IdenticalRetries is true when every stage_failed event had the same
	// error/tool_error signature — a deterministic bug, not a flaky one.
	IdenticalRetries bool     `json:"identical_retries,omitempty"`
	Stdout           string   `json:"stdout,omitempty"`
	Stderr           string   `json:"stderr,omitempty"`
	Errors           []string `json:"errors,omitempty"`
	ReachedFrom      string   `json:"reached_from,omitempty"` // node whose failure routed the run here (#650); see withFallbackOrigins
	ReachedVia       string   `json:"reached_via,omitempty"`  // how (#654): "fail_edge" for an authored `when fail` edge, else the fallback kind; empty pre-#651
}

NodeFailure captures everything known about a failed node.

type PlanStep

type PlanStep struct {
	Step   int       `json:"step"`
	NodeID string    `json:"node_id"`
	Edges  []SimEdge `json:"edges,omitempty"`
}

PlanStep is one step in an execution plan.

type ReimplementedFinding added in v0.72.0

type ReimplementedFinding struct {
	Helper           TestLocation `json:"helper"`
	ProductionSymbol string       `json:"production_symbol"`
	ProductionFile   string       `json:"production_file"`
	ProductionLine   int          `json:"production_line"`
	Similarity       float64      `json:"similarity"`
}

ReimplementedFinding is a test-local function that mirrors an uncovered production function (heuristic 3, advisory).

type RestartBudgetReset added in v0.74.0

type RestartBudgetReset struct {
	// NodeID is the nested restart target whose budget was reset.
	NodeID string `json:"node_id"`
	// ResetBy is the enclosing loop header whose restart triggered the reset.
	ResetBy string `json:"reset_by"`
	// PreviousCount is the per-target restart count before the reset (0 when
	// only the fallback latch was cleared).
	PreviousCount int `json:"previous_count"`
	// FallbackLatchCleared is true when the node's one-shot fallback latch was
	// re-armed by the same reset.
	FallbackLatchCleared bool `json:"fallback_latch_cleared,omitempty"`
}

RestartBudgetReset is one restart_budget_reset activity entry (#643): an enclosing loop header restarted and reset a nested target's per-target restart budget (and possibly its one-shot fallback latch). A reset is the engine working as designed, not a failure, so it raises no Suggestion; it is what lets an operator read "milestone loop advanced; TestMilestone budget reset" next to a later `max restarts exceeded` on that same target and know the exhaustion happened within ONE iteration.

type Result

type Result struct {
	RunID string
	// Status carries the run's terminal status. Known values today:
	//   - "success"
	//   - "fail"
	//   - "budget_exceeded"
	//   - "validation_overridden"
	//   - "paused_billing" — recoverable provider credit/quota exhaustion: the
	//     checkpoint is saved and in-flight work preserved, so the run resumes
	//     from the paused node (Config.ResumeRunID / `tracker -r`) once credits
	//     are topped up. Not a failure; offer resume, not a from-scratch redo.
	// The enum is OPEN — future minor releases may add new values, so never
	// switch exhaustively on the raw string. Use
	// pipeline.TerminalStatus(r.Status).IsSuccess() to classify (fail-closed).
	Status           string
	CompletedNodes   []string
	Context          map[string]string
	EngineResult     *pipeline.EngineResult
	Trace            *pipeline.Trace      // full execution trace (nodes, timing, stats)
	TokensByProvider map[string]llm.Usage // per-provider token totals
	ToolCallsByName  map[string]int       // tool call counts by name
	Cost             *CostReport          // per-provider cost rollup; nil when no usage recorded
	// ArtifactRunDir is the run-specific artifact directory (e.g.
	// "<artifactDir>/<runID>"). Populated when WithArtifactDir is set via
	// Config.ArtifactDir. Pass this to ExportBundle to create a portable
	// git bundle of the run's history.
	ArtifactRunDir string
	// BundlePath is the path of the exported git bundle. Populated only when
	// ExportBundle is invoked by the caller after Run completes.
	BundlePath string
	// BundleIdentity is the content-addressed identity ("sha256:<hex>") of
	// the .dipx bundle the run was loaded from, mirrored from Config.BundleIdentity
	// for the caller's convenience. Empty for plain .dip runs.
	BundleIdentity string
	// ValidationOverrides is the list of override edges traversed during the run.
	// Empty for runs with no override edges. Populated for every terminal status
	// (including fail and budget_exceeded) so forensics see overrides even when
	// failure dominates.
	ValidationOverrides []pipeline.OverrideDetail
}

Result contains the outcome of a pipeline execution.

func Run

func Run(ctx context.Context, source string, cfg Config) (*Result, error)

Run parses a pipeline source, auto-wires all internals, executes, and returns the result. This is the one-call convenience function. It handles Close() automatically.

type RetryRecord

type RetryRecord struct {
	NodeID   string `json:"node_id"`
	Attempts int    `json:"attempts"`
}

RetryRecord records how many times a node was retried.

type RunEstimate added in v0.46.0

type RunEstimate struct {
	Steps             int      `json:"steps"`              // execution-plan length
	AgentNodes        int      `json:"agent_nodes"`        // LLM agent nodes
	Models            []string `json:"models"`             // distinct models, sorted
	LowUSD            float64  `json:"low_usd"`            // ~1 turn per agent node
	ExpectedUSD       float64  `json:"expected_usd"`       // a typical fraction of max turns
	HighUSD           float64  `json:"high_usd"`           // max turns per agent node
	SubscriptionNodes int      `json:"subscription_nodes"` // openai agent nodes billed to the ChatGPT subscription ($0 here)
}

RunEstimate is a rough pre-run cost/scale ballpark. It is an ESTIMATE: actual cost depends on how many turns each agent uses and how many times loops run, which can't be known statically — hence the wide Low..High spread. Use it to set expectations and a budget ceiling, not as a precise quote.

func EstimateRun added in v0.46.0

func EstimateRun(ctx context.Context, source string, opts ...SourceOption) (*RunEstimate, error)

EstimateRun parses source and returns a rough cost/scale estimate. It runs the same static simulation as Simulate, then prices each agent node's model. Options (WithSource) are forwarded to Simulate.

type RunManager added in v0.46.0

type RunManager struct {
	// contains filtered or unexported fields
}

RunManager owns multiple concurrent pipeline runs keyed by an external id. It provides the mechanism (isolation, lifecycle, an optional capacity cap) and leaves admission policy (queue vs reject at capacity) to the caller. Safe for concurrent use.

func NewRunManager added in v0.46.0

func NewRunManager(opts ...RunManagerOption) *RunManager

NewRunManager creates a RunManager.

func (*RunManager) Cancel added in v0.46.0

func (rm *RunManager) Cancel(key string) bool

Cancel stops the run for key by cancelling its context. Returns false if no such run is tracked. Cancelling an already-finished run is a no-op.

func (*RunManager) Forget added in v0.46.0

func (rm *RunManager) Forget(key string) bool

Forget drops a finished run from the manager's tracking. Returns false if the run is unknown or still active (active runs cannot be forgotten; Cancel first).

func (*RunManager) Get added in v0.46.0

func (rm *RunManager) Get(key string) (*ManagedRun, bool)

Get returns the managed run for key, if present.

func (*RunManager) List added in v0.46.0

func (rm *RunManager) List() []*ManagedRun

List returns all runs the manager currently tracks, ordered by key.

func (*RunManager) Start added in v0.46.0

func (rm *RunManager) Start(ctx context.Context, key, source string, cfg Config) (*ManagedRun, error)

Start launches source under cfg as a new run keyed by key. The run executes in its own goroutine; ctx bounds its lifetime, so pass a long-lived context (not a short request context) and use Cancel to stop a single run. Returns ErrRunKeyActive if a run with the same key is still active, or ErrAtCapacity if the concurrency cap is reached — the caller decides whether to queue or reject. When cfg.WorkingDir is empty and a workdir base is configured, an isolated per-key directory is created.

type RunManagerOption added in v0.46.0

type RunManagerOption func(*RunManager)

RunManagerOption configures a RunManager.

func WithMaxConcurrent added in v0.46.0

func WithMaxConcurrent(n int) RunManagerOption

WithMaxConcurrent caps the number of simultaneously-active runs. When the cap is reached, Start returns ErrAtCapacity. A value <= 0 means unbounded.

func WithWorkDirBase added in v0.46.0

func WithWorkDirBase(dir string) RunManagerOption

WithWorkDirBase sets a base directory under which each run without an explicit Config.WorkingDir gets its own isolated subdirectory (base/<workdirSegment(key)>). Isolated workdirs keep concurrent runs from colliding on run-state files.

type RunState added in v0.46.0

type RunState string

RunState is the lifecycle state of a managed run.

const (
	RunStarting  RunState = "starting"
	RunRunning   RunState = "running"
	RunSucceeded RunState = "succeeded"
	RunFailed    RunState = "failed"
	RunCanceled  RunState = "canceled"
	// RunPaused is a run that stopped in a recoverable, resumable terminal —
	// today only the engine's paused_billing halt (#487): the checkpoint is
	// saved, in-flight work preserved, and the run continues from the paused
	// node once credits are topped up. It is NOT a failure: an embedder should
	// surface "add credit and resume", not "this run is dead". Feed
	// ManagedRun.ResumeRunID() back as Config.ResumeRunID to resume.
	RunPaused RunState = "paused"
)

The set of states is an OPEN enum — future minor releases may add more (e.g. a blocked-on-human-gate state). Classify with Terminal() and compare against the states you care about; do not switch exhaustively.

func (RunState) Terminal added in v0.46.0

func (s RunState) Terminal() bool

Terminal reports whether the state is a finished state — the run's goroutine has exited, Done() is closed, and Result() is populated.

RunPaused counts as Terminal even though the *work* is unfinished. It is "finished-but-resumable": this process's run really has ended, so a state where Terminal() were false would contradict a closed Done() channel, keep the key locked by the active-key guard in claim() (stranding the run — the caller could never start the resume under the same key), and make Forget() refuse it forever. Resumability is a separate axis from finishedness, surfaced by State() == RunPaused and ResumeRunID(), not by Terminal().

type RunSummary

type RunSummary struct {
	RunID string `json:"run_id"`
	// Status is one of: "success", "fail", "budget_exceeded",
	// "validation_overridden", "paused_billing". Open enum; prefer StatusClass
	// for stable {succeeded|failed|paused} bucketing. See classifyStatus for the
	// resolution algorithm.
	Status string `json:"status"`
	// StatusClass is one of "succeeded", "failed", or "paused" — stable
	// companion to Status. "paused" flags paused_billing (#514); everything else
	// is classified via pipeline.TerminalStatus(Status).IsSuccess().
	StatusClass string    `json:"status_class"`
	Nodes       int       `json:"nodes"`
	Retries     int       `json:"retries"`
	Restarts    int       `json:"restarts"`
	Timestamp   time.Time `json:"timestamp"`
	// Duration is encoded as integer nanoseconds in JSON ("duration_ns"),
	// not as a duration string.
	Duration time.Duration `json:"duration_ns"`
	// FailedAt is the node ID where the run halted, populated for both
	// "fail" and "budget_exceeded" runs (Gap 5.2: budget-halted runs are
	// no longer classified as "fail", so the gate also fires on
	// "budget_exceeded").
	FailedAt string `json:"failed_at,omitempty"`
	// BundleIdentity is the content-addressed identity ("sha256:<hex>") of
	// the .dipx bundle the run was executed against. Read from the run's
	// checkpoint at summary-build time. Empty for runs from a plain .dip file.
	BundleIdentity string `json:"bundle_identity,omitempty"`
	// OverrideCount is the number of override edges traversed in this run.
	// Sourced from activity log when present, else
	// len(Checkpoint.ValidationOverrides). RunSummary stays thin — for the
	// full OverrideDetail slice see AuditReport.ValidationOverrides.
	OverrideCount int `json:"override_count,omitempty"`
}

RunSummary is a condensed view of a single pipeline run for listing.

func ListRuns

func ListRuns(workdir string, opts ...AuditConfig) ([]RunSummary, error)

ListRuns returns all runs under workdir/.tracker/runs, sorted newest first. If the runs directory does not exist, ListRuns returns (nil, nil).

type SimEdge

type SimEdge struct {
	From      string `json:"from"`
	To        string `json:"to"`
	Label     string `json:"label,omitempty"`
	Condition string `json:"condition,omitempty"`
}

SimEdge is an edge in a SimulateReport.

type SimNode

type SimNode struct {
	ID      string            `json:"id"`
	Handler string            `json:"handler,omitempty"`
	Shape   string            `json:"shape,omitempty"`
	Label   string            `json:"label,omitempty"`
	Attrs   map[string]string `json:"attrs,omitempty"`
}

SimNode is a node in a SimulateReport.

type SimulateReport

type SimulateReport struct {
	Format        string            `json:"format"`
	Name          string            `json:"name,omitempty"`
	StartNode     string            `json:"start_node,omitempty"`
	ExitNode      string            `json:"exit_node,omitempty"`
	GraphAttrs    map[string]string `json:"graph_attrs,omitempty"`
	Nodes         []SimNode         `json:"nodes"`
	Edges         []SimEdge         `json:"edges"`
	ExecutionPlan []PlanStep        `json:"execution_plan"`
	Unreachable   []string          `json:"unreachable,omitempty"`
}

SimulateReport is the structured output of a dry-run over a pipeline source. No LLM calls, no side effects — pure graph introspection.

func Simulate

func Simulate(ctx context.Context, source string, opts ...SourceOption) (*SimulateReport, error)

Simulate parses source and returns a SimulateReport. Format is detected from content.

ctx is checked at entry so a caller that passes an already-cancelled context gets an immediate error instead of silent work. Full cancellation mid-parse would require threading ctx through parsePipelineSource → dippin-lang's parser, which is out of scope today (parses are fast and O(n) anyway). Nil is coalesced to context.Background().

Pass WithSource so *_file directives resolve against the source's origin (a file's directory, or the embed FS for a built-in) — see SourceRef.

func SimulateGraph

func SimulateGraph(ctx context.Context, graph *pipeline.Graph) (*SimulateReport, error)

SimulateGraph returns a SimulateReport for a pre-parsed graph. It is the graph-in variant of Simulate: callers that already parsed (e.g. the CLI's validate + simulate flow, or tooling that built a graph programmatically) avoid a second parse. Unlike Simulate, Format is left empty — there is no source string to inspect; the caller can set it if desired.

ctx is honored at entry; nil is coalesced to context.Background(). The graph is consumed synchronously. The graph is first finalized with pipeline.PrepareForExecution (#621), which rebuilds the adjacency index (so the BFS plan is O(nodes+edges) however the graph was built) and enforces the execution-critical invariants, returning an error for e.g. an edge to an undeclared node. Full structural validation is separate: NewEngineFromGraph also runs pipeline.Validate after preparation.

type SourceOption added in v0.74.0

type SourceOption func(*sourceConfig)

SourceOption configures the read-only source entry points (Simulate, EstimateRun, DescribeInputs).

func WithSource added in v0.74.0

func WithSource(ref SourceRef) SourceOption

WithSource anchors a source string for Simulate / EstimateRun / DescribeInputs — see SourceRef.

type SourceRef added in v0.74.0

type SourceRef struct {
	Path    string
	Builtin string
}

SourceRef says where a pipeline source string came from, so its *_file directives (prompt_file / command_file / system_prompt_file / prompt_include and the defaults cascade files) resolve against the right location. Exactly one of the fields is normally set; both empty means the origin is unknown.

  • Path: the on-disk file the source was read from. Directives resolve from disk relative to its directory, so a `tracker init` copy — edited sidecars included — loads the same from any cwd.
  • Builtin: the bare name of an embedded built-in (a Workflows() entry). Directives resolve inside the embed FS; no disk is consulted.
  • Neither: as a last resort, a source byte-identical to a built-in is treated as that built-in (so an embedder passing raw OpenWorkflow text still works); anything else resolves relative to the process cwd. Prefer an explicit ref — ResolveSource returns one via WorkflowInfo.Ref().

type StatusEntry added in v0.47.0

type StatusEntry struct {
	Timestamp string `json:"ts"`
	NodeID    string `json:"node_id,omitempty"`
	Text      string `json:"text"`
}

StatusEntry is one agent-authored status update.

func RunStatusTimeline added in v0.47.0

func RunStatusTimeline(runDir string) ([]StatusEntry, error)

RunStatusTimeline returns the agent-authored status updates for a run, in log order — a compact high-level timeline of what the run accomplished, without the per-turn/tool firehose. runDir must be a resolved run directory.

type StreamEvent

type StreamEvent struct {
	Timestamp string `json:"ts"`
	Source    string `json:"source"`              // "pipeline", "llm", "agent"
	Type      string `json:"type"`                // event type within source
	RunID     string `json:"run_id,omitempty"`    // pipeline run ID
	NodeID    string `json:"node_id,omitempty"`   // pipeline node ID
	Message   string `json:"message,omitempty"`   // human-readable message
	Error     string `json:"error,omitempty"`     // error text
	Provider  string `json:"provider,omitempty"`  // LLM provider
	Model     string `json:"model,omitempty"`     // LLM model
	ToolName  string `json:"tool_name,omitempty"` // tool name for agent/LLM tool events
	Content   string `json:"content,omitempty"`   // text content (LLM output, tool output)
	// TerminalStatus is set only on a pipeline run's terminal event
	// (pipeline_completed / pipeline_failed / budget_exceeded): "success",
	// "validation_overridden", "fail", or "budget_exceeded". Empty otherwise.
	TerminalStatus string `json:"terminal_status,omitempty"`
	// ResumeAfter is the provider's rate/usage reset time (RFC3339), set only on
	// a billing_paused event whose pause carried it. Empty when unknown. A
	// scheduler reads it to hold a usage-limit-paused run until the subscription
	// resets instead of relaunching straight into the same cap (#591).
	ResumeAfter string `json:"resume_after,omitempty"`
	// BundleIdentity is the content-addressed identity of the .dipx bundle the
	// run executes against ("sha256:<hex>"). Set on pipeline events of a bundle
	// run; empty for a plain .dip run.
	BundleIdentity string `json:"bundle_identity,omitempty"`

	// Decision fields — set on decision_edge / decision_condition /
	// decision_outcome / decision_restart / conditional_fallthrough events, from
	// pipeline.DecisionDetail. ConditionMatch and RestartCount are pointers so a
	// consumer can distinguish "false"/"0" from "not carried".
	EdgeFrom        string                   `json:"edge_from,omitempty"`
	EdgeTo          string                   `json:"edge_to,omitempty"`
	EdgeCondition   string                   `json:"edge_condition,omitempty"`
	EdgePriority    string                   `json:"edge_priority,omitempty"`
	ConditionMatch  *bool                    `json:"condition_match,omitempty"`
	OutcomeStatus   string                   `json:"outcome_status,omitempty"`
	ContextSnapshot map[string]string        `json:"context_snapshot,omitempty"`
	ContextUpdates  map[string]string        `json:"context_updates,omitempty"`
	RestartCount    *int                     `json:"restart_count,omitempty"`
	ClearedNodes    []string                 `json:"cleared_nodes,omitempty"`
	ConditionsTried []pipeline.ConditionEval `json:"conditions_tried,omitempty"`
	// ResetBy / FallbackLatchCleared ride on restart_budget_reset events
	// (#643): the enclosing loop header that reset this node's budget, and
	// whether its one-shot fallback latch was re-armed too.
	ResetBy              string `json:"reset_by,omitempty"`
	FallbackLatchCleared bool   `json:"fallback_latch_cleared,omitempty"`
	// RewindReason rides on resume_rewound events (#651): why the resume
	// re-entered at edge_to instead of the checkpoint's current node.
	RewindReason string `json:"rewind_reason,omitempty"`

	// TokenInput / TokenOutput carry the token counts of whatever the event
	// describes: the node's session stats on a pipeline decision event, the
	// turn's usage on an agent turn_metrics / llm_finish event. They are never
	// run-cumulative — that is total_tokens below.
	TokenInput  int `json:"token_input,omitempty"`
	TokenOutput int `json:"token_output,omitempty"`

	// Cost snapshot fields — set on cost_updated and budget_exceeded events,
	// from pipeline.CostSnapshot. Run-cumulative, not per-node. Estimated is
	// true when any contributing session was heuristic-derived.
	TotalTokens    int                               `json:"total_tokens,omitempty"`
	TotalCostUSD   float64                           `json:"total_cost_usd,omitempty"`
	ProviderTotals map[string]pipeline.ProviderUsage `json:"provider_totals,omitempty"`
	WallElapsedMs  int64                             `json:"wall_elapsed_ms,omitempty"`
	Estimated      bool                              `json:"estimated,omitempty"`

	// Truncation fields — set on tool_output_truncated events (#208).
	TruncStream   string `json:"trunc_stream,omitempty"`
	TruncLimit    int    `json:"trunc_limit,omitempty"`
	TruncCaptured int    `json:"trunc_captured_bytes,omitempty"`
	TruncDropped  int    `json:"trunc_dropped_bytes,omitempty"`
	TruncTotal    int    `json:"trunc_total_bytes,omitempty"`

	// Marker fields — set on tool_marker_missing events (#210).
	MarkerPattern string `json:"marker_pattern,omitempty"`
	MarkerTail    string `json:"marker_tail,omitempty"`
	MarkerError   string `json:"marker_error,omitempty"`

	// RouteTail is set on tool_route_missing events (#212).
	RouteTail string `json:"route_tail,omitempty"`

	// Tool-timeout fields — set on tool_timeout events (#644).
	ToolTimeoutMs       int64 `json:"tool_timeout_ms,omitempty"`
	ToolTimeoutCaptured int   `json:"tool_timeout_captured_bytes,omitempty"`

	// Jail fields — set on jail_degraded events (#648): the node's
	// writable_paths_mode (always "prefer"), the host-capability reason the
	// jail could not be applied, and the declared globs the Bash subprocess
	// is NOT bounded by on this run.
	JailMode          string   `json:"jail_mode,omitempty"`
	JailReason        string   `json:"jail_reason,omitempty"`
	JailDeclaredGlobs []string `json:"jail_declared_globs,omitempty"`

	// Backend fields — set on backend_resolved events: what a node's
	// backend: auto resolved to and why.
	BackendResolved   string `json:"backend_resolved,omitempty"`
	BackendReason     string `json:"backend_reason,omitempty"`
	ClaudePath        string `json:"claude_path,omitempty"`
	ClaudeVersion     string `json:"claude_version,omitempty"`
	BackendFromRecord bool   `json:"backend_from_record,omitempty"`

	// Auto-status fields — set on auto_status_missing events (#346).
	AutoStatusTail       string `json:"auto_status_tail,omitempty"`
	AutoStatusFailClosed bool   `json:"auto_status_fail_closed,omitempty"`

	// Override fields — set on validation_overridden events.
	OverrideGate         string         `json:"override_gate,omitempty"`
	OverrideLabel        string         `json:"override_label,omitempty"`
	OverrideActor        pipeline.Actor `json:"override_actor,omitempty"`
	OverrideSubgraphPath []string       `json:"override_subgraph_path,omitempty"`

	// GateID is set only on the gate lifecycle events (gate_opened /
	// gate_resolved) and correlates the pair (#509). NodeID identifies the gate
	// node on both.
	GateID string `json:"gate_id,omitempty"`
	// Gate payload fields, from pipeline.GateDetail. Open-time: GateMode,
	// GateLabel, GatePrompt, GateChoices, GateDefault, GateOptions, GateQuestions. Resolve-time:
	// GateResponse, GateOutcome, GateActor, GateTimedOut (plus Error above when
	// the gate failed to collect an answer). GateMode is repeated on the
	// resolution so GateResponse can be interpreted without joining.
	GateMode      string                  `json:"gate_mode,omitempty"`
	GateLabel     string                  `json:"gate_label,omitempty"`
	GatePrompt    string                  `json:"gate_prompt,omitempty"`
	GateChoices   []string                `json:"gate_choices,omitempty"`
	GateDefault   string                  `json:"gate_default,omitempty"`
	GateOptions   []pipeline.GateOption   `json:"gate_options,omitempty"`
	GateQuestions []pipeline.GateQuestion `json:"gate_questions,omitempty"`
	GateResponse  string                  `json:"gate_response,omitempty"`
	GateOutcome   string                  `json:"gate_outcome,omitempty"`
	GateActor     pipeline.Actor          `json:"gate_actor,omitempty"`
	GateTimedOut  bool                    `json:"gate_timed_out,omitempty"`

	// Run-reconstruction / capture fields (#519). These mirror the same-named
	// keys on pipeline's activity.jsonl entry so one struct decodes both NDJSON
	// schemas (the E1 parity contract). They carry the session/turn/call
	// identity, node kind, attempt number, untruncated tool input, and per-turn
	// economics that let a finished run be rebuilt as a tree. Populated on the
	// live --json wire too (#526): AgentHandler copies the agent identity/usage
	// detail and PipelineHandler copies NodeKind/AttemptNo, so the stream carries
	// the same parallel-branch attribution the audit log does. Still omitempty:
	// each field is present only on the event types that actually carry it.
	SessionID          string  `json:"session_id,omitempty"`
	ParentSessionID    string  `json:"parent_session_id,omitempty"`
	TurnNo             int     `json:"turn_no,omitempty"`
	AttemptNo          int     `json:"attempt_no,omitempty"`
	NodeKind           string  `json:"node_kind,omitempty"`
	ToolInput          string  `json:"tool_input,omitempty"`
	ToolDurationMs     int64   `json:"tool_duration_ms,omitempty"`
	CacheReadTokens    int     `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens   int     `json:"cache_write_tokens,omitempty"`
	ReasoningTokens    int     `json:"reasoning_tokens,omitempty"`
	EstimatedCost      float64 `json:"estimated_cost,omitempty"`
	ContextUtilization float64 `json:"context_utilization,omitempty"`
	ToolCacheHits      int     `json:"tool_cache_hits,omitempty"`
	ToolCacheMisses    int     `json:"tool_cache_misses,omitempty"`
	TurnDurationMs     int64   `json:"turn_duration_ms,omitempty"`
	CallID             string  `json:"call_id,omitempty"`
	FinishReason       string  `json:"finish_reason,omitempty"`

	// Run snapshot fields — set on pipeline_started, from pipeline.RunSnapshot:
	// the top-level node inventory (sorted by ID, not execution order) plus
	// resume state, so a subscriber joining at run start can seed its progress
	// model without separate access to the graph or checkpoint.
	// SnapshotCurrentNode / SnapshotCompletedNodes are populated only on resume.
	// No activity.jsonl counterpart — these names are new.
	SnapshotNodes          []StreamSnapshotNode `json:"snapshot_nodes,omitempty"`
	SnapshotStartNode      string               `json:"snapshot_start_node,omitempty"`
	SnapshotExitNode       string               `json:"snapshot_exit_node,omitempty"`
	SnapshotCurrentNode    string               `json:"snapshot_current_node,omitempty"`
	SnapshotCompletedNodes []string             `json:"snapshot_completed_nodes,omitempty"`
}

StreamEvent is the stable wire format for the tracker --json mode.

Field-stability contract:

  • Existing JSON field names are never renamed, retyped, or removed. New optional fields may be added without a major bump, so a consumer must tolerate unknown keys.
  • Every field except ts/source/type is `omitempty`: a field is present only on the event types that actually carry it. A consumer keys off `type` (within `source`) and reads the fields documented for it; it must not infer meaning from a field's absence beyond "this event does not carry it".
  • Where the same datum is also written to `activity.jsonl`, the JSON field name here is identical, so one decoder serves both streams. The only fields with no `activity.jsonl` counterpart are the run-snapshot group (`snapshot_*`).

Payload size: `context_snapshot` / `context_updates` are unbounded maps of routing-relevant context (a decision event on a context-heavy run can carry tens of kilobytes), `gate_prompt` is capped at pipeline.GateMaxPromptBytes, and the `*_tail` diagnostic fields are capped at 256 bytes by their emitters. `content` is unbounded (a tool_call_end carries full tool output). A consumer on a bounded transport should cap or drop those fields itself.

type StreamSnapshotNode added in v0.47.0

type StreamSnapshotNode struct {
	ID      string `json:"id"`
	Label   string `json:"label,omitempty"`
	Handler string `json:"handler,omitempty"`
}

StreamSnapshotNode is one node of a pipeline_started run snapshot on the NDJSON wire. It mirrors pipeline.SnapshotNode with explicit wire tags.

type StructuralTestGroup added in v0.72.0

type StructuralTestGroup struct {
	// Similarity is the minimum pairwise token-Jaccard similarity within the
	// cluster (≥ structuralSimilarityThreshold by construction).
	Similarity float64 `json:"similarity"`
	// SharedCalls are the production call targets the members have in common —
	// the corroborating signal that they exercise the same code, not just the
	// same test-harness shape.
	SharedCalls []string       `json:"shared_calls,omitempty"`
	Tests       []TestLocation `json:"tests"`
}

StructuralTestGroup is a cluster of test functions that are structurally near-duplicate (heuristic 1). Advisory-only.

type Suggestion

type Suggestion struct {
	NodeID  string         `json:"node_id,omitempty"`
	Kind    SuggestionKind `json:"kind"`
	Message string         `json:"message"`
}

Suggestion is an actionable recommendation produced by Diagnose.

type SuggestionKind

type SuggestionKind string

SuggestionKind is the typed string identifying which template produced a Suggestion. The underlying string values are stable; new kinds may be added additively.

const (
	SuggestionRetryPattern     SuggestionKind = "retry_pattern"
	SuggestionEscalateLimit    SuggestionKind = "escalate_limit"
	SuggestionNoOutput         SuggestionKind = "no_output"
	SuggestionShellCommand     SuggestionKind = "shell_command"
	SuggestionGoTest           SuggestionKind = "go_test"
	SuggestionSuspiciousTiming SuggestionKind = "suspicious_timing"
	SuggestionBudget           SuggestionKind = "budget"
	// SuggestionToolOutputTruncated fires when a tool node's output stream
	// exceeded its per-stream cap. Surfaces actionable copy pointing at
	// output_limit and at the canonical authoring pattern. Issue #208.
	SuggestionToolOutputTruncated SuggestionKind = "tool_output_truncated"
	// SuggestionConditionalFallthrough fires when a node's conditional
	// routing edges all evaluated false and routing fell back to an
	// unconditional edge. Issue #208.
	SuggestionConditionalFallthrough SuggestionKind = "conditional_fallthrough"
	// SuggestionToolMarkerMissing fires when a tool node declared
	// marker_grep but the regex matched nothing (or failed to compile).
	// Surfaces the configured pattern, the captured stdout tail, and the
	// recommended fix. Issue #210.
	SuggestionToolMarkerMissing SuggestionKind = "tool_marker_missing"
	// SuggestionToolRouteMissing fires when a tool node had
	// route_required: true but no _TRACKER_ROUTE= sentinel line was
	// emitted to stdout. Surfaces the captured stdout tail and the
	// recommended author pattern. Issue #212.
	SuggestionToolRouteMissing SuggestionKind = "tool_route_missing"
	// SuggestionAutoStatusMissing fires when an auto_status agent node
	// completed normally but its response contained no parseable STATUS
	// line (#346). Goal-gate nodes fail closed; plain auto_status nodes
	// keep the legacy success default — the suggestion copy distinguishes
	// the two so a silently-defaulted verdict is visible post-run.
	SuggestionAutoStatusMissing SuggestionKind = "auto_status_missing"
	// SuggestionToolTimeout fires when a tool node's command exceeded its
	// `timeout:` and was killed (#644). The node failed with OutcomeFail
	// (routable); the suggestion names the timeout, how much output was
	// captured, and the raise-timeout / route-on-fail fix.
	SuggestionToolTimeout SuggestionKind = "tool_timeout"
	// SuggestionFallbackLatched fires when a node exhausted its retry budget
	// a second time after its one-shot fallback_retry_target was already
	// taken (#642). The engine refuses to re-take the fallback (it would
	// loop forever) and halts; the suggestion explains the latch and points
	// at the fallback path that led back into the failing node.
	SuggestionFallbackLatched SuggestionKind = "fallback_latched"
	// SuggestionJailDegraded fires when an agent node declared writable_paths
	// with writable_paths_mode: prefer and ran UNJAILED because the host could
	// not enforce Landlock (#648). Informational, not a failure: it tells the
	// reviewer which node's declared write scope was NOT mechanically enforced
	// on this run (only the in-process Write/Edit/ApplyPatch glob policy held)
	// and how to make it refuse instead (writable_paths_mode: require).
	SuggestionJailDegraded SuggestionKind = "jail_degraded"
	// SuggestionAuditLogInjection fires when the integrity-protected
	// activity log has one or more lines missing the runtime sentinel
	// prefix (#213). Detection-only — the suggestion text is explicit
	// that the sentinel is not authentication; a motivated forger who
	// reads tracker's source can emit the bytes. Surfaces the count of
	// suspect lines and the audit-log path so operators can
	// investigate.
	SuggestionAuditLogInjection SuggestionKind = "audit_log_injection"
)

Suggestion kinds (stable; new ones may be added additively).

const SuggestionCostAsymmetry SuggestionKind = "cost_asymmetry"

SuggestionCostAsymmetry fires when one provider dominated a run's cost with no prompt caching — the failure mode where a slow, uncached backend in a fan-out silently drives most of the bill (#353). Actionable: cap it with per-node max_cost_usd, route the lane cheaper, or reshape a fan-out to escalate-on-fail.

type TestFidelityReport added in v0.47.0

type TestFidelityReport struct {
	// DuplicateGroups are sets of ≥2 test functions with the same body. "identical"
	// = byte-for-byte equal bodies; "near-identical" = equal after normalizing
	// literal values (so tests differing only in a string/number collide).
	DuplicateGroups []DuplicateTestGroup `json:"duplicate_groups,omitempty"`
	// StructuralGroups are sets of ≥2 test functions that the exact/literal-strip
	// hashes miss but whose canonicalized token streams are near-duplicates
	// (reordered statements, renamed locals, wrapped asserts) AND that call the
	// same production symbols (#532, heuristic 1). ADVISORY — these do not affect
	// the exit code; false positives are plausible on shared-harness tests, so the
	// signal is reviewer-facing until a corpus FP evaluation earns a promotion.
	StructuralGroups []StructuralTestGroup `json:"structural_groups,omitempty"`
}

TestFidelityReport lists groups of Go test functions that share a body — a fidelity red flag the structural VerifyMilestone checks are blind to (#489).

func AnalyzeTestFidelity added in v0.47.0

func AnalyzeTestFidelity(dir string) (*TestFidelityReport, error)

AnalyzeTestFidelity scans a directory tree for Go test files and reports test functions whose bodies duplicate one another. Unparseable files are skipped (best-effort), not fatal — the analysis is advisory. dir is walked recursively, skipping vendor/.git/node_modules.

type TestLocation added in v0.47.0

type TestLocation struct {
	Name string `json:"name"`
	File string `json:"file"`
	Line int    `json:"line"`
}

TestLocation identifies a test function.

type TestRaceReport added in v0.54.0

type TestRaceReport struct {
	// Ran is false when the race pass was skipped (no Go module, no toolchain,
	// or the race detector is unavailable in this environment, e.g. no cgo) — a
	// skip is not a pass and not a failure.
	Ran bool `json:"ran"`
	// SkipReason explains why Ran is false (empty when Ran is true).
	SkipReason string `json:"skip_reason,omitempty"`
	// RaceDetected is true when the race detector fired (a genuine data race).
	RaceDetected bool `json:"race_detected"`
	// Passed is true when `go test -race ./...` exited 0 (tests green, no race).
	Passed bool `json:"passed"`
	// Output is the portion of the combined output most relevant to the result:
	// the race stanza when a race fired, otherwise the tail (capped).
	Output string `json:"output,omitempty"`
}

TestRaceReport is the outcome of running the Go race detector over a tree.

func DetectTestRaces added in v0.54.0

func DetectTestRaces(ctx context.Context, dir string) (*TestRaceReport, error)

DetectTestRaces runs `go test -race ./...` in dir and reports whether the race detector fired. It is intentionally opt-in (heavyweight: it compiles and runs the whole suite under the race detector). It returns Ran=false — a skip, not a failure — when there is no Go module under dir, no `go` toolchain, or the race detector cannot run in this environment (no cgo / unsupported platform), so a non-Go milestone or a cgo-less CI image does not spuriously fail the gate.

type TestReachability added in v0.72.0

type TestReachability struct {
	Test                    TestLocation `json:"test"`
	Skipped                 bool         `json:"skipped"`
	ProductionBlocksCovered int          `json:"production_blocks_covered"`
}

TestReachability summarizes one test's production-coverage attribution.

type TestReachabilityReport added in v0.72.0

type TestReachabilityReport struct {
	// Ran is false when the pass was skipped (no Go module, no toolchain, no
	// tests, or the test count exceeded the budget). A skip is not a failure.
	Ran        bool   `json:"ran"`
	SkipReason string `json:"skip_reason,omitempty"`
	// Tests is the per-test coverage summary (one entry per analyzed test).
	Tests []TestReachability `json:"tests,omitempty"`
	// ZeroCoverage lists non-skipped tests that executed ZERO production
	// (non-_test.go) statements — the strong, low-false-positive signal that a
	// test asserts on a re-implemented copy or a path guarded out (heuristic 2).
	ZeroCoverage []TestLocation `json:"zero_coverage,omitempty"`
	// NameUnreached lists tests whose name matches an exported production symbol
	// in the same package that the test never covers — a fuzzy, advisory-only
	// name-attribution signal.
	NameUnreached []NameAttribution `json:"name_unreached,omitempty"`
	// Reimplemented lists test-local functions that structurally duplicate a
	// production function the whole suite never covers (heuristic 3). The
	// coverage==0 corroboration excludes legitimate oracle/differential tests
	// (which DO call the production symbol). Advisory-only.
	Reimplemented []ReimplementedFinding `json:"reimplemented,omitempty"`
}

TestReachabilityReport is the outcome of the opt-in coverage-attribution pass. Like DetectTestRaces it is heavyweight (one `go test` run per test) and skips cleanly (Ran=false) rather than failing when it cannot run. Every finding is ADVISORY — reviewer-facing evidence, not a gate — because coverage cannot see subprocess/cgo execution and honest tests can legitimately trip an edge case.

func AnalyzeTestReachability added in v0.72.0

func AnalyzeTestReachability(ctx context.Context, dir string) (*TestReachabilityReport, error)

AnalyzeTestReachability runs per-test Go coverage under dir and reports tests that reach no production code (heuristic 2) plus test-local re-implementations of uncovered production functions (heuristic 3). It is opt-in and heavyweight, mirroring DetectTestRaces: Ran=false (a skip, not a failure) when there is no Go module, no toolchain, no tests, or the test count exceeds the budget.

type TimelineEntry

type TimelineEntry struct {
	Timestamp time.Time `json:"ts"`
	Type      string    `json:"type"`
	NodeID    string    `json:"node_id,omitempty"`
	Message   string    `json:"message,omitempty"`
	// Duration is encoded as integer nanoseconds in JSON ("duration_ns"),
	// not as a duration string.
	Duration time.Duration `json:"duration_ns,omitempty"`
}

TimelineEntry is a single entry in the audit timeline.

type ValidateOption

type ValidateOption func(*validateConfig)

ValidateOption configures ValidateSource behavior.

func WithValidateFormat

func WithValidateFormat(format string) ValidateOption

WithValidateFormat sets the pipeline source format ("dip" or "dot").

func WithValidateSource added in v0.74.0

func WithValidateSource(ref SourceRef) ValidateOption

WithValidateSource anchors the source so its *_file directives resolve against where it came from — see SourceRef.

type ValidationResult

type ValidationResult struct {
	Graph    *pipeline.Graph
	Errors   []string
	Warnings []string
	Hints    []string
}

ValidationResult contains the outcome of pipeline validation.

func ValidateSource

func ValidateSource(source string, opts ...ValidateOption) (*ValidationResult, error)

ValidateSource parses and validates a pipeline source string without executing it. Returns a ValidationResult with structured errors, warnings, and hints. An error is returned when the source cannot be parsed or has structural errors.

type WebhookGateConfig

type WebhookGateConfig struct {
	WebhookURL    string        // required: URL to post gate payloads to
	CallbackAddr  string        // local listen addr for callback server (default: :0, an OS-assigned ephemeral port; the bound address is advertised via each gate's callback_url)
	Timeout       time.Duration // wait timeout per gate (default: 10m)
	TimeoutAction string        // "fail" (default) or "success" on timeout
	AuthHeader    string        // Authorization header for outbound requests
	RunID         string        // optional: run ID embedded in gate payloads
}

WebhookGateConfig controls headless webhook-based human gate handling. When set, human gate prompts are POSTed to WebhookURL and the pipeline waits for a callback POST to the local callback server.

type WorkflowInfo

type WorkflowInfo struct {
	Name        string   // bare name used for lookup, e.g. "build_product"
	File        string   // path within the embedded FS, e.g. "examples/build_product.dip"
	DisplayName string   // workflow declaration name, e.g. "BuildProduct"
	Goal        string   // parsed from the goal: field at the top of the .dip file
	Requires    []string // parsed from the `requires:` field (v0.29.0); nil if not declared
	// StartHere marks the first-win starters (explain, quickfix), which the
	// catalog lists first and `tracker workflows` tags "start here".
	StartHere bool
	// Path is set only by ResolveSource for a filesystem source: the resolved
	// on-disk path the source was read from (Name is empty in that case).
	Path string
	// contains filtered or unexported fields
}

WorkflowInfo describes a built-in workflow embedded in the tracker binary.

func LookupWorkflow

func LookupWorkflow(name string) (WorkflowInfo, bool)

LookupWorkflow returns the WorkflowInfo for a built-in workflow by bare name, or (zero, false) if no built-in matches. The returned value shares no mutable state with the cached catalog.

func OpenWorkflow

func OpenWorkflow(name string) ([]byte, WorkflowInfo, error)

OpenWorkflow returns the raw source bytes of a built-in workflow by bare name. This is the same content that `tracker init <name>` would copy to disk. Returns an error if the name is not a known built-in.

func ResolveSource

func ResolveSource(name, workDir string) (source string, info WorkflowInfo, err error)

ResolveSource resolves a pipeline name to the actual workflow source text. The resolution order matches the CLI's tracker.resolvePipelineSource:

  1. If name contains "/" or "\" (forward or back slash) or ends in ".dip"/".dipx"/".dot", treat it as a filesystem path and read it from disk.
  2. If "<name>.dip" exists under workDir, read that.
  3. If "<name>" exists under workDir, read that.
  4. If name matches a built-in workflow, return the embedded source.
  5. Otherwise return an error listing available built-ins.

For a built-in the returned WorkflowInfo is the catalog entry; for a filesystem source only WorkflowInfo.Path is set (the resolved path the text was read from). Either way info.Ref() is the SourceRef to pass on (Config.Source / WithSource / WithValidateSource) so the source's *_file directives resolve against where it actually lives, not the process cwd. workDir may be empty — in that case the current working directory is used for relative lookups.

func Workflows

func Workflows() []WorkflowInfo

Workflows returns the list of workflows embedded in the tracker binary, sorted by name. Library consumers can use this to show users the available built-ins without shelling out to `tracker workflows`. Returned values share no mutable state with the cached catalog.

func (WorkflowInfo) Ref added in v0.74.0

func (info WorkflowInfo) Ref() SourceRef

Ref returns the SourceRef that anchors this workflow's source for Run / Simulate / ValidateSource / DescribeInputs: the built-in name for an embedded workflow, the on-disk path for a filesystem source.

Directories

Path Synopsis
ABOUTME: Turn-budget checkpoint evaluation for the agent session loop.
ABOUTME: Turn-budget checkpoint evaluation for the agent session loop.
exec
ABOUTME: Builds the environment for commands run on the model's behalf, without credential names or credential-bearing values.
ABOUTME: Builds the environment for commands run on the model's behalf, without credential names or credential-bearing values.
tools
ABOUTME: Bash tool executes shell commands in the working directory.
ABOUTME: Bash tool executes shell commands in the working directory.
tools/sprintwriter
ABOUTME: Auditor-response parsing: tolerant verdict detection and Aider-style ABOUTME: SEARCH/REPLACE block extraction for the enriched-sprint audit pass.
ABOUTME: Auditor-response parsing: tolerant verdict detection and Aider-style ABOUTME: SEARCH/REPLACE block extraction for the enriched-sprint audit pass.
cmd
tracker command
ABOUTME: Audit subcommand — analyzes completed pipeline runs from on-disk artifacts.
ABOUTME: Audit subcommand — analyzes completed pipeline runs from on-disk artifacts.
tracker-conformance command
ABOUTME: golden-trace conformance fixtures — deterministic engine runs driven ABOUTME: by a stub completer (no API keys) and snapshotted so downstream ports ABOUTME: can diff for event-schema / handler-contract / usage-shape drift.
ABOUTME: golden-trace conformance fixtures — deterministic engine runs driven ABOUTME: by a stub completer (no API keys) and snapshotted so downstream ports ABOUTME: can diff for event-schema / handler-contract / usage-shape drift.
tracker-swebench command
ABOUTME: `tracker-swebench analyze <results-dir>` — bulk-triage a prior run's artifacts.
ABOUTME: `tracker-swebench analyze <results-dir>` — bulk-triage a prior run's artifacts.
tracker-swebench/agent-runner command
ABOUTME: In-container agent binary for SWE-bench evaluation harness.
ABOUTME: In-container agent binary for SWE-bench evaluation harness.
tracker-swebench/internal/agentsummary
ABOUTME: The JSON summary agent-runner prints as its last stdout line and the swebench harness parses.
ABOUTME: The JSON summary agent-runner prints as its last stdout line and the swebench harness parses.
trackerbot command
ABOUTME: Aliases the transport-neutral chatops types into package main so the ABOUTME: Slack transport (slack.go) and wiring (main.go) read cleanly.
ABOUTME: Aliases the transport-neutral chatops types into package main so the ABOUTME: Slack transport (slack.go) and wiring (main.go) read cleanly.
trackerchat command
ABOUTME: Entry point for trackerchat — a terminal REPL front-end for Tracker.
ABOUTME: Entry point for trackerchat — a terminal REPL front-end for Tracker.
ABOUTME: Reports tracker's run lifecycle to a Herdr terminal pane manager.
ABOUTME: Reports tracker's run lifecycle to a Herdr terminal pane manager.
internal
bundleid
Package bundleid provides shared formatting helpers for content-addressed .dipx bundle identities.
Package bundleid provides shared formatting helpers for content-addressed .dipx bundle identities.
chatgptauth
ABOUTME: Decodes ChatGPT identity (account ID, plan type) from an id_token's claims and reads ABOUTME: an access_token's exp claim — no signature check, matching Codex (spec §1 "Identity").
ABOUTME: Decodes ChatGPT identity (account ID, plan type) from an id_token's claims and reads ABOUTME: an access_token's exp claim — no signature check, matching Codex (spec §1 "Identity").
diag
ABOUTME: Injectable diagnostic sink for tracker's library packages (#449).
ABOUTME: Injectable diagnostic sink for tracker's library packages (#449).
dipxtest
ABOUTME: Test helper that packs real .dipx bundles for use in downstream tests.
ABOUTME: Test helper that packs real .dipx bundles for use in downstream tests.
envpolicy
ABOUTME: Registry of every environment variable Tracker reads, with the trust tier each may be set from.
ABOUTME: Registry of every environment variable Tracker reads, with the trust tier each may be set from.
testutil
Package testutil holds helpers for tests that must prove a concurrent interaction actually happened, rather than passing because the scheduler happened to keep the two sides apart (#658).
Package testutil holds helpers for tests that must prove a concurrent interaction actually happened, rather than passing because the scheduler happened to keep the two sides apart (#658).
xdgconfig
ABOUTME: Resolves the XDG config-home directory ($XDG_CONFIG_HOME or ABOUTME: $HOME/.config) shared by the CLI and the chatgptauth token-file reader.
ABOUTME: Resolves the XDG config-home directory ($XDG_CONFIG_HOME or ABOUTME: $HOME/.config) shared by the CLI and the chatgptauth token-file reader.
llm
ABOUTME: Detects provider credit/quota exhaustion and builds an actionable, ABOUTME: account-attributed message — never printing the raw API key (#487 Phase 1).
ABOUTME: Detects provider credit/quota exhaustion and builds an actionable, ABOUTME: account-attributed message — never printing the raw API key (#487 Phase 1).
anthropic
ABOUTME: Anthropic Messages API adapter implementing the ProviderAdapter interface.
ABOUTME: Anthropic Messages API adapter implementing the ProviderAdapter interface.
google
ABOUTME: Google Gemini API adapter implementing the ProviderAdapter interface.
ABOUTME: Google Gemini API adapter implementing the ProviderAdapter interface.
openai
ABOUTME: OpenAI Responses API adapter implementing the ProviderAdapter interface.
ABOUTME: OpenAI Responses API adapter implementing the ProviderAdapter interface.
openaicompat
ABOUTME: OpenAI Chat Completions compatible adapter implementing the ProviderAdapter interface.
ABOUTME: OpenAI Chat Completions compatible adapter implementing the ProviderAdapter interface.
ABOUTME: Resolves the on-disk location of the integrity-protected activity log.
ABOUTME: Resolves the on-disk location of the integrity-protected activity log.
handlers
ABOUTME: LLM-backed autopilot interviewer that replaces human gates with automated decisions.
ABOUTME: LLM-backed autopilot interviewer that replaces human gates with automated decisions.
scripts
gen/activitylog command
Command gen-activitylog renders the activity-log reader schema (schema.go, the single field authority) into the checked-in reader-side Go sources: the private decode struct activityRawLine, the exported ActivityEntry, and the toEntry copier that bridges them.
Command gen-activitylog renders the activity-log reader schema (schema.go, the single field authority) into the checked-in reader-side Go sources: the private decode struct activityRawLine, the exported ActivityEntry, and the toEntry copier that bridges them.
gen/models command
Command gen-models renders the supported-model catalog into a static HTML partial the website includes on the Models & Providers page.
Command gen-models renders the supported-model catalog into a static HTML partial the website includes on the Models & Providers page.
tools
jailcheck command
ABOUTME: jailcheck flags direct filesystem-mutation / subprocess calls in ABOUTME: agent/tools that bypass the ExecutionEnvironment seam guarding the writable_paths jail (#272/#275/#283).
ABOUTME: jailcheck flags direct filesystem-mutation / subprocess calls in ABOUTME: agent/tools that bypass the ExecutionEnvironment seam guarding the writable_paths jail (#272/#275/#283).
transport
chatops
ABOUTME: Posts a run's outcome to its thread — diagnosis on failure, adaptive on success (D3).
ABOUTME: Posts a run's outcome to its thread — diagnosis on failure, adaptive on success (D3).
cli
ABOUTME: A terminal REPL transport — a second consumer of transport/chatops that ABOUTME: proves the boundary: type a request, answer gates inline, watch it run.
ABOUTME: A terminal REPL transport — a second consumer of transport/chatops that ABOUTME: proves the boundary: type a request, answer gates inline, watch it run.
conformance
ABOUTME: A reusable conformance suite any transport runs to prove its handlers.Interviewer honours the gate contract (all modes + cancellation).
ABOUTME: A reusable conformance suite any transport runs to prove its handlers.Interviewer honours the gate contract (all modes + cancellation).
ABOUTME: Event adapter — converts raw engine events into typed TUI messages.
ABOUTME: Event adapter — converts raw engine events into typed TUI messages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL